Skip to content

Commit b5af9da

Browse files
daxfohlrht
authored andcommitted
Fix act-on specialization for all the Clifford simulators (quantumlib#4748)
Fixes quantumlib#4639, ref [tinyurl.com/clifford-simulators-refactor](https://tinyurl.com/clifford-simulators-refactor) This change makes the Clifford simulators more consistent with other simulators. Rather than the gate having all the logic of how to update the simulator state, which seemed out-of-place and limited the ability to reuse in any new kinds of Clifford simulators, this PR moves all the logic to the simulators themselves. It also creates an ABC for Clifford simulator states, allowing reuse of the evolution logic in the base class, such that the subclasses just have to implement the specific gate applicators. Note I *tried* doing this via a new Clifford protocol as well ([link](https://github.com/quantumlib/Cirq/compare/master...daxfohl:clifford3?expand=1)) as we originally discussed, and it works but I didn't like the result. The protocol ended up just duplicating all the information of the gate, in a weird structure, and felt like an extra, unnecessary, hacky wrapper around just using the gate itself. So I prefer the approach in this PR that just uses the gate instances directly, though I'm open to suggestion. @tanujkhattar @viathor (cc @ybc1991)
1 parent 02d9b00 commit b5af9da

11 files changed

+392
-423
lines changed

cirq-core/cirq/__init__.py

+1
Original file line numberDiff line numberDiff line change
@@ -414,6 +414,7 @@
414414
ActOnCliffordTableauArgs,
415415
ActOnDensityMatrixArgs,
416416
ActOnStabilizerCHFormArgs,
417+
ActOnStabilizerArgs,
417418
ActOnStateVectorArgs,
418419
StabilizerStateChForm,
419420
CIRCUIT_LIKE,

cirq-core/cirq/ops/common_channels.py

-10
Original file line numberDiff line numberDiff line change
@@ -316,16 +316,6 @@ def __str__(self) -> str:
316316
return f"depolarize(p={self._p})"
317317
return f"depolarize(p={self._p},n_qubits={self._n_qubits})"
318318

319-
def _act_on_(self, args: 'cirq.ActOnArgs', qubits: Sequence['cirq.Qid']) -> bool:
320-
from cirq.sim import clifford
321-
322-
if isinstance(args, clifford.ActOnCliffordTableauArgs):
323-
if args.prng.random() < self._p:
324-
gate = args.prng.choice([pauli_gates.X, pauli_gates.Y, pauli_gates.Z])
325-
protocols.act_on(gate, args, qubits)
326-
return True
327-
return NotImplemented
328-
329319
def _circuit_diagram_info_(self, args: 'protocols.CircuitDiagramInfoArgs') -> Tuple[str, ...]:
330320
result: Tuple[str, ...]
331321
if args.precision is not None:

cirq-core/cirq/ops/common_gates.py

-247
Original file line numberDiff line numberDiff line change
@@ -62,12 +62,6 @@
6262
"""
6363

6464

65-
def _act_with_gates(args, qubits, *gates: 'cirq.SupportsActOnQubits') -> None:
66-
"""Act on the given args with the given gates in order."""
67-
for gate in gates:
68-
assert gate._act_on_(args, qubits)
69-
70-
7165
def _pi(rads):
7266
return sympy.pi if protocols.is_parameterized(rads) else np.pi
7367

@@ -108,35 +102,6 @@ def _apply_unitary_(self, args: 'protocols.ApplyUnitaryArgs') -> Optional[np.nda
108102
args.available_buffer *= p
109103
return args.available_buffer
110104

111-
def _act_on_(self, args: 'cirq.ActOnArgs', qubits: Sequence['cirq.Qid']):
112-
from cirq.sim import clifford
113-
114-
if isinstance(args, clifford.ActOnCliffordTableauArgs):
115-
if not protocols.has_stabilizer_effect(self):
116-
return NotImplemented
117-
tableau = args.tableau
118-
q = args.qubit_map[qubits[0]]
119-
effective_exponent = self._exponent % 2
120-
if effective_exponent == 0.5:
121-
tableau.xs[:, q] ^= tableau.zs[:, q]
122-
tableau.rs[:] ^= tableau.xs[:, q] & tableau.zs[:, q]
123-
elif effective_exponent == 1:
124-
tableau.rs[:] ^= tableau.zs[:, q]
125-
elif effective_exponent == 1.5:
126-
tableau.rs[:] ^= tableau.xs[:, q] & tableau.zs[:, q]
127-
tableau.xs[:, q] ^= tableau.zs[:, q]
128-
return True
129-
130-
if isinstance(args, clifford.ActOnStabilizerCHFormArgs):
131-
if not protocols.has_stabilizer_effect(self):
132-
return NotImplemented
133-
_act_with_gates(args, qubits, H, ZPowGate(exponent=self._exponent), H)
134-
# Adjust the global phase based on the global_shift parameter.
135-
args.state.omega *= np.exp(1j * np.pi * self.global_shift * self.exponent)
136-
return True
137-
138-
return NotImplemented
139-
140105
def in_su2(self) -> 'Rx':
141106
"""Returns an equal-up-global-phase gate from the group SU2."""
142107
return Rx(rads=self._exponent * _pi(self._exponent))
@@ -362,51 +327,6 @@ def _apply_unitary_(self, args: 'protocols.ApplyUnitaryArgs') -> Optional[np.nda
362327
args.available_buffer *= p
363328
return args.available_buffer
364329

365-
def _act_on_(self, args: 'cirq.ActOnArgs', qubits: Sequence['cirq.Qid']):
366-
from cirq.sim import clifford
367-
368-
if isinstance(args, clifford.ActOnCliffordTableauArgs):
369-
if not protocols.has_stabilizer_effect(self):
370-
return NotImplemented
371-
tableau = args.tableau
372-
q = args.qubit_map[qubits[0]]
373-
effective_exponent = self._exponent % 2
374-
if effective_exponent == 0.5:
375-
tableau.rs[:] ^= tableau.xs[:, q] & (~tableau.zs[:, q])
376-
(tableau.xs[:, q], tableau.zs[:, q]) = (
377-
tableau.zs[:, q].copy(),
378-
tableau.xs[:, q].copy(),
379-
)
380-
elif effective_exponent == 1:
381-
tableau.rs[:] ^= tableau.xs[:, q] ^ tableau.zs[:, q]
382-
elif effective_exponent == 1.5:
383-
tableau.rs[:] ^= ~(tableau.xs[:, q]) & tableau.zs[:, q]
384-
(tableau.xs[:, q], tableau.zs[:, q]) = (
385-
tableau.zs[:, q].copy(),
386-
tableau.xs[:, q].copy(),
387-
)
388-
return True
389-
390-
if isinstance(args, clifford.ActOnStabilizerCHFormArgs):
391-
if not protocols.has_stabilizer_effect(self):
392-
return NotImplemented
393-
effective_exponent = self._exponent % 2
394-
state = args.state
395-
Z = ZPowGate()
396-
if effective_exponent == 0.5:
397-
_act_with_gates(args, qubits, Z, H)
398-
state.omega *= (1 + 1j) / (2 ** 0.5)
399-
elif effective_exponent == 1:
400-
_act_with_gates(args, qubits, Z, H, Z, H)
401-
state.omega *= 1j
402-
elif effective_exponent == 1.5:
403-
_act_with_gates(args, qubits, H, Z)
404-
state.omega *= (1 - 1j) / (2 ** 0.5)
405-
# Adjust the global phase based on the global_shift parameter.
406-
args.state.omega *= np.exp(1j * np.pi * self.global_shift * self.exponent)
407-
return True
408-
return NotImplemented
409-
410330
def in_su2(self) -> 'Ry':
411331
"""Returns an equal-up-global-phase gate from the group SU2."""
412332
return Ry(rads=self._exponent * _pi(self._exponent))
@@ -580,42 +500,6 @@ def _apply_unitary_(self, args: 'protocols.ApplyUnitaryArgs') -> Optional[np.nda
580500
args.target_tensor *= p
581501
return args.target_tensor
582502

583-
def _act_on_(self, args: 'cirq.ActOnArgs', qubits: Sequence['cirq.Qid']):
584-
from cirq.sim import clifford
585-
586-
if isinstance(args, clifford.ActOnCliffordTableauArgs):
587-
if not protocols.has_stabilizer_effect(self):
588-
return NotImplemented
589-
tableau = args.tableau
590-
q = args.qubit_map[qubits[0]]
591-
effective_exponent = self._exponent % 2
592-
if effective_exponent == 0.5:
593-
tableau.rs[:] ^= tableau.xs[:, q] & tableau.zs[:, q]
594-
tableau.zs[:, q] ^= tableau.xs[:, q]
595-
elif effective_exponent == 1:
596-
tableau.rs[:] ^= tableau.xs[:, q]
597-
elif effective_exponent == 1.5:
598-
tableau.rs[:] ^= tableau.xs[:, q] & (~tableau.zs[:, q])
599-
tableau.zs[:, q] ^= tableau.xs[:, q]
600-
return True
601-
602-
if isinstance(args, clifford.ActOnStabilizerCHFormArgs):
603-
if not protocols.has_stabilizer_effect(self):
604-
return NotImplemented
605-
q = args.qubit_map[qubits[0]]
606-
effective_exponent = self._exponent % 2
607-
state = args.state
608-
for _ in range(int(effective_exponent * 2)):
609-
# Prescription for S left multiplication.
610-
# Reference: https://arxiv.org/abs/1808.00128 Proposition 4 end
611-
state.M[q, :] ^= state.G[q, :]
612-
state.gamma[q] = (state.gamma[q] - 1) % 4
613-
# Adjust the global phase based on the global_shift parameter.
614-
args.state.omega *= np.exp(1j * np.pi * self.global_shift * self.exponent)
615-
return True
616-
617-
return NotImplemented
618-
619503
def _decompose_into_clifford_with_qubits_(self, qubits):
620504
from cirq.ops.clifford_gate import SingleQubitCliffordGate
621505

@@ -899,49 +783,6 @@ def _apply_unitary_(self, args: 'protocols.ApplyUnitaryArgs') -> Optional[np.nda
899783
args.target_tensor *= np.sqrt(2) * p
900784
return args.target_tensor
901785

902-
def _act_on_(self, args: 'cirq.ActOnArgs', qubits: Sequence['cirq.Qid']):
903-
from cirq.sim import clifford
904-
905-
if isinstance(args, clifford.ActOnCliffordTableauArgs):
906-
if not protocols.has_stabilizer_effect(self):
907-
return NotImplemented
908-
tableau = args.tableau
909-
q = args.qubit_map[qubits[0]]
910-
if self._exponent % 2 == 1:
911-
(tableau.xs[:, q], tableau.zs[:, q]) = (
912-
tableau.zs[:, q].copy(),
913-
tableau.xs[:, q].copy(),
914-
)
915-
tableau.rs[:] ^= tableau.xs[:, q] & tableau.zs[:, q]
916-
return True
917-
918-
if isinstance(args, clifford.ActOnStabilizerCHFormArgs):
919-
if not protocols.has_stabilizer_effect(self):
920-
return NotImplemented
921-
q = args.qubit_map[qubits[0]]
922-
state = args.state
923-
if self._exponent % 2 == 1:
924-
# Prescription for H left multiplication
925-
# Reference: https://arxiv.org/abs/1808.00128
926-
# Equations 48, 49 and Proposition 4
927-
t = state.s ^ (state.G[q, :] & state.v)
928-
u = state.s ^ (state.F[q, :] & (~state.v)) ^ (state.M[q, :] & state.v)
929-
930-
alpha = sum(state.G[q, :] & (~state.v) & state.s) % 2
931-
beta = sum(state.M[q, :] & (~state.v) & state.s)
932-
beta += sum(state.F[q, :] & state.v & state.M[q, :])
933-
beta += sum(state.F[q, :] & state.v & state.s)
934-
beta %= 2
935-
936-
delta = (state.gamma[q] + 2 * (alpha + beta)) % 4
937-
938-
state.update_sum(t, u, delta=delta, alpha=alpha)
939-
# Adjust the global phase based on the global_shift parameter.
940-
args.state.omega *= np.exp(1j * np.pi * self.global_shift * self.exponent)
941-
return True
942-
943-
return NotImplemented
944-
945786
def _decompose_(self, qubits):
946787
q = qubits[0]
947788

@@ -1063,52 +904,6 @@ def _apply_unitary_(
1063904
args.target_tensor *= p
1064905
return args.target_tensor
1065906

1066-
def _act_on_(self, args: 'cirq.ActOnArgs', qubits: Sequence['cirq.Qid']):
1067-
from cirq.sim import clifford
1068-
1069-
if isinstance(args, clifford.ActOnCliffordTableauArgs):
1070-
if not protocols.has_stabilizer_effect(self):
1071-
return NotImplemented
1072-
tableau = args.tableau
1073-
q1 = args.qubit_map[qubits[0]]
1074-
q2 = args.qubit_map[qubits[1]]
1075-
if self._exponent % 2 == 1:
1076-
(tableau.xs[:, q2], tableau.zs[:, q2]) = (
1077-
tableau.zs[:, q2].copy(),
1078-
tableau.xs[:, q2].copy(),
1079-
)
1080-
tableau.rs[:] ^= tableau.xs[:, q2] & tableau.zs[:, q2]
1081-
tableau.rs[:] ^= (
1082-
tableau.xs[:, q1]
1083-
& tableau.zs[:, q2]
1084-
& (~(tableau.xs[:, q2] ^ tableau.zs[:, q1]))
1085-
)
1086-
tableau.xs[:, q2] ^= tableau.xs[:, q1]
1087-
tableau.zs[:, q1] ^= tableau.zs[:, q2]
1088-
(tableau.xs[:, q2], tableau.zs[:, q2]) = (
1089-
tableau.zs[:, q2].copy(),
1090-
tableau.xs[:, q2].copy(),
1091-
)
1092-
tableau.rs[:] ^= tableau.xs[:, q2] & tableau.zs[:, q2]
1093-
return True
1094-
1095-
if isinstance(args, clifford.ActOnStabilizerCHFormArgs):
1096-
if not protocols.has_stabilizer_effect(self):
1097-
return NotImplemented
1098-
q1 = args.qubit_map[qubits[0]]
1099-
q2 = args.qubit_map[qubits[1]]
1100-
state = args.state
1101-
if self._exponent % 2 == 1:
1102-
# Prescription for CZ left multiplication.
1103-
# Reference: https://arxiv.org/abs/1808.00128 Proposition 4 end
1104-
state.M[q1, :] ^= state.G[q2, :]
1105-
state.M[q2, :] ^= state.G[q1, :]
1106-
# Adjust the global phase based on the global_shift parameter.
1107-
args.state.omega *= np.exp(1j * np.pi * self.global_shift * self.exponent)
1108-
return True
1109-
1110-
return NotImplemented
1111-
1112907
def _pauli_expansion_(self) -> value.LinearDict[str]:
1113908
if protocols.is_parameterized(self):
1114909
return NotImplemented
@@ -1291,48 +1086,6 @@ def _apply_unitary_(self, args: 'protocols.ApplyUnitaryArgs') -> Optional[np.nda
12911086
args.target_tensor *= p
12921087
return args.target_tensor
12931088

1294-
def _act_on_(self, args: 'cirq.ActOnArgs', qubits: Sequence['cirq.Qid']):
1295-
from cirq.sim import clifford
1296-
1297-
if isinstance(args, clifford.ActOnCliffordTableauArgs):
1298-
if not protocols.has_stabilizer_effect(self):
1299-
return NotImplemented
1300-
tableau = args.tableau
1301-
q1 = args.qubit_map[qubits[0]]
1302-
q2 = args.qubit_map[qubits[1]]
1303-
if self._exponent % 2 == 1:
1304-
tableau.rs[:] ^= (
1305-
tableau.xs[:, q1]
1306-
& tableau.zs[:, q2]
1307-
& (~(tableau.xs[:, q2] ^ tableau.zs[:, q1]))
1308-
)
1309-
tableau.xs[:, q2] ^= tableau.xs[:, q1]
1310-
tableau.zs[:, q1] ^= tableau.zs[:, q2]
1311-
return True
1312-
1313-
if isinstance(args, clifford.ActOnStabilizerCHFormArgs):
1314-
if not protocols.has_stabilizer_effect(self):
1315-
return NotImplemented
1316-
q1 = args.qubit_map[qubits[0]]
1317-
q2 = args.qubit_map[qubits[1]]
1318-
state = args.state
1319-
if self._exponent % 2 == 1:
1320-
# Prescription for CX left multiplication.
1321-
# Reference: https://arxiv.org/abs/1808.00128 Proposition 4 end
1322-
state.gamma[q1] = (
1323-
state.gamma[q1]
1324-
+ state.gamma[q2]
1325-
+ 2 * (sum(state.M[q1, :] & state.F[q2, :]) % 2)
1326-
) % 4
1327-
state.G[q2, :] ^= state.G[q1, :]
1328-
state.F[q1, :] ^= state.F[q2, :]
1329-
state.M[q1, :] ^= state.M[q2, :]
1330-
# Adjust the global phase based on the global_shift parameter.
1331-
args.state.omega *= np.exp(1j * np.pi * self.global_shift * self.exponent)
1332-
return True
1333-
1334-
return NotImplemented
1335-
13361089
def _pauli_expansion_(self) -> value.LinearDict[str]:
13371090
if protocols.is_parameterized(self):
13381091
return NotImplemented

cirq-core/cirq/ops/global_phase_op.py

-14
Original file line numberDiff line numberDiff line change
@@ -87,20 +87,6 @@ def _apply_unitary_(self, args) -> np.ndarray:
8787
def _has_stabilizer_effect_(self) -> bool:
8888
return True
8989

90-
def _act_on_(self, args: 'cirq.ActOnArgs', qubits):
91-
from cirq.sim import clifford
92-
93-
if isinstance(args, clifford.ActOnCliffordTableauArgs):
94-
# Since CliffordTableau does not keep track of the global phase,
95-
# it's safe to just ignore it here.
96-
return True
97-
98-
if isinstance(args, clifford.ActOnStabilizerCHFormArgs):
99-
args.state.omega *= self.coefficient
100-
return True
101-
102-
return NotImplemented
103-
10490
def __str__(self) -> str:
10591
return str(self.coefficient)
10692

cirq-core/cirq/ops/random_gate_channel.py

-15
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,6 @@
2222
cast,
2323
SupportsFloat,
2424
Optional,
25-
Sequence,
2625
)
2726

2827
import numpy as np
@@ -123,20 +122,6 @@ def _trace_distance_bound_(self) -> float:
123122
result *= float(self.probability)
124123
return result
125124

126-
def _act_on_(self, args: 'cirq.ActOnArgs', qubits: Sequence['cirq.Qid']):
127-
from cirq.sim import clifford
128-
129-
if self._is_parameterized_():
130-
return NotImplemented
131-
if isinstance(args, clifford.ActOnCliffordTableauArgs):
132-
if args.prng.random() < self.probability:
133-
# Note: because we're doing this probabilistically, it's not
134-
# safe to fallback to other strategies if act_on fails. Those
135-
# strategies could double-count the probability.
136-
protocols.act_on(self.sub_gate, args, qubits)
137-
return True
138-
return NotImplemented
139-
140125
def _json_dict_(self) -> Dict[str, Any]:
141126
return protocols.obj_to_dict_helper(self, ['sub_gate', 'probability'])
142127

0 commit comments

Comments
 (0)