From e17c7d97e11fd57ecc8b2814d01b22c89e9fc7f9 Mon Sep 17 00:00:00 2001 From: ShihCheng Tu Date: Mon, 25 Aug 2025 16:59:45 +0800 Subject: [PATCH 01/14] Update PauliString.__mul__ and __rmul__ to handle GateOperations Enhanced PauliString multiplication methods to handle GateOperations that can be interpreted as Pauli strings (e.g. X**2) by using the existing _try_interpret_as_pauli_string function. Addressed part of #7588, for the PauliString * GateOperation cases, e.g. `x*x*x**2` --- cirq-core/cirq/ops/pauli_string.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/cirq-core/cirq/ops/pauli_string.py b/cirq-core/cirq/ops/pauli_string.py index c86753a11ee..1a594817b75 100644 --- a/cirq-core/cirq/ops/pauli_string.py +++ b/cirq-core/cirq/ops/pauli_string.py @@ -271,6 +271,8 @@ def __mul__(self, other): known = True elif isinstance(other, (PauliString, numbers.Number)): known = True + elif (as_pauli_string := _try_interpret_as_pauli_string(other)) is not None: + return self * as_pauli_string if known: return PauliString( cast(PAULI_STRING_LIKE, other), @@ -297,6 +299,8 @@ def __rmul__(self, other) -> PauliString: if isinstance(other, raw_types.Operation) and isinstance(other.gate, identity.IdentityGate): return self # pragma: no cover + elif (as_pauli_string := _try_interpret_as_pauli_string(other)) is not None: + return as_pauli_string * self # Note: PauliString case handled by __mul__. return NotImplemented From fc2abac7374324d6f239b77c6fb96cbcd936bbed Mon Sep 17 00:00:00 2001 From: ShihCheng Tu Date: Mon, 25 Aug 2025 21:17:48 +0800 Subject: [PATCH 02/14] Update Gate._mul_with_qubits and _rmul_with_qubits ... to handle operations with single-item pauli expansions using PauliString. Addressed the GateOperation * GateOperation cases in #7588. --- cirq-core/cirq/ops/raw_types.py | 52 +++++++++++++++++++++++++++++++++ 1 file changed, 52 insertions(+) diff --git a/cirq-core/cirq/ops/raw_types.py b/cirq-core/cirq/ops/raw_types.py index a4548b1a303..dcc6e0b3a0b 100644 --- a/cirq-core/cirq/ops/raw_types.py +++ b/cirq-core/cirq/ops/raw_types.py @@ -483,10 +483,62 @@ def _commutes_(self, other: Any, *, atol: float = 1e-8) -> None | NotImplemented def _mul_with_qubits(self, qubits: tuple[cirq.Qid, ...], other): """cirq.GateOperation.__mul__ delegates to this method.""" + if isinstance(other, Operation): + try: + # Try using pauli expansion if both operations have single-item expansions + pauli_expansion_self = protocols.pauli_expansion(self.on(*qubits)) + pauli_expansion_other = protocols.pauli_expansion(other) + + if ( + pauli_expansion_self is not None + and len(pauli_expansion_self) == 1 + and pauli_expansion_other is not None + and len(pauli_expansion_other) == 1 + ): + + gate_self, coef_self = next(iter(pauli_expansion_self.items())) + gate_other, coef_other = next(iter(pauli_expansion_other.items())) + + from cirq.ops.pauli_string import PauliString + + return ( + coef_self + * PauliString({q: gate_self for q in qubits}) + * coef_other + * PauliString({q: gate_other for q in qubits}) + ) + except TypeError: + return NotImplemented return NotImplemented def _rmul_with_qubits(self, qubits: tuple[cirq.Qid, ...], other): """cirq.GateOperation.__rmul__ delegates to this method.""" + if isinstance(other, Operation): + try: + # Try using pauli expansion if both operations have single-item expansions + pauli_expansion_self = protocols.pauli_expansion(self.on(*qubits)) + pauli_expansion_other = protocols.pauli_expansion(other) + + if ( + pauli_expansion_self is not None + and len(pauli_expansion_self) == 1 + and pauli_expansion_other is not None + and len(pauli_expansion_other) == 1 + ): + + gate_self, coef_self = next(iter(pauli_expansion_self.items())) + gate_other, coef_other = next(iter(pauli_expansion_other.items())) + + from cirq.ops.pauli_string import PauliString + + return ( + coef_other + * PauliString({q: gate_other for q in qubits}) + * coef_self + * PauliString({q: gate_self for q in qubits}) + ) + except TypeError: + return NotImplemented return NotImplemented def _json_dict_(self) -> dict[str, Any]: From 0247926190863a2a3cf13af6e25776e723ccab0c Mon Sep 17 00:00:00 2001 From: ShihCheng Tu Date: Wed, 27 Aug 2025 00:31:06 +0800 Subject: [PATCH 03/14] Consolidate the PauliString conversion logic ...so that the PauliString construction logic in #7588 is defined in a single function "_try_interpret_as_pauli_string". --- cirq-core/cirq/ops/pauli_string.py | 19 ++++------ cirq-core/cirq/ops/raw_types.py | 60 ++++-------------------------- 2 files changed, 16 insertions(+), 63 deletions(-) diff --git a/cirq-core/cirq/ops/pauli_string.py b/cirq-core/cirq/ops/pauli_string.py index 1a594817b75..c4913b0c65c 100644 --- a/cirq-core/cirq/ops/pauli_string.py +++ b/cirq-core/cirq/ops/pauli_string.py @@ -1107,17 +1107,14 @@ def _validate_qubit_mapping( def _try_interpret_as_pauli_string(op: Any): """Return a reprepresentation of an operation as a pauli string, if it is possible.""" if isinstance(op, gate_operation.GateOperation): - gates = { - common_gates.XPowGate: pauli_gates.X, - common_gates.YPowGate: pauli_gates.Y, - common_gates.ZPowGate: pauli_gates.Z, - } - if (pauli := gates.get(type(op.gate), None)) is not None: - exponent = op.gate.exponent # type: ignore - if exponent % 2 == 0: - return PauliString() - if exponent % 2 == 1: - return pauli.on(op.qubits[0]) + try: + pauli_expansion_op = protocols.pauli_expansion(op) + if pauli_expansion_op is not None and len(pauli_expansion_op) == 1: + gate, coef = next(iter(pauli_expansion_op.items())) + return coef * PauliString({q: gate for q in op.qubits}) + except TypeError: + # return None if there is no Pauli expansion for this GateOperation. + pass return None diff --git a/cirq-core/cirq/ops/raw_types.py b/cirq-core/cirq/ops/raw_types.py index dcc6e0b3a0b..efd4f27f897 100644 --- a/cirq-core/cirq/ops/raw_types.py +++ b/cirq-core/cirq/ops/raw_types.py @@ -483,62 +483,18 @@ def _commutes_(self, other: Any, *, atol: float = 1e-8) -> None | NotImplemented def _mul_with_qubits(self, qubits: tuple[cirq.Qid, ...], other): """cirq.GateOperation.__mul__ delegates to this method.""" - if isinstance(other, Operation): - try: - # Try using pauli expansion if both operations have single-item expansions - pauli_expansion_self = protocols.pauli_expansion(self.on(*qubits)) - pauli_expansion_other = protocols.pauli_expansion(other) - - if ( - pauli_expansion_self is not None - and len(pauli_expansion_self) == 1 - and pauli_expansion_other is not None - and len(pauli_expansion_other) == 1 - ): - - gate_self, coef_self = next(iter(pauli_expansion_self.items())) - gate_other, coef_other = next(iter(pauli_expansion_other.items())) - - from cirq.ops.pauli_string import PauliString - - return ( - coef_self - * PauliString({q: gate_self for q in qubits}) - * coef_other - * PauliString({q: gate_other for q in qubits}) - ) - except TypeError: - return NotImplemented + from cirq.ops.pauli_string import _try_interpret_as_pauli_string + + if (as_pauli_string := _try_interpret_as_pauli_string(self.on(*qubits))) is not None: + return as_pauli_string * other return NotImplemented def _rmul_with_qubits(self, qubits: tuple[cirq.Qid, ...], other): """cirq.GateOperation.__rmul__ delegates to this method.""" - if isinstance(other, Operation): - try: - # Try using pauli expansion if both operations have single-item expansions - pauli_expansion_self = protocols.pauli_expansion(self.on(*qubits)) - pauli_expansion_other = protocols.pauli_expansion(other) - - if ( - pauli_expansion_self is not None - and len(pauli_expansion_self) == 1 - and pauli_expansion_other is not None - and len(pauli_expansion_other) == 1 - ): - - gate_self, coef_self = next(iter(pauli_expansion_self.items())) - gate_other, coef_other = next(iter(pauli_expansion_other.items())) - - from cirq.ops.pauli_string import PauliString - - return ( - coef_other - * PauliString({q: gate_other for q in qubits}) - * coef_self - * PauliString({q: gate_self for q in qubits}) - ) - except TypeError: - return NotImplemented + from cirq.ops.pauli_string import _try_interpret_as_pauli_string + + if (as_pauli_string := _try_interpret_as_pauli_string(self.on(*qubits))) is not None: + return other * as_pauli_string return NotImplemented def _json_dict_(self) -> dict[str, Any]: From 498b1e645150e5a353a43fe9c191dac9a02d6f67 Mon Sep 17 00:00:00 2001 From: ShihCheng Tu Date: Wed, 27 Aug 2025 08:34:35 +0800 Subject: [PATCH 04/14] Cleanup special cases in PauliString multiplication functions 1. Remove cases that have been handled in _mul_with_qubits 2. Remove try catch block in _try_interpret_as_pauli_string (#7588) --- cirq-core/cirq/ops/pauli_string.py | 20 ++++---------------- 1 file changed, 4 insertions(+), 16 deletions(-) diff --git a/cirq-core/cirq/ops/pauli_string.py b/cirq-core/cirq/ops/pauli_string.py index c4913b0c65c..70a2aa27682 100644 --- a/cirq-core/cirq/ops/pauli_string.py +++ b/cirq-core/cirq/ops/pauli_string.py @@ -271,8 +271,6 @@ def __mul__(self, other): known = True elif isinstance(other, (PauliString, numbers.Number)): known = True - elif (as_pauli_string := _try_interpret_as_pauli_string(other)) is not None: - return self * as_pauli_string if known: return PauliString( cast(PAULI_STRING_LIKE, other), @@ -299,8 +297,6 @@ def __rmul__(self, other) -> PauliString: if isinstance(other, raw_types.Operation) and isinstance(other.gate, identity.IdentityGate): return self # pragma: no cover - elif (as_pauli_string := _try_interpret_as_pauli_string(other)) is not None: - return as_pauli_string * self # Note: PauliString case handled by __mul__. return NotImplemented @@ -1107,14 +1103,10 @@ def _validate_qubit_mapping( def _try_interpret_as_pauli_string(op: Any): """Return a reprepresentation of an operation as a pauli string, if it is possible.""" if isinstance(op, gate_operation.GateOperation): - try: - pauli_expansion_op = protocols.pauli_expansion(op) - if pauli_expansion_op is not None and len(pauli_expansion_op) == 1: - gate, coef = next(iter(pauli_expansion_op.items())) - return coef * PauliString({q: gate for q in op.qubits}) - except TypeError: - # return None if there is no Pauli expansion for this GateOperation. - pass + pauli_expansion_op = protocols.pauli_expansion(op, default=None) + if pauli_expansion_op is not None and len(pauli_expansion_op) == 1: + gate, coef = next(iter(pauli_expansion_op.items())) + return coef * PauliString({q: gate for q in op.qubits}) return None @@ -1157,15 +1149,11 @@ def __mul__(self, other): return self._as_pauli_string() * other._as_pauli_string() if isinstance(other, (PauliString, numbers.Complex)): return self._as_pauli_string() * other - if (as_pauli_string := _try_interpret_as_pauli_string(other)) is not None: - return self * as_pauli_string return NotImplemented def __rmul__(self, other): if isinstance(other, (PauliString, numbers.Complex)): return other * self._as_pauli_string() - if (as_pauli_string := _try_interpret_as_pauli_string(other)) is not None: - return as_pauli_string * self return NotImplemented def __neg__(self): From c172c217f6097a0be7c38ee599ecf9c5f61c9adf Mon Sep 17 00:00:00 2001 From: ShihCheng Tu Date: Wed, 27 Aug 2025 17:46:29 +0800 Subject: [PATCH 05/14] Fix PauliString convertion logic --- cirq-core/cirq/ops/pauli_string.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cirq-core/cirq/ops/pauli_string.py b/cirq-core/cirq/ops/pauli_string.py index 70a2aa27682..33f120f8dec 100644 --- a/cirq-core/cirq/ops/pauli_string.py +++ b/cirq-core/cirq/ops/pauli_string.py @@ -1105,8 +1105,8 @@ def _try_interpret_as_pauli_string(op: Any): if isinstance(op, gate_operation.GateOperation): pauli_expansion_op = protocols.pauli_expansion(op, default=None) if pauli_expansion_op is not None and len(pauli_expansion_op) == 1: - gate, coef = next(iter(pauli_expansion_op.items())) - return coef * PauliString({q: gate for q in op.qubits}) + gates, coef = next(iter(pauli_expansion_op.items())) + return PauliString(dict(zip(op.qubits, gates)), coefficient=coef) return None From a12c82fe8a68c5e5915b624b5f4d5feac20e87e0 Mon Sep 17 00:00:00 2001 From: ShihCheng Tu Date: Wed, 27 Aug 2025 17:48:50 +0800 Subject: [PATCH 06/14] Remove redundant multiplication cases in PauliString --- cirq-core/cirq/ops/pauli_string.py | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/cirq-core/cirq/ops/pauli_string.py b/cirq-core/cirq/ops/pauli_string.py index 33f120f8dec..42646bb2e1a 100644 --- a/cirq-core/cirq/ops/pauli_string.py +++ b/cirq-core/cirq/ops/pauli_string.py @@ -266,12 +266,7 @@ def __mul__(self, other: complex) -> cirq.PauliString[TKey]: pass def __mul__(self, other): - known = False - if isinstance(other, raw_types.Operation) and isinstance(other.gate, identity.IdentityGate): - known = True - elif isinstance(other, (PauliString, numbers.Number)): - known = True - if known: + if isinstance(other, (PauliString, numbers.Number)): return PauliString( cast(PAULI_STRING_LIKE, other), qubit_pauli_map=self._qubit_pauli_map, @@ -295,9 +290,6 @@ def __rmul__(self, other) -> PauliString: qubit_pauli_map=self._qubit_pauli_map, coefficient=self._coefficient * other ) - if isinstance(other, raw_types.Operation) and isinstance(other.gate, identity.IdentityGate): - return self # pragma: no cover - # Note: PauliString case handled by __mul__. return NotImplemented From 4f1e08eb4a85101bb2689c0023a413c069684798 Mon Sep 17 00:00:00 2001 From: ShihCheng Tu Date: Wed, 27 Aug 2025 22:41:10 +0800 Subject: [PATCH 07/14] Add PauliString multiplication unit tests --- cirq-core/cirq/ops/parity_gates_test.py | 35 +++++++++++++++++++++ cirq-core/cirq/ops/pauli_string_test.py | 41 +++++++++++++++++++++++++ 2 files changed, 76 insertions(+) diff --git a/cirq-core/cirq/ops/parity_gates_test.py b/cirq-core/cirq/ops/parity_gates_test.py index be5495ef398..47de0bbc979 100644 --- a/cirq-core/cirq/ops/parity_gates_test.py +++ b/cirq-core/cirq/ops/parity_gates_test.py @@ -352,3 +352,38 @@ def test_clifford_protocols( else: assert not cirq.has_stabilizer_effect(gate) assert gate._decompose_into_clifford_with_qubits_(cirq.LineQubit.range(2)) is NotImplemented + + +def test_parity_gate_multiplication(): + q1, q2, q3 = cirq.LineQubit.range(3) + + # XX gate + xx_12 = cirq.XX(q1, q2) + xx_23 = cirq.XX(q2, q3) + result = xx_12 * xx_23 + expected = cirq.PauliString({q1: cirq.X, q3: cirq.X}) + assert result == expected + + # YY gate + yy_12 = cirq.YY(q1, q2) + yy_23 = cirq.YY(q2, q3) + result_yy = yy_12 * yy_23 + expected_yy = cirq.PauliString({q1: cirq.Y, q3: cirq.Y}) + assert result_yy == expected_yy + + # ZZ gate + zz_12 = cirq.ZZ(q1, q2) + zz_23 = cirq.ZZ(q2, q3) + result_zz = zz_12 * zz_23 + expected_zz = cirq.PauliString({q1: cirq.Z, q3: cirq.Z}) + assert result_zz == expected_zz + + +def test_parity_gate_multiplication_same_qubits(): + q1, q2 = cirq.LineQubit.range(2) + + # XX * XX should be identity + xx = cirq.XX(q1, q2) + result = xx * xx + expected = cirq.PauliString({q1: cirq.I, q2: cirq.I}) + assert result == expected diff --git a/cirq-core/cirq/ops/pauli_string_test.py b/cirq-core/cirq/ops/pauli_string_test.py index 99cea2c20dc..cd1d26fb63d 100644 --- a/cirq-core/cirq/ops/pauli_string_test.py +++ b/cirq-core/cirq/ops/pauli_string_test.py @@ -2025,3 +2025,44 @@ def test_pauli_ops_identity_gate_operation(gate1: cirq.Pauli, gate2: cirq.Pauli) subtraction = pauli1 - pauli2 assert isinstance(subtraction, cirq.PauliSum) assert np.array_equal(subtraction.matrix(), unitary1 - unitary2) + + +def test_pauli_gate_multiplication_with_power(): + q = cirq.LineQubit(0) + + # Test all Pauli gates (X, Y, Z) + pauli_gates = [cirq.X, cirq.Y, cirq.Z] + for pauli_gate in pauli_gates: + gate = pauli_gate(q) + + # Test multiplication + assert gate**2 * gate * gate * gate == gate**5 + assert gate * gate**2 * gate * gate == gate**5 + assert gate * gate * gate**2 * gate == gate**5 + assert gate * gate * gate * gate**2 == gate**5 + + # Test with different powers + assert gate**0 * gate**5 == gate**5 + assert gate**1 * gate**4 == gate**5 + assert gate**2 * gate**3 == gate**5 + assert gate**3 * gate**2 == gate**5 + assert gate**4 * gate**1 == gate**5 + assert gate**5 * gate**0 == gate**5 + + +def test_try_interpret_as_pauli_string(): + from cirq.ops.pauli_string import _try_interpret_as_pauli_string + + q = cirq.LineQubit(0) + + # Pauli gate operation + x_gate = cirq.X(q) + assert _try_interpret_as_pauli_string(x_gate) == cirq.PauliString({q: cirq.X}) + + # powered gates + x_squared = x_gate**2 + assert _try_interpret_as_pauli_string(x_squared) == cirq.PauliString({q: cirq.I}) + + # non-Pauli operation + h_gate = cirq.H(q) + assert _try_interpret_as_pauli_string(h_gate) is None From 6ea83a013de6e4ed014127f693634a08e2973f50 Mon Sep 17 00:00:00 2001 From: ShihCheng Tu Date: Thu, 28 Aug 2025 10:43:13 +0800 Subject: [PATCH 08/14] Handle DensePauliString/PauliString multiplication ... by trying to first convert the operation to be multiplied to a PauliString. With this change, the multiplication dunder function in pauli operation could be removed. --- cirq-core/cirq/ops/dense_pauli_string.py | 8 +++++--- cirq-core/cirq/ops/pauli_string.py | 12 ------------ 2 files changed, 5 insertions(+), 15 deletions(-) diff --git a/cirq-core/cirq/ops/dense_pauli_string.py b/cirq-core/cirq/ops/dense_pauli_string.py index ccbbe361a3c..63aec3f5a31 100644 --- a/cirq-core/cirq/ops/dense_pauli_string.py +++ b/cirq-core/cirq/ops/dense_pauli_string.py @@ -614,10 +614,12 @@ def _as_pauli_mask(val: Iterable[cirq.PAULI_GATE_LIKE] | np.ndarray) -> np.ndarr def _attempt_value_to_pauli_index(v: cirq.Operation) -> tuple[int, int] | None: - if not isinstance(v, raw_types.Operation): + if isinstance(v, pauli_string.PauliString): + pass + elif (v := pauli_string._try_interpret_as_pauli_string(v)) is None: return None - if not isinstance(v.gate, pauli_gates.Pauli): + if len(v.qubits) != 1: return None # pragma: no cover q = v.qubits[0] @@ -629,7 +631,7 @@ def _attempt_value_to_pauli_index(v: cirq.Operation) -> tuple[int, int] | None: 'other than `cirq.LineQubit` so its dense index is ambiguous.\n' f'v={repr(v)}.' ) - return pauli_string.PAULI_GATE_LIKE_TO_INDEX_MAP[v.gate], q.x + return pauli_string.PAULI_GATE_LIKE_TO_INDEX_MAP[v[q]], q.x def _vectorized_pauli_mul_phase(lhs: int | np.ndarray, rhs: int | np.ndarray) -> complex: diff --git a/cirq-core/cirq/ops/pauli_string.py b/cirq-core/cirq/ops/pauli_string.py index 42646bb2e1a..3808bb40e08 100644 --- a/cirq-core/cirq/ops/pauli_string.py +++ b/cirq-core/cirq/ops/pauli_string.py @@ -1136,18 +1136,6 @@ def qubit(self) -> raw_types.Qid: def _as_pauli_string(self) -> PauliString: return PauliString(qubit_pauli_map={self.qubit: self.pauli}) - def __mul__(self, other): - if isinstance(other, SingleQubitPauliStringGateOperation): - return self._as_pauli_string() * other._as_pauli_string() - if isinstance(other, (PauliString, numbers.Complex)): - return self._as_pauli_string() * other - return NotImplemented - - def __rmul__(self, other): - if isinstance(other, (PauliString, numbers.Complex)): - return other * self._as_pauli_string() - return NotImplemented - def __neg__(self): return -self._as_pauli_string() From 1c25b43c3e9a241afe344fef63c29d65097968c7 Mon Sep 17 00:00:00 2001 From: ShihCheng Tu Date: Fri, 29 Aug 2025 08:02:47 +0800 Subject: [PATCH 09/14] Handle Operation in _try_interpret_as_pauli_string Relax the type check from GateOperation to Operation. --- cirq-core/cirq/ops/pauli_string.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/cirq-core/cirq/ops/pauli_string.py b/cirq-core/cirq/ops/pauli_string.py index 3808bb40e08..d662039debf 100644 --- a/cirq-core/cirq/ops/pauli_string.py +++ b/cirq-core/cirq/ops/pauli_string.py @@ -1094,12 +1094,13 @@ def _validate_qubit_mapping( def _try_interpret_as_pauli_string(op: Any): """Return a reprepresentation of an operation as a pauli string, if it is possible.""" - if isinstance(op, gate_operation.GateOperation): - pauli_expansion_op = protocols.pauli_expansion(op, default=None) - if pauli_expansion_op is not None and len(pauli_expansion_op) == 1: - gates, coef = next(iter(pauli_expansion_op.items())) - return PauliString(dict(zip(op.qubits, gates)), coefficient=coef) - return None + if not isinstance(op, raw_types.Operation): + return None + + pauli_expansion_op = protocols.pauli_expansion(op, default=None) + if pauli_expansion_op is not None and len(pauli_expansion_op) == 1: + gates, coef = next(iter(pauli_expansion_op.items())) + return PauliString(dict(zip(op.qubits, gates)), coefficient=coef) # Ignoring type because mypy believes `with_qubits` methods are incompatible. From a15cb417b038f7686306a9df82669933ccd5d410 Mon Sep 17 00:00:00 2001 From: ShihCheng Tu Date: Fri, 29 Aug 2025 08:06:06 +0800 Subject: [PATCH 10/14] Remove _as_pauli_string ...since it is unused now. --- cirq-core/cirq/ops/pauli_string.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/cirq-core/cirq/ops/pauli_string.py b/cirq-core/cirq/ops/pauli_string.py index d662039debf..8d2687f3de9 100644 --- a/cirq-core/cirq/ops/pauli_string.py +++ b/cirq-core/cirq/ops/pauli_string.py @@ -1134,12 +1134,6 @@ def qubit(self) -> raw_types.Qid: assert len(self.qubits) == 1 return self.qubits[0] - def _as_pauli_string(self) -> PauliString: - return PauliString(qubit_pauli_map={self.qubit: self.pauli}) - - def __neg__(self): - return -self._as_pauli_string() - def _json_dict_(self) -> dict[str, Any]: return protocols.obj_to_dict_helper(self, ['pauli', 'qubit']) From 0ba3672ccefc706a268fc91f3d7519bc1cadf384 Mon Sep 17 00:00:00 2001 From: ShihCheng Tu Date: Fri, 29 Aug 2025 08:20:21 +0800 Subject: [PATCH 11/14] Remove PauliString type check in _attempt_value_to_pauli_index Given that _try_interpret_as_pauli_string also handles PauliString after relaxing the type check to Operation, remove the PauliString type check here. --- cirq-core/cirq/ops/dense_pauli_string.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/cirq-core/cirq/ops/dense_pauli_string.py b/cirq-core/cirq/ops/dense_pauli_string.py index 63aec3f5a31..75560b8ff85 100644 --- a/cirq-core/cirq/ops/dense_pauli_string.py +++ b/cirq-core/cirq/ops/dense_pauli_string.py @@ -614,15 +614,13 @@ def _as_pauli_mask(val: Iterable[cirq.PAULI_GATE_LIKE] | np.ndarray) -> np.ndarr def _attempt_value_to_pauli_index(v: cirq.Operation) -> tuple[int, int] | None: - if isinstance(v, pauli_string.PauliString): - pass - elif (v := pauli_string._try_interpret_as_pauli_string(v)) is None: + if (ps := pauli_string._try_interpret_as_pauli_string(v)) is None: return None - if len(v.qubits) != 1: + if len(ps.qubits) != 1: return None # pragma: no cover - q = v.qubits[0] + q = ps.qubits[0] from cirq import devices if not isinstance(q, devices.LineQubit): @@ -631,7 +629,7 @@ def _attempt_value_to_pauli_index(v: cirq.Operation) -> tuple[int, int] | None: 'other than `cirq.LineQubit` so its dense index is ambiguous.\n' f'v={repr(v)}.' ) - return pauli_string.PAULI_GATE_LIKE_TO_INDEX_MAP[v[q]], q.x + return pauli_string.PAULI_GATE_LIKE_TO_INDEX_MAP[ps[q]], q.x def _vectorized_pauli_mul_phase(lhs: int | np.ndarray, rhs: int | np.ndarray) -> complex: From 7a516c9fe8e74911f03a9b20c78e47b173f3b9aa Mon Sep 17 00:00:00 2001 From: ShihCheng Tu Date: Fri, 29 Aug 2025 08:33:02 +0800 Subject: [PATCH 12/14] Remove casting in PauliString.__mul__ --- cirq-core/cirq/ops/pauli_string.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/cirq-core/cirq/ops/pauli_string.py b/cirq-core/cirq/ops/pauli_string.py index 8d2687f3de9..3ad15884421 100644 --- a/cirq-core/cirq/ops/pauli_string.py +++ b/cirq-core/cirq/ops/pauli_string.py @@ -268,9 +268,7 @@ def __mul__(self, other: complex) -> cirq.PauliString[TKey]: def __mul__(self, other): if isinstance(other, (PauliString, numbers.Number)): return PauliString( - cast(PAULI_STRING_LIKE, other), - qubit_pauli_map=self._qubit_pauli_map, - coefficient=self.coefficient, + other, qubit_pauli_map=self._qubit_pauli_map, coefficient=self.coefficient ) return NotImplemented From 7b9c99f81b8fc02e82d5b883c9181782c65db756 Mon Sep 17 00:00:00 2001 From: ShihCheng Tu Date: Fri, 29 Aug 2025 10:20:57 +0800 Subject: [PATCH 13/14] Add commute test cases for DSP and GateOperation --- cirq-core/cirq/ops/dense_pauli_string_test.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/cirq-core/cirq/ops/dense_pauli_string_test.py b/cirq-core/cirq/ops/dense_pauli_string_test.py index da58141acde..f8c092a25bc 100644 --- a/cirq-core/cirq/ops/dense_pauli_string_test.py +++ b/cirq-core/cirq/ops/dense_pauli_string_test.py @@ -503,6 +503,10 @@ def test_commutes(): assert cirq.commutes(f('IIIXII'), cirq.X(cirq.LineQubit(2))) assert not cirq.commutes(f('IIIXII'), cirq.Z(cirq.LineQubit(3))) assert cirq.commutes(f('IIIXII'), cirq.Z(cirq.LineQubit(2))) + assert cirq.commutes(f('IIIXII'), cirq.X(cirq.LineQubit(3)) ** 3) + assert cirq.commutes(f('IIIXII'), cirq.X(cirq.LineQubit(2)) ** 3) + assert not cirq.commutes(f('IIIXII'), cirq.Z(cirq.LineQubit(3)) ** 3) + assert cirq.commutes(f('IIIXII'), cirq.Z(cirq.LineQubit(2)) ** 3) assert cirq.commutes(f('XX'), "test", default=NotImplemented) is NotImplemented From 87337dccfd7f01cdc12181f6c61ff2b7d008c2c4 Mon Sep 17 00:00:00 2001 From: ShihCheng Tu Date: Fri, 29 Aug 2025 12:12:54 +0800 Subject: [PATCH 14/14] Explicitly return None in the end of _try_interpret_as_pauli_string --- cirq-core/cirq/ops/pauli_string.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/cirq-core/cirq/ops/pauli_string.py b/cirq-core/cirq/ops/pauli_string.py index 3ad15884421..1fb90a8ce15 100644 --- a/cirq-core/cirq/ops/pauli_string.py +++ b/cirq-core/cirq/ops/pauli_string.py @@ -1090,15 +1090,16 @@ def _validate_qubit_mapping( ) -def _try_interpret_as_pauli_string(op: Any): +def _try_interpret_as_pauli_string(op: Any) -> PauliString | None: """Return a reprepresentation of an operation as a pauli string, if it is possible.""" if not isinstance(op, raw_types.Operation): return None pauli_expansion_op = protocols.pauli_expansion(op, default=None) - if pauli_expansion_op is not None and len(pauli_expansion_op) == 1: - gates, coef = next(iter(pauli_expansion_op.items())) - return PauliString(dict(zip(op.qubits, gates)), coefficient=coef) + if pauli_expansion_op is None or len(pauli_expansion_op) != 1: + return None + gates, coef = next(iter(pauli_expansion_op.items())) + return PauliString(dict(zip(op.qubits, gates)), coefficient=coef) # Ignoring type because mypy believes `with_qubits` methods are incompatible.