Skip to content

Commit 52c1a86

Browse files
authored
Optimize calculation of PhasedXZGate unitary (#7811)
Use analytical formula for the PhasedXZGate unitary resulting in about 6-fold decrease in calculation time. For Eliott's test colab this improves the speed of - `CZGaugeTransformer.as_sweep` by 25% (30 to 23s) - `merge_single_qubit_gates_to_phxz_symbolized` by 15% (77 to 66s) - `merge_single_qubit_moments_to_phxz` by 40% (29 to 17s) Also add pytest benchmarks for `PhasedXZGate` unitary and for the transformation chain in Eliott's colab. Partially implements #7797
1 parent 8e3e4d8 commit 52c1a86

4 files changed

Lines changed: 131 additions & 4 deletions

File tree

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
import itertools
2+
3+
import pytest
4+
5+
import cirq
6+
7+
QUBITS_PER_LAYER = [
8+
[cirq.GridQubit(5, 9), cirq.GridQubit(6, 9), cirq.GridQubit(5, 10), cirq.GridQubit(6, 8)],
9+
[
10+
cirq.GridQubit(8, 8),
11+
cirq.GridQubit(6, 9),
12+
cirq.GridQubit(7, 9),
13+
cirq.GridQubit(5, 10),
14+
cirq.GridQubit(6, 10),
15+
cirq.GridQubit(7, 8),
16+
],
17+
[cirq.GridQubit(6, 9), cirq.GridQubit(7, 9), cirq.GridQubit(6, 10), cirq.GridQubit(7, 8)],
18+
[cirq.GridQubit(5, 9), cirq.GridQubit(6, 9), cirq.GridQubit(6, 8), cirq.GridQubit(7, 8)],
19+
]
20+
21+
LAYERS_GRID_QUBIT_PAIRS = [
22+
[(cirq.GridQubit(5, 9), cirq.GridQubit(5, 10)), (cirq.GridQubit(6, 8), cirq.GridQubit(6, 9))],
23+
[
24+
(cirq.GridQubit(5, 10), cirq.GridQubit(6, 10)),
25+
(cirq.GridQubit(6, 9), cirq.GridQubit(7, 9)),
26+
(cirq.GridQubit(7, 8), cirq.GridQubit(8, 8)),
27+
],
28+
[(cirq.GridQubit(6, 9), cirq.GridQubit(6, 10)), (cirq.GridQubit(7, 8), cirq.GridQubit(7, 9))],
29+
[(cirq.GridQubit(5, 9), cirq.GridQubit(6, 9)), (cirq.GridQubit(6, 8), cirq.GridQubit(7, 8))],
30+
]
31+
32+
33+
def make_fake_trotter_circuit(num_cycles: int):
34+
all_qubits = sorted(set(itertools.chain.from_iterable(QUBITS_PER_LAYER)))
35+
moments = []
36+
for layer, qubits in zip(LAYERS_GRID_QUBIT_PAIRS, QUBITS_PER_LAYER):
37+
moments.append(cirq.Moment((cirq.Y**0.3)(qubit) for qubit in qubits))
38+
moments.append(cirq.Moment(cirq.CZ(*pair) for pair in layer))
39+
return cirq.Circuit.from_moments(*moments) * num_cycles + cirq.Moment(cirq.M(*all_qubits))
40+
41+
42+
@pytest.mark.parametrize(
43+
["num_cycles", "num_circuits"], [(5, 10), pytest.param(50, 100, marks=pytest.mark.slow)]
44+
)
45+
@pytest.mark.benchmark(group="dynamical_decoupling", max_time=10)
46+
def test_dynamical_decoupling_sweep(benchmark, num_cycles: int, num_circuits: int) -> None:
47+
def _f(num_cycles: int, num_circuits: int) -> cirq.Circuit:
48+
circuit = make_fake_trotter_circuit(num_cycles)
49+
circuit1, sweep1 = cirq.transformers.gauge_compiling.CZGaugeTransformer.as_sweep(
50+
circuit, N=num_circuits
51+
)
52+
circuit2, sweep2 = cirq.merge_single_qubit_gates_to_phxz_symbolized(circuit1, sweep=sweep1)
53+
assert len(sweep2)
54+
circuit3 = cirq.add_dynamical_decoupling(circuit2)
55+
return circuit3
56+
57+
_ = benchmark(_f, num_cycles=num_cycles, num_circuits=num_circuits)
58+
59+
60+
@pytest.mark.parametrize(
61+
["num_cycles", "num_circuits"], [(5, 10), pytest.param(50, 100, marks=pytest.mark.slow)]
62+
)
63+
@pytest.mark.benchmark(group="dynamical_decoupling", max_time=10)
64+
def test_dynamical_decoupling_batch(benchmark, num_cycles: int, num_circuits: int) -> None:
65+
def _f(num_cycles: int, num_circuits: int) -> list[cirq.Circuit]:
66+
circuit = make_fake_trotter_circuit(num_cycles)
67+
circuits_batch1 = [
68+
cirq.transformers.gauge_compiling.cz_gauge.CZGaugeTransformer(circuit)
69+
for _ in range(num_circuits)
70+
]
71+
circuits_batch2 = [cirq.merge_single_qubit_moments_to_phxz(c) for c in circuits_batch1]
72+
circuits_batch3 = [cirq.add_dynamical_decoupling(c) for c in circuits_batch2]
73+
return circuits_batch3
74+
75+
_ = benchmark(_f, num_cycles=num_cycles, num_circuits=num_circuits)

benchmarks/phased_x_z_gate_perf.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,3 +39,25 @@ def test_has_stabilizer_effect_for_random_gates(benchmark) -> None:
3939
f = lambda: any(cirq.has_stabilizer_effect(g) for g in random_phased_xz_gates)
4040
result = benchmark(f)
4141
assert not result
42+
43+
44+
@pytest.mark.benchmark(group="phased_x_z_gate")
45+
def test_unitary_for_cliffords(benchmark) -> None:
46+
phased_xz_cliffords = [
47+
g.to_phased_xz_gate() for g in cirq.SingleQubitCliffordGate.all_single_qubit_cliffords
48+
]
49+
f = lambda: all(cirq.unitary(g) is not None for g in phased_xz_cliffords)
50+
result = benchmark(f)
51+
assert result
52+
53+
54+
@pytest.mark.benchmark(group="phased_x_z_gate")
55+
def test_unitary_for_random_gates(benchmark) -> None:
56+
num_calls = len(cirq.SingleQubitCliffordGate.all_single_qubit_cliffords)
57+
random_phased_xz_gates = [
58+
cirq.PhasedXZGate(x_exponent=x, z_exponent=z, axis_phase_exponent=a)
59+
for x, z, a in np.random.uniform(-1, 1, (num_calls, 3))
60+
]
61+
f = lambda: all(cirq.unitary(g) is not None for g in random_phased_xz_gates)
62+
result = benchmark(f)
63+
assert result

cirq-core/cirq/ops/phased_x_z_gate.py

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
from __future__ import annotations
1616

1717
import functools
18+
import math
1819
import numbers
1920
from collections.abc import Iterator, Sequence, Set
2021
from typing import Any, TYPE_CHECKING
@@ -192,10 +193,24 @@ def _unitary_(self) -> np.ndarray | None:
192193
"""See `cirq.SupportsUnitary`."""
193194
if self._is_parameterized_():
194195
return None
195-
z_pre = protocols.unitary(ops.Z**-self._axis_phase_exponent)
196-
x = protocols.unitary(ops.X**self._x_exponent)
197-
z_post = protocols.unitary(ops.Z ** (self._axis_phase_exponent + self._z_exponent))
198-
return z_post @ x @ z_pre
196+
197+
a = self._axis_phase_exponent
198+
x = self._x_exponent
199+
z = self._z_exponent
200+
# evaluate unitary terms ucx = exp(1j * pi * x / 2) * cos(pi * x / 2) and
201+
# usx = -1j * exp(1j * pi * x / 2) * sin(pi * x / 2) without round-off
202+
# errors at half-integer x
203+
if x % 1 == 0.5:
204+
ucx = 0.5 + 0.5j * (-1) ** math.floor(x)
205+
usx = ucx.conjugate()
206+
else:
207+
cpxh = 1j**x
208+
ucx = cpxh * cpxh.real
209+
usx = -1j * cpxh * cpxh.imag
210+
u = np.array(
211+
[[ucx, usx * 1j ** (-2 * a)], [usx * 1j ** (2 * (z + a)), ucx * 1j ** (2 * z)]]
212+
)
213+
return u
199214

200215
def _decompose_(self, qubits: Sequence[cirq.Qid]) -> Iterator[cirq.OP_TREE]:
201216
q = qubits[0]

cirq-core/cirq/ops/phased_x_z_gate_test.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
from __future__ import annotations
44

5+
import itertools
56
import random
67
from typing import cast
78

@@ -227,6 +228,20 @@ def test_from_matrix_close_unitary(unitary: np.ndarray) -> None:
227228
)
228229

229230

231+
@pytest.mark.parametrize(
232+
['x_exponent', 'z_exponent', 'axis_phase_exponent'],
233+
itertools.product([-0.5, 0.0, 0.5, 1.0], repeat=3),
234+
)
235+
def test_exact_unitary_at_half_integers(
236+
x_exponent: float, z_exponent: float, axis_phase_exponent: float
237+
) -> None:
238+
gate = cirq.PhasedXZGate(
239+
x_exponent=x_exponent, z_exponent=z_exponent, axis_phase_exponent=axis_phase_exponent
240+
)
241+
u = cirq.unitary(gate)
242+
np.testing.assert_equal(u, np.round(u, decimals=1))
243+
244+
230245
@pytest.mark.parametrize(
231246
'unitary',
232247
[

0 commit comments

Comments
 (0)