Using the MQT Compiler Collection from Python¶
The mqt.core.mlir module provides Python access to the MQT Compiler
Collection. It accepts source strings, .qasm, .mlir, and
.jeff files, MQT QuantumComputation objects,
Qiskit QuantumCircuit objects, and typed compiler
programs. The requested output format determines where compilation stops and
which program type is returned.
Install MQT Core and import the compiler interface:
1from mqt.core.mlir import OutputFormat, QCProgram, QIRProfile, compile_program
To compile for a configured QDMI device, see target compilation.
Compile an OpenQASM program¶
The following OpenQASM program prepares a Bell state and records the outcome of measuring both qubits.
1bell_qasm = """OPENQASM 3.0;
2include "stdgates.inc";
3
4qubit[2] q;
5bit[2] result;
6
7h q[0];
8cx q[0], q[1];
9result = measure q;
10"""
11
12compiled = compile_program(bell_qasm)
13print(compiled.ir)
module {
func.func @main() -> memref<2xi1> attributes {passthrough = ["entry_point"]} {
%c1 = arith.constant 1 : index
%c0 = arith.constant 0 : index
%alloc = memref.alloc() {mqt.qubit_register_name = "q"} : memref<2x!qc.qubit>
%alloc_0 = memref.alloc() {mqt.classical_register_name = "result"} : memref<2xi1>
%0 = memref.load %alloc[%c0] : memref<2x!qc.qubit>
qc.h %0 : !qc.qubit
%1 = memref.load %alloc[%c1] : memref<2x!qc.qubit>
qc.ctrl(%0) targets (%arg0 = %1) {
qc.x %arg0 : !qc.qubit
qc.yield
} : {!qc.qubit}, {!qc.qubit}
%2 = qc.measure %0 : !qc.qubit -> i1
memref.store %2, %alloc_0[%c0] : memref<2xi1>
%3 = qc.measure %1 : !qc.qubit -> i1
memref.store %3, %alloc_0[%c1] : memref<2xi1>
memref.dealloc %alloc : memref<2x!qc.qubit>
return %alloc_0 : memref<2xi1>
}
}
By default, compile_program() runs the standard optimization pipeline and
returns a QCProgram. Its
ir property exposes the textual MLIR
representation for inspection and debugging. Programs do not need to be written
in MLIR to use the compiler.
Important
The compiler removes dead code. A circuit that only prepares a state has no observable effect and will be removed by optimizations. Programs intended for execution should measure the relevant qubits and return the measurement results.
In OpenQASM 3, assigning measurements to a classical register, as in the example above, makes those results return values of the imported program. When constructing MLIR directly, return the values produced by the measurement operations.
Select an output format¶
Select an output format to stop the pipeline at a particular representation:
Purpose |
Output format |
Result type |
|---|---|---|
Inspect frontend translation |
|
|
Inspect QCO immediately after conversion |
|
|
Inspect QCO after optimization |
|
|
Obtain the optimized circuit |
|
|
Emit an optimized OpenQASM program |
|
|
Serialize a compiler program |
|
|
Generate QIR |
|
|
For example, select optimized QCO to inspect the representation after the default QCO pass pipeline:
1optimized = compile_program(bell_qasm, output=OutputFormat.QCO_OPTIMIZED)
2print(optimized.ir)
module {
func.func @main() -> memref<2xi1> attributes {passthrough = ["entry_point"]} {
%c1 = arith.constant 1 : index
%c0 = arith.constant 0 : index
%c2 = arith.constant 2 : index
%0 = qtensor.alloc(%c2) {mqt.qubit_register_name = "q"} : tensor<2x!qco.qubit>
%alloc = memref.alloc() {mqt.classical_register_name = "result"} : memref<2xi1>
%out_tensor, %result = qtensor.extract %0[%c0] : tensor<2x!qco.qubit>
%1 = qco.h %result : !qco.qubit -> !qco.qubit
%out_tensor_0, %result_1 = qtensor.extract %out_tensor[%c1] : tensor<2x!qco.qubit>
%controls_out, %targets_out = qco.ctrl(%1) targets (%arg0 = %result_1) {
%4 = qco.x %arg0 : !qco.qubit -> !qco.qubit
qco.yield %4 : !qco.qubit
} : ({!qco.qubit}, {!qco.qubit}) -> ({!qco.qubit}, {!qco.qubit})
%qubit_out, %result_2 = qco.measure %controls_out : !qco.qubit
memref.store %result_2, %alloc[%c0] : memref<2xi1>
%2 = qtensor.insert %qubit_out into %out_tensor_0[%c0] : tensor<2x!qco.qubit>
%qubit_out_3, %result_4 = qco.measure %targets_out : !qco.qubit
%3 = qtensor.insert %qubit_out_3 into %2[%c1] : tensor<2x!qco.qubit>
memref.store %result_4, %alloc[%c1] : memref<2xi1>
qtensor.dealloc %3 : tensor<2x!qco.qubit>
return %alloc : memref<2xi1>
}
}
Emit OpenQASM¶
Request OPENQASM3 to emit the program
after the normal QCO optimization and conversion back to QC:
1openqasm = compile_program(bell_qasm, output=OutputFormat.OPENQASM3)
2print(openqasm.source)
OPENQASM 3.1;
include "stdgates.inc";
qubit[2] q;
output bit[2] result;
h q[0];
ctrl @ x q[0], q[1];
bit _mqt_b0 = measure q[0];
result[0] = _mqt_b0;
bit _mqt_b1 = measure q[1];
result[1] = _mqt_b1;
The returned OpenQASMProgram owns its source and can
write it directly:
1from pathlib import Path
2from tempfile import TemporaryDirectory
3
4with TemporaryDirectory() as directory:
5 path = Path(directory) / "bell.qasm"
6 openqasm.write(path)
7 reparsed = QCProgram.from_qasm_file(path)
8
9assert reparsed.is_valid
Use to_openqasm3() to clean up and export the
current QC program without QCO optimization. The resulting
OpenQASMProgram can be passed directly to
compile_program():
1recompiled = compile_program(openqasm, output=OutputFormat.QC_IMPORT)
2assert isinstance(recompiled, QCProgram)
The exporter targets practical structured programs with static qubit and bit indices. Dynamic indexing, dynamic ranges, surviving runtime assertions, checked-index machinery, and live poison values fail with an MLIR diagnostic. See OpenQASM input and output for the complete support table.
Use Qiskit circuits directly¶
Install the optional Qiskit integration with mqt-core[qiskit]. Qiskit
2.5.x circuits can be translated directly between
QuantumCircuit and
QCProgram:
1from qiskit import QuantumCircuit
2
3qiskit_bell = QuantumCircuit(2, 2)
4qiskit_bell.h(0)
5qiskit_bell.cx(0, 1)
6qiskit_bell.measure(range(2), range(2))
7
8direct = QCProgram.from_qiskit(qiskit_bell)
9restored = direct.to_qiskit()
10compiled_qiskit = compile_program(qiskit_bell)
11
12assert direct.is_valid # Export does not consume the QC program.
13assert restored.count_ops() == qiskit_bell.count_ops()
14assert compiled_qiskit.is_valid
This compiler route does not construct an intermediate
QuantumComputation. The existing
qiskit_to_mqt(),
mqt_to_qiskit(), and mqt.core.load()
interfaces remain independent and retain their existing version range and
behavior.
Import and export have different contracts because Qiskit 2.5 can inspect more program structures than its C API can construct.
Circuit feature |
Import |
Export |
|---|---|---|
Standard gates, constructible numeric modifiers, and global phase |
Supported |
Supported |
Other finite numeric modifiers |
Supported |
Rejected |
Measurement, reset, and barrier |
Supported |
Supported |
Canonical named registers and leading loose bits |
Supported |
Supported |
Custom instructions with finite, acyclic definitions |
Recursively expanded |
Not applicable |
Nested |
Supported |
Rejected |
Classical-bit and register conditions |
Supported |
Rejected |
Constant Boolean, |
Supported |
Rejected |
Standalone classical variables or variable expressions |
Rejected |
Rejected |
Free symbolic parameters |
Rejected |
Rejected |
Arbitrary unitaries |
Rejected |
Rejected |
Register aliases or interleaved membership |
Rejected |
Rejected |
Transpiler layout metadata |
Accepted and ignored |
Not emitted |
Lexically bound for-loop induction parameters are supported. Numeric
parameters passed to a custom instruction are bound before its definition is
expanded. Definition expansion rejects missing definitions, cycles, operand
arity mismatches, nesting beyond 64 levels, and more than 10 million expanded
operations.
A circuit remains valid when circ.layout is present. The importer
translates the circuit operations and deliberately does not preserve physical or
virtual layout metadata.
Input validation finishes before an MLIR module is created. Output validation finishes before a Qiskit circuit is allocated. Unsupported programs therefore fail without modifying the source object or exposing a partial result.
The binding imports Qiskit only when circuit translation is requested. It
accepts versions in the registered >=2.5.0,<2.6.0 range and verifies the
native API version before reading a circuit.
Run passes explicitly¶
QCProgram, QCOProgram, JeffProgram, and
QIRProgram own their MLIR modules. Conversions between these MLIR-backed
program objects consume their source by default, avoiding an implicit copy of a
potentially large module. Pass copy=True when the source must remain
available. OpenQASMProgram instead owns immutable source text and
remains reusable when passed to compile_program.
The following example keeps the imported QC program, applies transformations to QCO, and converts the result back to QC:
1qc = QCProgram.from_qasm_str(bell_qasm)
2qco = qc.to_qco(copy=True)
3qco.cleanup()
4qco.merge_single_qubit_rotation_gates()
5qco.lift_hadamards()
6final_qc = qco.to_qc()
7
8assert qc.is_valid
9assert not qco.is_valid
10print(final_qc.ir)
module {
func.func @main() -> memref<2xi1> attributes {passthrough = ["entry_point"]} {
%c1 = arith.constant 1 : index
%c0 = arith.constant 0 : index
%c2 = arith.constant 2 : index
%alloc = memref.alloc() {mqt.qubit_register_name = "q"} : memref<2x!qc.qubit>
%alloc_0 = memref.alloc() {mqt.classical_register_name = "result"} : memref<2xi1>
%0 = memref.load %alloc[%c0] : memref<2x!qc.qubit>
qc.h %0 : !qc.qubit
%1 = memref.load %alloc[%c1] : memref<2x!qc.qubit>
qc.ctrl(%0) targets (%arg0 = %1) {
qc.x %arg0 : !qc.qubit
qc.yield
} : {!qc.qubit}, {!qc.qubit}
%2 = qc.measure %0 : !qc.qubit -> i1
memref.store %2, %alloc_0[%c0] : memref<2xi1>
%3 = qc.measure %1 : !qc.qubit -> i1
memref.store %3, %alloc_0[%c1] : memref<2xi1>
memref.dealloc %alloc : memref<2x!qc.qubit>
return %alloc_0 : memref<2xi1>
}
}
Architecture-independent QCO transformations can also be composed with MLIR’s
textual pass-pipeline syntax. The same pass names and options are accepted by
mqt-cc:
1custom = compile_program(
2 bell_qasm,
3 output=OutputFormat.QCO_OPTIMIZED,
4 qco_pipeline="hadamard-lifting,merge-single-qubit-rotation-gates",
5)
The raw qubit-reuse pass and its composite preparation pipeline are both available through the compiler collection:
1raw_reuse = compile_program(bell_qasm, output=OutputFormat.QCO)
2raw_reuse.reuse_qubits()
3
4composite_reuse = compile_program(bell_qasm, output=OutputFormat.QCO)
5composite_reuse.run_qubit_reuse_pipeline()
The same flows can be composed with the default optimization pipeline in the compiler driver:
mqt-cc input.qasm --emit=qco-optimized \
--pass-pipeline='builtin.module(reuse-qubits,mqt-qco-default)'
mqt-cc input.qasm --emit=qco-optimized \
--pass-pipeline='builtin.module(mqt-qubit-reuse,mqt-qco-default)'
The mqt-qubit-reuse pipeline lifts measurements and replaces classical
controls before applying the raw reuse-qubits pass.
The qco_pipeline argument replaces the default QCO optimization
pipeline. It is applied when compilation proceeds beyond the raw
OutputFormat.QCO checkpoint.
Serialize programs and generate QIR¶
jeff is a serializable representation that can be stored and compiled
again in a later process.
1from pathlib import Path
2from tempfile import TemporaryDirectory
3
4with TemporaryDirectory() as directory:
5 path = Path(directory) / "bell.jeff"
6 jeff = compile_program(bell_qasm, output=OutputFormat.JEFF)
7 jeff.write(path)
8 restored = compile_program(path, output=OutputFormat.QC)
9
10assert restored.is_valid
To generate QIR, select a target profile. QIRProgram
provides the QIR MLIR through ir and the
translated LLVM IR through llvm_ir.
1qir = compile_program(bell_qasm, output=OutputFormat.QIR_BASE)
2assert qir.profile is QIRProfile.BASE
3print(qir.llvm_ir)
; ModuleID = 'LLVMDialectModule'
source_filename = "LLVMDialectModule"
@qir.result_label_result_1 = internal constant [9 x i8] c"result_1\00"
@qir.result_label_result_0 = internal constant [9 x i8] c"result_0\00"
@qir.result_label_result = internal constant [7 x i8] c"result\00"
define i64 @main() #0 {
call void @__quantum__rt__initialize(ptr null)
br label %1
1: ; preds = %0
call void @__quantum__qis__h__body(ptr null)
call void @__quantum__qis__cx__body(ptr null, ptr inttoptr (i64 1 to ptr))
br label %2
2: ; preds = %1
call void @__quantum__qis__mz__body(ptr null, ptr null)
call void @__quantum__qis__mz__body(ptr inttoptr (i64 1 to ptr), ptr inttoptr (i64 1 to ptr))
br label %3
3: ; preds = %2
call void @__quantum__rt__array_record_output(i64 2, ptr @qir.result_label_result)
call void @__quantum__rt__result_record_output(ptr null, ptr @qir.result_label_result_0)
call void @__quantum__rt__result_record_output(ptr inttoptr (i64 1 to ptr), ptr @qir.result_label_result_1)
ret i64 0
}
declare void @__quantum__rt__initialize(ptr)
declare void @__quantum__qis__h__body(ptr)
declare void @__quantum__qis__cx__body(ptr, ptr)
declare void @__quantum__qis__mz__body(ptr, ptr) #1
declare void @__quantum__rt__result_record_output(ptr, ptr)
declare void @__quantum__rt__array_record_output(i64, ptr)
attributes #0 = { "entry_point" "output_labeling_schema"="labeled" "qir_profiles"="base_profile" "required_num_qubits"="2" "required_num_results"="2" }
attributes #1 = { "irreversible" }
!llvm.module.flags = !{!0, !1, !2, !3, !4}
!0 = !{i32 1, !"qir_major_version", i32 2}
!1 = !{i32 7, !"qir_minor_version", i32 1}
!2 = !{i32 1, !"dynamic_qubit_management", i1 false}
!3 = !{i32 1, !"dynamic_result_management", i1 false}
!4 = !{i32 2, !"Debug Info Version", i32 3}
Use to_bitcode() to obtain LLVM bitcode as
bytes, or write_bitcode() to write a
.bc file directly. The
QIR guide shows how to
execute the generated bytes directly with QIR-Runner’s qirrunner Python
package.
The mqt-cc compiler driver selects the QIR serialization from the output
filename. Use .ll for textual LLVM IR and .bc for LLVM bitcode:
mqt-cc input.qasm --emit=qir-base -o output.ll
mqt-cc input.qasm --emit=qir-adaptive -o output.bc
Writing QIR to standard output also produces textual LLVM IR. All other output filenames, including filenames without an extension, retain the bitcode output used by earlier versions.
The QC, QCO, and QTensor references describe the underlying operations. See Conversions for the lowering steps between dialects.