Skip to content

Commit abf6051

Browse files
babacrypavoljuhas
andauthored
Update pass_operations_over with fixed conjugated_by (#7123)
* Small fixes. * Update pass_operations_over with fixed conjugated_by * Order the qubit_pauli_map in the output for PauliStringPhasor * Fix coverage * Fix pauli_string_phasor_test. pass_operations_over doesn't necessarily preserve the order of qubits. * Avoid broken wheels for qcs-sdk-python (#7126) * Avoid broken wheels for qcs-sdk-python The installation of `qcs-sdk-python==0.21.13` fails on Linux Python 3.10 with `zipfile.BadZipFile: Bad CRC-32 for file 'qcs_sdk/qcs_sdk.cpython-310-x86_64-linux-gnu.so'` Block new versions of qcs-sdk-python until this is fixed. Ref: rigetti/qcs-sdk-rust#531 * Also adjust Dockerfile to avoid the bad wheel * Fix coverage, the num_qubits func in the Faked gate in the test isn't necessary for the test --------- Co-authored-by: Pavol Juhas <juhas@google.com>
1 parent e014f4d commit abf6051

3 files changed

Lines changed: 87 additions & 47 deletions

File tree

cirq-core/cirq/ops/pauli_string.py

Lines changed: 19 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,6 @@
4141
import numpy as np
4242
import sympy
4343

44-
import cirq
4544
from cirq import value, protocols, linalg, qis, _compat
4645
from cirq._doc import document
4746
from cirq._import import LazyLoader
@@ -496,7 +495,7 @@ def matrix(self, qubits: Optional[Iterable[TKey]] = None) -> np.ndarray:
496495
"""
497496
qubits = self.qubits if qubits is None else qubits
498497
factors = [self.get(q, default=identity.I) for q in qubits]
499-
if cirq.is_parameterized(self):
498+
if protocols.is_parameterized(self):
500499
raise NotImplementedError('Cannot express as matrix when parameterized')
501500
assert isinstance(self.coefficient, complex)
502501
return linalg.kron(self.coefficient, *[protocols.unitary(f) for f in factors])
@@ -981,31 +980,28 @@ def conjugated_by(self, clifford: 'cirq.OP_TREE') -> 'PauliString':
981980
ps = PauliString(qubit_pauli_map=self._qubit_pauli_map, coefficient=self.coefficient)
982981
all_ops = list(op_tree.flatten_to_ops(clifford))
983982
all_qubits = set.union(set(self.qubits), [q for op in all_ops for q in op.qubits])
984-
985983
# Iteratively calculate the conjugation in reverse order of ops.
986984
for op in all_ops[::-1]:
987985
# To calcuate the conjugation of P (`ps`) with respect to C (`op`)
988986
# Decompose P = Pc⊗R, where Pc acts on the same qubits as C, R acts on the remaining.
989987
# Then the conjugation = (C^{-1}⊗I·Pc⊗R·C⊗I) = (C^{-1}·Pc·C)⊗R.
990988

991989
# 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)
990+
remain: 'cirq.PauliString' = PauliString(
991+
*(pauli(q) for q in all_qubits - set(op.qubits) if (pauli := ps.get(q)) is not None)
992+
)
997993

998994
# Initialize the conjugation of Pc.
999995
conjugated: 'cirq.DensePauliString' = (
1000996
dense_pauli_string.DensePauliString(pauli_mask=[identity.I for _ in op.qubits])
1001-
* self.coefficient
997+
* ps.coefficient
1002998
)
1003999

10041000
# Calculate the conjugation via CliffordGate's clifford_tableau.
10051001
# Note the clifford_tableau in CliffordGate represents C·P·C^-1 instead of C^-1·P·C.
10061002
# So we take the inverse of the tableau to match the definition of the conjugation here.
10071003
gate_in_clifford: 'cirq.CliffordGate'
1008-
if isinstance(op.gate, cirq.CliffordGate):
1004+
if isinstance(op.gate, clifford_gate.CliffordGate):
10091005
gate_in_clifford = op.gate
10101006
else:
10111007
# Convert the clifford gate to CliffordGate type.
@@ -1020,7 +1016,7 @@ def conjugated_by(self, clifford: 'cirq.OP_TREE') -> 'PauliString':
10201016
# Puali X_k's conjugation is from the destabilzer table;
10211017
# Puali Z_k's conjugation is from the stabilzer table;
10221018
# 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)
1019+
# 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).
10241020
for qid, qubit in enumerate(op.qubits):
10251021
pauli = ps.get(qubit)
10261022
match pauli:
@@ -1100,20 +1096,17 @@ def pass_operations_over(
11001096
pauli string, instead of before (and so are moving in the
11011097
opposite direction).
11021098
"""
1103-
pauli_map = dict(self._qubit_pauli_map)
1104-
should_negate = False
1105-
for op in ops:
1106-
if pauli_map.keys().isdisjoint(set(op.qubits)):
1107-
continue
1108-
decomposed = _decompose_into_cliffords(op)
1109-
if not after_to_before:
1110-
decomposed = decomposed[::-1]
1111-
for clifford_op in decomposed:
1112-
if pauli_map.keys().isdisjoint(set(clifford_op.qubits)):
1113-
continue
1114-
should_negate ^= _pass_operation_over(pauli_map, clifford_op, after_to_before)
1115-
coef = -self._coefficient if should_negate else self.coefficient
1116-
return PauliString(qubit_pauli_map=pauli_map, coefficient=coef)
1099+
# TODO(#6946): deprecate this method.
1100+
# Note: This method is supposed to be replaced by conjugated_by()
1101+
# (see #2351 for details).
1102+
if after_to_before:
1103+
return self.after(ops)
1104+
1105+
if isinstance(ops, gate_operation.GateOperation):
1106+
return self.before(ops)
1107+
1108+
all_ops = list(op_tree.flatten_to_ops(ops))
1109+
return self.before(all_ops[::-1])
11171110

11181111
def _is_parameterized_(self) -> bool:
11191112
return protocols.is_parameterized(self.coefficient)
@@ -1179,7 +1172,7 @@ def _try_interpret_as_pauli_string(op: Any):
11791172
if (pauli := gates.get(type(op.gate), None)) is not None:
11801173
exponent = op.gate.exponent # type: ignore
11811174
if exponent % 2 == 0:
1182-
return cirq.PauliString()
1175+
return PauliString()
11831176
if exponent % 2 == 1:
11841177
return pauli.on(op.qubits[0])
11851178
return None

cirq-core/cirq/ops/pauli_string_phasor_test.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -163,8 +163,10 @@ def test_pass_operations_over():
163163
ps_after = cirq.PauliString({q0: cirq.Z, q1: cirq.Y}, -1)
164164
before = cirq.PauliStringPhasor(ps_before, exponent_neg=0.1)
165165
after = cirq.PauliStringPhasor(ps_after, exponent_neg=0.1)
166-
assert before.pass_operations_over([op]) == after
167-
assert after.pass_operations_over([op], after_to_before=True) == before
166+
assert before.pass_operations_over([op]).pauli_string == after.pauli_string
167+
assert (
168+
after.pass_operations_over([op], after_to_before=True).pauli_string == before.pauli_string
169+
)
168170

169171

170172
def test_extrapolate_effect():

cirq-core/cirq/ops/pauli_string_test.py

Lines changed: 64 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -58,19 +58,53 @@ def _small_sample_qubit_pauli_maps():
5858

5959

6060
def assert_conjugation(
61-
input_ps: cirq.PauliString, ops: cirq.OP_TREE, expected: cirq.PauliString | None
61+
input_ps: cirq.PauliString,
62+
op: cirq.Operation,
63+
expected: cirq.PauliString | None = None,
64+
force_checking_unitary=True,
65+
):
66+
"""Verifies that conjugating `input_ps` by `op` results in `expected`.
67+
68+
Also ensures that the unitary representation of the Pauli string is
69+
preserved under the conjugation.
70+
"""
71+
72+
def _ps_on_qubits(ps: cirq.PauliString, qubits: tuple[cirq.Qid, ...]):
73+
"""Extracts a sub-PauliString from a given PauliString, restricted to
74+
a specified subset of qubits.
75+
"""
76+
pauli_map = {}
77+
for q, pauli in ps.items():
78+
if q in qubits:
79+
pauli_map[q] = pauli
80+
return cirq.PauliString(qubit_pauli_map=pauli_map, coefficient=ps.coefficient)
81+
82+
conjugation = input_ps.conjugated_by(op)
83+
if expected is None or force_checking_unitary:
84+
# Compares the unitary of the conjugation result and the expected unitary.
85+
clifford = cirq.CliffordGate.from_op_list([op], op.qubits)
86+
actual_unitary = cirq.unitary(_ps_on_qubits(conjugation, op.qubits).dense(op.qubits))
87+
c = cirq.unitary(clifford)
88+
expected_unitary = (
89+
np.conj(c.T) @ cirq.unitary(_ps_on_qubits(input_ps, op.qubits).dense(op.qubits)) @ c
90+
)
91+
assert np.allclose(actual_unitary, expected_unitary, atol=1e-8)
92+
if expected is not None:
93+
assert conjugation == expected
94+
95+
96+
def assert_conjugation_multi_ops(
97+
input_ps: cirq.PauliString, ops: list[cirq.Operation], expected: cirq.PauliString | None = None
6298
):
6399
conjugation = input_ps.conjugated_by(ops)
64100
if expected is not None:
65101
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)
102+
# conj_by(op_{n-1}).conj_by(op_{n-1}).....conj_by(op_0)
103+
conj_in_order = input_ps
104+
for op in ops[::-1]:
105+
assert_conjugation(conj_in_order, op)
106+
conj_in_order = conj_in_order.conjugated_by(op)
107+
assert conjugation == conj_in_order
74108

