Exchange structured programs with jeff

The previous tutorials chose an execution format and a device. Sometimes another compiler needs to work on the program first. What should cross that boundary? How can you check that serialization preserved the program, including its control flow and phase?

jeff provides a common exchange format for quantum compilers. This tutorial uses MQT Core on both sides of a handoff, including a fresh Python process. Another compiler can occupy either side when it supports the format version and operations being exchanged.

The notebook runs independently with the tutorial setup. MQT Core includes the required jeff integration. The jeff guide describes the API and conversion limits.

Download this notebook.

Hide code cell source

 1import json
 2import subprocess
 3import sys
 4from pathlib import Path
 5from tempfile import TemporaryDirectory
 6
 7import numpy as np
 8from IPython.display import Code, display
 9
10from mqt.core.mlir import (
11    JeffProgram,
12    OutputFormat,
13    build_functionality,
14    compile_program,
15    sample,
16    submit_program,
17)

Start with a program whose full unitary matters

Prepare a Bell state and include a global phase of \(\pi/4\). A global phase is invisible to this circuit’s measurement counts, but a full-unitary comparison can detect whether the compiler preserved it.

1source = """OPENQASM 3.0;
2include "stdgates.inc";
3qubit[2] q;
4gphase(pi / 4);
5h q[0];
6cx q[0], q[1];
7"""
8original_unitary = build_functionality(source)
9print(original_unitary)
[[ 0.5+0.5j  0.5+0.5j  0. +0.j   0. +0.j ]
 [ 0. +0.j   0. +0.j   0.5+0.5j -0.5-0.5j]
 [ 0. +0.j   0. +0.j   0.5+0.5j  0.5+0.5j]
 [ 0.5+0.5j -0.5-0.5j  0. +0.j   0. +0.j ]]

Two qubits give a \(4\times4\) matrix, so comparing the full unitary is cheap here. Dense matrices grow exponentially; larger compiler tests need an oracle suited to their program and scale.

Convert to an exchange representation

Compile the program to jeff and inspect its MLIR form:

1exchange = compile_program(source, output=OutputFormat.JEFF)
2display(Code(exchange.ir, language="mlir"))
module attributes {jeff.entrypoint = 0 : ui16, jeff.strings = ["main"], jeff.tool = "mqt-cc", jeff.toolVersion = "4.0.0", jeff.version = 0 : ui16, jeff.versionMinor = 3 : ui16, jeff.versionPatch = 0 : ui16} {
  func.func @main() {
    %0 = jeff.float_const64(0.78539816339744828) : f64
    %1 = jeff.int_const32(1) : i32
    %2 = jeff.int_const32(0) : i32
    %3 = jeff.int_const32(2) : i32
    %4 = jeff.qureg_alloc(%3) : !jeff.qureg<2>
    %out_qreg, %out_qubit = jeff.qureg_extract_index(%2) %4 : (!jeff.qureg<2>, i32) -> (!jeff.qureg<2>, !jeff.qubit)
    %out_qubit_0 = jeff.h {is_adjoint = false, num_ctrls = 0 : i8, power = 1 : i8} %out_qubit : !jeff.qubit
    %out_qreg_1, %out_qubit_2 = jeff.qureg_extract_index(%1) %out_qreg : (!jeff.qureg<2>, i32) -> (!jeff.qureg<2>, !jeff.qubit)
    %out_qubit_3, %out_ctrl_qubits = jeff.x {is_adjoint = false, num_ctrls = 1 : i8, power = 1 : i8} %out_qubit_2 ctrls(%out_qubit_0) : !jeff.qubit ctrls !jeff.qubit
    %5 = jeff.qureg_insert_index(%1) %out_qreg_1 %out_qubit_3 : (!jeff.qureg<2>, i32, !jeff.qubit) -> !jeff.qureg<2>
    %6 = jeff.qureg_insert_index(%2) %5 %out_ctrl_qubits : (!jeff.qureg<2>, i32, !jeff.qubit) -> !jeff.qureg<2>
    jeff.qureg_free_zero %6 : <2>
    jeff.gphase(%0) {is_adjoint = false, num_ctrls = 0 : i8, power = 1 : i8}
    return
  }
}

The operations represent allocation, gates, controls, and the global phase. The module metadata identifies its entry point, format version, and producing tool. This is a representation a compiler can work on before selecting a target.

exchange.ir is useful for inspection. To produce the binary exchange payload, serialize the program:

1payload = exchange.to_bytes()
2assert isinstance(payload, bytes)
3print(f"Serialized payload: {len(payload)} bytes")
Serialized payload: 1568 bytes

The payload is self-contained. Sending an in-memory MLIR object would require sharing its compiler context; serialized bytes or a file form the handoff.

Receive, compile, and check semantics

Deserialize the payload and enter the receiving compiler’s optimization pipeline. Predict whether the new MLIR text must look identical for the program to be equivalent.

