Runnable examples

The examples use finite shots and default to MQT Core’s local QDMI simulator. The showcase includes full Quantum-Selected Configuration Interaction (QSCI) for H₂ and seven MQT Bench algorithm families. The workloads use the shared Qiskit sampler and estimator primitives through QDMI.

The scripts live in the repository, rather than the installed Python package. Clone the repository and run commands from its root:

git clone https://github.com/munich-quantum-software/ibm-qdmi-device.git
cd ibm-qdmi-device

Install the example dependencies and run the portable examples:

uv sync --group examples
uv run --group examples python -m examples.native_job
uv run --group examples python -m examples.qiskit_workloads --workload bell
uv run --group examples python -m examples.mqt_bench --benchmark ghz
uv run --group examples python -m examples.qiskit_workloads --workload h2
uv run --group examples python -m examples.pennylane_qaoa

The basic native, Qiskit, and PennyLane commands print JSON. The showcase runners log circuit sizes, execution progress, counts, and validation summaries. Use --shots to select a positive shot count. Basic examples default to 128; the tables below give showcase defaults. native_job also accepts --timeout in seconds, defaults to 60, and attempts cancellation if its wait or result retrieval fails.

Execute in the documentation

These cells call the same example functions on MQT Core’s local simulator. They always select sim, require no IBM credentials, and submit no hardware jobs. The build caches successful execution and fails if a cell raises an error.

from pathlib import Path
import sys

sys.path.insert(0, str(Path.cwd().parent))

from examples.common import open_backend
from examples.native_job import run
from examples.qiskit_workloads import estimate_h2, sample_bell

backend = open_backend("sim", None)
counts = run(backend.device, shots=128, qubits=1)
assert counts == {"1": 128}
counts
{'1': 128}
counts = sample_bell(backend, shots=128)
assert set(counts) <= {"00", "11"}
assert sum(counts.values()) == 128
counts
{'00': 61, '11': 67}
energy = estimate_h2(backend, shots=128)
assert -2.0 < energy < -1.0
energy
-1.8540827624905516

Native QDMI jobs

examples/native_job.py opens a QDMI device, submits a full-width OpenQASM 3 program with one measured X gate, waits for completion, and reads counts. It accesses the native job interface directly through MQT Core’s bindings. The expected result is {"1": 128} with the default shot count. The simulator uses a one-qubit program; IBM uses the full physical register.

See the native API contract for session configuration, result sizing, job states, and cancellation semantics. Releasing a local job handle does not cancel a remote job.

examples/native/execute.c demonstrates the same lifecycle with the QDMI C ABI. Build it against a native installation containing both runtime and development components:

cmake -S examples/native -B build/example -DCMAKE_PREFIX_PATH=/path/to/install
cmake --build build/example --config Release

Running ibm-qdmi-execute without arguments prints help and makes no requests. To submit a hardware job, provide IBM_QUANTUM_API_KEY and IBM_QUANTUM_INSTANCE_CRN in the environment, then invoke ibm-qdmi-execute --run ibm_berlin (or ibm_aachen). This program submits one 16-shot job with a 60-second execution limit and a bounded wait. Use --timeout 1..60 to shorten the wait. It attempts cancellation after a timeout or execution failure and always releases local handles. The optional --test-port accepts a port from 1 to 65535 and connects only to 127.0.0.1 for synthetic tests. Session initialization loads credentials through the native library’s environment support.

Qiskit sampling and benchmarks

The Bell example transpiles a two-qubit circuit to the selected target and runs the shared sampler primitive. Counts contain only 00 and 11.

MQT Bench showcase

examples/mqt_bench.py generates a benchmark at MQT Bench’s mapped level using the selected backend’s target. It runs the resulting circuit through backend.sampler(default_shots=shots) and reads the benchmark’s classical register. Mapping and sampling preserve logical measurement order, even when hardware routing changes physical qubit positions.

--benchmark

Program

Default qubits

Default shots

Distribution check

ghz

GHZ entanglement

3

1024

Equal all-zero and all-one outcomes

dj

Deutsch–Jozsa oracle

4

1024

All-one outcome on the query register

qft

Quantum Fourier transform

3

1024

Uniform computational-basis distribution

graphstate

Graph-state preparation

4

1024

Uniform computational-basis distribution

wstate

W-state preparation

3

1024

Uniform single-excitation outcomes

grover

Grover search

7

8192

Marked-state probability after amplitude amplification

