Skip to content

Commit 50cb9b1

Browse files
Extend RemoveIdentityEquivalent and CommutativeOptimization with PauliProductRotations (#15810)
* Coercing integer types to floats when appending a PPR to a circuit * Extending RemoveIdentityEquiv and CommutativeOptimization to handle PPRs * reno * fix comment * review comments * Review comments * canonicalizing PPRs in CommutativeOptimization
1 parent 3942c9c commit 50cb9b1

6 files changed

Lines changed: 171 additions & 4 deletions

File tree

crates/circuit/src/operations.rs

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3532,7 +3532,7 @@ impl UnitaryGate {
35323532
}
35333533
}
35343534

3535-
/// A Pauli-based gate model, consisting of PauliProductRotation and PauliProductMeasurement ops.
3535+
/// A Pauli-based gate model, consisting of [PauliProductRotation] and [PauliProductMeasurement] ops.
35363536
#[derive(Clone, Debug, PartialEq, Eq)]
35373537
pub enum PauliBased {
35383538
PauliProductRotation(PauliProductRotation),
@@ -3587,6 +3587,45 @@ impl PauliProductRotation {
35873587
)?;
35883588
Ok(gate.unbind())
35893589
}
3590+
3591+
/// Attempts to merge `self` and `other`.
3592+
/// If successful, returns the merged [PauliProductRotation].
3593+
/// If not successful, returns `None`.
3594+
pub fn merge_with(&self, other: &Self) -> Option<Self> {
3595+
if self.x == other.x && self.z == other.z {
3596+
Some(PauliProductRotation {
3597+
z: self.z.clone(),
3598+
x: self.x.clone(),
3599+
angle: radd_param(self.angle.clone(), other.angle.clone()),
3600+
})
3601+
} else {
3602+
None
3603+
}
3604+
}
3605+
3606+
/// For a [PauliProductRotation] gate with a floating-point angle return a tuple `(Tr(gate) / dim, dim)`.
3607+
/// Return `None` if the angle is parameterized.
3608+
pub fn rotation_trace_and_dim(&self) -> Option<(Complex64, f64)> {
3609+
let Param::Float(angle) = self.angle else {
3610+
return None;
3611+
};
3612+
3613+
let num_qubits = self
3614+
.z
3615+
.iter()
3616+
.zip(self.x.iter())
3617+
.filter(|(z, x)| **z || **x)
3618+
.count();
3619+
let dim = 2u32.pow(num_qubits as u32);
3620+
let tr_over_dim = if num_qubits == 0 {
3621+
// This is an identity Pauli rotation.
3622+
(Complex64::new(0.0, -angle / 2.)).exp()
3623+
} else {
3624+
Complex64::new((angle / 2.).cos(), 0.)
3625+
};
3626+
3627+
Some((tr_over_dim, dim as f64))
3628+
}
35903629
}
35913630

35923631
impl PartialEq for PauliProductRotation {

crates/transpiler/src/passes/commutative_optimization.rs

Lines changed: 66 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@
1010
// copyright notice, and modified files need to carry a notice indicating
1111
// that they have been altered from the originals.
1212

13+
use itertools::Itertools;
14+
1315
use std::f64::consts::PI;
1416

1517
use num_complex::Complex64;
@@ -23,7 +25,8 @@ use crate::passes::remove_identity_equiv::{average_gate_fidelity_below_tol, is_i
2325
use qiskit_circuit::circuit_instruction::OperationFromPython;
2426
use qiskit_circuit::dag_circuit::DAGCircuit;
2527
use qiskit_circuit::operations::{
26-
Operation, OperationRef, Param, StandardGate, multiply_param, radd_param,
28+
Operation, OperationRef, Param, PauliBased, PauliProductRotation, StandardGate, multiply_param,
29+
radd_param,
2730
};
2831
use qiskit_circuit::{BlocksMode, Clbit, NoBlocks, Qubit, imports};
2932

@@ -192,6 +195,40 @@ fn canonicalize(
192195
}
193196
}
194197
}
198+
199+
if let OperationRef::PauliProductRotation(ppr) = inst.op.view() {
200+
let qargs = dag.get_qargs(inst.qubits);
201+
let mut paired = qargs
202+
.iter()
203+
.zip(ppr.z.iter())
204+
.zip(ppr.x.iter())
205+
.map(|((q, z), x)| (q, z, x))
206+
.collect::<Vec<_>>();
207+
paired.sort_by_key(|(q, _, _)| **q);
208+
let (sorted_qargs, sorted_z, sorted_x) =
209+
paired
210+
.into_iter()
211+
.multiunzip::<(Vec<Qubit>, Vec<bool>, Vec<bool>)>();
212+
let sorted_ppr = PauliProductRotation {
213+
z: sorted_z,
214+
x: sorted_x,
215+
angle: ppr.angle.clone(),
216+
};
217+
218+
let sorted_qubits = dag.add_qargs(&sorted_qargs);
219+
220+
let canonical_instruction = PackedInstruction {
221+
op: PauliBased::PauliProductRotation(sorted_ppr).into(),
222+
qubits: sorted_qubits,
223+
clbits: Default::default(),
224+
params: inst.params.clone(),
225+
label: None,
226+
#[cfg(feature = "cache_pygates")]
227+
py_op: std::sync::OnceLock::new(),
228+
};
229+
return Some((canonical_instruction, Param::Float(0.)));
230+
}
231+
195232
None
196233
}
197234

