From e69364d91e7531f04fe17054aebf716d35d04d26 Mon Sep 17 00:00:00 2001 From: Cosmin Dinu--Thiery Date: Thu, 16 Apr 2026 02:52:15 +0200 Subject: [PATCH 1/8] Fix LightCone pass ignoring mid-circuit measurements --- .../passes/optimization/light_cone.py | 42 ++++++++++++++----- 1 file changed, 32 insertions(+), 10 deletions(-) diff --git a/qiskit/transpiler/passes/optimization/light_cone.py b/qiskit/transpiler/passes/optimization/light_cone.py index b71c4b381edb..119891dbf66f 100644 --- a/qiskit/transpiler/passes/optimization/light_cone.py +++ b/qiskit/transpiler/passes/optimization/light_cone.py @@ -15,10 +15,11 @@ import warnings from qiskit.circuit import Gate, Qubit from qiskit.circuit.commutation_library import SessionCommutationChecker as scc -from qiskit.circuit.library import PauliGate, ZGate +from qiskit.circuit.library import PauliGate, ZGate , PauliProductMeasurement from qiskit.dagcircuit import DAGCircuit from qiskit.transpiler.basepasses import TransformationPass from qiskit.transpiler.passes.utils.remove_final_measurements import calc_final_ops +from qiskit.quantum_info import Pauli translation_table = str.maketrans({"+": "X", "-": "X", "l": "Y", "r": "Y", "0": "Z", "1": "Z"}) @@ -54,12 +55,20 @@ def __init__(self, bit_terms: str | None = None, indices: list[int] | None = Non self.indices = indices @staticmethod - def _find_measurement_qubits(dag: DAGCircuit) -> set[Qubit]: - final_nodes = calc_final_ops(dag, {"measure"}) - qubits_measured = set() - for node in final_nodes: - qubits_measured |= set(node.qargs) - return qubits_measured + def _measurement_commutation_ops(node) -> list[tuple]: + """Return the list of (measurement_gate, qubits) for a measurement node. + For a standard measure, we use one ZGate per qubit. + For a PauliProductMeasurement, we use a PauliGate. + """ + if node.op.name == "measure": + return [(ZGate(), [qubit]) for qubit in node.qargs] + elif isinstance(node.op, PauliProductMeasurement): + pauli_label = Pauli((node.op._pauli_z, node.op._pauli_x, 0)).to_label() + return [(PauliGate(pauli_label), list(node.qargs))] + else: + raise ValueError( + f"_measurement_commutation_ops called with unexpected operation" + ) def _get_initial_lightcone( self, dag: DAGCircuit @@ -69,14 +78,18 @@ def _get_initial_lightcone( If a `bit_terms` is provided, the qubits corresponding to the non-trivial Paulis define the light-cone. """ - lightcone_qubits = self._find_measurement_qubits(dag) if self.bit_terms is None: - lightcone_operations = [(ZGate(), [qubit_index]) for qubit_index in lightcone_qubits] + # will be discovered dynamically during the reverse traversal in run() + return set(), [] else: # Having both measurements and an observable is not allowed + all_measured_qubits = set() + for node in dag.topological_op_nodes(): + if node.op.name == "measure" or isinstance(node.op, PauliProductMeasurement): + all_measured_qubits.update(node.qargs) if len(dag.qubits) < max(self.indices) + 1: raise ValueError("`indices` contains values outside the qubit range.") - if lightcone_qubits: + if all_measured_qubits: raise ValueError( "The circuit contains measurements and an observable has been given: " "remove the observable or the measurements." @@ -105,6 +118,15 @@ def run(self, dag: DAGCircuit) -> DAGCircuit: # Iterate over the nodes in reverse topological order for node in dag.topological_op_nodes(reverse=True): + # Each time we encounter a measurement, extend the cone with its + # qubits and add the appropriate measurement gate. + if node.op.name in ("measure", "pauli_product_measurement"): + new_qubits = set(node.qargs) - lightcone_qubits + if new_qubits: + lightcone_qubits.update(new_qubits) + lightcone_operations.extend(self._measurement_commutation_ops(node)) + new_dag.apply_operation_front(node.op, node.qargs, node.cargs) + continue # Check if the node belongs to the light-cone if lightcone_qubits.intersection(node.qargs): # Check commutation with all previous operations From 3c267d20a48c9f04c5a1f11ed017e9050b5bfd29 Mon Sep 17 00:00:00 2001 From: Cosmin Dinu--Thiery Date: Thu, 16 Apr 2026 04:52:52 +0200 Subject: [PATCH 2/8] release note for the fix of lightcone mid circuit measurement --- ...htcone-mid-circuit-measurment-87f5c72c34d34978.yaml | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 releasenotes/notes/fix-lightcone-mid-circuit-measurment-87f5c72c34d34978.yaml diff --git a/releasenotes/notes/fix-lightcone-mid-circuit-measurment-87f5c72c34d34978.yaml b/releasenotes/notes/fix-lightcone-mid-circuit-measurment-87f5c72c34d34978.yaml new file mode 100644 index 000000000000..f3bd3d9b213e --- /dev/null +++ b/releasenotes/notes/fix-lightcone-mid-circuit-measurment-87f5c72c34d34978.yaml @@ -0,0 +1,10 @@ +--- +fixes: + - | + Fixed :class:`.LightCone` transpiler pass incorrectly returning an empty + circuit when mid-circuit measurements are present. The pass was using + ``calc_final_ops`` to find measured qubits, which only detects measurements + in final position in the DAG. Measurements are now discovered dynamically + during the reverse-topological review, handling mid-circuit measurements. + + See `#15937 `__. \ No newline at end of file From d6550a42b967cb5f247cac92291485a079862da7 Mon Sep 17 00:00:00 2001 From: Cosmin Dinu--Thiery Date: Thu, 16 Apr 2026 05:01:56 +0200 Subject: [PATCH 3/8] add test for the lightcone fix --- test/python/transpiler/test_light_cone.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/test/python/transpiler/test_light_cone.py b/test/python/transpiler/test_light_cone.py index f7f195d6398b..37c82b29c757 100644 --- a/test/python/transpiler/test_light_cone.py +++ b/test/python/transpiler/test_light_cone.py @@ -354,6 +354,25 @@ def test_raise_error_when_circuit_measurements_and_observable_present(self): ): light_cone.run(dag) + def test_mid_circuit_measurement_not_empty(self): + """Test that LightCone does not return an empty circuit when + mid-circuit measurements are present.""" + light_cone = LightCone() + pm = PassManager([light_cone]) + + qc = QuantumCircuit(2, 1) + qc.cx(0, 1) + qc.z(0) + qc.measure(0, 0) + qc.h(0) + + new_circuit = pm.run(qc) + + expected = QuantumCircuit(2, 1) + expected.measure(0, 0) + + self.assertEqual(expected, new_circuit) + if __name__ == "__main__": unittest.main() From 3c8f584c6261ae95e483f1e2cdfb1f3397b5ee6a Mon Sep 17 00:00:00 2001 From: Cosmin Dinu--Thiery Date: Thu, 16 Apr 2026 05:27:16 +0200 Subject: [PATCH 4/8] formatting update with black --- qiskit/transpiler/passes/optimization/light_cone.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/qiskit/transpiler/passes/optimization/light_cone.py b/qiskit/transpiler/passes/optimization/light_cone.py index 119891dbf66f..4a3182dc08f2 100644 --- a/qiskit/transpiler/passes/optimization/light_cone.py +++ b/qiskit/transpiler/passes/optimization/light_cone.py @@ -11,11 +11,12 @@ # that they have been altered from the originals. """Cancel the redundant (self-adjoint) gates through commutation relations.""" + from __future__ import annotations import warnings from qiskit.circuit import Gate, Qubit from qiskit.circuit.commutation_library import SessionCommutationChecker as scc -from qiskit.circuit.library import PauliGate, ZGate , PauliProductMeasurement +from qiskit.circuit.library import PauliGate, ZGate, PauliProductMeasurement from qiskit.dagcircuit import DAGCircuit from qiskit.transpiler.basepasses import TransformationPass from qiskit.transpiler.passes.utils.remove_final_measurements import calc_final_ops @@ -66,9 +67,7 @@ def _measurement_commutation_ops(node) -> list[tuple]: pauli_label = Pauli((node.op._pauli_z, node.op._pauli_x, 0)).to_label() return [(PauliGate(pauli_label), list(node.qargs))] else: - raise ValueError( - f"_measurement_commutation_ops called with unexpected operation" - ) + raise ValueError(f"_measurement_commutation_ops called with unexpected operation") def _get_initial_lightcone( self, dag: DAGCircuit From 2c1c4ba82b96d4cf3a5a4f603af314e67a71213a Mon Sep 17 00:00:00 2001 From: Cosmin Dinu--Thiery Date: Thu, 16 Apr 2026 06:05:34 +0200 Subject: [PATCH 5/8] pylint correction --- qiskit/transpiler/passes/optimization/light_cone.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/qiskit/transpiler/passes/optimization/light_cone.py b/qiskit/transpiler/passes/optimization/light_cone.py index 4a3182dc08f2..b144b5728ebd 100644 --- a/qiskit/transpiler/passes/optimization/light_cone.py +++ b/qiskit/transpiler/passes/optimization/light_cone.py @@ -19,7 +19,6 @@ from qiskit.circuit.library import PauliGate, ZGate, PauliProductMeasurement from qiskit.dagcircuit import DAGCircuit from qiskit.transpiler.basepasses import TransformationPass -from qiskit.transpiler.passes.utils.remove_final_measurements import calc_final_ops from qiskit.quantum_info import Pauli translation_table = str.maketrans({"+": "X", "-": "X", "l": "Y", "r": "Y", "0": "Z", "1": "Z"}) @@ -66,8 +65,8 @@ def _measurement_commutation_ops(node) -> list[tuple]: elif isinstance(node.op, PauliProductMeasurement): pauli_label = Pauli((node.op._pauli_z, node.op._pauli_x, 0)).to_label() return [(PauliGate(pauli_label), list(node.qargs))] - else: - raise ValueError(f"_measurement_commutation_ops called with unexpected operation") + + raise ValueError("_measurement_commutation_ops called with unexpected operation") def _get_initial_lightcone( self, dag: DAGCircuit From c1482a672a474c5a006b14a952d3f1929cee0989 Mon Sep 17 00:00:00 2001 From: Cosmin Dinu--Thiery Date: Thu, 16 Apr 2026 06:36:07 +0200 Subject: [PATCH 6/8] identation correction --- test/python/transpiler/test_light_cone.py | 26 +++++++++++------------ 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/test/python/transpiler/test_light_cone.py b/test/python/transpiler/test_light_cone.py index 37c82b29c757..67ab07f1f6f4 100644 --- a/test/python/transpiler/test_light_cone.py +++ b/test/python/transpiler/test_light_cone.py @@ -355,23 +355,23 @@ def test_raise_error_when_circuit_measurements_and_observable_present(self): light_cone.run(dag) def test_mid_circuit_measurement_not_empty(self): - """Test that LightCone does not return an empty circuit when - mid-circuit measurements are present.""" - light_cone = LightCone() - pm = PassManager([light_cone]) + """Test that LightCone does not return an empty circuit when + mid-circuit measurements are present.""" + light_cone = LightCone() + pm = PassManager([light_cone]) - qc = QuantumCircuit(2, 1) - qc.cx(0, 1) - qc.z(0) - qc.measure(0, 0) - qc.h(0) + qc = QuantumCircuit(2, 1) + qc.cx(0, 1) + qc.z(0) + qc.measure(0, 0) + qc.h(0) - new_circuit = pm.run(qc) + new_circuit = pm.run(qc) - expected = QuantumCircuit(2, 1) - expected.measure(0, 0) + expected = QuantumCircuit(2, 1) + expected.measure(0, 0) - self.assertEqual(expected, new_circuit) + self.assertEqual(expected, new_circuit) if __name__ == "__main__": From cf749917a615dae63a36a849795790e8519d3af2 Mon Sep 17 00:00:00 2001 From: Cosmin Dinu--Thiery Date: Tue, 21 Apr 2026 02:50:34 +0200 Subject: [PATCH 7/8] test: new tests --- test/python/transpiler/test_light_cone.py | 75 ++++++++++++++++++----- 1 file changed, 58 insertions(+), 17 deletions(-) diff --git a/test/python/transpiler/test_light_cone.py b/test/python/transpiler/test_light_cone.py index 67ab07f1f6f4..1fa827eb52ff 100644 --- a/test/python/transpiler/test_light_cone.py +++ b/test/python/transpiler/test_light_cone.py @@ -21,14 +21,13 @@ Parameter, QuantumCircuit, ) -from qiskit.circuit.library import real_amplitudes +from qiskit.circuit.library import real_amplitudes, PauliProductRotationGate, PauliProductMeasurement from qiskit.circuit.library.n_local.efficient_su2 import efficient_su2 from qiskit.converters import circuit_to_dag -from qiskit.quantum_info import SparsePauliOp, SparseObservable +from qiskit.quantum_info import SparsePauliOp, SparseObservable, Pauli from qiskit.transpiler.passes.optimization.light_cone import LightCone from qiskit.transpiler.passmanager import PassManager - @ddt.ddt class TestLightConePass(QiskitTestCase): """Test the LightCone pass.""" @@ -354,25 +353,67 @@ def test_raise_error_when_circuit_measurements_and_observable_present(self): ): light_cone.run(dag) - def test_mid_circuit_measurement_not_empty(self): - """Test that LightCone does not return an empty circuit when - mid-circuit measurements are present.""" - light_cone = LightCone() - pm = PassManager([light_cone]) + def test_mid_circuit_measurement_not_empty(self): + """Test that LightCone does not return an empty circuit when + mid-circuit measurements are present.""" + light_cone = LightCone() + pm = PassManager([light_cone]) + + qc = QuantumCircuit(2, 1) + qc.cx(0, 1) + qc.z(0) + qc.measure(0, 0) + qc.h(0) + + new_circuit = pm.run(qc) + + expected = QuantumCircuit(2, 1) + expected.measure(0, 0) + + self.assertEqual(expected, new_circuit) + + def test_pauli_product_measurement_circuit(self): + """Test LightCone on a circuit containing only PauliProductRotationGates + and PauliProductMeasurements. The XZ rotation does not commute with the + ZZ measurement observable and must be kept.""" + light_cone = LightCone() + pm = PassManager([light_cone]) + + qc = QuantumCircuit(2, 1) + qc.append(PauliProductRotationGate(Pauli("XZ"), 0.5), [0, 1]) + qc.append(PauliProductMeasurement(Pauli("ZZ")), [0, 1], [0]) - qc = QuantumCircuit(2, 1) - qc.cx(0, 1) - qc.z(0) - qc.measure(0, 0) - qc.h(0) + new_circuit = pm.run(qc) + + expected = QuantumCircuit(2, 1) + expected.append(PauliProductRotationGate(Pauli("XZ"), 0.5), [0, 1]) + expected.append(PauliProductMeasurement(Pauli("ZZ")), [0, 1], [0]) + + self.assertEqual(expected, new_circuit) + + def test_mid_circuit_and_final_measurements(self): + """Test LightCone on a circuit with both mid-circuit and final measurements. + Both measurements must be kept along with the gates in their light cones. + The X gate after the mid-circuit measurement is irrelevant and must be removed.""" + light_cone = LightCone() + pm = PassManager([light_cone]) - new_circuit = pm.run(qc) + qc = QuantumCircuit(2, 2) + qc.h(0) + qc.cx(0, 1) + qc.measure(0, 0) + qc.x(0) + qc.measure(1, 1) - expected = QuantumCircuit(2, 1) - expected.measure(0, 0) + new_circuit = pm.run(qc) - self.assertEqual(expected, new_circuit) + expected = QuantumCircuit(2, 2) + expected.h(0) + expected.cx(0, 1) + expected.measure(0, 0) + expected.measure(1, 1) + self.assertEqual(expected, new_circuit) if __name__ == "__main__": unittest.main() From 5dc3834eb2dc88987135fb34843ca64ee3960c79 Mon Sep 17 00:00:00 2001 From: Cosmin Dinu--Thiery Date: Tue, 21 Apr 2026 02:56:11 +0200 Subject: [PATCH 8/8] black corrections --- test/python/transpiler/test_light_cone.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/test/python/transpiler/test_light_cone.py b/test/python/transpiler/test_light_cone.py index 1fa827eb52ff..9989861d165b 100644 --- a/test/python/transpiler/test_light_cone.py +++ b/test/python/transpiler/test_light_cone.py @@ -21,13 +21,18 @@ Parameter, QuantumCircuit, ) -from qiskit.circuit.library import real_amplitudes, PauliProductRotationGate, PauliProductMeasurement +from qiskit.circuit.library import ( + real_amplitudes, + PauliProductRotationGate, + PauliProductMeasurement, +) from qiskit.circuit.library.n_local.efficient_su2 import efficient_su2 from qiskit.converters import circuit_to_dag from qiskit.quantum_info import SparsePauliOp, SparseObservable, Pauli from qiskit.transpiler.passes.optimization.light_cone import LightCone from qiskit.transpiler.passmanager import PassManager + @ddt.ddt class TestLightConePass(QiskitTestCase): """Test the LightCone pass.""" @@ -415,5 +420,6 @@ def test_mid_circuit_and_final_measurements(self): self.assertEqual(expected, new_circuit) + if __name__ == "__main__": unittest.main()