qpe

Exact quantum phase estimation

5

8192

Concentration in the most frequent outcome

GHZ and W states demonstrate different forms of multipartite entanglement. Deutsch–Jozsa queries an oracle; Grover amplifies a marked search result. The Fourier transform and phase estimation are building blocks for quantum algorithms. Graph states also support measurement-based quantum computation.

The runner compares measured distributions with ideal probabilities using Hellinger fidelity. QPE reports modal concentration; this diagnostic alone cannot establish that the dominant phase is correct. These are computational-basis checks, not complete state tomography. Finite sampling and hardware noise can reduce the reported scores.

Select a family and optionally override its size and shot count:

uv run --group examples python -m examples.mqt_bench --benchmark ghz --num-qubits 20 --shots 8192
uv run --group examples python -m examples.mqt_bench --benchmark dj
uv run --group examples python -m examples.mqt_bench --benchmark qft
uv run --group examples python -m examples.mqt_bench --benchmark graphstate
uv run --group examples python -m examples.mqt_bench --benchmark wstate
uv run --group examples python -m examples.mqt_bench --benchmark grover
uv run --group examples python -m examples.mqt_bench --benchmark qpe

--benchmark defaults to ghz. --num-qubits requires at least two qubits (three for graphstate), and the selected backend must support the requested width. Deutsch–Jozsa and Grover include an ancillary qubit in that width; QPE includes its eigenstate qubit. The runner selects each program’s correct result register.

Execute a GHZ benchmark

This cell runs the same public runner on the local simulator. Its twenty-qubit GHZ distribution contains only all-zero and all-one strings.

from examples.mqt_bench import run as run_benchmark

counts = run_benchmark(backend, "ghz", shots=8192, num_qubits=20)
assert set(counts) <= {"0" * 20, "1" * 20}
assert sum(counts.values()) == 8192
counts
{'11111111111111111111': 4065, '00000000000000000000': 4127}

Benchmark source

examples/mqt_bench.py
from __future__ import annotations

import logging
import math
from dataclasses import dataclass
from typing import TYPE_CHECKING

import numpy as np
from mqt.bench import BenchmarkLevel, get_benchmark
from qiskit.quantum_info import hellinger_fidelity

from examples.common import open_backend, parser, positive_integer

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 _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 / 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 in {"qft", "graphstate"}:
        # Unobserved outcomes contribute zero to the Hellinger overlap.
        total = sum(counts.values())
        fidelity = math.fsum(math.sqrt(count / total) for count in counts.values()) ** 2 * 2.0**-num_qubits
        title = "QFT" if key == "qft" else "Graph-state"
        return f"{title} fidelity={fidelity:.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)
        other_prob = (1 - success_prob) / (2**actual_qubits - 1)
        total = sum(counts.values())
        fidelity = (
            math.fsum(
                math.sqrt(count / total * (success_prob if state == "1" * num_qubits else other_prob))
                for state, count in counts.items()
                if state.startswith("1")
            )
            ** 2
        )
        return f"Grover fidelity={fidelity:.7f}"
    ideal_bitstring = max(counts, key=counts.__getitem__)
    expected = {ideal_bitstring: sum(counts.values())}
    return f"QPE modal outcome={ideal_bitstring}, concentration={hellinger_fidelity(counts, expected):.7f}"


def run(backend: QDMIBackend, benchmark: str = "ghz", *, shots: int = 1024, num_qubits: int = 3) -> dict[str, int]:
    """Map and sample a benchmark on the selected target.

    Returns:
        Counts in the benchmark's logical classical register.

    Raises:
        ValueError: The benchmark, shot count, or problem size is invalid.
    """
    if benchmark not in BENCHMARKS:
        msg = f"Unknown benchmark: {benchmark}"
        raise ValueError(msg)
    if shots <= 0 or num_qubits < 2:
        msg = "Use positive shots and at least two qubits."
        raise ValueError(msg)
    if benchmark == "graphstate" and num_qubits < 3:
        msg = "Graph-state benchmarks require at least three qubits."
        raise ValueError(msg)
    if backend.num_qubits < num_qubits:
        msg = f"Backend exposes {backend.num_qubits} qubits; requested {num_qubits}."
        raise ValueError(msg)
    config = BENCHMARKS[benchmark]
    circuit = get_benchmark(
        benchmark=config.benchmark,
        level=BenchmarkLevel.MAPPED,
        circuit_size=num_qubits,
        target=backend.target,
    )
    log.info(
        "%s circuit: %d qubits, %d gates, depth %d", config.title, circuit.num_qubits, circuit.size(), circuit.depth()
    )
    result = backend.sampler(default_shots=shots).run([(circuit,)]).result()
    counts: dict[str, int] = result[0].data[config.result_register].get_counts()
    log.info("Collected %d shots: %s", sum(counts.values()), sorted(counts.items()))
    log.info("Validation: %s", _describe_result(benchmark, counts, num_qubits, shots))
    return counts


