Skip to content

Commit 4594a1f

Browse files
authored
Add qubits to PauliStringPhasor (#5565)
1 parent 90c45d2 commit 4594a1f

File tree

8 files changed

+283
-57
lines changed

8 files changed

+283
-57
lines changed

cirq-core/cirq/contrib/paulistring/clifford_optimize.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414

1515
from typing import Tuple, cast
1616

17-
from cirq import ops, circuits
17+
from cirq import circuits, ops, protocols
1818
from cirq.contrib.paulistring.convert_gate_set import converted_gate_set
1919

2020

@@ -87,10 +87,13 @@ def try_merge_clifford(cliff_op: ops.GateOperation, start_i: int) -> bool:
8787
merge_i, merge_op, num_passed = find_merge_point(start_i, string_op, quarter_turns == 2)
8888
assert merge_i > start_i
8989
assert len(merge_op.pauli_string) == 1, 'PauliString length != 1'
90+
assert not protocols.is_parameterized(merge_op.pauli_string)
91+
coefficient = merge_op.pauli_string.coefficient
92+
assert isinstance(coefficient, complex)
9093

9194
qubit, pauli = next(iter(merge_op.pauli_string.items()))
9295
quarter_turns = round(merge_op.exponent_relative * 2)
93-
quarter_turns *= int(merge_op.pauli_string.coefficient.real)
96+
quarter_turns *= int(coefficient.real)
9497
quarter_turns %= 4
9598
part_cliff_gate = ops.SingleQubitCliffordGate.from_quarter_turns(pauli, quarter_turns)
9699

cirq-core/cirq/ops/gate_operation_test.py

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -488,6 +488,7 @@ def all_subclasses(cls):
488488

489489
skip_classes = {
490490
# Abstract or private parent classes.
491+
cirq.ArithmeticGate,
491492
cirq.BaseDensePauliString,
492493
cirq.EigenGate,
493494
cirq.Pauli,
@@ -503,17 +504,10 @@ def all_subclasses(cls):
503504
# Interop gates
504505
cirq.interop.quirk.QuirkQubitPermutationGate,
505506
cirq.interop.quirk.QuirkArithmeticGate,
506-
# No reason given for missing json.
507-
# TODO(#5353): Serialize these gates.
508-
cirq.ArithmeticGate,
509507
}
510508

511-
# Gates that do not satisfy the contract.
512-
# TODO(#5167): Fix this case.
513-
exceptions = {cirq.PauliStringPhasorGate}
514-
515509
skipped = set()
516-
for gate_cls in gate_subclasses - exceptions:
510+
for gate_cls in gate_subclasses:
517511
filename = test_module_spec.test_data_path.joinpath(f"{gate_cls.__name__}.json")
518512
if pathlib.Path(filename).is_file():
519513
gates = cirq.read_json(filename)

cirq-core/cirq/ops/pauli_string.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -391,7 +391,7 @@ def _repr_pretty_(self, p: Any, cycle: bool) -> None:
391391
p.text(str(self))
392392

393393
def __repr__(self) -> str:
394-
ordered_qubits = sorted(self.qubits)
394+
ordered_qubits = self.qubits
395395
prefix = ''
396396

397397
factors = []

cirq-core/cirq/ops/pauli_string_phasor.py

Lines changed: 106 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,18 @@
1212
# See the License for the specific language governing permissions and
1313
# limitations under the License.
1414

15-
from typing import AbstractSet, cast, Dict, Iterable, Union, TYPE_CHECKING, Sequence, Iterator
15+
from typing import (
16+
AbstractSet,
17+
cast,
18+
Dict,
19+
Iterable,
20+
Iterator,
21+
Optional,
22+
Sequence,
23+
TYPE_CHECKING,
24+
Union,
25+
)
26+
1627
import numbers
1728

1829
import sympy
@@ -35,16 +46,25 @@
3546

3647
@value.value_equality(approximate=True)
3748
class PauliStringPhasor(gate_operation.GateOperation):
38-
"""An operation that phases the eigenstates of a Pauli string.
49+
r"""An operation that phases the eigenstates of a Pauli string.
50+
51+
This class takes `PauliString`, which is a sequence of non-identity
52+
Pauli operators, potentially with a $\pm 1$ valued coefficient,
53+
acting on qubits.
3954
4055
The -1 eigenstates of the Pauli string will have their amplitude multiplied
4156
by e^(i pi exponent_neg) while +1 eigenstates of the Pauli string will have
4257
their amplitude multiplied by e^(i pi exponent_pos).
58+
59+
The class also takes a list of qubits, which can be a superset of those
60+
acted on by the provided `PauliString`. Those extra qubits are assumed to be
61+
acted upon via identity.
4362
"""
4463

4564
def __init__(
4665
self,
4766
pauli_string: ps.PauliString,
67+
qubits: Optional[Sequence['cirq.Qid']] = None,
4868
*,
4969
exponent_neg: Union[int, float, sympy.Expr] = 1,
5070
exponent_pos: Union[int, float, sympy.Expr] = 0,
@@ -54,20 +74,36 @@ def __init__(
5474
Args:
5575
pauli_string: The PauliString defining the positive and negative
5676
eigenspaces that will be independently phased.
77+
qubits: The qubits upon which the PauliStringPhasor acts. This
78+
must be a superset of the qubits of `pauli_string`.
79+
If None, it will use the qubits from `pauli_string`
80+
The `pauli_string` contains only the non-identity component
81+
of the phasor, while the qubits supplied here and not in
82+
`pauli_string` are acted upon by identity. The order of
83+
these qubits must match the order in `pauli_string`.
5784
exponent_neg: How much to phase vectors in the negative eigenspace,
5885
in the form of the t in (-1)**t = exp(i pi t).
5986
exponent_pos: How much to phase vectors in the positive eigenspace,
6087
in the form of the t in (-1)**t = exp(i pi t).
6188
6289
Raises:
63-
ValueError: If coefficient is not 1 or -1.
90+
ValueError: If coefficient is not 1 or -1 or the qubits of
91+
`pauli_string` are not a subset of `qubits`.
6492
"""
93+
if qubits is not None:
94+
it = iter(qubits)
95+
if any(not any(q0 == q1 for q1 in it) for q0 in pauli_string.qubits):
96+
raise ValueError(
97+
f"PauliStringPhasor's pauli string qubits ({pauli_string.qubits}) "
98+
f"are not an ordered subset of the explicit qubits ({qubits})."
99+
)
100+
else:
101+
qubits = pauli_string.qubits
102+
# Use qubits below instead of `qubits or pauli_string.qubits`
65103
gate = PauliStringPhasorGate(
66-
pauli_string.dense(pauli_string.qubits),
67-
exponent_neg=exponent_neg,
68-
exponent_pos=exponent_pos,
104+
pauli_string.dense(qubits), exponent_neg=exponent_neg, exponent_pos=exponent_pos
69105
)
70-
super().__init__(gate, pauli_string.qubits)
106+
super().__init__(gate, qubits)
71107
self._pauli_string = gate.dense_pauli_string.on(*self.qubits)
72108

73109
@property
@@ -76,17 +112,17 @@ def gate(self) -> 'cirq.PauliStringPhasorGate':
76112
return cast(PauliStringPhasorGate, self._gate)
77113

78114
@property
79-
def exponent_neg(self):
115+
def exponent_neg(self) -> Union[int, float, sympy.Expr]:
80116
"""The negative exponent."""
81117
return self.gate.exponent_neg
82118

83119
@property
84-
def exponent_pos(self):
120+
def exponent_pos(self) -> Union[int, float, sympy.Expr]:
85121
"""The positive exponent."""
86122
return self.gate.exponent_pos
87123

88124
@property
89-
def pauli_string(self):
125+
def pauli_string(self) -> 'cirq.PauliString':
90126
"""The underlying pauli string."""
91127
return self._pauli_string
92128

@@ -96,41 +132,70 @@ def exponent_relative(self) -> Union[int, float, sympy.Expr]:
96132
return self.gate.exponent_relative
97133

98134
def _value_equality_values_(self):
99-
return (self.pauli_string, self.exponent_neg, self.exponent_pos)
135+
return (self.pauli_string, self.qubits, self.exponent_neg, self.exponent_pos)
100136

101-
def equal_up_to_global_phase(self, other):
137+
def equal_up_to_global_phase(self, other: 'PauliStringPhasor') -> bool:
102138
"""Checks equality of two PauliStringPhasors, up to global phase."""
103139
if isinstance(other, PauliStringPhasor):
104-
rel1 = self.exponent_relative
105-
rel2 = other.exponent_relative
106-
return rel1 == rel2 and self.pauli_string == other.pauli_string
140+
return (
141+
self.exponent_relative == other.exponent_relative
142+
and self.pauli_string == other.pauli_string
143+
and self.qubits == other.qubits
144+
)
107145
return False
108146

109-
def map_qubits(self, qubit_map: Dict[raw_types.Qid, raw_types.Qid]):
110-
"""Maps the qubits inside the PauliString."""
147+
def map_qubits(self, qubit_map: Dict[raw_types.Qid, raw_types.Qid]) -> 'PauliStringPhasor':
148+
"""Maps the qubits inside the PauliStringPhasor.
149+
150+
Args:
151+
qubit_map: A map from the qubits in the phasor to new qubits.
152+
153+
Returns:
154+
A new PauliStringPhasor with remapped qubits.
155+
156+
Raises:
157+
ValueError: If the map does not contain an entry for all
158+
the qubits in the phasor.
159+
"""
160+
if not set(self.qubits) <= qubit_map.keys():
161+
raise ValueError(
162+
"qubit_map must have a key for every qubit in the phasors qubits. "
163+
f"keys: {qubit_map.keys()} phasor qubits: {self.qubits}"
164+
)
111165
return PauliStringPhasor(
112-
self.pauli_string.map_qubits(qubit_map),
166+
pauli_string=self.pauli_string.map_qubits(qubit_map),
167+
qubits=[qubit_map[q] for q in self.qubits],
113168
exponent_neg=self.exponent_neg,
114169
exponent_pos=self.exponent_pos,
115170
)
116171

117172
def can_merge_with(self, op: 'PauliStringPhasor') -> bool:
118173
"""Checks whether the underlying PauliStrings can be merged."""
119-
return self.pauli_string.equal_up_to_coefficient(op.pauli_string)
174+
return (
175+
self.pauli_string.equal_up_to_coefficient(op.pauli_string) and self.qubits == op.qubits
176+
)
120177

121178
def merged_with(self, op: 'PauliStringPhasor') -> 'PauliStringPhasor':
122179
"""Merges two PauliStringPhasors."""
123180
if not self.can_merge_with(op):
124181
raise ValueError(f'Cannot merge operations: {self}, {op}')
125182
pp = self.exponent_pos + op.exponent_pos
126183
pn = self.exponent_neg + op.exponent_neg
127-
return PauliStringPhasor(self.pauli_string, exponent_pos=pp, exponent_neg=pn)
184+
return PauliStringPhasor(
185+
self.pauli_string, qubits=self.qubits, exponent_pos=pp, exponent_neg=pn
186+
)
128187

129188
def _circuit_diagram_info_(
130189
self, args: 'cirq.CircuitDiagramInfoArgs'
131190
) -> 'cirq.CircuitDiagramInfo':
132191
qubits = self.qubits if args.known_qubits is None else args.known_qubits
133-
syms = tuple(f'[{self.pauli_string[qubit]}]' for qubit in qubits)
192+
193+
def sym(qubit):
194+
if qubit in self.pauli_string:
195+
return f'[{self.pauli_string[qubit]}]'
196+
return '[I]'
197+
198+
syms = tuple(sym(qubit) for qubit in qubits)
134199
return protocols.CircuitDiagramInfo(wire_symbols=syms, exponent=self.exponent_relative)
135200

136201
def pass_operations_over(
@@ -170,6 +235,7 @@ def pass_operations_over(
170235
def __repr__(self) -> str:
171236
return (
172237
f'cirq.PauliStringPhasor({self.pauli_string!r}, '
238+
f'qubits={self.qubits!r}, '
173239
f'exponent_neg={proper_repr(self.exponent_neg)}, '
174240
f'exponent_pos={proper_repr(self.exponent_pos)})'
175241
)
@@ -182,7 +248,19 @@ def __str__(self) -> str:
182248
return f'({self.pauli_string})**{self.exponent_relative}'
183249

184250
def _json_dict_(self):
185-
return protocols.obj_to_dict_helper(self, ['pauli_string', 'exponent_neg', 'exponent_pos'])
251+
return protocols.obj_to_dict_helper(
252+
self, ['pauli_string', 'qubits', 'exponent_neg', 'exponent_pos']
253+
)
254+
255+
@classmethod
256+
def _from_json_dict_(cls, pauli_string, exponent_neg, exponent_pos, **kwargs):
257+
qubits = kwargs['qubits'] if 'qubits' in kwargs else None
258+
return PauliStringPhasor(
259+
pauli_string=pauli_string,
260+
qubits=qubits,
261+
exponent_neg=exponent_neg,
262+
exponent_pos=exponent_pos,
263+
)
186264

187265

188266
@value.value_equality(approximate=True)
@@ -234,24 +312,24 @@ def exponent_relative(self) -> Union[int, float, sympy.Expr]:
234312
return value.canonicalize_half_turns(self.exponent_neg - self.exponent_pos)
235313

236314
@property
237-
def exponent_neg(self):
315+
def exponent_neg(self) -> Union[int, float, sympy.Expr]:
238316
"""The negative exponent."""
239317
return self._exponent_neg
240318

241319
@property
242-
def exponent_pos(self):
320+
def exponent_pos(self) -> Union[int, float, sympy.Expr]:
243321
"""The positive exponent."""
244322
return self._exponent_pos
245323

246324
@property
247-
def dense_pauli_string(self):
325+
def dense_pauli_string(self) -> 'cirq.DensePauliString':
248326
"""The underlying DensePauliString."""
249327
return self._dense_pauli_string
250328

251329
def _value_equality_values_(self):
252330
return (self.dense_pauli_string, self.exponent_neg, self.exponent_pos)
253331

254-
def equal_up_to_global_phase(self, other):
332+
def equal_up_to_global_phase(self, other: 'cirq.PauliStringPhasorGate') -> bool:
255333
"""Checks equality of two PauliStringPhasors, up to global phase."""
256334
if isinstance(other, PauliStringPhasorGate):
257335
rel1 = self.exponent_relative
@@ -266,7 +344,7 @@ def __pow__(self, exponent: Union[float, sympy.Symbol]) -> 'PauliStringPhasorGat
266344
return NotImplemented
267345
return PauliStringPhasorGate(self.dense_pauli_string, exponent_neg=pn, exponent_pos=pp)
268346

269-
def _has_unitary_(self):
347+
def _has_unitary_(self) -> bool:
270348
return not self._is_parameterized_()
271349

272350
def _to_z_basis_ops(self, qubits: Sequence['cirq.Qid']) -> Iterator[raw_types.Operation]:
@@ -352,6 +430,7 @@ def on(self, *qubits: 'cirq.Qid') -> 'cirq.PauliStringPhasor':
352430
"""Creates a PauliStringPhasor on the qubits."""
353431
return PauliStringPhasor(
354432
self.dense_pauli_string.on(*qubits),
433+
qubits=qubits,
355434
exponent_pos=self.exponent_pos,
356435
exponent_neg=self.exponent_neg,
357436
)

0 commit comments

Comments
 (0)