Skip to content

Commit 22ed25e

Browse files
authored
Fix conjugated_by of PauliString. (#7065)
* Fix conjugated_by of PauliString. * fix comments * Improve assert_conjugation and associated tests.
1 parent 8f2f198 commit 22ed25e

File tree

4 files changed

+144
-64
lines changed

4 files changed

+144
-64
lines changed

cirq-core/cirq/ops/clifford_gate.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -377,7 +377,10 @@ def __init__(self, *, _clifford_tableau: qis.CliffordTableau) -> None:
377377
# ZI [ 0 0 | 1 0 | 1 ]
378378
# IZ [ 1 0 | 1 1 | 0 ]
379379
# Take the third row as example: this means the ZI gate after the this gate,
380-
# more precisely the conjugate transformation of ZI by this gate, becomes -ZI.
380+
# more precisely the conjugate transformation of ZI by this gate, becomes -ZI:
381+
# ---(CliffordGate^-1)---ZI---CliffordGate---
382+
# = unitary(CliffordGate)@unitary(ZI)@unitary(CliffordGate).conj().T
383+
# = -ZI.
381384
# (Note the real clifford tableau has to satify the Symplectic property.
382385
# here is just for illustration)
383386
object.__setattr__(self, '_clifford_tableau', _clifford_tableau.copy())
@@ -438,7 +441,7 @@ def __repr__(self) -> str:
438441
def _commutes_(
439442
self, other: Any, *, atol: float = 1e-8
440443
) -> Union[bool, NotImplementedType, None]:
441-
# Note even if we assume two gates define the tabluea based on the same qubit order,
444+
# Note even if we assume two gates define the tableau based on the same qubit order,
442445
# the following approach cannot judge it:
443446
# self.clifford_tableau.then(other.clifford_tableau) == other.clifford_tableau.then(
444447
# self.clifford_tableau

cirq-core/cirq/ops/pauli_string.py

Lines changed: 64 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@
5555
pauli_gates,
5656
pauli_interaction_gate,
5757
raw_types,
58+
dense_pauli_string,
5859
)
5960

