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
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() -> (i1, i1) attributes {passthrough = ["entry_point"]} {
%c1 = arith.constant 1 : index
%c0 = arith.constant 0 : index
%alloc = memref.alloc() : memref<2x!qc.qubit>
%0 = memref.load %alloc[%c0] : memref<2x!qc.qubit>
%1 = memref.load %alloc[%c1] : memref<2x!qc.qubit>
qc.h %0 : !qc.qubit
qc.ctrl(%0) targets (%arg0 = %1) {
qc.x %arg0 : !qc.qubit
qc.yield
} : {!qc.qubit}, {!qc.qubit}
%2 = qc.measure("result", 2, 0) %0 : !qc.qubit -> i1
%3 = qc.measure("result", 2, 1) %1 : !qc.qubit -> i1
memref.dealloc %alloc : memref<2x!qc.qubit>
return %2, %3 : i1, i1
}
}
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 |
|
|
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() -> (i1, i1) attributes {passthrough = ["entry_point"]} {
%c1 = arith.constant 1 : index
%c0 = arith.constant 0 : index
%c2 = arith.constant 2 : index
%0 = qtensor.alloc(%c2) : tensor<2x!qco.qubit>
%out_tensor, %result = qtensor.extract %0[%c0] : tensor<2x!qco.qubit>
%out_tensor_0, %result_1 = qtensor.extract %out_tensor[%c1] : tensor<2x!qco.qubit>
%1 = qco.h %result : !qco.qubit -> !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("result", 2, 0) %controls_out : !qco.qubit
%qubit_out_3, %result_4 = qco.measure("result", 2, 1) %targets_out : !qco.qubit
%2 = qtensor.insert %qubit_out_3 into %out_tensor_0[%c1] : tensor<2x!qco.qubit>
%3 = qtensor.insert %qubit_out into %2[%c0] : tensor<2x!qco.qubit>
qtensor.dealloc %3 : tensor<2x!qco.qubit>
return %result_2, %result_4 : i1, i1
}
}
Run passes explicitly¶
QCProgram, QCOProgram, JeffProgram, and
QIRProgram own their MLIR modules. A conversion consumes its source
program by default, avoiding an implicit copy of a potentially large module.
Pass copy=True when the source must remain available.
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() -> (i1, i1) attributes {passthrough = ["entry_point"]} {
%c1 = arith.constant 1 : index
%c0 = arith.constant 0 : index
%c2 = arith.constant 2 : index
%alloc = memref.alloc() : memref<2x!qc.qubit>
%0 = memref.load %alloc[%c0] : memref<2x!qc.qubit>
%1 = memref.load %alloc[%c1] : memref<2x!qc.qubit>
qc.h %0 : !qc.qubit
qc.ctrl(%0) targets (%arg0 = %1) {
qc.x %arg0 : !qc.qubit
qc.yield
} : {!qc.qubit}, {!qc.qubit}
%2 = qc.measure("result", 2, 0) %0 : !qc.qubit -> i1
%3 = qc.measure("result", 2, 1) %1 : !qc.qubit -> i1
memref.dealloc %alloc : memref<2x!qc.qubit>
return %2, %3 : i1, i1
}
}
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 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___unnamed__1 = internal constant [13 x i8] c"__unnamed__1\00"
@qir.result_label___unnamed__0 = internal constant [13 x i8] c"__unnamed__0\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__result_record_output(ptr null, ptr @qir.result_label___unnamed__0)
call void @__quantum__rt__result_record_output(ptr inttoptr (i64 1 to ptr), ptr @qir.result_label___unnamed__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)
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", i32 0}
!3 = !{i32 1, !"dynamic_result_management", i32 0}
!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 QC, QCO, and QTensor references describe the underlying operations. See Conversions for the lowering steps between dialects.