Skip to content

make SingleQubitCliffordGate immutable singletons and use it in qubit_characterizations for a 37% speedup #6392

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
Jan 8, 2024
Merged
Show file tree
Hide file tree
Changes from all 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
73 changes: 42 additions & 31 deletions cirq-core/cirq/experiments/qubit_characterizations.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@

import dataclasses
import itertools
import functools

from typing import (
Any,
Expand Down Expand Up @@ -58,11 +59,11 @@ class Cliffords:
s1_y
"""

c1_in_xy: List[List[ops.Gate]]
c1_in_xz: List[List[ops.Gate]]
s1: List[List[ops.Gate]]
s1_x: List[List[ops.Gate]]
s1_y: List[List[ops.Gate]]
c1_in_xy: List[List[ops.SingleQubitCliffordGate]]
c1_in_xz: List[List[ops.SingleQubitCliffordGate]]
s1: List[List[ops.SingleQubitCliffordGate]]
s1_x: List[List[ops.SingleQubitCliffordGate]]
s1_y: List[List[ops.SingleQubitCliffordGate]]


class RandomizedBenchMarkResult:
Expand Down Expand Up @@ -299,17 +300,14 @@ def parallel_single_qubit_randomized_benchmarking(
A dictionary from qubits to RandomizedBenchMarkResult objects.
"""

cliffords = _single_qubit_cliffords()
c1 = cliffords.c1_in_xy if use_xy_basis else cliffords.c1_in_xz
clifford_mats = np.array([_gate_seq_to_mats(gates) for gates in c1])
clifford_group = _single_qubit_cliffords()
c1 = clifford_group.c1_in_xy if use_xy_basis else clifford_group.c1_in_xz

# create circuits
circuits_all: List['cirq.AbstractCircuit'] = []
for num_cliffords in num_clifford_range:
for _ in range(num_circuits):
circuits_all.append(
_create_parallel_rb_circuit(qubits, num_cliffords, c1, clifford_mats)
)
circuits_all.append(_create_parallel_rb_circuit(qubits, num_cliffords, c1))

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


def _create_parallel_rb_circuit(
qubits: Iterator['cirq.Qid'], num_cliffords: int, c1: list, clifford_mats: np.ndarray
qubits: Iterator['cirq.Qid'], num_cliffords: int, c1: list
) -> 'cirq.Circuit':
circuits_to_zip = [
_random_single_q_clifford(qubit, num_cliffords, c1, clifford_mats) for qubit in qubits
]
circuits_to_zip = [_random_single_q_clifford(qubit, num_cliffords, c1) for qubit in qubits]
circuit = circuits.Circuit.zip(*circuits_to_zip)
return circuits.Circuit.from_moments(*circuit, ops.measure_each(*qubits))

Expand Down Expand Up @@ -612,16 +608,12 @@ def _two_qubit_clifford_matrices(


def _random_single_q_clifford(
qubit: 'cirq.Qid',
num_cfds: int,
cfds: Sequence[Sequence['cirq.Gate']],
cfd_matrices: np.ndarray,
qubit: 'cirq.Qid', num_cfds: int, cfds: Sequence[Sequence['cirq.Gate']]
) -> 'cirq.Circuit':
clifford_group_size = 24
gate_ids = list(np.random.choice(clifford_group_size, num_cfds))
gate_sequence = [gate for gate_id in gate_ids for gate in cfds[gate_id]]
idx = _find_inv_matrix(_gate_seq_to_mats(gate_sequence), cfd_matrices)
gate_sequence.extend(cfds[idx])
gate_sequence.append(_reduce_gate_seq(gate_sequence) ** -1)
circuit = circuits.Circuit(gate(qubit) for gate in gate_sequence)
return circuit

Expand Down Expand Up @@ -681,11 +673,13 @@ def _matrix_bar_plot(
ax.set_title(title)


def _gate_seq_to_mats(gate_seq: Sequence['cirq.Gate']) -> np.ndarray:
mat_rep = protocols.unitary(gate_seq[0])
def _reduce_gate_seq(
gate_seq: Sequence[ops.SingleQubitCliffordGate],
) -> ops.SingleQubitCliffordGate:
cur = gate_seq[0]
for gate in gate_seq[1:]:
mat_rep = np.dot(protocols.unitary(gate), mat_rep)
return mat_rep
cur = cur.merged_with(gate)
return cur


def _two_qubit_clifford(
Expand Down Expand Up @@ -793,11 +787,16 @@ def _single_qubit_gates(
yield gate(qubit)


@functools.cache
def _single_qubit_cliffords() -> Cliffords:
Copy link
Collaborator

Choose a reason for hiding this comment

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

NIT: Should we cache _single_qubit_cliffords as well?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

done

X, Y, Z = ops.X, ops.Y, ops.Z
X, Y, Z = (
ops.SingleQubitCliffordGate.X,
ops.SingleQubitCliffordGate.Y,
ops.SingleQubitCliffordGate.Z,
)

c1_in_xy: List[List['cirq.Gate']] = []
c1_in_xz: List[List['cirq.Gate']] = []
c1_in_xy: List[List[ops.SingleQubitCliffordGate]] = []
c1_in_xz: List[List[ops.SingleQubitCliffordGate]] = []

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

s1: List[List['cirq.Gate']] = [[X**0.0], [Y**0.5, X**0.5], [X**-0.5, Y**-0.5]]
s1_x: List[List['cirq.Gate']] = [[X**0.5], [X**0.5, Y**0.5, X**0.5], [Y**-0.5]]
s1_y: List[List['cirq.Gate']] = [[Y**0.5], [X**-0.5, Y**-0.5, X**0.5], [Y, X**0.5]]
s1: List[List[ops.SingleQubitCliffordGate]] = [
[X**0.0],
[Y**0.5, X**0.5],
[X**-0.5, Y**-0.5],
]
s1_x: List[List[ops.SingleQubitCliffordGate]] = [
[X**0.5],
[X**0.5, Y**0.5, X**0.5],
[Y**-0.5],
]
s1_y: List[List[ops.SingleQubitCliffordGate]] = [
[Y**0.5],
[X**-0.5, Y**-0.5, X**0.5],
[Y, X**0.5],
]

return Cliffords(c1_in_xy, c1_in_xz, s1, s1_x, s1_y)
29 changes: 26 additions & 3 deletions cirq-core/cirq/experiments/qubit_characterizations_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -75,9 +75,32 @@ def check_distinct(unitaries):

# Check that XZ decomposition has at most one X gate per clifford.
for gates in cliffords.c1_in_xz:
num_x = len([gate for gate in gates if isinstance(gate, cirq.XPowGate)])
num_z = len([gate for gate in gates if isinstance(gate, cirq.ZPowGate)])
assert num_x + num_z == len(gates)
num_i = len([gate for gate in gates if gate == cirq.ops.SingleQubitCliffordGate.I])
num_x = len(
[
gate
for gate in gates
if gate
in (
cirq.ops.SingleQubitCliffordGate.X,
cirq.ops.SingleQubitCliffordGate.X_sqrt,
cirq.ops.SingleQubitCliffordGate.X_nsqrt,
)
]
)
num_z = len(
[
gate
for gate in gates
if gate
in (
cirq.ops.SingleQubitCliffordGate.Z,
cirq.ops.SingleQubitCliffordGate.Z_sqrt,
cirq.ops.SingleQubitCliffordGate.Z_nsqrt,
)
]
)
assert num_x + num_z + num_i == len(gates)
assert num_x <= 1


Expand Down
39 changes: 36 additions & 3 deletions cirq-core/cirq/ops/clifford_gate.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,13 @@

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


import functools
from dataclasses import dataclass
import numpy as np

from cirq import protocols, value, linalg, qis
from cirq._import import LazyLoader
from cirq._compat import cached_property, cached_method
from cirq.ops import common_gates, named_qubit, raw_types, pauli_gates, phased_x_z_gate
from cirq.ops.pauli_gates import Pauli
from cirq.type_workarounds import NotImplementedType
Expand Down Expand Up @@ -356,6 +358,8 @@ def _get_sqrt_map(
class CliffordGate(raw_types.Gate, CommonCliffordGates):
"""Clifford rotation for N-qubit."""

_clifford_tableau: qis.CliffordTableau
Copy link
Collaborator

Choose a reason for hiding this comment

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

Nit: We can move this to line 382 as

        self._clifford_tableau: qis.CliffordTableau


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

@property
def clifford_tableau(self):
Expand All @@ -399,6 +403,12 @@ def _has_stabilizer_effect_(self) -> Optional[bool]:
def __pow__(self, exponent) -> 'CliffordGate':
if exponent == -1:
return CliffordGate.from_clifford_tableau(self.clifford_tableau.inverse())
if exponent == 0:
return CliffordGate.from_clifford_tableau(
qis.CliffordTableau(num_qubits=self._num_qubits_())
)
if exponent == 1:
return self
if exponent > 0 and int(exponent) == exponent:
base_tableau = self.clifford_tableau.copy()
for _ in range(int(exponent) - 1):
Expand Down Expand Up @@ -457,6 +467,7 @@ def _act_on_(
return NotImplemented


@dataclass(frozen=True, init=False, eq=False, repr=False)
Copy link
Contributor

Choose a reason for hiding this comment

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

What does making this a dataclass actually do here with all these options set to False?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

makes the class frozen while not overriding any method. the default parameters will create an __init__, __eq__ and __repr__ but we already implement these.

@value.value_equality(manual_cls=True)
class SingleQubitCliffordGate(CliffordGate):
"""Any single qubit Clifford rotation."""
Expand All @@ -468,6 +479,7 @@ def _num_qubits_(self):
return 1

@staticmethod
@functools.cache
def from_clifford_tableau(tableau: qis.CliffordTableau) -> 'SingleQubitCliffordGate':
if not isinstance(tableau, qis.CliffordTableau):
raise ValueError('Input argument has to be a CliffordTableau instance.')
Expand Down Expand Up @@ -679,6 +691,10 @@ def to_phased_xz_gate(self) -> phased_x_z_gate.PhasedXZGate:
* {middle point of xyz in 4 Quadrant} * 120 is [[0, 1], [1, 1]]
* {middle point of xyz in 4 Quadrant} * 240 is [[1, 1], [1, 0]]
"""
return self._to_phased_xz_gate

@cached_property
def _to_phased_xz_gate(self) -> phased_x_z_gate.PhasedXZGate:
x_to_flip, z_to_flip = self.clifford_tableau.rs
flip_index = int(z_to_flip) * 2 + x_to_flip
a, x, z = 0.0, 0.0, 0.0
Expand Down Expand Up @@ -716,7 +732,7 @@ def to_phased_xz_gate(self) -> phased_x_z_gate.PhasedXZGate:
z = -0.5 if x_to_flip else 0.5
return phased_x_z_gate.PhasedXZGate(x_exponent=x, z_exponent=z, axis_phase_exponent=a)

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

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

def _unitary_(self) -> np.ndarray:
return self._unitary

@cached_property
def _unitary(self) -> np.ndarray:
mat = np.eye(2)
qubit = named_qubit.NamedQubit('arbitrary')
for op in protocols.decompose_once_with_qubits(self, (qubit,)):
Expand All @@ -787,6 +808,10 @@ def decompose_gate(self) -> Sequence['cirq.Gate']:
clifford gate if applied in order. This decomposition agrees with
cirq.unitary(self), including global phase.
"""
return self._decompose_gate

@cached_property
def _decompose_gate(self) -> Sequence['cirq.Gate']:
if self == SingleQubitCliffordGate.H:
return [common_gates.H]
rotations = self.decompose_rotation()
Expand All @@ -802,6 +827,10 @@ def decompose_rotation(self) -> Sequence[Tuple[Pauli, int]]:
Note that the combined unitary effect of these rotations may
differ from cirq.unitary(self) by a global phase.
"""
return self._decompose_rotation

@cached_property
def _decompose_rotation(self) -> Sequence[Tuple[Pauli, int]]:
x_rot = self.pauli_tuple(pauli_gates.X)
y_rot = self.pauli_tuple(pauli_gates.Y)
z_rot = self.pauli_tuple(pauli_gates.Z)
Expand Down Expand Up @@ -895,6 +924,10 @@ def _circuit_diagram_info_(
)

def _value_equality_values_(self):
return self._value_equality_values

@cached_property
def _value_equality_values(self):
return self._clifford_tableau.matrix().tobytes() + self._clifford_tableau.rs.tobytes()

def _value_equality_values_cls_(self):
Expand Down
6 changes: 5 additions & 1 deletion cirq-core/cirq/qis/clifford_tableau.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
import numpy as np

from cirq import protocols
from cirq._compat import proper_repr
from cirq._compat import proper_repr, cached_method
from cirq.qis import quantum_state_representation
from cirq.value import big_endian_int_to_digits, linear_dict, random_state

Expand Down Expand Up @@ -652,3 +652,7 @@ def measure(
self, axes: Sequence[int], seed: 'cirq.RANDOM_STATE_OR_SEED_LIKE' = None
) -> List[int]:
return [self._measure(axis, random_state.parse_random_state(seed)) for axis in axes]

@cached_method
def __hash__(self) -> int:
Copy link
Contributor

Choose a reason for hiding this comment

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

consider making this a cached_method as well.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

done

return hash(self.matrix().tobytes() + self.rs.tobytes())