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

OutputFormat.QC_IMPORT

QCProgram

Inspect QCO immediately after conversion

OutputFormat.QCO

QCOProgram

Inspect QCO after optimization

OutputFormat.QCO_OPTIMIZED

QCOProgram

Obtain the optimized circuit

OutputFormat.QC (default)

QCProgram

Serialize a compiler program

OutputFormat.JEFF

JeffProgram

Generate QIR

OutputFormat.QIR_BASE or OutputFormat.QIR_ADAPTIVE

QIRProgram

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.