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.
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.
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:
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.