From 70a9968a7604e59ebd69ec0cce17becd0415b34d Mon Sep 17 00:00:00 2001 From: AlexanderIvrii Date: Wed, 15 Nov 2023 16:27:49 +0200 Subject: [PATCH 1/7] pass, tests, reno --- qiskit/quantum_info/operators/predicates.py | 21 + .../commutative_inverse_cancellation.py | 52 ++- ...ncellation-uptophase-028525d21b199cef.yaml | 26 ++ .../test_commutative_inverse_cancellation.py | 379 ++++++++++++++---- 4 files changed, 395 insertions(+), 83 deletions(-) create mode 100644 releasenotes/notes/commutative-inv-cancellation-uptophase-028525d21b199cef.yaml diff --git a/qiskit/quantum_info/operators/predicates.py b/qiskit/quantum_info/operators/predicates.py index 8473e445b809..a98eee4e6729 100644 --- a/qiskit/quantum_info/operators/predicates.py +++ b/qiskit/quantum_info/operators/predicates.py @@ -21,6 +21,27 @@ RTOL_DEFAULT = 1e-5 +def _get_phase_difference(mat1, mat2, atol=ATOL_DEFAULT): + """Assumes that mat1 and mat2 only differ by a phase + (as in the case when ``matrix_equal(mat1, mat2, ignore_phase=True)`` returns True). + Returns this phase difference. + """ + if not isinstance(mat1, np.ndarray): + mat1 = np.array(mat1) + if not isinstance(mat2, np.ndarray): + mat2 = np.array(mat2) + phase_difference = 0 + for elt in mat1.flat: + if abs(elt) > atol: + phase_difference -= np.angle(elt) + break + for elt in mat2.flat: + if abs(elt) > atol: + phase_difference += np.angle(elt) + break + return phase_difference + + def matrix_equal(mat1, mat2, ignore_phase=False, rtol=RTOL_DEFAULT, atol=ATOL_DEFAULT): """Test if two arrays are equal. diff --git a/qiskit/transpiler/passes/optimization/commutative_inverse_cancellation.py b/qiskit/transpiler/passes/optimization/commutative_inverse_cancellation.py index 518ccbbef17e..a43007b14acd 100644 --- a/qiskit/transpiler/passes/optimization/commutative_inverse_cancellation.py +++ b/qiskit/transpiler/passes/optimization/commutative_inverse_cancellation.py @@ -14,6 +14,8 @@ from qiskit.dagcircuit import DAGCircuit, DAGOpNode +from qiskit.quantum_info import Operator +from qiskit.quantum_info.operators.predicates import matrix_equal, _get_phase_difference from qiskit.transpiler.basepasses import TransformationPass from qiskit.circuit.commutation_checker import CommutationChecker @@ -21,6 +23,20 @@ class CommutativeInverseCancellation(TransformationPass): """Cancel pairs of inverse gates exploiting commutation relations.""" + def __init__(self, max_qubits: int = 4, upto_phase_optimization: bool = False): + """Initialize CommutativeInverseCancellation pass. + + Args: + max_qubits: limits the number of qubits in matrix-based commutativity and inverse + checks. + upto_phase_optimization: if True, also cancels out pairs of gates that are + inverse up to a phase, keeping track of the phase difference in the global + phase of the circuit. However, the inverse check becomes more expensive. + """ + self._max_qubits = max_qubits + self._upto_phase_optimization = upto_phase_optimization + super().__init__() + def _skip_node(self, node): """Returns True if we should skip this node for the analysis.""" if not isinstance(node, DAGOpNode): @@ -36,9 +52,29 @@ def _skip_node(self, node): return True if node.op.is_parameterized(): return True - # ToDo: possibly also skip nodes on too many qubits return False + def _inverse_upto_phase(self, node1, node2): + """Checks whether op1 and op2 are inverse up to a phase, that is whether + ``op2 = e^{i * d} op1^{-1})`` for some phase difference ``d``. + If this is the case, we can replace ``op2 * op1`` by `e^{i * d} I``. + The input to this function is a pair of DAG nodes. + The output is a tuple representing the result of the check and the phase difference. + """ + phase_difference = 0 + if not self._upto_phase_optimization: + result = node1.op.inverse() == node2.op + elif len(node2.qargs) > self._max_qubits: + result = False + else: + mat1 = Operator(node1.op.inverse()).data + mat2 = Operator(node2.op).data + result = matrix_equal(mat1, mat2, ignore_phase=True) + if result: + # mat2 = e^{i * phase_difference} mat1 + phase_difference = _get_phase_difference(mat1, mat2) + return result, phase_difference + def run(self, dag: DAGCircuit): """ Run the CommutativeInverseCancellation pass on `dag`. @@ -55,6 +91,7 @@ def run(self, dag: DAGCircuit): removed = [False for _ in range(circ_size)] + phase_update = 0 cc = CommutationChecker() for idx1 in range(0, circ_size): @@ -71,10 +108,14 @@ def run(self, dag: DAGCircuit): not self._skip_node(topo_sorted_nodes[idx2]) and topo_sorted_nodes[idx2].qargs == topo_sorted_nodes[idx1].qargs and topo_sorted_nodes[idx2].cargs == topo_sorted_nodes[idx1].cargs - and topo_sorted_nodes[idx2].op == topo_sorted_nodes[idx1].op.inverse() ): - matched_idx2 = idx2 - break + result, phase = self._inverse_upto_phase( + topo_sorted_nodes[idx1], topo_sorted_nodes[idx2] + ) + if result: + phase_update += phase + matched_idx2 = idx2 + break if not cc.commute( topo_sorted_nodes[idx1].op, @@ -94,4 +135,7 @@ def run(self, dag: DAGCircuit): if removed[idx]: dag.remove_op_node(topo_sorted_nodes[idx]) + if phase_update != 0: + dag.global_phase += phase_update + return dag diff --git a/releasenotes/notes/commutative-inv-cancellation-uptophase-028525d21b199cef.yaml b/releasenotes/notes/commutative-inv-cancellation-uptophase-028525d21b199cef.yaml new file mode 100644 index 000000000000..5936de1dcae4 --- /dev/null +++ b/releasenotes/notes/commutative-inv-cancellation-uptophase-028525d21b199cef.yaml @@ -0,0 +1,26 @@ +--- +features: + - | + Added two new arguments ``max_qubits`` and ``upto_phase_optimization`` to the + constructor of :class:`.CommutativeInverseCancellation` transpiler pass. + When ``upto_phase_optimization`` is ``True`` the pass also cancels pairs of + gates that are inverse up to a phase, and updates the global phase of the circuit + accordingly. Generally this leads to more reductions at the expense of increased + runtime. Internally, the check is based on constructing and comparing unitary matrices + corresponding to various gates in the circuit. The argument ``max_qubits`` limits + the number of qubits in matrix-based commutativity and inverse checks. For example:: + + import numpy as np + from qiskit.circuit import QuantumCircuit + from qiskit.transpiler import PassManager + from qiskit.transpiler.passes import CommutativeInverseCancellation + + circuit = QuantumCircuit(1) + circuit.rz(np.pi / 4, 0) + circuit.p(-np.pi / 4, 0) + + passmanager = PassManager(CommutativeInverseCancellation(upto_phase_optimization=True)) + new_circuit = passmanager.run(circuit) + + The pass is able to cancel the ``RZ`` and ``P`` gates, while adjusting the circuit's global + phase to :math:`\frac{15 \pi}{8}`. diff --git a/test/python/transpiler/test_commutative_inverse_cancellation.py b/test/python/transpiler/test_commutative_inverse_cancellation.py index 86ec22e6752a..e4cb3dab3eca 100644 --- a/test/python/transpiler/test_commutative_inverse_cancellation.py +++ b/test/python/transpiler/test_commutative_inverse_cancellation.py @@ -14,14 +14,17 @@ import unittest import numpy as np +from ddt import ddt, data from qiskit.test import QiskitTestCase from qiskit.circuit import Parameter, QuantumCircuit -from qiskit.circuit.library import RZGate +from qiskit.circuit.library import RZGate, UnitaryGate from qiskit.transpiler import PassManager from qiskit.transpiler.passes import CommutativeInverseCancellation +from qiskit.quantum_info import Operator +@ddt class TestCommutativeInverseCancellation(QiskitTestCase): """Test the CommutativeInverseCancellation pass.""" @@ -29,7 +32,8 @@ class TestCommutativeInverseCancellation(QiskitTestCase): # excluding/modifying the tests the combine rotations gates or do # basis priority change. - def test_commutative_circuit1(self): + @data(False, True) + def test_commutative_circuit1(self, upto_phase_optimization): """A simple circuit where three CNOTs commute, the first and the last cancel. 0:----.---------------.-- 0:------------ @@ -44,7 +48,9 @@ def test_commutative_circuit1(self): circuit.cx(2, 1) circuit.cx(0, 1) - passmanager = PassManager(CommutativeInverseCancellation()) + passmanager = PassManager( + CommutativeInverseCancellation(upto_phase_optimization=upto_phase_optimization) + ) new_circuit = passmanager.run(circuit) expected = QuantumCircuit(3) @@ -53,7 +59,8 @@ def test_commutative_circuit1(self): self.assertEqual(expected, new_circuit) - def test_consecutive_cnots(self): + @data(False, True) + def test_consecutive_cnots(self, upto_phase_optimization): """A simple circuit equals identity 0:----.- ----.-- 0:------------ @@ -65,14 +72,17 @@ def test_consecutive_cnots(self): circuit.cx(0, 1) circuit.cx(0, 1) - passmanager = PassManager(CommutativeInverseCancellation()) + passmanager = PassManager( + CommutativeInverseCancellation(upto_phase_optimization=upto_phase_optimization) + ) new_circuit = passmanager.run(circuit) expected = QuantumCircuit(2) self.assertEqual(expected, new_circuit) - def test_consecutive_cnots2(self): + @data(False, True) + def test_consecutive_cnots2(self, upto_phase_optimization): """ Both CNOTs and rotations should cancel out. """ @@ -82,13 +92,16 @@ def test_consecutive_cnots2(self): circuit.cx(0, 1) circuit.rx(-np.pi / 2, 0) - passmanager = PassManager(CommutativeInverseCancellation()) + passmanager = PassManager( + CommutativeInverseCancellation(upto_phase_optimization=upto_phase_optimization) + ) new_circuit = passmanager.run(circuit) expected = QuantumCircuit(2) self.assertEqual(expected, new_circuit) - def test_2_alternating_cnots(self): + @data(False, True) + def test_2_alternating_cnots(self, upto_phase_optimization): """A simple circuit where nothing should be cancelled. 0:----.- ---(+)- 0:----.----(+)- @@ -101,7 +114,9 @@ def test_2_alternating_cnots(self): circuit.cx(0, 1) circuit.cx(1, 0) - passmanager = PassManager(CommutativeInverseCancellation()) + passmanager = PassManager( + CommutativeInverseCancellation(upto_phase_optimization=upto_phase_optimization) + ) new_circuit = passmanager.run(circuit) expected = QuantumCircuit(2) @@ -110,7 +125,8 @@ def test_2_alternating_cnots(self): self.assertEqual(expected, new_circuit) - def test_control_bit_of_cnot(self): + @data(False, True) + def test_control_bit_of_cnot(self, upto_phase_optimization): """A simple circuit where nothing should be cancelled. 0:----.------[X]------.-- 0:----.------[X]------.-- @@ -123,7 +139,9 @@ def test_control_bit_of_cnot(self): circuit.x(0) circuit.cx(0, 1) - passmanager = PassManager(CommutativeInverseCancellation()) + passmanager = PassManager( + CommutativeInverseCancellation(upto_phase_optimization=upto_phase_optimization) + ) new_circuit = passmanager.run(circuit) expected = QuantumCircuit(2) @@ -133,7 +151,8 @@ def test_control_bit_of_cnot(self): self.assertEqual(expected, new_circuit) - def test_control_bit_of_cnot1(self): + @data(False, True) + def test_control_bit_of_cnot1(self, upto_phase_optimization): """A simple circuit where the two cnots should be cancelled. 0:----.------[Z]------.-- 0:---[Z]--- @@ -146,7 +165,9 @@ def test_control_bit_of_cnot1(self): circuit.z(0) circuit.cx(0, 1) - passmanager = PassManager(CommutativeInverseCancellation()) + passmanager = PassManager( + CommutativeInverseCancellation(upto_phase_optimization=upto_phase_optimization) + ) new_circuit = passmanager.run(circuit) expected = QuantumCircuit(2) @@ -154,7 +175,8 @@ def test_control_bit_of_cnot1(self): self.assertEqual(expected, new_circuit) - def test_control_bit_of_cnot2(self): + @data(False, True) + def test_control_bit_of_cnot2(self, upto_phase_optimization): """A simple circuit where the two cnots should be cancelled. 0:----.------[T]------.-- 0:---[T]--- @@ -167,7 +189,9 @@ def test_control_bit_of_cnot2(self): circuit.t(0) circuit.cx(0, 1) - passmanager = PassManager(CommutativeInverseCancellation()) + passmanager = PassManager( + CommutativeInverseCancellation(upto_phase_optimization=upto_phase_optimization) + ) new_circuit = passmanager.run(circuit) expected = QuantumCircuit(2) @@ -175,7 +199,8 @@ def test_control_bit_of_cnot2(self): self.assertEqual(expected, new_circuit) - def test_control_bit_of_cnot3(self): + @data(False, True) + def test_control_bit_of_cnot3(self, upto_phase_optimization): """A simple circuit where the two cnots should be cancelled. 0:----.------[Rz]------.-- 0:---[Rz]--- @@ -188,7 +213,9 @@ def test_control_bit_of_cnot3(self): circuit.rz(np.pi / 3, 0) circuit.cx(0, 1) - passmanager = PassManager(CommutativeInverseCancellation()) + passmanager = PassManager( + CommutativeInverseCancellation(upto_phase_optimization=upto_phase_optimization) + ) new_circuit = passmanager.run(circuit) expected = QuantumCircuit(2) @@ -196,7 +223,8 @@ def test_control_bit_of_cnot3(self): self.assertEqual(expected, new_circuit) - def test_control_bit_of_cnot4(self): + @data(False, True) + def test_control_bit_of_cnot4(self, upto_phase_optimization): """A simple circuit where the two cnots should be cancelled. 0:----.------[T]------.-- 0:---[T]--- @@ -209,7 +237,9 @@ def test_control_bit_of_cnot4(self): circuit.t(0) circuit.cx(0, 1) - passmanager = PassManager(CommutativeInverseCancellation()) + passmanager = PassManager( + CommutativeInverseCancellation(upto_phase_optimization=upto_phase_optimization) + ) new_circuit = passmanager.run(circuit) expected = QuantumCircuit(2) @@ -217,7 +247,8 @@ def test_control_bit_of_cnot4(self): self.assertEqual(expected, new_circuit) - def test_target_bit_of_cnot(self): + @data(False, True) + def test_target_bit_of_cnot(self, upto_phase_optimization): """A simple circuit where nothing should be cancelled. 0:----.---------------.-- 0:----.---------------.-- @@ -230,7 +261,9 @@ def test_target_bit_of_cnot(self): circuit.z(1) circuit.cx(0, 1) - passmanager = PassManager(CommutativeInverseCancellation()) + passmanager = PassManager( + CommutativeInverseCancellation(upto_phase_optimization=upto_phase_optimization) + ) new_circuit = passmanager.run(circuit) expected = QuantumCircuit(2) @@ -240,7 +273,8 @@ def test_target_bit_of_cnot(self): self.assertEqual(expected, new_circuit) - def test_target_bit_of_cnot1(self): + @data(False, True) + def test_target_bit_of_cnot1(self, upto_phase_optimization): """A simple circuit where nothing should be cancelled. 0:----.---------------.-- 0:----.---------------.-- @@ -253,7 +287,9 @@ def test_target_bit_of_cnot1(self): circuit.t(1) circuit.cx(0, 1) - passmanager = PassManager(CommutativeInverseCancellation()) + passmanager = PassManager( + CommutativeInverseCancellation(upto_phase_optimization=upto_phase_optimization) + ) new_circuit = passmanager.run(circuit) expected = QuantumCircuit(2) @@ -263,7 +299,8 @@ def test_target_bit_of_cnot1(self): self.assertEqual(expected, new_circuit) - def test_target_bit_of_cnot2(self): + @data(False, True) + def test_target_bit_of_cnot2(self, upto_phase_optimization): """A simple circuit where nothing should be cancelled. 0:----.---------------.-- 0:----.---------------.-- @@ -276,7 +313,9 @@ def test_target_bit_of_cnot2(self): circuit.rz(np.pi / 3, 1) circuit.cx(0, 1) - passmanager = PassManager(CommutativeInverseCancellation()) + passmanager = PassManager( + CommutativeInverseCancellation(upto_phase_optimization=upto_phase_optimization) + ) new_circuit = passmanager.run(circuit) expected = QuantumCircuit(2) @@ -286,7 +325,8 @@ def test_target_bit_of_cnot2(self): self.assertEqual(expected, new_circuit) - def test_commutative_circuit2(self): + @data(False, True) + def test_commutative_circuit2(self, upto_phase_optimization): """ A simple circuit where three CNOTs commute, the first and the last cancel, also two X gates cancel. @@ -303,7 +343,9 @@ def test_commutative_circuit2(self): circuit.cx(0, 1) circuit.x(1) - passmanager = PassManager(CommutativeInverseCancellation()) + passmanager = PassManager( + CommutativeInverseCancellation(upto_phase_optimization=upto_phase_optimization) + ) new_circuit = passmanager.run(circuit) expected = QuantumCircuit(3) @@ -315,7 +357,8 @@ def test_commutative_circuit2(self): self.assertEqual(expected, new_circuit) - def test_commutative_circuit3(self): + @data(False, True) + def test_commutative_circuit3(self, upto_phase_optimization): """ A simple circuit where three CNOTs commute, the first and the last cancel, also two X gates cancel and two RX gates cancel. @@ -337,7 +380,9 @@ def test_commutative_circuit3(self): circuit.cx(0, 1) circuit.x(1) - passmanager = PassManager(CommutativeInverseCancellation()) + passmanager = PassManager( + CommutativeInverseCancellation(upto_phase_optimization=upto_phase_optimization) + ) new_circuit = passmanager.run(circuit) expected = QuantumCircuit(4) @@ -345,7 +390,8 @@ def test_commutative_circuit3(self): self.assertEqual(expected, new_circuit) - def test_cnot_cascade(self): + @data(False, True) + def test_cnot_cascade(self, upto_phase_optimization): """ A cascade of CNOTs that equals identity. """ @@ -371,14 +417,17 @@ def test_cnot_cascade(self): circuit.cx(1, 2) circuit.cx(0, 1) - passmanager = PassManager(CommutativeInverseCancellation()) + passmanager = PassManager( + CommutativeInverseCancellation(upto_phase_optimization=upto_phase_optimization) + ) new_circuit = passmanager.run(circuit) expected = QuantumCircuit(10) self.assertEqual(expected, new_circuit) - def test_conditional_gates_dont_commute(self): + @data(False, True) + def test_conditional_gates_dont_commute(self, upto_phase_optimization): """Conditional gates do not commute and do not cancel""" # ┌───┐┌─┐ @@ -398,7 +447,9 @@ def test_conditional_gates_dont_commute(self): circuit.cx(1, 2).c_if(circuit.cregs[0], 0) circuit.measure([1, 2], [0, 1]) - passmanager = PassManager(CommutativeInverseCancellation()) + passmanager = PassManager( + CommutativeInverseCancellation(upto_phase_optimization=upto_phase_optimization) + ) new_circuit = passmanager.run(circuit) self.assertEqual(circuit, new_circuit) @@ -406,70 +457,86 @@ def test_conditional_gates_dont_commute(self): # The second suite of tests is adapted from InverseCancellation, # modifying tests where more nonconsecutive gates cancel. - def test_basic_self_inverse(self): + @data(False, True) + def test_basic_self_inverse(self, upto_phase_optimization): """Test that a single self-inverse gate as input can be cancelled.""" circuit = QuantumCircuit(2, 2) circuit.h(0) circuit.h(0) - passmanager = PassManager(CommutativeInverseCancellation()) + passmanager = PassManager( + CommutativeInverseCancellation(upto_phase_optimization=upto_phase_optimization) + ) new_circuit = passmanager.run(circuit) gates_after = new_circuit.count_ops() self.assertNotIn("h", gates_after) - def test_odd_number_self_inverse(self): + @data(False, True) + def test_odd_number_self_inverse(self, upto_phase_optimization): """Test that an odd number of self-inverse gates leaves one gate remaining.""" circuit = QuantumCircuit(2, 2) circuit.h(0) circuit.h(0) circuit.h(0) - passmanager = PassManager(CommutativeInverseCancellation()) + passmanager = PassManager( + CommutativeInverseCancellation(upto_phase_optimization=upto_phase_optimization) + ) new_circuit = passmanager.run(circuit) gates_after = new_circuit.count_ops() self.assertIn("h", gates_after) self.assertEqual(gates_after["h"], 1) - def test_basic_cx_self_inverse(self): + @data(False, True) + def test_basic_cx_self_inverse(self, upto_phase_optimization): """Test that a single self-inverse cx gate as input can be cancelled.""" circuit = QuantumCircuit(2, 2) circuit.cx(0, 1) circuit.cx(0, 1) - passmanager = PassManager(CommutativeInverseCancellation()) + passmanager = PassManager( + CommutativeInverseCancellation(upto_phase_optimization=upto_phase_optimization) + ) new_circuit = passmanager.run(circuit) gates_after = new_circuit.count_ops() self.assertNotIn("cx", gates_after) - def test_basic_gate_inverse(self): + @data(False, True) + def test_basic_gate_inverse(self, upto_phase_optimization): """Test that a basic pair of gate inverse can be cancelled.""" circuit = QuantumCircuit(2, 2) circuit.rx(np.pi / 4, 0) circuit.rx(-np.pi / 4, 0) - passmanager = PassManager(CommutativeInverseCancellation()) + passmanager = PassManager( + CommutativeInverseCancellation(upto_phase_optimization=upto_phase_optimization) + ) new_circuit = passmanager.run(circuit) gates_after = new_circuit.count_ops() self.assertNotIn("rx", gates_after) - def test_non_inverse_do_not_cancel(self): + @data(False, True) + def test_non_inverse_do_not_cancel(self, upto_phase_optimization): """Test that non-inverse gate pairs do not cancel.""" circuit = QuantumCircuit(2, 2) circuit.rx(np.pi / 4, 0) circuit.rx(np.pi / 4, 0) - passmanager = PassManager(CommutativeInverseCancellation()) + passmanager = PassManager( + CommutativeInverseCancellation(upto_phase_optimization=upto_phase_optimization) + ) new_circuit = passmanager.run(circuit) gates_after = new_circuit.count_ops() self.assertIn("rx", gates_after) self.assertEqual(gates_after["rx"], 2) - def test_non_consecutive_gates(self): + @data(False, True) + def test_non_consecutive_gates(self, upto_phase_optimization): """Test that non-consecutive gates cancel as well.""" circuit = QuantumCircuit(2, 2) circuit.h(0) @@ -479,26 +546,32 @@ def test_non_consecutive_gates(self): circuit.cx(0, 1) circuit.h(0) - passmanager = PassManager(CommutativeInverseCancellation()) + passmanager = PassManager( + CommutativeInverseCancellation(upto_phase_optimization=upto_phase_optimization) + ) new_circuit = passmanager.run(circuit) gates_after = new_circuit.count_ops() self.assertNotIn("cx", gates_after) self.assertNotIn("h", gates_after) - def test_gate_inverse_phase_gate(self): + @data(False, True) + def test_gate_inverse_phase_gate(self, upto_phase_optimization): """Test that an inverse pair of a PhaseGate can be cancelled.""" circuit = QuantumCircuit(2, 2) circuit.p(np.pi / 4, 0) circuit.p(-np.pi / 4, 0) - passmanager = PassManager(CommutativeInverseCancellation()) + passmanager = PassManager( + CommutativeInverseCancellation(upto_phase_optimization=upto_phase_optimization) + ) new_circuit = passmanager.run(circuit) gates_after = new_circuit.count_ops() self.assertNotIn("p", gates_after) - def test_self_inverse_on_different_qubits(self): + @data(False, True) + def test_self_inverse_on_different_qubits(self, upto_phase_optimization): """Test that self_inverse gates cancel on the correct qubits.""" circuit = QuantumCircuit(2, 2) circuit.h(0) @@ -506,13 +579,16 @@ def test_self_inverse_on_different_qubits(self): circuit.h(0) circuit.h(1) - passmanager = PassManager(CommutativeInverseCancellation()) + passmanager = PassManager( + CommutativeInverseCancellation(upto_phase_optimization=upto_phase_optimization) + ) new_circuit = passmanager.run(circuit) gates_after = new_circuit.count_ops() self.assertNotIn("h", gates_after) - def test_consecutive_self_inverse_h_x_gate(self): + @data(False, True) + def test_consecutive_self_inverse_h_x_gate(self, upto_phase_optimization): """Test that consecutive self-inverse gates cancel.""" circuit = QuantumCircuit(2, 2) circuit.h(0) @@ -522,27 +598,33 @@ def test_consecutive_self_inverse_h_x_gate(self): circuit.x(0) circuit.h(0) - passmanager = PassManager(CommutativeInverseCancellation()) + passmanager = PassManager( + CommutativeInverseCancellation(upto_phase_optimization=upto_phase_optimization) + ) new_circuit = passmanager.run(circuit) gates_after = new_circuit.count_ops() self.assertNotIn("x", gates_after) self.assertNotIn("h", gates_after) - def test_inverse_with_different_names(self): + @data(False, True) + def test_inverse_with_different_names(self, upto_phase_optimization): """Test that inverse gates that have different names.""" circuit = QuantumCircuit(2, 2) circuit.t(0) circuit.tdg(0) - passmanager = PassManager(CommutativeInverseCancellation()) + passmanager = PassManager( + CommutativeInverseCancellation(upto_phase_optimization=upto_phase_optimization) + ) new_circuit = passmanager.run(circuit) gates_after = new_circuit.count_ops() self.assertNotIn("t", gates_after) self.assertNotIn("tdg", gates_after) - def test_three_alternating_inverse_gates(self): + @data(False, True) + def test_three_alternating_inverse_gates(self, upto_phase_optimization): """Test that inverse cancellation works correctly for alternating sequences of inverse gates of odd-length.""" circuit = QuantumCircuit(2, 2) @@ -550,14 +632,17 @@ def test_three_alternating_inverse_gates(self): circuit.p(-np.pi / 4, 0) circuit.p(np.pi / 4, 0) - passmanager = PassManager(CommutativeInverseCancellation()) + passmanager = PassManager( + CommutativeInverseCancellation(upto_phase_optimization=upto_phase_optimization) + ) new_circuit = passmanager.run(circuit) gates_after = new_circuit.count_ops() self.assertIn("p", gates_after) self.assertEqual(gates_after["p"], 1) - def test_four_alternating_inverse_gates(self): + @data(False, True) + def test_four_alternating_inverse_gates(self, upto_phase_optimization): """Test that inverse cancellation works correctly for alternating sequences of inverse gates of even-length.""" circuit = QuantumCircuit(2, 2) @@ -566,13 +651,16 @@ def test_four_alternating_inverse_gates(self): circuit.p(np.pi / 4, 0) circuit.p(-np.pi / 4, 0) - passmanager = PassManager(CommutativeInverseCancellation()) + passmanager = PassManager( + CommutativeInverseCancellation(upto_phase_optimization=upto_phase_optimization) + ) new_circuit = passmanager.run(circuit) gates_after = new_circuit.count_ops() self.assertNotIn("p", gates_after) - def test_five_alternating_inverse_gates(self): + @data(False, True) + def test_five_alternating_inverse_gates(self, upto_phase_optimization): """Test that inverse cancellation works correctly for alternating sequences of inverse gates of odd-length.""" circuit = QuantumCircuit(2, 2) @@ -582,14 +670,17 @@ def test_five_alternating_inverse_gates(self): circuit.p(-np.pi / 4, 0) circuit.p(np.pi / 4, 0) - passmanager = PassManager(CommutativeInverseCancellation()) + passmanager = PassManager( + CommutativeInverseCancellation(upto_phase_optimization=upto_phase_optimization) + ) new_circuit = passmanager.run(circuit) gates_after = new_circuit.count_ops() self.assertIn("p", gates_after) self.assertEqual(gates_after["p"], 1) - def test_sequence_of_inverse_gates_1(self): + @data(False, True) + def test_sequence_of_inverse_gates_1(self, upto_phase_optimization): """Test that inverse cancellation works correctly for more general sequences of inverse gates. In this test two pairs of inverse gates are supposed to cancel out.""" @@ -600,14 +691,17 @@ def test_sequence_of_inverse_gates_1(self): circuit.p(np.pi / 4, 0) circuit.p(np.pi / 4, 0) - passmanager = PassManager(CommutativeInverseCancellation()) + passmanager = PassManager( + CommutativeInverseCancellation(upto_phase_optimization=upto_phase_optimization) + ) new_circuit = passmanager.run(circuit) gates_after = new_circuit.count_ops() self.assertIn("p", gates_after) self.assertEqual(gates_after["p"], 1) - def test_sequence_of_inverse_gates_2(self): + @data(False, True) + def test_sequence_of_inverse_gates_2(self, upto_phase_optimization): """Test that inverse cancellation works correctly for more general sequences of inverse gates. In this test, in theory three pairs of inverse gates can cancel out, but in practice only two pairs are back-to-back.""" @@ -620,21 +714,26 @@ def test_sequence_of_inverse_gates_2(self): circuit.p(np.pi / 4, 0) circuit.p(np.pi / 4, 0) - passmanager = PassManager(CommutativeInverseCancellation()) + passmanager = PassManager( + CommutativeInverseCancellation(upto_phase_optimization=upto_phase_optimization) + ) new_circuit = passmanager.run(circuit) gates_after = new_circuit.count_ops() self.assertIn("p", gates_after) self.assertEqual(gates_after["p"] % 2, 1) - def test_cx_do_not_wrongly_cancel(self): + @data(False, True) + def test_cx_do_not_wrongly_cancel(self, upto_phase_optimization): """Test that CX(0,1) and CX(1, 0) do not cancel out, when (CX, CX) is passed as an inverse pair.""" circuit = QuantumCircuit(2, 0) circuit.cx(0, 1) circuit.cx(1, 0) - passmanager = PassManager(CommutativeInverseCancellation()) + passmanager = PassManager( + CommutativeInverseCancellation(upto_phase_optimization=upto_phase_optimization) + ) new_circuit = passmanager.run(circuit) gates_after = new_circuit.count_ops() @@ -643,7 +742,8 @@ def test_cx_do_not_wrongly_cancel(self): # A few more tests from issue 8020 - def test_cancel_both_x_and_z(self): + @data(False, True) + def test_cancel_both_x_and_z(self, upto_phase_optimization): """Test that Z commutes with control qubit of CX, and X commutes with the target qubit.""" circuit = QuantumCircuit(2) circuit.z(0) @@ -652,7 +752,9 @@ def test_cancel_both_x_and_z(self): circuit.z(0) circuit.x(1) - passmanager = PassManager(CommutativeInverseCancellation()) + passmanager = PassManager( + CommutativeInverseCancellation(upto_phase_optimization=upto_phase_optimization) + ) new_circuit = passmanager.run(circuit) expected = QuantumCircuit(2) @@ -660,7 +762,8 @@ def test_cancel_both_x_and_z(self): self.assertEqual(expected, new_circuit) - def test_gates_do_not_wrongly_cancel(self): + @data(False, True) + def test_gates_do_not_wrongly_cancel(self, upto_phase_optimization): """Test that X gates do not cancel for X-I-H-I-X.""" circuit = QuantumCircuit(1) circuit.x(0) @@ -669,7 +772,9 @@ def test_gates_do_not_wrongly_cancel(self): circuit.id(0) circuit.x(0) - passmanager = PassManager(CommutativeInverseCancellation()) + passmanager = PassManager( + CommutativeInverseCancellation(upto_phase_optimization=upto_phase_optimization) + ) new_circuit = passmanager.run(circuit) expected = QuantumCircuit(1) @@ -681,43 +786,53 @@ def test_gates_do_not_wrongly_cancel(self): # More tests to cover corner-cases: parameterized gates, directives, reset, etc. - def test_no_cancellation_across_barrier(self): + @data(False, True) + def test_no_cancellation_across_barrier(self, upto_phase_optimization): """Test that barrier prevents cancellation.""" circuit = QuantumCircuit(2) circuit.cx(0, 1) circuit.barrier() circuit.cx(0, 1) - passmanager = PassManager(CommutativeInverseCancellation()) + passmanager = PassManager( + CommutativeInverseCancellation(upto_phase_optimization=upto_phase_optimization) + ) new_circuit = passmanager.run(circuit) self.assertEqual(circuit, new_circuit) - def test_no_cancellation_across_measure(self): + @data(False, True) + def test_no_cancellation_across_measure(self, upto_phase_optimization): """Test that barrier prevents cancellation.""" circuit = QuantumCircuit(2, 1) circuit.cx(0, 1) circuit.measure(0, 0) circuit.cx(0, 1) - passmanager = PassManager(CommutativeInverseCancellation()) + passmanager = PassManager( + CommutativeInverseCancellation(upto_phase_optimization=upto_phase_optimization) + ) new_circuit = passmanager.run(circuit) self.assertEqual(circuit, new_circuit) - def test_no_cancellation_across_reset(self): + @data(False, True) + def test_no_cancellation_across_reset(self, upto_phase_optimization): """Test that reset prevents cancellation.""" circuit = QuantumCircuit(2) circuit.cx(0, 1) circuit.reset(0) circuit.cx(0, 1) - passmanager = PassManager(CommutativeInverseCancellation()) + passmanager = PassManager( + CommutativeInverseCancellation(upto_phase_optimization=upto_phase_optimization) + ) new_circuit = passmanager.run(circuit) self.assertEqual(circuit, new_circuit) - def test_no_cancellation_across_parameterized_gates(self): + @data(False, True) + def test_no_cancellation_across_parameterized_gates(self, upto_phase_optimization): """Test that parameterized gates prevent cancellation. This test should be modified when inverse and commutativity checking get improved to handle parameterized gates. @@ -727,11 +842,14 @@ def test_no_cancellation_across_parameterized_gates(self): circuit.rz(Parameter("Theta"), 0) circuit.rz(-np.pi / 2, 0) - passmanager = PassManager(CommutativeInverseCancellation()) + passmanager = PassManager( + CommutativeInverseCancellation(upto_phase_optimization=upto_phase_optimization) + ) new_circuit = passmanager.run(circuit) self.assertEqual(circuit, new_circuit) - def test_parameterized_gates_do_not_cancel(self): + @data(False, True) + def test_parameterized_gates_do_not_cancel(self, upto_phase_optimization): """Test that parameterized gates do not cancel. This test should be modified when inverse and commutativity checking get improved to handle parameterized gates. @@ -742,7 +860,110 @@ def test_parameterized_gates_do_not_cancel(self): circuit.append(gate, [0]) circuit.append(gate.inverse(), [0]) - passmanager = PassManager(CommutativeInverseCancellation()) + passmanager = PassManager( + CommutativeInverseCancellation(upto_phase_optimization=upto_phase_optimization) + ) + new_circuit = passmanager.run(circuit) + self.assertEqual(circuit, new_circuit) + + def test_phase_difference_rz_p(self): + """Test inverse rz and p gates which differ by a phase.""" + circuit = QuantumCircuit(1) + circuit.rz(np.pi / 4, 0) + circuit.p(-np.pi / 4, 0) + + # the gates should not cancel when upto_phase_optimization is False + passmanager = PassManager(CommutativeInverseCancellation(upto_phase_optimization=False)) + new_circuit = passmanager.run(circuit) + self.assertEqual(circuit, new_circuit) + + # the gates should be canceled when upto_phase_optimization is True + passmanager = PassManager(CommutativeInverseCancellation(upto_phase_optimization=True)) + new_circuit = passmanager.run(circuit) + self.assertEqual(new_circuit.size(), 0) + + # but check that the operators are the same (global phase is correct) + self.assertEqual(Operator(circuit), Operator(new_circuit)) + + def test_phase_difference_custom(self): + """Test inverse custom gates that differ by a phase.""" + cx_circuit_with_phase = QuantumCircuit(2) + cx_circuit_with_phase.cx(0, 1) + cx_circuit_with_phase.global_phase = np.pi / 4 + + circuit = QuantumCircuit(2) + circuit.append(cx_circuit_with_phase.to_gate(), [0, 1]) + circuit.cx(0, 1) + + # the gates should not cancel when upto_phase_optimization is False + passmanager = PassManager(CommutativeInverseCancellation(upto_phase_optimization=False)) + new_circuit = passmanager.run(circuit) + self.assertEqual(circuit, new_circuit) + + # the gates should be canceled when upto_phase_optimization is True + passmanager = PassManager(CommutativeInverseCancellation(upto_phase_optimization=True)) + new_circuit = passmanager.run(circuit) + self.assertEqual(new_circuit.size(), 0) + self.assertEqual(new_circuit.global_phase, np.pi / 4) + self.assertEqual(Operator(circuit), Operator(new_circuit)) + + def test_inverse_unitary_gates(self): + """Test inverse unitary gates that differ by a phase.""" + circuit = QuantumCircuit(2) + u1 = UnitaryGate([[1, 0], [0, 1]]) + u2 = UnitaryGate([[-1, 0], [0, -1]]) + circuit.append(u1, [0]) + circuit.append(u2, [0]) + passmanager = PassManager(CommutativeInverseCancellation(upto_phase_optimization=True)) + new_circuit = passmanager.run(circuit) + self.assertEqual(new_circuit.size(), 0) + self.assertEqual(new_circuit.global_phase, np.pi) + self.assertEqual(Operator(circuit), Operator(new_circuit)) + + def test_inverse_custom_gates(self): + """Test inverse custom gates.""" + cx_circuit1 = QuantumCircuit(3) + cx_circuit1.cx(0, 2) + + cx_circuit2 = QuantumCircuit(3) + cx_circuit2.cx(0, 1) + cx_circuit2.cx(1, 2) + cx_circuit2.cx(0, 1) + cx_circuit2.cx(1, 2) + + circuit = QuantumCircuit(4) + circuit.append(cx_circuit1.to_gate(), [0, 1, 2]) + circuit.cx(0, 3) + circuit.append(cx_circuit2.to_gate(), [0, 1, 2]) + + # the two custom gates commute through cx(0, 3) and cancel each other + passmanager = PassManager(CommutativeInverseCancellation(upto_phase_optimization=True)) + new_circuit = passmanager.run(circuit) + expected_circuit = QuantumCircuit(4) + expected_circuit.cx(0, 3) + self.assertEqual(new_circuit, expected_circuit) + + def test_max_qubits(self): + """Test max_qubits argument.""" + cx_circuit1 = QuantumCircuit(3) + cx_circuit1.cx(0, 2) + cx_circuit2 = QuantumCircuit(3) + + cx_circuit2.cx(0, 1) + cx_circuit2.cx(1, 2) + cx_circuit2.cx(0, 1) + cx_circuit2.cx(1, 2) + + circuit = QuantumCircuit(4) + circuit.append(cx_circuit1.to_gate(), [0, 1, 2]) + circuit.cx(0, 3) + circuit.append(cx_circuit2.to_gate(), [0, 1, 2]) + + # the two custom gates commute through cx(0, 3) and cancel each other, but + # we avoid the check by limiting max_qubits + passmanager = PassManager( + CommutativeInverseCancellation(upto_phase_optimization=True, max_qubits=2) + ) new_circuit = passmanager.run(circuit) self.assertEqual(circuit, new_circuit) From d934ff72d174c7a839a769b0472d12b64166516b Mon Sep 17 00:00:00 2001 From: AlexanderIvrii Date: Thu, 18 Jan 2024 16:10:26 +0200 Subject: [PATCH 2/7] using assertAlmostEqual for checking floating-point calculations --- .../transpiler/test_commutative_inverse_cancellation.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/python/transpiler/test_commutative_inverse_cancellation.py b/test/python/transpiler/test_commutative_inverse_cancellation.py index e4cb3dab3eca..d73c0bf3626d 100644 --- a/test/python/transpiler/test_commutative_inverse_cancellation.py +++ b/test/python/transpiler/test_commutative_inverse_cancellation.py @@ -904,7 +904,7 @@ def test_phase_difference_custom(self): passmanager = PassManager(CommutativeInverseCancellation(upto_phase_optimization=True)) new_circuit = passmanager.run(circuit) self.assertEqual(new_circuit.size(), 0) - self.assertEqual(new_circuit.global_phase, np.pi / 4) + self.assertAlmostEqual(new_circuit.global_phase, np.pi / 4) self.assertEqual(Operator(circuit), Operator(new_circuit)) def test_inverse_unitary_gates(self): @@ -917,7 +917,7 @@ def test_inverse_unitary_gates(self): passmanager = PassManager(CommutativeInverseCancellation(upto_phase_optimization=True)) new_circuit = passmanager.run(circuit) self.assertEqual(new_circuit.size(), 0) - self.assertEqual(new_circuit.global_phase, np.pi) + self.assertAlmostEqual(new_circuit.global_phase, np.pi) self.assertEqual(Operator(circuit), Operator(new_circuit)) def test_inverse_custom_gates(self): From 64c934e692e721e3e3819d5be391707452c7a9ba Mon Sep 17 00:00:00 2001 From: AlexanderIvrii Date: Sun, 21 Jan 2024 13:37:59 +0200 Subject: [PATCH 3/7] renaming --- .../commutative_inverse_cancellation.py | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/qiskit/transpiler/passes/optimization/commutative_inverse_cancellation.py b/qiskit/transpiler/passes/optimization/commutative_inverse_cancellation.py index a43007b14acd..788bb4faec73 100644 --- a/qiskit/transpiler/passes/optimization/commutative_inverse_cancellation.py +++ b/qiskit/transpiler/passes/optimization/commutative_inverse_cancellation.py @@ -59,21 +59,22 @@ def _inverse_upto_phase(self, node1, node2): ``op2 = e^{i * d} op1^{-1})`` for some phase difference ``d``. If this is the case, we can replace ``op2 * op1`` by `e^{i * d} I``. The input to this function is a pair of DAG nodes. - The output is a tuple representing the result of the check and the phase difference. + The output is a tuple representing whether the two nodes + are inverse up to a phase and that phase difference. """ phase_difference = 0 if not self._upto_phase_optimization: - result = node1.op.inverse() == node2.op + is_inverse = node1.op.inverse() == node2.op elif len(node2.qargs) > self._max_qubits: - result = False + is_inverse = False else: mat1 = Operator(node1.op.inverse()).data mat2 = Operator(node2.op).data - result = matrix_equal(mat1, mat2, ignore_phase=True) - if result: + is_inverse = matrix_equal(mat1, mat2, ignore_phase=True) + if is_inverse: # mat2 = e^{i * phase_difference} mat1 phase_difference = _get_phase_difference(mat1, mat2) - return result, phase_difference + return is_inverse, phase_difference def run(self, dag: DAGCircuit): """ @@ -109,10 +110,10 @@ def run(self, dag: DAGCircuit): and topo_sorted_nodes[idx2].qargs == topo_sorted_nodes[idx1].qargs and topo_sorted_nodes[idx2].cargs == topo_sorted_nodes[idx1].cargs ): - result, phase = self._inverse_upto_phase( + is_inverse, phase = self._inverse_upto_phase( topo_sorted_nodes[idx1], topo_sorted_nodes[idx2] ) - if result: + if is_inverse: phase_update += phase matched_idx2 = idx2 break From 1257de3f31fbc7776c9515e55cb23a0c7e51dbb3 Mon Sep 17 00:00:00 2001 From: AlexanderIvrii Date: Sun, 21 Jan 2024 14:18:45 +0200 Subject: [PATCH 4/7] improving description --- .../passes/optimization/commutative_inverse_cancellation.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/qiskit/transpiler/passes/optimization/commutative_inverse_cancellation.py b/qiskit/transpiler/passes/optimization/commutative_inverse_cancellation.py index 788bb4faec73..f3461063936c 100644 --- a/qiskit/transpiler/passes/optimization/commutative_inverse_cancellation.py +++ b/qiskit/transpiler/passes/optimization/commutative_inverse_cancellation.py @@ -30,8 +30,9 @@ def __init__(self, max_qubits: int = 4, upto_phase_optimization: bool = False): max_qubits: limits the number of qubits in matrix-based commutativity and inverse checks. upto_phase_optimization: if True, also cancels out pairs of gates that are - inverse up to a phase, keeping track of the phase difference in the global - phase of the circuit. However, the inverse check becomes more expensive. + inverse up to a phase, while keeping track of the phase difference in the + global phase of the circuit. However, this inverse check is now matrix-based + and becomes more expensive. """ self._max_qubits = max_qubits self._upto_phase_optimization = upto_phase_optimization From 7cfc20c2c79e2a7f02e1a4cc40eb63159f8b2934 Mon Sep 17 00:00:00 2001 From: AlexanderIvrii Date: Sun, 21 Jan 2024 14:52:53 +0200 Subject: [PATCH 5/7] simplifying code based on review --- qiskit/quantum_info/operators/predicates.py | 37 +++++++------------ .../commutative_inverse_cancellation.py | 7 ++-- 2 files changed, 18 insertions(+), 26 deletions(-) diff --git a/qiskit/quantum_info/operators/predicates.py b/qiskit/quantum_info/operators/predicates.py index a98eee4e6729..57b7df64f268 100644 --- a/qiskit/quantum_info/operators/predicates.py +++ b/qiskit/quantum_info/operators/predicates.py @@ -21,28 +21,7 @@ RTOL_DEFAULT = 1e-5 -def _get_phase_difference(mat1, mat2, atol=ATOL_DEFAULT): - """Assumes that mat1 and mat2 only differ by a phase - (as in the case when ``matrix_equal(mat1, mat2, ignore_phase=True)`` returns True). - Returns this phase difference. - """ - if not isinstance(mat1, np.ndarray): - mat1 = np.array(mat1) - if not isinstance(mat2, np.ndarray): - mat2 = np.array(mat2) - phase_difference = 0 - for elt in mat1.flat: - if abs(elt) > atol: - phase_difference -= np.angle(elt) - break - for elt in mat2.flat: - if abs(elt) > atol: - phase_difference += np.angle(elt) - break - return phase_difference - - -def matrix_equal(mat1, mat2, ignore_phase=False, rtol=RTOL_DEFAULT, atol=ATOL_DEFAULT): +def matrix_equal(mat1, mat2, ignore_phase=False, rtol=RTOL_DEFAULT, atol=ATOL_DEFAULT, props=None): """Test if two arrays are equal. The final comparison is implemented using Numpy.allclose. See its @@ -59,6 +38,9 @@ def matrix_equal(mat1, mat2, ignore_phase=False, rtol=RTOL_DEFAULT, atol=ATOL_DE matrices [Default: False] rtol (double): the relative tolerance parameter [Default {}]. atol (double): the absolute tolerance parameter [Default {}]. + props (dict | None): if not ``None`` and ``ignore_phase`` is ``True`` + returns the phase difference between the two matrices under + ``props['phase_difference']`` Returns: bool: True if the matrices are equal or False otherwise. @@ -80,16 +62,25 @@ def matrix_equal(mat1, mat2, ignore_phase=False, rtol=RTOL_DEFAULT, atol=ATOL_DE return False if ignore_phase: + phase_difference = 0 + # Get phase of first non-zero entry of mat1 and mat2 # and multiply all entries by the conjugate for elt in mat1.flat: if abs(elt) > atol: - mat1 = np.exp(-1j * np.angle(elt)) * mat1 + angle = np.angle(elt) + phase_difference -= angle + mat1 = np.exp(-1j * angle) * mat1 break for elt in mat2.flat: if abs(elt) > atol: + angle = np.angle(elt) + phase_difference += angle mat2 = np.exp(-1j * np.angle(elt)) * mat2 break + if props is not None: + props["phase_difference"] = phase_difference + return np.allclose(mat1, mat2, rtol=rtol, atol=atol) diff --git a/qiskit/transpiler/passes/optimization/commutative_inverse_cancellation.py b/qiskit/transpiler/passes/optimization/commutative_inverse_cancellation.py index f3461063936c..62c601dd68f1 100644 --- a/qiskit/transpiler/passes/optimization/commutative_inverse_cancellation.py +++ b/qiskit/transpiler/passes/optimization/commutative_inverse_cancellation.py @@ -15,7 +15,7 @@ from qiskit.dagcircuit import DAGCircuit, DAGOpNode from qiskit.quantum_info import Operator -from qiskit.quantum_info.operators.predicates import matrix_equal, _get_phase_difference +from qiskit.quantum_info.operators.predicates import matrix_equal from qiskit.transpiler.basepasses import TransformationPass from qiskit.circuit.commutation_checker import CommutationChecker @@ -71,10 +71,11 @@ def _inverse_upto_phase(self, node1, node2): else: mat1 = Operator(node1.op.inverse()).data mat2 = Operator(node2.op).data - is_inverse = matrix_equal(mat1, mat2, ignore_phase=True) + props = {} + is_inverse = matrix_equal(mat1, mat2, ignore_phase=True, props=props) if is_inverse: # mat2 = e^{i * phase_difference} mat1 - phase_difference = _get_phase_difference(mat1, mat2) + phase_difference = props["phase_difference"] return is_inverse, phase_difference def run(self, dag: DAGCircuit): From 8932b97328d1f15ca3033569cb676606c0e2ef62 Mon Sep 17 00:00:00 2001 From: AlexanderIvrii Date: Thu, 25 Jan 2024 12:35:20 +0200 Subject: [PATCH 6/7] renaming; changing order of arguments; propagating max_qubits to commutativity checker; docs improvements --- .../commutative_inverse_cancellation.py | 22 +- ...ncellation-uptophase-028525d21b199cef.yaml | 15 +- .../test_commutative_inverse_cancellation.py | 258 ++++++------------ 3 files changed, 109 insertions(+), 186 deletions(-) diff --git a/qiskit/transpiler/passes/optimization/commutative_inverse_cancellation.py b/qiskit/transpiler/passes/optimization/commutative_inverse_cancellation.py index 62c601dd68f1..26ce32cc02fa 100644 --- a/qiskit/transpiler/passes/optimization/commutative_inverse_cancellation.py +++ b/qiskit/transpiler/passes/optimization/commutative_inverse_cancellation.py @@ -1,6 +1,6 @@ # This code is part of Qiskit. # -# (C) Copyright IBM 2017, 2021. +# (C) Copyright IBM 2023, 2024. # # This code is licensed under the Apache License, Version 2.0. You may # obtain a copy of this license in the LICENSE.txt file in the root directory @@ -23,19 +23,20 @@ class CommutativeInverseCancellation(TransformationPass): """Cancel pairs of inverse gates exploiting commutation relations.""" - def __init__(self, max_qubits: int = 4, upto_phase_optimization: bool = False): + def __init__(self, matrix_based: bool = False, max_qubits: int = 4): """Initialize CommutativeInverseCancellation pass. Args: - max_qubits: limits the number of qubits in matrix-based commutativity and inverse - checks. - upto_phase_optimization: if True, also cancels out pairs of gates that are - inverse up to a phase, while keeping track of the phase difference in the - global phase of the circuit. However, this inverse check is now matrix-based - and becomes more expensive. + matrix_based: if True, uses matrix representations to check whether two + operations are inverse of each other. This makes the checks more powerful, + and in addition allows canceling pairs of operations that are inverse up to a + phase, while updating the global phase of the circuit accordingly. + Generally this leads to more reductions at the expense of increased runtime. + max_qubits: limits the number of qubits in matrix-based commutativity and + inverse checks. """ + self._matrix_based = matrix_based self._max_qubits = max_qubits - self._upto_phase_optimization = upto_phase_optimization super().__init__() def _skip_node(self, node): @@ -64,7 +65,7 @@ def _inverse_upto_phase(self, node1, node2): are inverse up to a phase and that phase difference. """ phase_difference = 0 - if not self._upto_phase_optimization: + if not self._matrix_based: is_inverse = node1.op.inverse() == node2.op elif len(node2.qargs) > self._max_qubits: is_inverse = False @@ -127,6 +128,7 @@ def run(self, dag: DAGCircuit): topo_sorted_nodes[idx2].op, topo_sorted_nodes[idx2].qargs, topo_sorted_nodes[idx2].cargs, + max_num_qubits=self._max_qubits, ): break diff --git a/releasenotes/notes/commutative-inv-cancellation-uptophase-028525d21b199cef.yaml b/releasenotes/notes/commutative-inv-cancellation-uptophase-028525d21b199cef.yaml index 5936de1dcae4..9b5c6b537951 100644 --- a/releasenotes/notes/commutative-inv-cancellation-uptophase-028525d21b199cef.yaml +++ b/releasenotes/notes/commutative-inv-cancellation-uptophase-028525d21b199cef.yaml @@ -1,14 +1,15 @@ --- features: - | - Added two new arguments ``max_qubits`` and ``upto_phase_optimization`` to the + Added two new arguments ``matrix_based`` and ``max_qubits`` to the constructor of :class:`.CommutativeInverseCancellation` transpiler pass. - When ``upto_phase_optimization`` is ``True`` the pass also cancels pairs of - gates that are inverse up to a phase, and updates the global phase of the circuit + When ``matrix_based`` is ``True`` the pass uses matrix representations to + check whether two operations are inverse of each other. This makes the + checks more powerful, and in addition allows canceling pairs of operations + that are inverse up to a phase, while updating the global phase of the circuit accordingly. Generally this leads to more reductions at the expense of increased - runtime. Internally, the check is based on constructing and comparing unitary matrices - corresponding to various gates in the circuit. The argument ``max_qubits`` limits - the number of qubits in matrix-based commutativity and inverse checks. For example:: + runtime. The argument ``max_qubits`` limits the number of qubits in matrix-based + commutativity and inverse checks. For example:: import numpy as np from qiskit.circuit import QuantumCircuit @@ -19,7 +20,7 @@ features: circuit.rz(np.pi / 4, 0) circuit.p(-np.pi / 4, 0) - passmanager = PassManager(CommutativeInverseCancellation(upto_phase_optimization=True)) + passmanager = PassManager(CommutativeInverseCancellation(matrix_based=True)) new_circuit = passmanager.run(circuit) The pass is able to cancel the ``RZ`` and ``P`` gates, while adjusting the circuit's global diff --git a/test/python/transpiler/test_commutative_inverse_cancellation.py b/test/python/transpiler/test_commutative_inverse_cancellation.py index d73c0bf3626d..220c16595d35 100644 --- a/test/python/transpiler/test_commutative_inverse_cancellation.py +++ b/test/python/transpiler/test_commutative_inverse_cancellation.py @@ -33,7 +33,7 @@ class TestCommutativeInverseCancellation(QiskitTestCase): # basis priority change. @data(False, True) - def test_commutative_circuit1(self, upto_phase_optimization): + def test_commutative_circuit1(self, matrix_based): """A simple circuit where three CNOTs commute, the first and the last cancel. 0:----.---------------.-- 0:------------ @@ -48,9 +48,7 @@ def test_commutative_circuit1(self, upto_phase_optimization): circuit.cx(2, 1) circuit.cx(0, 1) - passmanager = PassManager( - CommutativeInverseCancellation(upto_phase_optimization=upto_phase_optimization) - ) + passmanager = PassManager(CommutativeInverseCancellation(matrix_based=matrix_based)) new_circuit = passmanager.run(circuit) expected = QuantumCircuit(3) @@ -60,7 +58,7 @@ def test_commutative_circuit1(self, upto_phase_optimization): self.assertEqual(expected, new_circuit) @data(False, True) - def test_consecutive_cnots(self, upto_phase_optimization): + def test_consecutive_cnots(self, matrix_based): """A simple circuit equals identity 0:----.- ----.-- 0:------------ @@ -72,9 +70,7 @@ def test_consecutive_cnots(self, upto_phase_optimization): circuit.cx(0, 1) circuit.cx(0, 1) - passmanager = PassManager( - CommutativeInverseCancellation(upto_phase_optimization=upto_phase_optimization) - ) + passmanager = PassManager(CommutativeInverseCancellation(matrix_based=matrix_based)) new_circuit = passmanager.run(circuit) expected = QuantumCircuit(2) @@ -82,7 +78,7 @@ def test_consecutive_cnots(self, upto_phase_optimization): self.assertEqual(expected, new_circuit) @data(False, True) - def test_consecutive_cnots2(self, upto_phase_optimization): + def test_consecutive_cnots2(self, matrix_based): """ Both CNOTs and rotations should cancel out. """ @@ -92,16 +88,14 @@ def test_consecutive_cnots2(self, upto_phase_optimization): circuit.cx(0, 1) circuit.rx(-np.pi / 2, 0) - passmanager = PassManager( - CommutativeInverseCancellation(upto_phase_optimization=upto_phase_optimization) - ) + passmanager = PassManager(CommutativeInverseCancellation(matrix_based=matrix_based)) new_circuit = passmanager.run(circuit) expected = QuantumCircuit(2) self.assertEqual(expected, new_circuit) @data(False, True) - def test_2_alternating_cnots(self, upto_phase_optimization): + def test_2_alternating_cnots(self, matrix_based): """A simple circuit where nothing should be cancelled. 0:----.- ---(+)- 0:----.----(+)- @@ -114,9 +108,7 @@ def test_2_alternating_cnots(self, upto_phase_optimization): circuit.cx(0, 1) circuit.cx(1, 0) - passmanager = PassManager( - CommutativeInverseCancellation(upto_phase_optimization=upto_phase_optimization) - ) + passmanager = PassManager(CommutativeInverseCancellation(matrix_based=matrix_based)) new_circuit = passmanager.run(circuit) expected = QuantumCircuit(2) @@ -126,7 +118,7 @@ def test_2_alternating_cnots(self, upto_phase_optimization): self.assertEqual(expected, new_circuit) @data(False, True) - def test_control_bit_of_cnot(self, upto_phase_optimization): + def test_control_bit_of_cnot(self, matrix_based): """A simple circuit where nothing should be cancelled. 0:----.------[X]------.-- 0:----.------[X]------.-- @@ -139,9 +131,7 @@ def test_control_bit_of_cnot(self, upto_phase_optimization): circuit.x(0) circuit.cx(0, 1) - passmanager = PassManager( - CommutativeInverseCancellation(upto_phase_optimization=upto_phase_optimization) - ) + passmanager = PassManager(CommutativeInverseCancellation(matrix_based=matrix_based)) new_circuit = passmanager.run(circuit) expected = QuantumCircuit(2) @@ -152,7 +142,7 @@ def test_control_bit_of_cnot(self, upto_phase_optimization): self.assertEqual(expected, new_circuit) @data(False, True) - def test_control_bit_of_cnot1(self, upto_phase_optimization): + def test_control_bit_of_cnot1(self, matrix_based): """A simple circuit where the two cnots should be cancelled. 0:----.------[Z]------.-- 0:---[Z]--- @@ -165,9 +155,7 @@ def test_control_bit_of_cnot1(self, upto_phase_optimization): circuit.z(0) circuit.cx(0, 1) - passmanager = PassManager( - CommutativeInverseCancellation(upto_phase_optimization=upto_phase_optimization) - ) + passmanager = PassManager(CommutativeInverseCancellation(matrix_based=matrix_based)) new_circuit = passmanager.run(circuit) expected = QuantumCircuit(2) @@ -176,7 +164,7 @@ def test_control_bit_of_cnot1(self, upto_phase_optimization): self.assertEqual(expected, new_circuit) @data(False, True) - def test_control_bit_of_cnot2(self, upto_phase_optimization): + def test_control_bit_of_cnot2(self, matrix_based): """A simple circuit where the two cnots should be cancelled. 0:----.------[T]------.-- 0:---[T]--- @@ -189,9 +177,7 @@ def test_control_bit_of_cnot2(self, upto_phase_optimization): circuit.t(0) circuit.cx(0, 1) - passmanager = PassManager( - CommutativeInverseCancellation(upto_phase_optimization=upto_phase_optimization) - ) + passmanager = PassManager(CommutativeInverseCancellation(matrix_based=matrix_based)) new_circuit = passmanager.run(circuit) expected = QuantumCircuit(2) @@ -200,7 +186,7 @@ def test_control_bit_of_cnot2(self, upto_phase_optimization): self.assertEqual(expected, new_circuit) @data(False, True) - def test_control_bit_of_cnot3(self, upto_phase_optimization): + def test_control_bit_of_cnot3(self, matrix_based): """A simple circuit where the two cnots should be cancelled. 0:----.------[Rz]------.-- 0:---[Rz]--- @@ -213,9 +199,7 @@ def test_control_bit_of_cnot3(self, upto_phase_optimization): circuit.rz(np.pi / 3, 0) circuit.cx(0, 1) - passmanager = PassManager( - CommutativeInverseCancellation(upto_phase_optimization=upto_phase_optimization) - ) + passmanager = PassManager(CommutativeInverseCancellation(matrix_based=matrix_based)) new_circuit = passmanager.run(circuit) expected = QuantumCircuit(2) @@ -224,7 +208,7 @@ def test_control_bit_of_cnot3(self, upto_phase_optimization): self.assertEqual(expected, new_circuit) @data(False, True) - def test_control_bit_of_cnot4(self, upto_phase_optimization): + def test_control_bit_of_cnot4(self, matrix_based): """A simple circuit where the two cnots should be cancelled. 0:----.------[T]------.-- 0:---[T]--- @@ -237,9 +221,7 @@ def test_control_bit_of_cnot4(self, upto_phase_optimization): circuit.t(0) circuit.cx(0, 1) - passmanager = PassManager( - CommutativeInverseCancellation(upto_phase_optimization=upto_phase_optimization) - ) + passmanager = PassManager(CommutativeInverseCancellation(matrix_based=matrix_based)) new_circuit = passmanager.run(circuit) expected = QuantumCircuit(2) @@ -248,7 +230,7 @@ def test_control_bit_of_cnot4(self, upto_phase_optimization): self.assertEqual(expected, new_circuit) @data(False, True) - def test_target_bit_of_cnot(self, upto_phase_optimization): + def test_target_bit_of_cnot(self, matrix_based): """A simple circuit where nothing should be cancelled. 0:----.---------------.-- 0:----.---------------.-- @@ -261,9 +243,7 @@ def test_target_bit_of_cnot(self, upto_phase_optimization): circuit.z(1) circuit.cx(0, 1) - passmanager = PassManager( - CommutativeInverseCancellation(upto_phase_optimization=upto_phase_optimization) - ) + passmanager = PassManager(CommutativeInverseCancellation(matrix_based=matrix_based)) new_circuit = passmanager.run(circuit) expected = QuantumCircuit(2) @@ -274,7 +254,7 @@ def test_target_bit_of_cnot(self, upto_phase_optimization): self.assertEqual(expected, new_circuit) @data(False, True) - def test_target_bit_of_cnot1(self, upto_phase_optimization): + def test_target_bit_of_cnot1(self, matrix_based): """A simple circuit where nothing should be cancelled. 0:----.---------------.-- 0:----.---------------.-- @@ -287,9 +267,7 @@ def test_target_bit_of_cnot1(self, upto_phase_optimization): circuit.t(1) circuit.cx(0, 1) - passmanager = PassManager( - CommutativeInverseCancellation(upto_phase_optimization=upto_phase_optimization) - ) + passmanager = PassManager(CommutativeInverseCancellation(matrix_based=matrix_based)) new_circuit = passmanager.run(circuit) expected = QuantumCircuit(2) @@ -300,7 +278,7 @@ def test_target_bit_of_cnot1(self, upto_phase_optimization): self.assertEqual(expected, new_circuit) @data(False, True) - def test_target_bit_of_cnot2(self, upto_phase_optimization): + def test_target_bit_of_cnot2(self, matrix_based): """A simple circuit where nothing should be cancelled. 0:----.---------------.-- 0:----.---------------.-- @@ -313,9 +291,7 @@ def test_target_bit_of_cnot2(self, upto_phase_optimization): circuit.rz(np.pi / 3, 1) circuit.cx(0, 1) - passmanager = PassManager( - CommutativeInverseCancellation(upto_phase_optimization=upto_phase_optimization) - ) + passmanager = PassManager(CommutativeInverseCancellation(matrix_based=matrix_based)) new_circuit = passmanager.run(circuit) expected = QuantumCircuit(2) @@ -326,7 +302,7 @@ def test_target_bit_of_cnot2(self, upto_phase_optimization): self.assertEqual(expected, new_circuit) @data(False, True) - def test_commutative_circuit2(self, upto_phase_optimization): + def test_commutative_circuit2(self, matrix_based): """ A simple circuit where three CNOTs commute, the first and the last cancel, also two X gates cancel. @@ -343,9 +319,7 @@ def test_commutative_circuit2(self, upto_phase_optimization): circuit.cx(0, 1) circuit.x(1) - passmanager = PassManager( - CommutativeInverseCancellation(upto_phase_optimization=upto_phase_optimization) - ) + passmanager = PassManager(CommutativeInverseCancellation(matrix_based=matrix_based)) new_circuit = passmanager.run(circuit) expected = QuantumCircuit(3) @@ -358,7 +332,7 @@ def test_commutative_circuit2(self, upto_phase_optimization): self.assertEqual(expected, new_circuit) @data(False, True) - def test_commutative_circuit3(self, upto_phase_optimization): + def test_commutative_circuit3(self, matrix_based): """ A simple circuit where three CNOTs commute, the first and the last cancel, also two X gates cancel and two RX gates cancel. @@ -380,9 +354,7 @@ def test_commutative_circuit3(self, upto_phase_optimization): circuit.cx(0, 1) circuit.x(1) - passmanager = PassManager( - CommutativeInverseCancellation(upto_phase_optimization=upto_phase_optimization) - ) + passmanager = PassManager(CommutativeInverseCancellation(matrix_based=matrix_based)) new_circuit = passmanager.run(circuit) expected = QuantumCircuit(4) @@ -391,7 +363,7 @@ def test_commutative_circuit3(self, upto_phase_optimization): self.assertEqual(expected, new_circuit) @data(False, True) - def test_cnot_cascade(self, upto_phase_optimization): + def test_cnot_cascade(self, matrix_based): """ A cascade of CNOTs that equals identity. """ @@ -417,9 +389,7 @@ def test_cnot_cascade(self, upto_phase_optimization): circuit.cx(1, 2) circuit.cx(0, 1) - passmanager = PassManager( - CommutativeInverseCancellation(upto_phase_optimization=upto_phase_optimization) - ) + passmanager = PassManager(CommutativeInverseCancellation(matrix_based=matrix_based)) new_circuit = passmanager.run(circuit) expected = QuantumCircuit(10) @@ -427,7 +397,7 @@ def test_cnot_cascade(self, upto_phase_optimization): self.assertEqual(expected, new_circuit) @data(False, True) - def test_conditional_gates_dont_commute(self, upto_phase_optimization): + def test_conditional_gates_dont_commute(self, matrix_based): """Conditional gates do not commute and do not cancel""" # ┌───┐┌─┐ @@ -447,9 +417,7 @@ def test_conditional_gates_dont_commute(self, upto_phase_optimization): circuit.cx(1, 2).c_if(circuit.cregs[0], 0) circuit.measure([1, 2], [0, 1]) - passmanager = PassManager( - CommutativeInverseCancellation(upto_phase_optimization=upto_phase_optimization) - ) + passmanager = PassManager(CommutativeInverseCancellation(matrix_based=matrix_based)) new_circuit = passmanager.run(circuit) self.assertEqual(circuit, new_circuit) @@ -458,31 +426,27 @@ def test_conditional_gates_dont_commute(self, upto_phase_optimization): # modifying tests where more nonconsecutive gates cancel. @data(False, True) - def test_basic_self_inverse(self, upto_phase_optimization): + def test_basic_self_inverse(self, matrix_based): """Test that a single self-inverse gate as input can be cancelled.""" circuit = QuantumCircuit(2, 2) circuit.h(0) circuit.h(0) - passmanager = PassManager( - CommutativeInverseCancellation(upto_phase_optimization=upto_phase_optimization) - ) + passmanager = PassManager(CommutativeInverseCancellation(matrix_based=matrix_based)) new_circuit = passmanager.run(circuit) gates_after = new_circuit.count_ops() self.assertNotIn("h", gates_after) @data(False, True) - def test_odd_number_self_inverse(self, upto_phase_optimization): + def test_odd_number_self_inverse(self, matrix_based): """Test that an odd number of self-inverse gates leaves one gate remaining.""" circuit = QuantumCircuit(2, 2) circuit.h(0) circuit.h(0) circuit.h(0) - passmanager = PassManager( - CommutativeInverseCancellation(upto_phase_optimization=upto_phase_optimization) - ) + passmanager = PassManager(CommutativeInverseCancellation(matrix_based=matrix_based)) new_circuit = passmanager.run(circuit) gates_after = new_circuit.count_ops() @@ -490,45 +454,39 @@ def test_odd_number_self_inverse(self, upto_phase_optimization): self.assertEqual(gates_after["h"], 1) @data(False, True) - def test_basic_cx_self_inverse(self, upto_phase_optimization): + def test_basic_cx_self_inverse(self, matrix_based): """Test that a single self-inverse cx gate as input can be cancelled.""" circuit = QuantumCircuit(2, 2) circuit.cx(0, 1) circuit.cx(0, 1) - passmanager = PassManager( - CommutativeInverseCancellation(upto_phase_optimization=upto_phase_optimization) - ) + passmanager = PassManager(CommutativeInverseCancellation(matrix_based=matrix_based)) new_circuit = passmanager.run(circuit) gates_after = new_circuit.count_ops() self.assertNotIn("cx", gates_after) @data(False, True) - def test_basic_gate_inverse(self, upto_phase_optimization): + def test_basic_gate_inverse(self, matrix_based): """Test that a basic pair of gate inverse can be cancelled.""" circuit = QuantumCircuit(2, 2) circuit.rx(np.pi / 4, 0) circuit.rx(-np.pi / 4, 0) - passmanager = PassManager( - CommutativeInverseCancellation(upto_phase_optimization=upto_phase_optimization) - ) + passmanager = PassManager(CommutativeInverseCancellation(matrix_based=matrix_based)) new_circuit = passmanager.run(circuit) gates_after = new_circuit.count_ops() self.assertNotIn("rx", gates_after) @data(False, True) - def test_non_inverse_do_not_cancel(self, upto_phase_optimization): + def test_non_inverse_do_not_cancel(self, matrix_based): """Test that non-inverse gate pairs do not cancel.""" circuit = QuantumCircuit(2, 2) circuit.rx(np.pi / 4, 0) circuit.rx(np.pi / 4, 0) - passmanager = PassManager( - CommutativeInverseCancellation(upto_phase_optimization=upto_phase_optimization) - ) + passmanager = PassManager(CommutativeInverseCancellation(matrix_based=matrix_based)) new_circuit = passmanager.run(circuit) gates_after = new_circuit.count_ops() @@ -536,7 +494,7 @@ def test_non_inverse_do_not_cancel(self, upto_phase_optimization): self.assertEqual(gates_after["rx"], 2) @data(False, True) - def test_non_consecutive_gates(self, upto_phase_optimization): + def test_non_consecutive_gates(self, matrix_based): """Test that non-consecutive gates cancel as well.""" circuit = QuantumCircuit(2, 2) circuit.h(0) @@ -546,9 +504,7 @@ def test_non_consecutive_gates(self, upto_phase_optimization): circuit.cx(0, 1) circuit.h(0) - passmanager = PassManager( - CommutativeInverseCancellation(upto_phase_optimization=upto_phase_optimization) - ) + passmanager = PassManager(CommutativeInverseCancellation(matrix_based=matrix_based)) new_circuit = passmanager.run(circuit) gates_after = new_circuit.count_ops() @@ -556,22 +512,20 @@ def test_non_consecutive_gates(self, upto_phase_optimization): self.assertNotIn("h", gates_after) @data(False, True) - def test_gate_inverse_phase_gate(self, upto_phase_optimization): + def test_gate_inverse_phase_gate(self, matrix_based): """Test that an inverse pair of a PhaseGate can be cancelled.""" circuit = QuantumCircuit(2, 2) circuit.p(np.pi / 4, 0) circuit.p(-np.pi / 4, 0) - passmanager = PassManager( - CommutativeInverseCancellation(upto_phase_optimization=upto_phase_optimization) - ) + passmanager = PassManager(CommutativeInverseCancellation(matrix_based=matrix_based)) new_circuit = passmanager.run(circuit) gates_after = new_circuit.count_ops() self.assertNotIn("p", gates_after) @data(False, True) - def test_self_inverse_on_different_qubits(self, upto_phase_optimization): + def test_self_inverse_on_different_qubits(self, matrix_based): """Test that self_inverse gates cancel on the correct qubits.""" circuit = QuantumCircuit(2, 2) circuit.h(0) @@ -579,16 +533,14 @@ def test_self_inverse_on_different_qubits(self, upto_phase_optimization): circuit.h(0) circuit.h(1) - passmanager = PassManager( - CommutativeInverseCancellation(upto_phase_optimization=upto_phase_optimization) - ) + passmanager = PassManager(CommutativeInverseCancellation(matrix_based=matrix_based)) new_circuit = passmanager.run(circuit) gates_after = new_circuit.count_ops() self.assertNotIn("h", gates_after) @data(False, True) - def test_consecutive_self_inverse_h_x_gate(self, upto_phase_optimization): + def test_consecutive_self_inverse_h_x_gate(self, matrix_based): """Test that consecutive self-inverse gates cancel.""" circuit = QuantumCircuit(2, 2) circuit.h(0) @@ -598,9 +550,7 @@ def test_consecutive_self_inverse_h_x_gate(self, upto_phase_optimization): circuit.x(0) circuit.h(0) - passmanager = PassManager( - CommutativeInverseCancellation(upto_phase_optimization=upto_phase_optimization) - ) + passmanager = PassManager(CommutativeInverseCancellation(matrix_based=matrix_based)) new_circuit = passmanager.run(circuit) gates_after = new_circuit.count_ops() @@ -608,15 +558,13 @@ def test_consecutive_self_inverse_h_x_gate(self, upto_phase_optimization): self.assertNotIn("h", gates_after) @data(False, True) - def test_inverse_with_different_names(self, upto_phase_optimization): + def test_inverse_with_different_names(self, matrix_based): """Test that inverse gates that have different names.""" circuit = QuantumCircuit(2, 2) circuit.t(0) circuit.tdg(0) - passmanager = PassManager( - CommutativeInverseCancellation(upto_phase_optimization=upto_phase_optimization) - ) + passmanager = PassManager(CommutativeInverseCancellation(matrix_based=matrix_based)) new_circuit = passmanager.run(circuit) gates_after = new_circuit.count_ops() @@ -624,7 +572,7 @@ def test_inverse_with_different_names(self, upto_phase_optimization): self.assertNotIn("tdg", gates_after) @data(False, True) - def test_three_alternating_inverse_gates(self, upto_phase_optimization): + def test_three_alternating_inverse_gates(self, matrix_based): """Test that inverse cancellation works correctly for alternating sequences of inverse gates of odd-length.""" circuit = QuantumCircuit(2, 2) @@ -632,9 +580,7 @@ def test_three_alternating_inverse_gates(self, upto_phase_optimization): circuit.p(-np.pi / 4, 0) circuit.p(np.pi / 4, 0) - passmanager = PassManager( - CommutativeInverseCancellation(upto_phase_optimization=upto_phase_optimization) - ) + passmanager = PassManager(CommutativeInverseCancellation(matrix_based=matrix_based)) new_circuit = passmanager.run(circuit) gates_after = new_circuit.count_ops() @@ -642,7 +588,7 @@ def test_three_alternating_inverse_gates(self, upto_phase_optimization): self.assertEqual(gates_after["p"], 1) @data(False, True) - def test_four_alternating_inverse_gates(self, upto_phase_optimization): + def test_four_alternating_inverse_gates(self, matrix_based): """Test that inverse cancellation works correctly for alternating sequences of inverse gates of even-length.""" circuit = QuantumCircuit(2, 2) @@ -651,16 +597,14 @@ def test_four_alternating_inverse_gates(self, upto_phase_optimization): circuit.p(np.pi / 4, 0) circuit.p(-np.pi / 4, 0) - passmanager = PassManager( - CommutativeInverseCancellation(upto_phase_optimization=upto_phase_optimization) - ) + passmanager = PassManager(CommutativeInverseCancellation(matrix_based=matrix_based)) new_circuit = passmanager.run(circuit) gates_after = new_circuit.count_ops() self.assertNotIn("p", gates_after) @data(False, True) - def test_five_alternating_inverse_gates(self, upto_phase_optimization): + def test_five_alternating_inverse_gates(self, matrix_based): """Test that inverse cancellation works correctly for alternating sequences of inverse gates of odd-length.""" circuit = QuantumCircuit(2, 2) @@ -670,9 +614,7 @@ def test_five_alternating_inverse_gates(self, upto_phase_optimization): circuit.p(-np.pi / 4, 0) circuit.p(np.pi / 4, 0) - passmanager = PassManager( - CommutativeInverseCancellation(upto_phase_optimization=upto_phase_optimization) - ) + passmanager = PassManager(CommutativeInverseCancellation(matrix_based=matrix_based)) new_circuit = passmanager.run(circuit) gates_after = new_circuit.count_ops() @@ -680,7 +622,7 @@ def test_five_alternating_inverse_gates(self, upto_phase_optimization): self.assertEqual(gates_after["p"], 1) @data(False, True) - def test_sequence_of_inverse_gates_1(self, upto_phase_optimization): + def test_sequence_of_inverse_gates_1(self, matrix_based): """Test that inverse cancellation works correctly for more general sequences of inverse gates. In this test two pairs of inverse gates are supposed to cancel out.""" @@ -691,9 +633,7 @@ def test_sequence_of_inverse_gates_1(self, upto_phase_optimization): circuit.p(np.pi / 4, 0) circuit.p(np.pi / 4, 0) - passmanager = PassManager( - CommutativeInverseCancellation(upto_phase_optimization=upto_phase_optimization) - ) + passmanager = PassManager(CommutativeInverseCancellation(matrix_based=matrix_based)) new_circuit = passmanager.run(circuit) gates_after = new_circuit.count_ops() @@ -701,7 +641,7 @@ def test_sequence_of_inverse_gates_1(self, upto_phase_optimization): self.assertEqual(gates_after["p"], 1) @data(False, True) - def test_sequence_of_inverse_gates_2(self, upto_phase_optimization): + def test_sequence_of_inverse_gates_2(self, matrix_based): """Test that inverse cancellation works correctly for more general sequences of inverse gates. In this test, in theory three pairs of inverse gates can cancel out, but in practice only two pairs are back-to-back.""" @@ -714,9 +654,7 @@ def test_sequence_of_inverse_gates_2(self, upto_phase_optimization): circuit.p(np.pi / 4, 0) circuit.p(np.pi / 4, 0) - passmanager = PassManager( - CommutativeInverseCancellation(upto_phase_optimization=upto_phase_optimization) - ) + passmanager = PassManager(CommutativeInverseCancellation(matrix_based=matrix_based)) new_circuit = passmanager.run(circuit) gates_after = new_circuit.count_ops() @@ -724,16 +662,14 @@ def test_sequence_of_inverse_gates_2(self, upto_phase_optimization): self.assertEqual(gates_after["p"] % 2, 1) @data(False, True) - def test_cx_do_not_wrongly_cancel(self, upto_phase_optimization): + def test_cx_do_not_wrongly_cancel(self, matrix_based): """Test that CX(0,1) and CX(1, 0) do not cancel out, when (CX, CX) is passed as an inverse pair.""" circuit = QuantumCircuit(2, 0) circuit.cx(0, 1) circuit.cx(1, 0) - passmanager = PassManager( - CommutativeInverseCancellation(upto_phase_optimization=upto_phase_optimization) - ) + passmanager = PassManager(CommutativeInverseCancellation(matrix_based=matrix_based)) new_circuit = passmanager.run(circuit) gates_after = new_circuit.count_ops() @@ -743,7 +679,7 @@ def test_cx_do_not_wrongly_cancel(self, upto_phase_optimization): # A few more tests from issue 8020 @data(False, True) - def test_cancel_both_x_and_z(self, upto_phase_optimization): + def test_cancel_both_x_and_z(self, matrix_based): """Test that Z commutes with control qubit of CX, and X commutes with the target qubit.""" circuit = QuantumCircuit(2) circuit.z(0) @@ -752,9 +688,7 @@ def test_cancel_both_x_and_z(self, upto_phase_optimization): circuit.z(0) circuit.x(1) - passmanager = PassManager( - CommutativeInverseCancellation(upto_phase_optimization=upto_phase_optimization) - ) + passmanager = PassManager(CommutativeInverseCancellation(matrix_based=matrix_based)) new_circuit = passmanager.run(circuit) expected = QuantumCircuit(2) @@ -763,7 +697,7 @@ def test_cancel_both_x_and_z(self, upto_phase_optimization): self.assertEqual(expected, new_circuit) @data(False, True) - def test_gates_do_not_wrongly_cancel(self, upto_phase_optimization): + def test_gates_do_not_wrongly_cancel(self, matrix_based): """Test that X gates do not cancel for X-I-H-I-X.""" circuit = QuantumCircuit(1) circuit.x(0) @@ -772,9 +706,7 @@ def test_gates_do_not_wrongly_cancel(self, upto_phase_optimization): circuit.id(0) circuit.x(0) - passmanager = PassManager( - CommutativeInverseCancellation(upto_phase_optimization=upto_phase_optimization) - ) + passmanager = PassManager(CommutativeInverseCancellation(matrix_based=matrix_based)) new_circuit = passmanager.run(circuit) expected = QuantumCircuit(1) @@ -787,52 +719,46 @@ def test_gates_do_not_wrongly_cancel(self, upto_phase_optimization): # More tests to cover corner-cases: parameterized gates, directives, reset, etc. @data(False, True) - def test_no_cancellation_across_barrier(self, upto_phase_optimization): + def test_no_cancellation_across_barrier(self, matrix_based): """Test that barrier prevents cancellation.""" circuit = QuantumCircuit(2) circuit.cx(0, 1) circuit.barrier() circuit.cx(0, 1) - passmanager = PassManager( - CommutativeInverseCancellation(upto_phase_optimization=upto_phase_optimization) - ) + passmanager = PassManager(CommutativeInverseCancellation(matrix_based=matrix_based)) new_circuit = passmanager.run(circuit) self.assertEqual(circuit, new_circuit) @data(False, True) - def test_no_cancellation_across_measure(self, upto_phase_optimization): + def test_no_cancellation_across_measure(self, matrix_based): """Test that barrier prevents cancellation.""" circuit = QuantumCircuit(2, 1) circuit.cx(0, 1) circuit.measure(0, 0) circuit.cx(0, 1) - passmanager = PassManager( - CommutativeInverseCancellation(upto_phase_optimization=upto_phase_optimization) - ) + passmanager = PassManager(CommutativeInverseCancellation(matrix_based=matrix_based)) new_circuit = passmanager.run(circuit) self.assertEqual(circuit, new_circuit) @data(False, True) - def test_no_cancellation_across_reset(self, upto_phase_optimization): + def test_no_cancellation_across_reset(self, matrix_based): """Test that reset prevents cancellation.""" circuit = QuantumCircuit(2) circuit.cx(0, 1) circuit.reset(0) circuit.cx(0, 1) - passmanager = PassManager( - CommutativeInverseCancellation(upto_phase_optimization=upto_phase_optimization) - ) + passmanager = PassManager(CommutativeInverseCancellation(matrix_based=matrix_based)) new_circuit = passmanager.run(circuit) self.assertEqual(circuit, new_circuit) @data(False, True) - def test_no_cancellation_across_parameterized_gates(self, upto_phase_optimization): + def test_no_cancellation_across_parameterized_gates(self, matrix_based): """Test that parameterized gates prevent cancellation. This test should be modified when inverse and commutativity checking get improved to handle parameterized gates. @@ -842,14 +768,12 @@ def test_no_cancellation_across_parameterized_gates(self, upto_phase_optimizatio circuit.rz(Parameter("Theta"), 0) circuit.rz(-np.pi / 2, 0) - passmanager = PassManager( - CommutativeInverseCancellation(upto_phase_optimization=upto_phase_optimization) - ) + passmanager = PassManager(CommutativeInverseCancellation(matrix_based=matrix_based)) new_circuit = passmanager.run(circuit) self.assertEqual(circuit, new_circuit) @data(False, True) - def test_parameterized_gates_do_not_cancel(self, upto_phase_optimization): + def test_parameterized_gates_do_not_cancel(self, matrix_based): """Test that parameterized gates do not cancel. This test should be modified when inverse and commutativity checking get improved to handle parameterized gates. @@ -860,9 +784,7 @@ def test_parameterized_gates_do_not_cancel(self, upto_phase_optimization): circuit.append(gate, [0]) circuit.append(gate.inverse(), [0]) - passmanager = PassManager( - CommutativeInverseCancellation(upto_phase_optimization=upto_phase_optimization) - ) + passmanager = PassManager(CommutativeInverseCancellation(matrix_based=matrix_based)) new_circuit = passmanager.run(circuit) self.assertEqual(circuit, new_circuit) @@ -872,13 +794,13 @@ def test_phase_difference_rz_p(self): circuit.rz(np.pi / 4, 0) circuit.p(-np.pi / 4, 0) - # the gates should not cancel when upto_phase_optimization is False - passmanager = PassManager(CommutativeInverseCancellation(upto_phase_optimization=False)) + # the gates should not cancel when matrix_based is False + passmanager = PassManager(CommutativeInverseCancellation(matrix_based=False)) new_circuit = passmanager.run(circuit) self.assertEqual(circuit, new_circuit) - # the gates should be canceled when upto_phase_optimization is True - passmanager = PassManager(CommutativeInverseCancellation(upto_phase_optimization=True)) + # the gates should be canceled when matrix_based is True + passmanager = PassManager(CommutativeInverseCancellation(matrix_based=True)) new_circuit = passmanager.run(circuit) self.assertEqual(new_circuit.size(), 0) @@ -895,13 +817,13 @@ def test_phase_difference_custom(self): circuit.append(cx_circuit_with_phase.to_gate(), [0, 1]) circuit.cx(0, 1) - # the gates should not cancel when upto_phase_optimization is False - passmanager = PassManager(CommutativeInverseCancellation(upto_phase_optimization=False)) + # the gates should not cancel when matrix_based is False + passmanager = PassManager(CommutativeInverseCancellation(matrix_based=False)) new_circuit = passmanager.run(circuit) self.assertEqual(circuit, new_circuit) - # the gates should be canceled when upto_phase_optimization is True - passmanager = PassManager(CommutativeInverseCancellation(upto_phase_optimization=True)) + # the gates should be canceled when matrix_based is True + passmanager = PassManager(CommutativeInverseCancellation(matrix_based=True)) new_circuit = passmanager.run(circuit) self.assertEqual(new_circuit.size(), 0) self.assertAlmostEqual(new_circuit.global_phase, np.pi / 4) @@ -914,7 +836,7 @@ def test_inverse_unitary_gates(self): u2 = UnitaryGate([[-1, 0], [0, -1]]) circuit.append(u1, [0]) circuit.append(u2, [0]) - passmanager = PassManager(CommutativeInverseCancellation(upto_phase_optimization=True)) + passmanager = PassManager(CommutativeInverseCancellation(matrix_based=True)) new_circuit = passmanager.run(circuit) self.assertEqual(new_circuit.size(), 0) self.assertAlmostEqual(new_circuit.global_phase, np.pi) @@ -937,7 +859,7 @@ def test_inverse_custom_gates(self): circuit.append(cx_circuit2.to_gate(), [0, 1, 2]) # the two custom gates commute through cx(0, 3) and cancel each other - passmanager = PassManager(CommutativeInverseCancellation(upto_phase_optimization=True)) + passmanager = PassManager(CommutativeInverseCancellation(matrix_based=True)) new_circuit = passmanager.run(circuit) expected_circuit = QuantumCircuit(4) expected_circuit.cx(0, 3) @@ -961,9 +883,7 @@ def test_max_qubits(self): # the two custom gates commute through cx(0, 3) and cancel each other, but # we avoid the check by limiting max_qubits - passmanager = PassManager( - CommutativeInverseCancellation(upto_phase_optimization=True, max_qubits=2) - ) + passmanager = PassManager(CommutativeInverseCancellation(matrix_based=True, max_qubits=2)) new_circuit = passmanager.run(circuit) self.assertEqual(circuit, new_circuit) From f955fe75f5518a63ddd808059b51115866fb0ecf Mon Sep 17 00:00:00 2001 From: AlexanderIvrii Date: Thu, 25 Jan 2024 17:26:55 +0200 Subject: [PATCH 7/7] doc-string improvements --- .../commutative_inverse_cancellation.py | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/qiskit/transpiler/passes/optimization/commutative_inverse_cancellation.py b/qiskit/transpiler/passes/optimization/commutative_inverse_cancellation.py index 26ce32cc02fa..5ebd3eb8e64e 100644 --- a/qiskit/transpiler/passes/optimization/commutative_inverse_cancellation.py +++ b/qiskit/transpiler/passes/optimization/commutative_inverse_cancellation.py @@ -1,6 +1,6 @@ # This code is part of Qiskit. # -# (C) Copyright IBM 2023, 2024. +# (C) Copyright IBM 2017, 2024. # # This code is licensed under the Apache License, Version 2.0. You may # obtain a copy of this license in the LICENSE.txt file in the root directory @@ -24,15 +24,14 @@ class CommutativeInverseCancellation(TransformationPass): """Cancel pairs of inverse gates exploiting commutation relations.""" def __init__(self, matrix_based: bool = False, max_qubits: int = 4): - """Initialize CommutativeInverseCancellation pass. - + """ Args: - matrix_based: if True, uses matrix representations to check whether two + matrix_based: If ``True``, uses matrix representations to check whether two operations are inverse of each other. This makes the checks more powerful, - and in addition allows canceling pairs of operations that are inverse up to a + and, in addition, allows canceling pairs of operations that are inverse up to a phase, while updating the global phase of the circuit accordingly. Generally this leads to more reductions at the expense of increased runtime. - max_qubits: limits the number of qubits in matrix-based commutativity and + max_qubits: Limits the number of qubits in matrix-based commutativity and inverse checks. """ self._matrix_based = matrix_based @@ -56,7 +55,7 @@ def _skip_node(self, node): return True return False - def _inverse_upto_phase(self, node1, node2): + def _check_inverse(self, node1, node2): """Checks whether op1 and op2 are inverse up to a phase, that is whether ``op2 = e^{i * d} op1^{-1})`` for some phase difference ``d``. If this is the case, we can replace ``op2 * op1`` by `e^{i * d} I``. @@ -113,7 +112,7 @@ def run(self, dag: DAGCircuit): and topo_sorted_nodes[idx2].qargs == topo_sorted_nodes[idx1].qargs and topo_sorted_nodes[idx2].cargs == topo_sorted_nodes[idx1].cargs ): - is_inverse, phase = self._inverse_upto_phase( + is_inverse, phase = self._check_inverse( topo_sorted_nodes[idx1], topo_sorted_nodes[idx2] ) if is_inverse: