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.
|
Program |
Default qubits |
Default shots |
Distribution check |
|---|---|---|---|---|
|
GHZ entanglement |
3 |
1024 |
Equal all-zero and all-one outcomes |
|
Deutsch–Jozsa oracle |
4 |
1024 |
All-one outcome on the query register |
|
Quantum Fourier transform |
3 |
1024 |
Uniform computational-basis distribution |
|
Graph-state preparation |
4 |
1024 |
Uniform computational-basis distribution |
|
W-state preparation |
3 |
1024 |
Uniform single-excitation outcomes |
|
Grover search |
7 |
8192 |
Marked-state probability after amplitude amplification |
|
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¶
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:
PySCF builds H₂ at a bond length of 1 Å in the STO-3G basis.
Qiskit Nature maps its electronic Hamiltonian to four qubits with Jordan–Wigner encoding.
A Hartree–Fock state initializes a unitary coupled-cluster singles and doubles (UCCSD) ansatz.
The variational quantum eigensolver (VQE) optimizes that ansatz using the backend estimator and the derivative-free COBYLA optimizer.
The backend sampler measures the optimized circuit in logical orbital order.
QSCI retains the most frequent states with one alpha and one beta electron.
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 |
|---|---|---|
|
8192 |
Sampler shots and estimator precision |
|
30 |
Maximum COBYLA objective evaluations |
|
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¶
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.