diff --git a/.github/CHANGELOG.md b/.github/CHANGELOG.md index 4671e3097..bf0854a9e 100644 --- a/.github/CHANGELOG.md +++ b/.github/CHANGELOG.md @@ -3,7 +3,11 @@ ### New features * A general factory method and a duck style type-checker. - [#256](https://github.com/XanaduAI/MrMustard/pull/256) + [(#256)](https://github.com/XanaduAI/MrMustard/pull/256) + +* Data classes for the representations project. Includes `WavefunctionArrayData`, `GaussianData`, + `ABCData` and `SymplecticData`. + [(#258)](https://github.com/XanaduAI/MrMustard/pull/258) ### Breaking changes @@ -11,12 +15,14 @@ ### Bug fixes -* Fixed a bug about the variable names in functions (apply_kraus_to_ket, apply_kraus_to_dm, apply_choi_to_ket, apply_choi_to_dm). [(#271)](https://github.com/XanaduAI/MrMustard/pull/271) +* Fixed a bug about the variable names in functions (apply_kraus_to_ket, apply_kraus_to_dm, apply_choi_to_ket, apply_choi_to_dm). + [(#271)](https://github.com/XanaduAI/MrMustard/pull/271) ### Documentation ### Contributors -[Yuan Yao](https://github.com/sylviemonet), [Richard A. Wolf](https://github.com/ryk-wolf) + +[Richard A. Wolf](https://github.com/ryk-wolf), [Filippo Miatto](https://github.com/ziofil), [Yuan Yao](https://github.com/sylviemonet) # Release 0.5.0 (current release) @@ -105,6 +111,9 @@ * More robust implementation of cutoffs for States. [(#239)](https://github.com/XanaduAI/MrMustard/pull/239) +* Dependencies and versioning are now managed using Poetry. +[(#257)](https://github.com/XanaduAI/MrMustard/pull/257) + ### Bug fixes * Fixed a bug that would make two progress bars appear during an optimization @@ -126,6 +135,7 @@ cutoff of the first detector is equal to 1, the resulting density matrix is now [Robbe De Prins](https://github.com/rdprins), [Gabriele Gullì](https://github.com/ggulli), [Richard A. Wolf](https://github.com/ryk-wolf) + --- # Release 0.4.1 diff --git a/mrmustard/__init__.py b/mrmustard/__init__.py index f8ed18e69..fe3fba2ab 100644 --- a/mrmustard/__init__.py +++ b/mrmustard/__init__.py @@ -43,6 +43,7 @@ def __init__(self): self.EQ_TRANSFORMATION_CUTOFF = 3 # 3 is enough to include a full step of the rec relations self.EQ_TRANSFORMATION_RTOL_FOCK = 1e-3 self.EQ_TRANSFORMATION_RTOL_GAUSS = 1e-6 + self.EQUALITY_PRECISION_DECIMALS = 6 # decimals for (A,b,c) equality check # for the detectors self.PNR_INTERNAL_CUTOFF = 50 self.HOMODYNE_SQUEEZING = 10.0 diff --git a/mrmustard/lab/representations/data/__init__.py b/mrmustard/lab/representations/data/__init__.py new file mode 100644 index 000000000..02400932c --- /dev/null +++ b/mrmustard/lab/representations/data/__init__.py @@ -0,0 +1,23 @@ +# Copyright 2021 Xanadu Quantum Technologies Inc. + +# 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 + +# http://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. + +""" This package contains the modules implementing base classes for datas. +""" +from .gaussian_data import GaussianData +from .abc_data import ABCData +from .array_data import ArrayData +from .symplectic_data import SymplecticData +from .wavefunctionarray_data import WavefunctionArrayData + +__all__ = ["GaussianData", "ABCData", "ArrayData", "SymplecticData", "WavefunctionArrayData"] diff --git a/mrmustard/lab/representations/data/abc_data.py b/mrmustard/lab/representations/data/abc_data.py new file mode 100644 index 000000000..866b3e85f --- /dev/null +++ b/mrmustard/lab/representations/data/abc_data.py @@ -0,0 +1,170 @@ +# Copyright 2023 Xanadu Quantum Technologies Inc. + +# 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 + +# http://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. + +from __future__ import annotations + +from itertools import product +from typing import Optional, Union +import numpy as np + +from mrmustard.lab.representations.data.matvec_data import MatVecData +from mrmustard.math import Math +from mrmustard.typing import Batch, ComplexMatrix, ComplexVector, Scalar + +math = Math() + + +class ABCData(MatVecData): + r"""Exponential of quadratic polynomial for the Bargmann representation. + + Quadratic Gaussian data is made of: quadratic coefficients, linear coefficients, constant. + Each of these has a batch dimension, and the batch dimension is the same for all of them. + They are the parameters of the function `c * exp(x^T A x / 2 + x^T b)`. + + Note that if constants are not provided, they will all be initialized at 1. + + Args: + A (Batch[Matrix]): series of quadratic coefficient + b (Batch[Vector]): series of linear coefficients + c (Optional[Batch[Scalar]]):series of constants + """ + + def __init__( + self, A: Batch[ComplexMatrix], b: Batch[ComplexVector], c: Optional[Batch[Scalar]] = None + ) -> None: + super().__init__(mat=A, vec=b, coeffs=c) + self._contract_idxs = [] + + def value(self, x: ComplexVector) -> Scalar: + r"""Value of this function at x. + + Args: + x (Vector): point at which the function is evaluated + + Returns: + Scalar: value of the function + """ + val = 0.0 + for A, b, c in zip(self.A, self.b, self.c): + val += math.exp(0.5 * math.sum(x * math.matvec(A, x)) + math.sum(x * b)) * c + return val + + def __call__(self, x: ComplexVector) -> Scalar: + return self.value(x) + + @property + def A(self) -> Batch[ComplexMatrix]: + return self.mat + + @property + def b(self) -> Batch[ComplexVector]: + return self.vec + + @property + def c(self) -> Batch[Scalar]: + return self.coeffs + + def __mul__(self, other: Union[Scalar, ABCData]) -> ABCData: + if isinstance(other, ABCData): + new_a = [A1 + A2 for A1, A2 in product(self.A, other.A)] + new_b = [b1 + b2 for b1, b2 in product(self.b, other.b)] + new_c = [c1 * c2 for c1, c2 in product(self.c, other.c)] + return self.__class__(A=new_a, b=new_b, c=new_c) + else: + try: # scalar + return self.__class__(self.A, self.b, other * self.c) + except Exception as e: # Neither same object type nor a scalar case + raise TypeError(f"Cannot multiply {self.__class__} and {other.__class__}.") from e + + def __and__(self, other: ABCData) -> ABCData: + As = [math.block_diag(a1, a2) for a1 in self.A for a2 in other.A] + bs = [math.concat([b1, b2], axis=-1) for b1 in self.b for b2 in other.b] + cs = [c1 * c2 for c1 in self.c for c2 in other.c] + return self.__class__(As, bs, cs) + + def conj(self): + new = self.__class__(math.conj(self.A), math.conj(self.b), math.conj(self.c)) + new._contract_idxs = self._contract_idxs + return new + + def __matmul__(self, other: ABCData) -> ABCData: + r"""Implements the contraction of (A,b,c) triples across the marked indices.""" + # Useful for the future, but not for this PR + # raise NotImplementedError() + + graph = self & other + newA = graph.A + newb = graph.b + newc = graph.c + for n, (i, j) in enumerate(zip(self._contract_idxs, other._contract_idxs)): + i = i - np.sum(np.array(self._contract_idxs[:n]) < i) + j = j + self.dim - n - np.sum(np.array(other._contract_idxs[:n]) < j) + noij = list(range(i)) + list(range(i + 1, j)) + list(range(j + 1, newA.shape[-1])) + Abar = math.gather(math.gather(newA, noij, axis=1), noij, axis=2) + bbar = math.gather(newb, noij, axis=1) + D = math.gather( + math.concat([newA[..., i][..., None], newA[..., j][..., None]], axis=-1), + noij, + axis=1, + ) + M = math.concat( + [ + math.concat( + [ + newA[:, i, i][:, None, None], + newA[:, j, i][:, None, None] - 1, + ], + axis=-1, + ), + math.concat( + [ + newA[:, i, j][:, None, None] - 1, + newA[:, j, j][:, None, None], + ], + axis=-1, + ), + ], + axis=-2, + ) + Minv = math.inv(M) + b_ = math.concat([newb[:, i][:, None], newb[:, j][:, None]], axis=-1) + + newA = Abar - math.einsum("bij,bjk,blk", D, Minv, D) + newb = bbar - math.einsum("bij,bjk,bk", D, Minv, b_) + newc = ( + newc + * math.exp(-math.einsum("bi,bij,bj", b_, Minv, b_) / 2) + / math.sqrt(-math.det(M)) + ) + return self.__class__(newA, newb, newc) + + def __getitem__(self, idx: int | tuple[int, ...]) -> ABCData: + idx = (idx,) if isinstance(idx, int) else idx + for i in idx: + if i > self.dim: + raise IndexError( + f"Index {i} out of bounds for {self.__class__.__qualname__} of dimension {self.dim}." + ) + new = self.__class__(self.A, self.b, self.c) + new._contract_idxs = idx + return new + + def transpose(self, order: tuple[int, ...] | list[int]) -> ABCData: + new = self.__class__( + A=math.gather(math.gather(self.A, order, -1), order, -2), + b=math.gather(self.b, order, -1), + c=self.c, + ) + new._contract_idxs = self._contract_idxs + return new diff --git a/mrmustard/lab/representations/data/array_data.py b/mrmustard/lab/representations/data/array_data.py new file mode 100644 index 000000000..fac4ca87f --- /dev/null +++ b/mrmustard/lab/representations/data/array_data.py @@ -0,0 +1,71 @@ +# Copyright 2023 Xanadu Quantum Technologies Inc. + +# 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 + +# http://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. + +from __future__ import annotations + +import numpy as np + +from mrmustard.lab.representations.data.data import Data +from mrmustard.math import Math +from mrmustard.typing import Scalar, Vector + +math = Math() + + +class ArrayData(Data): + """Contains array-like data for certain Representation objects. + + Args: + array (Vector) : data to be contained in the class + """ + + def __init__(self, array: Vector) -> None: + self.array = array + + @property + def cutoffs(self): + return self.array.shape + + def __neg__(self) -> Data: + return self.__class__(array=-self.array) + + def __eq__(self, other: ArrayData) -> bool: + try: + return np.allclose(self.array, other.array) + except AttributeError as e: + raise TypeError(f"Cannot compare {self.__class__} and {other.__class__}.") from e + + def __add__(self, other: ArrayData) -> ArrayData: + try: + return self.__class__(array=self.array + other.array) + except AttributeError as e: + raise TypeError(f"Cannot add/subtract {self.__class__} and {other.__class__}.") from e + + def __truediv__(self, other: Scalar) -> ArrayData: + try: + return self.__class__(array=self.array / other) + except TypeError as e: + raise TypeError("Can only divide by a scalar.") from e + + def __mul__(self, other: Scalar) -> ArrayData: + try: + return self.__class__(array=self.array * other) + except TypeError as e: + raise TypeError("Can only multiply by a scalar.") from e + + def __and__(self, other: ArrayData) -> ArrayData: + try: + return self.__class__(array=np.outer(self.array, other.array)) + except AttributeError as e: + raise TypeError(f"Cannot tensor product {self.__class__} and {other.__class__}.") from e diff --git a/mrmustard/lab/representations/data/data.py b/mrmustard/lab/representations/data/data.py new file mode 100644 index 000000000..f55e23faa --- /dev/null +++ b/mrmustard/lab/representations/data/data.py @@ -0,0 +1,58 @@ +# Copyright 2023 Xanadu Quantum Technologies Inc. + +# 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 + +# http://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. + +from __future__ import annotations + +from abc import ABC, abstractmethod +from typing import Any, Union + +from mrmustard.typing import Scalar + + +class Data(ABC): + r"""Abstract parent class for types of data encoding a quantum state's representation.""" + + @abstractmethod + def __neg__(self) -> Data: + ... + + @abstractmethod + def __eq__(self, other: Data) -> bool: + ... + + @abstractmethod + def __add__(self, other: Data) -> Data: + ... + + def __sub__(self, other: Data) -> Data: + try: + return self.__add__(-other) + except AttributeError as e: + raise TypeError(f"Cannot subtract {self.__class__} and {other.__class__}.") from e + + # @abstractmethod + # def __call__(self, dom: Any) -> Scalar: + # r"""Evaluate the function at a point in the domain.""" + # ... + + @abstractmethod + def __truediv__(self, other: Union[Scalar, Data]) -> Data: + ... + + @abstractmethod + def __mul__(self, other: Union[Scalar, Data]) -> Data: + ... + + def __rmul__(self, other: Scalar) -> Data: + return self.__mul__(other=other) diff --git a/mrmustard/lab/representations/data/gaussian_data.py b/mrmustard/lab/representations/data/gaussian_data.py new file mode 100644 index 000000000..f77e9fb16 --- /dev/null +++ b/mrmustard/lab/representations/data/gaussian_data.py @@ -0,0 +1,178 @@ +# Copyright 2023 Xanadu Quantum Technologies Inc. + +# 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 + +# http://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. + +from __future__ import annotations + +import numpy as np +from itertools import product +from typing import Optional, Union # TYPE_CHECKING + +from mrmustard.lab.representations.data.matvec_data import MatVecData +from mrmustard.math import Math +from mrmustard.typing import Batch, Matrix, Scalar, Tensor, Vector + +# if TYPE_CHECKING: # This is to avoid the circular import issue with GaussianData<>ABCData +# from mrmustard.lab.representations.data.abc_data import ABCData + +math = Math() + + +class GaussianData(MatVecData): + r"""Gaussian data for certain representation objects. + + Gaussian data is made of covariance, mean vector and coefficient. Each of these has a batch + dimension, and the length of the batch dimension is the same for all three. + These are the parameters of a linear combination of Gaussians, which is Gaussian if there is + only one contribution for each. + Each contribution parametrizes the Gaussian function: + `coeffs * exp(-0.5*(x-mean)^T cov^-1 (x-mean))/sqrt((2pi)^k det(cov))` where k is the size of cov. + + Args: + cov (Optional[Batch[Matrix]]): covariance matrices (real symmetric) + means (Optional[Batch[Vector]]): mean vector of the state (real), note that the + dimension must be even + coeffs (Optional[Batch[Scalar]]): coefficients (complex) + + Raises: + ValueError: if neither means nor covariance is defined + """ + + def __init__( + self, + cov: Optional[Batch[Matrix]] = None, + means: Optional[Batch[Vector]] = None, + coeffs: Optional[Batch[Scalar]] = None, + ) -> None: + if cov is not None or means is not None: # at least one is defined -or both- + if cov is None: + dim = means.shape[-1] + batch_size = means.shape[0] + cov = math.astensor([math.eye(dim, dtype=means.dtype) for _ in range(batch_size)]) + + elif means is None: # we know cov is not None here + dim = cov.shape[-1] + batch_size = cov.shape[0] + means = math.zeros((batch_size, dim), dtype=cov.dtype) + else: + raise ValueError("You need to define at last one of covariance or mean") + + super().__init__(mat=cov, vec=means, coeffs=coeffs) + + @property + def cov(self) -> Matrix: + return self.mat + + @property + def means(self) -> Vector: + return self.vec + + @property + def c(self) -> Scalar: + return self.coeffs + + def value(self, x: Vector) -> Scalar: + r"""returns the value of the gaussian at x. + + Arguments: + x (array of floats): where to evaluate the function + """ + val = 0.0 + for sigma, mu, c in zip(self.cov, self.means, self.c): + exponent = -0.5 * math.sum(math.solve(sigma, (x - mu)) * (x - mu)) + denom = math.sqrt((2 * np.pi) ** len(x) * math.det(sigma)) + print(c) + print(math.exp(exponent)) + print(denom) + val += c * math.exp(exponent) / denom + return val + + def __mul__(self, other: Union[Scalar, GaussianData]) -> GaussianData: + if isinstance(other, GaussianData): + joint_covs = self._compute_mul_covs(other=other) + joint_means = self._compute_mul_means(other=other) + joint_coeffs = self._compute_mul_coeffs(other=other) + return self.__class__(cov=joint_covs, means=joint_means, coeffs=joint_coeffs) + else: + try: # hope it's a scalar + new_coeffs = self.coeffs * other + return self.__class__(cov=self.cov, means=self.means, coeffs=new_coeffs) + except (TypeError, ValueError) as e: # Neither GaussianData nor scalar + raise TypeError(f"Cannot multiply {self.__class__} and {other.__class__}.") from e + + def __and__(self, other: GaussianData) -> GaussianData: + "tensor product as block-wise block diag concatenation" + if isinstance(other, GaussianData): + new_covs = [ + math.symplectic_tensor_product(s1, s2) for s1, s2 in product(self.cov, other.cov) + ] + new_means = [] + d = len(self.means[0]) // 2 + for d1, d2 in product(self.means, other.means): + new_means.append(np.concatenate([d1[:d], d2[:d], d1[d:], d2[d:]])) + new_means = math.astensor(new_means) + new_coeffs = math.reshape(math.outer(self.coeffs, other.coeffs), -1) + return self.__class__(new_covs, new_means, new_coeffs) + else: + raise TypeError(f"Cannot tensor product {self.__class__} and {other.__class__}.") + + def _compute_mul_covs(self, other: GaussianData) -> Tensor: + r"""Computes the combined covariances when multiplying Gaussians. + The formula is cov1 (cov1 + cov2)^-1 cov2 for each pair of cov1 and cov2 + from self and other (see https://math.stackexchange.com/q/964103) + + Args: + other (GaussianData): another GaussianData object which covariance will be multiplied + + Returns: + (Tensor) The tensor of combined covariances + """ + combined_covs = [ + math.matmul(c1, math.solve(c1 + c2, c2)) for c1 in self.cov for c2 in other.cov + ] + return math.astensor(combined_covs) + + def _compute_mul_means(self, other: GaussianData) -> Tensor: + r"""Computes the combined means when multiplying Gaussians. + The formula is cov1 (cov1 + cov2)^-1 mu2 + cov2 (cov1 + cov2)^-1 mu1 for each + pair of (cov1, mu1) and (cov2, mu2) from self and other. + (see https://math.stackexchange.com/q/964103) + + Args: + other (GaussianData): another GaussianData object which means will be multiplied + + Returns: + (Tensor) The tensor of combined multiplied means + """ + combined_means = [ + math.matvec(c1, math.solve(c1 + c2, m2)) + math.matvec(c2, math.solve(c1 + c2, m1)) + for c1, m1 in zip(self.cov, self.means) + for c2, m2 in zip(other.cov, other.means) + ] + return math.astensor(combined_means) + + def _compute_mul_coeffs(self, other: GaussianData) -> Tensor: + r"""Computes the combined coefficients when multiplying Gaussians. + + Args: + other (GaussianData): another GaussianData object which coeffs will be multiplied + + Returns: + (Tensor) The tensor of multiplied coefficients + """ + combined_coeffs = [ + c1 * c2 * self.__class__(cov=[cov1 + cov2], means=[m1]).value(m2) + for cov1, m1, c1 in zip(self.cov, self.means, self.c) + for cov2, m2, c2 in zip(other.cov, other.means, other.c) + ] + return math.astensor(combined_coeffs) diff --git a/mrmustard/lab/representations/data/matvec_data.py b/mrmustard/lab/representations/data/matvec_data.py new file mode 100644 index 000000000..4ab4b54bd --- /dev/null +++ b/mrmustard/lab/representations/data/matvec_data.py @@ -0,0 +1,134 @@ +# Copyright 2023 Xanadu Quantum Technologies Inc. + +# 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 + +# http://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. + +from __future__ import annotations + +from typing import List, Optional, Set, Tuple, Union + +import numpy as np + +from mrmustard import settings +from mrmustard.lab.representations.data.data import Data +from mrmustard.math import Math +from mrmustard.typing import Batch, Matrix, Scalar, Vector + +math = Math() + + +class MatVecData(Data): # Note: this class is abstract! + r"""Contains matrix and vector -like data for certain Representation objects. + + Args: + mat (Batch[Matrix]): the matrix-like data to be contained in the class + vec (Batch[Vector]): the vector-like data to be contained in the class + coeffs (Batch[Scalar]): the coefficients + """ + + def __init__(self, mat: Batch[Matrix], vec: Batch[Vector], coeffs: Batch[Scalar]) -> None: + if coeffs is None: # default all 1s + coeffs = math.ones(len(vec), dtype=math.float64) + + self.mat = math.atleast_3d(math.astensor(mat)) + self.vec = math.atleast_2d(math.astensor(vec)) + self.coeffs = math.atleast_1d(math.astensor(coeffs)) + assert ( + len(self.mat) == len(self.vec) == len(self.coeffs) + ), "All inputs must have the same batch size." + assert ( + self.mat.shape[-1] == self.mat.shape[-2] == self.vec.shape[-1] + ), "A and b must have the same dimension and A must be symmetric" + self.batch_dim = self.mat.shape[0] + self.dim = self.mat.shape[-1] + + def __neg__(self) -> MatVecData: + return self.__class__(self.mat, self.vec, -self.coeffs) + + def __eq__(self, other: MatVecData, exclude_scalars: bool = False) -> bool: + A, B = sorted([self, other], key=lambda x: x.batch_dim) # A smaller or equal batch than B + # check scalars + Ac = np.around(A.coeffs, settings.EQUALITY_PRECISION_DECIMALS) + Bc = memoryview(np.around(B.coeffs, settings.EQUALITY_PRECISION_DECIMALS)).tobytes() + if exclude_scalars or all(memoryview(c).tobytes() in Bc for c in Ac): + # check vectors + Av = np.around(A.vec, settings.EQUALITY_PRECISION_DECIMALS) + Bv = memoryview(np.around(B.vec, settings.EQUALITY_PRECISION_DECIMALS)).tobytes() + if all(memoryview(v).tobytes() in Bv for v in Av): + # check matrices + Am = np.around(A.mat, settings.EQUALITY_PRECISION_DECIMALS) + Bm = memoryview(np.around(B.mat, settings.EQUALITY_PRECISION_DECIMALS)).tobytes() + if all(memoryview(m).tobytes() in Bm for m in Am): + return True + return False + + def __add__(self, other: MatVecData) -> MatVecData: + if self.__eq__(other, exclude_scalars=True): + new_coeffs = self.coeffs + other.coeffs + return self.__class__(self.mat, self.vec, new_coeffs) + combined_matrices = math.concat([self.mat, other.mat], axis=0) + combined_vectors = math.concat([self.vec, other.vec], axis=0) + combined_coeffs = math.concat([self.coeffs, other.coeffs], axis=0) + return self.__class__(combined_matrices, combined_vectors, combined_coeffs) + + def __truediv__(self, x: Scalar) -> MatVecData: + if not isinstance(x, (int, float, complex)): + raise TypeError(f"Cannot divide {self.__class__} by {x.__class__}.") + new_coeffs = self.coeffs / x + return self.__class__(self.mat, self.vec, new_coeffs) + + # # TODO: decide which simplify we want to keep + # def simplify(self, rtol:float=1e-6, atol:float=1e-6) -> MatVecData: + # N = self.mat.shape[0] + # mask = np.ones(N, dtype=np.int8) + + # for i in range(N): + + # for j in range(i + 1, N): + + # if mask[i] == 0 or i == j: # evaluated previously + # continue + + # if np.allclose( + # self.mat[i], self.mat[j], rtol=rtol, atol=atol, equal_nan=True + # ) and np.allclose( + # self.vec[i], self.vec[j], rtol=rtol, atol=atol, equal_nan=True + # ): + # self.coeffs[i] += self.coeffs[j] + # mask[j] = 0 + + # return self.__class__( + # mat = self.mat[mask == 1], + # vec = self.vec[mask == 1], + # coeffs = self.coeffs[mask == 1] + # ) + + # # TODO: decide which simplify we want to keep + # def old_simplify(self) -> None: + # indices_to_check = set(range(self.batch_size)) + # removed = set() + + # while indices_to_check: + # i = indices_to_check.pop() + + # for j in indices_to_check.copy(): + # if np.allclose(self.mat[i], self.mat[j]) and np.allclose( + # self.vec[i], self.vec[j] + # ): + # self.coeffs[i] += self.coeffs[j] + # indices_to_check.remove(j) + # removed.add(j) + + # to_keep = [i for i in range(self.batch_size) if i not in removed] + # self.mat = self.mat[to_keep] + # self.vec = self.vec[to_keep] + # self.coeffs = self.coeffs[to_keep] diff --git a/mrmustard/lab/representations/data/symplectic_data.py b/mrmustard/lab/representations/data/symplectic_data.py new file mode 100644 index 000000000..d202dd2ad --- /dev/null +++ b/mrmustard/lab/representations/data/symplectic_data.py @@ -0,0 +1,88 @@ +# Copyright 2023 Xanadu Quantum Technologies Inc. + +# 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 + +# http://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. + +from __future__ import annotations + +import numpy as np +from itertools import product +from typing import Optional + +from thewalrus.quantum.gaussian_checks import is_symplectic + +from mrmustard.lab.representations.data.matvec_data import MatVecData +from mrmustard.math import Math +from mrmustard.typing import Batch, Matrix, RealVector, Scalar + +math = Math() + + +class SymplecticData(MatVecData): + """Symplectic matrix-like data for certain Representation objects. + + Here the displacement vector is defined as: + :math:`\bm{d} = \sqrt{2\hbar}[\Re(\alpha), \Im(\alpha)]` + + Args: + symplectic (Batch[Matrix]): symplectic matrix with qqpp-ordering + displacement (Batch[RealVector]): the real displacement vector + coeffs (Optional[Batch[Scalar]]): default to be 1. + """ + + def __init__( + self, + symplectic: Batch[Matrix], + displacement: Batch[RealVector], + coeffs: Optional[Batch[Scalar]] = None, + ) -> None: + super().__init__(mat=symplectic, vec=displacement, coeffs=coeffs) + for mat in self.mat: + if is_symplectic(math.asnumpy(mat)) == False: + raise ValueError("The matrix given is not symplectic.") + + # reaching here means no matrix is non-symplectic + + @property + def symplectic(self) -> np.array: + return self.mat + + @property + def displacement(self) -> np.array: + return self.vec + + def __mul__(self, other: Scalar) -> SymplecticData: + if isinstance(other, SymplecticData): + raise TypeError("Symplectic can only be multiplied by a scalar") + else: + try: # Maybe other is a scalar + new_coeffs = self.coeffs * other + return self.__class__(self.symplectic, self.displacement, new_coeffs) + except (TypeError, ValueError) as e: + raise TypeError(f"Cannot multiply {self.__class__} and {other.__class__}.") from e + + def __and__(self, other: SymplecticData) -> SymplecticData: + "symplectic tensor product as block-wise block diag concatenation" + if isinstance(other, SymplecticData): + new_symplectics = [ + math.symplectic_tensor_product(s1, s2) + for s1, s2 in product(self.symplectic, other.symplectic) + ] + new_displacements = [] + d = len(self.displacement[0]) // 2 + for d1, d2 in product(self.displacement, other.displacement): + new_displacements.append(np.concatenate([d1[:d], d2[:d], d1[d:], d2[d:]])) + new_displacements = math.astensor(new_displacements) + new_coeffs = math.reshape(math.outer(self.coeffs, other.coeffs), -1) + return self.__class__(new_symplectics, new_displacements, new_coeffs) + else: + raise TypeError(f"Cannot tensor product {self.__class__} and {other.__class__}.") diff --git a/mrmustard/lab/representations/data/wavefunctionarray_data.py b/mrmustard/lab/representations/data/wavefunctionarray_data.py new file mode 100644 index 000000000..becc5d0ce --- /dev/null +++ b/mrmustard/lab/representations/data/wavefunctionarray_data.py @@ -0,0 +1,95 @@ +# Copyright 2023 Xanadu Quantum Technologies Inc. + +# 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 + +# http://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. + +from __future__ import annotations + +from typing import Union +import numpy as np + +from mrmustard.lab.representations.data.array_data import ArrayData +from mrmustard.lab.representations.data.data import Data +from mrmustard.math import Math +from mrmustard.typing import Scalar, Vector + +math = Math() + + +class WavefunctionArrayData(ArrayData): + r"""Data class for the Wavefunction, encapsulating q-variable points and corresponding values. + + Args: + qs (Vector): q-variable points + array (Vector): q-Wavefunction values corresponding to the qs + """ + + def __init__(self, qs: Vector, array: Vector) -> None: + super().__init__(array=array) + self.qs = qs + + def __neg__(self) -> Data: + return self.__class__(array=-self.array, qs=self.qs) + + def __eq__(self, other: ArrayData) -> bool: + try: + return super().__eq__(other) and np.allclose(self.qs, other.qs) + + except AttributeError as e: + raise TypeError(f"Cannot compare {self.__class__} and {other.__class__}.") from e + + def __truediv__(self, x: Scalar) -> ArrayData: + try: + return self.__class__(array=self.array / x, qs=self.qs) + except (AttributeError, TypeError) as e: + raise TypeError(f"Cannot divide {self.__class__} by {x}.") from e + + def __add__(self, other: ArrayData) -> WavefunctionArrayData: + if self._qs_is_same(other): + try: + return self.__class__(array=self.array + other.array, qs=self.qs) + + except AttributeError as e: + raise TypeError( + f"Cannot add/subtract {self.__class__} and {other.__class__}." + ) from e + else: + raise ValueError("The two wave functions must have the same qs. ") + + def __mul__(self, other: Union[Scalar, WavefunctionArrayData]) -> WavefunctionArrayData: + if isinstance(other, WavefunctionArrayData): + if self._qs_is_same(other): + new_array = self.array * other.array + return self.__class__(array=new_array, qs=self.qs) + else: + raise ValueError("The two wave functions must have the same qs. ") + else: + try: # Maybe it's a scalar... + new_array = self.array * other + return self.__class__(array=new_array, qs=self.qs) + except TypeError as e: # it's neither the same object type nor a scalar + raise TypeError(f"Cannot multiply {self.__class__} and {other.__class__}.") from e + + def __and__(self, other: WavefunctionArrayData) -> WavefunctionArrayData: + try: + new_array = np.outer(self.array, other.array) + new_qs = np.outer(self.qs, other.qs) + return self.__class__(array=new_array, qs=new_qs) + except AttributeError as e: + raise TypeError(f"Cannot tensor {self.__class__} and {other.__class__}.") from e + + def _qs_is_same(self, other: WavefunctionArrayData) -> bool: + r"""Compares the qs of two WavefunctionArrayData objects.""" + try: + return True if np.allclose(self.qs, other.qs) else False + except AttributeError as e: + raise TypeError(f"Cannot compare {self.__class__} and {other.__class__}.") from e diff --git a/mrmustard/math/tensorflow.py b/mrmustard/math/tensorflow.py index 5be6eba03..97d3cbddf 100644 --- a/mrmustard/math/tensorflow.py +++ b/mrmustard/math/tensorflow.py @@ -72,6 +72,33 @@ def astensor(self, array: Union[np.ndarray, tf.Tensor], dtype=None) -> tf.Tensor def atleast_1d(self, array: tf.Tensor, dtype=None) -> tf.Tensor: return self.cast(tf.reshape(array, [-1]), dtype) + def atleast_2d(self, array: tf.Tensor, dtype=None) -> tf.Tensor: + if len(array.shape) == 0: + array = self.expand_dims(array, 0) + if len(array.shape) == 1: + array = self.expand_dims(array, 0) + return self.cast(array, dtype) + + def atleast_3d(self, array: tf.Tensor, dtype=None) -> tf.Tensor: + if len(array.shape) == 0: + array = self.expand_dims(array, 0) + if len(array.shape) == 1: + array = self.expand_dims(array, 0) + if len(array.shape) == 2: + array = self.expand_dims(array, 0) + return self.cast(array, dtype) + + def arrayflatten(self, array_of_arrays: Union[tf.Tensor, List]) -> tf.Tensor: + arr = self.transpose(self.astensor(array_of_arrays), (0, 2, 1, 3)) + return self.reshape(arr, (arr.shape[0] * arr.shape[1], arr.shape[2] * arr.shape[3])) + + def block_diag(self, mat1: tf.Tensor, mat2: tf.Tensor) -> tf.Tensor: + Za = self.zeros((mat1.shape[-2], mat2.shape[-1]), dtype=mat1.dtype) + Zb = self.zeros((mat2.shape[-2], mat1.shape[-1]), dtype=mat1.dtype) + return self.concat( + [self.concat([mat1, Za], axis=-1), self.concat([Zb, mat2], axis=-1)], axis=-2 + ) + def cast(self, array: tf.Tensor, dtype=None) -> tf.Tensor: if dtype is None: return array @@ -278,6 +305,16 @@ def sqrt(self, x: tf.Tensor, dtype=None) -> tf.Tensor: def sum(self, array: tf.Tensor, axes: Sequence[int] = None): return tf.reduce_sum(array, axes) + def symplectic_tensor_product(self, S1, S2) -> tf.Tensor: + d = S1.shape[-1] // 2 + A1, B1, C1, D1 = S1[:d, :d], S1[:d, d:], S1[d:, :d], S1[d:, d:] + A2, B2, C2, D2 = S2[:d, :d], S2[:d, d:], S2[d:, :d], S2[d:, d:] + A = self.block_diag(A1, A2) + B = self.block_diag(B1, B2) + C = self.block_diag(C1, C2) + D = self.block_diag(D1, D2) + return self.concat([self.concat([A, B], axis=-1), self.concat([C, D], axis=-1)], axis=-2) + @Autocast() def tensordot(self, a: tf.Tensor, b: tf.Tensor, axes: List[int]) -> tf.Tensor: return tf.tensordot(a, b, axes) diff --git a/mrmustard/physics/gaussian.py b/mrmustard/physics/gaussian.py index 7f4f58b4b..78699ce73 100644 --- a/mrmustard/physics/gaussian.py +++ b/mrmustard/physics/gaussian.py @@ -15,6 +15,7 @@ """ This module contains functions for performing calculations on Gaussian states. """ +import numpy as np from typing import Any, Optional, Sequence, Tuple, Union @@ -127,9 +128,13 @@ def gaussian_cov(symplectic: Matrix, eigenvalues: Vector = None) -> Matrix: if eigenvalues is None: return math.matmul(symplectic, math.transpose(symplectic)) - return math.matmul( - math.matmul(symplectic, math.diag(math.concat([eigenvalues, eigenvalues], axis=0))), - math.transpose(symplectic), + return ( + settings.HBAR + / 2 + * math.matmul( + math.matmul(symplectic, math.diag(math.concat([eigenvalues, eigenvalues], axis=0))), + math.transpose(symplectic), + ) ) @@ -941,3 +946,21 @@ def XYd_dual(X: Matrix, Y: Matrix, d: Vector): if d is not None: d_dual = math.matvec(X_dual, d) if X_dual is not None else d return X_dual, Y_dual, d_dual + + +def reorder_matrix_from_qpqp_to_qqpp(N: int): + r"""Returns the reordered matrix from qpqp ordering variables to qqpp ordering.""" + list1 = [ + i + for i in range( + N, + ) + ] + list2 = [j for j in range(N, 2 * N)] + reorder_list = [None] * (len(list1) + len(list2)) + reorder_list[::2] = list1 + reorder_list[1::2] = list2 + matrix = np.zeros((2 * N, 2 * N)) + for i, n in enumerate(reorder_list): + matrix[i, n] = 1 + return matrix diff --git a/mrmustard/utils/misc_tools.py b/mrmustard/utils/misc_tools.py index 5b474e3eb..7956af269 100644 --- a/mrmustard/utils/misc_tools.py +++ b/mrmustard/utils/misc_tools.py @@ -42,9 +42,9 @@ def duck_type_checker(obj_a: object, obj_b: object) -> bool: True if both objects have exactly the same attributes, False otherwise. Raises: - TypeError: If at least one of the objects given can't be compared via duck-typing, i.e. - lack attributes. A typical example would be python basic types such as ``int``, ``float``, - ``bool``, etc. + TypeError: If at least one of the objects given can't be compared via duck-typing, i.e. + lack attributes. A typical example would be python basic types such as ``int``, ``float``, + ``bool``, etc. """ try: set_a = set(obj_a.__dict__.keys()) diff --git a/pytest.ini b/pytest.ini index c24fe5bb9..da3afadd1 100644 --- a/pytest.ini +++ b/pytest.ini @@ -1,3 +1,6 @@ [pytest] +addopts = --ignore=tests/test_lab/test_representations/test_data/test_data.py --ignore=tests/test_lab/test_representations/test_data/test_matvec_data.py + filterwarnings = ignore::DeprecationWarning + diff --git a/tests/test_lab/test_representations/test_data/mock_data.py b/tests/test_lab/test_representations/test_data/mock_data.py new file mode 100644 index 000000000..d8fb92505 --- /dev/null +++ b/tests/test_lab/test_representations/test_data/mock_data.py @@ -0,0 +1,123 @@ +# Copyright 2023 Xanadu Quantum Technologies Inc. + +# 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 + +# http://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 numpy as np + +mock_scalar = (int, float, complex) + + +class MockData: + r"""Mock class for Data objects and any child of Data that is still abstract.""" + + def __init__( + self, + mat=None, + vec=None, + coeffs=None, + array=np.ones(10), + cutoffs=None, + qs=None, + batch_dim=None, + ) -> None: + self.mat = mat + self.vec = vec + self.coeffs = coeffs + self.array = array + self.cutoffs = cutoffs + self.qs = qs + self.batch_dim = batch_dim + + def raise_error_if_different_type_and_not_scalar(self, other): + if (not isinstance(other, self.__class__)) and (not isinstance(other, mock_scalar)): + raise TypeError() + + def raise_error_if_not_scalar(self, other): + if not isinstance(other, mock_scalar): + raise TypeError() + + def __neg__(self): + return self.__class__(array=-self.array) + + def __eq__(self, other): + self.raise_error_if_different_type_and_not_scalar(other) + return self + + def __add__(self, other): + self.raise_error_if_different_type_and_not_scalar(other) + return self.__class__(array=self.array + other.array) + + def __sub__(self, other): + self.raise_error_if_different_type_and_not_scalar(other) + return self.__class__(array=self.array - other.array) + + def __truediv__(self, x): + self.raise_error_if_not_scalar(x) + return self.__class__(array=self.array / x) + + def __mul__(self, other): + self.raise_error_if_different_type_and_not_scalar(other) + return self + + def __rmul__(self, other): + self.raise_error_if_different_type_and_not_scalar(other) + return self + + def __and__(self, other): + self.raise_error_if_different_type_and_not_scalar(other) + return self + + +class MockNoCommonAttributesObject: + r"""Mock placeholder class for an object which has different attributes but same methods.""" + + def __init__(self) -> None: + self.apple = None + self.pear = None + self.banana = None + self.batch_dim = None + + +class MockCommonAttributesObject: + r"""Mock class for Data objects and any child of Data that is still abstract.""" + + def __init__(self) -> None: + self.mat = None + self.vec = None + self.coeffs = None + self.array = None + self.cutoffs = None + + # def __neg__(self): + # pass + + # def __eq__(self, other): + # pass + + # def __add__(self, other): + # pass + + # def __sub__(self, other): + # pass + + # def __truediv__(self, other): + # pass + + # # def __mul__(self, other): + # # pass + + # # def __rmul__(self, other): + # # pass + + # def __and__(self, other): + # pass diff --git a/tests/test_lab/test_representations/test_data/test_abc_data.py b/tests/test_lab/test_representations/test_data/test_abc_data.py new file mode 100644 index 000000000..709009bb4 --- /dev/null +++ b/tests/test_lab/test_representations/test_data/test_abc_data.py @@ -0,0 +1,195 @@ +# Copyright 2023 Xanadu Quantum Technologies Inc. + +# 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 + +# http://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. +""" This class corresponds to test child class for the MatVecData class. + +Unlike some of its -abstract- parent test classes, this class is meant to be run with pytest. + +Check parents test classe-s for more details on the rationale. + +The fixtures must correspond to the concrete class being tested, here ABCData. +""" + +from copy import deepcopy + +import numpy as np +import pytest + +from mrmustard.lab import Attenuator, Dgate, Gaussian, Ggate +from mrmustard.lab.representations.data.abc_data import ABCData +from mrmustard.physics import bargmann, gaussian +from mrmustard.typing import Batch, Matrix, Scalar, Vector +from mrmustard.utils.misc_tools import general_factory +from tests.test_lab.test_representations.test_data.test_matvec_data import TestMatVecData + +np.random.seed(42) + + +######### Instantiating class to test ######### +@pytest.fixture +def D() -> int: + """The dimension: matrices will be DxD and vectors will be D.""" + return 5 + + +@pytest.fixture +def N() -> int: + """The number of elements in the batch.""" + return 3 + + +@pytest.fixture +def TYPE(): + r"""Type of the object under test.""" + return ABCData + + +@pytest.fixture +def A(D, N) -> Batch[Matrix]: + r"""Some batch of matrices for the object's parameterization.""" + A = np.random.normal(size=(N, D, D)) + 1j * np.random.normal(size=(N, D, D)) + return A + A.transpose((0, 2, 1)) # symmetrize + + +@pytest.fixture +def B(D, N) -> Batch[Vector]: + r"""Some batch of vectors for the object's parameterization.""" + return np.random.normal(size=(N, D)) + 1j * np.random.normal(size=(N, D)) + + +@pytest.fixture +def C(N) -> Batch[Scalar]: + r"""Some batch of scalars for the object's parameterization.""" + return np.random.normal(size=N) + 1j * np.random.normal(size=N) + + +@pytest.fixture +def PARAMS(A, B, C) -> dict: + r"""Parameters for the class instance which is created.""" + params_dict = {"A": A, "b": B, "c": C} + return params_dict + + +@pytest.fixture() +def DATA(TYPE, PARAMS) -> ABCData: + r"""Instance of the class that must be tested.""" + return general_factory(TYPE, **PARAMS) + + +@pytest.fixture() +def OTHER(DATA) -> ABCData: + r"""Another instance of the class that must be tested.""" + return deepcopy(DATA) + + +@pytest.fixture() +def X(D) -> ABCData: + r"""Generates a random complex X-vector of the correct dimension.""" + return np.random.normal(size=D) + 1j * np.random.normal(size=D) + + +class TestABCData(TestMatVecData): # TODO re-add inheritance + #################### Init ###################### + def test_non_symmetric_matrix_raises_AssertionError(self, B, C): + non_symmetric_mat = np.eye(10) # TODO factory method for this + non_symmetric_mat[0] += np.array(range(10)) + with pytest.raises(AssertionError): + ABCData([non_symmetric_mat], B, C) + + ################## Negative #################### + # NOTE : tested in parent class + + ################## Equality #################### + # NOTE : tested in parent class + + ################## Addition #################### + # NOTE : tested in parent class + + ################ Subtraction ################### + # NOTE : tested in parent class + + ############# Scalar division ################## + # NOTE : tested in parent class + + ############### Multiplication ################# + def test_object_mul_result_has_correct_number_of_A_and_b_elements(self, DATA, OTHER, N): + post_op_result = DATA * OTHER + nb_combined_mats = post_op_result.A.shape[0] + nb_combined_vectors = post_op_result.b.shape[0] + nb_combined_constants = post_op_result.c.shape[0] + assert nb_combined_mats == nb_combined_vectors == nb_combined_constants == N * N + + def test_result_of_abc_objects_multiplication_is_correct(self, DATA, OTHER, X): + our_operation = (DATA * OTHER).value(X) + manual_operation = DATA.value(X) * OTHER.value(X) + assert np.isclose(our_operation, manual_operation) + + def test_abc_contraction_ket_into_unitary_1mode(self, DATA, OTHER, X): + ket = Gaussian(1) >> Dgate(*np.random.normal(size=2)) + unitary = Ggate(1) >> Dgate(*np.random.normal(size=2)) + KET = ABCData(*ket.bargmann()) + UNITARY = ABCData(*unitary.bargmann()) + ours = UNITARY[1] @ KET[0] + other = (ket >> unitary).bargmann() + assert np.allclose(ours.A[0], other[0]) + assert np.allclose(ours.b[0], other[1]) + assert np.isclose( + np.abs(ours.c[0]), np.abs(other[2]) + ) # note we can't check global phase :) + + def test_abc_contraction_ket_into_unitary_2mode(self, DATA, OTHER, X): + ket = Gaussian(2) >> Dgate(*np.random.normal(size=4)) + unitary = Ggate(2) >> Dgate(*np.random.normal(size=4)) + KET = ABCData(*ket.bargmann()) + UNITARY = ABCData(*unitary.bargmann()) + ours = UNITARY[2, 3] @ KET[0, 1] + other = (ket >> unitary).bargmann() + assert np.allclose(ours.A[0], other[0]) + assert np.allclose(ours.b[0], other[1]) + assert np.isclose( + np.abs(ours.c[0]), np.abs(other[2]) + ) # note we can't check global phase :) + + def test_abc_contraction_dm_into_channel_1mode(self, DATA, OTHER, X): + # old method + dm = Gaussian(1) >> Dgate(*np.random.normal(size=2)) >> Attenuator(0.4) + channel = Ggate(1) >> Dgate(*np.random.normal(size=2)) >> Attenuator(0.5) + other = (dm >> channel).bargmann() + + # new method + DM = ABCData(*dm.bargmann()) + CHOI = ABCData(*channel.bargmann()) + ours = DM[0, 1] @ CHOI[1, 3] # channel is out, in, out, in + + # compare + assert np.allclose(ours.A[0], other[0]) + assert np.allclose(ours.b[0], other[1]) + assert np.isclose(np.abs(ours.c[0]), np.abs(other[2])) + + def test_abc_contraction_generaldyne_1mode(self, DATA, OTHER, X): + # old method + ket = Gaussian(2) >> Dgate(*np.random.normal(size=4)) + proj = Gaussian(1) >> Dgate(*np.random.normal(size=2)) + _, prob, cov, means = gaussian.general_dyne(ket.cov, ket.means, proj.cov, proj.means, [0]) + A, b, c = bargmann.wigner_to_bargmann_psi(cov, means) + c *= np.sqrt(prob) + + # new method + KET = ABCData(*ket.bargmann()) + PROJ = ABCData(*proj.bargmann()) + ours = KET[0] @ PROJ[0] + + # compare + assert np.allclose(ours.A[0], A) + assert np.allclose(ours.b[0], b) + assert np.isclose(np.abs(ours.c[0]), np.abs(c)) diff --git a/tests/test_lab/test_representations/test_data/test_array_data.py b/tests/test_lab/test_representations/test_data/test_array_data.py new file mode 100644 index 000000000..c47901dda --- /dev/null +++ b/tests/test_lab/test_representations/test_data/test_array_data.py @@ -0,0 +1,98 @@ +# Copyright 2023 Xanadu Quantum Technologies Inc. + +# 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 + +# http://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. +""" This class corresponds to test child class for the ArrayData class. + +Unlike some of its -abstract- parent test classes, this class is meant to be run with pytest. + +Check parents test classe-s for more details on the rationale. + +The fixtures must correspond to the concrete class being tested, here ArrayData. +""" + +import numpy as np +import operator as op +import pytest +from copy import deepcopy + +from mrmustard.lab.representations.data.array_data import ArrayData +from mrmustard.utils.misc_tools import general_factory +from tests.test_lab.test_representations.test_data.test_data import TestData + + +######### Instantiating class to test ######### +@pytest.fixture +def TYPE(): + r"""Type of the object under test.""" + return ArrayData + + +@pytest.fixture +def PARAMS() -> dict: + r"""Parameters for the class instance which is created.""" + params_dict = {"array": np.ones(10)} + return params_dict + + +@pytest.fixture() +def DATA(PARAMS) -> ArrayData: + r"""Instance of the class that must be tested.""" + return general_factory(ArrayData, **PARAMS) + + +@pytest.fixture() +def OTHER(DATA) -> ArrayData: + r"""Another instance of the class that must be tested.""" + return deepcopy(DATA) + + +class TestArrayData(TestData): + r"""Test class for the ArrayData objects, inherits from parent test class TestData.""" + + ################## Negative #################### + def test_negative_returns_new_object_with_element_wise_negative_of_array(self, DATA): + new_data = deepcopy(DATA) + neg_data = -new_data # neg of the object + assert np.allclose(neg_data.array, -DATA.array) + + ################## Equality #################### + # NOTE: tested in parent + + # ############# Arity-2 operations ################ + @pytest.mark.parametrize("operator", [op.add, op.sub]) + def test_arity2_operation_returns_element_wise_operation_on_array(self, DATA, OTHER, operator): + res_from_object_op = operator(DATA, OTHER) + res_from_manual_op = operator(DATA.array, OTHER.array) + assert np.allclose(res_from_object_op.array, res_from_manual_op) + + ################## Addition and Subtraction #################### + @pytest.mark.parametrize("operator", [op.add, op.sub]) + def test_new_object_created_by_add_sub_operations_has_same_attribute_shapes_as_old_object( + self, DATA, OTHER, operator + ): + for k in DATA.__dict__.keys(): + new_data = operator(DATA, OTHER) + try: # numpy array attributes + assert getattr(DATA, k).shape == getattr(new_data, k).shape + except AttributeError: # scalar attributes + pass + + # ############# Scalar division ################## + @pytest.mark.parametrize("x", [2]) + def test_truediv_returns_new_object_with_element_wise_division_performed(self, DATA, x): + res_from_object_op = DATA / x + res_from_manual_op = DATA.array / x + assert np.allclose(res_from_object_op.array, res_from_manual_op) + + ############### Outer product ################## + # NOTE : not implemented => not tested diff --git a/tests/test_lab/test_representations/test_data/test_data.py b/tests/test_lab/test_representations/test_data/test_data.py new file mode 100644 index 000000000..4d58f2826 --- /dev/null +++ b/tests/test_lab/test_representations/test_data/test_data.py @@ -0,0 +1,156 @@ +# Copyright 2023 Xanadu Quantum Technologies Inc. + +# 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 + +# http://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. +""" This class corresponds to the abstract parent class for all Data objects. + +This file is *NOT* meant to be run on its own with pytest but meant to be inherited by children +test classes which will be run with pytest. + +Test inheritance - why? +- - - - - - - - - - - - +Just like standard inheritance, test inheritance allows us to test for properties that are common +throughout generations without having to use parameterization over types. +It promotes the Open-Closed principle from SOLID by making the implementation of new testing +classes seamless. When creating a new Data class NewData which inherits from Data, then the +correspodning test class TestNewData will inherit from TestData and guarantee that everything which +held true for the parents holds true for the child. +Moreover, Test Driven development (TDD) benefits from test inheritance as a test class can easily be +created for any new class. + +Test inheritance - how? +- - - - - - - - - - - - +In order to allow for test class inheritance, a few adjustments are necessary: + +1) A couple of pytest fixtures must be redefined in each child test file and adapted to match the +specific class of the child. + +2) We must accept that the instance created by OTHER is by default a deepcopy of the instance +created by the DATA fixture. Developpers are welcome to code their own versions of the OTHER +fixture and we encourage them to do so whenever the need arises. +We however advocate against using the `mark.parametrize` fixture for instances of `other` since it +will break when inheriting the test. With `mark.parametrize`, the class instance will be of the type +defined in the file where the test was written, blocking resolution sequence. +""" + +import operator as op +from copy import deepcopy + +import numpy as np +import pytest + +from mrmustard.utils.misc_tools import general_factory +from tests.test_lab.test_representations.test_data.mock_data import ( + MockCommonAttributesObject, + MockData, + MockNoCommonAttributesObject, +) + + +######### Instantiating class to test ######### +@pytest.fixture +def TYPE(): + r"""Type of the object under test.""" + return MockData + + +@pytest.fixture +def PARAMS() -> dict: + r"""Parameters for the class instance which is created, here all are None.""" + params_list = ["mat", "vec", "coeffs", "array", "cutoffs"] + return dict.fromkeys(params_list) + + +@pytest.fixture() +def DATA(PARAMS) -> MockData: + r"""Instance of the class that must be tested, here the class is a Mock.""" + return general_factory(MockData, **PARAMS) + + +@pytest.fixture() +def OTHER(DATA) -> MockData: + r"""Another instance of the class that must be tested, here again, the class is a Mock.""" + return deepcopy(DATA) + + +class TestData: + r"""Parent class for testing all children of the Data class. + + Here only the behaviours common to all children are tested. + """ + + ######### Common to different methods ######### + def test_original_data_object_is_left_untouched_after_applying_negation(self, DATA): + pre_op_data_control = deepcopy(DATA) + _ = -DATA + assert DATA == pre_op_data_control + + @pytest.mark.parametrize("operator", [op.add, op.sub, op.eq]) # op.and_ + def test_original_data_object_is_left_untouched_after_applying_operation_of_arity_two( + self, DATA, OTHER, operator + ): + pre_op_data_control = deepcopy(DATA) + _ = operator(DATA, OTHER) + assert DATA == pre_op_data_control + + @pytest.mark.parametrize("other", [MockData(), MockCommonAttributesObject(), deepcopy(DATA)]) + def test_truediv_raises_TypeError_if_divisor_is_not_scalar(self, DATA, other): + with pytest.raises(TypeError): + DATA / other + + @pytest.mark.parametrize("other", [MockNoCommonAttributesObject()]) + @pytest.mark.parametrize("operator", [op.add, op.sub, op.mul, op.truediv, op.eq]) # op.and_ + def test_algebraic_op_raises_Error_if_other_object_is_of_wrong_type( + self, DATA, other, operator + ): + with pytest.raises(TypeError): + operator(DATA, other) + + # @pytest.mark.parametrize("operator", [op.add, op.sub]) + # def test_new_object_created_by_add_sub_operations_has_same_attribute_shapes_as_old_object( + # self, DATA, OTHER, operator + # ): + # for k in DATA.__dict__.keys(): + # new_data = operator(DATA, OTHER) + # try: # numpy array attributes + # assert getattr(DATA, k).shape == getattr(new_data, k).shape + # except AttributeError: # scalar attributes + # pass + + # @pytest.mark.parametrize("operator", [op.neg]) + # def test_new_object_created_by_negation_has_same_attribute_shapes_as_old_object( + # self, DATA, operator + # ): + # for k in DATA.__dict__.keys(): + # new_data = operator(DATA) + # try: # numpy array attributes + # assert getattr(DATA, k).shape[0, :] == getattr(new_data, k).shape[0, :] + # except AttributeError: # scalar attributes + # pass + + ################## Equality #################### + def test_when_all_attributes_are_equal_objects_are_equal(self, DATA, PARAMS, TYPE): + other = general_factory(TYPE, **PARAMS) + for k in DATA.__dict__.keys(): + getattr(other, k) + try: # non-array, non-list attributes + assert getattr(DATA, k) == getattr(other, k) + except ValueError: + assert np.allclose(getattr(DATA, k), getattr(other, k)) + assert DATA == other + + def test_copy_of_same_objects_are_equal(self, DATA): + other_same = deepcopy(DATA) + assert DATA == other_same + + ############### Outer product ################## + # NOTE : not implemented => not tested diff --git a/tests/test_lab/test_representations/test_data/test_gaussian_data.py b/tests/test_lab/test_representations/test_data/test_gaussian_data.py new file mode 100644 index 000000000..9866fb782 --- /dev/null +++ b/tests/test_lab/test_representations/test_data/test_gaussian_data.py @@ -0,0 +1,173 @@ +# Copyright 2023 Xanadu Quantum Technologies Inc. + +# 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 + +# http://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. +""" This class corresponds to test child class for the GaussianData class. + +Unlike some of its -abstract- parent test classes, this class is meant to be run with pytest. + +Check parents test classe-s for more details on the rationale. + +The fixtures must correspond to the concrete class being tested, here GaussianData. + +""" + +import numpy as np +import operator as op +import pytest +from copy import deepcopy +from scipy.stats import multivariate_normal as mvg + +from mrmustard.lab.representations.data.gaussian_data import GaussianData +from mrmustard.typing import Matrix, Scalar, Vector +from mrmustard.utils.misc_tools import general_factory +from tests.test_lab.test_representations.test_data.test_matvec_data import TestMatVecData + +np.random.seed(42) + + +######### Instantiating class to test ######### +@pytest.fixture +def D() -> int: + """The dimension: matrices will be DxD and vectors will be D. D must be even.""" + return 4 + + +@pytest.fixture +def N() -> int: + """The number of elements in the batch.""" + return 3 + + +@pytest.fixture +def TYPE(): + r"""Type of the object under test.""" + return GaussianData + + +@pytest.fixture +def COV(N, D) -> Matrix: + r"""Some batch of matrices for the object's parameterization.""" + c = np.random.normal(size=(N, D, D)) + c = c + np.transpose(c, (0, 2, 1)) # symmetrize + c = np.einsum("bij,bkj->bik", c, np.conj(c)) # make positive semi-definite + return c + + +@pytest.fixture +def MEANS(N, D) -> Vector: + r"""Some batch of vectors for the object's parameterization.""" + return np.random.normal(size=(N, D)) + + +@pytest.fixture +def COEFFS(N) -> Scalar: + r"""Some batch of scalars for the object's parameterization.""" + return np.random.normal(size=N) + + +@pytest.fixture +def PARAMS(COV, MEANS, COEFFS) -> dict: + r"""Parameters for the class instance which is created.""" + params_dict = {"cov": COV, "means": MEANS, "coeffs": COEFFS} + return params_dict + + +@pytest.fixture() +def DATA(TYPE, PARAMS) -> GaussianData: + r"""Instance of the class that must be tested.""" + return general_factory(TYPE, **PARAMS) + + +@pytest.fixture() +def OTHER(DATA) -> GaussianData: + r"""Another instance of the class that must be tested.""" + return deepcopy(DATA) + + +class TestGaussianData(TestMatVecData): + #################### Init ###################### + def test_defining_neither_cov_nor_mean_raises_ValueError(self, COEFFS): + with pytest.raises(ValueError): + GaussianData(coeffs=COEFFS) + + @pytest.mark.parametrize("x", [2]) + def test_if_3D_cov_is_none_then_initialized_at_npeye_of_correct_shape(self, COEFFS, x, N): + comparison_covariance = np.stack([np.eye(x)] * N) + means = np.stack([np.ones(x)] * N) + print(comparison_covariance.shape, means.shape) + gaussian_data = GaussianData(means=means, coeffs=COEFFS) + assert np.allclose(gaussian_data.cov, comparison_covariance) + + @pytest.mark.parametrize("x", [2, 10, 250]) + def test_if_2D_mean_is_none_then_initialized_at_npzeros_of_correct_shape(self, COEFFS, x, N): + covariance = np.stack([np.eye(x)] * N) + comparison_means = np.stack([np.zeros(x)] * N) + gaussian_data = GaussianData(cov=covariance, coeffs=COEFFS) + assert np.allclose(gaussian_data.means, comparison_means) + + @pytest.mark.skip(reason="Currently not implemented") + def test_non_symmetric_covariance_raises_ValueError(self, MEAN): + with pytest.raises(ValueError): + non_symmetric_mat = np.eye(10) + non_symmetric_mat[0] += np.array(range(10)) + GaussianData(cov=[non_symmetric_mat], means=MEAN) + + # ################## Negative #################### + # # NOTE : tested in parent class + + # ################## Equality #################### + # # NOTE : tested in parent class + + # ################## Addition #################### + # # NOTE : tested in parent class + + # ################ Subtraction ################### + # # NOTE : tested in parent class + + # ############# Scalar division ################## + # # NOTE : tested in parent class + + # ############## Multiplication ################## + + @pytest.mark.parametrize("c", [2, 7, 200]) + def test_if_given_scalar_mul_multiplies_coeffs_and_nothing_else(self, DATA, c): + pre_op_data = deepcopy(DATA) + multiplied_data = DATA * c + assert np.allclose(multiplied_data.c, (pre_op_data.coeffs * c)) # coeffs are multiplied + assert np.allclose(multiplied_data.cov, pre_op_data.cov) # unaltered + assert np.allclose(multiplied_data.means, pre_op_data.means) # unaltered + + @pytest.mark.parametrize("operator", [op.add, op.mul, op.sub]) + def test_operating_on_two_gaussians_returns_a_gaussian_object( + self, DATA, OTHER, TYPE, operator + ): + output = operator(DATA, OTHER) + assert isinstance(output, TYPE) + + @pytest.mark.parametrize( + "x", [np.array([-0.1, 0.2, -0.3, 0.4]), np.array([0.1, -0.2, 0.3, -0.4])] + ) + def test_multiplication_is_correct(self, DATA, OTHER, N, x): + our_res = (DATA * OTHER).value(x) + + scipy_res = 0.0 + for i in range(N): + g1 = DATA.coeffs[i] * mvg.pdf(x, mean=DATA.means[i], cov=DATA.cov[i]) + for j in range(N): + g2 = OTHER.coeffs[j] * mvg.pdf(x, mean=OTHER.means[j], cov=OTHER.cov[j]) + scipy_res += g1 * g2 + + assert np.allclose(our_res, scipy_res) + + # ############### Outer product ################## + # # NOTE : not implemented => not tested diff --git a/tests/test_lab/test_representations/test_data/test_matvec_data.py b/tests/test_lab/test_representations/test_data/test_matvec_data.py new file mode 100644 index 000000000..d6176d7c1 --- /dev/null +++ b/tests/test_lab/test_representations/test_data/test_matvec_data.py @@ -0,0 +1,177 @@ +# Copyright 2023 Xanadu Quantum Technologies Inc. + +# 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 + +# http://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. +""" This class corresponds to test child class for the MatVecData class. + +Unlike some of its -abstract- parent test classes, this class is meant to be run with pytest. + +Check parents test classe-s for more details on the rationale. + +The fixtures must correspond to the concrete class being tested, here MatVecData. +""" +import numpy as np +import operator as op +import pytest +from copy import deepcopy + +from mrmustard import settings +from mrmustard.lab.representations.data.matvec_data import MatVecData +from mrmustard.typing import Batch, Matrix, Scalar, Vector +from mrmustard.utils.misc_tools import general_factory +from tests.test_lab.test_representations.test_data.test_data import TestData + +settings.SEED = 42 + +D = 10 # dimension, matrix will be DxD while means will be D +N = 3 # number of elements in the batch + + +################## FIXTURES ################### +@pytest.fixture +def D(): + """The dimension: matrices will be DxD and vectors will be D.""" + return 5 + + +@pytest.fixture +def N(): + """The number of elements in the batch.""" + return 3 + + +@pytest.fixture +def TYPE(): + r"""Type of the object under test.""" + return MatVecData + + +@pytest.fixture +def MAT(D, N) -> Batch[Matrix]: + r"""Some batch of matrices for the object's parameterization.""" + mats = [] + for _ in range(N): + m = settings.rng.normal(size=(D, D)) + 1j * settings.rng.normal(size=(D, D)) + m = m + m.T # symmetrize A + mats.append(m) + return np.array(mats) + + +@pytest.fixture +def VEC(D, N) -> Batch[Vector]: + r"""Some batch of vectors for the object's parameterization.""" + return np.array( + [settings.rng.normal(size=D) + 1j * settings.rng.normal(size=D) for _ in range(N)] + ) + + +@pytest.fixture +def COEFFS(N) -> Batch[Scalar]: + r"""Some batch of scalars for the object's parameterization.""" + return np.array([settings.rng.normal() + 1j * settings.rng.normal() for _ in range(N)]) + + +@pytest.fixture +def PARAMS(MAT, VEC, COEFFS) -> dict: + r"""Parameters for the class instance which is created.""" + params_dict = {"mat": MAT, "vec": VEC, "coeffs": COEFFS} + return params_dict + + +@pytest.fixture() +def DATA(TYPE, PARAMS) -> MatVecData: + r"""Instance of the class that must be tested.""" + return general_factory(TYPE, **PARAMS) + + +@pytest.fixture() +def OTHER(DATA) -> MatVecData: + r"""Another instance of the class that must be tested.""" + return deepcopy(DATA) + + +class TestMatVecData(TestData): # TestData + #################### Init ###################### + def if_coeffs_not_given_they_are_equal_to_1(self, TYPE, PARAMS): + params_without_coeffs = deepcopy(PARAMS) + del params_without_coeffs["coeffs"] + new_data = general_factory(TYPE, **params_without_coeffs) + n = len(new_data.coeffs) + assert np.allclose(new_data.coeffs, np.ones(n)) + + ################## Negative #################### + def test_negative_returns_new_object_with_neg_coeffs_and_unaltered_mat_and_vec(self, DATA): + pre_op_data = deepcopy(DATA) + neg_data = -DATA + manual_neg_coeffs = [-c for c in pre_op_data.coeffs] + assert np.allclose(manual_neg_coeffs, neg_data.coeffs) + assert np.allclose(neg_data.mat, pre_op_data.mat) + assert np.allclose(neg_data.vec, pre_op_data.vec) + + # ################## Equality #################### + # # NOTE: tested in parent + + # ########### Addition / subtraction ############# + # TODO: test correctness of addition/subtraction + # testing that the 1st elt of 2nd object is at index len of 1st elt + 1 + # just this basic stuff + @pytest.mark.parametrize("operator", [op.add, op.sub]) + def test_length_of_added_subtracted_objects_is_same_as_previous_if_objects_are_same( + self, DATA, operator + ): + l0 = DATA.coeffs.shape[0] + new_data = operator(DATA, DATA) + assert 2 * l0 == new_data.coeffs.shape[0] + + @pytest.mark.parametrize("operator", [op.add, op.sub]) + def test_length_of_added_subtracted_objects_is_sum_of_lengths_if_objects_are_diff( + self, DATA, TYPE, operator + ): + mat = DATA.mat + vec = DATA.vec + 1 + coeffs = DATA.coeffs + 2 + parameters = (mat, vec, coeffs) + other_data = general_factory(TYPE, *parameters) + l0 = DATA.coeffs.shape[0] + l1 = other_data.coeffs.shape[0] + new_data = operator(DATA, other_data) + total_length = l0 + l1 + assert total_length == new_data.coeffs.shape[0] + + ####### Scalar division / multiplication ######## + @pytest.mark.parametrize("operator", [op.truediv, op.mul]) + @pytest.mark.parametrize("x", [0.001, 7, 100]) + def test_scalar_mul_or_div_if_mat_vec_same_dont_change_mat_vec_but_change_coeffs( + self, DATA, operator, x + ): + precision = 3 + preop_data = deepcopy(DATA) + postop_data = operator(DATA, x) + f = lambda x: np.linalg.norm(x) + norms_of_mats_preop = set(np.around([f(m) for m in preop_data.mat], precision)) + norms_of_mats_postop = set(np.around([f(m) for m in postop_data.mat], precision)) + norms_of_vecs_preop = set(np.around([f(v) for v in preop_data.mat], precision)) + norms_of_vecs_postop = set(np.around([f(v) for v in postop_data.mat], precision)) + coeffs_preop = set(np.around(preop_data.coeffs, precision)) + coeffs_postop = set(np.around(postop_data.coeffs, precision)) + assert norms_of_mats_preop == norms_of_mats_postop + assert norms_of_vecs_preop == norms_of_vecs_postop + assert coeffs_preop != coeffs_postop + + # ############### Tensor product ################## + # TODO: to test in children + + # ############### Multiplication ################## + # NOTE: tested in children + + # ############### Outer product ################## + # NOTE: not implented yet so no tests diff --git a/tests/test_lab/test_representations/test_data/test_symplectic_data.py b/tests/test_lab/test_representations/test_data/test_symplectic_data.py new file mode 100644 index 000000000..1205ee5c7 --- /dev/null +++ b/tests/test_lab/test_representations/test_data/test_symplectic_data.py @@ -0,0 +1,135 @@ +# Copyright 2023 Xanadu Quantum Technologies Inc. + +# 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 + +# http://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. +""" This class corresponds to test child class for the MatVecData class. + +Unlike some of its -abstract- parent test classes, this class is meant to be run with pytest. + +Check parents test classe-s for more details on the rationale. + +The fixtures must correspond to the concrete class being tested, here SymplecticData. +""" + +import numpy as np +import operator as op +import pytest +from copy import deepcopy + +from mrmustard.lab.representations.data.symplectic_data import SymplecticData +from mrmustard.typing import Matrix, Scalar, Vector +from mrmustard.utils.misc_tools import general_factory +from tests.test_lab.test_representations.test_data.test_matvec_data import TestMatVecData +from tests.test_lab.test_representations.test_data.tools_for_tests import ( + helper_mat_vec_unchanged_computed_coeffs_are_correct, +) + + +######### Instantiating class to test ######### +@pytest.fixture +def D() -> int: + """The dimension: matrices will be DxD and vectors will be D.""" + return 4 + + +@pytest.fixture +def N() -> int: + """The number of elements in the batch.""" + return 3 + + +@pytest.fixture +def TYPE(): + r"""Type of the object under test.""" + return SymplecticData + + +@pytest.fixture +def SYMPLECTIC(N) -> Matrix: # TODO: generator for symplectic matrices + r"""A symplectic matrix used for object parameterization. + Taken from https://mathworld.wolfram.com/SymplecticGroup.html + """ + mats = [] + for _ in range(N): + m = np.array([[1, 0, 0, 1], [0, 1, 1, 0], [0, 0, 1, 0], [0, 0, 0, 1]]) + mats.append(m) + return np.array(mats) + + +@pytest.fixture +def DISPLACEMENT(N, D) -> Vector: + r"""Some vector for the object's parameterization.""" + return np.array([np.random.normal(size=D) + 1j * np.random.normal(size=D) for _ in range(N)]) + + +@pytest.fixture +def COEFFS(N) -> Scalar: + r"""Some scalar for the object's parameterization.""" + return np.array([np.random.normal() + 1j * np.random.normal() for _ in range(N)]) + + +@pytest.fixture +def PARAMS(SYMPLECTIC, DISPLACEMENT, COEFFS) -> dict: + r"""Parameters for the class instance which is created.""" + params_dict = {"symplectic": SYMPLECTIC, "displacement": DISPLACEMENT, "coeffs": COEFFS} + return params_dict + + +@pytest.fixture() +def DATA(TYPE, PARAMS) -> SymplecticData: + r"""Instance of the class that must be tested.""" + return general_factory(TYPE, **PARAMS) + + +@pytest.fixture() +def OTHER(DATA) -> SymplecticData: + r"""Another instance of the class that must be tested.""" + return deepcopy(DATA) + + +class TestSymplecticData(TestMatVecData): + #################### Init ###################### + def test_init_with_a_non_symplectic_matrix_raises_ValueError(self, DISPLACEMENT, COEFFS): + non_symplectic_mat = np.eye(10) # TODO factory method for this + non_symplectic_mat[0] += np.array(range(10)) + with pytest.raises(ValueError): + SymplecticData([non_symplectic_mat], DISPLACEMENT, COEFFS) + + ################## Negative #################### + # NOTE : tested in parent class + + ################## Equality #################### + # NOTE : tested in parent class + + ################## Addition #################### + # NOTE : tested in parent class + + ################ Subtraction ################### + # NOTE : tested in parent class + + ############# Scalar division ################## + # NOTE : tested in parent class + + ############### Multiplication ################# + + def test_mul_raises_TypeError_with_object(self, DATA, OTHER): + with pytest.raises(TypeError): + OTHER * DATA + + @pytest.mark.parametrize("x", [0, 2, 10, 2500]) # the only necessary correctness for Symplectic + def test_mul_with_scalar_multiplies_coeffs_and_leaves_mat_and_vec_unaltered(self, DATA, x): + pre_op_data = deepcopy(DATA) + post_op_data = DATA * x + helper_mat_vec_unchanged_computed_coeffs_are_correct(post_op_data, pre_op_data, op.mul, x) + + ############### Outer product ################## + # NOTE : not implemented => not tested diff --git a/tests/test_lab/test_representations/test_data/test_wavefunction_array_data.py b/tests/test_lab/test_representations/test_data/test_wavefunction_array_data.py new file mode 100644 index 000000000..72b164c11 --- /dev/null +++ b/tests/test_lab/test_representations/test_data/test_wavefunction_array_data.py @@ -0,0 +1,131 @@ +# Copyright 2023 Xanadu Quantum Technologies Inc. + +# 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 + +# http://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. +""" This class corresponds to test child class for the MatVecData class. + +Unlike some of its -abstract- parent test classes, this class is meant to be run with pytest. + +Check parents test classe-s for more details on the rationale. + +The fixtures must correspond to the concrete class being tested, here WavefunctionArrayData. +""" + +import numpy as np +import operator as op +import pytest +from copy import deepcopy + +from mock_data import MockNoCommonAttributesObject +from mrmustard.lab.representations.data.wavefunctionarray_data import ( + WavefunctionArrayData, +) +from mrmustard.utils.misc_tools import general_factory +from tests.test_lab.test_representations.test_data.test_array_data import TestArrayData + + +######### Instantiating class to test ######### +@pytest.fixture +def TYPE(): + r"""Type of the object under test.""" + return WavefunctionArrayData + + +@pytest.fixture +def PARAMS() -> dict: + r"""Parameters for the class instance which is created.""" + params = {"qs": np.ones(10), "array": np.ones(10)} + return params + + +@pytest.fixture() +def DATA(PARAMS) -> WavefunctionArrayData: + r"""Instance of the class that must be tested.""" + return general_factory(WavefunctionArrayData, **PARAMS) + + +@pytest.fixture() +def OTHER(DATA) -> WavefunctionArrayData: + r"""Alternative instance of the class to be tested.""" + return deepcopy(DATA) + + +class TestWavefunctionArrayData(TestArrayData): + r"""Class for tests of the WavefunctionArrayData class, inherits from parent tests.""" + + ######### Common to different methods ######### + @pytest.mark.parametrize( + "other", + [ + WavefunctionArrayData(qs=np.zeros(10), array=np.ones(10)), + WavefunctionArrayData(qs=np.zeros(10), array=np.zeros(10)), + WavefunctionArrayData(qs=np.zeros(9), array=np.ones(10)), + WavefunctionArrayData(qs=np.zeros(8), array=np.zeros(10)), + WavefunctionArrayData(qs=np.eye(8), array=np.zeros(10)), + ], + ) + @pytest.mark.parametrize("operator", [op.add, op.sub, op.mul]) + def test_different_value_or_shape_of_qs_raises_ValueError(self, DATA, other, operator): + with pytest.raises(ValueError): + operator(DATA, other) + + @pytest.mark.parametrize("other", [WavefunctionArrayData(qs=np.ones(10), array=np.ones(10))]) + @pytest.mark.parametrize("operator", [op.add, op.sub, op.mul]) # op.and_ + def test_qs_for_new_objects_are_same_as_initial_qs_after_arity2_operation( + self, DATA, other, operator + ): + new_obj = operator(DATA, other) + assert np.allclose(DATA.qs, new_obj.qs) + + def test_qs_for_new_objects_are_same_as_initial_qs_after_negation(self, DATA): + new_obj = -DATA + assert np.allclose(DATA.qs, new_obj.qs) + + #################### Init ###################### + # NOTE : tested in parent + + ################## Equality #################### + @pytest.mark.parametrize( + "other, truth_val", + [ + (WavefunctionArrayData(qs=np.zeros(10), array=np.ones(10)), False), + (WavefunctionArrayData(qs=np.zeros(10), array=np.zeros(10)), False), + ], + ) + def test_eq_returns_false_if_qs_different_irrespective_of_array(self, DATA, other, truth_val): + assert (DATA == other) == truth_val + + ########### Object multiplication ############## + # NOTE : tested in parent + + ############### Outer product ################## + # NOTE : not implemented => not tested + + #################### Other ####################### + @pytest.mark.parametrize( + "other, truth_val", + [ + (WavefunctionArrayData(qs=np.ones(10), array=np.zeros(10)), True), + (WavefunctionArrayData(qs=np.ones(10), array=np.ones(10)), True), + (WavefunctionArrayData(qs=np.zeros(10), array=np.zeros(10)), False), + (WavefunctionArrayData(qs=np.zeros(10), array=np.ones(10)), False), + ], + ) + def test_qs_is_same_returns_true_when_same_qs_and_false_when_diff_qs_irrespective_of_array( + self, DATA, other, truth_val + ): + assert DATA._qs_is_same(other) == truth_val + + @pytest.mark.parametrize("other", [MockNoCommonAttributesObject()]) + def test_qs_is_same_raises_TypeError_if_other_has_no_qs(self, DATA, other): + with pytest.raises(TypeError): + DATA._qs_is_same(other) diff --git a/tests/test_lab/test_representations/test_data/tools_for_tests.py b/tests/test_lab/test_representations/test_data/tools_for_tests.py new file mode 100644 index 000000000..e639eee32 --- /dev/null +++ b/tests/test_lab/test_representations/test_data/tools_for_tests.py @@ -0,0 +1,60 @@ +# Copyright 2023 Xanadu Quantum Technologies Inc. + +# 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 + +# http://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. +""" This module contains helper functions used in the tests for representations. +""" + +import numpy as np + +all = [ + "helper_coeffs_are_computed_correctly", + "helper_mat_vec_unchanged_computed_coeffs_are_correct", +] + + +def helper_coeffs_are_computed_correctly(new_data_object, old_data_object, operator, x) -> None: + r"""Helper assert function which ensures the coefficients are computed correctly. + + Based on the given operator and a scalar, this test ensures that the coefficients are + applied the element-wise operation. + + Args: + new_data_object: the data object that was created by the operation + old_data_object: the initial data object before operation + operator: the operator which should be applied, either + or * + x: the item by which to multiply or add the coefficients + + Returns: + None (performs the assert) + """ + manually_computed_coeffs = operator(old_data_object.coeffs, x) + assert np.allclose(new_data_object.coeffs, manually_computed_coeffs) + + +def helper_mat_vec_unchanged_computed_coeffs_are_correct( + new_data_object, old_data_object, operator, x +) -> None: + r"""Ensures the matrix and vector remain unchanged while the coefficients are updated. + + Args: + new_data_object: the data object that was created by the operation + old_data_object: the initial data object before operation + operator: the operator which should be applied, either + or * + x: the item by which to multiply or add the coefficients + + Returns: + None (performs the assert) + """ + helper_coeffs_are_computed_correctly(new_data_object, old_data_object, operator, x) + assert np.allclose(new_data_object.mat, old_data_object.mat) + assert np.allclose(new_data_object.vec, old_data_object.vec)