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/i

  • Hadamard: h

  • Phase gates: s, sdg, t, tdg, sx, sxdg, p, phase, gphase

  • Rotation gates (parametric): rx, ry, rz, r/prx

  • Universal gates (parametric): u, u1, u2, u3

  • Two-qubit gates: cx/cnot, cy, cz, ch, cs, csdg, csx, swap, iswap, dcx, ecr

  • Two-qubit parametric gates: cp, cu1, cu3, crx, cry, crz, rxx, ryy, rzz, rzx, xx_plus_yy, xx_minus_yy

  • Three-qubit gates: ccx, ccz, cswap, rccx

  • Multi-controlled gates: mcx, mcz, mcp, mcrx, mcry, mcrz

  • Non-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:

  1. All parameters must be bound: Circuits with unbound parameters raise CircuitValidationError

  2. Only supported operations: Operations not supported by the device raise UnsupportedOperationError

  3. Valid 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:

  1. Validates the circuit (checks for unbound parameters, supported operations, valid options)

  2. 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 exporters

  3. Submits the program to the QDMI device via device.submit_job()

  4. Returns a QDMIJob

Device Introspection

The backend builds its Target by:

  1. Querying the QDMI device for available operations

  2. Mapping each operation to the corresponding Qiskit gate

  3. Determining qubit connectivity from the device’s coupling map

  4. 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: