Qiskit Backend Integration¶
The mqt.core.plugins.qiskit module provides a Qiskit
BackendV2-compatible interface to QDMI devices via
the MQT Core QDMI bindings. This integration lets you execute Qiskit circuits on
QDMI devices with a standard Qiskit workflow.
Installation¶
Install MQT Core with Qiskit support:
uv pip install "mqt-core[qiskit]"
python -m pip install "mqt-core[qiskit]"
Quickstart¶
1from mqt.core.plugins.qiskit import QDMIBackend
2from qiskit import QuantumCircuit
3
4# Open the registered DDSIM device by its stable ID
5backend = QDMIBackend.from_device_id("mqt.ddsim.default")
6
7# Create a simple circuit
8qc = QuantumCircuit(2)
9qc.h(0)
10qc.cx(0, 1)
11qc.measure_all()
12
13# Execute the circuit
14job = backend.run(qc, shots=1024)
15result = job.result()
16counts = result.get_counts()
17
18print(f"Results: {counts}")
Results: {'00': 535, '11': 489}
Provider and Device Discovery¶
Using the Provider¶
The QDMIProvider discovers
registered QDMI devices. Use it when an application must enumerate backends.
1from mqt.core.plugins.qiskit import QDMIProvider
2
3# Create a provider
4provider = QDMIProvider()
5
6# List all available backends
7backends = provider.backends()
8for backend in backends:
9 print(f"{backend.name}: {backend.target.num_qubits} qubits")
MQT Core DDSIM QDMI Device: 65535 qubits
Getting a Specific Backend¶
1# Open a backend directly by stable device ID
2from mqt.core.plugins.qiskit import QDMIBackend
3
4backend = QDMIBackend.from_device_id("mqt.ddsim.default")
5print(f"Backend: {backend.name}")
6print(f"Qubits: {backend.target.num_qubits}")
Backend: MQT Core DDSIM QDMI Device
Qubits: 65535
Optional session keywords apply explicit overrides to this fresh device session.
Their names and value types are described by
mqt.core.typing.QDMISessionParameters; persistent configuration
remains the default:
backend = QDMIBackend.from_device_id(
"provider.device",
token="access-token",
custom1="provider-specific-value",
)
Filtering Backends¶
# Filter backends by name substring
filtered_qdmi = provider.backends(name="QDMI") # Matches all backends with "QDMI" in name
filtered_ddsim = provider.backends(name="DDSIM") # Matches "MQT Core DDSIM QDMI Device"
# Filter by full name also works
exact = provider.backends(name="MQT Core DDSIM QDMI Device")
Authentication¶
QDMIProvider does not define a
generic credential interface. It opens each registered device with its
persistent definition. Configure credentials through the selected QDMI device
implementation. For example, a provider can use a credential file, an
environment variable, or a platform credential-provider chain. See
QDMI device configuration for persistent session settings.
Device Capabilities and Target¶
The backend automatically introspects the QDMI device and constructs a Qiskit
Target object describing device capabilities.
1# Access device properties via the Target
2print(f"Number of qubits: {backend.target.num_qubits}")
3print(f"Supported operations: {backend.target.operation_names}")
4
5# Check coupling map (if device has limited connectivity)
6coupling_map = backend.target.build_coupling_map()
7if coupling_map:
8 print(f"Coupling map: {coupling_map}")
Number of qubits: 65535
Supported operations: dict_keys(['global_phase', 'id', 'x', 'cx', 'ccx', 'mcx', 'y', 'cy', 'z', 'cz', 'ccz', 'h', 'ch', 's', 'cs', 'sdg', 'csdg', 't', 'tdg', 'sx', 'csx', 'sxdg', 'r', 'rx', 'crx', 'ry', 'cry', 'rz', 'crz', 'p', 'cp', 'mcphase', 'u1', 'cu1', 'u2', 'u', 'cu', 'swap', 'cswap', 'iswap', 'dcx', 'ecr', 'rxx', 'ryy', 'rzz', 'rzx', 'xx_minus_yy', 'xx_plus_yy', 'rccx', 'measure', 'reset'])
The backend maps QDMI device operations to corresponding Qiskit gates, including:
Single-qubit Pauli gates:
x,y,z,id/iHadamard:
hPhase gates:
s,sdg,t,tdg,sx,sxdg,p,phase,gphaseRotation gates (parametric):
rx,ry,rz,r/prxUniversal gates (parametric):
u,u1,u2,u3Two-qubit gates:
cx/cnot,cy,cz,ch,cs,csdg,csx,swap,iswap,dcx,ecrTwo-qubit parametric gates:
cp,cu1,cu3,crx,cry,crz,rxx,ryy,rzz,rzx,xx_plus_yy,xx_minus_yyThree-qubit gates:
ccx,ccz,cswap,rccxMulti-controlled gates:
mcx,mcz,mcp,mcrx,mcry,mcrzNon-unitary operations:
reset,measure
Circuit Execution¶
1from qiskit import QuantumCircuit
2
3# Create a circuit
4qc = QuantumCircuit(2)
5qc.h(0)
6qc.cx(0, 1)
7qc.measure_all()
8
9# Run on the backend
10job = backend.run(qc, shots=500)
11result = job.result()
12counts = result.get_counts()
13
14print(f"Counts: {counts}")
15print(f"Total shots: {sum(counts.values())}")
Counts: {'00': 249, '11': 251}
Total shots: 500
Circuits must meet the following requirements before execution:
All parameters must be bound: Circuits with unbound parameters raise
CircuitValidationErrorOnly supported operations: Operations not supported by the device raise
UnsupportedOperationErrorValid shots value: Must be a non-negative integer
Parameter Binding¶
The backend supports automatic parameter binding through the parameter_values
argument. You can pass parameter values either as dictionaries or as sequences
of values:
from qiskit.circuit import Parameter
# Option 1: Bind parameters manually
theta = Parameter("theta")
qc = QuantumCircuit(1)
qc.ry(theta, 0)
qc.measure_all()
qc_bound = qc.assign_parameters({theta: 1.5708})
job = backend.run(qc_bound, shots=100)
# Option 2: Use parameter_values argument (recommended)
job = backend.run(qc, parameter_values=[{theta: 1.5708}], shots=100)
# For multiple circuits with different parameters
circuits = [qc, qc, qc]
param_values = [{theta: 0.5}, {theta: 1.0}, {theta: 1.5}]
job = backend.run(circuits, parameter_values=param_values, shots=100)
Job Handling¶
Job Status¶
The QDMIJob wraps a QDMI job and
provides status tracking:
from qiskit.providers import JobStatus
job = backend.run(qc, shots=1024)
# Check job status
status = job.status()
print(f"Job status: {status}")
Retrieving Results¶
Results are lazily fetched when you call result():
# Run the circuit
job = backend.run(qc, shots=1024)
# Get results (waits for completion if needed)
result = job.result()
# Access measurement counts
counts = result.get_counts()
# Access result metadata
exp_result = result.results[0]
print(f"Circuit name: {exp_result.header['name']}")
print(f"Shots: {exp_result.shots}")
print(f"Success: {exp_result.success}")
Multi-Circuit Execution¶
The backend supports both single-circuit and multi-circuit execution. You can submit multiple circuits in a single call:
# Create multiple circuits
qc1 = QuantumCircuit(2)
qc1.h(0)
qc1.cx(0, 1)
qc1.measure_all()
qc2 = QuantumCircuit(2)
qc2.x(0)
qc2.cx(0, 1)
qc2.measure_all()
qc3 = QuantumCircuit(2)
qc3.h([0, 1])
qc3.measure_all()
# Submit all circuits at once
circuits = [qc1, qc2, qc3]
job = backend.run(circuits, shots=1000)
# Get aggregated results
result = job.result()
# Process results for each circuit
for idx in range(len(circuits)):
counts = result.get_counts(idx)
print(f"Circuit {idx} results: {counts}")
Alternatively, you can still submit circuits individually:
results = []
for qc in circuits:
job = backend.run(qc, shots=1000)
result = job.result()
results.append(result)
Qiskit Primitives¶
The backend provides implementations of Qiskit’s
Primitives V2 interfaces:
QDMISampler and
QDMIEstimator. These primitives
allow for a simplified execution workflow for sampling bitstrings and estimating
expectation values.
Sampler¶
The QDMISampler implements the
BaseSamplerV2 interface. It is used to sample quantum circuits and obtain
measurement counts (bitstrings).
1from qiskit import QuantumCircuit
2
3# Construct a sampler from the backend
4sampler = backend.sampler(default_shots=1024)
5
6# Create a circuit
7qc = QuantumCircuit(2)
8qc.h(0)
9qc.cx(0, 1)
10qc.measure_all()
11
12# Run the sampler
13job = sampler.run([qc], shots=1024)
14result = job.result()
15
16# Get results for the first pub (Primitive Unified Bloc)
17pub_result = result[0]
18counts = pub_result.data.meas.get_counts()
19
20print(f"Sampler results: {counts}")
Sampler results: {'00': 505, '11': 519}
Estimator¶
The QDMIEstimator implements the
BaseEstimatorV2 interface. It is used to calculate expectation values of
observables.
1from qiskit import QuantumCircuit
2from qiskit.quantum_info import SparsePauliOp
3import numpy as np
4
5# Construct an estimator from the backend
6estimator = backend.estimator(default_precision=0.0, default_shots=1024)
7
8# Create a circuit and observable
9qc = QuantumCircuit(2)
10qc.h(0)
11qc.cx(0, 1)
12
13observable = SparsePauliOp("ZZ")
14
15# Run the estimator
16job = estimator.run([(qc, observable)])
17result = job.result()
18
19# Get the expectation value
20pub_result = result[0]
21ev = pub_result.data.evs
22std = pub_result.data.stds
23
24print(f"Expectation value: {ev}")
25print(f"Standard deviation: {std}")
Expectation value: 1.0
Standard deviation: 0.0
You can also use parameterized circuits with the estimator:
1from qiskit.circuit import Parameter
2
3# Parameterized circuit
4theta = Parameter("theta")
5qc_param = QuantumCircuit(1)
6qc_param.rx(theta, 0)
7
8op = SparsePauliOp("Z")
9
10# Run with specific parameter values
11# Format: (circuit, observable, parameter_values)
12vals = [0.0, np.pi/2, np.pi]
13job = estimator.run([(qc_param, op, vals)])
14result = job.result()
15
16print(f"Expectation values: {result[0].data.evs}")
Expectation values: [ 1. 0.00195312 -1. ]
Error Handling¶
The module provides specific exceptions for different error conditions:
from mqt.core.plugins.qiskit import (
CircuitValidationError,
UnsupportedOperationError,
UnsupportedDeviceError,
JobSubmissionError,
TranslationError,
UnsupportedFormatError,
)
try:
job = backend.run(qc, shots=1024)
result = job.result()
except CircuitValidationError as e:
# Invalid circuit (unbound parameters, invalid shots, etc.)
print(f"Circuit validation failed: {e}")
except UnsupportedOperationError as e:
# Circuit contains operations not supported by device
print(f"Unsupported operation: {e}")
except UnsupportedDeviceError as e:
# Device cannot be represented in Qiskit's Target model
print(f"Unsupported device: {e}")
except JobSubmissionError as e:
# Failed to submit job to device
print(f"Job submission failed: {e}")
except TranslationError as e:
# Failed to convert circuit to supported program format
print(f"Translation error: {e}")
except UnsupportedFormatError as e:
# No supported program format available
print(f"Unsupported format: {e}")
Implementation Details¶
Circuit Conversion¶
When you run a circuit, the backend:
Validates the circuit (checks for unbound parameters, supported operations, valid options)
Converts the circuit to one of the program formats supported by the target device (IQM JSON, OpenQASM 2, OpenQASM 3) using
qiskit_to_iqm_json()or Qiskit’s built-in QASM exportersSubmits the program to the QDMI device via
device.submit_job()Returns a
QDMIJob
Device Introspection¶
The backend builds its Target by:
Querying the QDMI device for available operations
Mapping each operation to the corresponding Qiskit gate
Determining qubit connectivity from the device’s coupling map
Including operation properties (duration, fidelity) if available
Primitives Implementation¶
The Qiskit Primitives are implemented as lightweight wrappers around the backend execution:
Sampler: Submits circuits to the backend and reshapes the resulting bitstrings into the requested structure (PubResult).
Estimator: Decomposes observables into Pauli terms, appends necessary basis rotations and measurements to the provided circuits, and submits them to the backend. It then reconstructs expectation values and standard deviations from the measurement counts of each term based on the provided precision or shots.
API Reference¶
For complete API documentation, see:
QDMIProvider— Device provider interfaceQDMIBackend— BackendV2 implementationQDMIJob— Job wrapper and result handlingQDMIEstimator— EstimatorV2 primitive implementationQDMISampler— SamplerV2 primitive implementationexceptions— Exception types