Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
62 changes: 47 additions & 15 deletions crates/transpiler/src/passes/commutation_cancellation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ use smallvec::{SmallVec, smallvec};

use super::analyze_commutations;
use crate::commutation_checker::CommutationChecker;
use approx::abs_diff_eq;
use qiskit_circuit::Qubit;
use qiskit_circuit::dag_circuit::{DAGCircuit, NodeType};
use qiskit_circuit::operations::{Operation, Param, StandardGate};
Expand All @@ -34,15 +35,22 @@ static VAR_Z_MAP: [(&str, StandardGate); 3] = [
("p", StandardGate::Phase),
("u1", StandardGate::U1),
];
static Z_ROTATIONS: [StandardGate; 6] = [
static Z_ROTATIONS: [StandardGate; 8] = [
StandardGate::Phase,
StandardGate::Z,
StandardGate::U1,
StandardGate::RZ,
StandardGate::T,
StandardGate::Tdg,
Comment thread
mtreinish marked this conversation as resolved.
StandardGate::S,
StandardGate::Sdg,
];
static X_ROTATIONS: [StandardGate; 4] = [
StandardGate::X,
StandardGate::RX,
StandardGate::SX,
StandardGate::SXdg,
];
Comment thread
ShellyGarion marked this conversation as resolved.
static X_ROTATIONS: [StandardGate; 2] = [StandardGate::X, StandardGate::RX];
static SUPPORTED_GATES: [StandardGate; 5] = [
StandardGate::CX,
StandardGate::CY,
Expand Down Expand Up @@ -92,6 +100,8 @@ pub fn cancel_commutations(
.map(|(_, gate)| gate)
})
});
let sx_supported = dag.get_op_counts().contains_key("sx") || basis.iter().any(|x| x == "sx");

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

General question: do we only run CC before routing? If no we'd have to take into account whether the wire contains SX/X instead of the DAG in general

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We run it both before and after routing (at optimization level 2 and 3). The pass is not target aware right now at all. It doesn't understand heterogeneous gate sets or per qubit requirements. Even though the pass takes a target it in reduces that to a global gate name list and then acts upon it here.

We can work on making the pass truly target aware but I think that should be a dedicated PR because it probably will need a larger refactor than what this is adding. I was also hesitant to work on this pass too much if we're planning to replace it with CommutativeOptimization eventually.

let x_supported = dag.get_op_counts().contains_key("x") || basis.iter().any(|x| x == "x");

