Qiskit integration

IBMBackend adapts the installed native device through MQT Core’s QDMIBackend and QDMIJob. Install the optional extra from the source checkout:

uv pip install '.[qiskit]'

The extra uses mqt-core[qiskit]~=4.0.0. The base package remains usable without Qiskit; qiskit-ibm-runtime is not required. Imports do not open sessions or contact IBM. Constructing a backend reads live metadata. Executing circuits creates paid quantum jobs. These examples are not executed during documentation builds. Quantum execution remains unverified live until the gated checks pass.

Select a backend

Supply IBM_QUANTUM_API_KEY and IBM_QUANTUM_INSTANCE_CRN through the process environment or a secret manager. Keep their values out of source code.

from ibm.qdmi.qiskit import IBMBackend

backend = IBMBackend("ibm.berlin")
# Alternatively: IBMBackend("ibm.aachen")
# Generic selection: IBMBackend(backend_name="ibm_berlin")

The generic ibm.default entry uses backend_name, IBM_QUANTUM_BACKEND, or the administrator’s registered backend default, in that order. A missing selection fails during native session initialization before contacting IBM. Concrete catalogue entries select their named backend. Explicit api_key, instance_crn, and backend_name arguments override defaults. Normally omit base_url and auth_url; the native library derives the region from the CRN. Trusted endpoint overrides support applications such as loopback testing. See session configuration.

Use IBMBackend(device=device) to adapt an already-open IBM QDMI device. An open device is exclusive with connection overrides. device_id and provider may supply identity metadata, including through the inherited IBMBackend.from_device_id() factory for registered devices. Registration preserves administrator definitions in MQT Core’s driver. Each normal construction creates a fresh session.

Credentials read from the environment become explicit native session parameters and override registered defaults. Adapt an already-open device to preserve authentication configured directly through QDMI.

Transpile and execute

The target uses physical indices, native gate signatures and applicable site tuples, connectivity, and available durations and errors. Measurement and reset appear only when advertised. Barriers are directives. Missing gate metadata is not treated as unrestricted support.

from qiskit import QuantumCircuit, transpile

circuit = QuantumCircuit(2, 2, name="bell", metadata={"experiment": "example"})
circuit.h(0)
circuit.cx(0, 1)
circuit.measure([0, 1], [0, 1])
compiled = transpile(circuit, backend, optimization_level=1, seed_transpiler=7)
job = backend.run(compiled, shots=128, memory=True)
result = job.result()
counts = result.get_counts()
shots = result.get_memory()

Transpile against the selected backend before submission. Serialization retains the full physical register, including idle qubits, and all measurement destinations. It uses stable internal classical register names; Qiskit results retain the original register names, boundaries, and classical-bit order. Global phase does not affect classified samples and is omitted. Native instructions outside stdgates.inc retain their gate declarations so that IBM can import the program without external definitions.

shots must be a positive integer and defaults to 1,024. memory defaults to false. Requested memory contains genuine ordered samples, including leading zeros, and agrees with counts. Circuit names, metadata, and experiment order are preserved. Unsupported instructions, control flow, unbound parameters, and unsupported site tuples fail before submission. Scheduled delays are outside the current native contract.

Each circuit creates one Sampler V2 job with a 60-second QPU execution cap, independent of queue time. This applies separately to every circuit in a batch. Direct QDMI callers can set the custom execution-time parameter and dynamical-decoupling options. The Qiskit path retains the shared shots and memory execution options.

Parameters and batches

import math

from qiskit.circuit import Parameter

theta = Parameter("theta")
parameterized = QuantumCircuit(1, 1)
parameterized.ry(theta, 0)
parameterized.measure(0, 0)
compiled = transpile(parameterized, backend, optimization_level=1)
job = backend.run(
    [compiled, compiled],
    parameter_values=[{theta: 0.0}, {theta: math.pi}],
    shots=128,
    memory=True,
)
results = job.result()
first_counts = results.get_counts(0)
second_memory = results.get_memory(1)

Bind circuits yourself or supply one parameter mapping or ordered value sequence per circuit. The adapter validates and serializes the entire batch before its first submission. Serialization lives in ibm.qdmi.serializers; backend, job, and primitive orchestration use MQT Core. A later submission failure triggers cancellation attempts for earlier jobs. Submission is never automatically retried. A lost response can mean IBM accepted a job; inspect the platform before replacing it.

Sampler and estimator

backend.sampler() and backend.estimator() return Qiskit’s BackendSamplerV2 and BackendEstimatorV2. They execute through the same QDMI backend and support parameterized PUBs. Primitive expansion can create multiple native jobs, each with its own execution cap. Agree on a total budget before running large sweeps or observable sets.

import numpy as np

sampler = backend.sampler(default_shots=128)
sampled = sampler.run([(compiled, {theta: np.array([[0.0], [math.pi]])})]).result()
register_counts = sampled[0].data[compiled.cregs[0].name].get_counts(0)

The estimator uses finite shots and may expand observable measurements into separate circuits. Apply the transpilation layout to observables:

from qiskit.quantum_info import SparsePauliOp

preparation = QuantumCircuit(1)
preparation.ry(theta, 0)
mapped = transpile(preparation, backend, optimization_level=1)
observable = SparsePauliOp("Z").apply_layout(mapped.layout)
estimated = (
    backend
    .estimator()
    .run(
        [(mapped, observable, {theta: np.array([[0.0], [math.pi]])})],
        precision=0.125,
    )
    .result()
)
expectations = estimated[0].data["evs"]
standard_errors = estimated[0].data["stds"]

A smaller positive precision requests more shots; it does not guarantee a hardware error bound. See Qiskit’s backend primitive interfaces. IBM Runtime sessions, batching modes, mitigation controls, and provider-specific Runtime primitives are outside this interface.

Status, cancellation, and retrieval

Use the shared job’s status(), cancel(), and job_id() methods. job.result() waits for completion. For an explicit native timeout or retrieval, use the device interface:

identifier = job.job_id()
fresh_backend = IBMBackend("ibm.berlin")
retrieved = fresh_backend.device.retrieve_job_by_id(identifier)
retrieved.wait(900)
ordered_shots = retrieved.get_shots()

Retrieval makes no new submission and requires the original backend and instance. It returns a native QDMI job; remote input does not preserve Python circuit names or metadata. For a batch, job_id() identifies its first native job. Timeout does not imply cancellation. Call cancel() explicitly when abandoning pending work. Freeing local handles does not cancel remote jobs. See native job contracts.