diff --git a/crates/transpiler/src/commutation_checker.rs b/crates/transpiler/src/commutation_checker.rs index a56afcaf7e98..ee0861a07e4f 100644 --- a/crates/transpiler/src/commutation_checker.rs +++ b/crates/transpiler/src/commutation_checker.rs @@ -16,7 +16,6 @@ use ndarray::linalg::kron; use num_complex::Complex64; use num_complex::ComplexFloat; use qiskit_circuit::object_registry::PyObjectAsKey; -use qiskit_circuit::operations::PauliProductMeasurement; use qiskit_circuit::standard_gate::standard_generators::standard_gate_exponent; use qiskit_quantum_info::sparse_observable::{PySparseObservable, SparseObservable}; use smallvec::SmallVec; @@ -227,18 +226,6 @@ fn try_extract_op_from_ppm( Some(out.compose_map(&local, |i| qubits[i as usize].0)) } -/// Given a pauli product measurement, returns its generator (represented as a sparse observable) -/// and the sign (representing the pauli phase). -fn observable_generator_from_ppm( - ppm: &PauliProductMeasurement, - qubits: &[Qubit], - num_qubits: u32, -) -> (SparseObservable, bool) { - let local = xz_to_observable(&ppm.x, &ppm.z); - let out = SparseObservable::identity(num_qubits); - (out.compose_map(&local, |i| qubits[i as usize].0), ppm.neg) -} - fn try_extract_op_from_ppr( operation: &OperationRef, qubits: &[Qubit], @@ -253,7 +240,8 @@ fn try_extract_op_from_ppr( Some(out.compose_map(&local, |i| qubits[i as usize].0)) } -fn try_pauli_generator( +/// Attempt to extract generator of a Pauli-based gate in the form of a sparse observable. +fn try_sparse_observable_generator_for_pauli_based( operation: &OperationRef, qubits: &[Qubit], num_qubits: u32, @@ -267,7 +255,8 @@ fn try_pauli_generator( } } -fn try_standard_gate_generator( +/// Attemp to extract generator of a standard gate in the form of a sparse observable. +fn try_sparse_observable_generator_for_standard_gate( operation: &OperationRef, params: &[Param], qubits: &[Qubit], @@ -282,6 +271,18 @@ fn try_standard_gate_generator( None } +/// Attempt to extract generator of a Pauli-based gate in the form of a single Pauli. +/// When successful, return the generator in the (Z, X) form. +pub fn try_pauli_generator_for_pauli_based<'a>( + operation: &'a OperationRef, +) -> Option<(&'a Vec, &'a Vec)> { + match operation { + OperationRef::PauliProductRotation(ppr) => Some((&ppr.z, &ppr.x)), + OperationRef::PauliProductMeasurement(ppm) => Some((&ppm.z, &ppm.x)), + _ => None, + } +} + fn get_bits_from_py( py_bits1: &Bound<'_, PyTuple>, py_bits2: &Bound<'_, PyTuple>, @@ -317,6 +318,9 @@ pub struct CommutationChecker { library: CommutationLibrary, #[pyo3(get)] gates: Option>, + // scratch_map is used as a temporary workspace to avoid repeated allocations + // when computing commutation relations between pauli-based gates. + scratch_map: HashMap, } #[pymethods] @@ -427,6 +431,7 @@ impl CommutationChecker { CommutationChecker { library: library.unwrap_or(CommutationLibrary { library: None }), gates, + scratch_map: HashMap::new(), } } @@ -511,35 +516,81 @@ impl CommutationChecker { // Special handling for commutativity of two pauli product measurements in the case they write to // the same classical bit. In this case, it's generally incorrect to interchange them, so we only - // do this if they have the same generators (represented as sparse observables + signs). + // do this if they have the same pauli generators. if let ( OperationRef::PauliProductMeasurement(ppm1), OperationRef::PauliProductMeasurement(ppm2), ) = (op1, op2) { if cargs1 == cargs2 { - let size = qargs1.iter().chain(qargs2.iter()).max().unwrap().0 + 1; - let pauli1 = observable_generator_from_ppm(ppm1, qargs1, size); - let pauli2 = observable_generator_from_ppm(ppm2, qargs2, size); - return Ok(pauli1 == pauli2); + if (ppm1.neg != ppm2.neg) || (qargs1.len() != qargs2.len()) { + return Ok(false); + } + // Mark all qubit indices in qargs1 + self.scratch_map.clear(); + for (i, &q) in qargs1.iter().enumerate() { + self.scratch_map.insert(q.index(), i); + } + // Check that every qubit index in qargs2 is marked and has the same z,x values. + for (j, &q) in qargs2.iter().enumerate() { + let Some(&i) = self.scratch_map.get(&q.index()) else { + return Ok(false); + }; + if ppm1.z[i] != ppm2.z[j] || ppm1.x[i] != ppm2.x[j] { + return Ok(false); + } + } + return Ok(true); + } + } + + // Sort the arguments, such that `op2` always is the larger one. + let reversed = (op1.num_qubits(), op1.name().len(), op1.name()) + > (op2.num_qubits(), op2.name().len(), op2.name()); + + let (op1, op2, params1, params2, qargs1, qargs2) = if reversed { + (op2, op1, params2, params1, qargs2, qargs1) + } else { + (op1, op2, params1, params2, qargs1, qargs2) + }; + + // Handle commutations using Pauli-based generators. + if let (Some((z1, x1)), Some((z2, x2))) = ( + try_pauli_generator_for_pauli_based(op1), + try_pauli_generator_for_pauli_based(op2), + ) { + self.scratch_map.clear(); + for (i, &q) in qargs1.iter().enumerate() { + self.scratch_map.insert(q.index(), i); + } + let mut parity = false; + for (j, &q) in qargs2.iter().enumerate() { + if let Some(&i) = self.scratch_map.get(&q.index()) { + parity ^= (x1[i] && z2[j]) ^ (z1[i] && x2[j]); + } } + return Ok(!parity); } // Handle commutations between Pauli-based gates among themselves, and with standard gates // TODO Support trivial commutations of standard gates with identities in the Paulis let size = qargs1.iter().chain(qargs2.iter()).max().unwrap().0 + 1; - let maybe_pauli1 = try_pauli_generator(op1, qargs1, size); - let maybe_pauli2 = try_pauli_generator(op2, qargs2, size); + let maybe_pauli1 = try_sparse_observable_generator_for_pauli_based(op1, qargs1, size); + let maybe_pauli2 = try_sparse_observable_generator_for_pauli_based(op2, qargs2, size); match (maybe_pauli1, maybe_pauli2) { (None, None) => (), // No gate is Pauli-based, continue (None, Some(pauli2)) => { - if let Some(pauli1) = try_standard_gate_generator(op1, params1, qargs1, size) { + if let Some(pauli1) = + try_sparse_observable_generator_for_standard_gate(op1, params1, qargs1, size) + { return Ok(pauli1.commutes(&pauli2, tol)); } } (Some(pauli1), None) => { - if let Some(pauli2) = try_standard_gate_generator(op2, params2, qargs2, size) { + if let Some(pauli2) = + try_sparse_observable_generator_for_standard_gate(op2, params2, qargs2, size) + { return Ok(pauli1.commutes(&pauli2, tol)); } } @@ -551,47 +602,21 @@ impl CommutationChecker { return Ok(false); } - // Sort the arguments, such that `second_op` always is the larger one. - let reversed = if op1.num_qubits() != op2.num_qubits() { - op1.num_qubits() > op2.num_qubits() - } else { - (op1.name().len(), op1.name()) >= (op2.name().len(), op2.name()) - }; - let (first_params, second_params) = if reversed { - (params2, params1) - } else { - (params1, params2) - }; - let (first_op, second_op) = if reversed { (op2, op1) } else { (op1, op2) }; - let (first_qargs, second_qargs) = if reversed { - (qargs2, qargs1) - } else { - (qargs1, qargs2) - }; - // Query commutation library - let relative_placement = get_relative_placement(first_qargs, second_qargs); + let relative_placement = get_relative_placement(qargs1, qargs2); if let Some(is_commuting) = self.library - .check_commutation_entries(first_op, second_op, &relative_placement) + .check_commutation_entries(op1, op2, &relative_placement) { return Ok(is_commuting); } - if second_qargs.len() > matrix_max_num_qubits as usize { + if qargs2.len() > matrix_max_num_qubits as usize { return Ok(false); } // Perform matrix multiplication to determine commutation - let is_commuting = self.commute_matmul( - first_op, - first_params, - first_qargs, - second_op, - second_params, - second_qargs, - tol, - )?; + let is_commuting = self.commute_matmul(op1, params1, qargs1, op2, params2, qargs2, tol)?; Ok(is_commuting) } @@ -1005,6 +1030,7 @@ pub fn get_standard_commutation_checker() -> CommutationChecker { CommutationChecker { library, gates: None, + scratch_map: HashMap::new(), } } diff --git a/crates/transpiler/src/passes/commutative_optimization.rs b/crates/transpiler/src/passes/commutative_optimization.rs index 72a1a5e07b64..ac4ebdacc62f 100644 --- a/crates/transpiler/src/passes/commutative_optimization.rs +++ b/crates/transpiler/src/passes/commutative_optimization.rs @@ -21,12 +21,14 @@ use qiskit_circuit::instruction::Parameters; use smallvec::smallvec; use crate::commutation_checker::{CommutationChecker, try_matrix_with_definition}; -use crate::passes::remove_identity_equiv::{average_gate_fidelity_below_tol, is_identity_equiv}; +use crate::passes::remove_identity_equiv::{ + MINIMUM_TOL, average_gate_fidelity_below_tol, is_identity_equiv, +}; use qiskit_circuit::circuit_instruction::OperationFromPython; use qiskit_circuit::dag_circuit::DAGCircuit; use qiskit_circuit::operations::{ - Operation, OperationRef, Param, PauliBased, PauliProductRotation, StandardGate, multiply_param, - radd_param, + Operation, OperationRef, Param, PauliBased, PauliProductMeasurement, PauliProductRotation, + StandardGate, multiply_param, radd_param, }; use qiskit_circuit::{BlocksMode, Clbit, NoBlocks, Qubit, imports}; @@ -104,7 +106,8 @@ static MERGEABLE_ROTATION_GATES: [StandardGate; 12] = [ /// Computes the canonical representative of a packed instruction, and in particular: /// * replaces all types of Z-rotations by RZ-gates, /// * replaces all types of X-rotations by RX-gates, -/// * sorts the qubits for symmetric gates. +/// * sorts qubits for symmetric gates, +/// * sorts qubits for [PauliProductRotation] and [PauliProductMeasurement]. /// /// # Arguments: /// @@ -196,6 +199,8 @@ fn canonicalize( } } + // Sort qubits for PauliProductRotations: this allows to merge scrambled pauli rotations + // and allows a faster commutativity check with other Pauli-based gates. if let OperationRef::PauliProductRotation(ppr) = inst.op.view() { let qargs = dag.get_qargs(inst.qubits); let mut paired = qargs @@ -229,9 +234,98 @@ fn canonicalize( return Some((canonical_instruction, Param::Float(0.))); } + // Sort qubits for PauliProductMeasurements: this allows a faster commutativity check with + // other Pauli-based gates. + if let OperationRef::PauliProductMeasurement(ppm) = inst.op.view() { + let qargs = dag.get_qargs(inst.qubits); + let mut paired = qargs + .iter() + .zip(ppm.z.iter()) + .zip(ppm.x.iter()) + .map(|((q, z), x)| (q, z, x)) + .collect::>(); + paired.sort_by_key(|(q, _, _)| **q); + let (sorted_qargs, sorted_z, sorted_x) = + paired + .into_iter() + .multiunzip::<(Vec, Vec, Vec)>(); + let sorted_ppm = PauliProductMeasurement { + z: sorted_z, + x: sorted_x, + neg: ppm.neg, + }; + + let sorted_qubits = dag.add_qargs(&sorted_qargs); + + let canonical_instruction = PackedInstruction { + op: PauliBased::PauliProductMeasurement(sorted_ppm).into(), + qubits: sorted_qubits, + clbits: inst.clbits, + params: inst.params.clone(), + label: None, + #[cfg(feature = "cache_pygates")] + py_op: std::sync::OnceLock::new(), + }; + return Some((canonical_instruction, Param::Float(0.))); + } + None } +/// Checks commutation between two pauli-based gates, assuming +/// * the qubits are sorted by index +/// * the first instruction is not a PPM +fn try_commute_pauli_based_with_sorted_qargs( + op1: &OperationRef, + op2: &OperationRef, + qargs1: &[Qubit], + qargs2: &[Qubit], +) -> Option { + // To check whether two Pauli-based gates commute, we extract their Pauli generators and + // test them for commutation. While we could also use the commutation checker, we can perform + // a more efficient check here because the qubits are already sorted by index. + // + // Two important notes: + // * We do not need to handle the case where both instructions are PPMs writing to the + // same clbit. This is because we do not attempt to merge a PPM with other gates, so + // the first instruction is never a PPM. + // * All PPRs equivalent to the identity up to a global phase have already been removed. + // As a result, commutation of Pauli generators is both a necessary and sufficient condition. + let (z1, x1, z2, x2) = match (op1, op2) { + (OperationRef::PauliProductMeasurement(_), _) => { + unreachable!( + "The commutative optimization pass does not merge Pauli product measurement instructions. \ + Thus, the first instruction cannot be a PauliProductMeasurement." + ); + } + (OperationRef::PauliProductRotation(pp1), OperationRef::PauliProductRotation(pp2)) => { + (&pp1.z, &pp1.x, &pp2.z, &pp2.x) + } + (OperationRef::PauliProductRotation(pp1), OperationRef::PauliProductMeasurement(pp2)) => { + (&pp1.z, &pp1.x, &pp2.z, &pp2.x) + } + _ => { + return None; + } + }; + + let mut parity = false; + let (n1, n2) = (qargs1.len(), qargs2.len()); + let (mut i1, mut i2) = (0, 0); + while i1 < n1 && i2 < n2 { + match qargs1[i1].cmp(&qargs2[i2]) { + std::cmp::Ordering::Less => i1 += 1, + std::cmp::Ordering::Greater => i2 += 1, + std::cmp::Ordering::Equal => { + parity ^= (x1[i1] && z2[i2]) ^ (z1[i1] && x2[i2]); + i1 += 1; + i2 += 1; + } + } + } + Some(!parity) +} + /// Return `true` if two instructions commute (up to the specified tolerance). /// /// # Arguments: @@ -256,6 +350,13 @@ fn commute( let op1 = inst1.op.view(); let op2 = inst2.op.view(); + // For pauli-based gates, we use a more efficient implementation instead of calling + // the generic commutation checker, using the fact we have already sorted the qubits by index + // during canonicalization. + if let Some(val) = try_commute_pauli_based_with_sorted_qargs(&op1, &op2, qargs1, qargs2) { + return Ok(val); + } + Ok(commutation_checker.commute( &op1, inst1.params.as_deref(), @@ -470,7 +571,8 @@ pub fn run_commutative_optimization( approximation_degree: f64, matrix_max_num_qubits: u32, ) -> PyResult> { - let tol = 1e-12_f64.max(1. - approximation_degree); + let tol = MINIMUM_TOL.max(1. - approximation_degree); + let error_cutoff_fn = |_inst: &PackedInstruction| -> f64 { tol }; // Create output DAG. // We will use it to intern qubits of canonicalized instructions. @@ -490,11 +592,18 @@ pub fn run_commutative_optimization( let node_index1 = node_indices[idx1]; let instr1 = dag[node_index1].unwrap_operation(); - // For now, assume that control-flow operations do not commute with anything. + // Right now the control-flow operations cannot be merged with anything. if instr1.op.try_control_flow().is_some() { continue; } + if let Some(phase_update) = is_identity_equiv(instr1, false, Some(0), error_cutoff_fn)? { + node_actions[idx1] = NodeAction::Drop; + new_global_phase = radd_param(new_global_phase, Param::Float(phase_update)); + modified = true; + continue; + } + if let Some((new_instruction, phase_update)) = canonicalize(&mut new_dag, instr1) { node_actions[idx1] = NodeAction::Canonical(new_instruction, phase_update); } @@ -508,6 +617,13 @@ pub fn run_commutative_optimization( } }; + // Right now we do not merge pauli product measurement instructions with other instructions. + // However, we did previously canonicalize them so that we can efficiently check commutation relations + // between them and pauli product rotations. + if matches!(instr1.op.view(), OperationRef::PauliProductMeasurement(_)) { + continue; + } + let qargs1: &[Qubit] = new_dag.get_qargs(instr1.qubits); let cargs1: &[Clbit] = new_dag.get_cargs(instr1.clbits); diff --git a/crates/transpiler/src/passes/remove_identity_equiv.rs b/crates/transpiler/src/passes/remove_identity_equiv.rs index eaa69dede8e5..70c95af7a915 100644 --- a/crates/transpiler/src/passes/remove_identity_equiv.rs +++ b/crates/transpiler/src/passes/remove_identity_equiv.rs @@ -34,7 +34,7 @@ use qiskit_util::getenv_use_multiple_threads; // if the performance of this pass changes over time. const PARALLEL_THRESHOLD: usize = 50_000; -const MINIMUM_TOL: f64 = 1e-12; +pub const MINIMUM_TOL: f64 = 1e-12; /// Fidelity-based computation to check whether an operation `G` is equivalent /// to identity up to a global phase. diff --git a/releasenotes/notes/speedup-pauli-rotations-commutation-checking-c9b65ad1e3d0c453.yaml b/releasenotes/notes/speedup-pauli-rotations-commutation-checking-c9b65ad1e3d0c453.yaml new file mode 100644 index 000000000000..dda530c7c669 --- /dev/null +++ b/releasenotes/notes/speedup-pauli-rotations-commutation-checking-c9b65ad1e3d0c453.yaml @@ -0,0 +1,14 @@ +--- +features_transpiler: + - | + Improved performance of :class:`.CommutationChecker` when evaluating commutations + between :class:`.PauliProductRotationGate` and :class:`.PauliProductMeasurement` objects. + This is achieved by representing their generators as single-term Paulis and performing + commutativity checks using these representations. + - | + Improved performance of the :class:`.CommutativeOptimization` transpiler pass on + circuits containing :class:`.PauliProductRotationGate` and :class:`.PauliProductMeasurement` + objects. + - | + The :class:`.CommutativeOptimization` transpiler pass now removes gates that are equivalent to + identity up to a global phase. diff --git a/test/python/circuit/test_commutation_checker.py b/test/python/circuit/test_commutation_checker.py index 9f2c5d7fb2b5..aef466ca1135 100644 --- a/test/python/circuit/test_commutation_checker.py +++ b/test/python/circuit/test_commutation_checker.py @@ -15,6 +15,8 @@ import unittest from test import QiskitTestCase +import itertools + import scipy import numpy as np from ddt import idata, ddt, data, unpack @@ -117,6 +119,9 @@ def _define(self): self.definition.rx(self.value, 0) +pauli_based_types = ["pauli", "measure", "evolution", "rotation"] + + @ddt class TestCommutationChecker(QiskitTestCase): """Test CommutationChecker class.""" @@ -510,9 +515,10 @@ def test_matrix_threshold_noncachable(self): scc.commute(other, [0], [], big, qubits, [], matrix_max_num_qubits=num_qubits - 1) ) - @data("pauli", "evolution", "measure") - def test_pauli_based_gates(self, gate_type): - """Test Pauli-based gates.""" + @data(*list(itertools.product(pauli_based_types, repeat=2))) + @unpack + def test_pauli_based_gates(self, gate_type1, gate_type2): + """Test commutation relations across different Pauli-based gates.""" cases = [ ("I", [0], "XYZ", list(range(3)), True), ("ZZZZ", list(range(4)), "XXXX", list(range(4)), True), @@ -523,34 +529,16 @@ def test_pauli_based_gates(self, gate_type): ] for p1, q1, p2, q2, expected in cases: - if p1 == "I" and gate_type == "measure": - continue # PPM doesn't support all-identity gates - c1, c2 = ([0], [1]) if gate_type == "measure" else ([], []) - - gate1 = build_pauli_gate(p1, gate_type) - gate2 = build_pauli_gate(p2, gate_type) - self.assertEqual(expected, scc.commute(gate1, q1, c1, gate2, q2, c2)) - - @data( - ("pauli", "measure"), - ("evolution", "measure"), - ("evolution", "pauli"), - ) - @unpack - def test_mix_pauli_gates(self, gate_type1, gate_type2): - """Test commutation relations across different Pauli-based gates.""" - cases = [ - ("ZZIIIIIIIY", list(range(10)), "YYIIIIIIIZ", list(range(10)), False), - ("ZX", [1, 10], "ZIZYIZXXZXZ", list(range(11)), True), - ] - - for p1, q1, p2, q2, expected in cases: + # For commutation of PPMs, use different classical bits. + # (we include separate tests for PPMs writing to the same clbit). c1 = [0] if gate_type1 == "measure" else [] c2 = [1] if gate_type2 == "measure" else [] + if p1 == "I" and gate_type1 == "measure": + continue # PPM doesn't support all-identity gates + gate1 = build_pauli_gate(p1, gate_type1) gate2 = build_pauli_gate(p2, gate_type2) - with self.subTest(p1=p1, p2=p2): self.assertEqual(expected, scc.commute(gate1, q1, c1, gate2, q2, c2)) @@ -616,7 +604,7 @@ def test_pauli_evolution_parameterized(self): with self.subTest(left=z, right=x): self.assertFalse(scc.commute(z, qargs, [], x, qargs, [])) - @data("evolution", "pauli", "measure", "rotation") + @data(*pauli_based_types) def test_pauli_and_standard_gate(self, pauli_type): """Test Pauli-based gates and standard gate commutations are efficiently supported.""" # 40-qubit Pauli gate with following terms: X: 0-9, Y: 10-19, Z: 20-29, I: 30-39 @@ -649,6 +637,34 @@ def test_pauli_and_standard_gate(self, pauli_type): ) self.assertEqual(expected, commutes) + @data(*list(itertools.product(["rotation", "measure"], repeat=2))) + @unpack + def test_pauli_based_with_indices_and_phases(self, gate_type1, gate_type2): + """Test commutation relations between pauli product rotations, + with varying Pauli phases and varying qubit indices. + """ + cases = [ + ("XXYY", [2, 1, 5, 3], "ZZ", [2, 0], False), + ("XXYY", [2, 1, 5, 3], "ZZ", [2, 3], True), + ("-XXYY", [2, 1, 5, 3], "ZZ", [2, 0], False), + ("-XXYY", [2, 1, 5, 3], "ZZ", [2, 3], True), + ("XXYY", [2, 1, 5, 3], "-ZZZ", [2, 3, 1], False), + ("XXYY", [2, 1, 5, 3], "-ZZZ", [3, 2, 6], True), + ("XXYY", [2, 1, 5, 3], "ZZ", [4, 7], True), + ("ZZZ", [4, 1, 7], "XXX", [1, 4, 7], False), + ("ZZZ", [2, 1, 7], "XXX", [1, 4, 7], True), + ("-ZZZ", [4, 1, 7], "-ZZZ", [1, 4, 7], True), + ("-ZZZ", [2, 1, 7], "-ZZZ", [1, 4, 7], True), + ] + for p1, q1, p2, q2, expected in cases: + gate1 = build_pauli_gate(p1, gate_type1) + gate2 = build_pauli_gate(p2, gate_type2) + c1 = [0] if gate_type1 == "measure" else [] + c2 = [1] if gate_type2 == "measure" else [] + + with self.subTest(p1=p1, q1=q1, p2=p2, q2=q2): + self.assertEqual(expected, scc.commute(gate1, q1, c1, gate2, q2, c2)) + @data("evolution", "pauli", "rotation", "measure") def test_pauli_based_with_matrix(self, pauli_type): """Test commutation with a matrix-based gate.""" diff --git a/test/python/transpiler/test_commutative_optimization.py b/test/python/transpiler/test_commutative_optimization.py index bf5b352ad26d..24d22b701a5e 100644 --- a/test/python/transpiler/test_commutative_optimization.py +++ b/test/python/transpiler/test_commutative_optimization.py @@ -16,7 +16,7 @@ import numpy as np -from ddt import ddt, data +from ddt import ddt, data, unpack from qiskit import QuantumRegister, QuantumCircuit from qiskit.converters import circuit_to_dag @@ -27,6 +27,7 @@ UnitaryGate, PauliEvolutionGate, PauliProductRotationGate, + PauliProductMeasurement, Initialize, U2Gate, CZGate, @@ -1227,6 +1228,92 @@ def test_circuit_with_global_phase(self): self.assertEqual(Operator(expected), Operator(qc)) self.assertEqual(qct, expected) + def test_merge_pprs_with_unordered_indices(self): + """Test merging PPRs with scrambled qubit indices.""" + qc = QuantumCircuit(8) + qc.append(PauliProductRotationGate(Pauli("IXYZ"), 1), [3, 5, 0, 2]) + qc.append(PauliProductRotationGate(Pauli("ZIXY"), 1), [5, 0, 2, 3]) + qc.append(PauliProductRotationGate(Pauli("YZIX"), 1), [0, 2, 3, 5]) + qc.append(PauliProductRotationGate(Pauli("XYZI"), 1), [2, 3, 5, 0]) + + # All of the product rotations gates above are the same (after sorting + # the indices), so all of these gates should get combined. + qct = CommutativeOptimization()(qc) + self.assertEqual(qct.size(), 1) + + @data( + ("XXXX", [2, 3, 1, 0], "ZZ", [0, 1], True), + ("XXXX", [2, 3, 1, 0], "ZZ", [1, 5], False), + ("XXXX", [2, 3, 1, 0], "ZZZZ", [1, 0, 3, 2], True), + ("XXXX", [2, 3, 1, 0], "ZZZZ", [1, 0, 3, 5], False), + ("XXXX", [2, 3, 1, 0], "-ZZ", [0, 1], True), + ("XXXX", [2, 3, 1, 0], "-ZZ", [1, 5], False), + ("XXXX", [2, 3, 1, 0], "-ZZZZ", [1, 0, 3, 2], True), + ("XXXX", [2, 3, 1, 0], "-ZZZZ", [1, 0, 3, 5], False), + ) + @unpack + def test_cancel_pprs_across_ppm(self, p1, q1, p2, q2, should_cancel): + """Test that two PPRs get canceled iff they commute with a PPM separating them.""" + qc = QuantumCircuit(8, 1) + qc.append(PauliProductRotationGate(Pauli(p1), 1), q1) + qc.append(PauliProductMeasurement(Pauli(p2)), q2, [0]) + qc.append(PauliProductRotationGate(Pauli(p1), -1), q1) + + # The two PPR gates should get canceled iff they commute with PPM + qct = CommutativeOptimization()(qc) + self.assertEqual(qct.size() == 1, should_cancel) + + def test_remove_equivalent_to_identity(self): + """Test that PPRs equivalent to identity get removed, and the + global phase is updated correctly. + """ + qc = QuantumCircuit(3) + qc.append(PauliProductRotationGate(Pauli("XYZ"), 2 * np.pi), [0, 1, 2]) + + qct = CommutativeOptimization()(qc) + + qc_expected = QuantumCircuit(3, global_phase=np.pi) + + self.assertEqual(qct, qc_expected) + self.assertEqual(Operator(qct), Operator(qc)) + + def test_merge_across_equivalent_to_identity(self): + """Test that two PPRs get merged when they are separated by a PPR + that is equivalent to identity. + """ + qc = QuantumCircuit(4) + qc.append(PauliProductRotationGate(Pauli("XXZ"), 1), [0, 1, 2]) + qc.append(PauliProductRotationGate(Pauli("XYZ"), 4 * np.pi), [0, 1, 2]) + qc.append(PauliProductRotationGate(Pauli("XXZ"), 1), [0, 1, 2]) + + qct = CommutativeOptimization()(qc) + + qc_expected = QuantumCircuit(4) + qc_expected.append(PauliProductRotationGate(Pauli("XXZ"), 2), [0, 1, 2]) + + self.assertEqual(qct, qc_expected) + self.assertEqual(Operator(qct), Operator(qc)) + + def test_consecutive_ppms(self): + """Test circuits with consecutive pauli product measurements""" + qc = QuantumCircuit(4, 2) + qc.append(PauliProductRotationGate(Pauli("XX"), 1), [0, 1]) + qc.append(PauliProductMeasurement(Pauli("ZZ")), [0, 1], [0]) + qc.append(PauliProductMeasurement(Pauli("ZZ")), [0, 1], [0]) + qc.append(PauliProductMeasurement(Pauli("YX")), [0, 2], [1]) + qc.append(PauliProductRotationGate(Pauli("XX"), -1), [0, 1]) + + qct = CommutativeOptimization()(qc) + + # The two rotation gates commute through all measurement gates and should cancel. + # The measurement gates should remain unchanged. + qc_expected = QuantumCircuit(4, 2) + qc_expected.append(PauliProductMeasurement(Pauli("ZZ")), [0, 1], [0]) + qc_expected.append(PauliProductMeasurement(Pauli("ZZ")), [0, 1], [0]) + qc_expected.append(PauliProductMeasurement(Pauli("YX")), [0, 2], [1]) + + self.assertEqual(qct, qc_expected) + if __name__ == "__main__": unittest.main()