75109

76110
def test_eq_ne_hash():
@@ -741,26 +775,31 @@ def test_pass_operations_over_double(shift: int, t_or_f1: bool, t_or_f2: bool, n
741775
op0 = cirq.PauliInteractionGate(Z, t_or_f1, X, t_or_f2)(q0, q1)
742776
ps_before = cirq.PauliString(qubit_pauli_map={q0: Z, q2: Y}, coefficient=sign)
743777
ps_after = cirq.PauliString(qubit_pauli_map={q0: Z, q2: Y}, coefficient=sign)
778+
assert_conjugation(ps_before, op0, ps_after, True)
744779
_assert_pass_over([op0], ps_before, ps_after)
745780

746781
op0 = cirq.PauliInteractionGate(Y, t_or_f1, X, t_or_f2)(q0, q1)
747782
ps_before = cirq.PauliString({q0: Z, q2: Y}, sign)
748-
ps_after = cirq.PauliString({q0: Z, q2: Y, q1: X}, sign)
783+
ps_after = cirq.PauliString({q0: Z, q2: Y, q1: X}, -sign if t_or_f2 else sign)
784+
assert_conjugation(ps_before, op0, ps_after, True)
749785
_assert_pass_over([op0], ps_before, ps_after)
750786

751787
op0 = cirq.PauliInteractionGate(Z, t_or_f1, X, t_or_f2)(q0, q1)
752788
ps_before = cirq.PauliString({q0: Z, q1: Y}, sign)
753-
ps_after = cirq.PauliString({q1: Y}, sign)
789+
ps_after = cirq.PauliString({q1: Y}, -sign if t_or_f1 else sign)
790+
assert_conjugation(ps_before, op0, ps_after, True)
754791
_assert_pass_over([op0], ps_before, ps_after)
755792

756793
op0 = cirq.PauliInteractionGate(Y, t_or_f1, X, t_or_f2)(q0, q1)
757794
ps_before = cirq.PauliString({q0: Z, q1: Y}, sign)
758795
ps_after = cirq.PauliString({q0: X, q1: Z}, -1 if neg ^ t_or_f1 ^ t_or_f2 else +1)
796+
assert_conjugation(ps_before, op0, ps_after, True)
759797
_assert_pass_over([op0], ps_before, ps_after)
760798

761799
op0 = cirq.PauliInteractionGate(X, t_or_f1, X, t_or_f2)(q0, q1)
762800
ps_before = cirq.PauliString({q0: Z, q1: Y}, sign)
763801
ps_after = cirq.PauliString({q0: Y, q1: Z}, +1 if neg ^ t_or_f1 ^ t_or_f2 else -1)
802+
assert_conjugation(ps_before, op0, ps_after, True)
764803
_assert_pass_over([op0], ps_before, ps_after)
765804

766805

@@ -774,7 +813,9 @@ def test_pass_operations_over_cz():
774813

775814
def test_pass_operations_over_no_common_qubits():
776815
class ExampleGate(cirq.testing.SingleQubitGate):
777-
pass
816+
817+
def _decompose_(self, qubits):
818+
return cirq.X(qubits[0])
778819

779820
q0, q1 = _make_qubits(2)
780821
op0 = ExampleGate()(q1)
@@ -786,7 +827,11 @@ class ExampleGate(cirq.testing.SingleQubitGate):
786827
def test_pass_unsupported_operations_over():
787828
(q0,) = _make_qubits(1)
788829
pauli_string = cirq.PauliString({q0: cirq.X})
789-
with pytest.raises(TypeError, match='not a known Clifford'):
830+
with pytest.raises(
831+
ValueError,
832+
match='Clifford Gate can only be constructed from the operations'
833+
' that has stabilizer effect.',
834+
):
790835
pauli_string.pass_operations_over([cirq.T(q0)])
791836

792837

@@ -1523,8 +1568,8 @@ def _decompose_(self, qubits):
15231568
def test_conjugated_by_move_into_uninvolved():
15241569
a, b, c, d = cirq.LineQubit.range(4)
15251570
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))
1571+
assert_conjugation_multi_ops(ps, [cirq.SWAP(c, d), cirq.SWAP(b, c)], cirq.X(a) * cirq.Z(d))
1572+
assert_conjugation_multi_ops(ps, [cirq.SWAP(b, c), cirq.SWAP(c, d)], cirq.X(a) * cirq.Z(c))
15281573

