From af8a5dd6c68a9cd6f7f573dc80b52ce27974846e Mon Sep 17 00:00:00 2001 From: Noureldin Date: Tue, 30 May 2023 18:53:35 +0100 Subject: [PATCH 01/10] Add support for allocating qubits in decompose to cirq.unitary --- cirq-core/cirq/protocols/unitary_protocol.py | 19 ++++++- .../cirq/protocols/unitary_protocol_test.py | 51 +++++++++++++++++++ 2 files changed, 68 insertions(+), 2 deletions(-) diff --git a/cirq-core/cirq/protocols/unitary_protocol.py b/cirq-core/cirq/protocols/unitary_protocol.py index e5acd30e7df..cc977cfdbfb 100644 --- a/cirq-core/cirq/protocols/unitary_protocol.py +++ b/cirq-core/cirq/protocols/unitary_protocol.py @@ -179,15 +179,30 @@ def _strat_unitary_from_decompose(val: Any) -> Optional[np.ndarray]: if operations is None: return NotImplemented + all_qubits = frozenset(q for op in operations for q in op.qubits) + work_qubits = frozenset(qubits) + ancillas = tuple(sorted(q for q in all_qubits if q not in work_qubits)) + + ordered_qubits = ancillas + tuple(qubits) + val_qid_shape = (2,) * len( + ancillas + ) + val_qid_shape # For now ancillas have only one qid_shape = (2,). + # Apply sub-operations' unitary effects to an identity matrix. state = qis.eye_tensor(val_qid_shape, dtype=np.complex128) buffer = np.empty_like(state) result = apply_unitaries( - operations, qubits, ApplyUnitaryArgs(state, buffer, range(len(val_qid_shape))), None + operations, ordered_qubits, ApplyUnitaryArgs(state, buffer, range(len(val_qid_shape))), None ) # Package result. if result is None: return None + state_len = np.prod(val_qid_shape, dtype=np.int64) - return result.reshape((state_len, state_len)) + work_state_len = np.prod(val_qid_shape[len(ancillas) :], dtype=np.int64) + result = result.reshape((state_len, state_len)) + # Assuming borrowable qubits are restored to their original state and + # clean qubits restord to the zero state then the desired unitary is + # the upper left square. + return result[:work_state_len, :work_state_len] diff --git a/cirq-core/cirq/protocols/unitary_protocol_test.py b/cirq-core/cirq/protocols/unitary_protocol_test.py index 46448e76d16..48476531570 100644 --- a/cirq-core/cirq/protocols/unitary_protocol_test.py +++ b/cirq-core/cirq/protocols/unitary_protocol_test.py @@ -93,6 +93,40 @@ def _decompose_(self, qubits): yield FullyImplemented(self.unitary_value).on(qubits[0]) +class GateThatAllocatesAQubit(cirq.Gate): + _target_unitary = np.array([[1, 0], [0, -1]]) + + def _num_qubits_(self): + return 1 + + def _decompose_(self, q): + anc = cirq.NamedQubit("anc") + yield cirq.CX(*q, anc) + yield (cirq.Z)(anc) + yield cirq.CX(*q, anc) + + +class GateThatAllocatesTwoQubits(cirq.Gate): + # Unitary = (-j I_2) \otimes Z + _target_unitary = np.array([[-1j, 0, 0, 0], [0, 1j, 0, 0], [0, 0, 1j, 0], [0, 0, 0, -1j]]) + + def _num_qubits_(self): + return 2 + + def _decompose_(self, qs): + q0, q1 = qs + anc = cirq.NamedQubit.range(2, prefix='two_ancillas_') + + yield cirq.X(anc[0]) + yield cirq.CX(q0, anc[0]) + yield (cirq.Y)(anc[0]) + yield cirq.CX(q0, anc[0]) + + yield cirq.CX(q1, anc[1]) + yield (cirq.Z)(anc[1]) + yield cirq.CX(q1, anc[1]) + + class DecomposableOperation(cirq.Operation): qubits = () with_qubits = NotImplemented @@ -201,6 +235,23 @@ def test_decompose_and_get_unitary(): np.testing.assert_allclose(_strat_unitary_from_decompose(DummyComposite()), np.eye(1)) np.testing.assert_allclose(_strat_unitary_from_decompose(OtherComposite()), m2) + np.testing.assert_allclose( + _strat_unitary_from_decompose(GateThatAllocatesAQubit()), + GateThatAllocatesAQubit._target_unitary, + ) + np.testing.assert_allclose( + _strat_unitary_from_decompose(GateThatAllocatesAQubit().on(a)), + GateThatAllocatesAQubit._target_unitary, + ) + np.testing.assert_allclose( + _strat_unitary_from_decompose(GateThatAllocatesTwoQubits()), + GateThatAllocatesTwoQubits._target_unitary, + ) + np.testing.assert_allclose( + _strat_unitary_from_decompose(GateThatAllocatesTwoQubits().on(a, b)), + GateThatAllocatesTwoQubits._target_unitary, + ) + def test_decomposed_has_unitary(): # Gates From d10ae048b31b3d8321a1badef3106ad6fcda7064 Mon Sep 17 00:00:00 2001 From: Noureldin Date: Thu, 1 Jun 2023 20:14:14 +0100 Subject: [PATCH 02/10] fixed apply_unitaries --- .../cirq/protocols/apply_unitary_protocol.py | 41 +++++++--- .../protocols/apply_unitary_protocol_test.py | 40 ++++++++++ cirq-core/cirq/protocols/unitary_protocol.py | 8 +- .../cirq/protocols/unitary_protocol_test.py | 79 +++++++++++++++---- 4 files changed, 137 insertions(+), 31 deletions(-) diff --git a/cirq-core/cirq/protocols/apply_unitary_protocol.py b/cirq-core/cirq/protocols/apply_unitary_protocol.py index 61881eddbf6..a5182a1332b 100644 --- a/cirq-core/cirq/protocols/apply_unitary_protocol.py +++ b/cirq-core/cirq/protocols/apply_unitary_protocol.py @@ -410,17 +410,18 @@ def _strat_apply_unitary_from_apply_unitary( def _strat_apply_unitary_from_unitary( - unitary_value: Any, args: ApplyUnitaryArgs + unitary_value: Any, args: ApplyUnitaryArgs, matrix: Optional[np.ndarray] = None ) -> Optional[np.ndarray]: - # Check for magic method. - method = getattr(unitary_value, '_unitary_', None) - if method is None: - return NotImplemented + if matrix is None: + # Check for magic method. + method = getattr(unitary_value, '_unitary_', None) + if method is None: + return NotImplemented - # Attempt to get the unitary matrix. - matrix = method() - if matrix is NotImplemented or matrix is None: - return matrix + # Attempt to get the unitary matrix. + matrix = method() + if matrix is NotImplemented or matrix is None: + return matrix if args.slices is None: val_qid_shape = qid_shape_protocol.qid_shape(unitary_value, default=(2,) * len(args.axes)) @@ -454,7 +455,27 @@ def _strat_apply_unitary_from_decompose(val: Any, args: ApplyUnitaryArgs) -> Opt operations, qubits, _ = _try_decompose_into_operations_and_qubits(val) if operations is None: return NotImplemented - return apply_unitaries(operations, qubits, args, None) + all_qubits = frozenset([q for op in operations for q in op.qubits]) + ancilla = tuple(sorted(all_qubits.difference(qubits))) + if not len(ancilla): + return apply_unitaries(operations, qubits, args, None) + ordered_qubits = ancilla + tuple(qubits) + all_qid_shapes = qid_shape_protocol.qid_shape(ordered_qubits) + state = qis.eye_tensor(all_qid_shapes, dtype=np.complex128) + buffer = np.empty_like(state) + result = apply_unitaries( + operations, + ordered_qubits, + ApplyUnitaryArgs(state, buffer, range(len(ordered_qubits))), + None, + ) + if result is None or result is NotImplemented: + return result + result = result.reshape((np.prod(all_qid_shapes, dtype=np.int64), -1)) + val_qid_shape = qid_shape_protocol.qid_shape(qubits) + state_vec_length = np.prod(val_qid_shape, dtype=np.int64) + result = result[:state_vec_length, :state_vec_length] + return _strat_apply_unitary_from_unitary(val, args, matrix=result) def apply_unitaries( diff --git a/cirq-core/cirq/protocols/apply_unitary_protocol_test.py b/cirq-core/cirq/protocols/apply_unitary_protocol_test.py index 1b455c6bd69..3f52e10ce12 100644 --- a/cirq-core/cirq/protocols/apply_unitary_protocol_test.py +++ b/cirq-core/cirq/protocols/apply_unitary_protocol_test.py @@ -717,3 +717,43 @@ def test_cast_to_complex(): np.ComplexWarning, match='Casting complex values to real discards the imaginary part' ): cirq.apply_unitary(y0, args) + + +class NotDecomposableGate(cirq.Gate): + def num_qubits(self): + return 1 + + +class DecomposableGate(cirq.Gate): + def __init__(self, sub_gate: cirq.Gate, allocate_ancilla: bool) -> None: + super().__init__() + self._sub_gate = sub_gate + self._allocate_ancilla = allocate_ancilla + + def num_qubits(self): + return 1 + + def _decompose_(self, qubits): + if self._allocate_ancilla: + yield cirq.Z(cirq.LineQubit(1)) + yield self._sub_gate(qubits[0]) + + +def test_strat_apply_unitary_from_decompose(): + state = np.eye(2, dtype=np.complex128) + args = cirq.ApplyUnitaryArgs( + target_tensor=state, available_buffer=np.zeros_like(state), axes=(0,) + ) + np.testing.assert_allclose( + cirq.apply_unitaries( + [DecomposableGate(cirq.X, False)(cirq.LineQubit(0))], [cirq.LineQubit(0)], args + ), + [[0, 1], [1, 0]], + ) + + with pytest.raises(TypeError): + _ = cirq.apply_unitaries( + [DecomposableGate(NotDecomposableGate(), True)(cirq.LineQubit(0))], + [cirq.LineQubit(0)], + args, + ) diff --git a/cirq-core/cirq/protocols/unitary_protocol.py b/cirq-core/cirq/protocols/unitary_protocol.py index cc977cfdbfb..9eb0be8e95a 100644 --- a/cirq-core/cirq/protocols/unitary_protocol.py +++ b/cirq-core/cirq/protocols/unitary_protocol.py @@ -181,12 +181,10 @@ def _strat_unitary_from_decompose(val: Any) -> Optional[np.ndarray]: all_qubits = frozenset(q for op in operations for q in op.qubits) work_qubits = frozenset(qubits) - ancillas = tuple(sorted(q for q in all_qubits if q not in work_qubits)) + ancillas = tuple(sorted(all_qubits.difference(work_qubits))) ordered_qubits = ancillas + tuple(qubits) - val_qid_shape = (2,) * len( - ancillas - ) + val_qid_shape # For now ancillas have only one qid_shape = (2,). + val_qid_shape = qid_shape_protocol.qid_shape(ancillas) + val_qid_shape # Apply sub-operations' unitary effects to an identity matrix. state = qis.eye_tensor(val_qid_shape, dtype=np.complex128) @@ -200,9 +198,9 @@ def _strat_unitary_from_decompose(val: Any) -> Optional[np.ndarray]: return None state_len = np.prod(val_qid_shape, dtype=np.int64) - work_state_len = np.prod(val_qid_shape[len(ancillas) :], dtype=np.int64) result = result.reshape((state_len, state_len)) # Assuming borrowable qubits are restored to their original state and # clean qubits restord to the zero state then the desired unitary is # the upper left square. + work_state_len = np.prod(val_qid_shape[len(ancillas) :], dtype=np.int64) return result[:work_state_len, :work_state_len] diff --git a/cirq-core/cirq/protocols/unitary_protocol_test.py b/cirq-core/cirq/protocols/unitary_protocol_test.py index 48476531570..267b54ed45e 100644 --- a/cirq-core/cirq/protocols/unitary_protocol_test.py +++ b/cirq-core/cirq/protocols/unitary_protocol_test.py @@ -11,7 +11,8 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. -from typing import Optional +from typing import Optional, cast +import functools import numpy as np import pytest @@ -94,7 +95,9 @@ def _decompose_(self, qubits): class GateThatAllocatesAQubit(cirq.Gate): - _target_unitary = np.array([[1, 0], [0, -1]]) + def __init__(self, theta: float) -> None: + super().__init__() + self._theta = theta def _num_qubits_(self): return 1 @@ -102,14 +105,14 @@ def _num_qubits_(self): def _decompose_(self, q): anc = cirq.NamedQubit("anc") yield cirq.CX(*q, anc) - yield (cirq.Z)(anc) + yield (cirq.Z**self._theta)(anc) yield cirq.CX(*q, anc) + def target_unitary(self) -> np.ndarray: + return np.array([[1, 0], [0, (-1 + 0j) ** self._theta]]) -class GateThatAllocatesTwoQubits(cirq.Gate): - # Unitary = (-j I_2) \otimes Z - _target_unitary = np.array([[-1j, 0, 0, 0], [0, 1j, 0, 0], [0, 0, 1j, 0], [0, 0, 0, -1j]]) +class GateThatAllocatesTwoQubits(cirq.Gate): def _num_qubits_(self): return 2 @@ -126,6 +129,33 @@ def _decompose_(self, qs): yield (cirq.Z)(anc[1]) yield cirq.CX(q1, anc[1]) + @classmethod + def target_unitary(cls) -> np.ndarray: + # Unitary = (-j I_2) \otimes Z + return np.array([[-1j, 0, 0, 0], [0, 1j, 0, 0], [0, 0, 1j, 0], [0, 0, 0, -1j]]) + + +class GateThatDecomposesIntoNGates(cirq.Gate): + def __init__(self, n: int, sub_gate: cirq.Gate, theta: float) -> None: + super().__init__() + self._n = n + self._subgate = sub_gate + self._name = str(sub_gate) + self._theta = theta + + def _num_qubits_(self) -> int: + return self._n + + def _decompose_(self, qs): + ancilla = cirq.NamedQubit.range(self._n, prefix=self._name) + yield self._subgate.on_each(ancilla) + yield (cirq.Z**self._theta).on_each(qs) + yield self._subgate.on_each(ancilla) + + def target_unitary(self) -> np.ndarray: + U = np.array([[1, 0], [0, (-1 + 0j) ** self._theta]]) + return functools.reduce(np.kron, [U] * self._n) + class DecomposableOperation(cirq.Operation): qubits = () @@ -222,6 +252,31 @@ def test_has_unitary(): assert not cirq.has_unitary(FullyImplemented(False)) +@pytest.mark.parametrize('theta', np.linspace(0, 2 * np.pi, 10)) +def test_decompose_gate_that_allocates_qubits(theta: float): + from cirq.protocols.unitary_protocol import _strat_unitary_from_decompose + + gate = GateThatAllocatesAQubit(theta) + np.testing.assert_allclose( + cast(np.ndarray, _strat_unitary_from_decompose(gate)), gate.target_unitary() + ) + np.testing.assert_allclose( + cast(np.ndarray, _strat_unitary_from_decompose(gate(a))), gate.target_unitary() + ) + + +@pytest.mark.parametrize('theta', np.linspace(0, 2 * np.pi, 10)) +@pytest.mark.parametrize('n', [*range(1, 6)]) +def test_recusive_decomposition(n: int, theta: float): + from cirq.protocols.unitary_protocol import _strat_unitary_from_decompose + + g1 = GateThatDecomposesIntoNGates(n, cirq.H, theta) + g2 = GateThatDecomposesIntoNGates(n, g1, theta) + np.testing.assert_allclose( + cast(np.ndarray, _strat_unitary_from_decompose(g2)), g2.target_unitary() + ) + + def test_decompose_and_get_unitary(): from cirq.protocols.unitary_protocol import _strat_unitary_from_decompose @@ -235,21 +290,13 @@ def test_decompose_and_get_unitary(): np.testing.assert_allclose(_strat_unitary_from_decompose(DummyComposite()), np.eye(1)) np.testing.assert_allclose(_strat_unitary_from_decompose(OtherComposite()), m2) - np.testing.assert_allclose( - _strat_unitary_from_decompose(GateThatAllocatesAQubit()), - GateThatAllocatesAQubit._target_unitary, - ) - np.testing.assert_allclose( - _strat_unitary_from_decompose(GateThatAllocatesAQubit().on(a)), - GateThatAllocatesAQubit._target_unitary, - ) np.testing.assert_allclose( _strat_unitary_from_decompose(GateThatAllocatesTwoQubits()), - GateThatAllocatesTwoQubits._target_unitary, + GateThatAllocatesTwoQubits.target_unitary(), ) np.testing.assert_allclose( _strat_unitary_from_decompose(GateThatAllocatesTwoQubits().on(a, b)), - GateThatAllocatesTwoQubits._target_unitary, + GateThatAllocatesTwoQubits.target_unitary(), ) From 4326084190758cd93b9c282ecf7e4b757c7d1f61 Mon Sep 17 00:00:00 2001 From: Noureldin Date: Thu, 1 Jun 2023 20:29:38 +0100 Subject: [PATCH 03/10] fix mypy --- cirq-core/cirq/protocols/apply_unitary_protocol.py | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/cirq-core/cirq/protocols/apply_unitary_protocol.py b/cirq-core/cirq/protocols/apply_unitary_protocol.py index a5182a1332b..4898b1324f1 100644 --- a/cirq-core/cirq/protocols/apply_unitary_protocol.py +++ b/cirq-core/cirq/protocols/apply_unitary_protocol.py @@ -13,7 +13,18 @@ # limitations under the License. """A protocol for implementing high performance unitary left-multiplies.""" import warnings -from typing import Any, cast, Iterable, Optional, Sequence, Tuple, TYPE_CHECKING, TypeVar, Union +from typing import ( + Any, + cast, + Iterable, + Optional, + Sequence, + Tuple, + TYPE_CHECKING, + TypeVar, + Union, + Callable, +) import numpy as np from typing_extensions import Protocol @@ -363,6 +374,7 @@ def apply_unitary( with warnings.catch_warnings(): warnings.filterwarnings(action="error", category=np.ComplexWarning) for strat in strats: + strat = cast(Callable[[Any, ApplyUnitaryArgs], Optional[np.ndarray]], strat) result = strat(unitary_value, args) if result is None: break From 5f3e52d2a5753768dd297a86cc67062c31aede2f Mon Sep 17 00:00:00 2001 From: Noureldin Date: Fri, 2 Jun 2023 20:58:57 +0100 Subject: [PATCH 04/10] refactored tests --- .../cirq/protocols/apply_unitary_protocol.py | 32 +++--- .../protocols/apply_unitary_protocol_test.py | 25 +---- cirq-core/cirq/protocols/unitary_protocol.py | 9 +- .../cirq/protocols/unitary_protocol_test.py | 81 ++------------- cirq-core/cirq/testing/__init__.py | 8 ++ cirq-core/cirq/testing/sample_gates.py | 99 +++++++++++++++++++ 6 files changed, 135 insertions(+), 119 deletions(-) create mode 100644 cirq-core/cirq/testing/sample_gates.py diff --git a/cirq-core/cirq/protocols/apply_unitary_protocol.py b/cirq-core/cirq/protocols/apply_unitary_protocol.py index 4898b1324f1..a285ceefa66 100644 --- a/cirq-core/cirq/protocols/apply_unitary_protocol.py +++ b/cirq-core/cirq/protocols/apply_unitary_protocol.py @@ -13,18 +13,7 @@ # limitations under the License. """A protocol for implementing high performance unitary left-multiplies.""" import warnings -from typing import ( - Any, - cast, - Iterable, - Optional, - Sequence, - Tuple, - TYPE_CHECKING, - TypeVar, - Union, - Callable, -) +from typing import Any, cast, Iterable, Optional, Sequence, Tuple, TYPE_CHECKING, TypeVar, Union import numpy as np from typing_extensions import Protocol @@ -235,6 +224,12 @@ def subspace_index( qid_shape=self.target_tensor.shape, ) + @classmethod + def for_unitary(cls, qid_shapes: Tuple[int, ...]) -> 'ApplyUnitaryArgs': + state = qis.eye_tensor(qid_shapes, dtype=np.complex128) + buffer = np.empty_like(state) + return ApplyUnitaryArgs(state, buffer, range(len(qid_shapes))) + class SupportsConsistentApplyUnitary(Protocol): """An object that can be efficiently left-multiplied into tensors.""" @@ -285,6 +280,10 @@ def _apply_unitary_( """ +def _strat_apply_unitary_from_unitary_(val: Any, args: ApplyUnitaryArgs) -> Optional[np.ndarray]: + return _strat_apply_unitary_from_unitary(val, args, matrix=None) + + def apply_unitary( unitary_value: Any, args: ApplyUnitaryArgs, @@ -357,14 +356,14 @@ def apply_unitary( if len(args.axes) <= 4: strats = [ _strat_apply_unitary_from_apply_unitary, - _strat_apply_unitary_from_unitary, + _strat_apply_unitary_from_unitary_, _strat_apply_unitary_from_decompose, ] else: strats = [ _strat_apply_unitary_from_apply_unitary, _strat_apply_unitary_from_decompose, - _strat_apply_unitary_from_unitary, + _strat_apply_unitary_from_unitary_, ] if not allow_decompose: strats.remove(_strat_apply_unitary_from_decompose) @@ -374,7 +373,6 @@ def apply_unitary( with warnings.catch_warnings(): warnings.filterwarnings(action="error", category=np.ComplexWarning) for strat in strats: - strat = cast(Callable[[Any, ApplyUnitaryArgs], Optional[np.ndarray]], strat) result = strat(unitary_value, args) if result is None: break @@ -473,12 +471,10 @@ def _strat_apply_unitary_from_decompose(val: Any, args: ApplyUnitaryArgs) -> Opt return apply_unitaries(operations, qubits, args, None) ordered_qubits = ancilla + tuple(qubits) all_qid_shapes = qid_shape_protocol.qid_shape(ordered_qubits) - state = qis.eye_tensor(all_qid_shapes, dtype=np.complex128) - buffer = np.empty_like(state) result = apply_unitaries( operations, ordered_qubits, - ApplyUnitaryArgs(state, buffer, range(len(ordered_qubits))), + ApplyUnitaryArgs.for_unitary(qid_shape_protocol.qid_shape(ordered_qubits)), None, ) if result is None or result is NotImplemented: diff --git a/cirq-core/cirq/protocols/apply_unitary_protocol_test.py b/cirq-core/cirq/protocols/apply_unitary_protocol_test.py index 3f52e10ce12..b85cfaee5d7 100644 --- a/cirq-core/cirq/protocols/apply_unitary_protocol_test.py +++ b/cirq-core/cirq/protocols/apply_unitary_protocol_test.py @@ -17,6 +17,7 @@ import cirq from cirq.protocols.apply_unitary_protocol import _incorporate_result_into_target +from cirq import testing def test_apply_unitary_presence_absence(): @@ -719,26 +720,6 @@ def test_cast_to_complex(): cirq.apply_unitary(y0, args) -class NotDecomposableGate(cirq.Gate): - def num_qubits(self): - return 1 - - -class DecomposableGate(cirq.Gate): - def __init__(self, sub_gate: cirq.Gate, allocate_ancilla: bool) -> None: - super().__init__() - self._sub_gate = sub_gate - self._allocate_ancilla = allocate_ancilla - - def num_qubits(self): - return 1 - - def _decompose_(self, qubits): - if self._allocate_ancilla: - yield cirq.Z(cirq.LineQubit(1)) - yield self._sub_gate(qubits[0]) - - def test_strat_apply_unitary_from_decompose(): state = np.eye(2, dtype=np.complex128) args = cirq.ApplyUnitaryArgs( @@ -746,14 +727,14 @@ def test_strat_apply_unitary_from_decompose(): ) np.testing.assert_allclose( cirq.apply_unitaries( - [DecomposableGate(cirq.X, False)(cirq.LineQubit(0))], [cirq.LineQubit(0)], args + [testing.DecomposableGate(cirq.X, False)(cirq.LineQubit(0))], [cirq.LineQubit(0)], args ), [[0, 1], [1, 0]], ) with pytest.raises(TypeError): _ = cirq.apply_unitaries( - [DecomposableGate(NotDecomposableGate(), True)(cirq.LineQubit(0))], + [testing.DecomposableGate(testing.NotDecomposableGate(), True)(cirq.LineQubit(0))], [cirq.LineQubit(0)], args, ) diff --git a/cirq-core/cirq/protocols/unitary_protocol.py b/cirq-core/cirq/protocols/unitary_protocol.py index 9eb0be8e95a..3c5f771b6d5 100644 --- a/cirq-core/cirq/protocols/unitary_protocol.py +++ b/cirq-core/cirq/protocols/unitary_protocol.py @@ -17,7 +17,6 @@ import numpy as np from typing_extensions import Protocol -from cirq import qis from cirq._doc import doc_private from cirq.protocols import qid_shape_protocol from cirq.protocols.apply_unitary_protocol import ApplyUnitaryArgs, apply_unitaries @@ -162,9 +161,7 @@ def _strat_unitary_from_apply_unitary(val: Any) -> Optional[np.ndarray]: return NotImplemented # Apply unitary effect to an identity matrix. - state = qis.eye_tensor(val_qid_shape, dtype=np.complex128) - buffer = np.empty_like(state) - result = method(ApplyUnitaryArgs(state, buffer, range(len(val_qid_shape)))) + result = method(ApplyUnitaryArgs.for_unitary(val_qid_shape)) if result is NotImplemented or result is None: return result @@ -187,10 +184,8 @@ def _strat_unitary_from_decompose(val: Any) -> Optional[np.ndarray]: val_qid_shape = qid_shape_protocol.qid_shape(ancillas) + val_qid_shape # Apply sub-operations' unitary effects to an identity matrix. - state = qis.eye_tensor(val_qid_shape, dtype=np.complex128) - buffer = np.empty_like(state) result = apply_unitaries( - operations, ordered_qubits, ApplyUnitaryArgs(state, buffer, range(len(val_qid_shape))), None + operations, ordered_qubits, ApplyUnitaryArgs.for_unitary(val_qid_shape), None ) # Package result. diff --git a/cirq-core/cirq/protocols/unitary_protocol_test.py b/cirq-core/cirq/protocols/unitary_protocol_test.py index 267b54ed45e..017dbd1d927 100644 --- a/cirq-core/cirq/protocols/unitary_protocol_test.py +++ b/cirq-core/cirq/protocols/unitary_protocol_test.py @@ -11,13 +11,13 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. -from typing import Optional, cast -import functools +from typing import cast, Optional import numpy as np import pytest import cirq +from cirq import testing m0: np.ndarray = np.array([]) # yapf: disable @@ -94,69 +94,6 @@ def _decompose_(self, qubits): yield FullyImplemented(self.unitary_value).on(qubits[0]) -class GateThatAllocatesAQubit(cirq.Gate): - def __init__(self, theta: float) -> None: - super().__init__() - self._theta = theta - - def _num_qubits_(self): - return 1 - - def _decompose_(self, q): - anc = cirq.NamedQubit("anc") - yield cirq.CX(*q, anc) - yield (cirq.Z**self._theta)(anc) - yield cirq.CX(*q, anc) - - def target_unitary(self) -> np.ndarray: - return np.array([[1, 0], [0, (-1 + 0j) ** self._theta]]) - - -class GateThatAllocatesTwoQubits(cirq.Gate): - def _num_qubits_(self): - return 2 - - def _decompose_(self, qs): - q0, q1 = qs - anc = cirq.NamedQubit.range(2, prefix='two_ancillas_') - - yield cirq.X(anc[0]) - yield cirq.CX(q0, anc[0]) - yield (cirq.Y)(anc[0]) - yield cirq.CX(q0, anc[0]) - - yield cirq.CX(q1, anc[1]) - yield (cirq.Z)(anc[1]) - yield cirq.CX(q1, anc[1]) - - @classmethod - def target_unitary(cls) -> np.ndarray: - # Unitary = (-j I_2) \otimes Z - return np.array([[-1j, 0, 0, 0], [0, 1j, 0, 0], [0, 0, 1j, 0], [0, 0, 0, -1j]]) - - -class GateThatDecomposesIntoNGates(cirq.Gate): - def __init__(self, n: int, sub_gate: cirq.Gate, theta: float) -> None: - super().__init__() - self._n = n - self._subgate = sub_gate - self._name = str(sub_gate) - self._theta = theta - - def _num_qubits_(self) -> int: - return self._n - - def _decompose_(self, qs): - ancilla = cirq.NamedQubit.range(self._n, prefix=self._name) - yield self._subgate.on_each(ancilla) - yield (cirq.Z**self._theta).on_each(qs) - yield self._subgate.on_each(ancilla) - - def target_unitary(self) -> np.ndarray: - U = np.array([[1, 0], [0, (-1 + 0j) ** self._theta]]) - return functools.reduce(np.kron, [U] * self._n) - - class DecomposableOperation(cirq.Operation): qubits = () with_qubits = NotImplemented @@ -256,7 +193,7 @@ def test_has_unitary(): def test_decompose_gate_that_allocates_qubits(theta: float): from cirq.protocols.unitary_protocol import _strat_unitary_from_decompose - gate = GateThatAllocatesAQubit(theta) + gate = testing.GateThatAllocatesAQubit(theta) np.testing.assert_allclose( cast(np.ndarray, _strat_unitary_from_decompose(gate)), gate.target_unitary() ) @@ -270,8 +207,8 @@ def test_decompose_gate_that_allocates_qubits(theta: float): def test_recusive_decomposition(n: int, theta: float): from cirq.protocols.unitary_protocol import _strat_unitary_from_decompose - g1 = GateThatDecomposesIntoNGates(n, cirq.H, theta) - g2 = GateThatDecomposesIntoNGates(n, g1, theta) + g1 = testing.GateThatDecomposesIntoNGates(n, cirq.H, theta) + g2 = testing.GateThatDecomposesIntoNGates(n, g1, theta) np.testing.assert_allclose( cast(np.ndarray, _strat_unitary_from_decompose(g2)), g2.target_unitary() ) @@ -291,12 +228,12 @@ def test_decompose_and_get_unitary(): np.testing.assert_allclose(_strat_unitary_from_decompose(OtherComposite()), m2) np.testing.assert_allclose( - _strat_unitary_from_decompose(GateThatAllocatesTwoQubits()), - GateThatAllocatesTwoQubits.target_unitary(), + _strat_unitary_from_decompose(testing.GateThatAllocatesTwoQubits()), + testing.GateThatAllocatesTwoQubits.target_unitary(), ) np.testing.assert_allclose( - _strat_unitary_from_decompose(GateThatAllocatesTwoQubits().on(a, b)), - GateThatAllocatesTwoQubits.target_unitary(), + _strat_unitary_from_decompose(testing.GateThatAllocatesTwoQubits().on(a, b)), + testing.GateThatAllocatesTwoQubits.target_unitary(), ) diff --git a/cirq-core/cirq/testing/__init__.py b/cirq-core/cirq/testing/__init__.py index 7e831b4d480..575153dd195 100644 --- a/cirq-core/cirq/testing/__init__.py +++ b/cirq-core/cirq/testing/__init__.py @@ -107,3 +107,11 @@ ) from cirq.testing.sample_circuits import nonoptimal_toffoli_circuit + +from cirq.testing.sample_gates import ( + DecomposableGate, + NotDecomposableGate, + GateThatAllocatesAQubit, + GateThatAllocatesTwoQubits, + GateThatDecomposesIntoNGates, +) diff --git a/cirq-core/cirq/testing/sample_gates.py b/cirq-core/cirq/testing/sample_gates.py new file mode 100644 index 00000000000..190a26e866e --- /dev/null +++ b/cirq-core/cirq/testing/sample_gates.py @@ -0,0 +1,99 @@ +# Copyright 2023 The Cirq Developers +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import functools +import numpy as np +from cirq import ops + + +class NotDecomposableGate(ops.Gate): + def num_qubits(self): + return 1 + + +class DecomposableGate(ops.Gate): + def __init__(self, sub_gate: ops.Gate, allocate_ancilla: bool) -> None: + super().__init__() + self._sub_gate = sub_gate + self._allocate_ancilla = allocate_ancilla + + def num_qubits(self): + return 1 + + def _decompose_(self, qubits): + if self._allocate_ancilla: + yield ops.Z(ops.NamedQubit('DecomposableGateQubit')) + yield self._sub_gate(qubits[0]) + + +class GateThatAllocatesAQubit(ops.Gate): + def __init__(self, theta: float) -> None: + super().__init__() + self._theta = theta + + def _num_qubits_(self): + return 1 + + def _decompose_(self, q): + anc = ops.NamedQubit("anc") + yield ops.CX(*q, anc) + yield (ops.Z**self._theta)(anc) + yield ops.CX(*q, anc) + + def target_unitary(self) -> np.ndarray: + return np.array([[1, 0], [0, (-1 + 0j) ** self._theta]]) + + +class GateThatAllocatesTwoQubits(ops.Gate): + def _num_qubits_(self): + return 2 + + def _decompose_(self, qs): + q0, q1 = qs + anc = ops.NamedQubit.range(2, prefix='two_ancillas_') + + yield ops.X(anc[0]) + yield ops.CX(q0, anc[0]) + yield (ops.Y)(anc[0]) + yield ops.CX(q0, anc[0]) + + yield ops.CX(q1, anc[1]) + yield (ops.Z)(anc[1]) + yield ops.CX(q1, anc[1]) + + @classmethod + def target_unitary(cls) -> np.ndarray: + # Unitary = (-j I_2) \otimes Z + return np.array([[-1j, 0, 0, 0], [0, 1j, 0, 0], [0, 0, 1j, 0], [0, 0, 0, -1j]]) + + +class GateThatDecomposesIntoNGates(ops.Gate): + def __init__(self, n: int, sub_gate: ops.Gate, theta: float) -> None: + super().__init__() + self._n = n + self._subgate = sub_gate + self._name = str(sub_gate) + self._theta = theta + + def _num_qubits_(self) -> int: + return self._n + + def _decompose_(self, qs): + ancilla = ops.NamedQubit.range(self._n, prefix=self._name) + yield self._subgate.on_each(ancilla) + yield (ops.Z**self._theta).on_each(qs) + yield self._subgate.on_each(ancilla) + + def target_unitary(self) -> np.ndarray: + U = np.array([[1, 0], [0, (-1 + 0j) ** self._theta]]) + return functools.reduce(np.kron, [U] * self._n) From c7fd8f7ff30637a8aab18e8a0fb1baa008f9ea71 Mon Sep 17 00:00:00 2001 From: Noureldin Date: Sat, 3 Jun 2023 12:15:22 +0100 Subject: [PATCH 05/10] addressing comments --- .../cirq/protocols/apply_unitary_protocol.py | 58 +++++++++---------- .../protocols/apply_unitary_protocol_test.py | 25 +++++++- cirq-core/cirq/testing/__init__.py | 2 - cirq-core/cirq/testing/sample_gates.py | 20 ------- 4 files changed, 49 insertions(+), 56 deletions(-) diff --git a/cirq-core/cirq/protocols/apply_unitary_protocol.py b/cirq-core/cirq/protocols/apply_unitary_protocol.py index a285ceefa66..88e6a51fa58 100644 --- a/cirq-core/cirq/protocols/apply_unitary_protocol.py +++ b/cirq-core/cirq/protocols/apply_unitary_protocol.py @@ -133,6 +133,12 @@ def default( state = qis.one_hot(index=(0,) * num_qubits, shape=qid_shape, dtype=np.complex128) return ApplyUnitaryArgs(state, np.empty_like(state), range(num_qubits)) + @classmethod + def for_unitary(cls, qid_shapes: Tuple[int, ...]) -> 'ApplyUnitaryArgs': + state = qis.eye_tensor(qid_shapes, dtype=np.complex128) + buffer = np.empty_like(state) + return ApplyUnitaryArgs(state, buffer, range(len(qid_shapes))) + def with_axes_transposed_to_start(self) -> 'ApplyUnitaryArgs': """Returns a transposed view of the same arguments. @@ -224,12 +230,6 @@ def subspace_index( qid_shape=self.target_tensor.shape, ) - @classmethod - def for_unitary(cls, qid_shapes: Tuple[int, ...]) -> 'ApplyUnitaryArgs': - state = qis.eye_tensor(qid_shapes, dtype=np.complex128) - buffer = np.empty_like(state) - return ApplyUnitaryArgs(state, buffer, range(len(qid_shapes))) - class SupportsConsistentApplyUnitary(Protocol): """An object that can be efficiently left-multiplied into tensors.""" @@ -280,10 +280,6 @@ def _apply_unitary_( """ -def _strat_apply_unitary_from_unitary_(val: Any, args: ApplyUnitaryArgs) -> Optional[np.ndarray]: - return _strat_apply_unitary_from_unitary(val, args, matrix=None) - - def apply_unitary( unitary_value: Any, args: ApplyUnitaryArgs, @@ -356,14 +352,14 @@ def apply_unitary( if len(args.axes) <= 4: strats = [ _strat_apply_unitary_from_apply_unitary, - _strat_apply_unitary_from_unitary_, + _strat_apply_unitary_from_unitary, _strat_apply_unitary_from_decompose, ] else: strats = [ _strat_apply_unitary_from_apply_unitary, _strat_apply_unitary_from_decompose, - _strat_apply_unitary_from_unitary_, + _strat_apply_unitary_from_unitary, ] if not allow_decompose: strats.remove(_strat_apply_unitary_from_decompose) @@ -419,20 +415,7 @@ def _strat_apply_unitary_from_apply_unitary( return _incorporate_result_into_target(args, sub_args, sub_result) -def _strat_apply_unitary_from_unitary( - unitary_value: Any, args: ApplyUnitaryArgs, matrix: Optional[np.ndarray] = None -) -> Optional[np.ndarray]: - if matrix is None: - # Check for magic method. - method = getattr(unitary_value, '_unitary_', None) - if method is None: - return NotImplemented - - # Attempt to get the unitary matrix. - matrix = method() - if matrix is NotImplemented or matrix is None: - return matrix - +def _apply_unitary_from_matrix(matrix: np.ndarray, unitary_value: Any, args: ApplyUnitaryArgs): if args.slices is None: val_qid_shape = qid_shape_protocol.qid_shape(unitary_value, default=(2,) * len(args.axes)) slices = tuple(slice(0, size) for size in val_qid_shape) @@ -461,6 +444,22 @@ def _strat_apply_unitary_from_unitary( return _incorporate_result_into_target(args, sub_args, sub_result) +def _strat_apply_unitary_from_unitary( + unitary_value: Any, args: ApplyUnitaryArgs +) -> Optional[np.ndarray]: + # Check for magic method. + method = getattr(unitary_value, '_unitary_', None) + if method is None: + return NotImplemented + + # Attempt to get the unitary matrix. + matrix = method() + if matrix is NotImplemented or matrix is None: + return matrix + + return _apply_unitary_from_matrix(matrix, unitary_value, args) + + def _strat_apply_unitary_from_decompose(val: Any, args: ApplyUnitaryArgs) -> Optional[np.ndarray]: operations, qubits, _ = _try_decompose_into_operations_and_qubits(val) if operations is None: @@ -472,10 +471,7 @@ def _strat_apply_unitary_from_decompose(val: Any, args: ApplyUnitaryArgs) -> Opt ordered_qubits = ancilla + tuple(qubits) all_qid_shapes = qid_shape_protocol.qid_shape(ordered_qubits) result = apply_unitaries( - operations, - ordered_qubits, - ApplyUnitaryArgs.for_unitary(qid_shape_protocol.qid_shape(ordered_qubits)), - None, + operations, ordered_qubits, ApplyUnitaryArgs.for_unitary(all_qid_shapes), None ) if result is None or result is NotImplemented: return result @@ -483,7 +479,7 @@ def _strat_apply_unitary_from_decompose(val: Any, args: ApplyUnitaryArgs) -> Opt val_qid_shape = qid_shape_protocol.qid_shape(qubits) state_vec_length = np.prod(val_qid_shape, dtype=np.int64) result = result[:state_vec_length, :state_vec_length] - return _strat_apply_unitary_from_unitary(val, args, matrix=result) + return _apply_unitary_from_matrix(result, val, args) def apply_unitaries( diff --git a/cirq-core/cirq/protocols/apply_unitary_protocol_test.py b/cirq-core/cirq/protocols/apply_unitary_protocol_test.py index b85cfaee5d7..b22f2bc5c65 100644 --- a/cirq-core/cirq/protocols/apply_unitary_protocol_test.py +++ b/cirq-core/cirq/protocols/apply_unitary_protocol_test.py @@ -17,7 +17,6 @@ import cirq from cirq.protocols.apply_unitary_protocol import _incorporate_result_into_target -from cirq import testing def test_apply_unitary_presence_absence(): @@ -720,6 +719,26 @@ def test_cast_to_complex(): cirq.apply_unitary(y0, args) +class NotDecomposableGate(cirq.Gate): + def num_qubits(self): + return 1 + + +class DecomposableGate(cirq.Gate): + def __init__(self, sub_gate: cirq.Gate, allocate_ancilla: bool) -> None: + super().__init__() + self._sub_gate = sub_gate + self._allocate_ancilla = allocate_ancilla + + def num_qubits(self): + return 1 + + def _decompose_(self, qubits): + if self._allocate_ancilla: + yield cirq.Z(cirq.NamedQubit('DecomposableGateQubit')) + yield self._sub_gate(qubits[0]) + + def test_strat_apply_unitary_from_decompose(): state = np.eye(2, dtype=np.complex128) args = cirq.ApplyUnitaryArgs( @@ -727,14 +746,14 @@ def test_strat_apply_unitary_from_decompose(): ) np.testing.assert_allclose( cirq.apply_unitaries( - [testing.DecomposableGate(cirq.X, False)(cirq.LineQubit(0))], [cirq.LineQubit(0)], args + [DecomposableGate(cirq.X, False)(cirq.LineQubit(0))], [cirq.LineQubit(0)], args ), [[0, 1], [1, 0]], ) with pytest.raises(TypeError): _ = cirq.apply_unitaries( - [testing.DecomposableGate(testing.NotDecomposableGate(), True)(cirq.LineQubit(0))], + [DecomposableGate(NotDecomposableGate(), True)(cirq.LineQubit(0))], [cirq.LineQubit(0)], args, ) diff --git a/cirq-core/cirq/testing/__init__.py b/cirq-core/cirq/testing/__init__.py index 575153dd195..dc8bc58c90e 100644 --- a/cirq-core/cirq/testing/__init__.py +++ b/cirq-core/cirq/testing/__init__.py @@ -109,8 +109,6 @@ from cirq.testing.sample_circuits import nonoptimal_toffoli_circuit from cirq.testing.sample_gates import ( - DecomposableGate, - NotDecomposableGate, GateThatAllocatesAQubit, GateThatAllocatesTwoQubits, GateThatDecomposesIntoNGates, diff --git a/cirq-core/cirq/testing/sample_gates.py b/cirq-core/cirq/testing/sample_gates.py index 190a26e866e..4d6a55fa830 100644 --- a/cirq-core/cirq/testing/sample_gates.py +++ b/cirq-core/cirq/testing/sample_gates.py @@ -16,26 +16,6 @@ from cirq import ops -class NotDecomposableGate(ops.Gate): - def num_qubits(self): - return 1 - - -class DecomposableGate(ops.Gate): - def __init__(self, sub_gate: ops.Gate, allocate_ancilla: bool) -> None: - super().__init__() - self._sub_gate = sub_gate - self._allocate_ancilla = allocate_ancilla - - def num_qubits(self): - return 1 - - def _decompose_(self, qubits): - if self._allocate_ancilla: - yield ops.Z(ops.NamedQubit('DecomposableGateQubit')) - yield self._sub_gate(qubits[0]) - - class GateThatAllocatesAQubit(ops.Gate): def __init__(self, theta: float) -> None: super().__init__() From 88f7f2d8e5599454491a5283726feb3b78ab2f68 Mon Sep 17 00:00:00 2001 From: Noureldin Date: Sat, 3 Jun 2023 15:21:03 +0100 Subject: [PATCH 06/10] added sample_gates_test.py --- cirq-core/cirq/testing/sample_gates.py | 14 +++-- cirq-core/cirq/testing/sample_gates_test.py | 58 +++++++++++++++++++++ 2 files changed, 68 insertions(+), 4 deletions(-) create mode 100644 cirq-core/cirq/testing/sample_gates_test.py diff --git a/cirq-core/cirq/testing/sample_gates.py b/cirq-core/cirq/testing/sample_gates.py index 4d6a55fa830..9feabc16453 100644 --- a/cirq-core/cirq/testing/sample_gates.py +++ b/cirq-core/cirq/testing/sample_gates.py @@ -17,6 +17,8 @@ class GateThatAllocatesAQubit(ops.Gate): + r"""A gate that applies $Z^\theta$ indirectly through a clean ancilla.""" + def __init__(self, theta: float) -> None: super().__init__() self._theta = theta @@ -35,6 +37,8 @@ def target_unitary(self) -> np.ndarray: class GateThatAllocatesTwoQubits(ops.Gate): + r"""A gate that applies $-j Z \otimes Z$ indirectly through two ancillas.""" + def _num_qubits_(self): return 2 @@ -53,16 +57,18 @@ def _decompose_(self, qs): @classmethod def target_unitary(cls) -> np.ndarray: - # Unitary = (-j I_2) \otimes Z + # Unitary = -j Z \otimes Z return np.array([[-1j, 0, 0, 0], [0, 1j, 0, 0], [0, 0, 1j, 0], [0, 0, 0, -1j]]) class GateThatDecomposesIntoNGates(ops.Gate): - def __init__(self, n: int, sub_gate: ops.Gate, theta: float) -> None: + r"""Applies $(Z^\theta)^{\otimes_n}$ on work qubits and `subgate` on $n$ borrowable ancillas.""" + + def __init__(self, n: int, subgate: ops.Gate, theta: float) -> None: super().__init__() self._n = n - self._subgate = sub_gate - self._name = str(sub_gate) + self._subgate = subgate + self._name = str(subgate) self._theta = theta def _num_qubits_(self) -> int: diff --git a/cirq-core/cirq/testing/sample_gates_test.py b/cirq-core/cirq/testing/sample_gates_test.py new file mode 100644 index 00000000000..90cc0928b1a --- /dev/null +++ b/cirq-core/cirq/testing/sample_gates_test.py @@ -0,0 +1,58 @@ +# Copyright 2023 The Cirq Developers +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import functools +import pytest + +import numpy as np +from cirq.testing import sample_gates +from cirq import protocols, ops + + +@pytest.mark.parametrize('theta', np.linspace(0, 2 * np.pi, 20)) +def test_GateThatAllocatesAQubit(theta: float): + g = sample_gates.GateThatAllocatesAQubit(theta) + + want = np.array([[1, 0], [0, (-1 + 0j) ** theta]], dtype=np.complex128) + # test unitary + np.testing.assert_allclose(g.target_unitary(), want) + + # test decomposition + np.testing.assert_allclose(protocols.unitary(g), g.target_unitary()) + + +def test_GateThatAllocatesTwoQubits(): + g = sample_gates.GateThatAllocatesTwoQubits() + + Z = np.array([[1, 0], [0, -1]]) + want = -1j * np.kron(Z, Z) + # test unitary + np.testing.assert_allclose(g.target_unitary(), want) + + # test decomposition + np.testing.assert_allclose(protocols.unitary(g), g.target_unitary()) + + +@pytest.mark.parametrize('n', [*range(1, 6)]) +@pytest.mark.parametrize('subgate', [ops.Z, ops.X, ops.Y, ops.T]) +@pytest.mark.parametrize('theta', np.linspace(0, 2 * np.pi, 5)) +def test_GateThatDecomposesIntoNGates(n: int, subgate: ops.Gate, theta: float): + g = sample_gates.GateThatDecomposesIntoNGates(n, subgate, theta) + + U = np.array([[1, 0], [0, (-1 + 0j) ** theta]], dtype=np.complex128) + want = functools.reduce(np.kron, [U] * n) + # test unitary + np.testing.assert_allclose(g.target_unitary(), want) + + # test decomposition + np.testing.assert_allclose(protocols.unitary(g), g.target_unitary()) From 14dd571d64d54b6aca18bb43d9871f4db6c976c2 Mon Sep 17 00:00:00 2001 From: Tanuj Khattar Date: Sun, 4 Jun 2023 22:17:49 -0700 Subject: [PATCH 07/10] Improved sample_gates.py implementation and unitary_protocol tests. Also added docstrings --- .../cirq/protocols/apply_unitary_protocol.py | 31 ++++- cirq-core/cirq/protocols/unitary_protocol.py | 4 +- .../cirq/protocols/unitary_protocol_test.py | 54 ++++----- cirq-core/cirq/testing/__init__.py | 6 +- cirq-core/cirq/testing/sample_gates.py | 108 +++++++++--------- cirq-core/cirq/testing/sample_gates_test.py | 77 +++++++------ 6 files changed, 147 insertions(+), 133 deletions(-) diff --git a/cirq-core/cirq/protocols/apply_unitary_protocol.py b/cirq-core/cirq/protocols/apply_unitary_protocol.py index 88e6a51fa58..e3dc092dca1 100644 --- a/cirq-core/cirq/protocols/apply_unitary_protocol.py +++ b/cirq-core/cirq/protocols/apply_unitary_protocol.py @@ -134,10 +134,31 @@ def default( return ApplyUnitaryArgs(state, np.empty_like(state), range(num_qubits)) @classmethod - def for_unitary(cls, qid_shapes: Tuple[int, ...]) -> 'ApplyUnitaryArgs': - state = qis.eye_tensor(qid_shapes, dtype=np.complex128) - buffer = np.empty_like(state) - return ApplyUnitaryArgs(state, buffer, range(len(qid_shapes))) + def for_unitary( + cls, num_qubits: Optional[int] = None, *, qid_shape: Optional[Tuple[int, ...]] = None + ) -> 'ApplyUnitaryArgs': + """A default instance corresponding to an identity matrix. + + Specify exactly one argument. + + Args: + num_qubits: The number of qubits to make space for in the state. + qid_shape: A tuple representing the number of quantum levels of each + qubit the identity matrix applies to. `qid_shape` is (2, 2, 2) for + a three-qubit identity operation tensor. + + Raises: + TypeError: If exactly neither `num_qubits` or `qid_shape` is provided or + both are provided. + """ + if (num_qubits is None) == (qid_shape is None): + raise TypeError('Specify exactly one of num_qubits or qid_shape.') + if num_qubits is not None: + qid_shape = (2,) * num_qubits + qid_shape = cast(Tuple[int, ...], qid_shape) # Satisfy mypy + num_qubits = len(qid_shape) + state = qis.eye_tensor(qid_shape, dtype=np.complex128) + return ApplyUnitaryArgs(state, np.empty_like(state), range(num_qubits)) def with_axes_transposed_to_start(self) -> 'ApplyUnitaryArgs': """Returns a transposed view of the same arguments. @@ -471,7 +492,7 @@ def _strat_apply_unitary_from_decompose(val: Any, args: ApplyUnitaryArgs) -> Opt ordered_qubits = ancilla + tuple(qubits) all_qid_shapes = qid_shape_protocol.qid_shape(ordered_qubits) result = apply_unitaries( - operations, ordered_qubits, ApplyUnitaryArgs.for_unitary(all_qid_shapes), None + operations, ordered_qubits, ApplyUnitaryArgs.for_unitary(qid_shape=all_qid_shapes), None ) if result is None or result is NotImplemented: return result diff --git a/cirq-core/cirq/protocols/unitary_protocol.py b/cirq-core/cirq/protocols/unitary_protocol.py index 3c5f771b6d5..4882dd96022 100644 --- a/cirq-core/cirq/protocols/unitary_protocol.py +++ b/cirq-core/cirq/protocols/unitary_protocol.py @@ -161,7 +161,7 @@ def _strat_unitary_from_apply_unitary(val: Any) -> Optional[np.ndarray]: return NotImplemented # Apply unitary effect to an identity matrix. - result = method(ApplyUnitaryArgs.for_unitary(val_qid_shape)) + result = method(ApplyUnitaryArgs.for_unitary(qid_shape=val_qid_shape)) if result is NotImplemented or result is None: return result @@ -185,7 +185,7 @@ def _strat_unitary_from_decompose(val: Any) -> Optional[np.ndarray]: # Apply sub-operations' unitary effects to an identity matrix. result = apply_unitaries( - operations, ordered_qubits, ApplyUnitaryArgs.for_unitary(val_qid_shape), None + operations, ordered_qubits, ApplyUnitaryArgs.for_unitary(qid_shape=val_qid_shape), None ) # Package result. diff --git a/cirq-core/cirq/protocols/unitary_protocol_test.py b/cirq-core/cirq/protocols/unitary_protocol_test.py index 017dbd1d927..2bbcd97c9ad 100644 --- a/cirq-core/cirq/protocols/unitary_protocol_test.py +++ b/cirq-core/cirq/protocols/unitary_protocol_test.py @@ -189,29 +189,40 @@ def test_has_unitary(): assert not cirq.has_unitary(FullyImplemented(False)) -@pytest.mark.parametrize('theta', np.linspace(0, 2 * np.pi, 10)) -def test_decompose_gate_that_allocates_qubits(theta: float): +def _test_gate_that_allocates_qubits(gate): from cirq.protocols.unitary_protocol import _strat_unitary_from_decompose - gate = testing.GateThatAllocatesAQubit(theta) - np.testing.assert_allclose( - cast(np.ndarray, _strat_unitary_from_decompose(gate)), gate.target_unitary() - ) - np.testing.assert_allclose( - cast(np.ndarray, _strat_unitary_from_decompose(gate(a))), gate.target_unitary() - ) + op = gate.on(*cirq.LineQubit.range(cirq.num_qubits(gate))) + moment = cirq.Moment(op) + circuit = cirq.FrozenCircuit(op) + circuit_op = cirq.CircuitOperation(circuit) + for val in [gate, op, moment, circuit, circuit_op]: + unitary_from_strat = _strat_unitary_from_decompose(val) + assert unitary_from_strat is not None + np.testing.assert_allclose(unitary_from_strat, gate.narrow_unitary()) @pytest.mark.parametrize('theta', np.linspace(0, 2 * np.pi, 10)) -@pytest.mark.parametrize('n', [*range(1, 6)]) -def test_recusive_decomposition(n: int, theta: float): - from cirq.protocols.unitary_protocol import _strat_unitary_from_decompose +@pytest.mark.parametrize('phase_state', [0, 1]) +@pytest.mark.parametrize('target_bitsize', [1, 2, 3]) +@pytest.mark.parametrize('ancilla_bitsize', [1, 4]) +def test_decompose_gate_that_allocates_clean_qubits( + theta: float, phase_state: int, target_bitsize: int, ancilla_bitsize: int +): - g1 = testing.GateThatDecomposesIntoNGates(n, cirq.H, theta) - g2 = testing.GateThatDecomposesIntoNGates(n, g1, theta) - np.testing.assert_allclose( - cast(np.ndarray, _strat_unitary_from_decompose(g2)), g2.target_unitary() - ) + gate = testing.PhaseUsingCleanAncilla(theta, phase_state, target_bitsize, ancilla_bitsize) + _test_gate_that_allocates_qubits(gate) + + +@pytest.mark.parametrize('phase_state', [0, 1]) +@pytest.mark.parametrize('target_bitsize', [1, 2, 3]) +@pytest.mark.parametrize('ancilla_bitsize', [1, 4]) +def test_decompose_gate_that_allocates_dirty_qubits( + phase_state: int, target_bitsize: int, ancilla_bitsize: int +): + + gate = testing.PhaseUsingDirtyAncilla(phase_state, target_bitsize, ancilla_bitsize) + _test_gate_that_allocates_qubits(gate) def test_decompose_and_get_unitary(): @@ -227,15 +238,6 @@ def test_decompose_and_get_unitary(): np.testing.assert_allclose(_strat_unitary_from_decompose(DummyComposite()), np.eye(1)) np.testing.assert_allclose(_strat_unitary_from_decompose(OtherComposite()), m2) - np.testing.assert_allclose( - _strat_unitary_from_decompose(testing.GateThatAllocatesTwoQubits()), - testing.GateThatAllocatesTwoQubits.target_unitary(), - ) - np.testing.assert_allclose( - _strat_unitary_from_decompose(testing.GateThatAllocatesTwoQubits().on(a, b)), - testing.GateThatAllocatesTwoQubits.target_unitary(), - ) - def test_decomposed_has_unitary(): # Gates diff --git a/cirq-core/cirq/testing/__init__.py b/cirq-core/cirq/testing/__init__.py index dc8bc58c90e..1c7ffaba28e 100644 --- a/cirq-core/cirq/testing/__init__.py +++ b/cirq-core/cirq/testing/__init__.py @@ -108,8 +108,4 @@ from cirq.testing.sample_circuits import nonoptimal_toffoli_circuit -from cirq.testing.sample_gates import ( - GateThatAllocatesAQubit, - GateThatAllocatesTwoQubits, - GateThatDecomposesIntoNGates, -) +from cirq.testing.sample_gates import PhaseUsingCleanAncilla, PhaseUsingDirtyAncilla diff --git a/cirq-core/cirq/testing/sample_gates.py b/cirq-core/cirq/testing/sample_gates.py index 9feabc16453..c4bb2c9b95f 100644 --- a/cirq-core/cirq/testing/sample_gates.py +++ b/cirq-core/cirq/testing/sample_gates.py @@ -11,75 +11,69 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. -import functools -import numpy as np -from cirq import ops - - -class GateThatAllocatesAQubit(ops.Gate): - r"""A gate that applies $Z^\theta$ indirectly through a clean ancilla.""" +import dataclasses - def __init__(self, theta: float) -> None: - super().__init__() - self._theta = theta +import cirq +import numpy as np +from cirq import ops, qis - def _num_qubits_(self): - return 1 - def _decompose_(self, q): - anc = ops.NamedQubit("anc") - yield ops.CX(*q, anc) - yield (ops.Z**self._theta)(anc) - yield ops.CX(*q, anc) +def _matrix_for_phasing_state(num_qubits, phase_state, phase): + matrix = qis.eye_tensor((2,) * num_qubits, dtype=np.complex128) + matrix = matrix.reshape((2**num_qubits, 2**num_qubits)) + matrix[phase_state, phase_state] = phase + print(num_qubits, phase_state, phase) + print(matrix) + return matrix - def target_unitary(self) -> np.ndarray: - return np.array([[1, 0], [0, (-1 + 0j) ** self._theta]]) +@dataclasses.dataclass(frozen=True) +class PhaseUsingCleanAncilla(ops.Gate): + r"""Phases the state $|phase_state>$ by $\exp(1j * \pi * \theta)$ using one clean ancilla.""" -class GateThatAllocatesTwoQubits(ops.Gate): - r"""A gate that applies $-j Z \otimes Z$ indirectly through two ancillas.""" + theta: float + phase_state: int = 1 + target_bitsize: int = 1 + ancilla_bitsize: int = 1 def _num_qubits_(self): - return 2 + return self.target_bitsize - def _decompose_(self, qs): - q0, q1 = qs - anc = ops.NamedQubit.range(2, prefix='two_ancillas_') + def _decompose_(self, qubits): + anc = ops.NamedQubit.range(self.ancilla_bitsize, prefix="anc") + cv = [int(x) for x in f'{self.phase_state:0{self.target_bitsize}b}'] + cnot_ladder = [cirq.CNOT(anc[i - 1], anc[i]) for i in range(1, self.ancilla_bitsize)] - yield ops.X(anc[0]) - yield ops.CX(q0, anc[0]) - yield (ops.Y)(anc[0]) - yield ops.CX(q0, anc[0]) + yield ops.X(anc[0]).controlled_by(*qubits, control_values=cv) + yield [cnot_ladder, ops.Z(anc[-1]) ** self.theta, reversed(cnot_ladder)] + yield ops.X(anc[0]).controlled_by(*qubits, control_values=cv) - yield ops.CX(q1, anc[1]) - yield (ops.Z)(anc[1]) - yield ops.CX(q1, anc[1]) + def narrow_unitary(self) -> np.ndarray: + """Narrowed unitary corresponding to the unitary effect applied on target qubits.""" + phase = np.exp(1j * np.pi * self.theta) + return _matrix_for_phasing_state(self.target_bitsize, self.phase_state, phase) - @classmethod - def target_unitary(cls) -> np.ndarray: - # Unitary = -j Z \otimes Z - return np.array([[-1j, 0, 0, 0], [0, 1j, 0, 0], [0, 0, 1j, 0], [0, 0, 0, -1j]]) +@dataclasses.dataclass(frozen=True) +class PhaseUsingDirtyAncilla(ops.Gate): + r"""Phases the state $|phase_state>$ by -1 using one dirty ancilla.""" -class GateThatDecomposesIntoNGates(ops.Gate): - r"""Applies $(Z^\theta)^{\otimes_n}$ on work qubits and `subgate` on $n$ borrowable ancillas.""" + phase_state: int = 1 + target_bitsize: int = 1 + ancilla_bitsize: int = 1 - def __init__(self, n: int, subgate: ops.Gate, theta: float) -> None: - super().__init__() - self._n = n - self._subgate = subgate - self._name = str(subgate) - self._theta = theta - - def _num_qubits_(self) -> int: - return self._n - - def _decompose_(self, qs): - ancilla = ops.NamedQubit.range(self._n, prefix=self._name) - yield self._subgate.on_each(ancilla) - yield (ops.Z**self._theta).on_each(qs) - yield self._subgate.on_each(ancilla) - - def target_unitary(self) -> np.ndarray: - U = np.array([[1, 0], [0, (-1 + 0j) ** self._theta]]) - return functools.reduce(np.kron, [U] * self._n) + def _num_qubits_(self): + return self.target_bitsize + + def _decompose_(self, qubits): + anc = ops.NamedQubit.range(self.ancilla_bitsize, prefix="anc") + cv = [int(x) for x in f'{self.phase_state:0{self.target_bitsize}b}'] + cnot_ladder = [cirq.CNOT(anc[i - 1], anc[i]) for i in range(1, self.ancilla_bitsize)] + yield ops.X(anc[0]).controlled_by(*qubits, control_values=cv) + yield [cnot_ladder, ops.Z(anc[-1]), reversed(cnot_ladder)] + yield ops.X(anc[0]).controlled_by(*qubits, control_values=cv) + yield [cnot_ladder, ops.Z(anc[-1]), reversed(cnot_ladder)] + + def narrow_unitary(self) -> np.ndarray: + """Narrowed unitary corresponding to the unitary effect applied on target qubits.""" + return _matrix_for_phasing_state(self.target_bitsize, self.phase_state, -1) diff --git a/cirq-core/cirq/testing/sample_gates_test.py b/cirq-core/cirq/testing/sample_gates_test.py index 90cc0928b1a..848928c0e33 100644 --- a/cirq-core/cirq/testing/sample_gates_test.py +++ b/cirq-core/cirq/testing/sample_gates_test.py @@ -11,48 +11,49 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. -import functools import pytest import numpy as np from cirq.testing import sample_gates -from cirq import protocols, ops +import cirq @pytest.mark.parametrize('theta', np.linspace(0, 2 * np.pi, 20)) -def test_GateThatAllocatesAQubit(theta: float): - g = sample_gates.GateThatAllocatesAQubit(theta) - - want = np.array([[1, 0], [0, (-1 + 0j) ** theta]], dtype=np.complex128) - # test unitary - np.testing.assert_allclose(g.target_unitary(), want) - - # test decomposition - np.testing.assert_allclose(protocols.unitary(g), g.target_unitary()) - - -def test_GateThatAllocatesTwoQubits(): - g = sample_gates.GateThatAllocatesTwoQubits() - - Z = np.array([[1, 0], [0, -1]]) - want = -1j * np.kron(Z, Z) - # test unitary - np.testing.assert_allclose(g.target_unitary(), want) - - # test decomposition - np.testing.assert_allclose(protocols.unitary(g), g.target_unitary()) - - -@pytest.mark.parametrize('n', [*range(1, 6)]) -@pytest.mark.parametrize('subgate', [ops.Z, ops.X, ops.Y, ops.T]) -@pytest.mark.parametrize('theta', np.linspace(0, 2 * np.pi, 5)) -def test_GateThatDecomposesIntoNGates(n: int, subgate: ops.Gate, theta: float): - g = sample_gates.GateThatDecomposesIntoNGates(n, subgate, theta) - - U = np.array([[1, 0], [0, (-1 + 0j) ** theta]], dtype=np.complex128) - want = functools.reduce(np.kron, [U] * n) - # test unitary - np.testing.assert_allclose(g.target_unitary(), want) - - # test decomposition - np.testing.assert_allclose(protocols.unitary(g), g.target_unitary()) +def test_phase_using_clean_ancilla(theta: float): + g = sample_gates.PhaseUsingCleanAncilla(theta) + q = cirq.LineQubit(0) + qubit_order = cirq.QubitOrder.explicit([q], fallback=cirq.QubitOrder.DEFAULT) + decomposed_unitary = cirq.Circuit(cirq.decompose_once(g.on(q))).unitary(qubit_order=qubit_order) + phase = np.exp(1j * np.pi * theta) + np.testing.assert_allclose(g.narrow_unitary(), np.array([[1, 0], [0, phase]])) + np.testing.assert_allclose( + decomposed_unitary, + # fmt: off + np.array( + [ + [1 , 0 , 0 , 0], + [0 , phase, 0 , 0], + [0 , 0 , phase, 0], + [0 , 0 , 0 , 1], + ] + ), + # fmt: on + ) + + +@pytest.mark.parametrize( + 'target_bitsize, phase_state', [(1, 0), (1, 1), (2, 0), (2, 1), (2, 2), (2, 3)] +) +@pytest.mark.parametrize('ancilla_bitsize', [1, 4]) +def test_phase_using_dirty_ancilla(target_bitsize, phase_state, ancilla_bitsize): + g = sample_gates.PhaseUsingDirtyAncilla(phase_state, target_bitsize, ancilla_bitsize) + q = cirq.LineQubit.range(target_bitsize) + qubit_order = cirq.QubitOrder.explicit(q, fallback=cirq.QubitOrder.DEFAULT) + decomposed_circuit = cirq.Circuit(cirq.decompose_once(g.on(*q))) + decomposed_unitary = decomposed_circuit.unitary(qubit_order=qubit_order) + phase_matrix = np.eye(2**target_bitsize) + phase_matrix[phase_state, phase_state] = -1 + np.testing.assert_allclose(g.narrow_unitary(), phase_matrix) + np.testing.assert_allclose( + decomposed_unitary, np.kron(phase_matrix, np.eye(2**ancilla_bitsize)), atol=1e-5 + ) From d32b22523e9de7b5ebc129f772713732998aafc5 Mon Sep 17 00:00:00 2001 From: Noureldin Date: Mon, 5 Jun 2023 10:14:04 +0100 Subject: [PATCH 08/10] fixed lint --- cirq-core/cirq/protocols/unitary_protocol_test.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cirq-core/cirq/protocols/unitary_protocol_test.py b/cirq-core/cirq/protocols/unitary_protocol_test.py index 2bbcd97c9ad..5d972c082ce 100644 --- a/cirq-core/cirq/protocols/unitary_protocol_test.py +++ b/cirq-core/cirq/protocols/unitary_protocol_test.py @@ -11,7 +11,7 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. -from typing import cast, Optional +from typing import Optional import numpy as np import pytest From ff03e6c247180f87e1860f3c8a3c161f7b01b1eb Mon Sep 17 00:00:00 2001 From: Noureldin Date: Mon, 5 Jun 2023 10:28:01 +0100 Subject: [PATCH 09/10] retrigger checks From 7617bbd84884ed3a512b237840f8c197092aeaca Mon Sep 17 00:00:00 2001 From: Noureldin Date: Mon, 5 Jun 2023 10:58:10 +0100 Subject: [PATCH 10/10] fix coverage --- .../cirq/protocols/apply_unitary_protocol_test.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/cirq-core/cirq/protocols/apply_unitary_protocol_test.py b/cirq-core/cirq/protocols/apply_unitary_protocol_test.py index b22f2bc5c65..1473d6fc117 100644 --- a/cirq-core/cirq/protocols/apply_unitary_protocol_test.py +++ b/cirq-core/cirq/protocols/apply_unitary_protocol_test.py @@ -757,3 +757,13 @@ def test_strat_apply_unitary_from_decompose(): [cirq.LineQubit(0)], args, ) + + +def test_unitary_construction(): + with pytest.raises(TypeError): + _ = cirq.ApplyUnitaryArgs.for_unitary() + + np.testing.assert_allclose( + cirq.ApplyUnitaryArgs.for_unitary(num_qubits=3).target_tensor, + cirq.eye_tensor((2,) * 3, dtype=np.complex128), + )