6061
if TYPE_CHECKING:
@@ -907,14 +908,13 @@ def dense(self, qubits: Sequence[TKey]) -> 'cirq.DensePauliString':
907908
Raises:
908909
ValueError: If the number of qubits is too small.
909910
"""
910-
from cirq.ops.dense_pauli_string import DensePauliString
911911

912912
if not self.keys() <= set(qubits):
913913
raise ValueError('not self.keys() <= set(qubits)')
914914
# pylint: disable=too-many-function-args
915915
pauli_mask = [self.get(q, identity.I) for q in qubits]
916916
# pylint: enable=too-many-function-args
917-
return DensePauliString(pauli_mask, coefficient=self.coefficient)
917+
return dense_pauli_string.DensePauliString(pauli_mask, coefficient=self.coefficient)
918918

919919
def conjugated_by(self, clifford: 'cirq.OP_TREE') -> 'PauliString':
920920
r"""Returns the Pauli string conjugated by a clifford operation.
@@ -976,17 +976,68 @@ def conjugated_by(self, clifford: 'cirq.OP_TREE') -> 'PauliString':
976976
Returns:
977977
The Pauli string conjugated by the given Clifford operation.
978978
"""
979-
pauli_map = dict(self._qubit_pauli_map)
980-
should_negate = False
981-
for op in list(op_tree.flatten_to_ops(clifford))[::-1]:
982-
if pauli_map.keys().isdisjoint(set(op.qubits)):
983-
continue
984-
for clifford_op in _decompose_into_cliffords(op)[::-1]:
985-
if pauli_map.keys().isdisjoint(set(clifford_op.qubits)):
986-
continue
987-
should_negate ^= _pass_operation_over(pauli_map, clifford_op, False)
988-
coef = -self._coefficient if should_negate else self.coefficient
989-
return PauliString(qubit_pauli_map=pauli_map, coefficient=coef)
979+
980+
# Initialize the ps the same as self.
981+
ps = PauliString(qubit_pauli_map=self._qubit_pauli_map, coefficient=self.coefficient)
982+
all_ops = list(op_tree.flatten_to_ops(clifford))
983+
all_qubits = set.union(set(self.qubits), [q for op in all_ops for q in op.qubits])
984+
985+
# Iteratively calculate the conjugation in reverse order of ops.
986+
for op in all_ops[::-1]:
987+
# To calcuate the conjugation of P (`ps`) with respect to C (`op`)
988+
# Decompose P = Pc⊗R, where Pc acts on the same qubits as C, R acts on the remaining.
989+
# Then the conjugation = (C^{-1}⊗I·Pc⊗R·C⊗I) = (C^{-1}·Pc·C)⊗R.
990+
991+
# Isolate R
992+
remain: 'cirq.PauliString' = PauliString()
993+
for q in all_qubits:
994+
pauli = ps.get(q)
995+
if pauli is not None and not q in op.qubits:
996+
remain *= pauli(q)
997+
998+
# Initialize the conjugation of Pc.
999+
conjugated: 'cirq.DensePauliString' = (
1000+
dense_pauli_string.DensePauliString(pauli_mask=[identity.I for _ in op.qubits])
1001+
* self.coefficient
1002+
)
1003+
1004+
# Calculate the conjugation via CliffordGate's clifford_tableau.
1005+
# Note the clifford_tableau in CliffordGate represents C·P·C^-1 instead of C^-1·P·C.
1006+
# So we take the inverse of the tableau to match the definition of the conjugation here.
1007+
gate_in_clifford: 'cirq.CliffordGate'
1008+
if isinstance(op.gate, cirq.CliffordGate):
1009+
gate_in_clifford = op.gate
1010+
else:
1011+
# Convert the clifford gate to CliffordGate type.
1012+
gate_in_clifford = clifford_gate.CliffordGate.from_op_list([op], op.qubits)
1013+
tableau = gate_in_clifford.clifford_tableau.inverse()
1014+
1015+
# Calculate the conjugation by `op` via mutiplying the conjugation of each Pauli:
1016+
# C^{-1}·(P_1⊗...⊗P_n)·C
1017+
# = C^{-1}·(P_1⊗I) ...·(P_n⊗I)·C
1018+
# = (C^{-1}(P_1⊗I)C)·...·(C^{-1}(P_n⊗I)C)
1019+
# For the Pauli on the kth qubit P_k. The conjugation is calculated as following.
1020+
# Puali X_k's conjugation is from the destabilzer table;
1021+
# Puali Z_k's conjugation is from the stabilzer table;
1022+
# Puali Y_k's conjugation is calcluated according to Y = iXZ. E.g., for the kth qubit,
1023+
# 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)
1024+
for qid, qubit in enumerate(op.qubits):
1025+
pauli = ps.get(qubit)
1026+
match pauli:
1027+
case None:
1028+
continue
1029+
case pauli_gates.X:
1030+
conjugated *= tableau.destabilizers()[qid]
1031+
case pauli_gates.Z:
1032+
conjugated *= tableau.stabilizers()[qid]
1033+
case pauli_gates.Y:
1034+
conjugated *= (
1035+
1j
1036+
* tableau.destabilizers()[qid] # conj X first
1037+
* tableau.stabilizers()[qid] # then conj Z
1038+
)
1039+
ps = remain * conjugated.on(*op.qubits)
1040+
return ps
9901041

9911042
def after(self, ops: 'cirq.OP_TREE') -> 'cirq.PauliString':
9921043
r"""Determines the equivalent pauli string after some operations.

cirq-core/cirq/ops/pauli_string_test.py

Lines changed: 70 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,22 @@ def _small_sample_qubit_pauli_maps():
5757
yield {qubits[0]: cirq.Z, qubits[1]: cirq.X, qubits[2]: cirq.Y}
5858

5959