15291574

15301575
def test_conjugated_by_common_single_qubit_gates():
@@ -1549,7 +1594,7 @@ def test_conjugated_by_common_single_qubit_gates():
15491594
# pauli gate on a, clifford on b: pauli gate preserves.
15501595
assert_conjugation(p(a), g(b), p(a))
15511596
# pauli gate on a, clifford on a: check conjugation in matrices.
1552-
assert_conjugation(p(a), g(a), None)
1597+
assert_conjugation(p(a), g(a))
15531598

15541599

15551600
def test_conjugated_by_common_two_qubit_gates():
@@ -1580,7 +1625,7 @@ def test_conjugated_by_common_two_qubit_gates():
15801625
assert_conjugation(p, g(c, d), p)
15811626
# pauli_string on (a,b), clifford on (a,b): compare unitaries of
15821627
# the conjugated_by and actual matrix conjugation.
1583-
assert_conjugation(p, g.on(a, b), None)
1628+
assert_conjugation(p, g.on(a, b))
15841629

15851630

15861631
def test_conjugated_by_ordering():
@@ -1602,7 +1647,7 @@ def _decompose_(self, qubits):
16021647

16031648
a, b = cirq.LineQubit.range(2)
16041649
inp = cirq.Z(b)
1605-
out1 = inp.pass_operations_over([OrderSensitiveGate().on(a, b)])
1650+
out1 = inp.pass_operations_over(OrderSensitiveGate().on(a, b))
16061651
out2 = inp.pass_operations_over([cirq.CNOT(a, b), cirq.Y(a) ** -0.5])
16071652
out3 = inp.pass_operations_over([cirq.CNOT(a, b)]).pass_operations_over([cirq.Y(a) ** -0.5])
16081653
assert out1 == out2 == out3 == cirq.X(a) * cirq.Z(b)
@@ -1618,7 +1663,7 @@ def _decompose_(self, qubits):
16181663

16191664
a, b = cirq.LineQubit.range(2)
16201665
inp = cirq.X(a) * cirq.Z(b)
1621-
out1 = inp.pass_operations_over([OrderSensitiveGate().on(a, b)], after_to_before=True)
1666+
out1 = inp.pass_operations_over(OrderSensitiveGate().on(a, b), after_to_before=True)
16221667
out2 = inp.pass_operations_over([cirq.Y(a) ** -0.5, cirq.CNOT(a, b)], after_to_before=True)
16231668
out3 = inp.pass_operations_over([cirq.Y(a) ** -0.5], after_to_before=True).pass_operations_over(
16241669
[cirq.CNOT(a, b)], after_to_before=True

0 commit comments

Comments
 (0)