Skip to content

Commit 574595f

Browse files
authored
Fix mutable_pauli_string.inplace_after() and inplace_before() (#7507)
Similar to the fix for PauliString.after() #7065, this is the fix for MutablePauliString. Also deleted the culprit _decompose_into_cliffords helper function that was used by PauliString.after() and MutablePauliString.inplace_after(). Issue: #6946.
1 parent 17c4e95 commit 574595f

File tree

2 files changed

+78
-113
lines changed

2 files changed

+78
-113
lines changed

cirq-core/cirq/ops/pauli_string.py

Lines changed: 67 additions & 113 deletions
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,6 @@
5353
identity,
5454
op_tree,
5555
pauli_gates,
56-
pauli_interaction_gate,
5756
raw_types,
5857
)
5958

@@ -968,53 +967,13 @@ def conjugated_by(self, clifford: cirq.OP_TREE) -> PauliString:
968967
# Decompose P = Pc⊗R, where Pc acts on the same qubits as C, R acts on the remaining.
969968
# Then the conjugation = (C^{-1}⊗I·Pc⊗R·C⊗I) = (C^{-1}·Pc·C)⊗R.
970969

971-
# Isolate R
972-
remain: cirq.PauliString = PauliString(
970+
# Conjugation on the qubits of op
971+
conjugated = _calc_conjugation(ps, op)
972+
# The pauli string on the remaining qubits
973+
remain: PauliString = PauliString(
973974
*(pauli(q) for q in all_qubits - set(op.qubits) if (pauli := ps.get(q)) is not None)
974975
)
975-
976-
# Initialize the conjugation of Pc.
977-
conjugated: cirq.DensePauliString = (
978-
dense_pauli_string.DensePauliString(pauli_mask=[identity.I for _ in op.qubits])
979-
* ps.coefficient
980-
)
981-
982-
# Calculate the conjugation via CliffordGate's clifford_tableau.
983-
# Note the clifford_tableau in CliffordGate represents C·P·C^-1 instead of C^-1·P·C.
984-
# So we take the inverse of the tableau to match the definition of the conjugation here.
985-
gate_in_clifford: cirq.CliffordGate
986-
if isinstance(op.gate, clifford_gate.CliffordGate):
987-
gate_in_clifford = op.gate
988-
else:
989-
# Convert the clifford gate to CliffordGate type.
990-
gate_in_clifford = clifford_gate.CliffordGate.from_op_list([op], op.qubits)
991-
tableau = gate_in_clifford.clifford_tableau.inverse()
992-
993-
# Calculate the conjugation by `op` via mutiplying the conjugation of each Pauli:
994-
# C^{-1}·(P_1⊗...⊗P_n)·C
995-
# = C^{-1}·(P_1⊗I) ...·(P_n⊗I)·C
996-
# = (C^{-1}(P_1⊗I)C)·...·(C^{-1}(P_n⊗I)C)
997-
# For the Pauli on the kth qubit P_k. The conjugation is calculated as following.
998-
# Puali X_k's conjugation is from the destabilzer table;
999-
# Puali Z_k's conjugation is from the stabilzer table;
1000-
# Puali Y_k's conjugation is calcluated according to Y = iXZ. E.g., for the kth qubit,
1001-
# 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).
1002-
for qid, qubit in enumerate(op.qubits):
1003-
pauli = ps.get(qubit)
1004-
match pauli:
1005-
case None:
1006-
continue
1007-
case pauli_gates.X:
1008-
conjugated *= tableau.destabilizers()[qid]
1009-
case pauli_gates.Z:
1010-
conjugated *= tableau.stabilizers()[qid]
1011-
case pauli_gates.Y:
1012-
conjugated *= (
1013-
1j
1014-
* tableau.destabilizers()[qid] # conj X first
1015-
* tableau.stabilizers()[qid] # then conj Z
1016-
)
1017-
ps = remain * conjugated.on(*op.qubits)
976+
ps = remain * conjugated
1018977
return ps
1019978

1020979
def after(self, ops: cirq.OP_TREE) -> cirq.PauliString:
@@ -1371,7 +1330,18 @@ def inplace_before(self, ops: cirq.OP_TREE) -> cirq.MutablePauliString:
13711330
Returns:
13721331
The mutable pauli string that was mutated.
13731332
"""
1374-
return self.inplace_after(protocols.inverse(ops))
1333+
# An inplace impl of PauliString.conjugated_by().
1334+
flattened_ops = list(op_tree.flatten_to_ops(ops))
1335+
for op in flattened_ops[::-1]:
1336+
conjugated = _calc_conjugation(self.frozen(), op)
1337+
self.coefficient = conjugated.coefficient
1338+
for q in op.qubits:
1339+
new_pauli_int = PAULI_GATE_LIKE_TO_INDEX_MAP[conjugated.get(q) or 0]
1340+
if new_pauli_int == 0:
1341+
self.pauli_int_dict.pop(cast(TKey, q), None)
1342+
else:
1343+
self.pauli_int_dict[cast(TKey, q)] = new_pauli_int
1344+
return self
13751345

13761346
def inplace_after(self, ops: cirq.OP_TREE) -> cirq.MutablePauliString:
13771347
r"""Propagates the pauli string from before to after a Clifford effect.
@@ -1391,43 +1361,7 @@ def inplace_after(self, ops: cirq.OP_TREE) -> cirq.MutablePauliString:
13911361
NotImplementedError: If any ops decompose into an unsupported
13921362
Clifford gate.
13931363
"""
1394-
for clifford in op_tree.flatten_to_ops(ops):
1395-
for op in _decompose_into_cliffords(clifford):
1396-
ps = [self.pauli_int_dict.pop(cast(TKey, q), 0) for q in op.qubits]
1397-
if not any(ps):
1398-
continue
1399-
gate = op.gate
1400-
1401-
if isinstance(gate, clifford_gate.SingleQubitCliffordGate):
1402-
out = gate.pauli_tuple(_INT_TO_PAULI[ps[0] - 1])
1403-
if out[1]:
1404-
self.coefficient *= -1
1405-
self.pauli_int_dict[cast(TKey, op.qubits[0])] = PAULI_GATE_LIKE_TO_INDEX_MAP[
1406-
out[0]
1407-
]
1408-
1409-
elif isinstance(gate, pauli_interaction_gate.PauliInteractionGate):
1410-
q0, q1 = op.qubits
1411-
p0 = _INT_TO_PAULI_OR_IDENTITY[ps[0]]
1412-
p1 = _INT_TO_PAULI_OR_IDENTITY[ps[1]]
1413-
1414-
# Kick across Paulis that anti-commute with the controls.
1415-
kickback_0_to_1 = not protocols.commutes(p0, gate.pauli0)
1416-
kickback_1_to_0 = not protocols.commutes(p1, gate.pauli1)
1417-
kick0 = gate.pauli1 if kickback_0_to_1 else identity.I
1418-
kick1 = gate.pauli0 if kickback_1_to_0 else identity.I
1419-
self.__imul__({q0: p0, q1: kick0})
1420-
self.__imul__({q0: kick1, q1: p1})
1421-
1422-
# Decompose inverted controls into single-qubit operations.
1423-
if gate.invert0:
1424-
self.inplace_after(gate.pauli1(q1))
1425-
if gate.invert1:
1426-
self.inplace_after(gate.pauli0(q0))
1427-
1428-
else: # pragma: no cover
1429-
raise NotImplementedError(f"Unrecognized decomposed Clifford: {op!r}")
1430-
return self
1364+
return self.inplace_before(protocols.inverse(ops))
14311365

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

15961530

1597-
def _decompose_into_cliffords(op: cirq.Operation) -> list[cirq.Operation]:
1598-
# An operation that can be ignored?
1599-
if isinstance(op.gate, global_phase_op.GlobalPhaseGate):
1600-
return []
1601-
1602-
# Already a known Clifford?
1603-
if isinstance(
1604-
op.gate,
1605-
(clifford_gate.SingleQubitCliffordGate, pauli_interaction_gate.PauliInteractionGate),
1606-
):
1607-
return [op]
1608-
1609-
# Specifies a decomposition into Cliffords?
1610-
v = getattr(op, '_decompose_into_clifford_', None)
1611-
if v is not None:
1612-
result = v()
1613-
if result is not None and result is not NotImplemented:
1614-
return list(op_tree.flatten_to_ops(result))
1615-
1616-
# Specifies a decomposition that happens to contain only Cliffords?
1617-
decomposed = protocols.decompose_once(op, None)
1618-
if decomposed is not None:
1619-
return [out for sub_op in decomposed for out in _decompose_into_cliffords(sub_op)]
1620-
1621-
raise TypeError( # pragma: no cover
1622-
f'Operation is not a known Clifford and did not decompose into known Cliffords: {op!r}'
1623-
)
1624-
1625-
16261531
# Mypy has extreme difficulty with these constants for some reason.
16271532
_i = cast(identity.IdentityGate, identity.I) # type: ignore
16281533
_x = cast(pauli_gates.Pauli, pauli_gates.X) # type: ignore
@@ -1667,3 +1572,52 @@ def _pauli_like_to_pauli_int(key: Any, pauli_gate_like: PAULI_GATE_LIKE):
16671572
f"{set(PAULI_GATE_LIKE_TO_INDEX_MAP.keys())!r}"
16681573
)
16691574
return pauli_int
1575+
1576+
1577+
def _calc_conjugation(ps: cirq.PauliString, clifford_op: cirq.Operation) -> cirq.PauliString:
1578+
"""Computes the conjugation of a Pauli string by a single Clifford operation.
1579+
1580+
It computes $C^-1 P C$ where P is the Pauli string `ps` and C is the `clifford_op`.
1581+
"""
1582+
1583+
# Initialize the conjugation of the pauli string.
1584+
conjugated = dense_pauli_string.DensePauliString('I' * len(clifford_op.qubits)) * ps.coefficient
1585+
1586+
# Calculate the conjugation via CliffordGate's clifford_tableau.
1587+
# Note the clifford_tableau in CliffordGate represents C·P·C^-1 instead of C^-1·P·C.
1588+
# So we take the inverse of the tableau to match the definition of the conjugation here.
1589+
if isinstance(clifford_op.gate, clifford_gate.CliffordGate):
1590+
gate_in_clifford = clifford_op.gate
1591+
else:
1592+
# Convert the clifford gate to CliffordGate type.
1593+
gate_in_clifford = clifford_gate.CliffordGate.from_op_list(
1594+
[clifford_op], clifford_op.qubits
1595+
)
1596+
tableau = gate_in_clifford.clifford_tableau.inverse()
1597+
1598+
# Calculate the conjugation by `clifford_op` via mutiplying the conjugation of each Pauli:
1599+
# C^{-1}·(P_1⊗...⊗P_n)·C
1600+
# = C^{-1}·(P_1⊗I) ...·(P_n⊗I)·C
1601+
# = (C^{-1}(P_1⊗I)C)·...·(C^{-1}(P_n⊗I)C)
1602+
# For the Pauli on the kth qubit P_k. The conjugation is calculated as following.
1603+
# Pauli X_k's conjugation is from the destabilizer table;
1604+
# Pauli Z_k's conjugation is from the stabilizer table;
1605+
# Pauli Y_k's conjugation is calculated according to Y = iXZ. E.g., for the kth qubit,
1606+
# 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).
1607+
for qid, qubit in enumerate(clifford_op.qubits):
1608+
pauli = ps.get(qubit)
1609+
match pauli:
1610+
case None:
1611+
continue
1612+
case pauli_gates.X:
1613+
conjugated *= tableau.destabilizers()[qid]
1614+
case pauli_gates.Z:
1615+
conjugated *= tableau.stabilizers()[qid]
1616+
case pauli_gates.Y:
1617+
conjugated *= (
1618+
1j
1619+
* tableau.destabilizers()[qid] # conj X first
1620+
* tableau.stabilizers()[qid] # then conj Z
1621+
)
1622+
1623+
return conjugated.on(*clifford_op.qubits)

cirq-core/cirq/ops/pauli_string_test.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1714,6 +1714,9 @@ def with_qubits(self, *new_qubits):
17141714
def _decompose_(self):
17151715
return []
17161716

1717+
def __pow__(self, power):
1718+
return []
1719+
17171720
# No-ops
17181721
p2 = p.inplace_after(cirq.global_phase_operation(1j))
17191722
assert p2 is p and p == cirq.X(a)
@@ -1825,6 +1828,14 @@ def _decompose_(self):
18251828
assert p2 is p and p == cirq.X(a) * cirq.Y(b)
18261829

18271830

1831+
def test_mps_inplace_after_clifford_gate_type():
1832+
q = cirq.LineQubit(0)
1833+
1834+
mps = cirq.MutablePauliString(cirq.X(q))
1835+
mps2 = mps.inplace_after(cirq.CliffordGate.from_op_list([cirq.H(q)], [q]).on(q))
1836+
assert mps2 is mps and mps == cirq.Z(q)
1837+
1838+
18281839
def test_after_before_vs_conjugate_by():
18291840
a, b, c = cirq.LineQubit.range(3)
18301841
p = cirq.X(a) * cirq.Y(b) * cirq.Z(c)

0 commit comments

Comments
 (0)