@@ -309,6 +346,34 @@ fn try_merge(
309346
}
310347
}
311348

349+
// Special handling for PauliProductRotations.
350+
if let (OperationRef::PauliProductRotation(ppr1), OperationRef::PauliProductRotation(ppr2)) =
351+
(inst1.op.view(), inst2.op.view())
352+
{
353+
let merge_result = ppr1.merge_with(ppr2);
354+
355+
if let Some(merged_ppr) = merge_result {
356+
let angle = merged_ppr.angle.clone();
357+
let merged_params = Some(Box::new(Parameters::Params(smallvec![angle])));
358+
359+
let packed = PackedInstruction {
360+
op: PauliBased::PauliProductRotation(merged_ppr).into(),
361+
qubits: inst1.qubits,
362+
clbits: inst1.clbits,
363+
params: merged_params,
364+
label: None,
365+
#[cfg(feature = "cache_pygates")]
366+
py_op: std::sync::OnceLock::new(),
367+
};
368+
369+
if let Some(phase_update) = is_identity_equiv(&packed, false, None, error_cutoff_fn)? {
370+
return Ok((true, None, phase_update));
371+
} else {
372+
return Ok((true, Some(packed), 0.));
373+
}
374+
}
375+
}
376+
312377
// Special handling for PauliEvolutionGates.
313378
if inst1.op.name() == "PauliEvolution" && inst2.op.name() == "PauliEvolution" {
314379
if let (OperationRef::Gate(py_gate1), OperationRef::Gate(py_gate2)) =

crates/transpiler/src/passes/remove_identity_equiv.rs

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -179,7 +179,21 @@ where
179179
));
180180
}
181181

