Skip to content

Commit f78bb32

Browse files
alexanderivriimergify[bot]
authored andcommitted
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
1 parent a31cf96 commit f78bb32

3 files changed

Lines changed: 121 additions & 2 deletions

File tree

crates/transpiler/src/commutation_checker.rs

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,14 @@ use ndarray::linalg::kron;
1616
use num_complex::Complex64;
1717
use num_complex::ComplexFloat;
1818
use qiskit_circuit::object_registry::PyObjectAsKey;
19+
<<<<<<< HEAD
1920
use qiskit_quantum_info::sparse_observable::PySparseObservable;
2021
use qiskit_quantum_info::sparse_observable::SparseObservable;
22+
=======
23+
use qiskit_circuit::operations::PauliProductMeasurement;
24+
use qiskit_circuit::standard_gate::standard_generators::standard_gate_exponent;
25+
use qiskit_quantum_info::sparse_observable::{PySparseObservable, SparseObservable};
26+
>>>>>>> 89ff976e0 (Fixed commutation checking between two Pauli product measurements (#16023))
2127
use smallvec::SmallVec;
2228
use std::fmt::Debug;
2329

@@ -220,6 +226,18 @@ fn try_extract_op_from_ppm(
220226
Some(out.compose_map(&local, |i| qubits[i as usize].0))
221227
}
222228

229+
/// Given a pauli product measurement, returns its generator (represented as a sparse observable)
230+
/// and the sign (representing the pauli phase).
231+
fn observable_generator_from_ppm(
232+
ppm: &PauliProductMeasurement,
233+
qubits: &[Qubit],
234+
num_qubits: u32,
235+
) -> (SparseObservable, bool) {
236+
let local = xz_to_observable(&ppm.x, &ppm.z);
237+
let out = SparseObservable::identity(num_qubits);
238+
(out.compose_map(&local, |i| qubits[i as usize].0), ppm.neg)
239+
}
240+
223241
fn try_extract_op_from_ppr(
224242
operation: &OperationRef,
225243
qubits: &[Qubit],
@@ -521,7 +539,28 @@ impl CommutationChecker {
521539
_ => (),
522540
};
523541

542+
<<<<<<< HEAD
524543
// Handle commutations in between Pauli-based gates, like PauliGate or PauliEvolutionGate
544+
=======
545+
// Special handling for commutativity of two pauli product measurements in the case they write to
546+
// the same classical bit. In this case, it's generally incorrect to interchange them, so we only
547+
// do this if they have the same generators (represented as sparse observables + signs).
548+
if let (
549+
OperationRef::PauliProductMeasurement(ppm1),
550+
OperationRef::PauliProductMeasurement(ppm2),
551+
) = (op1, op2)
552+
{
553+
if cargs1 == cargs2 {
554+
let size = qargs1.iter().chain(qargs2.iter()).max().unwrap().0 + 1;
555+
let pauli1 = observable_generator_from_ppm(ppm1, qargs1, size);
556+
let pauli2 = observable_generator_from_ppm(ppm2, qargs2, size);
557+
return Ok(pauli1 == pauli2);
558+
}
559+
}
560+
561+
// Handle commutations between Pauli-based gates among themselves, and with standard gates
562+
// TODO Support trivial commutations of standard gates with identities in the Paulis
563+
>>>>>>> 89ff976e0 (Fixed commutation checking between two Pauli product measurements (#16023))
525564
let size = qargs1.iter().chain(qargs2.iter()).max().unwrap().0 + 1;
526565
if let Some(obs1) = try_pauli_generator(op1, qargs1, size) {
527566
if let Some(obs2) = try_pauli_generator(op2, qargs2, 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: 74 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."""
@@ -589,6 +625,42 @@ def test_pauli_evolution_parameterized(self):
589625
with self.subTest(left=z, right=x):
590626
self.assertFalse(scc.commute(z, qargs, [], x, qargs, []))
591627

628+
<<<<<<< HEAD
629+
=======
630+
@data("evolution", "pauli", "measure", "rotation")
631+
def test_pauli_and_standard_gate(self, pauli_type):
632+
"""Test Pauli-based gates and standard gate commutations are efficiently supported."""
633+
# 40-qubit Pauli gate with following terms: X: 0-9, Y: 10-19, Z: 20-29, I: 30-39
634+
pauli = 10 * "I" + 10 * "Z" + 10 * "Y" + 10 * "X"
635+
pauli_qubits = list(range(len(pauli)))
636+
pauli_clbits = [0] if pauli_type == "measure" else []
637+
pauli_gate = build_pauli_gate(pauli, pauli_type)
638+
639+
# Test cases in the format: (gate, indices, commutes)
640+
x = Parameter("x")
641+
cases = [
642+
(CXGate(), [30, 0], True), # CX vs. IX
643+
(CXGate(), [29, 0], True), # CX vs. ZX
644+
(CXGate(), [29, 10], False), # CX vs. ZY
645+
(RXXGate(x), [0, 1], True),
646+
(RXXGate(1.2), [11, 15], True),
647+
(RXXGate(x), [0, 15], False),
648+
(HGate(), [33], True),
649+
(HGate(), [2], False),
650+
(CPhaseGate(0.1), [30, 31], True),
651+
(CPhaseGate(0.1), [2, 3], False),
652+
(XXPlusYYGate(1, 1), [0, 1], False),
653+
(XXPlusYYGate(1, 0), [0, 1], True),
654+
]
655+
656+
for std_gate, indices, expected in cases:
657+
with self.subTest(std_gate=std_gate, indices=indices):
658+
commutes = scc.commute(
659+
pauli_gate, pauli_qubits, pauli_clbits, std_gate, indices, []
660+
)
661+
self.assertEqual(expected, commutes)
662+
663+
>>>>>>> 89ff976e0 (Fixed commutation checking between two Pauli product measurements (#16023))
592664

593665
def build_pauli_gate(pauli_string: str, gate_type: str) -> Gate:
594666
"""Build a Pauli-based gate off a Pauli string.

0 commit comments

Comments
 (0)