-
Notifications
You must be signed in to change notification settings - Fork 2.4k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add RemoveIdentityEquivalent transpiler pass #12384
base: main
Are you sure you want to change the base?
Changes from all commits
e442e7a
f358c1e
136c476
9f57938
035eae1
5483856
c915797
da209e3
8fd8bcf
7672775
c5a8b1d
d1e42a4
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change | ||||||||
---|---|---|---|---|---|---|---|---|---|---|
@@ -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<f64>, | ||||||||||
target: Option<&Target>, | ||||||||||
) { | ||||||||||
let mut remove_list: Vec<NodeIndex> = 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<PhysicalQubit> = 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<PhysicalQubit> = 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) => { | ||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. How about adding a fast-path for Pauli rotations? We can compute their traces as Tr(exp(-i th/2 P)) = Tr(B exp(-i th/2 Z) B^\dagger) = Tr(exp(-i th/2 Z)) = exp(-i th/2) + exp(i th / 2) = 2 cos(th/2) For 2-qubit Pauli rotations we'd have ZZ instead and 4 terms, so that should be |
||||||||||
// 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.); | ||||||||||
Comment on lines
+87
to
+88
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This would save 1 flop (I don't remember the HPC courses enough though to remember if mult and div have the same cost) 😄
Suggested change
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. In hardware in most cases division is more expensive. For example, on my workstation's CPU architecture f64 division is 13 cycles while multiplication is 5 cycles: but the compiler knows this and will try to optimize away division or use different instructions for the same output if it can so it's not always clear cut. The only way to know for sure is to try it both ways and benchmark it. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Compilers often can't optimise away floating-point divisions because of rounding differences. It's what |
||||||||||
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<PyModule>) -> PyResult<()> { | ||||||||||
m.add_wrapped(wrap_pyfunction!(remove_identity_equiv))?; | ||||||||||
Ok(()) | ||||||||||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This should mention that |
||
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. | ||
""" | ||
mtreinish marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
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 |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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") |
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This test file and class name in it still use the old name - can we make sure they're the same so we don't lose the test file in the future? |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm a bit confused here: shouldn't the error cutoff be inversely proportional to the approximation degree? E.g.
approximation_degree=1
impliescutoff=f64::EPSILON
, as expected. But if I'm reading this correctlyapproximation_degree=0.99
and no target would givecutoff=0.99
? 🤔Shouldn't this be something like
cutoff = max(f64::EPSILON, 1 - approximation_degree)
in the case of no target?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah yeah, good call this logic is incorrect. I'll update these