Skip to content

Commit 7b840d4

Browse files
committed
Treat measure as Z-observable in CommutationChecker
1 parent 6be537f commit 7b840d4

4 files changed

Lines changed: 80 additions & 14 deletions

File tree

crates/transpiler/src/commutation_checker.rs

Lines changed: 29 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ use num_complex::Complex64;
1717
use num_complex::ComplexFloat;
1818
use qiskit_circuit::object_registry::PyObjectAsKey;
1919
use qiskit_circuit::standard_gate::standard_generators::standard_gate_exponent;
20-
use qiskit_quantum_info::sparse_observable::{PySparseObservable, SparseObservable};
20+
use qiskit_quantum_info::sparse_observable::{BitTerm, PySparseObservable, SparseObservable};
2121
use smallvec::SmallVec;
2222
use std::fmt::Debug;
2323

@@ -34,7 +34,7 @@ use qiskit_circuit::imports::QI_OPERATOR;
3434
use qiskit_circuit::instruction::{Instruction, Parameters};
3535
use qiskit_circuit::object_registry::ObjectRegistry;
3636
use qiskit_circuit::operations::{
37-
Operation, OperationRef, Param, STANDARD_GATE_SIZE, StandardGate,
37+
Operation, OperationRef, Param, STANDARD_GATE_SIZE, StandardGate, StandardInstruction,
3838
};
3939
use qiskit_circuit::{Clbit, Qubit};
4040
use qiskit_quantum_info::unitary_compose;
@@ -240,6 +240,20 @@ fn try_extract_op_from_ppr(
240240
Some(out.compose_map(&local, |i| qubits[i as usize].0))
241241
}
242242

243+
fn try_extract_op_from_measure(qubits: &[Qubit], num_qubits: u32) -> Option<SparseObservable> {
244+
let local = unsafe {
245+
SparseObservable::new_unchecked(
246+
1,
247+
vec![Complex64::new(1., 0.)],
248+
vec![BitTerm::Z],
249+
vec![0],
250+
vec![0, 1],
251+
)
252+
};
253+
let out = SparseObservable::identity(num_qubits);
254+
Some(out.compose_map(&local, |i| qubits[i as usize].0))
255+
}
256+
243257
fn try_pauli_generator(
244258
operation: &OperationRef,
245259
qubits: &[Qubit],
@@ -250,6 +264,7 @@ fn try_pauli_generator(
250264
"PauliEvolution" => try_extract_op_from_pauli_evo(operation, qubits, num_qubits),
251265
"pauli_product_measurement" => try_extract_op_from_ppm(operation, qubits, num_qubits),
252266
"pauli_product_rotation" => try_extract_op_from_ppr(operation, qubits, num_qubits),
267+
"measure" => try_extract_op_from_measure(qubits, num_qubits),
253268
_ => None,
254269
}
255270
}
@@ -830,14 +845,18 @@ fn commutation_precheck(
830845
}
831846
}
832847

