Skip to content

Commit 1736ae3

Browse files
Fixed commutation checking between two Pauli product measurements (backport #16023) (#16040)
* Fixed commutation checking between two Pauli product measurements (#16023) * Fix commutation checking between two PPMs * fix for the case qubits are different * additional fixes and improvements following the review * improve test formatting * formatting (again) (cherry picked from commit 89ff976) # Conflicts: # crates/transpiler/src/commutation_checker.rs # test/python/circuit/test_commutation_checker.py * fix conflicts * remove comment --------- Co-authored-by: Alexander Ivrii <alexi@il.ibm.com>
1 parent a31cf96 commit 1736ae3

3 files changed

Lines changed: 76 additions & 4 deletions

File tree

crates/transpiler/src/commutation_checker.rs

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,8 @@ use ndarray::linalg::kron;
1616
use num_complex::Complex64;
1717
use num_complex::ComplexFloat;
1818
use qiskit_circuit::object_registry::PyObjectAsKey;
19-
use qiskit_quantum_info::sparse_observable::PySparseObservable;
20-
use qiskit_quantum_info::sparse_observable::SparseObservable;
19+
use qiskit_circuit::operations::PauliProductMeasurement;
20+
use qiskit_quantum_info::sparse_observable::{PySparseObservable, SparseObservable};
2121
use smallvec::SmallVec;
2222
use std::fmt::Debug;
2323

@@ -220,6 +220,18 @@ fn try_extract_op_from_ppm(
220220
Some(out.compose_map(&local, |i| qubits[i as usize].0))
221221
}
222222

223+
/// Given a pauli product measurement, returns its generator (represented as a sparse observable)
224+
/// and the sign (representing the pauli phase).
225+
fn observable_generator_from_ppm(
226+
ppm: &PauliProductMeasurement,
227+
qubits: &[Qubit],
228+
num_qubits: u32,
229+
) -> (SparseObservable, bool) {
230+
let local = xz_to_observable(&ppm.x, &ppm.z);
231+
let out = SparseObservable::identity(num_qubits);
232+
(out.compose_map(&local, |i| qubits[i as usize].0), ppm.neg)
233+
}
234+
223235
fn try_extract_op_from_ppr(
224236
operation: &OperationRef,
225237
qubits: &[Qubit],
@@ -521,6 +533,22 @@ impl CommutationChecker {
521533
_ => (),
522534
};
523535

536+
// Special handling for commutativity of two pauli product measurements in the case they write to
537+
// the same classical bit. In this case, it's generally incorrect to interchange them, so we only
538+
// do this if they have the same generators (represented as sparse observables + signs).
539+
if let (
540+
OperationRef::PauliProductMeasurement(ppm1),
541+
OperationRef::PauliProductMeasurement(ppm2),
542+
) = (op1, op2)
543+
{
544+
if cargs1 == cargs2 {
545+
let size = qargs1.iter().chain(qargs2.iter()).max().unwrap().0 + 1;
546+
let pauli1 = observable_generator_from_ppm(ppm1, qargs1, size);
547+
let pauli2 = observable_generator_from_ppm(ppm2, qargs2, size);
548+
return Ok(pauli1 == pauli2);
549+
}
550+
}
551+
524552
// Handle commutations in between Pauli-based gates, like PauliGate or PauliEvolutionGate
525553
let size = qargs1.iter().chain(qargs2.iter()).max().unwrap().0 + 1;
526554
if let Some(obs1) = try_pauli_generator(op1, qargs1, size) {
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
---
2+
fixes:
3+
- |
4+
Fixed commutativity check between two :class:`.PauliProductMeasurement` instructions
5+
in the case that both instructions measure to the same classical bit. In this case,
6+
the later measurement overwrites the result of the earlier measurement, and consequently,
7+
interchanging the two measurement instructions inside the quantum circuit is generally
8+
invalid.

test/python/circuit/test_commutation_checker.py

Lines changed: 38 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -534,10 +534,11 @@ def test_pauli_based_gates(self, gate_type):
534534
for p1, q1, p2, q2, expected in cases:
535535
if p1 == "I" and gate_type == "measure":
536536
continue # PPM doesn't support all-identity gates
537+
c1, c2 = ([0], [1]) if gate_type == "measure" else ([], [])
537538

538539
gate1 = build_pauli_gate(p1, gate_type)
539540
gate2 = build_pauli_gate(p2, gate_type)
540-
self.assertEqual(expected, scc.commute(gate1, q1, [], gate2, q2, []))
541+
self.assertEqual(expected, scc.commute(gate1, q1, c1, gate2, q2, c2))
541542

542543
@data(
543544
("pauli", "measure"),
@@ -553,10 +554,45 @@ def test_mix_pauli_gates(self, gate_type1, gate_type2):
553554
]
554555

555556
for p1, q1, p2, q2, expected in cases:
557+
c1 = [0] if gate_type1 == "measure" else []
558+
c2 = [1] if gate_type2 == "measure" else []
559+
556560
gate1 = build_pauli_gate(p1, gate_type1)
557561
gate2 = build_pauli_gate(p2, gate_type2)
562+
558563
with self.subTest(p1=p1, p2=p2):
559-
self.assertEqual(expected, scc.commute(gate1, q1, [], gate2, q2, []))
564+
self.assertEqual(expected, scc.commute(gate1, q1, c1, gate2, q2, c2))
565+
566+
def test_ppms_with_same_clbit(self):
567+
"""Test commutativity of two Pauli product measurements with the same clbit."""
568+
569+
# Each case represents (pauli1, qubits1, pauli2, qubits2, expected result).
570+
# Recall that the convention is that pauli strings are read right-to-left, i.e.
571+
# pauli strings and qubits are in reverse order relative to each other.
572+
cases = [
573+
# different Paulis
574+
("XXII", [1, 0, 2, 3], "IIZZ", [1, 0, 2, 3], False),
575+
# different qubits
576+
("XYIZ", [1, 0, 2, 3], "XYIZ", [1, 0, 4, 3], False),
577+
# different Paulis (including sign)
578+
("ZXII", [1, 0, 2, 3], "-ZXII", [1, 0, 2, 3], False),
579+
# same Paulis and qubits
580+
("XYIZ", [1, 0, 2, 3], "XYIZ", [1, 0, 2, 3], True),
581+
# same Paulis and qubits
582+
("-XYIZ", [1, 0, 2, 3], "-XYIZ", [1, 0, 2, 3], True),
583+
# same Paulis and qubits up to reordering
584+
("XXIY", [0, 1, 2, 3], "YIXX", [3, 2, 1, 0], True),
585+
# same Paulis and qubits up to reordering
586+
("XXIY", [0, 1, 2, 3], "XXIY", [0, 1, 3, 2], True),
587+
# same Paulis and qubits up to reordering
588+
("-XXIY", [0, 1, 2, 3], "-YIXX", [2, 3, 1, 0], True),
589+
]
590+
591+
for pauli1, qubits1, pauli2, qubits2, expected in cases:
592+
with self.subTest(pauli1=pauli1, qubits1=qubits1, pauli2=pauli2, qubits2=qubits2):
593+
ppm1 = build_pauli_gate(pauli1, "measure")
594+
ppm2 = build_pauli_gate(pauli2, "measure")
595+
self.assertEqual(scc.commute(ppm1, qubits1, [0], ppm2, qubits2, [0]), expected)
560596

561597
def test_pauli_evolution_sums(self):
562598
"""Test PauliEvolutionGate commutations for operators that are sums of Paulis."""

0 commit comments

Comments
 (0)