diff --git a/crates/accelerate/src/lib.rs b/crates/accelerate/src/lib.rs index ed3b75d309d..e04f6d63a93 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 00000000000..0da892d1633 --- /dev/null +++ b/crates/accelerate/src/remove_identity_equiv.rs @@ -0,0 +1,121 @@ +// 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::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 => degree, + } + } + None => 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) => { + // Skip global phase 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) + } + } + } + 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 56054145513..49070e85db7 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 63e5a2a48a4..202ebc32be8 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 ca4e3545a98..5bc1ae555a5 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 8e2883b2778..c0e455b2065 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 00000000000..c1f62297b8d --- /dev/null +++ b/qiskit/transpiler/passes/optimization/remove_identity_equiv.py @@ -0,0 +1,67 @@ +# 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): + """Remove gates with negligible effects. + + Removes gates whose effect is close to an identity operation, up to the specified + tolerance. + 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 00000000000..85be9cb7830 --- /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_drop_negligible.py b/test/python/transpiler/test_drop_negligible.py new file mode 100644 index 00000000000..a2d183a1a1b --- /dev/null +++ b/test/python/transpiler/test_drop_negligible.py @@ -0,0 +1,107 @@ +# 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 +from qiskit.circuit.library import ( + CPhaseGate, + RXGate, + RXXGate, + RYGate, + RYYGate, + RZGate, + RZZGate, + XXMinusYYGate, + XXPlusYYGate, +) +from qiskit.quantum_info import Operator +from qiskit.transpiler.passes import RemoveIdentityEquivalent + +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_handles_number_types(self): + """Test that gates with different types of numbers are handled correctly.""" + qubits = QuantumRegister(2) + circuit = QuantumCircuit(qubits) + a, b = qubits + circuit.append(CPhaseGate(np.float32(1e-6)), [a, b]) + circuit.append(CPhaseGate(1e-3), [a, b]) + circuit.append(CPhaseGate(1e-8), [a, b]) + transpiled = RemoveIdentityEquivalent(approximation_degree=1e-7)(circuit) + self.assertEqual(circuit.count_ops()["cp"], 3) + self.assertEqual(transpiled.count_ops()["cp"], 1)