// RZ and P/U1 have a phase difference of angle/2, which we need to account for
let z_phase_shift = match z_var_gate {
Expand Down Expand Up @@ -247,9 +257,13 @@ pub fn cancel_commutations(
} else {
match node_op_name {
"t" => Ok((FRAC_PI_4, z_phase_shift("p", FRAC_PI_4))),
"tdg" => Ok((-FRAC_PI_4, -z_phase_shift("p", FRAC_PI_4))),
"s" => Ok((FRAC_PI_2, z_phase_shift("p", FRAC_PI_2))),
"sdg" => Ok((-FRAC_PI_2, -z_phase_shift("p", FRAC_PI_2))),
"z" => Ok((PI, z_phase_shift("p", PI))),
"x" => Ok((PI, FRAC_PI_2)),
"sx" => Ok((FRAC_PI_2, FRAC_PI_4)),
"sxdg" => Ok((-FRAC_PI_2, -FRAC_PI_4)),
_ => Err(PyRuntimeError::new_err(format!(
"Angle for operation {node_op_name} is not defined"
))),
Expand All @@ -259,24 +273,34 @@ pub fn cancel_commutations(
total_phase += phase_shift;
}

let new_op = match cancel_key.gate {
GateOrRotation::ZRotation => z_var_gate.unwrap(),
GateOrRotation::XRotation => &StandardGate::RX,
_ => unreachable!(),
};

let pi_multiple = total_angle / PI;

let mod4 = pi_multiple.rem_euclid(4.);
if mod4 < _CUTOFF_PRECISION || (4. - mod4) < _CUTOFF_PRECISION {
if is_multiple_of_pi(total_angle, 4.) {
// if the angle is close to a 4-pi multiple (from above or below), then the
// operator is equal to the identity
} else if (mod4 - 2.).abs() < _CUTOFF_PRECISION {
} else if is_multiple_of_pi(total_angle, 2.) {
// a 2-pi multiple has a phase of pi: RX(2pi) = RZ(2pi) = -I = I exp(i pi)
total_phase -= PI;
} else if cancel_key.gate == GateOrRotation::ZRotation {
let z_gate = z_var_gate.unwrap();
dag.insert_1q_on_incoming_qubit((*z_gate, &[total_angle]), cancel_set[0]);
} else {
// any other is not the identity and we add the gate
dag.insert_1q_on_incoming_qubit((*new_op, &[total_angle]), cancel_set[0]);
// cancel_gate.key is either ZRotation or XRotation in this block it is an
// XRotation.
if x_supported && is_multiple_of_pi(total_angle, 1.) {
let num_x = (total_angle / PI).round();
total_phase -= FRAC_PI_2 * num_x;
dag.insert_1q_on_incoming_qubit((StandardGate::X, &[]), cancel_set[0]);
} else if sx_supported && is_multiple_of_pi(total_angle, 0.5) {
let num_sx = (total_angle / FRAC_PI_2).round();
total_phase -= FRAC_PI_4 * num_sx;
for _ in 0..(num_sx as i64) % 4 {
dag.insert_1q_on_incoming_qubit((StandardGate::SX, &[]), cancel_set[0]);
}
} else {
dag.insert_1q_on_incoming_qubit(
(StandardGate::RX, &[total_angle]),
cancel_set[0],
);
}
}

dag.add_global_phase(&Param::Float(total_phase))?;
Expand All @@ -291,6 +315,14 @@ pub fn cancel_commutations(
Ok(())
}

/// Checks if angle is an integer multiple of ``factor * PI``.
fn is_multiple_of_pi(angle: f64, factor: f64) -> bool {
let modulo = angle / (factor * PI);
let remainder = modulo.rem_euclid(1.0);
abs_diff_eq!(remainder, 0., epsilon = _CUTOFF_PRECISION)
|| abs_diff_eq!(remainder, 1., epsilon = _CUTOFF_PRECISION)
}

pub fn commutation_cancellation_mod(m: &Bound<PyModule>) -> PyResult<()> {
m.add_wrapped(wrap_pyfunction!(cancel_commutations))?;
Ok(())
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,8 +65,8 @@ def __init__(self, basis_gates=None, target=None):

self._var_z_map = {"rz": RZGate, "p": PhaseGate, "u1": U1Gate}

self._z_rotations = {"p", "z", "u1", "rz", "t", "s"}
self._x_rotations = {"x", "rx"}
self._z_rotations = {"p", "z", "u1", "rz", "t", "s", "tdg", "sdg"}
self._x_rotations = {"x", "rx", "sx", "sxdg"}
self._gates = {"cx", "cy", "cz", "h", "y"} # Now the gates supported are hard-coded

# build a commutation checker restricted to the gates we cancel -- the others we
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
features_transpiler:
- The :class:`.CommutativeCancellation` transpiler pass will now emit an
:class:`.XGate` or :class:`.SXGate` if the tracked X rotation is an angle
of $\pi$ or a multiple of $\frac{\pi}{2}$ respectively and either the
:class:`.Target` supports class:`.XGate` or :class:`.SXGate`, or the
circuit contains the gates.
79 changes: 78 additions & 1 deletion test/python/transpiler/test_commutative_cancellation.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
import unittest

import numpy as np
import ddt

from qiskit import QuantumRegister, QuantumCircuit
from qiskit.converters import circuit_to_dag
Expand All @@ -25,9 +26,10 @@
from qiskit.transpiler import PassManager, PropertySet
from qiskit.transpiler.passes import CommutationAnalysis, CommutativeCancellation, FixedPoint, Size
from qiskit.quantum_info import Operator
from test import QiskitTestCase
from test import QiskitTestCase, combine


@ddt.ddt
class TestCommutativeCancellation(QiskitTestCase):
"""Test the CommutativeCancellation pass."""

Expand Down Expand Up @@ -210,6 +212,81 @@ def test_consecutive_cnots2(self):
self.assertEqual(expected, new_circuit)
self.assertTrue(np.allclose(Operator(circuit).data, Operator(expected).data))

@combine(
basis_gates=[["rz", "sx", "x"], ["rz", "sx"], ["rz", "rx"]],
circuit_gate=["x", "rx", "sx"],
name="basis_gates={basis_gates}_circuit_gate={circuit_gate}",
)
def test_xgate_accumulation(self, basis_gates, circuit_gate):
Comment thread
ShellyGarion marked this conversation as resolved.
circuit = QuantumCircuit(2)
if circuit_gate == "rx":
circuit.rx(np.pi / 2, 0)
circuit.rx(np.pi / 2, 0)
elif circuit_gate == "sx":
circuit.sx(0)
circuit.sx(0)
else:
circuit.x(0)
circuit.x(0)
circuit.x(0)
commuter_pass = CommutativeCancellation(basis_gates=basis_gates)
result = commuter_pass(circuit)
op_counts = result.count_ops()
self.assertEqual(Operator(circuit), Operator(result))
if "x" in basis_gates or "x" == circuit_gate:
self.assertEqual(op_counts.get("x", 0), 1)
self.assertNotIn("sx", op_counts)
self.assertNotIn("rx", op_counts)
elif "sx" in basis_gates or "sx" == circuit_gate:
self.assertEqual(op_counts.get("sx", 0), 2)
self.assertNotIn("x", op_counts)
self.assertNotIn("rx", op_counts)
else:
self.assertEqual(op_counts.get("rx", 0), 1)
self.assertNotIn("sx", op_counts)
self.assertNotIn("x", op_counts)

@combine(
basis_gates=[["rz", "sx", "x"], ["u1", "sx"], ["p", "sx"]],
Comment thread
ShellyGarion marked this conversation as resolved.
circuit_gate=["t", "tdg", "s", "sdg", "rz", "z"],
name="basis_gates={basis_gates}_circuit_gate={circuit_gate}",
)
def test_zgate_accumulation(self, basis_gates, circuit_gate):
circuit = QuantumCircuit(2)
if circuit_gate == "t":
circuit.t(0)
circuit.t(0)
circuit.t(0)
circuit.t(0)
elif circuit_gate == "tdg":
circuit.tdg(0)
circuit.tdg(0)
circuit.tdg(0)
circuit.tdg(0)
elif circuit_gate == "s":
circuit.s(0)
circuit.s(0)
elif circuit_gate == "sdg":
circuit.sdg(0)
circuit.sdg(0)
elif circuit_gate == "rz":
circuit.rz(np.pi / 2, 0)
circuit.rz(np.pi / 2, 0)
else:
circuit.z(0)
circuit.z(0)
circuit.z(0)
commuter_pass = CommutativeCancellation(basis_gates=basis_gates)
result = commuter_pass(circuit)
op_counts = result.count_ops()
self.assertEqual(Operator(circuit), Operator(result))
if "rz" in basis_gates or circuit_gate == "rz":
self.assertEqual(op_counts, {"rz": 1})
elif "u1" in basis_gates:
self.assertEqual(op_counts, {"u1": 1})
else:
self.assertEqual(op_counts, {"p": 1})

def test_2_alternating_cnots(self):
"""A simple circuit where nothing should be cancelled.

Expand Down