1received = JeffProgram.from_bytes(payload)
2optimized = compile_program(received, output=OutputFormat.QCO_OPTIMIZED)
3received_unitary = build_functionality(optimized)
4np.testing.assert_allclose(received_unitary, original_unitary, rtol=0, atol=1e-12)
5print("Largest matrix-entry difference:", np.max(np.abs(received_unitary - original_unitary)))
Largest matrix-entry difference: 0.0
Explain the result

SSA names, operation order, and intermediate representations may change. The comparison checks the resulting unitary, including phase and logical wire order, rather than requiring a particular printed representation.

Remove the phase from the source and compare again:

1without_phase = build_functionality(source.replace("gphase(pi / 4);", ""))
2np.testing.assert_allclose(received_unitary, np.exp(1j * np.pi / 4) * without_phase, rtol=0, atol=1e-12)
3assert not np.allclose(received_unitary, without_phase)
4print("The global phase survived the exchange.")
The global phase survived the exchange.

Experiment: change the phase in the source to pi / 2, rerun the handoff, and update the expected phase factor above. The full matrix changes while the Bell measurement probabilities stay the same.

Hand a file to an independent process

Write a .jeff file and start a fresh Python interpreter to load, compile, and sample it. The receiving process has no access to the producer’s MLIR context. The temporary directory only keeps the example from leaving files behind.

Hide code cell source

1consumer = """
2import json
3import sys
4from pathlib import Path
5from mqt.core.mlir import OutputFormat, compile_program, sample
6
7program = compile_program(Path(sys.argv[1]), output=OutputFormat.QCO_OPTIMIZED)
8print(json.dumps(sample(program, shots=64, seed=17)))
9"""
 1with TemporaryDirectory() as directory:
 2    path = Path(directory) / "exchange.jeff"
 3    exchange.write(path)
 4    completed = subprocess.run(
 5        [sys.executable, "-c", consumer, str(path)], check=True, capture_output=True, text=True
 6    )
 7
 8received_counts = json.loads(completed.stdout)
 9assert set(received_counts) <= {"00", "11"}
10assert sum(received_counts.values()) == 64
11received_counts
{'00': 35, '11': 29}

The receiver can also use JeffProgram.from_file(path) when it needs the jeff program object before choosing a compiler output. This example validates MQT’s producer and consumer paths; an integration with another compiler must also check that compiler’s supported operations and format version.

Exchange a measurement-dependent loop

A flat list of gates cannot capture the following program’s meaning by itself. Its loop condition depends on a measurement. As in the QIR tutorial, the program flips a measured 1 to 0 and then exits.

 1feedback_source = """OPENQASM 3.0;
 2include "stdgates.inc";
 3qubit q;
 4h q;
 5bit result = measure q;
 6while (result) {
 7    x q;
 8    result = measure q;
 9}
10"""
11feedback_exchange = compile_program(feedback_source, output=OutputFormat.JEFF)
12feedback_received = JeffProgram.from_bytes(feedback_exchange.to_bytes())
13restored_qco = compile_program(feedback_received, output=OutputFormat.QCO)
14assert sample(restored_qco, shots=64, seed=17) == {"0": 64}

Inspect the loop on each side of the conversion:

Hide code cell source

1for name, program, operation in (
2    ("jeff", feedback_received, "jeff.while"),
3    ("QCO", restored_qco, "scf.while"),
4):
5    assert operation in program.ir
6    print(name)
7    print("\n".join(line for line in program.ir.splitlines() if operation in line))
jeff
    %5:3 = jeff.while : (!jeff.qubit, tensor<1xi1>, i32) -> (!jeff.qubit, tensor<1xi1>, i32) args(%arg0 = %out_qubit_0, %arg1 = %4, %arg2 = %0) {
QCO
    %3 = scf.while (%arg0 = %qubit_out) : (!qco.qubit) -> !qco.qubit {

The loop remains explicit. Its state carries the qubit and classical information between iterations. The receiver can continue structured compilation instead of receiving only a predetermined gate sequence.

Choose an execution format after exchange

The received jeff program is still a compiler input. Compile it for DDSIM, which selects Adaptive QIR, then submit the resulting payload through QDMI:

1from mqt.core.qdmi.driver import open_device
2
3device = open_device("mqt.ddsim.default")
4compiled = compile_program(feedback_received, target=device)
5job = submit_program(compiled, target=device, num_shots=64, custom1=17)
6assert job.wait()
7assert job.get_counts() == {"0": 64}
8print("Execution format:", compiled.program_format.name)
9print("Counts:", job.get_counts())
Execution format: QIR_ADAPTIVE_MODULE
Counts: {'0': 64}

This completes the path from a structured exchange payload to a device result. Use jeff before physical mapping: MQT’s conversion does not preserve static hardware site IDs. The jeff guide lists further constraints on arrays, helper functions, and custom operations.

For the next experiment, exchange one of the supported structured benchmarks, then compare its evaluated results before and after the handoff. Use the compiler guide to choose another output format or optimization pipeline.