diff --git a/cirq-core/cirq/contrib/paulistring/clifford_optimize.py b/cirq-core/cirq/contrib/paulistring/clifford_optimize.py index b80b6cef6dc..b7beb983812 100644 --- a/cirq-core/cirq/contrib/paulistring/clifford_optimize.py +++ b/cirq-core/cirq/contrib/paulistring/clifford_optimize.py @@ -77,9 +77,9 @@ def try_merge_clifford(cliff_op: ops.GateOperation, start_i: int) -> bool: for pauli, quarter_turns in reversed( cast(ops.SingleQubitCliffordGate, cliff_op.gate).decompose_rotation() ): - trans = remaining_cliff_gate.transform(pauli) - pauli = trans.to - quarter_turns *= -1 if trans.flip else 1 + trans = remaining_cliff_gate.pauli_tuple(pauli) + pauli = trans[0] + quarter_turns *= -1 if trans[1] else 1 string_op = ops.PauliStringPhasor( ops.PauliString(pauli(cliff_op.qubits[0])), exponent_neg=quarter_turns / 2 ) diff --git a/cirq-core/cirq/ops/clifford_gate.py b/cirq-core/cirq/ops/clifford_gate.py index 78b0dade8f2..a0b84558218 100644 --- a/cirq-core/cirq/ops/clifford_gate.py +++ b/cirq-core/cirq/ops/clifford_gate.py @@ -12,23 +12,13 @@ # See the License for the specific language governing permissions and # limitations under the License. -from typing import ( - Any, - cast, - Dict, - List, - NamedTuple, - Optional, - Sequence, - Tuple, - TYPE_CHECKING, - Union, -) +import dataclasses +from typing import Any, cast, Dict, List, Optional, Sequence, Tuple, TYPE_CHECKING, Union + import numpy as np -from cirq import protocols, value, linalg, qis -from cirq._doc import document +from cirq import _compat, protocols, value, linalg, qis from cirq._import import LazyLoader from cirq.ops import common_gates, identity, named_qubit, raw_types, pauli_gates, phased_x_z_gate from cirq.ops.pauli_gates import Pauli @@ -42,11 +32,15 @@ sim = LazyLoader("sim", globals(), "cirq.sim") transformers = LazyLoader("transformers", globals(), "cirq.transformers") -PauliTransform = NamedTuple('PauliTransform', [('to', Pauli), ('flip', bool)]) -document(PauliTransform, """+X, -X, +Y, -Y, +Z, or -Z.""") + +@_compat.deprecated_class(deadline='v0.16', fix='Use DensePauliString instead.') +@dataclasses.dataclass +class PauliTransform: + to: Pauli + flip: bool -def _to_pauli_transform(matrix: np.ndarray) -> Optional[PauliTransform]: +def _to_pauli_tuple(matrix: np.ndarray) -> Optional[Tuple[Pauli, bool]]: """Converts matrix to PauliTransform. If matrix is not ±Pauli matrix, returns None. @@ -54,17 +48,17 @@ def _to_pauli_transform(matrix: np.ndarray) -> Optional[PauliTransform]: for pauli in Pauli._XYZ: p = protocols.unitary(pauli) if np.allclose(matrix, p): - return PauliTransform(pauli, False) + return (pauli, False) if np.allclose(matrix, -p): - return PauliTransform(pauli, True) + return (pauli, True) return None def _to_clifford_tableau( - rotation_map: Optional[Dict[Pauli, PauliTransform]] = None, + rotation_map: Optional[Dict[Pauli, Tuple[Pauli, bool]]] = None, *, - x_to: Optional[PauliTransform] = None, - z_to: Optional[PauliTransform] = None, + x_to: Optional[Tuple[Pauli, bool]] = None, + z_to: Optional[Tuple[Pauli, bool]] = None, ) -> qis.CliffordTableau: """Transfer the rotation map to clifford tableau representation""" if x_to is None and z_to is None and rotation_map is None: @@ -79,13 +73,13 @@ def _to_clifford_tableau( assert x_to is not None and z_to is not None, "Both x_to and z_to have to be provided." clifford_tableau = qis.CliffordTableau(num_qubits=1) - clifford_tableau.xs[0, 0] = x_to.to in (pauli_gates.X, pauli_gates.Y) - clifford_tableau.zs[0, 0] = x_to.to in (pauli_gates.Y, pauli_gates.Z) + clifford_tableau.xs[0, 0] = x_to[0] in (pauli_gates.X, pauli_gates.Y) + clifford_tableau.zs[0, 0] = x_to[0] in (pauli_gates.Y, pauli_gates.Z) - clifford_tableau.xs[1, 0] = z_to.to in (pauli_gates.X, pauli_gates.Y) - clifford_tableau.zs[1, 0] = z_to.to in (pauli_gates.Y, pauli_gates.Z) + clifford_tableau.xs[1, 0] = z_to[0] in (pauli_gates.X, pauli_gates.Y) + clifford_tableau.zs[1, 0] = z_to[0] in (pauli_gates.Y, pauli_gates.Z) - clifford_tableau.rs = (x_to.flip, z_to.flip) + clifford_tableau.rs = (x_to[1], z_to[1]) return clifford_tableau @@ -101,7 +95,7 @@ def _validate_map_input( x_to: Optional[Tuple[Pauli, bool]], y_to: Optional[Tuple[Pauli, bool]], z_to: Optional[Tuple[Pauli, bool]], -) -> Dict[Pauli, PauliTransform]: +) -> Dict[Pauli, Tuple[Pauli, bool]]: if pauli_map_to is None: xyz_to = {pauli_gates.X: x_to, pauli_gates.Y: y_to, pauli_gates.Z: z_to} pauli_map_to = {cast(Pauli, p): trans for p, trans in xyz_to.items() if trans is not None} @@ -121,7 +115,7 @@ def _validate_map_input( ) if len(set((to for to, _ in pauli_map_to.values()))) != len(pauli_map_to): raise ValueError('A rotation cannot map two Paulis to the same') - return {frm: PauliTransform(to, flip) for frm, (to, flip) in pauli_map_to.items()} + return {frm: (to, flip) for frm, (to, flip) in pauli_map_to.items()} def _pad_tableau( @@ -505,7 +499,7 @@ def from_xz_map( z_to: Which Pauli to transform Z to and if it should negate. """ return SingleQubitCliffordGate.from_clifford_tableau( - _to_clifford_tableau(x_to=PauliTransform(*x_to), z_to=PauliTransform(*z_to)) + _to_clifford_tableau(x_to=x_to, z_to=z_to) ) @staticmethod @@ -538,7 +532,7 @@ def from_single_map( trans_from2 = trans_to trans_to2 = trans_from flip2 = not flip - rotation_map[trans_from2] = PauliTransform(trans_to2, flip2) + rotation_map[trans_from2] = (trans_to2, flip2) return SingleQubitCliffordGate.from_double_map( cast(Dict[Pauli, Tuple[Pauli, bool]], rotation_map) ) @@ -566,9 +560,9 @@ def from_double_map( rotation_map = _validate_map_input(2, pauli_map_to, x_to=x_to, y_to=y_to, z_to=z_to) (from1, trans1), (from2, trans2) = tuple(rotation_map.items()) from3 = from1.third(from2) - to3 = trans1.to.third(trans2.to) - flip3 = trans1.flip ^ trans2.flip ^ ((from1 < from2) != (trans1.to < trans2.to)) - rotation_map[from3] = PauliTransform(to3, flip3) + to3 = trans1[0].third(trans2[0]) + flip3 = trans1[1] ^ trans2[1] ^ ((from1 < from2) != (trans1[0] < trans2[0])) + rotation_map[from3] = (to3, flip3) return SingleQubitCliffordGate.from_clifford_tableau(_to_clifford_tableau(rotation_map)) @@ -578,15 +572,15 @@ def from_pauli(pauli: Pauli, sqrt: bool = False) -> 'SingleQubitCliffordGate': next_pauli = Pauli.by_relative_index(pauli, 1) if sqrt: rotation_map = { - prev_pauli: PauliTransform(next_pauli, True), - pauli: PauliTransform(pauli, False), - next_pauli: PauliTransform(prev_pauli, False), + prev_pauli: (next_pauli, True), + pauli: (pauli, False), + next_pauli: (prev_pauli, False), } else: rotation_map = { - prev_pauli: PauliTransform(prev_pauli, True), - pauli: PauliTransform(pauli, False), - next_pauli: PauliTransform(next_pauli, True), + prev_pauli: (prev_pauli, True), + pauli: (pauli, False), + next_pauli: (next_pauli, True), } return SingleQubitCliffordGate.from_clifford_tableau(_to_clifford_tableau(rotation_map)) @@ -618,15 +612,22 @@ def from_unitary(u: np.ndarray) -> Optional['SingleQubitCliffordGate']: return None x = protocols.unitary(pauli_gates.X) z = protocols.unitary(pauli_gates.Z) - x_to = _to_pauli_transform(u @ x @ u.conj().T) - z_to = _to_pauli_transform(u @ z @ u.conj().T) + x_to = _to_pauli_tuple(u @ x @ u.conj().T) + z_to = _to_pauli_tuple(u @ z @ u.conj().T) if x_to is None or z_to is None: return None return SingleQubitCliffordGate.from_clifford_tableau( _to_clifford_tableau(x_to=x_to, z_to=z_to) ) - def transform(self, pauli: Pauli) -> PauliTransform: + def pauli_tuple(self, pauli: Pauli) -> Tuple[Pauli, bool]: + """Returns a tuple of a Pauli operator and a boolean. + + The pauli is the operator of the transform and the boolean + determines whether the operator should be flipped. For instance, + it is True if the coefficient is -1, and False if the coefficient + is 1. + """ x_to = self._clifford_tableau.destabilizers()[0] z_to = self._clifford_tableau.stabilizers()[0] if pauli == pauli_gates.X: @@ -638,7 +639,19 @@ def transform(self, pauli: Pauli) -> PauliTransform: to._coefficient *= 1j # pauli_mask returns a value between 0 and 4 for [I, X, Y, Z]. to_gate = Pauli._XYZ[to.pauli_mask[0] - 1] - return PauliTransform(to=to_gate, flip=bool(to.coefficient != 1.0)) + return (to_gate, bool(to.coefficient != 1.0)) + + def dense_pauli_string(self, pauli: Pauli) -> 'cirq.DensePauliString': + from cirq.ops import dense_pauli_string + + pauli_tuple = self.pauli_tuple(pauli) + coefficient = -1 if pauli_tuple[1] else 1 + return dense_pauli_string.DensePauliString(str(pauli_tuple[0]), coefficient=coefficient) + + @_compat.deprecated(deadline='v0.16', fix='Use pauli_tuple() or dense_pauli_string() instead') + def transform(self, pauli: Pauli) -> PauliTransform: + pauli_tuple = self.pauli_tuple(pauli) + return PauliTransform(to=pauli_tuple[0], flip=pauli_tuple[1]) def to_phased_xz_gate(self) -> phased_x_z_gate.PhasedXZGate: """Convert this gate to a PhasedXZGate instance. @@ -739,7 +752,7 @@ def commutes_with_single_qubit_gate(self, gate: 'SingleQubitCliffordGate') -> bo return self_then_gate == gate_then_self def commutes_with_pauli(self, pauli: Pauli) -> bool: - to, flip = self.transform(pauli) + to, flip = self.pauli_tuple(pauli) return to == pauli and not flip def merged_with(self, second: 'SingleQubitCliffordGate') -> 'SingleQubitCliffordGate': @@ -764,16 +777,16 @@ def decompose_rotation(self) -> Sequence[Tuple[Pauli, int]]: """Returns ((first_rotation_axis, first_rotation_quarter_turns), ...) This is a sequence of zero, one, or two rotations.""" - x_rot = self.transform(pauli_gates.X) - y_rot = self.transform(pauli_gates.Y) - z_rot = self.transform(pauli_gates.Z) + x_rot = self.pauli_tuple(pauli_gates.X) + y_rot = self.pauli_tuple(pauli_gates.Y) + z_rot = self.pauli_tuple(pauli_gates.Z) whole_arr = ( - x_rot.to == pauli_gates.X, - y_rot.to == pauli_gates.Y, - z_rot.to == pauli_gates.Z, + x_rot[0] == pauli_gates.X, + y_rot[0] == pauli_gates.Y, + z_rot[0] == pauli_gates.Z, ) num_whole = sum(whole_arr) - flip_arr = (x_rot.flip, y_rot.flip, z_rot.flip) + flip_arr = (x_rot[1], y_rot[1], z_rot[1]) num_flip = sum(flip_arr) if num_whole == 3: if num_flip == 0: @@ -793,7 +806,7 @@ def decompose_rotation(self) -> Sequence[Tuple[Pauli, int]]: # 180 degree rotation output.append((next_pauli, 2)) # 90 degree rotation about some axis - if self.transform(next_pauli).flip: + if self.pauli_tuple(next_pauli)[1]: # Negative 90 degree rotation output.append((pauli, -1)) else: @@ -802,16 +815,13 @@ def decompose_rotation(self) -> Sequence[Tuple[Pauli, int]]: return output elif num_whole == 0: # Gate is a 120 degree rotation - if x_rot.to == pauli_gates.Y: + if x_rot[0] == pauli_gates.Y: return [ - (pauli_gates.X, -1 if y_rot.flip else 1), - (pauli_gates.Z, -1 if x_rot.flip else 1), + (pauli_gates.X, -1 if y_rot[1] else 1), + (pauli_gates.Z, -1 if x_rot[1] else 1), ] - return [ - (pauli_gates.Z, 1 if y_rot.flip else -1), - (pauli_gates.X, 1 if z_rot.flip else -1), - ] + return [(pauli_gates.Z, 1 if y_rot[1] else -1), (pauli_gates.X, 1 if z_rot[1] else -1)] # coverage: ignore assert ( False @@ -824,15 +834,15 @@ def equivalent_gate_before(self, after: 'SingleQubitCliffordGate') -> 'SingleQub return self.merged_with(after).merged_with(self**-1) def __repr__(self) -> str: - x = self.transform(pauli_gates.X) - y = self.transform(pauli_gates.Y) - z = self.transform(pauli_gates.Z) - x_sign = '-' if x.flip else '+' - y_sign = '-' if y.flip else '+' - z_sign = '-' if z.flip else '+' + x = self.pauli_tuple(pauli_gates.X) + y = self.pauli_tuple(pauli_gates.Y) + z = self.pauli_tuple(pauli_gates.Z) + x_sign = '-' if x[1] else '+' + y_sign = '-' if y[1] else '+' + z_sign = '-' if z[1] else '+' return ( - f'cirq.SingleQubitCliffordGate(X:{x_sign}{x.to!s}, ' - f'Y:{y_sign}{y.to!s}, Z:{z_sign}{z.to!s})' + f'cirq.SingleQubitCliffordGate(X:{x_sign}{x[0]!s}, ' + f'Y:{y_sign}{y[0]!s}, Z:{z_sign}{z[0]!s})' ) def _circuit_diagram_info_( diff --git a/cirq-core/cirq/ops/clifford_gate_test.py b/cirq-core/cirq/ops/clifford_gate_test.py index adc5c6f43fa..12250046b7e 100644 --- a/cirq-core/cirq/ops/clifford_gate_test.py +++ b/cirq-core/cirq/ops/clifford_gate_test.py @@ -27,34 +27,34 @@ def _assert_not_mirror(gate) -> None: - trans_x = gate.transform(cirq.X) - trans_y = gate.transform(cirq.Y) - trans_z = gate.transform(cirq.Z) + trans_x = gate.pauli_tuple(cirq.X) + trans_y = gate.pauli_tuple(cirq.Y) + trans_z = gate.pauli_tuple(cirq.Z) right_handed = ( - trans_x.flip ^ trans_y.flip ^ trans_z.flip ^ (trans_x.to.relative_index(trans_y.to) != 1) + trans_x[1] ^ trans_y[1] ^ trans_z[1] ^ (trans_x[0].relative_index(trans_y[0]) != 1) ) assert right_handed, 'Mirrors' def _assert_no_collision(gate) -> None: - trans_x = gate.transform(cirq.X) - trans_y = gate.transform(cirq.Y) - trans_z = gate.transform(cirq.Z) - assert trans_x.to != trans_y.to, 'Collision' - assert trans_y.to != trans_z.to, 'Collision' - assert trans_z.to != trans_x.to, 'Collision' + trans_x = gate.pauli_tuple(cirq.X) + trans_y = gate.pauli_tuple(cirq.Y) + trans_z = gate.pauli_tuple(cirq.Z) + assert trans_x[0] != trans_y[0], 'Collision' + assert trans_y[0] != trans_z[0], 'Collision' + assert trans_z[0] != trans_x[0], 'Collision' def _all_rotations(): for (pauli, flip) in itertools.product(_paulis, _bools): - yield cirq.PauliTransform(pauli, flip) + yield (pauli, flip) def _all_rotation_pairs(): for px, flip_x, pz, flip_z in itertools.product(_paulis, _bools, _paulis, _bools): if px == pz: continue - yield cirq.PauliTransform(px, flip_x), cirq.PauliTransform(pz, flip_z) + yield (px, flip_x), (pz, flip_z) def _all_clifford_gates(): @@ -71,18 +71,31 @@ def test_init_value_error(pauli, flip_x, flip_z): @pytest.mark.parametrize('trans_x,trans_z', _all_rotation_pairs()) def test_init_from_xz(trans_x, trans_z): gate = cirq.SingleQubitCliffordGate.from_xz_map(trans_x, trans_z) - assert gate.transform(cirq.X) == trans_x - assert gate.transform(cirq.Z) == trans_z + assert gate.pauli_tuple(cirq.X) == trans_x + assert gate.pauli_tuple(cirq.Z) == trans_z _assert_not_mirror(gate) _assert_no_collision(gate) +def test_transform_deprecated(): + gate = cirq.SingleQubitCliffordGate.from_xz_map((cirq.X, True), (cirq.Y, False)) + with cirq.testing.assert_deprecated('pauli_tuple', deadline='v0.16', count=4): + assert gate.transform(cirq.X).to == cirq.X + assert gate.transform(cirq.Z).to == cirq.Y + + +def test_dense_pauli_string(): + gate = cirq.SingleQubitCliffordGate.from_xz_map((cirq.X, True), (cirq.Y, False)) + assert gate.dense_pauli_string(cirq.X) == cirq.DensePauliString('X', coefficient=-1) + assert gate.dense_pauli_string(cirq.Z) == cirq.DensePauliString('Y') + + @pytest.mark.parametrize( 'trans1,trans2,from1', ( (trans1, trans2, from1) for trans1, trans2, from1 in itertools.product(_all_rotations(), _all_rotations(), _paulis) - if trans1.to != trans2.to + if trans1[0] != trans2[0] ), ) def test_init_from_double_map_vs_kwargs(trans1, trans2, from1): @@ -94,8 +107,8 @@ def test_init_from_double_map_vs_kwargs(trans1, trans2, from1): assert gate_kw == gate_map # Test initializes what was expected - assert gate_map.transform(from1) == trans1 - assert gate_map.transform(from2) == trans2 + assert gate_map.pauli_tuple(from1) == trans1 + assert gate_map.pauli_tuple(from2) == trans2 _assert_not_mirror(gate_map) _assert_no_collision(gate_map) @@ -125,12 +138,12 @@ def test_init_from_single_map_vs_kwargs(trans, frm): ( (trans, frm) for trans, frm in itertools.product(_all_rotations(), _paulis) - if trans.to != frm + if trans[0] != frm ), ) def test_init_90rot_from_single(trans, frm): gate = cirq.SingleQubitCliffordGate.from_single_map({frm: trans}) - assert gate.transform(frm) == trans + assert gate.pauli_tuple(frm) == trans _assert_not_mirror(gate) _assert_no_collision(gate) # Check that it decomposes to one gate @@ -140,7 +153,7 @@ def test_init_90rot_from_single(trans, frm): gate.merged_with(gate).merged_with(gate).merged_with(gate) == cirq.SingleQubitCliffordGate.I ) # Check that flipping the transform produces the inverse rotation - trans_rev = cirq.PauliTransform(trans.to, not trans.flip) + trans_rev = (trans[0], not trans[1]) gate_rev = cirq.SingleQubitCliffordGate.from_single_map({frm: trans_rev}) assert gate**-1 == gate_rev @@ -150,12 +163,12 @@ def test_init_90rot_from_single(trans, frm): ( (trans, frm) for trans, frm in itertools.product(_all_rotations(), _paulis) - if trans.to == frm and trans.flip + if trans[0] == frm and trans[1] ), ) def test_init_180rot_from_single(trans, frm): gate = cirq.SingleQubitCliffordGate.from_single_map({frm: trans}) - assert gate.transform(frm) == trans + assert gate.pauli_tuple(frm) == trans _assert_not_mirror(gate) _assert_no_collision(gate) # Check that it decomposes to one gate @@ -169,12 +182,12 @@ def test_init_180rot_from_single(trans, frm): ( (trans, frm) for trans, frm in itertools.product(_all_rotations(), _paulis) - if trans.to == frm and not trans.flip + if trans[0] == frm and not trans[1] ), ) def test_init_ident_from_single(trans, frm): gate = cirq.SingleQubitCliffordGate.from_single_map({frm: trans}) - assert gate.transform(frm) == trans + assert gate.pauli_tuple(frm) == trans _assert_not_mirror(gate) _assert_no_collision(gate) # Check that it decomposes to zero gates @@ -341,7 +354,7 @@ def test_repr(gate, rep): ), ) def test_y_rotation(gate, trans_y): - assert gate.transform(cirq.Y) == trans_y + assert gate.pauli_tuple(cirq.Y) == trans_y @pytest.mark.parametrize( @@ -454,30 +467,20 @@ def test_commutes_pauli(gate, pauli, half_turns): def test_to_clifford_tableau_util_function(): tableau = cirq.ops.clifford_gate._to_clifford_tableau( - x_to=cirq.PauliTransform(to=cirq.X, flip=False), - z_to=cirq.PauliTransform(to=cirq.Z, flip=False), + x_to=(cirq.X, False), z_to=(cirq.Z, False) ) assert tableau == cirq.CliffordTableau(num_qubits=1, initial_state=0) - tableau = cirq.ops.clifford_gate._to_clifford_tableau( - x_to=cirq.PauliTransform(to=cirq.X, flip=False), - z_to=cirq.PauliTransform(to=cirq.Z, flip=True), - ) + tableau = cirq.ops.clifford_gate._to_clifford_tableau(x_to=(cirq.X, False), z_to=(cirq.Z, True)) assert tableau == cirq.CliffordTableau(num_qubits=1, initial_state=1) tableau = cirq.ops.clifford_gate._to_clifford_tableau( - rotation_map={ - cirq.X: cirq.PauliTransform(to=cirq.X, flip=False), - cirq.Z: cirq.PauliTransform(to=cirq.Z, flip=False), - } + rotation_map={cirq.X: (cirq.X, False), cirq.Z: (cirq.Z, False)} ) assert tableau == cirq.CliffordTableau(num_qubits=1, initial_state=0) tableau = cirq.ops.clifford_gate._to_clifford_tableau( - rotation_map={ - cirq.X: cirq.PauliTransform(to=cirq.X, flip=False), - cirq.Z: cirq.PauliTransform(to=cirq.Z, flip=True), - } + rotation_map={cirq.X: (cirq.X, False), cirq.Z: (cirq.Z, True)} ) assert tableau == cirq.CliffordTableau(num_qubits=1, initial_state=1) diff --git a/cirq-core/cirq/ops/pauli_string.py b/cirq-core/cirq/ops/pauli_string.py index dfb44ce052a..c142b14a0bf 100644 --- a/cirq-core/cirq/ops/pauli_string.py +++ b/cirq-core/cirq/ops/pauli_string.py @@ -1223,11 +1223,11 @@ def inplace_after(self, ops: 'cirq.OP_TREE') -> 'cirq.MutablePauliString': gate = op.gate if isinstance(gate, clifford_gate.SingleQubitCliffordGate): - out = gate.transform(cast(pauli_gates.Pauli, _INT_TO_PAULI[ps[0]])) - if out.flip: + out = gate.pauli_tuple(cast(pauli_gates.Pauli, _INT_TO_PAULI[ps[0]])) + if out[1]: self.coefficient *= -1 self.pauli_int_dict[cast(TKey, op.qubits[0])] = PAULI_GATE_LIKE_TO_INDEX_MAP[ - out.to + out[0] ] elif isinstance(gate, pauli_interaction_gate.PauliInteractionGate): @@ -1482,7 +1482,7 @@ def _pass_single_clifford_gate_over( return False # coverage: ignore if not after_to_before: gate **= -1 - pauli, inv = gate.transform(pauli_map[qubit]) + pauli, inv = gate.pauli_tuple(pauli_map[qubit]) pauli_map[qubit] = pauli return inv