MQT Core DD-based Simulator QDMI Device

DDSIM executes quantum programs locally through QDMI using decision diagrams.

Capabilities

The simulator device accepts OpenQASM 2, OpenQASM 3, and textual or binary QIR programs using the Base or Adaptive Profile. See the OpenQASM support table and QIR Support in the MQT for the supported operations, exact QDMI program formats, and payload contracts.

The device can perform weak simulation for every supported format, i.e., sample from the distribution produced by the program. It can also perform strong simulation for OpenQASM and eligible QIR Base or Adaptive Profile programs, i.e., compute a representation of the full state vector. Set the QDMI_DEVICE_JOB_PARAMETER_SHOTSNUM parameter to the desired number of shots, or to 0 to request only the state. In Python, use num_shots.

Sampling jobs also retain an uncollapsed state when the existing terminal sampling path prepares one state for all shots. Such jobs provide statevector and probability results alongside shots and counts. Dense and sparse vectors are materialized only when queried; queries do not rerun simulation or change the samples. Jobs that execute separately for each shot do not expose their last trajectory as a statevector and return QDMI_ERROR_NOTSUPPORTED for state queries. Zero-shot extraction remains useful for eligible programs outside the terminal-sampling fast path.

State extraction defers terminal measurements without collapsing the returned state. Measurement-dependent computation, resets and subsequent operations on measured wires are unsupported. Adaptive QIR may still execute classical loops, branches, dynamic allocations and direct helpers; see the QIR extraction contract for result-use and lifetime rules.

For reproducible stochastic execution, set QDMI_DEVICE_JOB_PARAMETER_CUSTOM1 to a positive int seed. The Python API exposes the same parameter as custom1. If custom1 is absent, the device seeds the random-number generator from the system. The seed controls OpenQASM and QIR sampling. State extraction does not use this seed.

Under the hood, the QDMI device imports OpenQASM into the compiler’s QC representation, lowers it to QCO, and executes it with the QCO DD utilities. This is the same compiler-backed simulation path exposed by QCOProgram.

OpenQASM 3 output bits are undefined until written, so direct QDMI jobs with a partially initialized output register fail during import. The Qiskit backend preserves Qiskit’s zero-initialized classical-bit semantics by writing every classical bit before submitting its generated OpenQASM 3 program.

Sampling returns ordered bitstrings through QDMI_JOB_RESULT_SHOTS and their histogram through QDMI_JOB_RESULT_HIST_KEYS and QDMI_JOB_RESULT_HIST_VALUES. Both results come from the same samples, including mid-circuit measurements. QIR Base or Adaptive programs with a static terminal measurement region can sample one prepared DD; other QIR programs run once per shot. See the QIR execution contract for eligibility and resource limits. OpenQASM classical registers use reverse declaration order, with each register most-significant-bit first. QIR records define increasing output-bit indices; the device reverses each recorded bitstring before returning shots and counts. Equivalent OpenQASM and QIR programs therefore use the same bitstring order. Adaptive QIR shots can record different numbers of bits; their histogram retains these variable-length outcomes.

Sparse statevector and probability results use ascending numerical basis-index order. Their keys and values share that order. Sparse exports require at most 64 qubits on a 64-bit platform; wider states return QDMI_ERROR_NOTSUPPORTED because their basis indices do not fit the sparse representation.

QIR output capture

Set QDMI_DEVICE_JOB_PARAMETER_CUSTOM2 to a bool value of true before submission to capture textual QIR output. In Python, pass custom2=True to submit_job or submit_program. This option requires a QIR Base or Adaptive program and a positive shot count; other jobs reject it with QDMI_ERROR_NOTSUPPORTED.

After successful execution, QDMI_JOB_RESULT_CUSTOM1 returns the complete, null-terminated output stream. Its reported size includes the terminator. The Python equivalent is job.get_custom_result(CustomProperty.CUSTOM1, str); C++ clients use job.getCustomResult<std::string>(qdmi::CustomProperty::Custom1). Without capture, that result is unsupported (None in Python).

Capture retains the QIR header, per-shot metadata, typed output records, and exit codes in memory. It executes the program once per shot, so capture jobs provide counts and shots but no uncollapsed statevector. Default sampling keeps its optimized path. See the executable QIR example.

Compile and execute

Compile a Bell circuit, sample its measurements, and inspect its statevector. DDSIM retains the state before terminal measurements, so state extraction does not require a separate job or zero shots. Circuits with mid-circuit measurements or resets do not support state extraction.

 1from mqt.core.mlir import compile_program, submit_program
 2from mqt.core.qdmi import ProgramFormat
 3from mqt.core.qdmi.driver import open_device
 4
 5bell_qasm = """OPENQASM 3.1;
 6include "stdgates.inc";
 7qubit[2] q;
 8bit[2] result;
 9h q[0];
10cx q[0], q[1];
11result = measure q;
12"""
13
14device = open_device("mqt.ddsim.default")
15program = compile_program(bell_qasm, target=device, program_format=ProgramFormat.QASM3)
16job = submit_program(program, target=device)
17job.wait()
18print(job.get_counts())
19print(job.get_dense_statevector())
{'00': 509, '11': 515}
[(0.7071067811865476+0j), 0j, 0j, (0.7071067811865476+0j)]