60+
def assert_conjugation(
61+
input_ps: cirq.PauliString, ops: cirq.OP_TREE, expected: cirq.PauliString | None
62+
):
63+
conjugation = input_ps.conjugated_by(ops)
64+
if expected is not None:
65+
assert conjugation == expected
66+
else: # Compares the unitary of the conjugation result and the expected unitary.
67+
op_list = list(cirq.flatten_to_ops(ops))
68+
qubits_of_clifford = [q for op in op_list for q in op.qubits]
69+
clifford = cirq.CliffordGate.from_op_list(op_list, qubits_of_clifford)
70+
actual_unitary = cirq.unitary(conjugation.dense(qubits_of_clifford))
71+
c = cirq.unitary(clifford)
72+
expected_unitary = np.conj(c.T) @ cirq.unitary(input_ps.dense(qubits_of_clifford)) @ c
73+
assert np.allclose(actual_unitary, expected_unitary, atol=1e-8)
74+
75+
6076
def test_eq_ne_hash():
6177
q0, q1, q2 = _make_qubits(3)
6278
eq = cirq.testing.EqualsTester()
@@ -1381,13 +1397,24 @@ def test_pauli_string_expectation_from_state_vector_mixed_state_linearity():
13811397
def test_conjugated_by_normal_gates():
13821398
a = cirq.LineQubit(0)
13831399

1384-
assert cirq.X(a).conjugated_by(cirq.H(a)) == cirq.Z(a)
1385-
assert cirq.Y(a).conjugated_by(cirq.H(a)) == -cirq.Y(a)
1386-
assert cirq.Z(a).conjugated_by(cirq.H(a)) == cirq.X(a)
1400+
assert_conjugation(cirq.X(a), cirq.H(a), cirq.Z(a))
1401+
assert_conjugation(cirq.Y(a), cirq.H(a), -cirq.Y(a))
1402+
assert_conjugation(cirq.Z(a), cirq.H(a), cirq.X(a))
1403+
1404+
assert_conjugation(cirq.X(a), cirq.S(a), -cirq.Y(a))
1405+
assert_conjugation(cirq.Y(a), cirq.S(a), cirq.X(a))
1406+
assert_conjugation(cirq.Z(a), cirq.S(a), cirq.Z(a))
1407+
1408+
clifford_op = cirq.PhasedXZGate(axis_phase_exponent=0.25, x_exponent=-1, z_exponent=0).on(a)
1409+
assert_conjugation(cirq.X(a), clifford_op, cirq.Y(a))
1410+
assert_conjugation(cirq.Y(a), clifford_op, cirq.X(a))
1411+
assert_conjugation(cirq.Z(a), clifford_op, -cirq.Z(a))
1412+
1413+
1414+
def test_conjugated_by_op_gate_of_clifford_gate_type():
1415+
a = cirq.LineQubit(0)
13871416

1388-
assert cirq.X(a).conjugated_by(cirq.S(a)) == -cirq.Y(a)
1389-
assert cirq.Y(a).conjugated_by(cirq.S(a)) == cirq.X(a)
1390-
assert cirq.Z(a).conjugated_by(cirq.S(a)) == cirq.Z(a)
1417+
assert_conjugation(cirq.X(a), cirq.CliffordGate.from_op_list([cirq.H(a)], [a]).on(a), cirq.Z(a))
13911418

13921419

13931420
def test_dense():
@@ -1430,16 +1457,25 @@ def test_conjugated_by_incorrectly_powered_cliffords():
14301457
cirq.ZZ(a, b),
14311458
]
14321459
for c in cliffords:
1433-
with pytest.raises(TypeError, match='not a known Clifford'):
1460+
with pytest.raises(
1461+
ValueError,
1462+
match='Clifford Gate can only be constructed from the operations'
1463+
' that has stabilizer effect.',
1464+
):
14341465
_ = p.conjugated_by(c**0.1)
1435-
with pytest.raises(TypeError, match='not a known Clifford'):
1466+
with pytest.raises(
1467+
ValueError,
1468+
match='Clifford Gate can only be constructed from the operations'
1469+
' that has stabilizer effect.',
1470+
):
14361471
_ = p.conjugated_by(c ** sympy.Symbol('t'))
14371472

14381473

