# Running End-to-End Quantum Workloads on IQM Hardware via QDMI-on-IQM

Welcome to the end-to-end tutorial. This guide walks you step by step through
driving real quantum workloads on IQM systems using QDMI-on-IQM and the packaged
[`IQMBackend`](api/iqm/qdmi/qiskit/index.html.md#iqm.qdmi.qiskit.IQMBackend).

Whether you want to estimate molecular ground-state energies with [QSCI](https://arxiv.org/abs/2302.11320)
or benchmark hardware with [MQT Bench](https://mqt.readthedocs.io/projects/bench/), the example scripts in this
repository provide a practical starting point. This tutorial focuses on two
application areas:

- **Quantum chemistry:** using [QSCI](https://arxiv.org/abs/2302.11320) and [Qiskit Nature](https://qiskit-community.github.io/qiskit-nature/)
  to estimate the ground-state energy of an H2 molecule.
- **Benchmarking:** running [MQT Bench](https://mqt.readthedocs.io/projects/bench/) programs such as
  [GHZ states](https://en.wikipedia.org/wiki/Greenberger%E2%80%93Horne%E2%80%93Zeilinger_state), [Deutsch-Jozsa](https://en.wikipedia.org/wiki/Deutsch%E2%80%93Jozsa_algorithm),
  [QFT](https://en.wikipedia.org/wiki/Quantum_Fourier_transform), [graph states](https://en.wikipedia.org/wiki/Graph_state),
  [W states](https://en.wikipedia.org/wiki/W_state), [Grover](https://en.wikipedia.org/wiki/Grover%27s_algorithm), or [Quantum Phase Estimation](https://en.wikipedia.org/wiki/Quantum_phase_estimation_algorithm).

#### IMPORTANT
The example scripts live in the QDMI-on-IQM repository and are not shipped with
the distribution of the [`iqm-qdmi` Python package](python_package.html.md) on PyPI.
Hence, you need to download or clone the repository to access them. You can do
so by running the following command in your terminal:

```console
git clone https://github.com/iqm-finland/QDMI-on-IQM.git
```

## Configure Your Environment

The IQM-backed path relies on a small environment-variable contract to
authenticate and route your jobs. Before running any of the examples, make sure
the following variables are set as needed:

- `IQM_SERVER_URL`: The endpoint of the IQM server you are targeting (e.g.,
  `https://resonance.iqm.tech` for IQM Resonance).
- `IQM_TOKEN`: Your authentication token.
- `IQM_QUANTUM_COMPUTER`: Optional explicit selection of the target quantum
  computer.

`IQM_BASE_URL` and `IQM_QC_ALIAS` remain supported as legacy aliases.

For the full set of authentication options available when configuring C++
sessions directly, see [Authentication Methods](usage.html.md#authentication-methods)
in the Usage Guide. Its
[session configuration example](usage.html.md#session-configuration) is taken from
the internal C++ test helper; it is not an installed client API.

You can either run the full suite of examples using the dedicated `nox` session
or individually execute the scripts from the command line.

```console
# Run the entire suite
uvx nox -s examples

# Run specific examples
./examples/qsci_h2.py --shots 256 --maxiter 5 --cutoff 4
./examples/mqt_bench.py --benchmark ghz --shots 128
```

## Quantum Chemistry

Our first workload is a quantum chemistry application that estimates the
ground-state energy of a hydrogen molecule. The script `examples/qsci_h2.py`
follows a [Quantum-Selected Configuration Interaction](https://arxiv.org/abs/2302.11320)-style workflow: a
hybrid quantum-classical approach that combines variational optimization,
circuit sampling, and classical diagonalization in a reduced subspace. To build
the chemistry problem, it leans on [Qiskit Nature](https://qiskit-community.github.io/qiskit-nature/) to map an
electronic-structure model to qubit operators.

By running `examples/qsci_h2.py`, you will:

1. Build an electronic-structure problem for an H2 molecule.
2. Map the physical system to qubits with [Qiskit Nature](https://qiskit-community.github.io/qiskit-nature/).
3. Optimize a UCCSD ansatz against an IQM backend.
4. Sample the circuit to gather bitstrings.
5. Reconstruct the energy estimate.

The script prints an execution trace along the way, showing the progress of the
demonstration. QSCI is a great first end-to-end workload beyond a simple toy
circuit.

#### NOTE
The QSCI example depends on PySCF for classical chemistry calculations, and
[PySCF is not supported on Windows](https://pyscf.org/user/install.html).

The QSCI example uses native Qiskit sampler and estimator primitives. Its
positive `--shots` value sets the sampler shot count and the estimator precision
to `1 / sqrt(shots)`. Qiskit rounds the estimator shot count up from
`1 / precision**2` for each measurement circuit and groups compatible
observables, so this value is not a total VQE shot budget.

## MQT Bench Programs

To understand how the backend behaves on standard programs, we move on to
[MQT Bench](https://mqt.readthedocs.io/projects/bench/). MQT Bench is an open-source benchmark suite that
collects representative quantum algorithms across several abstraction levels. In
this repository, the benchmark scripts show how to generate those programs,
transpile them for the selected target, execute them through
[`BackendSamplerV2`](https://quantum.cloud.ibm.com/docs/api/qiskit/qiskit.primitives.BackendSamplerV2), and inspect the resulting
bitstring distributions.

The `examples/mqt_bench.py` entrypoint currently covers the following
algorithms:

- `ghz`: Prepares a [GHZ state](https://en.wikipedia.org/wiki/Greenberger%E2%80%93Horne%E2%80%93Zeilinger_state), a highly entangled state that serves
  as a strong baseline test for multi-qubit entanglement fidelity.
- `dj`: Implements the [Deutsch-Jozsa algorithm](https://en.wikipedia.org/wiki/Deutsch%E2%80%93Jozsa_algorithm), one of the
  earliest examples of a quantum algorithm with an exponential query advantage
  over its classical counterpart.
- `qft`: Computes the [Quantum Fourier Transform](https://en.wikipedia.org/wiki/Quantum_Fourier_transform), a
  central building block used in algorithms such as phase estimation and Shor’s
  algorithm.
- `graphstate`: Generates [graph states](https://en.wikipedia.org/wiki/Graph_state), an important family of
  entangled states that also serve as key resources for
  [measurement-based quantum computing](https://en.wikipedia.org/wiki/Measurement-based_quantum_computation).
- `wstate`: Creates a [W state](https://en.wikipedia.org/wiki/W_state), a multipartite entangled state that
  retains pairwise entanglement even if one qubit is lost.
- `grover`: Implements [Grover’s algorithm](https://en.wikipedia.org/wiki/Grover%27s_algorithm), a quantum search algorithm
  that provides a quadratic speedup for unstructured search problems.
- `qpe`: Implements the [Quantum Phase Estimation algorithm](https://en.wikipedia.org/wiki/Quantum_phase_estimation_algorithm), a fundamental
  algorithm that estimates the eigenvalues of a unitary operator and underpins
  many quantum algorithms, including Shor’s factoring algorithm.

The benchmark entrypoint exposes a compact, consistent CLI:

- `--benchmark`: Selects the benchmark family to run.
- `--backend`: Selects `iqm` for hardware runs or `sim` for simulator runs.
- `--shots`: Controls how many samples are collected from the executed circuit.
- `--num-qubits`: Adjusts the problem size for the benchmark families that
  support it.

### Code Example: Preparing and Sampling from a GHZ State

The following snippet shows the benchmark runner in full. The same architecture
covers every supported benchmark family; only the selected benchmark and
validation logic differ.

```python

# /// script
# requires-python = ">=3.11"
# dependencies = [
#   "iqm-qdmi[qiskit]",
#   "mqt-bench>=2.2.2",
# ]
# [tool.uv.sources]
# iqm-qdmi = { path = ".." }
#
# [tool.ty.analysis]
# allowed-unresolved-imports = ["mqt.bench.**"]
# ///

"""Run an MQT Bench workload using the QDMI-on-IQM stack."""

from __future__ import annotations

import argparse
import logging
import sys
from dataclasses import dataclass

import numpy as np
from mqt.bench import BenchmarkLevel, get_benchmark
from mqt.core.plugins.qiskit.backend import QDMIBackend
from qiskit.quantum_info import hellinger_fidelity

from iqm.qdmi.qiskit import IQMBackend

log = logging.getLogger(__name__)


@dataclass(frozen=True)
class BenchmarkConfig:
    """Configuration for one benchmark family."""

    benchmark: str
    title: str
    default_shots: int
    default_qubits: int
    result_register: str
    description: str


BENCHMARKS: dict[str, BenchmarkConfig] = {
    "ghz": BenchmarkConfig(
        benchmark="ghz",
        title="GHZ",
        default_shots=1024,
        default_qubits=3,
        result_register="meas",
        description="GHZ state preparation for multi-qubit entanglement checks.",
    ),
    "dj": BenchmarkConfig(
        benchmark="dj",
        title="Deutsch-Jozsa",
        default_shots=1024,
        default_qubits=4,
        result_register="c",
        description="Deutsch-Jozsa oracle sampling.",
    ),
    "qft": BenchmarkConfig(
        benchmark="qft",
        title="QFT",
        default_shots=1024,
        default_qubits=3,
        result_register="meas",
        description="Quantum Fourier Transform sampling.",
    ),
    "graphstate": BenchmarkConfig(
        benchmark="graphstate",
        title="Graph State",
        default_shots=1024,
        default_qubits=4,
        result_register="meas",
        description="Graph-state preparation and sampling.",
    ),
    "wstate": BenchmarkConfig(
        benchmark="wstate",
        title="W State",
        default_shots=1024,
        default_qubits=3,
        result_register="meas",
        description="W-state sampling.",
    ),
    "grover": BenchmarkConfig(
        benchmark="grover",
        title="Grover",
        default_shots=8192,
        default_qubits=7,
        result_register="meas",
        description="Grover search sampling.",
    ),
    "qpe": BenchmarkConfig(
        benchmark="qpeexact",
        title="Quantum Phase Estimation",
        default_shots=8192,
        default_qubits=5,
        result_register="c",
        description="Quantum Phase Estimation sampling.",
    ),
}


def _build_backend(backend_name: str) -> QDMIBackend:
    if backend_name == "iqm":
        return IQMBackend()
    return QDMIBackend.from_device_id("mqt.ddsim.default")


def _describe_result(key: str, counts: dict[str, int], num_qubits: int, shots: int) -> str:
    """Summarize the observed result distribution for the selected benchmark.

    Returns:
        A short human-readable summary for the selected benchmark family.
    """
    if key == "ghz":
        expected = {"0" * num_qubits: shots // 2, "1" * num_qubits: shots - shots // 2}
        return f"GHZ fidelity={hellinger_fidelity(counts, expected):.7f}"
    if key == "dj":
        expected = {"1" * (num_qubits - 1): shots}
        return f"Deutsch-Jozsa fidelity={hellinger_fidelity(counts, expected):.7f}"
    if key == "qft":
        expected = {format(i, f"0{num_qubits}b"): shots / (2**num_qubits) for i in range(2**num_qubits)}
        return f"QFT fidelity={hellinger_fidelity(counts, expected):.7f}"
    if key == "graphstate":
        expected = {format(i, f"0{num_qubits}b"): shots / (2**num_qubits) for i in range(2**num_qubits)}
        return f"Graph-state fidelity={hellinger_fidelity(counts, expected):.7f}"
    if key == "wstate":
        expected = {
            f"{1 << (num_qubits - index - 1):0{num_qubits}b}": shots / num_qubits for index in range(num_qubits)
        }
        return f"W-state fidelity={hellinger_fidelity(counts, expected):.7f}"
    if key == "grover":
        actual_qubits = num_qubits - 1
        r = int(np.pi / 4 * np.sqrt(2**actual_qubits))
        theta = 2 * np.arcsin(1 / np.sqrt(2**actual_qubits))
        success_prob = float(np.sin((r + 0.5) * theta) ** 2)
        expected: dict[str, float] = {"1" * num_qubits: success_prob * shots}
        if success_prob < 1:
            remaining_prob = 1 - success_prob
            remaining_prob_per_bitstring = remaining_prob * shots / (2**actual_qubits - 1)
            for index in range(2**actual_qubits):
                bitstring = format(index, f"0{actual_qubits}b")
                if bitstring != "1" * actual_qubits:
                    expected["1" + bitstring] = remaining_prob_per_bitstring
        return f"Grover fidelity={hellinger_fidelity(counts, expected):.7f}"
    ideal_bitstring = max(counts, key=counts.__getitem__)
    expected = {ideal_bitstring: sum(counts.values())}
    return f"QPE fidelity={hellinger_fidelity(counts, expected):.7f}"


def main() -> None:
    """Run one of the MQT Bench showcase circuits."""
    logging.basicConfig(
        level=logging.INFO,
        format="%(asctime)s [%(levelname)s] %(message)s",
        datefmt="%Y-%m-%dT%H:%M:%S",
    )

    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument("--benchmark", choices=tuple(BENCHMARKS), default="ghz")
    parser.add_argument("--backend", choices=("iqm", "sim"), default="iqm")
    parser.add_argument("--shots", type=int, default=None)
    parser.add_argument("--num-qubits", type=int, default=None)
    args = parser.parse_args()

    config = BENCHMARKS[args.benchmark]
    shots = args.shots or config.default_shots
    num_qubits = args.num_qubits or config.default_qubits
    log.info(
        "Starting %s example (backend=%s, qubits=%d, shots=%d)",
        config.title,
        args.backend,
        num_qubits,
        shots,
    )

    log.info("Initialising '%s' backend...", args.backend)
    backend = _build_backend(args.backend)
    log.info("Backend ready: '%s' | %d qubits", backend.name, backend.num_qubits)

    if backend.num_qubits < num_qubits:
        sys.exit(
            f"Selected backend exposes {backend.num_qubits} qubits, but the {config.title} example needs {num_qubits}."
        )

    log.info("Building and mapping %s benchmark circuit (n=%d)...", config.title, num_qubits)
    circuit = get_benchmark(
        benchmark=config.benchmark,
        level=BenchmarkLevel.MAPPED,
        circuit_size=num_qubits,
        target=backend.target,
    )
    log.info("Circuit ready: %d qubits, %d gates, depth %d", circuit.num_qubits, circuit.size(), circuit.depth())

    log.info("Submitting job to '%s' (%d shots)...", backend.name, shots)
    sampler = backend.sampler(default_shots=shots)
    job = sampler.run([(circuit,)])
    counts: dict[str, int] = job.result()[0].data[config.result_register].get_counts()
    total_shots = sum(counts.values())
    log.info("Job completed. Collected %d shots across %d distinct bitstrings.", total_shots, len(counts))
    log.info("Measured counts: %s", sorted(counts.items()))
    log.info("Validation summary: %s", _describe_result(args.benchmark, counts, num_qubits, shots))
    log.info("Done.")


if __name__ == "__main__":
    main()
```

Execution on a simulator backend should yield a near-perfect distribution of the
expected bitstrings (all 0s and all 1s for the GHZ state):

```ipython3
!../examples/mqt_bench.py --benchmark ghz --backend sim --shots 8192 --num-qubits 20
```

```myst-ansi
2026-10-10T04:53:47 [INFO] Starting GHZ example (backend=sim, qubits=20, shots=8192)
2026-10-10T04:53:47 [INFO] Initialising 'sim' backend...
2026-10-10T04:53:47 [INFO] Backend ready: 'MQT Core DDSIM QDMI Device' | 65535 qubits
2026-10-10T04:53:47 [INFO] Building and mapping GHZ benchmark circuit (n=20)...
```

```myst-ansi
2026-10-10T04:53:47 [INFO] Pass: ContainsInstruction - 0.01454 (ms)
2026-10-10T04:53:47 [INFO] Pass: UnitarySynthesis - 0.02956 (ms)
2026-10-10T04:53:47 [INFO] Pass: HighLevelSynthesis - 0.02289 (ms)
2026-10-10T04:53:47 [INFO] Pass: BasisTranslator - 0.03886 (ms)
2026-10-10T04:53:47 [INFO] Pass: ElidePermutations - 0.00739 (ms)
2026-10-10T04:53:47 [INFO] Pass: RemoveDiagonalGatesBeforeMeasure - 0.01717 (ms)
2026-10-10T04:53:47 [INFO] Pass: RemoveIdentityEquivalent - 0.01216 (ms)
2026-10-10T04:53:47 [INFO] Pass: InverseCancellation - 0.01907 (ms)
2026-10-10T04:53:47 [INFO] Pass: ContractIdleWiresInControlFlow - 0.00262 (ms)
2026-10-10T04:53:47 [INFO] Pass: CommutativeCancellation - 0.59271 (ms)
2026-10-10T04:53:47 [INFO] Pass: ConsolidateBlocks - 0.17619 (ms)
2026-10-10T04:53:47 [INFO] Pass: Split2QUnitaries - 0.00429 (ms)
2026-10-10T04:53:47 [INFO] Pass: UnitarySynthesis - 0.00882 (ms)
2026-10-10T04:53:47 [INFO] Pass: HighLevelSynthesis - 0.00978 (ms)
2026-10-10T04:53:47 [INFO] Pass: BasisTranslator - 0.03839 (ms)
2026-10-10T04:53:47 [INFO] Pass: TwoQubitPeepholeOptimization - 14.78958 (ms)
2026-10-10T04:53:47 [INFO] Pass: Size - 0.00954 (ms)
2026-10-10T04:53:47 [INFO] Pass: Depth - 0.01860 (ms)
2026-10-10T04:53:47 [INFO] Pass: FixedPoint - 0.00978 (ms)
2026-10-10T04:53:47 [INFO] Pass: FixedPoint - 0.00477 (ms)
2026-10-10T04:53:47 [INFO] Pass: RemoveIdentityEquivalent - 0.01025 (ms)
2026-10-10T04:53:47 [INFO] Pass: Optimize1qGatesDecomposition - 0.07439 (ms)
2026-10-10T04:53:47 [INFO] Pass: CommutativeCancellation - 0.07987 (ms)
2026-10-10T04:53:47 [INFO] Pass: ContractIdleWiresInControlFlow - 0.00310 (ms)
2026-10-10T04:53:47 [INFO] Pass: GatesInBasis - 0.01025 (ms)
2026-10-10T04:53:47 [INFO] Pass: Size - 0.00262 (ms)
2026-10-10T04:53:47 [INFO] Pass: Depth - 0.00906 (ms)
2026-10-10T04:53:47 [INFO] Pass: FixedPoint - 0.00334 (ms)
2026-10-10T04:53:47 [INFO] Pass: FixedPoint - 0.00310 (ms)
2026-10-10T04:53:47 [INFO] Pass: ContainsInstruction - 0.00811 (ms)
2026-10-10T04:53:48 [INFO] Total Transpile Time - 208.49061 (ms)
2026-10-10T04:53:48 [INFO] Circuit ready: 20 qubits, 40 gates, depth 21
2026-10-10T04:53:48 [INFO] Submitting job to 'MQT Core DDSIM QDMI Device' (8192 shots)...
```

```myst-ansi
2026-10-10T04:53:48 [INFO] Job completed. Collected 8192 shots across 2 distinct bitstrings.
2026-10-10T04:53:48 [INFO] Measured counts: [('00000000000000000000', 4118), ('11111111111111111111', 4074)]
2026-10-10T04:53:48 [INFO] Validation summary: GHZ fidelity=0.9999928
2026-10-10T04:53:48 [INFO] Done.
```

Now try running the same script with `--backend iqm` to see how the distribution
looks on real hardware. Remember to set the required environment variables for
authentication before running the script.
