From e89cb474b686b08e764a7841bac52cd5b2b29596 Mon Sep 17 00:00:00 2001 From: "Kevin J. Sung" Date: Tue, 29 Oct 2024 05:41:29 -0400 Subject: [PATCH] Add RemoveIdentityEquivalent transpiler pass (#12384) * add DropNegligible transpiler pass * use math.isclose instead of numpy.allclose * use pass directly instead of constructing pass manager * lint * allow passing additional gate types * add docstrings to test classes * Generalize the pass for all gates and rewrite in rust This pivots the pass to work for all gates by checking all the parameters in the standard gate library are within the specified tolerance, and the matrix is equivalent to an identity for any other gate defined in Python (with a matrix defined). To improve the performance of the pass it is written in rust now. Additionally the class is named to RemoveIdentityEquivalent to make the purpose of the pass slightly more clear. * Don't remove zero qubit gates * Ensure computed gate error is always positive * Pass docstring improvements Co-authored-by: Julien Gacon * Move test file * Fix lint * Fix docs * Add optimized identity check for Pauli gates Co-authored-by: Julien Gacon * Mention zero qubit gates in the docstring * Update approximation degree logic in absense of target * Expand test coverage and fix bugs --------- Co-authored-by: Matthew Treinish Co-authored-by: Julien Gacon --- crates/accelerate/src/lib.rs | 1 + .../accelerate/src/remove_identity_equiv.rs | 149 ++++++++++++++ crates/pyext/src/lib.rs | 1 + qiskit/__init__.py | 1 + qiskit/transpiler/passes/__init__.py | 2 + .../passes/optimization/__init__.py | 1 + .../optimization/remove_identity_equiv.py | 69 +++++++ ...emove_identity_equiv-9c627c8c35b2298a.yaml | 29 +++ .../test_remove_identity_equivalent.py | 185 ++++++++++++++++++ 9 files changed, 438 insertions(+) create mode 100644 crates/accelerate/src/remove_identity_equiv.rs create mode 100644 qiskit/transpiler/passes/optimization/remove_identity_equiv.py create mode 100644 releasenotes/notes/remove_identity_equiv-9c627c8c35b2298a.yaml create mode 100644 test/python/transpiler/test_remove_identity_equivalent.py diff --git a/crates/accelerate/src/lib.rs b/crates/accelerate/src/lib.rs index ed3b75d309d6..e04f6d63a936 100644 --- a/crates/accelerate/src/lib.rs +++ b/crates/accelerate/src/lib.rs @@ -37,6 +37,7 @@ pub mod nlayout; pub mod optimize_1q_gates; pub mod pauli_exp_val; pub mod remove_diagonal_gates_before_measure; +pub mod remove_identity_equiv; pub mod results; pub mod sabre; pub mod sampled_exp_val; diff --git a/crates/accelerate/src/remove_identity_equiv.rs b/crates/accelerate/src/remove_identity_equiv.rs new file mode 100644 index 000000000000..a3eb921628e2 --- /dev/null +++ b/crates/accelerate/src/remove_identity_equiv.rs @@ -0,0 +1,149 @@ +// This code is part of Qiskit. +// +// (C) Copyright IBM 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 +// of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +// +// Any modifications or derivative works of this code must retain this +// copyright notice, and modified files need to carry a notice indicating +// that they have been altered from the originals. + +use num_complex::Complex64; +use num_complex::ComplexFloat; +use pyo3::prelude::*; +use rustworkx_core::petgraph::stable_graph::NodeIndex; + +use crate::nlayout::PhysicalQubit; +use crate::target_transpiler::Target; +use qiskit_circuit::dag_circuit::DAGCircuit; +use qiskit_circuit::operations::Operation; +use qiskit_circuit::operations::OperationRef; +use qiskit_circuit::operations::Param; +use qiskit_circuit::operations::StandardGate; +use qiskit_circuit::packed_instruction::PackedInstruction; + +#[pyfunction] +#[pyo3(signature=(dag, approx_degree=Some(1.0), target=None))] +fn remove_identity_equiv( + dag: &mut DAGCircuit, + approx_degree: Option, + target: Option<&Target>, +) { + let mut remove_list: Vec = Vec::new(); + + let get_error_cutoff = |inst: &PackedInstruction| -> f64 { + match approx_degree { + Some(degree) => { + if degree == 1.0 { + f64::EPSILON + } else { + match target { + Some(target) => { + let qargs: Vec = dag + .get_qargs(inst.qubits) + .iter() + .map(|x| PhysicalQubit::new(x.0)) + .collect(); + let error_rate = target.get_error(inst.op.name(), qargs.as_slice()); + match error_rate { + Some(err) => err * degree, + None => f64::EPSILON.max(1. - degree), + } + } + None => f64::EPSILON.max(1. - degree), + } + } + } + None => match target { + Some(target) => { + let qargs: Vec = dag + .get_qargs(inst.qubits) + .iter() + .map(|x| PhysicalQubit::new(x.0)) + .collect(); + let error_rate = target.get_error(inst.op.name(), qargs.as_slice()); + match error_rate { + Some(err) => err, + None => f64::EPSILON, + } + } + None => f64::EPSILON, + }, + } + }; + + for op_node in dag.op_nodes(false) { + let inst = dag.dag()[op_node].unwrap_operation(); + match inst.op.view() { + OperationRef::Standard(gate) => { + let (dim, trace) = match gate { + StandardGate::RXGate | StandardGate::RYGate | StandardGate::RZGate => { + if let Param::Float(theta) = inst.params_view()[0] { + let trace = (theta / 2.).cos() * 2.; + (2., trace) + } else { + continue; + } + } + StandardGate::RXXGate + | StandardGate::RYYGate + | StandardGate::RZZGate + | StandardGate::RZXGate => { + if let Param::Float(theta) = inst.params_view()[0] { + let trace = (theta / 2.).cos() * 4.; + (4., trace) + } else { + continue; + } + } + _ => { + // Skip global phase gate + if gate.num_qubits() < 1 { + continue; + } + if let Some(matrix) = gate.matrix(inst.params_view()) { + let dim = matrix.shape()[0] as f64; + let trace = matrix.diag().iter().sum::().abs(); + (dim, trace) + } else { + continue; + } + } + }; + let error = get_error_cutoff(inst); + let f_pro = (trace / dim).powi(2); + let gate_fidelity = (dim * f_pro + 1.) / (dim + 1.); + if (1. - gate_fidelity).abs() < error { + remove_list.push(op_node) + } + } + OperationRef::Gate(gate) => { + // Skip global phase like gate + if gate.num_qubits() < 1 { + continue; + } + if let Some(matrix) = gate.matrix(inst.params_view()) { + let error = get_error_cutoff(inst); + let dim = matrix.shape()[0] as f64; + let trace: Complex64 = matrix.diag().iter().sum(); + let f_pro = (trace / dim).abs().powi(2); + let gate_fidelity = (dim * f_pro + 1.) / (dim + 1.); + if (1. - gate_fidelity).abs() < error { + remove_list.push(op_node) + } + } + } + _ => continue, + } + } + for node in remove_list { + dag.remove_op_node(node); + } +} + +pub fn remove_identity_equiv_mod(m: &Bound) -> PyResult<()> { + m.add_wrapped(wrap_pyfunction!(remove_identity_equiv))?; + Ok(()) +} diff --git a/crates/pyext/src/lib.rs b/crates/pyext/src/lib.rs index 560541455138..49070e85db70 100644 --- a/crates/pyext/src/lib.rs +++ b/crates/pyext/src/lib.rs @@ -50,6 +50,7 @@ fn _accelerate(m: &Bound) -> PyResult<()> { add_submodule(m, ::qiskit_accelerate::optimize_1q_gates::optimize_1q_gates, "optimize_1q_gates")?; add_submodule(m, ::qiskit_accelerate::pauli_exp_val::pauli_expval, "pauli_expval")?; add_submodule(m, ::qiskit_accelerate::remove_diagonal_gates_before_measure::remove_diagonal_gates_before_measure, "remove_diagonal_gates_before_measure")?; + add_submodule(m, ::qiskit_accelerate::remove_identity_equiv::remove_identity_equiv_mod, "remove_identity_equiv")?; add_submodule(m, ::qiskit_accelerate::results::results, "results")?; add_submodule(m, ::qiskit_accelerate::sabre::sabre, "sabre")?; add_submodule(m, ::qiskit_accelerate::sampled_exp_val::sampled_exp_val, "sampled_exp_val")?; diff --git a/qiskit/__init__.py b/qiskit/__init__.py index 63e5a2a48a47..202ebc32be85 100644 --- a/qiskit/__init__.py +++ b/qiskit/__init__.py @@ -106,6 +106,7 @@ sys.modules["qiskit._accelerate.inverse_cancellation"] = _accelerate.inverse_cancellation sys.modules["qiskit._accelerate.check_map"] = _accelerate.check_map sys.modules["qiskit._accelerate.filter_op_nodes"] = _accelerate.filter_op_nodes +sys.modules["qiskit._accelerate.remove_identity_equiv"] = _accelerate.remove_identity_equiv from qiskit.exceptions import QiskitError, MissingOptionalLibraryError diff --git a/qiskit/transpiler/passes/__init__.py b/qiskit/transpiler/passes/__init__.py index ca4e3545a98b..5bc1ae555a5e 100644 --- a/qiskit/transpiler/passes/__init__.py +++ b/qiskit/transpiler/passes/__init__.py @@ -93,6 +93,7 @@ NormalizeRXAngle OptimizeAnnotated Split2QUnitaries + RemoveIdentityEquivalent Calibration ============= @@ -247,6 +248,7 @@ from .optimization import ElidePermutations from .optimization import NormalizeRXAngle from .optimization import OptimizeAnnotated +from .optimization import RemoveIdentityEquivalent from .optimization import Split2QUnitaries # circuit analysis diff --git a/qiskit/transpiler/passes/optimization/__init__.py b/qiskit/transpiler/passes/optimization/__init__.py index 8e2883b27781..c0e455b2065b 100644 --- a/qiskit/transpiler/passes/optimization/__init__.py +++ b/qiskit/transpiler/passes/optimization/__init__.py @@ -38,5 +38,6 @@ from .elide_permutations import ElidePermutations from .normalize_rx_angle import NormalizeRXAngle from .optimize_annotated import OptimizeAnnotated +from .remove_identity_equiv import RemoveIdentityEquivalent from .split_2q_unitaries import Split2QUnitaries from .collect_and_collapse import CollectAndCollapse diff --git a/qiskit/transpiler/passes/optimization/remove_identity_equiv.py b/qiskit/transpiler/passes/optimization/remove_identity_equiv.py new file mode 100644 index 000000000000..4f8551f12442 --- /dev/null +++ b/qiskit/transpiler/passes/optimization/remove_identity_equiv.py @@ -0,0 +1,69 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 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 +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. + +"""Transpiler pass to drop gates with negligible effects.""" + +from __future__ import annotations + +from qiskit.dagcircuit import DAGCircuit +from qiskit.transpiler.target import Target +from qiskit.transpiler.basepasses import TransformationPass +from qiskit._accelerate.remove_identity_equiv import remove_identity_equiv + + +class RemoveIdentityEquivalent(TransformationPass): + r"""Remove gates with negligible effects. + + Removes gates whose effect is close to an identity operation, up to the specified + tolerance. Zero qubit gates such as :class:`.GlobalPhaseGate` are not considered + by this pass. + + For a cutoff fidelity :math:`f`, this pass removes gates whose average + gate fidelity with respect to the identity is below :math:`f`. Concretely, + a gate :math:`G` is removed if :math:`\bar F < f` where + + .. math:: + + \bar{F} = \frac{1 + F_{\text{process}}{1 + d} + + F_{\text{process}} = \frac{|\mathrm{Tr}(G)|^2}{d^2} + + where :math:`d = 2^n` is the dimension of the gate for :math:`n` qubits. + """ + + def __init__( + self, *, approximation_degree: float | None = 1.0, target: None | Target = None + ) -> None: + """Initialize the transpiler pass. + + Args: + approximation_degree: The degree to approximate for the equivalence check. This can be a + floating point value between 0 and 1, or ``None``. If the value is 1 this does not + approximate above floating point precision. For a value < 1 this is used as a scaling + factor for the cutoff fidelity. If the value is ``None`` this approximates up to the + fidelity for the gate specified in ``target``. + + target: If ``approximation_degree`` is set to ``None`` and a :class:`.Target` is provided + for this field the tolerance for determining whether an operation is equivalent to + identity will be set to the reported error rate in the target. If + ``approximation_degree`` (the default) this has no effect, if + ``approximation_degree=None`` it uses the error rate specified in the ``Target`` for + the gate being evaluated, and a numeric value other than 1 with ``target`` set is + used as a scaling factor of the target's error rate. + """ + super().__init__() + self._approximation_degree = approximation_degree + self._target = target + + def run(self, dag: DAGCircuit) -> DAGCircuit: + remove_identity_equiv(dag, self._approximation_degree, self._target) + return dag diff --git a/releasenotes/notes/remove_identity_equiv-9c627c8c35b2298a.yaml b/releasenotes/notes/remove_identity_equiv-9c627c8c35b2298a.yaml new file mode 100644 index 000000000000..90d016803d62 --- /dev/null +++ b/releasenotes/notes/remove_identity_equiv-9c627c8c35b2298a.yaml @@ -0,0 +1,29 @@ +--- +features_transpiler: + - | + Added a new transpiler pass, :class:`.RemoveIdentityEquivalent` that is used + to remove gates that are equivalent to an identity up to some tolerance. + For example if you had a circuit like: + + .. plot:: + + from qiskit.circuit import QuantumCircuit + + qc = QuantumCircuit(2) + qc.cp(1e-20, 0, 1) + qc.draw("mpl") + + running the pass would eliminate the :class:`.CPhaseGate`: + + .. plot:: + :include-source: + + from qiskit.circuit import QuantumCircuit + from qiskit.transpiler.passes import RemoveIdentityEquivalent + + qc = QuantumCircuit(2) + qc.cp(1e-20, 0, 1) + + removal_pass = RemoveIdentityEquivalent() + result = removal_pass(qc) + result.draw("mpl") diff --git a/test/python/transpiler/test_remove_identity_equivalent.py b/test/python/transpiler/test_remove_identity_equivalent.py new file mode 100644 index 000000000000..1db392d3654b --- /dev/null +++ b/test/python/transpiler/test_remove_identity_equivalent.py @@ -0,0 +1,185 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 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 +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. + +"""Tests for the DropNegligible transpiler pass.""" + +import numpy as np + +from qiskit.circuit import Parameter, QuantumCircuit, QuantumRegister, Gate +from qiskit.circuit.library import ( + CPhaseGate, + RXGate, + RXXGate, + RYGate, + RYYGate, + RZGate, + RZZGate, + XXMinusYYGate, + XXPlusYYGate, + GlobalPhaseGate, +) +from qiskit.quantum_info import Operator +from qiskit.transpiler.passes import RemoveIdentityEquivalent +from qiskit.transpiler.target import Target, InstructionProperties + +from test import QiskitTestCase # pylint: disable=wrong-import-order + + +class TestDropNegligible(QiskitTestCase): + """Test the DropNegligible pass.""" + + def test_drops_negligible_gates(self): + """Test that negligible gates are dropped.""" + qubits = QuantumRegister(2) + circuit = QuantumCircuit(qubits) + a, b = qubits + circuit.append(CPhaseGate(1e-5), [a, b]) + circuit.append(CPhaseGate(1e-8), [a, b]) + circuit.append(RXGate(1e-5), [a]) + circuit.append(RXGate(1e-8), [a]) + circuit.append(RYGate(1e-5), [a]) + circuit.append(RYGate(1e-8), [a]) + circuit.append(RZGate(1e-5), [a]) + circuit.append(RZGate(1e-8), [a]) + circuit.append(RXXGate(1e-5), [a, b]) + circuit.append(RXXGate(1e-8), [a, b]) + circuit.append(RYYGate(1e-5), [a, b]) + circuit.append(RYYGate(1e-8), [a, b]) + circuit.append(RZZGate(1e-5), [a, b]) + circuit.append(RZZGate(1e-8), [a, b]) + circuit.append(XXPlusYYGate(1e-5, 1e-8), [a, b]) + circuit.append(XXPlusYYGate(1e-8, 1e-8), [a, b]) + circuit.append(XXMinusYYGate(1e-5, 1e-8), [a, b]) + circuit.append(XXMinusYYGate(1e-8, 1e-8), [a, b]) + transpiled = RemoveIdentityEquivalent()(circuit) + self.assertEqual(circuit.count_ops()["cp"], 2) + self.assertEqual(transpiled.count_ops()["cp"], 1) + self.assertEqual(circuit.count_ops()["rx"], 2) + self.assertEqual(transpiled.count_ops()["rx"], 1) + self.assertEqual(circuit.count_ops()["ry"], 2) + self.assertEqual(transpiled.count_ops()["ry"], 1) + self.assertEqual(circuit.count_ops()["rz"], 2) + self.assertEqual(transpiled.count_ops()["rz"], 1) + self.assertEqual(circuit.count_ops()["rxx"], 2) + self.assertEqual(transpiled.count_ops()["rxx"], 1) + self.assertEqual(circuit.count_ops()["ryy"], 2) + self.assertEqual(transpiled.count_ops()["ryy"], 1) + self.assertEqual(circuit.count_ops()["rzz"], 2) + self.assertEqual(transpiled.count_ops()["rzz"], 1) + self.assertEqual(circuit.count_ops()["xx_plus_yy"], 2) + self.assertEqual(transpiled.count_ops()["xx_plus_yy"], 1) + self.assertEqual(circuit.count_ops()["xx_minus_yy"], 2) + self.assertEqual(transpiled.count_ops()["xx_minus_yy"], 1) + np.testing.assert_allclose( + np.array(Operator(circuit)), np.array(Operator(transpiled)), atol=1e-7 + ) + + def test_handles_parameters(self): + """Test that gates with parameters are ignored gracefully.""" + qubits = QuantumRegister(2) + circuit = QuantumCircuit(qubits) + a, b = qubits + theta = Parameter("theta") + circuit.append(CPhaseGate(theta), [a, b]) + circuit.append(CPhaseGate(1e-5), [a, b]) + circuit.append(CPhaseGate(1e-8), [a, b]) + transpiled = RemoveIdentityEquivalent()(circuit) + self.assertEqual(circuit.count_ops()["cp"], 3) + self.assertEqual(transpiled.count_ops()["cp"], 2) + + def test_approximation_degree(self): + """Test that approximation degree handled correctly.""" + qubits = QuantumRegister(2) + circuit = QuantumCircuit(qubits) + a, b = qubits + circuit.append(CPhaseGate(1e-4), [a, b]) + # fidelity 0.9999850001249996 which is above the threshold and not excluded + # so 1e-2 is the only gate remaining + circuit.append(CPhaseGate(1e-2), [a, b]) + circuit.append(CPhaseGate(1e-20), [a, b]) + transpiled = RemoveIdentityEquivalent(approximation_degree=0.9999999)(circuit) + self.assertEqual(circuit.count_ops()["cp"], 3) + self.assertEqual(transpiled.count_ops()["cp"], 1) + self.assertEqual(transpiled.data[0].operation.params[0], 1e-2) + + def test_target_approx_none(self): + """Test error rate with target.""" + + target = Target() + props = {(0, 1): InstructionProperties(error=1e-10)} + target.add_instruction(CPhaseGate(Parameter("theta")), props) + circuit = QuantumCircuit(2) + circuit.append(CPhaseGate(1e-4), [0, 1]) + circuit.append(CPhaseGate(1e-2), [0, 1]) + circuit.append(CPhaseGate(1e-20), [0, 1]) + transpiled = RemoveIdentityEquivalent(approximation_degree=None, target=target)(circuit) + self.assertEqual(circuit.count_ops()["cp"], 3) + self.assertEqual(transpiled.count_ops()["cp"], 2) + + def test_target_approx_approx_degree(self): + """Test error rate with target.""" + + target = Target() + props = {(0, 1): InstructionProperties(error=1e-10)} + target.add_instruction(CPhaseGate(Parameter("theta")), props) + circuit = QuantumCircuit(2) + circuit.append(CPhaseGate(1e-4), [0, 1]) + circuit.append(CPhaseGate(1e-2), [0, 1]) + circuit.append(CPhaseGate(1e-20), [0, 1]) + transpiled = RemoveIdentityEquivalent(approximation_degree=0.9999999, target=target)( + circuit + ) + self.assertEqual(circuit.count_ops()["cp"], 3) + self.assertEqual(transpiled.count_ops()["cp"], 2) + + def test_custom_gate_no_matrix(self): + """Test that opaque gates are ignored.""" + + class CustomOpaqueGate(Gate): + """Custom opaque gate.""" + + def __init__(self): + super().__init__("opaque", 2, []) + + qc = QuantumCircuit(3) + qc.append(CustomOpaqueGate(), [0, 1]) + transpiled = RemoveIdentityEquivalent()(qc) + self.assertEqual(qc, transpiled) + + def test_custom_gate_identity_matrix(self): + """Test that custom gates with matrix are evaluated.""" + + class CustomGate(Gate): + """Custom gate.""" + + def __init__(self): + super().__init__("custom", 3, []) + + def to_matrix(self): + return np.eye(8, dtype=complex) + + qc = QuantumCircuit(3) + qc.append(CustomGate(), [0, 1, 2]) + transpiled = RemoveIdentityEquivalent()(qc) + expected = QuantumCircuit(3) + self.assertEqual(expected, transpiled) + + def test_global_phase_ignored(self): + """Test that global phase gate isn't considered.""" + + qc = QuantumCircuit(1) + qc.id(0) + qc.append(GlobalPhaseGate(0)) + transpiled = RemoveIdentityEquivalent()(qc) + expected = QuantumCircuit(1) + expected.append(GlobalPhaseGate(0)) + self.assertEqual(transpiled, expected)