14391474
def test_conjugated_by_global_phase():
1475+
"""Global phase gate preserves PauliString."""
14401476
a = cirq.LineQubit(0)
1441-
assert cirq.X(a).conjugated_by(cirq.global_phase_operation(1j)) == cirq.X(a)
1442-
assert cirq.Z(a).conjugated_by(cirq.global_phase_operation(np.exp(1.1j))) == cirq.Z(a)
1477+
assert_conjugation(cirq.X(a), cirq.global_phase_operation(1j), cirq.X(a))
1478+
assert_conjugation(cirq.X(a), cirq.global_phase_operation(np.exp(1.1j)), cirq.X(a))
14431479

14441480
class DecomposeGlobal(cirq.Gate):
14451481
def num_qubits(self):
@@ -1448,7 +1484,7 @@ def num_qubits(self):
14481484
def _decompose_(self, qubits):
14491485
yield cirq.global_phase_operation(1j)
14501486

1451-
assert cirq.X(a).conjugated_by(DecomposeGlobal().on(a)) == cirq.X(a)
1487+
assert_conjugation(cirq.X(a), DecomposeGlobal().on(a), cirq.X(a))
14521488

14531489

14541490
def test_conjugated_by_composite_with_disjoint_sub_gates():
@@ -1461,8 +1497,10 @@ def num_qubits(self):
14611497
def _decompose_(self, qubits):
14621498
yield cirq.H(qubits[1])
14631499

1464-
assert cirq.X(a).conjugated_by(DecomposeDisjoint().on(a, b)) == cirq.X(a)
1465-
assert cirq.X(a).pass_operations_over([DecomposeDisjoint().on(a, b)]) == cirq.X(a)
1500+
for g1 in [cirq.X, cirq.Y]:
1501+
for g2 in [cirq.X, cirq.Y]:
1502+
ps = g1(a) * g2(b)
1503+
assert ps.conjugated_by(DecomposeDisjoint().on(a, b)) == ps.conjugated_by(cirq.H(b))
14661504

14671505

14681506
def test_conjugated_by_clifford_composite():
@@ -1477,16 +1515,16 @@ def _decompose_(self, qubits):
14771515
yield cirq.SWAP(qubits[2], qubits[3])
14781516

14791517
a, b, c, d = cirq.LineQubit.range(4)
1480-
p = cirq.X(a) * cirq.Z(b)
1518+
ps = cirq.X(a) * cirq.Z(b)
14811519
u = UnknownGate()
1482-
assert p.conjugated_by(u(a, b, c, d)) == cirq.Z(a) * cirq.X(b)
1520+
assert_conjugation(ps, u(a, b, c, d), cirq.Z(a) * cirq.X(b))
14831521

14841522

14851523
def test_conjugated_by_move_into_uninvolved():
14861524
a, b, c, d = cirq.LineQubit.range(4)
1487-
p = cirq.X(a) * cirq.Z(b)
1488-
assert p.conjugated_by([cirq.SWAP(c, d), cirq.SWAP(b, c)]) == cirq.X(a) * cirq.Z(d)
1489-
assert p.conjugated_by([cirq.SWAP(b, c), cirq.SWAP(c, d)]) == cirq.X(a) * cirq.Z(c)
1525+
ps = cirq.X(a) * cirq.Z(b)
1526+
assert_conjugation(ps, [cirq.SWAP(c, d), cirq.SWAP(b, c)], cirq.X(a) * cirq.Z(d))
1527+
assert_conjugation(ps, [cirq.SWAP(b, c), cirq.SWAP(c, d)], cirq.X(a) * cirq.Z(c))
14901528

14911529

14921530
def test_conjugated_by_common_single_qubit_gates():
@@ -1508,21 +1546,13 @@ def test_conjugated_by_common_single_qubit_gates():
15081546
single_qubit_gates = [g**i for i in range(4) for g in base_single_qubit_gates]
15091547
for p in [cirq.X, cirq.Y, cirq.Z]:
15101548
for g in single_qubit_gates:
1511-
assert p.on(a).conjugated_by(g.on(b)) == p.on(a)
1512-
1513-
actual = cirq.unitary(p.on(a).conjugated_by(g.on(a)))
1514-
u = cirq.unitary(g)
1515-
expected = np.conj(u.T) @ cirq.unitary(p) @ u
1516-
assert cirq.allclose_up_to_global_phase(actual, expected, atol=1e-8)
1549+
# pauli gate on a, clifford on b: pauli gate preserves.
1550+
assert_conjugation(p(a), g(b), p(a))
1551+
# pauli gate on a, clifford on a: check conjugation in matrices.
1552+
assert_conjugation(p(a), g(a), None)
15171553

