Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ This project adheres to [Semantic Versioning], with the exception that minor rel

### Added

- ✨ Add support for the IQM QDMI device via a respective `iqm` Python extra ([#1696]) ([**@marcelwa**])
- ✨ Add a `hadamard-lifting` pass for lifting Hadamard gates above Pauli gates ([#1605]) ([**@lirem101**], [**@burgholzer**])
- ✨ Add a `merge-single-qubit-rotation-gates` pass for merging consecutive rotation gates using quaternions ([#1407], [#1674]) ([**@J4MMlE**], [**@denialhaag**], [**@MatthiasReumann**])
- ✨ Add conversions between `jeff` and QCO ([#1479], [#1548], [#1565], [#1637], [#1676]) ([**@denialhaag**], [**@burgholzer**])
Expand Down Expand Up @@ -362,6 +363,7 @@ _📚 Refer to the [GitHub Release Notes](https://github.com/munich-quantum-tool

<!-- PR links -->

[#1696]: https://github.com/munich-quantum-toolkit/core/pull/1696
[#1676]: https://github.com/munich-quantum-toolkit/core/pull/1676
[#1675]: https://github.com/munich-quantum-toolkit/core/pull/1675
[#1674]: https://github.com/munich-quantum-toolkit/core/pull/1674
Expand Down
1 change: 1 addition & 0 deletions docs/qdmi/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ This part of MQT Core contains the implementation of QDMI's different components
NA QDMI Device <na_device>
DDSIM QDMI Device <ddsim_device>
IQM QDMI Device <iqm_device>
QDMI Driver <driver>
QDMI-Qiskit Backend <qdmi_backend>
```
89 changes: 89 additions & 0 deletions docs/qdmi/iqm_device.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
---
file_format: mystnb
kernelspec:
name: python3
mystnb:
number_source_lines: true
---

# IQM QDMI Device

## Objective

MQT Core can integrate [IQM's QDMI device](https://github.com/iqm-finland/QDMI-on-IQM/) through the external {code}`iqm-qdmi` package and expose it through the same Qiskit-facing QDMI backend surface as the in-tree devices.
Unlike the DDSIM and neutral-atom devices, the IQM device is not built as part of MQT Core itself.

## Installation

Install MQT Core with the {code}`iqm` extra to get Qiskit and the external {code}`iqm-qdmi` wheel in one step:

::::{tab-set}
:sync-group: installer

:::{tab-item} {code}`uv` _(recommended)_
:sync: uv

```console
$ uv pip install "mqt-core[iqm]"
```

:::

:::{tab-item} {code}`pip`
:sync: pip

```console
(.venv) $ python -m pip install "mqt-core[iqm]"
```

:::
::::

If you prefer to work directly with the upstream IQM package, the {code}`iqm-qdmi[qiskit]` installation route remains supported as well.

## Usage

The IQM integration is exposed through {py:class}`~mqt.core.plugins.qiskit.iqm.IQMBackend`.
Importing that class loads the packaged IQM device library explicitly through the path exported as {code}`iqm.qdmi.IQM_QDMI_LIBRARY_PATH`, so {py:class}`~mqt.core.plugins.qiskit.QDMIProvider` remains reserved for devices that are already visible through the FoMaC session.
This keeps the platform-specific wheel layout inside {code}`iqm-qdmi` itself: the upstream package resolves {code}`bin/` on Windows and {code}`lib/` or {code}`lib64/` on Unix-like systems before MQT Core ever touches the path.

```python
from mqt.core.plugins.qiskit.iqm import IQMBackend
from qiskit import QuantumCircuit
from qiskit.compiler import transpile

backend = IQMBackend(
base_url="https://resonance.iqm.tech",
qc_alias="emerald:mock",
)

qc = QuantumCircuit(2)
qc.h(0)
qc.cx(0, 1)
qc.measure_all()

transpiled_qc = transpile(qc, backend)
result = backend.run(transpiled_qc, shots=128).result()
print(result.get_counts())
```

If you do not pass configuration explicitly, {py:class}`~mqt.core.plugins.qiskit.iqm.IQMBackend` reads the same environment variables as the upstream IQM wrapper:

- {code}`IQM_BASE_URL`
- {code}`IQM_TOKEN` or {code}`RESONANCE_API_KEY`
- {code}`IQM_TOKENS_FILE`
- {code}`IQM_QC_ID`
- {code}`IQM_QC_ALIAS`

The backend also exposes the same convenience helpers for binding Qiskit's sampler and estimator primitives:

```python
sampler = backend.sampler(default_shots=1024)
estimator = backend.estimator(default_precision=0.0)
```

## Relationship to the Upstream IQM Device Library

The {code}`iqm-qdmi` project still ships its own direct wrapper at {code}`iqm.qdmi.qiskit`.
The MQT Core integration in {py:mod}`mqt.core.plugins.qiskit.iqm` exists to provide a first-class MQT-side import path and an installation flow based on {code}`mqt-core[iqm]`.
See the [IQM QDMI Documentation](https://iqm-finland.github.io/QDMI-on-IQM/) for more details on authentication, device capabilities, and error handling.
28 changes: 28 additions & 0 deletions docs/qdmi/qdmi_backend.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,9 @@ print(f"Results: {counts}")
The {py:class}`~mqt.core.plugins.qiskit.QDMIProvider` discovers QDMI devices available through the FoMaC layer.
Backends should always be obtained through the provider rather than instantiated directly.

The IQM QDMI device is the main exception to that rule.
It is loaded explicitly through {py:class}`~mqt.core.plugins.qiskit.iqm.IQMBackend` rather than auto-discovered by {py:class}`~mqt.core.plugins.qiskit.QDMIProvider`, which keeps external device loading opt-in.

```{code-cell} ipython3
from mqt.core.plugins.qiskit import QDMIProvider

Expand Down Expand Up @@ -100,6 +103,31 @@ filtered_ddsim = provider.backends(name="DDSIM") # Matches "MQT Core DDSIM QDMI
exact = provider.backends(name="MQT Core DDSIM QDMI Device")
```

## External IQM Backend

The IQM integration is exposed from a dedicated module so the optional {code}`iqm-qdmi` dependency is only loaded when requested explicitly.

```python
from mqt.core.plugins.qiskit.iqm import IQMBackend

backend = IQMBackend(
base_url="https://resonance.iqm.tech",
qc_alias="emerald:mock",
)
```

If you omit these parameters, {py:class}`~mqt.core.plugins.qiskit.iqm.IQMBackend` falls back to the following environment variables:

- {code}`IQM_BASE_URL`
- {code}`IQM_TOKEN` or {code}`RESONANCE_API_KEY`
- {code}`IQM_TOKENS_FILE`
- {code}`IQM_QC_ID`
- {code}`IQM_QC_ALIAS`

Once instantiated, the IQM backend behaves like every other QDMI-backed Qiskit backend in MQT Core and supports the same {py:meth}`~mqt.core.plugins.qiskit.iqm.IQMBackend.sampler` and {py:meth}`~mqt.core.plugins.qiskit.iqm.IQMBackend.estimator` helper methods.

See the [IQM QDMI Documentation](https://iqm-finland.github.io/QDMI-on-IQM/) for more details on authentication, device capabilities, and error handling.

## Authentication

The {py:class}`~mqt.core.plugins.qiskit.QDMIProvider` supports authentication for accessing QDMI devices that require credentials.
Expand Down
5 changes: 5 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,10 @@ dynamic = ["version"]
qiskit = [
"qiskit[qasm3-import]>=1.0.0",
]
iqm = [
"iqm-qdmi>=1.0.1",
"qiskit[qasm3-import]>=1.0.0",
]

[project.scripts]
mqt-core-cli = "mqt.core.__main__:main"
Expand Down Expand Up @@ -361,6 +365,7 @@ test = [
"qiskit[qasm3-import]>=1.0.0",
"numpy>=2.1; python_version >= '3.13'",
"numpy>=2.3.2; python_version >= '3.14'",
"iqm-qdmi>=1.0.1",
]
dev = [
{include-group = "build"},
Expand Down
106 changes: 106 additions & 0 deletions python/mqt/core/plugins/qiskit/iqm.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
# Copyright (c) 2023 - 2026 Chair for Design Automation, TUM
# Copyright (c) 2025 - 2026 Munich Quantum Software Company GmbH
# All rights reserved.
#
# SPDX-License-Identifier: MIT
#
# Licensed under the MIT License

"""IQM-specific QDMI integration for Qiskit.

This module provides an MQT Core-native wrapper for the externally packaged
``iqm-qdmi`` device library. Importing this module is opt-in and requires the
``mqt-core[iqm]`` extra. The library path is ``iqm.qdmi.IQM_QDMI_LIBRARY_PATH``.
"""

from __future__ import annotations

import os
from typing import Any

from ... import fomac
from ..._compat.optional import OptionalDependencyTester
from .backend import QDMIBackend
from .estimator import QDMIEstimator
from .sampler import QDMISampler

HAS_IQM_QDMI = OptionalDependencyTester(
"iqm.qdmi",
install_msg="Install with 'pip install mqt-core[iqm]'",
)
HAS_QISKIT_IQM = OptionalDependencyTester(
"qiskit",
install_msg="Install with 'pip install mqt-core[iqm]'",
)

__all__ = ["HAS_IQM_QDMI", "IQMBackend"]

HAS_QISKIT_IQM.require_now("use the IQM QDMI backend")
HAS_IQM_QDMI.require_now("use the IQM QDMI backend")


def __dir__() -> list[str]:
return __all__


def _iqm_qdmi_library_path() -> str:
"""Return the packaged IQM QDMI device library path exported by ``iqm-qdmi``."""
iqm_qdmi_module = HAS_IQM_QDMI.require_module("use the IQM QDMI backend")
return str(iqm_qdmi_module.IQM_QDMI_LIBRARY_PATH)


class IQMBackend(QDMIBackend):
"""Qiskit backend for the packaged IQM QDMI device library.

This backend loads the shared library distributed with ``iqm-qdmi`` and
exposes it through MQT Core's Qiskit-compatible QDMI backend surface.

Args:
base_url: Base URL of the IQM service. Defaults to ``IQM_BASE_URL`` or
the standard Resonance endpoint.
token: Authentication token. Defaults to ``IQM_TOKEN`` or
``RESONANCE_API_KEY``.
tokens_file: Path to an authentication file. Defaults to
``IQM_TOKENS_FILE``.
qc_id: Optional IQM quantum computer identifier.
qc_alias: Optional IQM quantum computer alias.
"""

def __init__(
self,
*,
base_url: str | None = None,
token: str | None = None,
tokens_file: str | None = None,
qc_id: str | None = None,
qc_alias: str | None = None,
) -> None:
"""Initialize the IQM backend."""
device = fomac.add_dynamic_device_library(
library_path=_iqm_qdmi_library_path(),
prefix="IQM",
base_url=base_url or os.getenv("IQM_BASE_URL") or "https://resonance.iqm.tech",
token=token or os.getenv("IQM_TOKEN") or os.getenv("RESONANCE_API_KEY"),
auth_file=tokens_file or os.getenv("IQM_TOKENS_FILE"),
custom1=qc_id or os.getenv("IQM_QC_ID"),
custom2=qc_alias or os.getenv("IQM_QC_ALIAS"),
)
super().__init__(device=device)

def sampler(
self,
*,
default_shots: int = 1024,
options: dict[str, Any] | None = None,
) -> QDMISampler:
"""Return a SamplerV2 primitive bound to this backend."""
return QDMISampler(self, default_shots=default_shots, options=options)

def estimator(
self,
*,
default_precision: float = 0.0,
options: dict[str, Any] | None = None,
) -> QDMIEstimator:
"""Return an EstimatorV2 primitive bound to this backend."""
return QDMIEstimator(self, default_precision=default_precision, options=options)
Loading
Loading