Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 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
39 changes: 39 additions & 0 deletions crates/circuit/src/operations.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3372,6 +3372,45 @@ impl PauliProductRotation {
)?;
Ok(gate.unbind())
}

/// Attempts to merge `self` and `other`.
/// If successful, returns the merged `PauliProductRotation.
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

The quotes are a bit off here and below, also we could use [...] since it's Rust docs, right?

Suggested change
/// If successful, returns the merged `PauliProductRotation.
/// If successful, returns the merged [PauliProductRotation].

/// If not successful, returns `None`.
pub fn merge_with(&self, other: &Self) -> Option<Self> {
if self.x == other.x && self.z == other.z {
Some(PauliProductRotation {
z: self.z.clone(),
x: self.x.clone(),
angle: radd_param(self.angle.clone(), other.angle.clone()),
})
} else {
None
}
}

/// For a `PauliProductRotation`` gate with a floating-point angle return a tuple ``(Tr(gate) / dim, dim)``.
/// Return `None` if the angle is parameterized.
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Suggested change
/// For a `PauliProductRotation`` gate with a floating-point angle return a tuple ``(Tr(gate) / dim, dim)``.
/// Return `None` if the angle is parameterized.
/// For a [PauliProductRotation] gate with a floating-point angle return a tuple `(Tr(gate) / dim, dim)`.
/// Return `None` if the angle is parameterized.

pub fn rotation_trace_and_dim(&self) -> Option<(Complex64, f64)> {
let Param::Float(angle) = self.angle else {
return None;
};

let num_qubits = self
.z
.iter()
.zip(self.x.iter())
.filter(|(z, x)| **z || **x)
.count();
let dim = 2u32.pow(num_qubits as u32);
let tr_over_dim = if num_qubits == 0 {
// This is an identity Pauli rotation.
(Complex64::new(0.0, -angle / 2.)).exp()
} else {
Complex64::new((angle / 2.).cos(), 0.)
};

Some((tr_over_dim, dim as f64))
}
}

impl PartialEq for PauliProductRotation {
Expand Down
30 changes: 29 additions & 1 deletion crates/transpiler/src/passes/commutative_optimization.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ use crate::passes::remove_identity_equiv::{average_gate_fidelity_below_tol, is_i
use qiskit_circuit::circuit_instruction::OperationFromPython;
use qiskit_circuit::dag_circuit::DAGCircuit;
use qiskit_circuit::operations::{
Operation, OperationRef, Param, StandardGate, multiply_param, radd_param,
Operation, OperationRef, Param, PauliBased, StandardGate, multiply_param, radd_param,
};
use qiskit_circuit::{BlocksMode, Clbit, NoBlocks, Qubit, imports};

Expand Down Expand Up @@ -309,6 +309,34 @@ fn try_merge(
}
}

// Special handling for PauliProductRotations.
if let (OperationRef::PauliProductRotation(ppr1), OperationRef::PauliProductRotation(ppr2)) =
(inst1.op.view(), inst2.op.view())
{
let merge_result = ppr1.merge_with(ppr2);

if let Some(merged_ppr) = merge_result {
let angle = merged_ppr.angle.clone();
let merged_params = Some(Box::new(Parameters::Params(smallvec![angle])));

let packed = PackedInstruction {
op: PauliBased::PauliProductRotation(merged_ppr).into(),
qubits: inst1.qubits,
clbits: inst1.clbits,
params: merged_params,
label: None,
#[cfg(feature = "cache_pygates")]
py_op: std::sync::OnceLock::new(),
};

if let Some(phase_update) = is_identity_equiv(&packed, false, None, error_cutoff_fn)? {
return Ok((true, None, phase_update));
} else {
return Ok((true, Some(packed), 0.));
}
}
}

// Special handling for PauliEvolutionGates.
if inst1.op.name() == "PauliEvolution" && inst2.op.name() == "PauliEvolution" {
if let (OperationRef::Gate(py_gate1), OperationRef::Gate(py_gate2)) =
Expand Down
16 changes: 15 additions & 1 deletion crates/transpiler/src/passes/remove_identity_equiv.rs
Original file line number Diff line number Diff line change
Expand Up @@ -179,7 +179,21 @@ where
));
}

// Special handling for large pauli rotation gates.
// Special handling for pauli product rotation gates.
if let OperationRef::PauliProductRotation(ppr) = view {
if let Some((tr_over_dim, dim)) = ppr.rotation_trace_and_dim() {
return Ok(average_gate_fidelity_below_tol(
tr_over_dim,
dim,
error_cutoff_fn(inst),
));
} else {
// Parameterized rotation
return Ok(None);
}
}

