Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Extend CommutativeInverseCancellation to cancel pairs of gates up-to-phase #11248

Merged
merged 11 commits into from
Jan 25, 2024
16 changes: 14 additions & 2 deletions qiskit/quantum_info/operators/predicates.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
RTOL_DEFAULT = 1e-5


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
Expand All @@ -38,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.
Expand All @@ -59,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)


Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# This code is part of Qiskit.
#
# (C) Copyright IBM 2017, 2021.
# (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
Expand All @@ -14,13 +14,30 @@


from qiskit.dagcircuit import DAGCircuit, DAGOpNode
from qiskit.quantum_info import Operator
from qiskit.quantum_info.operators.predicates import matrix_equal
from qiskit.transpiler.basepasses import TransformationPass
from qiskit.circuit.commutation_checker import CommutationChecker


class CommutativeInverseCancellation(TransformationPass):
"""Cancel pairs of inverse gates exploiting commutation relations."""

def __init__(self, matrix_based: bool = False, max_qubits: int = 4):
"""
Args:
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
super().__init__()

def _skip_node(self, node):
"""Returns True if we should skip this node for the analysis."""
if not isinstance(node, DAGOpNode):
Expand All @@ -36,9 +53,31 @@ 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 _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``.
The input to this function is a pair of DAG nodes.
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._matrix_based:
is_inverse = node1.op.inverse() == node2.op
elif len(node2.qargs) > self._max_qubits:
is_inverse = False
else:
mat1 = Operator(node1.op.inverse()).data
mat2 = Operator(node2.op).data
props = {}
is_inverse = matrix_equal(mat1, mat2, ignore_phase=True, props=props)
if is_inverse:
# mat2 = e^{i * phase_difference} mat1
phase_difference = props["phase_difference"]
return is_inverse, phase_difference

def run(self, dag: DAGCircuit):
"""
Run the CommutativeInverseCancellation pass on `dag`.
Expand All @@ -55,6 +94,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):
Expand All @@ -71,10 +111,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
is_inverse, phase = self._check_inverse(
topo_sorted_nodes[idx1], topo_sorted_nodes[idx2]
)
if is_inverse:
phase_update += phase
matched_idx2 = idx2
break

if not cc.commute(
topo_sorted_nodes[idx1].op,
Expand All @@ -83,6 +127,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

Expand All @@ -94,4 +139,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
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
---
features:
- |
Added two new arguments ``matrix_based`` and ``max_qubits`` to the
constructor of :class:`.CommutativeInverseCancellation` transpiler pass.
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. 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(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
phase to :math:`\frac{15 \pi}{8}`.
Loading
Loading