def main() -> None:
    """Run one of the MQT Bench showcase circuits."""
    logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s")
    arguments = parser(__doc__ or "")
    arguments.set_defaults(shots=None)
    arguments.add_argument("--benchmark", choices=tuple(BENCHMARKS), default="ghz")
    arguments.add_argument("--num-qubits", type=positive_integer)
    options = arguments.parse_args()
    if options.backend == "ibm" and options.device is None:
        arguments.error("--backend ibm requires --device")
    config = BENCHMARKS[options.benchmark]
    num_qubits = options.num_qubits if options.num_qubits is not None else config.default_qubits
    if num_qubits < 2:
        arguments.error("--num-qubits must be at least two")
    if options.benchmark == "graphstate" and num_qubits < 3:
        arguments.error("graphstate requires at least three qubits")
    backend = open_backend(options.backend, options.device)
    run(
        backend,
        options.benchmark,
        shots=options.shots if options.shots is not None else config.default_shots,
        num_qubits=num_qubits,
    )


if __name__ == "__main__":
    main()

H₂ QSCI showcase

examples/qsci_h2.py follows the complete quantum chemistry workflow:

  1. PySCF builds H₂ at a bond length of 1 Å in the STO-3G basis.

  2. Qiskit Nature maps its electronic Hamiltonian to four qubits with Jordan–Wigner encoding.

  3. A Hartree–Fock state initializes a unitary coupled-cluster singles and doubles (UCCSD) ansatz.

  4. The variational quantum eigensolver (VQE) optimizes that ansatz using the backend estimator and the derivative-free COBYLA optimizer.

  5. The backend sampler measures the optimized circuit in logical orbital order.

  6. QSCI retains the most frequent states with one alpha and one beta electron.

  7. Classical diagonalization finds the lowest energy in that selected subspace; nuclear repulsion gives the total energy.

The reduced Hamiltonian comes from the same molecular integrals and logical qubit operator used by VQE. For this four-qubit molecule, projecting its 16-by-16 matrix is inexpensive. Larger molecules require sparse matrix-element construction instead of a full matrix.

The reference total energy is approximately −1.101150 hartree. The runner logs the VQE electronic energy, sampled counts, QSCI total energy, and absolute difference from that reference. A finite evaluation limit does not guarantee VQE convergence. Missing determinants can raise the QSCI energy; the cutoff limits the selected subspace, and nuclear repulsion is added once.

Chemistry dependencies and execution

Qiskit Nature and PySCF provide the molecular model. PySCF supports Linux and macOS. On Windows, run this example in a supported Linux environment, such as WSL. The portable benchmark and basic examples remain available on Windows.

Use Python 3.11–3.13 with the optional chemistry dependency group:

uv run --python 3.13 --group chemistry python -m examples.qsci_h2 --backend sim
uv run --python 3.13 --group chemistry python -m examples.qsci_h2 --shots 256 --maxiter 5 --cutoff 4

Option

Default

Meaning

--shots

8192

Sampler shots and estimator precision 1 / sqrt(shots)

--maxiter

30

Maximum COBYLA objective evaluations

--cutoff

10

Maximum number of valid sampled determinants

All three values must be positive. Qiskit rounds the estimator shot count up from 1 / precision**2 for each measurement circuit and groups compatible observables. Therefore, --shots is not a total VQE shot budget. Each optimizer evaluation can require several circuits. COBYLA needs at least five evaluations to initialize this three-parameter ansatz; smaller limits are raised to five by SciPy. The final sampling call uses the requested shot count.

VQE applies the transpiled layout to the observable internally. Final sampling adds measurements before mapping, so QSCI receives logical spin-orbital bitstrings rather than physical hardware indices.

QSCI source

examples/qsci_h2.py
from __future__ import annotations

import logging
import math
import sys
from typing import TYPE_CHECKING, cast