// Special handling for pauli evolution gates.
if view.name() == "PauliEvolution" {
if let OperationRef::Gate(py_gate) = view {
let result = Python::attach(|py| -> PyResult<Option<(Complex64, usize)>> {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
features_transpiler:
- |
The transpiler passes :class:`.RemoveIdentityEquivalent` and :class:`.CommutativeOptimization`
have been extended to handle :class:`.PauliProductRotationGate` gates.
45 changes: 44 additions & 1 deletion test/python/transpiler/test_commutative_optimization.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
PhaseGate,
UnitaryGate,
PauliEvolutionGate,
PauliProductRotationGate,
Initialize,
U2Gate,
CZGate,
Expand All @@ -43,7 +44,7 @@
)
from qiskit.circuit.parameter import Parameter
from qiskit.transpiler.passes import CommutativeOptimization
from qiskit.quantum_info import Operator, SparsePauliOp, Clifford
from qiskit.quantum_info import Operator, SparsePauliOp, Clifford, Pauli

from test import QiskitTestCase

Expand Down Expand Up @@ -313,6 +314,48 @@ def test_not_merge_pauli_evolutions(self):

self.assertEqual(qct, qc)

def test_merge_pauli_product_rotations(self):
"""Test that the pass merges PauliProductRotationGates."""

qc = QuantumCircuit(4)
qc.append(PauliProductRotationGate(Pauli("XXII"), -1), [0, 1, 2, 3])
qc.append(PauliProductRotationGate(Pauli("ZZIY"), 1), [0, 1, 2, 3])
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Could you shuffle the indices here to ensure the merging is not dependent on the index order? E.g.

Suggested change
qc.append(PauliProductRotationGate(Pauli("ZZIY"), 1), [0, 1, 2, 3])
qc.append(PauliProductRotationGate(Pauli("ZZYI"), 1), [1, 0, 2, 3])

(assuming I got it right here lol)

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.

Nice catch, the current PR does not support the suggested merging yet, but I can add it by canonicalizing PauliProductRotations/PauliProductMeasurements to have their qubits sorted by index. But of course this will incur some overhead.

If we could assume that the qubits of all PPRs/PPMs in a circuit are sorted, then we could implement commutation checking very efficiently (see the follow-up PR #15815, especially the comment on other potential optimizations).

When we canonicalize gates inside the CommutativeOptimization pass, we only do it tentatively, which means that when a gate is not canceled or merged we use the original instance of the gate in the final circuit. Should we perhaps change this behavior to always canonicalize PPRs/PPMs as a side-effect of the CommutativeOptimization pass? Should we do it in a separate pass?

Having PPRs/PPMs sorted by index would improve performance when running CommutativeOptimizations multiple times in the optimization loop. What do you think?

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.

Update: the time for canonicalizing PauliProductRotations by sorting the qubits (and adjusting the Paulis) is negligible both for small- and large- support gates. So this is now implemented in bdbc70d.

qc.append(PauliProductRotationGate(Pauli("ZZIY"), 1), [0, 1, 2, 3])
qc.append(PauliProductRotationGate(Pauli("YYXX"), 1), [0, 1, 2, 3])
qc.append(PauliProductRotationGate(Pauli("XXII"), 1), [0, 1, 2, 3])

# The two "XXII" rotations should cancel.
# The two "ZZIY" rotations should get combined.
qct = CommutativeOptimization()(qc)

expected = QuantumCircuit(4)
expected.append(PauliProductRotationGate(Pauli("ZZIY"), 2), [0, 1, 2, 3])
expected.append(PauliProductRotationGate(Pauli("YYXX"), 1), [0, 1, 2, 3])

self.assertEqual(Operator(expected), Operator(qc))
self.assertEqual(qct, expected)

def test_merge_parameterized_pauli_product_rotations(self):
"""Test that the pass merges parameterized PauliProductRotationGates."""

p = Parameter("p")
q = Parameter("q")
r = Parameter("r")

qc = QuantumCircuit(4)
qc.append(PauliProductRotationGate(Pauli("YYXX"), p), [0, 1, 2, 3])
qc.append(PauliProductRotationGate(Pauli("YYXX"), q), [0, 1, 2, 3])
qc.append(PauliProductRotationGate(Pauli("ZXXX"), r), [0, 1, 2, 3])
qc.append(PauliProductRotationGate(Pauli("YYXX"), -1), [0, 1, 2, 3])

qct = CommutativeOptimization()(qc)

expected = QuantumCircuit(4)
expected.append(PauliProductRotationGate(Pauli("ZXXX"), r), [0, 1, 2, 3])
expected.append(PauliProductRotationGate(Pauli("YYXX"), p + q - 1), [0, 1, 2, 3])

self.assertEqual(qct, expected)

def test_2pi_multiples(self):
"""Test 2pi multiples are handled with the correct phase they introduce."""
for eps in [0, 1e-10, -1e-10]:
Expand Down
2 changes: 2 additions & 0 deletions test/python/transpiler/test_remove_identity_equivalent.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
XXPlusYYGate,
GlobalPhaseGate,
UnitaryGate,
PauliProductRotationGate,
PauliEvolutionGate,
)
from qiskit.quantum_info import Operator, Pauli
Expand Down Expand Up @@ -192,6 +193,7 @@ def to_matrix(self):
UnitaryGate(np.exp(-0.123j) * np.eye(2)),
UnitaryGate(np.exp(-0.123j) * np.eye(4)),
UnitaryGate(np.exp(-0.123j) * np.eye(8)),
PauliProductRotationGate(Pauli("XYIZ"), 0),
)
def test_remove_identity_up_to_global_phase(self, gate):
"""Test that gates equivalent to identity up to a global phase are removed from the circuit,
Expand Down