From d9c9d1fdcd2b2c9336ef9fc9cbdff18f5034013a Mon Sep 17 00:00:00 2001 From: Renyi Chen Date: Mon, 3 Feb 2025 16:02:16 -0800 Subject: [PATCH 1/7] Small fixes. --- cirq-core/cirq/ops/pauli_string.py | 21 ++++++++++----------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/cirq-core/cirq/ops/pauli_string.py b/cirq-core/cirq/ops/pauli_string.py index 2360cecd17a..47938977137 100644 --- a/cirq-core/cirq/ops/pauli_string.py +++ b/cirq-core/cirq/ops/pauli_string.py @@ -41,7 +41,6 @@ import numpy as np import sympy -import cirq from cirq import value, protocols, linalg, qis, _compat from cirq._doc import document from cirq._import import LazyLoader @@ -496,7 +495,7 @@ def matrix(self, qubits: Optional[Iterable[TKey]] = None) -> np.ndarray: """ qubits = self.qubits if qubits is None else qubits factors = [self.get(q, default=identity.I) for q in qubits] - if cirq.is_parameterized(self): + if protocols.is_parameterized(self): raise NotImplementedError('Cannot express as matrix when parameterized') assert isinstance(self.coefficient, complex) return linalg.kron(self.coefficient, *[protocols.unitary(f) for f in factors]) @@ -980,7 +979,9 @@ def conjugated_by(self, clifford: 'cirq.OP_TREE') -> 'PauliString': # Initialize the ps the same as self. ps = PauliString(qubit_pauli_map=self._qubit_pauli_map, coefficient=self.coefficient) all_ops = list(op_tree.flatten_to_ops(clifford)) - all_qubits = set.union(set(self.qubits), [q for op in all_ops for q in op.qubits]) + all_qubits: set[TKey] = set.union( + set(self.qubits), [q for op in all_ops for q in op.qubits] + ) # Iteratively calculate the conjugation in reverse order of ops. for op in all_ops[::-1]: @@ -989,11 +990,9 @@ def conjugated_by(self, clifford: 'cirq.OP_TREE') -> 'PauliString': # Then the conjugation = (C^{-1}⊗I·Pc⊗R·C⊗I) = (C^{-1}·Pc·C)⊗R. # Isolate R - remain: 'cirq.PauliString' = PauliString() - for q in all_qubits: - pauli = ps.get(q) - if pauli is not None and not q in op.qubits: - remain *= pauli(q) + remain: 'cirq.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' = ( @@ -1005,7 +1004,7 @@ def conjugated_by(self, clifford: 'cirq.OP_TREE') -> 'PauliString': # 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, cirq.CliffordGate): + if isinstance(op.gate, clifford_gate.CliffordGate): gate_in_clifford = op.gate else: # Convert the clifford gate to CliffordGate type. @@ -1020,7 +1019,7 @@ def conjugated_by(self, clifford: 'cirq.OP_TREE') -> 'PauliString': # 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) + # 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: @@ -1179,7 +1178,7 @@ def _try_interpret_as_pauli_string(op: Any): if (pauli := gates.get(type(op.gate), None)) is not None: exponent = op.gate.exponent # type: ignore if exponent % 2 == 0: - return cirq.PauliString() + return PauliString() if exponent % 2 == 1: return pauli.on(op.qubits[0]) return None From 3997ec72d2309de7096588b4b6b33e8eb7ca842b Mon Sep 17 00:00:00 2001 From: Renyi Chen Date: Tue, 4 Mar 2025 19:38:12 -0800 Subject: [PATCH 2/7] Update pass_operations_over with fixed conjugated_by --- cirq-core/cirq/ops/pauli_string.py | 32 ++++------ cirq-core/cirq/ops/pauli_string_test.py | 82 ++++++++++++++++++++----- 2 files changed, 78 insertions(+), 36 deletions(-) diff --git a/cirq-core/cirq/ops/pauli_string.py b/cirq-core/cirq/ops/pauli_string.py index 47938977137..6c0b8aee0d9 100644 --- a/cirq-core/cirq/ops/pauli_string.py +++ b/cirq-core/cirq/ops/pauli_string.py @@ -979,10 +979,7 @@ def conjugated_by(self, clifford: 'cirq.OP_TREE') -> 'PauliString': # Initialize the ps the same as self. ps = PauliString(qubit_pauli_map=self._qubit_pauli_map, coefficient=self.coefficient) all_ops = list(op_tree.flatten_to_ops(clifford)) - all_qubits: set[TKey] = set.union( - set(self.qubits), [q for op in all_ops for q in op.qubits] - ) - + all_qubits = set.union(set(self.qubits), [q for op in all_ops for q in op.qubits]) # Iteratively calculate the conjugation in reverse order of ops. for op in all_ops[::-1]: # To calcuate the conjugation of P (`ps`) with respect to C (`op`) @@ -997,7 +994,7 @@ def conjugated_by(self, clifford: 'cirq.OP_TREE') -> 'PauliString': # Initialize the conjugation of Pc. conjugated: 'cirq.DensePauliString' = ( dense_pauli_string.DensePauliString(pauli_mask=[identity.I for _ in op.qubits]) - * self.coefficient + * ps.coefficient ) # Calculate the conjugation via CliffordGate's clifford_tableau. @@ -1099,20 +1096,17 @@ def pass_operations_over( pauli string, instead of before (and so are moving in the opposite direction). """ - pauli_map = dict(self._qubit_pauli_map) - should_negate = False - for op in ops: - if pauli_map.keys().isdisjoint(set(op.qubits)): - continue - decomposed = _decompose_into_cliffords(op) - if not after_to_before: - decomposed = decomposed[::-1] - for clifford_op in decomposed: - if pauli_map.keys().isdisjoint(set(clifford_op.qubits)): - continue - should_negate ^= _pass_operation_over(pauli_map, clifford_op, after_to_before) - coef = -self._coefficient if should_negate else self.coefficient - return PauliString(qubit_pauli_map=pauli_map, coefficient=coef) + # TODO(#6946): deprecate this method. + # Note: This method is supposed to be replaced by conjugated_by() + # (see #2351 for details). + if after_to_before: + return self.after(ops) + + if isinstance(ops, gate_operation.GateOperation): + return self.before(ops) + + all_ops = list(op_tree.flatten_to_ops(ops)) + return self.before(all_ops[::-1]) def _is_parameterized_(self) -> bool: return protocols.is_parameterized(self.coefficient) diff --git a/cirq-core/cirq/ops/pauli_string_test.py b/cirq-core/cirq/ops/pauli_string_test.py index 221efd0f513..98e60c18a72 100644 --- a/cirq-core/cirq/ops/pauli_string_test.py +++ b/cirq-core/cirq/ops/pauli_string_test.py @@ -58,19 +58,53 @@ def _small_sample_qubit_pauli_maps(): def assert_conjugation( - input_ps: cirq.PauliString, ops: cirq.OP_TREE, expected: cirq.PauliString | None + input_ps: cirq.PauliString, + op: cirq.Operation, + expected: cirq.PauliString | None = None, + force_checking_unitary=True, +): + """Verifies that conjugating `input_ps` by `op` results in `expected`. + + Also ensures that the unitary representation of the Pauli string is + preserved under the conjugation. + """ + + def _ps_on_qubits(ps: cirq.PauliString, qubits: tuple[cirq.Qid, ...]): + """Extracts a sub-PauliString from a given PauliString, restricted to + a specified subset of qubits. + """ + pauli_map = {} + for q, pauli in ps.items(): + if q in qubits: + pauli_map[q] = pauli + return cirq.PauliString(qubit_pauli_map=pauli_map, coefficient=ps.coefficient) + + conjugation = input_ps.conjugated_by(op) + if expected is None or force_checking_unitary: + # Compares the unitary of the conjugation result and the expected unitary. + clifford = cirq.CliffordGate.from_op_list([op], op.qubits) + actual_unitary = cirq.unitary(_ps_on_qubits(conjugation, op.qubits).dense(op.qubits)) + c = cirq.unitary(clifford) + expected_unitary = ( + np.conj(c.T) @ cirq.unitary(_ps_on_qubits(input_ps, op.qubits).dense(op.qubits)) @ c + ) + assert np.allclose(actual_unitary, expected_unitary, atol=1e-8) + if expected is not None: + assert conjugation == expected + + +def assert_conjugation_multi_ops( + input_ps: cirq.PauliString, ops: list[cirq.Operation], expected: cirq.PauliString | None = None ): conjugation = input_ps.conjugated_by(ops) if expected is not None: assert conjugation == expected - else: # Compares the unitary of the conjugation result and the expected unitary. - op_list = list(cirq.flatten_to_ops(ops)) - qubits_of_clifford = [q for op in op_list for q in op.qubits] - clifford = cirq.CliffordGate.from_op_list(op_list, qubits_of_clifford) - actual_unitary = cirq.unitary(conjugation.dense(qubits_of_clifford)) - c = cirq.unitary(clifford) - expected_unitary = np.conj(c.T) @ cirq.unitary(input_ps.dense(qubits_of_clifford)) @ c - assert np.allclose(actual_unitary, expected_unitary, atol=1e-8) + # conj_by(op_{n-1}).conj_by(op_{n-1}).....conj_by(op_0) + conj_in_order = input_ps + for op in ops[::-1]: + assert_conjugation(conj_in_order, op) + conj_in_order = conj_in_order.conjugated_by(op) + assert conjugation == conj_in_order 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 op0 = cirq.PauliInteractionGate(Z, t_or_f1, X, t_or_f2)(q0, q1) ps_before = cirq.PauliString(qubit_pauli_map={q0: Z, q2: Y}, coefficient=sign) ps_after = cirq.PauliString(qubit_pauli_map={q0: Z, q2: Y}, coefficient=sign) + assert_conjugation(ps_before, op0, ps_after, True) _assert_pass_over([op0], ps_before, ps_after) op0 = cirq.PauliInteractionGate(Y, t_or_f1, X, t_or_f2)(q0, q1) ps_before = cirq.PauliString({q0: Z, q2: Y}, sign) - ps_after = cirq.PauliString({q0: Z, q2: Y, q1: X}, sign) + ps_after = cirq.PauliString({q0: Z, q2: Y, q1: X}, -sign if t_or_f2 else sign) + assert_conjugation(ps_before, op0, ps_after, True) _assert_pass_over([op0], ps_before, ps_after) op0 = cirq.PauliInteractionGate(Z, t_or_f1, X, t_or_f2)(q0, q1) ps_before = cirq.PauliString({q0: Z, q1: Y}, sign) - ps_after = cirq.PauliString({q1: Y}, sign) + ps_after = cirq.PauliString({q1: Y}, -sign if t_or_f1 else sign) + assert_conjugation(ps_before, op0, ps_after, True) _assert_pass_over([op0], ps_before, ps_after) op0 = cirq.PauliInteractionGate(Y, t_or_f1, X, t_or_f2)(q0, q1) ps_before = cirq.PauliString({q0: Z, q1: Y}, sign) ps_after = cirq.PauliString({q0: X, q1: Z}, -1 if neg ^ t_or_f1 ^ t_or_f2 else +1) + assert_conjugation(ps_before, op0, ps_after, True) _assert_pass_over([op0], ps_before, ps_after) op0 = cirq.PauliInteractionGate(X, t_or_f1, X, t_or_f2)(q0, q1) ps_before = cirq.PauliString({q0: Z, q1: Y}, sign) ps_after = cirq.PauliString({q0: Y, q1: Z}, +1 if neg ^ t_or_f1 ^ t_or_f2 else -1) + assert_conjugation(ps_before, op0, ps_after, True) _assert_pass_over([op0], ps_before, ps_after) @@ -774,7 +813,12 @@ def test_pass_operations_over_cz(): def test_pass_operations_over_no_common_qubits(): class ExampleGate(cirq.testing.SingleQubitGate): - pass + + def num_qubits(self): + return 1 + + def _decompose_(self, qubits): + return cirq.X(qubits[0]) q0, q1 = _make_qubits(2) op0 = ExampleGate()(q1) @@ -786,7 +830,11 @@ class ExampleGate(cirq.testing.SingleQubitGate): def test_pass_unsupported_operations_over(): (q0,) = _make_qubits(1) pauli_string = cirq.PauliString({q0: cirq.X}) - with pytest.raises(TypeError, match='not a known Clifford'): + with pytest.raises( + ValueError, + match='Clifford Gate can only be constructed from the operations' + ' that has stabilizer effect.', + ): pauli_string.pass_operations_over([cirq.T(q0)]) @@ -1523,8 +1571,8 @@ def _decompose_(self, qubits): def test_conjugated_by_move_into_uninvolved(): a, b, c, d = cirq.LineQubit.range(4) ps = cirq.X(a) * cirq.Z(b) - assert_conjugation(ps, [cirq.SWAP(c, d), cirq.SWAP(b, c)], cirq.X(a) * cirq.Z(d)) - assert_conjugation(ps, [cirq.SWAP(b, c), cirq.SWAP(c, d)], cirq.X(a) * cirq.Z(c)) + assert_conjugation_multi_ops(ps, [cirq.SWAP(c, d), cirq.SWAP(b, c)], cirq.X(a) * cirq.Z(d)) + assert_conjugation_multi_ops(ps, [cirq.SWAP(b, c), cirq.SWAP(c, d)], cirq.X(a) * cirq.Z(c)) def test_conjugated_by_common_single_qubit_gates(): @@ -1549,7 +1597,7 @@ def test_conjugated_by_common_single_qubit_gates(): # pauli gate on a, clifford on b: pauli gate preserves. assert_conjugation(p(a), g(b), p(a)) # pauli gate on a, clifford on a: check conjugation in matrices. - assert_conjugation(p(a), g(a), None) + assert_conjugation(p(a), g(a)) def test_conjugated_by_common_two_qubit_gates(): @@ -1580,7 +1628,7 @@ def test_conjugated_by_common_two_qubit_gates(): assert_conjugation(p, g(c, d), p) # pauli_string on (a,b), clifford on (a,b): compare unitaries of # the conjugated_by and actual matrix conjugation. - assert_conjugation(p, g.on(a, b), None) + assert_conjugation(p, g.on(a, b)) def test_conjugated_by_ordering(): From ad8e790c98e083a7c2e4ef46fdcd24817d51e9ee Mon Sep 17 00:00:00 2001 From: Renyi Chen Date: Tue, 4 Mar 2025 23:58:57 -0800 Subject: [PATCH 3/7] Order the qubit_pauli_map in the output for PauliStringPhasor --- cirq-core/cirq/ops/pauli_string.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/cirq-core/cirq/ops/pauli_string.py b/cirq-core/cirq/ops/pauli_string.py index 6c0b8aee0d9..cf6ecc79c8c 100644 --- a/cirq-core/cirq/ops/pauli_string.py +++ b/cirq-core/cirq/ops/pauli_string.py @@ -1033,7 +1033,9 @@ def conjugated_by(self, clifford: 'cirq.OP_TREE') -> 'PauliString': * tableau.stabilizers()[qid] # then conj Z ) ps = remain * conjugated.on(*op.qubits) - return ps + return PauliString( + qubit_pauli_map=dict(sorted(ps._qubit_pauli_map.items())), coefficient=ps.coefficient + ) def after(self, ops: 'cirq.OP_TREE') -> 'cirq.PauliString': r"""Determines the equivalent pauli string after some operations. From 02474de19f1afcfb6ab49359d1c51a34db8dcff8 Mon Sep 17 00:00:00 2001 From: Renyi Chen Date: Wed, 5 Mar 2025 14:53:36 -0800 Subject: [PATCH 4/7] Fix coverage --- cirq-core/cirq/ops/pauli_string_test.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cirq-core/cirq/ops/pauli_string_test.py b/cirq-core/cirq/ops/pauli_string_test.py index 98e60c18a72..d214546c333 100644 --- a/cirq-core/cirq/ops/pauli_string_test.py +++ b/cirq-core/cirq/ops/pauli_string_test.py @@ -1650,7 +1650,7 @@ def _decompose_(self, qubits): a, b = cirq.LineQubit.range(2) inp = cirq.Z(b) - out1 = inp.pass_operations_over([OrderSensitiveGate().on(a, b)]) + out1 = inp.pass_operations_over(OrderSensitiveGate().on(a, b)) out2 = inp.pass_operations_over([cirq.CNOT(a, b), cirq.Y(a) ** -0.5]) out3 = inp.pass_operations_over([cirq.CNOT(a, b)]).pass_operations_over([cirq.Y(a) ** -0.5]) assert out1 == out2 == out3 == cirq.X(a) * cirq.Z(b) @@ -1666,7 +1666,7 @@ def _decompose_(self, qubits): a, b = cirq.LineQubit.range(2) inp = cirq.X(a) * cirq.Z(b) - out1 = inp.pass_operations_over([OrderSensitiveGate().on(a, b)], after_to_before=True) + out1 = inp.pass_operations_over(OrderSensitiveGate().on(a, b), after_to_before=True) out2 = inp.pass_operations_over([cirq.Y(a) ** -0.5, cirq.CNOT(a, b)], after_to_before=True) out3 = inp.pass_operations_over([cirq.Y(a) ** -0.5], after_to_before=True).pass_operations_over( [cirq.CNOT(a, b)], after_to_before=True From 740a2ebd242357a8dcb88056c9701518f24cb8b2 Mon Sep 17 00:00:00 2001 From: Renyi Chen Date: Wed, 5 Mar 2025 20:23:56 -0800 Subject: [PATCH 5/7] Fix pauli_string_phasor_test. pass_operations_over doesn't necessarily preserve the order of qubits. --- cirq-core/cirq/ops/pauli_string.py | 4 +--- cirq-core/cirq/ops/pauli_string_phasor_test.py | 6 ++++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/cirq-core/cirq/ops/pauli_string.py b/cirq-core/cirq/ops/pauli_string.py index cf6ecc79c8c..6c0b8aee0d9 100644 --- a/cirq-core/cirq/ops/pauli_string.py +++ b/cirq-core/cirq/ops/pauli_string.py @@ -1033,9 +1033,7 @@ def conjugated_by(self, clifford: 'cirq.OP_TREE') -> 'PauliString': * tableau.stabilizers()[qid] # then conj Z ) ps = remain * conjugated.on(*op.qubits) - return PauliString( - qubit_pauli_map=dict(sorted(ps._qubit_pauli_map.items())), coefficient=ps.coefficient - ) + return ps def after(self, ops: 'cirq.OP_TREE') -> 'cirq.PauliString': r"""Determines the equivalent pauli string after some operations. diff --git a/cirq-core/cirq/ops/pauli_string_phasor_test.py b/cirq-core/cirq/ops/pauli_string_phasor_test.py index 017ca168046..00838ff8b61 100644 --- a/cirq-core/cirq/ops/pauli_string_phasor_test.py +++ b/cirq-core/cirq/ops/pauli_string_phasor_test.py @@ -163,8 +163,10 @@ def test_pass_operations_over(): ps_after = cirq.PauliString({q0: cirq.Z, q1: cirq.Y}, -1) before = cirq.PauliStringPhasor(ps_before, exponent_neg=0.1) after = cirq.PauliStringPhasor(ps_after, exponent_neg=0.1) - assert before.pass_operations_over([op]) == after - assert after.pass_operations_over([op], after_to_before=True) == before + assert before.pass_operations_over([op]).pauli_string == after.pauli_string + assert ( + after.pass_operations_over([op], after_to_before=True).pauli_string == before.pauli_string + ) def test_extrapolate_effect(): From 67b1a8747ba9fcdd62d6bca1b39d1913eb1d2a98 Mon Sep 17 00:00:00 2001 From: Pavol Juhas Date: Thu, 6 Mar 2025 11:50:16 -0800 Subject: [PATCH 6/7] 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: https://github.com/rigetti/qcs-sdk-rust/issues/531 * Also adjust Dockerfile to avoid the bad wheel --- Dockerfile | 12 +++++++----- cirq-rigetti/requirements.txt | 3 +++ 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/Dockerfile b/Dockerfile index 6f9ef227ef0..aa45ee075ba 100644 --- a/Dockerfile +++ b/Dockerfile @@ -10,17 +10,19 @@ RUN DEBIAN_FRONTEND=noninteractive apt-get update && DEBIAN_FRONTEND=noninteract # Configure UTF-8 encoding. RUN sed -i -e 's/# en_US.UTF-8 UTF-8/en_US.UTF-8 UTF-8/' /etc/locale.gen && locale-gen -ENV LANG en_US.UTF-8 -ENV LANGUAGE en_US:en -ENV LC_ALL en_US.UTF-8 +ENV LANG en_US.UTF-8 +ENV LANGUAGE en_US:en +ENV LC_ALL en_US.UTF-8 # Make python3 default RUN rm -f /usr/bin/python \ && ln -s /usr/bin/python3 /usr/bin/python #cirq stable image FROM cirq_base AS cirq_stable -RUN pip3 install cirq +# TODO: adjust after the fix of https://github.com/rigetti/qcs-sdk-rust/issues/531 +RUN pip3 install cirq "qcs-sdk-python<=0.21.12" ##cirq pre_release image FROM cirq_base AS cirq_pre_release -RUN pip3 install cirq~=1.0.dev +# TODO: adjust after the fix of https://github.com/rigetti/qcs-sdk-rust/issues/531 +RUN pip3 install cirq~=1.0.dev "qcs-sdk-python<=0.21.12" diff --git a/cirq-rigetti/requirements.txt b/cirq-rigetti/requirements.txt index 48211460476..836a644be6b 100644 --- a/cirq-rigetti/requirements.txt +++ b/cirq-rigetti/requirements.txt @@ -1 +1,4 @@ pyquil>=4.14.3,<5.0.0 + +# TODO: remove after the fix of https://github.com/rigetti/qcs-sdk-rust/issues/531 +qcs-sdk-python<=0.21.12 From 57c53b729aea139f61473705113bf462bfd4e67b Mon Sep 17 00:00:00 2001 From: Renyi Chen Date: Thu, 6 Mar 2025 14:06:28 -0800 Subject: [PATCH 7/7] Fix coverage, the num_qubits func in the Faked gate in the test isn't necessary for the test --- cirq-core/cirq/ops/pauli_string_test.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/cirq-core/cirq/ops/pauli_string_test.py b/cirq-core/cirq/ops/pauli_string_test.py index d214546c333..2fb5f4c051a 100644 --- a/cirq-core/cirq/ops/pauli_string_test.py +++ b/cirq-core/cirq/ops/pauli_string_test.py @@ -814,9 +814,6 @@ def test_pass_operations_over_cz(): def test_pass_operations_over_no_common_qubits(): class ExampleGate(cirq.testing.SingleQubitGate): - def num_qubits(self): - return 1 - def _decompose_(self, qubits): return cirq.X(qubits[0])