diff --git a/.github/CHANGELOG.md b/.github/CHANGELOG.md index e45536d8..3b6732ed 100644 --- a/.github/CHANGELOG.md +++ b/.github/CHANGELOG.md @@ -2,6 +2,8 @@ ### New features since last release +* Quantum gate models have been added to the `jet` Python package. [(#16)](https://github.com/XanaduAI/jet/pull/16) + * Python bindings are now available for the `TaskBasedCpuContractor` class. [(#19)](https://github.com/XanaduAI/jet/pull/19) * Python bindings now include a factory method which accepts a `dtype` parameter. [(#18)](https://github.com/XanaduAI/jet/pull/18) @@ -22,7 +24,7 @@ * Tensor transposes are now significantly faster when all the dimensions are powers of two. [(#12)](https://github.com/XanaduAI/jet/pull/12) -* Use camel case for type aliases [(#17)](https://github.com/XanaduAI/jet/pull/17) +* Use camel case for type aliases. [(#17)](https://github.com/XanaduAI/jet/pull/17) * Exceptions are now favoured in place of `std::terminate` with `Exception` being the new base type for all exceptions thrown by Jet. [(#3)](https://github.com/XanaduAI/jet/pull/3) @@ -52,7 +54,7 @@ This release contains contributions from (in alphabetical order): -[Mikhail Andrenkov](https://github.com/Mandrenkov), [Jack Brown](https://github.com/brownj85), [Theodor Isacsson](https://github.com/thisac), [Lee J. O'Riordan](https://github.com/mlxd), [Trevor Vincent](https://github.com/trevor-vincent). +[Mikhail Andrenkov](https://github.com/Mandrenkov), [Jack Brown](https://github.com/brownj85), [Theodor Isacsson](https://github.com/thisac), [Josh Izaac](https://github.com/josh146), [Lee J. O'Riordan](https://github.com/mlxd), [Antal Száva](https://github.com/antalszava), [Trevor Vincent](https://github.com/trevor-vincent). ## Release 0.1.0 (current release) diff --git a/python/Makefile b/python/Makefile index 8a5894d1..2c6a8130 100644 --- a/python/Makefile +++ b/python/Makefile @@ -33,11 +33,11 @@ setup: $(.VENV_DIR)/touch .PHONY: format format: $(.VENV_DIR)/requirements.txt.touch ifdef check - $(.VENV_BIN)/black -l 100 --check tests - $(.VENV_BIN)/isort --profile black --check-only tests + $(.VENV_BIN)/black -l 100 --check jet tests + $(.VENV_BIN)/isort --profile black --check-only jet tests else - $(.VENV_BIN)/black -l 100 tests - $(.VENV_BIN)/isort --profile black tests + $(.VENV_BIN)/black -l 100 jet tests + $(.VENV_BIN)/isort --profile black jet tests endif diff --git a/python/jet/__init__.py b/python/jet/__init__.py index 7573d449..d98ec82b 100644 --- a/python/jet/__init__.py +++ b/python/jet/__init__.py @@ -1,83 +1,9 @@ -from typing import Union - -import numpy as np - # The existence of a Python binding is proof of its intention to be exposed. from .bindings import * - -def TaskBasedCpuContractor( - *args, **kwargs -) -> Union[TaskBasedCpuContractorC64, TaskBasedCpuContractorC128]: - """Constructs a task-based CPU contractor with the specified data type. If a - `dtype` keyword argument is not provided, a TaskBasedCpuContractorC128 - instance will be returned. - """ - dtype = kwargs.pop("dtype", "complex128") - if np.dtype(dtype) == np.complex64: - return TaskBasedCpuContractorC64(*args, **kwargs) - elif np.dtype(dtype) == np.complex128: - return TaskBasedCpuContractorC128(*args, **kwargs) - else: - raise TypeError(f"Data type '{dtype}' is not supported.") - - -def Tensor(*args, **kwargs) -> Union[TensorC64, TensorC128]: - """Constructs a tensor with the specified data type. If a `dtype` keyword - argument is not provided, a TensorC128 instance will be returned. - """ - dtype = kwargs.pop("dtype", "complex128") - if np.dtype(dtype) == np.complex64: - return TensorC64(*args, **kwargs) - elif np.dtype(dtype) == np.complex128: - return TensorC128(*args, **kwargs) - else: - raise TypeError(f"Data type '{dtype}' is not supported.") - - -def TensorNetwork(*args, **kwargs) -> Union[TensorNetworkC64, TensorNetworkC128]: - """Constructs a tensor network with the specified data type. If a `dtype` - keyword argument is not provided, a TensorNetworkC128 instance will be - returned. - """ - dtype = kwargs.pop("dtype", "complex128") - if np.dtype(dtype) == np.complex64: - return TensorNetworkC64(*args, **kwargs) - elif np.dtype(dtype) == np.complex128: - return TensorNetworkC128(*args, **kwargs) - else: - raise TypeError(f"Data type '{dtype}' is not supported.") - - -def TensorNetworkFile(*args, **kwargs) -> Union[TensorNetworkFileC64, TensorNetworkFileC128]: - """Constructs a tensor network file with the specified data type. If a - `dtype` keyword argument is not provided, a TensorNetworkFileC128 instance - will be returned. - """ - dtype = kwargs.pop("dtype", "complex128") - if np.dtype(dtype) == np.complex64: - return TensorNetworkFileC64(*args, **kwargs) - elif np.dtype(dtype) == np.complex128: - return TensorNetworkFileC128(*args, **kwargs) - else: - raise TypeError(f"Data type '{dtype}' is not supported.") - - -def TensorNetworkSerializer( - *args, **kwargs -) -> Union[TensorNetworkSerializerC64, TensorNetworkSerializerC128]: - """Constructs a tensor network serializer with the specified data type. If a - `dtype` keyword argument is not provided, a TensorNetworkSerializerC128 - instance will be returned. - """ - dtype = kwargs.pop("dtype", "complex128") - if np.dtype(dtype) == np.complex64: - return TensorNetworkSerializerC64(*args, **kwargs) - elif np.dtype(dtype) == np.complex128: - return TensorNetworkSerializerC128(*args, **kwargs) - else: - raise TypeError(f"Data type '{dtype}' is not supported.") - +# The rest of the modules control their exports using `__all__`. +from .factory import * +from .gate import * # Grab the current Jet version from the C++ headers. __version__ = version() diff --git a/python/jet/factory.py b/python/jet/factory.py new file mode 100644 index 00000000..50f2151e --- /dev/null +++ b/python/jet/factory.py @@ -0,0 +1,140 @@ +from typing import Union + +import numpy as np + +from .bindings import ( + TaskBasedCpuContractorC64, + TaskBasedCpuContractorC128, + TensorC64, + TensorC128, + TensorNetworkC64, + TensorNetworkC128, + TensorNetworkFileC64, + TensorNetworkFileC128, + TensorNetworkSerializerC64, + TensorNetworkSerializerC128, +) + +__all__ = [ + "TaskBasedCpuContractorType", + "TensorType", + "TensorNetworkType", + "TensorNetworkFileType", + "TensorNetworkSerializerType", + "TaskBasedCpuContractor", + "Tensor", + "TensorNetwork", + "TensorNetworkFile", + "TensorNetworkSerializer", +] + +# Type aliases to avoid enumerating class specializations. +TaskBasedCpuContractorType = Union[TaskBasedCpuContractorC64, TaskBasedCpuContractorC128] +TensorType = Union[TensorC64, TensorC128] +TensorNetworkType = Union[TensorNetworkC64, TensorNetworkC128] +TensorNetworkFileType = Union[TensorNetworkFileC64, TensorNetworkFileC128] +TensorNetworkSerializerType = Union[TensorNetworkSerializerC64, TensorNetworkSerializerC128] + + +def TaskBasedCpuContractor(*args, **kwargs) -> TaskBasedCpuContractorType: + """Constructs a task-based CPU contractor (TBCC) with the specified data + type. If a ``dtype`` keyword argument is not provided, a + ``TaskBasedCpuContractorC128`` instance will be returned. + + Args: + *args: Positional arguments to pass to the TBCC constructor. + **kwargs: Keyword arguments to pass to the TBCC constructor. + + Returns: + Task-based CPU contractor instance. + """ + dtype = kwargs.pop("dtype", np.complex128) + if np.dtype(dtype) == np.complex64: + return TaskBasedCpuContractorC64(*args, **kwargs) + elif np.dtype(dtype) == np.complex128: + return TaskBasedCpuContractorC128(*args, **kwargs) + else: + raise TypeError(f"Data type '{dtype}' is not supported.") + + +def Tensor(*args, **kwargs) -> TensorType: + """Constructs a tensor with the specified data type. If a ``dtype`` keyword + argument is not provided, a ``TensorC128`` instance will be returned. + + Args: + *args: Positional arguments to pass to the tensor constructor. + **kwargs: Keyword arguments to pass to the tensor constructor. + + Returns: + Tensor instance. + """ + dtype = kwargs.pop("dtype", np.complex128) + if np.dtype(dtype) == np.complex64: + return TensorC64(*args, **kwargs) + elif np.dtype(dtype) == np.complex128: + return TensorC128(*args, **kwargs) + else: + raise TypeError(f"Data type '{dtype}' is not supported.") + + +def TensorNetwork(*args, **kwargs) -> TensorNetworkType: + """Constructs a tensor network with the specified data type. If a ``dtype`` + keyword argument is not provided, a ``TensorNetworkC128`` instance will be + returned. + + Args: + *args: Positional arguments to pass to the tensor network constructor. + **kwargs: Keyword arguments to pass to the tensor network constructor. + + Returns: + Tensor network instance. + """ + dtype = kwargs.pop("dtype", np.complex128) + if np.dtype(dtype) == np.complex64: + return TensorNetworkC64(*args, **kwargs) + elif np.dtype(dtype) == np.complex128: + return TensorNetworkC128(*args, **kwargs) + else: + raise TypeError(f"Data type '{dtype}' is not supported.") + + +def TensorNetworkFile(*args, **kwargs) -> TensorNetworkFileType: + """Constructs a tensor network file with the specified data type. If a + ``dtype`` keyword argument is not provided, a ``TensorNetworkFileC128`` + instance will be returned. + + Args: + *args: Positional arguments to pass to the tensor network file constructor. + **kwargs: Keyword arguments to pass to the tensor network file constructor. + + Returns: + Tensor network file instance. + """ + dtype = kwargs.pop("dtype", np.complex128) + if np.dtype(dtype) == np.complex64: + return TensorNetworkFileC64(*args, **kwargs) + elif np.dtype(dtype) == np.complex128: + return TensorNetworkFileC128(*args, **kwargs) + else: + raise TypeError(f"Data type '{dtype}' is not supported.") + + +def TensorNetworkSerializer(*args, **kwargs) -> TensorNetworkSerializerType: + """Constructs a tensor network serializer with the specified data type. If a + ``dtype`` keyword argument is not provided, a ``TensorNetworkSerializerC128`` + instance will be returned. + + Args: + *args: Positional arguments to pass to the tensor network serializer constructor. + **kwargs: Keyword arguments to pass to the tensor network serializer constructor. + + Returns: + Tensor network serializer instance. + """ + dtype = kwargs.pop("dtype", np.complex128) + if np.dtype(dtype) == np.complex64: + return TensorNetworkSerializerC64(*args, **kwargs) + elif np.dtype(dtype) == np.complex128: + return TensorNetworkSerializerC128(*args, **kwargs) + else: + raise TypeError(f"Data type '{dtype}' is not supported.") diff --git a/python/jet/gate.py b/python/jet/gate.py new file mode 100644 index 00000000..7361b401 --- /dev/null +++ b/python/jet/gate.py @@ -0,0 +1,680 @@ +from abc import ABC, abstractmethod +from cmath import exp +from functools import lru_cache +from math import cos, sin, sqrt +from typing import List, Optional, Sequence + +import numpy as np +from thewalrus.fock_gradients import ( + beamsplitter, + displacement, + squeezing, + two_mode_squeezing, +) + +from .factory import Tensor, TensorType + +__all__ = [ + "Gate", + # CV Fock gates + "Displacement", + "Squeezing", + "TwoModeSqueezing", + "Beamsplitter", + # Qubit gates + "Hadamard", + "NOT", + "PauliX", + "PauliY", + "PauliZ", + "S", + "T", + "SX", + "CNOT", + "CX", + "CY", + "CZ", + "SWAP", + "ISWAP", + "CSWAP", + "Toffoli", + "RX", + "RY", + "RZ", + "PhaseShift", + "CPhaseShift", + "Rot", + "CRX", + "CRY", + "CRZ", + "CRot", + "U1", + "U2", + "U3", +] + + +INV_SQRT2 = 1 / sqrt(2) + + +class Gate(ABC): + def __init__( + self, + name: str, + num_wires: int, + params: Optional[List[float]] = None, + ): + """Constructs a quantum gate. + + Args: + name (str): Name of the gate. + num_wires (int): Number of wires the gate is applied to. + params (list or None): Parameters of the gate. + """ + self.name = name + + self._indices = None + self._num_wires = num_wires + self._params = params + + @property + def indices(self) -> Optional[Sequence[str]]: + """Returns the indices of this gate. An index is a label associated with + an axis of the tensor representation of a gate; the indices of a tensor + determine its connectivity in the context of a tensor network. + """ + return self._indices + + @indices.setter + def indices(self, indices: Optional[Sequence[str]]) -> None: + """Sets the indices of this gate. If the indices of a gate are not ``None``, + they are used to construct the tensor representation of that gate. See + @indices.getter for more information about tensor indices. + + Raises: + ValueError: if the given indices are not a sequence of unique strings + or the number of provided indices is invalid. + + Args: + indices (Sequence[str] or None): New indices of the gate. + """ + # Skip the sequence property checks if `indices` is None. + if indices is None: + pass + + # Check that `indices` is a sequence of unique strings. + elif ( + not isinstance(indices, Sequence) + or not all(isinstance(idx, str) for idx in indices) + or len(set(indices)) != len(indices) + ): + raise ValueError("Indices must be a sequence of unique strings.") + + # Check that `indices` has the correct length. + elif len(indices) != 2 * self._num_wires: + raise ValueError( + f"Gates must have two indices per wire; received {len(indices)}" + f"indices for {self._num_wires} wires." + ) + + self._indices = indices + + @property + def num_wires(self) -> int: + """Returns the number of wires this gate acts on.""" + return self._num_wires + + @property + def params(self) -> Optional[List[float]]: + """Returns the parameters of this gate.""" + return self._params + + @abstractmethod + def _data(self) -> np.ndarray: + """Returns the matrix representation of this gate.""" + pass + + def tensor(self, dtype: type = np.complex128, adjoint: bool = False) -> TensorType: + """Returns the tensor representation of this gate. + + Args: + dtype (type): Data type of the tensor. + adjoint (bool): Whether to take the adjoint of the tensor. + """ + if adjoint: + data = np.linalg.inv(self._data()).flatten() + else: + data = self._data().flatten() + + indices = self.indices + if indices is None: + indices = list(map(str, range(2 * self._num_wires))) + + dimension = int(round(len(data) ** (1 / len(indices)))) + shape = [dimension] * len(indices) + + return Tensor(indices=indices, shape=shape, data=data, dtype=dtype) + + +#################################################################################################### +# Continuous variable Fock gates +#################################################################################################### + + +class Displacement(Gate): + def __init__(self, r: float, phi: float, cutoff: int): + """Constructs a displacement gate. See `thewalrus.displacement + `__ + for more details. + + Args: + r (float): Displacement magnitude. + phi (float): Displacement angle. + cutoff (int): Fock ladder cutoff. + """ + super().__init__(name="Displacement", num_wires=1, params=[r, phi, cutoff]) + + @lru_cache + def _data(self) -> np.ndarray: + return displacement(*self.params) + + +class Squeezing(Gate): + def __init__(self, r: float, theta: float, cutoff: int): + """Constructs a squeezing gate. See `thewalrus.squeezing + `__ + for more details. + + Args: + r (float): Squeezing magnitude. + theta (float): Squeezing angle. + cutoff (int): Fock ladder cutoff. + """ + super().__init__(name="Squeezing", num_wires=1, params=[r, theta, cutoff]) + + @lru_cache + def _data(self) -> np.ndarray: + return squeezing(*self.params) + + +class TwoModeSqueezing(Gate): + def __init__(self, r: float, theta: float, cutoff: int): + """Constructs a two-mode squeezing gate. See `thewalrus.two_mode_squeezing + `__ + for more details. + + Args: + r (float): Squeezing magnitude. + theta (float): Squeezing angle. + cutoff (int): Fock ladder cutoff. + """ + super().__init__(name="TwoModeSqueezing", num_wires=2, params=[r, theta, cutoff]) + + @lru_cache + def _data(self) -> np.ndarray: + return two_mode_squeezing(*self.params) + + +class Beamsplitter(Gate): + def __init__(self, theta: float, phi: float, cutoff: int): + """Constructs a beamsplitter gate. See `thewalrus.beamsplitter + `__ + for more details. + + Args: + theta (float): Transmissivity angle of the beamsplitter. The + transmissivity is :math:`t=\\cos(\\theta)`. + phi (float): Reflection phase of the beamsplitter. + cutoff (int): Fock ladder cutoff. + """ + super().__init__(name="Beamsplitter", num_wires=2, params=[theta, phi, cutoff]) + + @lru_cache + def _data(self) -> np.ndarray: + return beamsplitter(*self.params) + + +#################################################################################################### +# Qubit gates +#################################################################################################### + + +class Hadamard(Gate): + def __init__(self): + """Constructs a Hadamard gate.""" + super().__init__(name="Hadamard", num_wires=1) + + @lru_cache + def _data(self) -> np.ndarray: + """Hadamard matrix""" + mat = [[INV_SQRT2, INV_SQRT2], [INV_SQRT2, -INV_SQRT2]] + return np.array(mat) + + +class PauliX(Gate): + def __init__(self): + """Constructs a Pauli-X gate.""" + super().__init__(name="PauliX", num_wires=1) + + @lru_cache + def _data(self) -> np.ndarray: + mat = [[0, 1], [1, 0]] + return np.array(mat) + + +class PauliY(Gate): + def __init__(self): + """Constructs a Pauli-Y gate.""" + super().__init__(name="PauliY", num_wires=1) + + @lru_cache + def _data(self) -> np.ndarray: + mat = [[0, -1j], [1j, 0]] + return np.array(mat) + + +class PauliZ(Gate): + def __init__(self): + """Constructs a Pauli-Z gate.""" + super().__init__(name="PauliZ", num_wires=1) + + @lru_cache + def _data(self) -> np.ndarray: + mat = [[1, 0], [0, -1]] + return np.array(mat) + + +class S(Gate): + def __init__(self): + """Constructs a single-qubit phase gate.""" + super().__init__(name="S", num_wires=1) + + @lru_cache + def _data(self) -> np.ndarray: + mat = [[1, 0], [0, 1j]] + return np.array(mat) + + +class T(Gate): + def __init__(self): + """Constructs a single-qubit T gate.""" + super().__init__(name="T", num_wires=1) + + @lru_cache + def _data(self) -> np.ndarray: + mat = [[1, 0], [0, exp(0.25j * np.pi)]] + return np.array(mat) + + +class SX(Gate): + def __init__(self): + """Constructs a single-qubit Square-Root X gate.""" + super().__init__(name="SX", num_wires=1) + + @lru_cache + def _data(self) -> np.ndarray: + mat = [[0.5 + 0.5j, 0.5 - 0.5j], [0.5 - 0.5j, 0.5 + 0.5j]] + return np.array(mat) + + +class PhaseShift(Gate): + def __init__(self, phi: float): + """Constructs a single-qubit local phase shift gate. + + Args: + phi (float): Phase shift angle. + """ + super().__init__(name="PhaseShift", num_wires=1, params=[phi]) + + @lru_cache + def _data(self) -> np.ndarray: + phi = self.params[0] + mat = [[1, 0], [0, exp(1j * phi)]] + return np.array(mat) + + +class CPhaseShift(Gate): + def __init__(self, phi: float): + """Constructs a controlled phase shift gate. + + Args: + phi (float): Phase shift angle. + """ + super().__init__(name="CPhaseShift", num_wires=2, params=[phi]) + + @lru_cache + def _data(self) -> np.ndarray: + phi = self.params[0] + mat = [[1, 0, 0, 0], [0, 1, 0, 0], [0, 0, 1, 0], [0, 0, 0, exp(1j * phi)]] + return np.array(mat) + + +class CX(Gate): + def __init__(self): + """Constructs a controlled-X gate.""" + super().__init__(name="CX", num_wires=2) + + @lru_cache + def _data(self) -> np.ndarray: + mat = [[1, 0, 0, 0], [0, 1, 0, 0], [0, 0, 0, 1], [0, 0, 1, 0]] + return np.array(mat) + + +class CY(Gate): + def __init__(self): + """Constructs a controlled-Y gate.""" + super().__init__(name="CY", num_wires=2) + + @lru_cache + def _data(self) -> np.ndarray: + mat = [[1, 0, 0, 0], [0, 1, 0, 0], [0, 0, 0, -1j], [0, 0, 1j, 0]] + return np.array(mat) + + +class CZ(Gate): + def __init__(self): + """Constructs a controlled-Z gate.""" + super().__init__(name="CZ", num_wires=2) + + @lru_cache + def _data(self) -> np.ndarray: + mat = [[1, 0, 0, 0], [0, 1, 0, 0], [0, 0, 1, 0], [0, 0, 0, -1]] + return np.array(mat) + + +class SWAP(Gate): + def __init__(self): + """Constructs a SWAP gate.""" + super().__init__(name="SWAP", num_wires=2) + + @lru_cache + def _data(self) -> np.ndarray: + mat = [[1, 0, 0, 0], [0, 0, 1, 0], [0, 1, 0, 0], [0, 0, 0, 1]] + return np.array(mat) + + +class ISWAP(Gate): + def __init__(self): + """Constructs an ISWAP gate.""" + super().__init__(name="ISWAP", num_wires=2) + + @lru_cache + def _data(self) -> np.ndarray: + mat = [[1, 0, 0, 0], [0, 0, 1j, 0], [0, 1j, 0, 0], [0, 0, 0, 1]] + return np.array(mat) + + +class CSWAP(Gate): + def __init__(self): + """Constructs a CSWAP gate.""" + super().__init__(name="CSWAP", num_wires=3) + + @lru_cache + def _data(self) -> np.ndarray: + mat = [ + [1, 0, 0, 0, 0, 0, 0, 0], + [0, 1, 0, 0, 0, 0, 0, 0], + [0, 0, 1, 0, 0, 0, 0, 0], + [0, 0, 0, 1, 0, 0, 0, 0], + [0, 0, 0, 0, 1, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 1, 0], + [0, 0, 0, 0, 0, 1, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 1], + ] + return np.array(mat) + + +class Toffoli(Gate): + def __init__(self): + """Constructs a Toffoli gate.""" + super().__init__(name="Toffoli", num_wires=3) + + @lru_cache + def _data(self) -> np.ndarray: + mat = [ + [1, 0, 0, 0, 0, 0, 0, 0], + [0, 1, 0, 0, 0, 0, 0, 0], + [0, 0, 1, 0, 0, 0, 0, 0], + [0, 0, 0, 1, 0, 0, 0, 0], + [0, 0, 0, 0, 1, 0, 0, 0], + [0, 0, 0, 0, 0, 1, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 1], + [0, 0, 0, 0, 0, 0, 1, 0], + ] + return np.array(mat) + + +class RX(Gate): + def __init__(self, theta: float): + """Constructs a single-qubit X rotation gate. + + Args: + theta (float): Rotation angle around the X-axis. + """ + super().__init__(name="RX", num_wires=1, params=[theta]) + + @lru_cache + def _data(self) -> np.ndarray: + theta = self.params[0] + c = cos(theta / 2) + js = 1j * sin(-theta / 2) + + mat = [[c, js], [js, c]] + return np.array(mat) + + +class RY(Gate): + def __init__(self, theta: float): + """Constructs a single-qubit Y rotation gate. + + Args: + theta (float): Rotation angle around the Y-axis. + """ + super().__init__(name="RY", num_wires=1, params=[theta]) + + @lru_cache + def _data(self) -> np.ndarray: + theta = self.params[0] + + c = cos(theta / 2) + s = sin(theta / 2) + + mat = [[c, -s], [s, c]] + return np.array(mat) + + +class RZ(Gate): + def __init__(self, theta: float): + """Constructs a single-qubit Z rotation gate. + + Args: + theta (float): Rotation angle around the Z-axis. + """ + super().__init__(name="RZ", num_wires=1, params=[theta]) + + @lru_cache + def _data(self) -> np.ndarray: + theta = self.params[0] + p = exp(-0.5j * theta) + + mat = [[p, 0], [0, np.conj(p)]] + return np.array(mat) + + +class Rot(Gate): + def __init__(self, phi: float, theta: float, omega: float): + """Constructs an arbitrary single-qubit rotation gate. Each of the Pauli + rotation gates can be recovered by fixing two of the three parameters: + + >>> assert RX(theta).tensor() == Rot(pi/2, -pi/2, theta).tensor() + >>> assert RY(theta).tensor() == Rot(0, 0, theta).tensor() + >>> assert RZ(theta).tensor() == Rot(theta, 0, 0).tensor() + + Args: + phi (float): First rotation angle. + theta (float): Second rotation angle. + omega (float): Third rotation angle. + """ + super().__init__(name="Rot", num_wires=1, params=[phi, theta, omega]) + + @lru_cache + def _data(self) -> np.ndarray: + phi, theta, omega = self.params + c = cos(theta / 2) + s = sin(theta / 2) + + mat = [ + [exp(-0.5j * (phi + omega)) * c, -exp(0.5j * (phi - omega)) * s], + [exp(-0.5j * (phi - omega)) * s, exp(0.5j * (phi + omega)) * c], + ] + return np.array(mat) + + +class CRX(Gate): + def __init__(self, theta: float): + """Constructs a controlled-RX gate. + + Args: + theta (float): Rotation angle around the X-axis. + """ + super().__init__(name="CRX", num_wires=2, params=[theta]) + + @lru_cache + def _data(self) -> np.ndarray: + theta = self.params[0] + c = cos(theta / 2) + js = 1j * sin(-theta / 2) + + mat = [[1, 0, 0, 0], [0, 1, 0, 0], [0, 0, c, js], [0, 0, js, c]] + return np.array(mat) + + +class CRY(Gate): + def __init__(self, theta: float): + """Constructs a controlled-RY gate. + + Args: + theta (float): Rotation angle around the Y-axis. + """ + super().__init__(name="CRY", num_wires=2, params=[theta]) + + @lru_cache + def _data(self) -> np.ndarray: + theta = self.params[0] + c = cos(theta / 2) + s = sin(theta / 2) + + mat = [[1, 0, 0, 0], [0, 1, 0, 0], [0, 0, c, -s], [0, 0, s, c]] + return np.array(mat) + + +class CRZ(Gate): + def __init__(self, theta: float): + """Constructs a controlled-RZ gate. + + Args: + theta (float): Rotation angle around the Z-axis. + """ + super().__init__(name="CRZ", num_wires=2, params=[theta]) + + @lru_cache + def _data(self) -> np.ndarray: + theta = self.params[0] + mat = [ + [1, 0, 0, 0], + [0, 1, 0, 0], + [0, 0, exp(-0.5j * theta), 0], + [0, 0, 0, exp(0.5j * theta)], + ] + return np.array(mat) + + +class CRot(Gate): + def __init__(self, phi: float, theta: float, omega: float): + """Constructs a controlled-rotation gate. + + Args: + phi (float): First rotation angle. + theta (float): Second rotation angle. + omega (float): Third rotation angle. + """ + super().__init__(name="CRot", num_wires=2, params=[phi, theta, omega]) + + @lru_cache + def _data(self) -> np.ndarray: + phi, theta, omega = self.params + c = cos(theta / 2) + s = sin(theta / 2) + + mat = [ + [1, 0, 0, 0], + [0, 1, 0, 0], + [0, 0, exp(-0.5j * (phi + omega)) * c, -exp(0.5j * (phi - omega)) * s], + [0, 0, exp(-0.5j * (phi - omega)) * s, exp(0.5j * (phi + omega)) * c], + ] + return np.array(mat) + + +class U1(Gate): + def __init__(self, phi: float): + """Constructs a U1 gate. + + Args: + phi (float): Rotation angle. + """ + super().__init__(name="U1", num_wires=1, params=[phi]) + + @lru_cache + def _data(self) -> np.ndarray: + phi = self.params[0] + mat = [[1, 0], [0, exp(1j * phi)]] + return np.array(mat) + + +class U2(Gate): + def __init__(self, phi: float, lam: float): + """Constructs a U2 gate. + + Args: + phi (float): First rotation angle. + lam (float): Second rotation angle. + """ + super().__init__(name="U2", num_wires=1, params=[phi, lam]) + + @lru_cache + def _data(self) -> np.ndarray: + phi, lam = self.params + mat = [ + [INV_SQRT2, -INV_SQRT2 * exp(1j * lam)], + [INV_SQRT2 * exp(1j * phi), INV_SQRT2 * exp(1j * (phi + lam))], + ] + return np.array(mat) + + +class U3(Gate): + def __init__(self, theta: float, phi: float, lam: float): + """Constructs a U3 gate. + + Args: + theta (float): First rotation angle. + phi (float): Second rotation angle. + lam (float): Third rotation angle. + """ + super().__init__(name="U3", num_wires=1, params=[theta, phi, lam]) + + @lru_cache + def _data(self) -> np.ndarray: + theta, phi, lam = self.params + c = cos(theta / 2) + s = sin(theta / 2) + + mat = [ + [c, -s * exp(1j * lam)], + [s * exp(1j * phi), c * exp(1j * (phi + lam))], + ] + return np.array(mat) + + +# Some gates have different names depending on their context. +NOT = PauliX +CNOT = CX diff --git a/python/setup.py b/python/setup.py index 67935fbd..bf8f2c38 100644 --- a/python/setup.py +++ b/python/setup.py @@ -52,8 +52,9 @@ def build_extension(self, ext): requirements = [ "lark-parser>=0.11.0", - "numpy", + "numpy>=1.0.0", "StrawberryFields==0.18.0", + "thewalrus>=0.15.0", ] info = { diff --git a/python/tests/jet/test_gate.py b/python/tests/jet/test_gate.py new file mode 100644 index 00000000..5d9deff2 --- /dev/null +++ b/python/tests/jet/test_gate.py @@ -0,0 +1,754 @@ +from itertools import chain +from math import pi, sqrt + +import numpy as np +import pytest + +import jet + +INV_SQRT2 = 1 / sqrt(2) + + +class MockGate(jet.Gate): + """MockGate represents a fictional, unnormalized gate which can be applied to pairs of qutrits.""" + + def __init__(self): + super().__init__(name="MockGate", num_wires=2) + + def _data(self) -> np.ndarray: + return np.eye(3 ** 2) * (1 + 1j) + + +class TestGate: + @pytest.fixture + def gate(self): + """Returns a mock gate instance.""" + return MockGate() + + def test_tensor_indices_not_set(self, gate): + """Tests that the correct tensor is returned for a gate with unset indices.""" + tensor = gate.tensor() + + assert tensor.indices == ["0", "1", "2", "3"] + assert tensor.shape == [3, 3, 3, 3] + assert tensor.data == np.ravel(np.eye(9) * (1 + 1j)).tolist() + + def test_tensor_indices_are_set(self, gate): + """Tests that the correct tensor is returned for a gate with specified indices.""" + gate.indices = ["r", "g", "b", "a"] + tensor = gate.tensor() + + assert tensor.indices == ["r", "g", "b", "a"] + assert tensor.shape == [3, 3, 3, 3] + assert tensor.data == np.ravel(np.eye(9) * (1 + 1j)).tolist() + + def test_tensor_adjoint(self, gate): + """Tests that the correct adjoint tensor is returned for a gate.""" + tensor = gate.tensor(adjoint=True) + + assert tensor.indices == ["0", "1", "2", "3"] + assert tensor.shape == [3, 3, 3, 3] + assert tensor.data == np.ravel(np.eye(9) * (1 - 1j) / 2).tolist() + + @pytest.mark.parametrize("indices", [1, ["i", "j", "k", 4], ["x", "x", "x", "x"], []]) + def test_indices_are_invalid(self, gate, indices): + """Tests that a ValueError is raised when the indices of a gate are set + to an invalid value. + """ + with pytest.raises(ValueError): + gate.indices = indices + + @pytest.mark.parametrize("indices", [None, ["1", "2", "3", "4"], ["ABC", "D", "E", "F"]]) + def test_indices_are_valid(self, gate, indices): + """Tests that the indices of a gate can be set and retrieved.""" + assert gate.indices is None + gate.indices = indices + assert gate.indices == indices + + +@pytest.mark.parametrize( + ["gate", "state", "want_tensor"], + [ + ############################################################################################ + # Continuous variable Fock gates + ############################################################################################ + pytest.param( + jet.Displacement(2, pi / 2, 3), + jet.Tensor(indices=["1"], shape=[3], data=[1, 0, 0]), + jet.Tensor( + indices=["0"], shape=[3], data=[0.135335283237, 0.270670566473j, -0.382785986042] + ), + id="Displacement(2,pi/2,3)|1>", + ), + pytest.param( + jet.Displacement(2, pi / 2, 3), + jet.Tensor(indices=["1"], shape=[3], data=[0, 1, 0]), + jet.Tensor( + indices=["0"], shape=[3], data=[0.270670566473j, -0.40600584971, -0.382785986042j] + ), + id="Displacement(2,pi/2,3)|2>", + ), + pytest.param( + jet.Displacement(2, pi / 2, 3), + jet.Tensor(indices=["1"], shape=[3], data=[0, 0, 1]), + jet.Tensor( + indices=["0"], shape=[3], data=[-0.382785986042, -0.382785986042j, 0.135335283237] + ), + id="Displacement(2,pi/2,3)|3>", + ), + pytest.param( + jet.Squeezing(2, pi / 2, 3), + jet.Tensor(indices=["1"], shape=[3], data=[1, 0, 0]), + jet.Tensor(indices=["0"], shape=[3], data=[0.515560111756, 0, -0.351442087775j]), + id="Squeezing(2,pi/2,3)|1>", + ), + pytest.param( + jet.Squeezing(2, pi / 2, 3), + jet.Tensor(indices=["1"], shape=[3], data=[0, 1, 0]), + jet.Tensor(indices=["0"], shape=[3], data=[0, 0.137037026803, 0]), + id="Squeezing(2,pi/2,3)|2>", + ), + pytest.param( + jet.Squeezing(2, pi / 2, 3), + jet.Tensor(indices=["1"], shape=[3], data=[0, 0, 1]), + jet.Tensor(indices=["0"], shape=[3], data=[-0.351442087775j, 0, -0.203142935143]), + id="Squeezing(2,pi/2,3)|3>", + ), + pytest.param( + jet.TwoModeSqueezing(3, pi / 4, 2), + jet.Tensor(indices=["2", "3"], shape=[2, 2], data=[1, 0, 0, 0]), + jet.Tensor( + indices=["0", "1"], + shape=[2, 2], + data=[0.099327927419, 0, 0, 0.069888119434 + 0.069888119434j], + ), + id="TwoModeSqueezing(3,pi/4,2)|00>", + ), + pytest.param( + jet.TwoModeSqueezing(3, pi / 4, 2), + jet.Tensor(indices=["2", "3"], shape=[2, 2], data=[0, 1, 0, 0]), + jet.Tensor( + indices=["0", "1"], + shape=[2, 2], + data=[0, 0.009866037165, 0, 0], + ), + id="TwoModeSqueezing(3,pi/4,2)|01>", + ), + pytest.param( + jet.TwoModeSqueezing(3, pi / 4, 2), + jet.Tensor(indices=["2", "3"], shape=[2, 2], data=[0, 0, 1, 0]), + jet.Tensor( + indices=["0", "1"], + shape=[2, 2], + data=[0, 0, 0.009866037165, 0], + ), + id="TwoModeSqueezing(3,pi/4,2)|10>", + ), + pytest.param( + jet.TwoModeSqueezing(3, pi / 4, 2), + jet.Tensor(indices=["2", "3"], shape=[2, 2], data=[0, 0, 0, 1]), + jet.Tensor( + indices=["0", "1"], + shape=[2, 2], + data=[-0.069888119434 + 0.069888119434j, 0, 0, -0.097367981372], + ), + id="TwoModeSqueezing(3,pi/4,2)|11>", + ), + pytest.param( + jet.Beamsplitter(pi / 4, pi / 2, 2), + jet.Tensor(indices=["2", "3"], shape=[2, 2], data=[1, 0, 0, 0]), + jet.Tensor( + indices=["0", "1"], + shape=[2, 2], + data=[1, 0, 0, 0], + ), + id="Beamsplitter(pi/4,pi/2,2)|00>", + ), + pytest.param( + jet.Beamsplitter(pi / 4, pi / 2, 2), + jet.Tensor(indices=["2", "3"], shape=[2, 2], data=[0, 1, 0, 0]), + jet.Tensor( + indices=["0", "1"], + shape=[2, 2], + data=[0, INV_SQRT2, INV_SQRT2 * 1j, 0], + ), + id="Beamsplitter(pi/4,pi/2,2)|01>", + ), + pytest.param( + jet.Beamsplitter(pi / 4, pi / 2, 2), + jet.Tensor(indices=["2", "3"], shape=[2, 2], data=[0, 0, 1, 0]), + jet.Tensor( + indices=["0", "1"], + shape=[2, 2], + data=[0, INV_SQRT2 * 1j, INV_SQRT2, 0], + ), + id="Beamsplitter(pi/4,pi/2,2)|10>", + ), + pytest.param( + jet.Beamsplitter(pi / 4, pi / 2, 2), + jet.Tensor(indices=["2", "3"], shape=[2, 2], data=[0, 0, 0, 1]), + jet.Tensor( + indices=["0", "1"], + shape=[2, 2], + data=[0, 0, 0, 0], + ), + id="Beamsplitter(pi/4,pi/2,2)|11>", + ), + ############################################################################################ + # Hadamard gate + ############################################################################################ + pytest.param( + jet.Hadamard(), + jet.Tensor(indices=["1"], shape=[2], data=[1, 0]), + jet.Tensor(indices=["0"], shape=[2], data=[INV_SQRT2, INV_SQRT2]), + id="H|0>", + ), + pytest.param( + jet.Hadamard(), + jet.Tensor(indices=["1"], shape=[2], data=[0, 1]), + jet.Tensor(indices=["0"], shape=[2], data=[INV_SQRT2, -INV_SQRT2]), + id="H|1>", + ), + ############################################################################################ + # Pauli gates + ############################################################################################ + pytest.param( + jet.PauliX(), + jet.Tensor(indices=["1"], shape=[2], data=[1, 0]), + jet.Tensor(indices=["0"], shape=[2], data=[0, 1]), + id="X|0>", + ), + pytest.param( + jet.PauliX(), + jet.Tensor(indices=["1"], shape=[2], data=[0, 1]), + jet.Tensor(indices=["0"], shape=[2], data=[1, 0]), + id="X|1>", + ), + pytest.param( + jet.PauliY(), + jet.Tensor(indices=["1"], shape=[2], data=[1, 0]), + jet.Tensor(indices=["0"], shape=[2], data=[0, 1j]), + id="Y|0>", + ), + pytest.param( + jet.PauliY(), + jet.Tensor(indices=["1"], shape=[2], data=[0, 1]), + jet.Tensor(indices=["0"], shape=[2], data=[-1j, 0]), + id="Y|1>", + ), + pytest.param( + jet.PauliZ(), + jet.Tensor(indices=["1"], shape=[2], data=[1, 0]), + jet.Tensor(indices=["0"], shape=[2], data=[1, 0]), + id="Z|0>", + ), + pytest.param( + jet.PauliZ(), + jet.Tensor(indices=["1"], shape=[2], data=[0, 1]), + jet.Tensor(indices=["0"], shape=[2], data=[0, -1]), + id="Z|1>", + ), + ############################################################################################ + # Unparametrized phase shift gates + ############################################################################################ + pytest.param( + jet.S(), + jet.Tensor(indices=["1"], shape=[2], data=[1, 0]), + jet.Tensor(indices=["0"], shape=[2], data=[1, 0]), + id="S|0>", + ), + pytest.param( + jet.S(), + jet.Tensor(indices=["1"], shape=[2], data=[0, 1]), + jet.Tensor(indices=["0"], shape=[2], data=[0, 1j]), + id="S|1>", + ), + pytest.param( + jet.T(), + jet.Tensor(indices=["1"], shape=[2], data=[1, 0]), + jet.Tensor(indices=["0"], shape=[2], data=[1, 0]), + id="T|0>", + ), + pytest.param( + jet.T(), + jet.Tensor(indices=["1"], shape=[2], data=[0, 1]), + jet.Tensor(indices=["0"], shape=[2], data=[0, INV_SQRT2 + INV_SQRT2 * 1j]), + id="T|1>", + ), + pytest.param( + jet.SX(), + jet.Tensor(indices=["1"], shape=[2], data=[1, 0]), + jet.Tensor(indices=["0"], shape=[2], data=[1 / 2 + 1j / 2, 1 / 2 - 1j / 2]), + id="SX|0>", + ), + pytest.param( + jet.SX(), + jet.Tensor(indices=["1"], shape=[2], data=[0, 1]), + jet.Tensor(indices=["0"], shape=[2], data=[1 / 2 - 1j / 2, 1 / 2 + 1j / 2]), + id="SX|1>", + ), + ############################################################################################ + # Parametrized phase shift gates + ############################################################################################ + pytest.param( + jet.PhaseShift(pi / 2), + jet.Tensor(indices=["1"], shape=[2], data=[1, 0]), + jet.Tensor(indices=["0"], shape=[2], data=[1, 0]), + id="P|0>", + ), + pytest.param( + jet.PhaseShift(pi / 2), + jet.Tensor(indices=["1"], shape=[2], data=[0, 1]), + jet.Tensor(indices=["0"], shape=[2], data=[0, 1j]), + id="P|1>", + ), + pytest.param( + jet.CPhaseShift(pi / 2), + jet.Tensor(indices=["2", "3"], shape=[2, 2], data=[1, 0, 0, 0]), + jet.Tensor(indices=["0", "1"], shape=[2, 2], data=[1, 0, 0, 0]), + id="CP|00>", + ), + pytest.param( + jet.CPhaseShift(pi / 2), + jet.Tensor(indices=["2", "3"], shape=[2, 2], data=[0, 1, 0, 0]), + jet.Tensor(indices=["0", "1"], shape=[2, 2], data=[0, 1, 0, 0]), + id="CP|01>", + ), + pytest.param( + jet.CPhaseShift(pi / 2), + jet.Tensor(indices=["2", "3"], shape=[2, 2], data=[0, 0, 1, 0]), + jet.Tensor(indices=["0", "1"], shape=[2, 2], data=[0, 0, 1, 0]), + id="CP|10>", + ), + pytest.param( + jet.CPhaseShift(pi / 2), + jet.Tensor(indices=["2", "3"], shape=[2, 2], data=[0, 0, 0, 1]), + jet.Tensor(indices=["0", "1"], shape=[2, 2], data=[0, 0, 0, 1j]), + id="CP|11>", + ), + ############################################################################################ + # Control gates + ############################################################################################ + pytest.param( + jet.CX(), + jet.Tensor(indices=["2", "3"], shape=[2, 2], data=[1, 0, 0, 0]), + jet.Tensor(indices=["0", "1"], shape=[2, 2], data=[1, 0, 0, 0]), + id="CX|00>", + ), + pytest.param( + jet.CX(), + jet.Tensor(indices=["2", "3"], shape=[2, 2], data=[0, 1, 0, 0]), + jet.Tensor(indices=["0", "1"], shape=[2, 2], data=[0, 1, 0, 0]), + id="CX|01>", + ), + pytest.param( + jet.CX(), + jet.Tensor(indices=["2", "3"], shape=[2, 2], data=[0, 0, 1, 0]), + jet.Tensor(indices=["0", "1"], shape=[2, 2], data=[0, 0, 0, 1]), + id="CX|10>", + ), + pytest.param( + jet.CX(), + jet.Tensor(indices=["2", "3"], shape=[2, 2], data=[0, 0, 0, 1]), + jet.Tensor(indices=["0", "1"], shape=[2, 2], data=[0, 0, 1, 0]), + id="CX|11>", + ), + pytest.param( + jet.CY(), + jet.Tensor(indices=["2", "3"], shape=[2, 2], data=[1, 0, 0, 0]), + jet.Tensor(indices=["0", "1"], shape=[2, 2], data=[1, 0, 0, 0]), + id="CY|00>", + ), + pytest.param( + jet.CY(), + jet.Tensor(indices=["2", "3"], shape=[2, 2], data=[0, 1, 0, 0]), + jet.Tensor(indices=["0", "1"], shape=[2, 2], data=[0, 1, 0, 0]), + id="CY|01>", + ), + pytest.param( + jet.CY(), + jet.Tensor(indices=["2", "3"], shape=[2, 2], data=[0, 0, 1, 0]), + jet.Tensor(indices=["0", "1"], shape=[2, 2], data=[0, 0, 0, 1j]), + id="CY|10>", + ), + pytest.param( + jet.CY(), + jet.Tensor(indices=["2", "3"], shape=[2, 2], data=[0, 0, 0, 1]), + jet.Tensor(indices=["0", "1"], shape=[2, 2], data=[0, 0, -1j, 0]), + id="CY|11>", + ), + pytest.param( + jet.CZ(), + jet.Tensor(indices=["2", "3"], shape=[2, 2], data=[1, 0, 0, 0]), + jet.Tensor(indices=["0", "1"], shape=[2, 2], data=[1, 0, 0, 0]), + id="CZ|00>", + ), + pytest.param( + jet.CZ(), + jet.Tensor(indices=["2", "3"], shape=[2, 2], data=[0, 1, 0, 0]), + jet.Tensor(indices=["0", "1"], shape=[2, 2], data=[0, 1, 0, 0]), + id="CZ|01>", + ), + pytest.param( + jet.CZ(), + jet.Tensor(indices=["2", "3"], shape=[2, 2], data=[0, 0, 1, 0]), + jet.Tensor(indices=["0", "1"], shape=[2, 2], data=[0, 0, 1, 0]), + id="CZ|10>", + ), + pytest.param( + jet.CZ(), + jet.Tensor(indices=["2", "3"], shape=[2, 2], data=[0, 0, 0, 1]), + jet.Tensor(indices=["0", "1"], shape=[2, 2], data=[0, 0, 0, -1]), + id="CZ|11>", + ), + ############################################################################################ + # SWAP gates + ############################################################################################ + pytest.param( + jet.SWAP(), + jet.Tensor(indices=["2", "3"], shape=[2, 2], data=[1, 0, 0, 0]), + jet.Tensor(indices=["0", "1"], shape=[2, 2], data=[1, 0, 0, 0]), + id="SWAP|00>", + ), + pytest.param( + jet.SWAP(), + jet.Tensor(indices=["2", "3"], shape=[2, 2], data=[0, 1, 0, 0]), + jet.Tensor(indices=["0", "1"], shape=[2, 2], data=[0, 0, 1, 0]), + id="SWAP|01>", + ), + pytest.param( + jet.SWAP(), + jet.Tensor(indices=["2", "3"], shape=[2, 2], data=[0, 0, 1, 0]), + jet.Tensor(indices=["0", "1"], shape=[2, 2], data=[0, 1, 0, 0]), + id="SWAP|10>", + ), + pytest.param( + jet.SWAP(), + jet.Tensor(indices=["2", "3"], shape=[2, 2], data=[0, 0, 0, 1]), + jet.Tensor(indices=["0", "1"], shape=[2, 2], data=[0, 0, 0, 1]), + id="SWAP|11>", + ), + pytest.param( + jet.ISWAP(), + jet.Tensor(indices=["2", "3"], shape=[2, 2], data=[1, 0, 0, 0]), + jet.Tensor(indices=["0", "1"], shape=[2, 2], data=[1, 0, 0, 0]), + id="ISWAP|00>", + ), + pytest.param( + jet.ISWAP(), + jet.Tensor(indices=["2", "3"], shape=[2, 2], data=[0, 1, 0, 0]), + jet.Tensor(indices=["0", "1"], shape=[2, 2], data=[0, 0, 1j, 0]), + id="ISWAP|01>", + ), + pytest.param( + jet.ISWAP(), + jet.Tensor(indices=["2", "3"], shape=[2, 2], data=[0, 0, 1, 0]), + jet.Tensor(indices=["0", "1"], shape=[2, 2], data=[0, 1j, 0, 0]), + id="ISWAP|10>", + ), + pytest.param( + jet.ISWAP(), + jet.Tensor(indices=["2", "3"], shape=[2, 2], data=[0, 0, 0, 1]), + jet.Tensor(indices=["0", "1"], shape=[2, 2], data=[0, 0, 0, 1]), + id="ISWAP|11>", + ), + pytest.param( + jet.CSWAP(), + jet.Tensor(indices=["3", "4", "5"], shape=[2, 2, 2], data=[1, 0, 0, 0, 0, 0, 0, 0]), + jet.Tensor(indices=["0", "1", "2"], shape=[2, 2, 2], data=[1, 0, 0, 0, 0, 0, 0, 0]), + id="CSWAP|000>", + ), + pytest.param( + jet.CSWAP(), + jet.Tensor(indices=["3", "4", "5"], shape=[2, 2, 2], data=[0, 1, 0, 0, 0, 0, 0, 0]), + jet.Tensor(indices=["0", "1", "2"], shape=[2, 2, 2], data=[0, 1, 0, 0, 0, 0, 0, 0]), + id="CSWAP|001>", + ), + pytest.param( + jet.CSWAP(), + jet.Tensor(indices=["3", "4", "5"], shape=[2, 2, 2], data=[0, 0, 1, 0, 0, 0, 0, 0]), + jet.Tensor(indices=["0", "1", "2"], shape=[2, 2, 2], data=[0, 0, 1, 0, 0, 0, 0, 0]), + id="CSWAP|010>", + ), + pytest.param( + jet.CSWAP(), + jet.Tensor(indices=["3", "4", "5"], shape=[2, 2, 2], data=[0, 0, 0, 1, 0, 0, 0, 0]), + jet.Tensor(indices=["0", "1", "2"], shape=[2, 2, 2], data=[0, 0, 0, 1, 0, 0, 0, 0]), + id="CSWAP|011>", + ), + pytest.param( + jet.CSWAP(), + jet.Tensor(indices=["3", "4", "5"], shape=[2, 2, 2], data=[0, 0, 0, 0, 1, 0, 0, 0]), + jet.Tensor(indices=["0", "1", "2"], shape=[2, 2, 2], data=[0, 0, 0, 0, 1, 0, 0, 0]), + id="CSWAP|100>", + ), + pytest.param( + jet.CSWAP(), + jet.Tensor(indices=["3", "4", "5"], shape=[2, 2, 2], data=[0, 0, 0, 0, 0, 1, 0, 0]), + jet.Tensor(indices=["0", "1", "2"], shape=[2, 2, 2], data=[0, 0, 0, 0, 0, 0, 1, 0]), + id="CSWAP|101>", + ), + pytest.param( + jet.CSWAP(), + jet.Tensor(indices=["3", "4", "5"], shape=[2, 2, 2], data=[0, 0, 0, 0, 0, 0, 1, 0]), + jet.Tensor(indices=["0", "1", "2"], shape=[2, 2, 2], data=[0, 0, 0, 0, 0, 1, 0, 0]), + id="CSWAP|110>", + ), + pytest.param( + jet.CSWAP(), + jet.Tensor(indices=["3", "4", "5"], shape=[2, 2, 2], data=[0, 0, 0, 0, 0, 0, 0, 1]), + jet.Tensor(indices=["0", "1", "2"], shape=[2, 2, 2], data=[0, 0, 0, 0, 0, 0, 0, 1]), + id="CSWAP|111>", + ), + ############################################################################################ + # Toffoli gate + ############################################################################################ + pytest.param( + jet.Toffoli(), + jet.Tensor(indices=["3", "4", "5"], shape=[2, 2, 2], data=[1, 0, 0, 0, 0, 0, 0, 0]), + jet.Tensor(indices=["0", "1", "2"], shape=[2, 2, 2], data=[1, 0, 0, 0, 0, 0, 0, 0]), + id="T|000>", + ), + pytest.param( + jet.Toffoli(), + jet.Tensor(indices=["3", "4", "5"], shape=[2, 2, 2], data=[0, 1, 0, 0, 0, 0, 0, 0]), + jet.Tensor(indices=["0", "1", "2"], shape=[2, 2, 2], data=[0, 1, 0, 0, 0, 0, 0, 0]), + id="T|001>", + ), + pytest.param( + jet.Toffoli(), + jet.Tensor(indices=["3", "4", "5"], shape=[2, 2, 2], data=[0, 0, 1, 0, 0, 0, 0, 0]), + jet.Tensor(indices=["0", "1", "2"], shape=[2, 2, 2], data=[0, 0, 1, 0, 0, 0, 0, 0]), + id="T|010>", + ), + pytest.param( + jet.Toffoli(), + jet.Tensor(indices=["3", "4", "5"], shape=[2, 2, 2], data=[0, 0, 0, 1, 0, 0, 0, 0]), + jet.Tensor(indices=["0", "1", "2"], shape=[2, 2, 2], data=[0, 0, 0, 1, 0, 0, 0, 0]), + id="T|011>", + ), + pytest.param( + jet.Toffoli(), + jet.Tensor(indices=["3", "4", "5"], shape=[2, 2, 2], data=[0, 0, 0, 0, 1, 0, 0, 0]), + jet.Tensor(indices=["0", "1", "2"], shape=[2, 2, 2], data=[0, 0, 0, 0, 1, 0, 0, 0]), + id="T|100>", + ), + pytest.param( + jet.Toffoli(), + jet.Tensor(indices=["3", "4", "5"], shape=[2, 2, 2], data=[0, 0, 0, 0, 0, 1, 0, 0]), + jet.Tensor(indices=["0", "1", "2"], shape=[2, 2, 2], data=[0, 0, 0, 0, 0, 1, 0, 0]), + id="T|101>", + ), + pytest.param( + jet.Toffoli(), + jet.Tensor(indices=["3", "4", "5"], shape=[2, 2, 2], data=[0, 0, 0, 0, 0, 0, 1, 0]), + jet.Tensor(indices=["0", "1", "2"], shape=[2, 2, 2], data=[0, 0, 0, 0, 0, 0, 0, 1]), + id="T|110>", + ), + pytest.param( + jet.Toffoli(), + jet.Tensor(indices=["3", "4", "5"], shape=[2, 2, 2], data=[0, 0, 0, 0, 0, 0, 0, 1]), + jet.Tensor(indices=["0", "1", "2"], shape=[2, 2, 2], data=[0, 0, 0, 0, 0, 0, 1, 0]), + id="T|111>", + ), + ############################################################################################ + # Rotation gates + ############################################################################################ + pytest.param( + jet.RX(pi), + jet.Tensor(indices=["1"], shape=[2], data=[1, 0]), + jet.Tensor(indices=["0"], shape=[2], data=[0, -1j]), + id="RX(pi)|0>", + ), + pytest.param( + jet.RX(pi), + jet.Tensor(indices=["1"], shape=[2], data=[0, 1]), + jet.Tensor(indices=["0"], shape=[2], data=[-1j, 0]), + id="RX(pi)|1>", + ), + pytest.param( + jet.RY(pi), + jet.Tensor(indices=["1"], shape=[2], data=[1, 0]), + jet.Tensor(indices=["0"], shape=[2], data=[0, 1]), + id="RY(pi)|0>", + ), + pytest.param( + jet.RY(pi), + jet.Tensor(indices=["1"], shape=[2], data=[0, 1]), + jet.Tensor(indices=["0"], shape=[2], data=[-1, 0]), + id="RY(pi)|1>", + ), + pytest.param( + jet.RZ(pi), + jet.Tensor(indices=["1"], shape=[2], data=[1, 0]), + jet.Tensor(indices=["0"], shape=[2], data=[-1j, 0]), + id="RZ(pi)|0>", + ), + pytest.param( + jet.RZ(pi), + jet.Tensor(indices=["1"], shape=[2], data=[0, 1]), + jet.Tensor(indices=["0"], shape=[2], data=[0, 1j]), + id="RY(pi)|1>", + ), + pytest.param( + jet.Rot(pi / 2, pi, 2 * pi), + jet.Tensor(indices=["1"], shape=[2], data=[1, 0]), + jet.Tensor(indices=["0"], shape=[2], data=[0, -INV_SQRT2 + INV_SQRT2 * 1j]), + id="Rot(pi/2,pi,2*pi)|0>", + ), + pytest.param( + jet.Rot(pi / 2, pi, 2 * pi), + jet.Tensor(indices=["1"], shape=[2], data=[0, 1]), + jet.Tensor(indices=["0"], shape=[2], data=[INV_SQRT2 + INV_SQRT2 * 1j, 0]), + id="Rot(pi/2,pi,2*pi)|1>", + ), + ############################################################################################ + # Controlled rotation gates + ############################################################################################ + pytest.param( + jet.CRX(pi), + jet.Tensor(indices=["2", "3"], shape=[2, 2], data=[1, 0, 0, 0]), + jet.Tensor(indices=["0", "1"], shape=[2, 2], data=[1, 0, 0, 0]), + id="CRX|00>", + ), + pytest.param( + jet.CRX(pi), + jet.Tensor(indices=["2", "3"], shape=[2, 2], data=[0, 1, 0, 0]), + jet.Tensor(indices=["0", "1"], shape=[2, 2], data=[0, 1, 0, 0]), + id="CRX|01>", + ), + pytest.param( + jet.CRX(pi), + jet.Tensor(indices=["2", "3"], shape=[2, 2], data=[0, 0, 1, 0]), + jet.Tensor(indices=["0", "1"], shape=[2, 2], data=[0, 0, 0, -1j]), + id="CRX|10>", + ), + pytest.param( + jet.CRX(pi), + jet.Tensor(indices=["2", "3"], shape=[2, 2], data=[0, 0, 0, 1]), + jet.Tensor(indices=["0", "1"], shape=[2, 2], data=[0, 0, -1j, 0]), + id="CRX|11>", + ), + pytest.param( + jet.CRY(pi), + jet.Tensor(indices=["2", "3"], shape=[2, 2], data=[1, 0, 0, 0]), + jet.Tensor(indices=["0", "1"], shape=[2, 2], data=[1, 0, 0, 0]), + id="CRY|00>", + ), + pytest.param( + jet.CRY(pi), + jet.Tensor(indices=["2", "3"], shape=[2, 2], data=[0, 1, 0, 0]), + jet.Tensor(indices=["0", "1"], shape=[2, 2], data=[0, 1, 0, 0]), + id="CRY|01>", + ), + pytest.param( + jet.CRY(pi), + jet.Tensor(indices=["2", "3"], shape=[2, 2], data=[0, 0, 1, 0]), + jet.Tensor(indices=["0", "1"], shape=[2, 2], data=[0, 0, 0, 1]), + id="CRY|10>", + ), + pytest.param( + jet.CRY(pi), + jet.Tensor(indices=["2", "3"], shape=[2, 2], data=[0, 0, 0, 1]), + jet.Tensor(indices=["0", "1"], shape=[2, 2], data=[0, 0, -1, 0]), + id="CRY|11>", + ), + pytest.param( + jet.CRZ(pi), + jet.Tensor(indices=["2", "3"], shape=[2, 2], data=[1, 0, 0, 0]), + jet.Tensor(indices=["0", "1"], shape=[2, 2], data=[1, 0, 0, 0]), + id="CRZ|00>", + ), + pytest.param( + jet.CRZ(pi), + jet.Tensor(indices=["2", "3"], shape=[2, 2], data=[0, 1, 0, 0]), + jet.Tensor(indices=["0", "1"], shape=[2, 2], data=[0, 1, 0, 0]), + id="CRZ|01>", + ), + pytest.param( + jet.CRZ(pi), + jet.Tensor(indices=["2", "3"], shape=[2, 2], data=[0, 0, 1, 0]), + jet.Tensor(indices=["0", "1"], shape=[2, 2], data=[0, 0, -1j, 0]), + id="CRZ|10>", + ), + pytest.param( + jet.CRZ(pi), + jet.Tensor(indices=["2", "3"], shape=[2, 2], data=[0, 0, 0, 1]), + jet.Tensor(indices=["0", "1"], shape=[2, 2], data=[0, 0, 0, 1j]), + id="CRZ|11>", + ), + pytest.param( + jet.CRot(pi / 2, pi, 2 * pi), + jet.Tensor(indices=["2", "3"], shape=[2, 2], data=[1, 0, 0, 0]), + jet.Tensor(indices=["0", "1"], shape=[2, 2], data=[1, 0, 0, 0]), + id="CRot|00>", + ), + pytest.param( + jet.CRot(pi / 2, pi, 2 * pi), + jet.Tensor(indices=["2", "3"], shape=[2, 2], data=[0, 1, 0, 0]), + jet.Tensor(indices=["0", "1"], shape=[2, 2], data=[0, 1, 0, 0]), + id="CRot|01>", + ), + pytest.param( + jet.CRot(pi / 2, pi, 2 * pi), + jet.Tensor(indices=["2", "3"], shape=[2, 2], data=[0, 0, 1, 0]), + jet.Tensor( + indices=["0", "1"], shape=[2, 2], data=[0, 0, 0, -INV_SQRT2 + INV_SQRT2 * 1j] + ), + id="CRot|10>", + ), + pytest.param( + jet.CRot(pi / 2, pi, 2 * pi), + jet.Tensor(indices=["2", "3"], shape=[2, 2], data=[0, 0, 0, 1]), + jet.Tensor( + indices=["0", "1"], shape=[2, 2], data=[0, 0, INV_SQRT2 + INV_SQRT2 * 1j, 0] + ), + id="CRot|11>", + ), + ############################################################################################ + # U gates + ############################################################################################ + pytest.param( + jet.U1(pi / 2), + jet.Tensor(indices=["1"], shape=[2], data=[1, 0]), + jet.Tensor(indices=["0"], shape=[2], data=[1, 0]), + id="U1|0>", + ), + pytest.param( + jet.U1(pi / 2), + jet.Tensor(indices=["1"], shape=[2], data=[0, 1]), + jet.Tensor(indices=["0"], shape=[2], data=[0, 1j]), + id="U1|1>", + ), + pytest.param( + jet.U2(pi / 2, pi), + jet.Tensor(indices=["1"], shape=[2], data=[1, 0]), + jet.Tensor(indices=["0"], shape=[2], data=[INV_SQRT2, INV_SQRT2 * 1j]), + id="U2|0>", + ), + pytest.param( + jet.U2(pi / 2, pi), + jet.Tensor(indices=["1"], shape=[2], data=[0, 1]), + jet.Tensor(indices=["0"], shape=[2], data=[INV_SQRT2, -INV_SQRT2 * 1j]), + id="U2|1>", + ), + pytest.param( + jet.U3(2 * pi, pi, pi / 2), + jet.Tensor(indices=["1"], shape=[2], data=[1, 0]), + jet.Tensor(indices=["0"], shape=[2], data=[-1, 0]), + id="U3(2*pi,pi,pi/2)|0>", + ), + pytest.param( + jet.U3(2 * pi, pi, pi / 2), + jet.Tensor(indices=["1"], shape=[2], data=[0, 1]), + jet.Tensor(indices=["0"], shape=[2], data=[0, 1j]), + id="U3(2*pi,pi,pi/2)|1>", + ), + ], +) +def test_gate(gate, state, want_tensor): + """Tests that the correct transformation is applied by a gate to a basis state.""" + have_tensor = jet.contract_tensors(gate.tensor(), state) + assert have_tensor.data == pytest.approx(want_tensor.data) + assert have_tensor.shape == want_tensor.shape + assert have_tensor.indices == want_tensor.indices