182-
// Special handling for large pauli rotation gates.
182+
// Special handling for pauli product rotation gates.
183+
if let OperationRef::PauliProductRotation(ppr) = view {
184+
if let Some((tr_over_dim, dim)) = ppr.rotation_trace_and_dim() {
185+
return Ok(average_gate_fidelity_below_tol(
186+
tr_over_dim,
187+
dim,
188+
error_cutoff_fn(inst),
189+
));
190+
} else {
191+
// Parameterized rotation
192+
return Ok(None);
193+
}
194+
}
195+
196+
// Special handling for pauli evolution gates.
183197
if view.name() == "PauliEvolution" {
184198
if let OperationRef::Gate(py_gate) = view {
185199
let result = Python::attach(|py| -> PyResult<Option<(Complex64, usize)>> {
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
features_transpiler:
2+
- |
3+
The transpiler passes :class:`.RemoveIdentityEquivalent` and :class:`.CommutativeOptimization`
4+
have been extended to handle :class:`.PauliProductRotationGate` gates.

test/python/transpiler/test_commutative_optimization.py

Lines changed: 44 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
PhaseGate,
2727
UnitaryGate,
2828
PauliEvolutionGate,
29+
PauliProductRotationGate,
2930
Initialize,
3031
U2Gate,
3132
CZGate,
@@ -43,7 +44,7 @@
4344
)
4445
from qiskit.circuit.parameter import Parameter
4546
from qiskit.transpiler.passes import CommutativeOptimization
46-
from qiskit.quantum_info import Operator, SparsePauliOp, Clifford
47+
from qiskit.quantum_info import Operator, SparsePauliOp, Clifford, Pauli
4748

4849
from test import QiskitTestCase
4950

@@ -313,6 +314,48 @@ def test_not_merge_pauli_evolutions(self):
313314

314315
self.assertEqual(qct, qc)
315316

317+
def test_merge_pauli_product_rotations(self):
318+
"""Test that the pass merges PauliProductRotationGates."""
319+
320+
qc = QuantumCircuit(4)
321+
qc.append(PauliProductRotationGate(Pauli("XXII"), -1), [0, 1, 2, 3])
322+
qc.append(PauliProductRotationGate(Pauli("ZZIY"), 1), [0, 1, 3, 2])
323+
qc.append(PauliProductRotationGate(Pauli("ZZYI"), 1), [1, 0, 2, 3])
324+
qc.append(PauliProductRotationGate(Pauli("YYXX"), 1), [0, 1, 2, 3])
325+
qc.append(PauliProductRotationGate(Pauli("XXII"), 1), [0, 1, 2, 3])
326+
327+
# The two "XXII" rotations should cancel.
328+
# The two "ZZIY" rotations (after canonicalizing) should get combined.
329+
qct = CommutativeOptimization()(qc)
330+
331+
expected = QuantumCircuit(4)
332+
expected.append(PauliProductRotationGate(Pauli("ZZIY"), 2), [0, 1, 2, 3])
333+
expected.append(PauliProductRotationGate(Pauli("YYXX"), 1), [0, 1, 2, 3])
334+
335+
self.assertEqual(Operator(expected), Operator(qc))
336+
self.assertEqual(qct, expected)
337+
338+
def test_merge_parameterized_pauli_product_rotations(self):
339+
"""Test that the pass merges parameterized PauliProductRotationGates."""
340+
341+
p = Parameter("p")
342+
q = Parameter("q")
343+
r = Parameter("r")
344+
345+
qc = QuantumCircuit(4)
346+
qc.append(PauliProductRotationGate(Pauli("YYXX"), p), [0, 1, 2, 3])
347+
qc.append(PauliProductRotationGate(Pauli("YYXX"), q), [0, 1, 2, 3])
348+
qc.append(PauliProductRotationGate(Pauli("ZXXX"), r), [0, 1, 2, 3])
349+
qc.append(PauliProductRotationGate(Pauli("YYXX"), -1), [0, 1, 2, 3])
350+
351+
qct = CommutativeOptimization()(qc)
352+
353+
expected = QuantumCircuit(4)
354+
expected.append(PauliProductRotationGate(Pauli("ZXXX"), r), [0, 1, 2, 3])
355+
expected.append(PauliProductRotationGate(Pauli("YYXX"), p + q - 1), [0, 1, 2, 3])
356+
357+
self.assertEqual(qct, expected)
358+
316359
def test_2pi_multiples(self):
317360
"""Test 2pi multiples are handled with the correct phase they introduce."""
318361
for eps in [0, 1e-10, -1e-10]:

test/python/transpiler/test_remove_identity_equivalent.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
XXPlusYYGate,
2929
GlobalPhaseGate,
3030
UnitaryGate,
31+
PauliProductRotationGate,
3132
PauliEvolutionGate,
3233
)
3334
from qiskit.quantum_info import Operator, Pauli
@@ -192,6 +193,7 @@ def to_matrix(self):
192193
UnitaryGate(np.exp(-0.123j) * np.eye(2)),
193194
UnitaryGate(np.exp(-0.123j) * np.eye(4)),
194195
UnitaryGate(np.exp(-0.123j) * np.eye(8)),
196+
PauliProductRotationGate(Pauli("XYIZ"), 0),
195197
)
196198
def test_remove_identity_up_to_global_phase(self, gate):
197199
"""Test that gates equivalent to identity up to a global phase are removed from the circuit,

0 commit comments

Comments
 (0)