Compile for hardware constraints¶
A logical program names qubits and gates. Hardware offers physical sites, connections, and a native gate set. What changes when the program’s interactions do not fit those connections?
This tutorial runs independently with the tutorial setup. The targets below are small models for compilation. Execution uses the bundled DDSIM simulator and requires no hardware account or external device.
Predict the logical result¶
The first two controlled-X gates prepare a three-qubit GHZ state. The final controlled-X flips the third qubit when the first is 1. Starting from \(|000\rangle\), the result is \((|000\rangle + |011\rangle)/\sqrt{2}\) in basis order \(|q_2q_1q_0\rangle\).
The three gates connect every pair of logical qubits. No assignment of three qubits to a three-site line can make all three pairs adjacent.
1source = """OPENQASM 3.0;
2include "stdgates.inc";
3qubit[3] q;
4bit[3] result;
5h q[0];
6cx q[0], q[1];
7cx q[1], q[2];
8cx q[0], q[2];
9result = measure q;
10"""
11shots = 4096
12seed = 17
13
14logical = QCProgram.from_openqasm_str(source)
15logical.to_qiskit().draw("mpl")
Check the expected logical output before introducing a target:
1logical_counts = sample(source, shots=shots, seed=seed)
2assert set(logical_counts) == {"000", "011"}
3assert sum(logical_counts.values()) == shots
4assert abs(logical_counts["000"] / shots - 0.5) < 0.05
5plot_distribution(logical_counts, title="Logical output distribution")
Describe two targets¶
A CompilerTarget is an immutable description used during compilation. It
specifies sites, connectivity, and supported operations. It does not open a
connection or execute a program.
Both models have three sites and the same native gates: RZ and RY rotations plus controlled-X. Measurement is also supported. H is not native, so it must be synthesized from rotations.
1native_operations = CompilerTarget.NativeOperations([
2 CompilerTarget.OperationCapability("rz", arity=1, num_parameters=1),
3 CompilerTarget.OperationCapability("ry", arity=1, num_parameters=1),
4 CompilerTarget.OperationCapability("cx", arity=2, num_parameters=0),
5 CompilerTarget.OperationCapability("measure", arity=1, num_parameters=0),
6])
7line_edges = [(0, 1), (1, 2)]
8targets = {
9 "All-to-all": CompilerTarget(
10 3,
11 connectivity=CompilerTarget.Connectivity.all_to_all(),
12 native_operations=native_operations,
13 ),
14 "Line": CompilerTarget(
15 3,
16 connectivity=CompilerTarget.Connectivity(line_edges),
17 native_operations=native_operations,
18 ),
19}
arity is the number of qubits an operation acts on; num_parameters is its
number of gate parameters. An omitted placement list makes an operation
available on every placement permitted by the target topology. Our CX gates have
no additional direction restriction.
These models omit noise and calibration. Their purpose is to isolate the effect of connectivity while keeping the native gate set fixed.
Compile, then inspect the native circuits¶
Placement assigns logical qubits to physical sites. Routing moves quantum states when an interaction requires different neighbors. Native synthesis expresses operations using the target’s supported gates. Routing can therefore increase the number of native gates even when target-independent optimization has removed redundant work.
Predict which target needs more controlled-X gates, then compile:
1mapped = {
2 name: compile_program(source, target=target, output=OutputFormat.OPENQASM3)
3 for name, target in targets.items()
4}
5circuits = {
6 name: QCProgram.from_openqasm_str(program.source).to_qiskit()
7 for name, program in mapped.items()
8}
9for name, circuit in circuits.items():
10 print(name)
11 display(circuit.draw("mpl", fold=12))
All-to-all
Line
The figures use the emitted physical site order. A routing exchange may already
be decomposed into native gates, so do not expect an explicit SWAP symbol.
Measurements still write to the declared result register, even when their
source physical qubits have moved.
Compare the gate counts of these straight-line outputs:
1print("Target RZ RY CX")
2for name, circuit in circuits.items():
3 counts = circuit.count_ops()
4 print(
5 f"{name:11} {counts.get('rz', 0):3} {counts.get('ry', 0):3} {counts.get('cx', 0):3}"
6 )
7assert circuits["Line"].count_ops()["cx"] > circuits["All-to-all"].count_ops()["cx"]
Target RZ RY CX
All-to-all 1 1 3
Line 14 7 6
This experiment illustrates routing cost, not an optimality guarantee. The compiler uses heuristics; exact layouts and decompositions can change between versions or machines. Mapping uses one initial-layout trial per available CPU by default. The target guide describes explicit pass options for reproducible mapping experiments.
Verify that the compiler used native gates and that every emitted CX on the line joins adjacent sites:
Native gates and CX connectivity checks passed.
The explicit topology check matters: an operation capability with no placement list does not itself encode the coupling graph. The target compiler checks both contracts.
Preserve logical output bits after routing¶
Expand the emitted OpenQASM and inspect its final measurement assignments:
1for name, program in mapped.items():
2 print(name)
3 display(Code(program.source, language="openqasm3"))
$0, $1, and $2 identify physical sites. The assignments to result retain
the logical output order. In particular, the rightmost character in a count
string is result[0], not necessarily the measurement of physical site 0.
The H gate has become rotations. Neither target lists gphase as native, so
target synthesis may remove the entry point’s unobservable global phase while
preserving relative phase effects. Unlike the target-independent cancellation
experiment, comparing raw unitary matrices without accounting for global phase
and physical permutations would not be an appropriate check here.
Sample the emitted programs through the local QCO interpreter:
1mapped_counts = {
2 name: sample(program, shots=shots, seed=seed) for name, program in mapped.items()
3}
4for counts in mapped_counts.values():
5 assert set(counts) == {"000", "011"}
6 assert sum(counts.values()) == shots
7 assert abs(counts["000"] / shots - 0.5) < 0.05
8plot_distribution(
9 [logical_counts, *mapped_counts.values()], legend=["Logical", *mapped_counts]
10)
Both targets retain the expected logical measurement distribution. This is an observable check for this input, not a proof of equivalence on every input state. Even with the same seed, transformed programs need not produce identical finite-shot histograms.
Experiment: remove the routing constraint¶
Replace the line with explicit triangle connectivity. Unlike all_to_all(),
this exercises the explicit-topology mapping path, but every site pair is now
connected. Predict whether the extra routing work remains:
1triangle = CompilerTarget(
2 3,
3 connectivity=CompilerTarget.Connectivity([*line_edges, (0, 2)]),
4 native_operations=native_operations,
5)
6triangle_program = compile_program(
7 source, target=triangle, output=OutputFormat.OPENQASM3
8)
9triangle_circuit = QCProgram.from_openqasm_str(triangle_program.source).to_qiskit()
10triangle_counts = sample(triangle_program, shots=shots, seed=seed)
11assert set(triangle_counts) == {"000", "011"}
12assert sum(triangle_counts.values()) == shots
13assert abs(triangle_counts["000"] / shots - 0.5) < 0.05
14assert triangle_circuit.count_ops()["cx"] < circuits["Line"].count_ops()["cx"]
15print(triangle_circuit.count_ops())
OrderedDict({'cx': 3, 'measure': 3, 'rz': 1, 'ry': 1})
Explain the result
The triangle allows all three logical interactions without routing exchanges. Native synthesis is still required because H is not native. Connectivity and the native gate set impose separate requirements.
Experiment: provide too few sites¶
A compiler cannot place three simultaneously live qubits on two sites in this program. The following cell deliberately tries it, catches the exception, and shows the expected diagnostic. This is the only intended failure in the tutorial; the later cells still run.
1too_small = CompilerTarget(
2 2,
3 connectivity=CompilerTarget.Connectivity.all_to_all(),
4 native_operations=native_operations,
5)
6try:
7 compile_program(source, target=too_small, output=OutputFormat.OPENQASM3)
8except RuntimeError as error:
9 print(f"Expected compilation failure: {error}")
10else:
11 raise AssertionError("The three-qubit program must not fit on this two-site target")
Expected compilation failure: Compiler action failed; see diagnostics for details.
loc("<qc-program-builder>":1:1): error: requires 3 program qubits, but the target site count is 2
loc("<qc-program-builder>":1:1): error: failed to compile the QCO program for the target
The diagnostic reports the required program qubits and available target sites. It does not indicate a Python syntax error or a missing installation. Qubit reuse is a separate optimization with its own applicability conditions; merely declaring a smaller target does not make it possible.
Connect compilation and execution with QDMI¶
The Quantum Device Management Interface (QDMI) defines a common C interface for querying device capabilities, submitting jobs, checking their status, and retrieving results. MQT Core’s driver loads device implementations and exposes them through Python and C++.
QDMI can connect to a local simulator, a QPU, or a cloud service. The device
reports which program formats it accepts; QDMI does not require every device to
use the same format. compile_program uses those capabilities to prepare a
payload, and submit_program creates a job for it. The
QDMI overview explains the layers and links to Amazon
Braket and IQM integration case studies.
Compile for an execution device¶
A target model describes constraints. A QDMI device also accepts jobs. Open the packaged DDSIM device and compile the logical source for its actual capabilities:
1device = open_device("mqt.ddsim.default")
2compiled = compile_program(source, target=device)
3print(f"Selected program format: {compiled.program_format.name}")
4print(f"Payload type: {type(compiled.payload).__name__}")
Selected program format: QIR_ADAPTIVE_MODULE
Payload type: bytes
Here DDSIM selects Adaptive QIR. The CompiledProgram owns the serialized
payload, the compiler target snapshot, and the selected payload specification.
This also allows structured programs such as the previous notebook’s feedback
example to be compiled for DDSIM.
Submission is a separate operation that explicitly names the destination:
1job = submit_program(compiled, target=device, num_shots=shots, custom1=seed)
2job.wait()
3device_counts = job.get_counts()
4assert set(device_counts) == {"000", "011"}
5assert sum(device_counts.values()) == shots
6assert abs(device_counts["000"] / shots - 0.5) < 0.05
7plot_distribution(device_counts, title="DDSIM logical outputs")
job.wait() waits for completion and reports execution failure. custom1
selects DDSIM’s random seed; custom job parameters are device-specific.
A CompiledProgram is tied to the target and payload contract used during
compilation. Submission checks that the destination matches that contract. Do
not compile a hardware-model artifact and assume that substituting DDSIM as its
destination will work. Here we simulated the models’ OpenQASM for comparison,
then compiled the logical source for DDSIM before submitting it. A compiled
DDSIM program can be submitted again without recompilation while its contract
continues to match.
Continue to execution and exchange¶
Continue with Execute programs with QIR to inspect the executable payload, then Run programs through QDMI to explore the device and job API. The Exchange structured programs with jeff tutorial shows how to hand programs to another compiler before choosing an execution format.
For further reference:
Use Compile for a QDMI device for device discovery, payload selection, target capabilities, and control-flow restrictions.
Read MQT Compiler Collection for compiler checkpoints, pass pipelines, serialization, and the Python, CLI, and C++ interfaces.
Explore Qiskit compatibility, OpenQASM input and output, and QIR in the MQT for interoperability and output formats.
Try Structured quantum benchmarks for structured programs with analytic references.
Use MLIR development policy when you are ready to implement a compiler change.