import numpy as np
from qiskit import transpile
from qiskit_algorithms.minimum_eigensolvers import VQE
from qiskit_algorithms.optimizers import SciPyOptimizer

from examples.common import open_backend, parser, positive_integer

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

log = logging.getLogger(__name__)
ATOM = "H 0 0 0; H 0 0 1"
BASIS = "sto-3g"
EXPECTED_ENERGY = -1.101150


def postprocess_counts(observable: SparsePauliOp, counts: dict[str, int], *, cutoff: int = 10) -> float:
    """Diagonalize the H2 Hamiltonian in the most frequently sampled valid states.

    The Jordan-Wigner register contains two alpha and two beta spin orbitals.
    Keep one electron in each spin sector. Integer bitstrings index the original
    logical Hamiltonian in Qiskit's little-endian convention.

    Returns:
        The lowest electronic energy in the selected subspace, in hartrees.

    Raises:
        ValueError: Counts, Hamiltonian width, or cutoff are invalid, or no valid states remain.
    """
    if observable.num_qubits != 4 or cutoff <= 0 or not counts:
        msg = "Use a four-qubit H2 Hamiltonian, nonempty counts, and a positive cutoff."
        raise ValueError(msg)
    if any(len(state) != 4 or set(state) - {"0", "1"} or count <= 0 for state, count in counts.items()):
        msg = "Counts must contain four-bit states and positive frequencies."
        raise ValueError(msg)
    selected = [
        int(state, 2)
        for state in sorted(counts, key=lambda state: (-counts[state], state))
        if state[:2].count("1") == state[2:].count("1") == 1
    ][:cutoff]
    if not selected:
        msg = "No states remain after filtering for one alpha and one beta electron."
        raise ValueError(msg)
    # This H2 example has only 16 basis states; larger molecules need sparse matrix elements.
    hamiltonian = observable.to_matrix()
    reduced = hamiltonian[np.ix_(selected, selected)]
    return float(np.linalg.eigvalsh(reduced)[0])


def optimize_and_sample(
    backend: QDMIBackend,
    ansatz: QuantumCircuit,
    observable: SparsePauliOp,
    *,
    shots: int = 8192,
    maxiter: int = 30,
) -> tuple[float, dict[str, int]]:
    """Run finite-shot VQE and sample the optimized state in logical qubit order.

    Returns:
        The VQE electronic energy and logical measurement counts.

    Raises:
        ValueError: Shots, iteration count, or backend width are invalid.
        RuntimeError: VQE returns no optimal parameters.
    """
    if shots <= 0 or maxiter <= 0 or backend.num_qubits < ansatz.num_qubits:
        msg = "Use positive shots and iterations and a backend wide enough for the ansatz."
        raise ValueError(msg)
    mapped = transpile(ansatz, backend, optimization_level=2, seed_transpiler=7)
    # Avoid numerical gradients whose tiny differences are dominated by shot noise.
    optimizer = SciPyOptimizer(method="COBYLA", options={"maxiter": maxiter})
    estimator = backend.estimator(default_precision=1 / math.sqrt(shots))
    # VQE applies mapped.layout to this logical observable internally.
    vqe = VQE(estimator, mapped, optimizer, initial_point=np.full(mapped.num_parameters, 0.1))
    result = vqe.compute_minimum_eigenvalue(observable)
    if result.optimal_parameters is None or result.eigenvalue is None:
        msg = "VQE returned no optimal parameters."
        raise RuntimeError(msg)
    energy = float(result.eigenvalue.real)
    log.info("VQE electronic energy: %.6f Ha (%s evaluations)", energy, result.optimizer_evals)
    # Add measurements before a fresh mapping to preserve logical bit order after routing.
    measured = ansatz.assign_parameters(result.optimal_parameters)
    measured.measure_all()
    sampled = transpile(measured, backend, optimization_level=2, seed_transpiler=7)
    counts = backend.sampler(default_shots=shots).run([(sampled,)]).result()[0].data["meas"].get_counts()
    log.info("Sampled %d shots: %s", sum(counts.values()), sorted(counts.items()))
    return energy, counts


