Skip to content

Commit c812f00

Browse files
authored
Generalize X and Z to Weyl–Heisenberg gates (#4919)
Closes #4782, xref #3190 Implements X and Z as Weyl-Heisenberg gates, allowing them to operate on qudits of arbitrary dimension. The implementations are done via eigen_components, thus allowing interoperability with arbitrary exponents and global phase. Tests are done to ensure the unitaries at exponent 1 correspond to the standard definitions of Shift and Clock, at dimension 3 and 4. We also test simulations at non-integral exponents to make sure they perform as expected. Following the precedent set by the identity gate, the diagram labels remain X and Z when applied to qudits, as the qudit dimension itself is already labeled in the diagram. There is no negative impact on standard X and Z performance. In fact the performance is slightly improved over the existing X and Z gate implementations because we now cache the eigen_components rather than generate them each time. Testing shows a roughly 10% improvement in unitary calculation due to this change. Additionally this removes all the existing `PlusGate` definitions and replaces their use with the new `X` gate functionality instead.
1 parent b172876 commit c812f00

File tree

6 files changed

+335
-140
lines changed

6 files changed

+335
-140
lines changed

cirq-core/cirq/ops/common_channels.py

Lines changed: 1 addition & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -717,31 +717,14 @@ def _act_on_(self, sim_state: 'cirq.SimulationStateBase', qubits: Sequence['cirq
717717
if len(qubits) != 1:
718718
return NotImplemented
719719

720-
class PlusGate(raw_types.Gate):
721-
"""A qudit gate that increments a qudit state mod its dimension."""
722-
723-
def __init__(self, dimension, increment=1):
724-
self.dimension = dimension
725-
self.increment = increment % dimension
726-
727-
def _qid_shape_(self):
728-
return (self.dimension,)
729-
730-
def _unitary_(self):
731-
inc = (self.increment - 1) % self.dimension + 1
732-
u = np.empty((self.dimension, self.dimension))
733-
u[inc:] = np.eye(self.dimension)[:-inc]
734-
u[:inc] = np.eye(self.dimension)[-inc:]
735-
return u
736-
737720
from cirq.sim import simulation_state
738721

739722
if (
740723
isinstance(sim_state, simulation_state.SimulationState)
741724
and not sim_state.can_represent_mixed_states
742725
):
743726
result = sim_state._perform_measurement(qubits)[0]
744-
gate = PlusGate(self.dimension, self.dimension - result)
727+
gate = common_gates.XPowGate(dimension=self.dimension) ** (self.dimension - result)
745728
protocols.act_on(gate, sim_state, qubits)
746729
return True
747730

cirq-core/cirq/ops/common_gates.py

Lines changed: 84 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -87,11 +87,19 @@ class XPowGate(eigen_gate.EigenGate):
8787
`cirq.X`, the Pauli X gate, is an instance of this gate at `exponent=1`.
8888
"""
8989

90+
_eigencomponents: Dict[int, List[Tuple[float, np.ndarray]]] = {}
91+
92+
def __init__(
93+
self, *, exponent: value.TParamVal = 1.0, global_shift: float = 0.0, dimension: int = 2
94+
):
95+
super().__init__(exponent=exponent, global_shift=global_shift)
96+
self._dimension = dimension
97+
9098
def _num_qubits_(self) -> int:
9199
return 1
92100

93101
def _apply_unitary_(self, args: 'protocols.ApplyUnitaryArgs') -> Optional[np.ndarray]:
94-
if self._exponent != 1:
102+
if self._exponent != 1 or self._dimension != 2:
95103
return NotImplemented
96104
zero = args.subspace_index(0)
97105
one = args.subspace_index(1)
@@ -108,10 +116,27 @@ def in_su2(self) -> 'Rx':
108116

109117
def with_canonical_global_phase(self) -> 'XPowGate':
110118
"""Returns an equal-up-global-phase standardized form of the gate."""
111-
return XPowGate(exponent=self._exponent)
119+
return XPowGate(exponent=self._exponent, dimension=self._dimension)
120+
121+
def _qid_shape_(self) -> Tuple[int, ...]:
122+
return (self._dimension,)
112123

113124
def _eigen_components(self) -> List[Tuple[float, np.ndarray]]:
114-
return [(0, np.array([[0.5, 0.5], [0.5, 0.5]])), (1, np.array([[0.5, -0.5], [-0.5, 0.5]]))]
125+
if self._dimension not in XPowGate._eigencomponents:
126+
components = []
127+
root = 1j ** (4 / self._dimension)
128+
for i in range(self._dimension):
129+
half_turns = i * 2 / self._dimension
130+
v = np.array([root ** (i * j) / self._dimension for j in range(self._dimension)])
131+
m = np.array([np.roll(v, j) for j in range(self._dimension)])
132+
components.append((half_turns, m))
133+
XPowGate._eigencomponents[self._dimension] = components
134+
return XPowGate._eigencomponents[self._dimension]
135+
136+
def _with_exponent(self, exponent: 'cirq.TParamVal') -> 'cirq.XPowGate':
137+
return XPowGate(
138+
exponent=exponent, global_shift=self._global_shift, dimension=self._dimension
139+
)
115140

116141
def _decompose_into_clifford_with_qubits_(self, qubits):
117142
from cirq.ops.clifford_gate import SingleQubitCliffordGate
@@ -127,7 +152,7 @@ def _decompose_into_clifford_with_qubits_(self, qubits):
127152
return NotImplemented
128153

129154
def _trace_distance_bound_(self) -> Optional[float]:
130-
if self._is_parameterized_():
155+
if self._is_parameterized_() or self._dimension != 2:
131156
return None
132157
return abs(np.sin(self._exponent * 0.5 * np.pi))
133158

@@ -179,7 +204,7 @@ def controlled(
179204
return result
180205

181206
def _pauli_expansion_(self) -> value.LinearDict[str]:
182-
if protocols.is_parameterized(self):
207+
if protocols.is_parameterized(self) or self._dimension != 2:
183208
return NotImplemented
184209
phase = 1j ** (2 * self._exponent * (self._global_shift + 0.5))
185210
angle = np.pi * self._exponent / 2
@@ -220,7 +245,7 @@ def _phase_by_(self, phase_turns, qubit_index):
220245
)
221246

222247
def _has_stabilizer_effect_(self) -> Optional[bool]:
223-
if self._is_parameterized_():
248+
if self._is_parameterized_() or self._dimension != 2:
224249
return None
225250
return self.exponent % 0.5 == 0
226251

@@ -232,13 +257,19 @@ def __str__(self) -> str:
232257
return f'XPowGate(exponent={self._exponent}, global_shift={self._global_shift!r})'
233258

234259
def __repr__(self) -> str:
235-
if self._global_shift == 0:
260+
if self._global_shift == 0 and self._dimension == 2:
236261
if self._exponent == 1:
237262
return 'cirq.X'
238263
return f'(cirq.X**{proper_repr(self._exponent)})'
239-
return 'cirq.XPowGate(exponent={}, global_shift={!r})'.format(
240-
proper_repr(self._exponent), self._global_shift
241-
)
264+
args = []
265+
if self._exponent != 1:
266+
args.append(f'exponent={proper_repr(self._exponent)}')
267+
if self._global_shift != 0:
268+
args.append(f'global_shift={self._global_shift}')
269+
if self._dimension != 2:
270+
args.append(f'dimension={self._dimension}')
271+
all_args = ', '.join(args)
272+
return f'cirq.XPowGate({all_args})'
242273

243274

244275
class Rx(XPowGate):
@@ -478,16 +509,25 @@ class ZPowGate(eigen_gate.EigenGate):
478509
`cirq.Z`, the Pauli Z gate, is an instance of this gate at `exponent=1`.
479510
"""
480511

512+
_eigencomponents: Dict[int, List[Tuple[float, np.ndarray]]] = {}
513+
514+
def __init__(
515+
self, *, exponent: value.TParamVal = 1.0, global_shift: float = 0.0, dimension: int = 2
516+
):
517+
super().__init__(exponent=exponent, global_shift=global_shift)
518+
self._dimension = dimension
519+
481520
def _num_qubits_(self) -> int:
482521
return 1
483522

484523
def _apply_unitary_(self, args: 'protocols.ApplyUnitaryArgs') -> Optional[np.ndarray]:
485524
if protocols.is_parameterized(self):
486525
return None
487526

488-
one = args.subspace_index(1)
489-
c = 1j ** (self._exponent * 2)
490-
args.target_tensor[one] *= c
527+
for i in range(1, self._dimension):
528+
subspace = args.subspace_index(i)
529+
c = 1j ** (self._exponent * 4 * i / self._dimension)
530+
args.target_tensor[subspace] *= c
491531
p = 1j ** (2 * self._exponent * self._global_shift)
492532
if p != 1:
493533
args.target_tensor *= p
@@ -512,7 +552,7 @@ def in_su2(self) -> 'Rz':
512552

513553
def with_canonical_global_phase(self) -> 'ZPowGate':
514554
"""Returns an equal-up-global-phase standardized form of the gate."""
515-
return ZPowGate(exponent=self._exponent)
555+
return ZPowGate(exponent=self._exponent, dimension=self._dimension)
516556

517557
def controlled(
518558
self,
@@ -561,16 +601,32 @@ def controlled(
561601
)
562602
return result
563603

604+
def _qid_shape_(self) -> Tuple[int, ...]:
605+
return (self._dimension,)
606+
564607
def _eigen_components(self) -> List[Tuple[float, np.ndarray]]:
565-
return [(0, np.diag([1, 0])), (1, np.diag([0, 1]))]
608+
if self._dimension not in ZPowGate._eigencomponents:
609+
components = []
610+
for i in range(self._dimension):
611+
half_turns = i * 2 / self._dimension
612+
m = np.zeros((self._dimension, self._dimension))
613+
m[i][i] = 1
614+
components.append((half_turns, m))
615+
ZPowGate._eigencomponents[self._dimension] = components
616+
return ZPowGate._eigencomponents[self._dimension]
617+
618+
def _with_exponent(self, exponent: 'cirq.TParamVal') -> 'cirq.ZPowGate':
619+
return ZPowGate(
620+
exponent=exponent, global_shift=self._global_shift, dimension=self._dimension
621+
)
566622

567623
def _trace_distance_bound_(self) -> Optional[float]:
568-
if self._is_parameterized_():
624+
if self._is_parameterized_() or self._dimension != 2:
569625
return None
570626
return abs(np.sin(self._exponent * 0.5 * np.pi))
571627

572628
def _pauli_expansion_(self) -> value.LinearDict[str]:
573-
if protocols.is_parameterized(self):
629+
if protocols.is_parameterized(self) or self._dimension != 2:
574630
return NotImplemented
575631
phase = 1j ** (2 * self._exponent * (self._global_shift + 0.5))
576632
angle = np.pi * self._exponent / 2
@@ -580,7 +636,7 @@ def _phase_by_(self, phase_turns: float, qubit_index: int):
580636
return self
581637

582638
def _has_stabilizer_effect_(self) -> Optional[bool]:
583-
if self._is_parameterized_():
639+
if self._is_parameterized_() or self._dimension != 2:
584640
return None
585641
return self.exponent % 0.5 == 0
586642

@@ -630,7 +686,7 @@ def __str__(self) -> str:
630686
return f'ZPowGate(exponent={self._exponent}, global_shift={self._global_shift!r})'
631687

632688
def __repr__(self) -> str:
633-
if self._global_shift == 0:
689+
if self._global_shift == 0 and self._dimension == 2:
634690
if self._exponent == 0.25:
635691
return 'cirq.T'
636692
if self._exponent == -0.25:
@@ -642,9 +698,15 @@ def __repr__(self) -> str:
642698
if self._exponent == 1:
643699
return 'cirq.Z'
644700
return f'(cirq.Z**{proper_repr(self._exponent)})'
645-
return 'cirq.ZPowGate(exponent={}, global_shift={!r})'.format(
646-
proper_repr(self._exponent), self._global_shift
647-
)
701+
args = []
702+
if self._exponent != 1:
703+
args.append(f'exponent={proper_repr(self._exponent)}')
704+
if self._global_shift != 0:
705+
args.append(f'global_shift={self._global_shift}')
706+
if self._dimension != 2:
707+
args.append(f'dimension={self._dimension}')
708+
all_args = ', '.join(args)
709+
return f'cirq.ZPowGate({all_args})'
648710

649711
def _commutes_on_qids_(
650712
self, qids: 'Sequence[cirq.Qid]', other: Any, *, atol: float = 1e-8

0 commit comments

Comments
 (0)