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.

Whether you want to estimate molecular ground-state energies with QSCI or benchmark hardware with MQT Bench, the example scripts in this repository provide a practical starting point. This tutorial focuses on two application areas:

Important

The example scripts live in the QDMI-on-IQM repository and are not shipped with the distribution of the iqm-qdmi Python package 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:

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_BASE_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_QC_ALIAS: Optional explicit selection of the target quantum computer.

For the full set of authentication options available when configuring C++ sessions directly, see Authentication Methods in the Usage Guide.

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

# 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-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 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.

  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.

MQT Bench Programs

To understand how the backend behaves on standard programs, we move on to MQT 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 QDMISampler, and inspect the resulting bitstring distributions.

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

  • ghz: Prepares a GHZ state, a highly entangled state that serves as a strong baseline test for multi-qubit entanglement fidelity.

  • dj: Implements the Deutsch-Jozsa 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, a central building block used in algorithms such as phase estimation and Shor’s algorithm.

  • graphstate: Generates graph states, an important family of entangled states that also serve as key resources for measurement-based quantum computing.

  • wstate: Creates a W state, a multipartite entangled state that retains pairwise entanglement even if one qubit is lost.

  • grover: Implements Grover’s algorithm, a quantum search algorithm that provides a quadratic speedup for unstructured search problems.

  • qpe: Implements the 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.

examples/mqt_bench.py

# /// script
# requires-python = ">=3.10"
# dependencies = [
#   "iqm-qdmi[qiskit]",
#   "mqt-bench>=2.2.2",
# ]
# [tool.uv.sources]
# iqm-qdmi = { path = ".." }
# ///

"""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
from typing import TYPE_CHECKING

import numpy as np
from mqt.bench import BenchmarkLevel, get_benchmark
from mqt.core.plugins.qiskit.provider import QDMIProvider
from mqt.core.plugins.qiskit.sampler import QDMISampler
from qiskit.quantum_info import hellinger_fidelity

from iqm.qdmi.qiskit import IQMBackend

if TYPE_CHECKING:
    from mqt.core.plugins.qiskit.backend import QDMIBackend

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 QDMIProvider().get_backend("MQT Core DDSIM QDMI Device")


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 = QDMISampler(backend, 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):

1!../examples/mqt_bench.py --benchmark ghz --backend sim --shots 8192 --num-qubits 20
2026-07-21T09:46:19 [INFO] Starting GHZ example (backend=sim, qubits=20, shots=8192)
2026-07-21T09:46:19 [INFO] Initialising 'sim' backend...
2026-07-21T09:46:19 [INFO] Backend ready: 'MQT Core DDSIM QDMI Device' | 65535 qubits
2026-07-21T09:46:19 [INFO] Building and mapping GHZ benchmark circuit (n=20)...
2026-07-21T09:46:20 [INFO] Pass: ContainsInstruction - 0.01049 (ms)
2026-07-21T09:46:20 [INFO] Pass: UnitarySynthesis - 0.00691 (ms)
2026-07-21T09:46:20 [INFO] Pass: HighLevelSynthesis - 0.00644 (ms)
2026-07-21T09:46:20 [INFO] Pass: BasisTranslator - 0.03624 (ms)
2026-07-21T09:46:20 [INFO] Pass: ElidePermutations - 0.01240 (ms)
2026-07-21T09:46:20 [INFO] Pass: RemoveDiagonalGatesBeforeMeasure - 0.01502 (ms)
2026-07-21T09:46:20 [INFO] Pass: RemoveIdentityEquivalent - 0.00739 (ms)
2026-07-21T09:46:20 [INFO] Pass: InverseCancellation - 0.01645 (ms)
2026-07-21T09:46:20 [INFO] Pass: ContractIdleWiresInControlFlow - 0.00310 (ms)
2026-07-21T09:46:20 [INFO] Pass: CommutativeCancellation - 0.22125 (ms)
2026-07-21T09:46:20 [INFO] Pass: ConsolidateBlocks - 0.14281 (ms)
2026-07-21T09:46:20 [INFO] Pass: Split2QUnitaries - 0.00381 (ms)
2026-07-21T09:46:20 [INFO] Pass: UnitarySynthesis - 0.00644 (ms)
2026-07-21T09:46:20 [INFO] Pass: HighLevelSynthesis - 0.00858 (ms)
2026-07-21T09:46:20 [INFO] Pass: BasisTranslator - 0.02646 (ms)
2026-07-21T09:46:20 [INFO] Pass: TwoQubitPeepholeOptimization - 9.84192 (ms)
2026-07-21T09:46:20 [INFO] Pass: Size - 0.01001 (ms)
2026-07-21T09:46:20 [INFO] Pass: Depth - 0.01788 (ms)
2026-07-21T09:46:20 [INFO] Pass: FixedPoint - 0.01359 (ms)
2026-07-21T09:46:20 [INFO] Pass: FixedPoint - 0.00572 (ms)
2026-07-21T09:46:20 [INFO] Pass: RemoveIdentityEquivalent - 0.01073 (ms)
2026-07-21T09:46:20 [INFO] Pass: Optimize1qGatesDecomposition - 0.05007 (ms)
2026-07-21T09:46:20 [INFO] Pass: CommutativeCancellation - 0.09608 (ms)
2026-07-21T09:46:20 [INFO] Pass: ContractIdleWiresInControlFlow - 0.00381 (ms)
2026-07-21T09:46:20 [INFO] Pass: GatesInBasis - 0.01121 (ms)
2026-07-21T09:46:20 [INFO] Pass: Size - 0.00405 (ms)
2026-07-21T09:46:20 [INFO] Pass: Depth - 0.01001 (ms)
2026-07-21T09:46:20 [INFO] Pass: FixedPoint - 0.00548 (ms)
2026-07-21T09:46:20 [INFO] Pass: FixedPoint - 0.00525 (ms)
2026-07-21T09:46:20 [INFO] Pass: ContainsInstruction - 0.00882 (ms)
2026-07-21T09:46:20 [INFO] Total Transpile Time - 291.25834 (ms)
2026-07-21T09:46:20 [INFO] Circuit ready: 20 qubits, 40 gates, depth 21
2026-07-21T09:46:20 [INFO] Submitting job to 'MQT Core DDSIM QDMI Device' (8192 shots)...
2026-07-21T09:46:20 [INFO] Job completed. Collected 8192 shots across 2 distinct bitstrings.
2026-07-21T09:46:20 [INFO] Measured counts: [('00000000000000000000', 3987), ('11111111111111111111', 4205)]
2026-07-21T09:46:20 [INFO] Validation summary: GHZ fidelity=0.9998229
2026-07-21T09:46:20 [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.