Skip to content

Fix mutable_pauli_string.inplace_after() and inplace_before() #7507

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
106 changes: 38 additions & 68 deletions cirq-core/cirq/ops/pauli_string.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,6 @@
identity,
op_tree,
pauli_gates,
pauli_interaction_gate,
raw_types,
)

Expand Down Expand Up @@ -1371,7 +1370,43 @@ def inplace_before(self, ops: cirq.OP_TREE) -> cirq.MutablePauliString:
Returns:
The mutable pauli string that was mutated.
"""
return self.inplace_after(protocols.inverse(ops))
# An inplace impl of PauliString.conjugated_by().
flattened_ops = list(op_tree.flatten_to_ops(ops))

for op in flattened_ops[::-1]:
conjugated: cirq.DensePauliString = dense_pauli_string.DensePauliString(
Copy link
Collaborator

@NoureldinYosri NoureldinYosri Jul 18, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe conjugates = self.dense(self.qubits) * dense_pauli_string.DensePauliString('I'*len(self.qubits)) should do the trick? ... no need for the logic below

pauli_mask=[identity.I for _ in op.qubits]
)
gate_in_clifford: cirq.CliffordGate
if isinstance(op.gate, clifford_gate.CliffordGate):
gate_in_clifford = op.gate
else:
gate_in_clifford = clifford_gate.CliffordGate.from_op_list([op], op.qubits)
tableau = gate_in_clifford.clifford_tableau.inverse()

for qid, q in enumerate(op.qubits):
pauli = self.get(cast(TKey, q), None)
match pauli:
case pauli_gates.X:
conjugated *= tableau.destabilizers()[qid]
case pauli_gates.Z:
conjugated *= tableau.stabilizers()[qid]
case pauli_gates.Y:
conjugated *= (
1j
* tableau.destabilizers()[qid] # conj X first
* tableau.stabilizers()[qid] # then conj Z
)
case _:
continue
self.coefficient *= conjugated.coefficient
for qid, q in enumerate(op.qubits):
new_pauli_int = PAULI_GATE_LIKE_TO_INDEX_MAP[conjugated[qid]]
if new_pauli_int == 0:
self.pauli_int_dict.pop(cast(TKey, q), None)
else:
self.pauli_int_dict[cast(TKey, q)] = new_pauli_int
return self

def inplace_after(self, ops: cirq.OP_TREE) -> cirq.MutablePauliString:
r"""Propagates the pauli string from before to after a Clifford effect.
Expand All @@ -1391,43 +1426,7 @@ def inplace_after(self, ops: cirq.OP_TREE) -> cirq.MutablePauliString:
NotImplementedError: If any ops decompose into an unsupported
Clifford gate.
"""
for clifford in op_tree.flatten_to_ops(ops):
for op in _decompose_into_cliffords(clifford):
ps = [self.pauli_int_dict.pop(cast(TKey, q), 0) for q in op.qubits]
if not any(ps):
continue
gate = op.gate

if isinstance(gate, clifford_gate.SingleQubitCliffordGate):
out = gate.pauli_tuple(_INT_TO_PAULI[ps[0] - 1])
if out[1]:
self.coefficient *= -1
self.pauli_int_dict[cast(TKey, op.qubits[0])] = PAULI_GATE_LIKE_TO_INDEX_MAP[
out[0]
]

elif isinstance(gate, pauli_interaction_gate.PauliInteractionGate):
q0, q1 = op.qubits
p0 = _INT_TO_PAULI_OR_IDENTITY[ps[0]]
p1 = _INT_TO_PAULI_OR_IDENTITY[ps[1]]

# Kick across Paulis that anti-commute with the controls.
kickback_0_to_1 = not protocols.commutes(p0, gate.pauli0)
kickback_1_to_0 = not protocols.commutes(p1, gate.pauli1)
kick0 = gate.pauli1 if kickback_0_to_1 else identity.I
kick1 = gate.pauli0 if kickback_1_to_0 else identity.I
self.__imul__({q0: p0, q1: kick0})
self.__imul__({q0: kick1, q1: p1})

# Decompose inverted controls into single-qubit operations.
if gate.invert0:
self.inplace_after(gate.pauli1(q1))
if gate.invert1:
self.inplace_after(gate.pauli0(q0))

else: # pragma: no cover
raise NotImplementedError(f"Unrecognized decomposed Clifford: {op!r}")
return self
return self.inplace_before(protocols.inverse(ops))

def _imul_helper(self, other: cirq.PAULI_STRING_LIKE, sign: int):
"""Left-multiplies or right-multiplies by a PAULI_STRING_LIKE.
Expand Down Expand Up @@ -1594,35 +1593,6 @@ def __repr__(self) -> str:
return f'{self.frozen()!r}.mutable_copy()'


def _decompose_into_cliffords(op: cirq.Operation) -> list[cirq.Operation]:
# An operation that can be ignored?
if isinstance(op.gate, global_phase_op.GlobalPhaseGate):
return []

# Already a known Clifford?
if isinstance(
op.gate,
(clifford_gate.SingleQubitCliffordGate, pauli_interaction_gate.PauliInteractionGate),
):
return [op]

# Specifies a decomposition into Cliffords?
v = getattr(op, '_decompose_into_clifford_', None)
if v is not None:
result = v()
if result is not None and result is not NotImplemented:
return list(op_tree.flatten_to_ops(result))

# Specifies a decomposition that happens to contain only Cliffords?
decomposed = protocols.decompose_once(op, None)
if decomposed is not None:
return [out for sub_op in decomposed for out in _decompose_into_cliffords(sub_op)]

raise TypeError( # pragma: no cover
f'Operation is not a known Clifford and did not decompose into known Cliffords: {op!r}'
)


# Mypy has extreme difficulty with these constants for some reason.
_i = cast(identity.IdentityGate, identity.I) # type: ignore
_x = cast(pauli_gates.Pauli, pauli_gates.X) # type: ignore
Expand Down
11 changes: 11 additions & 0 deletions cirq-core/cirq/ops/pauli_string_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -1714,6 +1714,9 @@ def with_qubits(self, *new_qubits):
def _decompose_(self):
return []

def __pow__(self, power):
return []

# No-ops
p2 = p.inplace_after(cirq.global_phase_operation(1j))
assert p2 is p and p == cirq.X(a)
Expand Down Expand Up @@ -1825,6 +1828,14 @@ def _decompose_(self):
assert p2 is p and p == cirq.X(a) * cirq.Y(b)


def test_mps_inplace_after_clifford_gate_type():
q = cirq.LineQubit(0)

mps = cirq.MutablePauliString(cirq.X(q))
mps2 = mps.inplace_after(cirq.CliffordGate.from_op_list([cirq.H(q)], [q]).on(q))
assert mps2 is mps and mps == cirq.Z(q)


def test_after_before_vs_conjugate_by():
a, b, c = cirq.LineQubit.range(3)
p = cirq.X(a) * cirq.Y(b) * cirq.Z(c)
Expand Down