From 4636c865eff748a6fe6a37c33c0afbe4394cf83c Mon Sep 17 00:00:00 2001 From: Simon Hofmann Date: Mon, 7 Sep 2026 11:23:27 +0200 Subject: [PATCH] =?UTF-8?q?=F0=9F=90=9B=20Export=20measurements=20across?= =?UTF-8?q?=20quantum=20operations?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Allow unitary operations and resets between a measurement and its destination store without moving the measurement. Retain conservative rejection across classical accesses and control flow. Assisted-by: GPT-6 via Codex Signed-off-by: Simon Hofmann --- bindings/mlir/qiskit/QiskitExport.cpp | 5 +- docs/mlir/python_compiler_collection.md | 16 ++--- test/python/test_mlir_qiskit_translation.py | 69 +++++++++++++++++++++ 3 files changed, 82 insertions(+), 8 deletions(-) diff --git a/bindings/mlir/qiskit/QiskitExport.cpp b/bindings/mlir/qiskit/QiskitExport.cpp index c92fa64ec4..b9846e5cb9 100644 --- a/bindings/mlir/qiskit/QiskitExport.cpp +++ b/bindings/mlir/qiskit/QiskitExport.cpp @@ -2025,8 +2025,11 @@ static void validateControlFlowDepth(const size_t controlFlowDepth) { } for (auto* operation = measure->getNextNode(); operation != store; operation = operation->getNextNode()) { + // Fusion writes the destination at the measurement. Quantum gates and + // resets cannot observe that earlier classical write and stay in place. if (operation == nullptr || - !llvm::isa(operation)) { + !llvm::isa(operation)) { return false; } } diff --git a/docs/mlir/python_compiler_collection.md b/docs/mlir/python_compiler_collection.md index 4183c9f815..9035077365 100644 --- a/docs/mlir/python_compiler_collection.md +++ b/docs/mlir/python_compiler_collection.md @@ -287,13 +287,15 @@ satisfy the existing snapshot checks. Each exported measurement must write to one static public CBit in the same block. Destinations may be reused; later measurements overwrite earlier values -in program order. A measurement's destination store must follow it directly, -apart from constant operations. A conditional or otherwise delayed destination -store is rejected because Qiskit cannot preserve it as one measurement -instruction. The measurement result may feed supported classical expressions -after that store. General scalar control flow saves live measurement results in -native variables before later writes. Deferred measurement expressions require -an unchanged destination CBit. +in program order. Constants, unitary quantum operations (including barriers), +and resets may separate a measurement from its destination store. The exporter +keeps the measurement at its original position and writes the destination there. +Other intervening operations, including classical accesses and control flow, are +rejected because this earlier write may change the program's meaning. The +measurement result may feed supported classical expressions after that store. +General scalar control flow saves live measurement results in native variables +before later writes. Deferred measurement expressions require an unchanged +destination CBit. Dense numeric unitaries remain explicit matrix operations during import and export. Target compilation synthesizes supported one- and two-qubit matrices to diff --git a/test/python/test_mlir_qiskit_translation.py b/test/python/test_mlir_qiskit_translation.py index 7f309e8907..1f8e856df6 100644 --- a/test/python/test_mlir_qiskit_translation.py +++ b/test/python/test_mlir_qiskit_translation.py @@ -2405,6 +2405,75 @@ def test_measurement_snapshot_rejects_overwritten_destination(overwrite: str) -> program.to_qiskit() +@pytest.mark.parametrize("via_qco", [False, True], ids=["qc", "qco"]) +@pytest.mark.parametrize("gate", ["x", "swap", "reset", "barrier"]) +def test_delayed_measurement_store_across_quantum_operations(gate: str, *, via_qco: bool) -> None: + """Keep measurements before quantum operations without extra classical bits.""" + instruction = f"qc.{gate} %q0 : !qc.qubit" + if gate in {"swap", "barrier"}: + instruction = f"qc.{gate} %q0, %q1 : !qc.qubit, !qc.qubit" + program = QCProgram.from_mlir_str( + f"""module {{ + func.func @main() -> !cbit.reg<1> attributes {{mqt.entry_point}} {{ + %q0 = qc.alloc : !qc.qubit + %q1 = qc.alloc : !qc.qubit + %classical = cbit.alloc(#cbit.init) {{mqt.register_name = "c"}} : !cbit.reg<1> + %zero = arith.constant 0 : index + qc.x %q0 : !qc.qubit + %measured = qc.measure %q0 : !qc.qubit -> i1 + {instruction} + cbit.store %measured, %classical[%zero] : !cbit.reg<1> + qc.dealloc %q0 : !qc.qubit + qc.dealloc %q1 : !qc.qubit + return %classical : !cbit.reg<1> + }} +}} +""" + ) + if via_qco: + program = program.to_qco().to_qc() + + restored = program.to_qiskit() + + assert restored.num_qubits == 2 + assert restored.num_clbits == 1 + assert [ + ( + item.operation.name, + [restored.find_bit(qubit).index for qubit in item.qubits], + [restored.find_bit(bit).index for bit in item.clbits], + ) + for item in restored.data + ] == [("x", [0], []), ("measure", [0], [0]), (gate, [0, 1] if gate in {"swap", "barrier"} else [0], [])] + assert QCProgram.from_qiskit(restored).to_qco().sample(shots=1, seed=1) == {"1": 1} + + +@pytest.mark.parametrize( + "write", + [ + "cbit.store %false, %classical[%zero] : !cbit.reg<1>", + "cbit.write %false, %classical : i1, !cbit.reg<1>", + ], + ids=["bit-store", "register-write"], +) +def test_delayed_measurement_store_rejects_intervening_write(write: str) -> None: + """Do not fuse a measurement across a write that would overwrite its result.""" + program = _single_qubit_program( + [ + '%classical = cbit.alloc(#cbit.init) {mqt.register_name = "c"} : !cbit.reg<1>', + "%zero = arith.constant 0 : index", + "%false = arith.constant false", + "qc.x %q : !qc.qubit", + "%measured = qc.measure %q : !qc.qubit -> i1", + write, + "cbit.store %measured, %classical[%zero] : !cbit.reg<1>", + ], + returns_classical=True, + ) + with pytest.raises(RuntimeError, match="destination must follow the measurement"): + program.to_qiskit() + + def test_delayed_measurement_store_is_rejected() -> None: """Reject a delayed write that would change a captured bit snapshot.""" program = QCProgram.from_mlir_str(