15181554

15191555
def test_conjugated_by_common_two_qubit_gates():
1520-
class OrderSensitiveGate(cirq.Gate):
1521-
def num_qubits(self):
1522-
return 2
1523-
1524-
def _decompose_(self, qubits):
1525-
return [cirq.Y(qubits[0]) ** -0.5, cirq.CNOT(*qubits)]
15261556

15271557
a, b, c, d = cirq.LineQubit.range(4)
15281558
two_qubit_gates = [
@@ -1541,34 +1571,25 @@ def _decompose_(self, qubits):
15411571
cirq.YY**-0.5,
15421572
cirq.ZZ**-0.5,
15431573
]
1544-
two_qubit_gates.extend([OrderSensitiveGate()])
15451574
for p1 in [cirq.I, cirq.X, cirq.Y, cirq.Z]:
15461575
for p2 in [cirq.I, cirq.X, cirq.Y, cirq.Z]:
15471576
pd = cirq.DensePauliString([p1, p2])
1548-
p = pd.sparse()
1577+
p = pd.sparse([a, b])
15491578
for g in two_qubit_gates:
1550-
assert p.conjugated_by(g.on(c, d)) == p
1551-
1552-
actual = cirq.unitary(p.conjugated_by(g.on(a, b)).dense([a, b]))
1553-
u = cirq.unitary(g)
1554-
expected = np.conj(u.T) @ cirq.unitary(pd) @ u
1555-
np.testing.assert_allclose(actual, expected, atol=1e-8)
1579+
# pauli_string on (a,b), clifford on (c,d): pauli_string preserves.
1580+
assert_conjugation(p, g(c, d), p)
1581+
# pauli_string on (a,b), clifford on (a,b): compare unitaries of
1582+
# the conjugated_by and actual matrix conjugation.
1583+
assert_conjugation(p, g.on(a, b), None)
15561584

15571585

15581586
def test_conjugated_by_ordering():
1559-
class OrderSensitiveGate(cirq.Gate):
1560-
def num_qubits(self):
1561-
return 2
1562-
1563-
def _decompose_(self, qubits):
1564-
return [cirq.Y(qubits[0]) ** -0.5, cirq.CNOT(*qubits)]
1565-
1587+
"""Tests .conjugated_by([op1, op2]) == .conjugated_by(op2).conjugated_by(op1)"""
15661588
a, b = cirq.LineQubit.range(2)
15671589
inp = cirq.Z(b)
1568-
out1 = inp.conjugated_by(OrderSensitiveGate().on(a, b))
1569-
out2 = inp.conjugated_by([cirq.H(a), cirq.CNOT(a, b)])
1570-
out3 = inp.conjugated_by(cirq.CNOT(a, b)).conjugated_by(cirq.H(a))
1571-
assert out1 == out2 == out3 == cirq.X(a) * cirq.Z(b)
1590+
out1 = inp.conjugated_by([cirq.H(a), cirq.CNOT(a, b)])
1591+
out2 = inp.conjugated_by(cirq.CNOT(a, b)).conjugated_by(cirq.H(a))
1592+
assert out1 == out2 == cirq.X(a) * cirq.Z(b)
15721593

15731594

15741595
def test_pass_operations_over_ordering():

cirq-core/cirq/ops/swap_gates.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -219,6 +219,11 @@ def _eigen_components(self) -> List[Tuple[float, np.ndarray]]:
219219
]
220220
# yapf: enable
221221

222+
def _has_stabilizer_effect_(self) -> Optional[bool]:
223+
if self._is_parameterized_():
224+
return None
225+
return self.exponent % 1 == 0
226+
222227
def _decompose_(self, qubits):
223228
a, b = qubits
224229

0 commit comments

Comments
 (0)