def run(backend: QDMIBackend, *, shots: int = 8192, maxiter: int = 30, cutoff: int = 10) -> float:
    """Build the H2 electronic problem and run VQE, sampling, and QSCI.

    Returns:
        The QSCI total energy, including nuclear repulsion, in hartrees.

    Raises:
        ValueError: The cutoff is not positive.
    """
    # Keep chemistry optional for --help and portable Windows tests.
    from qiskit_nature.second_q.circuit.library import UCCSD, HartreeFock  # ruff: ignore[import-outside-top-level]
    from qiskit_nature.second_q.drivers import PySCFDriver  # ruff: ignore[import-outside-top-level]
    from qiskit_nature.second_q.mappers import JordanWignerMapper  # ruff: ignore[import-outside-top-level]

    if cutoff <= 0:
        msg = "The cutoff must be positive."
        raise ValueError(msg)
    log.info("Building H2 at 1 angstrom in the %s basis", BASIS)
    problem = PySCFDriver(atom=ATOM, basis=BASIS).run()
    mapper = JordanWignerMapper()
    initial_state = HartreeFock(problem.num_spatial_orbitals, problem.num_particles, mapper)
    ansatz = UCCSD(problem.num_spatial_orbitals, problem.num_particles, mapper, initial_state=initial_state)
    observable = cast("SparsePauliOp", mapper.map(problem.hamiltonian.second_q_op()))
    _, counts = optimize_and_sample(backend, ansatz, observable, shots=shots, maxiter=maxiter)
    electronic_energy = postprocess_counts(observable, counts, cutoff=cutoff)
    total_energy = electronic_energy + (problem.hamiltonian.nuclear_repulsion_energy or 0.0)
    log.info("QSCI total energy: %.6f Ha", total_energy)
    log.info("Absolute difference from %.6f Ha: %.6f Ha", EXPECTED_ENERGY, abs(total_energy - EXPECTED_ENERGY))
    return total_energy


def main() -> None:
    """Run the chemistry showcase with explicit simulator or IBM selection."""
    logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s")
    arguments = parser(__doc__ or "")
    arguments.set_defaults(shots=8192)
    arguments.add_argument("--maxiter", type=positive_integer, default=30)
    arguments.add_argument("--cutoff", type=positive_integer, default=10)
    options = arguments.parse_args()
    if options.backend == "ibm" and options.device is None:
        arguments.error("--backend ibm requires --device")
    if sys.platform == "win32":
        arguments.error("QSCI requires PySCF on Linux or macOS; use a supported environment, such as WSL on Windows.")
    backend = open_backend(options.backend, options.device)
    run(backend, shots=options.shots, maxiter=options.maxiter, cutoff=options.cutoff)


if __name__ == "__main__":
    main()

H₂ energy estimation

The H₂ example prepares one parameterized trial state and evaluates the two-qubit Hamiltonian from IBM Quantum Learning’s variational examples. It applies the transpiled layout to the observable before calling the shared estimator, with precision 1 / sqrt(shots). The output is an energy estimate in hartrees. This example performs one evaluation; it does not optimize the state or calculate a molecular Hamiltonian from a geometry. The fixed Hamiltonian keeps the example portable without a chemistry dependency.

PennyLane QAOA

The QAOA example evaluates one layer for the two-vertex MaxCut problem and samples its state. It runs two finite-shot QNodes, one for the cut-value expectation and one for counts. The objective lies between zero and one. This is a fixed-parameter evaluation, without an optimization loop or gradient jobs.

Selecting IBM hardware

Hardware access is explicit. Configure credentials and the instance as described in Qiskit guide, then select both the IBM backend and a catalogue device:

uv run --group examples python -m examples.mqt_bench --backend ibm --device ibm.berlin --benchmark ghz --shots 128
uv run --python 3.13 --group chemistry python -m examples.qsci_h2 --backend ibm --device ibm.berlin --shots 256 --maxiter 5 --cutoff 4

Use ibm.aachen for Aachen or ibm.default with a configured backend name. The same options apply to all examples. Hardware jobs may incur charges; the shot count does not bound the total execution time or cost of a workload. Estimator workloads can submit several circuits. Configure native execution limits through the device API as needed.

Offline validation

The documentation executes only the simulator cells above. The full chemistry workflow runs in a separate session with PySCF, outside the documentation build:

uvx nox -s examples
uvx nox -s chemistry
uvx nox -s docs -- -D nb_execution_mode=force

The examples session exercises all seven benchmarks, QSCI post-processing, and the VQE/sampler integration. It also checks basic examples against a synthetic IBM loopback service. The chemistry session builds the molecular Hamiltonian and runs the complete H₂ workflow on a local simulator using Python 3.13. Linux CI runs both sessions without IBM credentials. On Windows, the chemistry session skips because PySCF is unavailable.