Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion bindings/mlir/qiskit/QiskitExport.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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<mlir::arith::ConstantOp>(operation)) {
!llvm::isa<mlir::arith::ConstantOp, mlir::qc::UnitaryOpInterface,
mlir::qc::ResetOp>(operation)) {
Comment thread
burgholzer marked this conversation as resolved.
return false;
}
}
Expand Down
16 changes: 9 additions & 7 deletions docs/mlir/python_compiler_collection.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
78 changes: 78 additions & 0 deletions test/python/test_mlir_qiskit_translation.py
Original file line number Diff line number Diff line change
Expand Up @@ -2405,6 +2405,84 @@ 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", "inv"])
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"
elif gate == "inv":
instruction = """qc.inv (%target = %q0) {
qc.x %target : !qc.qubit
qc.yield
} : !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<zero>) {{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]),
("x" if gate == "inv" else 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<zero>) {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(
Expand Down
Loading