833-
if matches!(
834-
op1,
835-
OperationRef::StandardInstruction(_) | OperationRef::Instruction(_)
836-
) || matches!(
837-
op2,
838-
OperationRef::StandardInstruction(_) | OperationRef::Instruction(_)
839-
) {
840-
return PrecheckStatus::NonCommuting;
848+
match (op1, op2) {
849+
(
850+
OperationRef::StandardInstruction(StandardInstruction::Measure),
851+
OperationRef::StandardInstruction(StandardInstruction::Measure),
852+
) => return PrecheckStatus::NonCommuting,
853+
(OperationRef::StandardInstruction(StandardInstruction::Measure), _)
854+
| (_, OperationRef::StandardInstruction(StandardInstruction::Measure)) => {}
855+
(OperationRef::StandardInstruction(_), _)
856+
| (_, OperationRef::StandardInstruction(_))
857+
| (OperationRef::Instruction(_), _)
858+
| (_, OperationRef::Instruction(_)) => return PrecheckStatus::NonCommuting,
859+
_ => {}
841860
}
842861

843862
if is_parameterized(params1) || is_parameterized(params2) {
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
---
2+
fixes:
3+
- |
4+
The :class:`.CommutationChecker` now treats a standard ``measure`` instruction
5+
as a Z-basis measurement when checking commutation against gates. Previously it
6+
was conservatively reported as non-commuting with everything. This also fixes
7+
the :class:`.LightCone` pass so that Z-diagonal gates preceding a measurement
8+
are correctly pruned from the light cone.
9+
Fixed `#15938 <https://github.com/Qiskit/qiskit/issues/15938>`__.

test/python/circuit/test_commutation_checker.py

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -286,9 +286,11 @@ def test_measure(self):
286286
# We should be able to swap these.
287287
self.assertTrue(scc.commute(Measure(), [0], [0], CXGate(), [1, 2], []))
288288

289-
# Measure and gate have intersecting set of qubits
290-
# We should not be able to swap these.
291-
self.assertFalse(scc.commute(Measure(), [0], [0], CXGate(), [0, 2], []))
289+
# Measure on the control of CX: CX is Z-diagonal on its control
290+
self.assertTrue(scc.commute(Measure(), [0], [0], CXGate(), [0, 2], []))
291+
292+
# Measure on the target of CX: X on the target , doesn't commute with Z
293+
self.assertFalse(scc.commute(Measure(), [0], [0], CXGate(), [1, 0], []))
292294

293295
# Measures over different qubits and clbits
294296
self.assertTrue(scc.commute(Measure(), [0], [0], Measure(), [1], [1]))
@@ -302,6 +304,15 @@ def test_measure(self):
302304
# Currently checker takes the safe approach and returns False.
303305
self.assertFalse(scc.commute(Measure(), [0], [0], Measure(), [0], [1]))
304306

307+
# Z-diagonal gates commute with Z-measures
308+
self.assertTrue(scc.commute(Measure(), [0], [0], ZGate(), [0], []))
309+
self.assertTrue(scc.commute(Measure(), [0], [0], SGate(), [0], []))
310+
self.assertTrue(scc.commute(Measure(), [0], [0], RZGate(0.5), [0], []))
311+
312+
# Non-Z Diagonal Gates Don't
313+
self.assertFalse(scc.commute(Measure(), [0], [0], HGate(), [0], []))
314+
self.assertFalse(scc.commute(Measure(), [0], [0], XGate(), [0], []))
315+
305316
def test_barrier(self):
306317
"""Check commutativity involving barriers."""
307318
# A gate should not commute with a barrier
@@ -566,6 +577,14 @@ def test_mix_pauli_gates(self, gate_type1, gate_type2):
566577
with self.subTest(p1=p1, p2=p2):
567578
self.assertEqual(expected, scc.commute(gate1, q1, [], gate2, q2, []))
568579

580+
def test_measure_vs_pauli_product_measurement(self):
581+
"""Standard measure and single-Z PPM should agree on computation."""
582+
ppmz = PauliProductMeasurement(Pauli("Z"))
583+
self.assertTrue(scc.commute(Measure(), [0], [0], ppmz, [0], []))
584+
585+
ppmx = PauliProductMeasurement(Pauli("X"))
586+
self.assertFalse(scc.commute(Measure(), [0], [0], ppmx, [0], []))
587+
569588
def test_pauli_evolution_sums(self):
570589
"""Test PauliEvolutionGate commutations for operators that are sums of Paulis."""
571590
xxyy = PauliEvolutionGate(SparsePauliOp(["XX", "YY"]))

test/python/transpiler/test_light_cone.py

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -227,10 +227,29 @@ def test_measurement_barriers(self):
227227
expected.barrier()
228228
expected.cx(0, 1)
229229
expected.barrier()
230-
expected.measure(0, 0)
231230

232231
self.assertEqual(expected, new_circuit)
233232

233+
def test_measure_commutes_with_z_diagonal(self):
234+
"""CZ should be pruned from the light cone when measuring in Z-basis.
235+
236+
Regression test for https://github.com/Qiskit/qiskit/issues/15938.
237+
"""
238+
239+
# observable-based: CZ commutes with Z on qubit 0
240+
qc1 = QuantumCircuit(2, 2)
241+
qc1.cz(0, 1)
242+
qct1 = LightCone("Z", [0])(qc1)
243+
244+
# measurement-based:m should give the same pruning
245+
qc2 = QuantumCircuit(2, 2)
246+
qc2.cz(0, 1)
247+
qc2.measure(0, 0)
248+
qct2 = LightCone()(qc2)
249+
250+
self.assertEqual(qct1.size(), 0)
251+
self.assertEqual(qct2.size(), 0) # only measure is left
252+
234253
@ddt.data(SparsePauliOp("IX"), SparseObservable("I+"))
235254
def test_parameter_expression(self, sparse_object):
236255
"""Test for Parameter expressions."""

0 commit comments

Comments
 (0)