Skip to content

Commit b3e0eeb

Browse files
make SingleQubitCliffordGate immutable singletons and use it in qubit_characterizations for a 37% speedup (#6392)
SingleQubitCliffordGates represents the 24 gates in the single qubit clifford group. Some of the operations this class implements are expensive computation but at the same time are properties of each of the 24 operators. turning those expensive computations into cached properties is benificial for performance but for that the objects need to be immutable. one of the places that heavily relies on single qubit cliffords is `qubit_characterizations.py` which implemented the single qubit clifford algebra from scratch by falling on to the matrix representation and doing matrix multiplication and inversion (for computing the adjoint) this led to a bottleneck while creating circuits (e.g. for example _create_parallel_rb_circuit for 50 qubits and a 1000 gates takes $3.886s$). using SingleQubitCliffordGates instead and using `merged_with` operation which maps two cliffords onto the clifford equaivalent to their composition leads to $2.148s$, with most of those 2s spent in `Moment.__init__` which will be the target of the next PR. I also made the 24 cliffords singleton, since there is no point in creating new object which won't have any of the cached properties. part of #6389
1 parent 8a7f675 commit b3e0eeb

File tree

4 files changed

+109
-38
lines changed

4 files changed

+109
-38
lines changed

cirq-core/cirq/experiments/qubit_characterizations.py

+42-31
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414

1515
import dataclasses
1616
import itertools
17+
import functools
1718

1819
from typing import (
1920
Any,
@@ -58,11 +59,11 @@ class Cliffords:
5859
s1_y
5960
"""
6061

61-
c1_in_xy: List[List[ops.Gate]]
62-
c1_in_xz: List[List[ops.Gate]]
63-
s1: List[List[ops.Gate]]
64-
s1_x: List[List[ops.Gate]]
65-
s1_y: List[List[ops.Gate]]
62+
c1_in_xy: List[List[ops.SingleQubitCliffordGate]]
63+
c1_in_xz: List[List[ops.SingleQubitCliffordGate]]
64+
s1: List[List[ops.SingleQubitCliffordGate]]
65+
s1_x: List[List[ops.SingleQubitCliffordGate]]
66+
s1_y: List[List[ops.SingleQubitCliffordGate]]
6667

6768

6869
class RandomizedBenchMarkResult:
@@ -299,17 +300,14 @@ def parallel_single_qubit_randomized_benchmarking(
299300
A dictionary from qubits to RandomizedBenchMarkResult objects.
300301
"""
301302

302-
cliffords = _single_qubit_cliffords()
303-
c1 = cliffords.c1_in_xy if use_xy_basis else cliffords.c1_in_xz
304-
clifford_mats = np.array([_gate_seq_to_mats(gates) for gates in c1])
303+
clifford_group = _single_qubit_cliffords()
304+
c1 = clifford_group.c1_in_xy if use_xy_basis else clifford_group.c1_in_xz
305305

306306
# create circuits
307307
circuits_all: List['cirq.AbstractCircuit'] = []
308308
for num_cliffords in num_clifford_range:
309309
for _ in range(num_circuits):
310-
circuits_all.append(
311-
_create_parallel_rb_circuit(qubits, num_cliffords, c1, clifford_mats)
312-
)
310+
circuits_all.append(_create_parallel_rb_circuit(qubits, num_cliffords, c1))
313311

314312
# run circuits
315313
results = sampler.run_batch(circuits_all, repetitions=repetitions)
@@ -562,11 +560,9 @@ def _measurement(two_qubit_circuit: circuits.Circuit) -> np.ndarray:
562560

563561

564562
def _create_parallel_rb_circuit(
565-
qubits: Iterator['cirq.Qid'], num_cliffords: int, c1: list, clifford_mats: np.ndarray
563+
qubits: Iterator['cirq.Qid'], num_cliffords: int, c1: list
566564
) -> 'cirq.Circuit':
567-
circuits_to_zip = [
568-
_random_single_q_clifford(qubit, num_cliffords, c1, clifford_mats) for qubit in qubits
569-
]
565+
circuits_to_zip = [_random_single_q_clifford(qubit, num_cliffords, c1) for qubit in qubits]
570566
circuit = circuits.Circuit.zip(*circuits_to_zip)
571567
return circuits.Circuit.from_moments(*circuit, ops.measure_each(*qubits))
572568

@@ -612,16 +608,12 @@ def _two_qubit_clifford_matrices(
612608

613609

614610
def _random_single_q_clifford(
615-
qubit: 'cirq.Qid',
616-
num_cfds: int,
617-
cfds: Sequence[Sequence['cirq.Gate']],
618-
cfd_matrices: np.ndarray,
611+
qubit: 'cirq.Qid', num_cfds: int, cfds: Sequence[Sequence['cirq.Gate']]
619612
) -> 'cirq.Circuit':
620613
clifford_group_size = 24
621614
gate_ids = list(np.random.choice(clifford_group_size, num_cfds))
622615
gate_sequence = [gate for gate_id in gate_ids for gate in cfds[gate_id]]
623-
idx = _find_inv_matrix(_gate_seq_to_mats(gate_sequence), cfd_matrices)
624-
gate_sequence.extend(cfds[idx])
616+
gate_sequence.append(_reduce_gate_seq(gate_sequence) ** -1)
625617
circuit = circuits.Circuit(gate(qubit) for gate in gate_sequence)
626618
return circuit
627619

@@ -681,11 +673,13 @@ def _matrix_bar_plot(
681673
ax.set_title(title)
682674

683675

684-
def _gate_seq_to_mats(gate_seq: Sequence['cirq.Gate']) -> np.ndarray:
685-
mat_rep = protocols.unitary(gate_seq[0])
676+
def _reduce_gate_seq(
677+
gate_seq: Sequence[ops.SingleQubitCliffordGate],
678+
) -> ops.SingleQubitCliffordGate:
679+
cur = gate_seq[0]
686680
for gate in gate_seq[1:]:
687-
mat_rep = np.dot(protocols.unitary(gate), mat_rep)
688-
return mat_rep
681+
cur = cur.merged_with(gate)
682+
return cur
689683

690684

691685
def _two_qubit_clifford(
@@ -793,11 +787,16 @@ def _single_qubit_gates(
793787
yield gate(qubit)
794788

795789

790+
@functools.cache
796791
def _single_qubit_cliffords() -> Cliffords:
797-
X, Y, Z = ops.X, ops.Y, ops.Z
792+
X, Y, Z = (
793+
ops.SingleQubitCliffordGate.X,
794+
ops.SingleQubitCliffordGate.Y,
795+
ops.SingleQubitCliffordGate.Z,
796+
)
798797

799-
c1_in_xy: List[List['cirq.Gate']] = []
800-
c1_in_xz: List[List['cirq.Gate']] = []
798+
c1_in_xy: List[List[ops.SingleQubitCliffordGate]] = []
799+
c1_in_xz: List[List[ops.SingleQubitCliffordGate]] = []
801800

802801
for phi_0, phi_1 in itertools.product([1.0, 0.5, -0.5], [0.0, 0.5, -0.5]):
803802
c1_in_xy.append([X**phi_0, Y**phi_1])
@@ -820,8 +819,20 @@ def _single_qubit_cliffords() -> Cliffords:
820819
for z0, x, z1 in phi_xz:
821820
c1_in_xz.append([Z**z0, X**x, Z**z1])
822821

823-
s1: List[List['cirq.Gate']] = [[X**0.0], [Y**0.5, X**0.5], [X**-0.5, Y**-0.5]]
824-
s1_x: List[List['cirq.Gate']] = [[X**0.5], [X**0.5, Y**0.5, X**0.5], [Y**-0.5]]
825-
s1_y: List[List['cirq.Gate']] = [[Y**0.5], [X**-0.5, Y**-0.5, X**0.5], [Y, X**0.5]]
822+
s1: List[List[ops.SingleQubitCliffordGate]] = [
823+
[X**0.0],
824+
[Y**0.5, X**0.5],
825+
[X**-0.5, Y**-0.5],
826+
]
827+
s1_x: List[List[ops.SingleQubitCliffordGate]] = [
828+
[X**0.5],
829+
[X**0.5, Y**0.5, X**0.5],
830+
[Y**-0.5],
831+
]
832+
s1_y: List[List[ops.SingleQubitCliffordGate]] = [
833+
[Y**0.5],
834+
[X**-0.5, Y**-0.5, X**0.5],
835+
[Y, X**0.5],
836+
]
826837

827838
return Cliffords(c1_in_xy, c1_in_xz, s1, s1_x, s1_y)

cirq-core/cirq/experiments/qubit_characterizations_test.py

+26-3
Original file line numberDiff line numberDiff line change
@@ -75,9 +75,32 @@ def check_distinct(unitaries):
7575

7676
# Check that XZ decomposition has at most one X gate per clifford.
7777
for gates in cliffords.c1_in_xz:
78-
num_x = len([gate for gate in gates if isinstance(gate, cirq.XPowGate)])
79-
num_z = len([gate for gate in gates if isinstance(gate, cirq.ZPowGate)])
80-
assert num_x + num_z == len(gates)
78+
num_i = len([gate for gate in gates if gate == cirq.ops.SingleQubitCliffordGate.I])
79+
num_x = len(
80+
[
81+
gate
82+
for gate in gates
83+
if gate
84+
in (
85+
cirq.ops.SingleQubitCliffordGate.X,
86+
cirq.ops.SingleQubitCliffordGate.X_sqrt,
87+
cirq.ops.SingleQubitCliffordGate.X_nsqrt,
88+
)
89+
]
90+
)
91+
num_z = len(
92+
[
93+
gate
94+
for gate in gates
95+
if gate
96+
in (
97+
cirq.ops.SingleQubitCliffordGate.Z,
98+
cirq.ops.SingleQubitCliffordGate.Z_sqrt,
99+
cirq.ops.SingleQubitCliffordGate.Z_nsqrt,
100+
)
101+
]
102+
)
103+
assert num_x + num_z + num_i == len(gates)
81104
assert num_x <= 1
82105

83106

cirq-core/cirq/ops/clifford_gate.py

+36-3
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,13 @@
1414

1515
from typing import Any, Dict, List, Optional, Sequence, Tuple, TYPE_CHECKING, Union
1616

17-
17+
import functools
18+
from dataclasses import dataclass
1819
import numpy as np
1920

2021
from cirq import protocols, value, linalg, qis
2122
from cirq._import import LazyLoader
23+
from cirq._compat import cached_property, cached_method
2224
from cirq.ops import common_gates, named_qubit, raw_types, pauli_gates, phased_x_z_gate
2325
from cirq.ops.pauli_gates import Pauli
2426
from cirq.type_workarounds import NotImplementedType
@@ -356,6 +358,8 @@ def _get_sqrt_map(
356358
class CliffordGate(raw_types.Gate, CommonCliffordGates):
357359
"""Clifford rotation for N-qubit."""
358360

361+
_clifford_tableau: qis.CliffordTableau
362+
359363
def __init__(self, *, _clifford_tableau: qis.CliffordTableau) -> None:
360364
# We use the Clifford tableau to represent a Clifford gate.
361365
# It is crucial to note that the meaning of tableau here is different
@@ -376,7 +380,7 @@ def __init__(self, *, _clifford_tableau: qis.CliffordTableau) -> None:
376380
# more precisely the conjugate transformation of ZI by this gate, becomes -ZI.
377381
# (Note the real clifford tableau has to satify the Symplectic property.
378382
# here is just for illustration)
379-
self._clifford_tableau = _clifford_tableau.copy()
383+
object.__setattr__(self, '_clifford_tableau', _clifford_tableau.copy())
380384

381385
@property
382386
def clifford_tableau(self):
@@ -399,6 +403,12 @@ def _has_stabilizer_effect_(self) -> Optional[bool]:
399403
def __pow__(self, exponent) -> 'CliffordGate':
400404
if exponent == -1:
401405
return CliffordGate.from_clifford_tableau(self.clifford_tableau.inverse())
406+
if exponent == 0:
407+
return CliffordGate.from_clifford_tableau(
408+
qis.CliffordTableau(num_qubits=self._num_qubits_())
409+
)
410+
if exponent == 1:
411+
return self
402412
if exponent > 0 and int(exponent) == exponent:
403413
base_tableau = self.clifford_tableau.copy()
404414
for _ in range(int(exponent) - 1):
@@ -457,6 +467,7 @@ def _act_on_(
457467
return NotImplemented
458468

459469

470+
@dataclass(frozen=True, init=False, eq=False, repr=False)
460471
@value.value_equality(manual_cls=True)
461472
class SingleQubitCliffordGate(CliffordGate):
462473
"""Any single qubit Clifford rotation."""
@@ -468,6 +479,7 @@ def _num_qubits_(self):
468479
return 1
469480

470481
@staticmethod
482+
@functools.cache
471483
def from_clifford_tableau(tableau: qis.CliffordTableau) -> 'SingleQubitCliffordGate':
472484
if not isinstance(tableau, qis.CliffordTableau):
473485
raise ValueError('Input argument has to be a CliffordTableau instance.')
@@ -679,6 +691,10 @@ def to_phased_xz_gate(self) -> phased_x_z_gate.PhasedXZGate:
679691
* {middle point of xyz in 4 Quadrant} * 120 is [[0, 1], [1, 1]]
680692
* {middle point of xyz in 4 Quadrant} * 240 is [[1, 1], [1, 0]]
681693
"""
694+
return self._to_phased_xz_gate
695+
696+
@cached_property
697+
def _to_phased_xz_gate(self) -> phased_x_z_gate.PhasedXZGate:
682698
x_to_flip, z_to_flip = self.clifford_tableau.rs
683699
flip_index = int(z_to_flip) * 2 + x_to_flip
684700
a, x, z = 0.0, 0.0, 0.0
@@ -716,7 +732,7 @@ def to_phased_xz_gate(self) -> phased_x_z_gate.PhasedXZGate:
716732
z = -0.5 if x_to_flip else 0.5
717733
return phased_x_z_gate.PhasedXZGate(x_exponent=x, z_exponent=z, axis_phase_exponent=a)
718734

719-
def __pow__(self, exponent) -> 'SingleQubitCliffordGate':
735+
def __pow__(self, exponent: Union[float, int]) -> 'SingleQubitCliffordGate':
720736
# First to check if we can get the sqrt and negative sqrt Clifford.
721737
if self._get_sqrt_map().get(exponent, None):
722738
pow_gate = self._get_sqrt_map()[exponent].get(self, None)
@@ -761,6 +777,7 @@ def commutes_with_pauli(self, pauli: Pauli) -> bool:
761777
to, flip = self.pauli_tuple(pauli)
762778
return to == pauli and not flip
763779

780+
@cached_method
764781
def merged_with(self, second: 'SingleQubitCliffordGate') -> 'SingleQubitCliffordGate':
765782
"""Returns a SingleQubitCliffordGate such that the circuits
766783
--output-- and --self--second--
@@ -773,6 +790,10 @@ def _has_unitary_(self) -> bool:
773790
return True
774791

775792
def _unitary_(self) -> np.ndarray:
793+
return self._unitary
794+
795+
@cached_property
796+
def _unitary(self) -> np.ndarray:
776797
mat = np.eye(2)
777798
qubit = named_qubit.NamedQubit('arbitrary')
778799
for op in protocols.decompose_once_with_qubits(self, (qubit,)):
@@ -787,6 +808,10 @@ def decompose_gate(self) -> Sequence['cirq.Gate']:
787808
clifford gate if applied in order. This decomposition agrees with
788809
cirq.unitary(self), including global phase.
789810
"""
811+
return self._decompose_gate
812+
813+
@cached_property
814+
def _decompose_gate(self) -> Sequence['cirq.Gate']:
790815
if self == SingleQubitCliffordGate.H:
791816
return [common_gates.H]
792817
rotations = self.decompose_rotation()
@@ -802,6 +827,10 @@ def decompose_rotation(self) -> Sequence[Tuple[Pauli, int]]:
802827
Note that the combined unitary effect of these rotations may
803828
differ from cirq.unitary(self) by a global phase.
804829
"""
830+
return self._decompose_rotation
831+
832+
@cached_property
833+
def _decompose_rotation(self) -> Sequence[Tuple[Pauli, int]]:
805834
x_rot = self.pauli_tuple(pauli_gates.X)
806835
y_rot = self.pauli_tuple(pauli_gates.Y)
807836
z_rot = self.pauli_tuple(pauli_gates.Z)
@@ -895,6 +924,10 @@ def _circuit_diagram_info_(
895924
)
896925

897926
def _value_equality_values_(self):
927+
return self._value_equality_values
928+
929+
@cached_property
930+
def _value_equality_values(self):
898931
return self._clifford_tableau.matrix().tobytes() + self._clifford_tableau.rs.tobytes()
899932

900933
def _value_equality_values_cls_(self):

cirq-core/cirq/qis/clifford_tableau.py

+5-1
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
import numpy as np
1818

1919
from cirq import protocols
20-
from cirq._compat import proper_repr
20+
from cirq._compat import proper_repr, cached_method
2121
from cirq.qis import quantum_state_representation
2222
from cirq.value import big_endian_int_to_digits, linear_dict, random_state
2323

@@ -652,3 +652,7 @@ def measure(
652652
self, axes: Sequence[int], seed: 'cirq.RANDOM_STATE_OR_SEED_LIKE' = None
653653
) -> List[int]:
654654
return [self._measure(axis, random_state.parse_random_state(seed)) for axis in axes]
655+
656+
@cached_method
657+
def __hash__(self) -> int:
658+
return hash(self.matrix().tobytes() + self.rs.tobytes())

0 commit comments

Comments
 (0)