From da209e3c4b8bc0ec27a18b4ccc888729b78303b8 Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Sat, 19 Oct 2024 13:45:33 -0400 Subject: [PATCH] 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. --- crates/accelerate/src/lib.rs | 1 + .../accelerate/src/remove_identity_equiv.rs | 113 ++++++++++++++++++ crates/pyext/src/lib.rs | 1 + qiskit/__init__.py | 1 + qiskit/transpiler/passes/__init__.py | 4 +- .../passes/optimization/__init__.py | 2 +- .../passes/optimization/drop_negligible.py | 100 ---------------- .../optimization/remove_identity_equiv.py | 56 +++++++++ ...emove_identity_equiv-9c627c8c35b2298a.yaml | 29 +++++ .../python/transpiler/test_drop_negligible.py | 38 +----- 10 files changed, 209 insertions(+), 136 deletions(-) create mode 100644 crates/accelerate/src/remove_identity_equiv.rs delete mode 100644 qiskit/transpiler/passes/optimization/drop_negligible.py create mode 100644 qiskit/transpiler/passes/optimization/remove_identity_equiv.py create mode 100644 releasenotes/notes/remove_identity_equiv-9c627c8c35b2298a.yaml diff --git a/crates/accelerate/src/lib.rs b/crates/accelerate/src/lib.rs index 5afe8c3259a0..a32e2e5e410d 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..44a95cb464e2 --- /dev/null +++ b/crates/accelerate/src/remove_identity_equiv.rs @@ -0,0 +1,113 @@ +// 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) => { + 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 < error { + remove_list.push(op_node) + } + } + } + OperationRef::Gate(gate) => { + 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 < 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 bc0d44a9dd44..02ed992ceb98 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 26a72de2f722..88a4154beff6 100644 --- a/qiskit/__init__.py +++ b/qiskit/__init__.py @@ -105,6 +105,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 69e720096dca..5bc1ae555a5e 100644 --- a/qiskit/transpiler/passes/__init__.py +++ b/qiskit/transpiler/passes/__init__.py @@ -93,7 +93,7 @@ NormalizeRXAngle OptimizeAnnotated Split2QUnitaries - DropNegligible + RemoveIdentityEquivalent Calibration ============= @@ -248,7 +248,7 @@ from .optimization import ElidePermutations from .optimization import NormalizeRXAngle from .optimization import OptimizeAnnotated -from .optimization import DropNegligible +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 d28ee8d83c3a..c0e455b2065b 100644 --- a/qiskit/transpiler/passes/optimization/__init__.py +++ b/qiskit/transpiler/passes/optimization/__init__.py @@ -38,6 +38,6 @@ from .elide_permutations import ElidePermutations from .normalize_rx_angle import NormalizeRXAngle from .optimize_annotated import OptimizeAnnotated -from .drop_negligible import DropNegligible +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/drop_negligible.py b/qiskit/transpiler/passes/optimization/drop_negligible.py deleted file mode 100644 index a23ec77d47bc..000000000000 --- a/qiskit/transpiler/passes/optimization/drop_negligible.py +++ /dev/null @@ -1,100 +0,0 @@ -# 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 - -import math -from collections.abc import Iterable - -from qiskit.circuit.library import ( - CPhaseGate, - PhaseGate, - RXGate, - RXXGate, - RYGate, - RYYGate, - RZGate, - RZZGate, - XXMinusYYGate, - XXPlusYYGate, -) -from qiskit.dagcircuit import DAGCircuit -from qiskit.transpiler.basepasses import TransformationPass - -# List of Gate classes with the property that if the gate's parameters are all -# (close to) zero then the gate has (close to) no effect. -DROP_NEGLIGIBLE_GATE_CLASSES = ( - CPhaseGate, - PhaseGate, - RXGate, - RYGate, - RZGate, - RXXGate, - RYYGate, - RZZGate, - XXPlusYYGate, - XXMinusYYGate, -) - - -class DropNegligible(TransformationPass): - """Drop gates with negligible effects. - - Removes certain gates whose parameters are all close to zero up to the specified - tolerance. By default, the gates subject to removal are those present in a - hard-coded list, specified below. Additional gate types to consider can be passed - as an argument to the constructor of this class. - - By default, the following gate classes are considered for removal: - - - :class:`CPhaseGate` - - :class:`PhaseGate` - - :class:`RXGate` - - :class:`RYGate` - - :class:`RZGate` - - :class:`RXXGate` - - :class:`RYYGate` - - :class:`RZZGate` - - :class:`XXPlusYYGate` - - :class:`XXMinusYYGate` - """ - - def __init__( - self, *, atol: float = 1e-8, additional_gate_types: Iterable[type] | None = None - ) -> None: - """Initialize the transpiler pass. - - Args: - atol: Absolute numerical tolerance for determining whether a gate's effect - is negligible. - additional_gate_types: List of :class:`Gate` subclasses that should be - considered for dropping in addition to the built-in gates. - """ - self.atol = atol - self.gate_types = DROP_NEGLIGIBLE_GATE_CLASSES - if additional_gate_types is not None: - self.gate_types += tuple(additional_gate_types) - super().__init__() - - def run(self, dag: DAGCircuit) -> DAGCircuit: - for node in dag.op_nodes(): - if not isinstance(node.op, self.gate_types): - continue - if not all(isinstance(param, (int, float, complex)) for param in node.op.params): - continue - if all( - math.isclose(param, 0, rel_tol=0, abs_tol=self.atol) for param in node.op.params - ): - dag.remove_op_node(node) - return dag 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..b19eb5971fe2 --- /dev/null +++ b/qiskit/transpiler/passes/optimization/remove_identity_equiv.py @@ -0,0 +1,56 @@ +# 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. + """ + + 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 target 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..85be9cb78306 --- /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 index 14f29cd06b43..a2d183a1a1b8 100644 --- a/test/python/transpiler/test_drop_negligible.py +++ b/test/python/transpiler/test_drop_negligible.py @@ -14,7 +14,7 @@ import numpy as np -from qiskit.circuit import Gate, Parameter, QuantumCircuit, QuantumRegister +from qiskit.circuit import Parameter, QuantumCircuit, QuantumRegister from qiskit.circuit.library import ( CPhaseGate, RXGate, @@ -27,7 +27,7 @@ XXPlusYYGate, ) from qiskit.quantum_info import Operator -from qiskit.transpiler.passes import DropNegligible +from qiskit.transpiler.passes import RemoveIdentityEquivalent from test import QiskitTestCase # pylint: disable=wrong-import-order @@ -58,7 +58,7 @@ def test_drops_negligible_gates(self): 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 = DropNegligible()(circuit) + transpiled = RemoveIdentityEquivalent()(circuit) self.assertEqual(circuit.count_ops()["cp"], 2) self.assertEqual(transpiled.count_ops()["cp"], 1) self.assertEqual(circuit.count_ops()["rx"], 2) @@ -90,7 +90,7 @@ def test_handles_parameters(self): circuit.append(CPhaseGate(theta), [a, b]) circuit.append(CPhaseGate(1e-5), [a, b]) circuit.append(CPhaseGate(1e-8), [a, b]) - transpiled = DropNegligible()(circuit) + transpiled = RemoveIdentityEquivalent()(circuit) self.assertEqual(circuit.count_ops()["cp"], 3) self.assertEqual(transpiled.count_ops()["cp"], 2) @@ -102,34 +102,6 @@ def test_handles_number_types(self): circuit.append(CPhaseGate(np.float32(1e-6)), [a, b]) circuit.append(CPhaseGate(1e-3), [a, b]) circuit.append(CPhaseGate(1e-8), [a, b]) - transpiled = DropNegligible(atol=1e-5)(circuit) + transpiled = RemoveIdentityEquivalent(approximation_degree=1e-7)(circuit) self.assertEqual(circuit.count_ops()["cp"], 3) self.assertEqual(transpiled.count_ops()["cp"], 1) - - def test_additional_gate_types(self): - """Test passing additional gate types.""" - - class TestGateA(Gate): - """A gate class.""" - pass - - class TestGateB(Gate): - """Another gate class.""" - pass - - 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(TestGateA("test_gate_a", 1, [1e-5, 1e-5]), [a]) - circuit.append(TestGateA("test_gate_a", 1, [1e-8, 1e-8]), [a]) - circuit.append(TestGateB("test_gate_b", 1, [1e-5, 1e-5]), [a]) - circuit.append(TestGateB("test_gate_b", 1, [1e-8, 1e-8]), [a]) - transpiled = DropNegligible(additional_gate_types=[TestGateA])(circuit) - self.assertEqual(circuit.count_ops()["cp"], 2) - self.assertEqual(transpiled.count_ops()["cp"], 1) - self.assertEqual(circuit.count_ops()["test_gate_a"], 2) - self.assertEqual(transpiled.count_ops()["test_gate_a"], 1) - self.assertEqual(circuit.count_ops()["test_gate_b"], 2) - self.assertEqual(transpiled.count_ops()["test_gate_b"], 2)