From e442e7abb5cc57c804213962797aaf88563640b8 Mon Sep 17 00:00:00 2001 From: "Kevin J. Sung" Date: Thu, 9 May 2024 18:49:01 -0400 Subject: [PATCH 01/17] add DropNegligible transpiler pass --- qiskit/transpiler/passes/__init__.py | 1 + .../passes/optimization/__init__.py | 1 + .../passes/optimization/drop_negligible.py | 70 +++++++++++ .../python/transpiler/test_drop_negligible.py | 112 ++++++++++++++++++ 4 files changed, 184 insertions(+) create mode 100644 qiskit/transpiler/passes/optimization/drop_negligible.py create mode 100644 test/python/transpiler/test_drop_negligible.py diff --git a/qiskit/transpiler/passes/__init__.py b/qiskit/transpiler/passes/__init__.py index 400d98304951..23e51101114f 100644 --- a/qiskit/transpiler/passes/__init__.py +++ b/qiskit/transpiler/passes/__init__.py @@ -244,6 +244,7 @@ from .optimization import ElidePermutations from .optimization import NormalizeRXAngle from .optimization import OptimizeAnnotated +from .optimization import DropNegligible # circuit analysis from .analysis import ResourceEstimation diff --git a/qiskit/transpiler/passes/optimization/__init__.py b/qiskit/transpiler/passes/optimization/__init__.py index 082cb3f67ec9..f9c5928d7727 100644 --- a/qiskit/transpiler/passes/optimization/__init__.py +++ b/qiskit/transpiler/passes/optimization/__init__.py @@ -38,3 +38,4 @@ from .elide_permutations import ElidePermutations from .normalize_rx_angle import NormalizeRXAngle from .optimize_annotated import OptimizeAnnotated +from .drop_negligible import DropNegligible diff --git a/qiskit/transpiler/passes/optimization/drop_negligible.py b/qiskit/transpiler/passes/optimization/drop_negligible.py new file mode 100644 index 000000000000..a8895a768cd0 --- /dev/null +++ b/qiskit/transpiler/passes/optimization/drop_negligible.py @@ -0,0 +1,70 @@ +# 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 numpy as np +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.""" + + def __init__(self, atol: float = 1e-8) -> None: + """Initialize the transpiler pass. + + Args: + atol: Absolute numerical tolerance for determining whether a gate's effect + is negligible. + """ + self.atol = atol + super().__init__() + + def run(self, dag: DAGCircuit) -> DAGCircuit: + for node in dag.op_nodes(): + if not isinstance(node.op, DROP_NEGLIGIBLE_GATE_CLASSES): + continue + if not all(isinstance(param, (int, float, complex)) for param in node.op.params): + continue + if np.allclose(node.op.params, 0, atol=self.atol): + dag.remove_op_node(node) + return dag diff --git a/test/python/transpiler/test_drop_negligible.py b/test/python/transpiler/test_drop_negligible.py new file mode 100644 index 000000000000..4f65d4342347 --- /dev/null +++ b/test/python/transpiler/test_drop_negligible.py @@ -0,0 +1,112 @@ +# 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 import QuantumCircuit, QuantumRegister +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 import PassManager +from qiskit.transpiler.passes import DropNegligible + +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]) + pass_manager = PassManager([DropNegligible()]) + transpiled = pass_manager.run(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]) + pass_manager = PassManager([DropNegligible()]) + transpiled = pass_manager.run(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]) + pass_manager = PassManager([DropNegligible(atol=1e-5)]) + transpiled = pass_manager.run(circuit) + self.assertEqual(circuit.count_ops()["cp"], 3) + self.assertEqual(transpiled.count_ops()["cp"], 1) From f358c1e6afe120feb7434dd25711494697348a61 Mon Sep 17 00:00:00 2001 From: "Kevin J. Sung" Date: Fri, 10 May 2024 12:13:26 -0400 Subject: [PATCH 02/17] use math.isclose instead of numpy.allclose --- qiskit/transpiler/passes/optimization/drop_negligible.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/qiskit/transpiler/passes/optimization/drop_negligible.py b/qiskit/transpiler/passes/optimization/drop_negligible.py index a8895a768cd0..50fee79c3b2b 100644 --- a/qiskit/transpiler/passes/optimization/drop_negligible.py +++ b/qiskit/transpiler/passes/optimization/drop_negligible.py @@ -14,7 +14,10 @@ from __future__ import annotations +import math + import numpy as np + from qiskit.circuit.library import ( CPhaseGate, PhaseGate, @@ -65,6 +68,8 @@ def run(self, dag: DAGCircuit) -> DAGCircuit: continue if not all(isinstance(param, (int, float, complex)) for param in node.op.params): continue - if np.allclose(node.op.params, 0, atol=self.atol): + 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 From 136c4765ae80a12ee0df45fde2d73f931ed9cab3 Mon Sep 17 00:00:00 2001 From: "Kevin J. Sung" Date: Fri, 10 May 2024 12:15:26 -0400 Subject: [PATCH 03/17] use pass directly instead of constructing pass manager --- test/python/transpiler/test_drop_negligible.py | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/test/python/transpiler/test_drop_negligible.py b/test/python/transpiler/test_drop_negligible.py index 4f65d4342347..6000b87858c3 100644 --- a/test/python/transpiler/test_drop_negligible.py +++ b/test/python/transpiler/test_drop_negligible.py @@ -28,7 +28,6 @@ XXPlusYYGate, ) from qiskit.quantum_info import Operator -from qiskit.transpiler import PassManager from qiskit.transpiler.passes import DropNegligible from test import QiskitTestCase # pylint: disable=wrong-import-order @@ -60,8 +59,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]) - pass_manager = PassManager([DropNegligible()]) - transpiled = pass_manager.run(circuit) + transpiled = DropNegligible()(circuit) self.assertEqual(circuit.count_ops()["cp"], 2) self.assertEqual(transpiled.count_ops()["cp"], 1) self.assertEqual(circuit.count_ops()["rx"], 2) @@ -93,8 +91,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]) - pass_manager = PassManager([DropNegligible()]) - transpiled = pass_manager.run(circuit) + transpiled = DropNegligible()(circuit) self.assertEqual(circuit.count_ops()["cp"], 3) self.assertEqual(transpiled.count_ops()["cp"], 2) @@ -106,7 +103,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]) - pass_manager = PassManager([DropNegligible(atol=1e-5)]) - transpiled = pass_manager.run(circuit) + transpiled = DropNegligible(atol=1e-5)(circuit) self.assertEqual(circuit.count_ops()["cp"], 3) self.assertEqual(transpiled.count_ops()["cp"], 1) From 9f579384a9f8c574e8128c7afc6b585ecad20246 Mon Sep 17 00:00:00 2001 From: "Kevin J. Sung" Date: Fri, 10 May 2024 12:16:52 -0400 Subject: [PATCH 04/17] lint --- qiskit/transpiler/passes/optimization/drop_negligible.py | 2 -- test/python/transpiler/test_drop_negligible.py | 1 - 2 files changed, 3 deletions(-) diff --git a/qiskit/transpiler/passes/optimization/drop_negligible.py b/qiskit/transpiler/passes/optimization/drop_negligible.py index 50fee79c3b2b..cb4cd0842119 100644 --- a/qiskit/transpiler/passes/optimization/drop_negligible.py +++ b/qiskit/transpiler/passes/optimization/drop_negligible.py @@ -16,8 +16,6 @@ import math -import numpy as np - from qiskit.circuit.library import ( CPhaseGate, PhaseGate, diff --git a/test/python/transpiler/test_drop_negligible.py b/test/python/transpiler/test_drop_negligible.py index 6000b87858c3..367d9b514dd4 100644 --- a/test/python/transpiler/test_drop_negligible.py +++ b/test/python/transpiler/test_drop_negligible.py @@ -14,7 +14,6 @@ import numpy as np -from qiskit import QuantumCircuit, QuantumRegister from qiskit.circuit import Parameter, QuantumCircuit, QuantumRegister from qiskit.circuit.library import ( CPhaseGate, From 035eae10ae8d740c2ab13dfee62a4cc89d2714ac Mon Sep 17 00:00:00 2001 From: "Kevin J. Sung" Date: Fri, 10 May 2024 13:19:09 -0400 Subject: [PATCH 05/17] allow passing additional gate types --- .../passes/optimization/drop_negligible.py | 33 +++++++++++++++++-- .../python/transpiler/test_drop_negligible.py | 28 +++++++++++++++- 2 files changed, 57 insertions(+), 4 deletions(-) diff --git a/qiskit/transpiler/passes/optimization/drop_negligible.py b/qiskit/transpiler/passes/optimization/drop_negligible.py index cb4cd0842119..a23ec77d47bc 100644 --- a/qiskit/transpiler/passes/optimization/drop_negligible.py +++ b/qiskit/transpiler/passes/optimization/drop_negligible.py @@ -15,6 +15,7 @@ from __future__ import annotations import math +from collections.abc import Iterable from qiskit.circuit.library import ( CPhaseGate, @@ -48,21 +49,47 @@ class DropNegligible(TransformationPass): - """Drop gates with negligible effects.""" + """Drop gates with negligible effects. - def __init__(self, atol: float = 1e-8) -> None: + 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, DROP_NEGLIGIBLE_GATE_CLASSES): + if not isinstance(node.op, self.gate_types): continue if not all(isinstance(param, (int, float, complex)) for param in node.op.params): continue diff --git a/test/python/transpiler/test_drop_negligible.py b/test/python/transpiler/test_drop_negligible.py index 367d9b514dd4..4390cec4d854 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 Parameter, QuantumCircuit, QuantumRegister +from qiskit.circuit import Gate, Parameter, QuantumCircuit, QuantumRegister from qiskit.circuit.library import ( CPhaseGate, RXGate, @@ -105,3 +105,29 @@ def test_handles_number_types(self): transpiled = DropNegligible(atol=1e-5)(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): + pass + + class TestGateB(Gate): + 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) From 5483856cc1f01532a6e90fd187b6e46386948b7b Mon Sep 17 00:00:00 2001 From: "Kevin J. Sung" Date: Fri, 10 May 2024 18:01:06 -0400 Subject: [PATCH 06/17] add docstrings to test classes --- test/python/transpiler/test_drop_negligible.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/test/python/transpiler/test_drop_negligible.py b/test/python/transpiler/test_drop_negligible.py index 4390cec4d854..14f29cd06b43 100644 --- a/test/python/transpiler/test_drop_negligible.py +++ b/test/python/transpiler/test_drop_negligible.py @@ -110,9 +110,11 @@ 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) From da209e3c4b8bc0ec27a18b4ccc888729b78303b8 Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Sat, 19 Oct 2024 13:45:33 -0400 Subject: [PATCH 07/17] 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) From 7672775ad9495d8cf82baf05aca84abb8f22623b Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Wed, 23 Oct 2024 13:19:19 -0400 Subject: [PATCH 08/17] Don't remove zero qubit gates --- crates/accelerate/src/remove_identity_equiv.rs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/crates/accelerate/src/remove_identity_equiv.rs b/crates/accelerate/src/remove_identity_equiv.rs index 44a95cb464e2..4c0e268990a6 100644 --- a/crates/accelerate/src/remove_identity_equiv.rs +++ b/crates/accelerate/src/remove_identity_equiv.rs @@ -76,6 +76,10 @@ fn remove_identity_equiv( 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; @@ -88,6 +92,10 @@ fn remove_identity_equiv( } } 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; From c5a8b1dfe17744edcece99ac73b04cca77b2d2ef Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Wed, 23 Oct 2024 13:24:39 -0400 Subject: [PATCH 09/17] Ensure computed gate error is always positive --- crates/accelerate/src/remove_identity_equiv.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/accelerate/src/remove_identity_equiv.rs b/crates/accelerate/src/remove_identity_equiv.rs index 4c0e268990a6..0da892d16339 100644 --- a/crates/accelerate/src/remove_identity_equiv.rs +++ b/crates/accelerate/src/remove_identity_equiv.rs @@ -86,7 +86,7 @@ fn remove_identity_equiv( 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 { + if (1. - gate_fidelity).abs() < error { remove_list.push(op_node) } } @@ -102,7 +102,7 @@ fn remove_identity_equiv( 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 { + if (1. - gate_fidelity).abs() < error { remove_list.push(op_node) } } From d1e42a4a969e943c47b7541862979b719e8737f3 Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Thu, 24 Oct 2024 09:01:44 -0400 Subject: [PATCH 10/17] Pass docstring improvements Co-authored-by: Julien Gacon --- .../passes/optimization/remove_identity_equiv.py | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/qiskit/transpiler/passes/optimization/remove_identity_equiv.py b/qiskit/transpiler/passes/optimization/remove_identity_equiv.py index b19eb5971fe2..c1f62297b8da 100644 --- a/qiskit/transpiler/passes/optimization/remove_identity_equiv.py +++ b/qiskit/transpiler/passes/optimization/remove_identity_equiv.py @@ -25,6 +25,17 @@ class RemoveIdentityEquivalent(TransformationPass): 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__( @@ -36,7 +47,7 @@ def __init__( 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 + 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 From 1568105bc87b9e8ab02a220cf80daf965d3013d1 Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Mon, 28 Oct 2024 10:16:37 -0400 Subject: [PATCH 11/17] Move test file --- ...test_drop_negligible.py => test_remove_identity_equivalent.py} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename test/python/transpiler/{test_drop_negligible.py => test_remove_identity_equivalent.py} (100%) diff --git a/test/python/transpiler/test_drop_negligible.py b/test/python/transpiler/test_remove_identity_equivalent.py similarity index 100% rename from test/python/transpiler/test_drop_negligible.py rename to test/python/transpiler/test_remove_identity_equivalent.py From d255dd7501db01ef24e1222dcf4540b656e687ac Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Mon, 28 Oct 2024 10:22:37 -0400 Subject: [PATCH 12/17] Fix lint --- .../passes/optimization/remove_identity_equiv.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/qiskit/transpiler/passes/optimization/remove_identity_equiv.py b/qiskit/transpiler/passes/optimization/remove_identity_equiv.py index c1f62297b8da..a316f29cfd05 100644 --- a/qiskit/transpiler/passes/optimization/remove_identity_equiv.py +++ b/qiskit/transpiler/passes/optimization/remove_identity_equiv.py @@ -25,16 +25,16 @@ class RemoveIdentityEquivalent(TransformationPass): 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 - + 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} - + \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. """ From 0b126be367f253b1ff5cc1dc5ac4647c42e53fe6 Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Mon, 28 Oct 2024 10:23:16 -0400 Subject: [PATCH 13/17] Fix docs --- .../notes/remove_identity_equiv-9c627c8c35b2298a.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/releasenotes/notes/remove_identity_equiv-9c627c8c35b2298a.yaml b/releasenotes/notes/remove_identity_equiv-9c627c8c35b2298a.yaml index 85be9cb78306..90d016803d62 100644 --- a/releasenotes/notes/remove_identity_equiv-9c627c8c35b2298a.yaml +++ b/releasenotes/notes/remove_identity_equiv-9c627c8c35b2298a.yaml @@ -10,7 +10,7 @@ features_transpiler: from qiskit.circuit import QuantumCircuit qc = QuantumCircuit(2) - qc.cp(1e-20, [0, 1]) + qc.cp(1e-20, 0, 1) qc.draw("mpl") running the pass would eliminate the :class:`.CPhaseGate`: @@ -22,7 +22,7 @@ features_transpiler: from qiskit.transpiler.passes import RemoveIdentityEquivalent qc = QuantumCircuit(2) - qc.cp(1e-20, [0, 1]) + qc.cp(1e-20, 0, 1) removal_pass = RemoveIdentityEquivalent() result = removal_pass(qc) From 835cd35575bad8d635f95e37d4ca49f5dc288641 Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Mon, 28 Oct 2024 10:58:09 -0400 Subject: [PATCH 14/17] Add optimized identity check for Pauli gates Co-authored-by: Julien Gacon --- .../accelerate/src/remove_identity_equiv.rs | 49 ++++++++++++++----- 1 file changed, 37 insertions(+), 12 deletions(-) diff --git a/crates/accelerate/src/remove_identity_equiv.rs b/crates/accelerate/src/remove_identity_equiv.rs index 0da892d16339..81b656b00418 100644 --- a/crates/accelerate/src/remove_identity_equiv.rs +++ b/crates/accelerate/src/remove_identity_equiv.rs @@ -20,6 +20,8 @@ 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] @@ -76,18 +78,41 @@ fn remove_identity_equiv( 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) + match gate { + StandardGate::RXGate | StandardGate::RYGate | StandardGate::RZGate => { + if let Param::Float(theta) = inst.params_view()[0] { + let error = get_error_cutoff(inst); + if (theta / 2.).cos() * 2. < error { + remove_list.push(op_node); + } + } + } + StandardGate::RXXGate + | StandardGate::RYYGate + | StandardGate::RZZGate + | StandardGate::RZXGate => { + if let Param::Float(theta) = inst.params_view()[0] { + let error = get_error_cutoff(inst); + if (theta / 2.).cos() * 4. < error { + remove_list.push(op_node); + } + } + } + _ => { + // 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) + } + } } } } From 4e3a76be3b19bab199078fddc551ac1e7adef355 Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Mon, 28 Oct 2024 11:04:04 -0400 Subject: [PATCH 15/17] Mention zero qubit gates in the docstring --- .../transpiler/passes/optimization/remove_identity_equiv.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/qiskit/transpiler/passes/optimization/remove_identity_equiv.py b/qiskit/transpiler/passes/optimization/remove_identity_equiv.py index a316f29cfd05..3c3dc80cff9b 100644 --- a/qiskit/transpiler/passes/optimization/remove_identity_equiv.py +++ b/qiskit/transpiler/passes/optimization/remove_identity_equiv.py @@ -24,7 +24,9 @@ class RemoveIdentityEquivalent(TransformationPass): """Remove gates with negligible effects. Removes gates whose effect is close to an identity operation, up to the specified - tolerance. + 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 From 09b7c4769eb18698a65299432b686aee8985c164 Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Mon, 28 Oct 2024 11:14:32 -0400 Subject: [PATCH 16/17] Update approximation degree logic in absense of target --- crates/accelerate/src/remove_identity_equiv.rs | 4 ++-- .../transpiler/passes/optimization/remove_identity_equiv.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/crates/accelerate/src/remove_identity_equiv.rs b/crates/accelerate/src/remove_identity_equiv.rs index 81b656b00418..234c1e5eac0d 100644 --- a/crates/accelerate/src/remove_identity_equiv.rs +++ b/crates/accelerate/src/remove_identity_equiv.rs @@ -49,10 +49,10 @@ fn remove_identity_equiv( let error_rate = target.get_error(inst.op.name(), qargs.as_slice()); match error_rate { Some(err) => err * degree, - None => degree, + None => f64::EPSILON.max(1. - degree), } } - None => degree, + None => f64::EPSILON.max(1. - degree), } } } diff --git a/qiskit/transpiler/passes/optimization/remove_identity_equiv.py b/qiskit/transpiler/passes/optimization/remove_identity_equiv.py index 3c3dc80cff9b..4f8551f12442 100644 --- a/qiskit/transpiler/passes/optimization/remove_identity_equiv.py +++ b/qiskit/transpiler/passes/optimization/remove_identity_equiv.py @@ -21,7 +21,7 @@ class RemoveIdentityEquivalent(TransformationPass): - """Remove gates with negligible effects. + 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 From 235c610438732afd325940c065c8f08d82c7250c Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Mon, 28 Oct 2024 13:04:55 -0400 Subject: [PATCH 17/17] Expand test coverage and fix bugs --- .../accelerate/src/remove_identity_equiv.rs | 35 +++---- .../test_remove_identity_equivalent.py | 92 +++++++++++++++++-- 2 files changed, 104 insertions(+), 23 deletions(-) diff --git a/crates/accelerate/src/remove_identity_equiv.rs b/crates/accelerate/src/remove_identity_equiv.rs index 234c1e5eac0d..a3eb921628e2 100644 --- a/crates/accelerate/src/remove_identity_equiv.rs +++ b/crates/accelerate/src/remove_identity_equiv.rs @@ -78,13 +78,13 @@ fn remove_identity_equiv( let inst = dag.dag()[op_node].unwrap_operation(); match inst.op.view() { OperationRef::Standard(gate) => { - match gate { + let (dim, trace) = match gate { StandardGate::RXGate | StandardGate::RYGate | StandardGate::RZGate => { if let Param::Float(theta) = inst.params_view()[0] { - let error = get_error_cutoff(inst); - if (theta / 2.).cos() * 2. < error { - remove_list.push(op_node); - } + let trace = (theta / 2.).cos() * 2.; + (2., trace) + } else { + continue; } } StandardGate::RXXGate @@ -92,10 +92,10 @@ fn remove_identity_equiv( | StandardGate::RZZGate | StandardGate::RZXGate => { if let Param::Float(theta) = inst.params_view()[0] { - let error = get_error_cutoff(inst); - if (theta / 2.).cos() * 4. < error { - remove_list.push(op_node); - } + let trace = (theta / 2.).cos() * 4.; + (4., trace) + } else { + continue; } } _ => { @@ -104,16 +104,19 @@ fn remove_identity_equiv( 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) - } + 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) => { diff --git a/test/python/transpiler/test_remove_identity_equivalent.py b/test/python/transpiler/test_remove_identity_equivalent.py index a2d183a1a1b8..1db392d3654b 100644 --- a/test/python/transpiler/test_remove_identity_equivalent.py +++ b/test/python/transpiler/test_remove_identity_equivalent.py @@ -14,7 +14,7 @@ import numpy as np -from qiskit.circuit import Parameter, QuantumCircuit, QuantumRegister +from qiskit.circuit import Parameter, QuantumCircuit, QuantumRegister, Gate from qiskit.circuit.library import ( CPhaseGate, RXGate, @@ -25,9 +25,11 @@ 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 @@ -94,14 +96,90 @@ def test_handles_parameters(self): 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.""" + def test_approximation_degree(self): + """Test that approximation degree 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) + 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)