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

Merged
merged 6 commits into from
Aug 5, 2025
Merged
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
180 changes: 67 additions & 113 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 @@ -968,53 +967,13 @@ def conjugated_by(self, clifford: cirq.OP_TREE) -> PauliString:
# Decompose P = Pc⊗R, where Pc acts on the same qubits as C, R acts on the remaining.
# Then the conjugation = (C^{-1}⊗I·Pc⊗R·C⊗I) = (C^{-1}·Pc·C)⊗R.

# Isolate R
remain: cirq.PauliString = PauliString(
# Conjugation on the qubits of op
conjugated = _calc_conjugation(ps, op)
# The pauli string on the remaining qubits
remain: PauliString = PauliString(
*(pauli(q) for q in all_qubits - set(op.qubits) if (pauli := ps.get(q)) is not None)
)

# Initialize the conjugation of Pc.
conjugated: cirq.DensePauliString = (
dense_pauli_string.DensePauliString(pauli_mask=[identity.I for _ in op.qubits])
* ps.coefficient
)

# Calculate the conjugation via CliffordGate's clifford_tableau.
# Note the clifford_tableau in CliffordGate represents C·P·C^-1 instead of C^-1·P·C.
# So we take the inverse of the tableau to match the definition of the conjugation here.
gate_in_clifford: cirq.CliffordGate
if isinstance(op.gate, clifford_gate.CliffordGate):
gate_in_clifford = op.gate
else:
# Convert the clifford gate to CliffordGate type.
gate_in_clifford = clifford_gate.CliffordGate.from_op_list([op], op.qubits)
tableau = gate_in_clifford.clifford_tableau.inverse()

# Calculate the conjugation by `op` via mutiplying the conjugation of each Pauli:
# C^{-1}·(P_1⊗...⊗P_n)·C
# = C^{-1}·(P_1⊗I) ...·(P_n⊗I)·C
# = (C^{-1}(P_1⊗I)C)·...·(C^{-1}(P_n⊗I)C)
# For the Pauli on the kth qubit P_k. The conjugation is calculated as following.
# Puali X_k's conjugation is from the destabilzer table;
# Puali Z_k's conjugation is from the stabilzer table;
# Puali Y_k's conjugation is calcluated according to Y = iXZ. E.g., for the kth qubit,
# C^{-1}·Y_k⊗I·C = C^{-1}·(iX_k⊗I·Z_k⊗I)·C = i (C^{-1}·X_k⊗I·C)·(C^{-1}·Z_k⊗I·C).
for qid, qubit in enumerate(op.qubits):
pauli = ps.get(qubit)
match pauli:
case None:
continue
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
)
ps = remain * conjugated.on(*op.qubits)
ps = remain * conjugated
return ps

def after(self, ops: cirq.OP_TREE) -> cirq.PauliString:
Expand Down Expand Up @@ -1371,7 +1330,18 @@ 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 = _calc_conjugation(self.frozen(), op)
self.coefficient = conjugated.coefficient
for q in op.qubits:
new_pauli_int = PAULI_GATE_LIKE_TO_INDEX_MAP[conjugated.get(q) or 0]
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 +1361,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 +1528,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 Expand Up @@ -1667,3 +1572,52 @@ def _pauli_like_to_pauli_int(key: Any, pauli_gate_like: PAULI_GATE_LIKE):
f"{set(PAULI_GATE_LIKE_TO_INDEX_MAP.keys())!r}"
)
return pauli_int


def _calc_conjugation(ps: cirq.PauliString, clifford_op: cirq.Operation) -> cirq.PauliString:
"""Computes the conjugation of a Pauli string by a single Clifford operation.

It computes $C^-1 P C$ where P is the Pauli string `ps` and C is the `clifford_op`.
"""

# Initialize the conjugation of the pauli string.
conjugated = dense_pauli_string.DensePauliString('I' * len(clifford_op.qubits)) * ps.coefficient

# Calculate the conjugation via CliffordGate's clifford_tableau.
# Note the clifford_tableau in CliffordGate represents C·P·C^-1 instead of C^-1·P·C.
# So we take the inverse of the tableau to match the definition of the conjugation here.
if isinstance(clifford_op.gate, clifford_gate.CliffordGate):
gate_in_clifford = clifford_op.gate
else:
# Convert the clifford gate to CliffordGate type.
gate_in_clifford = clifford_gate.CliffordGate.from_op_list(
[clifford_op], clifford_op.qubits
)
tableau = gate_in_clifford.clifford_tableau.inverse()

# Calculate the conjugation by `clifford_op` via mutiplying the conjugation of each Pauli:
# C^{-1}·(P_1⊗...⊗P_n)·C
# = C^{-1}·(P_1⊗I) ...·(P_n⊗I)·C
# = (C^{-1}(P_1⊗I)C)·...·(C^{-1}(P_n⊗I)C)
# For the Pauli on the kth qubit P_k. The conjugation is calculated as following.
# Pauli X_k's conjugation is from the destabilizer table;
# Pauli Z_k's conjugation is from the stabilizer table;
# Pauli Y_k's conjugation is calculated according to Y = iXZ. E.g., for the kth qubit,
# C^{-1}·Y_k⊗I·C = C^{-1}·(iX_k⊗I·Z_k⊗I)·C = i (C^{-1}·X_k⊗I·C)·(C^{-1}·Z_k⊗I·C).
for qid, qubit in enumerate(clifford_op.qubits):
pauli = ps.get(qubit)
match pauli:
case None:
continue
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
)

return conjugated.on(*clifford_op.qubits)
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