Skip to content

Commit f5300e0

Browse files
authored
Change canonicalization of PhasedXZGate to preserve pi-pulse rotation axis (#8053)
This changes the way we canonicalize `PhasedXZGate` exponents for pi-pulses (x_exponent=1) so that we preserve the axis of rotation of pi pulses as much as possible, and don't treat different pi pulses as "equal" to each other. Pi pulses are important for things like dynamical decoupling sequences, and the difference between a series of rotations about the same axis (X, X, X, X) versus about alternating axes (X, -X, X, -X) is important in real experiments. We want the `PhasedXZGate` canonicalization and equality to preserve these distinctions. Note that there was some discussion of the canonicalization in the original implementation of the phased xz gate: https://github.com/quantumlib/Cirq/pull/2566/changes#r348222277
1 parent 2821085 commit f5300e0

2 files changed

Lines changed: 91 additions & 68 deletions

File tree

cirq-core/cirq/ops/phased_x_z_gate.py

Lines changed: 47 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -94,15 +94,18 @@ def _canonical(self) -> cirq.PhasedXZGate:
9494
z = self.z_exponent
9595
a = self.axis_phase_exponent
9696

97-
# Canonicalize X exponent into (-1, +1].
97+
# Canonicalize X exponent into [0, +1].
9898
if not isinstance(x, sympy.Expr):
99+
if x < 0:
100+
x *= -1
101+
a += 1
99102
x %= 2
100-
if x > 1.0:
101-
x -= 2
103+
if x == 0:
104+
a = 0 # Axis phase exponent is irrelevant if there is no X exponent.
105+
elif x > 1.0:
106+
x = 2 - x
107+
a += 1
102108

103-
# Axis phase exponent is irrelevant if there is no X exponent.
104-
if x == 0:
105-
a = 0.0
106109
# For 180 degree X rotations, the axis phase and z exponent overlap.
107110
if x == 1 and z != 0:
108111
a += z / 2
@@ -114,19 +117,11 @@ def _canonical(self) -> cirq.PhasedXZGate:
114117
if z > 1.0:
115118
z -= 2
116119

117-
# Canonicalize axis phase exponent into (-0.5, +0.5].
120+
# Canonicalize axis phase exponent into (-1, +1].
118121
if not isinstance(a, sympy.Expr):
119122
a %= 2
120123
if a > 1.0:
121124
a -= 2
122-
if a <= -0.5:
123-
a += 1
124-
if x != 1:
125-
x = -x
126-
elif a > 0.5:
127-
a -= 1
128-
if x != 1:
129-
x = -x
130125

131126
return PhasedXZGate(x_exponent=x, z_exponent=z, axis_phase_exponent=a)
132127

@@ -349,45 +344,55 @@ def _canonical_xza_mod_2(
349344
Optimized helper for `PhasedXZGate._has_stabilizer_effect_`.
350345
"""
351346
# The result must be consistent with PhasedXZGate._canonical
352-
x = x_exponent % 2
353-
a = 0.0 if x == 0 else axis_phase_exponent % 2
354-
z = z_exponent % 2
347+
x = x_exponent
348+
a = axis_phase_exponent
349+
if x < 0:
350+
x *= -1
351+
a += 1
352+
x %= 2
353+
if x == 0:
354+
a = 0
355+
elif x > 1:
356+
x = 2 - x
357+
a += 1
358+
z = z_exponent
355359
if x == 1 and z != 0:
356-
a = (a + z / 2) % 2
360+
a += z / 2
357361
z = 0
358-
if 0.5 < a <= 1.5:
359-
a = (a - 1) % 2
360-
x = 2 - x if x else x
361-
return (x, z, a)
362+
return (x, z % 2, a % 2)
362363

363364

364365
@functools.cache
365366
def _clifford_as_phasedzx_params() -> frozenset[tuple[float, float, float]]:
366367
return frozenset(
367368
{
368-
(0.0, 1.0, 0.0),
369-
(0.5, 1.5, 0.0),
370-
(1.5, 1.5, 0.0),
371-
(1.5, 1.5, 0.5),
369+
(0.0, 0.0, 0.0),
372370
(0.0, 0.5, 0.0),
373-
(0.5, 1.0, 0.0),
374-
(0.5, 1.0, 0.5),
375-
(1.0, 0.0, 1.75),
371+
(0.0, 1.0, 0.0),
372+
(0.0, 1.5, 0.0),
373+
(0.5, 0.0, 0.0),
374+
(0.5, 0.0, 0.5),
375+
(0.5, 0.0, 1.0),
376+
(0.5, 0.0, 1.5),
376377
(0.5, 0.5, 0.0),
377378
(0.5, 0.5, 0.5),
378-
(1.5, 0.5, 0.5),
379-
(1.5, 1.0, 0.5),
380-
(1.5, 1.0, 0.0),
381-
(1.5, 0.5, 0.0),
382-
(0.0, 0.0, 0.0),
379+
(0.5, 0.5, 1.0),
380+
(0.5, 0.5, 1.5),
381+
(0.5, 1.0, 0.0),
382+
(0.5, 1.0, 0.5),
383+
(0.5, 1.0, 1.0),
384+
(0.5, 1.0, 1.5),
385+
(0.5, 1.5, 0.0),
386+
(0.5, 1.5, 0.5),
387+
(0.5, 1.5, 1.0),
388+
(0.5, 1.5, 1.5),
383389
(1.0, 0.0, 0.0),
384-
(1.0, 0.0, 0.5),
385390
(1.0, 0.0, 0.25),
386-
(0.5, 0.0, 0.5),
387-
(0.0, 1.5, 0.0),
388-
(0.5, 0.0, 0.0),
389-
(1.5, 0.0, 0.0),
390-
(1.5, 0.0, 0.5),
391-
(0.5, 1.5, 0.5),
391+
(1.0, 0.0, 0.5),
392+
(1.0, 0.0, 0.75),
393+
(1.0, 0.0, 1.0),
394+
(1.0, 0.0, 1.25),
395+
(1.0, 0.0, 1.5),
396+
(1.0, 0.0, 1.75),
392397
}
393398
)

cirq-core/cirq/ops/phased_x_z_gate_test.py

Lines changed: 44 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -69,31 +69,36 @@ def f(x, z, a):
6969

7070
# Canonicalizations are equivalent.
7171
eq = cirq.testing.EqualsTester()
72-
eq.add_equality_group(f(-1, 0, 0), f(-3, 0, 0), f(1, 1, 0.5))
72+
eq.add_equality_group(f(1, 0, 0), f(3, 0, 0), f(-1, 0, 1))
7373
"""
74-
# Canonicalize X exponent (-1, +1].
75-
if isinstance(x, numbers.Real):
74+
# Canonicalize X exponent into [0, +1].
75+
if not isinstance(x, sympy.Expr):
76+
if x < 0:
77+
x *= -1
78+
a += 1
7679
x %= 2
77-
if x > 1:
78-
x -= 2
79-
# Axis phase exponent is irrelevant if there is no X exponent.
80-
# Canonicalize Z exponent (-1, +1].
81-
if isinstance(z, numbers.Real):
80+
if x == 0:
81+
a = 0 # Axis phase exponent is irrelevant if there is no X exponent.
82+
elif x > 1.0:
83+
x = 2 - x
84+
a += 1
85+
86+
# For 180 degree X rotations, the axis phase and z exponent overlap.
87+
if x == 1 and z != 0:
88+
a += z / 2
89+
z = 0.0
90+
91+
# Canonicalize Z exponent into (-1, +1].
92+
if not isinstance(z, sympy.Expr):
8293
z %= 2
83-
if z > 1:
94+
if z > 1.0:
8495
z -= 2
8596
86-
# Canonicalize axis phase exponent into (-0.5, +0.5].
87-
if isinstance(a, numbers.Real):
97+
# Canonicalize axis phase exponent into (-1, +1].
98+
if not isinstance(a, sympy.Expr):
8899
a %= 2
89-
if a > 1:
100+
if a > 1.0:
90101
a -= 2
91-
if a <= -0.5:
92-
a += 1
93-
x = -x
94-
elif a > 0.5:
95-
a -= 1
96-
x = -x
97102
"""
98103

99104
# X rotation gets canonicalized.
@@ -102,9 +107,9 @@ def f(x, z, a):
102107
assert t.z_exponent == 0
103108
assert t.axis_phase_exponent == 0
104109
t = f(1.5, 0, 0)._canonical()
105-
assert t.x_exponent == -0.5
110+
assert t.x_exponent == 0.5
106111
assert t.z_exponent == 0
107-
assert t.axis_phase_exponent == 0
112+
assert t.axis_phase_exponent == 1
108113

109114
# Z rotation gets canonicalized.
110115
t = f(0, 3, 0)._canonical()
@@ -122,23 +127,23 @@ def f(x, z, a):
122127
assert t.z_exponent == 0
123128
assert t.axis_phase_exponent == 0.25
124129
t = f(0.5, 0, 1.25)._canonical()
125-
assert t.x_exponent == -0.5
130+
assert t.x_exponent == 0.5
126131
assert t.z_exponent == 0
127-
assert t.axis_phase_exponent == 0.25
132+
assert t.axis_phase_exponent == -0.75
128133
t = f(0.5, 0, 0.75)._canonical()
129-
assert t.x_exponent == -0.5
134+
assert t.x_exponent == 0.5
130135
assert t.z_exponent == 0
131-
assert t.axis_phase_exponent == -0.25
136+
assert t.axis_phase_exponent == 0.75
132137

133138
# 180 degree rotations don't need a virtual Z.
134139
t = f(1, 1, 0.5)._canonical()
135140
assert t.x_exponent == 1
136141
assert t.z_exponent == 0
137-
assert t.axis_phase_exponent == 0
142+
assert t.axis_phase_exponent == 1
138143
t = f(1, 0.25, 0.5)._canonical()
139144
assert t.x_exponent == 1
140145
assert t.z_exponent == 0
141-
assert t.axis_phase_exponent == -0.375
146+
assert t.axis_phase_exponent == 0.625
142147
cirq.testing.assert_allclose_up_to_global_phase(
143148
cirq.unitary(t), cirq.unitary(f(1, 0.25, 0.5)), atol=1e-8
144149
)
@@ -150,6 +155,19 @@ def f(x, z, a):
150155
assert t.axis_phase_exponent == 0
151156

152157

158+
def test_pi_pulse_canonicalization() -> None:
159+
"""Pi-pulses (x_exponent=1) about different axes should not be considered equal."""
160+
161+
def f(x, z, a):
162+
return cirq.PhasedXZGate(x_exponent=x, z_exponent=z, axis_phase_exponent=a)
163+
164+
assert f(1, 0, 0.25) == f(-1, 0, 1.25)
165+
assert f(1, 0, 0.25) != f(1, 0, 1.25)
166+
167+
assert cirq.approx_eq(f(1, 0, 0.2), f(-1, 0, 1.2))
168+
assert not cirq.approx_eq(f(1, 0, 0.2), f(1, 0, 1.2))
169+
170+
153171
def test_from_matrix() -> None:
154172
# Axis rotations.
155173
assert cirq.approx_eq(

0 commit comments

Comments
 (0)