From 31b651a15d5c23cd7a055d2484aed78ed1d86441 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Duque=20Mesa?= <675763+sduquemesa@users.noreply.github.com> Date: Fri, 14 Oct 2022 15:19:08 -0400 Subject: [PATCH 01/53] =?UTF-8?q?Update=20CI=20to=20agree=20with=20new=20d?= =?UTF-8?q?ev=20policies=20=F0=9F=91=A9=F0=9F=8F=BD=E2=80=8D=E2=9A=96?= =?UTF-8?q?=EF=B8=8F=20(#166)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit **Context:** Now the default branch for Mr Mustard repository is develop. **Description of the Change:** This PR updates the CI checks so that - Every PR pointing to develop is checked using python 3.8 - Every PR pointing to develop is built and checked using python 3.8, 3.9, 3.10 so that code is surely releasable - adds codeowners file - adds `.coveragerc` file to avoid some non-sense codecov issues **Benefits:** CI checks now will be in sync with MrMustard development policies, this also reduces usage of CI checks (meaning less bills 💸 and less CO2 ☘️). Also, codecov should be less annoying from now. **Possible Drawbacks:** We'll have to wait for a full release cycle to check that all workflows are working as expected --- .coveragerc | 47 ++++++++++++++++++++++++++ .github/CODEOWNERS | 5 +++ .github/workflows/builds.yml | 56 +++++++++++++++++++++++++++++++ .github/workflows/format.yml | 21 +++++++++--- .github/workflows/tests.yml | 21 ++++++------ .github/workflows/upload.yml | 4 +-- mrmustard/__init__.py | 25 ++++++++++---- tests/test_math/test_interface.py | 56 +++++++++++++++++++++++++++++++ 8 files changed, 210 insertions(+), 25 deletions(-) create mode 100644 .coveragerc create mode 100644 .github/CODEOWNERS create mode 100644 .github/workflows/builds.yml create mode 100644 tests/test_math/test_interface.py diff --git a/.coveragerc b/.coveragerc new file mode 100644 index 000000000..98af845ac --- /dev/null +++ b/.coveragerc @@ -0,0 +1,47 @@ +# .coveragerc to control coverage.py +[run] +source = mrmustard + +[report] +# Regexes for lines to exclude from consideration +exclude_lines = + # Have to re-enable the standard pragma + pragma: no cover + + # Don't complain about missing debug-only code: + def __repr__ + def __eq__ + if self\.debug + + # print statements + def __str__ + def __format__ + def _print_list + + # Don't complain if tests don't hit defensive assertion code: + raise AssertionError + raise NotImplementedError + raise NotApplicableError + raise Exception + raise ValueError + raise ZeroDivisionError + raise TypeError + raise RuntimeError + raise IndexError + raise FileNotFoundError + raise NotADirectoryError + raise MergeFailure + return __version__ + if len(lst) != self._num_modes: + elif max(lst) >= self._trunc: + except NotImplementedError as err + + # Don't complain if non-runnable code isn't run: + if 0: + pass + if __name__ == .__main__.: + +ignore_errors = True + +[html] +directory = coverage_html_report diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 000000000..4150eeb7e --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1,5 @@ +# Code owners are automatically requested for review when someone opens a pull request that modifies code that they own. +# Code owners are not automatically requested to review draft pull requests. +# When you mark a draft pull request as ready for review, code owners are automatically notified. +# If you convert a pull request to a draft, people who are already subscribed to notifications are not automatically unsubscribed. +@XanaduAI/mrmustard-dev-team diff --git a/.github/workflows/builds.yml b/.github/workflows/builds.yml new file mode 100644 index 000000000..bd2bb9dc7 --- /dev/null +++ b/.github/workflows/builds.yml @@ -0,0 +1,56 @@ +name: builds +on: + push: + branches: + - main + pull_request: + branches: + - main + +jobs: + build: + runs-on: ubuntu-latest + env: + HYPOTHESIS_PROFILE: ci + + strategy: + fail-fast: false + matrix: + python-version: ['3.8', '3.9', '3.10'] + + steps: + - name: Cancel Previous Runs + uses: styfle/cancel-workflow-action@0.11.0 + with: + access_token: ${{ github.token }} + + - uses: actions/checkout@v3 + + - name: Set up Python + uses: actions/setup-python@v4 + with: + python-version: ${{ matrix.python-version }} + + - name: Build and install Mr Mustard + run: | + python -m pip install --upgrade pip wheel + python setup.py bdist_wheel + pip install dist/mrmustard*.whl + + - name: Install test dependencies + run: | + pip install -r requirements.txt + pip install -r requirements-dev.txt + pip install wheel pytest pytest-cov pytest-mock hypothesis --upgrade + + - name: Run tests + run: | + if [ ${{ matrix.python-version }} == "3.8" ]; then COVERAGE="--cov=mrmustard --cov-report=term-missing --cov-report=xml"; else COVERAGE=""; fi + python -m pytest tests -p no:warnings --tb=native ${{COVERAGE}} + + - name: Upload coverage to Codecov + uses: codecov/codecov-action@v3 + if: ${{ matrix.python-version }} == '3.8' + with: + files: ./coverage.xml + fail_ci_if_error: true diff --git a/.github/workflows/format.yml b/.github/workflows/format.yml index ec372587c..7ea0f2990 100644 --- a/.github/workflows/format.yml +++ b/.github/workflows/format.yml @@ -1,13 +1,24 @@ name: Formatting check on: -- pull_request + pull_request: jobs: black: runs-on: ubuntu-latest + steps: - - uses: actions/checkout@v1 - - name: Black Code Formatter - uses: lgeiger/black-action@v1.0.1 + - uses: actions/checkout@v3 + + - name: Set up Python + uses: actions/setup-python@v4 with: - args: "-l 100 mrmustard/ tests/ --check" + python-version: 3.8 + + - name: Install black formatter + run: | + python -m pip install --upgrade pip + pip install black + + - name: Check formatting + run: | + python -m black -l 100 mrmustard/ tests/ --check diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 3d0e4fd84..18a5ac9f5 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -2,27 +2,27 @@ name: Tests on: push: branches: - - main + - develop pull_request: jobs: - build: + pytest: runs-on: ubuntu-latest env: HYPOTHESIS_PROFILE: ci - strategy: - fail-fast: false - matrix: - python-version: ['3.8', '3.9', '3.10'] - steps: - - uses: actions/checkout@v2 + - name: Cancel Previous Runs + uses: styfle/cancel-workflow-action@0.11.0 + with: + access_token: ${{ github.token }} + + - uses: actions/checkout@v3 - name: Setup python ${{ matrix.python-version }} - uses: actions/setup-python@v2 + uses: actions/setup-python@v4 with: - python-version: ${{ matrix.python-version }} + python-version: '3.8' - name: Install dependencies run: | @@ -32,7 +32,6 @@ jobs: pip install wheel pytest pytest-cov --upgrade python setup.py bdist_wheel pip install dist/*.whl - export HYPOTHESIS_PROFILE=ci - name: Run tests run: python -m pytest tests --cov=mrmustard --cov-report=term-missing --cov-report=xml -p no:warnings --tb=native diff --git a/.github/workflows/upload.yml b/.github/workflows/upload.yml index 9f590f797..c25a3470d 100644 --- a/.github/workflows/upload.yml +++ b/.github/workflows/upload.yml @@ -10,10 +10,10 @@ jobs: HYPOTHESIS_PROFILE: ci steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: Set up Python - uses: actions/setup-python@v2 + uses: actions/setup-python@v4 with: python-version: 3.8 diff --git a/mrmustard/__init__.py b/mrmustard/__init__.py index fde823341..add7adb58 100644 --- a/mrmustard/__init__.py +++ b/mrmustard/__init__.py @@ -1,7 +1,19 @@ -# this is the topmost __init__.py file of the mrmustard package +# Copyright 2022 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 is the top-most `__init__.py` file of MrMustard package.""" -# from rich.pretty import install # NOTE: just for the looks -# install() from ._version import __version__ # pylint: disable=too-many-instance-attributes @@ -37,7 +49,7 @@ def backend(self): @backend.setter def backend(self, backend_name: str): - if backend_name not in ["tensorflow", "torch"]: + if backend_name not in ["tensorflow", "torch"]: # pragma: no cover raise ValueError("Backend must be either 'tensorflow' or 'torch'") self._backend = backend_name @@ -106,11 +118,10 @@ def about(): print("The Walrus version: {}".format(thewalrus.__version__)) print("TensorFlow version: {}".format(tensorflow.__version__)) - try: + try: # pragma: no cover import torch torch_version = torch.__version__ + print("Torch version: {}".format(torch_version)) except ImportError: torch_version = None - - print("Torch version: {}".format(torch_version)) diff --git a/tests/test_math/test_interface.py b/tests/test_math/test_interface.py new file mode 100644 index 000000000..4fee9dfd9 --- /dev/null +++ b/tests/test_math/test_interface.py @@ -0,0 +1,56 @@ +# Copyright 2022 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. + +""" +Unit tests for the :class:`Math`. +""" + +import pytest +from mrmustard.math import Math +from mrmustard import settings + +try: + import torch + + torch_available = True +except ImportError: + torch_available = False + + +def test_backend_redirection_tf(): + """Test Math class is redirecting calls to the backend set on MM settings""" + math = Math() + + settings.backend = "tensorflow" + assert math._MathInterface__instance.__module__ == "mrmustard.math.tensorflow" + + +@pytest.mark.skipif(not torch_available, reason="Test only works if Torch is installed") +def test_backend_redirection_torch(): + """Test Math class is redirecting calls to the backend set on MM settings""" + math = Math() + + settings.backend = "torch" + assert math._MathInterface__instance.__module__ == "mrmustard.math.torch" + + +def test_error_for_wrong_backend(): + """Test error is raise when using a backend that is not allowed""" + backend = settings.backend + with pytest.raises(ValueError) as exception_info: + settings.backend = "unexisting_backend" + assert exception_info.value.args[0] == f"Backend must be either 'tensorflow' or 'torch'" + + # set back to initial value to avoir side effects + settings.backend = backend From 4f9ade2c3361fb060358618e34729634bf14c353 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Duque=20Mesa?= <675763+sduquemesa@users.noreply.github.com> Date: Mon, 17 Oct 2022 11:01:41 -0500 Subject: [PATCH 02/53] =?UTF-8?q?Sampling=20from=20Gaussian=20measurements?= =?UTF-8?q?=20=F0=9F=8E=B2=20(#143)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit **Context:** Currently homodyne and heterodyne measurements on Mr Mustard require user-specified outcome values. **Description of the Change:** This PR implements sampling for homodyne and heterodyne measurements: when no measurement outcome value is specified (`result=None` for homodyne and `x, y = None, None` for heterodyne), the outcome is sampled from the measurement probability distribution and the conditional state (conditional on the outcome) on the remaining modes is generated. ```python import numpy as np from mrmustard.lab import Homodyne, Heterodyne, TMSV, SqueezedVacuum # conditional state from measurement conditional_state1 = TMSV(r=0.5, phi=np.pi)[0, 1] >> Homodyne(quadrature_angle=np.pi/2, result=None)[1] conditional_state2 = TMSV(r=0.5, phi=np.pi)[0, 1] >> Heterodyne(x=None, y=None)[1] # outcome probability outcome_prob1 = SqueezedVacuum(r=0.5) >> Homodyne(result=None) outcome_prob2 = SqueezedVacuum(r=0.5) >> Heterodyne(x=None, y=None) ``` To do so: - `tensorflow-probability==0.17.0` is added to Mr Mustard's dependencies. [TensorFlow Probability](https://www.tensorflow.org/probability/overview) provides integration of probabilistic methods with automatic differentiation and hardware acceleration (GPUs). Pytorch also provides similar functionality through the [`torch.distributions`](https://pytorch.org/docs/stable/distributions.html) module. - For gaussian states 𝛠, samples are drawn from the gaussian PDF generated by a highly squeezed state ξ and the state of interest, i.e., `PDF=Tr[𝛠 ξ]`. Sampling from the distribution is implemented using TensorFlow's multivariate normal distribution. - For Homodyne only: To calculate the pdf for Fock states the q-quadrature eigenfunctions `|x> Co-authored-by: elib20 <53090166+elib20@users.noreply.github.com> --- .github/CHANGELOG.md | 17 + mrmustard/lab/abstract/__init__.py | 2 +- mrmustard/lab/abstract/measurement.py | 142 +++++-- mrmustard/lab/abstract/state.py | 158 +++++--- mrmustard/lab/circuit.py | 3 +- mrmustard/lab/detectors.py | 250 ++++++++++-- mrmustard/lab/gates.py | 3 +- mrmustard/lab/states.py | 2 +- mrmustard/math/math_interface.py | 72 +++- mrmustard/math/tensorflow.py | 27 +- mrmustard/math/torch.py | 2 +- mrmustard/physics/fock.py | 2 +- mrmustard/physics/gaussian.py | 72 ++-- mrmustard/utils/homodyne.py | 243 ++++++++++++ requirements.txt | 1 + setup.py | 1 + tests/test_lab/test_detectors.py | 542 +++++++++++++++----------- tests/test_lab/test_states.py | 13 + tests/test_math/test_special.py | 39 ++ 19 files changed, 1189 insertions(+), 402 deletions(-) create mode 100644 mrmustard/utils/homodyne.py create mode 100644 tests/test_math/test_special.py diff --git a/.github/CHANGELOG.md b/.github/CHANGELOG.md index d0ad00f99..acf49d39f 100644 --- a/.github/CHANGELOG.md +++ b/.github/CHANGELOG.md @@ -2,6 +2,22 @@ ### New features +* Sampling for homodyne measurements is now integrated in Mr Mustard: when no measurement outcome value is +specified by the user, a value is sampled from the reduced state probability distribution and the +conditional state on the remaining modes is generated. +[(#143)](https://github.com/XanaduAI/MrMustard/pull/143) + + ```python + import numpy as np + from mrmustard.lab import Homodyne, TMSV, SqueezedVacuum + + # conditional state from measurement + conditional_state = TMSV(r=0.5, phi=np.pi)[0, 1] >> Homodyne(quadrature_angle=np.pi/2)[1] + + # measurement outcome + measurement_outcome = SqueezedVacuum(r=0.5) >> Homodyne() + ``` + ### Breaking changes ### Improvements @@ -79,6 +95,7 @@ This release contains contributions from (in alphabetical order): ``` [(#140)](https://github.com/XanaduAI/MrMustard/pull/140) + ### Breaking changes * The Parametrized and Training classes have been refactored: now trainable tensors are wrapped in an instance of the `Parameter` class. To define a set of parameters do diff --git a/mrmustard/lab/abstract/__init__.py b/mrmustard/lab/abstract/__init__.py index 5c1a641e9..bdc730ea3 100644 --- a/mrmustard/lab/abstract/__init__.py +++ b/mrmustard/lab/abstract/__init__.py @@ -16,5 +16,5 @@ """ from .state import State -from .measurement import FockMeasurement +from .measurement import FockMeasurement, Measurement from .transformation import Transformation diff --git a/mrmustard/lab/abstract/measurement.py b/mrmustard/lab/abstract/measurement.py index aa0341d76..50cbf2425 100644 --- a/mrmustard/lab/abstract/measurement.py +++ b/mrmustard/lab/abstract/measurement.py @@ -15,18 +15,99 @@ """This module contains the implementation of the class :class:`FockMeasurement`.""" from __future__ import annotations -from abc import ABC +from abc import ABC, abstractmethod from mrmustard.math import Math -from mrmustard.types import Tensor, Callable, Sequence, Iterable +from mrmustard.types import Tensor, Callable, Sequence, Iterable, Union from mrmustard import settings from .state import State math = Math() -class FockMeasurement(ABC): - r"""A Fock measurement projecting onto a Fock measurement pattern. +class Measurement(ABC): + """this is an abstract class holding the common methods and properties that any measurement should + implement + + Args: + outcome (optional, List[float] or Array): the result of the measurement + modes (List[int]): the modes on which the measurement is acting on + """ + + def __init__(self, outcome: Tensor, modes: Iterable[int]) -> None: + super().__init__() + + if modes is None: + raise ValueError(f"Modes not defined for {self.__class__.__name__}.") + self._modes = modes + + self._is_postselected = False if outcome is None else True + """used to evaluate if the measurement outcome should be + sampled or is already defined by the user (postselection)""" + + @property + def modes(self): + r"""returns the modes being measured""" + return self._modes + + @property + def num_modes(self): + r"""returns the number of modes being measured""" + return len(self.modes) + + @property + def postselected(self): + r"""returns whether the measurement is postselected, i.e, a outcome has been provided""" + return self._is_postselected + + @property + @abstractmethod + def outcome(self): + """Returns outcome of the measurement. If no measurement has been carried out returns `None`.""" + ... + + @abstractmethod + def _measure_fock(self, other: State) -> Union[State, float]: + ... + + @abstractmethod + def _measure_gaussian(self, other: State) -> Union[State, float]: + ... + + def primal(self, other: State) -> Union[State, float]: + """performs the measurement procedure according to the representation of the incoming state""" + if other.is_gaussian: + return self._measure_gaussian(other) + + return self._measure_fock(other) + + def __lshift__(self, other) -> Union[State, float]: + if isinstance(other, State): + self.primal(other) + + raise TypeError( + f"Cannot apply Measurement '{self.__qualname__}' to '{other.__qualname__}'." + ) + + def __getitem__(self, items) -> Callable: + """Assign modes via the getitem syntax: allows measurements to be used as + ``output = meas[0,1](input)``, e.g. measuring modes 0 and 1. + """ + if isinstance(items, int): + modes = [items] + elif isinstance(items, slice): + modes = list(range(items.start, items.stop, items.step)) + elif isinstance(items, (Sequence, Iterable)): + modes = list(items) + else: + raise ValueError(f"{items} is not a valid slice or list of modes.") + self._modes = modes + + return self + + +class FockMeasurement(Measurement): + """A Fock measurement projecting onto a Fock measurement pattern. It works by representing the state in the Fock basis and then applying a stochastic channel matrix ``P(meas|n)`` to the Fock probabilities (belief propagation). @@ -35,11 +116,19 @@ class FockMeasurement(ABC): in the Fock basis. """ - def __init__(self) -> None: - super().__init__() - self.modes = None + def __init__(self, outcome: Tensor, modes: Iterable[int], cutoffs: Iterable[int]) -> None: - def primal(self, state: State) -> Tensor: + self._cutoffs = cutoffs or [settings.PNR_INTERNAL_CUTOFF] * len(modes) + super().__init__(outcome, modes) + + @property + def outcome(self): + raise NotImplementedError + + def _measure_gaussian(self, other: State) -> Union[State, float]: + return self._measure_fock(other) + + def _measure_fock(self, other: State) -> Union[State, float]: r""" Returns a tensor representing the post-measurement state in the unmeasured modes in the Fock basis. The first `N` indices of the returned tensor correspond to the Fock measurements of the `N` modes that @@ -52,22 +141,22 @@ def primal(self, state: State) -> Tensor: """ cutoffs = [] used = 0 - for mode in state.modes: + for mode in other.modes: if mode in self._modes: cutoffs.append( - max(settings.PNR_INTERNAL_CUTOFF, state.cutoffs[state.indices(mode)]) + max(settings.PNR_INTERNAL_CUTOFF, other.cutoffs[other.indices(mode)]) ) used += 1 else: - cutoffs.append(state.cutoffs[state.indices(mode)]) + cutoffs.append(other.cutoffs[other.indices(mode)]) if self.should_recompute_stochastic_channel() or math.any( - [c > settings.PNR_INTERNAL_CUTOFF for c in state.cutoffs] + [c > settings.PNR_INTERNAL_CUTOFF for c in other.cutoffs] ): self.recompute_stochastic_channel(cutoffs) - dm = state.dm(cutoffs) + dm = other.dm(cutoffs) for k, (mode, stoch) in enumerate(zip(self._modes, self._internal_stochastic_channel)): # move the mode indices to the end - last = [mode - k, mode + state.num_modes - 2 * k] + last = [mode - k, mode + other.num_modes - 2 * k] perm = [m for m in range(dm.ndim) if m not in last] + last dm = math.transpose(dm, perm) # compute sum_m P(meas|m)rho_mm @@ -90,28 +179,3 @@ def should_recompute_stochastic_channel(self) -> bool: # override in subclasses This method should be overriden by subclasses accordingly. """ return False - - def __lshift__(self, other) -> Tensor: - if isinstance(other, State): - self.primal(other) - else: - raise TypeError( - f"unsupported operand type(s) '{type(self).__name__}' << '{type(other).__name__}'" - ) - - def __getitem__(self, items) -> Callable: - r""" - Allows measurements to be used as ``output = meas[0,1](input)``, e.g. measuring modes 0 - and 1. - """ - - if isinstance(items, int): - modes = [items] - elif isinstance(items, slice): - modes = list(range(items.start, items.stop, items.step)) - elif isinstance(items, (Sequence, Iterable)): - modes = list(items) - else: - raise ValueError(f"{items} is not a valid slice or list of modes.") - self.modes = modes - return self diff --git a/mrmustard/lab/abstract/state.py b/mrmustard/lab/abstract/state.py index 0a6636561..2c9fb0975 100644 --- a/mrmustard/lab/abstract/state.py +++ b/mrmustard/lab/abstract/state.py @@ -145,7 +145,7 @@ def is_mixed(self): @property def is_pure(self): r"""Returns ``True`` if the state is pure and ``False`` otherwise.""" - return np.isclose(self.purity, 1.0, atol=1e-6) + return True if self._ket is not None else np.isclose(self.purity, 1.0, atol=1e-6) @property def means(self) -> Optional[Vector]: @@ -334,58 +334,8 @@ def primal(self, other: Union[State, Transformation]) -> State: Note that the returned state is not normalized. To normalize a state you can use ``mrmustard.physics.normalize``. """ - if issubclass(other.__class__, State): - remaining_modes = [m for m in other.modes if m not in self.modes] - - if self.is_gaussian and other.is_gaussian: - prob, cov, means = gaussian.general_dyne( - other.cov, - other.means, - self.cov, - self.means, - other.indices(self.modes), - settings.HBAR, - ) - if len(remaining_modes) > 0: - return State( - means=means, - cov=cov, - modes=remaining_modes, - _norm=prob if not getattr(self, "_normalize", False) else 1.0, - ) - return prob - - # either self or other is not gaussian - other_cutoffs = [ - None if m not in self.modes else other.cutoffs[other.indices(m)] - for m in other.modes - ] - try: - out_fock = self._preferred_projection(other, other.indices(self.modes)) - except AttributeError: - # matching other's cutoffs - self_cutoffs = [other.cutoffs[other.indices(m)] for m in self.modes] - out_fock = fock.contract_states( - stateA=other.ket(other_cutoffs) if other.is_pure else other.dm(other_cutoffs), - stateB=self.ket(self_cutoffs) if self.is_pure else self.dm(self_cutoffs), - a_is_mixed=other.is_mixed, - b_is_mixed=self.is_mixed, - modes=other.indices(self.modes), # TODO: change arg name to indices - normalize=self._normalize if hasattr(self, "_normalize") else False, - ) - - if len(remaining_modes) > 0: - return ( - State(dm=out_fock, modes=remaining_modes) - if other.is_mixed or self.is_mixed - else State(ket=out_fock, modes=remaining_modes) - ) - - return ( - fock.math.abs(out_fock) ** 2 - if other.is_pure and self.is_pure - else fock.math.abs(out_fock) - ) + if isinstance(other, State): + return self._project_onto_state(other) try: return other.dual(self) @@ -394,6 +344,99 @@ def primal(self, other: Union[State, Transformation]) -> State: f"Cannot apply {other.__class__.__qualname__} to {self.__class__.__qualname__}" ) from e + def _project_onto_state(self, other: State) -> Union[State, float]: + """If states are gaussian use generaldyne measurement, else use + the states' Fock representation.""" + + # if both states are gaussian + if self.is_gaussian and other.is_gaussian: + return self._project_onto_gaussian(other) + + # either self or other is not gaussian + return self._project_onto_fock(other) + + def _project_onto_fock(self, other: State) -> Union[State, float]: + """Returns the post-measurement state of the projection between two non-Gaussian + states on the remaining modes or the probability of the result. When doing homodyne sampling, + returns the post-measurement state or the measument outcome if no modes remain. + + Args: + other (State): state being projected onto self + + Returns: + State or float: returns the conditional state on the remaining modes + or the probability. + """ + remaining_modes = list(set(other.modes) - set(self.modes)) + + out_fock = self._contract_with_other(other) + if len(remaining_modes) > 0: + return ( + State(dm=out_fock, modes=remaining_modes) + if other.is_mixed or self.is_mixed + else State(ket=out_fock, modes=remaining_modes) + ) + + # return the probability (norm) of the state when there are no modes left + return ( + fock.math.abs(out_fock) ** 2 + if other.is_pure and self.is_pure + else fock.math.abs(out_fock) + ) + + def _contract_with_other(self, other): + other_cutoffs = [ + None if m not in self.modes else other.cutoffs[other.indices(m)] for m in other.modes + ] + if hasattr(self, "_preferred_projection"): + out_fock = self._preferred_projection(other, other.indices(self.modes)) + else: + # matching other's cutoffs + self_cutoffs = [other.cutoffs[other.indices(m)] for m in self.modes] + out_fock = fock.contract_states( + stateA=other.ket(other_cutoffs) if other.is_pure else other.dm(other_cutoffs), + stateB=self.ket(self_cutoffs) if self.is_pure else self.dm(self_cutoffs), + a_is_mixed=other.is_mixed, + b_is_mixed=self.is_mixed, + modes=other.indices(self.modes), # TODO: change arg name to indices + normalize=self._normalize if hasattr(self, "_normalize") else False, + ) + + return out_fock + + def _project_onto_gaussian(self, other: State) -> Union[State, float]: + """Returns the result of a generaldyne measurement given that states ``self`` and + ``other`` are gaussian. + + Args: + other (State): gaussian state being projected onto self + + Returns: + State or float: returns the output conditional state on the remaining modes + or the probability. + """ + # here `self` is the measurement device state and `other` is the incoming state + # being projected onto the measurement state + remaining_modes = list(set(other.modes) - set(self.modes)) + + _, probability, new_cov, new_means = gaussian.general_dyne( + other.cov, + other.means, + self.cov, + self.means, + self.modes, + ) + + if len(remaining_modes) > 0: + return State( + means=new_means, + cov=new_cov, + modes=remaining_modes, + _norm=probability if not getattr(self, "_normalize", False) else 1.0, + ) + + return probability + def __iter__(self) -> Iterable[State]: """Iterates over the modes and their corresponding tensors.""" return (self.get_modes(i) for i in range(self.num_modes)) @@ -458,12 +501,19 @@ def get_modes(self, item): else: raise TypeError("item must be int or iterable") + if item == self.modes: + return self + + if not set(item) & set(self.modes): + raise ValueError( + f"Failed to request modes {item} for state {self} on modes {self.modes}." + ) + if self.is_gaussian: cov, _, _ = gaussian.partition_cov(self.cov, item) means, _ = gaussian.partition_means(self.means, item) return State(cov=cov, means=means, modes=item) - # if not gaussian fock_partitioned = fock.trace( self.dm(self.cutoffs), keep=[m for m in range(self.num_modes) if m in item] ) diff --git a/mrmustard/lab/circuit.py b/mrmustard/lab/circuit.py index 1255c9ec7..6a5898c77 100644 --- a/mrmustard/lab/circuit.py +++ b/mrmustard/lab/circuit.py @@ -25,8 +25,7 @@ from mrmustard.types import Matrix, Vector from mrmustard.training import Parametrized from mrmustard.utils.xptensor import XPMatrix, XPVector -from mrmustard.lab.abstract import Transformation -from mrmustard.lab.abstract import State +from .abstract import Transformation, State class Circuit(Transformation, Parametrized): diff --git a/mrmustard/lab/detectors.py b/mrmustard/lab/detectors.py index f28be784f..051732f19 100644 --- a/mrmustard/lab/detectors.py +++ b/mrmustard/lab/detectors.py @@ -16,17 +16,20 @@ This module implements the set of detector classes that perform measurements on quantum circuits. """ -from typing import List, Tuple, Union, Optional -from mrmustard.types import Matrix +from typing import List, Tuple, Union, Optional, Iterable +from mrmustard.types import Matrix, Tensor from mrmustard.training import Parametrized -from mrmustard.lab.abstract import FockMeasurement -from mrmustard.lab.states import DisplacedSqueezed, Coherent from mrmustard import settings from mrmustard.math import Math +from mrmustard.physics import gaussian, fock +from mrmustard.utils import homodyne as utils_homodyne +from .abstract import FockMeasurement, Measurement, State +from .states import DisplacedSqueezed, Coherent +from .gates import Rgate math = Math() -__all__ = ["PNRDetector", "ThresholdDetector", "Homodyne", "Heterodyne"] +__all__ = ["PNRDetector", "ThresholdDetector", "Generaldyne", "Homodyne", "Heterodyne"] # pylint: disable=no-member class PNRDetector(Parametrized, FockMeasurement): @@ -67,13 +70,6 @@ def __init__( modes: List[int] = None, cutoffs: Union[int, List[int]] = None, ): - if modes is not None: - num_modes = len(modes) - elif cutoffs is not None: - num_modes = len(cutoffs) - else: - num_modes = max(len(math.atleast_1d(efficiency)), len(math.atleast_1d(dark_counts))) - Parametrized.__init__( self, efficiency=math.atleast_1d(efficiency), @@ -83,13 +79,21 @@ def __init__( efficiency_bounds=efficiency_bounds, dark_counts_bounds=dark_counts_bounds, ) - FockMeasurement.__init__(self) self._stochastic_channel = stochastic_channel - self._modes = modes or list(range(num_modes)) - self._cutoffs = cutoffs or [settings.PNR_INTERNAL_CUTOFF] * num_modes self._should_recompute_stochastic_channel = efficiency_trainable or dark_counts_trainable + if modes is not None: + num_modes = len(modes) + elif cutoffs is not None: + num_modes = len(cutoffs) + else: + num_modes = max(len(math.atleast_1d(efficiency)), len(math.atleast_1d(dark_counts))) + + modes = modes or list(range(num_modes)) + outcome = None + FockMeasurement.__init__(self, outcome, modes, cutoffs) + self.recompute_stochastic_channel() def should_recompute_stochastic_channel(self): @@ -164,10 +168,14 @@ def __init__( num_modes = len(modes) else: num_modes = max(len(math.atleast_1d(efficiency)), len(math.atleast_1d(dark_count_prob))) + if len(math.atleast_1d(efficiency)) == 1 and num_modes > 1: efficiency = math.tile(math.atleast_1d(efficiency), [num_modes]) if len(math.atleast_1d(dark_count_prob)) == 1 and num_modes > 1: dark_count_prob = math.tile(math.atleast_1d(dark_count_prob), [num_modes]) + + modes = modes or list(range(num_modes)) + Parametrized.__init__( self, efficiency=efficiency, @@ -177,15 +185,17 @@ def __init__( efficiency_bounds=efficiency_bounds, dark_count_prob_bounds=dark_count_prob_bounds, ) - FockMeasurement.__init__(self) self._stochastic_channel = stochastic_channel - self._modes = modes or list(range(num_modes)) - self._cutoffs = [2] * num_modes + + cutoffs = [2] * num_modes self._should_recompute_stochastic_channel = ( efficiency_trainable or dark_count_prob_trainable ) + outcome = None + FockMeasurement.__init__(outcome, modes, cutoffs) + self.recompute_stochastic_channel() def should_recompute_stochastic_channel(self): @@ -213,14 +223,75 @@ def recompute_stochastic_channel(self, cutoffs: List[int] = None): self._internal_stochastic_channel.append(condprob) -class Heterodyne(Coherent): +class Generaldyne(Measurement): + r"""Generaldyne measurement on given modes. + + Args: + state (State): the Gaussian state of the measurment device + outcome (optional or List[float]): the means of the measurement state, defaults to ``None`` + modes (List[int]): the modes on which the measurement is acting on + """ + + def __init__( + self, state: State, outcome: Optional[Tensor] = None, modes: Optional[Iterable[int]] = None + ) -> None: + + if not state.is_gaussian: + raise TypeError("Generaldyne measurement state must be Gaussian.") + if outcome is not None and not outcome.shape == state.means.shape: + raise TypeError( + f"Expected `outcome` of size {state.means.shape} but got {outcome.shape}." + ) + + self.state = state + + if modes is None: + modes = self.state.modes + else: + # ensure measurement and state act on the same modes + self.state = state[modes] + + super().__init__(outcome, modes) + + @property + def outcome(self) -> Tensor: + return self.state.means + + def primal(self, other: State) -> Union[State, float]: + if self.postselected: + # return the projection of self.state onto other + return self.state.primal(other) + + return super().primal(other) + + def _measure_gaussian(self, other) -> Union[State, float]: + + remaining_modes = list(set(other.modes) - set(self.modes)) + + outcome, prob, new_cov, new_means = gaussian.general_dyne( + other.cov, other.means, self.state.cov, None, modes=self.modes + ) + self.state = State(cov=self.state.cov, means=outcome) + + return ( + prob + if len(remaining_modes) == 0 + else State(cov=new_cov, means=new_means, modes=remaining_modes, _norm=prob) + ) + + def _measure_fock(self, other) -> Union[State, float]: + raise NotImplementedError(f"Fock sampling not implemented for {self.__class__.__name__}") + + +class Heterodyne(Generaldyne): r"""Heterodyne measurement on given modes. This class is just a thin wrapper around the :class:`Coherent`. + If neither ``x`` or ``y`` is provided then values will be sampled. Args: - x (float or List[float]): the x-displacement of the coherent state - y (float or List[float]): the y-displacement of the coherent state + x (optional float or List[float]): the x-displacement of the coherent state, defaults to ``None`` + y (optional or List[float]): the y-displacement of the coherent state, defaults to ``None`` modes (List[int]): the modes of the coherent state """ @@ -230,11 +301,30 @@ def __init__( y: Union[float, List[float]] = 0.0, modes: List[int] = None, ): - super().__init__(x, y, modes=modes) + if (x is None) ^ (y is None): # XOR + raise ValueError("Both `x` and `y` arguments should be defined or set to `None`.") + + # if no x and y provided, sample the outcome + if x is None and y is None: + num_modes = len(modes) if modes is not None else 1 + x, y = math.zeros([num_modes]), math.zeros([num_modes]) + outcome = None + else: + x = math.atleast_1d(x, dtype="float64") + y = math.atleast_1d(y, dtype="float64") + outcome = math.concat([x, y], axis=0) # XXPP ordering + + modes = modes or list(range(x.shape[0])) + + units_factor = math.sqrt(2.0 * settings.HBAR, dtype="float64") + state = Coherent(x / units_factor, y / units_factor) + super().__init__(state=state, outcome=outcome, modes=modes) -class Homodyne(DisplacedSqueezed): - r"""Homodyne measurement on given modes. + +class Homodyne(Generaldyne): + """Homodyne measurement on given modes. If ``result`` is not provided then the value + is sampled. Args: quadrature_angle (float or List[float]): measurement quadrature angle @@ -246,19 +336,105 @@ class Homodyne(DisplacedSqueezed): def __init__( self, quadrature_angle: Union[float, List[float]], - result: Union[float, List[float]] = 0.0, - modes: List[int] = None, + result: Optional[Union[float, List[float]]] = None, + modes: Optional[List[int]] = None, r: Union[float, List[float]] = settings.HOMODYNE_SQUEEZING, ): - quadrature_angle = math.astensor(quadrature_angle, dtype="float64") - result = math.astensor(result, dtype="float64") - x = result * math.cos(quadrature_angle) - y = result * math.sin(quadrature_angle) - r = math.astensor(r, dtype="float64") - super().__init__( - r=r, - phi=2 * quadrature_angle, - x=x, - y=y, - modes=modes, + + self.r = r + self.quadrature_angle = math.atleast_1d(quadrature_angle, dtype="float64") + + # if no ``result`` provided, sample the outcome + if result is None: + x = math.zeros_like(self.quadrature_angle) + y = math.zeros_like(self.quadrature_angle) + outcome = None + else: + result = math.atleast_1d(result, dtype="float64") + if result.shape[-1] == 1: + result = math.tile(result, self.quadrature_angle.shape) + + x = result * math.cos(self.quadrature_angle) + y = result * math.sin(self.quadrature_angle) + outcome = math.concat([x, y], axis=0) # XXPP ordering + + modes = modes or list(range(self.quadrature_angle.shape[0])) + + units_factor = math.sqrt(2.0 * settings.HBAR, dtype="float64") + state = DisplacedSqueezed( + r=r, phi=2 * self.quadrature_angle, x=x / units_factor, y=y / units_factor + ) + super().__init__(state=state, outcome=outcome, modes=modes) + + def _measure_gaussian(self, other) -> Union[State, float]: + + # rotate modes to be measured to the Homodyne basis + other >>= Rgate(-self.quadrature_angle, modes=self.modes) + self.state >>= Rgate(-self.quadrature_angle, modes=self.modes) + + # perform homodyne measurement as a generaldyne one + out = super()._measure_gaussian(other) + + # set p-outcomes to 0 and rotate the measurement device state back to the original basis, + # this is in turn rotating the outcomes to the original basis + self_state_means = math.concat( + [self.state.means[: self.num_modes], math.zeros((self.num_modes,))], axis=0 + ) + self.state = State(cov=self.state.cov, means=self_state_means, modes=self.modes) >> Rgate( + self.quadrature_angle, modes=self.modes + ) + + return out + + def _measure_fock(self, other) -> Union[State, float]: + + if len(self.modes) > 1: + raise NotImplementedError( + "Multimode Homodyne sampling for Fock representation is not yet implemented." + ) + + other_cutoffs = [ + None if m not in self.modes else other.cutoffs[other.indices(m)] for m in other.modes + ] + remaining_modes = list(set(other.modes) - set(self.modes)) + + # create reduced state of modes to be measured on the homodyne basis + reduced_state = other.get_modes(self.modes) >> Rgate( + -self.quadrature_angle, modes=self.modes + ) + + # build pdf and sample homodyne outcome + x_outcome, probability = utils_homodyne.sample_homodyne_fock( + state=reduced_state.ket() if reduced_state.is_pure else reduced_state.dm(), + hbar=settings.HBAR, + ) + + # Define conditional state of the homodyne measurement device and rotate back to the original basis. + # Note: x_outcome already has units of sqrt(hbar). Here is divided by sqrt(2*hbar) to cancel the multiplication + # factor of the displacement symplectic inside the DisplacedSqueezed state. + x_arg = x_outcome / math.sqrt(2.0 * settings.HBAR, dtype="float64") + self.state = DisplacedSqueezed( + r=self.r, phi=self.quadrature_angle, x=x_arg, y=0.0, modes=self.modes + ) >> Rgate(self.quadrature_angle, modes=self.modes) + + if remaining_modes == 0: + return probability + + self_cutoffs = [other.cutoffs[other.indices(m)] for m in self.modes] + other_cutoffs = [ + None if m not in self.modes else other.cutoffs[other.indices(m)] for m in other.modes + ] + out_fock = fock.contract_states( + stateA=other.ket(other_cutoffs) if other.is_pure else other.dm(other_cutoffs), + stateB=self.state.ket(self_cutoffs), + a_is_mixed=other.is_mixed, + b_is_mixed=False, + modes=other.indices(self.modes), + normalize=False, + ) + + return ( + State(dm=out_fock, modes=remaining_modes, _norm=probability) + if other.is_mixed + else State(ket=out_fock, modes=remaining_modes, _norm=probability) ) diff --git a/mrmustard/lab/gates.py b/mrmustard/lab/gates.py index aa871a0f0..167c830e4 100644 --- a/mrmustard/lab/gates.py +++ b/mrmustard/lab/gates.py @@ -21,11 +21,10 @@ from typing import Union, Optional, List, Tuple from mrmustard.types import Tensor from mrmustard import settings -from mrmustard.lab.abstract import Transformation from mrmustard.training import Parametrized from mrmustard.physics import gaussian - from mrmustard.math import Math +from .abstract import Transformation math = Math() diff --git a/mrmustard/lab/states.py b/mrmustard/lab/states.py index a8d06835e..1b6ee4ebe 100644 --- a/mrmustard/lab/states.py +++ b/mrmustard/lab/states.py @@ -19,10 +19,10 @@ from typing import Union, Optional, List, Tuple, Sequence from mrmustard.types import Scalar, Vector, Matrix from mrmustard import settings -from mrmustard.lab.abstract import State from mrmustard.physics import gaussian, fock from mrmustard.training import Parametrized from mrmustard.math import Math +from .abstract import State math = Math() diff --git a/mrmustard/math/math_interface.py b/mrmustard/math/math_interface.py index e5520d723..ea4ec0c1a 100644 --- a/mrmustard/math/math_interface.py +++ b/mrmustard/math/math_interface.py @@ -377,7 +377,9 @@ def hash_tensor(self, tensor: Tensor) -> int: """ @abstractmethod - def hermite_renormalized(self, A: Matrix, B: Vector, C: Scalar, shape: Sequence[int]) -> Tensor: + def hermite_renormalized( + self, A: Matrix, B: Vector, C: Scalar, shape: Sequence[int], modified: bool + ) -> Tensor: r"""Returns the array of hermite renormalized polynomials of the given coefficients. Args: @@ -385,6 +387,8 @@ def hermite_renormalized(self, A: Matrix, B: Vector, C: Scalar, shape: Sequence[ B (array): Vector coefficient of the hermite polynomial C (array): Scalar coefficient of the hermite polynomial shape (tuple): shape of the hermite polynomial + modified (bool): whether to return the modified multidimensional + Hermite polynomials or the standard ones Returns: array: renormalized hermite polynomials @@ -803,6 +807,72 @@ def zeros_like(self, array: Tensor) -> Tensor: array: array of zeros """ + @abstractmethod + def map_fn(self, func, elements: Tensor) -> Tensor: + """Transforms elems by applying fn to each element unstacked on axis 0. + + Args: + func (func): The callable to be performed. It accepts one argument, + which will have the same (possibly nested) structure as elems. + elements (Tensor): A tensor or (possibly nested) sequence of tensors, + each of which will be unstacked along their first dimension. + ``func`` will be applied to the nested sequence of the resulting slices. + + Returns: + Tensor: applied ``func`` on ``elements`` + """ + + @abstractmethod + def squeeze(self, tensor: Tensor, axis: Optional[List[int]]) -> Tensor: + """Removes dimensions of size 1 from the shape of a tensor. + + Args: + tensor (Tensor): the tensor to squeeze + axis (Optional[List[int]]): if specified, only squeezes the + dimensions listed, defaults to [] + + Returns: + Tensor: tensor with one or more dimensions of size 1 removed + """ + + @abstractmethod + def cholesky(self, input: Tensor) -> Tensor: + """Computes the Cholesky decomposition of square matrices. + + Args: + input (Tensor) + + Returns: + Tensor: tensor with the same type as input + """ + + @abstractmethod + def Categorical(self, probs: Tensor, name: str): + """Categorical distribution over integers. + + Args: + probs (Tensor): tensor representing the probabilities of a set of Categorical + distributions. + name (str): name prefixed to operations created by this class + + Returns: + tfp.distributions.Categorical: instance of ``tfp.distributions.Categorical`` class + """ + + @abstractmethod + def MultivariateNormalTriL(self, loc: Tensor, scale_tril: Tensor): + """Multivariate normal distribution on `R^k` and parameterized by a (batch of) length-k loc + vector (aka "mu") and a (batch of) k x k scale matrix; covariance = scale @ scale.T + where @ denotes matrix-multiplication. + + Args: + loc (Tensor): if this is set to None, loc is implicitly 0 + scale_tril: lower-triangular Tensor with non-zero diagonal elements + + Returns: + tfp.distributions.MultivariateNormalTriL: instance of ``tfp.distributions.MultivariateNormalTriL`` + """ + # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ # Methods that build on the basic ops and don't need to be overridden in the backend implementation # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/mrmustard/math/tensorflow.py b/mrmustard/math/tensorflow.py index d37b00029..54262065c 100644 --- a/mrmustard/math/tensorflow.py +++ b/mrmustard/math/tensorflow.py @@ -16,6 +16,7 @@ import numpy as np import tensorflow as tf +import tensorflow_probability as tfp from thewalrus import hermite_multidimensional, grad_hermite_multidimensional from mrmustard.math.autocast import Autocast @@ -294,6 +295,21 @@ def zeros(self, shape: Sequence[int], dtype=tf.float64) -> tf.Tensor: def zeros_like(self, array: tf.Tensor) -> tf.Tensor: return tf.zeros_like(array) + def map_fn(self, func, elements): + return tf.map_fn(func, elements) + + def squeeze(self, tensor, axis=None): + return tf.squeeze(tensor, axis=axis or []) + + def cholesky(self, input: Tensor): + return tf.linalg.cholesky(input) + + def Categorical(self, probs: Tensor, name: str): + return tfp.distributions.Categorical(probs=probs, name=name) + + def MultivariateNormalTriL(self, loc: Tensor, scale_tril: Tensor): + return tfp.distributions.MultivariateNormalTriL(loc=loc, scale_tril=scale_tril) + # ~~~~~~~~~~~~~~~~~ # Special functions # ~~~~~~~~~~~~~~~~~ @@ -323,7 +339,7 @@ def value_and_gradients( @tf.custom_gradient def hermite_renormalized( - self, A: tf.Tensor, B: tf.Tensor, C: tf.Tensor, shape: Tuple[int] + self, A: tf.Tensor, B: tf.Tensor, C: tf.Tensor, shape: Tuple[int], modified: bool = True ) -> tf.Tensor: # TODO this is not ready r"""Renormalized multidimensional Hermite polynomial given by the "exponential" Taylor series of :math:`exp(C + Bx - Ax^2)` at zero, where the series has :math:`sqrt(n!)` at the @@ -334,12 +350,17 @@ def hermite_renormalized( B: The B vector. C: The C scalar. shape: The shape of the final tensor. + modified (bool): whether to return the modified multidimensional + Hermite polynomials or the standard ones Returns: The renormalized Hermite polynomial of given shape. """ - poly = tf.numpy_function( - hermite_multidimensional, [A, shape, B, C, True, True, True], A.dtype + if isinstance(shape, List) and len(shape) == 1: + shape = shape[0] + + poly = hermite_multidimensional( + self.asnumpy(A), shape, self.asnumpy(B), self.asnumpy(C), True, True, modified ) def grad(dLdpoly): diff --git a/mrmustard/math/torch.py b/mrmustard/math/torch.py index 424b34c0e..b931ca4ac 100644 --- a/mrmustard/math/torch.py +++ b/mrmustard/math/torch.py @@ -303,7 +303,7 @@ def hash_tensor(self, tensor: torch.Tensor) -> str: return hash(tensor) def hermite_renormalized( - self, A: torch.Tensor, B: torch.Tensor, C: torch.Tensor, shape: Tuple[int] + self, A: torch.Tensor, B: torch.Tensor, C: torch.Tensor, shape: Tuple[int], modified: bool ) -> torch.Tensor: # TODO this is not ready r"""Renormalized multidimensional Hermite polynomial. diff --git a/mrmustard/physics/fock.py b/mrmustard/physics/fock.py index 7c543a8d3..daeca9c31 100644 --- a/mrmustard/physics/fock.py +++ b/mrmustard/physics/fock.py @@ -380,7 +380,7 @@ def contract_states( normalize: whether to normalize the result Returns: - State: the contracted state (subsystem of ``A``) + Tensor: the contracted state tensor (subsystem of ``A``). Either ket or dm. """ indices = list(range(len(modes))) if not a_is_mixed and not b_is_mixed: diff --git a/mrmustard/physics/gaussian.py b/mrmustard/physics/gaussian.py index 2e32ff970..2f56b1339 100644 --- a/mrmustard/physics/gaussian.py +++ b/mrmustard/physics/gaussian.py @@ -16,8 +16,7 @@ This module contains functions for performing calculations on Gaussian states. """ -from typing import Tuple, Union, Sequence, Any -from numpy import pi +from typing import Tuple, Union, Sequence, Any, Optional from thewalrus.quantum import is_pure_cov from mrmustard.types import Matrix, Vector, Scalar from mrmustard.utils.xptensor import XPMatrix, XPVector @@ -560,36 +559,56 @@ def general_dyne( cov: Matrix, means: Vector, proj_cov: Matrix, - proj_means: Vector, - modes: Sequence[int], - hbar: float, + proj_means: Optional[Vector] = None, + modes: Optional[Sequence[int]] = None, ) -> Tuple[Scalar, Matrix, Vector]: - r"""Returns the results of a general dyne measurement. + r"""Returns the results of a general-dyne measurement. If ``proj_means`` are not provided + (as ``None``), they are sampled from the probability distribution. Args: - cov (Matrix): covariance matrix of the state being measured - means (Vector): means vector of the state being measured - proj_cov (Matrix): covariance matrix of the state being projected onto - proj_means (Vector): means vector of the state being projected onto (i.e. the measurement outcome) - modes (Sequence[int]): modes being measured (modes are indexed from 0 to num_modes-1) + cov (Matrix): covariance matrix of the state being measured [units of `2\hbar`] + means (Vector): means vector of the state being measured [units of `\sqrt(\hbar)`] + proj_cov (Matrix): covariance matrix of the state being projected onto [units of `2\hbar`] + proj_means (Optional Vector): means vector of the state being projected onto + (i.e. the measurement outcome) [units of `\sqrt(\hbar)`]. If not provided, the means vector + is sampled from the generaldyne probability distribution. + modes (Optional Sequence[int]): modes being measured (modes are indexed from 0 to num_modes-1), + if modes are not provided then the first modes (according to the size of ``cov``) are measured. Returns: - Tuple[Scalar, Matrix, Vector]: the outcome probability, the post-measurement cov and means vector + Tuple[Scalar, Scalar, Matrix, Vector]: + outcome (sampled means vector of the measured subsystem) [units of `\sqrt(\hbar)`], + oucome probability [units of `\hbar**N`], + post-measurement covariace [units of `2\hbar`] + post-measurement means vector [units of `\sqrt{\hbar}`]. """ - N = cov.shape[-1] // 2 - nB = proj_cov.shape[-1] // 2 # B is the system being measured - Amodes = [i for i in range(N) if i not in modes] + N, M = cov.shape[-1] // 2, proj_cov.shape[-1] // 2 + # Bmodes are the modes being measured and Amodes are the leftover modes + Bmodes = modes or list(range(M)) + Amodes = list(set(list(range(N))) - set(Bmodes)) + A, B, AB = partition_cov(cov, Amodes) a, b = partition_means(means, Amodes) - proj_cov = math.cast(proj_cov, B.dtype) - proj_means = math.cast(proj_means, b.dtype) - inv = math.inv(B + proj_cov) - new_cov = A - math.matmul(math.matmul(AB, inv), math.transpose(AB)) - new_means = a + math.matvec(math.matmul(AB, inv), proj_means - b) - prob = math.exp(-math.sum(math.matvec(inv, proj_means - b) * (proj_means - b))) / ( - pi**nB * (hbar**-nB) * math.sqrt(math.det(B + proj_cov)) - ) # TODO: check this (hbar part especially) - return prob, new_cov, new_means + reduced_cov = B + proj_cov + + # covariances are divided by 2 to match tensorflow and MrMustard conventions + # (MrMustard uses Serafini convention where `sigma_MM = 2 sigma_TF`) + pdf = math.MultivariateNormalTriL(loc=b, scale_tril=math.cholesky(reduced_cov / 2)) + outcome = ( + pdf.sample(dtype=cov.dtype) if proj_means is None else math.cast(proj_means, cov.dtype) + ) + prob = pdf.prob(outcome) + + # calculate conditional output state of unmeasured modes + num_remaining_modes = N - M + if num_remaining_modes == 0: + return outcome, prob, None, None + + AB_inv = math.matmul(AB, math.inv(reduced_cov)) + new_cov = A - math.matmul(AB_inv, math.transpose(AB)) + new_means = a + math.matvec(AB_inv, outcome - b) + + return outcome, prob, new_cov, new_means # ~~~~~~~~~ @@ -747,8 +766,11 @@ def von_neumann_entropy(cov: Matrix, hbar: float) -> float: Returns: float: the Von Neumann entropy """ + + def g(x): + return math.xlogy((x + 1) / 2, (x + 1) / 2) - math.xlogy((x - 1) / 2, (x - 1) / 2 + 1e-9) + symp_vals = symplectic_eigenvals(cov, hbar) - g = lambda x: math.xlogy((x + 1) / 2, (x + 1) / 2) - math.xlogy((x - 1) / 2, (x - 1) / 2 + 1e-9) entropy = math.sum(g(symp_vals)) return entropy diff --git a/mrmustard/utils/homodyne.py b/mrmustard/utils/homodyne.py new file mode 100644 index 000000000..2cadbbb59 --- /dev/null +++ b/mrmustard/utils/homodyne.py @@ -0,0 +1,243 @@ +# Copyright 2022 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. + +"""Utility functions related to homodyne sampling in Fock representation""" +from __future__ import annotations +from typing import TYPE_CHECKING +from functools import lru_cache, wraps + +import numpy as np + +from mrmustard.types import Tuple, Tensor +from mrmustard.math import Math + +if TYPE_CHECKING: + from mrmustard.lab.abstract import State + +math = Math() + + +def hermite_cache(fn): + """Decorator function to cache outcomes of the physicist_hermite_polys + function. To do so the input tensor (non-hashable) is converted into a + numpy array (non-hashable) and then a tuple (hashable) is used in conjuntion + with ``functools.lru_cache``.""" + + @lru_cache + def cached_wrapper(hashable_array, cutoff): + array = np.array(hashable_array) + return fn(array, cutoff) + + @wraps(fn) + def wrapper(tensor, cutoff): + return cached_wrapper(tuple(tensor.numpy()), cutoff) + + # copy lru_cache attributes over too + wrapper.cache_info = cached_wrapper.cache_info + wrapper.cache_clear = cached_wrapper.cache_clear + + return wrapper + + +@hermite_cache +def physicist_hermite_polys(x: Tensor, cutoff: int): + r"""Reduction of the multidimensional hermite polynomials into the one-dimensional + renormalized physicist polys. + + Args: + x (Tensor): argument values of the Hermite polynomial + cutoff (int): maximum size of the subindices in the Hermite polynomial + + Returns: + Tensor: the evaluated renormalized Hermite polynomials + """ + R = math.astensor(2 * np.ones([1, 1])) # to get the physicist polys + + def f_hermite_polys(xi): + return math.hermite_renormalized(R, math.astensor([xi]), 1, cutoff, modified=False) + + return math.map_fn(f_hermite_polys, x) + + +@lru_cache +def estimate_dx(cutoff, period_resolution=20): + r"""Estimates a suitable quadrature discretization interval `dx`. Uses the fact + that Fock state `n` oscillates with angular frequency :math:`\sqrt{2(n + 1)}`, + which follows from the relation + + .. math:: + + \psi^{[n]}'(q) = q - sqrt(2*(n + 1))*\psi^{[n+1]}(q) + + by setting q = 0, and approximating the oscillation amplitude by `\psi^{[n+1]}(0) + + Ref: https://en.wikipedia.org/wiki/Hermite_polynomials#Hermite_functions + + Args + cutoff (int): Fock cutoff + period_resolution (int): Number of points used to sample one Fock + wavefunction oscillation. Larger values yields better approximations + and thus smaller `dx`. + + Returns + (float): discretization value of quadrature + """ + fock_cutoff_frequency = np.sqrt(2 * (cutoff + 1)) + fock_cutoff_period = 2 * np.pi / fock_cutoff_frequency + dx_estimate = fock_cutoff_period / period_resolution + return dx_estimate + + +@lru_cache +def estimate_xmax(cutoff, minimum=5): + r"""Estimates a suitable quadrature axis length + + Args + cutoff (int): Fock cutoff + minimum (float): Minimum value of the returned xmax + + Returns + (float): maximum quadrature value + """ + if cutoff == 0: + xmax_estimate = 3 + else: + # maximum q for a classical particle with energy n=cutoff + classical_endpoint = np.sqrt(2 * cutoff) + # approximate probability of finding particle outside classical region + excess_probability = 1 / (7.464 * cutoff ** (1 / 3)) + # Emperical factor that yields reasonable results + A = 5 + xmax_estimate = classical_endpoint * (1 + A * excess_probability) + return max(minimum, xmax_estimate) + + +@lru_cache +def estimate_quadrature_axis(cutoff, minimum=5, period_resolution=20): + """Generates a suitable quadrature axis. + + Args + cutoff (int): Fock cutoff + minimum (float): Minimum value of the returned xmax + period_resolution (int): Number of points used to sample one Fock + wavefunction oscillation. Larger values yields better approximations + and thus smaller dx. + + Returns + (array): quadrature axis + """ + xmax = estimate_xmax(cutoff, minimum=minimum) + dx = estimate_dx(cutoff, period_resolution=period_resolution) + xaxis = np.arange(-xmax, xmax, dx) + xaxis = np.append(xaxis, xaxis[-1] + dx) + xaxis = xaxis - np.mean(xaxis) # center around 0 + return xaxis + + +def sample_homodyne_fock(state: State, hbar: float) -> Tuple[float, float, State]: + r"""Given a single-mode state, it generates the pdf of :math:`\tr [ \rho |x>=0.17.0", "tensorflow>=2.4.0", + "tensorflow-probability", "rich", "tqdm", "matplotlib", diff --git a/tests/test_lab/test_detectors.py b/tests/test_lab/test_detectors.py index 9c1ca81eb..95d890e83 100644 --- a/tests/test_lab/test_detectors.py +++ b/tests/test_lab/test_detectors.py @@ -12,12 +12,13 @@ # See the License for the specific language governing permissions and # limitations under the License. -from hypothesis import settings, given, strategies as st +import pytest +from hypothesis import given, strategies as st from hypothesis.extra.numpy import arrays import numpy as np -import tensorflow as tf from scipy.stats import poisson +import tensorflow as tf from mrmustard.math import Math from mrmustard.lab import ( @@ -33,143 +34,176 @@ TMSV, Dgate, Fock, + State, + SqueezedVacuum, ) -from mrmustard import physics -from mrmustard import settings +from mrmustard import physics, settings +from tests.random import none_or_ math = Math() np.random.seed(137) +hbar = settings.HBAR -@given( - alpha=st.complex_numbers(min_magnitude=0, max_magnitude=1), - eta=st.floats(0, 1), - dc=st.floats(0, 0.2), -) -def test_detector_coherent_state(alpha, eta, dc): - """Tests the correct Poisson statistics are generated when a coherent state hits an imperfect detector""" - detector = PNRDetector(efficiency=eta, dark_counts=dc, modes=[0]) - ps = Coherent(x=alpha.real, y=alpha.imag) << detector - expected = poisson.pmf(k=np.arange(len(ps)), mu=eta * np.abs(alpha) ** 2 + dc) - assert np.allclose(ps, expected) - - -@given( - r=st.floats(0, 0.5), - phi=st.floats(0, 2 * np.pi), - eta=st.floats(0, 1), - dc=st.floats(0, 0.2), -) -def test_detector_squeezed_state(r, phi, eta, dc): - """Tests the correct mean and variance are generated when a squeezed state hits an imperfect detector""" - S = Sgate(r=r, phi=phi) - ps = Vacuum(1) >> S >> PNRDetector(efficiency=eta, dark_counts=dc) - assert np.allclose(np.sum(ps), 1.0) - mean = np.arange(len(ps)) @ ps.numpy() - expected_mean = eta * np.sinh(r) ** 2 + dc - assert np.allclose(mean, expected_mean) - variance = np.arange(len(ps)) ** 2 @ ps.numpy() - mean**2 - expected_variance = eta * np.sinh(r) ** 2 * (1 + eta * (1 + 2 * np.sinh(r) ** 2)) + dc - assert np.allclose(variance, expected_variance) - - -@given( - r=st.floats(0, 0.5), - phi=st.floats(0, 2 * np.pi), - eta_s=st.floats(0, 1), - eta_i=st.floats(0, 1), - dc_s=st.floats(0, 0.2), - dc_i=st.floats(0, 0.2), -) -def test_detector_two_mode_squeezed_state(r, phi, eta_s, eta_i, dc_s, dc_i): - """Tests the correct mean and variance are generated when a two mode squeezed state hits an imperfect detector""" - pnr = PNRDetector(efficiency=[eta_s, eta_i], dark_counts=[dc_s, dc_i]) - ps = Vacuum(2) >> S2gate(r=r, phi=phi) >> pnr - n = np.arange(len(ps)) - mean_s = np.sum(ps, axis=1) @ n - n_s = eta_s * np.sinh(r) ** 2 - expected_mean_s = n_s + dc_s - mean_i = np.sum(ps, axis=0) @ n - n_i = eta_i * np.sinh(r) ** 2 - expected_mean_i = n_i + dc_i - expected_mean_s = n_s + dc_s - var_s = np.sum(ps, axis=1) @ n**2 - mean_s**2 - var_i = np.sum(ps, axis=0) @ n**2 - mean_i**2 - expected_var_s = n_s * (n_s + 1) + dc_s - expected_var_i = n_i * (n_i + 1) + dc_i - covar = n @ ps.numpy() @ n - mean_s * mean_i - expected_covar = eta_s * eta_i * (np.sinh(r) * np.cosh(r)) ** 2 - assert np.allclose(mean_s, expected_mean_s) - assert np.allclose(mean_i, expected_mean_i) - assert np.allclose(var_s, expected_var_s) - assert np.allclose(var_i, expected_var_i) - assert np.allclose(covar, expected_covar) - - -def test_postselection(): - """Check the correct state is heralded for a two-mode squeezed vacuum with perfect detector""" - n_mean = 1.0 - n_measured = 1 - cutoff = 3 - detector = PNRDetector(efficiency=1.0, dark_counts=0.0, cutoffs=[cutoff]) - S2 = S2gate(r=np.arcsinh(np.sqrt(n_mean)), phi=0.0) - proj_state = (Vacuum(2) >> S2 >> detector)[n_measured] - success_prob = math.real(math.trace(proj_state)) - proj_state = proj_state / math.trace(proj_state) - # outputs the ket/dm in the third mode by projecting the first and second in 1,2 photons - expected_prob = 1 / (1 + n_mean) * (n_mean / (1 + n_mean)) ** n_measured - assert np.allclose(success_prob, expected_prob) - expected_state = np.zeros_like(proj_state) - expected_state[n_measured, n_measured] = 1.0 - assert np.allclose(proj_state, expected_state) - - -@given(eta=st.floats(0, 1)) -def test_loss_probs(eta): - "Checks that a lossy channel is equivalent to quantum efficiency on detection probs" - ideal_detector = PNRDetector(efficiency=1.0, dark_counts=0.0) - lossy_detector = PNRDetector(efficiency=eta, dark_counts=0.0) - S = Sgate(r=0.2, phi=[0.0, 0.7]) - BS = BSgate(theta=1.4, phi=0.0) - L = Attenuator(transmissivity=eta) - dms_lossy = Vacuum(2) >> S[0, 1] >> BS[0, 1] >> lossy_detector[0] - dms_ideal = Vacuum(2) >> S[0, 1] >> BS[0, 1] >> L[0] >> ideal_detector[0] - assert np.allclose(dms_lossy, dms_ideal) - - -@given(s=st.floats(min_value=0.0, max_value=10.0), X=st.floats(-10.0, 10.0)) -def test_homodyne_on_2mode_squeezed_vacuum(s, X): - homodyne = Homodyne(quadrature_angle=0.0, result=X) - r = homodyne.r.value - remaining_state = TMSV(r=np.arcsinh(np.sqrt(abs(s)))) << homodyne[0] - cov = ( - np.diag( +class TestPNRDetector: + """tests related to PNR detectors""" + + @given( + alpha=st.complex_numbers(min_magnitude=0, max_magnitude=1), + eta=st.floats(0, 1), + dc=st.floats(0, 0.2), + ) + def test_detector_coherent_state(self, alpha, eta, dc): + """Tests the correct Poisson statistics are generated when a coherent state hits an imperfect detector""" + detector = PNRDetector(efficiency=eta, dark_counts=dc, modes=[0]) + ps = Coherent(x=alpha.real, y=alpha.imag) << detector + expected = poisson.pmf(k=np.arange(len(ps)), mu=eta * np.abs(alpha) ** 2 + dc) + assert np.allclose(ps, expected) + + @given( + r=st.floats(0, 0.5), + phi=st.floats(0, 2 * np.pi), + eta=st.floats(0, 1), + dc=st.floats(0, 0.2), + ) + def test_detector_squeezed_state(self, r, phi, eta, dc): + """Tests the correct mean and variance are generated when a squeezed state hits an imperfect detector""" + S = Sgate(r=r, phi=phi) + ps = Vacuum(1) >> S >> PNRDetector(efficiency=eta, dark_counts=dc) + assert np.allclose(np.sum(ps), 1.0) + mean = np.arange(len(ps)) @ ps.numpy() + expected_mean = eta * np.sinh(r) ** 2 + dc + assert np.allclose(mean, expected_mean) + variance = np.arange(len(ps)) ** 2 @ ps.numpy() - mean**2 + expected_variance = eta * np.sinh(r) ** 2 * (1 + eta * (1 + 2 * np.sinh(r) ** 2)) + dc + assert np.allclose(variance, expected_variance) + + @given( + r=st.floats(0, 0.5), + phi=st.floats(0, 2 * np.pi), + eta_s=st.floats(0, 1), + eta_i=st.floats(0, 1), + dc_s=st.floats(0, 0.2), + dc_i=st.floats(0, 0.2), + ) + def test_detector_two_mode_squeezed_state(self, r, phi, eta_s, eta_i, dc_s, dc_i): + """Tests the correct mean and variance are generated when a two mode squeezed state hits an imperfect detector""" + pnr = PNRDetector(efficiency=[eta_s, eta_i], dark_counts=[dc_s, dc_i]) + ps = Vacuum(2) >> S2gate(r=r, phi=phi) >> pnr + n = np.arange(len(ps)) + mean_s = np.sum(ps, axis=1) @ n + n_s = eta_s * np.sinh(r) ** 2 + expected_mean_s = n_s + dc_s + mean_i = np.sum(ps, axis=0) @ n + n_i = eta_i * np.sinh(r) ** 2 + expected_mean_i = n_i + dc_i + expected_mean_s = n_s + dc_s + var_s = np.sum(ps, axis=1) @ n**2 - mean_s**2 + var_i = np.sum(ps, axis=0) @ n**2 - mean_i**2 + expected_var_s = n_s * (n_s + 1) + dc_s + expected_var_i = n_i * (n_i + 1) + dc_i + covar = n @ ps.numpy() @ n - mean_s * mean_i + expected_covar = eta_s * eta_i * (np.sinh(r) * np.cosh(r)) ** 2 + assert np.allclose(mean_s, expected_mean_s) + assert np.allclose(mean_i, expected_mean_i) + assert np.allclose(var_s, expected_var_s) + assert np.allclose(var_i, expected_var_i) + assert np.allclose(covar, expected_covar) + + def test_postselection(self): + """Check the correct state is heralded for a two-mode squeezed vacuum with perfect detector""" + n_mean = 1.0 + n_measured = 1 + cutoff = 3 + detector = PNRDetector(efficiency=1.0, dark_counts=0.0, cutoffs=[cutoff]) + S2 = S2gate(r=np.arcsinh(np.sqrt(n_mean)), phi=0.0) + proj_state = (Vacuum(2) >> S2 >> detector)[n_measured] + success_prob = math.real(math.trace(proj_state)) + proj_state = proj_state / math.trace(proj_state) + # outputs the ket/dm in the third mode by projecting the first and second in 1,2 photons + expected_prob = 1 / (1 + n_mean) * (n_mean / (1 + n_mean)) ** n_measured + assert np.allclose(success_prob, expected_prob) + expected_state = np.zeros_like(proj_state) + expected_state[n_measured, n_measured] = 1.0 + assert np.allclose(proj_state, expected_state) + + @given(eta=st.floats(0, 1)) + def test_loss_probs(self, eta): + "Checks that a lossy channel is equivalent to quantum efficiency on detection probs" + ideal_detector = PNRDetector(efficiency=1.0, dark_counts=0.0) + lossy_detector = PNRDetector(efficiency=eta, dark_counts=0.0) + S = Sgate(r=0.2, phi=[0.0, 0.7]) + BS = BSgate(theta=1.4, phi=0.0) + L = Attenuator(transmissivity=eta) + dms_lossy = Vacuum(2) >> S[0, 1] >> BS[0, 1] >> lossy_detector[0] + dms_ideal = Vacuum(2) >> S[0, 1] >> BS[0, 1] >> L[0] >> ideal_detector[0] + assert np.allclose(dms_lossy, dms_ideal) + + +class TestHomodyneDetector: + """tests related to homodyne detectors""" + + @pytest.mark.parametrize("outcome", [None] + np.random.uniform(-5, 5, size=(10, 2)).tolist()) + def test_homodyne_mode_kwargs(self, outcome): + """Test that S gates and Homodyne mesurements are applied to the correct modes via the + `modes` kwarg. + + Here the initial state is a "diagonal" (angle=pi/2) squeezed state in mode 0, a "vertical" + (angle=0) squeezed state in mode 1 and vacuum in mode 2. Because the modes are separable, + measuring modes 1 and 2 should leave the state in the mode 0 unchaged. + + Also checks postselection ensuring the x-quadrature value is consistent with the + postselected value. + """ + + S1 = Sgate(modes=[0], r=1, phi=np.pi / 2) + S2 = Sgate(modes=[1], r=1, phi=0) + initial_state = Vacuum(3) >> S1 >> S2 + detector = Homodyne(result=outcome, quadrature_angle=[0.0, 0.0], modes=[1, 2]) + final_state = initial_state << detector + + expected_state = Vacuum(1) >> S1 + + assert np.allclose(final_state.dm(), expected_state.dm()) + + if outcome is not None: + # checks postselection ensuring the x-quadrature + # value is consistent with the postselected value + x_outcome = detector.outcome.numpy()[:2] + assert np.allclose(x_outcome, outcome) + + @given(s=st.floats(min_value=0.0, max_value=10.0), outcome=none_or_(st.floats(-10.0, 10.0))) + def test_homodyne_on_2mode_squeezed_vacuum(self, s, outcome): + """Check that homodyne detection on TMSV for q-quadrature (``quadrature_angle=0.0``)""" + r = settings.HOMODYNE_SQUEEZING + detector = Homodyne(quadrature_angle=0.0, result=outcome, r=r) + remaining_state = TMSV(r=np.arcsinh(np.sqrt(abs(s)))) << detector[0] + + # assert expected covariance matrix + cov = (hbar / 2.0) * np.diag( [ 1 - 2 * s / (1 / np.tanh(r) * (1 + s) + s), 1 + 2 * s / (1 / np.tanh(r) * (1 + s) - s), ] ) - * settings.HBAR - / 2.0 - ) - assert np.allclose(remaining_state.cov, cov) - means = np.array([2 * np.sqrt(s * (1 + s)) * X / (np.exp(-2 * r) + 1 + 2 * s), 0.0]) * np.sqrt( - 2 * settings.HBAR - ) - assert np.allclose(remaining_state.means, means) - - -@given(s=st.floats(1.0, 10.0), X=st.floats(-5.0, 5.0), angle=st.floats(0, np.pi)) -def test_homodyne_on_2mode_squeezed_vacuum_with_angle(s, X, angle): - r"""Check that homodyne detection on TMSV works with an arbitrary quadrature angle""" - homodyne = Homodyne(quadrature_angle=angle, result=X) - r = homodyne.r.value - remaining_state = TMSV(r=np.arcsinh(np.sqrt(abs(s)))) << homodyne[0] - denom = 1 + 2 * s * (s + 1) + (2 * s + 1) * np.cosh(2 * r) - cov = ( - settings.HBAR - / 2 - * np.array( + assert np.allclose(remaining_state.cov, cov) + + # assert expected means vector, not tested when sampling (i.e. ``outcome == None``) + # because we cannot access the sampled outcome value + if outcome is not None: + means = np.array( + [2 * np.sqrt(s * (1 + s)) * outcome / (np.exp(-2 * r) + 1 + 2 * s), 0.0] + ) + assert np.allclose(remaining_state.means.numpy(), means) + + @given(s=st.floats(1.0, 10.0), outcome=none_or_(st.floats(-2, 2)), angle=st.floats(0, np.pi)) + def test_homodyne_on_2mode_squeezed_vacuum_with_angle(self, s, outcome, angle): + """Check that homodyne detection on TMSV works with an arbitrary quadrature angle""" + r = settings.HOMODYNE_SQUEEZING + detector = Homodyne(quadrature_angle=angle, result=outcome, r=r) + remaining_state = TMSV(r=np.arcsinh(np.sqrt(abs(s)))) << detector[0] + denom = 1 + 2 * s * (s + 1) + (2 * s + 1) * np.cosh(2 * r) + cov = (hbar / 2) * np.array( [ [ 1 @@ -193,134 +227,172 @@ def test_homodyne_on_2mode_squeezed_vacuum_with_angle(s, X, angle): ], ] ) + assert np.allclose(remaining_state.cov.numpy(), cov, atol=1e-5) + # TODO: figure out why this is not working + # if outcome is not None: + # outcome = outcome * np.sqrt(hbar) + # denom = 1 + 2 * s * (1 + s) + (1 + 2 * s) * np.cosh(2 * r) + # expected_means = ( + # np.array( + # [ + # np.sqrt(s * (1 + s)) * outcome * (np.cos(angle) * (1 + 2 * s + np.cosh(2 * r)) + np.sinh(2 * r)) / denom, + # -np.sqrt(s * (1 + s)) * outcome * (np.sin(angle) * (1 + 2 * s + np.cosh(2 * r))) / denom + # ] + # ) + # ) + # means = remaining_state.means.numpy() + # assert np.allclose(means, expected_means) + + @given( + s=st.floats(min_value=0.0, max_value=10.0), + X=st.floats(-10.0, 10.0), + d=arrays(np.float64, 4, elements=st.floats(-10.0, 10.0)), ) - assert np.allclose(remaining_state.cov, cov) - # TODO: figure out why this is not working - # denom = 1 + 2 * s * (1 + s) + (1 + 2 * s) * np.cosh(2 * r) - # means = ( - # np.array( - # [ - # np.sqrt(s * (1 + s)) - # * X - # * (np.cos(angle) * (1 + 2 * s + np.cosh(2 * r)) + np.sinh(2 * r)) - # / denom, - # -np.sqrt(s * (1 + s)) * X * (np.sin(angle) * (1 + 2 * s + np.cosh(2 * r))) / denom, - # ] - # ) - # * np.sqrt(2 * settings.HBAR) - # ) - # assert np.allclose(remaining_state.means, means) - - -@given( - s=st.floats(min_value=0.0, max_value=10.0), - X=st.floats(-10.0, 10.0), - d=arrays(np.float64, 4, elements=st.floats(-10.0, 10.0)), -) -def test_homodyne_on_2mode_squeezed_vacuum_with_displacement(s, X, d): - tmsv = TMSV(r=np.arcsinh(np.sqrt(s))) >> Dgate(x=d[:2], y=d[2:]) - homodyne = Homodyne(modes=[0], quadrature_angle=0.0, result=X) - r = homodyne.r.value - remaining_state = tmsv << homodyne[0] - xb, xa, pb, pa = d - means = np.array( - [ - xa - + (2 * np.sqrt(s * (s + 1)) * (X - xb)) / (1 + 2 * s + np.cosh(2 * r) - np.sinh(2 * r)), - pa + (2 * np.sqrt(s * (s + 1)) * pb) / (1 + 2 * s + np.cosh(2 * r) + np.sinh(2 * r)), - ] - ) * np.sqrt(2 * settings.HBAR) - assert np.allclose(remaining_state.means, means) - - -@given( - s=st.floats(min_value=0.0, max_value=10.0), - x=st.floats(-10.0, 10.0), - y=st.floats(-10.0, 10.0), - d=arrays(np.float64, 4, elements=st.floats(-10.0, 10.0)), -) -def test_heterodyne_on_2mode_squeezed_vacuum_with_displacement( - s, x, y, d -): # TODO: check if this is correct - r"""Check that heterodyne detection on TMSV works with an arbitrary displacement""" - tmsv = TMSV(r=np.arcsinh(np.sqrt(s))) >> Dgate(x=d[:2], y=d[2:]) - heterodyne = Heterodyne(modes=[0], x=x, y=y) - remaining_state = tmsv << heterodyne[0] - cov = settings.HBAR / 2 * np.array([[1, 0], [0, 1]]) - assert np.allclose(remaining_state.cov, cov) - xb, xa, pb, pa = d - means = ( - np.array( + def test_homodyne_on_2mode_squeezed_vacuum_with_displacement(self, s, X, d): + """Check that homodyne detection on displaced TMSV works""" + tmsv = TMSV(r=np.arcsinh(np.sqrt(s))) >> Dgate(x=d[:2], y=d[2:]) + r = settings.HOMODYNE_SQUEEZING + detector = Homodyne(modes=[0], quadrature_angle=0.0, result=X, r=r) + remaining_state = tmsv << detector + xb, xa, pb, pa = np.sqrt(2 * hbar) * d + expected_means = np.array( [ - xa * (1 + s) + np.sqrt(s * (1 + s)) * (x - xb), - pa * (1 + s) + np.sqrt(s * (1 + s)) * (pb - y), + xa + + (2 * np.sqrt(s * (s + 1)) * (X - xb)) + / (1 + 2 * s + np.cosh(2 * r) - np.sinh(2 * r)), + pa + + (2 * np.sqrt(s * (s + 1)) * pb) / (1 + 2 * s + np.cosh(2 * r) + np.sinh(2 * r)), ] ) - * np.sqrt(2 * settings.HBAR) - / (1 + s) - ) - assert np.allclose(remaining_state.means, means, atol=1e-5) - -def test_norm_1mode(): - assert np.allclose( - Coherent(2.0) << Fock(3), - np.abs((2.0**3) / np.sqrt(6) * np.exp(-0.5 * 4.0)) ** 2, - ) + means = remaining_state.means.numpy() + assert np.allclose(means, expected_means) + N_MEAS = 350 # number of homodyne measurements to perform + NUM_STDS = 10.0 + std_10 = NUM_STDS / np.sqrt(N_MEAS) -def test_norm_2mode(): - leftover = Coherent(x=[2.0, 2.0]) << Fock(3)[0] - assert np.isclose( - (2.0**3) / np.sqrt(6) * np.exp(-0.5 * 4.0), physics.norm(leftover), atol=1e-5 + @pytest.mark.parametrize( + "state, mean_expected, var_expected", + [ + (Vacuum(1), 0.0, settings.HBAR / 2), + (Coherent(2.0, 0.5), 2.0 * np.sqrt(2 * settings.HBAR), settings.HBAR / 2), + (SqueezedVacuum(0.25, 0.0), 0.0, 0.25 * settings.HBAR / 2), + ], ) + @pytest.mark.parametrize("gaussian_state", [True, False]) + def test_sampling_mean_and_var(self, state, mean_expected, var_expected, gaussian_state): + """Tests that the mean and variance estimates of many homodyne + measurements are in agreement with the expected values for the states""" + tf.random.set_seed(123) + if not gaussian_state: + state = State(dm=state.dm(cutoffs=[40])) + detector = Homodyne(0.0) -def test_norm_2mode_normalized(): - leftover = Coherent(x=[2.0, 2.0]) << Fock(3, normalize=True)[0] - assert np.isclose(1.0, physics.norm(leftover), atol=1e-5) - + results = np.empty((self.N_MEAS, 2)) + for i in range(self.N_MEAS): + _ = state << detector + results[i] = detector.outcome.numpy() -def test_norm_2mode_gaussian_normalized(): - leftover = Coherent(x=[2.0, 2.0]) << Coherent(x=1.0, normalize=True)[0] - assert np.isclose(1.0, physics.norm(leftover), atol=1e-5) + mean = results.mean(axis=0) + assert np.allclose(mean[0], mean_expected, atol=self.std_10, rtol=0) + var = results.var(axis=0) + assert np.allclose(var[0], var_expected, atol=self.std_10, rtol=0) + def test_homodyne_squeezing_setting(self): + r"""Check default homodyne squeezing on settings leads to the correct generaldyne + covarince matrix: one that has tends to :math:`diag(1/\sigma[1,1],0)`.""" -def test_homodyne_mode_kwargs(): - """Test that S gates and Homodyne mesurements are applied to the correct modes via the `modes` kwarg. + sigma = np.identity(2) + sigma_m = SqueezedVacuum(r=settings.HOMODYNE_SQUEEZING, phi=0).cov.numpy() - Here the initial state is a "diagonal" (angle=pi/2) squeezed state in mode 0 - and a "vertical" (angle=0) squeezed state in mode 1. + inverse_covariance = np.linalg.inv(sigma + sigma_m) + assert np.allclose(inverse_covariance, np.diag([1 / sigma[1, 1], 0])) - Because the modes are separable, measuring in one mode should leave the state in the - other mode unchaged. - """ - S1 = Sgate(modes=[0], r=1, phi=np.pi / 2) - S2 = Sgate(modes=[1], r=1, phi=0) - initial_state = Vacuum(2) >> S1 >> S2 - final_state = initial_state << Homodyne(modes=[1], quadrature_angle=0, result=[0.3]) +class TestHeterodyneDetector: + """tests related to heterodyne detectors""" - expected_state = Vacuum(1) >> S1 - - assert np.allclose(final_state.dm(), expected_state.dm()) + @pytest.mark.parametrize( + "xy", [[None, None]] + np.random.uniform(-10, 10, size=(5, 2, 2)).tolist() + ) + def test_heterodyne_mode_kwargs(self, xy): + """Test that S gates and Heterodyne mesurements are applied to the correct modes via the `modes` kwarg. + Here the initial state is a "diagonal" (angle=pi/2) squeezed state in mode 0, + a "vertical" (angle=0) squeezed state in mode 1 and vacumm state in mode 2. -def test_heterodyne_mode_kwargs(): - """Test that S gates and Heterodyne mesurements are applied to the correct modes via the `modes` kwarg. + Because the modes are separable, measuring in mode 1 and 2 should leave the state in the + 0th mode unchaged. + """ + x, y = xy - Here the initial state is a "diagonal" (angle=pi/2) squeezed state in mode 0 - and a "vertical" (angle=0) squeezed state in mode 1. + S1 = Sgate(modes=[0], r=1, phi=np.pi / 2) + S2 = Sgate(modes=[1], r=1, phi=0) + initial_state = Vacuum(3) >> S1 >> S2 + final_state = initial_state << Heterodyne(x, y, modes=[1, 2]) - Because the modes are separable, measuring in one mode should leave the state in the - other mode unchaged. - """ + expected_state = Vacuum(1) >> S1 - S1 = Sgate(modes=[0], r=1, phi=np.pi / 2) - S2 = Sgate(modes=[1], r=1, phi=0) - initial_state = Vacuum(2) >> S1 >> S2 - final_state = initial_state << Heterodyne(modes=[1]) + assert np.allclose(final_state.dm(), expected_state.dm()) - expected_state = Vacuum(1) >> S1 + @given( + s=st.floats(min_value=0.0, max_value=10.0), + x=none_or_(st.floats(-10.0, 10.0)), + y=none_or_(st.floats(-10.0, 10.0)), + d=arrays(np.float64, 4, elements=st.floats(-10.0, 10.0)), + ) + def test_heterodyne_on_2mode_squeezed_vacuum_with_displacement( + self, s, x, y, d + ): # TODO: check if this is correct + """Check that heterodyne detection on TMSV works with an arbitrary displacement""" + if x is None or y is None: + x, y = None, None + + tmsv = TMSV(r=np.arcsinh(np.sqrt(s))) >> Dgate(x=d[:2], y=d[2:]) + heterodyne = Heterodyne(modes=[0], x=x, y=y) + remaining_state = tmsv << heterodyne[0] + + # assert expected covariance + cov = hbar / 2 * np.array([[1, 0], [0, 1]]) + assert np.allclose(remaining_state.cov.numpy(), cov) + + # assert expected means vector, not tested when x or y is None + # because we cannot access the sampled outcome value + if (x is not None) and (y is not None): + xb, xa, pb, pa = d * np.sqrt(2 * hbar) + means = np.array( + [ + xa * (1 + s) + np.sqrt(s * (1 + s)) * (x - xb), + pa * (1 + s) + np.sqrt(s * (1 + s)) * (pb - y), + ] + ) / (1 + s) + assert np.allclose(remaining_state.means, means, atol=1e-5) + + +class TestNormalization: + """tests evaluating normalization of output states after projection""" + + def test_norm_1mode(self): + """Checks that projecting a single mode coherent state onto a number state + returns the expected norm.""" + assert np.allclose( + Coherent(2.0) << Fock(3), + np.abs((2.0**3) / np.sqrt(6) * np.exp(-0.5 * 4.0)) ** 2, + ) - assert np.allclose(final_state.dm(), expected_state.dm()) + @pytest.mark.parametrize( + "normalize, expected_norm", + ([True, 1.0], [False, (2.0**3) / np.sqrt(6) * np.exp(-0.5 * 4.0)]), + ) + def test_norm_2mode(self, normalize, expected_norm): + """Checks that projecting a two-mode coherent state onto a number state + produces a state with the expected norm.""" + leftover = Coherent(x=[2.0, 2.0]) << Fock(3, normalize=normalize)[0] + assert np.isclose(expected_norm, physics.norm(leftover), atol=1e-5) + + def test_norm_2mode_gaussian_normalized(self): + """Checks that after projection the norm of the leftover state is as expected.""" + leftover = Coherent(x=[2.0, 2.0]) << Coherent(x=1.0, normalize=True)[0] + assert np.isclose(1.0, physics.norm(leftover), atol=1e-5) diff --git a/tests/test_lab/test_states.py b/tests/test_lab/test_states.py index 5eb79d932..29c8ad737 100644 --- a/tests/test_lab/test_states.py +++ b/tests/test_lab/test_states.py @@ -167,6 +167,19 @@ def test_get_modes(): assert b == (a & b).get_modes([2, 3]) +def test_get_single_mode(): + """Test get_modes leaves a single-mode state untouched.""" + a = Gaussian(1)[1] + assert a == a.get_modes([1]) + + +def test_get_single_mode_fail(): + """Test get_modes leaves a single-mode state untouched.""" + a = Gaussian(1)[1] + with pytest.raises(ValueError): + a.get_modes([0]) + + def test_iter(): """Test we can iterate individual modes in states.""" a = Gaussian(1) diff --git a/tests/test_math/test_special.py b/tests/test_math/test_special.py new file mode 100644 index 000000000..542f39332 --- /dev/null +++ b/tests/test_math/test_special.py @@ -0,0 +1,39 @@ +# Copyright 2022 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. + +"""Test special functions of the math backend""" + +import numpy as np +from scipy.special import eval_hermite, factorial +from mrmustard.math import Math + +math = Math() + + +def test_reduction_to_renorm_physicists_polys(): + """Tests that the math interface provides the expected renormalized Hermite polys""" + x = np.arange(-1, 1, 0.1) + init = 1 + n_max = 5 + A = np.ones([init, init], dtype=complex) + vals = np.array( + [ + math.hermite_renormalized( + 2 * A, np.array([x0], dtype=complex), 1, n_max, modified=False + ) + for x0 in x + ] + ).T + expected = np.array([eval_hermite(i, x) / np.sqrt(factorial(i)) for i in range(len(vals))]) + assert np.allclose(vals, expected) From 30f8ce40e3af6b6727f6dbf1e47fc33b4f5c234f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Duque=20Mesa?= <675763+sduquemesa@users.noreply.github.com> Date: Mon, 17 Oct 2022 11:18:12 -0500 Subject: [PATCH 03/53] Rotation gate unitary (#155) **Context:** The rotation gate unitary is currently being calculated using the Choi isomorphisms which generates `nan` for angles of value zero where the identity matrix should be returned. **Description of the Change:** The rotation gate being diagonal in the Fock basis is easy to implement with better speed performance. This PR implements the `U` method of the rotation gate overriding the default way of calculating the unitaries of Gaussian transforms. **Benefits:** Fast computation of the rotation gate Fock representation which avoids invalid numerical outcomes. **Possible Drawbacks:** None --- mrmustard/lab/gates.py | 41 ++++++++++++++++++++++++++++++- mrmustard/math/math_interface.py | 12 +++++++++ mrmustard/math/tensorflow.py | 3 +++ tests/test_lab/test_gates_fock.py | 32 +++++++++++++++++++++++- 4 files changed, 86 insertions(+), 2 deletions(-) diff --git a/mrmustard/lab/gates.py b/mrmustard/lab/gates.py index 167c830e4..5db1aa175 100644 --- a/mrmustard/lab/gates.py +++ b/mrmustard/lab/gates.py @@ -18,7 +18,8 @@ This module defines gates and operations that can be applied to quantum modes to construct a quantum circuit. """ -from typing import Union, Optional, List, Tuple +from typing import Union, Optional, List, Tuple, Sequence +import numpy as np from mrmustard.types import Tensor from mrmustard import settings from mrmustard.training import Parametrized @@ -176,6 +177,44 @@ def __init__( def X_matrix(self): return gaussian.rotation_symplectic(self.angle.value) + def U(self, cutoffs: Sequence[int]): + + angles = self._parse_modes_and_args(cutoffs) + num_modes = len(cutoffs) + + # calculate rotation unitary for each mode and concatenate with outer product + Ur = None + for idx, cutoff in enumerate(cutoffs): + theta = math.arange(cutoff) * angles[idx] + if Ur is None: + Ur = math.diag(math.make_complex(math.cos(theta), math.sin(theta))) + else: + U_next = math.diag(math.make_complex(math.cos(theta), math.sin(theta))) + Ur = math.outer(Ur, U_next) + + # return total unitary with indexes reordered according to MM convetion + return math.transpose( + Ur, + list(range(0, 2 * num_modes, 2)) + list(range(1, 2 * num_modes, 2)), + ) + + def _parse_modes_and_args(self, cutoffs): + num_modes = len(cutoffs) + modes = self.modes # modes in which the gate is acting on + args = self.angle.value + num_args = ( + args.shape[0] if len(args.shape.as_list()) > 0 else 1 + ) # number or arguments given to the gate + angles = np.zeros((num_modes,)) + if num_args == 1: + # one arg for all modes + angles[modes] = args + else: + # an arg for each mode + angles = args + + return math.new_variable(angles, bounds=None, name="Rgate_angles") + class Pgate(Parametrized, Transformation): r"""Quadratic phase gate. diff --git a/mrmustard/math/math_interface.py b/mrmustard/math/math_interface.py index ea4ec0c1a..f8ea4d487 100644 --- a/mrmustard/math/math_interface.py +++ b/mrmustard/math/math_interface.py @@ -252,6 +252,18 @@ def cosh(self, array: Tensor) -> Tensor: array: hyperbolic cosine of array """ + def make_complex(self, real: Tensor, imag: Tensor) -> Tensor: + """Given two real tensors representing the real and imaginary part of a complex number, + this operation returns a complex tensor. The input tensors must have the same shape. + + Args: + real (array): real part of the complex number + imag (array): imaginary part of the complex number + + Returns: + array: complex array ``real + 1j * imag`` + """ + @abstractmethod def det(self, matrix: Tensor) -> Tensor: r"""Returns the determinant of matrix. diff --git a/mrmustard/math/tensorflow.py b/mrmustard/math/tensorflow.py index 54262065c..3bddea474 100644 --- a/mrmustard/math/tensorflow.py +++ b/mrmustard/math/tensorflow.py @@ -116,6 +116,9 @@ def cos(self, array: tf.Tensor) -> tf.Tensor: def cosh(self, array: tf.Tensor) -> tf.Tensor: return tf.math.cosh(array) + def make_complex(self, real: tf.Tensor, imag: tf.Tensor) -> tf.Tensor: + return tf.complex(real, imag) + def det(self, matrix: tf.Tensor) -> tf.Tensor: return tf.linalg.det(matrix) diff --git a/tests/test_lab/test_gates_fock.py b/tests/test_lab/test_gates_fock.py index f4cace8ac..498484b59 100644 --- a/tests/test_lab/test_gates_fock.py +++ b/tests/test_lab/test_gates_fock.py @@ -13,8 +13,9 @@ # limitations under the License. import pytest -from mrmustard.lab.circuit import Circuit +from mrmustard import settings from mrmustard.lab.states import Fock, State, SqueezedVacuum, TMSV +from mrmustard.physics import fock from mrmustard.lab.gates import ( Dgate, Sgate, @@ -128,6 +129,35 @@ def test_fock_representation_mzgate(phi_a, phi_b): assert np.allclose(expected, MZ.U(cutoffs=[20, 20]), atol=1e-5) +@pytest.mark.parametrize( + "cutoffs,angles,modes", + [ + [[5, 4, 3], [np.pi, np.pi / 2, np.pi / 4], None], + [[3, 4], [np.pi / 3, np.pi / 2], [0, 1]], + [[3], np.pi / 6, [0]], + ], +) +def test_fock_representation_rgate(cutoffs, angles, modes): + """Tests that DGate returns the correct unitary.""" + + # apply gate + rgate = Rgate(angles, modes=modes) + R = rgate.U(cutoffs) + + # compare with the standard way of calculating + # transformation unitaries using the Choi isomorphism + choi_state = rgate.bell >> rgate + expected_R = fock.fock_representation( + choi_state.cov, + choi_state.means, + shape=cutoffs * 2, + return_unitary=True, + choi_r=settings.CHOI_R, + ) + + assert np.allclose(R, expected_R, atol=1e-5) + + def test_raise_interferometer_error(): """test Interferometer raises an error when both `modes` and `num_modes` are given""" num_modes = 3 From 8265f8981387353969de1f2facd2941158b8f814 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Duque=20Mesa?= <675763+sduquemesa@users.noreply.github.com> Date: Mon, 17 Oct 2022 12:47:16 -0500 Subject: [PATCH 04/53] Calculate displacement operator unitary matrix using The Walrus (#147) **Context:** [The Walrus PR #351](https://github.com/XanaduAI/thewalrus/pull/351) implemented a more stable calculation of the displacement unitary. **Description of the Change:** The `Dgate` now uses The Walrus to calculate the unitary and gradients of the displacement gate. Before: ![before](https://user-images.githubusercontent.com/675763/193073762-42a82fe8-7bcf-405b-ade0-e59e4fcdf270.png) After: ![after](https://user-images.githubusercontent.com/675763/193073791-e573381f-3e97-4dc1-97e5-161897b34f0d.png) **Benefits:** This provides better numerical stability for larger cutoff and displacement values. **Possible Drawbacks:** None **Related GitHub Issues:** None --- .github/CHANGELOG.md | 4 +++ mrmustard/lab/gates.py | 51 ++++++++++++++++++++++++++++ mrmustard/math/math_interface.py | 25 ++++++++++++++ mrmustard/math/tensorflow.py | 22 ++++++++++++ tests/test_lab/test_gates_fock.py | 56 ++++++++++++++++++++++--------- 5 files changed, 143 insertions(+), 15 deletions(-) diff --git a/.github/CHANGELOG.md b/.github/CHANGELOG.md index acf49d39f..748d79fcc 100644 --- a/.github/CHANGELOG.md +++ b/.github/CHANGELOG.md @@ -22,6 +22,9 @@ conditional state on the remaining modes is generated. ### Improvements +* The `Dgate` now uses The Walrus to calculate the unitary and gradients of the displacement gate in fock representation, + providing better numerical stability for larger cutoff and displacement values. [(#147)](https://github.com/XanaduAI/MrMustard/pull/147) + ### Bug fixes ### Documentation @@ -150,6 +153,7 @@ This release contains contributions from (in alphabetical order): applied to. [(#121)](https://github.com/XanaduAI/MrMustard/pull/121) + ### Bug fixes * Fixed a bug in the `State.ket()` method. An attribute was called with a typo in its name. [(#135)](https://github.com/XanaduAI/MrMustard/pull/135) diff --git a/mrmustard/lab/gates.py b/mrmustard/lab/gates.py index 5db1aa175..77da4968e 100644 --- a/mrmustard/lab/gates.py +++ b/mrmustard/lab/gates.py @@ -93,6 +93,57 @@ def __init__( def d_vector(self): return gaussian.displacement(self.x.value, self.y.value, settings.HBAR) + def U(self, cutoffs: List[int]): + """Returns the unitary representation of the Displacement gate using the Laguerre + polynomials.""" + + N = len(cutoffs) + x, y = self._parse_modes_and_args(cutoffs) + + r = math.sqrt(x**2 + y**2) + phi = math.atan2(y, x) + + # calculate displacement unitary for each mode and concatenate with outer product + Ud = None + for idx, cutoff in enumerate(cutoffs): + if Ud is None: + Ud = math.displacement(r[idx], phi[idx], cutoff) + else: + U_next = math.displacement(r[idx], phi[idx], cutoff) + Ud = math.outer(Ud, U_next) + + return math.transpose( + Ud, + list(range(0, 2 * N, 2)) + list(range(1, 2 * N, 2)), + ) + + def _parse_modes_and_args(self, cutoffs): + num_modes = len(cutoffs) + modes = self.modes # modes in which the gate is acting on + xargs = self.x.value + yargs = self.y.value + num_args_x = ( + xargs.shape[0] if len(xargs.shape.as_list()) > 0 else 1 + ) # number or arguments given to the gate + num_args_y = ( + yargs.shape[0] if len(xargs.shape.as_list()) > 0 else 1 + ) # number or arguments given to the gate + x = np.zeros((num_modes,)) + y = np.zeros((num_modes,)) + + if num_args_x != num_args_y: + raise ValueError("Number of parameters for `x` and `y` is different.") + + if num_args_x == 1 or num_args_x == len(modes): + # one arg for all modes + x[modes] = xargs + y[modes] = yargs + elif num_args_x == len(modes): + # number of args and number of modes don't match + raise ValueError("Number of args and modes don't match") + + return x, y + class Sgate(Parametrized, Transformation): r"""Squeezing gate. diff --git a/mrmustard/math/math_interface.py b/mrmustard/math/math_interface.py index f8ea4d487..b2486cbe1 100644 --- a/mrmustard/math/math_interface.py +++ b/mrmustard/math/math_interface.py @@ -264,6 +264,18 @@ def make_complex(self, real: Tensor, imag: Tensor) -> Tensor: array: complex array ``real + 1j * imag`` """ + @abstractmethod + def atan2(self, y: Tensor, x: Tensor) -> Tensor: + r"""Computes the trignometric inverse tangent of y/x element-wise. + + Args: + y (array): numerator array + x (array): denominator array + + Returns: + array: arctan of y/x + """ + @abstractmethod def det(self, matrix: Tensor) -> Tensor: r"""Returns the determinant of matrix. @@ -406,6 +418,19 @@ def hermite_renormalized( array: renormalized hermite polynomials """ + @abstractmethod + def displacement(self, r: Scalar, phi: Scalar, cutoff: Scalar, dtype): + r"""Calculates the matrix elements of the displacement gate and its derivatives. + + Args: + r (float): displacement magnitude + phi (float): displacement angle + cutoff (int): Fock ladder cutoff + dtype (data type): Specifies the data type used for the calculation + Returns: + Tuple(array[complex], function): matrix representing the displacement operation and its gradient + """ + @abstractmethod def imag(self, array: Tensor) -> Tensor: r"""Returns the imaginary part of array. diff --git a/mrmustard/math/tensorflow.py b/mrmustard/math/tensorflow.py index 3bddea474..6469bca9e 100644 --- a/mrmustard/math/tensorflow.py +++ b/mrmustard/math/tensorflow.py @@ -18,6 +18,10 @@ import tensorflow as tf import tensorflow_probability as tfp from thewalrus import hermite_multidimensional, grad_hermite_multidimensional +from thewalrus.fock_gradients import ( + displacement as displacement_tw, + grad_displacement as grad_displacement_tw, +) from mrmustard.math.autocast import Autocast from mrmustard.types import ( @@ -116,6 +120,9 @@ def cos(self, array: tf.Tensor) -> tf.Tensor: def cosh(self, array: tf.Tensor) -> tf.Tensor: return tf.math.cosh(array) + def atan2(self, y: tf.Tensor, x: tf.Tensor) -> tf.Tensor: + return tf.math.atan2(y, x) + def make_complex(self, real: tf.Tensor, imag: tf.Tensor) -> tf.Tensor: return tf.complex(real, imag) @@ -378,6 +385,21 @@ def grad(dLdpoly): return poly, grad + @tf.custom_gradient + def displacement(self, r, phi, cutoff, dtype=tf.complex64.as_numpy_dtype, tol=1e-15): + """creates a single mode displacement matrix""" + r = r.numpy() + phi = phi.numpy() + gate = displacement_tw(r, phi, cutoff, dtype) if r > tol else self.eye(cutoff) + + def grad(dy): # pragma: no cover + Dr, Dphi = grad_displacement_tw(gate, r, phi) + grad_r = tf.math.real(tf.reduce_sum(dy * tf.math.conj(Dr))) + grad_phi = tf.math.real(tf.reduce_sum(dy * tf.math.conj(Dphi))) + return grad_r, grad_phi, None + + return gate, grad + @staticmethod def eigvals(tensor: tf.Tensor) -> Tensor: """Returns the eigenvalues of a matrix.""" diff --git a/tests/test_lab/test_gates_fock.py b/tests/test_lab/test_gates_fock.py index 498484b59..20266bd03 100644 --- a/tests/test_lab/test_gates_fock.py +++ b/tests/test_lab/test_gates_fock.py @@ -13,6 +13,17 @@ # limitations under the License. import pytest +from hypothesis import given, strategies as st +import numpy as np +from thewalrus.fock_gradients import ( + squeezing, + beamsplitter, + two_mode_squeezing, + mzgate, +) + +from tests import random +from mrmustard.physics import fock from mrmustard import settings from mrmustard.lab.states import Fock, State, SqueezedVacuum, TMSV from mrmustard.physics import fock @@ -29,16 +40,6 @@ Attenuator, Interferometer, ) -from hypothesis import given, strategies as st -from thewalrus.fock_gradients import ( - displacement, - squeezing, - beamsplitter, - two_mode_squeezing, - mzgate, -) -import numpy as np -from tests import random @given(state=random.pure_state(num_modes=1), xy=random.vector(2)) @@ -88,11 +89,36 @@ def test_two_mode_fock_equals_gaussian(gate): assert np.allclose(via_fock_space_dm, via_phase_space_dm) -@given(x=st.floats(min_value=-2, max_value=2), y=st.floats(min_value=-2, max_value=2)) -def test_fock_representation_displacement(x, y): - D = Dgate(x=x, y=y) - expected = displacement(r=np.sqrt(x**2 + y**2), phi=np.arctan2(y, x), cutoff=20) - assert np.allclose(expected, D.U(cutoffs=[20]), atol=1e-5) +@pytest.mark.parametrize( + "cutoffs,x,y", + [ + [[5], 0.3, 0.5], + [[5], 0.0, 0.0], + [[2, 2], [0.1, 0.1], [0.25, -0.2]], + [[3, 3], [0.0, 0.0], [0.0, 0.0]], + [[2, 5, 1], [0.1, 5.0, 1.0], [-0.3, 0.1, 0.0]], + [[3, 3, 3, 3], [0.1, 0.2, 0.3, 0.4], [-0.5, -4, 3.1, 4.2]], + ], +) +def test_fock_representation_displacement(cutoffs, x, y): + """Tests that DGate returns the correct unitary.""" + + # apply gate + dgate = Dgate(x, y) + Ud = dgate.U(cutoffs) + + # compare with the standard way of calculating + # transformation unitaries using the Choi isomorphism + choi_state = dgate.bell >> dgate + expected_Ud = fock.fock_representation( + choi_state.cov, + choi_state.means, + shape=cutoffs * 2, + return_unitary=True, + choi_r=settings.CHOI_R, + ) + + assert np.allclose(Ud, expected_Ud, atol=1e-5) @given(r=st.floats(min_value=0, max_value=2), phi=st.floats(min_value=0, max_value=2 * np.pi)) From 39c8b4bcd49699c88a90baadfc462a11c21bb4f6 Mon Sep 17 00:00:00 2001 From: ziofil Date: Wed, 19 Oct 2022 21:12:43 -0400 Subject: [PATCH 05/53] adds eye_like function to interface and tf backend (#170) ------------------------------------------------------------------------------------------------------------ **Context:** Math interface **Description of the Change:** Adds `eye_like` function, which returns the identity matrix to match the shape and dtype of the argument. **Benefits:** Simpler to get the identity of the right size and dtype **Possible Drawbacks:** Not standard **Related GitHub Issues:** None --- mrmustard/math/math_interface.py | 11 +++++++++++ mrmustard/math/tensorflow.py | 3 +++ 2 files changed, 14 insertions(+) diff --git a/mrmustard/math/math_interface.py b/mrmustard/math/math_interface.py index b2486cbe1..7b9829fb2 100644 --- a/mrmustard/math/math_interface.py +++ b/mrmustard/math/math_interface.py @@ -372,6 +372,17 @@ def eye(self, size: int, dtype) -> Tensor: matrix: identity matrix """ + @abstractmethod + def eye_like(self, array: Tensor) -> Tensor: + r"""Returns the identity matrix of the same shape and dtype as array. + + Args: + array (array): array to create the identity matrix of + + Returns: + matrix: identity matrix + """ + @abstractmethod def from_backend(self, value: Any) -> bool: r"""Returns whether the given tensor is a tensor of the concrete backend.""" diff --git a/mrmustard/math/tensorflow.py b/mrmustard/math/tensorflow.py index 6469bca9e..8742dbad7 100644 --- a/mrmustard/math/tensorflow.py +++ b/mrmustard/math/tensorflow.py @@ -150,6 +150,9 @@ def expm(self, matrix: tf.Tensor) -> tf.Tensor: def eye(self, size: int, dtype=tf.float64) -> tf.Tensor: return tf.eye(size, dtype=dtype) + def eye_like(self, array: tf.Tensor) -> Tensor: + return tf.eye(array.shape[-1], dtype=array.dtype) + def from_backend(self, value) -> bool: return isinstance(value, (tf.Tensor, tf.Variable)) From a3a238a9558aee1200ba7867e75ddc1213ae85b4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Duque=20Mesa?= <675763+sduquemesa@users.noreply.github.com> Date: Tue, 1 Nov 2022 11:32:59 -0500 Subject: [PATCH 06/53] Refactor Wigner function into module (#171) **Context:** Calculation of the Wigner function is already included in Mr Mustard but hidden in the graphics module. **Description of the Change:** This PR moves the Wigner function calculation to its own module and numbifies it. **Benefits:** Now you can calculate the Wigner function using ```python from mrmustard.utils.wigner import wigner_discretized wigner_discretized(dm, q, p) # dm is a density matrix ``` It should be faster as well because it uses numba jit. **Possible Drawbacks:** None --- .github/CHANGELOG.md | 17 +++-- mrmustard/utils/graphics.py | 81 +++++---------------- mrmustard/utils/wigner.py | 66 +++++++++++++++++ tests/test_utils/test_wigner.py | 123 ++++++++++++++++++++++++++++++++ 4 files changed, 218 insertions(+), 69 deletions(-) create mode 100644 mrmustard/utils/wigner.py create mode 100644 tests/test_utils/test_wigner.py diff --git a/.github/CHANGELOG.md b/.github/CHANGELOG.md index 748d79fcc..8868c5586 100644 --- a/.github/CHANGELOG.md +++ b/.github/CHANGELOG.md @@ -3,9 +3,9 @@ ### New features * Sampling for homodyne measurements is now integrated in Mr Mustard: when no measurement outcome value is -specified by the user, a value is sampled from the reduced state probability distribution and the -conditional state on the remaining modes is generated. -[(#143)](https://github.com/XanaduAI/MrMustard/pull/143) + specified by the user, a value is sampled from the reduced state probability distribution and the + conditional state on the remaining modes is generated. + [(#143)](https://github.com/XanaduAI/MrMustard/pull/143) ```python import numpy as np @@ -23,7 +23,16 @@ conditional state on the remaining modes is generated. ### Improvements * The `Dgate` now uses The Walrus to calculate the unitary and gradients of the displacement gate in fock representation, - providing better numerical stability for larger cutoff and displacement values. [(#147)](https://github.com/XanaduAI/MrMustard/pull/147) + providing better numerical stability for larger cutoff and displacement values. + [(#147)](https://github.com/XanaduAI/MrMustard/pull/147) + +* Now the Wigner function is implemented in its own module and uses numba for speed. + [(#171)](https://github.com/XanaduAI/MrMustard/pull/171) + + ```python + from mrmustard.utils.wigner import wigner_discretized + W, Q, P = wigner_discretized(dm, q, p) # dm is a density matrix + ``` ### Bug fixes diff --git a/mrmustard/utils/graphics.py b/mrmustard/utils/graphics.py index ea35fb598..3d9510cfd 100644 --- a/mrmustard/utils/graphics.py +++ b/mrmustard/utils/graphics.py @@ -14,13 +14,13 @@ """A module containing utility classes and functions for graphical display.""" -from copy import copy from typing import Tuple from rich.progress import Progress, TextColumn, BarColumn, TimeRemainingColumn import matplotlib.pyplot as plt from matplotlib import cm import numpy as np from mrmustard import settings +from .wigner import wigner_discretized # pylint: disable=disallowed-name class Progressbar: @@ -64,54 +64,6 @@ def __exit__(self, exc_type, exc_val, exc_tb): return self.bar.__exit__(exc_type, exc_val, exc_tb) -def plot_wigner(rho, xvec, pvec, hbar): - r"""Calculates the discretized Wigner function of the specified mode. - - Lifted from `strawberryfields `_ - - Args: - rho (complex array): the state in Fock representation (can be pure or mixed) - xvec (array): array of discretized :math:`x` quadrature values - pvec (array): array of discretized :math:`p` quadrature values - hbar (float): the value of ``\hbar`` - """ - - Q, P = np.meshgrid(xvec, pvec) - cutoff = rho.shape[-1] - A = (Q + P * 1.0j) / (2 * np.sqrt(hbar / 2)) - - Wlist = np.array([np.zeros(np.shape(A), dtype=complex) for k in range(cutoff)]) - - # Wigner function for |0><0| - Wlist[0] = np.exp(-2.0 * np.abs(A) ** 2) / np.pi - - # W = rho(0,0)W(|0><0|) - W = np.real(rho[0, 0]) * np.real(Wlist[0]) - - for n in range(1, cutoff): - Wlist[n] = (2.0 * A * Wlist[n - 1]) / np.sqrt(n) - W += 2 * np.real(rho[0, n] * Wlist[n]) - - for m in range(1, cutoff): - temp = copy(Wlist[m]) - # Wlist[m] = Wigner function for |m>` + + Args: + rho (complex array): the density matrix of the state in Fock representation + xvec (array): array of discretized :math:`x` quadrature values + pvec (array): array of discretized :math:`p` quadrature values + hbar (optional float): the value of `\hbar`, defaults to ``settings.HBAR``. + + Retunrs: + tuple(array, array, array): array containing the discretized Wigner function, and the Q and + P coordinates (in meshgrid form) in which the function is calculated + """ + + Q = np.outer(qvec, np.ones_like(pvec)) + P = np.outer(np.ones_like(qvec), pvec) + + cutoff = rho.shape[-1] + A = (Q + P * 1.0j) / (2 * np.sqrt(hbar / 2)) + + Wmat = np.zeros((2, cutoff) + A.shape, dtype=rho.dtype) + + # Wigner function for |0><0| + Wmat[0, 0] = np.exp(-2.0 * np.abs(A) ** 2) / np.pi + W = np.real(rho[0, 0]) * np.real(Wmat[0, 0]) + + for n in range(1, cutoff): + Wmat[0, n] = (2.0 * A * Wmat[0, n - 1]) / np.sqrt(n) + W += 2 * np.real(rho[0, n] * Wmat[0, n]) + + for m in range(1, cutoff): + # Wigner function for |m>""" + x = ( + 0.5 + * np.sqrt(1 / (np.pi * hbar)) + * np.exp(-1 * (qvec**2) / hbar) + * (4 / hbar) + * (qvec**2) + ) + p = x + return x, p + + +def vacuum_marginal(qvec): + """q and p marginal distributions for the vacuum state""" + x = np.sqrt(1 / (np.pi * hbar)) * np.exp(-1 * (qvec**2) / hbar) + p = x + return x, p + + +def coherent_marginal(qvec): + r"""q and p marginal distributions for the coherent state with `\alpha=1`""" + x = np.sqrt(1 / (np.pi * hbar)) * np.exp(-1 * ((qvec - 0.5 * np.sqrt(2 * hbar)) ** 2) / hbar) + p = np.sqrt(1 / (np.pi * hbar)) * np.exp(-1 * (qvec**2) / hbar) + return x, p + + +@pytest.mark.parametrize( + "state, f_marginal", + [ + (Vacuum(1), vacuum_marginal), + (Coherent(1.0, 0.0), coherent_marginal), + (Fock([1]), fock1_marginal), + ], +) +def test_marginal_wigner(state, f_marginal): + """test marginals of Wigner function agree with the expected ones""" + + # calculate Wigner from state dm + qvec = np.arange(-5, 5, 100) + pvec = qvec + dm = state.dm(cutoffs=[5]).numpy() + W_calc, _, _ = wigner_discretized(dm, qvec, pvec) + + # calculate marginals + q_marginal = np.sum(W_calc, axis=1) + p_marginal = np.sum(W_calc, axis=0) + + expected_q_marginal, expected_p_marginal = f_marginal(qvec) + + assert np.allclose(q_marginal, expected_q_marginal, atol=0.001, rtol=0) + assert np.allclose(p_marginal, expected_p_marginal, atol=0.001, rtol=0) From ff94a45e00077530c9b0a81153dd2d03964c206f Mon Sep 17 00:00:00 2001 From: ziofil Date: Thu, 10 Nov 2022 17:17:50 -0500 Subject: [PATCH 07/53] Bugfix dgate gradient (#176) **Context:** There was a bug in the gradient computation of the Gate **Description of the Change:** Bug is fixed (there was a numpy array at some point that was breaking the chain rule) **Benefits:** It works **Possible Drawbacks:** None **Related GitHub Issues:** None --- mrmustard/lab/gates.py | 43 +++++++++++++------------------- mrmustard/math/math_interface.py | 4 +-- mrmustard/math/tensorflow.py | 11 ++++---- tests/test_lab/test_detectors.py | 2 +- 4 files changed, 26 insertions(+), 34 deletions(-) diff --git a/mrmustard/lab/gates.py b/mrmustard/lab/gates.py index 77da4968e..1fba98573 100644 --- a/mrmustard/lab/gates.py +++ b/mrmustard/lab/gates.py @@ -93,14 +93,13 @@ def __init__( def d_vector(self): return gaussian.displacement(self.x.value, self.y.value, settings.HBAR) - def U(self, cutoffs: List[int]): + def U(self, cutoffs: Sequence[int]): """Returns the unitary representation of the Displacement gate using the Laguerre polynomials.""" N = len(cutoffs) x, y = self._parse_modes_and_args(cutoffs) - - r = math.sqrt(x**2 + y**2) + r = math.sqrt(x * x + y * y) phi = math.atan2(y, x) # calculate displacement unitary for each mode and concatenate with outer product @@ -118,30 +117,22 @@ def U(self, cutoffs: List[int]): ) def _parse_modes_and_args(self, cutoffs): - num_modes = len(cutoffs) - modes = self.modes # modes in which the gate is acting on - xargs = self.x.value - yargs = self.y.value - num_args_x = ( - xargs.shape[0] if len(xargs.shape.as_list()) > 0 else 1 - ) # number or arguments given to the gate - num_args_y = ( - yargs.shape[0] if len(xargs.shape.as_list()) > 0 else 1 - ) # number or arguments given to the gate - x = np.zeros((num_modes,)) - y = np.zeros((num_modes,)) - + num_modes_state = len(cutoffs) + xargs = math.atleast_1d(self.x.value) + yargs = math.atleast_1d(self.y.value) + num_args_x = max(1, xargs.shape[-1]) + num_args_y = max(1, yargs.shape[-1]) if num_args_x != num_args_y: - raise ValueError("Number of parameters for `x` and `y` is different.") - - if num_args_x == 1 or num_args_x == len(modes): - # one arg for all modes - x[modes] = xargs - y[modes] = yargs - elif num_args_x == len(modes): - # number of args and number of modes don't match - raise ValueError("Number of args and modes don't match") - + raise ValueError("Number of parameters for `x` and `y` should be the same.") + if num_args_x == 1: + # same arg for all modes + x = math.tile(xargs, [num_modes_state]) + y = math.tile(yargs, [num_modes_state]) + else: + x = math.zeros([num_modes_state]) + y = math.zeros([num_modes_state]) + x = math.update_tensor(x, [[m] for m in self.modes], xargs) + y = math.update_tensor(y, [[m] for m in self.modes], yargs) return x, y diff --git a/mrmustard/math/math_interface.py b/mrmustard/math/math_interface.py index 7b9829fb2..858d46c79 100644 --- a/mrmustard/math/math_interface.py +++ b/mrmustard/math/math_interface.py @@ -430,14 +430,14 @@ def hermite_renormalized( """ @abstractmethod - def displacement(self, r: Scalar, phi: Scalar, cutoff: Scalar, dtype): + def displacement(self, r: Scalar, phi: Scalar, cutoff: Scalar, tol): r"""Calculates the matrix elements of the displacement gate and its derivatives. Args: r (float): displacement magnitude phi (float): displacement angle cutoff (int): Fock ladder cutoff - dtype (data type): Specifies the data type used for the calculation + tol (float): r tolerance for returning identity instead of displacement Returns: Tuple(array[complex], function): matrix representing the displacement operation and its gradient """ diff --git a/mrmustard/math/tensorflow.py b/mrmustard/math/tensorflow.py index 8742dbad7..09bda8b9d 100644 --- a/mrmustard/math/tensorflow.py +++ b/mrmustard/math/tensorflow.py @@ -389,14 +389,15 @@ def grad(dLdpoly): return poly, grad @tf.custom_gradient - def displacement(self, r, phi, cutoff, dtype=tf.complex64.as_numpy_dtype, tol=1e-15): + def displacement(self, r, phi, cutoff, tol=1e-15): """creates a single mode displacement matrix""" - r = r.numpy() - phi = phi.numpy() - gate = displacement_tw(r, phi, cutoff, dtype) if r > tol else self.eye(cutoff) + if r > tol: + gate = displacement_tw(self.asnumpy(r), self.asnumpy(phi), cutoff) + else: + gate = self.eye(cutoff, dtype="complex128") def grad(dy): # pragma: no cover - Dr, Dphi = grad_displacement_tw(gate, r, phi) + Dr, Dphi = tf.numpy_function(grad_displacement_tw, (gate, r, phi), (gate.dtype,) * 2) grad_r = tf.math.real(tf.reduce_sum(dy * tf.math.conj(Dr))) grad_phi = tf.math.real(tf.reduce_sum(dy * tf.math.conj(Dphi))) return grad_r, grad_phi, None diff --git a/tests/test_lab/test_detectors.py b/tests/test_lab/test_detectors.py index 95d890e83..fe9a487fc 100644 --- a/tests/test_lab/test_detectors.py +++ b/tests/test_lab/test_detectors.py @@ -137,7 +137,7 @@ def test_loss_probs(self, eta): L = Attenuator(transmissivity=eta) dms_lossy = Vacuum(2) >> S[0, 1] >> BS[0, 1] >> lossy_detector[0] dms_ideal = Vacuum(2) >> S[0, 1] >> BS[0, 1] >> L[0] >> ideal_detector[0] - assert np.allclose(dms_lossy, dms_ideal) + assert np.allclose(dms_lossy, dms_ideal, atol=1e-6) class TestHomodyneDetector: From 318c9ab5e5a7cdd40a95c3289172bb01fc978b57 Mon Sep 17 00:00:00 2001 From: ziofil Date: Fri, 11 Nov 2022 09:37:59 -0500 Subject: [PATCH 08/53] settings repr (#174) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit **Context:** settings are not easy do discover **Description of the Change:** the settings object has now a nice repr **Benefits:** users can see al the settings at once **Possible Drawbacks:** None I can think of **Related GitHub Issues:** None Screen Shot 2022-11-08 at 9 55 08 AM Co-authored-by: Sebastián Duque Mesa <675763+sduquemesa@users.noreply.github.com> --- mrmustard/__init__.py | 29 ++++++++++++++++++++++++----- mrmustard/math/__init__.py | 6 +++--- tests/test_math/test_interface.py | 12 ++++++------ 3 files changed, 33 insertions(+), 14 deletions(-) diff --git a/mrmustard/__init__.py b/mrmustard/__init__.py index add7adb58..1eaaa947d 100644 --- a/mrmustard/__init__.py +++ b/mrmustard/__init__.py @@ -14,12 +14,20 @@ """This is the top-most `__init__.py` file of MrMustard package.""" +import rich.table +from rich import print + from ._version import __version__ # pylint: disable=too-many-instance-attributes class Settings: """Settings class.""" + def __new__(cls): # singleton + if not hasattr(cls, "instance"): + cls.instance = super(Settings, cls).__new__(cls) + return cls.instance + def __init__(self): self._backend = "tensorflow" self.HBAR = 2.0 @@ -40,25 +48,36 @@ def __init__(self): self.PROGRESSBAR = True @property - def backend(self): + def BACKEND(self): """The backend which is used. Can be either ``'tensorflow'`` or ``'torch'``. """ return self._backend - @backend.setter - def backend(self, backend_name: str): + @BACKEND.setter + def BACKEND(self, backend_name: str): if backend_name not in ["tensorflow", "torch"]: # pragma: no cover raise ValueError("Backend must be either 'tensorflow' or 'torch'") self._backend = backend_name + # use rich.table to print the settings + def __repr__(self): + """Returns a string representation of the settings.""" + table = rich.table.Table(title="MrMustard Settings") + table.add_column("Setting") + table.add_column("Value") + table.add_row("BACKEND", self.BACKEND) + for key, value in self.__dict__.items(): + if key == key.upper(): + table.add_row(key, str(value)) + print(table) + return "" + settings = Settings() """Settings object.""" -settings.backend = "tensorflow" - def version(): r"""Version number of Mr Mustard. diff --git a/mrmustard/math/__init__.py b/mrmustard/math/__init__.py index eca01aac6..46e2ad779 100644 --- a/mrmustard/math/__init__.py +++ b/mrmustard/math/__init__.py @@ -51,11 +51,11 @@ class Math: """ # pylint: disable=no-else-return def __getattribute__(self, name): - if settings.backend == "tensorflow": + if settings.BACKEND == "tensorflow": return object.__getattribute__(TFMath(), name) - elif settings.backend == "torch": + elif settings.BACKEND == "torch": return object.__getattribute__(TorchMath(), name) raise ValueError( - f"No `{settings.backend}` backend found. Ensure your backend is either ``'tensorflow'`` or ``'torch'``" + f"No `{settings.BACKEND}` backend found. Ensure your backend is either ``'tensorflow'`` or ``'torch'``" ) diff --git a/tests/test_math/test_interface.py b/tests/test_math/test_interface.py index 4fee9dfd9..ad463f616 100644 --- a/tests/test_math/test_interface.py +++ b/tests/test_math/test_interface.py @@ -32,7 +32,7 @@ def test_backend_redirection_tf(): """Test Math class is redirecting calls to the backend set on MM settings""" math = Math() - settings.backend = "tensorflow" + settings.BACKEND = "tensorflow" assert math._MathInterface__instance.__module__ == "mrmustard.math.tensorflow" @@ -41,16 +41,16 @@ def test_backend_redirection_torch(): """Test Math class is redirecting calls to the backend set on MM settings""" math = Math() - settings.backend = "torch" + settings.BACKEND = "torch" assert math._MathInterface__instance.__module__ == "mrmustard.math.torch" def test_error_for_wrong_backend(): """Test error is raise when using a backend that is not allowed""" - backend = settings.backend + backend = settings.BACKEND with pytest.raises(ValueError) as exception_info: - settings.backend = "unexisting_backend" + settings.BACKEND = "unexisting_backend" assert exception_info.value.args[0] == f"Backend must be either 'tensorflow' or 'torch'" - # set back to initial value to avoir side effects - settings.backend = backend + # set back to initial value to avoid side effects + settings.BACKEND = backend From 555c6a66af46db43b81d81c3fdfded287ceec89a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Duque=20Mesa?= <675763+sduquemesa@users.noreply.github.com> Date: Thu, 17 Nov 2022 12:28:32 -0500 Subject: [PATCH 09/53] Refactor `utils.homodyne` into `physics.fock` (#177) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit **Context:** PR #143 introduced sampling for homodyne measurements and with it a bunch of functionality in the `utils/homodyne.py` module. Most of this functions are related to the sampling in fock representation and are not only useful for homodyne sampling — take for example the calculation of the quadrature distribution. **Description of the Change:** This PR refactors the homodyne module into: - `physics.fock` — functions related to the fock representation were moved into this module. Some of them are even split and written as separated functions such that they are available for use in other contexts (for example `quadrature_distribution` and `oscillator_eigenstate`). - `math.caching` — the cache decorator used for the Hermite polys is refactored into this new module, this decorator is not only applicable to the Hermite polys but also to any function taking a 1D tensor + int parameters. Now it can be used generically by any function with this same signature. The idea of this module is to contain caching functions of this kind. Also - Hermite polynomials have the modified flag removed (as per [this](https://github.com/XanaduAI/MrMustard/pull/143#pullrequestreview-1142728731) comment) and now only the regular polynomials are used. _Note:_ This PR only refactors code, meaning where the code is placed and _not_ what it does and _nor_ how it is done; there is no change to the logic whatsoever. **Benefits:** Pieces of the code that were there already are now reusable. The code for the sampling logic is more readable now. **Possible Drawbacks:** None **Related GitHub Issues:** None --- mrmustard/lab/detectors.py | 10 +- mrmustard/math/caching.py | 42 ++++++ mrmustard/math/math_interface.py | 6 +- mrmustard/math/tensorflow.py | 6 +- mrmustard/math/torch.py | 2 +- mrmustard/physics/fock.py | 203 ++++++++++++++++++++++++++ mrmustard/utils/homodyne.py | 243 ------------------------------- mrmustard/utils/wigner.py | 2 +- tests/test_math/test_special.py | 7 +- 9 files changed, 255 insertions(+), 266 deletions(-) create mode 100644 mrmustard/math/caching.py delete mode 100644 mrmustard/utils/homodyne.py diff --git a/mrmustard/lab/detectors.py b/mrmustard/lab/detectors.py index 051732f19..75547eae7 100644 --- a/mrmustard/lab/detectors.py +++ b/mrmustard/lab/detectors.py @@ -22,7 +22,6 @@ from mrmustard import settings from mrmustard.math import Math from mrmustard.physics import gaussian, fock -from mrmustard.utils import homodyne as utils_homodyne from .abstract import FockMeasurement, Measurement, State from .states import DisplacedSqueezed, Coherent from .gates import Rgate @@ -399,13 +398,12 @@ def _measure_fock(self, other) -> Union[State, float]: remaining_modes = list(set(other.modes) - set(self.modes)) # create reduced state of modes to be measured on the homodyne basis - reduced_state = other.get_modes(self.modes) >> Rgate( - -self.quadrature_angle, modes=self.modes - ) + reduced_state = other.get_modes(self.modes) # build pdf and sample homodyne outcome - x_outcome, probability = utils_homodyne.sample_homodyne_fock( + x_outcome, probability = fock.sample_homodyne( state=reduced_state.ket() if reduced_state.is_pure else reduced_state.dm(), + quadrature_angle=self.quadrature_angle, hbar=settings.HBAR, ) @@ -414,7 +412,7 @@ def _measure_fock(self, other) -> Union[State, float]: # factor of the displacement symplectic inside the DisplacedSqueezed state. x_arg = x_outcome / math.sqrt(2.0 * settings.HBAR, dtype="float64") self.state = DisplacedSqueezed( - r=self.r, phi=self.quadrature_angle, x=x_arg, y=0.0, modes=self.modes + r=self.r, phi=0.0, x=x_arg, y=0.0, modes=self.modes ) >> Rgate(self.quadrature_angle, modes=self.modes) if remaining_modes == 0: diff --git a/mrmustard/math/caching.py b/mrmustard/math/caching.py new file mode 100644 index 000000000..712bd2f08 --- /dev/null +++ b/mrmustard/math/caching.py @@ -0,0 +1,42 @@ +# Copyright 2022 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 the logic for cachin tensor functions in Mr Mustard.""" + +from functools import lru_cache, wraps +import numpy as np + + +def tensor_int_cache(fn): + """Decorator function to cache functions with a 1D Tensor (Vector) and int as arguments, + that is, functions with signature ``func(x: Vector, n: int)``. + + To do so the input vector (non-hashable) is converted into a numpy array (non-hashable) + and then into a tuple (hashable). This tuple is used by ``functools.lru_cache`` to cache + the result.""" + + @lru_cache + def cached_wrapper(hashable_array, cutoff): + array = np.array(hashable_array, dtype=np.float64) + return fn(array, cutoff) + + @wraps(fn) + def wrapper(tensor, cutoff): + return cached_wrapper(tuple(tensor.numpy()), cutoff) + + # copy lru_cache attributes over too + wrapper.cache_info = cached_wrapper.cache_info + wrapper.cache_clear = cached_wrapper.cache_clear + + return wrapper diff --git a/mrmustard/math/math_interface.py b/mrmustard/math/math_interface.py index 858d46c79..871fc4f87 100644 --- a/mrmustard/math/math_interface.py +++ b/mrmustard/math/math_interface.py @@ -412,9 +412,7 @@ def hash_tensor(self, tensor: Tensor) -> int: """ @abstractmethod - def hermite_renormalized( - self, A: Matrix, B: Vector, C: Scalar, shape: Sequence[int], modified: bool - ) -> Tensor: + def hermite_renormalized(self, A: Matrix, B: Vector, C: Scalar, shape: Sequence[int]) -> Tensor: r"""Returns the array of hermite renormalized polynomials of the given coefficients. Args: @@ -422,8 +420,6 @@ def hermite_renormalized( B (array): Vector coefficient of the hermite polynomial C (array): Scalar coefficient of the hermite polynomial shape (tuple): shape of the hermite polynomial - modified (bool): whether to return the modified multidimensional - Hermite polynomials or the standard ones Returns: array: renormalized hermite polynomials diff --git a/mrmustard/math/tensorflow.py b/mrmustard/math/tensorflow.py index 09bda8b9d..188e83d30 100644 --- a/mrmustard/math/tensorflow.py +++ b/mrmustard/math/tensorflow.py @@ -352,7 +352,7 @@ def value_and_gradients( @tf.custom_gradient def hermite_renormalized( - self, A: tf.Tensor, B: tf.Tensor, C: tf.Tensor, shape: Tuple[int], modified: bool = True + self, A: tf.Tensor, B: tf.Tensor, C: tf.Tensor, shape: Tuple[int] ) -> tf.Tensor: # TODO this is not ready r"""Renormalized multidimensional Hermite polynomial given by the "exponential" Taylor series of :math:`exp(C + Bx - Ax^2)` at zero, where the series has :math:`sqrt(n!)` at the @@ -363,8 +363,6 @@ def hermite_renormalized( B: The B vector. C: The C scalar. shape: The shape of the final tensor. - modified (bool): whether to return the modified multidimensional - Hermite polynomials or the standard ones Returns: The renormalized Hermite polynomial of given shape. @@ -373,7 +371,7 @@ def hermite_renormalized( shape = shape[0] poly = hermite_multidimensional( - self.asnumpy(A), shape, self.asnumpy(B), self.asnumpy(C), True, True, modified + self.asnumpy(A), shape, self.asnumpy(B), self.asnumpy(C), True, True, True ) def grad(dLdpoly): diff --git a/mrmustard/math/torch.py b/mrmustard/math/torch.py index b931ca4ac..424b34c0e 100644 --- a/mrmustard/math/torch.py +++ b/mrmustard/math/torch.py @@ -303,7 +303,7 @@ def hash_tensor(self, tensor: torch.Tensor) -> str: return hash(tensor) def hermite_renormalized( - self, A: torch.Tensor, B: torch.Tensor, C: torch.Tensor, shape: Tuple[int], modified: bool + self, A: torch.Tensor, B: torch.Tensor, C: torch.Tensor, shape: Tuple[int] ) -> torch.Tensor: # TODO this is not ready r"""Renormalized multidimensional Hermite polynomial. diff --git a/mrmustard/physics/fock.py b/mrmustard/physics/fock.py index daeca9c31..227a2ed87 100644 --- a/mrmustard/physics/fock.py +++ b/mrmustard/physics/fock.py @@ -18,8 +18,10 @@ This module contains functions for performing calculations on Fock states. """ +from functools import lru_cache import numpy as np +from mrmustard.math.caching import tensor_int_cache from mrmustard.types import List, Tuple, Tensor, Scalar, Matrix, Sequence, Vector from mrmustard import settings from mrmustard.math import Math @@ -467,3 +469,204 @@ def trace(dm, keep: List[int]): # make it square on those indices dm = math.reshape(dm, dm.shape[: 2 * len(keep)] + (d, d)) return math.trace(dm) + + +@tensor_int_cache +def oscillator_eigenstate(q: Vector, cutoff: int) -> Tensor: + r"""Harmonic oscillator eigenstate wavefunction `\psi_n(q) = `. + + Args: + q (Vector): a vector containing the q points at which the function is evaluated (units of \sqrt{\hbar}) + cutoff (int): maximum number of photons + hbar (optional): value of `\hbar`, defaults to Mr Mustard's internal value + + Returns: + Tensor: a tensor of size ``len(q)*cutoff``. Each entry with index ``[i, j]`` represents the eigenstate evaluated + with number of photons ``i`` evaluated at position ``q[j]``, i.e., `\psi_i(q_j)`. + + .. details:: + + .. admonition:: Definition + :class: defn + + The q-quadrature eigenstates are defined as + + .. math:: + + \psi_n(x) = 1/sqrt[2^n n!](\frac{\omega}{\pi \hbar})^{1/4} + \exp{-\frac{\omega}{2\hbar} x^2} H_n(\sqrt{\frac{\omega}{\pi}} x) + + where :math:`H_n(x)` is the (physicists) `n`-th Hermite polynomial. + """ + omega_over_hbar = math.cast(1 / settings.HBAR, "float64") + x_tensor = math.sqrt(omega_over_hbar) * math.cast(q, "float64") # unit-less vector + + # prefactor term (\Omega/\hbar \pi)**(1/4) * 1 / sqrt(2**n) + prefactor = (omega_over_hbar / np.pi) ** (1 / 4) * math.sqrt(2 ** (-math.arange(0, cutoff))) + + # Renormalized physicist hermite polys: Hn / sqrt(n!) + R = math.astensor(2 * np.ones([1, 1])) # to get the physicist polys + + def f_hermite_polys(xi): + return math.hermite_renormalized(R, 2 * math.astensor([xi]), 1, cutoff) + + hermite_polys = math.cast(math.map_fn(f_hermite_polys, x_tensor), "float64") + + # wavefunction + psi = math.exp(-(x_tensor**2 / 2)) * math.transpose(prefactor * hermite_polys) + return psi + + +@lru_cache +def estimate_dx(cutoff, period_resolution=20): + r"""Estimates a suitable quadrature discretization interval `dx`. Uses the fact + that Fock state `n` oscillates with angular frequency :math:`\sqrt{2(n + 1)}`, + which follows from the relation + + .. math:: + + \psi^{[n]}'(q) = q - sqrt(2*(n + 1))*\psi^{[n+1]}(q) + + by setting q = 0, and approximating the oscillation amplitude by `\psi^{[n+1]}(0) + + Ref: https://en.wikipedia.org/wiki/Hermite_polynomials#Hermite_functions + + Args + cutoff (int): Fock cutoff + period_resolution (int): Number of points used to sample one Fock + wavefunction oscillation. Larger values yields better approximations + and thus smaller `dx`. + + Returns + (float): discretization value of quadrature + """ + fock_cutoff_frequency = np.sqrt(2 * (cutoff + 1)) + fock_cutoff_period = 2 * np.pi / fock_cutoff_frequency + dx_estimate = fock_cutoff_period / period_resolution + return dx_estimate + + +@lru_cache +def estimate_xmax(cutoff, minimum=5): + r"""Estimates a suitable quadrature axis length + + Args + cutoff (int): Fock cutoff + minimum (float): Minimum value of the returned xmax + + Returns + (float): maximum quadrature value + """ + if cutoff == 0: + xmax_estimate = 3 + else: + # maximum q for a classical particle with energy n=cutoff + classical_endpoint = np.sqrt(2 * cutoff) + # approximate probability of finding particle outside classical region + excess_probability = 1 / (7.464 * cutoff ** (1 / 3)) + # Emperical factor that yields reasonable results + A = 5 + xmax_estimate = classical_endpoint * (1 + A * excess_probability) + return max(minimum, xmax_estimate) + + +@lru_cache +def estimate_quadrature_axis(cutoff, minimum=5, period_resolution=20): + """Generates a suitable quadrature axis. + + Args + cutoff (int): Fock cutoff + minimum (float): Minimum value of the returned xmax + period_resolution (int): Number of points used to sample one Fock + wavefunction oscillation. Larger values yields better approximations + and thus smaller dx. + + Returns + (array): quadrature axis + """ + xmax = estimate_xmax(cutoff, minimum=minimum) + dx = estimate_dx(cutoff, period_resolution=period_resolution) + xaxis = np.arange(-xmax, xmax, dx) + xaxis = np.append(xaxis, xaxis[-1] + dx) + xaxis = xaxis - np.mean(xaxis) # center around 0 + return xaxis + + +def quadrature_distribution( + state: Tensor, quadrature_angle: float = 0.0, x: Vector = None, hbar: float = settings.HBAR +): + r"""Given the ket or density matrix of a single-mode state, it generates the probability + density distribution :math:`\tr [ \rho |x_\phi> the quadrature eigenvector with angle `\phi` + equal to ``quadrature_angle``. + + Args: + state (Tensor): single mode state ket or density matrix + quadrature_angle (float): angle of the quadrature basis vector + x (Vector): points at which the quadrature distribution is evaluated + + Returns: + tuple(Vector, Vector): coordinates at which the pdf is evaluated and the probability distribution + """ + dims = len(state.shape) + if dims > 2: + raise ValueError( + "Input state has dimension {state.shape}. Make sure is either a single-mode ket or dm." + ) + + is_dm = dims == 2 + cutoff = state.shape[0] + + if not np.isclose(quadrature_angle, 0.0): + # rotate mode to the homodyne basis + theta = -math.arange(cutoff) * quadrature_angle + Ur = math.diag(math.make_complex(math.cos(theta), math.sin(theta))) + state = ( + math.einsum("ij,jk,kl->il", Ur, state, math.dagger(Ur)) + if is_dm + else math.matvec(Ur, state) + ) + + if x is None: + x = np.sqrt(hbar) * math.new_constant(estimate_quadrature_axis(cutoff), "q_tensor") + + psi_x = math.cast(oscillator_eigenstate(x, cutoff), "complex128") + pdf = ( + math.einsum("nm,nj,mj->j", state, psi_x, psi_x) + if is_dm + else math.abs(math.einsum("n,nj->j", state, psi_x)) ** 2 + ) + + return x, math.cast(pdf, "float64") + + +def sample_homodyne( + state: Tensor, quadrature_angle: float = 0.0, hbar: float = settings.HBAR +) -> Tuple[float, float]: + r"""Given a single-mode state, it generates the pdf of :math:`\tr [ \rho |x_\phi> 2: + raise ValueError( + "Input state has dimension {state.shape}. Make sure is either a single-mode ket or dm." + ) + + x, pdf = quadrature_distribution(state, quadrature_angle, hbar=hbar) + probs = pdf * (x[1] - x[0]) + + # draw a sample from the distribution + pdf = math.Categorical(probs=probs, name="homodyne_dist") + sample_idx = pdf.sample() + homodyne_sample = math.gather(x, sample_idx) + probability_sample = math.gather(probs, sample_idx) + + return homodyne_sample, probability_sample diff --git a/mrmustard/utils/homodyne.py b/mrmustard/utils/homodyne.py deleted file mode 100644 index 2cadbbb59..000000000 --- a/mrmustard/utils/homodyne.py +++ /dev/null @@ -1,243 +0,0 @@ -# Copyright 2022 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. - -"""Utility functions related to homodyne sampling in Fock representation""" -from __future__ import annotations -from typing import TYPE_CHECKING -from functools import lru_cache, wraps - -import numpy as np - -from mrmustard.types import Tuple, Tensor -from mrmustard.math import Math - -if TYPE_CHECKING: - from mrmustard.lab.abstract import State - -math = Math() - - -def hermite_cache(fn): - """Decorator function to cache outcomes of the physicist_hermite_polys - function. To do so the input tensor (non-hashable) is converted into a - numpy array (non-hashable) and then a tuple (hashable) is used in conjuntion - with ``functools.lru_cache``.""" - - @lru_cache - def cached_wrapper(hashable_array, cutoff): - array = np.array(hashable_array) - return fn(array, cutoff) - - @wraps(fn) - def wrapper(tensor, cutoff): - return cached_wrapper(tuple(tensor.numpy()), cutoff) - - # copy lru_cache attributes over too - wrapper.cache_info = cached_wrapper.cache_info - wrapper.cache_clear = cached_wrapper.cache_clear - - return wrapper - - -@hermite_cache -def physicist_hermite_polys(x: Tensor, cutoff: int): - r"""Reduction of the multidimensional hermite polynomials into the one-dimensional - renormalized physicist polys. - - Args: - x (Tensor): argument values of the Hermite polynomial - cutoff (int): maximum size of the subindices in the Hermite polynomial - - Returns: - Tensor: the evaluated renormalized Hermite polynomials - """ - R = math.astensor(2 * np.ones([1, 1])) # to get the physicist polys - - def f_hermite_polys(xi): - return math.hermite_renormalized(R, math.astensor([xi]), 1, cutoff, modified=False) - - return math.map_fn(f_hermite_polys, x) - - -@lru_cache -def estimate_dx(cutoff, period_resolution=20): - r"""Estimates a suitable quadrature discretization interval `dx`. Uses the fact - that Fock state `n` oscillates with angular frequency :math:`\sqrt{2(n + 1)}`, - which follows from the relation - - .. math:: - - \psi^{[n]}'(q) = q - sqrt(2*(n + 1))*\psi^{[n+1]}(q) - - by setting q = 0, and approximating the oscillation amplitude by `\psi^{[n+1]}(0) - - Ref: https://en.wikipedia.org/wiki/Hermite_polynomials#Hermite_functions - - Args - cutoff (int): Fock cutoff - period_resolution (int): Number of points used to sample one Fock - wavefunction oscillation. Larger values yields better approximations - and thus smaller `dx`. - - Returns - (float): discretization value of quadrature - """ - fock_cutoff_frequency = np.sqrt(2 * (cutoff + 1)) - fock_cutoff_period = 2 * np.pi / fock_cutoff_frequency - dx_estimate = fock_cutoff_period / period_resolution - return dx_estimate - - -@lru_cache -def estimate_xmax(cutoff, minimum=5): - r"""Estimates a suitable quadrature axis length - - Args - cutoff (int): Fock cutoff - minimum (float): Minimum value of the returned xmax - - Returns - (float): maximum quadrature value - """ - if cutoff == 0: - xmax_estimate = 3 - else: - # maximum q for a classical particle with energy n=cutoff - classical_endpoint = np.sqrt(2 * cutoff) - # approximate probability of finding particle outside classical region - excess_probability = 1 / (7.464 * cutoff ** (1 / 3)) - # Emperical factor that yields reasonable results - A = 5 - xmax_estimate = classical_endpoint * (1 + A * excess_probability) - return max(minimum, xmax_estimate) - - -@lru_cache -def estimate_quadrature_axis(cutoff, minimum=5, period_resolution=20): - """Generates a suitable quadrature axis. - - Args - cutoff (int): Fock cutoff - minimum (float): Minimum value of the returned xmax - period_resolution (int): Number of points used to sample one Fock - wavefunction oscillation. Larger values yields better approximations - and thus smaller dx. - - Returns - (array): quadrature axis - """ - xmax = estimate_xmax(cutoff, minimum=minimum) - dx = estimate_dx(cutoff, period_resolution=period_resolution) - xaxis = np.arange(-xmax, xmax, dx) - xaxis = np.append(xaxis, xaxis[-1] + dx) - xaxis = xaxis - np.mean(xaxis) # center around 0 - return xaxis - - -def sample_homodyne_fock(state: State, hbar: float) -> Tuple[float, float, State]: - r"""Given a single-mode state, it generates the pdf of :math:`\tr [ \rho |x><0| Wmat[0, 0] = np.exp(-2.0 * np.abs(A) ** 2) / np.pi diff --git a/tests/test_math/test_special.py b/tests/test_math/test_special.py index 542f39332..6f08d450a 100644 --- a/tests/test_math/test_special.py +++ b/tests/test_math/test_special.py @@ -28,12 +28,7 @@ def test_reduction_to_renorm_physicists_polys(): n_max = 5 A = np.ones([init, init], dtype=complex) vals = np.array( - [ - math.hermite_renormalized( - 2 * A, np.array([x0], dtype=complex), 1, n_max, modified=False - ) - for x0 in x - ] + [math.hermite_renormalized(2 * A, 2 * np.array([x0], dtype=complex), 1, n_max) for x0 in x] ).T expected = np.array([eval_hermite(i, x) / np.sqrt(factorial(i)) for i in range(len(vals))]) assert np.allclose(vals, expected) From c984ed11675df3486bdb8d9516837ae7aad430b6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Duque=20Mesa?= <675763+sduquemesa@users.noreply.github.com> Date: Thu, 24 Nov 2022 12:41:34 -0500 Subject: [PATCH 10/53] =?UTF-8?q?Use=20marginal=20distribution=20when=20dr?= =?UTF-8?q?awing=20=F0=9F=8E=A8=20(#179)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit **Context:** Currently marginals are calculated form the Wigner function. In cases in which not all features of the state are captured on the Wigner function, the marginals contain negativities hence not representing a true probability density. **Description of the Change:** This PR makes `mikkel_plot` - calculate marginals independently from the Wigner function thus ensuring that the marginals are physical even though the Wigner function might not contain all the features of the state within the defined window, - expose the `ticks`, `tick_labels` and `grid` arguments to customize the visualization — useful for example when checking that your state has peaks in those dreaded multiples of √(π) - return the figure and axes for further processing if needed **Benefits:** - Marginals in the visualization will always represent physical states _before_: note how this lion is not fully displayed in the Wigner function leading to negativities in the marginal distributions image _after_: although the big kittie is not displayed in its full glory its marginals show the true probability distribution image - More configurability of the visualization ```python from matplotlib import cm ticks = [-2*np.sqrt(2),2*np.sqrt(2)] labels = [r"$-2\sqrt{\hbar}$", r"$2\sqrt{\hbar}$"] graphics.mikkel_plot(dm, xticks=ticks, yticks=ticks, xtick_labels=labels, ytick_labels=[r"$-2\sqrt{\hbar}$", r"$2\sqrt{\hbar}$"], grid=True, cmap=cm.PuOr) ``` ![image](https://user-images.githubusercontent.com/675763/203838559-06b81520-5abf-4f90-87f5-5f9e9947237c.png) - Users can take the figure and axes for post-processing or storing **Possible Drawbacks:** This will _marginally_ increase the computation time for the visualization but shouldn't be too impactful. **Related GitHub Issues:** None Co-authored-by: ziofil Co-authored-by: Luke Helt <31250931+heltluke@users.noreply.github.com> --- .github/CHANGELOG.md | 5 ++ mrmustard/utils/graphics.py | 79 +++++++++++++++++++++++-------- tests/test_utils/test_graphics.py | 27 +++++++++++ 3 files changed, 90 insertions(+), 21 deletions(-) create mode 100644 tests/test_utils/test_graphics.py diff --git a/.github/CHANGELOG.md b/.github/CHANGELOG.md index 8868c5586..c3a802de6 100644 --- a/.github/CHANGELOG.md +++ b/.github/CHANGELOG.md @@ -34,6 +34,11 @@ W, Q, P = wigner_discretized(dm, q, p) # dm is a density matrix ``` +* Calculate marginals independently from the Wigner function thus ensuring that the marginals are +physical even though the Wigner function might not contain all the features of the state +within the defined window. Also, expose some plot parameters and return the figure and axes. + [(#179)](https://github.com/XanaduAI/MrMustard/pull/179) + ### Bug fixes ### Documentation diff --git a/mrmustard/utils/graphics.py b/mrmustard/utils/graphics.py index 3d9510cfd..5d749cfd3 100644 --- a/mrmustard/utils/graphics.py +++ b/mrmustard/utils/graphics.py @@ -20,6 +20,7 @@ from matplotlib import cm import numpy as np from mrmustard import settings +from mrmustard.physics.fock import quadrature_distribution from .wigner import wigner_discretized # pylint: disable=disallowed-name @@ -64,67 +65,101 @@ def __exit__(self, exc_type, exc_val, exc_tb): return self.bar.__exit__(exc_type, exc_val, exc_tb) -def mikkel_plot(rho: np.ndarray, xbounds: Tuple[int] = (-6, 6), ybounds: Tuple[int] = (-6, 6)): +def mikkel_plot( + rho: np.ndarray, + xbounds: Tuple[int] = (-6, 6), + ybounds: Tuple[int] = (-6, 6), + **kwargs, +): # pylint: disable=too-many-statements """Plots the Wigner function of a state given its density matrix. Args: rho (np.ndarray): density matrix of the state xbounds (Tuple[int]): range of the x axis ybounds (Tuple[int]): range of the y axis + + Keyword args: + resolution (int): number of points used to calculate the wigner function + xticks (Tuple[int]): ticks of the x axis + xtick_labels (Optional[Tuple[str]]): labels of the x axis; if None uses default formatter + yticks (Tuple[int]): ticks of the y axis + ytick_labels (Optional[Tuple[str]]): labels of the y axis; if None uses default formatter + grid (bool): whether to display the grid + cmap (matplotlib.colormap): colormap of the figure + + Returns: + tuple: figure and axes """ - xvec = np.linspace(*xbounds, 200) - pvec = np.linspace(*ybounds, 200) + plot_args = { + "resolution": 200, + "xticks": (-5, 0, 5), + "xtick_labels": None, + "yticks": (-5, 0, 5), + "ytick_labels": None, + "grid": False, + "cmap": cm.RdBu, + } + plot_args.update(kwargs) + + if plot_args["xtick_labels"] is None: + plot_args["xtick_labels"] = plot_args["xticks"] + if plot_args["ytick_labels"] is None: + plot_args["ytick_labels"] = plot_args["yticks"] + + q, ProbX = quadrature_distribution(rho) + p, ProbP = quadrature_distribution(rho, np.pi / 2) + + xvec = np.linspace(*xbounds, plot_args["resolution"]) + pvec = np.linspace(*ybounds, plot_args["resolution"]) W, X, P = wigner_discretized(rho, xvec, pvec, settings.HBAR) - ProbX = np.sum(W, axis=1) - ProbP = np.sum(W, axis=0) ### PLOTTING ### - _, ax = plt.subplots( + fig, ax = plt.subplots( 2, 2, figsize=(6, 6), gridspec_kw={"width_ratios": [2, 1], "height_ratios": [1, 2]} ) - ticks = [-5, 0, 5] - grid = False plt.subplots_adjust(wspace=0.05, hspace=0.05) # Wigner function - ax[1][0].contourf(X, P, W, 60, cmap=cm.RdBu, vmin=-abs(W).max(), vmax=abs(W).max()) + ax[1][0].contourf(X, P, W, 120, cmap=plot_args["cmap"], vmin=-abs(W).max(), vmax=abs(W).max()) ax[1][0].set_xlabel("$x$", fontsize=12) ax[1][0].set_ylabel("$p$", fontsize=12) - ax[1][0].get_xaxis().set_ticks(ticks) - ax[1][0].get_yaxis().set_ticks(ticks) + ax[1][0].get_xaxis().set_ticks(plot_args["xticks"]) + ax[1][0].xaxis.set_ticklabels(plot_args["xtick_labels"]) + ax[1][0].get_yaxis().set_ticks(plot_args["yticks"]) + ax[1][0].yaxis.set_ticklabels(plot_args["ytick_labels"], rotation="vertical", va="center") ax[1][0].tick_params(direction="in") ax[1][0].set_xlim(xbounds) ax[1][0].set_ylim(ybounds) - ax[1][0].grid(grid) + ax[1][0].grid(plot_args["grid"]) # X quadrature probability distribution - ax[0][0].fill(xvec, ProbX, color=cm.RdBu(0.5)) - ax[0][0].plot(xvec, ProbX) - ax[0][0].get_xaxis().set_ticks(ticks) + ax[0][0].fill(q, ProbX, color=plot_args["cmap"](0.5)) + ax[0][0].plot(q, ProbX, color=plot_args["cmap"](0.8)) + ax[0][0].get_xaxis().set_ticks(plot_args["xticks"]) ax[0][0].xaxis.set_ticklabels([]) ax[0][0].get_yaxis().set_ticks([]) ax[0][0].tick_params(direction="in") ax[0][0].set_ylabel("Prob($x$)", fontsize=12) ax[0][0].set_xlim(xbounds) ax[0][0].set_ylim([0, 1.1 * max(ProbX)]) - ax[0][0].grid(grid) + ax[0][0].grid(plot_args["grid"]) # P quadrature probability distribution - ax[1][1].fill(ProbP, pvec, color=cm.RdBu(0.5)) - ax[1][1].plot(ProbP, pvec) + ax[1][1].fill(ProbP, p, color=plot_args["cmap"](0.5)) + ax[1][1].plot(ProbP, p, color=plot_args["cmap"](0.8)) ax[1][1].get_xaxis().set_ticks([]) - ax[1][1].get_yaxis().set_ticks(ticks) + ax[1][1].get_yaxis().set_ticks(plot_args["yticks"]) ax[1][1].yaxis.set_ticklabels([]) ax[1][1].tick_params(direction="in") ax[1][1].set_xlabel("Prob($p$)", fontsize=12) ax[1][1].set_xlim([0, 1.1 * max(ProbP)]) ax[1][1].set_ylim(ybounds) - ax[1][1].grid(grid) + ax[1][1].grid(plot_args["grid"]) # Density matrix - ax[0][1].matshow(abs(rho), cmap=cm.RdBu, vmin=-abs(rho).max(), vmax=abs(rho).max()) + ax[0][1].matshow(abs(rho), cmap=plot_args["cmap"], vmin=-abs(rho).max(), vmax=abs(rho).max()) ax[0][1].set_title(r"abs($\rho$)", fontsize=12) ax[0][1].tick_params(direction="in") ax[0][1].get_xaxis().set_ticks([]) @@ -132,3 +167,5 @@ def mikkel_plot(rho: np.ndarray, xbounds: Tuple[int] = (-6, 6), ybounds: Tuple[i ax[0][1].set_aspect("auto") ax[0][1].set_ylabel(f"cutoff = {len(rho)}", fontsize=12) ax[0][1].yaxis.set_label_position("right") + + return fig, ax diff --git a/tests/test_utils/test_graphics.py b/tests/test_utils/test_graphics.py new file mode 100644 index 000000000..5cfa84a90 --- /dev/null +++ b/tests/test_utils/test_graphics.py @@ -0,0 +1,27 @@ +# Copyright 2022 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. + +"""Test related to visualization on MrMustard.""" + +from mrmustard.lab import Coherent +from mrmustard.utils.graphics import mikkel_plot + + +def test_mikkel_plot(): + """Tests that mikkel plot returns figure and axes.""" + dm = Coherent().dm(cutoffs=[10]) + fig, axs = mikkel_plot(dm.numpy()) + + assert fig is not None + assert axs is not None From 5d72b0e8c43bffc666bfd15ea289321b6b56279d Mon Sep 17 00:00:00 2001 From: ziofil Date: Mon, 5 Dec 2022 16:24:00 -0500 Subject: [PATCH 11/53] optional callback at each opt step (#175) **Context:** Optimizer is opaque **Description of the Change:** in the minimize function now we can pass a callback that will be executed at the end of each step (with `trainable_parameters` as argument) and the return is stored in `self.callback_history`. **Benefits:** Can do lots of things, e.g. ![test](https://user-images.githubusercontent.com/8944955/200894110-a34b8f5d-caa7-40fc-9716-1d67a8667ca6.gif) **Possible Drawbacks:** None **Related GitHub Issues:** None --- .github/CHANGELOG.md | 10 +++++++--- mrmustard/training/optimizer.py | 21 +++++++++++++++++---- 2 files changed, 24 insertions(+), 7 deletions(-) diff --git a/.github/CHANGELOG.md b/.github/CHANGELOG.md index c3a802de6..ae4d3df12 100644 --- a/.github/CHANGELOG.md +++ b/.github/CHANGELOG.md @@ -18,12 +18,15 @@ measurement_outcome = SqueezedVacuum(r=0.5) >> Homodyne() ``` + * The optimizer `minimize` method now accepts an optional callback function, which will be called at each step of the optimization and it will be passed the step number, the cost value, and the value of the trainable parameters. + The result is added to the `callback_history` attribute of the optimizer. + [(#175)](https://github.com/XanaduAI/MrMustard/pull/175) + ### Breaking changes ### Improvements -* The `Dgate` now uses The Walrus to calculate the unitary and gradients of the displacement gate in fock representation, - providing better numerical stability for larger cutoff and displacement values. +* The `Dgate` now uses The Walrus to calculate the unitary and gradients of the displacement gate in fock representation, providing better numerical stability for larger cutoff and displacement values. [(#147)](https://github.com/XanaduAI/MrMustard/pull/147) * Now the Wigner function is implemented in its own module and uses numba for speed. @@ -46,6 +49,7 @@ within the defined window. Also, expose some plot parameters and return the figu ### Contributors This release contains contributions from (in alphabetical order): +[Sebastian Duque Mesa](https://github.com/sduquemesa), [Filippo Miatto](https://github.com/ziofil) --- @@ -57,7 +61,7 @@ This release contains contributions from (in alphabetical order): * States in Gaussian and Fock representation now can be concatenated. ```python - from mrmustard.lab.states import Gaussian, Fock' + from mrmustard.lab.states import Gaussian, Fock from mrmustard.lab.gates import Attenuator # concatenate pure states diff --git a/mrmustard/training/optimizer.py b/mrmustard/training/optimizer.py index b91471b85..f65ecf472 100644 --- a/mrmustard/training/optimizer.py +++ b/mrmustard/training/optimizer.py @@ -49,10 +49,15 @@ def __init__( "orthogonal": orthogonal_lr, } self.opt_history: List[float] = [0] + self.callback_history: List = [] self.log = create_logger(__name__) def minimize( - self, cost_fn: Callable, by_optimizing: Sequence[Trainable], max_steps: int = 1000 + self, + cost_fn: Callable, + by_optimizing: Sequence[Trainable], + max_steps: int = 1000, + callback: Callable = None, ): r"""Minimizes the given cost function by optimizing circuits and/or detectors. @@ -63,14 +68,17 @@ def minimize( contain the parameters to optimize max_steps (int): the minimization keeps going until the loss is stable or max_steps are reached (if ``max_steps=0`` it will only stop when the loss is stable) + callback (Callable): a function that will be executed at each step of the optimization, which + takes as arguments the training step (int), the cost and the trainable parameters. + The return value is stored in self.callback_history. """ try: - self._minimize(cost_fn, by_optimizing, max_steps) + self._minimize(cost_fn, by_optimizing, max_steps, callback) except KeyboardInterrupt: # graceful exit self.log.info("Optimizer execution halted due to keyboard interruption.") raise self.OptimizerInterruptedError() from None - def _minimize(self, cost_fn, by_optimizing, max_steps): + def _minimize(self, cost_fn, by_optimizing, max_steps, callback): # finding out which parameters are trainable from the ops trainable_params = self._get_trainable_params(by_optimizing) @@ -82,6 +90,10 @@ def _minimize(self, cost_fn, by_optimizing, max_steps): self.opt_history.append(cost) bar.step(math.asnumpy(cost)) + if callback is not None: + self.callback_history.append( + callback(len(self.opt_history) - 1, cost, trainable_params) + ) def apply_gradients(self, trainable_params, grads): """Apply gradients to variables. @@ -155,7 +167,8 @@ def compute_loss_and_gradients(cost_fn: Callable, parameters: List[Parameter]): return loss, grads def should_stop(self, max_steps: int) -> bool: - r"""Returns ``True`` if the optimization should stop (either because the loss is stable or because the maximum number of steps is reached).""" + r"""Returns ``True`` if the optimization should stop (either because + the loss is stable or because the maximum number of steps is reached).""" if max_steps != 0 and len(self.opt_history) > max_steps: return True if len(self.opt_history) > 20: # if cost varies less than 10e-6 over 20 steps From fc3dfec78afc61dad0ecc412aec8a01ebbbff1fa Mon Sep 17 00:00:00 2001 From: ziofil Date: Mon, 12 Dec 2022 22:53:42 -0800 Subject: [PATCH 12/53] Bugfix rgate modes (#180) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit **Context:** The `Rgate` and the `Dgate` don't work correctly in Fock representation in some cases. **Description of the Change:** 1. Fixed the `Rgate` and `Dgate` by removing the parser method and simplifying the code. 2. Added two functions in the fock module for explicitly applying an operator to a ket or to a dm (sandwich) which avoid constructing unitaries as large as the whole circuit. Now they are used in the `transform_fock` method of the `Transformation` class. 3. fixed a bug in the trace function 4. minor improvements **Benefits:** **Possible Drawbacks:** **Related GitHub Issues:** Co-authored-by: Sebastián Duque Mesa <675763+sduquemesa@users.noreply.github.com> --- .github/CHANGELOG.md | 16 ++++++ .pylintrc | 2 +- mrmustard/lab/abstract/state.py | 10 ++-- mrmustard/lab/abstract/transformation.py | 17 +++--- mrmustard/lab/gates.py | 44 ++------------- mrmustard/math/tensorflow.py | 4 +- mrmustard/physics/fock.py | 67 +++++++++++++++++++---- tests/test_lab/test_circuit.py | 25 +++++++++ tests/test_physics/test_fock/test_fock.py | 46 ++++++++++++++-- 9 files changed, 161 insertions(+), 70 deletions(-) diff --git a/.github/CHANGELOG.md b/.github/CHANGELOG.md index ae4d3df12..a99ecefeb 100644 --- a/.github/CHANGELOG.md +++ b/.github/CHANGELOG.md @@ -42,8 +42,24 @@ physical even though the Wigner function might not contain all the features of t within the defined window. Also, expose some plot parameters and return the figure and axes. [(#179)](https://github.com/XanaduAI/MrMustard/pull/179) +* Added two functions in the `fock` module to apply operators to ket and dm. When used by the circuit it avoid having to fall back to unitaries as large as the whole circuit. + [(#180)](https://github.com/XanaduAI/MrMustard/pull/180) + ### Bug fixes +* The `Dgate` and the `Rgate` now correctly parse the case when a single scalar is intended as the same parameter of a number of gates in pallel. + [(#180)](https://github.com/XanaduAI/MrMustard/pull/180) + +* The trace function in the fock module was giving incorrect results when called with certain choices of modes. This is now fixed. + [(#180)](https://github.com/XanaduAI/MrMustard/pull/180) + +* The purity function for fock states no longer normalizes the density matrix before computing the purity. + [(#180)](https://github.com/XanaduAI/MrMustard/pull/180) + +* The function `dm_to_ket` no longer normalizes the density matrix before diagonalizing it. + [(#180)](https://github.com/XanaduAI/MrMustard/pull/180) + + ### Documentation ### Contributors diff --git a/.pylintrc b/.pylintrc index e61530971..baf0b4455 100644 --- a/.pylintrc +++ b/.pylintrc @@ -28,4 +28,4 @@ ignored-classes=numpy,tensorflow,scipy,networkx,strawberryfields,thewalrus # can either give multiple identifier separated by comma (,) or put this option # multiple time (only on the command line, not in the configuration file where # it should appear only once). -disable=no-member,line-too-long,invalid-name,too-many-lines,redefined-builtin,too-many-locals,duplicate-code,too-many-arguments,too-few-public-methods +disable=no-member,line-too-long,invalid-name,too-many-lines,redefined-builtin,too-many-locals,duplicate-code,too-many-arguments,too-few-public-methods,no-else-return diff --git a/mrmustard/lab/abstract/state.py b/mrmustard/lab/abstract/state.py index 2c9fb0975..2ccc9eeff 100644 --- a/mrmustard/lab/abstract/state.py +++ b/mrmustard/lab/abstract/state.py @@ -508,15 +508,13 @@ def get_modes(self, item): raise ValueError( f"Failed to request modes {item} for state {self} on modes {self.modes}." ) - + item_idx = [self.modes.index(m) for m in item] if self.is_gaussian: - cov, _, _ = gaussian.partition_cov(self.cov, item) - means, _ = gaussian.partition_means(self.means, item) + cov, _, _ = gaussian.partition_cov(self.cov, item_idx) + means, _ = gaussian.partition_means(self.means, item_idx) return State(cov=cov, means=means, modes=item) - fock_partitioned = fock.trace( - self.dm(self.cutoffs), keep=[m for m in range(self.num_modes) if m in item] - ) + fock_partitioned = fock.trace(self.dm(self.cutoffs), keep=item_idx) return State(dm=fock_partitioned, modes=item) # TODO: refactor diff --git a/mrmustard/lab/abstract/transformation.py b/mrmustard/lab/abstract/transformation.py index e8a1c3132..ef60c5ae4 100644 --- a/mrmustard/lab/abstract/transformation.py +++ b/mrmustard/lab/abstract/transformation.py @@ -117,8 +117,12 @@ def transform_fock(self, state: State, dual: bool) -> State: State: the transformed state """ if self.is_unitary: - U = self.U(cutoffs=state.cutoffs) + op_idx = [state.modes.index(m) for m in self.modes] + U = self.U(cutoffs=[state.cutoffs[i] for i in op_idx]) transformation = fock.math.dagger(U) if dual else U + if state.is_pure: + return State(ket=fock.apply_op_to_ket(U, state.ket(), op_idx), modes=state.modes) + return State(dm=fock.apply_op_to_dm(U, state.dm(), op_idx), modes=state.modes) else: transformation = self.choi(cutoffs=state.cutoffs) if dual: @@ -142,13 +146,10 @@ def transform_fock(self, state: State, dual: bool) -> State: def modes(self) -> Sequence[int]: """Returns the list of modes on which the transformation acts on.""" if self._modes in (None, []): - X, Y, d = self.XYd - if d is not None: - self._modes = list(range(d.shape[-1] // 2)) - elif X is not None: - self._modes = list(range(X.shape[-1] // 2)) - elif Y is not None: - self._modes = list(range(Y.shape[-1] // 2)) + for elem in self.XYd: + if elem is not None: + self._modes = list(range(elem.shape[-1] // 2)) + break return self._modes @modes.setter diff --git a/mrmustard/lab/gates.py b/mrmustard/lab/gates.py index 1fba98573..c7f19bfd4 100644 --- a/mrmustard/lab/gates.py +++ b/mrmustard/lab/gates.py @@ -19,7 +19,6 @@ """ from typing import Union, Optional, List, Tuple, Sequence -import numpy as np from mrmustard.types import Tensor from mrmustard import settings from mrmustard.training import Parametrized @@ -97,8 +96,9 @@ def U(self, cutoffs: Sequence[int]): """Returns the unitary representation of the Displacement gate using the Laguerre polynomials.""" - N = len(cutoffs) - x, y = self._parse_modes_and_args(cutoffs) + N = self.num_modes + x = self.x.value * math.ones(N, dtype=self.x.value.dtype) + y = self.y.value * math.ones(N, dtype=self.y.value.dtype) r = math.sqrt(x * x + y * y) phi = math.atan2(y, x) @@ -116,25 +116,6 @@ def U(self, cutoffs: Sequence[int]): list(range(0, 2 * N, 2)) + list(range(1, 2 * N, 2)), ) - def _parse_modes_and_args(self, cutoffs): - num_modes_state = len(cutoffs) - xargs = math.atleast_1d(self.x.value) - yargs = math.atleast_1d(self.y.value) - num_args_x = max(1, xargs.shape[-1]) - num_args_y = max(1, yargs.shape[-1]) - if num_args_x != num_args_y: - raise ValueError("Number of parameters for `x` and `y` should be the same.") - if num_args_x == 1: - # same arg for all modes - x = math.tile(xargs, [num_modes_state]) - y = math.tile(yargs, [num_modes_state]) - else: - x = math.zeros([num_modes_state]) - y = math.zeros([num_modes_state]) - x = math.update_tensor(x, [[m] for m in self.modes], xargs) - y = math.update_tensor(y, [[m] for m in self.modes], yargs) - return x, y - class Sgate(Parametrized, Transformation): r"""Squeezing gate. @@ -221,7 +202,7 @@ def X_matrix(self): def U(self, cutoffs: Sequence[int]): - angles = self._parse_modes_and_args(cutoffs) + angles = self.angle.value * math.ones(self.num_modes, dtype=self.angle.value.dtype) num_modes = len(cutoffs) # calculate rotation unitary for each mode and concatenate with outer product @@ -240,23 +221,6 @@ def U(self, cutoffs: Sequence[int]): list(range(0, 2 * num_modes, 2)) + list(range(1, 2 * num_modes, 2)), ) - def _parse_modes_and_args(self, cutoffs): - num_modes = len(cutoffs) - modes = self.modes # modes in which the gate is acting on - args = self.angle.value - num_args = ( - args.shape[0] if len(args.shape.as_list()) > 0 else 1 - ) # number or arguments given to the gate - angles = np.zeros((num_modes,)) - if num_args == 1: - # one arg for all modes - angles[modes] = args - else: - # an arg for each mode - angles = args - - return math.new_variable(angles, bounds=None, name="Rgate_angles") - class Pgate(Parametrized, Transformation): r"""Quadratic phase gate. diff --git a/mrmustard/math/tensorflow.py b/mrmustard/math/tensorflow.py index 188e83d30..71606e445 100644 --- a/mrmustard/math/tensorflow.py +++ b/mrmustard/math/tensorflow.py @@ -136,7 +136,9 @@ def diag_part(self, array: tf.Tensor) -> tf.Tensor: return tf.linalg.diag_part(array) def einsum(self, string: str, *tensors) -> tf.Tensor: - return tf.einsum(string, *tensors) + if type(string) is str: + return tf.einsum(string, *tensors) + return None # provide same functionality as numpy.einsum or upgrade to opt_einsum def exp(self, array: tf.Tensor) -> tf.Tensor: return tf.math.exp(array) diff --git a/mrmustard/physics/fock.py b/mrmustard/physics/fock.py index 227a2ed87..c247a01a8 100644 --- a/mrmustard/physics/fock.py +++ b/mrmustard/physics/fock.py @@ -148,7 +148,6 @@ def dm_to_ket(dm: Tensor) -> Tensor: cutoffs = dm.shape[: len(dm.shape) // 2] d = int(np.prod(cutoffs)) dm = math.reshape(dm, (d, d)) - dm = normalize(dm, is_dm=True) _, eigvecs = math.eigh(dm) # eigenvalues and related eigenvectors are sorted in non-decreasing order, @@ -327,10 +326,59 @@ def purity(dm: Tensor) -> Scalar: cutoffs = dm.shape[: len(dm.shape) // 2] d = int(np.prod(cutoffs)) # combined cutoffs in all modes dm = math.reshape(dm, (d, d)) - dm = normalize(dm, is_dm=True) return math.abs(math.sum(math.transpose(dm) * dm)) # tr(rho^2) +def apply_op_to_ket(op, ket, op_indices): + r"""Applies an operator to a ket in the sense + ket_abcde = sum_{ijk}U_abc,ijk ket_ijkde + + Args: + op (array): the operator to be applied + ket (array): the ket to which the operator is applied + op_indices (list): the indices the operator acts on + Returns: + array: the resulting ket + """ + K = ket.ndim + N = op.ndim // 2 + op_ket = math.tensordot(ket, op, axes=[op_indices, list(range(N, 2 * N))]) + perm = list(range(K - N)) + for i, o in enumerate(op_indices): + perm.insert(o, K - N + i) + return math.transpose(op_ket, perm) + + +def apply_op_to_dm(op, dm, op_modes): + r"""Applies an operator to a density matrix in the sense + dm_abcd = sum_{ij}op_ai dm_ibkd dagger(op)_kc + + Args: + op (array): the operator to be applied + dm (array): the density matrix to which the operator is applied + op_modes (list): the modes the operator acts on (counting from 0) + + Returns: + array: the resulting density matrix + """ + D = dm.ndim + N = len(op_modes) + op_dm = math.tensordot(dm, op, axes=[op_modes, np.arange(N, 2 * N)]) + # the N output indices of op are now at the end. We need to move them at op_modes + perm = list(range(D - N)) + for i, o in enumerate(op_modes): + perm.insert(o, D - N + i) + op_dm = math.transpose(op_dm, perm) + op_dm_op = math.tensordot( + op_dm, math.conj(op), axes=[[o + D // 2 for o in op_modes], np.arange(N, 2 * N)] + ) + # the N output indices of op are now at the end. We need to move them at op_modes + D//2 + perm = list(range(D - N)) + for i, o in enumerate(op_modes): + perm.insert(o + D // 2, D - N + i) + return math.transpose(op_dm_op, perm) + + def CPTP(transformation, fock_state, transformation_is_unitary: bool, state_is_dm: bool) -> Tensor: r"""Computes the CPTP (note: CP, really) channel given by a transformation (unitary matrix or choi operator) on a state. @@ -451,22 +499,21 @@ def is_mixed_dm(dm): def trace(dm, keep: List[int]): r"""Computes the partial trace of a density matrix. + The indices of the density matrix are in the order (out0, ..., outN-1, in0, ..., inN-1). + The indices to keep are a subset of the first N indices (they are doubled automatically + and applied to the second N indices as the trace is computed). Args: dm: the density matrix - keep: the modes to keep + keep: the modes to keep (0-based) """ N = len(dm.shape) // 2 trace = [m for m in range(N) if m not in keep] # put at the end all of the indices to trace over - keep_idx = [i for pair in [(k, k + N) for k in keep] for i in pair] - keep_idx = keep_idx[::2] + keep_idx[1::2] - trace_idx = [i for pair in [(t, t + N) for t in trace] for i in pair] - trace_idx = trace_idx[::2] + trace_idx[1::2] # stagger the indices + keep_idx = keep + [i + N for i in keep] + trace_idx = trace + [i + N for i in trace] dm = math.transpose(dm, keep_idx + trace_idx) - - d = int(np.prod(dm.shape[-len(trace) :])) - # make it square on those indices + d = int(np.prod([dm.shape[t] for t in trace])) dm = math.reshape(dm, dm.shape[: 2 * len(keep)] + (d, d)) return math.trace(dm) diff --git a/tests/test_lab/test_circuit.py b/tests/test_lab/test_circuit.py index 41e92b726..d49268ebe 100644 --- a/tests/test_lab/test_circuit.py +++ b/tests/test_lab/test_circuit.py @@ -20,3 +20,28 @@ from mrmustard.lab import * from mrmustard import settings from tests import random + + +def test_circuit_placement_SD(): + "tests that Sgate and Dgate can be placed in any order" + assert Sgate(1.0)[1] >> Dgate(1.0)[0] == Dgate(1.0)[0] >> Sgate(1.0)[1] + + +def test_circuit_placement_SR(): + "tests that Sgate and Rgate can be placed in any order" + assert Sgate(1.0)[1] >> Rgate(1.0)[0] == Rgate(1.0)[0] >> Sgate(1.0)[1] + + +def test_circuit_placement_RD(): + "tests that Rgate and Dgate can be placed in any order" + assert Rgate(1.0)[1] >> Dgate(1.0)[0] == Dgate(1.0)[0] >> Rgate(1.0)[1] + + +def test_circuit_placement_BS(): + "tests that BSgate and Sgate can be placed in any order" + assert BSgate(1.0)[1, 2] >> Sgate(1.0)[0] == Sgate(1.0)[0] >> BSgate(1.0)[1, 2] + + +def test_circuit_placement_BSBS(): + "tests that BSgates can be placed in any order" + assert BSgate(1.0)[1, 2] >> BSgate(1.0)[0, 3] == BSgate(1.0)[0, 3] >> BSgate(1.0)[1, 2] diff --git a/tests/test_physics/test_fock/test_fock.py b/tests/test_physics/test_fock/test_fock.py index 6bde403b0..a45c0eb9a 100644 --- a/tests/test_physics/test_fock/test_fock.py +++ b/tests/test_physics/test_fock/test_fock.py @@ -18,8 +18,23 @@ import numpy as np from scipy.special import factorial from thewalrus.quantum import total_photon_number_distribution -from mrmustard.lab import * -from mrmustard.physics.fock import dm_to_ket, ket_to_dm +from mrmustard.lab import ( + Vacuum, + Circuit, + S2gate, + BSgate, + Coherent, + SqueezedVacuum, + Attenuator, + Ggate, + Fock, + Gaussian, + Dgate, + Rgate, + State, + TMSV, +) +from mrmustard.physics.fock import dm_to_ket, ket_to_dm, trace # helper strategies @@ -111,7 +126,7 @@ def test_lossy_squeezing(n_mean, phi, eta): [cutoff] ) expected = np.array([total_photon_number_distribution(n, 1, r, eta) for n in range(cutoff)]) - assert np.allclose(ps, expected, atol=1e-6) + assert np.allclose(ps, expected, atol=1e-5) @given(n_mean=st.floats(0, 2), phi=st_angle, eta_0=st.floats(0, 1), eta_1=st.floats(0, 1)) @@ -155,7 +170,6 @@ def test_density_matrix(num_modes): def test_dm_to_ket(state): """Tests pure state density matrix conversion to ket""" dm = state.dm() - ket = dm_to_ket(dm) # check if ket is normalized assert np.allclose(np.linalg.norm(ket), 1) @@ -173,3 +187,27 @@ def test_dm_to_ket_error(): with pytest.raises(ValueError): dm_to_ket(state) + + +def test_fock_trace_mode1(): + """tests that the Fock state is correctly traced out from mode 1""" + state = Vacuum(2) >> Ggate(2) + from_gaussian = state.get_modes(0).dm([3]) + from_fock = State(dm=state.dm([40])).get_modes(0).dm([3]) + assert np.allclose(from_gaussian, from_fock, atol=1e-5) + + +def test_fock_trace_mode0(): + """tests that the Fock state is correctly traced out from mode 0""" + state = Vacuum(2) >> Ggate(2) + from_gaussian = state.get_modes(1).dm([3]) + from_fock = State(dm=state.dm([40])).get_modes(1).dm([3]) + assert np.allclose(from_gaussian, from_fock, atol=1e-5) + + +def test_fock_trace_function(): + """tests that the Fock state is correctly traced""" + state = Vacuum(2) >> Ggate(2) + dm = state.dm([10, 10]) + dm_traced = trace(dm, keep=[0]) + assert np.allclose(dm_traced, State(dm=dm).get_modes(0).dm(), atol=1e-5) From 4dc12a3035215e264d7e42df92777ce7c3d9268a Mon Sep 17 00:00:00 2001 From: ziofil Date: Tue, 13 Dec 2022 14:20:39 -0800 Subject: [PATCH 13/53] allows for full cutoff specification (#181) **Context:** sometimes it's useful to compute the fock representation using different cutoffs for input-output indices of the same mode **Description of the Change:** transformations (gates, circuit) now accept also a list of double (or quadruple for choi) length which specifies the cutoffs per index (rather than per mode) **Benefits:** saves runtime if one needs e.g. to input fock states into a gaussian circuit **Possible Drawbacks:** none **Related GitHub Issues:** --- .github/CHANGELOG.md | 6 +++++- mrmustard/lab/abstract/transformation.py | 4 ++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/.github/CHANGELOG.md b/.github/CHANGELOG.md index a99ecefeb..506e2fec0 100644 --- a/.github/CHANGELOG.md +++ b/.github/CHANGELOG.md @@ -42,9 +42,13 @@ physical even though the Wigner function might not contain all the features of t within the defined window. Also, expose some plot parameters and return the figure and axes. [(#179)](https://github.com/XanaduAI/MrMustard/pull/179) -* Added two functions in the `fock` module to apply operators to ket and dm. When used by the circuit it avoid having to fall back to unitaries as large as the whole circuit. +* Allows for full cutoff specification (index-wise rather than mode-wise) for subclasses of `Transformation`. This allows for a more compact Fock representation where needed. + [(#181)](https://github.com/XanaduAI/MrMustard/pull/181) + +* Added two functions in the `fock` module to apply operators to ket and dm. When used by the circuit it avoids having to fall back to unitaries as large as the whole circuit. [(#180)](https://github.com/XanaduAI/MrMustard/pull/180) + ### Bug fixes * The `Dgate` and the `Rgate` now correctly parse the case when a single scalar is intended as the same parameter of a number of gates in pallel. diff --git a/mrmustard/lab/abstract/transformation.py b/mrmustard/lab/abstract/transformation.py index ef60c5ae4..bd5fc0bc8 100644 --- a/mrmustard/lab/abstract/transformation.py +++ b/mrmustard/lab/abstract/transformation.py @@ -235,7 +235,7 @@ def U(self, cutoffs: Sequence[int]): return fock.fock_representation( choi_state.cov, choi_state.means, - shape=cutoffs * 2, + shape=cutoffs * 2 if len(cutoffs) == self.num_modes else cutoffs, return_unitary=True, choi_r=settings.CHOI_R, ) @@ -250,7 +250,7 @@ def choi(self, cutoffs: Sequence[int]): choi_op = fock.fock_representation( choi_state.cov, choi_state.means, - shape=cutoffs * 4, + shape=cutoffs * 4 if len(cutoffs) == self.num_modes else cutoffs, return_unitary=False, choi_r=settings.CHOI_R, ) From 7fb563bc178d9f425304028def288b95637f587a Mon Sep 17 00:00:00 2001 From: ziofil Date: Tue, 13 Dec 2022 14:36:13 -0800 Subject: [PATCH 14/53] changes norm to probability in repr of State (#182) **Context:** norm is confusing field in the repr of `State` (it's the sqrt of the probability if the state is pure and the probability if it's mixed). **Description of the Change:** Replace norm with probability **Benefits:** Consistent meaning and also more practically useful **Possible Drawbacks:** some users may miss the good old norm? **Related GitHub Issues:** --- .github/CHANGELOG.md | 3 +++ mrmustard/lab/abstract/state.py | 21 ++++++++++++++++++--- 2 files changed, 21 insertions(+), 3 deletions(-) diff --git a/.github/CHANGELOG.md b/.github/CHANGELOG.md index 506e2fec0..25798db03 100644 --- a/.github/CHANGELOG.md +++ b/.github/CHANGELOG.md @@ -48,6 +48,9 @@ within the defined window. Also, expose some plot parameters and return the figu * Added two functions in the `fock` module to apply operators to ket and dm. When used by the circuit it avoids having to fall back to unitaries as large as the whole circuit. [(#180)](https://github.com/XanaduAI/MrMustard/pull/180) +* Replaced norm with probability in the repr of `State`. This improves consistency over the old behaviour (norm was the sqrt of prob if the state was pure and prob if the state was mixed). + [(#182)](https://github.com/XanaduAI/MrMustard/pull/182) + ### Bug fixes diff --git a/mrmustard/lab/abstract/state.py b/mrmustard/lab/abstract/state.py index 2ccc9eeff..39674723b 100644 --- a/mrmustard/lab/abstract/state.py +++ b/mrmustard/lab/abstract/state.py @@ -227,9 +227,15 @@ def norm(self) -> float: r"""Returns the norm of the state.""" if self.is_gaussian: return self._norm - return fock.norm(self.fock, self.is_mixed) + @property + def probability(self) -> float: + r"""Returns the probability of the state.""" + if self.is_pure: + return self.norm**2 + return self.norm + def ket(self, cutoffs: List[int] = None) -> Optional[Tensor]: r"""Returns the ket of the state in Fock representation or ``None`` if the state is mixed. @@ -592,12 +598,21 @@ def __truediv__(self, other): return State(ket=self.ket() / other, modes=self.modes) raise ValueError("No fock representation available") + @staticmethod + def _format_probability(prob: float) -> str: + if prob < 0.001: + return f"{100*prob:.3e} %" + else: + return f"{prob:.3%}" + def _repr_markdown_(self): table = ( f"#### {self.__class__.__qualname__}\n\n" - + "| Purity | Norm | Num modes | Bosonic size | Gaussian | Fock |\n" + + "| Purity | Probability | Num modes | Bosonic size | Gaussian | Fock |\n" + "| :----: | :----: | :----: | :----: | :----: | :----: |\n" - + f"| {self.purity :.2e} | {self.norm :.2e} | {self.num_modes} | {'1' if self.is_gaussian else 'N/A'} | {'✅' if self.is_gaussian else '❌'} | {'✅' if self._ket is not None or self._dm is not None else '❌'} |" + + f"| {self.purity :.2e} | " + + self._format_probability(self.probability) + + f" | {self.num_modes} | {'1' if self.is_gaussian else 'N/A'} | {'✅' if self.is_gaussian else '❌'} | {'✅' if self._ket is not None or self._dm is not None else '❌'} |" ) if self.num_modes == 1: From 2f474c3615788d4ac5e20d03c857397afb4264d4 Mon Sep 17 00:00:00 2001 From: ziofil Date: Mon, 9 Jan 2023 14:36:21 -0800 Subject: [PATCH 15/53] Choi application bugfix (#188) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit **Context:** The application of a choi operator to a density matrix was resulting in a transposed dm **Description of the Change:** Fixes the order of the indices in the application of a choi operator to dm and ket **Benefits:** Correct result **Possible Drawbacks:** None **Related GitHub Issues:** None Co-authored-by: Sebastián Duque Mesa <675763+sduquemesa@users.noreply.github.com> --- mrmustard/lab/abstract/transformation.py | 6 ++++-- mrmustard/physics/fock.py | 20 ++++++++++++-------- tests/test_physics/test_fock/test_fock.py | 7 +++++++ 3 files changed, 23 insertions(+), 10 deletions(-) diff --git a/mrmustard/lab/abstract/transformation.py b/mrmustard/lab/abstract/transformation.py index bd5fc0bc8..3d8b23cb5 100644 --- a/mrmustard/lab/abstract/transformation.py +++ b/mrmustard/lab/abstract/transformation.py @@ -124,14 +124,16 @@ def transform_fock(self, state: State, dual: bool) -> State: return State(ket=fock.apply_op_to_ket(U, state.ket(), op_idx), modes=state.modes) return State(dm=fock.apply_op_to_dm(U, state.dm(), op_idx), modes=state.modes) else: - transformation = self.choi(cutoffs=state.cutoffs) + transformation = self.choi(cutoffs=state.cutoffs) # [out_r, in_r, out_l, in_l] if dual: n = len(state.cutoffs) N0 = list(range(0, n)) N1 = list(range(n, 2 * n)) N2 = list(range(2 * n, 3 * n)) N3 = list(range(3 * n, 4 * n)) - transformation = fock.math.transpose(transformation, N3 + N0 + N1 + N2) + transformation = fock.math.transpose( + transformation, N1 + N0 + N3 + N2 + ) # swap input and output indices [in_r, out_r, in_l, out_l] new_fock = fock.CPTP( transformation=transformation, fock_state=state.ket(state.cutoffs) if state.is_pure else state.dm(state.cutoffs), diff --git a/mrmustard/physics/fock.py b/mrmustard/physics/fock.py index c247a01a8..0566b0943 100644 --- a/mrmustard/physics/fock.py +++ b/mrmustard/physics/fock.py @@ -396,7 +396,7 @@ def CPTP(transformation, fock_state, transformation_is_unitary: bool, state_is_d num_modes = len(fock_state.shape) // 2 if state_is_dm else len(fock_state.shape) N0 = list(range(0, num_modes)) N1 = list(range(num_modes, 2 * num_modes)) - N2 = list(range(2 * num_modes, 3 * num_modes)) + N2 = list(range(2 * num_modes, 3 * num_modes)) # pylint: disable=unused-variable N3 = list(range(3 * num_modes, 4 * num_modes)) if transformation_is_unitary: U = transformation @@ -406,14 +406,18 @@ def CPTP(transformation, fock_state, transformation_is_unitary: bool, state_is_d # is state is dm, the input indices of dm are still at the end of Us return math.tensordot(Us, math.dagger(U), axes=(N1, N0)) - C = transformation # choi operator + # choi operator with indices in the order [out_r, in_r, out_l, in_l] + # note that the left and right indices of a dm have to contract with in_l and in_r + C = transformation if state_is_dm: - return math.tensordot(C, fock_state, axes=(N1 + N3, N0 + N1)) - - Cs = math.tensordot(C, fock_state, axes=(N1, N0)) - return math.tensordot( - Cs, math.conj(fock_state), axes=(N2, N0) - ) # N2 is the last set of indices now + output = math.tensordot(C, fock_state, axes=(N3 + N1, N0 + N1)) + # transpose because otherwise the output would be [out_r, out_l]: + return math.transpose(output, N1 + N0) + + # the order of the indices of a ket is just [out_l], which need to contract with in_l of the choi operator (N3) + Cs = math.tensordot(C, fock_state, axes=(N3, N0)) # now order is [out_r, in_r, out_l] + output = math.tensordot(Cs, math.conj(fock_state), axes=(N1, N0)) + return math.transpose(output, N1 + N0) # N2 is the last set of indices now def contract_states( diff --git a/tests/test_physics/test_fock/test_fock.py b/tests/test_physics/test_fock/test_fock.py index a45c0eb9a..6845d8df4 100644 --- a/tests/test_physics/test_fock/test_fock.py +++ b/tests/test_physics/test_fock/test_fock.py @@ -211,3 +211,10 @@ def test_fock_trace_function(): dm = state.dm([10, 10]) dm_traced = trace(dm, keep=[0]) assert np.allclose(dm_traced, State(dm=dm).get_modes(0).dm(), atol=1e-5) + + +def test_single_mode_choi_application_order(): + """Test dual operations output the correct mode ordering""" + s = Attenuator(1.0) << State(dm=SqueezedVacuum(1.0, np.pi / 2).dm([40])) + assert np.allclose(s.dm([10])[:10, :10], SqueezedVacuum(1.0, np.pi / 2).dm([10])) + # NOTE: the [:10,:10] part is not necessary once PR #184 is merged From 3ff0cab2a11bff30adbb4b0e19b81404bf2d5006 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Duque=20Mesa?= <675763+sduquemesa@users.noreply.github.com> Date: Tue, 10 Jan 2023 13:49:22 -0500 Subject: [PATCH 16/53] Check changelog entry is created on PRs (#189) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit **Context:** Always forgetting to add entries to the CHANGELOG file? **Description of the Change:** Now github will remind you to do so — this PR implements a new CI workflow checking if an entry has been added to the CHANGELOG file. This check is not mandatory meaning it won't block the ability to merge the PR, however checks will appear as failed. In case no changelog entry is needed one can use the `no changelog` label to disable the check. This PR also adds the changelog entry for PR #188. **Benefits:** No more PRs without CHANGELOG entries **Possible Drawbacks:** None --- .github/CHANGELOG.md | 4 ++++ .github/workflows/changelog.yml | 18 ++++++++++++++++++ 2 files changed, 22 insertions(+) create mode 100644 .github/workflows/changelog.yml diff --git a/.github/CHANGELOG.md b/.github/CHANGELOG.md index 25798db03..cfe88fd7e 100644 --- a/.github/CHANGELOG.md +++ b/.github/CHANGELOG.md @@ -204,6 +204,10 @@ transposing the indices of the input tensor. Now `math.dagger` appropriately cal Hermitian conjugate of an operator. [(#156)](https://github.com/XanaduAI/MrMustard/pull/156) +* The application of a Choi operator to a density matrix was resulting in a transposed dm. Now +the order of the indices in the application of a choi operator to dm and ket is correct. + [(#188)](https://github.com/XanaduAI/MrMustard/pull/188) + ### Documentation * The centralized [Xanadu Sphinx Theme](https://github.com/XanaduAI/xanadu-sphinx-theme) diff --git a/.github/workflows/changelog.yml b/.github/workflows/changelog.yml new file mode 100644 index 000000000..81ef582a5 --- /dev/null +++ b/.github/workflows/changelog.yml @@ -0,0 +1,18 @@ +name: Changelog entry +on: + pull_request: + types: [opened, synchronize, reopened, labeled, unlabeled] + +jobs: + changelog: + runs-on: ubuntu-latest + if: contains( github.event.pull_request.labels.*.name, 'no changelog') != true + + steps: + - uses: actions/checkout@v3 + with: + fetch-depth: 2 + + - name: check changelog entry + run: | + git diff --name-only -r HEAD^1 HEAD | grep .github/CHANGELOG.md From 0655361b51fa4f327398302eeb7e3a8a3b41bb23 Mon Sep 17 00:00:00 2001 From: ziofil Date: Wed, 11 Jan 2023 12:42:23 -0800 Subject: [PATCH 17/53] Seed (#183) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit **Context:** Setting a seed in MM is not trivial and often we need reproducible reults **Description of the Change:** The `settings` object now supports the `SEED` attribute, which is random unless it is set by the user. To unset it, just set it equal to `None`. **Benefits:** Easy to get reproducible results without messing with numpy.random. **Possible Drawbacks:** None? **Related GitHub Issues:** Co-authored-by: Sebastián Duque Mesa <675763+sduquemesa@users.noreply.github.com> --- .github/CHANGELOG.md | 5 +- mrmustard/__init__.py | 19 ++++++ mrmustard/math/math_interface.py | 18 +++--- tests/test_lab/test_detectors.py | 1 - tests/test_lab/test_states.py | 42 ++++++++++++- tests/test_physics/test_fock/test_fock.py | 2 +- tests/test_settings.py | 39 ++++++++++++ tests/test_training/test_opt.py | 76 +++++++++++------------ 8 files changed, 150 insertions(+), 52 deletions(-) create mode 100644 tests/test_settings.py diff --git a/.github/CHANGELOG.md b/.github/CHANGELOG.md index cfe88fd7e..42d9dbfd7 100644 --- a/.github/CHANGELOG.md +++ b/.github/CHANGELOG.md @@ -22,12 +22,15 @@ The result is added to the `callback_history` attribute of the optimizer. [(#175)](https://github.com/XanaduAI/MrMustard/pull/175) + * MrMustard's settings object (accessible via `from mrmustard import settings`) now supports `SEED` (an int). This will give reproducible results whenever randomness is involved. The seed is unset by default, and it can be unset again with `settings.SEED = None`. If one desires, the seeded random number generator is accessible directly via `settings.rng` (e.g. `settings.rng.normal()`). + [(#183)](https://github.com/XanaduAI/MrMustard/pull/183) + ### Breaking changes ### Improvements * The `Dgate` now uses The Walrus to calculate the unitary and gradients of the displacement gate in fock representation, providing better numerical stability for larger cutoff and displacement values. - [(#147)](https://github.com/XanaduAI/MrMustard/pull/147) + [(#147)](https://github.com/XanaduAI/MrMustard/pull/147) * Now the Wigner function is implemented in its own module and uses numba for speed. [(#171)](https://github.com/XanaduAI/MrMustard/pull/171) diff --git a/mrmustard/__init__.py b/mrmustard/__init__.py index 1eaaa947d..43eb1bfcb 100644 --- a/mrmustard/__init__.py +++ b/mrmustard/__init__.py @@ -14,6 +14,8 @@ """This is the top-most `__init__.py` file of MrMustard package.""" +import numpy as np + import rich.table from rich import print @@ -46,6 +48,22 @@ def __init__(self): self.HOMODYNE_SQUEEZING = 10.0 # misc self.PROGRESSBAR = True + self._seed = np.random.randint(0, 2**32) + self.rng = np.random.default_rng(self._seed) + + @property + def SEED(self): + """Returns the seed value if set, otherwise returns a random seed.""" + if self._seed is None: + self._seed = np.random.randint(0, 2**32) + self.rng = np.random.default_rng(self._seed) + return self._seed + + @SEED.setter + def SEED(self, value): + """Sets the seed value.""" + self._seed = value + self.rng = np.random.default_rng(self._seed) @property def BACKEND(self): @@ -68,6 +86,7 @@ def __repr__(self): table.add_column("Setting") table.add_column("Value") table.add_row("BACKEND", self.BACKEND) + table.add_row("SEED", str(self.SEED)) for key, value in self.__dict__.items(): if key == key.upper(): table.add_row(key, str(value)) diff --git a/mrmustard/math/math_interface.py b/mrmustard/math/math_interface.py index 871fc4f87..8029a363a 100644 --- a/mrmustard/math/math_interface.py +++ b/mrmustard/math/math_interface.py @@ -20,7 +20,7 @@ import numpy as np from scipy.special import binom from scipy.stats import unitary_group, ortho_group - +from mrmustard import settings from mrmustard.types import ( List, Tensor, @@ -978,12 +978,12 @@ def random_symplectic(self, num_modes: int, max_r: float = 1.0) -> Tensor: Squeezing is sampled uniformly from 0.0 to ``max_r`` (1.0 by default). """ if num_modes == 1: - W = np.exp(1j * np.random.uniform(size=(1, 1))) - V = np.exp(1j * np.random.uniform(size=(1, 1))) + W = np.exp(1j * settings.rng.uniform(size=(1, 1))) + V = np.exp(1j * settings.rng.uniform(size=(1, 1))) else: - W = unitary_group.rvs(dim=num_modes) - V = unitary_group.rvs(dim=num_modes) - r = np.random.uniform(low=0.0, high=max_r, size=num_modes) + W = unitary_group.rvs(dim=num_modes, random_state=settings.rng) + V = unitary_group.rvs(dim=num_modes, random_state=settings.rng) + r = settings.rng.uniform(low=0.0, high=max_r, size=num_modes) OW = self.unitary_to_orthogonal(W) OV = self.unitary_to_orthogonal(V) dd = self.diag(self.concat([self.exp(-r), np.exp(r)], axis=0), k=0) @@ -994,13 +994,13 @@ def random_orthogonal(N: int) -> Tensor: """A random orthogonal matrix in :math:`O(N)`.""" if N == 1: return np.array([[1.0]]) - return ortho_group.rvs(dim=N) + return ortho_group.rvs(dim=N, random_state=settings.rng) def random_unitary(self, N: int) -> Tensor: """a random unitary matrix in :math:`U(N)`""" if N == 1: - return self.exp(1j * np.random.uniform(size=(1, 1))) - return unitary_group.rvs(dim=N) + return self.exp(1j * settings.rng.uniform(size=(1, 1))) + return unitary_group.rvs(dim=N, random_state=settings.rng) def single_mode_to_multimode_vec(self, vec, num_modes: int): r"""Apply the same 2-vector (i.e. single-mode) to a larger number of modes.""" diff --git a/tests/test_lab/test_detectors.py b/tests/test_lab/test_detectors.py index fe9a487fc..e7b24c714 100644 --- a/tests/test_lab/test_detectors.py +++ b/tests/test_lab/test_detectors.py @@ -41,7 +41,6 @@ from tests.random import none_or_ math = Math() -np.random.seed(137) hbar = settings.HBAR diff --git a/tests/test_lab/test_states.py b/tests/test_lab/test_states.py index 29c8ad737..804a7478e 100644 --- a/tests/test_lab/test_states.py +++ b/tests/test_lab/test_states.py @@ -201,7 +201,7 @@ def test_modes_after_projection(m): def test_modes_after_double_projection(n, m): """Test number of modes is correct after double projection.""" assume(n != m) - a = Gaussian(4) << Fock([1, 2])[n, m] + a = Gaussian(4) >> Dgate(x=1.0)[0, 1, 2, 3] << Fock([1, 2])[n, m] assert np.allclose(a.modes, [k for k in range(4) if k != m and k != n]) assert len(a.modes) == 2 @@ -268,3 +268,43 @@ def test_ket_from_pure_dm(n, cutoffs): # check test state calculated the same ket as the original state assert np.allclose(test_ket, fock_state.ket()) + + +def test_ket_probability(): + "Test that the probability of a ket is calculated correctly." + state = State(ket=np.array([0.5, 0.5])) + assert np.isclose(state.probability, 2 * 0.5**2) + + +def test_dm_probability(): + "Test that the probability of a density matrix is calculated correctly." + state = State(dm=np.array([[0.4, 0.1], [0.1, 0.4]])) + assert np.isclose(state.probability, 0.8) + + +def test_padding_ket(): + "Test that padding a ket works correctly." + state = State(ket=SqueezedVacuum(r=1.0).ket(cutoffs=[20])) + assert len(state.ket(cutoffs=[10])) == 10 + assert len(state._ket) == 20 # pylint: disable=protected-access + + +def test_padding_dm(): + "Test that padding a density matrix works correctly." + state = State(dm=(SqueezedVacuum(r=1.0) >> Attenuator(0.6)).dm(cutoffs=[20])) + assert tuple(int(c) for c in state.dm(cutoffs=[10]).shape) == (10, 10) + assert tuple(int(c) for c in state._dm.shape) == (20, 20) # pylint: disable=protected-access + + +def test_state_repr_small_prob(): + "test that small probabilities are displayed correctly" + state = State(ket=np.array([0.0001, 0.0001])) + table = state._repr_markdown_() # pylint: disable=protected-access + assert "2.000e-06 %" in table + + +def test_state_repr_big_prob(): + "test that big probabilities are displayed correctly" + state = State(ket=np.array([0.5, 0.5])) + table = state._repr_markdown_() # pylint: disable=protected-access + assert "50.000%" in table diff --git a/tests/test_physics/test_fock/test_fock.py b/tests/test_physics/test_fock/test_fock.py index 6845d8df4..7a867e81f 100644 --- a/tests/test_physics/test_fock/test_fock.py +++ b/tests/test_physics/test_fock/test_fock.py @@ -31,8 +31,8 @@ Gaussian, Dgate, Rgate, - State, TMSV, + State, ) from mrmustard.physics.fock import dm_to_ket, ket_to_dm, trace diff --git a/tests/test_settings.py b/tests/test_settings.py new file mode 100644 index 000000000..78667b24c --- /dev/null +++ b/tests/test_settings.py @@ -0,0 +1,39 @@ +# Copyright 2022 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. + +""" +Tests for the Settings class. +""" + +from mrmustard import Settings + + +def test_settings_seed_randomness_at_init(): + """Test that the random seed is set randomly as MM is initialized.""" + settings = Settings() + seed0 = settings.SEED + del Settings.instance + settings = Settings() + seed1 = settings.SEED + assert seed0 != seed1 + + +def test_reproducibility(): + """Test that the random state is reproducible.""" + settings = Settings() + settings.SEED = 42 + seq0 = [settings.rng.integers(0, 2**32) for _ in range(10)] + settings.SEED = 42 + seq1 = [settings.rng.integers(0, 2**32) for _ in range(10)] + assert seq0 == seq1 diff --git a/tests/test_training/test_opt.py b/tests/test_training/test_opt.py index a99c1844d..e027dadde 100644 --- a/tests/test_training/test_opt.py +++ b/tests/test_training/test_opt.py @@ -45,10 +45,10 @@ @given(n=st.integers(0, 3)) def test_S2gate_coincidence_prob(n): """Testing the optimal probability of obtaining |n,n> from a two mode squeezed vacuum""" - tf.random.set_seed(137) + settings.SEED = 42 S = S2gate( - r=abs(np.random.normal()), - phi=np.random.normal(), + r=abs(settings.rng.normal()), + phi=settings.rng.normal(), r_trainable=True, phi_trainable=True, ) @@ -70,14 +70,14 @@ def test_hong_ou_mandel_optimizer(i, k): see Eq. 20 of https://journals.aps.org/prresearch/pdf/10.1103/PhysRevResearch.3.043065 which lacks a square root in the right hand side. """ - tf.random.set_seed(137) + settings.SEED = 42 r = np.arcsinh(1.0) s2_0, s2_1, bs = ( S2gate(r=r, phi=0.0, phi_trainable=True)[0, 1], S2gate(r=r, phi=0.0, phi_trainable=True)[2, 3], BSgate( - theta=np.arccos(np.sqrt(k / (i + k))) + 0.1 * np.random.normal(), - phi=np.random.normal(), + theta=np.arccos(np.sqrt(k / (i + k))) + 0.1 * settings.rng.normal(), + phi=settings.rng.normal(), theta_trainable=True, phi_trainable=True, )[1, 2], @@ -96,23 +96,22 @@ def cost_fn(): def test_learning_two_mode_squeezing(): """Finding the optimal beamsplitter transmission to make a pair of single photons""" - tf.random.set_seed(137) + settings.SEED = 42 ops = [ Sgate( - r=abs(np.random.normal(size=(2))), - phi=np.random.normal(size=(2)), + r=abs(settings.rng.normal(size=(2))), + phi=settings.rng.normal(size=(2)), r_trainable=True, phi_trainable=True, ), BSgate( - theta=np.random.normal(), - phi=np.random.normal(), + theta=settings.rng.normal(), + phi=settings.rng.normal(), theta_trainable=True, phi_trainable=True, ), ] circ = Circuit(ops) - tf.random.set_seed(20) state_in = Vacuum(num_modes=2) def cost_fn(): @@ -127,9 +126,8 @@ def cost_fn(): def test_learning_two_mode_Ggate(): """Finding the optimal Ggate to make a pair of single photons""" - tf.random.set_seed(137) + settings.SEED = 42 G = Ggate(num_modes=2, symplectic_trainable=True) - tf.random.set_seed(20) def cost_fn(): amps = (Vacuum(2) >> G).ket(cutoffs=[2, 2]) @@ -143,11 +141,11 @@ def cost_fn(): def test_learning_two_mode_Interferometer(): """Finding the optimal Interferometer to make a pair of single photons""" - np.random.seed(11) + settings.SEED = 42 ops = [ Sgate( - r=np.random.normal(size=(2)) ** 2, - phi=np.random.normal(size=(2)), + r=settings.rng.normal(size=(2)) ** 2, + phi=settings.rng.normal(size=(2)), r_trainable=True, phi_trainable=True, ), @@ -168,11 +166,11 @@ def cost_fn(): def test_learning_two_mode_RealInterferometer(): """Finding the optimal Interferometer to make a pair of single photons""" - np.random.seed(11) + settings.SEED = 2 ops = [ Sgate( - r=np.random.normal(size=(2)) ** 2, - phi=np.random.normal(size=(2)), + r=settings.rng.normal(size=(2)) ** 2, + phi=settings.rng.normal(size=(2)), r_trainable=True, phi_trainable=True, ), @@ -193,11 +191,11 @@ def cost_fn(): def test_learning_four_mode_Interferometer(): """Finding the optimal Interferometer to make a NOON state with N=2""" - np.random.seed(11) + settings.SEED = 4 ops = [ Sgate( - r=np.random.uniform(size=4), - phi=np.random.normal(size=4), + r=settings.rng.uniform(size=4), + phi=settings.rng.normal(size=4), r_trainable=True, phi_trainable=True, ), @@ -209,8 +207,8 @@ def test_learning_four_mode_Interferometer(): def cost_fn(): amps = (state_in >> circ).ket(cutoffs=[3, 3, 3, 3]) return ( - -tf.abs( - tf.reduce_sum( + -math.abs( + math.sum( amps[1, 1] * np.array([[0, 0, 1 / np.sqrt(2)], [0, 0, 0], [1 / np.sqrt(2), 0, 0]]) ) @@ -218,19 +216,18 @@ def cost_fn(): ** 2 ) - opt = Optimizer(symplectic_lr=0.5, euclidean_lr=0.01) - + opt = Optimizer(orthogonal_lr=0.05) opt.minimize(cost_fn, by_optimizing=[circ], max_steps=1000) assert np.allclose(-cost_fn(), 0.0625, atol=1e-5) def test_learning_four_mode_RealInterferometer(): """Finding the optimal Interferometer to make a NOON state with N=2""" - np.random.seed(11) + settings.SEED = 6 ops = [ Sgate( - r=np.random.uniform(size=4), - phi=np.random.normal(size=4), + r=settings.rng.uniform(size=4), + phi=settings.rng.normal(size=4), r_trainable=True, phi_trainable=True, ), @@ -242,8 +239,8 @@ def test_learning_four_mode_RealInterferometer(): def cost_fn(): amps = (state_in >> circ).ket(cutoffs=[3, 3, 3, 3]) return ( - -tf.abs( - tf.reduce_sum( + -math.abs( + math.sum( amps[1, 1] * np.array([[0, 0, 1 / np.sqrt(2)], [0, 0, 0], [1 / np.sqrt(2), 0, 0]]) ) @@ -251,9 +248,9 @@ def cost_fn(): ** 2 ) - opt = Optimizer(symplectic_lr=0.5, euclidean_lr=0.01) + opt = Optimizer() - opt.minimize(cost_fn, by_optimizing=[circ], max_steps=1000) + opt.minimize(cost_fn, by_optimizing=[circ], max_steps=400) assert np.allclose(-cost_fn(), 0.0625, atol=1e-5) @@ -261,12 +258,12 @@ def test_squeezing_hong_ou_mandel_optimizer(): """Finding the optimal squeezing parameter to get Hong-Ou-Mandel dip in time see https://www.pnas.org/content/117/52/33107/tab-article-info """ - tf.random.set_seed(137) + settings.SEED = 42 r = np.arcsinh(1.0) S_01 = S2gate(r=r, phi=0.0, phi_trainable=True)[0, 1] S_23 = S2gate(r=r, phi=0.0, phi_trainable=True)[2, 3] - S_12 = S2gate(r=1.0, phi=np.random.normal(), r_trainable=True, phi_trainable=True)[1, 2] + S_12 = S2gate(r=1.0, phi=settings.rng.normal(), r_trainable=True, phi_trainable=True)[1, 2] circ = Circuit([S_01, S_23, S_12]) @@ -280,11 +277,11 @@ def cost_fn(): def test_parameter_passthrough(): """Same as the test above, but with param passthrough""" - tf.random.set_seed(137) + settings.SEED = 42 r = np.arcsinh(1.0) par = Parametrized( r=math.new_variable(r, (0.0, None), "r"), - phi=math.new_variable(np.random.normal(), (None, None), "phi"), + phi=math.new_variable(settings.rng.normal(), (None, None), "phi"), ) ops = [ S2gate(r=r, phi=0.0, phi_trainable=True)[0, 1], @@ -304,7 +301,7 @@ def cost_fn(): def test_making_thermal_state_as_one_half_two_mode_squeezed_vacuum(): """Optimizes a Ggate on two modes so as to prepare a state with the same entropy and mean photon number as a thermal state""" - + settings.SEED = 42 S_init = two_mode_squeezing(np.arcsinh(1.0), 0.0) nbar = 1.4 @@ -333,6 +330,7 @@ def cost_fn(): def test_opt_backend_param(): """Test the optimization of a backend parameter defined outside a gate.""" # rotated displaced squeezed state + settings.SEED = 42 rotation_angle = np.pi / 2 target_state = SqueezedVacuum(r=1.0, phi=rotation_angle) From 1a1a314ab3fe5ec0e66306859b7c587bf09ae490 Mon Sep 17 00:00:00 2001 From: ziofil Date: Wed, 11 Jan 2023 13:47:59 -0800 Subject: [PATCH 18/53] Fock cutoff bugfix (#184) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit **Context:** The fock representation of a state is stored internally for future access. However, if in the meantime the cutoff is updated this is not taken into account, this PR solves this issue. **Description of the Change:** The cutoffs are applied to the internal fock representation upon access. **Benefits:** More correct implementation. **Possible Drawbacks:** None? **Related GitHub Issues:** None Co-authored-by: Sebastián Duque Mesa <675763+sduquemesa@users.noreply.github.com> --- .github/CHANGELOG.md | 2 ++ mrmustard/lab/abstract/state.py | 19 +++++++------------ mrmustard/physics/fock.py | 4 ++-- tests/test_lab/test_states.py | 7 +++++++ 4 files changed, 18 insertions(+), 14 deletions(-) diff --git a/.github/CHANGELOG.md b/.github/CHANGELOG.md index 42d9dbfd7..56b5de6a1 100644 --- a/.github/CHANGELOG.md +++ b/.github/CHANGELOG.md @@ -69,6 +69,8 @@ within the defined window. Also, expose some plot parameters and return the figu * The function `dm_to_ket` no longer normalizes the density matrix before diagonalizing it. [(#180)](https://github.com/XanaduAI/MrMustard/pull/180) +* The internal fock representation of states returns the correct cutoffs in all cases (solves an issue when a pure dm was converted to ket). +[(#184)](https://github.com/XanaduAI/MrMustard/pull/184) ### Documentation diff --git a/mrmustard/lab/abstract/state.py b/mrmustard/lab/abstract/state.py index 39674723b..a564ce6ac 100644 --- a/mrmustard/lab/abstract/state.py +++ b/mrmustard/lab/abstract/state.py @@ -249,23 +249,18 @@ def ket(self, cutoffs: List[int] = None) -> Optional[Tensor]: if self.is_mixed: return None - cutoffs = ( - self.cutoffs - if cutoffs is None - else [c if c is not None else self.cutoffs[i] for i, c in enumerate(cutoffs)] - ) + if cutoffs is None: + cutoffs = self.cutoffs + else: + cutoffs = [c if c is not None else self.cutoffs[i] for i, c in enumerate(cutoffs)] if self.is_gaussian: - self._ket = fock.fock_representation( - self.cov, self.means, shape=cutoffs, return_dm=False - ) + self._ket = fock.fock_representation(self.cov, self.means, cutoffs, False) else: # only fock representation is available if self._ket is None: # if state is pure and has a density matrix, calculate the ket if self.is_pure: self._ket = fock.dm_to_ket(self._dm) - return self._ket - return None current_cutoffs = list(self._ket.shape[: self.num_modes]) if cutoffs != current_cutoffs: paddings = [(0, max(0, new - old)) for new, old in zip(cutoffs, current_cutoffs)] @@ -274,7 +269,7 @@ def ket(self, cutoffs: List[int] = None) -> Optional[Tensor]: else: padded = self._ket return padded[tuple(slice(s) for s in cutoffs)] - return self._ket + return self._ket[tuple(slice(s) for s in cutoffs)] def dm(self, cutoffs: List[int] = None) -> Tensor: r"""Returns the density matrix of the state in Fock representation. @@ -306,7 +301,7 @@ def dm(self, cutoffs: List[int] = None) -> Tensor: else: padded = self._dm return padded[tuple(slice(s) for s in cutoffs + cutoffs)] - return self._dm + return self._dm[tuple(slice(s) for s in cutoffs + cutoffs)] def fock_probabilities(self, cutoffs: Sequence[int]) -> Tensor: r"""Returns the probabilities in Fock representation. diff --git a/mrmustard/physics/fock.py b/mrmustard/physics/fock.py index 0566b0943..cc325e314 100644 --- a/mrmustard/physics/fock.py +++ b/mrmustard/physics/fock.py @@ -504,8 +504,8 @@ def is_mixed_dm(dm): def trace(dm, keep: List[int]): r"""Computes the partial trace of a density matrix. The indices of the density matrix are in the order (out0, ..., outN-1, in0, ..., inN-1). - The indices to keep are a subset of the first N indices (they are doubled automatically - and applied to the second N indices as the trace is computed). + The indices to keep are a subset of the N 'out' indices + (they count for the 'in' indices as well). Args: dm: the density matrix diff --git a/tests/test_lab/test_states.py b/tests/test_lab/test_states.py index 804a7478e..caae48806 100644 --- a/tests/test_lab/test_states.py +++ b/tests/test_lab/test_states.py @@ -270,6 +270,13 @@ def test_ket_from_pure_dm(n, cutoffs): assert np.allclose(test_ket, fock_state.ket()) +def test_ket_from_pure_dm_new_cutoffs(): + "tests that the shape of the internal fock representation reflects the new cutoffs" + state = Vacuum(1) >> Sgate(0.1) >> Dgate(0.1, 0.1) # weak gaussian state + state = State(dm=state.dm(cutoffs=[20])) # assign pure dm directly + assert state.ket(cutoffs=[5]).shape.as_list() == [5] # shape should be [5] + + def test_ket_probability(): "Test that the probability of a ket is calculated correctly." state = State(ket=np.array([0.5, 0.5])) From 7511b952460eb9dcc439e212a43e0cfa7c621409 Mon Sep 17 00:00:00 2001 From: ziofil Date: Wed, 11 Jan 2023 19:42:03 -0800 Subject: [PATCH 19/53] fix max int32 --- mrmustard/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/mrmustard/__init__.py b/mrmustard/__init__.py index 43eb1bfcb..55ebf5be3 100644 --- a/mrmustard/__init__.py +++ b/mrmustard/__init__.py @@ -48,14 +48,14 @@ def __init__(self): self.HOMODYNE_SQUEEZING = 10.0 # misc self.PROGRESSBAR = True - self._seed = np.random.randint(0, 2**32) + self._seed = np.random.randint(0, 2**31 - 1) self.rng = np.random.default_rng(self._seed) @property def SEED(self): """Returns the seed value if set, otherwise returns a random seed.""" if self._seed is None: - self._seed = np.random.randint(0, 2**32) + self._seed = np.random.randint(0, 2**31 - 1) self.rng = np.random.default_rng(self._seed) return self._seed From aa9be5e74b3a00cfa524ac035d709a51ed899e78 Mon Sep 17 00:00:00 2001 From: ziofil Date: Fri, 13 Jan 2023 13:23:01 -0800 Subject: [PATCH 20/53] Bargmann methods (#185) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit **Context:** Going forward we need a big refactoring of the methods to transform between representations. This PR is the first in this direction. **Description of the Change:** Introduced two new modules and refactored various methods **Benefits:** Allows for easier extension of representation methods **Possible Drawbacks:** Some methods and functions have a new name and/or arguments **Related GitHub Issues:** None Co-authored-by: Sebastián Duque Mesa <675763+sduquemesa@users.noreply.github.com> --- .github/CHANGELOG.md | 24 +- mrmustard/lab/abstract/state.py | 26 +- mrmustard/lab/abstract/transformation.py | 90 ++-- mrmustard/lab/circuit.py | 5 + mrmustard/lab/detectors.py | 4 +- mrmustard/lab/gates.py | 2 +- mrmustard/math/__init__.py | 10 +- mrmustard/math/math_interface.py | 12 + mrmustard/math/mmtensor.py | 171 +++++++ mrmustard/math/tensorflow.py | 6 + mrmustard/physics/bargmann.py | 114 +++++ mrmustard/physics/fock.py | 423 ++++++++++-------- mrmustard/physics/husimi.py | 44 ++ tests/test_lab/test_circuit.py | 8 + tests/test_lab/test_detectors.py | 2 +- tests/test_lab/test_gates_fock.py | 22 +- tests/test_math/test_mmtensor.py | 73 +++ tests/test_physics/test_fock/test_fock.py | 62 ++- .../test_gaussian/test_symplectics.py | 21 +- tests/test_training/test_opt.py | 2 +- 20 files changed, 805 insertions(+), 316 deletions(-) create mode 100644 mrmustard/math/mmtensor.py create mode 100644 mrmustard/physics/bargmann.py create mode 100644 mrmustard/physics/husimi.py create mode 100644 tests/test_math/test_mmtensor.py diff --git a/.github/CHANGELOG.md b/.github/CHANGELOG.md index 56b5de6a1..3e4846295 100644 --- a/.github/CHANGELOG.md +++ b/.github/CHANGELOG.md @@ -22,9 +22,30 @@ The result is added to the `callback_history` attribute of the optimizer. [(#175)](https://github.com/XanaduAI/MrMustard/pull/175) + * We introduce the tensor wrapper `MMTensor` (available in `math.mmtensor`) that allows for a very easy handling of tensor contractions. Internally MrMustard performs lots of tensor contractions and this wrapper allows one to label each index of a tensor and perform contractions using the `@` symbol as if it were a simple matrix multiplication (the indices with the same name get contracted). + [(#185)](https://github.com/XanaduAI/MrMustard/pull/185) + + * the Math interface now supports linear system solving via `math.solve`. + [(#185)](https://github.com/XanaduAI/MrMustard/pull/185) + + ```python + from mrmustard.math.mmtensor import MMTensor + + # define two tensors + A = MMTensor(np.random.rand(2, 3, 4), axis_labels=["foo", "bar", "contract"]) + B = MMTensor(np.random.rand(4, 5, 6), axis_labels=["contract", "baz", "qux"]) + + # perform a tensor contraction + C = A @ B + C.axis_labels # ["foo", "bar", "baz", "qux"] + C.shape # (2, 3, 5, 6) + C.tensor # extract actual result + ``` + * MrMustard's settings object (accessible via `from mrmustard import settings`) now supports `SEED` (an int). This will give reproducible results whenever randomness is involved. The seed is unset by default, and it can be unset again with `settings.SEED = None`. If one desires, the seeded random number generator is accessible directly via `settings.rng` (e.g. `settings.rng.normal()`). [(#183)](https://github.com/XanaduAI/MrMustard/pull/183) + ### Breaking changes ### Improvements @@ -54,7 +75,8 @@ within the defined window. Also, expose some plot parameters and return the figu * Replaced norm with probability in the repr of `State`. This improves consistency over the old behaviour (norm was the sqrt of prob if the state was pure and prob if the state was mixed). [(#182)](https://github.com/XanaduAI/MrMustard/pull/182) - +* Added two new modules (`physics.bargmann` and `physics.husimi`) to host the functions related to those representation, which have been refactored and moved out of `physics.fock`. + [(#185)](https://github.com/XanaduAI/MrMustard/pull/185) ### Bug fixes * The `Dgate` and the `Rgate` now correctly parse the case when a single scalar is intended as the same parameter of a number of gates in pallel. diff --git a/mrmustard/lab/abstract/state.py b/mrmustard/lab/abstract/state.py index a564ce6ac..04d7d43dd 100644 --- a/mrmustard/lab/abstract/state.py +++ b/mrmustard/lab/abstract/state.py @@ -132,9 +132,8 @@ def purity(self) -> float: if self._purity is None: if self.is_gaussian: self._purity = gaussian.purity(self.cov, settings.HBAR) - # TODO: add symplectic representation else: - self._purity = fock.purity(self.fock) # has to be dm + self._purity = fock.purity(self._dm) return self._purity @property @@ -195,7 +194,7 @@ def shape(self) -> List[int]: def fock(self) -> Array: r"""Returns the Fock representation of the state.""" if self._dm is None and self._ket is None: - _fock = fock.fock_representation( + _fock = fock.wigner_to_fock_state( self.cov, self.means, shape=self.shape, return_dm=self.is_mixed ) if self.is_mixed: @@ -227,14 +226,15 @@ def norm(self) -> float: r"""Returns the norm of the state.""" if self.is_gaussian: return self._norm - return fock.norm(self.fock, self.is_mixed) + return fock.norm(self.fock, self._dm is not None) @property def probability(self) -> float: r"""Returns the probability of the state.""" - if self.is_pure: - return self.norm**2 - return self.norm + norm = self.norm + if self.is_pure and self._ket is not None: + return norm**2 + return norm def ket(self, cutoffs: List[int] = None) -> Optional[Tensor]: r"""Returns the ket of the state in Fock representation or ``None`` if the state is mixed. @@ -255,7 +255,9 @@ def ket(self, cutoffs: List[int] = None) -> Optional[Tensor]: cutoffs = [c if c is not None else self.cutoffs[i] for i, c in enumerate(cutoffs)] if self.is_gaussian: - self._ket = fock.fock_representation(self.cov, self.means, cutoffs, False) + self._ket = fock.wigner_to_fock_state( + self.cov, self.means, shape=cutoffs, return_dm=False + ) else: # only fock representation is available if self._ket is None: # if state is pure and has a density matrix, calculate the ket @@ -291,7 +293,7 @@ def dm(self, cutoffs: List[int] = None) -> Tensor: return fock.ket_to_dm(ket) else: if self.is_gaussian: - self._dm = fock.fock_representation( + self._dm = fock.wigner_to_fock_state( self.cov, self.means, shape=cutoffs * 2, return_dm=True ) elif cutoffs != (current_cutoffs := list(self._dm.shape[: self.num_modes])): @@ -397,9 +399,9 @@ def _contract_with_other(self, other): out_fock = fock.contract_states( stateA=other.ket(other_cutoffs) if other.is_pure else other.dm(other_cutoffs), stateB=self.ket(self_cutoffs) if self.is_pure else self.dm(self_cutoffs), - a_is_mixed=other.is_mixed, - b_is_mixed=self.is_mixed, - modes=other.indices(self.modes), # TODO: change arg name to indices + a_is_dm=other.is_mixed, + b_is_dm=self.is_mixed, + modes=other.indices(self.modes), normalize=self._normalize if hasattr(self, "_normalize") else False, ) diff --git a/mrmustard/lab/abstract/transformation.py b/mrmustard/lab/abstract/transformation.py index 3d8b23cb5..4c934f432 100644 --- a/mrmustard/lab/abstract/transformation.py +++ b/mrmustard/lab/abstract/transformation.py @@ -40,7 +40,6 @@ class Transformation: r"""Base class for all Transformations.""" - _bell = None # single-mode TMSV state for gaussian-to-fock conversion is_unitary = True # whether the transformation is unitary (True by default) def primal(self, state: State) -> State: @@ -73,22 +72,6 @@ def dual(self, state: State) -> State: new_state = self.transform_fock(state, dual=True) return new_state - @property - def bell(self): - r"""The N-mode two-mode squeezed vacuum for the choi-jamiolkowksi isomorphism.""" - if self._bell is None: - cov = gaussian.two_mode_squeezed_vacuum_cov( - r=settings.CHOI_R, phi=0.0, hbar=settings.HBAR - ) - means = gaussian.vacuum_means(num_modes=2, hbar=settings.HBAR) - bell = bell_single = State(cov=cov, means=means) - for _ in range(self.num_modes - 1): - bell = bell & bell_single - tot = 2 * self.num_modes - order = tuple(range(0, tot, 2)) + tuple(range(1, tot, 2)) - self._bell = bell.get_modes(order) - return self._bell[self.modes + [m + self.num_modes for m in self.modes]] - def transform_gaussian(self, state: State, dual: bool) -> State: r"""Transforms a Gaussian state into a Gaussian state. @@ -116,33 +99,39 @@ def transform_fock(self, state: State, dual: bool) -> State: Returns: State: the transformed state """ + op_idx = [state.modes.index(m) for m in self.modes] if self.is_unitary: - op_idx = [state.modes.index(m) for m in self.modes] U = self.U(cutoffs=[state.cutoffs[i] for i in op_idx]) - transformation = fock.math.dagger(U) if dual else U + U = math.dagger(U) if dual else U if state.is_pure: - return State(ket=fock.apply_op_to_ket(U, state.ket(), op_idx), modes=state.modes) - return State(dm=fock.apply_op_to_dm(U, state.dm(), op_idx), modes=state.modes) + return State(ket=fock.apply_kraus_to_ket(U, state.ket(), op_idx), modes=state.modes) + return State(dm=fock.apply_kraus_to_dm(U, state.dm(), op_idx), modes=state.modes) else: - transformation = self.choi(cutoffs=state.cutoffs) # [out_r, in_r, out_l, in_l] + choi = self.choi(cutoffs=state.cutoffs) + n = state.num_modes + N0 = list(range(0, n)) + N1 = list(range(n, 2 * n)) + N2 = list(range(2 * n, 3 * n)) + N3 = list(range(3 * n, 4 * n)) if dual: - n = len(state.cutoffs) - N0 = list(range(0, n)) - N1 = list(range(n, 2 * n)) - N2 = list(range(2 * n, 3 * n)) - N3 = list(range(3 * n, 4 * n)) - transformation = fock.math.transpose( - transformation, N1 + N0 + N3 + N2 - ) # swap input and output indices [in_r, out_r, in_l, out_l] - new_fock = fock.CPTP( - transformation=transformation, - fock_state=state.ket(state.cutoffs) if state.is_pure else state.dm(state.cutoffs), - transformation_is_unitary=self.is_unitary, - state_is_dm=state.is_mixed, - ) - if state.is_mixed or not self.is_unitary: - return State(dm=new_fock, modes=state.modes) - return State(ket=new_fock, modes=state.modes) + choi = math.transpose(choi, N1 + N3 + N0 + N2) # we flip out-in + + if state.is_pure: + # applies choi to ket by applying a "kraus op" with no outgoing indices (ket) to a "dm" (choi) + # choi though has index order [out_l, in_l, out_r, in_r] + choi = ( + math.transpose(choi, N0 + N2 + N1 + N3) + if not dual + else math.transpose(choi, N1 + N3 + N0 + N2) + ) + # now choi looks like a proper dm with index order [out_l, out_r, in_l, in_r] # or l <-> r if dual + return State( + dm=fock.apply_kraus_to_dm( + kraus=state.ket(), dm=choi, kraus_in_idx=op_idx, kraus_out_idx=[] + ), + modes=state.modes, + ) + return State(dm=fock.apply_choi_to_dm(choi, state.dm(), op_idx), modes=state.modes) @property def modes(self) -> Sequence[int]: @@ -233,13 +222,11 @@ def U(self, cutoffs: Sequence[int]): r"""Returns the unitary representation of the transformation.""" if not self.is_unitary: return None - choi_state = self.bell >> self - return fock.fock_representation( - choi_state.cov, - choi_state.means, + X, _, d = self.XYd + return fock.wigner_to_fock_U( + X if X is not None else math.eye(2 * self.num_modes), + d if d is not None else math.zeros((2 * self.num_modes,)), shape=cutoffs * 2 if len(cutoffs) == self.num_modes else cutoffs, - return_unitary=True, - choi_r=settings.CHOI_R, ) def choi(self, cutoffs: Sequence[int]): @@ -247,16 +234,13 @@ def choi(self, cutoffs: Sequence[int]): if self.is_unitary: U = self.U(cutoffs) return fock.U_to_choi(U) - - choi_state = self.bell >> self - choi_op = fock.fock_representation( - choi_state.cov, - choi_state.means, + X, Y, d = self.XYd + return fock.wigner_to_fock_Choi( + X if X is not None else math.eye(2 * self.num_modes), + Y if Y is not None else math.zeros((2 * self.num_modes, 2 * self.num_modes)), + d if d is not None else math.zeros((2 * self.num_modes,)), shape=cutoffs * 4 if len(cutoffs) == self.num_modes else cutoffs, - return_unitary=False, - choi_r=settings.CHOI_R, ) - return choi_op def __getitem__(self, items) -> Callable: r"""Sets the modes on which the transformation acts. diff --git a/mrmustard/lab/circuit.py b/mrmustard/lab/circuit.py index 6a5898c77..e4a57e6cb 100644 --- a/mrmustard/lab/circuit.py +++ b/mrmustard/lab/circuit.py @@ -88,6 +88,11 @@ def is_gaussian(self): """Returns `true` if all operations in the circuit are Gaussian.""" return all(op.is_gaussian for op in self._ops) + @property + def is_unitary(self): + """Returns `true` if all operations in the circuit are unitary.""" + return all(op.is_unitary for op in self._ops) + def __len__(self): return len(self._ops) diff --git a/mrmustard/lab/detectors.py b/mrmustard/lab/detectors.py index 75547eae7..55dcbfcf7 100644 --- a/mrmustard/lab/detectors.py +++ b/mrmustard/lab/detectors.py @@ -425,8 +425,8 @@ def _measure_fock(self, other) -> Union[State, float]: out_fock = fock.contract_states( stateA=other.ket(other_cutoffs) if other.is_pure else other.dm(other_cutoffs), stateB=self.state.ket(self_cutoffs), - a_is_mixed=other.is_mixed, - b_is_mixed=False, + a_is_dm=other.is_mixed, + b_is_dm=False, modes=other.indices(self.modes), normalize=False, ) diff --git a/mrmustard/lab/gates.py b/mrmustard/lab/gates.py index c7f19bfd4..b21adc155 100644 --- a/mrmustard/lab/gates.py +++ b/mrmustard/lab/gates.py @@ -215,7 +215,7 @@ def U(self, cutoffs: Sequence[int]): U_next = math.diag(math.make_complex(math.cos(theta), math.sin(theta))) Ur = math.outer(Ur, U_next) - # return total unitary with indexes reordered according to MM convetion + # return total unitary with indexes reordered according to MM convention return math.transpose( Ur, list(range(0, 2 * num_modes, 2)) + list(range(1, 2 * num_modes, 2)), diff --git a/mrmustard/math/__init__.py b/mrmustard/math/__init__.py index 46e2ad779..1ff9415ca 100644 --- a/mrmustard/math/__init__.py +++ b/mrmustard/math/__init__.py @@ -49,13 +49,13 @@ class Math: r""" This class is a switcher for performing math operations on the currently active backend. """ - # pylint: disable=no-else-return + def __getattribute__(self, name): if settings.BACKEND == "tensorflow": return object.__getattribute__(TFMath(), name) elif settings.BACKEND == "torch": return object.__getattribute__(TorchMath(), name) - - raise ValueError( - f"No `{settings.BACKEND}` backend found. Ensure your backend is either ``'tensorflow'`` or ``'torch'``" - ) + else: + raise ValueError( + f"No `{settings.BACKEND}` backend found. Ensure your backend is either ``'tensorflow'`` or ``'torch'``" + ) diff --git a/mrmustard/math/math_interface.py b/mrmustard/math/math_interface.py index 8029a363a..d214767de 100644 --- a/mrmustard/math/math_interface.py +++ b/mrmustard/math/math_interface.py @@ -706,6 +706,18 @@ def sinh(self, array: Tensor) -> Tensor: array: hyperbolic sine of ``array`` """ + @abstractmethod + def solve(self, matrix: Tensor, rhs: Tensor) -> Tensor: + r"""Returns the solution of the linear system :math:`Ax = b`. + + Args: + matrix (array): matrix :math:`A` + rhs (array): vector :math:`b` + + Returns: + array: solution :math:`x` + """ + @abstractmethod def sqrt(self, x: Tensor, dtype=None) -> Tensor: r"""Returns the square root of ``x``. diff --git a/mrmustard/math/mmtensor.py b/mrmustard/math/mmtensor.py new file mode 100644 index 000000000..57e6644e8 --- /dev/null +++ b/mrmustard/math/mmtensor.py @@ -0,0 +1,171 @@ +# 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. + +# pylint: disable=redefined-outer-name + +""" +This module contains the implementation of a tensor wrapper class. +""" + +from typing import List, Optional, Union +import string +from mrmustard.math import Math + +math = Math() + + +class MMTensor: + r"""A Mr Mustard tensor (a wrapper around an array that implements the numpy array API).""" + + def __init__(self, array, axis_labels=None): + # If the input array is an MMTensor, use its tensor and axis labels (or the provided ones if specified) + if isinstance(array, MMTensor): + self.tensor = array.tensor + self.axis_labels = axis_labels or array.axis_labels + else: + self.tensor = array + self.axis_labels = axis_labels + + # If axis labels are not provided, generate default labels + if self.axis_labels is None: + self.axis_labels = [str(n) for n in range(len(self.tensor.shape))] + + # Validate the number of axis labels + if len(self.axis_labels) != len(self.tensor.shape): + raise ValueError("The number of axis labels must be equal to the number of axes.") + + def __array__(self): + """ + Implement the NumPy array interface. + """ + return self.tensor + + def __array_ufunc__(self, ufunc, method, *inputs, **kwargs): + """ + Implement the NumPy ufunc interface. + """ + if method == "__call__": + return MMTensor(ufunc(*inputs, **kwargs), self.axis_labels) + else: + return NotImplemented + + def __matmul__(self, other): + """ + Overload the @ operator to perform tensor contractions. + """ + # if not isinstance(other, MMTensor): + # raise TypeError(f"Cannot contract with object of type {type(other)}") + + # Find common axis labels + common_labels = set(self.axis_labels) & set(other.axis_labels) + + if not common_labels: + raise ValueError("No common axis labels found") + + # Determine the indices to contract along + left_indices = [self.axis_labels.index(label) for label in common_labels] + right_indices = [other.axis_labels.index(label) for label in common_labels] + + # Create a list of the new axis labels + new_axis_labels = [label for label in self.axis_labels if label not in common_labels] + [ + label for label in other.axis_labels if label not in common_labels + ] + + return MMTensor( + math.tensordot(self.tensor, other.tensor, axes=(left_indices, right_indices)), + new_axis_labels, + ) + + def contract(self, relabeling: Optional[List[str]] = None): + """ + Contract the tensor along the specified indices using einsum. + + Args: + relabeling (list[str]): An optional list of new axis labels. + The tensor is contracted along all groups of axes with matching labels. + """ + # check that labels are valid + if relabeling is None: + relabeling = self.axis_labels + elif len(relabeling) != len(self.axis_labels): + raise ValueError("The number of labels must be equal to the number of axes.") + + self.axis_labels = relabeling + + # Find all unique labels but keep the order + unique_labels = [] + for label in relabeling: + if label not in unique_labels: + unique_labels.append(label) + repeated = [label for label in unique_labels if self.axis_labels.count(label) > 1] + + # Turn labels into consecutive ascii lower-case letters, with same letters corresponding to the same label + label_map = {label: string.ascii_lowercase[i] for i, label in enumerate(unique_labels)} + labels = [label_map[label] for label in self.axis_labels] + + # create einsum string from labels + einsum_str = "".join(labels) + + # Contract the tensor and assign new axis labels (unique labels except for the contracted ones) + return MMTensor( + math.einsum(einsum_str, self.tensor), + [label for label in unique_labels if label not in repeated], + ) + + def transpose(self, perm: Union[List[int], List[str]]): + """ + Transpose the tensor using a list of axis labels or indices. + """ + if set(perm) == set(self.axis_labels): + perm = [self.axis_labels.index(label) for label in perm] + return MMTensor(math.transpose(self.tensor, perm), [self.axis_labels[i] for i in perm]) + + def reshape(self, shape, axis_labels=None): + """ + Reshape the tensor. Allows to change the axis labels. + """ + return MMTensor(math.reshape(self.tensor, shape), axis_labels or self.axis_labels) + + def __getitem__(self, indices): + """ + Implement indexing into the tensor. + """ + if isinstance(indices, tuple): + axis_labels = [] + for i, ind in enumerate(indices): + if ind is Ellipsis and i == 0: + axis_labels += self.axis_labels[:i] + elif isinstance(ind, slice): + axis_labels += self.axis_labels[i] + elif ind is Ellipsis and i > 0: + axis_labels += self.axis_labels[i:] + break + return MMTensor(self.tensor[indices], axis_labels) + else: + # Index along a single axis and take care of the axis labels + return MMTensor( + self.tensor[indices], self.axis_labels[:indices] + self.axis_labels[indices + 1 :] + ) + + def __repr__(self): + return f"MMTensor({self.tensor}, {self.axis_labels})" + + def __getattribute__(self, name): + """ + Implement the underlying array's methods. + """ + try: + return super().__getattribute__(name) + except AttributeError: + return getattr(self.tensor, name) diff --git a/mrmustard/math/tensorflow.py b/mrmustard/math/tensorflow.py index 71606e445..fa4eb1c4b 100644 --- a/mrmustard/math/tensorflow.py +++ b/mrmustard/math/tensorflow.py @@ -265,6 +265,12 @@ def sin(self, array: tf.Tensor) -> tf.Tensor: def sinh(self, array: tf.Tensor) -> tf.Tensor: return tf.math.sinh(array) + def solve(self, matrix: tf.Tensor, rhs: tf.Tensor) -> tf.Tensor: + if len(rhs.shape) == len(matrix.shape) - 1: + rhs = tf.expand_dims(rhs, -1) + return tf.linalg.solve(matrix, rhs)[..., 0] + return tf.linalg.solve(matrix, rhs) + def sqrt(self, x: tf.Tensor, dtype=None) -> tf.Tensor: return tf.sqrt(self.cast(x, dtype)) diff --git a/mrmustard/physics/bargmann.py b/mrmustard/physics/bargmann.py new file mode 100644 index 000000000..6efb94a01 --- /dev/null +++ b/mrmustard/physics/bargmann.py @@ -0,0 +1,114 @@ +# 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. + +# pylint: disable=redefined-outer-name + +""" +This module contains functions for transforming to the Bargmann representation. +""" +import numpy as np +from mrmustard.physics.husimi import wigner_to_husimi, pq_to_aadag +from mrmustard import settings +from mrmustard.math import Math + +math = Math() + + +def cayley(X, c): + r"""Returns the Cayley transform of a matrix: + :math:`cay(X) = (X - cI)(X + cI)^{-1}` + + Args: + c (float): the parameter of the Cayley transform + X (Tensor): a matrix + + Returns: + Tensor: the Cayley transform of X + """ + I = math.eye(X.shape[0], dtype=X.dtype) + return math.solve(X + c * I, X - c * I) + + +def wigner_to_bargmann_rho(cov, means): + r"""Converts the wigner representation in terms of covariance matrix and mean vector into the Bargmann `A,B,C` triple + for a density matrix (i.e. for `M` modes, `A` has shape `2M x 2M` and `B` has shape `2M`). + The order of the rows/columns of A and B corresponds to a density matrix with the usual ordering of the indices. + + Note that here A and B are defined with inverted blocks with respect to the literature, + otherwise the density matrix would have the left and the right indices swapped once we convert to Fock. + By inverted blocks we mean that if A is normally defined as `A = [[A_00, A_01], [A_10, A_11]]`, + here we define it as `A = [[A_11, A_10], [A_01, A_00]]`. For `B` we have `B = [B_0, B_1] -> B = [B_1, B_0]`. + """ + N = cov.shape[-1] // 2 + Q, beta = wigner_to_husimi(cov, means) + A = math.matmul( + cayley(pq_to_aadag(cov), c=0.5), math.Xmat(N) + ) # X on the right, so the index order will be rho_{left,right}: + B = math.solve(Q, beta) # no conjugate, so that the index order will be rho_{left,right} + C = math.exp(-0.5 * math.sum(math.conj(beta) * B)) / math.sqrt(math.det(Q)) + return A, B, C + + +def wigner_to_bargmann_psi(cov, means): + r"""Converts the wigner representation in terms of covariance matrix and mean vector into the Bargmann A,B,C triple + for a Hilbert vector (i.e. for M modes, A has shape M x M and B has shape M). + """ + N = cov.shape[-1] // 2 + A, B, C = wigner_to_bargmann_rho(cov, means) + # NOTE: with A_rho and B_rho defined with inverted blocks, we now keep the first half rather than the second + return A[:N, :N], B[:N], math.sqrt(C) + + +def wigner_to_bargmann_Choi(X, Y, d): + r"""Converts the wigner representation in terms of covariance matrix and mean vector into the Bargmann `A,B,C` triple + for a channel (i.e. for M modes, A has shape 4M x 4M and B has shape 4M). + We have freedom to choose the order of the indices of the Choi matrix by rearranging the `MxM` blocks of A and the M-subvectors of B. + Here we choose the order `[out_l, in_l out_r, in_r]` (`in_l` and `in_r` to be contracted with the left and right indices of the density matrix) + so that after the contraction the result has the right order `[out_l, out_r]`.""" + N = X.shape[-1] // 2 + I2 = math.eye(2 * N, dtype=X.dtype) + XT = math.transpose(X) + xi = 0.5 * (I2 + math.matmul(X, XT) + 2 * Y / settings.HBAR) + xi_inv = math.inv(xi) + A = math.block( + [ + [I2 - xi_inv, math.matmul(xi_inv, X)], + [math.matmul(XT, xi_inv), I2 - math.matmul(math.matmul(XT, xi_inv), X)], + ] + ) + I = math.eye(N, dtype="complex128") + o = math.zeros_like(I) + R = math.block( + [[I, 1j * I, o, o], [o, o, I, -1j * I], [I, -1j * I, o, o], [o, o, I, 1j * I]] + ) / np.sqrt(2) + A = math.matmul(math.matmul(R, A), math.dagger(R)) + A = math.matmul(A, math.Xmat(2 * N)) # yes: X on the right + b = math.matvec(xi_inv, d) + B = math.matvec(math.conj(R), math.concat([b, -math.matvec(XT, b)], axis=-1)) / math.sqrt( + settings.HBAR, dtype=R.dtype + ) + B = math.concat([B[2 * N :], B[: 2 * N]], axis=-1) # yes: opposite order + C = math.exp(-0.5 * math.sum(d * b) / settings.HBAR) / math.sqrt(math.det(xi), dtype=b.dtype) + # now A and B have order [out_l, in_l out_r, in_r]. + return A, B, C + + +def wigner_to_bargmann_U(X, d): + r"""Converts the wigner representation in terms of covariance matrix and mean vector into the Bargmann `A,B,C` triple + for a unitary (i.e. for `M` modes, `A` has shape `2M x 2M` and `B` has shape `2M`). + """ + N = X.shape[-1] // 2 + A, B, C = wigner_to_bargmann_Choi(X, math.zeros_like(X), d) + # NOTE: with A_Choi and B_Choi defined with inverted blocks, we now keep the first half rather than the second + return A[: 2 * N, : 2 * N], B[: 2 * N], math.sqrt(C) diff --git a/mrmustard/physics/fock.py b/mrmustard/physics/fock.py index cc325e314..0d58cffbc 100644 --- a/mrmustard/physics/fock.py +++ b/mrmustard/physics/fock.py @@ -21,6 +21,15 @@ from functools import lru_cache import numpy as np + +from mrmustard.physics.bargmann import ( + wigner_to_bargmann_psi, + wigner_to_bargmann_rho, + wigner_to_bargmann_Choi, + wigner_to_bargmann_U, +) + +from mrmustard.math.mmtensor import MMTensor from mrmustard.math.caching import tensor_int_cache from mrmustard.types import List, Tuple, Tensor, Scalar, Matrix, Sequence, Vector from mrmustard import settings @@ -28,7 +37,6 @@ math = Math() - # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ # ~~~~~~~~~~~~~~ static functions ~~~~~~~~~~~~~~ # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -72,45 +80,71 @@ def autocutoffs( return [int(n) for n in math.clip(autocutoffs, min_cutoff, max_cutoff)] -def fock_representation( +def wigner_to_fock_state( cov: Matrix, means: Vector, shape: Sequence[int], - return_dm: bool = None, - return_unitary: bool = None, - choi_r: float = None, + return_dm: bool = True, ) -> Tensor: - r"""Returns the Fock representation of a state or Choi state. + r"""Returns the Fock representation of a Gaussian state. + Use with caution: if the cov matrix is that of a mixed state, + setting return_dm to False will produce nonsense. - * If the state is pure it returns the state vector (ket). - * If the state is mixed it returns the density matrix. - * If the transformation is unitary it returns the unitary transformation matrix. - * If the transformation is not unitary it returns the Choi matrix. + * If the state is pure it can return the state vector (ket) or the density matrix. + The index order is going to be ket_i where i is the only multimode index. + * If the state is mixed it can return the density matrix. + The index order is going to be dm_ij where j is the right multimode index and i is the left one. Args: cov: the Wigner covariance matrix means: the Wigner means vector shape: the shape of the tensor - return_dm: whether the state vector is mixed or not - return_unitary: whether the transformation is unitary or not - choi_r: the TMSV squeezing magnitude + return_dm: whether to return the density matrix (otherwise it returns the ket) Returns: Tensor: the fock representation """ - if return_dm is not None and return_unitary is not None: - raise ValueError("Cannot specify both mixed and unitary.") - if return_dm is None and return_unitary is None: - raise ValueError("Must specify either mixed or unitary.") - if return_unitary is not None and choi_r is None: - raise ValueError("Must specify the choi_r value.") - if return_dm is not None: # i.e. it's a state - A, B, C = ABC(cov, means, full=return_dm) - elif return_unitary is not None and choi_r is not None: # i.e. it's a transformation - A, B, C = ABC(cov, means, full=not return_unitary, choi_r=choi_r) - return math.hermite_renormalized( - math.conj(-A), math.conj(B), math.conj(C), shape=shape - ) # NOTE: remove conj when TW is updated + if return_dm: + A, B, C = wigner_to_bargmann_rho(cov, means) + return math.hermite_renormalized(-A, B, C, shape=shape) + else: + A, B, C = wigner_to_bargmann_psi(cov, means) + return math.hermite_renormalized(-A, B, C, shape=shape) + + +def wigner_to_fock_U(X, d, shape): + r"""Returns the Fock representation of a Gaussian unitary transformation. + The index order is out_l, in_l, where in_l is to be contracted with the indices of a ket, + or with the left indices of a density matrix. + + Arguments: + X: the X matrix + d: the d vector + shape: the shape of the tensor + + Returns: + Tensor: the fock representation of the unitary transformation + """ + A, B, C = wigner_to_bargmann_U(X, d) + return math.hermite_renormalized(-A, B, C, shape=shape) + + +def wigner_to_fock_Choi(X, Y, d, shape): + r"""Returns the Fock representation of a Gaussian Choi matrix. + The order of choi indices is :math:`[\mathrm{out}_l, \mathrm{in}_l, \mathrm{out}_r, \mathrm{in}_r]` + where :math:`\mathrm{in}_l` and :math:`\mathrm{in}_r` are to be contracted with the left and right indices of a density matrix. + + Arguments: + X: the X matrix + Y: the Y matrix + d: the d vector + shape: the shape of the tensor + + Returns: + Tensor: the fock representation of the Choi matrix + """ + A, B, C = wigner_to_bargmann_Choi(X, Y, d) + return math.hermite_renormalized(-A, B, C, shape=shape) def ket_to_dm(ket: Tensor) -> Tensor: @@ -149,10 +183,10 @@ def dm_to_ket(dm: Tensor) -> Tensor: d = int(np.prod(cutoffs)) dm = math.reshape(dm, (d, d)) - _, eigvecs = math.eigh(dm) + eigvals, eigvecs = math.eigh(dm) # eigenvalues and related eigenvectors are sorted in non-decreasing order, - # meaning the associated eigvec to eigval 1 is stored last. - ket = eigvecs[:, -1] + # meaning the associated eigvec to largest eigval is stored last. + ket = eigvecs[:, -1] * math.sqrt(eigvals[-1]) ket = math.reshape(ket, cutoffs) return ket @@ -189,65 +223,10 @@ def U_to_choi(U: Tensor) -> Tensor: U: the unitary transformation Returns: - Tensor: the Choi tensor + Tensor: the Choi tensor. The index order is going to be :math:`[\mathrm{out}_l, \mathrm{in}_l, \mathrm{out}_r, \mathrm{in}_r]` + where :math:`\mathrm{in}_l` and :math:`\mathrm{in}_r` are to be contracted with the left and right indices of the density matrix. """ - cutoffs = U.shape[: len(U.shape) // 2] - N = len(cutoffs) - outer = math.outer(U, math.conj(U)) - return math.transpose( - outer, - list(range(0, N)) - + list(range(2 * N, 3 * N)) - + list(range(N, 2 * N)) - + list(range(3 * N, 4 * N)), - ) # NOTE: mode blocks 1 and 3 are at the end so we can tensordot dm with them - - -def ABC(cov, means, full: bool, choi_r: float = None) -> Tuple[Matrix, Vector, Scalar]: - r"""Returns the full-size ``A`` matrix, ``B`` vector and ``C`` scalar. - - Args: - cov: the Wigner covariance matrix - means: the Wigner means vector - full: whether to return the full-size ``A``, ``B`` and ``C`` or the half-size ``A``, ``B`` - and ``C`` - choi_r: the TMSV squeezing magnitude if not None we consider ABC of a Choi state - - Returns: - Tuple[Matrix, Vector, Scalar]: full-size ``A`` matrix, ``B`` vector and ``C`` scalar - """ - is_state = choi_r is None - N = cov.shape[-1] // 2 - R = math.rotmat(N) - sigma = math.matmul(math.matmul(R, cov / settings.HBAR), math.dagger(R)) - beta = math.matvec(R, means / math.sqrt(settings.HBAR, dtype=means.dtype)) - Q = sigma + 0.5 * math.eye(2 * N, dtype=sigma.dtype) # Husimi covariance matrix - Qinv = math.inv(Q) - A = math.matmul(math.Xmat(N), math.eye(2 * N, dtype=Qinv.dtype) - Qinv) - denom = math.sqrt(math.det(Q)) if is_state else math.sqrt(math.det(Q / np.cosh(choi_r))) - if full: - B = math.matvec(math.transpose(Qinv), math.conj(beta)) - exponent = -0.5 * math.sum(math.conj(beta)[:, None] * Qinv * beta[None, :]) - C = math.exp(exponent) / denom - else: - A = A[ - :N, :N - ] # TODO: find a way to compute the half-size A without computing the full-size A first - B = beta[N:] - math.matvec(A, beta[:N]) - exponent = -0.5 * math.sum(beta[:N] * B) - C = math.exp(exponent) / math.sqrt(denom) - if choi_r is not None: - ones = math.ones( - N // 2, dtype=A.dtype - ) # N//2 is the actual number of modes because of the choi trick - factor = 1.0 / np.tanh(choi_r) - if full: - rescaling = math.concat([ones, factor * ones, ones, factor * ones], axis=0) - else: - rescaling = math.concat([ones, factor * ones], axis=0) - A = rescaling[:, None] * rescaling[None, :] * A - B = rescaling * B - return A, B, C + return math.outer(U, math.conj(U)) def fidelity(state_a, state_b, a_ket: bool, b_ket: bool) -> Scalar: @@ -326,143 +305,200 @@ def purity(dm: Tensor) -> Scalar: cutoffs = dm.shape[: len(dm.shape) // 2] d = int(np.prod(cutoffs)) # combined cutoffs in all modes dm = math.reshape(dm, (d, d)) + dm = dm / math.trace(dm) # assumes all nonzero values are included in the density matrix return math.abs(math.sum(math.transpose(dm) * dm)) # tr(rho^2) -def apply_op_to_ket(op, ket, op_indices): - r"""Applies an operator to a ket in the sense - ket_abcde = sum_{ijk}U_abc,ijk ket_ijkde +def validate_contraction_indices(in_idx, out_idx, M, name): + r"""Validates the indices used for the contraction of a tensor.""" + if len(set(in_idx)) != len(in_idx): + raise ValueError(f"{name}_in_idx should not contain repeated indices.") + if len(set(out_idx)) != len(out_idx): + raise ValueError(f"{name}_out_idx should not contain repeated indices.") + if not set(range(M)).intersection(out_idx).issubset(set(in_idx)): + wrong_indices = set(range(M)).intersection(out_idx) - set(in_idx) + raise ValueError( + f"Indices {wrong_indices} in {name}_out_idx are trying to replace uncontracted indices." + ) + + +def apply_kraus_to_ket(kraus, ket, kraus_in_idx, kraus_out_idx): + r"""Applies a kraus operator to a ket. + It assumes that the ket is indexed as left_1, ..., left_n. + + The kraus op has indices that contract with the ket (kraus_in_idx) and indices that are left over (kraus_out_idx). + The final index order will be sorted (note that an index appearing in both kraus_in_idx and kraus_out_idx will replace the original index). Args: - op (array): the operator to be applied + kraus (array): the kraus operator to be applied ket (array): the ket to which the operator is applied - op_indices (list): the indices the operator acts on + kraus_in_idx (list of ints): the indices (counting from 0) of the kraus operator that contract with the ket + kraus_out_idx (list of ints): the indices (counting from 0) of the kraus operator that are leftover + Returns: - array: the resulting ket + array: the resulting ket with indices as kraus_out_idx + uncontracted ket indices """ - K = ket.ndim - N = op.ndim // 2 - op_ket = math.tensordot(ket, op, axes=[op_indices, list(range(N, 2 * N))]) - perm = list(range(K - N)) - for i, o in enumerate(op_indices): - perm.insert(o, K - N + i) - return math.transpose(op_ket, perm) + if not set(kraus_in_idx).issubset(range(ket.ndim)): + raise ValueError("kraus_in_idx should be a subset of the ket indices.") + # check that there are no repeated indices in kraus_in_idx and kraus_out_idx (separately) + validate_contraction_indices(kraus_in_idx, kraus_out_idx, ket.ndim, "kraus") + + ket = MMTensor(ket, axis_labels=[f"left_{i}" for i in range(ket.ndim)]) + kraus = MMTensor( + kraus, + axis_labels=[f"out_left_{i}" for i in kraus_out_idx] + [f"left_{i}" for i in kraus_in_idx], + ) + + # contract the operator with the ket. + # now the leftover indices are in the order kraus_out_idx + uncontracted ket indices + kraus_ket = kraus @ ket + + # sort kraus_ket.axis_labels by the int at the end of each label + new_axis_labels = sorted(kraus_ket.axis_labels, key=lambda x: int(x.split("_")[-1])) + + return kraus_ket.transpose(new_axis_labels).tensor -def apply_op_to_dm(op, dm, op_modes): - r"""Applies an operator to a density matrix in the sense - dm_abcd = sum_{ij}op_ai dm_ibkd dagger(op)_kc + +def apply_kraus_to_dm(kraus, dm, kraus_in_idx, kraus_out_idx=None): + r"""Applies a kraus operator to a density matrix. + It assumes that the density matrix is indexed as left_1, ..., left_n, right_1, ..., right_n. + + The kraus operator has indices that contract with the density matrix (kraus_in_idx) and indices that are leftover (kraus_out_idx). + `kraus` will contract from the left and from the right with the density matrix. For right contraction the kraus op is conjugated. Args: - op (array): the operator to be applied + kraus (array): the operator to be applied dm (array): the density matrix to which the operator is applied - op_modes (list): the modes the operator acts on (counting from 0) + kraus_in_idx (list of ints): the indices (counting from 0) of the kraus operator that contract with the density matrix + kraus_out_idx (list of ints): the indices (counting from 0) of the kraus operator that are leftover (default None, in which case kraus_out_idx = kraus_in_idx) Returns: array: the resulting density matrix """ - D = dm.ndim - N = len(op_modes) - op_dm = math.tensordot(dm, op, axes=[op_modes, np.arange(N, 2 * N)]) - # the N output indices of op are now at the end. We need to move them at op_modes - perm = list(range(D - N)) - for i, o in enumerate(op_modes): - perm.insert(o, D - N + i) - op_dm = math.transpose(op_dm, perm) - op_dm_op = math.tensordot( - op_dm, math.conj(op), axes=[[o + D // 2 for o in op_modes], np.arange(N, 2 * N)] + if kraus_out_idx is None: + kraus_out_idx = kraus_in_idx + + if not set(kraus_in_idx).issubset(range(dm.ndim // 2)): + raise ValueError("kraus_in_idx should be a subset of the density matrix indices.") + + # check that there are no repeated indices in kraus_in_idx and kraus_out_idx (separately) + validate_contraction_indices(kraus_in_idx, kraus_out_idx, dm.ndim // 2, "kraus") + + dm = MMTensor( + dm, + axis_labels=[f"left_{i}" for i in range(dm.ndim // 2)] + + [f"right_{i}" for i in range(dm.ndim // 2)], + ) + kraus = MMTensor( + kraus, + axis_labels=[f"out_left_{i}" for i in kraus_out_idx] + [f"left_{i}" for i in kraus_in_idx], ) - # the N output indices of op are now at the end. We need to move them at op_modes + D//2 - perm = list(range(D - N)) - for i, o in enumerate(op_modes): - perm.insert(o + D // 2, D - N + i) - return math.transpose(op_dm_op, perm) + kraus_conj = MMTensor( + math.conj(kraus.tensor), + axis_labels=[f"out_right_{i}" for i in kraus_out_idx] + + [f"right_{i}" for i in kraus_in_idx], + ) + + # contract the kraus operator with the density matrix from the left and from the right. + k_dm_k = kraus @ dm @ kraus_conj + # now the leftover indices are in the order: + # out_left_idx + uncontracted left indices + uncontracted right indices + out_right_idx + # sort k_dm_k.axis_labels by the int at the end of each label, first left, then right + N = k_dm_k.tensor.ndim // 2 + left = sorted(k_dm_k.axis_labels[:N], key=lambda x: int(x.split("_")[-1])) + right = sorted(k_dm_k.axis_labels[N:], key=lambda x: int(x.split("_")[-1])) -def CPTP(transformation, fock_state, transformation_is_unitary: bool, state_is_dm: bool) -> Tensor: - r"""Computes the CPTP (note: CP, really) channel given by a transformation (unitary matrix or choi operator) on a state. + return k_dm_k.transpose(left + right).tensor - It assumes that the cutoffs of the transformation matches the cutoffs of the relevant axes of the state. + +def apply_choi_to_dm(choi, dm, choi_in_idx, choi_out_idx): + r"""Applies a choi operator to a density matrix. + It assumes that the density matrix is indexed as left_1, ..., left_n, right_1, ..., right_n. + + The choi operator has indices that contract with the density matrix (choi_in_idx) and indices that are left over (choi_out_idx). + `choi` will contract choi_in_idx from the left and from the right with the density matrix. Args: - transformation: the transformation tensor - fock_state: the state to transform - transformation_is_unitary: whether the transformation is a unitary matrix or a Choi operator - state_is_dm: whether the state is a density matrix or a ket + choi (array): the choi operator to be applied + dm (array): the density matrix to which the choi operator is applied + choi_in_idx (list of ints): the indices of the choi operator that contract with the density matrix + choi_out_idx (list of ints): the indices of the choi operator that re leftover Returns: - Tensor: the transformed state + array: the resulting density matrix """ - num_modes = len(fock_state.shape) // 2 if state_is_dm else len(fock_state.shape) - N0 = list(range(0, num_modes)) - N1 = list(range(num_modes, 2 * num_modes)) - N2 = list(range(2 * num_modes, 3 * num_modes)) # pylint: disable=unused-variable - N3 = list(range(3 * num_modes, 4 * num_modes)) - if transformation_is_unitary: - U = transformation - Us = math.tensordot(U, fock_state, axes=(N1, N0)) - if not state_is_dm: - return Us - # is state is dm, the input indices of dm are still at the end of Us - return math.tensordot(Us, math.dagger(U), axes=(N1, N0)) - - # choi operator with indices in the order [out_r, in_r, out_l, in_l] - # note that the left and right indices of a dm have to contract with in_l and in_r - C = transformation - if state_is_dm: - output = math.tensordot(C, fock_state, axes=(N3 + N1, N0 + N1)) - # transpose because otherwise the output would be [out_r, out_l]: - return math.transpose(output, N1 + N0) - - # the order of the indices of a ket is just [out_l], which need to contract with in_l of the choi operator (N3) - Cs = math.tensordot(C, fock_state, axes=(N3, N0)) # now order is [out_r, in_r, out_l] - output = math.tensordot(Cs, math.conj(fock_state), axes=(N1, N0)) - return math.transpose(output, N1 + N0) # N2 is the last set of indices now + if not set(choi_in_idx).issubset(range(dm.ndim // 2)): + raise ValueError("choi_in_idx should be a subset of the density matrix indices.") + + # check that there are no repeated indices in kraus_in_idx and kraus_out_idx (separately) + validate_contraction_indices(choi_in_idx, choi_out_idx, dm.ndim // 2, "choi") + + dm = MMTensor( + dm, + axis_labels=[f"left_{i}" for i in range(dm.ndim // 2)] + + [f"right_{i}" for i in range(dm.ndim // 2)], + ) + choi = MMTensor( + choi, + axis_labels=[f"out_left_{i}" for i in choi_out_idx] + + [f"left_{i}" for i in choi_in_idx] + + [f"out_right_{i}" for i in choi_out_idx] + + [f"right_{i}" for i in choi_in_idx], + ) + + # contract the choi matrix with the density matrix. + # now the leftover indices are in the order out_left_idx + out_right_idx + uncontracted left indices + uncontracted right indices + choi_dm = choi @ dm + + # sort choi_dm.axis_labels by the int at the end of each label, first left, then right + N = choi_dm.tensor.ndim // 2 + left = sorted(choi_dm.axis_labels[:N], key=lambda x: int(x.split("_")[-1])) + right = sorted(choi_dm.axis_labels[N:], key=lambda x: int(x.split("_")[-1])) + + return choi_dm.transpose(left + right).tensor def contract_states( - stateA, stateB, a_is_mixed: bool, b_is_mixed: bool, modes: List[int], normalize: bool + stateA, stateB, a_is_dm: bool, b_is_dm: bool, modes: List[int], normalize: bool ): - r"""Contracts two states in the specified modes, it assumes that the modes spanned by ``B`` are a subset of the modes spanned by ``A``. + r"""Contracts two states in the specified modes. + Assumes that the modes of B are a subset of the modes of A. Args: stateA: the first state - stateB: the second state (assumed to be on a subset of the modes of stateA) - a_is_mixed: whether the first state is mixed or not. - b_is_mixed: whether the second state is mixed or not. + stateB: the second state + a_is_dm: whether the first state is a density matrix. + b_is_dm: whether the second state is a density matrix. modes: the modes on which to contract the states. normalize: whether to normalize the result Returns: Tensor: the contracted state tensor (subsystem of ``A``). Either ket or dm. """ - indices = list(range(len(modes))) - if not a_is_mixed and not b_is_mixed: - out = math.tensordot(math.conj(stateB), stateA, axes=(indices, modes)) - if normalize: - out = out / math.norm(out) - return out - - if a_is_mixed and not b_is_mixed: - Ab = math.tensordot( - stateA, stateB, axes=([m + len(stateA.shape) // 2 for m in modes], indices) - ) - out = math.tensordot(math.conj(stateB), Ab, axes=(indices, modes)) - elif not a_is_mixed and b_is_mixed: - Ba = math.tensordot(stateB, stateA, axes=(indices, modes)) # now B indices are all first - out = math.tensordot(math.conj(stateA), Ba, axes=(modes, indices)) - elif a_is_mixed and b_is_mixed: - out = math.tensordot( - stateA, - math.conj(stateB), - axes=( - list(modes) + [m + len(stateA.shape) // 2 for m in modes], - list(indices) + [i + len(stateB.shape) // 2 for i in indices], - ), - ) - if normalize: - out = out / math.sum(math.all_diagonals(out, real=False)) - return out + + if a_is_dm: + if b_is_dm: # a DM, b DM + dm = apply_choi_to_dm(choi=stateB, dm=stateA, choi_in_idx=modes, choi_out_idx=[]) + else: # a DM, b ket + dm = apply_kraus_to_dm( + kraus=math.conj(stateB), dm=stateA, kraus_in_idx=modes, kraus_out_idx=[] + ) + else: + if b_is_dm: # a ket, b DM + dm = apply_kraus_to_dm( + kraus=math.conj(stateA), dm=stateB, kraus_in_idx=modes, kraus_out_idx=[] + ) + else: # a ket, b ket + ket = apply_kraus_to_ket( + kraus=math.conj(stateB), ket=stateA, kraus_in_idx=modes, kraus_out_idx=[] + ) + + try: + return dm / math.sum(math.all_diagonals(dm, real=False)) if normalize else dm + except NameError: + return ket / math.norm(ket) if normalize else ket def normalize(fock: Tensor, is_dm: bool): @@ -511,15 +547,14 @@ def trace(dm, keep: List[int]): dm: the density matrix keep: the modes to keep (0-based) """ - N = len(dm.shape) // 2 - trace = [m for m in range(N) if m not in keep] - # put at the end all of the indices to trace over - keep_idx = keep + [i + N for i in keep] - trace_idx = trace + [i + N for i in trace] - dm = math.transpose(dm, keep_idx + trace_idx) - d = int(np.prod([dm.shape[t] for t in trace])) - dm = math.reshape(dm, dm.shape[: 2 * len(keep)] + (d, d)) - return math.trace(dm) + dm = MMTensor( + dm, + axis_labels=[ + f"out_{i}" if i in keep else f"contract_{i}" for i in range(len(dm.shape) // 2) + ] + + [f"in_{i}" if i in keep else f"contract_{i}" for i in range(len(dm.shape) // 2)], + ) + return dm.contract().tensor @tensor_int_cache diff --git a/mrmustard/physics/husimi.py b/mrmustard/physics/husimi.py new file mode 100644 index 000000000..f759c96f5 --- /dev/null +++ b/mrmustard/physics/husimi.py @@ -0,0 +1,44 @@ +# 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. + +# pylint: disable=redefined-outer-name + +""" +This module contains functions for transforming to the Husimi representation. +""" +from mrmustard import settings +from mrmustard.math import Math + +math = Math() + + +def pq_to_aadag(X): + r"""maps a matrix or vector from the q/p basis to the a/adagger basis""" + N = X.shape[0] // 2 + R = math.rotmat(N) + if X.ndim == 2: + return math.matmul(math.matmul(R, X / settings.HBAR), math.dagger(R)) + elif X.ndim == 1: + return math.matvec(R, X / math.sqrt(settings.HBAR, dtype=X.dtype)) + else: + raise ValueError("Input to complexify must be a matrix or vector") + + +def wigner_to_husimi(cov, means): + r"Returns the husimi complex covariance matrix and means vector." + N = cov.shape[-1] // 2 + sigma = pq_to_aadag(cov) + beta = pq_to_aadag(means) + Q = sigma + 0.5 * math.eye(2 * N, dtype=sigma.dtype) + return Q, beta diff --git a/tests/test_lab/test_circuit.py b/tests/test_lab/test_circuit.py index d49268ebe..b8e9954db 100644 --- a/tests/test_lab/test_circuit.py +++ b/tests/test_lab/test_circuit.py @@ -45,3 +45,11 @@ def test_circuit_placement_BS(): def test_circuit_placement_BSBS(): "tests that BSgates can be placed in any order" assert BSgate(1.0)[1, 2] >> BSgate(1.0)[0, 3] == BSgate(1.0)[0, 3] >> BSgate(1.0)[1, 2] + + +def test_is_unitary(): + "test that the is_unitary property is correct" + assert not (Ggate(1) >> Attenuator(0.1)).is_unitary + assert Ggate(1).is_unitary + assert (Ggate(1) >> Ggate(1)).is_unitary + assert not (Ggate(2) >> Attenuator([0.1, 0.2])).is_unitary diff --git a/tests/test_lab/test_detectors.py b/tests/test_lab/test_detectors.py index e7b24c714..a53c526ee 100644 --- a/tests/test_lab/test_detectors.py +++ b/tests/test_lab/test_detectors.py @@ -267,7 +267,7 @@ def test_homodyne_on_2mode_squeezed_vacuum_with_displacement(self, s, X, d): means = remaining_state.means.numpy() assert np.allclose(means, expected_means) - N_MEAS = 350 # number of homodyne measurements to perform + N_MEAS = 150 # number of homodyne measurements to perform NUM_STDS = 10.0 std_10 = NUM_STDS / np.sqrt(N_MEAS) diff --git a/tests/test_lab/test_gates_fock.py b/tests/test_lab/test_gates_fock.py index 20266bd03..ea04bbee1 100644 --- a/tests/test_lab/test_gates_fock.py +++ b/tests/test_lab/test_gates_fock.py @@ -24,7 +24,6 @@ from tests import random from mrmustard.physics import fock -from mrmustard import settings from mrmustard.lab.states import Fock, State, SqueezedVacuum, TMSV from mrmustard.physics import fock from mrmustard.lab.gates import ( @@ -109,14 +108,8 @@ def test_fock_representation_displacement(cutoffs, x, y): # compare with the standard way of calculating # transformation unitaries using the Choi isomorphism - choi_state = dgate.bell >> dgate - expected_Ud = fock.fock_representation( - choi_state.cov, - choi_state.means, - shape=cutoffs * 2, - return_unitary=True, - choi_r=settings.CHOI_R, - ) + X = np.eye(2 * len(cutoffs)) + expected_Ud = fock.wigner_to_fock_U(X, dgate.XYd[-1], cutoffs * 2) assert np.allclose(Ud, expected_Ud, atol=1e-5) @@ -172,15 +165,8 @@ def test_fock_representation_rgate(cutoffs, angles, modes): # compare with the standard way of calculating # transformation unitaries using the Choi isomorphism - choi_state = rgate.bell >> rgate - expected_R = fock.fock_representation( - choi_state.cov, - choi_state.means, - shape=cutoffs * 2, - return_unitary=True, - choi_r=settings.CHOI_R, - ) - + d = np.zeros(2 * len(cutoffs)) + expected_R = fock.wigner_to_fock_U(rgate.XYd[0], d, cutoffs * 2) assert np.allclose(R, expected_R, atol=1e-5) diff --git a/tests/test_math/test_mmtensor.py b/tests/test_math/test_mmtensor.py new file mode 100644 index 000000000..4c26ecebb --- /dev/null +++ b/tests/test_math/test_mmtensor.py @@ -0,0 +1,73 @@ +# 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. + +""" +Unit tests for the :class:`MMTensor`. +""" +import pytest +from mrmustard.math.mmtensor import MMTensor +from mrmustard.math import Math + +math = Math() +import numpy as np + + +def test_mmtensor_creation(): + """Test creation of MMTensor""" + array = np.array([[1, 2, 3]]) + mmtensor = MMTensor(array) + assert isinstance(mmtensor, MMTensor) + assert isinstance(mmtensor.tensor, np.ndarray) + assert mmtensor.axis_labels == ["0", "1"] + + +def test_mmtensor_creation_using_mmtensor(): + """Test creation of MMTensor using MMTensor""" + array = np.array([[1, 2, 3]]) + mmtensor = MMTensor(array) + mmtensor2 = MMTensor(mmtensor) + assert isinstance(mmtensor2, MMTensor) + assert mmtensor2.tensor is mmtensor.tensor + assert mmtensor2.axis_labels == ["0", "1"] + + +def test_mmtensor_creation_with_axis_labels(): + """Test creation of MMTensor with axis labels""" + array = np.array([[[1, 2, 3]]]) + mmtensor = MMTensor(array, axis_labels=["a", "b", "c"]) + assert isinstance(mmtensor, MMTensor) + assert isinstance(mmtensor.tensor, np.ndarray) + assert mmtensor.axis_labels == ["a", "b", "c"] + + +def test_mmtensor_creation_with_axis_labels_wrong_length(): + """Test creation of MMTensor with axis labels of wrong length""" + array = np.array([1, 2, 3]) + with pytest.raises(ValueError): + MMTensor(array, axis_labels=["a", "b"]) + + +def test_mmtensor_transposes_labels_too(): + """Test that MMTensor transposes axis labels""" + array = np.array([[1, 2, 3], [4, 5, 6]]) + mmtensor = MMTensor(array, axis_labels=["a", "b"]) + mmtensor = mmtensor.transpose([1, 0]) + assert mmtensor.axis_labels == ["b", "a"] + + +def test_mmtensor_contract(): + """Test that MMTensor contracts correctly""" + array = np.array([[1, 2], [3, 4]]) + trace = MMTensor(array, axis_labels=["a", "a"]).contract().tensor + assert trace == 5 diff --git a/tests/test_physics/test_fock/test_fock.py b/tests/test_physics/test_fock/test_fock.py index 7a867e81f..ac5cfba19 100644 --- a/tests/test_physics/test_fock/test_fock.py +++ b/tests/test_physics/test_fock/test_fock.py @@ -19,22 +19,24 @@ from scipy.special import factorial from thewalrus.quantum import total_photon_number_distribution from mrmustard.lab import ( - Vacuum, Circuit, + Vacuum, S2gate, BSgate, - Coherent, + Sgate, + Rgate, + Dgate, + Ggate, + Interferometer, SqueezedVacuum, + TMSV, + State, Attenuator, - Ggate, Fock, + Coherent, Gaussian, - Dgate, - Rgate, - TMSV, - State, ) -from mrmustard.physics.fock import dm_to_ket, ket_to_dm, trace +from mrmustard.physics.fock import dm_to_ket, ket_to_dm, trace, apply_choi_to_dm # helper strategies @@ -189,30 +191,54 @@ def test_dm_to_ket_error(): dm_to_ket(state) -def test_fock_trace_mode1(): - """tests that the Fock state is correctly traced out from mode 1""" - state = Vacuum(2) >> Ggate(2) +def test_fock_trace_mode1_dm(): + """tests that the Fock state is correctly traced out from mode 1 for mixed states""" + state = Vacuum(2) >> Ggate(2) >> Attenuator([0.1, 0.1]) + from_gaussian = state.get_modes(0).dm([3]) + from_fock = State(dm=state.dm([3, 30])).get_modes(0).dm([3]) + assert np.allclose(from_gaussian, from_fock, atol=1e-5) + + +def test_fock_trace_mode0_dm(): + """tests that the Fock state is correctly traced out from mode 0 for mixed states""" + state = Vacuum(2) >> Ggate(2) >> Attenuator([0.1, 0.1]) + from_gaussian = state.get_modes(1).dm([3]) + from_fock = State(dm=state.dm([30, 3])).get_modes(1).dm([3]) + assert np.allclose(from_gaussian, from_fock, atol=1e-5) + + +def test_fock_trace_mode1_ket(): + """tests that the Fock state is correctly traced out from mode 1 for pure states""" + state = Vacuum(2) >> Sgate(r=[0.1, 0.2], phi=[0.3, 0.4]) from_gaussian = state.get_modes(0).dm([3]) - from_fock = State(dm=state.dm([40])).get_modes(0).dm([3]) + from_fock = State(dm=state.dm([3, 30])).get_modes(0).dm([3]) assert np.allclose(from_gaussian, from_fock, atol=1e-5) -def test_fock_trace_mode0(): - """tests that the Fock state is correctly traced out from mode 0""" - state = Vacuum(2) >> Ggate(2) +def test_fock_trace_mode0_ket(): + """tests that the Fock state is correctly traced out from mode 0 for pure states""" + state = Vacuum(2) >> Sgate(r=[0.1, 0.2], phi=[0.3, 0.4]) from_gaussian = state.get_modes(1).dm([3]) - from_fock = State(dm=state.dm([40])).get_modes(1).dm([3]) + from_fock = State(dm=state.dm([30, 3])).get_modes(1).dm([3]) assert np.allclose(from_gaussian, from_fock, atol=1e-5) def test_fock_trace_function(): """tests that the Fock state is correctly traced""" - state = Vacuum(2) >> Ggate(2) - dm = state.dm([10, 10]) + state = Vacuum(2) >> Ggate(2) >> Attenuator([0.1, 0.1]) + dm = state.dm([3, 20]) dm_traced = trace(dm, keep=[0]) assert np.allclose(dm_traced, State(dm=dm).get_modes(0).dm(), atol=1e-5) +def test_dm_choi(): + """tests that choi op is correctly applied to a dm""" + circ = Ggate(1) >> Attenuator([0.1]) + dm_out = apply_choi_to_dm(circ.choi([10]), Vacuum(1).dm([10]), [0], [0]) + dm_expected = (Vacuum(1) >> circ).dm([10]) + assert np.allclose(dm_out, dm_expected, atol=1e-5) + + def test_single_mode_choi_application_order(): """Test dual operations output the correct mode ordering""" s = Attenuator(1.0) << State(dm=SqueezedVacuum(1.0, np.pi / 2).dm([40])) diff --git a/tests/test_physics/test_gaussian/test_symplectics.py b/tests/test_physics/test_gaussian/test_symplectics.py index 8afff20b3..09fa2dff2 100644 --- a/tests/test_physics/test_gaussian/test_symplectics.py +++ b/tests/test_physics/test_gaussian/test_symplectics.py @@ -13,11 +13,12 @@ # limitations under the License. import pytest -from hypothesis import settings, given, strategies as st +from hypothesis import given, strategies as st from thewalrus.symplectic import two_mode_squeezing, squeezing, rotation, beam_splitter, expand import numpy as np +from mrmustard import settings from mrmustard.lab.gates import ( Sgate, BSgate, @@ -134,10 +135,10 @@ def test_BSgate(theta, phi): @given(r=st.floats(0, 1), phi=st.floats(0, 2 * np.pi)) def test_S2gate(r, phi): """Tests the S2gate is implemented correctly by applying it on one half of a maximally entangled state""" - r_choi = np.arcsinh(1.0) + r_choi = settings.CHOI_R S2 = S2gate(r=r, phi=phi) - # bell = (TMSV(r_choi) & TMSV(r_choi)).get_modes([0, 2, 1, 3]) - cov = (S2.bell >> S2[0, 1]).cov + bell = (TMSV(r_choi) & TMSV(r_choi)).get_modes([0, 2, 1, 3]) + cov = (bell[0, 1, 2, 3] >> S2[0, 1]).cov expected = expand(two_mode_squeezing(2 * r_choi, 0.0), [0, 2], 4) @ expand( two_mode_squeezing(2 * r_choi, 0.0), [1, 3], 4 ) @@ -149,10 +150,10 @@ def test_S2gate(r, phi): @given(phi_ex=st.floats(0, 2 * np.pi), phi_in=st.floats(0, 2 * np.pi)) def test_MZgate_external_tms(phi_ex, phi_in): """Tests the MZgate is implemented correctly by applying it on one half of a maximally entangled state""" - r_choi = np.arcsinh(1.0) - # bell = (TMSV(r_choi) & TMSV(r_choi)).get_modes([0, 2, 1, 3]) + r_choi = settings.CHOI_R + bell = (TMSV(r_choi) & TMSV(r_choi)).get_modes([0, 2, 1, 3]) MZ = MZgate(phi_a=phi_ex, phi_b=phi_in, internal=False) - cov = (MZ.bell >> MZ[0, 1]).cov + cov = (bell[0, 1, 2, 3] >> MZ[0, 1]).cov bell = expand(two_mode_squeezing(2 * r_choi, 0.0), [0, 2], 4) @ expand( two_mode_squeezing(2 * r_choi, 0.0), [1, 3], 4 @@ -172,10 +173,10 @@ def test_MZgate_external_tms(phi_ex, phi_in): @given(phi_a=st.floats(0, 2 * np.pi), phi_b=st.floats(0, 2 * np.pi)) def test_MZgate_internal_tms(phi_a, phi_b): """Tests the MZgate is implemented correctly by applying it on one half of a maximally entangled state""" - r_choi = np.arcsinh(1.0) - # bell = (TMSV(r_choi) & TMSV(r_choi))[0, 2, 1, 3] + r_choi = settings.CHOI_R + bell = (TMSV(r_choi) & TMSV(r_choi)).get_modes([0, 2, 1, 3]) MZ = MZgate(phi_a=phi_a, phi_b=phi_b, internal=True) - cov = (MZ.bell >> MZ[0, 1]).cov + cov = (bell[0, 1, 2, 3] >> MZ[0, 1]).cov expected = expand(two_mode_squeezing(2 * r_choi, 0.0), [0, 2], 4) @ expand( two_mode_squeezing(2 * r_choi, 0.0), [1, 3], 4 ) diff --git a/tests/test_training/test_opt.py b/tests/test_training/test_opt.py index e027dadde..f72bd1dcd 100644 --- a/tests/test_training/test_opt.py +++ b/tests/test_training/test_opt.py @@ -346,4 +346,4 @@ def cost_fn_sympl(): opt = Optimizer(symplectic_lr=0.1, euclidean_lr=0.1) opt.minimize(cost_fn_sympl, by_optimizing=[S, r_angle]) - print(np.allclose(r_angle.numpy(), rotation_angle / 2, atol=1e-2)) + assert np.allclose(r_angle.numpy(), rotation_angle / 2, atol=1e-2) From 44650f0e1a1f273ca6285f8b682c9e17c2d0e42a Mon Sep 17 00:00:00 2001 From: ziofil Date: Tue, 17 Jan 2023 14:17:03 -0800 Subject: [PATCH 21/53] Bugfix axis labels (#190) **Context:** Fixes a few bugs related to transformation of states in Fock representation **Description of the Change:** - adds missing default argument - adds a function for convenience - fixes a bug due to old code not removed in `Transformation.transform_fock` - adds 11 tests to avoid same type of bug - adds 2 parametrized tests (10 in total) to check correct application of unitaries and channels to kets and dm in Fock representation **Benefits:** Correct code **Possible Drawbacks:** None **Related GitHub Issues:** No issue but FTB simulation not running on develop branch --- .github/CHANGELOG.md | 10 ++- mrmustard/lab/abstract/transformation.py | 15 +--- mrmustard/physics/fock.py | 68 +++++++++++++++-- tests/test_lab/test_gates_fock.py | 49 ++++++++++-- tests/test_physics/test_fock/test_fock.py | 90 ++++++++++++++++++++++- 5 files changed, 205 insertions(+), 27 deletions(-) diff --git a/.github/CHANGELOG.md b/.github/CHANGELOG.md index 3e4846295..b1881978d 100644 --- a/.github/CHANGELOG.md +++ b/.github/CHANGELOG.md @@ -69,9 +69,17 @@ within the defined window. Also, expose some plot parameters and return the figu * Allows for full cutoff specification (index-wise rather than mode-wise) for subclasses of `Transformation`. This allows for a more compact Fock representation where needed. [(#181)](https://github.com/XanaduAI/MrMustard/pull/181) -* Added two functions in the `fock` module to apply operators to ket and dm. When used by the circuit it avoids having to fall back to unitaries as large as the whole circuit. +* The `mrmustard.physics.fock` module now provides convenience functions for applying kraus operators and choi operators to kets and density matrices. [(#180)](https://github.com/XanaduAI/MrMustard/pull/180) + ```python + from mrmustard.physics.fock import apply_kraus_to_ket, apply_kraus_to_dm, apply_choi_to_ket, apply_choi_to_dm + ket_out = apply_kraus_to_ket(kraus, ket_in, indices) + dm_out = apply_choi_to_dm(choi, dm_in, indices) + dm_out = apply_kraus_to_dm(kraus, dm_in, indices) + dm_out = apply_choi_to_ket(choi, ket_in, indices) + ``` + * Replaced norm with probability in the repr of `State`. This improves consistency over the old behaviour (norm was the sqrt of prob if the state was pure and prob if the state was mixed). [(#182)](https://github.com/XanaduAI/MrMustard/pull/182) diff --git a/mrmustard/lab/abstract/transformation.py b/mrmustard/lab/abstract/transformation.py index 4c934f432..d0eed3eff 100644 --- a/mrmustard/lab/abstract/transformation.py +++ b/mrmustard/lab/abstract/transformation.py @@ -114,22 +114,11 @@ def transform_fock(self, state: State, dual: bool) -> State: N2 = list(range(2 * n, 3 * n)) N3 = list(range(3 * n, 4 * n)) if dual: - choi = math.transpose(choi, N1 + N3 + N0 + N2) # we flip out-in + choi = math.transpose(choi, N1 + N0 + N3 + N2) # we flip out-in if state.is_pure: - # applies choi to ket by applying a "kraus op" with no outgoing indices (ket) to a "dm" (choi) - # choi though has index order [out_l, in_l, out_r, in_r] - choi = ( - math.transpose(choi, N0 + N2 + N1 + N3) - if not dual - else math.transpose(choi, N1 + N3 + N0 + N2) - ) - # now choi looks like a proper dm with index order [out_l, out_r, in_l, in_r] # or l <-> r if dual return State( - dm=fock.apply_kraus_to_dm( - kraus=state.ket(), dm=choi, kraus_in_idx=op_idx, kraus_out_idx=[] - ), - modes=state.modes, + dm=fock.apply_choi_to_ket(choi, state.ket(), op_idx), modes=state.modes ) return State(dm=fock.apply_choi_to_dm(choi, state.dm(), op_idx), modes=state.modes) diff --git a/mrmustard/physics/fock.py b/mrmustard/physics/fock.py index 0d58cffbc..d06f8b080 100644 --- a/mrmustard/physics/fock.py +++ b/mrmustard/physics/fock.py @@ -322,7 +322,7 @@ def validate_contraction_indices(in_idx, out_idx, M, name): ) -def apply_kraus_to_ket(kraus, ket, kraus_in_idx, kraus_out_idx): +def apply_kraus_to_ket(kraus, ket, kraus_in_idx, kraus_out_idx=None): r"""Applies a kraus operator to a ket. It assumes that the ket is indexed as left_1, ..., left_n. @@ -338,6 +338,9 @@ def apply_kraus_to_ket(kraus, ket, kraus_in_idx, kraus_out_idx): Returns: array: the resulting ket with indices as kraus_out_idx + uncontracted ket indices """ + if kraus_out_idx is None: + kraus_out_idx = kraus_in_idx + if not set(kraus_in_idx).issubset(range(ket.ndim)): raise ValueError("kraus_in_idx should be a subset of the ket indices.") @@ -354,7 +357,8 @@ def apply_kraus_to_ket(kraus, ket, kraus_in_idx, kraus_out_idx): # now the leftover indices are in the order kraus_out_idx + uncontracted ket indices kraus_ket = kraus @ ket - # sort kraus_ket.axis_labels by the int at the end of each label + # sort kraus_ket.axis_labels by the int at the end of each label. + # Each label is guaranteed to have a unique int at the end. new_axis_labels = sorted(kraus_ket.axis_labels, key=lambda x: int(x.split("_")[-1])) return kraus_ket.transpose(new_axis_labels).tensor @@ -413,7 +417,7 @@ def apply_kraus_to_dm(kraus, dm, kraus_in_idx, kraus_out_idx=None): return k_dm_k.transpose(left + right).tensor -def apply_choi_to_dm(choi, dm, choi_in_idx, choi_out_idx): +def apply_choi_to_dm(choi, dm, choi_in_idx, choi_out_idx=None): r"""Applies a choi operator to a density matrix. It assumes that the density matrix is indexed as left_1, ..., left_n, right_1, ..., right_n. @@ -429,6 +433,9 @@ def apply_choi_to_dm(choi, dm, choi_in_idx, choi_out_idx): Returns: array: the resulting density matrix """ + if choi_out_idx is None: + choi_out_idx = choi_in_idx + if not set(choi_in_idx).issubset(range(dm.ndim // 2)): raise ValueError("choi_in_idx should be a subset of the density matrix indices.") @@ -453,13 +460,62 @@ def apply_choi_to_dm(choi, dm, choi_in_idx, choi_out_idx): choi_dm = choi @ dm # sort choi_dm.axis_labels by the int at the end of each label, first left, then right - N = choi_dm.tensor.ndim // 2 - left = sorted(choi_dm.axis_labels[:N], key=lambda x: int(x.split("_")[-1])) - right = sorted(choi_dm.axis_labels[N:], key=lambda x: int(x.split("_")[-1])) + left_labels = [label for label in choi_dm.axis_labels if "left" in label] + left = sorted(left_labels, key=lambda x: int(x.split("_")[-1])) + right_labels = [label for label in choi_dm.axis_labels if "right" in label] + right = sorted(right_labels, key=lambda x: int(x.split("_")[-1])) return choi_dm.transpose(left + right).tensor +def apply_choi_to_ket(choi, ket, choi_in_idx, choi_out_idx=None): + r"""Applies a choi operator to a ket. + It assumes that the ket is indexed as left_1, ..., left_n. + + The choi operator has indices that contract with the ket (choi_in_idx) and indices that are left over (choi_out_idx). + `choi` will contract choi_in_idx from the left and from the right with the ket. + + Args: + choi (array): the choi operator to be applied + ket (array): the ket to which the choi operator is applied + choi_in_idx (list of ints): the indices of the choi operator that contract with the ket + choi_out_idx (list of ints): the indices of the choi operator that re leftover + + Returns: + array: the resulting ket + """ + if choi_out_idx is None: + choi_out_idx = choi_in_idx + + if not set(choi_in_idx).issubset(range(ket.ndim)): + raise ValueError("choi_in_idx should be a subset of the ket indices.") + + # check that there are no repeated indices in kraus_in_idx and kraus_out_idx (separately) + validate_contraction_indices(choi_in_idx, choi_out_idx, ket.ndim, "choi") + + ket = MMTensor(ket, axis_labels=[f"left_{i}" for i in range(ket.ndim)]) + ket_dual = MMTensor(math.conj(ket.tensor), axis_labels=[f"right_{i}" for i in range(ket.ndim)]) + choi = MMTensor( + choi, + axis_labels=[f"out_left_{i}" for i in choi_out_idx] + + [f"left_{i}" for i in choi_in_idx] + + [f"out_right_{i}" for i in choi_out_idx] + + [f"right_{i}" for i in choi_in_idx], + ) + + # contract the choi matrix with the ket and its dual, like choi @ |ket>> Attenuator(0.5) fock_state = State(dm=gaussian_state.dm(cutoffs)) - via_fock_space_dm = (fock_state >> gate).dm(cutoffs).numpy() - via_phase_space_dm = (gaussian_state >> gate).dm(cutoffs).numpy() + via_fock_space_dm = (fock_state >> gate).dm(cutoffs) + via_phase_space_dm = (gaussian_state >> gate).dm(cutoffs) + assert np.allclose(via_fock_space_dm, via_phase_space_dm) + + +@pytest.mark.parametrize("gate", [Sgate(r=1), Dgate(0.3, 0.3), Pgate(10), Rgate(np.pi / 2)]) +def test_single_mode_fock_equals_gaussian_ket(gate): + """Test same state is obtained via fock representation or phase space + for single mode circuits.""" + cutoffs = [60] + gaussian_state = SqueezedVacuum(0.5) + fock_state = State(ket=gaussian_state.ket(cutoffs)) + + via_fock_space_ket = (fock_state >> gate).ket([10]) + via_phase_space_ket = (gaussian_state >> gate).ket([10]) + phase = np.exp(1j * np.angle(via_fock_space_ket[0])) + assert np.allclose(via_fock_space_ket, phase * via_phase_space_ket) + + +@pytest.mark.parametrize( + "gate", + [ + Sgate(r=0.5, phi=0.2) >> Attenuator(0.4), + Dgate(0.4, 0.4) >> Attenuator(0.4), + Pgate(1) >> Attenuator(0.4), + Rgate(np.pi / 2) >> Attenuator(0.4), + ], +) +def test_single_mode_fock_equals_gaussian_ket_dm(gate): + """Test same state is obtained via fock representation or phase space + for single mode circuits.""" + cutoffs = [60] + gaussian_state = SqueezedVacuum(0.5) + fock_state = State(ket=gaussian_state.ket(cutoffs)) + + via_fock_space_dm = (fock_state >> gate).dm([10]) + via_phase_space_dm = (gaussian_state >> gate).dm([10]) assert np.allclose(via_fock_space_dm, via_phase_space_dm) @@ -83,8 +120,8 @@ def test_two_mode_fock_equals_gaussian(gate): gaussian_state = TMSV(0.1) >> BSgate(np.pi / 2) >> Attenuator(0.5) fock_state = State(dm=gaussian_state.dm(cutoffs)) - via_fock_space_dm = (fock_state >> gate).dm(cutoffs).numpy() - via_phase_space_dm = (gaussian_state >> gate).dm(cutoffs).numpy() + via_fock_space_dm = (fock_state >> gate).dm(cutoffs) + via_phase_space_dm = (gaussian_state >> gate).dm(cutoffs) assert np.allclose(via_fock_space_dm, via_phase_space_dm) diff --git a/tests/test_physics/test_fock/test_fock.py b/tests/test_physics/test_fock/test_fock.py index ac5cfba19..998c8c87b 100644 --- a/tests/test_physics/test_fock/test_fock.py +++ b/tests/test_physics/test_fock/test_fock.py @@ -36,7 +36,15 @@ Coherent, Gaussian, ) -from mrmustard.physics.fock import dm_to_ket, ket_to_dm, trace, apply_choi_to_dm +from mrmustard.physics.fock import ( + dm_to_ket, + ket_to_dm, + trace, + apply_choi_to_dm, + apply_choi_to_ket, + apply_kraus_to_dm, + apply_kraus_to_ket, +) # helper strategies @@ -244,3 +252,83 @@ def test_single_mode_choi_application_order(): s = Attenuator(1.0) << State(dm=SqueezedVacuum(1.0, np.pi / 2).dm([40])) assert np.allclose(s.dm([10])[:10, :10], SqueezedVacuum(1.0, np.pi / 2).dm([10])) # NOTE: the [:10,:10] part is not necessary once PR #184 is merged + + +def test_apply_kraus_to_ket_1mode(): + """Test that Kraus operators are applied to a ket on the correct indices""" + ket = np.random.normal(size=(2, 3, 4)) + kraus = np.random.normal(size=(5, 3)) + ket_out = apply_kraus_to_ket(kraus, ket, [1], [1]) + assert ket_out.shape == (2, 5, 4) + + +def test_apply_kraus_to_ket_2mode(): + """Test that Kraus operators are applied to a ket on the correct indices""" + ket = np.random.normal(size=(2, 3, 4)) + kraus = np.random.normal(size=(5, 3, 4)) + ket_out = apply_kraus_to_ket(kraus, ket, [1, 2], [1]) + assert ket_out.shape == (2, 5) + + +def test_apply_kraus_to_ket_2mode_2(): + """Test that Kraus operators are applied to a ket on the correct indices""" + ket = np.random.normal(size=(2, 3)) + kraus = np.random.normal(size=(5, 4, 3)) + ket_out = apply_kraus_to_ket(kraus, ket, [1], [1, 2]) + assert ket_out.shape == (2, 5, 4) + + +def test_apply_kraus_to_dm_1mode(): + """Test that Kraus operators are applied to a dm on the correct indices""" + dm = np.random.normal(size=(2, 3, 2, 3)) + kraus = np.random.normal(size=(5, 3)) + dm_out = apply_kraus_to_dm(kraus, dm, [1], [1]) + assert dm_out.shape == (2, 5, 2, 5) + + +def test_apply_kraus_to_dm_2mode(): + """Test that Kraus operators are applied to a dm on the correct indices""" + dm = np.random.normal(size=(2, 3, 4, 2, 3, 4)) + kraus = np.random.normal(size=(5, 3, 4)) + dm_out = apply_kraus_to_dm(kraus, dm, [1, 2], [1]) + assert dm_out.shape == (2, 5, 2, 5) + + +def test_apply_kraus_to_dm_2mode_2(): + """Test that Kraus operators are applied to a dm on the correct indices""" + dm = np.random.normal(size=(2, 3, 4, 2, 3, 4)) + kraus = np.random.normal(size=(5, 6, 3)) + dm_out = apply_kraus_to_dm(kraus, dm, [1], [3, 1]) + assert dm_out.shape == (2, 6, 4, 5, 2, 6, 4, 5) + + +def test_apply_choi_to_ket_1mode(): + """Test that choi operators are applied to a ket on the correct indices""" + ket = np.random.normal(size=(3, 5)) + choi = np.random.normal(size=(4, 3, 4, 3)) # [out_l, in_l, out_r, in_r] + ket_out = apply_choi_to_ket(choi, ket, [0], [0]) + assert ket_out.shape == (4, 5, 4, 5) + + +def test_apply_choi_to_ket_2mode(): + """Test that choi operators are applied to a ket on the correct indices""" + ket = np.random.normal(size=(3, 5)) + choi = np.random.normal(size=(2, 3, 5, 2, 3, 5)) # [out_l, in_l, out_r, in_r] + ket_out = apply_choi_to_ket(choi, ket, [0, 1], [0]) + assert ket_out.shape == (2, 2) + + +def test_apply_choi_to_dm_1mode(): + """Test that choi operators are applied to a dm on the correct indices""" + dm = np.random.normal(size=(3, 5, 3, 5)) + choi = np.random.normal(size=(4, 3, 4, 3)) # [out_l, in_l, out_r, in_r] + dm_out = apply_choi_to_dm(choi, dm, [0], [0]) + assert dm_out.shape == (4, 5, 4, 5) + + +def test_apply_choi_to_dm_2mode(): + """Test that choi operators are applied to a dm on the correct indices""" + dm = np.random.normal(size=(4, 5, 4, 5)) + choi = np.random.normal(size=(2, 3, 5, 2, 3, 5)) # [out_l, in_l, out_r, in_r] + dm_out = apply_choi_to_dm(choi, dm, [1], [1, 2]) + assert dm_out.shape == (4, 2, 3, 4, 2, 3) From a9d6ddbaa631ba00f1405c9aa01d66f92b486290 Mon Sep 17 00:00:00 2001 From: ziofil Date: Fri, 20 Jan 2023 14:51:00 -0800 Subject: [PATCH 22/53] Bugfix typerror (#192) **Context:** Fixing a bug related to tensorflow not liking products of complex128 and float64 **Description of the Change:** cast where necessary **Benefits:** Code that runs **Possible Drawbacks:** None **Related GitHub Issues:** None --- .github/CHANGELOG.md | 2 +- mrmustard/physics/bargmann.py | 2 +- mrmustard/physics/fock.py | 9 +++++---- 3 files changed, 7 insertions(+), 6 deletions(-) diff --git a/.github/CHANGELOG.md b/.github/CHANGELOG.md index b1881978d..ca0eff90b 100644 --- a/.github/CHANGELOG.md +++ b/.github/CHANGELOG.md @@ -104,7 +104,7 @@ within the defined window. Also, expose some plot parameters and return the figu ### Documentation -### Contributors +### Contributors This release contains contributions from (in alphabetical order): [Sebastian Duque Mesa](https://github.com/sduquemesa), [Filippo Miatto](https://github.com/ziofil) diff --git a/mrmustard/physics/bargmann.py b/mrmustard/physics/bargmann.py index 6efb94a01..43b7738cb 100644 --- a/mrmustard/physics/bargmann.py +++ b/mrmustard/physics/bargmann.py @@ -101,7 +101,7 @@ def wigner_to_bargmann_Choi(X, Y, d): B = math.concat([B[2 * N :], B[: 2 * N]], axis=-1) # yes: opposite order C = math.exp(-0.5 * math.sum(d * b) / settings.HBAR) / math.sqrt(math.det(xi), dtype=b.dtype) # now A and B have order [out_l, in_l out_r, in_r]. - return A, B, C + return A, B, math.cast(C, "complex128") def wigner_to_bargmann_U(X, d): diff --git a/mrmustard/physics/fock.py b/mrmustard/physics/fock.py index d06f8b080..273d0b913 100644 --- a/mrmustard/physics/fock.py +++ b/mrmustard/physics/fock.py @@ -647,14 +647,15 @@ def oscillator_eigenstate(q: Vector, cutoff: int) -> Tensor: prefactor = (omega_over_hbar / np.pi) ** (1 / 4) * math.sqrt(2 ** (-math.arange(0, cutoff))) # Renormalized physicist hermite polys: Hn / sqrt(n!) - R = math.astensor(2 * np.ones([1, 1])) # to get the physicist polys + R = np.array([[2 + 0j]]) # to get the physicist polys def f_hermite_polys(xi): - return math.hermite_renormalized(R, 2 * math.astensor([xi]), 1, cutoff) + poly = math.hermite_renormalized(R, 2 * math.astensor([xi], "complex128"), 1 + 0j, cutoff) + return math.cast(poly, "float64") - hermite_polys = math.cast(math.map_fn(f_hermite_polys, x_tensor), "float64") + hermite_polys = math.map_fn(f_hermite_polys, x_tensor) - # wavefunction + # (real) wavefunction psi = math.exp(-(x_tensor**2 / 2)) * math.transpose(prefactor * hermite_polys) return psi From f856be72b35f97fc563706816fbb59ba35847569 Mon Sep 17 00:00:00 2001 From: ziofil Date: Thu, 2 Feb 2023 16:03:03 -0800 Subject: [PATCH 23/53] blacked (#197) **Context:** new black version, wants to make new changes **Description of the Change:** black all the files that need to be blacked **Benefits:** to bring the develop branch up to date so that all the PRs that are open don't have to **Possible Drawbacks:** None **Related GitHub Issues:** None --- mrmustard/__init__.py | 1 + mrmustard/lab/abstract/measurement.py | 1 - mrmustard/lab/abstract/state.py | 1 + mrmustard/lab/abstract/transformation.py | 2 -- mrmustard/lab/detectors.py | 7 +------ mrmustard/lab/gates.py | 2 +- mrmustard/math/math_interface.py | 1 + mrmustard/math/tensorflow.py | 1 + mrmustard/math/torch.py | 1 + mrmustard/physics/__init__.py | 1 + mrmustard/training/optimizer.py | 1 + mrmustard/training/parameter_update.py | 1 - mrmustard/training/parametrized.py | 2 -- mrmustard/utils/graphics.py | 1 + mrmustard/utils/xptensor.py | 1 - tests/test_fidelity.py | 1 - tests/test_lab/test_states.py | 1 - tests/test_physics/test_fock/test_fock.py | 3 ++- 18 files changed, 12 insertions(+), 17 deletions(-) diff --git a/mrmustard/__init__.py b/mrmustard/__init__.py index 55ebf5be3..d47244fc9 100644 --- a/mrmustard/__init__.py +++ b/mrmustard/__init__.py @@ -21,6 +21,7 @@ from ._version import __version__ + # pylint: disable=too-many-instance-attributes class Settings: """Settings class.""" diff --git a/mrmustard/lab/abstract/measurement.py b/mrmustard/lab/abstract/measurement.py index 50cbf2425..d4348fcc3 100644 --- a/mrmustard/lab/abstract/measurement.py +++ b/mrmustard/lab/abstract/measurement.py @@ -117,7 +117,6 @@ class FockMeasurement(Measurement): """ def __init__(self, outcome: Tensor, modes: Iterable[int], cutoffs: Iterable[int]) -> None: - self._cutoffs = cutoffs or [settings.PNR_INTERNAL_CUTOFF] * len(modes) super().__init__(outcome, modes) diff --git a/mrmustard/lab/abstract/state.py b/mrmustard/lab/abstract/state.py index 04d7d43dd..494d3ef4a 100644 --- a/mrmustard/lab/abstract/state.py +++ b/mrmustard/lab/abstract/state.py @@ -42,6 +42,7 @@ math = Math() + # pylint: disable=too-many-instance-attributes class State: r"""Base class for quantum states.""" diff --git a/mrmustard/lab/abstract/transformation.py b/mrmustard/lab/abstract/transformation.py index d0eed3eff..3f4cb0b2d 100644 --- a/mrmustard/lab/abstract/transformation.py +++ b/mrmustard/lab/abstract/transformation.py @@ -360,7 +360,6 @@ def __eq__(self, other): return True def __repr__(self): - class_name = self.__class__.__name__ modes = self.modes @@ -374,7 +373,6 @@ def __repr__(self): return f"{class_name}({params_str}, modes = {modes})".replace("\n", "") def __str__(self): - class_name = self.__class__.__name__ modes = self.modes return f"<{class_name} object at {hex(id(self))} acting on modes {modes}>" diff --git a/mrmustard/lab/detectors.py b/mrmustard/lab/detectors.py index 55dcbfcf7..10f738797 100644 --- a/mrmustard/lab/detectors.py +++ b/mrmustard/lab/detectors.py @@ -30,6 +30,7 @@ __all__ = ["PNRDetector", "ThresholdDetector", "Generaldyne", "Homodyne", "Heterodyne"] + # pylint: disable=no-member class PNRDetector(Parametrized, FockMeasurement): r"""Photon Number Resolving detector. @@ -234,7 +235,6 @@ class Generaldyne(Measurement): def __init__( self, state: State, outcome: Optional[Tensor] = None, modes: Optional[Iterable[int]] = None ) -> None: - if not state.is_gaussian: raise TypeError("Generaldyne measurement state must be Gaussian.") if outcome is not None and not outcome.shape == state.means.shape: @@ -264,7 +264,6 @@ def primal(self, other: State) -> Union[State, float]: return super().primal(other) def _measure_gaussian(self, other) -> Union[State, float]: - remaining_modes = list(set(other.modes) - set(self.modes)) outcome, prob, new_cov, new_means = gaussian.general_dyne( @@ -300,7 +299,6 @@ def __init__( y: Union[float, List[float]] = 0.0, modes: List[int] = None, ): - if (x is None) ^ (y is None): # XOR raise ValueError("Both `x` and `y` arguments should be defined or set to `None`.") @@ -339,7 +337,6 @@ def __init__( modes: Optional[List[int]] = None, r: Union[float, List[float]] = settings.HOMODYNE_SQUEEZING, ): - self.r = r self.quadrature_angle = math.atleast_1d(quadrature_angle, dtype="float64") @@ -366,7 +363,6 @@ def __init__( super().__init__(state=state, outcome=outcome, modes=modes) def _measure_gaussian(self, other) -> Union[State, float]: - # rotate modes to be measured to the Homodyne basis other >>= Rgate(-self.quadrature_angle, modes=self.modes) self.state >>= Rgate(-self.quadrature_angle, modes=self.modes) @@ -386,7 +382,6 @@ def _measure_gaussian(self, other) -> Union[State, float]: return out def _measure_fock(self, other) -> Union[State, float]: - if len(self.modes) > 1: raise NotImplementedError( "Multimode Homodyne sampling for Fock representation is not yet implemented." diff --git a/mrmustard/lab/gates.py b/mrmustard/lab/gates.py index b21adc155..4c868cb88 100644 --- a/mrmustard/lab/gates.py +++ b/mrmustard/lab/gates.py @@ -201,7 +201,6 @@ def X_matrix(self): return gaussian.rotation_symplectic(self.angle.value) def U(self, cutoffs: Sequence[int]): - angles = self.angle.value * math.ones(self.num_modes, dtype=self.angle.value.dtype) num_modes = len(cutoffs) @@ -609,6 +608,7 @@ def __repr__(self): # NON-UNITARY # ~~~~~~~~~~~~~ + # pylint: disable=no-member class Attenuator(Parametrized, Transformation): r"""The noisy attenuator channel. diff --git a/mrmustard/math/math_interface.py b/mrmustard/math/math_interface.py index d214767de..9e9e48568 100644 --- a/mrmustard/math/math_interface.py +++ b/mrmustard/math/math_interface.py @@ -36,6 +36,7 @@ Any, ) + # pylint: disable=too-many-public-methods class MathInterface(ABC): r"""The interface that all backends must implement.""" diff --git a/mrmustard/math/tensorflow.py b/mrmustard/math/tensorflow.py index fa4eb1c4b..661b5daa5 100644 --- a/mrmustard/math/tensorflow.py +++ b/mrmustard/math/tensorflow.py @@ -36,6 +36,7 @@ ) from .math_interface import MathInterface + # pylint: disable=too-many-public-methods,no-self-argument,arguments-differ class TFMath(MathInterface): r"""Tensorflow implemantion of the :class:`Math` interface.""" diff --git a/mrmustard/math/torch.py b/mrmustard/math/torch.py index 424b34c0e..7b8d0c0fa 100644 --- a/mrmustard/math/torch.py +++ b/mrmustard/math/torch.py @@ -31,6 +31,7 @@ from mrmustard.math.autocast import Autocast from .math_interface import MathInterface + # pylint: disable=too-many-public-methods,no-self-use class TorchMath(MathInterface): r"""Torch implemantion of the :class:`Math` interface.""" diff --git a/mrmustard/physics/__init__.py b/mrmustard/physics/__init__.py index 9942418d2..37ce555f0 100644 --- a/mrmustard/physics/__init__.py +++ b/mrmustard/physics/__init__.py @@ -23,6 +23,7 @@ from mrmustard.physics import fock, gaussian from mrmustard import settings + # pylint: disable=protected-access def fidelity(A, B) -> float: r"""Calculates the fidelity between two quantum states. diff --git a/mrmustard/training/optimizer.py b/mrmustard/training/optimizer.py index f65ecf472..2152f9161 100644 --- a/mrmustard/training/optimizer.py +++ b/mrmustard/training/optimizer.py @@ -29,6 +29,7 @@ __all__ = ["Optimizer"] + # pylint: disable=disallowed-name class Optimizer: r"""An optimizer for any parametrized object: it can optimize euclidean, orthogonal and symplectic parameters. diff --git a/mrmustard/training/parameter_update.py b/mrmustard/training/parameter_update.py index 569bff1eb..aff90926e 100644 --- a/mrmustard/training/parameter_update.py +++ b/mrmustard/training/parameter_update.py @@ -23,7 +23,6 @@ def update_symplectic(grads_and_vars: Sequence[Tuple[Tensor, Trainable]], symplectic_lr: float): - r"""Updates the symplectic parameters using the given symplectic gradients. Implemented from: Wang J, Sun H, Fiori S. A Riemannian-steepest-descent approach diff --git a/mrmustard/training/parametrized.py b/mrmustard/training/parametrized.py index 83fb35ecf..6ccd883a9 100644 --- a/mrmustard/training/parametrized.py +++ b/mrmustard/training/parametrized.py @@ -39,11 +39,9 @@ class Parametrized: """ def __init__(self, **kwargs): # NOTE: only kwargs so that we can use the arg names - owner = f"{self.__class__.__qualname__}" for name, value in kwargs.items(): - # filter out `{name}_trainable` or `{name}_bounds`` to become fields # of the class as those kwargs are used to define the variables if "_trainable" in name or "_bounds" in name: diff --git a/mrmustard/utils/graphics.py b/mrmustard/utils/graphics.py index 5d749cfd3..e3b98634d 100644 --- a/mrmustard/utils/graphics.py +++ b/mrmustard/utils/graphics.py @@ -23,6 +23,7 @@ from mrmustard.physics.fock import quadrature_distribution from .wigner import wigner_discretized + # pylint: disable=disallowed-name class Progressbar: "A spiffy loading bar to display the progress during an optimization." diff --git a/mrmustard/utils/xptensor.py b/mrmustard/utils/xptensor.py index 19722c66b..31074e57b 100644 --- a/mrmustard/utils/xptensor.py +++ b/mrmustard/utils/xptensor.py @@ -65,7 +65,6 @@ def __init__( isVector: bool, modes: Union[Tuple[List[int], List[int]], None], ): - self.like_0 = like_0 self.shape = ( None if tensor is None else tensor.shape[: len(tensor.shape) // 2] diff --git a/tests/test_fidelity.py b/tests/test_fidelity.py index b802f36a7..dd4929913 100644 --- a/tests/test_fidelity.py +++ b/tests/test_fidelity.py @@ -104,7 +104,6 @@ def test_fidelity_vac_to_displaced_squeezed(self, r, alpha, hbar): class TestMixedStates: - state1 = 1 / 2 * np.eye(2) state2 = 1 / 3 * np.ones((2, 2)) diff --git a/tests/test_lab/test_states.py b/tests/test_lab/test_states.py index caae48806..3dcd9542a 100644 --- a/tests/test_lab/test_states.py +++ b/tests/test_lab/test_states.py @@ -256,7 +256,6 @@ def test_concat_pure_states(pure): @pytest.mark.parametrize("n", ([1, 0, 0], [1, 1, 0], [0, 0, 1])) @pytest.mark.parametrize("cutoffs", ([2, 2, 2], [2, 3, 3], [3, 3, 2])) def test_ket_from_pure_dm(n, cutoffs): - # prepare a fock (pure) state fock_state = Fock(n=n, cutoffs=cutoffs) dm_fock = fock_state.dm() diff --git a/tests/test_physics/test_fock/test_fock.py b/tests/test_physics/test_fock/test_fock.py index 998c8c87b..a0a3596d2 100644 --- a/tests/test_physics/test_fock/test_fock.py +++ b/tests/test_physics/test_fock/test_fock.py @@ -93,7 +93,8 @@ def test_coherent_state(alpha): @given(r=st.floats(0, 2), phi=st_angle) def test_squeezed_state(r, phi): """Test that squeezed states have the correct photon number statistics - Note that we use the same sign with respect to SMSV in https://en.wikipedia.org/wiki/Squeezed_coherent_state""" + Note that we use the same sign with respect to SMSV in https://en.wikipedia.org/wiki/Squeezed_coherent_state + """ cutoff = 10 amps = SqueezedVacuum(r=r, phi=phi).ket(cutoffs=[cutoff]) assert np.allclose(amps[1::2], 0.0) From 8f17c23299187b87bfe93709952a9810ad4b3c42 Mon Sep 17 00:00:00 2001 From: zeyueN <48225584+zeyueN@users.noreply.github.com> Date: Wed, 8 Feb 2023 22:49:33 -0500 Subject: [PATCH 24/53] Adds ray-based distributed trainer. (#194) **Context:** Adds `map_trainer` as the interface for distributing optimization workflows using ray so that things can happen in parallel -- replacing the need for `for` loops. _Code here has been personally looked at by Trudeau._ **Description of the Change:** Demo notebook in recipes. Documentation page for the `trainer` module is added with some examples, which is also in the docstring of `map_trainer` so that it can be conveniently read in Jupyter directly with shift+tab. I've also had some weird problem with the unit tests (all passing locally) running on github action where some times it would just hang. I've tried removing my new tests one by one and adding them back again one by one, and it somehow worked at the end... I changed the testing workflow file with direct `pip install .[ray]` instead of building wheel first, which I don't think is the reason. Thanks Trudeau I guess? **Benefits:** fast and simple experimentation -> more research done. **Possible Drawbacks:** more interface to introduce **Related GitHub Issues:** --- .github/CHANGELOG.md | 45 +++ .github/workflows/tests.yml | 5 +- .pylintrc | 2 +- doc/code/training.rst | 1 + doc/code/training/trainer.rst | 7 + mrmustard/training/trainer.py | 441 ++++++++++++++++++++++++++++ setup.py | 5 + tests/test_training/test_trainer.py | 226 ++++++++++++++ 8 files changed, 729 insertions(+), 3 deletions(-) create mode 100644 doc/code/training/trainer.rst create mode 100644 mrmustard/training/trainer.py create mode 100644 tests/test_training/test_trainer.py diff --git a/.github/CHANGELOG.md b/.github/CHANGELOG.md index ca0eff90b..535717ed8 100644 --- a/.github/CHANGELOG.md +++ b/.github/CHANGELOG.md @@ -2,6 +2,51 @@ ### New features + * Ray-based distributed trainer is now added to `training.trainer`. It acts as a replacement for `for` loops and enables the parallelization of running many circuits as well as their optimizations. To install the extra dependencies: `pip install .[ray]`. + [(#194)](https://github.com/XanaduAI/MrMustard/pull/194) + + ```python + from mrmustard.lab import Vacuum, Dgate, Ggate + from mrmustard.physics import fidelity + from mrmustard.training.trainer import map_trainer + + def make_circ(x=0.): + return Ggate(num_modes=1, symplectic_trainable=True) >> Dgate(x=x, x_trainable=True, y_trainable=True) + + def cost_fn(circ=make_circ(0.1), y_targ=0.): + target = Gaussian(1) >> Dgate(-1.5, y_targ) + s = Vacuum(1) >> circ + return -fidelity(s, target) + + # Use case 0: Calculate the cost of a randomly initialized circuit 5 times without optimizing it. + results_0 = map_trainer( + cost_fn=cost_fn, + tasks=5, + ) + + # Use case 1: Run circuit optimization 5 times on randomly initialized circuits. + results_1 = map_trainer( + cost_fn=cost_fn, + device_factory=make_circ, + tasks=5, + max_steps=50, + symplectic_lr=0.05, + ) + + # Use case 2: Run circuit optimization 2 times on randomly initialized circuits with custom parameters. + results_2 = map_trainer( + cost_fn=cost_fn, + device_factory=make_circ, + tasks=[ + {'x': 0.1, 'euclidean_lr': 0.005, 'max_steps': 50, 'HBAR': 1.}, + {'x': -0.7, 'euclidean_lr': 0.1, 'max_steps': 2, 'HBAR': 2.}, + ], + y_targ=0.35, + symplectic_lr=0.05, + AUTOCUTOFF_MAX_CUTOFF=7, + ) + ``` + * Sampling for homodyne measurements is now integrated in Mr Mustard: when no measurement outcome value is specified by the user, a value is sampled from the reduced state probability distribution and the conditional state on the remaining modes is generated. diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 18a5ac9f5..8a62db5db 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -30,8 +30,9 @@ jobs: pip install -r requirements.txt pip install -r requirements-dev.txt pip install wheel pytest pytest-cov --upgrade - python setup.py bdist_wheel - pip install dist/*.whl + pip install .[ray] + # python setup.py bdist_wheel + # pip install dist/*.whl - name: Run tests run: python -m pytest tests --cov=mrmustard --cov-report=term-missing --cov-report=xml -p no:warnings --tb=native diff --git a/.pylintrc b/.pylintrc index baf0b4455..0aba4296b 100644 --- a/.pylintrc +++ b/.pylintrc @@ -28,4 +28,4 @@ ignored-classes=numpy,tensorflow,scipy,networkx,strawberryfields,thewalrus # can either give multiple identifier separated by comma (,) or put this option # multiple time (only on the command line, not in the configuration file where # it should appear only once). -disable=no-member,line-too-long,invalid-name,too-many-lines,redefined-builtin,too-many-locals,duplicate-code,too-many-arguments,too-few-public-methods,no-else-return +disable=no-member,line-too-long,invalid-name,too-many-lines,redefined-builtin,too-many-locals,duplicate-code,too-many-arguments,too-few-public-methods,no-else-return,isinstance-second-argument-not-valid-type diff --git a/doc/code/training.rst b/doc/code/training.rst index 5d69190c5..1ab03e1c2 100644 --- a/doc/code/training.rst +++ b/doc/code/training.rst @@ -7,6 +7,7 @@ mrmustard.training training/optimizer training/parameter training/parametrized + training/trainer .. currentmodule:: mrmustard.training diff --git a/doc/code/training/trainer.rst b/doc/code/training/trainer.rst new file mode 100644 index 000000000..c3069e1ea --- /dev/null +++ b/doc/code/training/trainer.rst @@ -0,0 +1,7 @@ +trainer +============ + +.. currentmodule:: mrmustard.training.trainer + +.. automodapi:: mrmustard.training.trainer + :no-heading: diff --git a/mrmustard/training/trainer.py b/mrmustard/training/trainer.py new file mode 100644 index 000000000..91096ba7d --- /dev/null +++ b/mrmustard/training/trainer.py @@ -0,0 +1,441 @@ +# Copyright 2022 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 the implementation of distributed training utilities for parallelized +optimization of MrMustard circuits/devices through the function :meth:`map_trainer`. + +This module requires extra dependencies, to install: + +.. code-block:: bash + + git clone https://github.com/XanaduAI/MrMustard + cd MrMustard + pip install -e .[ray] + + +User-provided Wrapper Functions +=============================== +To distribute your optimization workflow, two user-defined functions are needed for wrapping up user logic: + +* A `device_factory` that wraps around the logic for making your circuits/states to be optimized; it is expected to return a single, or list of, :class:`Circuit`(s). +* A `cost_fn` that takes the circuits made and additional keyword arguments and returns a backprop-able scalar cost. + +Separating the circuit-making logic from the cost calculation logic has the benefit of returning the optimized circuit in the result dict for further inspection. One can also pass extra `metric_fns` to directly extract info from the circuit. + + +Examples: +========= + +.. code-block:: + + from mrmustard.lab import Vacuum, Dgate, Ggate, Gaussian + from mrmustard.physics import fidelity + from mrmustard.training.trainer import map_trainer + + def make_circ(x=0.): + return Ggate(num_modes=1, symplectic_trainable=True) >> Dgate(x=x, x_trainable=True, y_trainable=True) + + def cost_fn(circ=make_circ(0.1), y_targ=0.): + target = Gaussian(1) >> Dgate(-1.5, y_targ) + s = Vacuum(1) >> circ + return -fidelity(s, target) + + # Use case 0: Calculate the cost of a randomly initialized circuit 5 times without optimizing it. + results_0 = map_trainer( + cost_fn=cost_fn, + tasks=5, + ) + + # Use case 1: Run circuit optimization 5 times on randomly initialized circuits. + results_1 = map_trainer( + cost_fn=cost_fn, + device_factory=make_circ, + tasks=5, + max_steps=50, + symplectic_lr=0.05, + ) + + # Use case 2: Run 2 sets of circuit optimization with custom parameters passed as list. + results_2 = map_trainer( + cost_fn=cost_fn, + device_factory=make_circ, + tasks=[ + {'x': 0.1, 'euclidean_lr': 0.005, 'max_steps': 50, 'HBAR': 1.}, + {'x': -0.7, 'euclidean_lr': 0.1, 'max_steps': 2, 'HBAR': 2.}, + ], + y_targ=0.35, + symplectic_lr=0.05, + AUTOCUTOFF_MAX_CUTOFF=7, + ) + + # Use case 3: Run 2 sets of circuit optimization with custom parameters passed as dict with extra metric functions for evaluating the final optimized circuit. + results_3 = map_trainer( + cost_fn=cost_fn, + device_factory=make_circ, + tasks={ + 'my-job': {'x': 0.1, 'euclidean_lr': 0.005, 'max_steps': 50}, + 'my-other-job': {'x': -0.7, 'euclidean_lr': 0.1, 'max_steps': 2}, + }, + y_targ=0.35, + symplectic_lr=0.05, + metric_fns={ + 'is_gaussian': lambda c: c.is_gaussian, + 'foo': lambda _: 17. + }, +) + + +""" + +from inspect import signature, Parameter +from functools import partial +from typing import Sequence, Mapping +import warnings +import numpy as np +from rich.progress import track +import mrmustard as mm +from .optimizer import Optimizer + + +def _apply_partial_cost(device, cost_fn, **kwargs): + """Helper partial cost fn maker.""" + if isinstance(device, Sequence): + cost_fn, kwargs = partial_pop(cost_fn, *device, **kwargs) + optimized = device + elif isinstance(device, Mapping): + cost_fn, kwargs = partial_pop(cost_fn, **device, **kwargs) + optimized = list(device.values()) + return cost_fn, kwargs, optimized + + +def train_device( + cost_fn, + device_factory=None, + metric_fns=None, + return_kwargs=True, + skip_opt=False, + tag=None, + **kwargs, +): + """A general and flexible training loop for circuit optimizations with configurations adjustable through kwargs. + + Args: + cost_fn (callable): The optimized cost function to be distributed. It's expected to accept the + output of `device_factory` as *args as well as user-defined **kwargs, and returns a scalar cost. + Its user-defined **kwargs will be passed from this function's **kwargs which must include all its + required arguments. + device_factory (callable): Function that (partially) takes `kwargs` and returns a device, or + list/dict of devices. If None, `cost_fn` will be assumed to take no positional argument (for + example, when device-making is contained in `cost_fn`). Defaults to None. + metric_fns (Union[Sequence[callable], Mapping[callable], callable]): Optional collection of functions that takes the + output of `device_factory` after optimization and returns arbitrary evaluation/information. + return_kwargs (bool): Whether to include input config `kwargs` in the output dict. Defualts to True. + skip_opt (bool): Whether to skip the optimization and directly calculate cost. + tag (str): Optional label of the training task associated with the `kwargs` to be included in the output dict. + kwargs: + Dict containing all arguments to any of the functions below: + - `cost_fn`: exluding the output of `device_factory`. + - `device_factory`: e.g. `x`, `r`, `theta`, etc. + - `Optimizer`: e.g. `euclidean_lr`. + - `Optimizer.minimize`: excluding `cost_fn` and `by_optimizing`, e.g. `max_steps`. + + Returns: + dict: A result dict summarizing the optimized circuit, cost, metrics and/or input configs. + + """ + + setting_updates, kwargs = update_pop(mm.settings, **kwargs) + + input_kwargs = kwargs.copy() if return_kwargs else {} + + device, kwargs = ( + curry_pop(device_factory, **kwargs) if callable(device_factory) else ([], kwargs) + ) + device = [device] if not isinstance(device, (Sequence, Mapping)) else device + + cost_fn, kwargs, optimized = _apply_partial_cost(device, cost_fn, **kwargs) + + opt = None + if optimized and not skip_opt: + opt, kwargs = curry_pop(Optimizer, **kwargs) + _, kwargs = curry_pop( + opt.minimize, **{"cost_fn": cost_fn, "by_optimizing": optimized}, **kwargs + ) + + if kwargs: + warnings.warn(f"Unused kwargs: {kwargs}") + + final_cost = cost_fn() + + results = { + "cost": np.array(final_cost).item(), + "device": device, + "optimizer": opt, + } + + if callable(metric_fns): + results["metrics"] = metric_fns(*device) + elif isinstance(metric_fns, Sequence): + results["metrics"] = [f(*device) for f in metric_fns if callable(f)] + elif isinstance(metric_fns, Mapping): + results = { + **results, + **{k: f(*device) for k, f in metric_fns.items() if callable(f)}, + } + + return { + **({"tag": tag} if tag is not None else {}), + **results, + **input_kwargs, + **setting_updates, + } + + +def _iter_futures(futures): + """Make ray futures iterable for easy passing to a progress bar. + Hacky: https://github.com/ray-project/ray/issues/5554 + """ + import ray # pylint: disable=import-outside-toplevel + + while futures: + done, futures = ray.wait(futures) + yield ray.get(done[0]) + + +def map_trainer(trainer=train_device, tasks=1, pbar=True, unblock=False, num_cpus=None, **kwargs): + """Maps multiple training tasks across multiple workers using `ray`. + + In practice, the most common use case is to ignore the keywords `trainer` (as it defaults to + :meth:`train_device`), `pbar`, `unblock`, etc. and just concentrate on `tasks` and `**kwargs` + which passes arguments to the wrapper functions that contain the task execution logic, as well + as the :class:`Optimizer` and its :meth:`Optimizer.minimize`. + + For example, with the default `trainer` :meth:`train_device`, two user-defined functions are used for wrapping up user logic: + + * A `device_factory` (optional) that wraps around the logic for making circuits/states to be optimized; it is expected to return a single, or list of, :class:`Circuit`(s). + + * A `cost_fn` (required) that takes the circuits made and additional keyword arguments and returns a backprop-able scalar cost. + + Refer to the `kwargs` section below for more available options. + + Args: + trainer (callable): The function containing the training loop to be distributed, whose + fixed arguments are to be passed by `**kwargs` and task-specific arguments iterated + through `tasks`. Provide only when custom evaluation/training logic is needed. + Defaults to :meth:`train_device`. + tasks (Union[int, Sequence, Mapping]): Number of repeats or collection of task-specific training + config arguments feeding into :meth:`train_device`. + Refer to `kwargs` below for the available options. + Defaults to 1 which runs `trainer` exactly once. + pbar (bool): Whether to show a progress bar, available only in blocking mode (i.e. `unblock==False`). Defaults to True. + unblock (bool): Whether to unblock the process and returns a getter function returning the available results. + Defaults to False. + num_cpus (int): Number of cpu workers to initialize ray. Defaults to the number of virtual cores. + kwargs: Additional arguments containing fixed training config kwargs feeding into `trainer`. + For the default `trainer` :meth:`train_device`, available options are: + - cost_fn (callable): + The optimized cost function to be distributed. It's expected to accept the + output of `device_factory` as *args as well as user-defined **kwargs, and returns a scalar cost. + Its user-defined **kwargs will be passed from this function's **kwargs which must include all its + required arguments. + - device_factory (callable): + Function that (partially) takes `kwargs` and returns a device, or + list/dict of devices. If None, `cost_fn` will be assumed to take no positional argument (for + example, when device-making is contained in `cost_fn`). Defaults to None. + - metric_fns (Union[Sequence[callable], Mapping[callable], callable]): + Optional collection of functions that takes the + output of `device_factory` after optimization and returns arbitrary evaluation/information. + - return_kwargs (bool): + Whether to include input config `kwargs` in the output dict. Defualts to True. + - skip_opt (bool): + Whether to skip the optimization and directly calculate cost. + - tag (str): + Optional label of the training task associated with the `kwargs` to be included in the output dict. + - any kwargs to `cost_fn`: exluding the output of `device_factory`. + - any kwargs to `device_factory`: e.g. `x`, `r`, `theta`, etc. + - any kwargs to `Optimizer`: e.g. `euclidean_lr`. + - any kwargs to `Optimizer.minimize`: excluding `cost_fn` and `by_optimizing`, e.g. `max_steps`. + + Returns + Union[List, Dict]: The collection of results from each training task. Returns + - a list if `tasks` is provided as an int or a list; or + - a dict with the same keys if `tasks` is provided as a dict. + + + Examples: + ========= + + .. code-block:: + + from mrmustard.lab import Vacuum, Dgate, Ggate, Gaussian + from mrmustard.physics import fidelity + from mrmustard.training.trainer import map_trainer + + def make_circ(x=0.): + return Ggate(num_modes=1, symplectic_trainable=True) >> Dgate(x=x, x_trainable=True, y_trainable=True) + + def cost_fn(circ=make_circ(0.1), y_targ=0.): + target = Gaussian(1) >> Dgate(-1.5, y_targ) + s = Vacuum(1) >> circ + return -fidelity(s, target) + + # Use case 0: Calculate the cost of a randomly initialized circuit 5 times without optimizing it. + results_0 = map_trainer( + cost_fn=cost_fn, + tasks=5, + ) + + # Use case 1: Run circuit optimization 5 times on randomly initialized circuits. + results_1 = map_trainer( + cost_fn=cost_fn, + device_factory=make_circ, + tasks=5, + max_steps=50, + symplectic_lr=0.05, + ) + + # Use case 2: Run 2 sets of circuit optimization with custom parameters passed as list. + results_2 = map_trainer( + cost_fn=cost_fn, + device_factory=make_circ, + tasks=[ + {'x': 0.1, 'euclidean_lr': 0.005, 'max_steps': 50, 'HBAR': 1.}, + {'x': -0.7, 'euclidean_lr': 0.1, 'max_steps': 2, 'HBAR': 2.}, + ], + y_targ=0.35, + symplectic_lr=0.05, + AUTOCUTOFF_MAX_CUTOFF=7, + ) + + # Use case 3: Run 2 sets of circuit optimization with custom parameters passed as dict with extra metric functions for evaluating the final optimized circuit. + results_3 = map_trainer( + cost_fn=cost_fn, + device_factory=make_circ, + tasks={ + 'my-job': {'x': 0.1, 'euclidean_lr': 0.005, 'max_steps': 50}, + 'my-other-job': {'x': -0.7, 'euclidean_lr': 0.1, 'max_steps': 2}, + }, + y_targ=0.35, + symplectic_lr=0.05, + metric_fns={ + 'is_gaussian': lambda c: c.is_gaussian, + 'foo': lambda _: 17. + }, + ) + """ + try: + import ray # pylint: disable=import-outside-toplevel + except ImportError as e: + raise ImportError( + "Failed to import `ray` which is an extra dependency. Please install with `pip install -e .[ray]`." + ) from e + + if not ray.is_initialized(): + ray.init(num_cpus=num_cpus) + + return_dict = False + if isinstance(tasks, int): + tasks = [{} for _ in range(tasks)] + elif isinstance(tasks, Mapping): + return_dict = True + tasks = [{"tag": tag, **task} for tag, task in tasks.items()] + + remote_trainer, kwargs = partial_pop( + ray.remote(trainer).remote, + **kwargs, + ) + + if isinstance(tasks, Sequence): + promises = [ + curry_pop( + remote_trainer, + **task_kwargs, + )[0] + for task_kwargs in tasks + if isinstance(task_kwargs, Mapping) + ] + else: + raise ValueError( + f"`tasks` is expected to be of type int, list, or dict. got {type(tasks)}: {tasks}" + ) + + if not unblock: + # blocks and wait till all tasks complete to return the end results. + if pbar: + results = list( + track( + _iter_futures(promises), + description=f"{len(promises)} tasks running...", + total=len(promises), + ) + ) + else: + results = ray.get(promises) + + if return_dict: + return {r["tag"]: r for r in results} + else: + return results + + else: + # does not block and returns a getter function that returns the available results so far. + def get_avail_results(): + results, running_tasks = ray.wait( # pylint: disable=unused-variable + promises, num_returns=len(promises) + ) + if return_dict: + return {r["tag"]: r for r in ray.get(results)} + else: + return ray.get(results) + + return get_avail_results + + +def kwargs_of(fn): + """Gets the kwarg signature of a callable.""" + params = signature(fn).parameters + kwarg_kinds = [Parameter.POSITIONAL_OR_KEYWORD, Parameter.KEYWORD_ONLY] + + keywords = [k for k, p in params.items() if p.kind in kwarg_kinds] + has_var_keyword = any(p.kind is Parameter.VAR_KEYWORD for p in params.values()) + + return keywords, has_var_keyword + + +def partial_pop(fn, *args, **kwargs): + """Partially applies known kwargs to fn and returns the rest.""" + keywords, has_var_keyword = kwargs_of(fn) + known_kwargs = {k: kwargs.pop(k) for k in set(kwargs).intersection(keywords)} + partial_fn = partial(fn, *args, **known_kwargs, **(kwargs if has_var_keyword else {})) + return partial_fn, kwargs + + +def curry_pop(fn, *args, **kwargs): + """A poor man's reader monad bind function.""" + partial_fn, kwargs = partial_pop(fn, *args, **kwargs) + return partial_fn(), kwargs + + +def update_pop(obj, **kwargs): + """Updates an object/dict while popping keys out and returns the updated dict and remaining kwargs.""" + updated = {} + if isinstance(obj, Mapping): + for k in set(kwargs).intersection(obj): + obj[k] = kwargs.pop(k) + updated[k] = obj[k] + else: + for k in set(kwargs).intersection(dir(obj)): + setattr(obj, k, kwargs.pop(k)) + updated[k] = getattr(obj, k) + return updated, kwargs diff --git a/setup.py b/setup.py index 12dc2acbd..bf547ca49 100644 --- a/setup.py +++ b/setup.py @@ -28,6 +28,10 @@ "matplotlib", ] +extra_requirements = { + "ray": ["ray[tune]", "scikit-optimize"], +} + info = { "name": "mrmustard", "version": version, @@ -38,6 +42,7 @@ "license": "Apache License 2.0", "packages": find_packages(where="."), "install_requires": requirements, + "extras_require": extra_requirements, "long_description": open("README.md", encoding="utf-8").read(), "long_description_content_type": "text/markdown", } diff --git a/tests/test_training/test_trainer.py b/tests/test_training/test_trainer.py new file mode 100644 index 000000000..f9012e48e --- /dev/null +++ b/tests/test_training/test_trainer.py @@ -0,0 +1,226 @@ +# Copyright 2022 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. + +"""Tests for the ray-based trainer.""" + +import sys + +from time import sleep +import pytest + +import numpy as np +from mrmustard.lab import Vacuum, Dgate, Ggate, Gaussian +from mrmustard.physics import fidelity +from mrmustard.training import Optimizer +from mrmustard.training.trainer import map_trainer, train_device, update_pop + + +@pytest.fixture(scope="function") +def wrappers(): + """Dummy wrappers tested.""" + + def make_circ(x=0.0, return_type=None): + circ = Ggate(num_modes=1, symplectic_trainable=True) >> Dgate( + x=x, x_trainable=True, y_trainable=True + ) + return ( + [circ] if return_type == "list" else {"circ": circ} if return_type == "dict" else circ + ) + + def cost_fn(circ=make_circ(0.1), y_targ=0.0): + target = Gaussian(1) >> Dgate(-0.1, y_targ) + s = Vacuum(1) >> circ + return -fidelity(s, target) + + return make_circ, cost_fn + + +@pytest.mark.parametrize( + "tasks", [5, [{"y_targ": 0.1}, {"y_targ": -0.2}], {"c0": {}, "c1": {"y_targ": 0.07}}] +) +@pytest.mark.parametrize("seed", [None, 42]) +def test_circ_cost(wrappers, tasks, seed): # pylint: disable=redefined-outer-name + """Test distributed cost calculations.""" + has_seed = isinstance(seed, int) + _, cost_fn = wrappers + results = map_trainer( + cost_fn=cost_fn, + tasks=tasks, + **({"SEED": seed} if has_seed else {}), + ) + + if isinstance(tasks, dict): + assert set(results.keys()) == set(tasks.keys()) + results = list(results.values()) + assert all(r["optimizer"] is None for r in results) + assert all(r["device"] == [] for r in results) + if has_seed and isinstance(tasks, int): + assert len(set(r["cost"] for r in results)) == 1 + else: + assert ( + len(set(r["cost"] for r in results)) + >= (tasks if isinstance(tasks, int) else len(tasks)) - 1 + ) + + +@pytest.mark.parametrize( + "tasks", [[{"x": 0.1}, {"y_targ": 0.2}], {"c0": {}, "c1": {"euclidean_lr": 0.02, "HBAR": 1.0}}] +) +@pytest.mark.parametrize( + "return_type", + [None, "dict"], +) +def test_circ_optimize(wrappers, tasks, return_type): # pylint: disable=redefined-outer-name + """Test distributed optimizations.""" + max_steps = 15 + make_circ, cost_fn = wrappers + results = map_trainer( + cost_fn=cost_fn, + device_factory=make_circ, + tasks=tasks, + max_steps=max_steps, + symplectic_lr=0.05, + return_type=return_type, + ) + + if isinstance(tasks, dict): + assert set(results.keys()) == set(tasks.keys()) + results = list(results.values()) + assert ( + len(set(r["cost"] for r in results)) + >= (tasks if isinstance(tasks, int) else len(tasks)) - 1 + ) + assert all(isinstance(r["optimizer"], Optimizer) for r in results) + assert all((r["optimizer"].opt_history) for r in results) + + # Check if optimization history is actually decreasing. + opt_history = np.array(results[0]["optimizer"].opt_history) + assert len(opt_history) == max_steps + 1 + assert opt_history[0] - opt_history[-1] > 1e-6 + assert (np.diff(opt_history) < 0).sum() >= 3 + + +@pytest.mark.parametrize( + "metric_fns", + [ + {"is_gaussian": lambda c: c.is_gaussian, "foo": lambda _: 17.0}, + [ + lambda c: c.modes, + len, + ], + lambda c: (Vacuum(1) >> c >> c >> c).fock_probabilities([5]), + ], +) +def test_circ_optimize_metrics(wrappers, metric_fns): # pylint: disable=redefined-outer-name + """Tests custom metric functions on final circuits.""" + make_circ, cost_fn = wrappers + + tasks = { + "my-job": {"x": 0.1, "euclidean_lr": 0.005, "max_steps": 20}, + "my-other-job": {"x": -0.7, "euclidean_lr": 0.1, "max_steps": 12}, + } + + results = map_trainer( + cost_fn=cost_fn, + device_factory=make_circ, + tasks=tasks, + y_targ=0.35, + symplectic_lr=0.05, + metric_fns=metric_fns, + return_list=True, + ) + + assert set(results.keys()) == set(tasks.keys()) + results = list(results.values()) + assert all(("metrics" in r or set(metric_fns.keys()).issubset(set(r.keys()))) for r in results) + assert ( + len(set(r["cost"] for r in results)) + >= (tasks if isinstance(tasks, int) else len(tasks)) - 1 + ) + assert all(isinstance(r["optimizer"], Optimizer) for r in results) + assert all((r["optimizer"].opt_history) for r in results) + + # Check if optimization history is actually decreasing. + opt_history = np.array(results[0]["optimizer"].opt_history) + assert opt_history[1] - opt_history[-1] > 1e-6 + + +def test_update_pop(): + """Test for coverage.""" + d = {"a": 3, "b": "foo"} + kwargs = {"b": "bar", "c": 22} + d1, kwargs = update_pop(d, **kwargs) + assert d1["b"] == "bar" + assert len(kwargs) == 1 + + +def test_no_ray(monkeypatch): + """Tests ray import error""" + monkeypatch.setitem(sys.modules, "ray", None) + with pytest.raises(ImportError, match="Failed to import `ray`"): + _ = map_trainer( + tasks=2, + ) + + +def test_invalid_tasks(): + """Tests unexpected tasks arg""" + with pytest.raises(ValueError, match="`tasks` is expected to be of type int, list, or dict."): + _ = map_trainer( + tasks=2.3, + ) + + +def test_warn_unused_kwargs(wrappers): # pylint: disable=redefined-outer-name + """Test warning of unused kwargs""" + _, cost_fn = wrappers + with pytest.warns(UserWarning, match="Unused kwargs:"): + results = train_device( + cost_fn=cost_fn, + foo="bar", + ) + assert len(results) >= 4 + assert isinstance(results["cost"], float) + + +def test_no_pbar(wrappers): # pylint: disable=redefined-outer-name + """Test turning off pregress bar""" + _, cost_fn = wrappers + results = map_trainer( + cost_fn=cost_fn, + tasks=2, + pbar=False, + ) + assert len(results) == 2 + + +@pytest.mark.parametrize("tasks", [2, {"c0": {}, "c1": {"y_targ": -0.7}}]) +def test_unblock(wrappers, tasks): # pylint: disable=redefined-outer-name + """Test unblock async mode""" + _, cost_fn = wrappers + result_getter = map_trainer( + cost_fn=cost_fn, + tasks=tasks, + unblock=True, + ) + assert callable(result_getter) + + sleep(0.2) + results = result_getter() + if len(results) <= (tasks if isinstance(tasks, int) else len(tasks)): + # safer on slower machines + sleep(1) + results = result_getter() + + assert len(results) == (tasks if isinstance(tasks, int) else len(tasks)) From b135bf75ca1ad25251e9e15ed14b5f65ea38ed5e Mon Sep 17 00:00:00 2001 From: ziofil Date: Fri, 10 Feb 2023 16:49:43 -0800 Subject: [PATCH 25/53] Circuit drawer (#196) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit **Context:** Circuit building is a bit obscure because there's no visual circuit representation **Description of the Change:** Adds a basic circuit drawer (adapted from pennylane). **Benefits:** Circuit visualizations **Possible Drawbacks:** Does not yet include initializations and measurements, but for this we need to refactor the `Circuit` class first. **Related GitHub Issues:** None --------- Co-authored-by: Sebastián Duque Mesa <675763+sduquemesa@users.noreply.github.com> --- .github/CHANGELOG.md | 41 +++++--- mrmustard/__init__.py | 15 +-- mrmustard/lab/abstract/transformation.py | 23 ++--- mrmustard/lab/circuit.py | 25 ++--- mrmustard/lab/gates.py | 27 +++++- mrmustard/training/parametrized.py | 45 ++++++++- mrmustard/utils/circdrawer.py | 107 +++++++++++++++++++++ tests/test_lab/test_detectors.py | 38 ++++---- tests/test_utils/test_circuitdrawer.py | 116 +++++++++++++++++++++++ 9 files changed, 367 insertions(+), 70 deletions(-) create mode 100644 mrmustard/utils/circdrawer.py create mode 100644 tests/test_utils/test_circuitdrawer.py diff --git a/.github/CHANGELOG.md b/.github/CHANGELOG.md index 535717ed8..25118e380 100644 --- a/.github/CHANGELOG.md +++ b/.github/CHANGELOG.md @@ -63,11 +63,14 @@ measurement_outcome = SqueezedVacuum(r=0.5) >> Homodyne() ``` - * The optimizer `minimize` method now accepts an optional callback function, which will be called at each step of the optimization and it will be passed the step number, the cost value, and the value of the trainable parameters. +* The optimizer `minimize` method now accepts an optional callback function, which will be called at each step + of the optimization and it will be passed the step number, the cost value, and the value of the trainable parameters. The result is added to the `callback_history` attribute of the optimizer. [(#175)](https://github.com/XanaduAI/MrMustard/pull/175) - * We introduce the tensor wrapper `MMTensor` (available in `math.mmtensor`) that allows for a very easy handling of tensor contractions. Internally MrMustard performs lots of tensor contractions and this wrapper allows one to label each index of a tensor and perform contractions using the `@` symbol as if it were a simple matrix multiplication (the indices with the same name get contracted). +* We introduce the tensor wrapper `MMTensor` (available in `math.mmtensor`) that allows for a very easy handling of tensor contractions. + Internally MrMustard performs lots of tensor contractions and this wrapper allows one to label each index of a tensor and perform + contractions using the `@` symbol as if it were a simple matrix multiplication (the indices with the same name get contracted). [(#185)](https://github.com/XanaduAI/MrMustard/pull/185) * the Math interface now supports linear system solving via `math.solve`. @@ -87,15 +90,24 @@ C.tensor # extract actual result ``` - * MrMustard's settings object (accessible via `from mrmustard import settings`) now supports `SEED` (an int). This will give reproducible results whenever randomness is involved. The seed is unset by default, and it can be unset again with `settings.SEED = None`. If one desires, the seeded random number generator is accessible directly via `settings.rng` (e.g. `settings.rng.normal()`). +* MrMustard's settings object (accessible via `from mrmustard import settings`) now supports `SEED` (an int). + This will give reproducible results whenever randomness is involved. The seed is unset by default, + and it can be unset again with `settings.SEED = None`. If one desires, + the seeded random number generator is accessible directly via `settings.rng` (e.g. `settings.rng.normal()`). [(#183)](https://github.com/XanaduAI/MrMustard/pull/183) +* The `Circuit` class now has an ascii representation, which can be accessed via the repr method. + It looks great in Jupyter notebooks! There is a new option at `settings.CIRCUIT_DECIMALS` which controls + the number of decimals shown in the ascii representation. If None only the name of the gate is shown. + [(#196)](https://github.com/XanaduAI/MrMustard/pull/196) + ### Breaking changes ### Improvements -* The `Dgate` now uses The Walrus to calculate the unitary and gradients of the displacement gate in fock representation, providing better numerical stability for larger cutoff and displacement values. +* The `Dgate` now uses The Walrus to calculate the unitary and gradients of the displacement gate in fock representation, +providing better numerical stability for larger cutoff and displacement values. [(#147)](https://github.com/XanaduAI/MrMustard/pull/147) * Now the Wigner function is implemented in its own module and uses numba for speed. @@ -111,10 +123,12 @@ physical even though the Wigner function might not contain all the features of t within the defined window. Also, expose some plot parameters and return the figure and axes. [(#179)](https://github.com/XanaduAI/MrMustard/pull/179) -* Allows for full cutoff specification (index-wise rather than mode-wise) for subclasses of `Transformation`. This allows for a more compact Fock representation where needed. +* Allows for full cutoff specification (index-wise rather than mode-wise) for subclasses of `Transformation`. +This allows for a more compact Fock representation where needed. [(#181)](https://github.com/XanaduAI/MrMustard/pull/181) -* The `mrmustard.physics.fock` module now provides convenience functions for applying kraus operators and choi operators to kets and density matrices. +* The `mrmustard.physics.fock` module now provides convenience functions for applying kraus operators and +choi operators to kets and density matrices. [(#180)](https://github.com/XanaduAI/MrMustard/pull/180) ```python @@ -125,17 +139,21 @@ within the defined window. Also, expose some plot parameters and return the figu dm_out = apply_choi_to_ket(choi, ket_in, indices) ``` -* Replaced norm with probability in the repr of `State`. This improves consistency over the old behaviour (norm was the sqrt of prob if the state was pure and prob if the state was mixed). +* Replaced norm with probability in the repr of `State`. This improves consistency over the old behaviour +(norm was the sqrt of prob if the state was pure and prob if the state was mixed). [(#182)](https://github.com/XanaduAI/MrMustard/pull/182) -* Added two new modules (`physics.bargmann` and `physics.husimi`) to host the functions related to those representation, which have been refactored and moved out of `physics.fock`. +* Added two new modules (`physics.bargmann` and `physics.husimi`) to host the functions related to those representation, +which have been refactored and moved out of `physics.fock`. [(#185)](https://github.com/XanaduAI/MrMustard/pull/185) ### Bug fixes -* The `Dgate` and the `Rgate` now correctly parse the case when a single scalar is intended as the same parameter of a number of gates in pallel. +* The `Dgate` and the `Rgate` now correctly parse the case when a single scalar is intended as the same parameter +of a number of gates in pallel. [(#180)](https://github.com/XanaduAI/MrMustard/pull/180) -* The trace function in the fock module was giving incorrect results when called with certain choices of modes. This is now fixed. +* The trace function in the fock module was giving incorrect results when called with certain choices of modes. +This is now fixed. [(#180)](https://github.com/XanaduAI/MrMustard/pull/180) * The purity function for fock states no longer normalizes the density matrix before computing the purity. @@ -144,7 +162,8 @@ within the defined window. Also, expose some plot parameters and return the figu * The function `dm_to_ket` no longer normalizes the density matrix before diagonalizing it. [(#180)](https://github.com/XanaduAI/MrMustard/pull/180) -* The internal fock representation of states returns the correct cutoffs in all cases (solves an issue when a pure dm was converted to ket). +* The internal fock representation of states returns the correct cutoffs in all cases (solves an issue when +a pure dm was converted to ket). [(#184)](https://github.com/XanaduAI/MrMustard/pull/184) ### Documentation diff --git a/mrmustard/__init__.py b/mrmustard/__init__.py index d47244fc9..6d8678aaf 100644 --- a/mrmustard/__init__.py +++ b/mrmustard/__init__.py @@ -15,7 +15,6 @@ """This is the top-most `__init__.py` file of MrMustard package.""" import numpy as np - import rich.table from rich import print @@ -40,6 +39,7 @@ def __init__(self): self.AUTOCUTOFF_STDEV_FACTOR = 5 self.AUTOCUTOFF_MAX_CUTOFF = 100 self.AUTOCUTOFF_MIN_CUTOFF = 1 + self.CIRCUIT_DECIMALS = 3 # using cutoff=5 for each mode when determining if two transformations in fock repr are equal self.EQ_TRANSFORMATION_CUTOFF = 5 self.EQ_TRANSFORMATION_RTOL_FOCK = 1e-3 @@ -120,7 +120,7 @@ def about(): >>> mm.about() Mr Mustard: a differentiable bridge between phase space and Fock space. - Copyright 2018-2021 Xanadu Quantum Technologies Inc. + Copyright 2021 Xanadu Quantum Technologies Inc. Python version: 3.6.10 Platform info: Linux-5.8.18-1-MANJARO-x86_64-with-arch-Manjaro-Linux @@ -134,18 +134,19 @@ def about(): Torch version: 1.10.0+cu102 """ # pylint: disable=import-outside-toplevel - import sys - import platform import os - import numpy + import platform + import sys + import numba + import numpy import scipy - import thewalrus import tensorflow + import thewalrus # a QuTiP-style infobox print("\nMr Mustard: a differentiable bridge between phase space and Fock space.") - print("Copyright 2018-2021 Xanadu Quantum Technologies Inc.\n") + print("Copyright 2021 Xanadu Quantum Technologies Inc.\n") print("Python version: {}.{}.{}".format(*sys.version_info[0:3])) print("Platform info: {}".format(platform.platform())) diff --git a/mrmustard/lab/abstract/transformation.py b/mrmustard/lab/abstract/transformation.py index 3f4cb0b2d..81e1c397c 100644 --- a/mrmustard/lab/abstract/transformation.py +++ b/mrmustard/lab/abstract/transformation.py @@ -18,21 +18,22 @@ import numpy as np -from mrmustard.physics import gaussian, fock +from mrmustard import settings +from mrmustard.math import Math +from mrmustard.physics import fock, gaussian +from mrmustard.training.parameter import Parameter from mrmustard.types import ( - Sequence, - List, - Tuple, - Optional, - Matrix, - Vector, Callable, Iterable, + List, + Matrix, + Optional, + Sequence, + Tuple, Union, + Vector, ) -from mrmustard import settings -from mrmustard.math import Math -from mrmustard.training.parameter import Parameter + from .state import State math = Math() @@ -267,7 +268,7 @@ def __rshift__(self, other: Transformation): """ from ..circuit import ( Circuit, - ) # WARNING - circular import: this is called at runtime so it's ok + ) ops1 = self._ops if isinstance(self, Circuit) else [self] ops2 = other._ops if isinstance(other, Circuit) else [other] diff --git a/mrmustard/lab/circuit.py b/mrmustard/lab/circuit.py index e4a57e6cb..b8f4c956f 100644 --- a/mrmustard/lab/circuit.py +++ b/mrmustard/lab/circuit.py @@ -21,11 +21,14 @@ __all__ = ["Circuit"] -from typing import List, Tuple, Optional -from mrmustard.types import Matrix, Vector +from typing import List, Optional, Tuple + +from mrmustard import settings +from mrmustard.lab.abstract import State, Transformation from mrmustard.training import Parametrized +from mrmustard.types import Matrix, Vector +from mrmustard.utils.circdrawer import circuit_text from mrmustard.utils.xptensor import XPMatrix, XPVector -from .abstract import Transformation, State class Circuit(Transformation, Parametrized): @@ -63,7 +66,7 @@ def dual(self, state: State) -> State: @property def XYd( self, - ) -> Tuple[Matrix, Matrix, Vector]: # NOTE: Overriding Transformation.XYd for efficiency + ) -> Tuple[Matrix, Matrix, Vector]: # NOTE: Overriding Transformation.XYd for efficiency. X = XPMatrix(like_1=True) Y = XPMatrix(like_0=True) d = XPVector() @@ -96,17 +99,15 @@ def is_unitary(self): def __len__(self): return len(self._ops) - def _repr_markdown_(self) -> str: - """Markdown string to display the object on ipython notebooks.""" - header = f"#### Circuit - {len(self._ops)} ops - compiled = `{self._compiled}`\n\n" - ops_repr = [op._repr_markdown_() for op in self._ops] # pylint: disable=protected-access - return header + "\n".join(ops_repr) + _repr_markdown_ = None def __repr__(self) -> str: """String to display the object on the command line.""" - ops_repr = [repr(op) for op in self._ops] - return " >> ".join(ops_repr) + # ops_repr = [repr(op) for op in self._ops] + # return " >> ".join(ops_repr) + return circuit_text(self._ops, decimals=settings.CIRCUIT_DECIMALS) def __str__(self): """String representation of the circuit.""" - return f"< Circuit | {len(self._ops)} ops | compiled = {self._compiled} >" + ops_repr = [repr(op) for op in self._ops] + return " >> ".join(ops_repr) diff --git a/mrmustard/lab/gates.py b/mrmustard/lab/gates.py index 4c868cb88..14c0815ce 100644 --- a/mrmustard/lab/gates.py +++ b/mrmustard/lab/gates.py @@ -18,13 +18,15 @@ This module defines gates and operations that can be applied to quantum modes to construct a quantum circuit. """ -from typing import Union, Optional, List, Tuple, Sequence -from mrmustard.types import Tensor +from typing import List, Optional, Sequence, Tuple, Union + from mrmustard import settings -from mrmustard.training import Parametrized -from mrmustard.physics import gaussian from mrmustard.math import Math -from .abstract import Transformation +from mrmustard.physics import gaussian +from mrmustard.training import Parametrized +from mrmustard.types import Tensor + +from mrmustard.lab.abstract import Transformation math = Math() @@ -87,6 +89,7 @@ def __init__( ) self._modes = modes self.is_gaussian = True + self.short_name = "D" @property def d_vector(self): @@ -157,6 +160,7 @@ def __init__( ) self._modes = modes self.is_gaussian = True + self.short_name = "S" @property def X_matrix(self): @@ -195,6 +199,7 @@ def __init__( ) self._modes = modes self.is_gaussian = True + self.short_name = "R" @property def X_matrix(self): @@ -251,6 +256,7 @@ def __init__( ) self._modes = modes self.is_gaussian = True + self.short_name = "P" @property def X_matrix(self): @@ -284,6 +290,7 @@ def __init__( ) self._modes = modes self.is_gaussian = True + self.short_name = "CX" @property def X_matrix(self): @@ -317,6 +324,7 @@ def __init__( ) self._modes = modes self.is_gaussian = True + self.short_name = "CZ" @property def X_matrix(self): @@ -359,6 +367,7 @@ def __init__( ) self._modes = modes self.is_gaussian = True + self.short_name = "BS" @property def X_matrix(self): @@ -413,6 +422,7 @@ def __init__( self._internal = internal self._modes = modes self.is_gaussian = True + self.short_name = "MZ" @property def X_matrix(self): @@ -460,6 +470,7 @@ def __init__( ) self._modes = modes self.is_gaussian = True + self.short_name = "S2" @property def X_matrix(self): @@ -502,6 +513,7 @@ def __init__( ) self._modes = modes or list(range(num_modes)) self.is_gaussian = True + self.short_name = "I" @property def X_matrix(self): @@ -540,6 +552,7 @@ def __init__( super().__init__(orthogonal=orthogonal, orthogonal_trainable=orthogonal_trainable) self._modes = list(range(num_modes)) self._is_gaussian = True + self.short_name = "RI" @property def X_matrix(self): @@ -587,6 +600,7 @@ def __init__( ) self._modes = list(range(num_modes)) self.is_gaussian = True + self.short_name = "G" @property def X_matrix(self): @@ -661,6 +675,7 @@ def __init__( self._modes = modes self.is_unitary = False self.is_gaussian = True + self.short_name = "Att" @property def X_matrix(self): @@ -717,6 +732,7 @@ def __init__( self._modes = modes self.is_unitary = False self.is_gaussian = True + self.short_name = "Amp" @property def X_matrix(self): @@ -770,6 +786,7 @@ def __init__( self._modes = modes self.is_unitary = False self.is_gaussian = True + self.short_name = "Add" @property def Y_matrix(self): diff --git a/mrmustard/training/parametrized.py b/mrmustard/training/parametrized.py index 6ccd883a9..94ac56c4f 100644 --- a/mrmustard/training/parametrized.py +++ b/mrmustard/training/parametrized.py @@ -18,9 +18,16 @@ class constructor generate a backend Tensor and are assigned to fields of the class. """ -from typing import Sequence, List, Generator, Any +from typing import Any, Generator, List, Sequence, Tuple + from mrmustard.math import Math -from .parameter import create_parameter, Trainable, Constant, Parameter +from mrmustard.training.parameter import ( + Constant, + Parameter, + Trainable, + create_parameter, +) +from mrmustard.types import Tensor math = Math() @@ -40,7 +47,7 @@ class Parametrized: def __init__(self, **kwargs): # NOTE: only kwargs so that we can use the arg names owner = f"{self.__class__.__qualname__}" - + self.param_names = [] # list of parameter names to preserve order for name, value in kwargs.items(): # filter out `{name}_trainable` or `{name}_bounds`` to become fields # of the class as those kwargs are used to define the variables @@ -52,8 +59,34 @@ def __init__(self, **kwargs): # NOTE: only kwargs so that we can use the arg na bounds = kwargs.get(f"{name}_bounds", None) param = create_parameter(value, name, is_trainable, bounds, owner) - # dynamically assign variable as attribute of the class + # dynamically assign parameter as attribute of the class self.__dict__[name] = param + self.param_names.append(name) + + def param_string(self, decimals: int) -> str: + r"""Returns a string representation of the parameter values, separated by commas and rounded + to the specified number of decimals. It includes only the parameters that are not arrays + and not the number of modes, or other parameters that are not in principle trainable. + Keeps the order of the parameters as they are defined in the class constructor. + + Args: + decimals (int): number of decimals to round to + + Returns: + str: string representation of the parameter values + """ + string = "" + for _, value in self.kw_parameters: + if math.asnumpy(value).ndim == 0: # don't show arrays + string += f"{math.asnumpy(value):.{decimals}g}, " + return string.rstrip(", ") + + @property + def kw_parameters(self) -> Tuple[Tuple[str, Tensor]]: + r"""Return a list of parameters within the Parametrized object + if they have been passed as keyword arguments to the class constructor. + """ + return tuple((name, getattr(self, name).value) for name in self.param_names) @property def trainable_parameters(self) -> Sequence[Trainable]: @@ -77,7 +110,9 @@ def _traverse_parametrized(object_: Any, extract_type: Parameter) -> Generator: """ for obj in object_: - if isinstance(obj, Sequence): # pylint: disable=isinstance-second-argument-not-valid-type + if isinstance( + obj, (List, Tuple) + ): # pylint: disable=isinstance-second-argument-not-valid-type yield from _traverse_parametrized(obj, extract_type) elif isinstance(obj, Parametrized): yield from _traverse_parametrized(obj.__dict__.values(), extract_type) diff --git a/mrmustard/utils/circdrawer.py b/mrmustard/utils/circdrawer.py new file mode 100644 index 000000000..56455556b --- /dev/null +++ b/mrmustard/utils/circdrawer.py @@ -0,0 +1,107 @@ +# 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 logic for the text-based circuit drawer for MrMustard +""" +from collections import defaultdict + + +def mode_set(op): + "includes modes in between min and max of op.modes" + return set(range(min(op.modes), max(op.modes) + 1)) + + +def drawable_layers(ops): + r"""Determine non-overlapping yet dense placement of ops into layers for drawing. + Arguments: + ops Iterable[op]: a list of operations + + Returns: + dict[int:list[op]] : At index k is a list of operations for the k-th layer + """ + layers = defaultdict(list) + k = 0 + for new_op in ops: + # if there's any overlap, add to next layer + if any(mode_set(new_op) & mode_set(op) for op in layers[k]): + k += 1 + layers[k].append(new_op) + return layers + + +def _add_grouping_symbols(op, layer_str): + r"""Adds symbols indicating the extent of a given object.""" + S = mode_set(op) + if len(S) > 1: + layer_str[min(S)] = "╭" + layer_str[max(S)] = "╰" + for w in range(min(S) + 1, max(S)): + layer_str[w] = "├" if w in op.modes else "│" # other option: ┼ + return layer_str + + +def _add_op(op, layer_str, decimals): + r"""Updates `layer_str` with `op` operation.""" + layer_str = _add_grouping_symbols(op, layer_str) + control = [] + if op.__class__.__qualname__ in ["BSgate", "MZgate", "CZgate", "CXgate"]: + control = [op.modes[0]] + label = op.short_name + if decimals is not None: + param_string = op.param_string(decimals) + if param_string == "": + param_string = str(len(op.modes)) + label += "(" + param_string + ")" + + for w in op.modes: + layer_str[w] += "•" if w in control else label + + return layer_str + + +def circuit_text( + ops, + decimals=None, +): + r"""Text based diagram for a Quantum circuit. + Arguments: + ops (List[Transformation]): the operations and measurements to draw as a list of MrMustard operations + decimals (optional(int)): How many decimal points to include when formatting operation parameters. + Default ``None`` will omit parameters from operation labels. + Returns: + str : String based graphic of the circuit. + """ + # get all modes used by the ops and sort them + modes = sorted(list(set().union(*[op.modes for op in ops]))) + # include all modes between min and max (need to draw over them) + all_modes = range(min(modes), max(modes) + 1) + + totals = [f"{mode}: " for mode in all_modes] + line_length = max(len(s) for s in totals) + totals = [s.rjust(line_length, " ") for s in totals] + filler = "─" + + for layer in drawable_layers(ops).values(): + layer_str = [filler] * (max(all_modes) - min(all_modes) + 1) + for op in layer: + layer_str = _add_op(op, layer_str, decimals) + + max_label_len = max(len(s) for s in layer_str) + layer_str = [s.ljust(max_label_len, filler) for s in layer_str] + + line_length += max_label_len + 1 # one for the filler character + + totals = [filler.join([t, s]) + filler for t, s in zip(totals, layer_str)] + + return "\n".join(totals) diff --git a/tests/test_lab/test_detectors.py b/tests/test_lab/test_detectors.py index a53c526ee..66a1ca172 100644 --- a/tests/test_lab/test_detectors.py +++ b/tests/test_lab/test_detectors.py @@ -12,32 +12,32 @@ # See the License for the specific language governing permissions and # limitations under the License. +import numpy as np import pytest -from hypothesis import given, strategies as st +import tensorflow as tf +from hypothesis import given +from hypothesis import strategies as st from hypothesis.extra.numpy import arrays - -import numpy as np from scipy.stats import poisson -import tensorflow as tf -from mrmustard.math import Math +from mrmustard import physics, settings from mrmustard.lab import ( - PNRDetector, - Coherent, - Sgate, - Vacuum, - S2gate, - BSgate, - Attenuator, - Homodyne, - Heterodyne, TMSV, + Attenuator, + BSgate, + Coherent, Dgate, Fock, - State, + Heterodyne, + Homodyne, + PNRDetector, + S2gate, + Sgate, SqueezedVacuum, + State, + Vacuum, ) -from mrmustard import physics, settings +from mrmustard.math import Math from tests.random import none_or_ math = Math() @@ -243,9 +243,9 @@ def test_homodyne_on_2mode_squeezed_vacuum_with_angle(self, s, outcome, angle): # assert np.allclose(means, expected_means) @given( - s=st.floats(min_value=0.0, max_value=10.0), - X=st.floats(-10.0, 10.0), - d=arrays(np.float64, 4, elements=st.floats(-10.0, 10.0)), + s=st.floats(min_value=0.0, max_value=1.0), + X=st.floats(-1.0, 1.0), + d=arrays(np.float64, 4, elements=st.floats(-1.0, 1.0)), ) def test_homodyne_on_2mode_squeezed_vacuum_with_displacement(self, s, X, d): """Check that homodyne detection on displaced TMSV works""" diff --git a/tests/test_utils/test_circuitdrawer.py b/tests/test_utils/test_circuitdrawer.py new file mode 100644 index 000000000..bc01b7fb6 --- /dev/null +++ b/tests/test_utils/test_circuitdrawer.py @@ -0,0 +1,116 @@ +# 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 tests for the circuitdrawer.py module.""" + +from mrmustard import settings +from mrmustard.lab import BSgate, Ggate +from mrmustard.utils.circdrawer import ( + _add_grouping_symbols, + _add_op, + circuit_text, + drawable_layers, + mode_set, +) + + +def test_mode_set(): + r"""Tests that mode_set returns the correct set of modes""" + op = BSgate(0.5)[3, 11] + assert mode_set(op) == set(range(3, 12)) + + +def test_drawable_layers_overlap(): + r"""Tests that drawable_layers returns the correct layers""" + ops = [BSgate(0.5)[3, 11], BSgate(0.5)[3, 11], BSgate(0.5)[3, 11]] + assert drawable_layers(ops) == {0: [ops[0]], 1: [ops[1]], 2: [ops[2]]} + + +def test_drawable_layers_no_overlap(): + r"""Tests that drawable_layers returns the correct layers""" + ops = [BSgate(0.5)[3, 11], BSgate(0.5)[12, 13], BSgate(0.5)[14, 15]] + assert drawable_layers(ops) == {0: [ops[0], ops[1], ops[2]]} + + +def test_drawable_layers_mix_overlap(): + r"""Tests that drawable_layers returns the correct layers""" + ops = [BSgate(0.5)[3, 11], BSgate(0.5)[3, 11], BSgate(0.5)[12, 13], BSgate(0.5)[14, 15]] + assert drawable_layers(ops) == {0: [ops[0]], 1: [ops[1], ops[2], ops[3]]} + + +def test_add_grouping_symbols_BS(): + r"""Tests that _add_grouping_symbols returns the correct symbols""" + op = BSgate(0.5)[3, 11] + assert _add_grouping_symbols(op, ["-"] * 12) == [ + "-", + "-", + "-", + "╭", + "│", + "│", + "│", + "│", + "│", + "│", + "│", + "╰", + ] + + +def test_add_grouping_symbols_G(): + r"""Tests that _add_grouping_symbols returns the correct symbols""" + op = Ggate(5)[1, 2, 3, 4, 5] + assert _add_grouping_symbols(op, ["-"] * 6) == [ + "-", + "╭", + "├", + "├", + "├", + "╰", + ] + + +def test_add_op(): + r"""Tests that _add_op returns the correct symbols""" + op = Ggate(5)[1, 2, 3, 4, 5] + layer_str = _add_grouping_symbols(op, ["-"] * 6) + decimals = None + assert _add_op(op, layer_str, decimals) == [ + "-", + "╭G", + "├G", + "├G", + "├G", + "╰G", + ] + + +def test_circuit_text(): + r"""Tests that circuit_text returns the correct circuit""" + settings.CIRCUIT_DECIMALS = None + ops = [BSgate(0.5)[0, 1], Ggate(3)[2, 3, 5], BSgate(0.5)[7, 6]] + decimals = None + assert ( + circuit_text(ops, decimals) + == "0: ─╭•──\n1: ─╰BS─\n2: ─╭G──\n3: ─├G──\n4: ─│───\n5: ─╰G──\n6: ─╭BS─\n7: ─╰•──" + ) + + +def test_param_order(): + r"""Tests that Parametrized.param_string returns the parameters in the correct order""" + B = BSgate(theta=0.4, phi=0.5) + assert B.param_string(decimals=1) == "0.4, 0.5" + + B = BSgate(phi=0.5, theta=0.4) + assert B.param_string(decimals=1) == "0.4, 0.5" # same order as class constructor, not call From a69fff2c8997cd22f1c81d9e7cca223c7785c26b Mon Sep 17 00:00:00 2001 From: ziofil Date: Thu, 16 Feb 2023 13:38:36 -0800 Subject: [PATCH 26/53] updated README.md --- README.md | 45 ++++++++++++++++++++++++--------------------- 1 file changed, 24 insertions(+), 21 deletions(-) diff --git a/README.md b/README.md index 955e2c3ec..4979ff5df 100644 --- a/README.md +++ b/README.md @@ -5,26 +5,25 @@ [![Actions Status](https://github.com/XanaduAI/MrMustard/workflows/Tests/badge.svg)](https://github.com/XanaduAI/MrMustard/actions) [![Python version](https://img.shields.io/pypi/pyversions/mrmustard.svg?style=popout-square)](https://pypi.org/project/MrMustard/) -Mr Mustard is a differentiable simulator with a sophisticated built-in optimizer, that operates across phase space and Fock space. -It is built on top of an agnostic autodiff interface, to allow for plug-and-play backends (TensorFlow by default, PyTorch coming soon). +Mr Mustard is a differentiable simulator with a sophisticated built-in optimizer, that operates seamlessly across phase space and Fock space. It is built on top of an agnostic autodiff interface, to allow for plug-and-play backends (TensorFlow by default). Mr Mustard supports: - Phase space representation of Gaussian states and Gaussian channels on an arbitrary number of modes - Exact Fock representation of any Gaussian circuit and any Gaussian state up to an arbitrary cutoff -- Riemannian optimization on the symplectic group (for Gaussian transformations) and on the orthogonal group (for interferometers) -- Adam optimizer for euclidean parameters. +- Riemannian optimization on the symplectic group (for Gaussian transformations) and on the unitary group (for interferometers) +- Adam optimizer for euclidean parameters - single-mode gates (parallelizable): - - squeezing, displacement, phase rotation, attenuator, amplifier, additive noise + - squeezing, displacement, phase rotation, attenuator, amplifier, additive noise, phase noise - two-mode gates: - beam splitter, Mach-Zehnder interferometer, two-mode squeezing, CX, CZ, CPHASE - N-mode gates (with dedicated Riemannian optimization): - - interferometer (orthogonal), Gaussian transformation (symplectic) + - Interferometer (unitary), RealInterferometer (orthogonal), Gaussian transformation (symplectic) - single-mode states (parallelizable): - - vacuum, coherent, squeezed, displaced-squeezed, thermal + - Vacuum, Coherent, SqueezedVacuum, Thermal, Fock - two-mode states: - - two-mode squeezed vacuum + - TMSV (two-mode squeezed vacuum) - N-mode states: - - Gaussian state + - Gaussian - Photon number moments and entropic measures - PNR detectors and Threshold detectors with trainable quantum efficiency and dark counts - Homodyne, Heterodyne and Generaldyne measurements @@ -50,6 +49,10 @@ fock4 = Fock(4) # fock state |4> D = Dgate(x=1.0, y=-0.4) # Displacement by 1.0 along x and -0.4 along y S = Sgate(r=0.5) # Squeezer with r=0.5 +R = Rgate(phi=0.3) # Phase rotation by 0.3 +A = Amplifier(gain=2.0) # noisy amplifier with 200% gain +L = Attenuator(0.5) # pure loss channel with 50% transmissivity +N = AdditiveNoise(noise=0.1) # additive noise with noise level 0.1 BS = BSgate(theta=np.pi/4) # 50/50 beam splitter S2 = S2gate(r=0.5) # two-mode squeezer @@ -105,14 +108,14 @@ output = Vacuum(4) >> X8 # lossy X8 noise = lambda: np.random.uniform(size=4) -X8_realistic = (Sgate(r=0.9 + 0.1*noise(), phi=0.1*noise()) +X8_noisy = (Sgate(r=0.9 + 0.1*noise(), phi=0.1*noise()) >> Attenuator(0.89 + 0.01*noise()) >> Interferometer(4) >> Attenuator(0.95 + 0.01*noise()) ) # 2-mode Bloch Messiah decomposition -bloch_messiah = Sgate(r=[0.1,0.2]) >> BSgate(-0.1, 2.1) >> Dgate(x=[0.1, -0.4]) +bloch_messiah = Sgate(r=[0.1,0.2]) >> BSgate(theta=-0.1, phi=2.1) >> Dgate(x=[0.1, -0.4]) my_state = Vacuum(2) >> bloch_messiah ``` @@ -129,7 +132,7 @@ Attenuator(0.5) << Coherent(0.1, 0.2) == Coherent(0.1, 0.2) >> Amplifier(2.0) This has the advantage of modelling lossy detectors without applying the loss channel to the state going into the detector, which can be overall faster e.g. if the state is kept pure by doing so. ## 5. Detectors -There are two types of detectors in Mr Mustard. Fock detectors (PNRDetector and ThresholdDetector) and Gaussian detectors (Homodyne, Heterodyne). However, Gaussian detectors are a thin wrapper over just Gaussian states, as Gaussian states can be used as projectors (i.e. `state << DisplacedSqueezed(...)` is how Homodyne performs a measurement). +There are two types of detectors in Mr Mustard. Fock detectors (PNRDetector and ThresholdDetector) and Gaussian detectors (Homodyne, Heterodyne, Generaldyne). The PNR and Threshold detectors return an array of unnormalized measurement results, meaning that the elements of the array are the density matrices of the leftover systems, conditioned on the outcomes: ```python @@ -179,15 +182,15 @@ swapped = joint.get_modes([1,0]) ``` ## 8. Fock representation -The Fock representation of a State is obtained via `.ket(cutoffs)` or `.dm(cutoffs)`. For circuits and gates it's `.U(cutoffs)` or `.choi(cutoffs)`. The Fock representation is exact (with minor caveats) and it doesn't break differentiability. This means that one can define cost functions on the Fock representation and backpropagate back to the phase space representation. +The Fock representation of a State is obtained via `.ket(cutoffs)` or `.dm(cutoffs)`. For circuits and gates (transformations in general) it's `.U(cutoffs)` or `.choi(cutoffs)`, if available. The Fock representation is exact and it doesn't break differentiability. This means that one can define cost functions on the Fock representation and backpropagate back to the phase space representation. ```python # Fock representation of a coherent state Coherent(0.5).ket(cutoffs=[5]) # ket Coherent(0.5).dm(cutoffs=[5]) # density matrix -Dgate(x=1.0).U(cutoffs=[15]) # truncated unitary op -Dgate(x=1.0).choi(cutoffs=[15]) # truncated choi op +Dgate(x=1.0).U(cutoffs=[15]) # truncated unitary matrix +Dgate(x=1.0).choi(cutoffs=[15]) # truncated choi tensor ``` States can be initialized in Fock representation and used as any other state: @@ -214,8 +217,7 @@ The physics module contains a growing number of functions that we can apply to s # The math module -The math module is the backbone of Mr Mustard, which consists in the [`Math`](https://github.com/XanaduAI/MrMustard/blob/main/mrmustard/math/math_interface.py) interface -Mr Mustard comes with a plug-and-play backends through a math interface. You can use it as a drop-in replacement for tensorflow or pytorch and your code will be plug-and-play too! +The math module is the backbone of Mr Mustard, which consists in the [`Math`](https://github.com/XanaduAI/MrMustard/blob/main/mrmustard/math/math_interface.py) interface. Mr Mustard comes with a plug-and-play backends through a math interface. You can use it as a drop-in replacement for tensorflow or pytorch and your code will be plug-and-play too! ```python from mrmustard import settings from mrmustard.math import Math @@ -229,10 +231,9 @@ math.cos(0.1) # pytorch (upcoming) ``` ### Optimization -The `Optimizer` (available in `mrmustard.training` uses Adam underneath the hood for Euclidean parameters and a custom symplectic optimizer for Gaussian gates and states and an orthogonal optimizer for interferometers. +The `Optimizer` (available in `mrmustard.training` uses Adam underneath the hood for Euclidean parameters and a custom symplectic optimizer for Gaussian gates and states and a unitary/orthogonal optimizer for interferometers. -We can turn any simulation in Mr Mustard into an optimization by marking which parameters we wish to be trainable. Let's take a simple example: synthesizing a -displaced squeezed state. +We can turn any simulation in Mr Mustard into an optimization by marking which parameters we wish to be trainable. Let's take a simple example: synthesizing a displaced squeezed state. ```python from mrmustard.lab import Dgate, Ggate, Attenuator, Vacuum, Coherent, DisplacedSqueezed @@ -252,9 +253,11 @@ def cost_fn_sympl(): state_out = Vacuum(1) >> G >> D >> L return 1 - fidelity(state_out, DisplacedSqueezed(r=0.3, phi=1.1, x=0.4, y=-0.2)) +# For illustration, here the Euclidean optimization doesn't include squeezing opt = Optimizer(symplectic_lr=0.1, euclidean_lr=0.01) opt.minimize(cost_fn_eucl, by_optimizing=[D]) # using Adam for D +# But the symplectic optimization always does opt = Optimizer(symplectic_lr=0.1, euclidean_lr=0.01) -opt.minimize(cost_fn_sympl, by_optimizing=[G,D]) # using Adam for D and the symplectic opt for G +opt.minimize(cost_fn_sympl, by_optimizing=[G,D]) # uses Adam for D and the symplectic opt for G ``` \ No newline at end of file From 7d79a37a4f8114d71cab169e5bbaeac06927f5f2 Mon Sep 17 00:00:00 2001 From: zeyueN <48225584+zeyueN@users.noreply.github.com> Date: Fri, 17 Feb 2023 10:35:58 -0500 Subject: [PATCH 27/53] ray test fix (#201) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit **Context:** Attempt to fix the (random) never ending tests on github actions. I have so far rerun the test 5 times without the issue reoccurring. 🤞 **Description of the Change:** Forces ray to init with 1 cpu for testing. **Benefits:** **Possible Drawbacks:** **Related GitHub Issues:** --------- Co-authored-by: Sebastián Duque Mesa <675763+sduquemesa@users.noreply.github.com> --- .github/CHANGELOG.md | 3 +++ tests/test_training/test_trainer.py | 11 +++++++++++ 2 files changed, 14 insertions(+) diff --git a/.github/CHANGELOG.md b/.github/CHANGELOG.md index 25118e380..577ba388a 100644 --- a/.github/CHANGELOG.md +++ b/.github/CHANGELOG.md @@ -166,6 +166,9 @@ This is now fixed. a pure dm was converted to ket). [(#184)](https://github.com/XanaduAI/MrMustard/pull/184) +* The ray related tests were hanging in github action causing test to halt and fail. Now ray is forced to init with 1 cpu when running tests preventing the issue. +[(#201)](https://github.com/XanaduAI/MrMustard/pull/201) + ### Documentation ### Contributors diff --git a/tests/test_training/test_trainer.py b/tests/test_training/test_trainer.py index f9012e48e..0a664d0ba 100644 --- a/tests/test_training/test_trainer.py +++ b/tests/test_training/test_trainer.py @@ -20,11 +20,15 @@ import pytest import numpy as np +import ray from mrmustard.lab import Vacuum, Dgate, Ggate, Gaussian from mrmustard.physics import fidelity from mrmustard.training import Optimizer from mrmustard.training.trainer import map_trainer, train_device, update_pop +NUM_CPUS = 1 +ray.init(num_cpus=NUM_CPUS) + @pytest.fixture(scope="function") def wrappers(): @@ -57,6 +61,7 @@ def test_circ_cost(wrappers, tasks, seed): # pylint: disable=redefined-outer-na results = map_trainer( cost_fn=cost_fn, tasks=tasks, + num_cpus=NUM_CPUS, **({"SEED": seed} if has_seed else {}), ) @@ -92,6 +97,7 @@ def test_circ_optimize(wrappers, tasks, return_type): # pylint: disable=redefin max_steps=max_steps, symplectic_lr=0.05, return_type=return_type, + num_cpus=NUM_CPUS, ) if isinstance(tasks, dict): @@ -139,6 +145,7 @@ def test_circ_optimize_metrics(wrappers, metric_fns): # pylint: disable=redefin symplectic_lr=0.05, metric_fns=metric_fns, return_list=True, + num_cpus=NUM_CPUS, ) assert set(results.keys()) == set(tasks.keys()) @@ -171,6 +178,7 @@ def test_no_ray(monkeypatch): with pytest.raises(ImportError, match="Failed to import `ray`"): _ = map_trainer( tasks=2, + num_cpus=NUM_CPUS, ) @@ -179,6 +187,7 @@ def test_invalid_tasks(): with pytest.raises(ValueError, match="`tasks` is expected to be of type int, list, or dict."): _ = map_trainer( tasks=2.3, + num_cpus=NUM_CPUS, ) @@ -201,6 +210,7 @@ def test_no_pbar(wrappers): # pylint: disable=redefined-outer-name cost_fn=cost_fn, tasks=2, pbar=False, + num_cpus=NUM_CPUS, ) assert len(results) == 2 @@ -213,6 +223,7 @@ def test_unblock(wrappers, tasks): # pylint: disable=redefined-outer-name cost_fn=cost_fn, tasks=tasks, unblock=True, + num_cpus=NUM_CPUS, ) assert callable(result_getter) From 8f6d70d7ebb611d652dd28e3c0bbb8312fa9eaba Mon Sep 17 00:00:00 2001 From: ziofil Date: Mon, 27 Feb 2023 12:50:13 -0500 Subject: [PATCH 28/53] Improving tests (#191) **Context:** Tests could improve in MM **Description of the Change:** Add several new tests and improves hypothesis strategies **Benefits:** Better test suite **Possible Drawbacks:** More tests to maintain? Nah **Related GitHub Issues:** None --- .github/CHANGELOG.md | 4 + mrmustard/lab/gates.py | 1 - tests/conftest.py | 6 +- tests/random.py | 257 ++++++++++++++++------ tests/test_lab/test_gates_fock.py | 71 +++--- tests/test_lab/test_states.py | 56 ++--- tests/test_math/test_mmtensor.py | 28 +++ tests/{ => test_physics}/test_fidelity.py | 11 +- tests/test_settings.py | 4 +- tests/test_utils/test_xptensor.py | 2 +- 10 files changed, 283 insertions(+), 157 deletions(-) rename tests/{ => test_physics}/test_fidelity.py (98%) diff --git a/.github/CHANGELOG.md b/.github/CHANGELOG.md index 577ba388a..edfdd6ebd 100644 --- a/.github/CHANGELOG.md +++ b/.github/CHANGELOG.md @@ -146,6 +146,10 @@ choi operators to kets and density matrices. * Added two new modules (`physics.bargmann` and `physics.husimi`) to host the functions related to those representation, which have been refactored and moved out of `physics.fock`. [(#185)](https://github.com/XanaduAI/MrMustard/pull/185) + +* Added multiple tests and improved the use of Hypothesis. + [(#191)](https://github.com/XanaduAI/MrMustard/pull/191) + ### Bug fixes * The `Dgate` and the `Rgate` now correctly parse the case when a single scalar is intended as the same parameter diff --git a/mrmustard/lab/gates.py b/mrmustard/lab/gates.py index 14c0815ce..619608f74 100644 --- a/mrmustard/lab/gates.py +++ b/mrmustard/lab/gates.py @@ -781,7 +781,6 @@ def __init__( noise=noise, noise_trainable=noise_trainable, noise_bounds=noise_bounds, - modes=modes, ) self._modes = modes self.is_unitary = False diff --git a/tests/conftest.py b/tests/conftest.py index be80a8e55..572cbeb4f 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -13,12 +13,12 @@ # limitations under the License. import os -import pytest -from hypothesis import settings, Verbosity + +from hypothesis import Verbosity, settings print("pytest.conf -----------------------") -settings.register_profile("ci", max_examples=50, deadline=None) +settings.register_profile("ci", max_examples=10, deadline=None) settings.register_profile("dev", max_examples=10, deadline=None) settings.register_profile("debug", max_examples=10, verbosity=Verbosity.verbose, deadline=None) diff --git a/tests/random.py b/tests/random.py index 64bcaf825..6570ae2d5 100644 --- a/tests/random.py +++ b/tests/random.py @@ -15,61 +15,84 @@ import numpy as np from hypothesis import given, strategies as st from hypothesis.extra.numpy import arrays -from mrmustard.lab import * - +from mrmustard.lab import ( + Dgate, + Sgate, + Pgate, + Rgate, + CZgate, + CXgate, + BSgate, + MZgate, + S2gate, + Attenuator, + Amplifier, + AdditiveNoise, + Interferometer, + Ggate, + Vacuum, +) +from mrmustard import settings -angle = st.floats(min_value=0, max_value=2 * np.pi) -positive = st.floats(min_value=0, allow_infinity=False, allow_nan=False) +# numbers +integer32bits = st.integers(min_value=0, max_value=2**31 - 1) real = st.floats(allow_infinity=False, allow_nan=False) -r = st.floats( - min_value=0, max_value=0.5, allow_infinity=False, allow_nan=False -) # reasonable squeezing magnitude -real_not_zero = st.one_of(st.floats(max_value=-0.00001), st.floats(min_value=0.00001)) -integer = st.integers(min_value=0, max_value=2**32 - 1) +positive = st.floats(min_value=0, exclude_min=True, allow_infinity=False, allow_nan=False) +negative = st.floats(max_value=0, exclude_max=True, allow_infinity=False, allow_nan=False) +real_not_zero = st.one_of(negative, positive) small_float = st.floats(min_value=-0.1, max_value=0.1, allow_infinity=False, allow_nan=False) medium_float = st.floats(min_value=-1.0, max_value=1.0, allow_infinity=False, allow_nan=False) -large_float = st.floats(min_value=-10.0, max_value=10.0, allow_infinity=False, allow_nan=False) -num_modes = st.integers(min_value=0, max_value=10) + +# physical parameters +nmodes = st.integers(min_value=1, max_value=10) +angle = st.floats(min_value=0, max_value=2 * np.pi) +r = st.floats(min_value=0, max_value=1.25, allow_infinity=False, allow_nan=False) +prob = st.floats(min_value=0, max_value=1, allow_infinity=False, allow_nan=False) +gain = st.floats(min_value=1, max_value=2, allow_infinity=False, allow_nan=False) @st.composite def vector(draw, length): - return draw( - st.lists(st.floats(min_value=-1.0, max_value=1.0), min_size=length, max_size=length) - ) + r"""Return a vector of length `length`.""" + return draw(arrays(np.float, (length,), elements=st.floats(min_value=-1.0, max_value=1.0))) -# a strategy to produce a list of integers of length num_modes. the integers are all different and between 0 and num_modes @st.composite -def modes(draw, num_modes): +def list_of_ints(draw, N): + r"""Return a list of N unique integers between 0 and N-1.""" return draw( - st.lists( - st.integers(min_value=0, max_value=num_modes), min_size=num_modes, max_size=num_modes - ).filter(lambda x: len(set(x)) == len(x)) + st.lists(st.integers(min_value=0, max_value=N), min_size=N, max_size=N, unique=True) ) -def array_of_(strategy, minlen=0, maxlen=None): - return arrays(dtype=np.float64, shape=(st.integers(minlen, maxlen),), elements=strategy) +def array_of_(strategy, minlen=0, maxlen=100): + r"""Return a strategy that returns an array of values from `strategy`.""" + return arrays( + shape=(st.integers(minlen, maxlen).example(),), + elements=strategy, + dtype=type(strategy.example()), + ) def none_or_(strategy): + r"""Return a strategy that returns either None or a value from `strategy`.""" return st.one_of(st.just(None), strategy) -angle_bounds = st.tuples(none_or_(angle), none_or_(angle)).filter( - lambda t: t[0] < t[1] if t[0] is not None and t[1] is not None else True -) -positive_bounds = st.tuples(none_or_(positive), none_or_(positive)).filter( - lambda t: t[0] < t[1] if t[0] is not None and t[1] is not None else True -) -real_bounds = st.tuples(none_or_(real), none_or_(real)).filter( - lambda t: t[0] < t[1] if t[0] is not None and t[1] is not None else True -) +# bounds +bounds_check = lambda t: t[0] < t[1] if t[0] is not None and t[1] is not None else True +angle_bounds = st.tuples(none_or_(angle), none_or_(angle)).filter(bounds_check) +positive_bounds = st.tuples(none_or_(positive), none_or_(positive)).filter(bounds_check) +real_bounds = st.tuples(none_or_(real), none_or_(real)).filter(bounds_check) +gain_bounds = st.tuples(none_or_(gain), none_or_(gain)).filter(bounds_check) +prob_bounds = st.tuples(none_or_(prob), none_or_(prob)).filter(bounds_check) + +# gates @st.composite -def random_Rgate(draw, num_modes=None, trainable=False): +def random_Rgate(draw, trainable=False): + r"""Return a random Rgate.""" return Rgate( angle=draw(angle), angle_bounds=draw(angle_bounds), @@ -78,9 +101,10 @@ def random_Rgate(draw, num_modes=None, trainable=False): @st.composite -def random_Sgate(draw, num_modes=None, trainable=False, small=False): +def random_Sgate(draw, trainable=False): + r"""Return a random Sgate.""" return Sgate( - r=np.abs(draw(small_float)) if small else draw(r), + r=draw(r), phi=draw(angle), r_bounds=draw(positive_bounds), phi_bounds=draw(angle_bounds), @@ -90,13 +114,10 @@ def random_Sgate(draw, num_modes=None, trainable=False, small=False): @st.composite -def random_Dgate(draw, num_modes=None, trainable=False, small=False): - if small: - x = draw(small_float) - y = draw(small_float) - else: - x = draw(medium_float) - y = draw(medium_float) +def random_Dgate(draw, trainable=False): + r"""Return a random Dgate.""" + x = draw(small_float) + y = draw(small_float) return Dgate( x=x, y=y, @@ -107,8 +128,49 @@ def random_Dgate(draw, num_modes=None, trainable=False, small=False): ) +@st.composite +def random_Pgate(draw, trainable=False): + r"""Return a random Pgate.""" + return Pgate( + shearing=draw(prob), + shearing_bounds=draw(prob_bounds), + shearing_trainable=trainable, + ) + + +@st.composite +def random_Attenuator(draw, trainable=False): + r"""Return a random Attenuator.""" + return Attenuator( + transmissivity=draw(prob), + transmissivity_bounds=draw(prob_bounds), + transmissivity_trainable=trainable, + ) + + +@st.composite +def random_Amplifier(draw, trainable=False): + r"""Return a random Amplifier.""" + return Amplifier( + gain=draw(gain), + gain_bounds=draw(gain_bounds), + gain_trainable=trainable, + ) + + +@st.composite +def random_AdditiveNoise(draw, trainable=False): + r"""Return a random AdditiveNoise.""" + return AdditiveNoise( + noise=draw(prob), + noise_bounds=draw(prob_bounds), + noise_trainable=trainable, + ) + + @st.composite def random_S2gate(draw, trainable=False): + r"""Return a random S2gate.""" return S2gate( r=draw(r), phi=draw(angle), @@ -119,8 +181,29 @@ def random_S2gate(draw, trainable=False): ) +@st.composite +def random_CXgate(draw, trainable=False): + r"""Return a random CXgate.""" + return CXgate( + s=draw(medium_float), + s_bounds=draw(real_bounds), + s_trainable=trainable, + ) + + +@st.composite +def random_CZgate(draw, trainable=False): + r"""Return a random CZgate.""" + return CZgate( + s=draw(medium_float), + s_bounds=draw(real_bounds), + s_trainable=trainable, + ) + + @st.composite def random_BSgate(draw, trainable=False): + r"""Return a random BSgate.""" return BSgate( theta=draw(angle), phi=draw(angle), @@ -133,6 +216,7 @@ def random_BSgate(draw, trainable=False): @st.composite def random_MZgate(draw, trainable=False): + r"""Return a random MZgate.""" return MZgate( phi_a=draw(angle), phi_b=draw(angle), @@ -146,33 +230,54 @@ def random_MZgate(draw, trainable=False): @st.composite def random_Interferometer(draw, num_modes, trainable=False): + r"""Return a random Interferometer.""" + settings.SEED = draw(integer32bits) return Interferometer(num_modes=num_modes, orthogonal_trainable=trainable) @st.composite def random_Ggate(draw, num_modes, trainable=False): - displacement = vector(2 * num_modes) - return Ggate( - num_modes=num_modes, - displacement=draw(displacement), - displacement_trainable=trainable, + r"""Return a random Ggate.""" + settings.SEED = draw(integer32bits) + return Ggate(num_modes=num_modes, symplectic_trainable=trainable) + + +@st.composite +def single_mode_unitary_gate(draw): + r"""Return a random single mode unitary gate.""" + return draw( + st.one_of( + random_Rgate(), + random_Sgate(), + random_Dgate(), + random_Pgate(), + random_Interferometer(num_modes=1), # like Rgate + ) ) @st.composite -def single_mode_unitary(draw, small=False): +def single_mode_cv_channel(draw): + r"""Return a random single mode unitary gate.""" return draw( - st.one_of(random_Rgate(1), random_Sgate(1, small=small), random_Dgate(1, small=small)) + st.one_of( + random_Attenuator(), + random_Amplifier(), + random_AdditiveNoise(), + ) ) @st.composite -def two_mode_gate(draw): +def two_mode_unitary_gate(draw): + r"""Return a random two mode unitary gate.""" return draw( st.one_of( random_S2gate(), random_BSgate(), random_MZgate(), + random_CXgate(), + random_CZgate(), random_Ggate(num_modes=2), random_Interferometer(num_modes=2), ) @@ -180,13 +285,15 @@ def two_mode_gate(draw): @st.composite -def n_mode_gate(draw, num_modes=None): +def n_mode_unitary_gate(draw, num_modes=None): + r"""Return a random n mode unitary gate.""" return draw(st.one_of(random_Interferometer(num_modes), random_Ggate(num_modes))) ## states @st.composite def squeezed_vacuum(draw, num_modes): + r"""Return a random squeezed vacuum state.""" r = array_of_(r, num_modes, num_modes) phi = array_of_(angle, num_modes, num_modes) return SqueezedVacuum(r=draw(r), phi=draw(phi)) @@ -194,8 +301,9 @@ def squeezed_vacuum(draw, num_modes): @st.composite def displacedsqueezed(draw, num_modes): - r = array_of_(small_float.filter(lambda r: r > 0.0), num_modes, num_modes) - phi_ = array_of_(angle, num_modes, num_modes) + r"""Return a random displaced squeezed state.""" + r = array_of_(r, num_modes, num_modes) + phi = array_of_(angle, num_modes, num_modes) x = array_of_(medium_float, num_modes, num_modes) y = array_of_(medium_float, num_modes, num_modes) return DisplacedSqueezed(r=draw(r), phi=draw(phi), x=draw(x), y=draw(x)) @@ -203,6 +311,7 @@ def displacedsqueezed(draw, num_modes): @st.composite def coherent(draw, num_modes): + r"""Return a random coherent state.""" x = array_of_(medium_float, num_modes, num_modes) y = array_of_(medium_float, num_modes, num_modes) return Coherent(x=draw(x), y=draw(y)) @@ -210,45 +319,59 @@ def coherent(draw, num_modes): @st.composite def tmsv(draw): - r = array_of_(medium_float.filter(lambda r: r > 0.0), 2, 2) - phi = array_of_(angle, 2, 2) + r"""Return a random two-mode squeezed vacuum state.""" return TMSV(r=draw(r), phi=draw(phi)) @st.composite def thermal(draw, num_modes): - n_mean = array_of_(medium_float.filter(lambda r: r > 0.0), num_modes, num_modes) + r"""Return a random thermal state.""" + n_mean = array_of_(r, num_modes, num_modes) # using r here return Thermal(n_mean=draw(n_mean)) +# generic states @st.composite -def default_state(draw, num_modes): +def n_mode_separable_pure_state(draw, num_modes): + r"""Return a random n mode separable pure state.""" return draw( st.one_of( squeezed_vacuum(num_modes), displacedsqueezed(num_modes), coherent(num_modes), - tmsv(num_modes), - thermal(num_modes), ) ) @st.composite -def default_pure_state(draw, num_modes): - return draw( - st.one_of( - squeezed_vacuum(num_modes), - displacedsqueezed(num_modes), - coherent(num_modes), - tmsv(num_modes), +def n_mode_separable_mixed_state(draw, num_modes): + r"""Return a random n mode separable mixed state.""" + attenuator = Attenuator(draw(st.floats(min_value=0.2, max_value=0.9))) + return ( + draw( + st.one_of( + squeezed_vacuum(num_modes), + displacedsqueezed(num_modes), + coherent(num_modes), + thermal(num_modes), + ) ) + >> attenuator ) @st.composite -def pure_state(draw, num_modes=1, small=False): - S = draw(random_Sgate(num_modes, small=small)) +def n_mode_pure_state(draw, num_modes=1): + r"""Return a random n mode pure state.""" + S = draw(random_Sgate(num_modes)) I = draw(random_Interferometer(num_modes)) - D = draw(random_Dgate(num_modes, small=small)) + D = draw(random_Dgate(num_modes)) return Vacuum(num_modes) >> S >> I >> D + + +@st.composite +def n_mode_mixed_state(draw, num_modes=1): + r"""Return a random n mode mixed state.""" + state = draw(n_mode_pure_state(num_modes)) + attenuator = Attenuator(draw(st.floats(min_value=0.5, max_value=0.9))) + return state >> attenuator diff --git a/tests/test_lab/test_gates_fock.py b/tests/test_lab/test_gates_fock.py index 1a8550d98..a56f79869 100644 --- a/tests/test_lab/test_gates_fock.py +++ b/tests/test_lab/test_gates_fock.py @@ -22,11 +22,11 @@ mzgate, ) -from tests import random +from tests.random import * from mrmustard.physics import fock from mrmustard.lab.states import Fock, State, SqueezedVacuum, TMSV from mrmustard.physics import fock -from mrmustard.lab.gates import ( +from mrmustard.lab import ( Dgate, Sgate, Pgate, @@ -38,12 +38,12 @@ S2gate, Attenuator, Interferometer, + Vacuum, ) -@given(state=random.pure_state(num_modes=1), xy=random.vector(2)) -def test_Dgate_1mode(state, xy): - x, y = xy +@given(state=n_mode_pure_state(num_modes=1), x=medium_float, y=medium_float) +def test_Dgate_1mode(state, x, y): state_out = state >> Dgate(x, y) >> Dgate(-x, -y) assert state_out == state @@ -53,20 +53,18 @@ def test_attenuator_on_fock(): assert (Fock(10) >> Attenuator(0.5)).is_pure == False -@given(state=random.pure_state(num_modes=2), xxyy=random.vector(4)) +@given(state=n_mode_pure_state(num_modes=2), xxyy=array_of_(medium_float, minlen=4, maxlen=4)) def test_Dgate_2mode(state, xxyy): x1, x2, y1, y2 = xxyy state_out = state >> Dgate([x1, x2], [y1, y2]) >> Dgate([-x1, -x2], [-y1, -y2]) assert state_out == state -@pytest.mark.parametrize( - "gate", [Sgate(r=1), Dgate(1.0, 1.0), Pgate(10), Rgate(np.pi / 2), Attenuator(0.5)] -) +@given(gate=single_mode_cv_channel()) def test_single_mode_fock_equals_gaussian_dm(gate): """Test same state is obtained via fock representation or phase space for single mode circuits.""" - cutoffs = [30] + cutoffs = [60] gaussian_state = SqueezedVacuum(0.5) >> Attenuator(0.5) fock_state = State(dm=gaussian_state.dm(cutoffs)) @@ -75,7 +73,20 @@ def test_single_mode_fock_equals_gaussian_dm(gate): assert np.allclose(via_fock_space_dm, via_phase_space_dm) -@pytest.mark.parametrize("gate", [Sgate(r=1), Dgate(0.3, 0.3), Pgate(10), Rgate(np.pi / 2)]) +@given(gate=single_mode_cv_channel()) +def test_single_mode_fock_equals_gaussian_ket_dm(gate): + """Test same state is obtained via fock representation or phase space + for single mode circuits.""" + cutoffs = [60] + gaussian_state = SqueezedVacuum(0.5) + fock_state = State(ket=gaussian_state.ket(cutoffs)) + + via_fock_space_dm = (fock_state >> gate).dm([10]) + via_phase_space_dm = (gaussian_state >> gate).dm([10]) + assert np.allclose(via_fock_space_dm, via_phase_space_dm) + + +@given(gate=single_mode_unitary_gate()) def test_single_mode_fock_equals_gaussian_ket(gate): """Test same state is obtained via fock representation or phase space for single mode circuits.""" @@ -89,30 +100,20 @@ def test_single_mode_fock_equals_gaussian_ket(gate): assert np.allclose(via_fock_space_ket, phase * via_phase_space_ket) -@pytest.mark.parametrize( - "gate", - [ - Sgate(r=0.5, phi=0.2) >> Attenuator(0.4), - Dgate(0.4, 0.4) >> Attenuator(0.4), - Pgate(1) >> Attenuator(0.4), - Rgate(np.pi / 2) >> Attenuator(0.4), - ], -) -def test_single_mode_fock_equals_gaussian_ket_dm(gate): +@given(gate=single_mode_unitary_gate()) +def test_single_mode_fock_equals_gaussian_ket_dm_2(gate): """Test same state is obtained via fock representation or phase space for single mode circuits.""" - cutoffs = [60] - gaussian_state = SqueezedVacuum(0.5) + cutoffs = [40] + gaussian_state = SqueezedVacuum(0.3) fock_state = State(ket=gaussian_state.ket(cutoffs)) - via_fock_space_dm = (fock_state >> gate).dm([10]) - via_phase_space_dm = (gaussian_state >> gate).dm([10]) + via_fock_space_dm = (fock_state >> gate >> Attenuator(0.2)).dm([10]) + via_phase_space_dm = (gaussian_state >> gate >> Attenuator(0.2)).dm([10]) assert np.allclose(via_fock_space_dm, via_phase_space_dm) -@pytest.mark.parametrize( - "gate", [BSgate(np.pi / 2), MZgate(np.pi / 2), CZgate(0.1), CXgate(0.1), S2gate(0.1)] -) +@given(gate=two_mode_unitary_gate()) def test_two_mode_fock_equals_gaussian(gate): """Test same state is obtained via fock representation or phase space for two modes circuits.""" @@ -151,34 +152,28 @@ def test_fock_representation_displacement(cutoffs, x, y): assert np.allclose(Ud, expected_Ud, atol=1e-5) -@given(r=st.floats(min_value=0, max_value=2), phi=st.floats(min_value=0, max_value=2 * np.pi)) +@given(r=r, phi=angle) def test_fock_representation_squeezing(r, phi): S = Sgate(r=r, phi=phi) expected = squeezing(r=r, theta=phi, cutoff=20) assert np.allclose(expected, S.U(cutoffs=[20]), atol=1e-5) -@given( - theta=st.floats(min_value=0, max_value=2 * np.pi), - phi=st.floats(min_value=0, max_value=2 * np.pi), -) +@given(theta=angle, phi=angle) def test_fock_representation_beamsplitter(theta, phi): BS = BSgate(theta=theta, phi=phi) expected = beamsplitter(theta=theta, phi=phi, cutoff=20) assert np.allclose(expected, BS.U(cutoffs=[20, 20]), atol=1e-5) -@given(r=st.floats(min_value=0, max_value=2), phi=st.floats(min_value=0, max_value=2 * np.pi)) +@given(r=r, phi=angle) def test_fock_representation_two_mode_squeezing(r, phi): S2 = S2gate(r=r, phi=phi) expected = two_mode_squeezing(r=r, theta=phi, cutoff=20) assert np.allclose(expected, S2.U(cutoffs=[20, 20]), atol=1e-5) -@given( - phi_a=st.floats(min_value=0, max_value=2 * np.pi, allow_infinity=False, allow_nan=False), - phi_b=st.floats(min_value=0, max_value=2 * np.pi, allow_infinity=False, allow_nan=False), -) +@given(phi_a=angle, phi_b=angle) def test_fock_representation_mzgate(phi_a, phi_b): MZ = MZgate(phi_a=phi_a, phi_b=phi_b, internal=False) expected = mzgate(theta=phi_b, phi=phi_a, cutoff=20) diff --git a/tests/test_lab/test_states.py b/tests/test_lab/test_states.py index 3dcd9542a..e431d0c2b 100644 --- a/tests/test_lab/test_states.py +++ b/tests/test_lab/test_states.py @@ -30,7 +30,7 @@ from mrmustard.lab.gates import Attenuator, Sgate, Dgate, Ggate from mrmustard.lab.abstract import State from mrmustard import settings -from tests.random import pure_state +from tests.random import * from mrmustard.math import Math @@ -43,34 +43,26 @@ def xy_arrays(draw): return draw(arrays(dtype=np.float, shape=(2, length), elements=st.floats(-5.0, 5.0))) -@st.composite -def rphi_arrays(draw): - length = draw(st.integers(2, 10)) - r = arrays(dtype=np.float, shape=(2, length), elements=st.floats(0.0, 1.0)) - phi = arrays(dtype=np.float, shape=(2, length), elements=st.floats(0.0, 2 * np.pi)) - return r, phi - - -@given(st.integers(0, 10), st.floats(0.1, 5.0)) -def test_vacuum_state(num_modes, hbar): - cov, disp = gp.vacuum_cov(num_modes, hbar), gp.vacuum_means(num_modes, hbar) - assert np.allclose(cov, np.eye(2 * num_modes) * hbar / 2) +@given(nmodes, st.floats(0.1, 5.0)) +def test_vacuum_state(nmodes, hbar): + cov, disp = gp.vacuum_cov(nmodes, hbar), gp.vacuum_means(nmodes, hbar) + assert np.allclose(cov, np.eye(2 * nmodes) * hbar / 2) assert np.allclose(disp, np.zeros_like(disp)) -@given(x=st.floats(-5.0, 5.0), y=st.floats(-5.0, 5.0)) +@given(x=medium_float, y=medium_float) def test_coherent_state_single(x, y): state = Coherent(x, y) assert np.allclose(state.cov, np.array([[settings.HBAR / 2, 0], [0, settings.HBAR / 2]])) assert np.allclose(state.means, np.array([x, y]) * np.sqrt(2 * settings.HBAR)) -@given(hbar=st.floats(0.5, 2.0), x=st.floats(-5.0, 5.0), y=st.floats(-5.0, 5.0)) +@given(hbar=st.floats(0.5, 2.0), x=medium_float, y=medium_float) def test_coherent_state_list(hbar, x, y): assert np.allclose(gp.displacement([x], [y], hbar), np.array([x, y]) * np.sqrt(2 * hbar)) -@given(hbar=st.floats(0.5, 2.0), x=st.floats(-5.0, 5.0), y=st.floats(-5.0, 5.0)) +@given(hbar=st.floats(0.5, 2.0), x=medium_float, y=medium_float) def test_coherent_state_array(hbar, x, y): assert np.allclose( gp.displacement(np.array([x]), np.array([y]), hbar), @@ -87,10 +79,8 @@ def test_coherent_state_multiple(xy): assert np.allclose(state.means, np.concatenate([x, y], axis=-1) * np.sqrt(2 * settings.HBAR)) -@given(xy=xy_arrays()) -def test_the_purity_of_a_pure_state(xy): - x, y = xy - state = Coherent(x, y) +@given(state=n_mode_pure_state(num_modes=1)) +def test_the_purity_of_a_pure_state(state): purity = gp.purity(state.cov, settings.HBAR) expected = 1.0 assert np.isclose(purity, expected) @@ -104,12 +94,7 @@ def test_the_purity_of_a_mixed_state(nbar): assert np.isclose(purity, expected) -@given( - r1=st.floats(0.0, 1.0), - phi1=st.floats(0.0, 2 * np.pi), - r2=st.floats(0.0, 1.0), - phi2=st.floats(0.0, 2 * np.pi), -) +@given(r1=r, phi1=angle, r2=r, phi2=angle) def test_join_two_states(r1, phi1, r2, phi2): """Test Sgate acts the same in parallel or individually for two states.""" S1 = Vacuum(1) >> Sgate(r=r1, phi=phi1) @@ -118,14 +103,7 @@ def test_join_two_states(r1, phi1, r2, phi2): assert S1 & S2 == S12 -@given( - r1=st.floats(0.0, 1.0), - phi1=st.floats(0.0, 2 * np.pi), - r2=st.floats(0.0, 1.0), - phi2=st.floats(0.0, 2 * np.pi), - r3=st.floats(0.0, 1.0), - phi3=st.floats(0.0, 2 * np.pi), -) +@given(r1=r, phi1=angle, r2=r, phi2=angle, r3=r, phi3=angle) def test_join_three_states(r1, phi1, r2, phi2, r3, phi3): """Test Sgate acts the same in parallel or individually for three states.""" S1 = Vacuum(1) >> Sgate(r=r1, phi=phi1) @@ -142,17 +120,17 @@ def test_coh_state(xy): assert Vacuum(len(x)) >> Dgate(x, y) == Coherent(x, y) -@given(r=st.floats(0.0, 1.0), phi=st.floats(0.0, 2 * np.pi)) +@given(r=r, phi=angle) def test_sq_state(r, phi): """Test squeezed vacuum preparation.""" assert Vacuum(1) >> Sgate(r, phi) == SqueezedVacuum(r, phi) @given( - x=st.floats(-1.0, 1.0), - y=st.floats(-1.0, 1.0), - r=st.floats(0.0, 1.0), - phi=st.floats(0.0, 2 * np.pi), + x=medium_float, + y=medium_float, + r=r, + phi=angle, ) def test_dispsq_state(x, y, r, phi): """Test displaced squeezed state.""" diff --git a/tests/test_math/test_mmtensor.py b/tests/test_math/test_mmtensor.py index 4c26ecebb..254ab3ac7 100644 --- a/tests/test_math/test_mmtensor.py +++ b/tests/test_math/test_mmtensor.py @@ -71,3 +71,31 @@ def test_mmtensor_contract(): array = np.array([[1, 2], [3, 4]]) trace = MMTensor(array, axis_labels=["a", "a"]).contract().tensor assert trace == 5 + + +def test_mmtensor_contract_multiple_indices(): + """Test that MMTensor contracts multiple indices correctly""" + array = np.array([[[1, 2], [3, 4]], [[5, 6], [7, 8]]]) + mmtensor = MMTensor(array, axis_labels=["a", "a", "b"]) + trace = mmtensor.contract() + assert np.allclose(trace.tensor, np.einsum("aab", array)) + + +def test_mmtensor_matmul(): + """Test that MMTensor objects contract correctly using the @ operator""" + array1 = np.array([[1, 2], [3, 4]]) + array2 = np.array([[5, 6], [7, 8]]) + T1 = MMTensor(array1, axis_labels=["a", "b"]) + T2 = MMTensor(array2, axis_labels=["b", "c"]) + contracted_tensor = T1 @ T2 + assert np.allclose(contracted_tensor.tensor, np.array([[19, 22], [43, 50]])) + + +def test_mmtensor_matmul_high_rank(): + """Test that higher-rank MMTensor objects contract correctly using the @ operator""" + array1 = np.random.normal(size=(2, 3, 4, 5)) + array2 = np.random.normal(size=(5, 4, 3, 2)) + T1 = MMTensor(array1, axis_labels=["a", "b", "c", "d"]) + T2 = MMTensor(array2, axis_labels=["d", "c", "b", "e"]) + contracted_tensor = T1 @ T2 + assert np.allclose(contracted_tensor.tensor, np.einsum("abcd,dcbe->ae", array1, array2)) diff --git a/tests/test_fidelity.py b/tests/test_physics/test_fidelity.py similarity index 98% rename from tests/test_fidelity.py rename to tests/test_physics/test_fidelity.py index dd4929913..48308a861 100644 --- a/tests/test_fidelity.py +++ b/tests/test_physics/test_fidelity.py @@ -1,13 +1,12 @@ -import pytest - -from mrmustard import * import numpy as np -import tensorflow as tf -from thewalrus.random import random_covariance +import pytest from thewalrus.quantum import real_to_complex_displacements -from mrmustard.physics import gaussian as gp, fock as fp +from thewalrus.random import random_covariance +from mrmustard import * from mrmustard.math import Math +from mrmustard.physics import fock as fp +from mrmustard.physics import gaussian as gp math = Math() diff --git a/tests/test_settings.py b/tests/test_settings.py index 78667b24c..e1e93e552 100644 --- a/tests/test_settings.py +++ b/tests/test_settings.py @@ -33,7 +33,7 @@ def test_reproducibility(): """Test that the random state is reproducible.""" settings = Settings() settings.SEED = 42 - seq0 = [settings.rng.integers(0, 2**32) for _ in range(10)] + seq0 = [settings.rng.integers(0, 2**31 - 1) for _ in range(10)] settings.SEED = 42 - seq1 = [settings.rng.integers(0, 2**32) for _ in range(10)] + seq1 = [settings.rng.integers(0, 2**31 - 1) for _ in range(10)] assert seq0 == seq1 diff --git a/tests/test_utils/test_xptensor.py b/tests/test_utils/test_xptensor.py index a374c5544..b29983c84 100644 --- a/tests/test_utils/test_xptensor.py +++ b/tests/test_utils/test_xptensor.py @@ -18,7 +18,7 @@ from mrmustard.lab.states import DisplacedSqueezed from mrmustard.utils.xptensor import XPVector, XPMatrix import numpy as np -from tests.random import pure_state +from tests.random import n_mode_pure_state even = st.integers(min_value=2, max_value=10).filter(lambda x: x % 2 == 0) floats = st.floats(min_value=-1e3, max_value=1e3, allow_nan=False, allow_infinity=False) From 2b9b1827255bd28710c2b24223986514ea2ad17d Mon Sep 17 00:00:00 2001 From: Robbe De Prins <52749580+rdprins@users.noreply.github.com> Date: Tue, 28 Feb 2023 18:14:13 +0100 Subject: [PATCH 29/53] Compact fock (#154) **Context:** Make PNR sampling faster for Gaussian circuits when using density matrices. This is done by applying the recurrence relation in a selective manner such that useless (off-diagonal) amplitudes from the Fock representation are not calculated. When all modes are detected, `math.hermite_renormalized` can be replaced by `math.hermite_renormalized_diagonal`. In case all but the first mode are detected, `math.hermite_renormalized_1leftoverMode` can be used. The complexity of these new methods is equal to performing a pure state simulation. The methods are differentiable, such that they can be used for defining costfunctions. **Description of the Change:** Adds the function `math.hermite_renormalized_diagonal` and `math.hermite_renormalized_1leftoverMode`. **Benefits:** Faster simulation and optimization of Gaussian circuits with PNR detectors when using density matrices. --------- Co-authored-by: Robbe De Prins (UGent-imec) Co-authored-by: ziofil Co-authored-by: ziofil --- .github/CHANGELOG.md | 179 +++-- .gitignore | 2 +- doc/conf.py | 7 +- mrmustard/__init__.py | 5 +- mrmustard/lab/abstract/state.py | 9 +- .../numba/compactFock_1leftoverMode_amps.py | 336 ++++++++ .../numba/compactFock_1leftoverMode_grad.py | 741 ++++++++++++++++++ .../math/numba/compactFock_diagonal_amps.py | 180 +++++ .../math/numba/compactFock_diagonal_grad.py | 310 ++++++++ .../math/numba/compactFock_helperFunctions.py | 45 ++ .../math/numba/compactFock_inputValidation.py | 83 ++ mrmustard/math/tensorflow.py | 116 ++- mrmustard/physics/fock.py | 39 +- mrmustard/training/trainer.py | 11 +- tests/test_lab/test_detectors.py | 6 +- tests/test_lab/test_gates_fock.py | 10 +- tests/test_math/test_compactFock.py | 116 +++ 17 files changed, 2069 insertions(+), 126 deletions(-) create mode 100644 mrmustard/math/numba/compactFock_1leftoverMode_amps.py create mode 100644 mrmustard/math/numba/compactFock_1leftoverMode_grad.py create mode 100644 mrmustard/math/numba/compactFock_diagonal_amps.py create mode 100644 mrmustard/math/numba/compactFock_diagonal_grad.py create mode 100644 mrmustard/math/numba/compactFock_helperFunctions.py create mode 100644 mrmustard/math/numba/compactFock_inputValidation.py create mode 100644 tests/test_math/test_compactFock.py diff --git a/.github/CHANGELOG.md b/.github/CHANGELOG.md index edfdd6ebd..7240b04b3 100644 --- a/.github/CHANGELOG.md +++ b/.github/CHANGELOG.md @@ -2,80 +2,81 @@ ### New features - * Ray-based distributed trainer is now added to `training.trainer`. It acts as a replacement for `for` loops and enables the parallelization of running many circuits as well as their optimizations. To install the extra dependencies: `pip install .[ray]`. +* Ray-based distributed trainer is now added to `training.trainer`. It acts as a replacement for `for` loops and enables + the parallelization of running many circuits as well as their optimizations. To install the extra dependencies: `pip install .[ray]`. [(#194)](https://github.com/XanaduAI/MrMustard/pull/194) - ```python - from mrmustard.lab import Vacuum, Dgate, Ggate - from mrmustard.physics import fidelity - from mrmustard.training.trainer import map_trainer - - def make_circ(x=0.): - return Ggate(num_modes=1, symplectic_trainable=True) >> Dgate(x=x, x_trainable=True, y_trainable=True) - - def cost_fn(circ=make_circ(0.1), y_targ=0.): - target = Gaussian(1) >> Dgate(-1.5, y_targ) - s = Vacuum(1) >> circ - return -fidelity(s, target) - - # Use case 0: Calculate the cost of a randomly initialized circuit 5 times without optimizing it. - results_0 = map_trainer( - cost_fn=cost_fn, - tasks=5, - ) - - # Use case 1: Run circuit optimization 5 times on randomly initialized circuits. - results_1 = map_trainer( - cost_fn=cost_fn, - device_factory=make_circ, - tasks=5, - max_steps=50, - symplectic_lr=0.05, - ) - - # Use case 2: Run circuit optimization 2 times on randomly initialized circuits with custom parameters. - results_2 = map_trainer( - cost_fn=cost_fn, - device_factory=make_circ, - tasks=[ - {'x': 0.1, 'euclidean_lr': 0.005, 'max_steps': 50, 'HBAR': 1.}, - {'x': -0.7, 'euclidean_lr': 0.1, 'max_steps': 2, 'HBAR': 2.}, - ], - y_targ=0.35, - symplectic_lr=0.05, - AUTOCUTOFF_MAX_CUTOFF=7, - ) - ``` + ```python + from mrmustard.lab import Vacuum, Dgate, Ggate + from mrmustard.physics import fidelity + from mrmustard.training.trainer import map_trainer + + def make_circ(x=0.): + return Ggate(num_modes=1, symplectic_trainable=True) >> Dgate(x=x, x_trainable=True, y_trainable=True) + + def cost_fn(circ=make_circ(0.1), y_targ=0.): + target = Gaussian(1) >> Dgate(-1.5, y_targ) + s = Vacuum(1) >> circ + return -fidelity(s, target) + + # Use case 0: Calculate the cost of a randomly initialized circuit 5 times without optimizing it. + results_0 = map_trainer( + cost_fn=cost_fn, + tasks=5, + ) + + # Use case 1: Run circuit optimization 5 times on randomly initialized circuits. + results_1 = map_trainer( + cost_fn=cost_fn, + device_factory=make_circ, + tasks=5, + max_steps=50, + symplectic_lr=0.05, + ) + + # Use case 2: Run circuit optimization 2 times on randomly initialized circuits with custom parameters. + results_2 = map_trainer( + cost_fn=cost_fn, + device_factory=make_circ, + tasks=[ + {'x': 0.1, 'euclidean_lr': 0.005, 'max_steps': 50, 'HBAR': 1.}, + {'x': -0.7, 'euclidean_lr': 0.1, 'max_steps': 2, 'HBAR': 2.}, + ], + y_targ=0.35, + symplectic_lr=0.05, + AUTOCUTOFF_MAX_CUTOFF=7, + ) + ``` * Sampling for homodyne measurements is now integrated in Mr Mustard: when no measurement outcome value is specified by the user, a value is sampled from the reduced state probability distribution and the conditional state on the remaining modes is generated. [(#143)](https://github.com/XanaduAI/MrMustard/pull/143) - ```python - import numpy as np - from mrmustard.lab import Homodyne, TMSV, SqueezedVacuum + ```python + import numpy as np + from mrmustard.lab import Homodyne, TMSV, SqueezedVacuum - # conditional state from measurement - conditional_state = TMSV(r=0.5, phi=np.pi)[0, 1] >> Homodyne(quadrature_angle=np.pi/2)[1] + # conditional state from measurement + conditional_state = TMSV(r=0.5, phi=np.pi)[0, 1] >> Homodyne(quadrature_angle=np.pi/2)[1] - # measurement outcome - measurement_outcome = SqueezedVacuum(r=0.5) >> Homodyne() - ``` + # measurement outcome + measurement_outcome = SqueezedVacuum(r=0.5) >> Homodyne() + ``` * The optimizer `minimize` method now accepts an optional callback function, which will be called at each step of the optimization and it will be passed the step number, the cost value, and the value of the trainable parameters. The result is added to the `callback_history` attribute of the optimizer. [(#175)](https://github.com/XanaduAI/MrMustard/pull/175) +* the Math interface now supports linear system solving via `math.solve`. + [(#185)](https://github.com/XanaduAI/MrMustard/pull/185) + * We introduce the tensor wrapper `MMTensor` (available in `math.mmtensor`) that allows for a very easy handling of tensor contractions. Internally MrMustard performs lots of tensor contractions and this wrapper allows one to label each index of a tensor and perform contractions using the `@` symbol as if it were a simple matrix multiplication (the indices with the same name get contracted). [(#185)](https://github.com/XanaduAI/MrMustard/pull/185) - * the Math interface now supports linear system solving via `math.solve`. - [(#185)](https://github.com/XanaduAI/MrMustard/pull/185) - ```python from mrmustard.math.mmtensor import MMTensor @@ -101,34 +102,40 @@ the number of decimals shown in the ascii representation. If None only the name of the gate is shown. [(#196)](https://github.com/XanaduAI/MrMustard/pull/196) - +* PNR sampling from Gaussian circuits using density matrices can now be performed faster. When all modes are detected, + this is done by replacing `math.hermite_renormalized` by `math.hermite_renormalized_diagonal`. + In case all but the first mode are detected, `math.hermite_renormalized_1leftoverMode` can be used. + The complexity of these new methods is equal to performing a pure state simulation. + The methods are differentiable, such that they can be used for defining a costfunction. + [(#154)](https://github.com/XanaduAI/MrMustard/pull/154) + ### Breaking changes ### Improvements * The `Dgate` now uses The Walrus to calculate the unitary and gradients of the displacement gate in fock representation, -providing better numerical stability for larger cutoff and displacement values. + providing better numerical stability for larger cutoff and displacement values. [(#147)](https://github.com/XanaduAI/MrMustard/pull/147) * Now the Wigner function is implemented in its own module and uses numba for speed. [(#171)](https://github.com/XanaduAI/MrMustard/pull/171) - ```python - from mrmustard.utils.wigner import wigner_discretized - W, Q, P = wigner_discretized(dm, q, p) # dm is a density matrix - ``` + ```python + from mrmustard.utils.wigner import wigner_discretized + W, Q, P = wigner_discretized(dm, q, p) # dm is a density matrix + ``` * Calculate marginals independently from the Wigner function thus ensuring that the marginals are -physical even though the Wigner function might not contain all the features of the state -within the defined window. Also, expose some plot parameters and return the figure and axes. + physical even though the Wigner function might not contain all the features of the state + within the defined window. Also, expose some plot parameters and return the figure and axes. [(#179)](https://github.com/XanaduAI/MrMustard/pull/179) * Allows for full cutoff specification (index-wise rather than mode-wise) for subclasses of `Transformation`. -This allows for a more compact Fock representation where needed. + This allows for a more compact Fock representation where needed. [(#181)](https://github.com/XanaduAI/MrMustard/pull/181) * The `mrmustard.physics.fock` module now provides convenience functions for applying kraus operators and -choi operators to kets and density matrices. + choi operators to kets and density matrices. [(#180)](https://github.com/XanaduAI/MrMustard/pull/180) ```python @@ -140,45 +147,49 @@ choi operators to kets and density matrices. ``` * Replaced norm with probability in the repr of `State`. This improves consistency over the old behaviour -(norm was the sqrt of prob if the state was pure and prob if the state was mixed). + (norm was the sqrt of prob if the state was pure and prob if the state was mixed). [(#182)](https://github.com/XanaduAI/MrMustard/pull/182) * Added two new modules (`physics.bargmann` and `physics.husimi`) to host the functions related to those representation, -which have been refactored and moved out of `physics.fock`. + which have been refactored and moved out of `physics.fock`. [(#185)](https://github.com/XanaduAI/MrMustard/pull/185) * Added multiple tests and improved the use of Hypothesis. [(#191)](https://github.com/XanaduAI/MrMustard/pull/191) +* The `fock.autocutoff` function now uses the new diagonal methods for calculating a probability-based cutoff. + Use `settings.AUTOCUTOFF_PROBABILITY` to set the probability threshold. + [(#203)](https://github.com/XanaduAI/MrMustard/pull/203) + ### Bug fixes * The `Dgate` and the `Rgate` now correctly parse the case when a single scalar is intended as the same parameter -of a number of gates in pallel. - [(#180)](https://github.com/XanaduAI/MrMustard/pull/180) + of a number of gates in pallel. + [(#180)](https://github.com/XanaduAI/MrMustard/pull/180) * The trace function in the fock module was giving incorrect results when called with certain choices of modes. -This is now fixed. - [(#180)](https://github.com/XanaduAI/MrMustard/pull/180) + This is now fixed. + [(#180)](https://github.com/XanaduAI/MrMustard/pull/180) * The purity function for fock states no longer normalizes the density matrix before computing the purity. - [(#180)](https://github.com/XanaduAI/MrMustard/pull/180) + [(#180)](https://github.com/XanaduAI/MrMustard/pull/180) * The function `dm_to_ket` no longer normalizes the density matrix before diagonalizing it. - [(#180)](https://github.com/XanaduAI/MrMustard/pull/180) + [(#180)](https://github.com/XanaduAI/MrMustard/pull/180) * The internal fock representation of states returns the correct cutoffs in all cases (solves an issue when -a pure dm was converted to ket). -[(#184)](https://github.com/XanaduAI/MrMustard/pull/184) + a pure dm was converted to ket). + [(#184)](https://github.com/XanaduAI/MrMustard/pull/184) * The ray related tests were hanging in github action causing test to halt and fail. Now ray is forced to init with 1 cpu when running tests preventing the issue. -[(#201)](https://github.com/XanaduAI/MrMustard/pull/201) + [(#201)](https://github.com/XanaduAI/MrMustard/pull/201) ### Documentation ### Contributors This release contains contributions from (in alphabetical order): -[Sebastian Duque Mesa](https://github.com/sduquemesa), [Filippo Miatto](https://github.com/ziofil) +[Robbe De Prins](https://github.com/rdprins), [Sebastian Duque Mesa](https://github.com/sduquemesa), [Filippo Miatto](https://github.com/ziofil) --- @@ -208,15 +219,15 @@ This release contains contributions from (in alphabetical order): [(#130)](https://github.com/XanaduAI/MrMustard/pull/130) * Parameter passthrough allows one to use custom variables and/or functions as parameters. For example we can use parameters of other gates: - ```python - from mrmustard.lab.gates import Sgate, BSgate + ```python + from mrmustard.lab.gates import Sgate, BSgate - BS = BSgate(theta=np.pi/4, theta_trainable=True)[0,1] - S0 = Sgate(r=BS.theta)[0] - S1 = Sgate(r=-BS.theta)[1] + BS = BSgate(theta=np.pi/4, theta_trainable=True)[0,1] + S0 = Sgate(r=BS.theta)[0] + S1 = Sgate(r=-BS.theta)[1] - circ = S0 >> S1 >> BS - ``` + circ = S0 >> S1 >> BS + ``` Another possibility is with functions: ```python @@ -245,7 +256,6 @@ This release contains contributions from (in alphabetical order): ``` [(#140)](https://github.com/XanaduAI/MrMustard/pull/140) - ### Breaking changes * The Parametrized and Training classes have been refactored: now trainable tensors are wrapped in an instance of the `Parameter` class. To define a set of parameters do @@ -300,8 +310,8 @@ This release contains contributions from (in alphabetical order): applied to. [(#121)](https://github.com/XanaduAI/MrMustard/pull/121) - ### Bug fixes + * Fixed a bug in the `State.ket()` method. An attribute was called with a typo in its name. [(#135)](https://github.com/XanaduAI/MrMustard/pull/135) @@ -372,6 +382,7 @@ This release contains contributions from (in alphabetical order): * [Basic API reference](https://mrmustard.readthedocs.io/en/latest/introduction/basic_reference.html) is updated to use the latest Mr Mustard API. [(#119)](https://github.com/XanaduAI/MrMustard/pull/119) + ### Contributors This release contains contributions from (in alphabetical order): diff --git a/.gitignore b/.gitignore index d148ce031..103737aee 100644 --- a/.gitignore +++ b/.gitignore @@ -17,4 +17,4 @@ __pycache__/ doc/_build doc/code/api/* coverage.xml -.coverage +.coverage \ No newline at end of file diff --git a/doc/conf.py b/doc/conf.py index 6d556661f..adf2c0036 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -136,7 +136,6 @@ html_theme_options = { "navbar_name": "Mr Mustard", "navbar_logo_path": "_static/mm_logo.png", - "navbar_right_links": [ { "name": "GitHub", @@ -144,14 +143,10 @@ "icon": "fab fa-github", } ], - "extra_copyrights": [ - "TensorFlow, the TensorFlow logo, and any related marks are trademarks " - "of Google Inc." + "TensorFlow, the TensorFlow logo, and any related marks are trademarks " "of Google Inc." ], - "google_analytics_tracking_id": "UA-116279123-2", - "prev_next_button_colour": "#b79226", "prev_next_button_hover_colour": "#d7b348", "toc_marker_colour": "#b79226", diff --git a/mrmustard/__init__.py b/mrmustard/__init__.py index 6d8678aaf..a6556e73f 100644 --- a/mrmustard/__init__.py +++ b/mrmustard/__init__.py @@ -35,12 +35,11 @@ def __init__(self): self.HBAR = 2.0 self.CHOI_R = 0.881373587019543 # np.arcsinh(1.0) self.DEBUG = False - # clip(mean + 5*std, min, max) when auto-detecting the Fock cutoff - self.AUTOCUTOFF_STDEV_FACTOR = 5 + self.AUTOCUTOFF_PROBABILITY = 0.999 # capture at least 99.9% of the probability self.AUTOCUTOFF_MAX_CUTOFF = 100 self.AUTOCUTOFF_MIN_CUTOFF = 1 self.CIRCUIT_DECIMALS = 3 - # using cutoff=5 for each mode when determining if two transformations in fock repr are equal + # use cutoff=5 for each mode when determining if two transformations in fock repr are equal self.EQ_TRANSFORMATION_CUTOFF = 5 self.EQ_TRANSFORMATION_RTOL_FOCK = 1e-3 self.EQ_TRANSFORMATION_RTOL_GAUSS = 1e-6 diff --git a/mrmustard/lab/abstract/state.py b/mrmustard/lab/abstract/state.py index 494d3ef4a..c04068d5e 100644 --- a/mrmustard/lab/abstract/state.py +++ b/mrmustard/lab/abstract/state.py @@ -173,10 +173,7 @@ def cutoffs(self) -> List[int]: if self._cutoffs is not None: return self._cutoffs # TODO: allow self._cutoffs = [N, None] if self._ket is None and self._dm is None: - return fock.autocutoffs( - self.number_stdev, self.number_means - ) # TODO: move autocutoffs in gaussian.py and pass cov, means - + return fock.autocutoffs(self.cov, self.means, settings.AUTOCUTOFF_PROBABILITY) return list( self.fock.shape[: self.num_modes] ) # NOTE: triggered only if the fock representation already exists @@ -338,9 +335,11 @@ def primal(self, other: Union[State, Transformation]) -> State: Note that the returned state is not normalized. To normalize a state you can use ``mrmustard.physics.normalize``. """ + # import pdb + + # pdb.set_trace() if isinstance(other, State): return self._project_onto_state(other) - try: return other.dual(self) except AttributeError as e: diff --git a/mrmustard/math/numba/compactFock_1leftoverMode_amps.py b/mrmustard/math/numba/compactFock_1leftoverMode_amps.py new file mode 100644 index 000000000..f6751f30a --- /dev/null +++ b/mrmustard/math/numba/compactFock_1leftoverMode_amps.py @@ -0,0 +1,336 @@ +""" +This module calculates all possible Fock representations of mode 0,where all other modes are PNR detected. +This is done by applying the recursion relation in a selective manner. +""" + +import numpy as np +import numba +from numba import njit, int64 +from numba.cpython.unsafe.tuple import tuple_setitem +from mrmustard.math.numba.compactFock_helperFunctions import ( + SQRT, + repeat_twice, + construct_dict_params, +) + + +@njit +def write_block( + i, arr_write, write, arr_read_pivot, read_GB, G_in, GB, A, K_i, cutoff_leftoverMode +): + """ + Apply the recurrence relation to blocks of Fock amplitudes (of shape cutoff_leftoverMode x cutoff_leftoverMode) + This is the coarse-grained version of applying the recurrence relation of mrmustard.math.numba.compactFock_diagonal_amps once. + """ + m, n = 0, 0 + A_adapted = A[i, 2:] + G_in_adapted = G_in[0, 0] + arr_write[(0, 0) + write] = (GB[0, 0, i] + A_adapted @ G_in_adapted) / K_i[i - 2] + + m = 0 + A_adapted = A[i, 1:] + for n in range(1, cutoff_leftoverMode): + G_in_adapted = np.hstack( + (np.array([arr_read_pivot[(0, n - 1) + read_GB] * np.sqrt(n)]), G_in[0, n]) + ) + arr_write[(0, n) + write] = (GB[0, n, i] + A_adapted @ G_in_adapted) / K_i[i - 2] + + n = 0 + A_adapted = np.hstack((np.array([A[i, 0]]), A[i, 2:])) + for m in range(1, cutoff_leftoverMode): + G_in_adapted = np.hstack( + (np.array([arr_read_pivot[(m - 1, 0) + read_GB] * np.sqrt(m)]), G_in[m, 0]) + ) + arr_write[(m, 0) + write] = (GB[m, 0, i] + A_adapted @ G_in_adapted) / K_i[i - 2] + + A_adapted = A[i] + for m in range(1, cutoff_leftoverMode): + for n in range(1, cutoff_leftoverMode): + G_in_adapted = np.hstack( + ( + np.array( + [ + arr_read_pivot[(m - 1, n) + read_GB] * np.sqrt(m), + arr_read_pivot[(m, n - 1) + read_GB] * np.sqrt(n), + ] + ), + G_in[m, n], + ) + ) + arr_write[(m, n) + write] = (GB[m, n, i] + A_adapted @ G_in_adapted) / K_i[i - 2] + return arr_write + + +@njit +def read_block(arr_write, idx_write, arr_read, idx_read_tail, cutoff_leftoverMode): + """ + Read the blocks of Fock amplitudes (of shape cutoff_leftoverMode x cutoff_leftoverMode) + that are required to apply the recurrence relation and write them to G_in + """ + for m in range(cutoff_leftoverMode): + for n in range(cutoff_leftoverMode): + arr_write[m, n, idx_write] = arr_read[ + ( + m, + n, + ) + + idx_read_tail + ] + return arr_write + + +@njit +def use_offDiag_pivot( + A, B, M, cutoff_leftoverMode, cutoffs_tail, params, d, arr0, arr2, arr1010, arr1001, arr1 +): + """ + Apply recurrence relation for pivot of type [a+1,a,b,b,c,c,...] / [a,a,b+1,b,c,c,...] / [a,a,b,b,c+1,c,...] + Args: + A, B (array, Vector): required input for recurrence relation (given by mrmustard.physics.fock.ABC) + M (int): number of detected modes + cutoffs (tuple): upper bounds for the number of photons in each mode + params (tuple): (a,b,c,...) + d (int): mode index in which the considered Fock amplitude is off diagonal + e.g. [a,a,b+1,b,c,c,...] --> b is off diagonal --> d=1 + arr0, arr2, arr1010, arr1001, arr1 (array, array, array, array, array): submatrices of the fock representation + Returns: + (array, array, array, array, array): updated versions of arr0, arr2, arr1010, arr1001, arr1 + """ + pivot = repeat_twice(params) + pivot[2 * d] += 1 + K_l = SQRT[pivot] + K_i = SQRT[pivot + 1] + G_in = np.zeros((cutoff_leftoverMode, cutoff_leftoverMode, 2 * M), dtype=np.complex128) + + ########## READ ########## + read_GB = (2 * d,) + params + GB = np.empty((cutoff_leftoverMode, cutoff_leftoverMode, len(B)), dtype=np.complex128) + for m in range(cutoff_leftoverMode): + for n in range(cutoff_leftoverMode): + GB[m, n] = arr1[(m, n) + read_GB] * B + + # Array0 + G_in = read_block(G_in, 2 * d, arr0, params, cutoff_leftoverMode) + + # read from Array2 + if params[d] > 0: + params_adapted = tuple_setitem(params, d, params[d] - 1) + G_in = read_block(G_in, 2 * d + 1, arr2, (d,) + params_adapted, cutoff_leftoverMode) + + # read from Array11 + for i in range(d + 1, M): # i>d + if params[i] > 0: + params_adapted = tuple_setitem(params, i, params[i] - 1) + G_in = read_block( + G_in, 2 * i, arr1001, (d, i - d - 1) + params_adapted, cutoff_leftoverMode + ) + G_in = read_block( + G_in, 2 * i + 1, arr1010, (d, i - d - 1) + params_adapted, cutoff_leftoverMode + ) + + ########## WRITE ########## + G_in = np.multiply(K_l, G_in) + + # Array0 + params_adapted = tuple_setitem(params, d, params[d] + 1) + write = params_adapted + arr0 = write_block(2 * d + 3, arr0, write, arr1, read_GB, G_in, GB, A, K_i, cutoff_leftoverMode) + + # Array2 + if params[d] + 2 < cutoffs_tail[d]: + write = (d,) + params + arr2 = write_block( + 2 * d + 2, arr2, write, arr1, read_GB, G_in, GB, A, K_i, cutoff_leftoverMode + ) + + # Array11 + for i in range(d + 1, M): + if params[i] + 1 < cutoffs_tail[i]: + write = (d, i - d - 1) + params + arr1010 = write_block( + 2 * i + 2, arr1010, write, arr1, read_GB, G_in, GB, A, K_i, cutoff_leftoverMode + ) + arr1001 = write_block( + 2 * i + 3, arr1001, write, arr1, read_GB, G_in, GB, A, K_i, cutoff_leftoverMode + ) + + return arr0, arr2, arr1010, arr1001 + + +@njit +def use_diag_pivot(A, B, M, cutoff_leftoverMode, cutoffs_tail, params, arr0, arr1): + """ + Apply recurrence relation for pivot of type [a,a,b,b,c,c...] + Args: + A, B (array, Vector): required input for recurrence relation (given by mrmustard.physics.fock.ABC) + M (int): number of detected modes + cutoffs (tuple): upper bounds for the number of photons in each mode + params (tuple): (a,b,c,...) + arr0, arr1 (array, array): submatrices of the fock representation + Returns: + (array, array): updated versions of arr0, arr1 + """ + pivot = repeat_twice(params) + K_l = SQRT[pivot] + K_i = SQRT[pivot + 1] + G_in = np.zeros((cutoff_leftoverMode, cutoff_leftoverMode, 2 * M), dtype=np.complex128) + + ########## READ ########## + read_GB = params + GB = np.empty((cutoff_leftoverMode, cutoff_leftoverMode, len(B)), dtype=np.complex128) + for m in range(cutoff_leftoverMode): + for n in range(cutoff_leftoverMode): + GB[m, n] = arr0[(m, n) + read_GB] * B + + # Array1 + for i in range(2 * M): + if params[i // 2] > 0: + params_adapted = tuple_setitem(params, i // 2, params[i // 2] - 1) + G_in = read_block( + G_in, i, arr1, (i + 1 - 2 * (i % 2),) + params_adapted, cutoff_leftoverMode + ) # [i+1-2*(i%2) for i in range(6)] == [1,0,3,2,5,4] + + ########## WRITE ########## + for m in range(cutoff_leftoverMode): + for n in range(cutoff_leftoverMode): + G_in[m, n] = np.multiply(K_l, G_in[m, n]) + + # Array1 + for i in range(2 * M): + if params[i // 2] + 1 < cutoffs_tail[i // 2]: + # this if statement prevents a few elements from being written that will never be read + if i != 1 or params[0] + 2 < cutoffs_tail[0]: + write = (i,) + params + arr1 = write_block( + i + 2, arr1, write, arr0, read_GB, G_in, GB, A, K_i, cutoff_leftoverMode + ) + + return arr1 + + +@njit +def fock_representation_1leftoverMode_amps_NUMBA( + A, + B, + M, + cutoff_leftoverMode, + cutoffs_tail, + arr0, + arr2, + arr1010, + arr1001, + arr1, + tuple_type, + list_type, + zero_tuple, +): + """ + Returns the PNR probabilities of a state or Choi state (by using the recurrence relation to calculate a limited number of Fock amplitudes) + Args: + A, B (array, vector): required input for recurrence relation (given by mrmustard.physics.fock.ABC) + M (int): number of modes + cutoffs (tuple): upper bounds for the number of photons in each mode + arr0 (array): submatrix of the fock representation that contains Fock amplitudes of the type [a,a,b,b,c,c...] + (!) should already contain G0 at position (0,)*M + arr2 (array): submatrix of the fock representation that contains Fock amplitudes of the types [a+2,a,b,b,c,c...] / [a,a,b+2,b,c,c...] / ... + arr1010 (array): submatrix of the fock representation that contains Fock amplitudes of the types [a+1,a,b+1,b,c,c,...] / [a+1,a,b,b,c+1,c,...] / [a,a,b+1,b,c+1,c,...] / ... + arr1001 (array): submatrix of the fock representation that contains Fock amplitudes of the types [a+1,a,b,b+1,c,c,...] / [a+1,a,b,b,c,c+1,...] / [a,a,b+1,b,c,c+1,...] / ... + arr1 (array): submatrix of the fock representation that contains Fock amplitudes of the types [a+1,a,b,b,c,c...] / [a,a+1,b,b,c,c...] / [a,a,b+1,b,c,c...] / ... + tuple_type, list_type (Numba types): numba types that need to be defined outside of numba compiled functions + Returns: + Tensor: the fock representation + """ + + # fill first mode for all PNR detections equal to zero + for m in range(cutoff_leftoverMode - 1): + arr0[(m + 1, 0) + zero_tuple] = ( + arr0[(m, 0) + zero_tuple] * B[0] + np.sqrt(m) * A[0, 0] * arr0[(m - 1, 0) + zero_tuple] + ) / np.sqrt(m + 1) + for m in range(cutoff_leftoverMode): + for n in range(cutoff_leftoverMode - 1): + arr0[(m, n + 1) + zero_tuple] = ( + arr0[(m, n) + zero_tuple] * B[1] + + np.sqrt(m) * A[1, 0] * arr0[(m - 1, n) + zero_tuple] + + np.sqrt(n) * A[1, 1] * arr0[(m, n - 1) + zero_tuple] + ) / np.sqrt(n + 1) + + dict_params = construct_dict_params(cutoffs_tail, tuple_type, list_type) + for sum_params in range(sum(cutoffs_tail)): + for params in dict_params[sum_params]: + # diagonal pivots: aa,bb,cc,dd,... + if params[0] < cutoffs_tail[0] - 1: + arr1 = use_diag_pivot( + A, B, M - 1, cutoff_leftoverMode, cutoffs_tail, params, arr0, arr1 + ) + # off-diagonal pivots: d=0: (a+1)a,bb,cc,dd,... | d=1: 00,(b+1)b,cc,dd | 00,00,(c+1)c,dd | ... + for d in range(M - 1): + if np.all(np.array(params)[:d] == 0) and (params[d] < cutoffs_tail[d] - 1): + arr0, arr2, arr1010, arr1001 = use_offDiag_pivot( + A, + B, + M - 1, + cutoff_leftoverMode, + cutoffs_tail, + params, + d, + arr0, + arr2, + arr1010, + arr1001, + arr1, + ) + return arr0, arr2, arr1010, arr1001, arr1 + + +def fock_representation_1leftoverMode_amps(A, B, G0, M, cutoffs): + """ + First initialise the submatrices of G (of which the shape depends on cutoff and M) + and some other constants + (These initialisations currently cannot be done using Numba.) + Then calculate the fock representation. + """ + + cutoff_leftoverMode = cutoffs[0] + cutoffs_tail = tuple(cutoffs[1:]) + tuple_type = numba.types.UniTuple(int64, M - 1) + list_type = numba.types.ListType(tuple_type) + zero_tuple = (0,) * (M - 1) + + arr0 = np.zeros( + (cutoff_leftoverMode, cutoff_leftoverMode) + cutoffs_tail, dtype=np.complex128 + ) # doesn't work with np.empty + arr0[(0,) * (M + 1)] = G0 + arr2 = np.empty( + (cutoff_leftoverMode, cutoff_leftoverMode) + (M - 1,) + cutoffs_tail, dtype=np.complex128 + ) + arr1 = np.empty( + (cutoff_leftoverMode, cutoff_leftoverMode) + (2 * (M - 1),) + cutoffs_tail, + dtype=np.complex128, + ) + if M == 2: + arr1010 = np.empty((1, 1, 1, 1, 1), dtype=np.complex128) + arr1001 = np.empty((1, 1, 1, 1, 1), dtype=np.complex128) + else: + arr1010 = np.empty( + (cutoff_leftoverMode, cutoff_leftoverMode) + (M - 1, M - 2) + cutoffs_tail, + dtype=np.complex128, + ) + arr1001 = np.empty( + (cutoff_leftoverMode, cutoff_leftoverMode) + (M - 1, M - 2) + cutoffs_tail, + dtype=np.complex128, + ) + return fock_representation_1leftoverMode_amps_NUMBA( + A, + B, + M, + cutoff_leftoverMode, + cutoffs_tail, + arr0, + arr2, + arr1010, + arr1001, + arr1, + tuple_type, + list_type, + zero_tuple, + ) diff --git a/mrmustard/math/numba/compactFock_1leftoverMode_grad.py b/mrmustard/math/numba/compactFock_1leftoverMode_grad.py new file mode 100644 index 000000000..0bb14c6c5 --- /dev/null +++ b/mrmustard/math/numba/compactFock_1leftoverMode_grad.py @@ -0,0 +1,741 @@ +""" +This module calculates the derivatives for all possible Fock representations of mode 0, where all other modes are PNR detected. +This is done by applying the derivated recursion relation in a selective manner. +""" + +import numpy as np +import numba +from numba import njit, int64 +from numba.cpython.unsafe.tuple import tuple_setitem +from mrmustard.math.numba.compactFock_helperFunctions import ( + SQRT, + repeat_twice, + construct_dict_params, +) + + +@njit +def calc_dA_dB( + m, + n, + i, + arr_read_pivot, + read_GB, + G_in_adapted, + A_adapted, + B, + K_i, + K_l_adapted, + arr_read_pivot_dA, + G_in_dA_adapted, + arr_read_pivot_dB, + G_in_dB_adapted, + l_range, +): + """ + Apply the derivated recurrence relation. + """ + dA = arr_read_pivot_dA[(m, n) + read_GB] * B[i] + dB = arr_read_pivot_dB[(m, n) + read_GB] * B[i] + dB[i] += arr_read_pivot[(m, n) + read_GB] + for l_prime, l in enumerate(l_range): + dA += K_l_adapted[l_prime] * A_adapted[l_prime] * G_in_dA_adapted[l_prime] + dB += K_l_adapted[l_prime] * A_adapted[l_prime] * G_in_dB_adapted[l_prime] + dA[i, l] += G_in_adapted[l_prime] + return dA / K_i[i - 2], dB / K_i[i - 2] + + +@njit +def write_block_grad( + i, + write, + arr_read_pivot, + read_GB, + G_in, + A, + B, + K_i, + K_l, + cutoff_leftoverMode, + arr_write_dA, + arr_read_pivot_dA, + G_in_dA, + arr_write_dB, + arr_read_pivot_dB, + G_in_dB, +): + """ + Apply the derivated recurrence relation to blocks of Fock amplitudes (of shape cutoff_leftoverMode x cutoff_leftoverMode) + This is the coarse-grained version of applying the derivated recurrence relation of mrmustard.math.numba.compactFock_diagonal_grad once. + """ + # m,n = 0,0 + m, n = 0, 0 + l_range = np.arange(2, A.shape[1]) + A_adapted = A[i, 2:] + G_in_adapted = G_in[0, 0] + G_in_dA_adapted = G_in_dA[0, 0] + G_in_dB_adapted = G_in_dB[0, 0] + K_l_adapted = K_l + arr_write_dA[(0, 0) + write], arr_write_dB[(0, 0) + write] = calc_dA_dB( + m, + n, + i, + arr_read_pivot, + read_GB, + G_in_adapted, + A_adapted, + B, + K_i, + K_l_adapted, + arr_read_pivot_dA, + G_in_dA_adapted, + arr_read_pivot_dB, + G_in_dB_adapted, + l_range, + ) + # m=0 + m = 0 + l_range = np.arange(1, A.shape[1]) + A_adapted = A[i, 1:] + for n in range(1, cutoff_leftoverMode): + K_l_adapted = np.hstack((np.array([np.sqrt(n)]), K_l)) + G_in_adapted = np.hstack( + (np.array([arr_read_pivot[(0, n - 1) + read_GB] * np.sqrt(n)]), G_in[0, n]) + ) + G_in_dA_adapted = np.concatenate( + ( + np.expand_dims(arr_read_pivot_dA[(0, n - 1) + read_GB], axis=0), + G_in_dA[0, n], + ), + axis=0, + ) + G_in_dB_adapted = np.concatenate( + ( + np.expand_dims(arr_read_pivot_dB[(0, n - 1) + read_GB], axis=0), + G_in_dB[0, n], + ), + axis=0, + ) + arr_write_dA[(0, n) + write], arr_write_dB[(0, n) + write] = calc_dA_dB( + m, + n, + i, + arr_read_pivot, + read_GB, + G_in_adapted, + A_adapted, + B, + K_i, + K_l_adapted, + arr_read_pivot_dA, + G_in_dA_adapted, + arr_read_pivot_dB, + G_in_dB_adapted, + l_range, + ) + # n=0 + n = 0 + l_range = np.arange(1, A.shape[1]) + l_range[0] = 0 + A_adapted = np.hstack((np.array([A[i, 0]]), A[i, 2:])) + for m in range(1, cutoff_leftoverMode): + K_l_adapted = np.hstack((np.array([np.sqrt(m)]), K_l)) + G_in_adapted = np.hstack( + (np.array([arr_read_pivot[(m - 1, 0) + read_GB] * np.sqrt(m)]), G_in[m, 0]) + ) + G_in_dA_adapted = np.concatenate( + ( + np.expand_dims(arr_read_pivot_dA[(m - 1, 0) + read_GB], axis=0), + G_in_dA[m, 0], + ), + axis=0, + ) + G_in_dB_adapted = np.concatenate( + ( + np.expand_dims(arr_read_pivot_dB[(m - 1, 0) + read_GB], axis=0), + G_in_dB[m, 0], + ), + axis=0, + ) + arr_write_dA[(m, 0) + write], arr_write_dB[(m, 0) + write] = calc_dA_dB( + m, + n, + i, + arr_read_pivot, + read_GB, + G_in_adapted, + A_adapted, + B, + K_i, + K_l_adapted, + arr_read_pivot_dA, + G_in_dA_adapted, + arr_read_pivot_dB, + G_in_dB_adapted, + l_range, + ) + # m>0,n>0 + l_range = np.arange(A.shape[1]) + A_adapted = A[i] + for m in range(1, cutoff_leftoverMode): + for n in range(1, cutoff_leftoverMode): + K_l_adapted = np.hstack((np.array([np.sqrt(m), np.sqrt(n)]), K_l)) + G_in_adapted = np.hstack( + ( + np.array( + [ + arr_read_pivot[(m - 1, n) + read_GB] * np.sqrt(m), + arr_read_pivot[(m, n - 1) + read_GB] * np.sqrt(n), + ] + ), + G_in[m, n], + ) + ) + G_in_dA_adapted = np.concatenate( + ( + np.expand_dims(arr_read_pivot_dA[(m - 1, n) + read_GB], axis=0), + np.expand_dims(arr_read_pivot_dA[(m, n - 1) + read_GB], axis=0), + G_in_dA[m, n], + ), + axis=0, + ) + G_in_dB_adapted = np.concatenate( + ( + np.expand_dims(arr_read_pivot_dB[(m - 1, n) + read_GB], axis=0), + np.expand_dims(arr_read_pivot_dB[(m, n - 1) + read_GB], axis=0), + G_in_dB[m, n], + ), + axis=0, + ) + arr_write_dA[(m, n) + write], arr_write_dB[(m, n) + write] = calc_dA_dB( + m, + n, + i, + arr_read_pivot, + read_GB, + G_in_adapted, + A_adapted, + B, + K_i, + K_l_adapted, + arr_read_pivot_dA, + G_in_dA_adapted, + arr_read_pivot_dB, + G_in_dB_adapted, + l_range, + ) + return arr_write_dA, arr_write_dB + + +@njit +def read_block( + arr_write, + arr_write_dA, + arr_write_dB, + idx_write, + arr_read, + arr_read_dA, + arr_read_dB, + idx_read_tail, + cutoff_leftoverMode, +): + """ + Read the blocks of Fock amplitudes(of shape cutoff_leftoverMode x cutoff_leftoverMode) + and their derivatives w.r.t A and B and write them to G_in, G_in_dA, G_in_dB + """ + for m in range(cutoff_leftoverMode): + for n in range(cutoff_leftoverMode): + arr_write[m, n, idx_write] = arr_read[ + ( + m, + n, + ) + + idx_read_tail + ] + arr_write_dA[m, n, idx_write] = arr_read_dA[ + ( + m, + n, + ) + + idx_read_tail + ] + arr_write_dB[m, n, idx_write] = arr_read_dB[ + ( + m, + n, + ) + + idx_read_tail + ] + + return arr_write, arr_write_dA, arr_write_dB + + +@njit +def use_offDiag_pivot_grad( + A, + B, + M, + cutoff_leftoverMode, + cutoffs_tail, + params, + d, + arr0, + arr2, + arr1010, + arr1001, + arr1, + arr0_dA, + arr2_dA, + arr1010_dA, + arr1001_dA, + arr1_dA, + arr0_dB, + arr2_dB, + arr1010_dB, + arr1001_dB, + arr1_dB, +): + """ + Apply recurrence relation for pivot of type [a+1,a,b,b,c,c,...] / [a,a,b+1,b,c,c,...] / [a,a,b,b,c+1,c,...] + Args: + A, B (array, Vector): required input for recurrence relation (given by mrmustard.physics.fock.ABC) + M (int): number of detected modes + cutoffs (tuple): upper bounds for the number of photons in each mode + params (tuple): (a,b,c,...) + d (int): mode index in which the considered Fock amplitude is off diagonal + e.g. [a,a,b+1,b,c,c,...] --> b is off diagonal --> d=1 + arr0, arr2, arr1010, arr1001, arr1 (array, array, array, array, array): submatrices of the fock representation + arr..._dA, arr..._dB (array, array): derivatives of submatrices w.r.t A and B + Returns: + (array, array, array, array, array): updated versions of arr0, arr2, arr1010, arr1001, arr1 + """ + + pivot = repeat_twice(params) + pivot[2 * d] += 1 + K_l = SQRT[pivot] + K_i = SQRT[pivot + 1] + G_in = np.zeros((cutoff_leftoverMode, cutoff_leftoverMode, 2 * M), dtype=np.complex128) + G_in_dA = np.zeros(G_in.shape + A.shape, dtype=np.complex128) + G_in_dB = np.zeros(G_in.shape + B.shape, dtype=np.complex128) + + ########## READ ########## + read_GB = (2 * d,) + params + GB = np.zeros((cutoff_leftoverMode, cutoff_leftoverMode, len(B)), dtype=np.complex128) + for m in range(cutoff_leftoverMode): + for n in range(cutoff_leftoverMode): + GB[m, n] = arr1[(m, n) + read_GB] * B + + # Array0 + G_in, G_in_dA, G_in_dB = read_block( + G_in, + G_in_dA, + G_in_dB, + 2 * d, + arr0, + arr0_dA, + arr0_dB, + params, + cutoff_leftoverMode, + ) + + # read from Array2 + if params[d] > 0: + params_adapted = tuple_setitem(params, d, params[d] - 1) + G_in, G_in_dA, G_in_dB = read_block( + G_in, + G_in_dA, + G_in_dB, + 2 * d + 1, + arr2, + arr2_dA, + arr2_dB, + (d,) + params_adapted, + cutoff_leftoverMode, + ) + + # read from Array11 + for i in range(d + 1, M): # i>d + if params[i] > 0: + params_adapted = tuple_setitem(params, i, params[i] - 1) + G_in, G_in_dA, G_in_dB = read_block( + G_in, + G_in_dA, + G_in_dB, + 2 * i, + arr1001, + arr1001_dA, + arr1001_dB, + (d, i - d - 1) + params_adapted, + cutoff_leftoverMode, + ) + G_in, G_in_dA, G_in_dB = read_block( + G_in, + G_in_dA, + G_in_dB, + 2 * i + 1, + arr1010, + arr1010_dA, + arr1010_dB, + (d, i - d - 1) + params_adapted, + cutoff_leftoverMode, + ) + + ########## WRITE ########## + for m in range(cutoff_leftoverMode): + for n in range(cutoff_leftoverMode): + G_in[m, n] = np.multiply(K_l, G_in[m, n]) + + # Array0 + write = tuple_setitem(params, d, params[d] + 1) + arr0_dA, arr0_dB = write_block_grad( + 2 * d + 3, + write, + arr1, + read_GB, + G_in, + A, + B, + K_i, + K_l, + cutoff_leftoverMode, + arr0_dA, + arr1_dA, + G_in_dA, + arr0_dB, + arr1_dB, + G_in_dB, + ) + + # Array2 + if params[d] + 2 < cutoffs_tail[d]: + write = (d,) + params + arr2_dA, arr2_dB = write_block_grad( + 2 * d + 2, + write, + arr1, + read_GB, + G_in, + A, + B, + K_i, + K_l, + cutoff_leftoverMode, + arr2_dA, + arr1_dA, + G_in_dA, + arr2_dB, + arr1_dB, + G_in_dB, + ) + + # Array11 + for i in range(d + 1, M): + if params[i] + 1 < cutoffs_tail[i]: + write = (d, i - d - 1) + params + arr1010_dA, arr1010_dB = write_block_grad( + 2 * i + 2, + write, + arr1, + read_GB, + G_in, + A, + B, + K_i, + K_l, + cutoff_leftoverMode, + arr1010_dA, + arr1_dA, + G_in_dA, + arr1010_dB, + arr1_dB, + G_in_dB, + ) + arr1001_dA, arr1001_dB = write_block_grad( + 2 * i + 3, + write, + arr1, + read_GB, + G_in, + A, + B, + K_i, + K_l, + cutoff_leftoverMode, + arr1001_dA, + arr1_dA, + G_in_dA, + arr1001_dB, + arr1_dB, + G_in_dB, + ) + + return ( + arr0_dA, + arr2_dA, + arr1010_dA, + arr1001_dA, + arr0_dB, + arr2_dB, + arr1010_dB, + arr1001_dB, + ) + + +@njit +def use_diag_pivot_grad( + A, + B, + M, + cutoff_leftoverMode, + cutoffs_tail, + params, + arr0, + arr1, + arr0_dA, + arr1_dA, + arr0_dB, + arr1_dB, +): + """ + Apply recurrence relation for pivot of type [a,a,b,b,c,c...] + Args: + A, B (array, Vector): required input for recurrence relation (given by mrmustard.physics.fock.ABC) + M (int): number of detected modes + cutoffs (tuple): upper bounds for the number of photons in each mode + params (tuple): (a,b,c,...) + arr0, arr1 (array, array): submatrices of the fock representation + arr..._dA, arr..._dB (array, array): derivatives of submatrices w.r.t A and B + Returns: + (array, array): updated versions of arr0, arr1 + """ + pivot = repeat_twice(params) + K_l = SQRT[pivot] + K_i = SQRT[pivot + 1] + G_in = np.zeros((cutoff_leftoverMode, cutoff_leftoverMode, 2 * M), dtype=np.complex128) + G_in_dA = np.zeros(G_in.shape + A.shape, dtype=np.complex128) + G_in_dB = np.zeros(G_in.shape + B.shape, dtype=np.complex128) + + ########## READ ########## + read_GB = params + GB = np.zeros((cutoff_leftoverMode, cutoff_leftoverMode, len(B)), dtype=np.complex128) + for m in range(cutoff_leftoverMode): + for n in range(cutoff_leftoverMode): + GB[m, n] = arr0[(m, n) + read_GB] * B + + # Array1 + for i in range(2 * M): + if params[i // 2] > 0: + params_adapted = tuple_setitem(params, i // 2, params[i // 2] - 1) + read = ( + i + 1 - 2 * (i % 2), + ) + params_adapted # [i+1-2*(i%2) for i in range(6)] == [1,0,3,2,5,4] + G_in, G_in_dA, G_in_dB = read_block( + G_in, + G_in_dA, + G_in_dB, + i, + arr1, + arr1_dA, + arr1_dB, + read, + cutoff_leftoverMode, + ) + + ########## WRITE ########## + for m in range(cutoff_leftoverMode): + for n in range(cutoff_leftoverMode): + G_in[m, n] = np.multiply(K_l, G_in[m, n]) + + # Array1 + for i in range(2 * M): + if params[i // 2] + 1 < cutoffs_tail[i // 2]: + # this prevents a few elements from being written that will never be read + if i != 1 or params[0] + 2 < cutoffs_tail[0]: + write = (i,) + params + arr1_dA, arr1_dB = write_block_grad( + i + 2, + write, + arr0, + read_GB, + G_in, + A, + B, + K_i, + K_l, + cutoff_leftoverMode, + arr1_dA, + arr0_dA, + G_in_dA, + arr1_dB, + arr0_dB, + G_in_dB, + ) + + return arr1_dA, arr1_dB + + +@njit +def fock_representation_1leftoverMode_grad_NUMBA( + A, + B, + M, + cutoff_leftoverMode, + cutoffs_tail, + arr0, + arr2, + arr1010, + arr1001, + arr1, + tuple_type, + list_type, + zero_tuple, +): + """ + Returns the PNR probabilities of a state or Choi state (by using the recurrence relation to calculate a limited number of Fock amplitudes) + Args: + A, B (array, Vector): required input for recurrence relation (given by mrmustard.physics.fock.ABC) + M (int): number of modes + cutoffs (tuple): upper bounds for the number of photons in each mode + arr0 (array): submatrix of the fock representation that contains Fock amplitudes of the type [a,a,b,b,c,c...] + arr2 (array): submatrix of the fock representation that contains Fock amplitudes of the types [a+2,a,b,b,c,c...] / [a,a,b+2,b,c,c...] / ... + arr1010 (array): submatrix of the fock representation that contains Fock amplitudes of the types [a+1,a,b+1,b,c,c,...] / [a+1,a,b,b,c+1,c,...] / [a,a,b+1,b,c+1,c,...] / ... + arr1001 (array): submatrix of the fock representation that contains Fock amplitudes of the types [a+1,a,b,b+1,c,c,...] / [a+1,a,b,b,c,c+1,...] / [a,a,b+1,b,c,c+1,...] / ... + arr1 (array): submatrix of the fock representation that contains Fock amplitudes of the types [a+1,a,b,b,c,c...] / [a,a+1,b,b,c,c...] / [a,a,b+1,b,c,c...] / ... + tuple_type, list_type (numba types): numba types that need to be defined outside of numba compiled functions + Returns: + Tensor: the fock representation + """ + arr0_dA = np.zeros(arr0.shape + A.shape, dtype=np.complex128) + arr2_dA = np.zeros(arr2.shape + A.shape, dtype=np.complex128) + arr1010_dA = np.zeros(arr1010.shape + A.shape, dtype=np.complex128) + arr1001_dA = np.zeros(arr1001.shape + A.shape, dtype=np.complex128) + arr1_dA = np.zeros(arr1.shape + A.shape, dtype=np.complex128) + arr0_dB = np.zeros(arr0.shape + B.shape, dtype=np.complex128) + arr2_dB = np.zeros(arr2.shape + B.shape, dtype=np.complex128) + arr1010_dB = np.zeros(arr1010.shape + B.shape, dtype=np.complex128) + arr1001_dB = np.zeros(arr1001.shape + B.shape, dtype=np.complex128) + arr1_dB = np.zeros(arr1.shape + B.shape, dtype=np.complex128) + + # fill first mode for all PNR detections equal to zero + for m in range(cutoff_leftoverMode - 1): + arr0_dA[(m + 1, 0) + zero_tuple] = ( + arr0_dA[(m, 0) + zero_tuple] * B[0] + + np.sqrt(m) * A[0, 0] * arr0_dA[(m - 1, 0) + zero_tuple] + ) / np.sqrt(m + 1) + arr0_dA[(m + 1, 0) + zero_tuple][0, 0] += ( + np.sqrt(m) * arr0[(m - 1, 0) + zero_tuple] + ) / np.sqrt(m + 1) + arr0_dB[(m + 1, 0) + zero_tuple] = ( + arr0_dB[(m, 0) + zero_tuple] * B[0] + + np.sqrt(m) * A[0, 0] * arr0_dB[(m - 1, 0) + zero_tuple] + ) / np.sqrt(m + 1) + arr0_dB[(m + 1, 0) + zero_tuple][0] += arr0[(m, 0) + zero_tuple] / np.sqrt(m + 1) + + for m in range(cutoff_leftoverMode): + for n in range(cutoff_leftoverMode - 1): + arr0_dA[(m, n + 1) + zero_tuple] = ( + arr0_dA[(m, n) + zero_tuple] * B[1] + + np.sqrt(m) * A[1, 0] * arr0_dA[(m - 1, n) + zero_tuple] + + np.sqrt(n) * A[1, 1] * arr0_dA[(m, n - 1) + zero_tuple] + ) / np.sqrt(n + 1) + arr0_dA[(m, n + 1) + zero_tuple][1, 0] += ( + np.sqrt(m) * arr0[(m - 1, n) + zero_tuple] + ) / np.sqrt(n + 1) + arr0_dA[(m, n + 1) + zero_tuple][1, 1] += ( + np.sqrt(n) * arr0[(m, n - 1) + zero_tuple] + ) / np.sqrt(n + 1) + arr0_dB[(m, n + 1) + zero_tuple] = ( + arr0_dB[(m, n) + zero_tuple] * B[1] + + np.sqrt(m) * A[1, 0] * arr0_dB[(m - 1, n) + zero_tuple] + + np.sqrt(n) * A[1, 1] * arr0_dB[(m, n - 1) + zero_tuple] + ) / np.sqrt(n + 1) + arr0_dB[(m, n + 1) + zero_tuple][1] += arr0[(m, n) + zero_tuple] / np.sqrt(n + 1) + + dict_params = construct_dict_params(cutoffs_tail, tuple_type, list_type) + for sum_params in range(sum(cutoffs_tail)): + for params in dict_params[sum_params]: + # diagonal pivots: aa,bb,cc,dd,... + if params[0] < cutoffs_tail[0] - 1: + arr1_dA, arr1_dB = use_diag_pivot_grad( + A, + B, + M - 1, + cutoff_leftoverMode, + cutoffs_tail, + params, + arr0, + arr1, + arr0_dA, + arr1_dA, + arr0_dB, + arr1_dB, + ) + # off-diagonal pivots: d=0: (a+1)a,bb,cc,dd,... | d=1: 00,(b+1)b,cc,dd | 00,00,(c+1)c,dd | ... + for d in range(M - 1): + if np.all(np.array(params)[:d] == 0) and (params[d] < cutoffs_tail[d] - 1): + ( + arr0_dA, + arr2_dA, + arr1010_dA, + arr1001_dA, + arr0_dB, + arr2_dB, + arr1010_dB, + arr1001_dB, + ) = use_offDiag_pivot_grad( + A, + B, + M - 1, + cutoff_leftoverMode, + cutoffs_tail, + params, + d, + arr0, + arr2, + arr1010, + arr1001, + arr1, + arr0_dA, + arr2_dA, + arr1010_dA, + arr1001_dA, + arr1_dA, + arr0_dB, + arr2_dB, + arr1010_dB, + arr1001_dB, + arr1_dB, + ) + return arr0_dA, arr0_dB + + +def fock_representation_1leftoverMode_grad(A, B, M, arr0, arr2, arr1010, arr1001, arr1): + """ + First initialise the submatrices of G (of which the shape depends on cutoff and M) + and some other constants + (These initialisations currently cannot be done using Numba.) + Then calculate the fock representation. + """ + + cutoffs = tuple(arr0.shape[1:]) + cutoff_leftoverMode = cutoffs[0] + cutoffs_tail = tuple(cutoffs[1:]) + tuple_type = numba.types.UniTuple(int64, M - 1) + list_type = numba.types.ListType(tuple_type) + zero_tuple = (0,) * (M - 1) + + return fock_representation_1leftoverMode_grad_NUMBA( + A, + B, + M, + cutoff_leftoverMode, + cutoffs_tail, + arr0, + arr2, + arr1010, + arr1001, + arr1, + tuple_type, + list_type, + zero_tuple, + ) diff --git a/mrmustard/math/numba/compactFock_diagonal_amps.py b/mrmustard/math/numba/compactFock_diagonal_amps.py new file mode 100644 index 000000000..d65e4ca58 --- /dev/null +++ b/mrmustard/math/numba/compactFock_diagonal_amps.py @@ -0,0 +1,180 @@ +""" +This module calculates the diagonal of the Fock representation (i.e. the PNR detection probabilities of all modes) +by applying the recursion relation in a selective manner. +""" + +import numpy as np +import numba +from numba import njit, int64 +from numba.cpython.unsafe.tuple import tuple_setitem +from mrmustard.math.numba.compactFock_helperFunctions import ( + SQRT, + repeat_twice, + construct_dict_params, +) + + +@njit +def use_offDiag_pivot(A, B, M, cutoffs, params, d, arr0, arr2, arr1010, arr1001, arr1): + """ + Apply recurrence relation for pivot of type [a+1,a,b,b,c,c,...] / [a,a,b+1,b,c,c,...] / [a,a,b,b,c+1,c,...] + Args: + A, B (array, vector): required input for recurrence relation (given by mrmustard.physics.fock.ABC) + M (int): number of modes + cutoffs (tuple): upper bounds for the number of photons in each mode + params (tuple): (a,b,c,...) + d (int): mode index in which the considered Fock amplitude is off diagonal + e.g. [a,a,b+1,b,c,c,...] --> b is off diagonal --> d=1 + arr0, arr2, arr1010, arr1001, arr1 (array, array, array, array, array): submatrices of the fock representation + Returns: + (array, array, array, array, array): updated versions of arr0, arr2, arr1010, arr1001, arr1 + """ + pivot = repeat_twice(params) + pivot[2 * d] += 1 + K_l = SQRT[pivot] + K_i = SQRT[pivot + 1] + G_in = np.empty(2 * M, dtype=np.complex128) + + ########## READ ########## + GB = arr1[(2 * d,) + params] * B + + # Array0 + G_in[2 * d] = arr0[params] + + # read from Array2 + if params[d] > 0: + params_adapted = tuple_setitem(params, d, params[d] - 1) + G_in[2 * d + 1] = arr2[(d,) + params_adapted] + + # read from Array11 + for i in range(d + 1, M): # i>d + if params[i] > 0: + params_adapted = tuple_setitem(params, i, params[i] - 1) + G_in[2 * i] = arr1001[(d, i - d - 1) + params_adapted] + G_in[2 * i + 1] = arr1010[(d, i - d - 1) + params_adapted] + + ########## WRITE ########## + G_in = np.multiply(K_l, G_in) + + # Array0 + params_adapted = tuple_setitem(params, d, params[d] + 1) + arr0[params_adapted] = (GB[2 * d + 1] + A[2 * d + 1] @ G_in) / K_i[2 * d + 1] + + # Array2 + if params[d] + 2 < cutoffs[d]: + arr2[(d,) + params] = (GB[2 * d] + A[2 * d] @ G_in) / K_i[2 * d] + + # Array11 + for i in range(d + 1, M): + if params[i] + 1 < cutoffs[i]: + arr1010[(d, i - d - 1) + params] = (GB[2 * i] + A[2 * i] @ G_in) / K_i[2 * i] + arr1001[(d, i - d - 1) + params] = (GB[2 * i + 1] + A[2 * i + 1] @ G_in) / K_i[ + 2 * i + 1 + ] + + return arr0, arr2, arr1010, arr1001 + + +@njit +def use_diag_pivot(A, B, M, cutoffs, params, arr0, arr1): + """ + Apply recurrence relation for pivot of type [a,a,b,b,c,c...] + Args: + A, B (array, vector): required input for recurrence relation (given by mrmustard.physics.fock.ABC) + M (int): number of modes + cutoffs (tuple): upper bounds for the number of photons in each mode + params (tuple): (a,b,c,...) + arr0, arr1 (array, array): submatrices of the fock representation + Returns: + (array, array): updated versions of arr0, arr1 + """ + pivot = repeat_twice(params) + K_l = SQRT[pivot] + K_i = SQRT[pivot + 1] + G_in = np.empty(2 * M, dtype=np.complex128) + + ########## READ ########## + GB = arr0[params] * B + + # Array1 + for i in range(2 * M): + if params[i // 2] > 0: + params_adapted = tuple_setitem(params, i // 2, params[i // 2] - 1) + G_in[i] = arr1[ + (i + 1 - 2 * (i % 2),) + params_adapted + ] # [i+1-2*(i%2) for i in range(6)] = [1,0,3,2,5,4] + + ########## WRITE ########## + G_in = np.multiply(K_l, G_in) + + # Array1 + for i in range(2 * M): + if params[i // 2] + 1 < cutoffs[i // 2]: + # this prevents a few elements from being written that will never be read + if i != 1 or params[0] + 2 < cutoffs[0]: + arr1[(i,) + params] = (GB[i] + A[i] @ G_in) / K_i[i] + + return arr1 + + +@njit +def fock_representation_diagonal_amps_NUMBA( + A, B, M, cutoffs, arr0, arr2, arr1010, arr1001, arr1, tuple_type, list_type +): + """ + Returns the PNR probabilities of a state or Choi state + (by using the recurrence relation to calculate a limited number of Fock amplitudes) + Args: + A, B (array, vector): required input for recurrence relation (given by mrmustard.physics.fock.ABC) + M (int): number of modes + cutoffs (tuple): upper bounds for the number of photons in each mode + arr0 (array): submatrix of the fock representation that contains Fock amplitudes of the type [a,a,b,b,c,c...] + (!) should already contain G0 at position (0,)*M + arr2 (array): submatrix of the fock representation that contains Fock amplitudes of the types [a+2,a,b,b,c,c...] / [a,a,b+2,b,c,c...] / ... + arr1010 (array): submatrix of the fock representation that contains Fock amplitudes of the types [a+1,a,b+1,b,c,c,...] / [a+1,a,b,b,c+1,c,...] / [a,a,b+1,b,c+1,c,...] / ... + arr1001 (array): submatrix of the fock representation that contains Fock amplitudes of the types [a+1,a,b,b+1,c,c,...] / [a+1,a,b,b,c,c+1,...] / [a,a,b+1,b,c,c+1,...] / ... + arr1 (array): submatrix of the fock representation that contains Fock amplitudes of the types [a+1,a,b,b,c,c...] / [a,a+1,b,b,c,c...] / [a,a,b+1,b,c,c...] / ... + tuple_type, list_type (numba types): numba types that need to be defined outside of numba compiled functions + Returns: + array: the fock representation + """ + dict_params = construct_dict_params(cutoffs, tuple_type, list_type) + for sum_params in range(sum(cutoffs)): + for params in dict_params[sum_params]: + # diagonal pivots: aa,bb,cc,dd,... + if params[0] < cutoffs[0] - 1: + arr1 = use_diag_pivot(A, B, M, cutoffs, params, arr0, arr1) + # off-diagonal pivots: d=0: (a+1)a,bb,cc,dd,... | d=1: 00,(b+1)b,cc,dd | 00,00,(c+1)c,dd | ... + for d in range(M): + if np.all(np.array(params)[:d] == 0) and (params[d] < cutoffs[d] - 1): + arr0, arr2, arr1010, arr1001 = use_offDiag_pivot( + A, B, M, cutoffs, params, d, arr0, arr2, arr1010, arr1001, arr1 + ) + return arr0, arr2, arr1010, arr1001, arr1 + + +def fock_representation_diagonal_amps(A, B, G0, M, cutoffs): + """ + First initialise the submatrices of G (of which the shape depends on cutoff and M) + and some other constants + (These initialisations currently cannot be done using Numba.) + Then calculate the fock representation. + """ + + cutoffs = tuple(cutoffs) + tuple_type = numba.types.UniTuple(int64, M) + list_type = numba.types.ListType(tuple_type) + + arr0 = np.empty(cutoffs, dtype=np.complex128) + arr0[(0,) * M] = G0 + arr2 = np.empty((M,) + cutoffs, dtype=np.complex128) + arr1 = np.empty((2 * M,) + cutoffs, dtype=np.complex128) + if M == 1: + arr1010 = np.empty((1, 1, 1), dtype=np.complex128) + arr1001 = np.empty((1, 1, 1), dtype=np.complex128) + else: + arr1010 = np.empty((M, M - 1) + cutoffs, dtype=np.complex128) + arr1001 = np.empty((M, M - 1) + cutoffs, dtype=np.complex128) + return fock_representation_diagonal_amps_NUMBA( + A, B, M, cutoffs, arr0, arr2, arr1010, arr1001, arr1, tuple_type, list_type + ) diff --git a/mrmustard/math/numba/compactFock_diagonal_grad.py b/mrmustard/math/numba/compactFock_diagonal_grad.py new file mode 100644 index 000000000..6b767b182 --- /dev/null +++ b/mrmustard/math/numba/compactFock_diagonal_grad.py @@ -0,0 +1,310 @@ +""" +This module calculates the derivatives of the diagonal of the Fock representation (i.e. the PNR detection probabilities of all modes) +by applying the derivated recursion relation in a selective manner. +""" + +import numpy as np +import numba +from numba import njit, int64 +from numba.cpython.unsafe.tuple import tuple_setitem +from mrmustard.math.numba.compactFock_helperFunctions import ( + SQRT, + repeat_twice, + construct_dict_params, +) + + +@njit +def calc_dA_dB(i, G_in_dA, G_in_dB, G_in, A, B, K_l, K_i, M, pivot_val, pivot_val_dA, pivot_val_dB): + """ + Calculate the derivatives of one Fock amplitude w.r.t A and B. + Args: + i (int): the element of the multidim index that is increased + G_in, G_in_dA, G_in_dB (array, array, array): all Fock amplitudes from the 'read' group in the recurrence relation and their derivatives w.r.t. A and B + A, B (array, vector): required input for recurrence relation (given by mrmustard.physics.fock.ABC) + K_l, K_i (vector, vector): SQRT[pivot], SQRT[pivot + 1] + M (int): number of modes + pivot_val, pivot_val_dA, pivot_val_dB (array, array, array): Fock amplitude at the position of the pivot and its derivatives w.r.t. A and B + """ + dA = pivot_val_dA * B[i] + dB = pivot_val_dB * B[i] + dB[i] += pivot_val + for l in range(2 * M): + dA += K_l[l] * A[i, l] * G_in_dA[l] + dB += K_l[l] * A[i, l] * G_in_dB[l] + dA[i, l] += G_in[l] + return dA / K_i[i], dB / K_i[i] + + +@njit +def use_offDiag_pivot_grad( + A, + B, + M, + cutoffs, + params, + d, + arr0, + arr2, + arr1010, + arr1001, + arr1, + arr0_dA, + arr2_dA, + arr1010_dA, + arr1001_dA, + arr1_dA, + arr0_dB, + arr2_dB, + arr1010_dB, + arr1001_dB, + arr1_dB, +): + """ + Apply recurrence relation for pivot of type [a+1,a,b,b,c,c,...] / [a,a,b+1,b,c,c,...] / [a,a,b,b,c+1,c,...] + Args: + A, B (array, vector): required input for recurrence relation (given by mrmustard.physics.fock.ABC) + M (int): number of modes + cutoffs (tuple): upper bounds for the number of photons in each mode + params (tuple): (a,b,c,...) + d (int): mode index in which the considered Fock amplitude is off diagonal + e.g. [a,a,b+1,b,c,c,...] --> b is off diagonal --> d=1 + arr0, arr2, arr1010, arr1001, arr1 (array, array, array, array, array): submatrices of the fock representation + arr..._dA, arr..._dB (array, array): derivatives of submatrices w.r.t A and B + Returns: + (array, array, array, array, array): updated versions of arr0, arr2, arr1010, arr1001, arr1 + """ + pivot = repeat_twice(params) + pivot[2 * d] += 1 + K_l = SQRT[pivot] + K_i = SQRT[pivot + 1] + G_in = np.zeros(2 * M, dtype=np.complex128) + G_in_dA = np.zeros((2 * M,) + A.shape, dtype=np.complex128) + G_in_dB = np.zeros((2 * M,) + B.shape, dtype=np.complex128) + + ########## READ ########## + pivot_val = arr1[2 * d][params] + pivot_val_dA = arr1_dA[2 * d][params] + pivot_val_dB = arr1_dB[2 * d][params] + + # Array0 + G_in[2 * d] = arr0[params] + G_in_dA[2 * d] = arr0_dA[params] + G_in_dB[2 * d] = arr0_dB[params] + + # read from Array2 + if params[d] > 0: + G_in[2 * d + 1] = arr2[d][tuple_setitem(params, d, params[d] - 1)] + G_in_dA[2 * d + 1] = arr2_dA[d][tuple_setitem(params, d, params[d] - 1)] + G_in_dB[2 * d + 1] = arr2_dB[d][tuple_setitem(params, d, params[d] - 1)] + + # read from Array11 + for i in range(d + 1, M): # i>d + if params[i] > 0: + params_adapted = tuple_setitem(params, i, params[i] - 1) + G_in[2 * i] = arr1001[d][i - d - 1][params_adapted] + G_in_dA[2 * i] = arr1001_dA[d][i - d - 1][params_adapted] + G_in_dB[2 * i] = arr1001_dB[d][i - d - 1][params_adapted] + G_in[2 * i + 1] = arr1010[d][i - d - 1][params_adapted] + G_in_dA[2 * i + 1] = arr1010_dA[d][i - d - 1][params_adapted] + G_in_dB[2 * i + 1] = arr1010_dB[d][i - d - 1][params_adapted] + + ########## WRITE ########## + G_in = np.multiply(K_l, G_in) + + # Array0 + params_adapted = tuple_setitem(params, d, params[d] + 1) + arr0_dA[params_adapted], arr0_dB[params_adapted] = calc_dA_dB( + 2 * d + 1, G_in_dA, G_in_dB, G_in, A, B, K_l, K_i, M, pivot_val, pivot_val_dA, pivot_val_dB + ) + + # Array2 + if params[d] + 2 < cutoffs[d]: + arr2_dA[d][params], arr2_dB[d][params] = calc_dA_dB( + 2 * d, G_in_dA, G_in_dB, G_in, A, B, K_l, K_i, M, pivot_val, pivot_val_dA, pivot_val_dB + ) + + # Array11 + for i in range(d + 1, M): + if params[i] + 1 < cutoffs[i]: + arr1010_dA[d][i - d - 1][params], arr1010_dB[d][i - d - 1][params] = calc_dA_dB( + 2 * i, + G_in_dA, + G_in_dB, + G_in, + A, + B, + K_l, + K_i, + M, + pivot_val, + pivot_val_dA, + pivot_val_dB, + ) + arr1001_dA[d][i - d - 1][params], arr1001_dB[d][i - d - 1][params] = calc_dA_dB( + 2 * i + 1, + G_in_dA, + G_in_dB, + G_in, + A, + B, + K_l, + K_i, + M, + pivot_val, + pivot_val_dA, + pivot_val_dB, + ) + + return arr0_dA, arr2_dA, arr1010_dA, arr1001_dA, arr0_dB, arr2_dB, arr1010_dB, arr1001_dB + + +@njit +def use_diag_pivot_grad(A, B, M, cutoffs, params, arr0, arr1, arr0_dA, arr1_dA, arr0_dB, arr1_dB): + """ + Apply recurrence relation for pivot of type [a,a,b,b,c,c...] + Args: + A, B (array, vector): required input for recurrence relation (given by mrmustard.physics.fock.ABC) + M (int): number of modes + cutoffs (tuple): upper bounds for the number of photons in each mode + params (tuple): (a,b,c,...) + arr0, arr1 (array, array): submatrices of the fock representation + arr..._dA, arr..._dB (array, array): derivatives of submatrices w.r.t A and B + Returns: + (array, array): updated versions of arr0, arr1 + """ + pivot = repeat_twice(params) + K_l = SQRT[pivot] + K_i = SQRT[pivot + 1] + G_in = np.zeros(2 * M, dtype=np.complex128) + G_in_dA = np.zeros((2 * M,) + A.shape, dtype=np.complex128) + G_in_dB = np.zeros((2 * M,) + B.shape, dtype=np.complex128) + + ########## READ ########## + pivot_val = arr0[params] + pivot_val_dA = arr0_dA[params] + pivot_val_dB = arr0_dB[params] + + # Array1 + for i in range(2 * M): + if params[i // 2] > 0: + i_staggered = i + 1 - 2 * (i % 2) # [i+1-2*(i%2) for i in range(6)] == [1,0,3,2,5,4] + params_adapted = tuple_setitem(params, i // 2, params[i // 2] - 1) + G_in[i] = arr1[i_staggered][params_adapted] + G_in_dA[i] = arr1_dA[i_staggered][params_adapted] + G_in_dB[i] = arr1_dB[i_staggered][params_adapted] + + ########## WRITE ########## + G_in = np.multiply(K_l, G_in) + + # Array1 + for i in range(2 * M): + if params[i // 2] + 1 < cutoffs[i // 2]: + # this if statement prevents a few elements from being written that will never be read + if i != 1 or params[0] + 2 < cutoffs[0]: + arr1_dA[i][params], arr1_dB[i][params] = calc_dA_dB( + i, + G_in_dA, + G_in_dB, + G_in, + A, + B, + K_l, + K_i, + M, + pivot_val, + pivot_val_dA, + pivot_val_dB, + ) + + return arr1_dA, arr1_dB + + +@njit +def fock_representation_diagonal_grad_NUMBA( + A, B, M, cutoffs, arr0, arr2, arr1010, arr1001, arr1, tuple_type, list_type +): + """ + Returns the PNR probabilities of a state or Choi state (by using the recurrence relation to calculate a limited number of Fock amplitudes) + Args: + A, B (array, vector): required input for recurrence relation (given by mrmustard.physics.fock.ABC) + M (int): number of modes + cutoffs (tuple): upper bounds for the number of photons in each mode + arr0 (array): submatrix of the fock representation that contains Fock amplitudes of the type [a,a,b,b,c,c...] + arr2 (array): submatrix of the fock representation that contains Fock amplitudes of the types [a+2,a,b,b,c,c...] / [a,a,b+2,b,c,c...] / ... + arr1010 (array): submatrix of the fock representation that contains Fock amplitudes of the types [a+1,a,b+1,b,c,c,...] / [a+1,a,b,b,c+1,c,...] / [a,a,b+1,b,c+1,c,...] / ... + arr1001 (array): submatrix of the fock representation that contains Fock amplitudes of the types [a+1,a,b,b+1,c,c,...] / [a+1,a,b,b,c,c+1,...] / [a,a,b+1,b,c,c+1,...] / ... + arr1 (array): submatrix of the fock representation that contains Fock amplitudes of the types [a+1,a,b,b,c,c...] / [a,a+1,b,b,c,c...] / [a,a,b+1,b,c,c...] / ... + tuple_type, list_type (Numba types): numba types that need to be defined outside of Numba compiled functions + Returns: + array: the fock representation + """ + arr0_dA = np.zeros(arr0.shape + A.shape, dtype=np.complex128) + arr2_dA = np.zeros(arr2.shape + A.shape, dtype=np.complex128) + arr1010_dA = np.zeros(arr1010.shape + A.shape, dtype=np.complex128) + arr1001_dA = np.zeros(arr1001.shape + A.shape, dtype=np.complex128) + arr1_dA = np.zeros(arr1.shape + A.shape, dtype=np.complex128) + arr0_dB = np.zeros(arr0.shape + B.shape, dtype=np.complex128) + arr2_dB = np.zeros(arr2.shape + B.shape, dtype=np.complex128) + arr1010_dB = np.zeros(arr1010.shape + B.shape, dtype=np.complex128) + arr1001_dB = np.zeros(arr1001.shape + B.shape, dtype=np.complex128) + arr1_dB = np.zeros(arr1.shape + B.shape, dtype=np.complex128) + + dict_params = construct_dict_params(cutoffs, tuple_type, list_type) + for sum_params in range(sum(cutoffs)): + for params in dict_params[sum_params]: + # diagonal pivots: aa,bb,cc,dd,... + if params[0] < cutoffs[0] - 1: + arr1_dA, arr1_dB = use_diag_pivot_grad( + A, B, M, cutoffs, params, arr0, arr1, arr0_dA, arr1_dA, arr0_dB, arr1_dB + ) + # off-diagonal pivots: d=0: (a+1)a,bb,cc,dd,... | d=1: 00,(b+1)b,cc,dd | 00,00,(c+1)c,dd | ... + for d in range(M): + if np.all(np.array(params)[:d] == 0) and (params[d] < cutoffs[d] - 1): + ( + arr0_dA, + arr2_dA, + arr1010_dA, + arr1001_dA, + arr0_dB, + arr2_dB, + arr1010_dB, + arr1001_dB, + ) = use_offDiag_pivot_grad( + A, + B, + M, + cutoffs, + params, + d, + arr0, + arr2, + arr1010, + arr1001, + arr1, + arr0_dA, + arr2_dA, + arr1010_dA, + arr1001_dA, + arr1_dA, + arr0_dB, + arr2_dB, + arr1010_dB, + arr1001_dB, + arr1_dB, + ) + return arr0_dA, arr0_dB + + +def fock_representation_diagonal_grad(A, B, M, arr0, arr2, arr1010, arr1001, arr1): + """ + First initialise some Numba types (needs to be done outside of Numba compiled function) + Then calculate the fock representation. + """ + + cutoffs = arr0.shape + tuple_type = numba.types.UniTuple(int64, M) + list_type = numba.types.ListType(tuple_type) + return fock_representation_diagonal_grad_NUMBA( + A, B, M, cutoffs, arr0, arr2, arr1010, arr1001, arr1, tuple_type, list_type + ) diff --git a/mrmustard/math/numba/compactFock_helperFunctions.py b/mrmustard/math/numba/compactFock_helperFunctions.py new file mode 100644 index 000000000..6b0381193 --- /dev/null +++ b/mrmustard/math/numba/compactFock_helperFunctions.py @@ -0,0 +1,45 @@ +""" +This module contains helper functions that are used in +compactFock_diagonal_amps.py, compactFock_diagonal_grad.py, compactFock_1leftoverMode_amps.py and compactFock_1leftoverMode_grad.py +""" + +import numpy as np +from numba import njit, int64 +from numba.typed import Dict +import numba + +SQRT = np.sqrt(np.arange(1000)) # saving the time to recompute square roots + + +@njit +def repeat_twice(params): + """ + This function is equivalent to np.repeat(params,2), but runs faster. + Args: + params (1D array): [a,b,c,...] + Returns: + (1D array): [a,a,b,b,c,c,...] + """ + pivot = np.empty(2 * len(params), dtype=np.int64) + for i, val in enumerate(params): + pivot[2 * i] = val + pivot[2 * i + 1] = val + return pivot + + +@njit +def construct_dict_params(cutoffs, tuple_type, list_type): + """ + Args: + cutoffs (tuple): upper bounds for the number of photons in each mode + tuple_type,list_type (numba types): numba types that need to be defined outside of numba compiled functions + Returns: + (typed Dict): all possible values for (a,b,c,...), grouped in lists according to their sum a+b+c+... + """ + indices = Dict.empty(key_type=int64, value_type=list_type) + for sum_params in range(sum(cutoffs)): + indices[sum_params] = numba.typed.List.empty_list(tuple_type) + + for params in np.ndindex(cutoffs): + indices[sum(params)].append(params) + return indices diff --git a/mrmustard/math/numba/compactFock_inputValidation.py b/mrmustard/math/numba/compactFock_inputValidation.py new file mode 100644 index 000000000..30b1607d2 --- /dev/null +++ b/mrmustard/math/numba/compactFock_inputValidation.py @@ -0,0 +1,83 @@ +""" +This module contains helper functions that are used in +compactFock_diagonal_amps.py, compactFock_diagonal_grad.py, compactFock_1leftoverMode_amps.py and compactFock_1leftoverMode_grad.py +to validate the input provided by the user. +""" + +from typing import Iterable +import numpy as np +from mrmustard.math.numba.compactFock_diagonal_amps import fock_representation_diagonal_amps +from mrmustard.math.numba.compactFock_diagonal_grad import fock_representation_diagonal_grad +from mrmustard.math.numba.compactFock_1leftoverMode_amps import ( + fock_representation_1leftoverMode_amps, +) +from mrmustard.math.numba.compactFock_1leftoverMode_grad import ( + fock_representation_1leftoverMode_grad, +) +from thewalrus._hafnian import input_validation + + +def hermite_multidimensional_diagonal(A, B, G0, cutoffs, rtol=1e-05, atol=1e-08): + """ + Validation of user input for mrmustard.math.tensorflow.hermite_renormalized_diagonal + """ + input_validation(A, atol=atol, rtol=rtol) + if A.shape[0] != B.shape[0]: + raise ValueError("The matrix A and vector B have incompatible dimensions") + if isinstance(cutoffs, Iterable): + cutoffs = tuple(cutoffs) + else: + raise ValueError("cutoffs should be array like of length M") + M = len(cutoffs) + if A.shape[0] // 2 != M: + raise ValueError("The matrix A and cutoffs have incompatible dimensions") + return fock_representation_diagonal_amps(A, B, G0, M, cutoffs) + + +def grad_hermite_multidimensional_diagonal(A, B, G0, arr0, arr2, arr1010, arr1001, arr1): + """ + Validation of user input for gradients of mrmustard.math.tensorflow.hermite_renormalized_diagonal + """ + if A.shape[0] != B.shape[0]: + raise ValueError("The matrix A and vector B have incompatible dimensions") + M = A.shape[0] // 2 + arr0_dA, arr0_dB = fock_representation_diagonal_grad( + A, B, M, arr0, arr2, arr1010, arr1001, arr1 + ) + arr0_dG0 = np.array(arr0 / G0).astype(np.complex128) + return arr0_dG0, arr0_dA, arr0_dB + + +def hermite_multidimensional_1leftoverMode(A, B, G0, cutoffs, rtol=1e-05, atol=1e-08): + """ + Validation of user input for mrmustard.math.tensorflow.hermite_renormalized_1leftoverMode + """ + input_validation(A, atol=atol, rtol=rtol) + if A.shape[0] != B.shape[0]: + raise ValueError("The matrix A and vector B have incompatible dimensions") + if isinstance(cutoffs, Iterable): + cutoffs = tuple(cutoffs) + else: + raise ValueError("cutoffs should be array like of length M") + M = len(cutoffs) + if A.shape[0] // 2 != M: + raise ValueError("The matrix A and cutoffs have incompatible dimensions") + if M <= 1: + raise ValueError("The number of modes should be greater than 1.") + return fock_representation_1leftoverMode_amps(A, B, G0, M, cutoffs) + + +def grad_hermite_multidimensional_1leftoverMode(A, B, G0, arr0, arr2, arr1010, arr1001, arr1): + """ + Validation of user input for gradients of mrmustard.math.tensorflow.hermite_renormalized_1leftoverMode + """ + if A.shape[0] != B.shape[0]: + raise ValueError("The matrix A and vector B have incompatible dimensions") + M = A.shape[0] // 2 + if M <= 1: + raise ValueError("The number of modes should be greater than 1.") + arr0_dA, arr0_dB = fock_representation_1leftoverMode_grad( + A, B, M, arr0, arr2, arr1010, arr1001, arr1 + ) + arr0_dG0 = np.array(arr0 / G0).astype(np.complex128) + return arr0_dG0, arr0_dA, arr0_dB diff --git a/mrmustard/math/tensorflow.py b/mrmustard/math/tensorflow.py index 661b5daa5..6b3c04849 100644 --- a/mrmustard/math/tensorflow.py +++ b/mrmustard/math/tensorflow.py @@ -22,7 +22,14 @@ displacement as displacement_tw, grad_displacement as grad_displacement_tw, ) - +from mrmustard.math.numba.compactFock_inputValidation import ( + hermite_multidimensional_diagonal, + grad_hermite_multidimensional_diagonal, +) +from mrmustard.math.numba.compactFock_inputValidation import ( + hermite_multidimensional_1leftoverMode, + grad_hermite_multidimensional_1leftoverMode, +) from mrmustard.math.autocast import Autocast from mrmustard.types import ( List, @@ -395,6 +402,113 @@ def grad(dLdpoly): return poly, grad + def reorder_AB_bargmann(self, A: tf.Tensor, B: tf.Tensor) -> Tuple[tf.Tensor, tf.Tensor]: + r"""In mrmustard.math.numba.compactFock~ dimensions of the Fock representation are ordered like [mode0,mode0,mode1,mode1,...] + while in mrmustard.physics.bargmann the ordering is [mode0,mode1,...,mode0,mode1,...]. Here we reorder A and B. + Moreover, the recurrence relation in mrmustard.math.numba.compactFock~ is defined such that A = -A compared to mrmustard.physics.bargmann. + """ + A = -A + ordering = np.arange(2 * A.shape[0] // 2).reshape(2, -1).T.flatten() + A = tf.gather(A, ordering, axis=1) + A = tf.gather(A, ordering) + B = tf.gather(B, ordering) + return A, B + + def hermite_renormalized_diagonal( + self, A: tf.Tensor, B: tf.Tensor, C: tf.Tensor, cutoffs: Tuple[int] + ) -> tf.Tensor: + r"""First, reorder A and B parameters of Bargmann representation to match conventions in mrmustard.math.numba.compactFock~ + Then, calculate the required renormalized multidimensional Hermite polynomial. + """ + A, B = self.reorder_AB_bargmann(A, B) + return self.hermite_renormalized_diagonal_reorderedAB(A, B, C, cutoffs=cutoffs) + + @tf.custom_gradient + def hermite_renormalized_diagonal_reorderedAB( + self, A: tf.Tensor, B: tf.Tensor, C: tf.Tensor, cutoffs: Tuple[int] + ) -> tf.Tensor: + r"""Renormalized multidimensional Hermite polynomial given by the "exponential" Taylor + series of :math:`exp(C + Bx - Ax^2)` at zero, where the series has :math:`sqrt(n!)` at the + denominator rather than :math:`n!`. Note the minus sign in front of ``A``. + + Calculates the diagonal of the Fock representation (i.e. the PNR detection probabilities of all modes) + by applying the recursion relation in a selective manner. + + Args: + A: The A matrix. + B: The B vector. + C: The C scalar. + cutoffs: upper boundary of photon numbers in each mode + + Returns: + The renormalized Hermite polynomial. + """ + poly0, poly2, poly1010, poly1001, poly1 = tf.numpy_function( + hermite_multidimensional_diagonal, [A, B, C, cutoffs], [A.dtype] * 5 + ) + + def grad(dLdpoly): + dpoly_dC, dpoly_dA, dpoly_dB = tf.numpy_function( + grad_hermite_multidimensional_diagonal, + [A, B, C, poly0, poly2, poly1010, poly1001, poly1], + [poly0.dtype] * 3, + ) + ax = tuple(range(dLdpoly.ndim)) + dLdA = self.sum(dLdpoly[..., None, None] * self.conj(dpoly_dA), axes=ax) + dLdB = self.sum(dLdpoly[..., None] * self.conj(dpoly_dB), axes=ax) + dLdC = self.sum(dLdpoly * self.conj(dpoly_dC), axes=ax) + return dLdA, dLdB, dLdC + + return poly0, grad + + def hermite_renormalized_1leftoverMode( + self, A: tf.Tensor, B: tf.Tensor, C: tf.Tensor, cutoffs: Tuple[int] + ) -> tf.Tensor: + r"""First, reorder A and B parameters of Bargmann representation to match conventions in mrmustard.math.numba.compactFock~ + Then, calculate the required renormalized multidimensional Hermite polynomial. + """ + A, B = self.reorder_AB_bargmann(A, B) + return self.hermite_renormalized_1leftoverMode_reorderedAB(A, B, C, cutoffs=cutoffs) + + @tf.custom_gradient + def hermite_renormalized_1leftoverMode_reorderedAB( + self, A: tf.Tensor, B: tf.Tensor, C: tf.Tensor, cutoffs: Tuple[int] + ) -> tf.Tensor: + r"""Renormalized multidimensional Hermite polynomial given by the "exponential" Taylor + series of :math:`exp(C + Bx - Ax^2)` at zero, where the series has :math:`sqrt(n!)` at the + denominator rather than :math:`n!`. Note the minus sign in front of ``A``. + + Calculates all possible Fock representations of mode 0, + where all other modes are PNR detected. + This is done by applying the recursion relation in a selective manner. + + Args: + A: The A matrix. + B: The B vector. + C: The C scalar. + cutoffs: upper boundary of photon numbers in each mode + + Returns: + The renormalized Hermite polynomial. + """ + poly0, poly2, poly1010, poly1001, poly1 = tf.numpy_function( + hermite_multidimensional_1leftoverMode, [A, B, C, cutoffs], [A.dtype] * 5 + ) + + def grad(dLdpoly): + dpoly_dC, dpoly_dA, dpoly_dB = tf.numpy_function( + grad_hermite_multidimensional_1leftoverMode, + [A, B, C, poly0, poly2, poly1010, poly1001, poly1], + [poly0.dtype] * 3, + ) + ax = tuple(range(dLdpoly.ndim)) + dLdA = self.sum(dLdpoly[..., None, None] * self.conj(dpoly_dA), axes=ax) + dLdB = self.sum(dLdpoly[..., None] * self.conj(dpoly_dB), axes=ax) + dLdC = self.sum(dLdpoly * self.conj(dpoly_dC), axes=ax) + return dLdA, dLdB, dLdC + + return poly0, grad + @tf.custom_gradient def displacement(self, r, phi, cutoff, tol=1e-15): """creates a single mode displacement matrix""" diff --git a/mrmustard/physics/fock.py b/mrmustard/physics/fock.py index 273d0b913..a8b984362 100644 --- a/mrmustard/physics/fock.py +++ b/mrmustard/physics/fock.py @@ -29,6 +29,7 @@ wigner_to_bargmann_U, ) +from mrmustard.math.numba.compactFock_diagonal_amps import fock_representation_diagonal_amps from mrmustard.math.mmtensor import MMTensor from mrmustard.math.caching import tensor_int_cache from mrmustard.types import List, Tuple, Tensor, Scalar, Matrix, Sequence, Vector @@ -56,28 +57,34 @@ def fock_state(n: Sequence[int]) -> Tensor: return psi -def autocutoffs( - number_stdev: Matrix, number_means: Vector, max_cutoff: int = None, min_cutoff: int = None -) -> Tuple[int, ...]: - r"""Returns the autocutoffs of a Wigner state. +def autocutoffs(cov: Matrix, means: Vector, probability: float): + r"""Returns the cutoffs of a Gaussian state by computing the 1-mode marginals until + the probability of the marginal is less than ``probability``. Args: - number_stdev: the photon number standard deviation in each mode - (i.e. the square root of the diagonal of the covariance matrix) - number_means: the photon number means vector - max_cutoff: the maximum cutoff + cov: the covariance matrix + means: the means vector + probability: the cutoff probability Returns: Tuple[int, ...]: the suggested cutoffs """ - if max_cutoff is None: - max_cutoff = settings.AUTOCUTOFF_MAX_CUTOFF - if min_cutoff is None: - min_cutoff = settings.AUTOCUTOFF_MIN_CUTOFF - autocutoffs = settings.AUTOCUTOFF_MIN_CUTOFF + math.cast( - number_means + number_stdev * settings.AUTOCUTOFF_STDEV_FACTOR, "int32" - ) - return [int(n) for n in math.clip(autocutoffs, min_cutoff, max_cutoff)] + M = len(means) // 2 + cutoffs = [] + for i in range(M): + cov_i = np.array([[cov[i, i], cov[i, i + M]], [cov[i + M, i], cov[i + M, i + M]]]) + means_i = np.array([means[i], means[i + M]]) + # apply 1-d recursion until probability is less than 0.99 + A, B, C = [math.asnumpy(x) for x in wigner_to_bargmann_rho(cov_i, means_i)] + diag = fock_representation_diagonal_amps(A, B, C, 1, cutoffs=[100])[0] + # find at what index in the cumsum the probability is more than 0.99 + for i, val in enumerate(np.cumsum(diag)): + if val > probability: + cutoffs.append(max(i + 1, settings.AUTOCUTOFF_MIN_CUTOFF)) + break + else: + cutoffs.append(settings.AUTOCUTOFF_MAX_CUTOFF) + return cutoffs def wigner_to_fock_state( diff --git a/mrmustard/training/trainer.py b/mrmustard/training/trainer.py index 91096ba7d..738b6b8a2 100644 --- a/mrmustard/training/trainer.py +++ b/mrmustard/training/trainer.py @@ -98,13 +98,16 @@ def cost_fn(circ=make_circ(0.1), y_targ=0.): """ -from inspect import signature, Parameter -from functools import partial -from typing import Sequence, Mapping import warnings +from functools import partial +from inspect import Parameter, signature +from typing import Mapping, Sequence + import numpy as np from rich.progress import track + import mrmustard as mm + from .optimizer import Optimizer @@ -341,7 +344,7 @@ def cost_fn(circ=make_circ(0.1), y_targ=0.): "Failed to import `ray` which is an extra dependency. Please install with `pip install -e .[ray]`." ) from e - if not ray.is_initialized(): + if not ray.is_initialized(): # pragma: no cover ray.init(num_cpus=num_cpus) return_dict = False diff --git a/tests/test_lab/test_detectors.py b/tests/test_lab/test_detectors.py index 66a1ca172..a3e84d013 100644 --- a/tests/test_lab/test_detectors.py +++ b/tests/test_lab/test_detectors.py @@ -389,7 +389,11 @@ def test_norm_2mode(self, normalize, expected_norm): """Checks that projecting a two-mode coherent state onto a number state produces a state with the expected norm.""" leftover = Coherent(x=[2.0, 2.0]) << Fock(3, normalize=normalize)[0] - assert np.isclose(expected_norm, physics.norm(leftover), atol=1e-5) + assert np.isclose( + expected_norm * np.sqrt(settings.AUTOCUTOFF_PROBABILITY), + physics.norm(leftover), + rtol=1 - settings.AUTOCUTOFF_PROBABILITY, + ) def test_norm_2mode_gaussian_normalized(self): """Checks that after projection the norm of the leftover state is as expected.""" diff --git a/tests/test_lab/test_gates_fock.py b/tests/test_lab/test_gates_fock.py index a56f79869..7e2845666 100644 --- a/tests/test_lab/test_gates_fock.py +++ b/tests/test_lab/test_gates_fock.py @@ -104,13 +104,13 @@ def test_single_mode_fock_equals_gaussian_ket(gate): def test_single_mode_fock_equals_gaussian_ket_dm_2(gate): """Test same state is obtained via fock representation or phase space for single mode circuits.""" - cutoffs = [40] - gaussian_state = SqueezedVacuum(0.3) + cutoffs = [50] + gaussian_state = SqueezedVacuum(-0.1) fock_state = State(ket=gaussian_state.ket(cutoffs)) - via_fock_space_dm = (fock_state >> gate >> Attenuator(0.2)).dm([10]) - via_phase_space_dm = (gaussian_state >> gate >> Attenuator(0.2)).dm([10]) - assert np.allclose(via_fock_space_dm, via_phase_space_dm) + via_fock_space_dm = (fock_state >> gate >> Attenuator(0.1)).dm([10]) + via_phase_space_dm = (gaussian_state >> gate >> Attenuator(0.1)).dm([10]) + assert np.allclose(via_fock_space_dm, via_phase_space_dm, atol=1e-5) @given(gate=two_mode_unitary_gate()) diff --git a/tests/test_math/test_compactFock.py b/tests/test_math/test_compactFock.py new file mode 100644 index 000000000..e7f5d0cbf --- /dev/null +++ b/tests/test_math/test_compactFock.py @@ -0,0 +1,116 @@ +""" +Unit tests for mrmustard.math.numba.compactFock~ +""" +import numpy as np +from mrmustard.lab import Vacuum, State, SqueezedVacuum, Thermal, Sgate, Dgate, Ggate +from mrmustard.physics.bargmann import wigner_to_bargmann_rho +from mrmustard.physics import fidelity, normalize +from mrmustard.training import Optimizer +from mrmustard.math import Math + +math = Math() # use methods in math if you want them to be differentiable + + +def random_ABC(M): + """ + generate random Bargmann parameters A,B,C + for a multimode Gaussian state with displacement + """ + random_vals = np.random.uniform(low=0, high=1, size=[5, M]) + state = ( + Thermal(nbar=random_vals[0] * 10) + >> Sgate(r=random_vals[1], phi=random_vals[2] * 2 * np.pi) + >> Dgate(x=random_vals[3], y=random_vals[4]) + >> Ggate(num_modes=M) + ) + A, B, G0 = wigner_to_bargmann_rho(state.cov, state.means) + return A, B, G0 + + +def test_compactFock_diagonal(): + """Test getting Fock amplitudes if all modes are detected (math.hermite_renormalized_diagonal)""" + M = 3 + cutoffs = [7, 4, 8] + A, B, G0 = random_ABC(M) # Create random state (M mode Gaussian state with displacement) + + # Vanilla MM + G_ref = math.hermite_renormalized( + math.conj(-A), math.conj(B), math.conj(G0), shape=list(cutoffs) * 2 + ).numpy() # note: shape=[C1,C2,C3,...,C1,C2,C3,...] + + # Extract diagonal amplitudes from vanilla MM + ref_diag = np.zeros(cutoffs, dtype=np.complex128) + for inds in np.ndindex(*cutoffs): + inds_expanded = list(inds) + list(inds) # a,b,c,a,b,c + ref_diag[inds] = G_ref[tuple(inds_expanded)] + + # New MM + G_diag = math.hermite_renormalized_diagonal(math.conj(-A), math.conj(B), math.conj(G0), cutoffs) + assert np.allclose(ref_diag, G_diag) + + +def test_compactFock_1leftover(): + """Test getting Fock amplitudes if all but the first mode are detected (math.hermite_renormalized_1leftoverMode)""" + M = 3 + cutoffs = [7, 4, 8] + A, B, G0 = random_ABC(M) # Create random state (M mode Gaussian state with displacement) + + # New algorithm + G_leftover = math.hermite_renormalized_1leftoverMode( + math.conj(-A), math.conj(B), math.conj(G0), cutoffs + ) + + # Vanilla MM + G_ref = math.hermite_renormalized( + math.conj(-A), math.conj(B), math.conj(G0), shape=list(cutoffs) * 2 + ).numpy() # note: shape=[C1,C2,C3,...,C1,C2,C3,...] + + # Extract amplitudes of leftover mode from vanilla MM + ref_leftover = np.zeros([cutoffs[0]] * 2 + list(cutoffs)[1:], dtype=np.complex128) + for inds in np.ndindex(*cutoffs[1:]): + ref_leftover[tuple([slice(cutoffs[0]), slice(cutoffs[0])] + list(inds))] = G_ref[ + tuple([slice(cutoffs[0])] + list(inds) + [slice(cutoffs[0])] + list(inds)) + ] + assert np.allclose(ref_leftover, G_leftover) + + +def test_compactFock_diagonal_gradients(): + """Test getting Fock amplitudes AND GRADIENTS if all modes are detected (math.hermite_renormalized_diagonal)""" + + def cost_fn(): + n1, n2, n3 = 2, 2, 4 # number of detected photons + state_opt = Vacuum(3) >> I + A, B, G0 = wigner_to_bargmann_rho(state_opt.cov, state_opt.means) + G = math.hermite_renormalized_diagonal( + math.conj(-A), math.conj(B), math.conj(G0), cutoffs=[3, 3, 5] + ) + p = G[n1, n2, n3] + p_target = 0.5 + return math.abs(p_target - p) + + I = Ggate(num_modes=3, symplectic_trainable=True) + opt = Optimizer(symplectic_lr=0.1) + opt.minimize(cost_fn, by_optimizing=[I], max_steps=50) + for iter in range(2, 50): + assert opt.opt_history[iter - 1] >= opt.opt_history[iter] + + +def test_compactFock_1leftover_gradients(): + """Test getting Fock amplitudes AND GRADIENTS if all but the first mode are detected (math.hermite_renormalized_1leftoverMode)""" + + def cost_fn(): + n2, n3 = 1, 3 # number of detected photons + state_opt = Vacuum(3) >> I + A, B, G0 = wigner_to_bargmann_rho(state_opt.cov, state_opt.means) + G = math.hermite_renormalized_1leftoverMode( + math.conj(-A), math.conj(B), math.conj(G0), cutoffs=[8, 2, 4] + ) + G_firstMode = G[:, :, n2, n3] + conditional_state = normalize(State(dm=G_firstMode)) + return -fidelity(conditional_state, SqueezedVacuum(r=1)) + + I = Ggate(num_modes=3, symplectic_trainable=True) + opt = Optimizer(symplectic_lr=0.1) + opt.minimize(cost_fn, by_optimizing=[I], max_steps=50) + for iter in range(2, 50): + assert opt.opt_history[iter - 1] >= opt.opt_history[iter] From a95c987dcbb09fd9c65d285d000a2c7cb8bc160a Mon Sep 17 00:00:00 2001 From: ziofil Date: Tue, 28 Feb 2023 09:51:25 -0800 Subject: [PATCH 30/53] Bugfix misc (#202) **Context:** Fixing small bugs/typos before 0.4 release **Description of the Change:** - Threshold detector can now be correctly initialized - changes in settings.HBAR are correctly reflected everywhere - Interferometer can be placed on any set of modes - number of decimals are respected in circuit drawer **Benefits:** Fewer bugs/typos **Possible Drawbacks:** None **Related GitHub Issues:** None --- .github/CHANGELOG.md | 3 ++ mrmustard/lab/abstract/measurement.py | 4 +-- mrmustard/lab/detectors.py | 23 +++++++------- mrmustard/lab/gates.py | 14 ++++----- mrmustard/physics/fock.py | 10 +++--- mrmustard/physics/gaussian.py | 2 +- mrmustard/training/parametrized.py | 15 ++++++--- tests/test_lab/test_gates_fock.py | 45 ++++++++++++++------------- tests/test_lab/test_states.py | 25 ++++++++------- 9 files changed, 76 insertions(+), 65 deletions(-) diff --git a/.github/CHANGELOG.md b/.github/CHANGELOG.md index 7240b04b3..cc33e3478 100644 --- a/.github/CHANGELOG.md +++ b/.github/CHANGELOG.md @@ -184,6 +184,9 @@ * The ray related tests were hanging in github action causing test to halt and fail. Now ray is forced to init with 1 cpu when running tests preventing the issue. [(#201)](https://github.com/XanaduAI/MrMustard/pull/201) +* Various minor bug fixes. +[(#202)](https://github.com/XanaduAI/MrMustard/pull/202) + ### Documentation ### Contributors diff --git a/mrmustard/lab/abstract/measurement.py b/mrmustard/lab/abstract/measurement.py index d4348fcc3..724a11d00 100644 --- a/mrmustard/lab/abstract/measurement.py +++ b/mrmustard/lab/abstract/measurement.py @@ -134,18 +134,16 @@ def _measure_fock(self, other: State) -> Union[State, float]: the detector is measuring. The remaining indices correspond to the density matrix of the unmeasured modes. Args - state (State): the quatum state + other (State): the quantum state Returns Tensor: a tensor representing the post-measurement state """ cutoffs = [] - used = 0 for mode in other.modes: if mode in self._modes: cutoffs.append( max(settings.PNR_INTERNAL_CUTOFF, other.cutoffs[other.indices(mode)]) ) - used += 1 else: cutoffs.append(other.cutoffs[other.indices(mode)]) if self.should_recompute_stochastic_channel() or math.any( diff --git a/mrmustard/lab/detectors.py b/mrmustard/lab/detectors.py index 10f738797..ebbf8c0ff 100644 --- a/mrmustard/lab/detectors.py +++ b/mrmustard/lab/detectors.py @@ -16,15 +16,17 @@ This module implements the set of detector classes that perform measurements on quantum circuits. """ -from typing import List, Tuple, Union, Optional, Iterable -from mrmustard.types import Matrix, Tensor -from mrmustard.training import Parametrized +from typing import Iterable, List, Optional, Tuple, Union + from mrmustard import settings from mrmustard.math import Math -from mrmustard.physics import gaussian, fock +from mrmustard.physics import fock, gaussian +from mrmustard.training import Parametrized +from mrmustard.types import Matrix, Tensor + from .abstract import FockMeasurement, Measurement, State -from .states import DisplacedSqueezed, Coherent from .gates import Rgate +from .states import Coherent, DisplacedSqueezed math = Math() @@ -194,7 +196,7 @@ def __init__( ) outcome = None - FockMeasurement.__init__(outcome, modes, cutoffs) + FockMeasurement.__init__(self, outcome, modes, cutoffs) self.recompute_stochastic_channel() @@ -327,7 +329,7 @@ class Homodyne(Generaldyne): quadrature_angle (float or List[float]): measurement quadrature angle result (optional float or List[float]): displacement amount modes (optional List[int]): the modes of the displaced squeezed state - r (optional float or List[float]): squeezing amount + r (optional float or List[float]): squeezing amount (default: ``settings.HOMODYNE_SQUEEZING``) """ def __init__( @@ -335,9 +337,9 @@ def __init__( quadrature_angle: Union[float, List[float]], result: Optional[Union[float, List[float]]] = None, modes: Optional[List[int]] = None, - r: Union[float, List[float]] = settings.HOMODYNE_SQUEEZING, + r: Optional[Union[float, List[float]]] = None, ): - self.r = r + self.r = r or settings.HOMODYNE_SQUEEZING self.quadrature_angle = math.atleast_1d(quadrature_angle, dtype="float64") # if no ``result`` provided, sample the outcome @@ -358,7 +360,7 @@ def __init__( units_factor = math.sqrt(2.0 * settings.HBAR, dtype="float64") state = DisplacedSqueezed( - r=r, phi=2 * self.quadrature_angle, x=x / units_factor, y=y / units_factor + r=self.r, phi=2 * self.quadrature_angle, x=x / units_factor, y=y / units_factor ) super().__init__(state=state, outcome=outcome, modes=modes) @@ -399,7 +401,6 @@ def _measure_fock(self, other) -> Union[State, float]: x_outcome, probability = fock.sample_homodyne( state=reduced_state.ket() if reduced_state.is_pure else reduced_state.dm(), quadrature_angle=self.quadrature_angle, - hbar=settings.HBAR, ) # Define conditional state of the homodyne measurement device and rotate back to the original basis. diff --git a/mrmustard/lab/gates.py b/mrmustard/lab/gates.py index 619608f74..18097dc4f 100644 --- a/mrmustard/lab/gates.py +++ b/mrmustard/lab/gates.py @@ -21,13 +21,12 @@ from typing import List, Optional, Sequence, Tuple, Union from mrmustard import settings +from mrmustard.lab.abstract import Transformation from mrmustard.math import Math from mrmustard.physics import gaussian from mrmustard.training import Parametrized from mrmustard.types import Tensor -from mrmustard.lab.abstract import Transformation - math = Math() __all__ = [ @@ -500,10 +499,8 @@ def __init__( orthogonal_trainable: bool = False, modes: Optional[List[int]] = None, ): - if modes is not None and ( - num_modes != len(modes) or any(mode >= num_modes for mode in modes) - ): - raise ValueError("Invalid number of modes and the mode list here!") + if modes is not None and (num_modes != len(modes)): + raise ValueError(f"Invalid number of modes: got {len(modes)}, should be {num_modes}") if orthogonal is None: U = math.random_unitary(num_modes) orthogonal = math.block([[math.real(U), -math.imag(U)], [math.imag(U), math.real(U)]]) @@ -546,11 +543,14 @@ def __init__( num_modes: int, orthogonal: Optional[Tensor] = None, orthogonal_trainable: bool = False, + modes: Optional[List[int]] = None, ): + if modes is not None and (num_modes != len(modes)): + raise ValueError(f"Invalid number of modes: got {len(modes)}, should be {num_modes}") if orthogonal is None: orthogonal = math.random_orthogonal(num_modes) super().__init__(orthogonal=orthogonal, orthogonal_trainable=orthogonal_trainable) - self._modes = list(range(num_modes)) + self._modes = modes or list(range(num_modes)) self._is_gaussian = True self.short_name = "RI" diff --git a/mrmustard/physics/fock.py b/mrmustard/physics/fock.py index a8b984362..e14c4fa52 100644 --- a/mrmustard/physics/fock.py +++ b/mrmustard/physics/fock.py @@ -32,7 +32,7 @@ from mrmustard.math.numba.compactFock_diagonal_amps import fock_representation_diagonal_amps from mrmustard.math.mmtensor import MMTensor from mrmustard.math.caching import tensor_int_cache -from mrmustard.types import List, Tuple, Tensor, Scalar, Matrix, Sequence, Vector +from mrmustard.types import List, Tuple, Tensor, Scalar, Matrix, Sequence, Vector, Optional from mrmustard import settings from mrmustard.math import Math @@ -743,7 +743,7 @@ def estimate_quadrature_axis(cutoff, minimum=5, period_resolution=20): def quadrature_distribution( - state: Tensor, quadrature_angle: float = 0.0, x: Vector = None, hbar: float = settings.HBAR + state: Tensor, quadrature_angle: float = 0.0, x: Vector = None, hbar: Optional[float] = None ): r"""Given the ket or density matrix of a single-mode state, it generates the probability density distribution :math:`\tr [ \rho |x_\phi> Tuple[float, float]: r"""Given a single-mode state, it generates the pdf of :math:`\tr [ \rho |x_\phi> Matrix: N = means.shape[-1] // 2 mCm = cov * means[:, None] * means[None, :] dd = math.diag(math.diag_part(mCm[:N, :N] + mCm[N:, N:] + mCm[:N, N:] + mCm[N:, :N])) / ( - 2 * hbar**2 + 2 * hbar**2 # TODO: sum(diag_part) is better than diag_part(sum) ) CC = (cov**2 + mCm) / (2 * hbar**2) return ( diff --git a/mrmustard/training/parametrized.py b/mrmustard/training/parametrized.py index 94ac56c4f..ff424a531 100644 --- a/mrmustard/training/parametrized.py +++ b/mrmustard/training/parametrized.py @@ -20,6 +20,8 @@ class constructor generate a backend Tensor and are assigned to fields from typing import Any, Generator, List, Sequence, Tuple +import numpy as np + from mrmustard.math import Math from mrmustard.training.parameter import ( Constant, @@ -75,11 +77,16 @@ def param_string(self, decimals: int) -> str: Returns: str: string representation of the parameter values """ - string = "" + strings = [] for _, value in self.kw_parameters: - if math.asnumpy(value).ndim == 0: # don't show arrays - string += f"{math.asnumpy(value):.{decimals}g}, " - return string.rstrip(", ") + value = math.asnumpy(value) + if value.ndim == 0: # don't show arrays + value = np.round(value, decimals) + int_part = int(value) + decimal_part = np.round(value - int_part, decimals) + string = str(int_part) + f"{decimal_part:.{decimals}g}"[1:] + strings.append(string) + return ", ".join(strings) @property def kw_parameters(self) -> Tuple[Tuple[str, Tensor]]: diff --git a/tests/test_lab/test_gates_fock.py b/tests/test_lab/test_gates_fock.py index 7e2845666..efa9f2f79 100644 --- a/tests/test_lab/test_gates_fock.py +++ b/tests/test_lab/test_gates_fock.py @@ -12,33 +12,37 @@ # See the License for the specific language governing permissions and # limitations under the License. -import pytest -from hypothesis import given, strategies as st import numpy as np +import pytest +from hypothesis import given from thewalrus.fock_gradients import ( - squeezing, beamsplitter, - two_mode_squeezing, mzgate, + squeezing, + two_mode_squeezing, ) -from tests.random import * -from mrmustard.physics import fock -from mrmustard.lab.states import Fock, State, SqueezedVacuum, TMSV -from mrmustard.physics import fock from mrmustard.lab import ( - Dgate, - Sgate, - Pgate, - Rgate, - CZgate, - CXgate, + Attenuator, BSgate, + Dgate, + Interferometer, MZgate, + Rgate, S2gate, - Attenuator, - Interferometer, - Vacuum, + Sgate, +) +from mrmustard.lab.states import TMSV, Fock, SqueezedVacuum, State +from mrmustard.physics import fock +from tests.random import ( + angle, + array_of_, + medium_float, + n_mode_pure_state, + r, + single_mode_cv_channel, + single_mode_unitary_gate, + two_mode_unitary_gate, ) @@ -50,7 +54,7 @@ def test_Dgate_1mode(state, x, y): def test_attenuator_on_fock(): "tests that attenuating a fock state makes it mixed" - assert (Fock(10) >> Attenuator(0.5)).is_pure == False + assert not (Fock(10) >> Attenuator(0.5)).is_pure @given(state=n_mode_pure_state(num_modes=2), xxyy=array_of_(medium_float, minlen=4, maxlen=4)) @@ -203,11 +207,8 @@ def test_fock_representation_rgate(cutoffs, angles, modes): def test_raise_interferometer_error(): - """test Interferometer raises an error when both `modes` and `num_modes` are given""" + """test Interferometer raises an error when both `modes` and `num_modes` don't match""" num_modes = 3 modes = [0, 2] with pytest.raises(ValueError): Interferometer(num_modes=num_modes, modes=modes) - modes = [2, 5, 6] - with pytest.raises(ValueError): - Interferometer(num_modes=num_modes, modes=modes) diff --git a/tests/test_lab/test_states.py b/tests/test_lab/test_states.py index e431d0c2b..721620fdd 100644 --- a/tests/test_lab/test_states.py +++ b/tests/test_lab/test_states.py @@ -12,27 +12,27 @@ # See the License for the specific language governing permissions and # limitations under the License. -from threading import currentThread import numpy as np import pytest -from hypothesis import given, strategies as st, assume +from hypothesis import assume, given +from hypothesis import strategies as st from hypothesis.extra.numpy import arrays -from mrmustard.physics import gaussian as gp + +from mrmustard import settings +from mrmustard.lab.abstract import State +from mrmustard.lab.gates import Attenuator, Dgate, Ggate, Sgate from mrmustard.lab.states import ( - Fock, Coherent, - Vacuum, + DisplacedSqueezed, + Fock, Gaussian, SqueezedVacuum, - DisplacedSqueezed, Thermal, + Vacuum, ) -from mrmustard.lab.gates import Attenuator, Sgate, Dgate, Ggate -from mrmustard.lab.abstract import State -from mrmustard import settings -from tests.random import * - from mrmustard.math import Math +from mrmustard.physics import gaussian as gp +from tests.random import angle, medium_float, n_mode_pure_state, nmodes, r math = Math() @@ -202,7 +202,8 @@ def test_random_state_is_entangled(): @given(modes=st.lists(st.integers(), min_size=2, max_size=5, unique=True)) def test_getitem_set_modes(modes): - """Test that using `State.__getitem__` and `modes` kwarg correctly set the modes of the state.""" + """Test that using `State.__getitem__` and `modes` + kwarg correctly set the modes of the state.""" cutoff = len(modes) + 1 ket = np.zeros([cutoff] * len(modes), dtype=np.complex128) From df427714c62801081be9c80188080b0cfec57348 Mon Sep 17 00:00:00 2001 From: ziofil Date: Tue, 28 Feb 2023 10:35:03 -0800 Subject: [PATCH 31/53] Improve mmtensor (#195) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit **Context:** `MMTensor` is already very useful, in this PR we improve it further. **Description of the Change:** Adds basic algebraic operations and a more robust getitem **Benefits:** More use cases to tackle with MMTensor **Possible Drawbacks:** None **Related GitHub Issues:** None --------- Co-authored-by: Sebastián Duque Mesa <675763+sduquemesa@users.noreply.github.com> --- .github/CHANGELOG.md | 3 +- mrmustard/math/mmtensor.py | 134 +++++++++++++++++++++---------- mrmustard/math/tensorflow.py | 5 +- tests/test_math/test_mmtensor.py | 103 +++++++++++++++++++++++- 4 files changed, 198 insertions(+), 47 deletions(-) diff --git a/.github/CHANGELOG.md b/.github/CHANGELOG.md index cc33e3478..4aed2ba9d 100644 --- a/.github/CHANGELOG.md +++ b/.github/CHANGELOG.md @@ -75,7 +75,8 @@ * We introduce the tensor wrapper `MMTensor` (available in `math.mmtensor`) that allows for a very easy handling of tensor contractions. Internally MrMustard performs lots of tensor contractions and this wrapper allows one to label each index of a tensor and perform contractions using the `@` symbol as if it were a simple matrix multiplication (the indices with the same name get contracted). - [(#185)](https://github.com/XanaduAI/MrMustard/pull/185) + [(#185)](https://github.com/XanaduAI/MrMustard/pull/185)
+ [(#195)](https://github.com/XanaduAI/MrMustard/pull/195) ```python from mrmustard.math.mmtensor import MMTensor diff --git a/mrmustard/math/mmtensor.py b/mrmustard/math/mmtensor.py index 57e6644e8..46a71aaeb 100644 --- a/mrmustard/math/mmtensor.py +++ b/mrmustard/math/mmtensor.py @@ -17,9 +17,10 @@ """ This module contains the implementation of a tensor wrapper class. """ - -from typing import List, Optional, Union import string +from numbers import Number +from typing import List, Optional, Union + from mrmustard.math import Math math = Math() @@ -29,7 +30,8 @@ class MMTensor: r"""A Mr Mustard tensor (a wrapper around an array that implements the numpy array API).""" def __init__(self, array, axis_labels=None): - # If the input array is an MMTensor, use its tensor and axis labels (or the provided ones if specified) + # If the input array is an MMTensor, + # use its tensor and axis labels (or the provided ones if specified) if isinstance(array, MMTensor): self.tensor = array.tensor self.axis_labels = axis_labels or array.axis_labels @@ -46,24 +48,71 @@ def __init__(self, array, axis_labels=None): raise ValueError("The number of axis labels must be equal to the number of axes.") def __array__(self): - """ - Implement the NumPy array interface. - """ + r"""Implement the NumPy array interface.""" return self.tensor def __array_ufunc__(self, ufunc, method, *inputs, **kwargs): - """ + r""" Implement the NumPy ufunc interface. """ if method == "__call__": + inputs = [i.tensor if isinstance(i, MMTensor) else i for i in inputs] return MMTensor(ufunc(*inputs, **kwargs), self.axis_labels) else: - return NotImplemented + return NotImplemented(f"Cannot call {method} on {ufunc}.") + + def __mul__(self, other): + r"""implement the * operator""" + if isinstance(other, Number): + return MMTensor(self.tensor * other, self.axis_labels) + if isinstance(other, MMTensor): + self.check_axis_labels_match(other) + return MMTensor(self.tensor * other.tensor, self.axis_labels) + return NotImplemented(f"Cannot multiply {type(self)} and {type(other)}") + + def __rmul__(self, other): + return self.__mul__(other) + + def __truediv__(self, other): + r"""implement the / operator""" + if isinstance(other, MMTensor): + self.check_axis_labels_match(other) + return MMTensor(self.tensor / other.tensor, self.axis_labels) + try: + return MMTensor(self.tensor / other, self.axis_labels) + except TypeError: + return NotImplemented(f"Cannot divide {type(self)} by {type(other)}") + + def __add__(self, other): + r"""implement the + operator""" + if isinstance(other, MMTensor): + self.check_axis_labels_match(other) + return MMTensor(self.tensor + other.tensor, self.axis_labels) + try: + return MMTensor(self.tensor + other, self.axis_labels) + except TypeError: + return NotImplemented(f"Cannot add {type(self)} and {type(other)}") - def __matmul__(self, other): - """ - Overload the @ operator to perform tensor contractions. + def __sub__(self, other): + return self.__add__(-other) + + def __neg__(self): + return MMTensor(-self.tensor, self.axis_labels) + + def check_axis_labels_match(self, other): + r""" + Check that the axis labels of *this* tensor match those of another tensor. """ + if self.axis_labels != other.axis_labels: + raise ValueError( + f"Axis labels must match (got {self.axis_labels} and {other.axis_labels})" + ) + + def __radd__(self, other): + return self.__add__(other) + + def __matmul__(self, other): + r"""Overload the @ operator to perform tensor contractions.""" # if not isinstance(other, MMTensor): # raise TypeError(f"Cannot contract with object of type {type(other)}") @@ -88,8 +137,8 @@ def __matmul__(self, other): ) def contract(self, relabeling: Optional[List[str]] = None): - """ - Contract the tensor along the specified indices using einsum. + r""" + Contract *this* tensor along the specified indices using einsum. Args: relabeling (list[str]): An optional list of new axis labels. @@ -110,61 +159,60 @@ def contract(self, relabeling: Optional[List[str]] = None): unique_labels.append(label) repeated = [label for label in unique_labels if self.axis_labels.count(label) > 1] - # Turn labels into consecutive ascii lower-case letters, with same letters corresponding to the same label + # Turn labels into consecutive ascii lower-case letters, + # with same letters corresponding to the same label label_map = {label: string.ascii_lowercase[i] for i, label in enumerate(unique_labels)} labels = [label_map[label] for label in self.axis_labels] # create einsum string from labels einsum_str = "".join(labels) - # Contract the tensor and assign new axis labels (unique labels except for the contracted ones) + # Contract the tensor and assign new axis labels (unique except for the contracted ones) return MMTensor( math.einsum(einsum_str, self.tensor), [label for label in unique_labels if label not in repeated], ) def transpose(self, perm: Union[List[int], List[str]]): - """ - Transpose the tensor using a list of axis labels or indices. - """ + """Transpose the tensor using a list of axis labels or indices.""" if set(perm) == set(self.axis_labels): perm = [self.axis_labels.index(label) for label in perm] return MMTensor(math.transpose(self.tensor, perm), [self.axis_labels[i] for i in perm]) def reshape(self, shape, axis_labels=None): - """ - Reshape the tensor. Allows to change the axis labels. - """ + """Reshape the tensor. Allows to change the axis labels.""" return MMTensor(math.reshape(self.tensor, shape), axis_labels or self.axis_labels) def __getitem__(self, indices): - """ - Implement indexing into the tensor. - """ - if isinstance(indices, tuple): - axis_labels = [] - for i, ind in enumerate(indices): - if ind is Ellipsis and i == 0: - axis_labels += self.axis_labels[:i] - elif isinstance(ind, slice): - axis_labels += self.axis_labels[i] - elif ind is Ellipsis and i > 0: - axis_labels += self.axis_labels[i:] - break - return MMTensor(self.tensor[indices], axis_labels) - else: - # Index along a single axis and take care of the axis labels - return MMTensor( - self.tensor[indices], self.axis_labels[:indices] + self.axis_labels[indices + 1 :] - ) + """Implement indexing into the tensor.""" + indices = indices if isinstance(indices, tuple) else (indices,) + axis_labels = self.axis_labels.copy() + offset = 0 + for i, ind in enumerate(indices): + if isinstance(ind, int): + axis_labels.pop(i + offset) + offset -= 1 + elif ind is Ellipsis and i == 0: + offset = len(self.tensor.shape) - len(indices) + elif ind is Ellipsis and i > 0: + break + + return MMTensor(self.tensor[indices], axis_labels) def __repr__(self): return f"MMTensor({self.tensor}, {self.axis_labels})" def __getattribute__(self, name): - """ - Implement the underlying array's methods. - """ + r""" + Overrides built-in getattribute method for fallback attribute lookup. + Tries to get attribute from self, then from self.tensor + + Args: + self (object): instance + name (str): attribute name + + Returns: + attribute value or raises AttributeError""" try: return super().__getattribute__(name) except AttributeError: diff --git a/mrmustard/math/tensorflow.py b/mrmustard/math/tensorflow.py index 6b3c04849..815e7e2c2 100644 --- a/mrmustard/math/tensorflow.py +++ b/mrmustard/math/tensorflow.py @@ -104,7 +104,10 @@ def constraint_func( np.inf if bounds[1] is None else bounds[1], ) if bounds != (-np.inf, np.inf): - constraint: Optional[Callable] = lambda x: tf.clip_by_value(x, bounds[0], bounds[1]) + + def constraint(x): + return tf.clip_by_value(x, bounds[0], bounds[1]) + else: constraint = None return constraint diff --git a/tests/test_math/test_mmtensor.py b/tests/test_math/test_mmtensor.py index 254ab3ac7..d5fd63713 100644 --- a/tests/test_math/test_mmtensor.py +++ b/tests/test_math/test_mmtensor.py @@ -15,12 +15,13 @@ """ Unit tests for the :class:`MMTensor`. """ +import numpy as np import pytest -from mrmustard.math.mmtensor import MMTensor + from mrmustard.math import Math +from mrmustard.math.mmtensor import MMTensor math = Math() -import numpy as np def test_mmtensor_creation(): @@ -73,6 +74,104 @@ def test_mmtensor_contract(): assert trace == 5 +def test_mmtensor_getitem_slice(): + """Test that MMTensor slices correctly""" + array = np.random.normal(size=(2, 3, 4)) + mmtensor = MMTensor(array, axis_labels=["0", "1", "2"]) + sliced = mmtensor[0, :, 0] + assert sliced.axis_labels == ["1"] + assert np.allclose(sliced, array[0, :, 0]) + + +def test_mmtensor_getitem_int(): + """Test that MMTensor slices correctly""" + array = np.random.normal(size=(2, 3, 4)) + mmtensor = MMTensor(array, axis_labels=["0", "1", "2"]) + sliced = mmtensor[0, 0, 0] + assert sliced.axis_labels == [] + assert np.allclose(sliced, array[0, 0, 0]) + + +def test_mmtensor_getitem_ellipsis_beginning(): + """Test that MMTensor slices correctly""" + array = np.random.normal(size=(2, 3, 4)) + mmtensor = MMTensor(array, axis_labels=["0", "1", "2"]) + sliced = mmtensor[..., 2] + assert mmtensor[..., 2].axis_labels == ["0", "1"] + assert np.allclose(sliced, array[..., 2]) + + +def test_ufunc(): + """Test that MMTensor ufuncs work""" + array = np.random.normal(size=(2, 3, 4)) + mmtensor = MMTensor(array, axis_labels=["0", "1", "2"]) + assert np.allclose(np.sin(mmtensor), np.sin(array)) + + +def test_mmtensor_algebra_add(): + """Test that MMTensor addition works""" + array = np.random.normal(size=(2, 3, 4)) + mmtensor = MMTensor(array, axis_labels=["0", "1", "2"]) + assert np.allclose(mmtensor + mmtensor, array + array) + + +def test_mmtensor_algebra_add_different_labels(): + """Test that MMTensor addition with different labels raises error""" + array = np.random.normal(size=(2, 3, 4)) + mmtensor1 = MMTensor(array, axis_labels=["0", "1", "2"]) + mmtensor2 = MMTensor(array, axis_labels=["0", "1", "3"]) + with pytest.raises(ValueError): + mmtensor1 + mmtensor2 # pylint: disable=pointless-statement + + +def test_mmtensor_algebra_subtract(): + """Test that MMTensor subtraction works""" + array = np.random.normal(size=(2, 3, 4)) + mmtensor = MMTensor(array, axis_labels=["0", "1", "2"]) + assert np.allclose(mmtensor - mmtensor, array - array) + + +def test_mmtensor_algebra_subtract_different_labels(): + """Test that MMTensor subtraction with different labels raises error""" + array = np.random.normal(size=(2, 3, 4)) + mmtensor1 = MMTensor(array, axis_labels=["0", "1", "2"]) + mmtensor2 = MMTensor(array, axis_labels=["0", "1", "3"]) + with pytest.raises(ValueError): + mmtensor1 - mmtensor2 # pylint: disable=pointless-statement + + +def test_mmtensor_algebra_multiply(): + """Test that MMTensor multiplication works""" + array = np.random.normal(size=(2, 3, 4)) + mmtensor = MMTensor(array, axis_labels=["0", "1", "2"]) + assert np.allclose(mmtensor * mmtensor, array * array) + + +def test_mmtensor_algebra_multiply_different_labels(): + """Test that MMTensor multiplication with different labels raises error""" + array = np.random.normal(size=(2, 3, 4)) + mmtensor1 = MMTensor(array, axis_labels=["0", "1", "2"]) + mmtensor2 = MMTensor(array, axis_labels=["0", "1", "3"]) + with pytest.raises(ValueError): + mmtensor1 * mmtensor2 # pylint: disable=pointless-statement + + +def test_mmtensor_algebra_divide(): + """Test that MMTensor division works""" + array = np.random.normal(size=(2, 3, 4)) + mmtensor = MMTensor(array, axis_labels=["0", "1", "2"]) + assert np.allclose(mmtensor / mmtensor, array / array) + + +def test_mmtensor_algebra_divide_different_labels(): + """Test that MMTensor division with different labels raises error""" + array = np.random.normal(size=(2, 3, 4)) + mmtensor1 = MMTensor(array, axis_labels=["0", "1", "2"]) + mmtensor2 = MMTensor(array, axis_labels=["0", "1", "3"]) + with pytest.raises(ValueError): + mmtensor1 / mmtensor2 # pylint: disable=pointless-statement + + def test_mmtensor_contract_multiple_indices(): """Test that MMTensor contracts multiple indices correctly""" array = np.array([[[1, 2], [3, 4]], [[5, 6], [7, 8]]]) From b936c395e4dc0c70f924ab566e8dcb6358367e42 Mon Sep 17 00:00:00 2001 From: ziofil Date: Tue, 28 Feb 2023 14:42:04 -0800 Subject: [PATCH 32/53] displays sign correctly (#209) **Context:** negative parameters were showing a 0 instead of the minus sign. **Description of the Change:** Fixes this issue **Benefits:** Correct parameters! **Possible Drawbacks:** None **Related GitHub Issues:** None --- .github/CHANGELOG.md | 3 +++ mrmustard/training/parametrized.py | 5 +++-- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/.github/CHANGELOG.md b/.github/CHANGELOG.md index 4aed2ba9d..abf2b157a 100644 --- a/.github/CHANGELOG.md +++ b/.github/CHANGELOG.md @@ -188,6 +188,9 @@ * Various minor bug fixes. [(#202)](https://github.com/XanaduAI/MrMustard/pull/202) +* The sign of parameters in the circuit drawer are now displayed correctly. + [(#209)](https://github.com/XanaduAI/MrMustard/pull/209) + ### Documentation ### Contributors diff --git a/mrmustard/training/parametrized.py b/mrmustard/training/parametrized.py index ff424a531..340d3a718 100644 --- a/mrmustard/training/parametrized.py +++ b/mrmustard/training/parametrized.py @@ -81,10 +81,11 @@ def param_string(self, decimals: int) -> str: for _, value in self.kw_parameters: value = math.asnumpy(value) if value.ndim == 0: # don't show arrays - value = np.round(value, decimals) + sign = "-" if value < 0 else "" + value = np.abs(np.round(value, decimals)) int_part = int(value) decimal_part = np.round(value - int_part, decimals) - string = str(int_part) + f"{decimal_part:.{decimals}g}"[1:] + string = sign + str(int_part) + f"{decimal_part:.{decimals}g}"[1:] strings.append(string) return ", ".join(strings) From fbc1614c171409691a522cd04ab614ed528b30cc Mon Sep 17 00:00:00 2001 From: ziofil Date: Tue, 28 Feb 2023 15:21:50 -0800 Subject: [PATCH 33/53] better type annotations (#199) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit **Context:** We never addressed type annotations in MrMustard **Description of the Change:** This PR begins making improvements all across the board on type annotations **Benefits:** Easier to spot errors and bugs, finally we can rely on a type checker as well **Possible Drawbacks:** One more thing to maintain, but it should be easy once set up **Related GitHub Issues:** None --------- Co-authored-by: Sebastián Duque Mesa <675763+sduquemesa@users.noreply.github.com> --- .github/CHANGELOG.md | 5 + .github/workflows/builds.yml | 6 +- .github/workflows/format.yml | 2 +- .github/workflows/tests.yml | 2 +- .github/workflows/upload.yml | 2 +- .readthedocs.yml | 29 ++++-- mrmustard/lab/abstract/measurement.py | 24 +++-- mrmustard/lab/abstract/state.py | 75 ++++++++------- mrmustard/lab/abstract/transformation.py | 37 ++++---- mrmustard/lab/circuit.py | 7 +- mrmustard/lab/detectors.py | 14 ++- mrmustard/lab/gates.py | 11 +-- mrmustard/lab/states.py | 4 +- mrmustard/math/autocast.py | 2 +- mrmustard/math/math_interface.py | 10 +- mrmustard/math/tensorflow.py | 15 +-- mrmustard/math/torch.py | 28 +++--- mrmustard/physics/fock.py | 15 +-- mrmustard/physics/gaussian.py | 2 +- mrmustard/training/parameter.py | 8 +- mrmustard/training/parameter_update.py | 4 +- mrmustard/training/parametrized.py | 2 +- mrmustard/types.py | 31 ------ mrmustard/typing.py | 88 +++++++++++++++++ mrmustard/utils/xptensor.py | 10 +- setup.py | 1 - tests/test_typing.py | 115 +++++++++++++++++++++++ 27 files changed, 375 insertions(+), 174 deletions(-) delete mode 100644 mrmustard/types.py create mode 100644 mrmustard/typing.py create mode 100644 tests/test_typing.py diff --git a/.github/CHANGELOG.md b/.github/CHANGELOG.md index abf2b157a..b1f450a65 100644 --- a/.github/CHANGELOG.md +++ b/.github/CHANGELOG.md @@ -155,6 +155,11 @@ which have been refactored and moved out of `physics.fock`. [(#185)](https://github.com/XanaduAI/MrMustard/pull/185) + +* The internal type system in MrMustard has been beefed up with much clearer types, like ComplexVector, RealMatrix, etc... as well as a generic type `Batch`, which can be parametrized using +the other types, like `Batch[ComplexTensor]`. This will allow for better type checking and better error messages. + [(#199)](https://github.com/XanaduAI/MrMustard/pull/199) + * Added multiple tests and improved the use of Hypothesis. [(#191)](https://github.com/XanaduAI/MrMustard/pull/191) diff --git a/.github/workflows/builds.yml b/.github/workflows/builds.yml index bd2bb9dc7..1acc3dfc0 100644 --- a/.github/workflows/builds.yml +++ b/.github/workflows/builds.yml @@ -16,7 +16,7 @@ jobs: strategy: fail-fast: false matrix: - python-version: ['3.8', '3.9', '3.10'] + python-version: ['3.9', '3.10'] steps: - name: Cancel Previous Runs @@ -45,12 +45,12 @@ jobs: - name: Run tests run: | - if [ ${{ matrix.python-version }} == "3.8" ]; then COVERAGE="--cov=mrmustard --cov-report=term-missing --cov-report=xml"; else COVERAGE=""; fi + if [ ${{ matrix.python-version }} == "3.9" ]; then COVERAGE="--cov=mrmustard --cov-report=term-missing --cov-report=xml"; else COVERAGE=""; fi python -m pytest tests -p no:warnings --tb=native ${{COVERAGE}} - name: Upload coverage to Codecov uses: codecov/codecov-action@v3 - if: ${{ matrix.python-version }} == '3.8' + if: ${{ matrix.python-version }} == '3.9' with: files: ./coverage.xml fail_ci_if_error: true diff --git a/.github/workflows/format.yml b/.github/workflows/format.yml index 7ea0f2990..7d2457a96 100644 --- a/.github/workflows/format.yml +++ b/.github/workflows/format.yml @@ -12,7 +12,7 @@ jobs: - name: Set up Python uses: actions/setup-python@v4 with: - python-version: 3.8 + python-version: 3.9 - name: Install black formatter run: | diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 8a62db5db..b081aae43 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -22,7 +22,7 @@ jobs: - name: Setup python ${{ matrix.python-version }} uses: actions/setup-python@v4 with: - python-version: '3.8' + python-version: '3.9' - name: Install dependencies run: | diff --git a/.github/workflows/upload.yml b/.github/workflows/upload.yml index c25a3470d..e52e7ba79 100644 --- a/.github/workflows/upload.yml +++ b/.github/workflows/upload.yml @@ -15,7 +15,7 @@ jobs: - name: Set up Python uses: actions/setup-python@v4 with: - python-version: 3.8 + python-version: 3.9 - name: Build and install Mr Mustard run: | diff --git a/.readthedocs.yml b/.readthedocs.yml index 2de688ba5..195e00930 100644 --- a/.readthedocs.yml +++ b/.readthedocs.yml @@ -1,15 +1,24 @@ +# Required version: 2 +# Set the version of Python and other tools you might need +build: + os: ubuntu-22.04 + tools: + python: "3.9" + +# Build documentation in the docs/ directory with Sphinx sphinx: - configuration: doc/conf.py + configuration: doc/conf.py -python: - version: 3.8 - install: - - requirements: doc/requirements.txt - - method: pip - path: . - system_packages: true +# If using Sphinx, optionally build your docs in additional formats such as PDF +# formats: +# - pdf -build: - image: latest +# Optionally declare the Python requirements required to build your docs +python: + install: + - requirements: doc/requirements.txt + - method: pip + path: . + system_packages: true diff --git a/mrmustard/lab/abstract/measurement.py b/mrmustard/lab/abstract/measurement.py index 724a11d00..60a66471c 100644 --- a/mrmustard/lab/abstract/measurement.py +++ b/mrmustard/lab/abstract/measurement.py @@ -15,11 +15,14 @@ """This module contains the implementation of the class :class:`FockMeasurement`.""" from __future__ import annotations + from abc import ABC, abstractmethod -from mrmustard.math import Math +from typing import Iterable, Sequence, Union -from mrmustard.types import Tensor, Callable, Sequence, Iterable, Union from mrmustard import settings +from mrmustard.math import Math +from mrmustard.typing import Tensor + from .state import State math = Math() @@ -30,11 +33,11 @@ class Measurement(ABC): implement Args: - outcome (optional, List[float] or Array): the result of the measurement + outcome (optional, List[float] or Tensor): the result of the measurement modes (List[int]): the modes on which the measurement is acting on """ - def __init__(self, outcome: Tensor, modes: Iterable[int]) -> None: + def __init__(self, outcome: Tensor, modes: Sequence[int]) -> None: super().__init__() if modes is None: @@ -89,7 +92,7 @@ def __lshift__(self, other) -> Union[State, float]: f"Cannot apply Measurement '{self.__qualname__}' to '{other.__qualname__}'." ) - def __getitem__(self, items) -> Callable: + def __getitem__(self, items) -> Measurement: """Assign modes via the getitem syntax: allows measurements to be used as ``output = meas[0,1](input)``, e.g. measuring modes 0 and 1. """ @@ -116,7 +119,7 @@ class FockMeasurement(Measurement): in the Fock basis. """ - def __init__(self, outcome: Tensor, modes: Iterable[int], cutoffs: Iterable[int]) -> None: + def __init__(self, outcome: Tensor, modes: Sequence[int], cutoffs: Sequence[int]) -> None: self._cutoffs = cutoffs or [settings.PNR_INTERNAL_CUTOFF] * len(modes) super().__init__(outcome, modes) @@ -173,6 +176,13 @@ def _measure_fock(self, other: State) -> Union[State, float]: def should_recompute_stochastic_channel(self) -> bool: # override in subclasses """Returns `True` if the stochastic channel has to be recomputed. - This method should be overriden by subclasses accordingly. + This method should be overriden by subclasses as needed. """ return False + + def recompute_stochastic_channel(self, cutoffs: Sequence[int]) -> None: + """Recomputes the stochastic channel. + + This method should be overriden by subclasses as needed. + """ + raise NotImplementedError diff --git a/mrmustard/lab/abstract/state.py b/mrmustard/lab/abstract/state.py index c04068d5e..cc2945e30 100644 --- a/mrmustard/lab/abstract/state.py +++ b/mrmustard/lab/abstract/state.py @@ -15,27 +15,30 @@ """This module contains the implementation of the :class:`State` class.""" from __future__ import annotations -from typing import TYPE_CHECKING import warnings -import numpy as np - -from mrmustard.types import ( - Matrix, - Vector, - Array, - Tensor, +from typing import ( + TYPE_CHECKING, + Iterable, + List, + Optional, Sequence, - Union, Tuple, - Optional, - List, - Iterable, + Union, ) -from mrmustard.utils import graphics + +import numpy as np + from mrmustard import settings -from mrmustard.physics import gaussian, fock from mrmustard.math import Math +from mrmustard.physics import fock, gaussian +from mrmustard.typing import ( + RealMatrix, + RealVector, + RealTensor, + ComplexTensor, +) +from mrmustard.utils import graphics if TYPE_CHECKING: from .transformation import Transformation @@ -49,12 +52,12 @@ class State: def __init__( self, - cov: Matrix = None, - means: Vector = None, - eigenvalues: Array = None, - symplectic: Matrix = None, - ket: Array = None, - dm: Array = None, + cov: RealMatrix = None, + means: RealVector = None, + eigenvalues: RealVector = None, + symplectic: RealMatrix = None, + ket: ComplexTensor = None, + dm: ComplexTensor = None, modes: Sequence[int] = None, cutoffs: Sequence[int] = None, _norm: float = 1.0, @@ -69,9 +72,9 @@ def __init__( Args: cov (Matrix): the covariance matrix means (Vector): the means vector - eigenvalues (Array): the eigenvalues of the covariance matrix + eigenvalues (Tensor): the eigenvalues of the covariance matrix symplectic (Matrix): the symplectic matrix mapping the thermal state with given eigenvalues to this state - fock (Array): the Fock representation + fock (Tensor): the Fock representation modes (optional, Sequence[int]): the modes in which the state is defined cutoffs (Sequence[int], default=None): set to force the cutoff dimensions of the state _norm (float, default=1.0): the norm of the state. Warning: only set if you know what you are doing. @@ -148,17 +151,17 @@ def is_pure(self): return True if self._ket is not None else np.isclose(self.purity, 1.0, atol=1e-6) @property - def means(self) -> Optional[Vector]: + def means(self) -> Optional[RealVector]: r"""Returns the means vector of the state.""" return self._means @property - def cov(self) -> Optional[Matrix]: + def cov(self) -> Optional[RealMatrix]: r"""Returns the covariance matrix of the state.""" return self._cov @property - def number_stdev(self) -> Vector: + def number_stdev(self) -> RealVector: r"""Returns the square root of the photon number variances (standard deviation) in each mode.""" if self.is_gaussian: return math.sqrt(math.diag_part(self.number_cov)) @@ -189,7 +192,7 @@ def shape(self) -> List[int]: return self.cutoffs if self.is_pure else self.cutoffs + self.cutoffs @property - def fock(self) -> Array: + def fock(self) -> ComplexTensor: r"""Returns the Fock representation of the state.""" if self._dm is None and self._ket is None: _fock = fock.wigner_to_fock_state( @@ -204,7 +207,7 @@ def fock(self) -> Array: return self._ket if self._ket is not None else self._dm @property - def number_means(self) -> Vector: + def number_means(self) -> RealVector: r"""Returns the mean photon number for each mode.""" if self.is_gaussian: return gaussian.number_means(self.cov, self.means, settings.HBAR) @@ -212,7 +215,7 @@ def number_means(self) -> Vector: return fock.number_means(tensor=self.fock, is_dm=self.is_mixed) @property - def number_cov(self) -> Matrix: + def number_cov(self) -> RealMatrix: r"""Returns the complete photon number covariance matrix.""" if not self.is_gaussian: raise NotImplementedError("number_cov not yet implemented for non-gaussian states") @@ -234,7 +237,7 @@ def probability(self) -> float: return norm**2 return norm - def ket(self, cutoffs: List[int] = None) -> Optional[Tensor]: + def ket(self, cutoffs: List[int] = None) -> Optional[ComplexTensor]: r"""Returns the ket of the state in Fock representation or ``None`` if the state is mixed. Args: @@ -271,7 +274,7 @@ def ket(self, cutoffs: List[int] = None) -> Optional[Tensor]: return padded[tuple(slice(s) for s in cutoffs)] return self._ket[tuple(slice(s) for s in cutoffs)] - def dm(self, cutoffs: List[int] = None) -> Tensor: + def dm(self, cutoffs: Optional[List[int]] = None) -> ComplexTensor: r"""Returns the density matrix of the state in Fock representation. Args: @@ -303,7 +306,7 @@ def dm(self, cutoffs: List[int] = None) -> Tensor: return padded[tuple(slice(s) for s in cutoffs + cutoffs)] return self._dm[tuple(slice(s) for s in cutoffs + cutoffs)] - def fock_probabilities(self, cutoffs: Sequence[int]) -> Tensor: + def fock_probabilities(self, cutoffs: Sequence[int]) -> RealTensor: r"""Returns the probabilities in Fock representation. If the state is pure, they are the absolute value squared of the ket amplitudes. @@ -313,7 +316,7 @@ def fock_probabilities(self, cutoffs: Sequence[int]) -> Tensor: cutoffs List[int]: the cutoff dimensions for each mode Returns: - Array: the probabilities + Tensor: the probabilities """ if self._fock_probabilities is None: if self.is_mixed: @@ -480,7 +483,7 @@ def __and__(self, other: State) -> State: cov=cov, means=means, modes=self.modes + [m + self.num_modes for m in other.modes] ) - def __getitem__(self, item): + def __getitem__(self, item) -> State: "setting the modes of a state (same API of `Transformation`)" if isinstance(item, int): item = [item] @@ -495,7 +498,7 @@ def __getitem__(self, item): self._modes = item return self - def get_modes(self, item): + def get_modes(self, item) -> State: r"""Returns the state on the given modes.""" if isinstance(item, int): item = [item] @@ -521,7 +524,7 @@ def get_modes(self, item): return State(dm=fock_partitioned, modes=item) # TODO: refactor - def __eq__(self, other): + def __eq__(self, other) -> bool: r"""Returns whether the states are equal.""" if self.num_modes != other.num_modes: return False @@ -542,7 +545,7 @@ def __eq__(self, other): self.dm(cutoffs=other.cutoffs), other.dm(cutoffs=other.cutoffs), atol=1e-6 ) - def __rshift__(self, other): + def __rshift__(self, other: Transformation) -> State: r"""Applies other (a Transformation) to self (a State), e.g., ``Coherent(x=0.1) >> Sgate(r=0.1)``.""" if issubclass(other.__class__, State): raise TypeError( diff --git a/mrmustard/lab/abstract/transformation.py b/mrmustard/lab/abstract/transformation.py index 81e1c397c..b459fb7fe 100644 --- a/mrmustard/lab/abstract/transformation.py +++ b/mrmustard/lab/abstract/transformation.py @@ -14,26 +14,29 @@ """This module contains the implementation of the :class:`Transformation` class.""" -from __future__ import annotations -import numpy as np +# pylint: disable = missing-function-docstring -from mrmustard import settings -from mrmustard.math import Math -from mrmustard.physics import fock, gaussian -from mrmustard.training.parameter import Parameter -from mrmustard.types import ( +from __future__ import annotations + +from typing import ( Callable, Iterable, List, - Matrix, Optional, Sequence, Tuple, Union, - Vector, ) +import numpy as np + +from mrmustard import settings +from mrmustard.math import Math +from mrmustard.physics import fock, gaussian +from mrmustard.training.parameter import Parameter +from mrmustard.typing import RealMatrix, RealVector + from .state import State math = Math() @@ -148,25 +151,25 @@ def _validate_modes(self, modes): pass @property - def X_matrix(self) -> Optional[Matrix]: + def X_matrix(self) -> Optional[RealMatrix]: return None @property - def Y_matrix(self) -> Optional[Matrix]: + def Y_matrix(self) -> Optional[RealMatrix]: return None @property - def d_vector(self) -> Optional[Vector]: + def d_vector(self) -> Optional[RealVector]: return None @property - def X_matrix_dual(self) -> Optional[Matrix]: + def X_matrix_dual(self) -> Optional[RealMatrix]: if (X := self.X_matrix) is None: return None return gaussian.math.inv(X) @property - def Y_matrix_dual(self) -> Optional[Matrix]: + def Y_matrix_dual(self) -> Optional[RealMatrix]: if (Y := self.Y_matrix) is None: return None if (Xdual := self.X_matrix_dual) is None: @@ -174,7 +177,7 @@ def Y_matrix_dual(self) -> Optional[Matrix]: return math.matmul(math.matmul(Xdual, Y), math.transpose(Xdual)) @property - def d_vector_dual(self) -> Optional[Vector]: + def d_vector_dual(self) -> Optional[RealVector]: if (d := self.d_vector) is None: return None if (Xdual := self.X_matrix_dual) is None: @@ -182,7 +185,7 @@ def d_vector_dual(self) -> Optional[Vector]: return math.matmul(Xdual, d) @property - def XYd(self) -> Tuple[Optional[Matrix], Optional[Matrix], Optional[Vector]]: + def XYd(self) -> Tuple[Optional[RealMatrix], Optional[RealMatrix], Optional[RealVector]]: r"""Returns the ```(X, Y, d)``` triple. Override in subclasses if computing ``X``, ``Y`` and ``d`` together is more efficient. @@ -190,7 +193,7 @@ def XYd(self) -> Tuple[Optional[Matrix], Optional[Matrix], Optional[Vector]]: return self.X_matrix, self.Y_matrix, self.d_vector @property - def XYd_dual(self) -> Tuple[Optional[Matrix], Optional[Matrix], Optional[Vector]]: + def XYd_dual(self) -> Tuple[Optional[RealMatrix], Optional[RealMatrix], Optional[RealVector]]: r"""Returns the ```(X, Y, d)``` triple of the dual of the current transformation. Override in subclasses if computing ``Xdual``, ``Ydual`` and ``ddual`` together is more efficient. diff --git a/mrmustard/lab/circuit.py b/mrmustard/lab/circuit.py index b8f4c956f..dc34d2708 100644 --- a/mrmustard/lab/circuit.py +++ b/mrmustard/lab/circuit.py @@ -20,13 +20,12 @@ __all__ = ["Circuit"] - +from mrmustard.typing import RealMatrix, RealVector from typing import List, Optional, Tuple from mrmustard import settings from mrmustard.lab.abstract import State, Transformation from mrmustard.training import Parametrized -from mrmustard.types import Matrix, Vector from mrmustard.utils.circdrawer import circuit_text from mrmustard.utils.xptensor import XPMatrix, XPVector @@ -66,7 +65,9 @@ def dual(self, state: State) -> State: @property def XYd( self, - ) -> Tuple[Matrix, Matrix, Vector]: # NOTE: Overriding Transformation.XYd for efficiency. + ) -> Tuple[ + RealMatrix, RealMatrix, RealVector + ]: # NOTE: Overriding Transformation.XYd for efficiency X = XPMatrix(like_1=True) Y = XPMatrix(like_0=True) d = XPVector() diff --git a/mrmustard/lab/detectors.py b/mrmustard/lab/detectors.py index ebbf8c0ff..f6fb600ae 100644 --- a/mrmustard/lab/detectors.py +++ b/mrmustard/lab/detectors.py @@ -16,13 +16,14 @@ This module implements the set of detector classes that perform measurements on quantum circuits. """ -from typing import Iterable, List, Optional, Tuple, Union +from typing import List, Tuple, Union, Optional, Iterable + from mrmustard import settings from mrmustard.math import Math from mrmustard.physics import fock, gaussian from mrmustard.training import Parametrized -from mrmustard.types import Matrix, Tensor +from mrmustard.typing import RealMatrix, RealVector from .abstract import FockMeasurement, Measurement, State from .gates import Rgate @@ -68,7 +69,7 @@ def __init__( dark_counts_trainable: bool = False, efficiency_bounds: Tuple[Optional[float], Optional[float]] = (0.0, 1.0), dark_counts_bounds: Tuple[Optional[float], Optional[float]] = (0.0, None), - stochastic_channel: Matrix = None, + stochastic_channel: RealMatrix = None, modes: List[int] = None, cutoffs: Union[int, List[int]] = None, ): @@ -235,7 +236,10 @@ class Generaldyne(Measurement): """ def __init__( - self, state: State, outcome: Optional[Tensor] = None, modes: Optional[Iterable[int]] = None + self, + state: State, + outcome: Optional[RealVector] = None, + modes: Optional[Iterable[int]] = None, ) -> None: if not state.is_gaussian: raise TypeError("Generaldyne measurement state must be Gaussian.") @@ -255,7 +259,7 @@ def __init__( super().__init__(outcome, modes) @property - def outcome(self) -> Tensor: + def outcome(self) -> RealVector: return self.state.means def primal(self, other: State) -> Union[State, float]: diff --git a/mrmustard/lab/gates.py b/mrmustard/lab/gates.py index 18097dc4f..d1c8a0455 100644 --- a/mrmustard/lab/gates.py +++ b/mrmustard/lab/gates.py @@ -18,14 +18,13 @@ This module defines gates and operations that can be applied to quantum modes to construct a quantum circuit. """ -from typing import List, Optional, Sequence, Tuple, Union - +from typing import Union, Optional, List, Tuple, Sequence +from mrmustard.typing import RealMatrix from mrmustard import settings from mrmustard.lab.abstract import Transformation from mrmustard.math import Math from mrmustard.physics import gaussian from mrmustard.training import Parametrized -from mrmustard.types import Tensor math = Math() @@ -495,7 +494,7 @@ class Interferometer(Parametrized, Transformation): def __init__( self, num_modes: int, - orthogonal: Optional[Tensor] = None, + orthogonal: Optional[RealMatrix] = None, orthogonal_trainable: bool = False, modes: Optional[List[int]] = None, ): @@ -541,7 +540,7 @@ class RealInterferometer(Parametrized, Transformation): def __init__( self, num_modes: int, - orthogonal: Optional[Tensor] = None, + orthogonal: Optional[RealMatrix] = None, orthogonal_trainable: bool = False, modes: Optional[List[int]] = None, ): @@ -590,7 +589,7 @@ class Ggate(Parametrized, Transformation): def __init__( self, num_modes: int, - symplectic: Optional[Tensor] = None, + symplectic: Optional[RealMatrix] = None, symplectic_trainable: bool = False, ): symplectic = symplectic if symplectic is not None else math.random_symplectic(num_modes) diff --git a/mrmustard/lab/states.py b/mrmustard/lab/states.py index 1b6ee4ebe..f8cb3a580 100644 --- a/mrmustard/lab/states.py +++ b/mrmustard/lab/states.py @@ -17,7 +17,7 @@ """ from typing import Union, Optional, List, Tuple, Sequence -from mrmustard.types import Scalar, Vector, Matrix +from mrmustard.typing import Scalar, Vector, RealMatrix from mrmustard import settings from mrmustard.physics import gaussian, fock from mrmustard.training import Parametrized @@ -415,7 +415,7 @@ class Gaussian(Parametrized, State): def __init__( self, num_modes: int, - symplectic: Matrix = None, + symplectic: RealMatrix = None, eigenvalues: Vector = None, symplectic_trainable: bool = False, eigenvalues_trainable: bool = False, diff --git a/mrmustard/math/autocast.py b/mrmustard/math/autocast.py index 3bd66ef3e..de267bf4c 100644 --- a/mrmustard/math/autocast.py +++ b/mrmustard/math/autocast.py @@ -15,7 +15,7 @@ """This module contains the implementation of the decorator class :class:`Autocast`.""" from functools import wraps -from mrmustard.types import List +from typing import List class Autocast: diff --git a/mrmustard/math/math_interface.py b/mrmustard/math/math_interface.py index 9e9e48568..80d722221 100644 --- a/mrmustard/math/math_interface.py +++ b/mrmustard/math/math_interface.py @@ -15,25 +15,19 @@ """This module contains the :class:`Math` interface that every backend has to implement.""" from abc import ABC, abstractmethod +from typing import Any, Callable, Dict, List, Optional, Sequence, Tuple from functools import lru_cache from itertools import product import numpy as np from scipy.special import binom from scipy.stats import unitary_group, ortho_group from mrmustard import settings -from mrmustard.types import ( - List, +from mrmustard.typing import ( Tensor, Matrix, Scalar, Vector, - Sequence, - Tuple, - Optional, - Dict, Trainable, - Callable, - Any, ) diff --git a/mrmustard/math/tensorflow.py b/mrmustard/math/tensorflow.py index 815e7e2c2..bb947d136 100644 --- a/mrmustard/math/tensorflow.py +++ b/mrmustard/math/tensorflow.py @@ -14,9 +14,12 @@ """This module contains the Tensorflow implementation of the :class:`Math` interface.""" +from typing import Callable, List, Sequence, Tuple, Union, Optional + import numpy as np import tensorflow as tf import tensorflow_probability as tfp + from thewalrus import hermite_multidimensional, grad_hermite_multidimensional from thewalrus.fock_gradients import ( displacement as displacement_tw, @@ -30,17 +33,9 @@ hermite_multidimensional_1leftoverMode, grad_hermite_multidimensional_1leftoverMode, ) + from mrmustard.math.autocast import Autocast -from mrmustard.types import ( - List, - Tensor, - Sequence, - Tuple, - Optional, - Trainable, - Callable, - Union, -) +from mrmustard.typing import Tensor, Trainable from .math_interface import MathInterface diff --git a/mrmustard/math/torch.py b/mrmustard/math/torch.py index 7b8d0c0fa..d60913da1 100644 --- a/mrmustard/math/torch.py +++ b/mrmustard/math/torch.py @@ -14,21 +14,14 @@ """This module contains the Pytorch implementation of the :class:`Math` interface.""" +from typing import Callable, List, Optional, Sequence, Tuple, Union, Dict + import numpy as np import torch -from mrmustard.types import ( - List, - Tensor, - Sequence, - Tuple, - Optional, - Dict, - Trainable, - Callable, - Union, -) from mrmustard.math.autocast import Autocast +from mrmustard.typing import Tensor, Trainable + from .math_interface import MathInterface @@ -181,12 +174,12 @@ def convolution( Returns: """ - batch_size = array.shape[0] + array.shape[0] input_channels = array.shape[1] output_channels = ... # TODO: unsure of how to get output channels if array.dim() == 3: # 1D case - signal_length = array.shape[2] + array.shape[2] m = torch.nn.Conv1d( input_channels, @@ -200,8 +193,8 @@ def convolution( return m(array) if array.dim() == 4: # 2D case - input_height = array.shape[2] - input_width = array.shape[3] + array.shape[2] + array.shape[3] m = torch.nn.Conv2d( input_channels, @@ -284,7 +277,10 @@ def constraint_func( np.inf if bounds[1] is None else bounds[1], ) if bounds != (-np.inf, np.inf): - constraint: Optional[Callable] = lambda x: torch.clamp(x, min=bounds[0], max=bounds[1]) + + def constraint(x): + return torch.clamp(x, min=bounds[0], max=bounds[1]) + else: constraint = None return constraint diff --git a/mrmustard/physics/fock.py b/mrmustard/physics/fock.py index e14c4fa52..0de86d23d 100644 --- a/mrmustard/physics/fock.py +++ b/mrmustard/physics/fock.py @@ -19,22 +19,23 @@ """ from functools import lru_cache -import numpy as np +from typing import List, Sequence, Tuple, Optional +import numpy as np +from mrmustard import settings +from mrmustard.math import Math +from mrmustard.math.caching import tensor_int_cache +from mrmustard.math.mmtensor import MMTensor from mrmustard.physics.bargmann import ( + wigner_to_bargmann_Choi, wigner_to_bargmann_psi, wigner_to_bargmann_rho, - wigner_to_bargmann_Choi, wigner_to_bargmann_U, ) from mrmustard.math.numba.compactFock_diagonal_amps import fock_representation_diagonal_amps -from mrmustard.math.mmtensor import MMTensor -from mrmustard.math.caching import tensor_int_cache -from mrmustard.types import List, Tuple, Tensor, Scalar, Matrix, Sequence, Vector, Optional -from mrmustard import settings -from mrmustard.math import Math +from mrmustard.typing import Matrix, Scalar, Tensor, Vector math = Math() diff --git a/mrmustard/physics/gaussian.py b/mrmustard/physics/gaussian.py index 454609015..9e31943cc 100644 --- a/mrmustard/physics/gaussian.py +++ b/mrmustard/physics/gaussian.py @@ -18,7 +18,7 @@ from typing import Tuple, Union, Sequence, Any, Optional from thewalrus.quantum import is_pure_cov -from mrmustard.types import Matrix, Vector, Scalar +from mrmustard.typing import Matrix, Vector, Scalar from mrmustard.utils.xptensor import XPMatrix, XPVector from mrmustard import settings from mrmustard.math import Math diff --git a/mrmustard/training/parameter.py b/mrmustard/training/parameter.py index 48cda17fe..c9cc84d9e 100644 --- a/mrmustard/training/parameter.py +++ b/mrmustard/training/parameter.py @@ -55,15 +55,15 @@ def __init__(self, r: float, modes: List, r_trainable: bool): .. code-block:: class SymplecticGate(Parametrized): - def __init__(self, symplectic: Array): + def __init__(self, symplectic: Tensor): super.__init__(symplectic=symplectic, symplectic_trainable=True) class EuclideanGate(Parametrized): - def __init__(self, euclidean: Array): + def __init__(self, euclidean: Tensor): super.__init__(euclidean=euclidean, euclidean_trainable=True) class OrthogonalGate(Parametrized): - def __init__(self, orthogonal: Array): + def __init__(self, orthogonal: Tensor): super.__init__(orthogonal=orthogonal, orthogonal_trainable=True) The optimization procedure updates the value of the trainables *in-place*. @@ -85,7 +85,7 @@ def __init__(self, r: float): from typing import Optional, Sequence, Any from mrmustard.math import Math -from mrmustard.types import Tensor +from mrmustard.typing import Tensor math = Math() diff --git a/mrmustard/training/parameter_update.py b/mrmustard/training/parameter_update.py index aff90926e..6763fe141 100644 --- a/mrmustard/training/parameter_update.py +++ b/mrmustard/training/parameter_update.py @@ -15,8 +15,10 @@ """TODO: document this module """ -from mrmustard.types import Sequence, Tensor, Tuple +from typing import Tuple, Sequence from mrmustard.math import Math +from mrmustard.typing import Tensor + from .parameter import Trainable math = Math() diff --git a/mrmustard/training/parametrized.py b/mrmustard/training/parametrized.py index 340d3a718..d8f55401e 100644 --- a/mrmustard/training/parametrized.py +++ b/mrmustard/training/parametrized.py @@ -29,7 +29,7 @@ class constructor generate a backend Tensor and are assigned to fields Trainable, create_parameter, ) -from mrmustard.types import Tensor +from mrmustard.typing import Tensor math = Math() diff --git a/mrmustard/types.py b/mrmustard/types.py deleted file mode 100644 index 9c7921636..000000000 --- a/mrmustard/types.py +++ /dev/null @@ -1,31 +0,0 @@ -# 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. - -"""A module containing all base type annotations.""" - -# pylint: disable=unused-wildcard-import,wildcard-import - -from typing import * - -# NOTE: when type-annotating with typevars, objects with the same typevars must have the same type -# E.g. in `def f(x: Vector, y: Vector) -> Tensor: ...` -# the type of `x` and the type of `y` are assumed to be the same, even though "Vector" can mean different things. -Scalar = TypeVar("Scalar") -Vector = TypeVar("Vector") -Matrix = TypeVar("Matrix") -Tensor = TypeVar("Tensor") -Array = TypeVar("Array") # TODO: let mypy know that this is Vector, Matrix, or Tensor -Trainable = TypeVar("Trainable") - -Numeric = Union[Scalar, Vector, Matrix, Tensor] diff --git a/mrmustard/typing.py b/mrmustard/typing.py new file mode 100644 index 000000000..6f374ff8a --- /dev/null +++ b/mrmustard/typing.py @@ -0,0 +1,88 @@ +# 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. + +"""A module containing all base type annotations.""" +__all__ = [ + "RealVector", + "ComplexVector", + "IntVector", + "UIntVector", + "RealMatrix", + "ComplexMatrix", + "IntMatrix", + "UIntMatrix", + "RealTensor", + "ComplexTensor", + "IntTensor", + "UIntTensor", + "Batch", + "Scalar", + "Vector", + "Matrix", + "Tensor", + "Trainable", +] +from typing import Iterator, Protocol, Tuple, TypeVar, Union, runtime_checkable + +import numpy as np + +R = TypeVar("R", np.float16, np.float32, np.float64) +C = TypeVar("C", np.complex64, np.complex128) +Z = TypeVar("Z", np.int16, np.int32, np.int64) +N = TypeVar("N", np.uint16, np.uint32, np.uint64) + +Scalar = Union[R, C, Z, N] +Vector = np.ndarray[Tuple[int], Scalar] +Matrix = np.ndarray[Tuple[int, int], Scalar] +Tensor = np.ndarray[Tuple[int, ...], Scalar] + +RealVector = np.ndarray[Tuple[int], R] +ComplexVector = np.ndarray[Tuple[int], C] +IntVector = np.ndarray[Tuple[int], Z] +UIntVector = np.ndarray[Tuple[int], N] + +RealMatrix = np.ndarray[Tuple[int, int], R] +ComplexMatrix = np.ndarray[Tuple[int, int], C] +IntMatrix = np.ndarray[Tuple[int, int], Z] +UIntMatrix = np.ndarray[Tuple[int, int], N] + +RealTensor = np.ndarray[Tuple[int, ...], R] +ComplexTensor = np.ndarray[Tuple[int, ...], C] +IntTensor = np.ndarray[Tuple[int, ...], Z] +UIntTensor = np.ndarray[Tuple[int, ...], N] + +T_co = TypeVar( + "T_co", + RealVector, + ComplexVector, + IntVector, + UIntVector, + RealMatrix, + ComplexMatrix, + IntMatrix, + UIntMatrix, + RealTensor, + ComplexTensor, + IntTensor, + UIntTensor, + covariant=True, +) + +Trainable = TypeVar("Trainable") + + +@runtime_checkable +class Batch(Protocol[T_co]): # pylint: disable=missing-class-docstring + def __iter__(self) -> Iterator[T_co]: + ... diff --git a/mrmustard/utils/xptensor.py b/mrmustard/utils/xptensor.py index 31074e57b..63c3b1503 100644 --- a/mrmustard/utils/xptensor.py +++ b/mrmustard/utils/xptensor.py @@ -17,9 +17,17 @@ """This module contains the classes for representing Matrices and Vectors in phase space.""" from __future__ import annotations + from abc import ABC, abstractmethod -from mrmustard.types import Optional, Union, Matrix, Vector, List, Tensor, Tuple, Scalar +from typing import ( + List, + Optional, + Tuple, + Union, +) + from mrmustard.math import Math +from mrmustard.typing import Matrix, Scalar, Tensor, Vector math = Math() diff --git a/setup.py b/setup.py index bf547ca49..4b39d7e69 100644 --- a/setup.py +++ b/setup.py @@ -59,7 +59,6 @@ "Operating System :: Microsoft :: Windows", "Programming Language :: Python", "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3 :: Only", diff --git a/tests/test_typing.py b/tests/test_typing.py new file mode 100644 index 000000000..2766916e9 --- /dev/null +++ b/tests/test_typing.py @@ -0,0 +1,115 @@ +# 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. + +# pylint: disable=missing-function-docstring, missing-module-docstring + +from typing import get_origin, get_args +import numpy as np + +from mrmustard.typing import ( + Batch, + ComplexMatrix, + ComplexTensor, + ComplexVector, + IntMatrix, + IntTensor, + IntVector, + RealMatrix, + RealTensor, + RealVector, + UIntMatrix, + UIntTensor, + UIntVector, +) + + +def test_complexvector(): + vec: ComplexVector = np.array([1.0 + 1.0j]) + assert isinstance(vec, get_origin(ComplexVector)) + assert isinstance(vec[0], get_args(ComplexVector)[1].__constraints__) + + +def test_realvector(): + vec: RealVector = np.array([1.0, 2.0, 3.0]) + assert isinstance(vec, get_origin(RealVector)) + assert isinstance(vec[0], get_args(RealVector)[1].__constraints__) + + +def test_intvector(): + vec: IntVector = np.array([1, 2, 3]) + assert isinstance(vec, get_origin(IntVector)) + assert isinstance(vec[0], get_args(IntVector)[1].__constraints__) + + +def test_uintvector(): + vec: UIntVector = np.array([1, 2, 3], dtype=np.uint32) + assert isinstance(vec, get_origin(UIntVector)) + assert isinstance(vec[0], get_args(UIntVector)[1].__constraints__) + + +def test_complexmatrix(): + mat: ComplexMatrix = np.array([[1.0 + 1.0j, 2.0 + 2.0j, 3.0 + 3.0j]]) + assert isinstance(mat, get_origin(ComplexMatrix)) + assert isinstance(mat[0, 0], get_args(ComplexMatrix)[1].__constraints__) + + +def test_realmatrix(): + mat: RealMatrix = np.array([[1.0, 2.0, 3.0], [4.0, 5.0, 6.0]]) + assert isinstance(mat, get_origin(RealMatrix)) + assert isinstance(mat[0, 0], get_args(RealMatrix)[1].__constraints__) + + +def test_intmatrix(): + mat: IntMatrix = np.array([[1, 2, 3], [4, 5, 6]]) + assert isinstance(mat, get_origin(IntMatrix)) + assert isinstance(mat[0, 0], get_args(IntMatrix)[1].__constraints__) + + +def test_uintmatrix(): + mat: UIntMatrix = np.array([[1, 2, 3], [4, 5, 6]], dtype=np.uint32) + assert isinstance(mat, get_origin(UIntMatrix)) + assert isinstance(mat[0, 0], get_args(UIntMatrix)[1].__constraints__) + + +def test_complextensor(): + ten: ComplexTensor = np.array( + [[[1.0 + 1.0j, 2.0 + 2.0j, 3.0 + 3.0j], [4.0 + 4.0j, 5.0 + 5.0j, 6.0 + 6.0j]]] + ) + assert isinstance(ten, get_origin(ComplexTensor)) + assert isinstance(ten[0, 0, 0], get_args(ComplexTensor)[1].__constraints__) + + +def test_realtensor(): + ten: RealTensor = np.array([[[1.0, 2.0, 3.0], [4.0, 5.0, 6.0]]]) + assert isinstance(ten, get_origin(RealTensor)) + assert isinstance(ten[0, 0, 0], get_args(RealTensor)[1].__constraints__) + + +def test_inttensor(): + ten: IntTensor = np.array([[[1, 2, -3], [4, 5, -6]], [[7, 8, 9], [10, 11, 12]]]) + assert isinstance(ten, get_origin(IntTensor)) + assert isinstance(ten[0, 0, 0], get_args(IntTensor)[1].__constraints__) + + +def test_uinttensor(): + ten: UIntTensor = np.array([[[1, 2, 3], [4, 5, 6]], [[7, 8, 9], [10, 11, 12]]], dtype=np.uint32) + assert isinstance(ten, get_origin(UIntTensor)) + assert isinstance(ten[0, 0, 0], get_args(UIntTensor)[1].__constraints__) + + +def test_batch(): + batch: Batch[RealVector] = np.array([[1.0, 2.0, 3.0], [4.0, 5.0, 6.0]]) + # remember batch is a protocol, so we can't use isinstance + assert issubclass(type(batch), Batch) + assert isinstance(batch[0][0], get_args(get_args(Batch[RealVector])[0])[1].__constraints__) From 4fa732dedbc7d7300f2c74be062fac92cb61c153 Mon Sep 17 00:00:00 2001 From: Yuan <16817699+sylviemonet@users.noreply.github.com> Date: Wed, 1 Mar 2023 09:06:42 +0100 Subject: [PATCH 34/53] Add unitary update (#208) Fix the bugs about the Riemannian optimization. Add two optimization methods of the Interferometer (unitary update) and RealInterferometer (orthogonal update). --------- Co-authored-by: ziofil Co-authored-by: ziofil --- .github/CHANGELOG.md | 12 ++- doc/introduction/basic_reference.md | 2 +- mrmustard/lab/gates.py | 46 +++++----- mrmustard/math/math_interface.py | 17 ++++ mrmustard/training/optimizer.py | 11 ++- mrmustard/training/parameter.py | 40 +++++--- mrmustard/training/parameter_update.py | 24 +++-- mrmustard/training/parametrized.py | 4 +- tests/random.py | 2 +- tests/test_lab/test_gates_fock.py | 10 ++ tests/test_training/test_opt.py | 8 +- tests/test_training/test_parameter.py | 5 +- tests/test_training/test_parametrized.py | 15 ++- tests/test_training/test_riemannian_opt.py | 101 +++++++++++++++++++++ 14 files changed, 239 insertions(+), 58 deletions(-) create mode 100644 tests/test_training/test_riemannian_opt.py diff --git a/.github/CHANGELOG.md b/.github/CHANGELOG.md index b1f450a65..e0b988510 100644 --- a/.github/CHANGELOG.md +++ b/.github/CHANGELOG.md @@ -166,6 +166,11 @@ the other types, like `Batch[ComplexTensor]`. This will allow for better type ch * The `fock.autocutoff` function now uses the new diagonal methods for calculating a probability-based cutoff. Use `settings.AUTOCUTOFF_PROBABILITY` to set the probability threshold. [(#203)](https://github.com/XanaduAI/MrMustard/pull/203) + +* The unitary group optimization (for the interferometer) and the orthogonal group optimization (for the real interferometer) have been added. + The symplectic matrix that describes an interferometer belongs to the intersection of the orthogonal group and the symplectic group, which is a unitary group, + so we needed both. + [(#208)](https://github.com/XanaduAI/MrMustard/pull/208) ### Bug fixes @@ -191,7 +196,10 @@ the other types, like `Batch[ComplexTensor]`. This will allow for better type ch [(#201)](https://github.com/XanaduAI/MrMustard/pull/201) * Various minor bug fixes. -[(#202)](https://github.com/XanaduAI/MrMustard/pull/202) + [(#202)](https://github.com/XanaduAI/MrMustard/pull/202) + +* Fixed the issue that the optimization of the interferometer was using orthogonal group optimization rather than unitary. + [(#208)](https://github.com/XanaduAI/MrMustard/pull/208) * The sign of parameters in the circuit drawer are now displayed correctly. [(#209)](https://github.com/XanaduAI/MrMustard/pull/209) @@ -201,7 +209,7 @@ the other types, like `Batch[ComplexTensor]`. This will allow for better type ch ### Contributors This release contains contributions from (in alphabetical order): -[Robbe De Prins](https://github.com/rdprins), [Sebastian Duque Mesa](https://github.com/sduquemesa), [Filippo Miatto](https://github.com/ziofil) +[Robbe De Prins](https://github.com/rdprins), [Sebastian Duque Mesa](https://github.com/sduquemesa), [Filippo Miatto](https://github.com/ziofil), [Yuan Yao](https://github.com/sylviemonet) --- diff --git a/doc/introduction/basic_reference.md b/doc/introduction/basic_reference.md index 5fe982e90..ff3c2c00b 100644 --- a/doc/introduction/basic_reference.md +++ b/doc/introduction/basic_reference.md @@ -193,7 +193,7 @@ math.cos(0.1) # pytorch (upcoming) ``` ### Optimization -The `Optimizer` (available in `mrmustard.training` uses Adam underneath the hood for Euclidean parameters and a custom symplectic optimizer for Gaussian gates and states and an orthogonal optimizer for interferometers. +The `Optimizer` (available in `mrmustard.training` uses Adam underneath the hood for Euclidean parameters and a custom symplectic optimizer for Gaussian gates and states, an unitary optimizer for interferometers, and an orthogonal optimizer for real interferometers. We can turn any simulation in Mr Mustard into an optimization by marking which parameters we wish to be trainable. Let's take a simple example: synthesizing a displaced squeezed state. diff --git a/mrmustard/lab/gates.py b/mrmustard/lab/gates.py index d1c8a0455..1e8d826c2 100644 --- a/mrmustard/lab/gates.py +++ b/mrmustard/lab/gates.py @@ -19,7 +19,7 @@ """ from typing import Union, Optional, List, Tuple, Sequence -from mrmustard.typing import RealMatrix +from mrmustard.typing import RealMatrix, ComplexMatrix from mrmustard import settings from mrmustard.lab.abstract import Transformation from mrmustard.math import Math @@ -482,30 +482,29 @@ def _validate_modes(self, modes): class Interferometer(Parametrized, Transformation): r"""N-mode interferometer. - It corresponds to a Ggate with zero mean and a ``2N x 2N`` orthogonal symplectic matrix. + It corresponds to a Ggate with zero mean and a ``2N x 2N`` unitary symplectic matrix. Args: num_modes (int): the num_modes-mode interferometer - orthogonal (2d array): a valid orthogonal matrix. For N modes it must have shape `(2N,2N)` - orthogonal_trainable (bool): whether orthogonal is a trainable variable + unitary (2d array): a valid unitary matrix U. For N modes it must have shape `(N,N)` + unitary_trainable (bool): whether unitary is a trainable variable modes (optional, List[int]): the list of modes this gate is applied to """ def __init__( self, num_modes: int, - orthogonal: Optional[RealMatrix] = None, - orthogonal_trainable: bool = False, + unitary: Optional[ComplexMatrix] = None, + unitary_trainable: bool = False, modes: Optional[List[int]] = None, ): - if modes is not None and (num_modes != len(modes)): + if modes is not None and num_modes != len(modes): raise ValueError(f"Invalid number of modes: got {len(modes)}, should be {num_modes}") - if orthogonal is None: - U = math.random_unitary(num_modes) - orthogonal = math.block([[math.real(U), -math.imag(U)], [math.imag(U), math.real(U)]]) + if unitary is None: + unitary = math.random_unitary(num_modes) super().__init__( - orthogonal=orthogonal, - orthogonal_trainable=orthogonal_trainable, + unitary=unitary, + unitary_trainable=unitary_trainable, ) self._modes = modes or list(range(num_modes)) self.is_gaussian = True @@ -513,27 +512,32 @@ def __init__( @property def X_matrix(self): - return self.orthogonal.value + return math.block( + [ + [math.real(self.unitary.value), -math.imag(self.unitary.value)], + [math.imag(self.unitary.value), math.real(self.unitary.value)], + ] + ) def _validate_modes(self, modes): - if len(modes) != self.orthogonal.value.shape[-1] // 2: + if len(modes) != self.unitary.value.shape[-1]: raise ValueError( - f"Invalid number of modes: {len(modes)} (should be {self.orthogonal.shape[-1] // 2})" + f"Invalid number of modes: {len(modes)} (should be {self.unitary.shape[-1]})" ) def __repr__(self): modes = self.modes - orthogonal = repr(math.asnumpy(self.orthogonal.value)).replace("\n", "") - return f"Interferometer(num_modes = {len(modes)}, orthogonal = {orthogonal}){modes}" + unitary = repr(math.asnumpy(self.unitary.value)).replace("\n", "") + return f"Interferometer(num_modes = {len(modes)}, unitary = {unitary}){modes}" class RealInterferometer(Parametrized, Transformation): - r"""N-mode interferometer with a real unitary matrix (or block-diagonal orthogonal matrix). + r"""N-mode interferometer parametrized by an NxN orthogonal matrix (or 2N x 2N block-diagonal orthogonal matrix). This interferometer does not mix q and p. Does not mix q's and p's. Args: - orthogonal (2d array, optional): a valid orthogonal matrix. For N modes it must have shape `(N,N)`. - If set to `None` a random orthogonal matrix is used. + orthogonal (2d array, optional): a real unitary (orthogonal) matrix. For N modes it must have shape `(N,N)`. + If set to `None` a random real unitary (orthogonal) matrix is used. orthogonal_trainable (bool): whether orthogonal is a trainable variable """ @@ -557,7 +561,7 @@ def __init__( def X_matrix(self): return math.block( [ - [self.orthogonal.value, math.zeros_like(self.orthogonal.value)], + [self.orthogonal.value, -math.zeros_like(self.orthogonal.value)], [math.zeros_like(self.orthogonal.value), self.orthogonal.value], ] ) diff --git a/mrmustard/math/math_interface.py b/mrmustard/math/math_interface.py index 80d722221..f20be590b 100644 --- a/mrmustard/math/math_interface.py +++ b/mrmustard/math/math_interface.py @@ -1210,3 +1210,20 @@ def euclidean_to_symplectic(self, S: Matrix, dS_euclidean: Matrix) -> Matrix: Jmat = self.J(S.shape[-1] // 2) Z = self.matmul(self.transpose(S), dS_euclidean) return 0.5 * (Z + self.matmul(self.matmul(Jmat, self.transpose(Z)), Jmat)) + + def euclidean_to_unitary(self, U: Matrix, dU_euclidean: Matrix) -> Matrix: + r"""Convert the Euclidean gradient to a Riemannian gradient on the + tangent bundle of the unitary manifold. + + Implemented from: + Y Yao, F Miatto, N Quesada - arXiv preprint arXiv:2209.06069, 2022. + + Args: + U (Matrix): unitary matrix + dU_euclidean (Matrix): Euclidean gradient tensor + + Returns: + Matrix: unitary gradient tensor + """ + Z = self.matmul(self.conj(self.transpose(U)), dU_euclidean) + return 0.5 * (Z - self.conj(self.transpose(Z))) diff --git a/mrmustard/training/optimizer.py b/mrmustard/training/optimizer.py index 2152f9161..8ee5e0d19 100644 --- a/mrmustard/training/optimizer.py +++ b/mrmustard/training/optimizer.py @@ -42,11 +42,16 @@ class Optimizer: """ def __init__( - self, symplectic_lr: float = 0.1, orthogonal_lr: float = 0.1, euclidean_lr: float = 0.001 + self, + symplectic_lr: float = 0.1, + unitary_lr: float = 0.1, + orthogonal_lr: float = 0.1, + euclidean_lr: float = 0.001, ): self.learning_rate = { "euclidean": euclidean_lr, "symplectic": symplectic_lr, + "unitary": unitary_lr, "orthogonal": orthogonal_lr, } self.opt_history: List[float] = [0] @@ -104,7 +109,7 @@ def apply_gradients(self, trainable_params, grads): registered on :mod:`parameter_update` module. """ - # group grads and vars by type (i.e. euclidean, symplectic, orthogonal) + # group grads and vars by type (i.e. euclidean, symplectic, orthogonal, unitary) grouped_vars_and_grads = self._group_vars_and_grads_by_type(trainable_params, grads) for param_type, grads_vars in grouped_vars_and_grads.items(): @@ -133,7 +138,7 @@ def _get_trainable_params(trainable_items): @staticmethod def _group_vars_and_grads_by_type(trainable_params, grads): """Groups `trainable_params` and `grads` by type into a dict of the form - `{"euclidean": [...], "orthogonal": [...], "symplectic": [...]}`.""" + `{"euclidean": [...], "orthogonal": [...], "symplectic": [...]}, "unitary": [...]`.""" sorted_grads_and_vars = sorted( zip(grads, trainable_params), key=lambda grads_vars: grads_vars[1].type ) diff --git a/mrmustard/training/parameter.py b/mrmustard/training/parameter.py index c9cc84d9e..901757b84 100644 --- a/mrmustard/training/parameter.py +++ b/mrmustard/training/parameter.py @@ -49,7 +49,7 @@ def __init__(self, r: float, modes: List, r_trainable: bool): The dynamically assigned property is an instance of :class:`Parameter` and contains the ``value`` property which is a tensor of the autograd backend. - There are three types of trainable parameters: symplectic, euclidean and orthogonal. + There are four types of trainable parameters: symplectic, euclidean, unitary and orthogonal. Each type defines a different optimization procedure on the :py:training: module. .. code-block:: @@ -66,6 +66,10 @@ class OrthogonalGate(Parametrized): def __init__(self, orthogonal: Tensor): super.__init__(orthogonal=orthogonal, orthogonal_trainable=True) + class UnitaryGate(Parametrized): + def __init__(self, unitary: Array): + super.__init__(unitary=unitary, unitary_trainable=True) + The optimization procedure updates the value of the trainables *in-place*. 3. **Constant parameters** (bound and fixed): This class of parameters belong to the autograd @@ -174,17 +178,27 @@ def __init__(self, value: Any, name: str, owner: Optional[str] = None) -> None: self._owner = owner +class Unitary(Trainable): + """Unitary trainable. Uses :meth:`training.parameter_update.update_unitary`.""" + + def __init__(self, value: Any, name: str, owner: Optional[str] = None) -> None: + self._value = value_to_trainable(value, None, name) + self._name = name + self._owner = owner + + class Constant(Parameter): """Constant parameter. It belongs to the autograd backend but remains fixed during any optimization procedure """ def __init__(self, value: Any, name: str, owner: Optional[str] = None) -> None: - self._value = ( - value - if math.from_backend(value) and not math.is_trainable(value) - else math.new_constant(value, name) - ) + if math.from_backend(value) and not math.is_trainable(value): + self._value = value + elif type(value) in [list, int, float]: + self._value = math.new_constant(value, name) + else: + self._value = math.new_constant(value, name, value.dtype) self._name = name self._owner = owner @@ -218,6 +232,9 @@ def create_parameter( if name.startswith("orthogonal"): return Orthogonal(value, name, owner) + if name.startswith("unitary"): + return Unitary(value, name, owner) + return Euclidean(value, bounds, name, owner) @@ -230,8 +247,9 @@ def value_to_trainable(value: Any, bounds: Optional[Sequence], name: str) -> Ten for Euclidean parameters name (str): name of the parameter """ - return ( - value - if math.from_backend(value) and math.is_trainable(value) - else math.new_variable(value, bounds, name) - ) + if math.from_backend(value) and math.is_trainable(value): + return value + elif type(value) in [list, int, float]: + return math.new_variable(value, bounds, name) + else: + return math.new_variable(value, bounds, name, value.dtype) diff --git a/mrmustard/training/parameter_update.py b/mrmustard/training/parameter_update.py index 6763fe141..912fc3bf4 100644 --- a/mrmustard/training/parameter_update.py +++ b/mrmustard/training/parameter_update.py @@ -43,20 +43,25 @@ def update_symplectic(grads_and_vars: Sequence[Tuple[Tensor, Trainable]], symple def update_orthogonal(grads_and_vars: Sequence[Tuple[Tensor, Trainable]], orthogonal_lr: float): r"""Updates the orthogonal parameters using the given orthogonal gradients. Implemented from: - Fiori S, Bengio Y. Quasi-Geodesic Neural Learning Algorithms - Over the Orthogonal Group: A Tutorial. - Journal of Machine Learning Research. 2005 May 1;6(5). + Y Yao, F Miatto, N Quesada - arXiv preprint arXiv:2209.06069, 2022. """ for dO_euclidean, O in grads_and_vars: - dO_orthogonal = 0.5 * ( - dO_euclidean - math.matmul(math.matmul(O, math.transpose(dO_euclidean)), O) - ) - new_value = math.matmul( - O, math.expm(orthogonal_lr * math.matmul(math.transpose(dO_orthogonal), O)) - ) + Y = math.euclidean_to_unitary(O, math.real(dO_euclidean)) + new_value = math.matmul(O, math.expm(-orthogonal_lr * Y)) math.assign(O, new_value) +def update_unitary(grads_and_vars: Sequence[Tuple[Tensor, Trainable]], unitary_lr: float): + r"""Updates the unitary parameters using the given unitary gradients. + Implemented from: + Y Yao, F Miatto, N Quesada - arXiv preprint arXiv:2209.06069, 2022. + """ + for dU_euclidean, U in grads_and_vars: + Y = math.euclidean_to_unitary(U, dU_euclidean) + new_value = math.matmul(U, math.expm(-unitary_lr * Y)) + math.assign(U, new_value) + + def update_euclidean(grads_and_vars: Sequence[Tuple[Tensor, Trainable]], euclidean_lr: float): """Updates the parameters using the euclidian gradients.""" math.euclidean_opt.lr = euclidean_lr @@ -67,5 +72,6 @@ def update_euclidean(grads_and_vars: Sequence[Tuple[Tensor, Trainable]], euclide param_update_method = { "euclidean": update_euclidean, "symplectic": update_symplectic, + "unitary": update_unitary, "orthogonal": update_orthogonal, } diff --git a/mrmustard/training/parametrized.py b/mrmustard/training/parametrized.py index d8f55401e..1648f8034 100644 --- a/mrmustard/training/parametrized.py +++ b/mrmustard/training/parametrized.py @@ -78,7 +78,7 @@ def param_string(self, decimals: int) -> str: str: string representation of the parameter values """ strings = [] - for _, value in self.kw_parameters: + for name, value in self.kw_parameters: value = math.asnumpy(value) if value.ndim == 0: # don't show arrays sign = "-" if value < 0 else "" @@ -86,6 +86,8 @@ def param_string(self, decimals: int) -> str: int_part = int(value) decimal_part = np.round(value - int_part, decimals) string = sign + str(int_part) + f"{decimal_part:.{decimals}g}"[1:] + else: + string = f"{name}" strings.append(string) return ", ".join(strings) diff --git a/tests/random.py b/tests/random.py index 6570ae2d5..d7054881e 100644 --- a/tests/random.py +++ b/tests/random.py @@ -232,7 +232,7 @@ def random_MZgate(draw, trainable=False): def random_Interferometer(draw, num_modes, trainable=False): r"""Return a random Interferometer.""" settings.SEED = draw(integer32bits) - return Interferometer(num_modes=num_modes, orthogonal_trainable=trainable) + return Interferometer(num_modes=num_modes, unitary_trainable=trainable) @st.composite diff --git a/tests/test_lab/test_gates_fock.py b/tests/test_lab/test_gates_fock.py index efa9f2f79..e36742ae4 100644 --- a/tests/test_lab/test_gates_fock.py +++ b/tests/test_lab/test_gates_fock.py @@ -30,6 +30,9 @@ MZgate, Rgate, S2gate, + Attenuator, + RealInterferometer, + Vacuum, Sgate, ) from mrmustard.lab.states import TMSV, Fock, SqueezedVacuum, State @@ -212,3 +215,10 @@ def test_raise_interferometer_error(): modes = [0, 2] with pytest.raises(ValueError): Interferometer(num_modes=num_modes, modes=modes) + with pytest.raises(ValueError): + RealInterferometer(num_modes=num_modes, modes=modes) + modes = [2, 5, 6, 7] + with pytest.raises(ValueError): + Interferometer(num_modes=num_modes, modes=modes) + with pytest.raises(ValueError): + RealInterferometer(num_modes=num_modes, modes=modes) diff --git a/tests/test_training/test_opt.py b/tests/test_training/test_opt.py index f72bd1dcd..14e2029b6 100644 --- a/tests/test_training/test_opt.py +++ b/tests/test_training/test_opt.py @@ -149,7 +149,7 @@ def test_learning_two_mode_Interferometer(): r_trainable=True, phi_trainable=True, ), - Interferometer(num_modes=2, orthogonal_trainable=True), + Interferometer(num_modes=2, unitary_trainable=True), ] circ = Circuit(ops) state_in = Vacuum(num_modes=2) @@ -158,7 +158,7 @@ def cost_fn(): amps = (state_in >> circ).ket(cutoffs=[2, 2]) return -tf.abs(amps[1, 1]) ** 2 + tf.abs(amps[0, 1]) ** 2 - opt = Optimizer(orthogonal_lr=0.5, euclidean_lr=0.01) + opt = Optimizer(unitary_lr=0.5, euclidean_lr=0.01) opt.minimize(cost_fn, by_optimizing=[circ], max_steps=1000) assert np.allclose(-cost_fn(), 0.25, atol=1e-5) @@ -199,7 +199,7 @@ def test_learning_four_mode_Interferometer(): r_trainable=True, phi_trainable=True, ), - Interferometer(num_modes=4, orthogonal_trainable=True), + Interferometer(num_modes=4, unitary_trainable=True), ] circ = Circuit(ops) state_in = Vacuum(num_modes=4) @@ -216,7 +216,7 @@ def cost_fn(): ** 2 ) - opt = Optimizer(orthogonal_lr=0.05) + opt = Optimizer(unitary_lr=0.05) opt.minimize(cost_fn, by_optimizing=[circ], max_steps=1000) assert np.allclose(-cost_fn(), 0.0625, atol=1e-5) diff --git a/tests/test_training/test_parameter.py b/tests/test_training/test_parameter.py index e2296c86e..9734bcf9d 100644 --- a/tests/test_training/test_parameter.py +++ b/tests/test_training/test_parameter.py @@ -21,6 +21,7 @@ create_parameter, Constant, Orthogonal, + Unitary, Euclidean, Symplectic, Trainable, @@ -48,12 +49,12 @@ def test_create_constant(from_backend): assert param.name == name -@pytest.mark.parametrize("trainable_class", (Euclidean, Orthogonal, Symplectic)) +@pytest.mark.parametrize("trainable_class", (Euclidean, Orthogonal, Symplectic, Unitary)) @pytest.mark.parametrize("from_backend", [True, False]) @pytest.mark.parametrize("bounds", [None, (0, 10)]) def test_create_trainable(trainable_class, from_backend, bounds): """Checks if the factory function `create_parameter` - returns an instance of the Euclidean/Orthogonal/Symplectic class when args + returns an instance of the Euclidean/Orthogonal/Symplectic/Unitary class when args are trainable.""" value = 5 diff --git a/tests/test_training/test_parametrized.py b/tests/test_training/test_parametrized.py index f54c6bbee..171dc4685 100644 --- a/tests/test_training/test_parametrized.py +++ b/tests/test_training/test_parametrized.py @@ -20,7 +20,14 @@ from mrmustard.math import Math from mrmustard.lab.circuit import Circuit from mrmustard.lab.gates import BSgate, S2gate -from mrmustard.training.parameter import Constant, Orthogonal, Euclidean, Symplectic, Trainable +from mrmustard.training.parameter import ( + Constant, + Unitary, + Orthogonal, + Euclidean, + Symplectic, + Trainable, +) math = Math() @@ -39,7 +46,7 @@ def test_attribute_assignment(kwargs): assert instance_attributes[f"{name}"].name == name -@pytest.mark.parametrize("trainable_class", (Euclidean, Orthogonal, Symplectic)) +@pytest.mark.parametrize("trainable_class", (Euclidean, Orthogonal, Symplectic, Unitary)) @pytest.mark.parametrize("bounds", [None, (0, 10)]) def test_attribute_from_backend_type_assignment(trainable_class, bounds): """Test that arguments that are trainable get defined on the backend, @@ -95,11 +102,13 @@ def test_get_parameters(): "euclidian_attribute_trainable": True, "orthogonal_attribute": math.new_variable(4, None, "orthogonal_attribute"), "orthogonal_attribute_trainable": True, + "unitary_attribute": math.new_variable(4, None, "unitary_attribute"), + "unitary_attribute_trainable": True, } parametrized = Parametrized(**kwargs) trainable_params = parametrized.trainable_parameters - assert len(trainable_params) == 3 + assert len(trainable_params) == 4 assert all(isinstance(param, Trainable) for param in trainable_params) constant_params = parametrized.constant_parameters diff --git a/tests/test_training/test_riemannian_opt.py b/tests/test_training/test_riemannian_opt.py new file mode 100644 index 000000000..d002e1603 --- /dev/null +++ b/tests/test_training/test_riemannian_opt.py @@ -0,0 +1,101 @@ +# Copyright 2022 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. + +"""optimization tests""" + +from hypothesis import given, strategies as st + +import numpy as np +import tensorflow as tf + +from scipy.stats import unitary_group + +from thewalrus.symplectic import is_symplectic +from thewalrus.random import random_symplectic + +from mrmustard.training.parameter_update import update_symplectic, update_unitary, update_orthogonal + +from mrmustard.math import Math + +math = Math() + + +def is_unitary(M, rtol=1e-05, atol=1e-08): + """Testing if the matrix M is unitary""" + M_dagger = np.transpose(M.conj()) + return np.allclose(M @ M_dagger, np.identity(M.shape[-1]), rtol=rtol, atol=atol) + + +def is_orthogonal(M, rtol=1e-05, atol=1e-08): + """Testing if the matrix M is orthogonal""" + M_T = np.transpose(M) + return np.allclose(M @ M_T, np.identity(M.shape[-1]), rtol=rtol, atol=atol) + + +@given(n=st.integers(2, 4)) +def test_update_symplectic(n): + """Testing the update of symplectic matrix remains to be symplectic""" + S = math.new_variable(random_symplectic(n), name=None, dtype=tf.complex128, bounds=None) + for _ in range(20): + dS_euclidean = math.new_variable( + np.random.random((2 * n, 2 * n)) + 1j * np.random.random((2 * n, 2 * n)), + name=None, + dtype=tf.complex128, + bounds=None, + ) + update_symplectic([[dS_euclidean, S]], 0.01) + assert is_symplectic(S.numpy()), "training step does not result in a symplectic matrix" + + +@given(n=st.integers(2, 4)) +def test_update_unitary(n): + """Testing the update of unitary matrix remains to be unitary""" + U = math.new_variable(unitary_group.rvs(dim=n), name=None, dtype=tf.complex128, bounds=None) + for _ in range(20): + dU_euclidean = math.new_variable( + np.random.random((n, n)) + 1j * np.random.random((n, n)), + name=None, + dtype=tf.complex128, + bounds=None, + ) + update_unitary([[dU_euclidean, U]], 0.01) + assert is_unitary(U.numpy()), "training step does not result in a unitary matrix" + sym = np.block( + [[np.real(U.numpy()), -np.imag(U.numpy())], [np.imag(U.numpy()), np.real(U.numpy())]] + ) + assert is_symplectic(sym), "training step does not result in a symplectic matrix" + assert is_orthogonal(sym), "training step does not result in an orthogonal matrix" + + +@given(n=st.integers(2, 4)) +def test_update_orthogonal(n): + """Testing the update of orthogonal matrix remains to be orthogonal""" + O = math.new_variable(math.random_orthogonal(n), name=None, dtype=tf.complex128, bounds=None) + for _ in range(20): + dO_euclidean = math.new_variable( + np.random.random((n, n)) + 1j * np.random.random((n, n)), + name=None, + dtype=tf.complex128, + bounds=None, + ) + update_orthogonal([[dO_euclidean, O]], 0.01) + assert is_unitary(O.numpy()), "training step does not result in a unitary matrix" + ortho = np.block( + [ + [np.real(O.numpy()), -math.zeros_like(O.numpy())], + [math.zeros_like(O.numpy()), np.real(O.numpy())], + ] + ) + assert is_symplectic(ortho), "training step does not result in a symplectic matrix" + assert is_orthogonal(ortho), "training step does not result in an orthogonal matrix" From adda279eb016d0b2358dbdaa9afed01f74b280da Mon Sep 17 00:00:00 2001 From: ziofil Date: Wed, 1 Mar 2023 00:29:02 -0800 Subject: [PATCH 35/53] fix issue and add test (#210) **Context:** Computing a fidelity across representations (gaussian and fock) was causing an issue with taking a slice **Description of the Change:** Fixed **Benefits:** Works **Possible Drawbacks:** None **Related GitHub Issues:** None --- .github/CHANGELOG.md | 4 ++++ mrmustard/lab/gates.py | 8 ++++++-- mrmustard/physics/fock.py | 24 ++++++++++++++---------- mrmustard/training/parametrized.py | 2 +- tests/test_physics/test_fidelity.py | 28 +++++++++++++++++++++++++++- 5 files changed, 52 insertions(+), 14 deletions(-) diff --git a/.github/CHANGELOG.md b/.github/CHANGELOG.md index e0b988510..b884a3ebe 100644 --- a/.github/CHANGELOG.md +++ b/.github/CHANGELOG.md @@ -201,9 +201,13 @@ the other types, like `Batch[ComplexTensor]`. This will allow for better type ch * Fixed the issue that the optimization of the interferometer was using orthogonal group optimization rather than unitary. [(#208)](https://github.com/XanaduAI/MrMustard/pull/208) +* Fixes a slicing issue that arises when we compute the fidelity between gaussian and fock states. + [(#210)](https://github.com/XanaduAI/MrMustard/pull/210) + * The sign of parameters in the circuit drawer are now displayed correctly. [(#209)](https://github.com/XanaduAI/MrMustard/pull/209) + ### Documentation ### Contributors diff --git a/mrmustard/lab/gates.py b/mrmustard/lab/gates.py index 1e8d826c2..dd27baec2 100644 --- a/mrmustard/lab/gates.py +++ b/mrmustard/lab/gates.py @@ -595,13 +595,17 @@ def __init__( num_modes: int, symplectic: Optional[RealMatrix] = None, symplectic_trainable: bool = False, + modes: Optional[List[int]] = None, ): - symplectic = symplectic if symplectic is not None else math.random_symplectic(num_modes) + if modes is not None and (num_modes != len(modes)): + raise ValueError(f"Invalid number of modes: got {len(modes)}, should be {num_modes}") + if symplectic is None: + symplectic = math.random_symplectic(num_modes) super().__init__( symplectic=symplectic, symplectic_trainable=symplectic_trainable, ) - self._modes = list(range(num_modes)) + self._modes = modes or list(range(num_modes)) self.is_gaussian = True self.short_name = "G" diff --git a/mrmustard/physics/fock.py b/mrmustard/physics/fock.py index 0de86d23d..1cdd5b0ed 100644 --- a/mrmustard/physics/fock.py +++ b/mrmustard/physics/fock.py @@ -241,8 +241,8 @@ def fidelity(state_a, state_b, a_ket: bool, b_ket: bool) -> Scalar: r"""Computes the fidelity between two states in Fock representation.""" if a_ket and b_ket: min_cutoffs = [slice(min(a, b)) for a, b in zip(state_a.shape, state_b.shape)] - state_a = state_a[min_cutoffs] - state_b = state_b[min_cutoffs] + state_a = state_a[tuple(min_cutoffs)] + state_b = state_b[tuple(min_cutoffs)] return math.abs(math.sum(math.conj(state_a) * state_b)) ** 2 if a_ket: @@ -250,8 +250,8 @@ def fidelity(state_a, state_b, a_ket: bool, b_ket: bool) -> Scalar: slice(min(a, b)) for a, b in zip(state_a.shape, state_b.shape[: len(state_b.shape) // 2]) ] - state_a = state_a[min_cutoffs] - state_b = state_b[min_cutoffs * 2] + state_a = state_a[tuple(min_cutoffs)] + state_b = state_b[tuple(min_cutoffs * 2)] a = math.reshape(state_a, -1) return math.real( math.sum(math.conj(a) * math.matvec(math.reshape(state_b, (len(a), len(a))), a)) @@ -262,8 +262,8 @@ def fidelity(state_a, state_b, a_ket: bool, b_ket: bool) -> Scalar: slice(min(a, b)) for a, b in zip(state_a.shape[: len(state_a.shape) // 2], state_b.shape) ] - state_a = state_a[min_cutoffs * 2] - state_b = state_b[min_cutoffs] + state_a = state_a[tuple(min_cutoffs * 2)] + state_b = state_b[tuple(min_cutoffs)] b = math.reshape(state_b, -1) return math.real( math.sum(math.conj(b) * math.matvec(math.reshape(state_a, (len(b), len(b))), b)) @@ -271,11 +271,15 @@ def fidelity(state_a, state_b, a_ket: bool, b_ket: bool) -> Scalar: # mixed state # Richard Jozsa (1994) Fidelity for Mixed Quantum States, Journal of Modern Optics, 41:12, 2315-2323, DOI: 10.1080/09500349414552171 - return ( - math.trace( - math.sqrtm(math.matmul(math.matmul(math.sqrtm(state_a), state_b), math.sqrtm(state_a))) + return math.abs( + ( + math.trace( + math.sqrtm( + math.matmul(math.matmul(math.sqrtm(state_a), state_b), math.sqrtm(state_a)) + ) + ) + ** 2 ) - ** 2 ) diff --git a/mrmustard/training/parametrized.py b/mrmustard/training/parametrized.py index 1648f8034..176863243 100644 --- a/mrmustard/training/parametrized.py +++ b/mrmustard/training/parametrized.py @@ -85,7 +85,7 @@ def param_string(self, decimals: int) -> str: value = np.abs(np.round(value, decimals)) int_part = int(value) decimal_part = np.round(value - int_part, decimals) - string = sign + str(int_part) + f"{decimal_part:.{decimals}g}"[1:] + string = sign + str(int_part) + f"{decimal_part:.{decimals}g}".lstrip("0") else: string = f"{name}" strings.append(string) diff --git a/tests/test_physics/test_fidelity.py b/tests/test_physics/test_fidelity.py index 48308a861..39abadafa 100644 --- a/tests/test_physics/test_fidelity.py +++ b/tests/test_physics/test_fidelity.py @@ -3,7 +3,8 @@ from thewalrus.quantum import real_to_complex_displacements from thewalrus.random import random_covariance -from mrmustard import * +from mrmustard.lab import Coherent, Fock, State +from mrmustard import physics from mrmustard.math import Math from mrmustard.physics import fock as fp from mrmustard.physics import gaussian as gp @@ -127,3 +128,28 @@ def test_fidelity_formula(self): """Test fidelity of known mixed states.""" expected = 5 / 6 assert np.allclose(expected, fp.fidelity(self.state1, self.state2, False, False)) + + +class TestGaussianFock: + """Tests for the fidelity between a pair of single-mode states in Gaussian and Fock representation""" + + state1ket = Coherent(x=1.0) + state1dm = State(dm=state1ket.dm()) + state2ket = Fock(n=1) + state2dm = State(dm=state2ket.dm(state1dm.cutoffs)) + + def test_fidelity_across_representations_ket_ket(self): + """Test that the fidelity of these two states is what it should be""" + assert np.allclose(physics.fidelity(self.state1ket, self.state2ket), 0.36787944, atol=1e-4) + + def test_fidelity_across_representations_ket_dm(self): + """Test that the fidelity of these two states is what it should be""" + assert np.allclose(physics.fidelity(self.state1ket, self.state2dm), 0.36787944, atol=1e-4) + + def test_fidelity_across_representations_dm_ket(self): + """Test that the fidelity of these two states is what it should be""" + assert np.allclose(physics.fidelity(self.state1dm, self.state2ket), 0.36787944, atol=1e-4) + + def test_fidelity_across_representations_dm_dm(self): + """Test that the fidelity of these two states is what it should be""" + assert np.allclose(physics.fidelity(self.state1dm, self.state2dm), 0.36787944, atol=1e-4) From ad74f57f108f02f81476efc6cb4c64a6c692771c Mon Sep 17 00:00:00 2001 From: ziofil Date: Wed, 1 Mar 2023 10:06:25 -0800 Subject: [PATCH 36/53] dm-dm fidelity cutoffs (#212) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit **Context:** an error occurs if two mixed fock states with different cutoff are the args of `fidelity` **Description of the Change:** enforce same cutoff **Benefits:** works **Possible Drawbacks:** none **Related GitHub Issues:** none --------- Co-authored-by: Sebastián Duque Mesa <675763+sduquemesa@users.noreply.github.com> --- .github/CHANGELOG.md | 3 +++ mrmustard/lab/states.py | 14 ++++++++------ mrmustard/physics/fock.py | 10 ++++++++++ mrmustard/physics/gaussian.py | 23 ++++++++++------------- tests/test_lab/test_states.py | 9 +++++++++ tests/test_training/test_trainer.py | 10 +++++----- 6 files changed, 45 insertions(+), 24 deletions(-) diff --git a/.github/CHANGELOG.md b/.github/CHANGELOG.md index b884a3ebe..16f2fe25e 100644 --- a/.github/CHANGELOG.md +++ b/.github/CHANGELOG.md @@ -207,6 +207,9 @@ the other types, like `Batch[ComplexTensor]`. This will allow for better type ch * The sign of parameters in the circuit drawer are now displayed correctly. [(#209)](https://github.com/XanaduAI/MrMustard/pull/209) +* Fixed a bug in the Gaussian state which caused it to be multiplied by hbar/2 twice. Adds the argument `modes` to `Ggate`. + [(#212)](https://github.com/XanaduAI/MrMustard/pull/212) + ### Documentation diff --git a/mrmustard/lab/states.py b/mrmustard/lab/states.py index f8cb3a580..7d08ff140 100644 --- a/mrmustard/lab/states.py +++ b/mrmustard/lab/states.py @@ -16,12 +16,14 @@ This module implements the quantum states upon which a quantum circuits acts on. """ -from typing import Union, Optional, List, Tuple, Sequence -from mrmustard.typing import Scalar, Vector, RealMatrix +from typing import List, Optional, Sequence, Tuple, Union + from mrmustard import settings -from mrmustard.physics import gaussian, fock -from mrmustard.training import Parametrized from mrmustard.math import Math +from mrmustard.physics import fock, gaussian +from mrmustard.training import Parametrized +from mrmustard.typing import RealMatrix, Scalar, Vector + from .abstract import State math = Math() @@ -444,13 +446,13 @@ def __init__( self._modes = modes self._normalize = normalize - cov = gaussian.gaussian_cov(self.symplectic.value, self.eigenvalues.value, settings.HBAR) + cov = gaussian.gaussian_cov(self.symplectic.value, self.eigenvalues.value) means = gaussian.vacuum_means(cov.shape[-1] // 2, settings.HBAR) State.__init__(self, cov=cov, means=means, cutoffs=cutoffs) @property def cov(self): - return gaussian.gaussian_cov(self.symplectic.value, self.eigenvalues.value, settings.HBAR) + return gaussian.gaussian_cov(self.symplectic.value, self.eigenvalues.value) @property def is_mixed(self): diff --git a/mrmustard/physics/fock.py b/mrmustard/physics/fock.py index 1cdd5b0ed..aa964321e 100644 --- a/mrmustard/physics/fock.py +++ b/mrmustard/physics/fock.py @@ -271,6 +271,16 @@ def fidelity(state_a, state_b, a_ket: bool, b_ket: bool) -> Scalar: # mixed state # Richard Jozsa (1994) Fidelity for Mixed Quantum States, Journal of Modern Optics, 41:12, 2315-2323, DOI: 10.1080/09500349414552171 + + # trim states to have same cutoff + min_cutoffs = [ + slice(min(a, b)) + for a, b in zip( + state_a.shape[: len(state_a.shape) // 2], state_b.shape[: len(state_b.shape) // 2] + ) + ] + state_a = state_a[tuple(min_cutoffs * 2)] + state_b = state_b[tuple(min_cutoffs * 2)] return math.abs( ( math.trace( diff --git a/mrmustard/physics/gaussian.py b/mrmustard/physics/gaussian.py index 9e31943cc..f8dcd8069 100644 --- a/mrmustard/physics/gaussian.py +++ b/mrmustard/physics/gaussian.py @@ -16,12 +16,14 @@ This module contains functions for performing calculations on Gaussian states. """ -from typing import Tuple, Union, Sequence, Any, Optional +from typing import Any, Optional, Sequence, Tuple, Union + from thewalrus.quantum import is_pure_cov -from mrmustard.typing import Matrix, Vector, Scalar -from mrmustard.utils.xptensor import XPMatrix, XPVector + from mrmustard import settings from mrmustard.math import Math +from mrmustard.typing import Matrix, Scalar, Vector +from mrmustard.utils.xptensor import XPMatrix, XPVector math = Math() @@ -112,27 +114,22 @@ def two_mode_squeezed_vacuum_cov(r: Vector, phi: Vector, hbar: float) -> Matrix: return math.matmul(S, math.transpose(S)) * hbar / 2 -def gaussian_cov(symplectic: Matrix, eigenvalues: Vector = None, hbar: float = 2.0) -> Matrix: +def gaussian_cov(symplectic: Matrix, eigenvalues: Vector = None) -> Matrix: r"""Returns the covariance matrix of a Gaussian state. Args: symplectic (Tensor): symplectic matrix of a channel eigenvalues (vector): symplectic eigenvalues - hbar (float): value of hbar Returns: Tensor: covariance matrix of the Gaussian state """ if eigenvalues is None: - return hbar / 2 * math.matmul(symplectic, math.transpose(symplectic)) + return math.matmul(symplectic, math.transpose(symplectic)) - return ( - hbar - / 2 - * math.matmul( - math.matmul(symplectic, math.diag(math.concat([eigenvalues, eigenvalues], axis=0))), - math.transpose(symplectic), - ) + return math.matmul( + math.matmul(symplectic, math.diag(math.concat([eigenvalues, eigenvalues], axis=0))), + math.transpose(symplectic), ) diff --git a/tests/test_lab/test_states.py b/tests/test_lab/test_states.py index 721620fdd..990699b52 100644 --- a/tests/test_lab/test_states.py +++ b/tests/test_lab/test_states.py @@ -145,6 +145,15 @@ def test_get_modes(): assert b == (a & b).get_modes([2, 3]) +def test_hbar(): + """Test cov matrix is linear in hbar.""" + g = Gaussian(2) + p = g.purity + settings.HBAR = 1.234 + assert g.purity == p + settings.HBAR = 2 + + def test_get_single_mode(): """Test get_modes leaves a single-mode state untouched.""" a = Gaussian(1)[1] diff --git a/tests/test_training/test_trainer.py b/tests/test_training/test_trainer.py index 0a664d0ba..7e620ca26 100644 --- a/tests/test_training/test_trainer.py +++ b/tests/test_training/test_trainer.py @@ -15,13 +15,13 @@ """Tests for the ray-based trainer.""" import sys - from time import sleep -import pytest import numpy as np +import pytest import ray -from mrmustard.lab import Vacuum, Dgate, Ggate, Gaussian + +from mrmustard.lab import Dgate, Gaussian, Ggate, Vacuum from mrmustard.physics import fidelity from mrmustard.training import Optimizer from mrmustard.training.trainer import map_trainer, train_device, update_pop @@ -133,8 +133,8 @@ def test_circ_optimize_metrics(wrappers, metric_fns): # pylint: disable=redefin make_circ, cost_fn = wrappers tasks = { - "my-job": {"x": 0.1, "euclidean_lr": 0.005, "max_steps": 20}, - "my-other-job": {"x": -0.7, "euclidean_lr": 0.1, "max_steps": 12}, + "my-job": {"x": 0.1, "euclidean_lr": 0.01, "max_steps": 100}, + "my-other-job": {"x": -0.7, "euclidean_lr": 0.1, "max_steps": 20}, } results = map_trainer( From ff1c94748dcf017c9b0e36e1b9eb00dfa08c8bc9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Duque=20Mesa?= <675763+sduquemesa@users.noreply.github.com> Date: Fri, 3 Mar 2023 13:21:41 -0500 Subject: [PATCH 37/53] Displacement gate on MrMustard (#211) **Context:** Mr Mustard uses The Walrus' implementation of the displacement operation. This creates an unnecessary coupling between the two packages. **Description of the Change:** Implements the displacement gate on MrMustard directly. To do so and to avoid circular imports - the implementation of the displacement gate has been moved from the `TFMath` class/backend to the `fock` module and - the `custom_gradient` method wrapping tensorflow's custom gradient decorator has been implemented on `TFMath` **Benefits:** No need to release both packages (TheWalrus and MrMustard) at the same time for the `Dgate` to be available. **Possible Drawbacks:** None **Related GitHub Issues:** None --------- Co-authored-by: ziofil --- .github/CHANGELOG.md | 18 ++-- mrmustard/lab/gates.py | 6 +- mrmustard/math/math_interface.py | 13 --- mrmustard/math/tensorflow.py | 26 ++---- mrmustard/physics/fock.py | 102 ++++++++++++++++++++++ tests/test_physics/test_fock/test_fock.py | 71 +++++++++++++++ 6 files changed, 192 insertions(+), 44 deletions(-) diff --git a/.github/CHANGELOG.md b/.github/CHANGELOG.md index 16f2fe25e..faa6fcb50 100644 --- a/.github/CHANGELOG.md +++ b/.github/CHANGELOG.md @@ -13,12 +13,12 @@ def make_circ(x=0.): return Ggate(num_modes=1, symplectic_trainable=True) >> Dgate(x=x, x_trainable=True, y_trainable=True) - + def cost_fn(circ=make_circ(0.1), y_targ=0.): target = Gaussian(1) >> Dgate(-1.5, y_targ) s = Vacuum(1) >> circ return -fidelity(s, target) - + # Use case 0: Calculate the cost of a randomly initialized circuit 5 times without optimizing it. results_0 = map_trainer( cost_fn=cost_fn, @@ -109,14 +109,16 @@ The complexity of these new methods is equal to performing a pure state simulation. The methods are differentiable, such that they can be used for defining a costfunction. [(#154)](https://github.com/XanaduAI/MrMustard/pull/154) - + ### Breaking changes ### Improvements -* The `Dgate` now uses The Walrus to calculate the unitary and gradients of the displacement gate in fock representation, - providing better numerical stability for larger cutoff and displacement values. - [(#147)](https://github.com/XanaduAI/MrMustard/pull/147) +* The `Dgate` is now implemented directly in MrMustard (instead of on The Walrus) to calculate the + unitary and gradients of the displacement gate in fock representation, providing better numerical + stability for larger cutoff and displacement values. + [(#147)](https://github.com/XanaduAI/MrMustard/pull/147) + [(#211)](https://github.com/XanaduAI/MrMustard/pull/211) * Now the Wigner function is implemented in its own module and uses numba for speed. [(#171)](https://github.com/XanaduAI/MrMustard/pull/171) @@ -166,7 +168,7 @@ the other types, like `Batch[ComplexTensor]`. This will allow for better type ch * The `fock.autocutoff` function now uses the new diagonal methods for calculating a probability-based cutoff. Use `settings.AUTOCUTOFF_PROBABILITY` to set the probability threshold. [(#203)](https://github.com/XanaduAI/MrMustard/pull/203) - + * The unitary group optimization (for the interferometer) and the orthogonal group optimization (for the real interferometer) have been added. The symplectic matrix that describes an interferometer belongs to the intersection of the orthogonal group and the symplectic group, which is a unitary group, so we needed both. @@ -213,7 +215,7 @@ the other types, like `Batch[ComplexTensor]`. This will allow for better type ch ### Documentation -### Contributors +### Contributors This release contains contributions from (in alphabetical order): [Robbe De Prins](https://github.com/rdprins), [Sebastian Duque Mesa](https://github.com/sduquemesa), [Filippo Miatto](https://github.com/ziofil), [Yuan Yao](https://github.com/sylviemonet) diff --git a/mrmustard/lab/gates.py b/mrmustard/lab/gates.py index dd27baec2..586ce3fc8 100644 --- a/mrmustard/lab/gates.py +++ b/mrmustard/lab/gates.py @@ -23,7 +23,7 @@ from mrmustard import settings from mrmustard.lab.abstract import Transformation from mrmustard.math import Math -from mrmustard.physics import gaussian +from mrmustard.physics import gaussian, fock from mrmustard.training import Parametrized math = Math() @@ -107,9 +107,9 @@ def U(self, cutoffs: Sequence[int]): Ud = None for idx, cutoff in enumerate(cutoffs): if Ud is None: - Ud = math.displacement(r[idx], phi[idx], cutoff) + Ud = fock.displacement(r[idx], phi[idx], cutoff) else: - U_next = math.displacement(r[idx], phi[idx], cutoff) + U_next = fock.displacement(r[idx], phi[idx], cutoff) Ud = math.outer(Ud, U_next) return math.transpose( diff --git a/mrmustard/math/math_interface.py b/mrmustard/math/math_interface.py index f20be590b..fb074c009 100644 --- a/mrmustard/math/math_interface.py +++ b/mrmustard/math/math_interface.py @@ -420,19 +420,6 @@ def hermite_renormalized(self, A: Matrix, B: Vector, C: Scalar, shape: Sequence[ array: renormalized hermite polynomials """ - @abstractmethod - def displacement(self, r: Scalar, phi: Scalar, cutoff: Scalar, tol): - r"""Calculates the matrix elements of the displacement gate and its derivatives. - - Args: - r (float): displacement magnitude - phi (float): displacement angle - cutoff (int): Fock ladder cutoff - tol (float): r tolerance for returning identity instead of displacement - Returns: - Tuple(array[complex], function): matrix representing the displacement operation and its gradient - """ - @abstractmethod def imag(self, array: Tensor) -> Tensor: r"""Returns the imaginary part of array. diff --git a/mrmustard/math/tensorflow.py b/mrmustard/math/tensorflow.py index bb947d136..99caf41cd 100644 --- a/mrmustard/math/tensorflow.py +++ b/mrmustard/math/tensorflow.py @@ -21,10 +21,7 @@ import tensorflow_probability as tfp from thewalrus import hermite_multidimensional, grad_hermite_multidimensional -from thewalrus.fock_gradients import ( - displacement as displacement_tw, - grad_displacement as grad_displacement_tw, -) + from mrmustard.math.numba.compactFock_inputValidation import ( hermite_multidimensional_diagonal, grad_hermite_multidimensional_diagonal, @@ -507,22 +504,6 @@ def grad(dLdpoly): return poly0, grad - @tf.custom_gradient - def displacement(self, r, phi, cutoff, tol=1e-15): - """creates a single mode displacement matrix""" - if r > tol: - gate = displacement_tw(self.asnumpy(r), self.asnumpy(phi), cutoff) - else: - gate = self.eye(cutoff, dtype="complex128") - - def grad(dy): # pragma: no cover - Dr, Dphi = tf.numpy_function(grad_displacement_tw, (gate, r, phi), (gate.dtype,) * 2) - grad_r = tf.math.real(tf.reduce_sum(dy * tf.math.conj(Dr))) - grad_phi = tf.math.real(tf.reduce_sum(dy * tf.math.conj(Dphi))) - return grad_r, grad_phi, None - - return gate, grad - @staticmethod def eigvals(tensor: tf.Tensor) -> Tensor: """Returns the eigenvalues of a matrix.""" @@ -561,6 +542,11 @@ def boolean_mask(tensor: tf.Tensor, mask: tf.Tensor) -> Tensor: """Returns a tensor based on the truth value of the boolean mask.""" return tf.boolean_mask(tensor, mask) + @staticmethod + def custom_gradient(func): + """Decorator to define a function with a custom gradient.""" + return tf.custom_gradient(func) + # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ # Extras (not in the Interface) # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/mrmustard/physics/fock.py b/mrmustard/physics/fock.py index aa964321e..b54964967 100644 --- a/mrmustard/physics/fock.py +++ b/mrmustard/physics/fock.py @@ -22,6 +22,7 @@ from typing import List, Sequence, Tuple, Optional import numpy as np +from numba import jit from mrmustard import settings from mrmustard.math import Math @@ -38,6 +39,7 @@ from mrmustard.typing import Matrix, Scalar, Tensor, Vector math = Math() +SQRT = np.sqrt(np.arange(1e6)) # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ # ~~~~~~~~~~~~~~ static functions ~~~~~~~~~~~~~~ @@ -835,3 +837,103 @@ def sample_homodyne( probability_sample = math.gather(probs, sample_idx) return homodyne_sample, probability_sample + + +@jit(nopython=True) +def _displacement(r, phi, cutoff, dtype=np.complex128): # pragma: no cover + r"""Calculates the matrix elements of the displacement gate using a recurrence relation. + Uses the log of the matrix elements to avoid numerical issues and then takes the exponential. + + Args: + r (float): displacement magnitude + phi (float): displacement angle + cutoff (int): Fock ladder cutoff + dtype (data type): Specifies the data type used for the calculation + + Returns: + array[complex]: matrix representing the displacement operation. + """ + D = np.zeros((cutoff, cutoff), dtype=dtype) + rng = np.arange(cutoff) + rng[0] = 1 + log_k_fac = np.cumsum(np.log(rng)) + for n_minus_m in range(cutoff): + m_max = cutoff - n_minus_m + logL = np.log(_laguerre(r**2.0, m_max, n_minus_m)) + for m in range(m_max): + n = n_minus_m + m + D[n, m] = np.exp( + 0.5 * (log_k_fac[m] - log_k_fac[n]) + + n_minus_m * np.log(r) + - (r**2.0) / 2.0 + + 1j * phi * n_minus_m + + logL[m] + ) + D[m, n] = (-1.0) ** (n_minus_m) * np.conj(D[n, m]) + return D + + +@jit(nopython=True, cache=True) +def _laguerre(x, N, alpha, dtype=np.complex128): # pragma: no cover + r"""Returns the N first generalized Laguerre polynomials evaluated at x. + + Args: + x (float): point at which to evaluate the polynomials + N (int): maximum Laguerre polynomial to calculate + alpha (float): continuous parameter for the generalized Laguerre polynomials + """ + L = np.zeros(N, dtype=dtype) + L[0] = 1.0 + if N > 1: + for m in range(0, N - 1): + L[m + 1] = ((2 * m + 1 + alpha - x) * L[m] - (m + alpha) * L[m - 1]) / (m + 1) + return L + + +@jit(nopython=True) +def _grad_displacement(T, r, phi): # pragma: no cover + r"""Calculates the gradients of the displacement gate with respect to the displacement magnitude and angle. + + Args: + T (array[complex]): array representing the gate + r (float): displacement magnitude + phi (float): displacement angle + + Returns: + tuple[array[complex], array[complex]]: The gradient of the displacement gate with respect to r and phi + """ + cutoff = T.shape[0] + dtype = T.dtype + ei = np.exp(1j * phi) + eic = np.exp(-1j * phi) + alpha = r * ei + alphac = r * eic + sqrt = np.sqrt(np.arange(cutoff, dtype=dtype)) + grad_r = np.zeros((cutoff, cutoff), dtype=dtype) + grad_phi = np.zeros((cutoff, cutoff), dtype=dtype) + + for m in range(cutoff): + for n in range(cutoff): + grad_r[m, n] = -r * T[m, n] + sqrt[m] * ei * T[m - 1, n] - sqrt[n] * eic * T[m, n - 1] + grad_phi[m, n] = ( + sqrt[m] * 1j * alpha * T[m - 1, n] + sqrt[n] * 1j * alphac * T[m, n - 1] + ) + + return grad_r, grad_phi + + +@math.custom_gradient +def displacement(r, phi, cutoff, tol=1e-15): + """creates a single mode displacement matrix""" + if r > tol: + gate = _displacement(math.asnumpy(r), math.asnumpy(phi), cutoff) + else: + gate = math.eye(cutoff, dtype="complex128") + + def grad(dy): # pragma: no cover + Dr, Dphi = math.numpy_function(_grad_displacement, (gate, r, phi), (gate.dtype,) * 2) + grad_r = math.real(math.reduce_sum(dy * math.conj(Dr))) + grad_phi = math.real(math.reduce_sum(dy * math.conj(Dphi))) + return grad_r, grad_phi, None + + return gate, grad diff --git a/tests/test_physics/test_fock/test_fock.py b/tests/test_physics/test_fock/test_fock.py index a0a3596d2..70e465653 100644 --- a/tests/test_physics/test_fock/test_fock.py +++ b/tests/test_physics/test_fock/test_fock.py @@ -44,6 +44,8 @@ apply_choi_to_ket, apply_kraus_to_dm, apply_kraus_to_ket, + _grad_displacement, + _displacement, ) @@ -333,3 +335,72 @@ def test_apply_choi_to_dm_2mode(): choi = np.random.normal(size=(2, 3, 5, 2, 3, 5)) # [out_l, in_l, out_r, in_r] dm_out = apply_choi_to_dm(choi, dm, [1], [1, 2]) assert dm_out.shape == (4, 2, 3, 4, 2, 3) + + +class TestDisplacement: + def test_grad_displacement(self): + """Tests the value of the analytic gradient for the Dgate against finite differences""" + cutoff = 4 + r = 1.0 + theta = np.pi / 8 + T = _displacement(r, theta, cutoff) + Dr, Dtheta = _grad_displacement(T, r, theta) + + dr = 0.001 + dtheta = 0.001 + Drp = _displacement(r + dr, theta, cutoff) + Drm = _displacement(r - dr, theta, cutoff) + Dthetap = _displacement(r, theta + dtheta, cutoff) + Dthetam = _displacement(r, theta - dtheta, cutoff) + Drapprox = (Drp - Drm) / (2 * dr) + Dthetaapprox = (Dthetap - Dthetam) / (2 * dtheta) + assert np.allclose(Dr, Drapprox, atol=1e-5, rtol=0) + assert np.allclose(Dtheta, Dthetaapprox, atol=1e-5, rtol=0) + + def test_displacement_values(self): + """Tests the correct construction of the single mode displacement operation""" + cutoff = 5 + alpha = 0.3 + 0.5 * 1j + # This data is obtained by using qutip + # np.array(displace(40,alpha).data.todense())[0:5,0:5] + expected = np.array( + [ + [ + 0.84366482 + 0.00000000e00j, + -0.25309944 + 4.21832408e-01j, + -0.09544978 - 1.78968334e-01j, + 0.06819609 + 3.44424719e-03j, + -0.01109048 + 1.65323865e-02j, + ], + [ + 0.25309944 + 4.21832408e-01j, + 0.55681878 + 0.00000000e00j, + -0.29708743 + 4.95145724e-01j, + -0.14658716 - 2.74850926e-01j, + 0.12479885 + 6.30297236e-03j, + ], + [ + -0.09544978 + 1.78968334e-01j, + 0.29708743 + 4.95145724e-01j, + 0.31873657 + 0.00000000e00j, + -0.29777767 + 4.96296112e-01j, + -0.18306015 - 3.43237787e-01j, + ], + [ + -0.06819609 + 3.44424719e-03j, + -0.14658716 + 2.74850926e-01j, + 0.29777767 + 4.96296112e-01j, + 0.12389162 + 1.10385981e-17j, + -0.27646677 + 4.60777945e-01j, + ], + [ + -0.01109048 - 1.65323865e-02j, + -0.12479885 + 6.30297236e-03j, + -0.18306015 + 3.43237787e-01j, + 0.27646677 + 4.60777945e-01j, + -0.03277289 + 1.88440656e-17j, + ], + ] + ) + T = _displacement(np.abs(alpha), np.angle(alpha), cutoff) + assert np.allclose(T, expected, atol=1e-5, rtol=0) From f24e3a4f18f49a9a888b4c25b5ecafd047414d60 Mon Sep 17 00:00:00 2001 From: ziofil Date: Fri, 3 Mar 2023 14:15:28 -0800 Subject: [PATCH 38/53] Bump version and get changelog ready for release (#213) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit **Context:** v0.4 release **Description of the Change:** shortened long lines and fixed some typos **Benefits:** good changelog **Possible Drawbacks:** None **Related GitHub Issues:** None --------- Co-authored-by: Sebastián Duque Mesa <675763+sduquemesa@users.noreply.github.com> --- .github/CHANGELOG.md | 140 ++++++++++++++++++++++++------------------ mrmustard/_version.py | 2 +- 2 files changed, 81 insertions(+), 61 deletions(-) diff --git a/.github/CHANGELOG.md b/.github/CHANGELOG.md index faa6fcb50..d476b3e1f 100644 --- a/.github/CHANGELOG.md +++ b/.github/CHANGELOG.md @@ -1,9 +1,10 @@ -# Release 0.4.0 (development release) +# Release 0.4.0 ### New features -* Ray-based distributed trainer is now added to `training.trainer`. It acts as a replacement for `for` loops and enables - the parallelization of running many circuits as well as their optimizations. To install the extra dependencies: `pip install .[ray]`. +* Ray-based distributed trainer is now added to `training.trainer`. It acts as a replacement + for `for` loops and enables the parallelization of running many circuits as well as their + optimizations. To install the extra dependencies: `pip install .[ray]`. [(#194)](https://github.com/XanaduAI/MrMustard/pull/194) ```python @@ -48,9 +49,9 @@ ) ``` -* Sampling for homodyne measurements is now integrated in Mr Mustard: when no measurement outcome value is - specified by the user, a value is sampled from the reduced state probability distribution and the - conditional state on the remaining modes is generated. +* Sampling for homodyne measurements is now integrated in Mr Mustard: when no measurement outcome + value is specified by the user, a value is sampled from the reduced state probability distribution + and the conditional state on the remaining modes is generated. [(#143)](https://github.com/XanaduAI/MrMustard/pull/143) ```python @@ -64,17 +65,20 @@ measurement_outcome = SqueezedVacuum(r=0.5) >> Homodyne() ``` -* The optimizer `minimize` method now accepts an optional callback function, which will be called at each step - of the optimization and it will be passed the step number, the cost value, and the value of the trainable parameters. - The result is added to the `callback_history` attribute of the optimizer. +* The optimizer `minimize` method now accepts an optional callback function, which will be called + at each step of the optimization and it will be passed the step number, the cost value, + and the value of the trainable parameters. The result is added to the `callback_history` + attribute of the optimizer. [(#175)](https://github.com/XanaduAI/MrMustard/pull/175) * the Math interface now supports linear system solving via `math.solve`. [(#185)](https://github.com/XanaduAI/MrMustard/pull/185) -* We introduce the tensor wrapper `MMTensor` (available in `math.mmtensor`) that allows for a very easy handling of tensor contractions. - Internally MrMustard performs lots of tensor contractions and this wrapper allows one to label each index of a tensor and perform - contractions using the `@` symbol as if it were a simple matrix multiplication (the indices with the same name get contracted). +* We introduce the tensor wrapper `MMTensor` (available in `math.mmtensor`) that allows for + a very easy handling of tensor contractions. Internally MrMustard performs lots of tensor + contractions and this wrapper allows one to label each index of a tensor and perform + contractions using the `@` symbol as if it were a simple matrix multiplication (the indices + with the same name get contracted). [(#185)](https://github.com/XanaduAI/MrMustard/pull/185)
[(#195)](https://github.com/XanaduAI/MrMustard/pull/195) @@ -92,22 +96,24 @@ C.tensor # extract actual result ``` -* MrMustard's settings object (accessible via `from mrmustard import settings`) now supports `SEED` (an int). - This will give reproducible results whenever randomness is involved. The seed is unset by default, - and it can be unset again with `settings.SEED = None`. If one desires, - the seeded random number generator is accessible directly via `settings.rng` (e.g. `settings.rng.normal()`). +* MrMustard's settings object (accessible via `from mrmustard import settings`) now supports + `SEED` (an int). This will give reproducible results whenever randomness is involved. + The seed is assigned randomly by default, and it can be reassigned again by setting it to None: + `settings.SEED = None`. If one desires, the seeded random number generator is accessible directly + via `settings.rng` (e.g. `settings.rng.normal()`). [(#183)](https://github.com/XanaduAI/MrMustard/pull/183) * The `Circuit` class now has an ascii representation, which can be accessed via the repr method. - It looks great in Jupyter notebooks! There is a new option at `settings.CIRCUIT_DECIMALS` which controls - the number of decimals shown in the ascii representation. If None only the name of the gate is shown. + It looks great in Jupyter notebooks! There is a new option at `settings.CIRCUIT_DECIMALS` + which controls the number of decimals shown in the ascii representation of the gate parameters. + If `None`, only the name of the gate is shown. [(#196)](https://github.com/XanaduAI/MrMustard/pull/196) -* PNR sampling from Gaussian circuits using density matrices can now be performed faster. When all modes are detected, - this is done by replacing `math.hermite_renormalized` by `math.hermite_renormalized_diagonal`. - In case all but the first mode are detected, `math.hermite_renormalized_1leftoverMode` can be used. +* PNR sampling from Gaussian circuits using density matrices can now be performed faster. + When all modes are detected, this is done by replacing `math.hermite_renormalized` by `math.hermite_renormalized_diagonal`. If all but the first mode are detected, + `math.hermite_renormalized_1leftoverMode` can be used. The complexity of these new methods is equal to performing a pure state simulation. - The methods are differentiable, such that they can be used for defining a costfunction. + The methods are differentiable, so that they can be used for defining a cost function. [(#154)](https://github.com/XanaduAI/MrMustard/pull/154) ### Breaking changes @@ -133,12 +139,12 @@ within the defined window. Also, expose some plot parameters and return the figure and axes. [(#179)](https://github.com/XanaduAI/MrMustard/pull/179) -* Allows for full cutoff specification (index-wise rather than mode-wise) for subclasses of `Transformation`. - This allows for a more compact Fock representation where needed. +* Allows for full cutoff specification (index-wise rather than mode-wise) for subclasses + of `Transformation`. This allows for a more compact Fock representation where needed. [(#181)](https://github.com/XanaduAI/MrMustard/pull/181) -* The `mrmustard.physics.fock` module now provides convenience functions for applying kraus operators and - choi operators to kets and density matrices. +* The `mrmustard.physics.fock` module now provides convenience functions for applying kraus + operators and choi operators to kets and density matrices. [(#180)](https://github.com/XanaduAI/MrMustard/pull/180) ```python @@ -149,58 +155,62 @@ dm_out = apply_choi_to_ket(choi, ket_in, indices) ``` -* Replaced norm with probability in the repr of `State`. This improves consistency over the old behaviour - (norm was the sqrt of prob if the state was pure and prob if the state was mixed). +* Replaced norm with probability in the repr of `State`. This improves consistency over the + old behaviour (norm was the sqrt of prob if the state was pure and prob if the state was mixed). [(#182)](https://github.com/XanaduAI/MrMustard/pull/182) -* Added two new modules (`physics.bargmann` and `physics.husimi`) to host the functions related to those representation, - which have been refactored and moved out of `physics.fock`. +* Added two new modules (`physics.bargmann` and `physics.husimi`) to host the functions related + to those representations, which have been refactored and moved out of `physics.fock`. [(#185)](https://github.com/XanaduAI/MrMustard/pull/185) - -* The internal type system in MrMustard has been beefed up with much clearer types, like ComplexVector, RealMatrix, etc... as well as a generic type `Batch`, which can be parametrized using -the other types, like `Batch[ComplexTensor]`. This will allow for better type checking and better error messages. +* The internal type system in MrMustard has been beefed up with much clearer types, like ComplexVector, + RealMatrix, etc... as well as a generic type `Batch`, which can be parametrized using the other types, + like `Batch[ComplexTensor]`. This will allow for better type checking and better error messages. [(#199)](https://github.com/XanaduAI/MrMustard/pull/199) * Added multiple tests and improved the use of Hypothesis. [(#191)](https://github.com/XanaduAI/MrMustard/pull/191) -* The `fock.autocutoff` function now uses the new diagonal methods for calculating a probability-based cutoff. - Use `settings.AUTOCUTOFF_PROBABILITY` to set the probability threshold. +* The `fock.autocutoff` function now uses the new diagonal methods for calculating a + probability-based cutoff. Use `settings.AUTOCUTOFF_PROBABILITY` to set the probability threshold. [(#203)](https://github.com/XanaduAI/MrMustard/pull/203) -* The unitary group optimization (for the interferometer) and the orthogonal group optimization (for the real interferometer) have been added. - The symplectic matrix that describes an interferometer belongs to the intersection of the orthogonal group and the symplectic group, which is a unitary group, - so we needed both. +* The unitary group optimization (for the interferometer) and the orthogonal group optimization + (for the real interferometer) have been added. The symplectic matrix that describes an + interferometer belongs to the intersection of the orthogonal group and the symplectic group, + which is a unitary group, so we needed both. [(#208)](https://github.com/XanaduAI/MrMustard/pull/208) ### Bug fixes -* The `Dgate` and the `Rgate` now correctly parse the case when a single scalar is intended as the same parameter - of a number of gates in pallel. +* The `Dgate` and the `Rgate` now correctly parse the case when a single scalar is intended + as the same parameter of a number of gates in parallel. [(#180)](https://github.com/XanaduAI/MrMustard/pull/180) -* The trace function in the fock module was giving incorrect results when called with certain choices of modes. - This is now fixed. +* The trace function in the fock module was giving incorrect results when called with certain + choices of modes. This is now fixed. [(#180)](https://github.com/XanaduAI/MrMustard/pull/180) -* The purity function for fock states no longer normalizes the density matrix before computing the purity. +* The purity function for fock states no longer normalizes the density matrix before computing + the purity. [(#180)](https://github.com/XanaduAI/MrMustard/pull/180) * The function `dm_to_ket` no longer normalizes the density matrix before diagonalizing it. [(#180)](https://github.com/XanaduAI/MrMustard/pull/180) -* The internal fock representation of states returns the correct cutoffs in all cases (solves an issue when - a pure dm was converted to ket). +* The internal fock representation of states returns the correct cutoffs in all cases + (solves an issue when a pure dm was converted to ket). [(#184)](https://github.com/XanaduAI/MrMustard/pull/184) -* The ray related tests were hanging in github action causing test to halt and fail. Now ray is forced to init with 1 cpu when running tests preventing the issue. +* The ray related tests were hanging in github action causing tests to halt and fail. + Now ray is forced to init with 1 cpu when running tests preventing the issue. [(#201)](https://github.com/XanaduAI/MrMustard/pull/201) * Various minor bug fixes. [(#202)](https://github.com/XanaduAI/MrMustard/pull/202) -* Fixed the issue that the optimization of the interferometer was using orthogonal group optimization rather than unitary. +* Fixed the issue that the optimization of the interferometer was using orthogonal group + optimization rather than unitary. [(#208)](https://github.com/XanaduAI/MrMustard/pull/208) * Fixes a slicing issue that arises when we compute the fidelity between gaussian and fock states. @@ -209,7 +219,8 @@ the other types, like `Batch[ComplexTensor]`. This will allow for better type ch * The sign of parameters in the circuit drawer are now displayed correctly. [(#209)](https://github.com/XanaduAI/MrMustard/pull/209) -* Fixed a bug in the Gaussian state which caused it to be multiplied by hbar/2 twice. Adds the argument `modes` to `Ggate`. +* Fixed a bug in the Gaussian state which caused its covariance matrix to be multiplied + by hbar/2 twice. Adds the argument `modes` to `Ggate`. [(#212)](https://github.com/XanaduAI/MrMustard/pull/212) @@ -218,14 +229,17 @@ the other types, like `Batch[ComplexTensor]`. This will allow for better type ch ### Contributors This release contains contributions from (in alphabetical order): -[Robbe De Prins](https://github.com/rdprins), [Sebastian Duque Mesa](https://github.com/sduquemesa), [Filippo Miatto](https://github.com/ziofil), [Yuan Yao](https://github.com/sylviemonet) +[Robbe De Prins](https://github.com/rdprins), [Sebastian Duque Mesa](https://github.com/sduquemesa), +[Filippo Miatto](https://github.com/ziofil), [Zeyue Niu](https://github.com/zeyueN), +[Yuan Yao](https://github.com/sylviemonet) --- -# Release 0.3.0 (current release) +# Release 0.3.0 ### New features -* Can switch progress bar on and off (default is on) from the settings via `settings.PROGRESSBAR = True/False`. +* Can switch progress bar on and off (default is on) from the settings via + `settings.PROGRESSBAR = True/False`. [(#128)](https://github.com/XanaduAI/MrMustard/issues/128) * States in Gaussian and Fock representation now can be concatenated. @@ -247,7 +261,8 @@ This release contains contributions from (in alphabetical order): ``` [(#130)](https://github.com/XanaduAI/MrMustard/pull/130) -* Parameter passthrough allows one to use custom variables and/or functions as parameters. For example we can use parameters of other gates: +* Parameter passthrough allows one to use custom variables and/or functions as parameters. + For example we can use parameters of other gates: ```python from mrmustard.lab.gates import Sgate, BSgate @@ -275,7 +290,8 @@ This release contains contributions from (in alphabetical order): ``` [(#131)](https://github.com/XanaduAI/MrMustard/pull/131) -* Adds the new trainable gate `RealInterferometer`: an interferometer that doesn't mix the q and p quadratures. +* Adds the new trainable gate `RealInterferometer`: an interferometer that doesn't mix + the q and p quadratures. [(#132)](https://github.com/XanaduAI/MrMustard/pull/132) * Now marginals can be iterated over: @@ -287,7 +303,8 @@ This release contains contributions from (in alphabetical order): ### Breaking changes -* The Parametrized and Training classes have been refactored: now trainable tensors are wrapped in an instance of the `Parameter` class. To define a set of parameters do +* The Parametrized and Training classes have been refactored: now trainable tensors are wrapped + in an instance of the `Parameter` class. To define a set of parameters do ```python from mrmustard.training import Parametrized @@ -322,8 +339,9 @@ This release contains contributions from (in alphabetical order): ### Improvements -* The Parametrized and Training classes have been refactored. The new training module has been added - and with it the new `Parameter` class: now trainable tensors are being wrapped in an instance of `Parameter`. +* The Parametrized and Training classes have been refactored. The new training module has been + added and with it the new `Parameter` class: now trainable tensors are being wrapped + in an instance of `Parameter`. [(#133)](https://github.com/XanaduAI/MrMustard/pull/133), patch [(#144)](https://github.com/XanaduAI/MrMustard/pull/144) @@ -335,8 +353,8 @@ This release contains contributions from (in alphabetical order): and on a jupyter notebook produces a table with valuable information of the Transformation objects. [(#141)](https://github.com/XanaduAI/MrMustard/pull/141) -* Add the argument 'modes' to the `Interferometer` operation to indicate which modes the Interferometer is - applied to. +* Add the argument 'modes' to the `Interferometer` operation to indicate which modes + the Interferometer is applied to. [(#121)](https://github.com/XanaduAI/MrMustard/pull/121) ### Bug fixes @@ -397,7 +415,8 @@ This release contains contributions from (in alphabetical order): ### Bug fixes -* Setting the modes on which detectors and state acts using `modes` kwarg or `__getitem__` give consistent results. +* Setting the modes on which detectors and state acts using `modes` kwarg or `__getitem__` + give consistent results. [(#114)](https://github.com/XanaduAI/MrMustard/pull/114) * Lists are used instead of generators for indices in fidelity calculations. @@ -416,7 +435,8 @@ This release contains contributions from (in alphabetical order): This release contains contributions from (in alphabetical order): -[Sebastián Duque](https://github.com/sduquemesa), [Theodor Isacsson](https://github.com/thisac/), [Filippo Miatto](https://github.com/ziofil) +[Sebastián Duque](https://github.com/sduquemesa), [Theodor Isacsson](https://github.com/thisac/), +[Filippo Miatto](https://github.com/ziofil) # Release 0.1.1 diff --git a/mrmustard/_version.py b/mrmustard/_version.py index 269c338b9..effcb89c7 100644 --- a/mrmustard/_version.py +++ b/mrmustard/_version.py @@ -16,4 +16,4 @@ Version number (major.minor.patch[-label]) """ -__version__ = "0.4.0-dev" +__version__ = "0.4.0" From 3582c5ad49c9ed239c58ba7d33a1a2fb9c4bf13f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Duque=20Mesa?= <675763+sduquemesa@users.noreply.github.com> Date: Mon, 6 Mar 2023 15:09:21 -0500 Subject: [PATCH 39/53] Dockerize Mr Mustard and create vscode dev-container (#214) **Context:** Due to the flaky tensorflow support for arm architectures it is hard to install MrMustard in Mac machines as well as creating reproducible environments. **Description of the Change:** This PR implements a fully furnished [vscode devcontainer](https://code.visualstudio.com/docs/devcontainers/containers) and provides a Dockerfile in case one wants to run a completely isolated environment with an installation of MrMustard. **Benefits:** No more dependency and architecture conflicts: MrMustard is now installed and ready to be develop with a single command. On top of that, MrMustard repo now can use github's web devcontainers feature. _How-to?_ - To try it out the devcontainer locally on vscode follow the instructions [here](https://code.visualstudio.com/docs/devcontainers/containers#_quick-start-open-a-git-repository-or-github-pr-in-an-isolated-container-volume). - Use github codespaces [ ![Open in Remote - Containers]( https://img.shields.io/static/v1?label=Remote%20-%20Containers&message=Open&color=blue&logo=visualstudiocode ) ]( https://vscode.dev/redirect?url=vscode://ms-vscode-remote.remote-containers/cloneInVolume?url=https://github.com/XanaduAI/mrmustard ) - Use `docker build` to build and image with MrMustard from the `Dockerfile` **Possible Drawbacks:** None **Related GitHub Issues:** None --------- Co-authored-by: ziofil --- .devcontainer/Dockerfile | 42 ++++ .devcontainer/devcontainer.json | 37 +++ .devcontainer/post-install.sh | 8 + .github/CHANGELOG.md | 7 +- Dockerfile | 50 ++++ doc/development/development_guide.rst | 2 +- requirements.txt | 9 +- setup.py | 4 +- tests/test_training/test_trainer.py | 345 +++++++++++++------------- 9 files changed, 329 insertions(+), 175 deletions(-) create mode 100644 .devcontainer/Dockerfile create mode 100644 .devcontainer/devcontainer.json create mode 100755 .devcontainer/post-install.sh create mode 100644 Dockerfile diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile new file mode 100644 index 000000000..12f6a982d --- /dev/null +++ b/.devcontainer/Dockerfile @@ -0,0 +1,42 @@ +FROM python:3.9 + +# Configure apt for setup +ENV DEBIAN_FRONTEND=noninteractive + +RUN apt-get update && \ + apt-get -y install --no-install-recommends sudo \ + zsh \ + less \ + curl \ + wget \ + fonts-powerline \ + locales \ + graphviz \ + && apt-get clean && rm -rf /var/lib/apt/lists/* + +### GIT GLOBAL SETUP ### + +RUN git config --global core.excludesfile /.globalgitignore +RUN touch /.globalgitignore +RUN echo ".notebooks" >> /.globalgitignore +RUN echo "nohup.out" >> /.globalgitignore + +### ZSH TERMINAL SETUP ### + +# generate locale for zsh terminal agnoster theme +RUN echo "en_US.UTF-8 UTF-8" > /etc/locale.gen && /usr/sbin/locale-gen +RUN locale-gen en_US.UTF-8 +# set term to be bash instead of sh +ENV TERM xterm +ENV SHELL /bin/zsh +# install oh-my-zsh +RUN sh -c "$(wget -nv -O- https://raw.githubusercontent.com/ohmyzsh/ohmyzsh/master/tools/install.sh)" + +### PYTHON DEPENDENCIES INTALLATION ### + +# upgrade pip and install package manager +RUN python -m pip install --no-cache-dir --upgrade pip + +### TEAR DOWN IMAGE SETUP ### +# switch back to dialog for any ad-hoc use of apt-get +ENV DEBIAN_FRONTEND=dialog diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 000000000..19d98ebca --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -0,0 +1,37 @@ +// For format details, see https://aka.ms/devcontainer.json. For config options, see the +// README at: https://github.com/devcontainers/templates/tree/main/src/anaconda +{ + "name": "Mr Mustard", + "build": { + "context": "..", + "dockerfile": "Dockerfile" + }, + "postCreateCommand": "/bin/zsh ./.devcontainer/post-install.sh", + "customizations": { + "vscode": { + "extensions": [ + "ms-python.python", + "ms-python.vscode-pylance", + "eamodio.gitlens", + "dbaeumer.vscode-eslint", + "GitHub.vscode-pull-request-github", + "mutantdino.resourcemonitor" + ], + "settings": { + "python.testing.pytestArgs": [ + "tests" + ], + "python.testing.unittestEnabled": false, + "python.testing.pytestEnabled": true, + "python.linting.pylintEnabled": true, + "python.linting.enabled": true, + "python.languageServer": "Pylance", + "python.defaultInterpreterPath": "/usr/local/bin/python", + "python.terminal.executeInFileDir": true, + "code-runner.fileDirectoryAsCwd": true, + "terminal.integrated.env.linux": {"PYTHONPATH": "/workspaces/mrmustard"} + } + } + }, + "remoteUser": "root" +} diff --git a/.devcontainer/post-install.sh b/.devcontainer/post-install.sh new file mode 100755 index 000000000..c7cbf8aab --- /dev/null +++ b/.devcontainer/post-install.sh @@ -0,0 +1,8 @@ +#! /bin/zsh + +pip install --no-cache-dir -r requirements.txt +pip install --no-cache-dir -r requirements-dev.txt +pip install --no-cache-dir -r doc/requirements.txt +pip install ray +pip install pylint +pip install -e . diff --git a/.github/CHANGELOG.md b/.github/CHANGELOG.md index d476b3e1f..3ab2558c4 100644 --- a/.github/CHANGELOG.md +++ b/.github/CHANGELOG.md @@ -116,12 +116,17 @@ The methods are differentiable, so that they can be used for defining a cost function. [(#154)](https://github.com/XanaduAI/MrMustard/pull/154) +* MrMustard repo now provides a fully furnished vscode development container and a Dockerfile. To + find out how to use dev containers for development check the documentation + [here](https://code.visualstudio.com/docs/devcontainers/containers). + [(#214)](https://github.com/XanaduAI/MrMustard/pull/214) + ### Breaking changes ### Improvements * The `Dgate` is now implemented directly in MrMustard (instead of on The Walrus) to calculate the - unitary and gradients of the displacement gate in fock representation, providing better numerical + unitary and gradients of the displacement gate in Fock representation, providing better numerical stability for larger cutoff and displacement values. [(#147)](https://github.com/XanaduAI/MrMustard/pull/147) [(#211)](https://github.com/XanaduAI/MrMustard/pull/211) diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 000000000..cf329f62f --- /dev/null +++ b/Dockerfile @@ -0,0 +1,50 @@ +FROM python:3.10 + +# Configure apt for setup +ENV DEBIAN_FRONTEND=noninteractive + +WORKDIR /mrmustard +COPY . . + +RUN apt-get update && \ + apt-get -y install --no-install-recommends sudo \ + zsh \ + less \ + curl \ + wget \ + graphviz \ + fonts-powerline \ + locales \ + git \ + && apt-get clean && rm -rf /var/lib/apt/lists/* + +### GIT GLOBAL SETUP ### + +RUN git config --global core.excludesfile /.globalgitignore +RUN touch /.globalgitignore +RUN echo ".notebooks" >> /.globalgitignore +RUN echo "nohup.out" >> /.globalgitignore + +### ZSH TERMINAL SETUP ### + +# generate locale for zsh terminal agnoster theme +RUN echo "en_US.UTF-8 UTF-8" > /etc/locale.gen && /usr/sbin/locale-gen +RUN locale-gen en_US.UTF-8 +# set term to be bash instead of sh +ENV TERM xterm +ENV SHELL /bin/zsh +# install oh-my-zsh +RUN sh -c "$(wget -nv -O- https://raw.githubusercontent.com/ohmyzsh/ohmyzsh/master/tools/install.sh)" + +### PYTHON DEPENDENCIES INTALLATION ### + +# upgrade pip and install package manager +RUN python -m pip install --no-cache-dir --upgrade pip +RUN pip install --no-cache-dir -r requirements.txt +RUN pip install --no-cache-dir -r requirements-dev.txt +RUN pip install --no-cache-dir ray +RUN pip install --no-cache-dir -e . + +### TEAR DOWN IMAGE SETUP ### +# switch back to dialog for any ad-hoc use of apt-get +ENV DEBIAN_FRONTEND=dialog diff --git a/doc/development/development_guide.rst b/doc/development/development_guide.rst index 1692e656e..b7ffbf419 100644 --- a/doc/development/development_guide.rst +++ b/doc/development/development_guide.rst @@ -6,7 +6,7 @@ Dependencies Mr Mustard requires the following libraries be installed: -* `Python `_ >= 3.8 +* `Python `_ >= 3.9 as well as the following Python packages: diff --git a/requirements.txt b/requirements.txt index 7a05fc9e1..fd24f5edc 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,9 +1,10 @@ -numpy==1.22 +numpy==1.23.5 scipy==1.8.0 -numba==0.56.2 +numba==0.56.4 thewalrus==0.19.0 -tensorflow==2.9.2 -tensorflow-probability==0.17.0 +tensorflow==2.10.1; sys_platform != "darwin" +tensorflow_macos==2.10.0; sys_platform == "darwin" +tensorflow-probability==0.18.0 rich==10.15.1 tqdm==4.62.3 matplotlib==3.5.0 diff --git a/setup.py b/setup.py index 4b39d7e69..2ff2121ce 100644 --- a/setup.py +++ b/setup.py @@ -21,8 +21,8 @@ "scipy", "numba", "thewalrus>=0.17.0", - "tensorflow>=2.4.0", - "tensorflow-probability", + "tensorflow<=2.10.1", + "tensorflow-probability<=0.18.0", "rich", "tqdm", "matplotlib", diff --git a/tests/test_training/test_trainer.py b/tests/test_training/test_trainer.py index 7e620ca26..1ac08eb9e 100644 --- a/tests/test_training/test_trainer.py +++ b/tests/test_training/test_trainer.py @@ -19,16 +19,22 @@ import numpy as np import pytest -import ray + +try: + import ray + + ray_available = True + + NUM_CPUS = 1 + ray.init(num_cpus=NUM_CPUS) +except ImportError: + ray_available = False from mrmustard.lab import Dgate, Gaussian, Ggate, Vacuum from mrmustard.physics import fidelity from mrmustard.training import Optimizer from mrmustard.training.trainer import map_trainer, train_device, update_pop -NUM_CPUS = 1 -ray.init(num_cpus=NUM_CPUS) - @pytest.fixture(scope="function") def wrappers(): @@ -50,188 +56,193 @@ def cost_fn(circ=make_circ(0.1), y_targ=0.0): return make_circ, cost_fn -@pytest.mark.parametrize( - "tasks", [5, [{"y_targ": 0.1}, {"y_targ": -0.2}], {"c0": {}, "c1": {"y_targ": 0.07}}] -) -@pytest.mark.parametrize("seed", [None, 42]) -def test_circ_cost(wrappers, tasks, seed): # pylint: disable=redefined-outer-name - """Test distributed cost calculations.""" - has_seed = isinstance(seed, int) - _, cost_fn = wrappers - results = map_trainer( - cost_fn=cost_fn, - tasks=tasks, - num_cpus=NUM_CPUS, - **({"SEED": seed} if has_seed else {}), - ) +@pytest.mark.skipif(not ray_available, reason="ray is not available") +class TestTrainer: + """Class containinf ray-related tests.""" - if isinstance(tasks, dict): - assert set(results.keys()) == set(tasks.keys()) - results = list(results.values()) - assert all(r["optimizer"] is None for r in results) - assert all(r["device"] == [] for r in results) - if has_seed and isinstance(tasks, int): - assert len(set(r["cost"] for r in results)) == 1 - else: - assert ( - len(set(r["cost"] for r in results)) - >= (tasks if isinstance(tasks, int) else len(tasks)) - 1 + @pytest.mark.parametrize( + "tasks", [5, [{"y_targ": 0.1}, {"y_targ": -0.2}], {"c0": {}, "c1": {"y_targ": 0.07}}] + ) + @pytest.mark.parametrize("seed", [None, 42]) + def test_circ_cost(self, wrappers, tasks, seed): # pylint: disable=redefined-outer-name + """Test distributed cost calculations.""" + has_seed = isinstance(seed, int) + _, cost_fn = wrappers + results = map_trainer( + cost_fn=cost_fn, + tasks=tasks, + num_cpus=NUM_CPUS, + **({"SEED": seed} if has_seed else {}), ) - -@pytest.mark.parametrize( - "tasks", [[{"x": 0.1}, {"y_targ": 0.2}], {"c0": {}, "c1": {"euclidean_lr": 0.02, "HBAR": 1.0}}] -) -@pytest.mark.parametrize( - "return_type", - [None, "dict"], -) -def test_circ_optimize(wrappers, tasks, return_type): # pylint: disable=redefined-outer-name - """Test distributed optimizations.""" - max_steps = 15 - make_circ, cost_fn = wrappers - results = map_trainer( - cost_fn=cost_fn, - device_factory=make_circ, - tasks=tasks, - max_steps=max_steps, - symplectic_lr=0.05, - return_type=return_type, - num_cpus=NUM_CPUS, + if isinstance(tasks, dict): + assert set(results.keys()) == set(tasks.keys()) + results = list(results.values()) + assert all(r["optimizer"] is None for r in results) + assert all(r["device"] == [] for r in results) + if has_seed and isinstance(tasks, int): + assert len(set(r["cost"] for r in results)) == 1 + else: + assert ( + len(set(r["cost"] for r in results)) + >= (tasks if isinstance(tasks, int) else len(tasks)) - 1 + ) + + @pytest.mark.parametrize( + "tasks", + [[{"x": 0.1}, {"y_targ": 0.2}], {"c0": {}, "c1": {"euclidean_lr": 0.02, "HBAR": 1.0}}], ) - - if isinstance(tasks, dict): - assert set(results.keys()) == set(tasks.keys()) - results = list(results.values()) - assert ( - len(set(r["cost"] for r in results)) - >= (tasks if isinstance(tasks, int) else len(tasks)) - 1 + @pytest.mark.parametrize( + "return_type", + [None, "dict"], ) - assert all(isinstance(r["optimizer"], Optimizer) for r in results) - assert all((r["optimizer"].opt_history) for r in results) + def test_circ_optimize( + self, wrappers, tasks, return_type + ): # pylint: disable=redefined-outer-name + """Test distributed optimizations.""" + max_steps = 15 + make_circ, cost_fn = wrappers + results = map_trainer( + cost_fn=cost_fn, + device_factory=make_circ, + tasks=tasks, + max_steps=max_steps, + symplectic_lr=0.05, + return_type=return_type, + num_cpus=NUM_CPUS, + ) - # Check if optimization history is actually decreasing. - opt_history = np.array(results[0]["optimizer"].opt_history) - assert len(opt_history) == max_steps + 1 - assert opt_history[0] - opt_history[-1] > 1e-6 - assert (np.diff(opt_history) < 0).sum() >= 3 + if isinstance(tasks, dict): + assert set(results.keys()) == set(tasks.keys()) + results = list(results.values()) + assert ( + len(set(r["cost"] for r in results)) + >= (tasks if isinstance(tasks, int) else len(tasks)) - 1 + ) + assert all(isinstance(r["optimizer"], Optimizer) for r in results) + assert all((r["optimizer"].opt_history) for r in results) + # Check if optimization history is actually decreasing. + opt_history = np.array(results[0]["optimizer"].opt_history) + assert len(opt_history) == max_steps + 1 + assert opt_history[0] - opt_history[-1] > 1e-6 + assert (np.diff(opt_history) < 0).sum() >= 3 -@pytest.mark.parametrize( - "metric_fns", - [ - {"is_gaussian": lambda c: c.is_gaussian, "foo": lambda _: 17.0}, + @pytest.mark.parametrize( + "metric_fns", [ - lambda c: c.modes, - len, + {"is_gaussian": lambda c: c.is_gaussian, "foo": lambda _: 17.0}, + [ + lambda c: c.modes, + len, + ], + lambda c: (Vacuum(1) >> c >> c >> c).fock_probabilities([5]), ], - lambda c: (Vacuum(1) >> c >> c >> c).fock_probabilities([5]), - ], -) -def test_circ_optimize_metrics(wrappers, metric_fns): # pylint: disable=redefined-outer-name - """Tests custom metric functions on final circuits.""" - make_circ, cost_fn = wrappers - - tasks = { - "my-job": {"x": 0.1, "euclidean_lr": 0.01, "max_steps": 100}, - "my-other-job": {"x": -0.7, "euclidean_lr": 0.1, "max_steps": 20}, - } - - results = map_trainer( - cost_fn=cost_fn, - device_factory=make_circ, - tasks=tasks, - y_targ=0.35, - symplectic_lr=0.05, - metric_fns=metric_fns, - return_list=True, - num_cpus=NUM_CPUS, ) - - assert set(results.keys()) == set(tasks.keys()) - results = list(results.values()) - assert all(("metrics" in r or set(metric_fns.keys()).issubset(set(r.keys()))) for r in results) - assert ( - len(set(r["cost"] for r in results)) - >= (tasks if isinstance(tasks, int) else len(tasks)) - 1 - ) - assert all(isinstance(r["optimizer"], Optimizer) for r in results) - assert all((r["optimizer"].opt_history) for r in results) - - # Check if optimization history is actually decreasing. - opt_history = np.array(results[0]["optimizer"].opt_history) - assert opt_history[1] - opt_history[-1] > 1e-6 - - -def test_update_pop(): - """Test for coverage.""" - d = {"a": 3, "b": "foo"} - kwargs = {"b": "bar", "c": 22} - d1, kwargs = update_pop(d, **kwargs) - assert d1["b"] == "bar" - assert len(kwargs) == 1 - - -def test_no_ray(monkeypatch): - """Tests ray import error""" - monkeypatch.setitem(sys.modules, "ray", None) - with pytest.raises(ImportError, match="Failed to import `ray`"): - _ = map_trainer( - tasks=2, + def test_circ_optimize_metrics( + self, wrappers, metric_fns + ): # pylint: disable=redefined-outer-name + """Tests custom metric functions on final circuits.""" + make_circ, cost_fn = wrappers + + tasks = { + "my-job": {"x": 0.1, "euclidean_lr": 0.01, "max_steps": 100}, + "my-other-job": {"x": -0.7, "euclidean_lr": 0.1, "max_steps": 20}, + } + + results = map_trainer( + cost_fn=cost_fn, + device_factory=make_circ, + tasks=tasks, + y_targ=0.35, + symplectic_lr=0.05, + metric_fns=metric_fns, + return_list=True, num_cpus=NUM_CPUS, ) - -def test_invalid_tasks(): - """Tests unexpected tasks arg""" - with pytest.raises(ValueError, match="`tasks` is expected to be of type int, list, or dict."): - _ = map_trainer( - tasks=2.3, + assert set(results.keys()) == set(tasks.keys()) + results = list(results.values()) + assert all( + ("metrics" in r or set(metric_fns.keys()).issubset(set(r.keys()))) for r in results + ) + assert ( + len(set(r["cost"] for r in results)) + >= (tasks if isinstance(tasks, int) else len(tasks)) - 1 + ) + assert all(isinstance(r["optimizer"], Optimizer) for r in results) + assert all((r["optimizer"].opt_history) for r in results) + + # Check if optimization history is actually decreasing. + opt_history = np.array(results[0]["optimizer"].opt_history) + assert opt_history[1] - opt_history[-1] > 1e-6 + + def test_update_pop(self): + """Test for coverage.""" + d = {"a": 3, "b": "foo"} + kwargs = {"b": "bar", "c": 22} + d1, kwargs = update_pop(d, **kwargs) + assert d1["b"] == "bar" + assert len(kwargs) == 1 + + def test_no_ray(self, monkeypatch): + """Tests ray import error""" + monkeypatch.setitem(sys.modules, "ray", None) + with pytest.raises(ImportError, match="Failed to import `ray`"): + _ = map_trainer( + tasks=2, + num_cpus=NUM_CPUS, + ) + + def test_invalid_tasks(self): + """Tests unexpected tasks arg""" + with pytest.raises( + ValueError, match="`tasks` is expected to be of type int, list, or dict." + ): + _ = map_trainer( + tasks=2.3, + num_cpus=NUM_CPUS, + ) + + def test_warn_unused_kwargs(self, wrappers): # pylint: disable=redefined-outer-name + """Test warning of unused kwargs""" + _, cost_fn = wrappers + with pytest.warns(UserWarning, match="Unused kwargs:"): + results = train_device( + cost_fn=cost_fn, + foo="bar", + ) + assert len(results) >= 4 + assert isinstance(results["cost"], float) + + def test_no_pbar(self, wrappers): # pylint: disable=redefined-outer-name + """Test turning off pregress bar""" + _, cost_fn = wrappers + results = map_trainer( + cost_fn=cost_fn, + tasks=2, + pbar=False, num_cpus=NUM_CPUS, ) + assert len(results) == 2 - -def test_warn_unused_kwargs(wrappers): # pylint: disable=redefined-outer-name - """Test warning of unused kwargs""" - _, cost_fn = wrappers - with pytest.warns(UserWarning, match="Unused kwargs:"): - results = train_device( + @pytest.mark.parametrize("tasks", [2, {"c0": {}, "c1": {"y_targ": -0.7}}]) + def test_unblock(self, wrappers, tasks): # pylint: disable=redefined-outer-name + """Test unblock async mode""" + _, cost_fn = wrappers + result_getter = map_trainer( cost_fn=cost_fn, - foo="bar", + tasks=tasks, + unblock=True, + num_cpus=NUM_CPUS, ) - assert len(results) >= 4 - assert isinstance(results["cost"], float) - - -def test_no_pbar(wrappers): # pylint: disable=redefined-outer-name - """Test turning off pregress bar""" - _, cost_fn = wrappers - results = map_trainer( - cost_fn=cost_fn, - tasks=2, - pbar=False, - num_cpus=NUM_CPUS, - ) - assert len(results) == 2 - - -@pytest.mark.parametrize("tasks", [2, {"c0": {}, "c1": {"y_targ": -0.7}}]) -def test_unblock(wrappers, tasks): # pylint: disable=redefined-outer-name - """Test unblock async mode""" - _, cost_fn = wrappers - result_getter = map_trainer( - cost_fn=cost_fn, - tasks=tasks, - unblock=True, - num_cpus=NUM_CPUS, - ) - assert callable(result_getter) + assert callable(result_getter) - sleep(0.2) - results = result_getter() - if len(results) <= (tasks if isinstance(tasks, int) else len(tasks)): - # safer on slower machines - sleep(1) + sleep(0.2) results = result_getter() + if len(results) <= (tasks if isinstance(tasks, int) else len(tasks)): + # safer on slower machines + sleep(1) + results = result_getter() - assert len(results) == (tasks if isinstance(tasks, int) else len(tasks)) + assert len(results) == (tasks if isinstance(tasks, int) else len(tasks)) From 1eab73f6169f9df560d72a067d49f54dcacd0694 Mon Sep 17 00:00:00 2001 From: ziofil Date: Tue, 7 Mar 2023 10:59:32 -0800 Subject: [PATCH 40/53] Choi cutoff fix (#216) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit **Context:** the choi method sometimes uses wrong cutoffs **Description of the Change:** fixes it by picking the cutoffs from the input state **Benefits:** correct implementation **Possible Drawbacks:** none **Related GitHub Issues:** none --------- Co-authored-by: Sebastián Duque Mesa <675763+sduquemesa@users.noreply.github.com> --- .github/CHANGELOG.md | 3 +++ mrmustard/lab/abstract/transformation.py | 2 +- tests/test_lab/test_gates_fock.py | 10 +++++++--- 3 files changed, 11 insertions(+), 4 deletions(-) diff --git a/.github/CHANGELOG.md b/.github/CHANGELOG.md index 3ab2558c4..280b3d119 100644 --- a/.github/CHANGELOG.md +++ b/.github/CHANGELOG.md @@ -228,6 +228,9 @@ by hbar/2 twice. Adds the argument `modes` to `Ggate`. [(#212)](https://github.com/XanaduAI/MrMustard/pull/212) +* Fixes a bug in the cutoffs of the choi operator. + [(#216)](https://github.com/XanaduAI/MrMustard/pull/216) + ### Documentation diff --git a/mrmustard/lab/abstract/transformation.py b/mrmustard/lab/abstract/transformation.py index b459fb7fe..f8a1456dc 100644 --- a/mrmustard/lab/abstract/transformation.py +++ b/mrmustard/lab/abstract/transformation.py @@ -111,7 +111,7 @@ def transform_fock(self, state: State, dual: bool) -> State: return State(ket=fock.apply_kraus_to_ket(U, state.ket(), op_idx), modes=state.modes) return State(dm=fock.apply_kraus_to_dm(U, state.dm(), op_idx), modes=state.modes) else: - choi = self.choi(cutoffs=state.cutoffs) + choi = self.choi(cutoffs=[state.cutoffs[i] for i in op_idx]) n = state.num_modes N0 = list(range(0, n)) N1 = list(range(n, 2 * n)) diff --git a/tests/test_lab/test_gates_fock.py b/tests/test_lab/test_gates_fock.py index e36742ae4..7a4e8bd96 100644 --- a/tests/test_lab/test_gates_fock.py +++ b/tests/test_lab/test_gates_fock.py @@ -25,14 +25,13 @@ from mrmustard.lab import ( Attenuator, BSgate, + Coherent, Dgate, Interferometer, MZgate, + RealInterferometer, Rgate, S2gate, - Attenuator, - RealInterferometer, - Vacuum, Sgate, ) from mrmustard.lab.states import TMSV, Fock, SqueezedVacuum, State @@ -222,3 +221,8 @@ def test_raise_interferometer_error(): Interferometer(num_modes=num_modes, modes=modes) with pytest.raises(ValueError): RealInterferometer(num_modes=num_modes, modes=modes) + + +def test_choi_cutoffs(): + output = State(dm=Coherent([1.0, 1.0]).dm([5, 8])) >> Attenuator(0.5, modes=[1]) + assert output.cutoffs == [5, 8] # cutoffs are respected by the gate From a84bb6e66465af92d9f02ab59bdc129158d4a865 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Duque=20Mesa?= <675763+sduquemesa@users.noreply.github.com> Date: Tue, 7 Mar 2023 14:18:33 -0500 Subject: [PATCH 41/53] Update requirements (#215) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit **Context:** Dependencies have not been revisited in a while. **Description of the Change:** Updates version of dependencies. In specific, tensorflow is pinned to version `2.10.1` since version `2.11.0` has changes in the functions `stack` and `concat` that break the integration with Mr Mustard. To unpin it we will have to revisit the new tf api for those functions so that users installing MrMustard with `pip install mrmustard` won't run into unexpected errors. Also, since `ray` is an optional dependency, tests related to ray are now skipped if the package is not installed. **Benefits:** Fixes a lot of security issues in TF as alerted by dependabot and also provides updated versions of the packages. **Possible Drawbacks:** TF is pinned — we should address this for the next release. Also, we should consider moving to a [PEP 621](https://peps.python.org/pep-0621/) compliant packaging system by either using a `pyproject.toml` file for dependency specification or a dependency manager like poetry. **Related GitHub Issues:** None --------- Co-authored-by: ziofil Co-authored-by: ziofil --- setup.py | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/setup.py b/setup.py index 2ff2121ce..3e0b0f73f 100644 --- a/setup.py +++ b/setup.py @@ -11,21 +11,23 @@ # 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 setuptools import setup, find_packages +import platform + +from setuptools import find_packages, setup with open("mrmustard/_version.py") as f: version = f.readlines()[-1].split()[-1].strip("\"'") requirements = [ - "numpy", - "scipy", - "numba", - "thewalrus>=0.17.0", - "tensorflow<=2.10.1", - "tensorflow-probability<=0.18.0", - "rich", - "tqdm", - "matplotlib", + "numpy==1.23.5", + "scipy==1.8.0", + "numba==0.56.4", + "thewalrus==0.19.0", + "tensorflow_macos==2.10.0" if platform.system() == "Darwin" else "tensorflow==2.10.1", + "tensorflow-probability==0.18.0", + "rich==10.15.1", + "tqdm==4.62.3", + "matplotlib==3.5.0", ] extra_requirements = { From af67a53c93f362e72eabf8c6437c5ddff8bf6e9d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Duque=20Mesa?= <675763+sduquemesa@users.noreply.github.com> Date: Tue, 7 Mar 2023 15:30:14 -0500 Subject: [PATCH 42/53] Fix build action on main branch --- .github/workflows/builds.yml | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/.github/workflows/builds.yml b/.github/workflows/builds.yml index 1acc3dfc0..f92a611a9 100644 --- a/.github/workflows/builds.yml +++ b/.github/workflows/builds.yml @@ -45,12 +45,4 @@ jobs: - name: Run tests run: | - if [ ${{ matrix.python-version }} == "3.9" ]; then COVERAGE="--cov=mrmustard --cov-report=term-missing --cov-report=xml"; else COVERAGE=""; fi - python -m pytest tests -p no:warnings --tb=native ${{COVERAGE}} - - - name: Upload coverage to Codecov - uses: codecov/codecov-action@v3 - if: ${{ matrix.python-version }} == '3.9' - with: - files: ./coverage.xml - fail_ci_if_error: true + python -m pytest tests -p no:warnings --tb=native From 849c1ee22bb2e05d78592b30c24a3501c412a138 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Duque=20Mesa?= <675763+sduquemesa@users.noreply.github.com> Date: Wed, 8 Mar 2023 17:16:39 -0500 Subject: [PATCH 43/53] Increment version to 0.5.0-dev (#220) Incrementing version to 0.5.0-dev --- .github/CHANGELOG.md | 17 ++++++++++++++++- mrmustard/_version.py | 2 +- 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/.github/CHANGELOG.md b/.github/CHANGELOG.md index 280b3d119..135ddc4f2 100644 --- a/.github/CHANGELOG.md +++ b/.github/CHANGELOG.md @@ -1,4 +1,19 @@ -# Release 0.4.0 +# Release 0.5.0 (development release) + +### New features + +### Breaking Changes + +### Improvements + +### Bug fixes + +### Documentation + +### Contributors + +This release contains contributions from (in alphabetical order): +# Release 0.4.0 (current release) ### New features diff --git a/mrmustard/_version.py b/mrmustard/_version.py index effcb89c7..4df8b481b 100644 --- a/mrmustard/_version.py +++ b/mrmustard/_version.py @@ -16,4 +16,4 @@ Version number (major.minor.patch[-label]) """ -__version__ = "0.4.0" +__version__ = "0.5.0-dev" From 8fbaaeb7ca890c395b1790a1e5e2454d203d1e76 Mon Sep 17 00:00:00 2001 From: ziofil Date: Wed, 8 Mar 2023 15:27:01 -0800 Subject: [PATCH 44/53] Merge branch 'develop' of github.com:XanaduAI/MrMustard into develop --- .github/CHANGELOG.md | 17 ++++++++++++++++- mrmustard/_version.py | 2 +- 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/.github/CHANGELOG.md b/.github/CHANGELOG.md index 280b3d119..135ddc4f2 100644 --- a/.github/CHANGELOG.md +++ b/.github/CHANGELOG.md @@ -1,4 +1,19 @@ -# Release 0.4.0 +# Release 0.5.0 (development release) + +### New features + +### Breaking Changes + +### Improvements + +### Bug fixes + +### Documentation + +### Contributors + +This release contains contributions from (in alphabetical order): +# Release 0.4.0 (current release) ### New features diff --git a/mrmustard/_version.py b/mrmustard/_version.py index effcb89c7..4df8b481b 100644 --- a/mrmustard/_version.py +++ b/mrmustard/_version.py @@ -16,4 +16,4 @@ Version number (major.minor.patch[-label]) """ -__version__ = "0.4.0" +__version__ = "0.5.0-dev" From 1abad670dc76a83efd73f8189f3875bd7915c234 Mon Sep 17 00:00:00 2001 From: zeyueN <48225584+zeyueN@users.noreply.github.com> Date: Fri, 10 Mar 2023 21:01:31 -0500 Subject: [PATCH 45/53] Improve callbacks and adds tensorboard (#219) **Context:** To facilitate the investigation of the optimization process on the numerics side, as well as giving users more control over the optimization progress, the callback functionality is expanded to allow for realtime monitoring and updates. A builtin callback that tracks optimization progresses in Tensorboard is also provided. **Description of the Change:** - The `callback` argument to `Optimizer.minimize` is now `callbacks` with the new possibility of passing multiple callbacks. - Adds tagged parameter traversal `Parametrized.traverse_trainables()` which accumulates the path of tags while traversing. This is now used by the optimizer to get unique human interpretable identifiers to parameters (for better tracking). - Callback functions now accept as argument the optimizer, the cost value, the cost function, and the trainable parameter values as well as their gradient. This gives users a lot of run time control, but it's not necessary to use them all. - The execution of callbacks moved to after the backprop but before the parameter update (originally at the end of the step). - A `Callback` base class for users to extend and inherit from. - A `TensorboardCallbacks` that does automatic tracking of costs and parameters (values and gradients) in real time. Misc changes: - I added a `path` item to the testing workflow so that non-code changes won't trigger it. **Benefits:** - Better observability. - Finer control. - Opens up a lot of possibilities for optimization. **Possible Drawbacks:** Users could introduce too much overhead if the callbacks are too heavy, thus potentially slowing down the optimization. **Related GitHub Issues:** None --------- Co-authored-by: ziofil --- .github/CHANGELOG.md | 40 ++++ .github/workflows/tests.yml | 7 + doc/code/training.rst | 1 + doc/code/training/callbacks.rst | 7 + mrmustard/training/__init__.py | 1 + mrmustard/training/callbacks.py | 286 +++++++++++++++++++++++ mrmustard/training/optimizer.py | 137 +++++++++-- mrmustard/training/parametrized.py | 66 +++++- mrmustard/typing.py | 9 +- tests/test_training/test_callbacks.py | 73 ++++++ tests/test_training/test_opt.py | 24 +- tests/test_training/test_parametrized.py | 22 ++ 12 files changed, 642 insertions(+), 31 deletions(-) create mode 100644 doc/code/training/callbacks.rst create mode 100644 mrmustard/training/callbacks.py create mode 100644 tests/test_training/test_callbacks.py diff --git a/.github/CHANGELOG.md b/.github/CHANGELOG.md index 135ddc4f2..bcfc1b58e 100644 --- a/.github/CHANGELOG.md +++ b/.github/CHANGELOG.md @@ -2,8 +2,44 @@ ### New features +* Optimization callback functionalities has been improved. A dedicated `Callback` class is added which + is able to access the optimizer, the cost function, the parameters as well as gradients, during the + optimization. In addition, multiple callbacks can be specified. This opens up the endless possiblities + of customizing the the optimization progress with schedulers, trackers, heuristics, tricks, etc. + [(#219)](https://github.com/XanaduAI/MrMustard/pull/219) + +* Tensorboard based optimization tracking is added as a builtin `Callback` class: `TensorboardCallback`. + It can automatically track costs as well as all trainable parameters during optimization in realtime. + Tensorboard can be most conveniently viewed from VScode. + [(#219)](https://github.com/XanaduAI/MrMustard/pull/219) + + ```python + import numpy as np + from mrmustard.training import Optimizer, TensorboardCallback + + def cost_fn(): + ... + + def as_dB(cost): + delta = np.sqrt(np.log(1 / (abs(cost) ** 2)) / (2 * np.pi)) + cost_dB = -10 * np.log10(delta**2) + return cost_dB + + tb_cb = TensorboardCallback(cost_converter=as_dB, track_grads=True) + + opt = Optimizer(euclidean_lr = 0.001); + opt.minimize(cost_fn, max_steps=200, by_optimizing=[...], callbacks=tb_cb) + + # Logs will be stored in `tb_cb.logdir` which defaults to `./tb_logdir/...` but can be customized. + # VScode can be used to open the Tensorboard frontend for live monitoring. + # Or, in command line: `tensorboard --logdir={tb_cb.logdir}` and open link in browser. + ``` + ### Breaking Changes +* The previous `callback` argument to `Optimizer.minimize` is now `callbacks` since we can now pass + multiple callbacks to it. + ### Improvements ### Bug fixes @@ -13,6 +49,10 @@ ### Contributors This release contains contributions from (in alphabetical order): +[Zeyue Niu](https://github.com/zeyueN) + +--- + # Release 0.4.0 (current release) ### New features diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index b081aae43..3c2d9b47e 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -4,6 +4,13 @@ on: branches: - develop pull_request: + paths: + - '.github/workflows/tests.yml' + - 'mrmustard/**' + - 'tests/**' + - 'setup.py' + - 'requirements*' + - 'pytest.ini' jobs: pytest: diff --git a/doc/code/training.rst b/doc/code/training.rst index 1ab03e1c2..d65fb613a 100644 --- a/doc/code/training.rst +++ b/doc/code/training.rst @@ -8,6 +8,7 @@ mrmustard.training training/parameter training/parametrized training/trainer + training/callbacks .. currentmodule:: mrmustard.training diff --git a/doc/code/training/callbacks.rst b/doc/code/training/callbacks.rst new file mode 100644 index 000000000..ae216849f --- /dev/null +++ b/doc/code/training/callbacks.rst @@ -0,0 +1,7 @@ +callbacks +============ + +.. currentmodule:: mrmustard.training.callbacks + +.. automodapi:: mrmustard.training.callbacks + :no-heading: diff --git a/mrmustard/training/__init__.py b/mrmustard/training/__init__.py index b6492b490..8f13cda51 100644 --- a/mrmustard/training/__init__.py +++ b/mrmustard/training/__init__.py @@ -68,3 +68,4 @@ def cost_fn(): from .parametrized import Parametrized from .optimizer import Optimizer +from .callbacks import TensorboardCallback diff --git a/mrmustard/training/callbacks.py b/mrmustard/training/callbacks.py new file mode 100644 index 000000000..1c47c7ffd --- /dev/null +++ b/mrmustard/training/callbacks.py @@ -0,0 +1,286 @@ +# 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 the implementation of callback functionalities for optimizations. + +Callbacks allow users to have finer control over the optimization process by executing +predefined routines as optimization progresses. Even though the :meth:`Optimizer.minimize` accepts +`Callable` functions directly, the :class:`Callback` class modularizes the logic and makes it +easier for users to inherit from it and come up with their own custom callbacks. + +Things you can do with callbacks: + +* Logging custom metrics. +* Tracking parameters and costs with Tensorboard. +* Scheduling learning rates. +* Modifying the gradient update that gets applied. +* Updating cost_fn to alter the optimization landscape in our favour. +* Adding some RL into the optimizer. +* ... + +Builtin callbacks: + +* :class:`Callback`: The base class, to be used for building custom callbacks. +* :class:`TensorboardCallback`: Tracks costs, parameter values and gradients in Tensorboard. + +Examples: +========= + +.. code-block:: + + import numpy as np + from mrmustard.training import Optimizer, TensorboardCallback + + def cost_fn(): + ... + + def as_dB(cost): + delta = np.sqrt(np.log(1 / (abs(cost) ** 2)) / (2 * np.pi)) + cost_dB = -10 * np.log10(delta**2) + return cost_dB + + tb_cb = TensorboardCallback(cost_converter=as_dB, track_grads=True) + + def rolling_cost_cb(optimizer, cost, **kwargs): + return { + 'rolling_cost': np.mean(optimizer.opt_history[-10:] + [cost]), + } + + opt = Optimizer(euclidean_lr = 0.001); + opt.minimize(cost_fn, max_steps=200, by_optimizing=[...], callbacks=[tb_cb, rolling_cost_cb]) + + # VScode can be used to open the Tensorboard frontend for live monitoring. + + opt.callback_history['TensorboardCallback'] + opt.callback_history['rolling_cost_cb'] + +""" + +from dataclasses import dataclass +from datetime import datetime +import hashlib +from pathlib import Path +from typing import Callable, Optional, Mapping, Sequence, Union +import numpy as np +import tensorflow as tf +from mrmustard.math import Math + +math = Math() + + +@dataclass +class Callback: + """Base callback class for optimizers. Users can inherit from this class and define the + following custom logic: + + * `.trigger`: + Custom triggering condition, other than the regular schedule set by `step_per_call`. + * `.call`: + The main routine to be customized. + * `.update_cost_fn`: + The custom cost_fn updater, which is expected to return a new cost_fn callable to + replace the original one passed to the optimizer. + * `.update_grads`: + The custom grads modifyer, which is expected to return a list of parameter gradients + after modification, to be applied to the parameters. + * `.update_optimizer`: + The custom optimizer updater, which is expected to modify the optimizer inplace for + things like scheduling learning rates. + + """ + + #: Custom tag for a callback instance to be used as keys in `Optimizer.callback_history`. + #: Defaults to the class name. + tag: str = None + + #: Sets calling frequency of this callback. Defaults to once per optimization step. + #: Use higher values to reduce overhead. + steps_per_call: int = 1 + + def __post_init__(self): + self.tag = self.tag or self.__class__.__name__ + self.optimizer_step: int = 0 + self.callback_step: int = 0 + + def get_opt_step(self, optimizer, **kwargs): # pylint: disable=unused-argument + """Gets current step from optimizer.""" + self.optimizer_step = len(optimizer.opt_history) + return self.optimizer_step + + def _should_call(self, **kwargs) -> bool: + return (self.get_opt_step(**kwargs) % self.steps_per_call == 0) or self.trigger(**kwargs) + + def trigger(self, **kwargs) -> bool: # pylint: disable=unused-argument + """User implemented custom trigger conditions.""" + + def call(self, **kwargs) -> Optional[Mapping]: # pylint: disable=unused-argument + """User implemented main callback logic.""" + + def update_cost_fn(self, **kwargs) -> Optional[Callable]: # pylint: disable=unused-argument + """User implemented cost_fn modifier.""" + + def update_grads(self, **kwargs) -> Optional[Sequence]: # pylint: disable=unused-argument + """User implemented gradient modifier.""" + + def update_optimizer(self, optimizer, **kwargs): # pylint: disable=unused-argument + """User implemented optimizer update scheduler.""" + + def __call__( + self, + **kwargs, + ): + if self._should_call(**kwargs): + self.callback_step += 1 + callback_result = { + "optimizer_step": self.optimizer_step, + "callback_step": self.callback_step, + } + + callback_result.update(self.call(**kwargs) or {}) + + new_cost_fn = self.update_cost_fn(callback_result=callback_result, **kwargs) + if callable(new_cost_fn): + callback_result["cost_fn"] = new_cost_fn + + new_grads = self.update_grads(callback_result=callback_result, **kwargs) + if new_grads is not None: + callback_result["grads"] = new_grads + + # Modifies the optimizer inplace, e.g. its learning rates. + self.update_optimizer(callback_result=callback_result, **kwargs) + + return callback_result + return {} + + +@dataclass +class TensorboardCallback(Callback): # pylint: disable=too-many-instance-attributes + """Callback for enabling Tensorboard tracking of optimization progresses. + + Things tracked: + + * the cost + * the transformed cost, if a `cost_converter` is provided + * trainable parameter values + * trainable parameter gradients (if `track_grads` is `True`) + + To start the Tensorboard frontend, either: + + * use VSCode: F1 -> Tensorboard -> select your `root_logdir/experiment_tag`. + * use command line: `tensorboard --logdir=root_logdir/experiment_tag` and open link in browser. + + + """ + + #: The root logdir for tensorboard logging. + root_logdir: Union[str, Path] = "./tb_logdir" + + #: The tag for experiment subfolder to group similar optimizations together for easy comparisons. + #: Defaults to the hash of all trainable variables' names. + experiment_tag: Optional[str] = None + + #: Extra prefix to name the optimization experiment. + prefix: Optional[str] = None + + #: Transformation on cost for the purpose of better interpretation. + cost_converter: Optional[Callable] = None + + #: Whether to track gradients as well as the values for trainable parameters. + track_grads: bool = False + + #: Whether to return objectives in the callback results to be stored. + log_objectives: bool = True + + #: Whether to return parameter values in the callback results to be stored. + log_trainables: bool = False + + def __post_init__(self): + super().__post_init__() + self.root_logdir = Path(self.root_logdir) + + # Initialize only when first called to use optimization time rather than init time: + self.logdir = None + self.writter_logdir = None + self.tb_writer = None + + def init_writer(self, trainables): + """Initializes tb logdir folders and writer.""" + if (self.writter_logdir is None) or (self.optimizer_step <= self.steps_per_call): + trainable_key_hash = hashlib.sha256( + ",".join(trainables.keys()).encode("utf-8") + ).hexdigest() + self.experiment_tag = self.experiment_tag or f"experiment-{trainable_key_hash[:7]}" + self.logdir = self.root_logdir / self.experiment_tag + self.prefix = self.prefix or "optim" + existing_exp = [path for path in self.logdir.glob("*") if path.is_dir()] + optim_timestamp = datetime.now().strftime("%Y%m%d-%H%M%S") + + self.writter_logdir = self.logdir / ( + f"{self.prefix}-{len(existing_exp):03d}-{optim_timestamp}" + ) + self.tb_writer = tf.summary.create_file_writer(str(self.writter_logdir)) + self.tb_writer.set_as_default() + + def call( + self, + optimizer, + cost, + trainables, + **kwargs, + ): # pylint: disable=unused-argument,arguments-differ + """Logs costs and parameters to Tensorboard.""" + + self.init_writer(trainables=trainables) + obj_tag = "objectives" + + cost = np.array(cost).item() + + obj_scalars = { + f"{obj_tag}/cost": cost, + } + if self.cost_converter is not None: + obj_scalars[f"{obj_tag}/{self.cost_converter.__name__}(cost)"] = self.cost_converter( + cost + ) + + if "orig_cost" in optimizer.callback_history: + orig_cost = np.array(optimizer.callback_history["orig_cost"][-1]).item() + obj_scalars[f"{obj_tag}/orig_cost"] = orig_cost + if self.cost_converter is not None: + obj_scalars[ + f"{obj_tag}/{self.cost_converter.__name__}(orig_cost)" + ] = self.cost_converter(orig_cost) + + for k, v in obj_scalars.items(): + tf.summary.scalar(k, data=v, step=self.optimizer_step) + + for k, (x, dx) in trainables.items(): + x = np.array(x.value) + if self.track_grads: + dx = np.array(dx) + + tag = k if np.size(x) <= 1 else None + for ind, val in np.ndenumerate(x): + tag = tag or k + str(list(ind)).replace(" ", "") + tf.summary.scalar(tag + ":value", data=val, step=self.optimizer_step) + if self.track_grads: + tf.summary.scalar(tag + ":grad", data=dx[ind], step=self.optimizer_step) + tag = None + + result = obj_scalars if self.log_objectives else {} + + if self.log_trainables: + result.update(trainables) + + return result diff --git a/mrmustard/training/optimizer.py b/mrmustard/training/optimizer.py index 8ee5e0d19..4a49fc847 100644 --- a/mrmustard/training/optimizer.py +++ b/mrmustard/training/optimizer.py @@ -17,7 +17,8 @@ """ from itertools import chain, groupby -from typing import List, Callable, Sequence +from typing import List, Callable, Sequence, Union, Mapping, Dict +from mrmustard.training.callbacks import Callback from mrmustard.utils import graphics from mrmustard.logger import create_logger from mrmustard.math import Math @@ -55,7 +56,7 @@ def __init__( "orthogonal": orthogonal_lr, } self.opt_history: List[float] = [0] - self.callback_history: List = [] + self.callback_history: Dict[str, List] = {} self.log = create_logger(__name__) def minimize( @@ -63,7 +64,7 @@ def minimize( cost_fn: Callable, by_optimizing: Sequence[Trainable], max_steps: int = 1000, - callback: Callable = None, + callbacks: Union[Callable, Sequence[Callable], Mapping[str, Callable]] = None, ): r"""Minimizes the given cost function by optimizing circuits and/or detectors. @@ -74,32 +75,55 @@ def minimize( contain the parameters to optimize max_steps (int): the minimization keeps going until the loss is stable or max_steps are reached (if ``max_steps=0`` it will only stop when the loss is stable) - callback (Callable): a function that will be executed at each step of the optimization, which - takes as arguments the training step (int), the cost and the trainable parameters. - The return value is stored in self.callback_history. + callbacks (:class:`Callback`, `Callable`, or List/Dict of them): callback functions that + will be executed at each step of the optimization after backprop but before gradient + gets applied. It takes as arguments the optimizer itself, training step (int), the + cost value, the cost function, and the trainable parameters (values & grads) dict. + The optional returned dict for each step is stored in self.callback_history which + is a callback-name-keyed dict with each value a list of such callback result dicts. + Learn more about how to use callbacks to have finer control of the optimization + process in the :mod:`.callbacks` module. """ + callbacks = self._coerce_callbacks(callbacks) + try: - self._minimize(cost_fn, by_optimizing, max_steps, callback) + self._minimize(cost_fn, by_optimizing, max_steps, callbacks) except KeyboardInterrupt: # graceful exit self.log.info("Optimizer execution halted due to keyboard interruption.") raise self.OptimizerInterruptedError() from None - def _minimize(self, cost_fn, by_optimizing, max_steps, callback): + def _minimize(self, cost_fn, by_optimizing, max_steps, callbacks): # finding out which parameters are trainable from the ops trainable_params = self._get_trainable_params(by_optimizing) + cost_fn_modified = False + orig_cost_fn = cost_fn bar = graphics.Progressbar(max_steps) with bar: while not self.should_stop(max_steps): - cost, grads = self.compute_loss_and_gradients(cost_fn, trainable_params) - self.apply_gradients(trainable_params, grads) + cost, grads = self.compute_loss_and_gradients(cost_fn, trainable_params.values()) + + trainables = {tag: (x, dx) for (tag, x), dx in zip(trainable_params.items(), grads)} + + if cost_fn_modified: + self.callback_history["orig_cost"].append(orig_cost_fn()) + new_cost_fn, new_grads = self._run_callbacks( + callbacks=callbacks, + cost_fn=cost_fn, + cost=cost, + trainables=trainables, + ) + + self.apply_gradients(trainable_params.values(), new_grads or grads) self.opt_history.append(cost) bar.step(math.asnumpy(cost)) - if callback is not None: - self.callback_history.append( - callback(len(self.opt_history) - 1, cost, trainable_params) - ) + + if callable(new_cost_fn): + cost_fn = new_cost_fn + if not cost_fn_modified: + cost_fn_modified = True + self.callback_history["orig_cost"] = self.opt_history.copy() def apply_gradients(self, trainable_params, grads): """Apply gradients to variables. @@ -120,20 +144,33 @@ def apply_gradients(self, trainable_params, grads): update_method(grads_and_vars, param_lr) @staticmethod - def _get_trainable_params(trainable_items): - """Returns a list of trainable parameters from instances of Parametrized or - items that belong to the backend and are trainable + def _get_trainable_params(trainable_items, root_tag: str = "optimized"): + """Traverses all instances of Parametrized or trainable items that belong to the backend + and return a dict of trainables of the form `{tags: trainable_parameters}` where the `tags` + are traversal paths of collecting all parent tags for reaching each parameter. """ trainables = [] - for item in trainable_items: + for i, item in enumerate(trainable_items): + owner_tag = f"{root_tag}[{i}]" if isinstance(item, Parametrized): - trainables.append(item.trainable_parameters) + tag = f"{owner_tag}:{item.__class__.__qualname__}" + trainables.append(item.traverse_trainables(owner_tag=tag).items()) elif math.from_backend(item) and math.is_trainable(item): # the created parameter is wrapped into a list because the case above # returns a list, hence ensuring we have a list of lists - trainables.append([create_parameter(item, name="from_backend", is_trainable=True)]) - - return list(chain(*trainables)) + tag = f"{owner_tag}:{math.__class__.__name__}/{getattr(item, 'name', item.__class__.__name__)}" + trainables.append( + [ + ( + tag, + create_parameter( + item, name="from_backend", is_trainable=True, owner=tag + ), + ) + ] + ) + + return dict(chain(*trainables)) @staticmethod def _group_vars_and_grads_by_type(trainable_params, grads): @@ -186,6 +223,62 @@ def should_stop(self, max_steps: int) -> bool: return True return False + @staticmethod + def _coerce_callbacks(callbacks): + r"""Coerce callbacks into dict and validate them.""" + if callbacks is None: + callbacks = {} + elif callable(callbacks): + callbacks = { + callbacks.tag if isinstance(callbacks, Callback) else callbacks.__name__: callbacks + } + elif isinstance(callbacks, Sequence): + callbacks = { + cb.tag if isinstance(cb, Callback) else cb.__name__: cb for cb in callbacks + } + elif not isinstance(callbacks, Mapping): + raise TypeError( + f"Argument `callbacks` expected to be a callable or a list/dict of callables, got {type(callbacks)}." + ) + + if any(not callable(cb) for cb in callbacks.values()): + raise TypeError("Not all provided callbacks is callable.") + + return callbacks + + def _run_callbacks(self, callbacks, cost_fn, cost, trainables): + """Iteratively calls all callbacks and applies the necessary updates.""" + new_cost_fn, new_grads = None, None + + for cb_tag, cb in callbacks.items(): + if cb_tag not in self.callback_history: + self.callback_history[cb_tag] = [] + + cb_result = cb( + optimizer=self, + cost_fn=cost_fn if new_cost_fn is None else new_cost_fn, + cost=cost, + trainables=trainables, + ) + + if not isinstance(cb_result, (Mapping, type(None))): + raise TypeError( + f"The expected return type of callback functions is dict, got {type(cb_result)}." + ) + + new_cost_fn = cb_result.pop("cost_fn", None) + + if "grads" in cb_result: + new_grads = cb_result["grads"] + trainables = { + tag: (x, dx) for (tag, (x, _)), dx in zip(trainables.items(), new_grads) + } + + if cb_result is not None and cb_result: + self.callback_history[cb_tag].append(cb_result) + + return new_cost_fn, new_grads + class OptimizerInterruptedError(Exception): """A helper class to quietly stop execution without printing a traceback.""" diff --git a/mrmustard/training/parametrized.py b/mrmustard/training/parametrized.py index 176863243..e2a05578c 100644 --- a/mrmustard/training/parametrized.py +++ b/mrmustard/training/parametrized.py @@ -18,7 +18,7 @@ class constructor generate a backend Tensor and are assigned to fields of the class. """ -from typing import Any, Generator, List, Sequence, Tuple +from typing import Any, Generator, List, Sequence, Tuple, Mapping import numpy as np @@ -103,28 +103,82 @@ def trainable_parameters(self) -> Sequence[Trainable]: """Return a list of trainable parameters within the Parametrized object by recursively traversing the object's fields """ - return list(_traverse_parametrized(self.__dict__.values(), Trainable)) + return list(_traverse_parametrized(self.__dict__, Trainable)) @property def constant_parameters(self) -> List[Constant]: """Return a list of constant parameters within the Parametrized object by recursively traversing the object's fields """ - return list(_traverse_parametrized(self.__dict__.values(), Constant)) + return list(_traverse_parametrized(self.__dict__, Constant)) + + def traverse_trainables(self, owner_tag=None) -> Mapping[str, Trainable]: + """Return a dict of trainable parameters within the Parametrized object + by recursively traversing the object's fields. The key for each parameter + will be the path of tags for reaching it from the top level Parametrized. + """ + owner_tag = owner_tag or f"{self.__class__.__qualname__}" + return dict(_traverse_parametrized(self.__dict__, Trainable, owner_tag)) + + def traverse_constants(self, owner_tag=None) -> Mapping[str, Constant]: + """Return a dict of constant parameters within the Parametrized object + by recursively traversing the object's fields. The key for each parameter + will be the path of tags for reaching it from the top level Parametrized. + """ + owner_tag = owner_tag or f"{self.__class__.__qualname__}" + return dict(_traverse_parametrized(self.__dict__, Constant, owner_tag)) -def _traverse_parametrized(object_: Any, extract_type: Parameter) -> Generator: +def _traverse_parametrized_untagged(object_: Sequence, extract_type: Parameter) -> Generator: """This private method traverses recursively all the object's attributes for objects present in ``iterable`` which are instances of ``parameter_type`` or ``Parametrized`` returning a generator with objects of type ``extract_type``. """ - for obj in object_: if isinstance( - obj, (List, Tuple) + obj, (List, Tuple, Mapping) ): # pylint: disable=isinstance-second-argument-not-valid-type yield from _traverse_parametrized(obj, extract_type) elif isinstance(obj, Parametrized): yield from _traverse_parametrized(obj.__dict__.values(), extract_type) elif isinstance(obj, extract_type): yield obj + + +def _traverse_parametrized_tagged( + object_: Mapping, extract_type: Parameter, owner_tag: str = None +) -> Generator: + """This private method traverses recursively, while accumulating tags, all the object's + attributes for objects present in ``iterable`` which are instances of ``parameter_type`` + or ``Parametrized`` returning a generator of 2-tuples of the form (str, ``extract_type``). + """ + + delim = "/" + for k, obj in object_.items(): + obj_tag = f"{owner_tag}[{k}]" if isinstance(k, int) else f"{owner_tag}{delim}{k}" + if isinstance(obj, (Mapping, List, Tuple)): + yield from _traverse_parametrized(obj, extract_type, owner_tag=obj_tag) + elif isinstance(obj, Parametrized): + yield from _traverse_parametrized(obj.__dict__, extract_type, owner_tag=obj_tag) + elif isinstance(obj, extract_type): + yield obj_tag, obj + + +def _traverse_parametrized( + object_: Any, extract_type: Parameter, owner_tag: str = None +) -> Generator: + """The recursive parameter traversal to be used for both tagged and untagged collection + Depending on if the argument `owner_tag` is provided. + """ + + if owner_tag: + yield from _traverse_parametrized_tagged( + object_=dict(enumerate(object_)) if isinstance(object_, Sequence) else object_, + extract_type=extract_type, + owner_tag=owner_tag, + ) + else: + yield from _traverse_parametrized_untagged( + object_=list(object_.values()) if isinstance(object_, Mapping) else object_, + extract_type=extract_type, + ) diff --git a/mrmustard/typing.py b/mrmustard/typing.py index 6f374ff8a..9a8508a0f 100644 --- a/mrmustard/typing.py +++ b/mrmustard/typing.py @@ -33,7 +33,14 @@ "Tensor", "Trainable", ] -from typing import Iterator, Protocol, Tuple, TypeVar, Union, runtime_checkable +from typing import ( + Iterator, + Protocol, + Tuple, + TypeVar, + Union, + runtime_checkable, +) import numpy as np diff --git a/tests/test_training/test_callbacks.py b/tests/test_training/test_callbacks.py new file mode 100644 index 000000000..eb84284b4 --- /dev/null +++ b/tests/test_training/test_callbacks.py @@ -0,0 +1,73 @@ +# Copyright 2022 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. + +"""callbacks tests""" + +import numpy as np +import tensorflow as tf + +from mrmustard.lab.gates import ( + BSgate, + S2gate, +) +from mrmustard.lab.circuit import Circuit +from mrmustard.training import Optimizer, TensorboardCallback +from mrmustard.lab.states import Vacuum +from mrmustard import settings + +from mrmustard.math import Math + +math = Math() + + +def test_tensorboard_callback(tmp_path): + """Tests tensorboard callbacks on hong-ou-mandel optimization.""" + settings.SEED = 42 + i, k = 2, 3 + r = np.arcsinh(1.0) + s2_0, s2_1, bs = ( + S2gate(r=r, phi=0.0, phi_trainable=True)[0, 1], + S2gate(r=r, phi=0.0, phi_trainable=True)[2, 3], + BSgate( + theta=np.arccos(np.sqrt(k / (i + k))) + 0.1 * settings.rng.normal(), + phi=settings.rng.normal(), + theta_trainable=True, + phi_trainable=True, + )[1, 2], + ) + circ = Circuit([s2_0, s2_1, bs]) + state_in = Vacuum(num_modes=4) + cutoff = 1 + i + k + + free_var = math.new_variable([1.1, -0.2], None, "free_var") + + def cost_fn(): + return tf.abs( + (state_in >> circ).ket(cutoffs=[cutoff] * 4)[i, 1, i + k - 1, k] + ) ** 2 + tf.reduce_sum(free_var**2) + + tbcb = TensorboardCallback( + steps_per_call=2, + root_logdir=tmp_path, + cost_converter=np.log10, + track_grads=True, + ) + + opt = Optimizer(euclidean_lr=0.01) + opt.minimize(cost_fn, by_optimizing=[circ, free_var], max_steps=300, callbacks={"tb": tbcb}) + + assert np.allclose(np.cos(bs.theta.value) ** 2, k / (i + k), atol=1e-2) + assert tbcb.logdir.exists() + assert len(list(tbcb.writter_logdir.glob("events*"))) > 0 + assert len(opt.callback_history["tb"]) == (len(opt.opt_history) - 1) // tbcb.steps_per_call diff --git a/tests/test_training/test_opt.py b/tests/test_training/test_opt.py index 14e2029b6..ebbddb530 100644 --- a/tests/test_training/test_opt.py +++ b/tests/test_training/test_opt.py @@ -32,6 +32,7 @@ ) from mrmustard.lab.circuit import Circuit from mrmustard.training import Optimizer, Parametrized +from mrmustard.training.callbacks import Callback from mrmustard.lab.states import Vacuum, SqueezedVacuum from mrmustard.physics import fidelity from mrmustard.physics.gaussian import trace, von_neumann_entropy @@ -56,12 +57,24 @@ def test_S2gate_coincidence_prob(n): def cost_fn(): return -tf.abs((Vacuum(2) >> S[0, 1]).ket(cutoffs=[n + 1, n + 1])[n, n]) ** 2 + def cb(optimizer, cost, trainables, **kwargs): # pylint: disable=unused-argument + return { + "cost": cost, + "lr": optimizer.learning_rate["euclidean"], + "num_trainables": len(trainables), + } + opt = Optimizer(euclidean_lr=0.01) - opt.minimize(cost_fn, by_optimizing=[S], max_steps=300) + opt.minimize(cost_fn, by_optimizing=[S], max_steps=300, callbacks=cb) expected = 1 / (n + 1) * (n / (n + 1)) ** n assert np.allclose(-cost_fn(), expected, atol=1e-5) + cb_result = opt.callback_history.get("cb") + assert {res["num_trainables"] for res in cb_result} == {2} + assert {res["lr"] for res in cb_result} == {0.01} + assert [res["cost"] for res in cb_result] == opt.opt_history[1:] + @given(i=st.integers(1, 5), k=st.integers(1, 5)) def test_hong_ou_mandel_optimizer(i, k): @@ -90,8 +103,15 @@ def cost_fn(): return tf.abs((state_in >> circ).ket(cutoffs=[cutoff] * 4)[i, 1, i + k - 1, k]) ** 2 opt = Optimizer(euclidean_lr=0.01) - opt.minimize(cost_fn, by_optimizing=[circ], max_steps=300) + opt.minimize( + cost_fn, + by_optimizing=[circ], + max_steps=300, + callbacks=[Callback(tag="null_cb", steps_per_call=3)], + ) assert np.allclose(np.cos(bs.theta.value) ** 2, k / (i + k), atol=1e-2) + assert "null_cb" in opt.callback_history + assert len(opt.callback_history["null_cb"]) == (len(opt.opt_history) - 1) // 3 def test_learning_two_mode_squeezing(): diff --git a/tests/test_training/test_parametrized.py b/tests/test_training/test_parametrized.py index 171dc4685..55ca3f7b4 100644 --- a/tests/test_training/test_parametrized.py +++ b/tests/test_training/test_parametrized.py @@ -115,6 +115,18 @@ def test_get_parameters(): assert len(constant_params) == 2 assert all(isinstance(param, Constant) for param in constant_params) + trainable_params = parametrized.traverse_trainables(owner_tag="foo") + assert len(trainable_params) == 4 + assert all(isinstance(param, Trainable) for param in trainable_params.values()) + assert all(tag.startswith("foo") for tag in trainable_params) + assert all(tag.split("/")[1] in kwargs for tag in trainable_params) + + constant_params = parametrized.traverse_constants() + assert len(constant_params) == 2 + assert all(isinstance(param, Constant) for param in constant_params.values()) + assert all(tag.startswith("Parametrized") for tag in constant_params) + assert all(tag.split("/")[1] in kwargs for tag in constant_params) + def test_get_nested_parameters(): """Test that nested Parametrized objects (e.g. a circuit) return all the trainable @@ -137,3 +149,13 @@ def test_get_nested_parameters(): assert (s2.phi in trainables) and (bs.theta in trainables) assert (s2.r in constants) and (bs.phi in constants) + + trainables = circ.traverse_trainables() + constants = circ.traverse_constants("Device") + assert len(trainables) == 2 + assert len(constants) == 2 + assert all(tag.startswith("Circuit/_ops[") for tag in trainables) + assert all(tag.startswith("Device/_ops[") for tag in constants) + + assert (s2.phi in trainables.values()) and (bs.theta in trainables.values()) + assert (s2.r in constants.values()) and (bs.phi in constants.values()) From 6349d3019dbca75a2ae5c90844105b15417b95af Mon Sep 17 00:00:00 2001 From: ziofil Date: Thu, 13 Apr 2023 09:30:44 -0700 Subject: [PATCH 46/53] Modular fock strategies (#235) **Context:** Modularized Fock strategies allow for a simple way to construct strategies out of building blocks. **Description of the Change:** - All new lattice module. - some fixes in tests - bargmann method for State --- .github/CHANGELOG.md | 55 +++- mrmustard/lab/abstract/state.py | 51 +++- mrmustard/math/lattice/__init__.py | 0 mrmustard/math/lattice/neighbors.py | 74 +++++ mrmustard/math/lattice/paths.py | 81 ++++++ mrmustard/math/lattice/pivots.py | 55 ++++ mrmustard/math/lattice/steps.py | 223 +++++++++++++++ mrmustard/math/lattice/strategies.py | 266 ++++++++++++++++++ mrmustard/math/math_interface.py | 10 +- mrmustard/math/tensorflow.py | 86 ++++-- mrmustard/physics/bargmann.py | 5 +- mrmustard/physics/fock.py | 32 ++- tests/test_lab/test_state.py | 49 ++++ tests/test_math/test_lattice.py | 29 ++ tests/test_math/test_special.py | 8 +- tests/test_physics/test_bargmann.py | 20 ++ tests/test_physics/test_fock/test_fock.py | 45 ++- .../test_gaussian/test_symplectics.py | 32 +-- tests/test_training/test_callbacks.py | 7 +- tests/test_training/test_opt.py | 18 +- 20 files changed, 1034 insertions(+), 112 deletions(-) create mode 100644 mrmustard/math/lattice/__init__.py create mode 100644 mrmustard/math/lattice/neighbors.py create mode 100644 mrmustard/math/lattice/paths.py create mode 100644 mrmustard/math/lattice/pivots.py create mode 100644 mrmustard/math/lattice/steps.py create mode 100644 mrmustard/math/lattice/strategies.py create mode 100644 tests/test_lab/test_state.py create mode 100644 tests/test_math/test_lattice.py create mode 100644 tests/test_physics/test_bargmann.py diff --git a/.github/CHANGELOG.md b/.github/CHANGELOG.md index 2f73a28b3..f438ba33f 100644 --- a/.github/CHANGELOG.md +++ b/.github/CHANGELOG.md @@ -35,10 +35,62 @@ # Or, in command line: `tensorboard --logdir={tb_cb.logdir}` and open link in browser. ``` +* Gaussian states support a `bargmann` method for returning the bargmann representation. + [(#235)](https://github.com/XanaduAI/MrMustard/pull/235) + +* The `ket` method of `State` now supports new keyword arguments `max_prob` and `max_photons`. + Use them to speed-up the filling of a ket array up to a certain probability or *total* photon number. + [(#235)](https://github.com/XanaduAI/MrMustard/pull/235) + + ```python + from mrmustard.lab import Gaussian + + # Fills the ket array up to 99% probability or up to the |0,3>, |1,2>, |2,1>, |3,0> subspace, whichever is reached first. + # The array has the autocutoff shape, unless the cutoffs are specified explicitly. + ket = Gaussian(2).ket(max_prob=0.99, max_photons=3) + ``` + ### Breaking Changes * The previous `callback` argument to `Optimizer.minimize` is now `callbacks` since we can now pass multiple callbacks to it. + [(#219)](https://github.com/XanaduAI/MrMustard/pull/219) + +* The `opt_history` attribute of `Optimizer` does not have the placeholder at the beginning anymore. + [(#235)](https://github.com/XanaduAI/MrMustard/pull/235) + +### Improvements + +* The math module now has a submodule `lattice` for constructing recurrence relation strategies in the Fock lattice. + There are a few predefined strategies in `mrmustard.math.lattice.strategies`. + [(#235)](https://github.com/XanaduAI/MrMustard/pull/235) + +* Gradients in the Fock lattice are now computed using the vector-jacobian product. + This saves a lot of memory and speeds up the optimization process by roughly 4x. + [(#235)](https://github.com/XanaduAI/MrMustard/pull/235) + +* Tests of the compact_fock module now use hypothesis. + [(#235)](https://github.com/XanaduAI/MrMustard/pull/235) + +### Bug fixes + +* Fixed a bug that would make two progress bars appear during an optimization + [(#235)](https://github.com/XanaduAI/MrMustard/pull/235) + +### Bug fixes + +### Documentation + +### Contributors +[Filippo Miatto](https://github.com/ziofil), [Zeyue Niu](https://github.com/zeyueN) + +--- + +# Release 0.4.1 + +### New features + +### Breaking changes ### Improvements @@ -59,9 +111,6 @@ ### Contributors [Filippo Miatto](https://github.com/ziofil), [Sebastian Duque Mesa](https://github.com/sduquemesa) -This release contains contributions from (in alphabetical order): -[Zeyue Niu](https://github.com/zeyueN) - --- # Release 0.4.0 (current release) diff --git a/mrmustard/lab/abstract/state.py b/mrmustard/lab/abstract/state.py index cc2945e30..99549f42a 100644 --- a/mrmustard/lab/abstract/state.py +++ b/mrmustard/lab/abstract/state.py @@ -31,12 +31,14 @@ from mrmustard import settings from mrmustard.math import Math -from mrmustard.physics import fock, gaussian +from mrmustard.physics import bargmann, fock, gaussian from mrmustard.typing import ( + ComplexMatrix, + ComplexTensor, + ComplexVector, RealMatrix, - RealVector, RealTensor, - ComplexTensor, + RealVector, ) from mrmustard.utils import graphics @@ -47,7 +49,7 @@ # pylint: disable=too-many-instance-attributes -class State: +class State: # pylint: disable=too-many-public-methods r"""Base class for quantum states.""" def __init__( @@ -237,12 +239,21 @@ def probability(self) -> float: return norm**2 return norm - def ket(self, cutoffs: List[int] = None) -> Optional[ComplexTensor]: + def ket( + self, + cutoffs: List[int] = None, + max_prob: float = 1.0, + max_photons: int = None, + ) -> Optional[ComplexTensor]: r"""Returns the ket of the state in Fock representation or ``None`` if the state is mixed. Args: cutoffs List[int or None]: The cutoff dimensions for each mode. If a mode cutoff is ``None``, it's guessed automatically. + max_prob (float): The maximum probability of the state. Defaults to 1.0. + (used to stop the calculation of the amplitudes early) + max_photons (int): The maximum number of photons in the state, summing over all modes + (used to stop the calculation of the amplitudes early) Returns: Tensor: the ket @@ -255,9 +266,15 @@ def ket(self, cutoffs: List[int] = None) -> Optional[ComplexTensor]: else: cutoffs = [c if c is not None else self.cutoffs[i] for i, c in enumerate(cutoffs)] + # TODO: shouldn't we check if trainable instead? that's when we want to recompute fock if self.is_gaussian: self._ket = fock.wigner_to_fock_state( - self.cov, self.means, shape=cutoffs, return_dm=False + self.cov, + self.means, + shape=cutoffs, + return_dm=False, + max_prob=max_prob, + max_photons=max_photons, ) else: # only fock representation is available if self._ket is None: @@ -498,6 +515,17 @@ def __getitem__(self, item) -> State: self._modes = item return self + def bargmann(self) -> Optional[tuple[ComplexMatrix, ComplexVector, complex]]: + r"""Returns the Bargmann representation of the state.""" + if self.is_gaussian: + if self.is_pure: + A, B, C = bargmann.wigner_to_bargmann_psi(self.cov, self.means) + else: + A, B, C = bargmann.wigner_to_bargmann_rho(self.cov, self.means) + else: + return None + return A, B, C + def get_modes(self, item) -> State: r"""Returns the state on the given modes.""" if isinstance(item, int): @@ -524,7 +552,7 @@ def get_modes(self, item) -> State: return State(dm=fock_partitioned, modes=item) # TODO: refactor - def __eq__(self, other) -> bool: + def __eq__(self, other) -> bool: # pylint: disable=too-many-return-statements r"""Returns whether the states are equal.""" if self.num_modes != other.num_modes: return False @@ -576,7 +604,9 @@ def __rmul__(self, other): warnings.warn( "scalar multiplication forces conversion to fock representation", UserWarning ) - return self.fock # trigger creation of fock representation + if self.is_pure: + return State(ket=self.ket() * other) + return State(dm=self.dm() * other) if self._dm is not None: return State(dm=self.dm() * other, modes=self.modes) if self._ket is not None: @@ -590,8 +620,9 @@ def __truediv__(self, other): """ if self.is_gaussian: warnings.warn("scalar division forces conversion to fock representation", UserWarning) - return self.fock - + if self.is_pure: + return State(ket=self.ket() / other) + return State(dm=self.dm() / other) if self._dm is not None: return State(dm=self.dm() / other, modes=self.modes) if self._ket is not None: diff --git a/mrmustard/math/lattice/__init__.py b/mrmustard/math/lattice/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/mrmustard/math/lattice/neighbors.py b/mrmustard/math/lattice/neighbors.py new file mode 100644 index 000000000..b9fe131dc --- /dev/null +++ b/mrmustard/math/lattice/neighbors.py @@ -0,0 +1,74 @@ +# 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. +# + +"""neighbours functions""" + +from typing import Iterator + +from numba import njit +from numba.cpython.unsafe.tuple import tuple_setitem + +################################################################################# +## All neighbours means all the indices that differ from the given pivot by ±1 ## +################################################################################# + + +@njit +def all_neighbors(pivot: tuple[int, ...]) -> Iterator[tuple[int, tuple[int, ...]]]: + r"""yields the indices of all the neighbours of the given index.""" + for j in range(len(pivot)): # pylint: disable=consider-using-enumerate + yield j, tuple_setitem(pivot, j, pivot[j] - 1) + yield j, tuple_setitem(pivot, j, pivot[j] + 1) + + +#################################################################################### +## Lower neighbours means all the indices that differ from the given index by -1 ## +#################################################################################### + + +@njit +def lower_neighbors(pivot: tuple[int, ...]) -> Iterator[tuple[int, tuple[int, ...]]]: + r"""yields the indices of the lower neighbours of the given index.""" + for j in range(len(pivot)): # pylint: disable=consider-using-enumerate + yield j, tuple_setitem(pivot, j, pivot[j] - 1) + + +#################################################################################### +## Upper neighbours means all the indices that differ from the given index by +1 ## +#################################################################################### + + +@njit +def upper_neighbors(pivot: tuple[int, ...]) -> Iterator[tuple[int, tuple[int, ...]]]: + r"""yields the indices of the lower neighbours of the given index.""" + for j in range(len(pivot)): + yield j, tuple_setitem(pivot, j, pivot[j] + 1) + + +#################################################################################################### +## bitstring neighbours are indices that differ from the given index by ±1 according to a bitstring +#################################################################################################### + + +@njit +def bitstring_neighbors( + pivot: tuple[int, ...], bitstring: tuple[int, ...] +) -> Iterator[tuple[int, tuple[int, ...]]]: + r"yields the indices of the bitstring neighbours of the given index" + for i, b in enumerate(bitstring): + if b: # b == 1 -> lower + yield i, tuple_setitem(pivot, i, pivot[i] - 1) + else: # b == 0 -> upper + yield i, tuple_setitem(pivot, i, pivot[i] + 1) diff --git a/mrmustard/math/lattice/paths.py b/mrmustard/math/lattice/paths.py new file mode 100644 index 000000000..4f3169a30 --- /dev/null +++ b/mrmustard/math/lattice/paths.py @@ -0,0 +1,81 @@ +# 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. + +"""Path functions""" + +from numba import njit, typed, typeof, types +from numba.cpython.unsafe.tuple import tuple_setitem + +BINOMIAL_PATHS_PYTHON = {} + + +@njit +def _binomial_subspace_basis( + cutoffs: tuple[int, ...], + weight: int, + mode: int, + basis_element: tuple[int, ...], + basis: typed.List[tuple[int, ...]], +): + r"""Step of the recursive function to generate all indices + of a tensor with equal weight. + If cutoffs is an empty tuple, the the basis element is appended to the list. + Otherwise it loops over the values of the given mode, and it calls itself recursively + to construct the rest of the basis elements. + + Arguments: + cutoffs (tuple[int, ...]): the cutoffs of the tensor + weight (int): the weight of the subspace + mode (int): the mode to loop over + basis_element (tuple[int, ...]): the current basis element to construct + basis (list[tuple[int, ...]]): the list of basis elements to eventually append to + """ + if mode == len(cutoffs): + if weight == 0: # ran out of photons to distribute + basis.append(basis_element) + return + + for photons in range(cutoffs[mode]): # could be prange? + if weight - photons >= 0: + basis_element = tuple_setitem(basis_element, mode, photons) + _binomial_subspace_basis(cutoffs, weight - photons, mode + 1, basis_element, basis) + + +@njit +def binomial_subspace_basis(cutoffs: tuple[int, ...], weight: int): + r"""Returns all indices of a tensor with given weight. + + Arguments: + cutoffs (tuple[int, ...]): the cutoffs of the tensor + weight (int): the weight of the subspace + + Returns: + list[tuple[int, ...]]: the list of basis elements of the subspace + """ + basis = typed.List( + [cutoffs] + ) # this is just so that numba can infer the type, then we remove it + _binomial_subspace_basis(cutoffs, weight, 0, cutoffs, basis) + return basis[1:] # remove the dummy element + + +def BINOMIAL_PATHS_NUMBA_n(modes): + r"Creates a numba dictionary to store the paths and effectively cache them." + return typed.Dict.empty( + key_type=typeof(((0,) * modes, 0)), + value_type=types.ListType(typeof((0,) * modes)), + ) + + +BINOMIAL_PATHS_NUMBA = {modes: BINOMIAL_PATHS_NUMBA_n(modes) for modes in range(1, 100)} diff --git a/mrmustard/math/lattice/pivots.py b/mrmustard/math/lattice/pivots.py new file mode 100644 index 000000000..bc564f614 --- /dev/null +++ b/mrmustard/math/lattice/pivots.py @@ -0,0 +1,55 @@ +# 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 numba import njit +from numba.cpython.unsafe.tuple import tuple_setitem + + +@njit +def first_available_pivot(index: tuple[int, ...]) -> tuple[int, tuple[int, ...]]: + r"""returns the first available pivot for the given index. A pivot is a nearest neighbor + of the index. Here we pick the first available pivot. + + Arguments: + index: the index to get the first available pivot of. + + Returns: + the index that was decremented and the pivot + """ + for i, v in enumerate(index): + if v > 0: + return i, tuple_setitem(index, i, v - 1) + raise ValueError("Index is zero") + + +@njit +def smallest_pivot(index: tuple[int, ...]) -> tuple[int, tuple[int, ...]]: + r"""returns the pivot closest to a zero index. A pivot is a nearest neighbor + of the index. Here we pick the pivot with the smallest non-zero element. + + Arguments: + index: the index to get the smallest pivot of. + + Returns: + (int, tuple) the index of the element that was decremented and the pivot + """ + min_ = 2**64 - 1 + for i, v in enumerate(index): + if 0 < v < min_: + min_ = v + min_i = i + if min_ == 2**64 - 1: + raise ValueError("Index is zero") + return min_i, tuple_setitem(index, min_i, min_ - 1) diff --git a/mrmustard/math/lattice/steps.py b/mrmustard/math/lattice/steps.py new file mode 100644 index 000000000..573e04e5a --- /dev/null +++ b/mrmustard/math/lattice/steps.py @@ -0,0 +1,223 @@ +# 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. + + +# Recurrencies for Fock-Bargmann amplitudes put together a strategy for +# enumerating the indices in a specific order and functions for calculating +# which neighbours to use in the calculation of the amplitude at a given index. +# In summary, they return the value of the amplitude at the given index by following +# a recipe made of two parts. The function to recompute A and b is determined by +# which neighbours are used. + +"""Fock-Bargmann recurrence relation steps for Gaussian states.""" + +import numpy as np +from numba import njit, prange, types +from numba.cpython.unsafe.tuple import tuple_setitem + +from mrmustard.math.lattice.neighbors import lower_neighbors +from mrmustard.math.lattice.pivots import first_available_pivot +from mrmustard.typing import ComplexMatrix, ComplexTensor, ComplexVector + +SQRT = np.sqrt(np.arange(100000)) + + +@njit +def vanilla_step( + G: ComplexTensor, + A: ComplexMatrix, + b: ComplexVector, + index: tuple[int, ...], +) -> complex: + r"""Fock-Bargmann recurrence relation step, vanilla version. + This function returns the amplitude of the Gaussian tensor G + at G[index]. It does not modify G. + The necessary pivot and neighbours must have already been computed, + as this step will read those values from G. + + Args: + G (array or dict): fock amplitudes data store that supports getitem[tuple[int, ...]] + A (array): A matrix of the Fock-Bargmann representation + b (array): B vector of the Fock-Bargmann representation + index (Sequence): index of the amplitude to calculate + Returns: + complex: the value of the amplitude at the given index + """ + # get pivot + i, pivot = first_available_pivot(index) + + # pivot contribution + value_at_index = b[i] * G[pivot] + + # neighbors contribution + for j, neighbor in lower_neighbors(pivot): + value_at_index += A[i, j] * SQRT[pivot[j]] * G[neighbor] + + return value_at_index / SQRT[index[i]] + + +@njit +def vanilla_step_jacobian( + G: ComplexTensor, + A: ComplexMatrix, + b: ComplexVector, + index: tuple[int, ...], + dGdA: ComplexTensor, + dGdB: ComplexTensor, +) -> tuple[ComplexTensor, ComplexTensor]: + r"""Jacobian contribution of a single Fock-Bargmann recurrence relation step, vanilla version. + It updates the dGdB and dGdA tensors at the given index. + + Args: + G (array or dict): fully computed store that supports __getitem__(index: tuple[int, ...]) + A (array): A matrix of the Fock-Bargmann representation + b (array): B vector of the Fock-Bargmann representation + c (complex): vacuum amplitude + index (Sequence): index at which to compute the jacobian + dGdB (array): gradient of G with respect to b (partially computed) + dGdA (array): gradient of G with respect to A (partially computed) + Returns: + tuple[array, array]: the dGdB and dGdA tensors updated at the given index + """ + # index -> pivot + i, pivot = first_available_pivot(index) + + # pivot contribution + dGdB[index] += b[i] * dGdB[pivot] / SQRT[index[i]] + dGdB[index + (i,)] += G[pivot] / SQRT[index[i]] + dGdA[index] += b[i] * dGdA[pivot] / SQRT[index[i]] + + # neighbors contribution + for j, neighbor in lower_neighbors(pivot): + dGdB[index] += A[i, j] * dGdB[neighbor] * SQRT[pivot[j]] / SQRT[index[i]] + dGdA[index] += A[i, j] * dGdA[neighbor] * SQRT[pivot[j]] / SQRT[index[i]] + dGdA[index + (i, j)] += G[neighbor] * SQRT[pivot[j]] / SQRT[index[i]] + + return dGdA, dGdB + + +@njit +def vanilla_step_grad( + G: ComplexTensor, + index: tuple[int, ...], + dA: ComplexMatrix, + db: ComplexVector, +) -> tuple[ComplexMatrix, ComplexVector]: + r"""Gradient with respect to A and b of a single Fock-Bargmann recurrence relation step, + vanilla version. dA and db can be used to update the dGdB and dGdA tensors at `index`, + or as part of the contraction with the gradient of the Gaussian tensor G. + Note that the gradient depends only on G and not on the values of A and b. + + Args: + G (array or dict): fully computed store that supports __getitem__(index: tuple[int, ...]) + index (Sequence): index of the amplitude to calculate the gradient of + dA (array): empty array to store the gradient of G[index] with respect to A + db (array): empty array to store the gradient of G[index] with respect to B + Returns: + tuple[array, array]: the updated dGdB and dGdA tensors + """ + for i in range(len(db)): # pylint: disable=consider-using-enumerate + pivot_i = tuple_setitem(index, i, index[i] - 1) + db[i] = SQRT[index[i]] * G[pivot_i] + dA[i, i] = 0.5 * SQRT[index[i] * pivot_i[i]] * G[tuple_setitem(pivot_i, i, pivot_i[i] - 1)] + for j in range(i + 1, len(db)): + dA[i, j] = SQRT[index[i] * pivot_i[j]] * G[tuple_setitem(pivot_i, j, pivot_i[j] - 1)] + + return dA, db + + +@njit +def vanilla_step_dict( + data: types.DictType, A: ComplexMatrix, b: ComplexVector, index: tuple[int, ...] +) -> complex: + r"""Fock-Bargmann recurrence relation step, vanilla version with numba dict. + This function calculates the index `index` of the Gaussian tensor `G`. + The appropriate pivot and neighbours must exist. + + Args: + data: dict(tuple[int,...], complex): fock amplitudes numba dict + A (array): A matrix of the Fock-Bargmann representation + b (array): b vector of the Fock-Bargmann representation + index (Sequence): index of the amplitude to calculate + Returns: + complex: the value of the amplitude at the given index + """ + # index -> pivot + i, pivot = first_available_pivot(index) + + # calculate value at index: pivot contribution + denom = SQRT[pivot[i] + 1] + value_at_index = b[i] / denom * data[pivot] + + # neighbors contribution + for j, neighbor in lower_neighbors(pivot): + value_at_index += A[i, j] / denom * SQRT[pivot[j]] * data.get(neighbor, 0.0 + 0.0j) + + return value_at_index + + +@njit +def binomial_step( + G: ComplexTensor, A: ComplexMatrix, b: ComplexVector, subspace_indices: list[tuple[int, ...]] +) -> tuple[ComplexTensor, float]: + r"""Computes a whole subspace of the ``G`` tensor at the indices in + ``subspace_indices`` (a subspace is such that `sum(index) = const`). + It updates the tensor ``G``. + It returns the updated tensor and the probability of the subspace. + + Args: + G (np.ndarray): Tensor filled up to the previous subspace + A (np.ndarray): A matrix of the Fock-Bargmann representation + b (np.ndarray): B vector of the Fock-Bargmann representation + subspace_indices (list[tuple[int, ...]]): list of indices to be updated + + Returns: + tuple[np.ndarray, float]: updated tensor and probability of the subspace + """ + norm = 0.0 + + for i in prange(len(subspace_indices)): + value = vanilla_step(G, A, b, subspace_indices[i]) + G[subspace_indices[i]] = value + norm = norm + np.abs(value) ** 2 + + return G, norm + + +@njit +def binomial_step_dict( + G: types.DictType, A: ComplexMatrix, b: ComplexVector, subspace_indices: list[tuple[int, ...]] +) -> tuple[types.DictType, float]: + r"""Computes a whole subspace of the ``G`` dict at the indices in + ``subspace_indices`` (a subspace is such that `sum(index) = const`). + It updates the dict ``G``. It returns the updated G dict + and the total probability of the subspace. + + Args: + G (types.DictType): Dictionary filled up to the previous subspace + A (np.ndarray): A matrix of the Fock-Bargmann representation + b (np.ndarray): B vector of the Fock-Bargmann representation + subspace_indices (list[tuple[int, ...]]): list of indices to be updated + + Returns: + tuple[types.DictType, float]: updated dictionary and probability of the subspace + """ + prob = 0.0 + + for i in prange(len(subspace_indices)): + value = vanilla_step_dict(G, A, b, subspace_indices[i]) + G[subspace_indices[i]] = value + prob = prob + np.abs(value) ** 2 + + return G, prob diff --git a/mrmustard/math/lattice/strategies.py b/mrmustard/math/lattice/strategies.py new file mode 100644 index 000000000..6b90631e9 --- /dev/null +++ b/mrmustard/math/lattice/strategies.py @@ -0,0 +1,266 @@ +# 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. + +"""Fock-Bargmann strategies.""" + +from typing import Optional + +import numpy as np +from numba import njit, typed, types + +from mrmustard.math.lattice import paths, steps +from mrmustard.typing import ComplexMatrix, ComplexTensor, ComplexVector, IntVector + + +@njit +def vanilla(shape: tuple[int, ...], A, b, c) -> ComplexTensor: + r"""Vanilla Fock-Bargmann strategy. Fills the tensor by iterating over all indices + in ndindex order. + + Args: + shape (tuple[int, ...]): shape of the output tensor + A (np.ndarray): A matrix of the Fock-Bargmann representation + b (np.ndarray): B vector of the Fock-Bargmann representation + c (complex): vacuum amplitude + + Returns: + np.ndarray: Fock representation of the Gaussian tensor with shape ``shape`` + """ + + # init output tensor + G = np.zeros(shape, dtype=np.complex128) + + # initialize path iterator + path = np.ndindex(shape) + + # write vacuum amplitude + G[next(path)] = c + + # iterate over the rest of the indices + for index in path: + G[index] = steps.vanilla_step(G, A, b, index) + return G + + +@njit +def vanilla_jacobian(G, A, b, c) -> tuple[ComplexTensor, ComplexTensor, ComplexTensor]: + r"""Vanilla Fock-Bargmann strategy gradient. Returns dG/dA, dG/db, dG/dc. + Notice that G is a holomorphic function of A, b, c. This means that there is only + one gradient to care about for each parameter (i.e. not dG/dA.conj() etc). + """ + + # init output tensors + dGdA = np.zeros(G.shape + A.shape, dtype=np.complex128) + dGdb = np.zeros(G.shape + b.shape, dtype=np.complex128) + dGdc = G / c + + # initialize path iterator + path = paths.ndindex_path(G.shape) + + # skip first index + next(path) + + # iterate over the rest of the indices + for index in path: + dGdA, dGdb = steps.vanilla_step_jacobian(G, A, b, index, dGdA, dGdb) + + return dGdA, dGdb, dGdc + + +@njit +def vanilla_vjp(G, c, dLdG) -> tuple[ComplexMatrix, ComplexVector, complex]: + r"""Vanilla Fock-Bargmann strategy gradient. Returns dL/dA, dL/db, dL/dc. + + Args: + G (np.ndarray): Tensor result of the forward pass + c (complex): vacuum amplitude + dLdG (np.ndarray): gradient of the loss with respect to the output tensor + + Returns: + tuple[np.ndarray, np.ndarray, complex]: dL/dA, dL/db, dL/dc + """ + D = G.ndim + + # init gradients + dA = np.zeros((D, D), dtype=np.complex128) # component of dL/dA + db = np.zeros(D, dtype=np.complex128) # component of dL/db + dLdA = np.zeros_like(dA) + dLdb = np.zeros_like(db) + + # initialize path iterator + path = np.ndindex(G.shape) + + # skip first index + next(path) + + # iterate over the rest of the indices + for index in path: + dA, db = steps.vanilla_step_grad(G, index, dA, db) + dLdA += dA * dLdG[index] + dLdb += db * dLdG[index] + + dLdc = np.sum(G * dLdG) / c + + return dLdA, dLdb, dLdc + + +def binomial( + local_cutoffs: tuple[int, ...], + A: ComplexMatrix, + b: ComplexVector, + c: complex, + max_l2: float, + global_cutoff: int, +) -> ComplexTensor: + r"""Binomial strategy (fill ket by weight), python version with numba function/loop. + + Args: + local_cutoffs (tuple[int, ...]): local cutoffs of the tensor (used at least as shape) + A (np.ndarray): A matrix of the Fock-Bargmann representation + b (np.ndarray): B vector of the Fock-Bargmann representation + c (complex): vacuum amplitude + max_l2 (float): max L2 norm. If reached, the computation is stopped early. + global_cutoff (Optional[int]): global cutoff (max total photon number considered + 1). + + Returns: + G, prob (np.ndarray, float): Fock representation of the Gaussian tensor with shape ``shape`` and L2 norm + """ + # init output tensor + G = np.zeros(local_cutoffs, dtype=np.complex128) + + # write vacuum amplitude + G.flat[0] = c + norm = np.abs(c) ** 2 + + # iterate over subspaces by weight and stop if norm is large enough. Caches indices. + for photons in range(1, global_cutoff): + try: + indices = paths.BINOMIAL_PATHS_PYTHON[(local_cutoffs, photons)] + except KeyError: + indices = paths.binomial_subspace_basis(local_cutoffs, photons) + paths.BINOMIAL_PATHS_PYTHON[(local_cutoffs, photons)] = indices + G, subspace_norm = steps.binomial_step(G, A, b, indices) # numba parallelized function + norm += subspace_norm + try: + if norm > max_l2: + break + except TypeError: # max_l2 is None + pass + return G, norm + + +def binomial_dict( + local_cutoffs: tuple[int, ...], + A: ComplexMatrix, + b: ComplexVector, + c: complex, + max_prob: Optional[float] = None, + global_cutoff: Optional[int] = None, +) -> dict[tuple[int, ...], complex]: + r"""Factorial speedup strategy (fill ket by weight), python version with numba function/loop. + Uses a dictionary to store the output. + + Args: + local_cutoffs (tuple[int, ...]): local cutoffs of the tensor (used at least as shape) + A (np.ndarray): A matrix of the Fock-Bargmann representation + b (np.ndarray): B vector of the Fock-Bargmann representation + c (complex): vacuum amplitude + max_prob (float): max L2 norm. If reached, the computation is stopped early. + global_cutoff (Optional[int]): global cutoff (max total photon number considered). + If not given it is calculated from the local cutoffs. + + Returns: + dict[tuple[int, ...], complex]: Fock representation of the Gaussian tensor. + """ + if global_cutoff is None: + global_cutoff = sum(local_cutoffs) - len(local_cutoffs) + + # init numba output dict + G = typed.Dict.empty( + key_type=types.UniTuple(types.int64, len(local_cutoffs)), + value_type=types.complex128, + ) + + # write vacuum amplitude + G[(0,) * len(local_cutoffs)] = c + prob = np.abs(c) ** 2 + + # iterate over subspaces by weight and stop if norm is large enough. Caches indices. + for photons in range(1, global_cutoff): + try: + indices = paths.BINOMIAL_PATHS_PYTHON[(local_cutoffs, photons)] + except KeyError: + indices = paths.binomial_subspace_basis(local_cutoffs, photons) + paths.BINOMIAL_PATHS_PYTHON[(local_cutoffs, photons)] = indices + G, prob_subspace = steps.binomial_step_dict(G, A, b, indices) # numba parallelized function + prob += prob_subspace + try: + if prob > max_prob: + break + except TypeError: + pass + return G + + +@njit +def binomial_numba( + local_cutoffs: tuple[int, ...], + A: ComplexMatrix, + b: ComplexVector, + c: complex, + FP: dict[tuple[tuple[int, ...], int], list[tuple[int, ...]]], + max_prob: float = 0.999, + global_cutoff: Optional[int] = None, +) -> ComplexTensor: + r"""Binomial strategy (fill by weight), fully numba version.""" + if global_cutoff is None: + global_cutoff = sum(local_cutoffs) - len(local_cutoffs) + + # init output tensor + G = np.zeros(local_cutoffs, dtype=np.complex128) + + # write vacuum amplitude + G.flat[0] = c + prob = np.abs(c) ** 2 + + # iterate over all other indices in parallel and stop if norm is large enough + for photons in range(1, global_cutoff): + try: + indices = FP[(local_cutoffs, photons)] + except Exception: # pylint: disable=broad-except + indices = paths.binomial_subspace_basis(local_cutoffs, photons) + FP[(local_cutoffs, photons)] = indices + G, prob_subspace = steps.binomial_step(G, A, b, indices) + prob += prob_subspace + if prob > max_prob: + break + return G + + +@njit +def wormhole(shape: IntVector) -> IntVector: + r"wormhole strategy, not implemented yet" + raise NotImplementedError("Wormhole strategy not implemented yet") + + +@njit +def diagonal(shape: IntVector) -> IntVector: + r"diagonal strategy, not implemented yet" + raise NotImplementedError("Diagonal strategy not implemented yet") + + +@njit +def dynamic_U(shape: IntVector) -> IntVector: + r"dynamic U strategy, not implemented yet" + raise NotImplementedError("Dynamic strategy not implemented yet") diff --git a/mrmustard/math/math_interface.py b/mrmustard/math/math_interface.py index fb074c009..986c9171b 100644 --- a/mrmustard/math/math_interface.py +++ b/mrmustard/math/math_interface.py @@ -15,19 +15,21 @@ """This module contains the :class:`Math` interface that every backend has to implement.""" from abc import ABC, abstractmethod -from typing import Any, Callable, Dict, List, Optional, Sequence, Tuple from functools import lru_cache from itertools import product +from typing import Any, Callable, Dict, List, Optional, Sequence, Tuple + import numpy as np from scipy.special import binom -from scipy.stats import unitary_group, ortho_group +from scipy.stats import ortho_group, unitary_group + from mrmustard import settings from mrmustard.typing import ( - Tensor, Matrix, Scalar, - Vector, + Tensor, Trainable, + Vector, ) diff --git a/mrmustard/math/tensorflow.py b/mrmustard/math/tensorflow.py index 99caf41cd..1d07473f0 100644 --- a/mrmustard/math/tensorflow.py +++ b/mrmustard/math/tensorflow.py @@ -14,25 +14,23 @@ """This module contains the Tensorflow implementation of the :class:`Math` interface.""" -from typing import Callable, List, Sequence, Tuple, Union, Optional +from typing import Callable, List, Optional, Sequence, Tuple, Union import numpy as np import tensorflow as tf import tensorflow_probability as tfp -from thewalrus import hermite_multidimensional, grad_hermite_multidimensional - +from mrmustard import settings +from mrmustard.math.autocast import Autocast +from mrmustard.math.lattice import strategies from mrmustard.math.numba.compactFock_inputValidation import ( - hermite_multidimensional_diagonal, + grad_hermite_multidimensional_1leftoverMode, grad_hermite_multidimensional_diagonal, -) -from mrmustard.math.numba.compactFock_inputValidation import ( hermite_multidimensional_1leftoverMode, - grad_hermite_multidimensional_1leftoverMode, + hermite_multidimensional_diagonal, ) - -from mrmustard.math.autocast import Autocast from mrmustard.typing import Tensor, Trainable + from .math_interface import MathInterface @@ -364,10 +362,11 @@ def value_and_gradients( @tf.custom_gradient def hermite_renormalized( self, A: tf.Tensor, B: tf.Tensor, C: tf.Tensor, shape: Tuple[int] - ) -> tf.Tensor: # TODO this is not ready + ) -> tf.Tensor: r"""Renormalized multidimensional Hermite polynomial given by the "exponential" Taylor - series of :math:`exp(C + Bx - Ax^2)` at zero, where the series has :math:`sqrt(n!)` at the - denominator rather than :math:`n!`. Note the minus sign in front of ``A``. + series of :math:`exp(C + Bx + 1/2*Ax^2)` at zero, where the series has :math:`sqrt(n!)` + at the denominator rather than :math:`n!`. It computes all the amplitudes within the + tensor of given shape. Args: A: The A matrix. @@ -378,31 +377,62 @@ def hermite_renormalized( Returns: The renormalized Hermite polynomial of given shape. """ - if isinstance(shape, List) and len(shape) == 1: - shape = shape[0] + _A, _B, _C = self.asnumpy(A), self.asnumpy(B), self.asnumpy(C) + G = strategies.vanilla(tuple(shape), _A, _B, _C) + + def grad(dLdGconj): + dLdA, dLdB, dLdC = strategies.vanilla_vjp(G, _C, np.conj(dLdGconj)) + return np.conj(dLdA), np.conj(dLdB), np.conj(dLdC) - poly = hermite_multidimensional( - self.asnumpy(A), shape, self.asnumpy(B), self.asnumpy(C), True, True, True + return G, grad + + @tf.custom_gradient + def hermite_renormalized_binomial( + self, + A: tf.Tensor, + B: tf.Tensor, + C: tf.Tensor, + shape: Tuple[int], + max_l2: Optional[float], + global_cutoff: Optional[int], + ) -> tf.Tensor: + r"""Renormalized multidimensional Hermite polynomial given by the "exponential" Taylor + series of :math:`exp(C + Bx + 1/2*Ax^2)` at zero, where the series has :math:`sqrt(n!)` + at the denominator rather than :math:`n!`. The computation fills a tensor of given shape + up to a given L2 norm or global cutoff, whichever applies first. The max_l2 value, if + not provided, is set to the default value of the AUTOCUTOFF_PROBABILITY setting. + + Args: + A: The A matrix. + B: The B vector. + C: The C scalar. + shape: The shape of the final tensor (local cutoffs). + max_l2 (float): The maximum squared L2 norm of the tensor. + global_cutoff (optional int): The global cutoff. + + Returns: + The renormalized Hermite polynomial of given shape. + """ + _A, _B, _C = self.asnumpy(A), self.asnumpy(B), self.asnumpy(C) + G, _ = strategies.binomial( + tuple(shape), + _A, + _B, + _C, + max_l2=max_l2 or settings.AUTOCUTOFF_PROBABILITY, + global_cutoff=global_cutoff or sum(shape) - len(shape) + 1, ) - def grad(dLdpoly): - dpoly_dC, dpoly_dA, dpoly_dB = tf.numpy_function( - grad_hermite_multidimensional, [poly, A, B, C], [poly.dtype] * 3 - ) - ax = tuple(range(dLdpoly.ndim)) - dLdA = self.sum(dLdpoly[..., None, None] * self.conj(dpoly_dA), axes=ax) - dLdB = self.sum(dLdpoly[..., None] * self.conj(dpoly_dB), axes=ax) - dLdC = self.sum(dLdpoly * self.conj(dpoly_dC), axes=ax) - return dLdA, dLdB, dLdC + def grad(dLdGconj): + dLdA, dLdB, dLdC = strategies.vanilla_vjp(G, _C, np.conj(dLdGconj)) + return np.conj(dLdA), np.conj(dLdB), np.conj(dLdC) - return poly, grad + return G, grad def reorder_AB_bargmann(self, A: tf.Tensor, B: tf.Tensor) -> Tuple[tf.Tensor, tf.Tensor]: r"""In mrmustard.math.numba.compactFock~ dimensions of the Fock representation are ordered like [mode0,mode0,mode1,mode1,...] while in mrmustard.physics.bargmann the ordering is [mode0,mode1,...,mode0,mode1,...]. Here we reorder A and B. - Moreover, the recurrence relation in mrmustard.math.numba.compactFock~ is defined such that A = -A compared to mrmustard.physics.bargmann. """ - A = -A ordering = np.arange(2 * A.shape[0] // 2).reshape(2, -1).T.flatten() A = tf.gather(A, ordering, axis=1) A = tf.gather(A, ordering) diff --git a/mrmustard/physics/bargmann.py b/mrmustard/physics/bargmann.py index 43b7738cb..f9513fe8f 100644 --- a/mrmustard/physics/bargmann.py +++ b/mrmustard/physics/bargmann.py @@ -18,9 +18,10 @@ This module contains functions for transforming to the Bargmann representation. """ import numpy as np -from mrmustard.physics.husimi import wigner_to_husimi, pq_to_aadag + from mrmustard import settings from mrmustard.math import Math +from mrmustard.physics.husimi import pq_to_aadag, wigner_to_husimi math = Math() @@ -51,10 +52,10 @@ def wigner_to_bargmann_rho(cov, means): here we define it as `A = [[A_11, A_10], [A_01, A_00]]`. For `B` we have `B = [B_0, B_1] -> B = [B_1, B_0]`. """ N = cov.shape[-1] // 2 - Q, beta = wigner_to_husimi(cov, means) A = math.matmul( cayley(pq_to_aadag(cov), c=0.5), math.Xmat(N) ) # X on the right, so the index order will be rho_{left,right}: + Q, beta = wigner_to_husimi(cov, means) B = math.solve(Q, beta) # no conjugate, so that the index order will be rho_{left,right} C = math.exp(-0.5 * math.sum(math.conj(beta) * B)) / math.sqrt(math.det(Q)) return A, B, C diff --git a/mrmustard/physics/fock.py b/mrmustard/physics/fock.py index 3eaa6f1c6..a6a51c693 100644 --- a/mrmustard/physics/fock.py +++ b/mrmustard/physics/fock.py @@ -93,21 +93,27 @@ def wigner_to_fock_state( cov: Matrix, means: Vector, shape: Sequence[int], + max_prob: float = 1.0, + max_photons: Optional[int] = None, return_dm: bool = True, ) -> Tensor: r"""Returns the Fock representation of a Gaussian state. Use with caution: if the cov matrix is that of a mixed state, setting return_dm to False will produce nonsense. + If return_dm=False, we can apply max_prob and max_photons to stop the + computation of the Fock representation early, when those conditions are met. * If the state is pure it can return the state vector (ket) or the density matrix. - The index order is going to be ket_i where i is the only multimode index. + The index ordering is going to be [i's] in ket_i * If the state is mixed it can return the density matrix. - The index order is going to be dm_ij where j is the right multimode index and i is the left one. + The index order is going to be [i's,j's] in dm_ij Args: cov: the Wigner covariance matrix means: the Wigner means vector shape: the shape of the tensor + max_prob: the maximum probability of a the state (applies only if the ket is returned) + max_photons: the maximum number of photons in the state (applies only if the ket is returned) return_dm: whether to return the density matrix (otherwise it returns the ket) Returns: @@ -115,10 +121,16 @@ def wigner_to_fock_state( """ if return_dm: A, B, C = wigner_to_bargmann_rho(cov, means) - return math.hermite_renormalized(-A, B, C, shape=shape) - else: + return math.hermite_renormalized(A, B, C, shape=shape) + else: # here we can apply max prob and max photons A, B, C = wigner_to_bargmann_psi(cov, means) - return math.hermite_renormalized(-A, B, C, shape=shape) + if max_photons is None: + max_photons = sum(shape) - len(shape) + if max_prob < 1.0 or max_photons < sum(shape) - len(shape): + return math.hermite_renormalized_binomial( + A, B, C, shape=shape, max_l2=max_prob, global_cutoff=max_photons + 1 + ) + return math.hermite_renormalized(A, B, C, shape=shape) def wigner_to_fock_U(X, d, shape): @@ -135,7 +147,7 @@ def wigner_to_fock_U(X, d, shape): Tensor: the fock representation of the unitary transformation """ A, B, C = wigner_to_bargmann_U(X, d) - return math.hermite_renormalized(-A, B, C, shape=shape) + return math.hermite_renormalized(A, B, C, shape=shape) def wigner_to_fock_Choi(X, Y, d, shape): @@ -153,7 +165,7 @@ def wigner_to_fock_Choi(X, Y, d, shape): Tensor: the fock representation of the Choi matrix """ A, B, C = wigner_to_bargmann_Choi(X, Y, d) - return math.hermite_renormalized(-A, B, C, shape=shape) + return math.hermite_renormalized(A, B, C, shape=shape) def ket_to_dm(ket: Tensor) -> Tensor: @@ -670,10 +682,12 @@ def oscillator_eigenstate(q: Vector, cutoff: int) -> Tensor: prefactor = (omega_over_hbar / np.pi) ** (1 / 4) * math.sqrt(2 ** (-math.arange(0, cutoff))) # Renormalized physicist hermite polys: Hn / sqrt(n!) - R = np.array([[2 + 0j]]) # to get the physicist polys + R = -np.array([[2 + 0j]]) # to get the physicist polys def f_hermite_polys(xi): - poly = math.hermite_renormalized(R, 2 * math.astensor([xi], "complex128"), 1 + 0j, cutoff) + poly = math.hermite_renormalized( + R, 2 * math.astensor([xi], "complex128"), 1 + 0j, (cutoff,) + ) return math.cast(poly, "float64") hermite_polys = math.map_fn(f_hermite_polys, x_tensor) diff --git a/tests/test_lab/test_state.py b/tests/test_lab/test_state.py new file mode 100644 index 000000000..184a019ee --- /dev/null +++ b/tests/test_lab/test_state.py @@ -0,0 +1,49 @@ +import numpy as np + +from mrmustard.lab import Attenuator, Gaussian + + +def test_addition(): + """Test that addition of Gaussians is correct""" + G0 = Gaussian(1, cutoffs=[10]) + G1 = Gaussian(1, cutoffs=[10]) + + mixed = G0 + G1 + + assert np.allclose(mixed.dm([10]), G0.dm([10]) + G1.dm([10])) + + +def test_multiplication_ket(): + """Test that multiplication of Gaussians is correct""" + G = Gaussian(1, cutoffs=[10]) + + scaled = 42.0 * G + + assert np.allclose(scaled.ket(G.cutoffs), 42.0 * G.ket()) + + +def test_multiplication_dm(): + """Test that multiplication of Gaussians is correct""" + G = Gaussian(1) >> Attenuator(0.9) + + scaled = 42.0 * G + + assert np.allclose(scaled.dm(), 42.0 * G.dm()) + + +def test_division_ket(): + """Test that division of Gaussians is correct""" + G = Gaussian(1, cutoffs=[10]) + + scaled = G / 42.0 + + assert np.allclose(scaled.ket([10]), G.ket([10]) / 42.0) + + +def test_division_dm(): + """Test that division of Gaussians is correct""" + G = Gaussian(1) >> Attenuator(0.9) + + scaled = G / 42.0 + + assert np.allclose(scaled.dm(G.cutoffs), G.dm() / 42.0) diff --git a/tests/test_math/test_lattice.py b/tests/test_math/test_lattice.py new file mode 100644 index 000000000..f7cb43a1f --- /dev/null +++ b/tests/test_math/test_lattice.py @@ -0,0 +1,29 @@ +# 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. + +"""Tests for the lattice module""" + +import numpy as np + +from mrmustard.lab import Gaussian + + +def test_vanilla_vs_binomial(): + """Test that the vanilla and binomial methods give the same result""" + G = Gaussian(2) + + ket_vanilla = G.ket(cutoffs=[10, 10])[:5, :5] + ket_binomial = G.ket(max_photons=10)[:5, :5] + + assert np.allclose(ket_vanilla, ket_binomial) diff --git a/tests/test_math/test_special.py b/tests/test_math/test_special.py index 6f08d450a..04916d3b8 100644 --- a/tests/test_math/test_special.py +++ b/tests/test_math/test_special.py @@ -16,6 +16,7 @@ import numpy as np from scipy.special import eval_hermite, factorial + from mrmustard.math import Math math = Math() @@ -26,9 +27,12 @@ def test_reduction_to_renorm_physicists_polys(): x = np.arange(-1, 1, 0.1) init = 1 n_max = 5 - A = np.ones([init, init], dtype=complex) + A = -np.ones([init, init], dtype=complex) vals = np.array( - [math.hermite_renormalized(2 * A, 2 * np.array([x0], dtype=complex), 1, n_max) for x0 in x] + [ + math.hermite_renormalized(2 * A, 2 * np.array([x0], dtype=complex), 1, (n_max,)) + for x0 in x + ] ).T expected = np.array([eval_hermite(i, x) / np.sqrt(factorial(i)) for i in range(len(vals))]) assert np.allclose(vals, expected) diff --git a/tests/test_physics/test_bargmann.py b/tests/test_physics/test_bargmann.py new file mode 100644 index 000000000..b10ac2b8f --- /dev/null +++ b/tests/test_physics/test_bargmann.py @@ -0,0 +1,20 @@ +import numpy as np + +from mrmustard.lab import Attenuator, Dgate, Gaussian +from mrmustard.physics.bargmann import wigner_to_bargmann_psi, wigner_to_bargmann_rho + + +def test_wigner_to_bargmann_psi(): + """Test that the Bargmann representation of a ket is correct""" + G = Gaussian(2) >> Dgate(0.1, 0.2) + + for x, y in zip(G.bargmann(), wigner_to_bargmann_psi(G.cov, G.means)): + assert np.allclose(x, y) + + +def test_wigner_to_bargmann_rho(): + """Test that the Bargmann representation of a dm is correct""" + G = Gaussian(2) >> Dgate(0.1, 0.2) >> Attenuator(0.9) + + for x, y in zip(G.bargmann(), wigner_to_bargmann_rho(G.cov, G.means)): + assert np.allclose(x, y) diff --git a/tests/test_physics/test_fock/test_fock.py b/tests/test_physics/test_fock/test_fock.py index 70e465653..85d101767 100644 --- a/tests/test_physics/test_fock/test_fock.py +++ b/tests/test_physics/test_fock/test_fock.py @@ -12,43 +12,40 @@ # See the License for the specific language governing permissions and # limitations under the License. -from hypothesis import settings, given, strategies as st -import pytest - import numpy as np +import pytest +from hypothesis import given +from hypothesis import strategies as st from scipy.special import factorial from thewalrus.quantum import total_photon_number_distribution + from mrmustard.lab import ( + TMSV, + Attenuator, + BSgate, Circuit, - Vacuum, + Coherent, + Fock, + Gaussian, + Ggate, S2gate, - BSgate, Sgate, - Rgate, - Dgate, - Ggate, - Interferometer, SqueezedVacuum, - TMSV, State, - Attenuator, - Fock, - Coherent, - Gaussian, + Vacuum, ) from mrmustard.physics.fock import ( - dm_to_ket, - ket_to_dm, - trace, + _displacement, + _grad_displacement, apply_choi_to_dm, apply_choi_to_ket, apply_kraus_to_dm, apply_kraus_to_ket, - _grad_displacement, - _displacement, + dm_to_ket, + ket_to_dm, + trace, ) - # helper strategies st_angle = st.floats(min_value=0, max_value=2 * np.pi) @@ -176,8 +173,8 @@ def test_density_matrix(num_modes): [ Vacuum(num_modes=2), Fock([4, 3], modes=[0, 1]), - Coherent(x=[0.1, 0.2], y=[-0.4, 0.4], cutoffs=[25]), - Gaussian(num_modes=2, cutoffs=[35]), + Coherent(x=[0.1, 0.2], y=[-0.4, 0.4], cutoffs=[10, 10]), + Gaussian(num_modes=2, cutoffs=[35, 35]), ], ) def test_dm_to_ket(state): @@ -185,9 +182,9 @@ def test_dm_to_ket(state): dm = state.dm() ket = dm_to_ket(dm) # check if ket is normalized - assert np.allclose(np.linalg.norm(ket), 1) + assert np.allclose(np.linalg.norm(ket), 1, atol=1e-4) # check kets are equivalent - assert np.allclose(ket, state.ket()) + assert np.allclose(ket, state.ket(), atol=1e-4) dm_reconstructed = ket_to_dm(ket) # check ket leads to same dm diff --git a/tests/test_physics/test_gaussian/test_symplectics.py b/tests/test_physics/test_gaussian/test_symplectics.py index 09fa2dff2..b8d3bc57c 100644 --- a/tests/test_physics/test_gaussian/test_symplectics.py +++ b/tests/test_physics/test_gaussian/test_symplectics.py @@ -12,28 +12,26 @@ # See the License for the specific language governing permissions and # limitations under the License. -import pytest -from hypothesis import given, strategies as st - -from thewalrus.symplectic import two_mode_squeezing, squeezing, rotation, beam_splitter, expand import numpy as np +from hypothesis import given +from hypothesis import strategies as st +from thewalrus.symplectic import beam_splitter, expand, rotation, squeezing, two_mode_squeezing -from mrmustard import settings from mrmustard.lab.gates import ( - Sgate, + Amplifier, + Attenuator, BSgate, - S2gate, - Rgate, - MZgate, - Pgate, CXgate, CZgate, Dgate, - Amplifier, - Attenuator, + MZgate, + Pgate, + Rgate, + S2gate, + Sgate, ) -from mrmustard.lab.states import Vacuum, TMSV, Thermal -from mrmustard.physics.gaussian import quadratic_phase, controlled_Z, controlled_X +from mrmustard.lab.states import TMSV, Thermal, Vacuum +from mrmustard.physics.gaussian import controlled_X, controlled_Z @given(r=st.floats(0, 2)) @@ -135,7 +133,7 @@ def test_BSgate(theta, phi): @given(r=st.floats(0, 1), phi=st.floats(0, 2 * np.pi)) def test_S2gate(r, phi): """Tests the S2gate is implemented correctly by applying it on one half of a maximally entangled state""" - r_choi = settings.CHOI_R + r_choi = np.arcsinh(1.0) S2 = S2gate(r=r, phi=phi) bell = (TMSV(r_choi) & TMSV(r_choi)).get_modes([0, 2, 1, 3]) cov = (bell[0, 1, 2, 3] >> S2[0, 1]).cov @@ -150,7 +148,7 @@ def test_S2gate(r, phi): @given(phi_ex=st.floats(0, 2 * np.pi), phi_in=st.floats(0, 2 * np.pi)) def test_MZgate_external_tms(phi_ex, phi_in): """Tests the MZgate is implemented correctly by applying it on one half of a maximally entangled state""" - r_choi = settings.CHOI_R + r_choi = np.arcsinh(1.0) bell = (TMSV(r_choi) & TMSV(r_choi)).get_modes([0, 2, 1, 3]) MZ = MZgate(phi_a=phi_ex, phi_b=phi_in, internal=False) cov = (bell[0, 1, 2, 3] >> MZ[0, 1]).cov @@ -173,7 +171,7 @@ def test_MZgate_external_tms(phi_ex, phi_in): @given(phi_a=st.floats(0, 2 * np.pi), phi_b=st.floats(0, 2 * np.pi)) def test_MZgate_internal_tms(phi_a, phi_b): """Tests the MZgate is implemented correctly by applying it on one half of a maximally entangled state""" - r_choi = settings.CHOI_R + r_choi = np.arcsinh(1.0) bell = (TMSV(r_choi) & TMSV(r_choi)).get_modes([0, 2, 1, 3]) MZ = MZgate(phi_a=phi_a, phi_b=phi_b, internal=True) cov = (bell[0, 1, 2, 3] >> MZ[0, 1]).cov diff --git a/tests/test_training/test_callbacks.py b/tests/test_training/test_callbacks.py index eb84284b4..0091b9ff5 100644 --- a/tests/test_training/test_callbacks.py +++ b/tests/test_training/test_callbacks.py @@ -17,16 +17,15 @@ import numpy as np import tensorflow as tf +from mrmustard import settings +from mrmustard.lab.circuit import Circuit from mrmustard.lab.gates import ( BSgate, S2gate, ) -from mrmustard.lab.circuit import Circuit -from mrmustard.training import Optimizer, TensorboardCallback from mrmustard.lab.states import Vacuum -from mrmustard import settings - from mrmustard.math import Math +from mrmustard.training import Optimizer, TensorboardCallback math = Math() diff --git a/tests/test_training/test_opt.py b/tests/test_training/test_opt.py index e5da493df..38badd3d1 100644 --- a/tests/test_training/test_opt.py +++ b/tests/test_training/test_opt.py @@ -44,7 +44,7 @@ @given(n=st.integers(0, 3)) def test_S2gate_coincidence_prob(n): """Testing the optimal probability of obtaining |n,n> from a two mode squeezed vacuum""" - settings.SEED = 42 + settings.SEED = 40 S = S2gate( r=abs(settings.rng.normal(loc=1.0, scale=0.1)), r_trainable=True, @@ -67,7 +67,7 @@ def cb(optimizer, cost, trainables, **kwargs): # pylint: disable=unused-argumen assert np.allclose(-cost_fn(), expected, atol=1e-5) cb_result = opt.callback_history.get("cb") - assert {res["num_trainables"] for res in cb_result} == {2} + assert {res["num_trainables"] for res in cb_result} == {1} assert {res["lr"] for res in cb_result} == {0.01} assert [res["cost"] for res in cb_result] == opt.opt_history[1:] @@ -115,8 +115,8 @@ def test_learning_two_mode_squeezing(): settings.SEED = 42 ops = [ Sgate( - r=abs(settings.rng.normal(size=(2))), - phi=settings.rng.normal(size=(2)), + r=abs(settings.rng.normal(size=2)), + phi=settings.rng.normal(size=2), r_trainable=True, phi_trainable=True, ), @@ -146,7 +146,7 @@ def test_learning_two_mode_Ggate(): G = Ggate(num_modes=2, symplectic_trainable=True) def cost_fn(): - amps = (Vacuum(2) >> G).ket(cutoffs=[2, 2]) + amps = (Vacuum(2) >> G).ket(cutoffs=[2, 2], max_prob=0.9999) return -math.abs(amps[1, 1]) ** 2 + math.abs(amps[0, 1]) ** 2 opt = Optimizer(symplectic_lr=0.5, euclidean_lr=0.01) @@ -160,8 +160,8 @@ def test_learning_two_mode_Interferometer(): settings.SEED = 42 ops = [ Sgate( - r=settings.rng.normal(size=(2)) ** 2, - phi=settings.rng.normal(size=(2)), + r=settings.rng.normal(size=2) ** 2, + phi=settings.rng.normal(size=2), r_trainable=True, phi_trainable=True, ), @@ -185,8 +185,8 @@ def test_learning_two_mode_RealInterferometer(): settings.SEED = 2 ops = [ Sgate( - r=settings.rng.normal(size=(2)) ** 2, - phi=settings.rng.normal(size=(2)), + r=settings.rng.normal(size=2) ** 2, + phi=settings.rng.normal(size=2), r_trainable=True, phi_trainable=True, ), From c736dc32405cb8f275a925708f5ffab7518e292b Mon Sep 17 00:00:00 2001 From: Filippo Miatto Date: Wed, 31 May 2023 13:32:20 -0700 Subject: [PATCH 47/53] More strategies (#239) **Context:** Some fundamental gates gain specialized Fock methods that account for their symmetries (for example the BS gate can be built with 3 loops rather than 4 by using photon number conservation). We also now have Vector-Jacobian products directly implemented rather than implemented as the product of the actual jacobian with the upstream gradient: the jacobian can be very big (num_params x size_of_forward_pass) so instead we "accumulate" the downstream gradient without ever needing to allocate memory for a jacobian. **Description of the Change:** 1. Introduces special fock methods for `BSgate` (50x faster), `Sgate` (5x faster) and `SqueezedVacuum` (10x faster) 2. Replaces Jacobians with VJPs --> low-memory backprop **Benefits:** wooosh! --- .github/CHANGELOG.md | 12 +- doc/requirements.txt | 2 +- mrmustard/__init__.py | 2 +- mrmustard/lab/abstract/state.py | 39 ++-- mrmustard/lab/abstract/transformation.py | 148 +++++-------- mrmustard/lab/circuit.py | 6 +- mrmustard/lab/detectors.py | 3 +- mrmustard/lab/gates.py | 116 ++++++++-- mrmustard/lab/states.py | 15 +- mrmustard/math/lattice/strategies/__init__.py | 19 ++ .../math/lattice/strategies/beamsplitter.py | 156 +++++++++++++ .../{strategies.py => strategies/binomial.py} | 116 +--------- .../math/lattice/strategies/displacement.py | 139 ++++++++++++ mrmustard/math/lattice/strategies/squeezer.py | 188 ++++++++++++++++ mrmustard/math/lattice/strategies/vanilla.py | 117 ++++++++++ .../numba/compactFock_1leftoverMode_amps.py | 13 +- mrmustard/physics/fock.py | 195 ++++++++--------- mrmustard/utils/wigner.py | 7 +- tests/test_lab/test_circuit.py | 20 +- tests/test_lab/test_gates_fock.py | 130 +++++++++-- tests/test_lab/test_state.py | 2 +- tests/test_math/test_interface.py | 13 +- tests/test_physics/test_bargmann.py | 25 ++- tests/test_physics/test_fock/test_fock.py | 206 +++++++++--------- .../test_gaussian/test_gaussian_utils.py | 36 ++- .../test_gaussian/test_symplectics.py | 9 +- tests/test_training/test_opt.py | 54 ++++- tests/test_utils/test_wigner.py | 15 +- 28 files changed, 1280 insertions(+), 523 deletions(-) create mode 100644 mrmustard/math/lattice/strategies/__init__.py create mode 100644 mrmustard/math/lattice/strategies/beamsplitter.py rename mrmustard/math/lattice/{strategies.py => strategies/binomial.py} (63%) create mode 100644 mrmustard/math/lattice/strategies/displacement.py create mode 100644 mrmustard/math/lattice/strategies/squeezer.py create mode 100644 mrmustard/math/lattice/strategies/vanilla.py diff --git a/.github/CHANGELOG.md b/.github/CHANGELOG.md index f438ba33f..89af599a5 100644 --- a/.github/CHANGELOG.md +++ b/.github/CHANGELOG.md @@ -50,6 +50,9 @@ ket = Gaussian(2).ket(max_prob=0.99, max_photons=3) ``` +* Gaussian transformations support a `bargmann` method for returning the bargmann representation. + [(#239)](https://github.com/XanaduAI/MrMustard/pull/239) + ### Breaking Changes * The previous `callback` argument to `Optimizer.minimize` is now `callbacks` since we can now pass @@ -72,12 +75,19 @@ * Tests of the compact_fock module now use hypothesis. [(#235)](https://github.com/XanaduAI/MrMustard/pull/235) +* Faster implementation of the fock representation of `BSgate`, `Sgate` and `SqueezedVacuum`, ranging from 5x to 50x. + [(#239)](https://github.com/XanaduAI/MrMustard/pull/239) + +* More robust implementation of cutoffs for States. +[(#239)](https://github.com/XanaduAI/MrMustard/pull/239) + ### Bug fixes * Fixed a bug that would make two progress bars appear during an optimization [(#235)](https://github.com/XanaduAI/MrMustard/pull/235) -### Bug fixes +* The displacement of the dual of an operation had the wrong sign + [(#239)](https://github.com/XanaduAI/MrMustard/pull/239) ### Documentation diff --git a/doc/requirements.txt b/doc/requirements.txt index 8ea6a7332..deb3032e8 100644 --- a/doc/requirements.txt +++ b/doc/requirements.txt @@ -6,4 +6,4 @@ sphinx-copybutton sphinx-automodapi sphinxcontrib-bibtex mistune==0.8.4 -xanadu-sphinx-theme==0.1.0 +xanadu-sphinx-theme==0.1.0 \ No newline at end of file diff --git a/mrmustard/__init__.py b/mrmustard/__init__.py index a6556e73f..725a29548 100644 --- a/mrmustard/__init__.py +++ b/mrmustard/__init__.py @@ -40,7 +40,7 @@ def __init__(self): self.AUTOCUTOFF_MIN_CUTOFF = 1 self.CIRCUIT_DECIMALS = 3 # use cutoff=5 for each mode when determining if two transformations in fock repr are equal - self.EQ_TRANSFORMATION_CUTOFF = 5 + 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 # for the detectors diff --git a/mrmustard/lab/abstract/state.py b/mrmustard/lab/abstract/state.py index 99549f42a..9d12c11ef 100644 --- a/mrmustard/lab/abstract/state.py +++ b/mrmustard/lab/abstract/state.py @@ -94,12 +94,15 @@ def __init__( self._norm = _norm if cov is not None and means is not None: self.is_gaussian = True + self.is_hilbert_vector = np.allclose(gaussian.purity(self.cov, settings.HBAR), 1.0) self.num_modes = cov.shape[-1] // 2 elif eigenvalues is not None and symplectic is not None: self.is_gaussian = True + self.is_hilbert_vector = np.allclose(eigenvalues, 2.0 / settings.HBAR) self.num_modes = symplectic.shape[-1] // 2 elif ket is not None or dm is not None: self.is_gaussian = False + self.is_hilbert_vector = ket is not None self.num_modes = len(ket.shape) if ket is not None else len(dm.shape) // 2 self._purity = 1.0 if ket is not None else None else: @@ -150,7 +153,7 @@ def is_mixed(self): @property def is_pure(self): r"""Returns ``True`` if the state is pure and ``False`` otherwise.""" - return True if self._ket is not None else np.isclose(self.purity, 1.0, atol=1e-6) + return np.isclose(self.purity, 1.0, atol=1e-6) @property def means(self) -> Optional[RealVector]: @@ -174,14 +177,22 @@ def number_stdev(self) -> RealVector: @property def cutoffs(self) -> List[int]: - r"""Returns the cutoff dimensions for each mode.""" - if self._cutoffs is not None: - return self._cutoffs # TODO: allow self._cutoffs = [N, None] - if self._ket is None and self._dm is None: - return fock.autocutoffs(self.cov, self.means, settings.AUTOCUTOFF_PROBABILITY) - return list( - self.fock.shape[: self.num_modes] - ) # NOTE: triggered only if the fock representation already exists + r"""Returns the Hilbert space dimension of each mode.""" + if self._cutoffs is None: + if self._ket is None and self._dm is None: + self._cutoffs = fock.autocutoffs( + self.cov, self.means, settings.AUTOCUTOFF_PROBABILITY + ) + else: + self._cutoffs = [ + int(c) + for c in ( + self._ket.shape + if self._ket is not None + else self._dm.shape[: self.num_modes] + ) + ] + return self._cutoffs @property def shape(self) -> List[int]: @@ -191,14 +202,14 @@ def shape(self) -> List[int]: the first two moments of the number operator. """ # NOTE: if we initialize State(dm=pure_dm), self.fock returns the dm, which does not have shape self.cutoffs - return self.cutoffs if self.is_pure else self.cutoffs + self.cutoffs + return self.cutoffs if self.is_hilbert_vector else self.cutoffs + self.cutoffs @property def fock(self) -> ComplexTensor: r"""Returns the Fock representation of the state.""" if self._dm is None and self._ket is None: _fock = fock.wigner_to_fock_state( - self.cov, self.means, shape=self.shape, return_dm=self.is_mixed + self.cov, self.means, shape=self.shape, return_dm=not self.is_hilbert_vector ) if self.is_mixed: self._dm = _fock @@ -229,7 +240,7 @@ def norm(self) -> float: r"""Returns the norm of the state.""" if self.is_gaussian: return self._norm - return fock.norm(self.fock, self._dm is not None) + return fock.norm(self.fock, not self.is_hilbert_vector) @property def probability(self) -> float: @@ -281,7 +292,7 @@ def ket( # if state is pure and has a density matrix, calculate the ket if self.is_pure: self._ket = fock.dm_to_ket(self._dm) - current_cutoffs = list(self._ket.shape[: self.num_modes]) + current_cutoffs = [int(s) for s in self._ket.shape] if cutoffs != current_cutoffs: paddings = [(0, max(0, new - old)) for new, old in zip(cutoffs, current_cutoffs)] if any(p != (0, 0) for p in paddings): @@ -312,7 +323,7 @@ def dm(self, cutoffs: Optional[List[int]] = None) -> ComplexTensor: else: if self.is_gaussian: self._dm = fock.wigner_to_fock_state( - self.cov, self.means, shape=cutoffs * 2, return_dm=True + self.cov, self.means, shape=cutoffs + cutoffs, return_dm=True ) elif cutoffs != (current_cutoffs := list(self._dm.shape[: self.num_modes])): paddings = [(0, max(0, new - old)) for new, old in zip(cutoffs, current_cutoffs)] diff --git a/mrmustard/lab/abstract/transformation.py b/mrmustard/lab/abstract/transformation.py index f8a1456dc..c8db2d0bd 100644 --- a/mrmustard/lab/abstract/transformation.py +++ b/mrmustard/lab/abstract/transformation.py @@ -33,7 +33,7 @@ from mrmustard import settings from mrmustard.math import Math -from mrmustard.physics import fock, gaussian +from mrmustard.physics import bargmann, fock, gaussian from mrmustard.training.parameter import Parameter from mrmustard.typing import RealMatrix, RealVector @@ -46,6 +46,20 @@ class Transformation: r"""Base class for all Transformations.""" is_unitary = True # whether the transformation is unitary (True by default) + def bargmann(self): + X, Y, d = self.XYd(allow_none=False) + if self.is_unitary: + return bargmann.wigner_to_bargmann_U( + X if X is not None else math.identity(d.shape[-1], dtype=d.dtype), + d if d is not None else math.zeros(X.shape[-1], dtype=X.dtype), + ) + else: + return bargmann.wigner_to_bargmann_Choi( + X if X is not None else math.identity(d.shape[-1], dtype=d.dtype), + Y if Y is not None else math.zeros((d.shape[-1], d.shape[-1]), dtype=d.dtype), + d if d is not None else math.zeros(X.shape[-1], dtype=X.dtype), + ) + def primal(self, state: State) -> State: r"""Applies ``self`` (a ``Transformation``) to other (a ``State``) and returns the transformed state. @@ -86,7 +100,7 @@ def transform_gaussian(self, state: State, dual: bool) -> State: Returns: State: the transformed state """ - X, Y, d = self.XYd if not dual else self.XYd_dual + X, Y, d = self.XYd(allow_none=False) if not dual else self.XYd_dual(allow_none=False) cov, means = gaussian.CPTP(state.cov, state.means, X, Y, d, state.modes, self.modes) new_state = State( cov=cov, means=means, modes=state.modes, _norm=state.norm @@ -105,13 +119,15 @@ def transform_fock(self, state: State, dual: bool) -> State: """ op_idx = [state.modes.index(m) for m in self.modes] if self.is_unitary: - U = self.U(cutoffs=[state.cutoffs[i] for i in op_idx]) + # until we have output autocutoff we use the same input cutoff list + U = self.U(cutoffs=[state.cutoffs[i] for i in op_idx] * 2) U = math.dagger(U) if dual else U if state.is_pure: return State(ket=fock.apply_kraus_to_ket(U, state.ket(), op_idx), modes=state.modes) return State(dm=fock.apply_kraus_to_dm(U, state.dm(), op_idx), modes=state.modes) else: - choi = self.choi(cutoffs=[state.cutoffs[i] for i in op_idx]) + # until we have output autocutoff we use the same input cutoff list + choi = self.choi(cutoffs=[state.cutoffs[i] for i in op_idx] * 4) n = state.num_modes N0 = list(range(0, n)) N1 = list(range(n, 2 * n)) @@ -130,7 +146,7 @@ def transform_fock(self, state: State, dual: bool) -> State: def modes(self) -> Sequence[int]: """Returns the list of modes on which the transformation acts on.""" if self._modes in (None, []): - for elem in self.XYd: + for elem in self.XYd(allow_none=True): if elem is not None: self._modes = list(range(elem.shape[-1] // 2)) break @@ -181,59 +197,52 @@ def d_vector_dual(self) -> Optional[RealVector]: if (d := self.d_vector) is None: return None if (Xdual := self.X_matrix_dual) is None: - return d - return math.matmul(Xdual, d) + return -d + return -math.matmul(Xdual, d) - @property - def XYd(self) -> Tuple[Optional[RealMatrix], Optional[RealMatrix], Optional[RealVector]]: + def XYd( + self, allow_none: bool = True + ) -> Tuple[Optional[RealMatrix], Optional[RealMatrix], Optional[RealVector]]: r"""Returns the ```(X, Y, d)``` triple. Override in subclasses if computing ``X``, ``Y`` and ``d`` together is more efficient. """ - return self.X_matrix, self.Y_matrix, self.d_vector - - @property - def XYd_dual(self) -> Tuple[Optional[RealMatrix], Optional[RealMatrix], Optional[RealVector]]: + if allow_none: + return self.X_matrix, self.Y_matrix, self.d_vector + X = math.eye(2 * self.num_modes) if self.X_matrix is None else self.X_matrix + Y = math.zeros_like(X) if self.Y_matrix is None else self.Y_matrix + d = math.zeros_like(X[:, 0]) if self.d_vector is None else self.d_vector + return X, Y, d + + def XYd_dual( + self, allow_none: bool = True + ) -> Tuple[Optional[RealMatrix], Optional[RealMatrix], Optional[RealVector]]: r"""Returns the ```(X, Y, d)``` triple of the dual of the current transformation. Override in subclasses if computing ``Xdual``, ``Ydual`` and ``ddual`` together is more efficient. """ - return self.X_matrix_dual, self.Y_matrix_dual, self.d_vector_dual - - @property - def is_phase_covariant(self) -> bool: - X, Y, d = self.XYd - if d is not None: - return False - if X is not None and not math.allclose(self.X, math.diag(math.diag_part(self.X))): - return False - if Y is not None and not math.allclose(self.Y, math.diag(math.diag_part(self.Y))): - return False - return True + if allow_none: + return self.X_matrix_dual, self.Y_matrix_dual, self.d_vector_dual + Xdual = math.eye(2 * self.num_modes) if self.X_matrix_dual is None else self.X_matrix_dual + Ydual = math.zeros_like(Xdual) if self.Y_matrix_dual is None else self.Y_matrix_dual + ddual = math.zeros_like(Xdual[:, 0]) if self.d_vector_dual is None else self.d_vector_dual + return Xdual, Ydual, ddual def U(self, cutoffs: Sequence[int]): r"""Returns the unitary representation of the transformation.""" if not self.is_unitary: return None - X, _, d = self.XYd - return fock.wigner_to_fock_U( - X if X is not None else math.eye(2 * self.num_modes), - d if d is not None else math.zeros((2 * self.num_modes,)), - shape=cutoffs * 2 if len(cutoffs) == self.num_modes else cutoffs, - ) + X, _, d = self.XYd(allow_none=False) + return fock.wigner_to_fock_U(X, d, shape=tuple(cutoffs)) def choi(self, cutoffs: Sequence[int]): r"""Returns the Choi representation of the transformation.""" if self.is_unitary: - U = self.U(cutoffs) - return fock.U_to_choi(U) - X, Y, d = self.XYd - return fock.wigner_to_fock_Choi( - X if X is not None else math.eye(2 * self.num_modes), - Y if Y is not None else math.zeros((2 * self.num_modes, 2 * self.num_modes)), - d if d is not None else math.zeros((2 * self.num_modes,)), - shape=cutoffs * 4 if len(cutoffs) == self.num_modes else cutoffs, - ) + U = self.U(cutoffs[: self.num_modes]) + Udual = self.U(cutoffs[self.num_modes :]) + return fock.U_to_choi(U, Udual) + X, Y, d = self.XYd(allow_none=False) + return fock.wigner_to_fock_Choi(X, Y, d, shape=cutoffs) def __getitem__(self, items) -> Callable: r"""Sets the modes on which the transformation acts. @@ -300,7 +309,9 @@ def __lshift__(self, other: Union[State, Transformation]): return self.dual(other) if isinstance(other, Transformation): return self >> other # so that the dual is self.dual(other.dual(x)) - raise ValueError(f"{other} is not a valid state or transformation.") + raise ValueError( + f"{other} of type {other.__class__} is not a valid state or transformation." + ) # pylint: disable=too-many-branches,too-many-return-statements def __eq__(self, other): @@ -309,59 +320,14 @@ def __eq__(self, other): return False if not (self.is_gaussian and other.is_gaussian): return np.allclose( - self.choi(cutoffs=[settings.EQ_TRANSFORMATION_CUTOFF] * self.num_modes), - other.choi(cutoffs=[settings.EQ_TRANSFORMATION_CUTOFF] * self.num_modes), + self.choi(cutoffs=[settings.EQ_TRANSFORMATION_CUTOFF] * 4 * self.num_modes), + other.choi(cutoffs=[settings.EQ_TRANSFORMATION_CUTOFF] * 4 * self.num_modes), rtol=settings.EQ_TRANSFORMATION_RTOL_FOCK, ) - sX, sY, sd = self.XYd - oX, oY, od = other.XYd - if sX is None: - if oX is not None: - if not np.allclose( - oX, np.eye(oX.shape[0]), rtol=settings.EQ_TRANSFORMATION_RTOL_GAUSS - ): - return False - if oX is None: - if sX is not None: - if not np.allclose( - sX, np.eye(sX.shape[0]), rtol=settings.EQ_TRANSFORMATION_RTOL_GAUSS - ): - return False - if sX is not None and oX is not None: - if not np.allclose(sX, oX): - return False - if sY is None: - if oY is not None: - if not np.allclose( - oY, np.zeros_like(oY), rtol=settings.EQ_TRANSFORMATION_RTOL_GAUSS - ): - return False - if oY is None: - if sY is not None: - if not np.allclose( - sY, np.zeros_like(sY), rtol=settings.EQ_TRANSFORMATION_RTOL_GAUSS - ): - return False - if sY is not None and oY is not None: - if not np.allclose(sY, oY): - return False - if sd is None: - if od is not None: - if not np.allclose( - sd, np.zeros_like(sd), rtol=settings.EQ_TRANSFORMATION_RTOL_GAUSS - ): - return False - if od is None: - if sd is not None: - if not np.allclose( - sd, np.zeros_like(sd), rtol=settings.EQ_TRANSFORMATION_RTOL_GAUSS - ): - return False - if sd is not None and od is not None: - if not np.allclose(sd, od): - return False - return True + sX, sY, sd = self.XYd(allow_none=False) + oX, oY, od = other.XYd(allow_none=False) + return np.allclose(sX, oX) and np.allclose(sY, oY) and np.allclose(sd, od) def __repr__(self): class_name = self.__class__.__name__ diff --git a/mrmustard/lab/circuit.py b/mrmustard/lab/circuit.py index dc34d2708..3b1c2040b 100644 --- a/mrmustard/lab/circuit.py +++ b/mrmustard/lab/circuit.py @@ -20,12 +20,12 @@ __all__ = ["Circuit"] -from mrmustard.typing import RealMatrix, RealVector from typing import List, Optional, Tuple from mrmustard import settings from mrmustard.lab.abstract import State, Transformation from mrmustard.training import Parametrized +from mrmustard.typing import RealMatrix, RealVector from mrmustard.utils.circdrawer import circuit_text from mrmustard.utils.xptensor import XPMatrix, XPVector @@ -62,9 +62,9 @@ def dual(self, state: State) -> State: state = op.dual(state) return state - @property def XYd( self, + allow_none: bool = True, ) -> Tuple[ RealMatrix, RealMatrix, RealVector ]: # NOTE: Overriding Transformation.XYd for efficiency @@ -72,7 +72,7 @@ def XYd( Y = XPMatrix(like_0=True) d = XPVector() for op in self._ops: - opx, opy, opd = op.XYd + opx, opy, opd = op.XYd(allow_none) opX = XPMatrix.from_xxpp(opx, modes=(op.modes, op.modes), like_1=True) opY = XPMatrix.from_xxpp(opy, modes=(op.modes, op.modes), like_0=True) opd = XPVector.from_xxpp(opd, modes=op.modes) diff --git a/mrmustard/lab/detectors.py b/mrmustard/lab/detectors.py index f6fb600ae..578f30d1d 100644 --- a/mrmustard/lab/detectors.py +++ b/mrmustard/lab/detectors.py @@ -16,8 +16,7 @@ This module implements the set of detector classes that perform measurements on quantum circuits. """ -from typing import List, Tuple, Union, Optional, Iterable - +from typing import Iterable, List, Optional, Tuple, Union from mrmustard import settings from mrmustard.math import Math diff --git a/mrmustard/lab/gates.py b/mrmustard/lab/gates.py index 586ce3fc8..54e0e236a 100644 --- a/mrmustard/lab/gates.py +++ b/mrmustard/lab/gates.py @@ -18,13 +18,14 @@ This module defines gates and operations that can be applied to quantum modes to construct a quantum circuit. """ -from typing import Union, Optional, List, Tuple, Sequence -from mrmustard.typing import RealMatrix, ComplexMatrix +from typing import List, Optional, Sequence, Tuple, Union + from mrmustard import settings from mrmustard.lab.abstract import Transformation from mrmustard.math import Math -from mrmustard.physics import gaussian, fock +from mrmustard.physics import fock, gaussian from mrmustard.training import Parametrized +from mrmustard.typing import ComplexMatrix, RealMatrix math = Math() @@ -94,28 +95,37 @@ def d_vector(self): return gaussian.displacement(self.x.value, self.y.value, settings.HBAR) def U(self, cutoffs: Sequence[int]): - """Returns the unitary representation of the Displacement gate using the Laguerre - polynomials.""" + r"""Returns the unitary representation of the Displacement gate using + the Laguerre polynomials. + + Arguments: + cutoffs (Sequence[int]): the Fock basis truncation for each index of U + in the order (out_1, out_2,..., in_1, in_2,...) + + Returns: + array[complex]: the unitary matrix + """ N = self.num_modes x = self.x.value * math.ones(N, dtype=self.x.value.dtype) y = self.y.value * math.ones(N, dtype=self.y.value.dtype) - r = math.sqrt(x * x + y * y) - phi = math.atan2(y, x) - - # calculate displacement unitary for each mode and concatenate with outer product - Ud = None - for idx, cutoff in enumerate(cutoffs): - if Ud is None: - Ud = fock.displacement(r[idx], phi[idx], cutoff) - else: - U_next = fock.displacement(r[idx], phi[idx], cutoff) - Ud = math.outer(Ud, U_next) - return math.transpose( - Ud, - list(range(0, 2 * N, 2)) + list(range(1, 2 * N, 2)), - ) + if N > 1: + # calculate displacement unitary for each mode and concatenate with outer product + Ud = None + for idx, out_in in enumerate(zip(cutoffs[:N], cutoffs[N:])): + if Ud is None: + Ud = fock.displacement(x[idx], y[idx], out_in) + else: + U_next = fock.displacement(x[idx], y[idx], out_in) + Ud = math.outer(Ud, U_next) + + return math.transpose( + Ud, + list(range(0, 2 * N, 2)) + list(range(1, 2 * N, 2)), + ) + else: + return fock.displacement(x[0], y[0], tuple(cutoffs)) class Sgate(Parametrized, Transformation): @@ -160,6 +170,37 @@ def __init__( self.is_gaussian = True self.short_name = "S" + def U(self, cutoffs: Sequence[int]): + r"""Returns the unitary representation of the Squeezing gate. + Args: + cutoffs (Sequence[int]): cutoff dimension for each mode + in the order (out_1, out_2,..., in_1, in_2,...) + + Returns: + array[complex]: the unitary matrix + + """ + N = self.num_modes + # this works both or scalar r/phi and vector r/phi: + r = self.r.value * math.ones(N, dtype=self.r.value.dtype) + phi = self.phi.value * math.ones(N, dtype=self.phi.value.dtype) + + if N > 1: + # calculate squeezing unitary for each mode and concatenate with outer product + Us = None + for idx, out_in in enumerate(zip(cutoffs[:N], cutoffs[N:])): + if Us is None: + Us = fock.squeezer(r=r[idx], phi=phi[idx], cutoffs=out_in) + else: + U_next = fock.squeezer(r=r[idx], phi=phi[idx], cutoffs=out_in) + Us = math.outer(Us, U_next) + return math.transpose( + Us, + list(range(0, 2 * N, 2)) + list(range(1, 2 * N, 2)), + ) + else: + return fock.squeezer(r=r[0], phi=phi[0], cutoffs=tuple(cutoffs)) + @property def X_matrix(self): return gaussian.squeezing_symplectic(self.r.value, self.phi.value) @@ -203,13 +244,24 @@ def __init__( def X_matrix(self): return gaussian.rotation_symplectic(self.angle.value) - def U(self, cutoffs: Sequence[int]): + def U(self, cutoffs: Sequence[int], diag_only=False): + r"""Returns the unitary representation of the Rotation gate. + + Args: + cutoffs (Sequence[int]): cutoff dimension for each mode + diag_only (bool): if True, only return the diagonal of the unitary matrix. + + Returns: + array[complex]: the unitary matrix + """ + if diag_only: + raise NotImplementedError("Rgate does not support diag_only=True yet") + angles = self.angle.value * math.ones(self.num_modes, dtype=self.angle.value.dtype) - num_modes = len(cutoffs) # calculate rotation unitary for each mode and concatenate with outer product Ur = None - for idx, cutoff in enumerate(cutoffs): + for idx, cutoff in enumerate(cutoffs[: self.num_modes]): theta = math.arange(cutoff) * angles[idx] if Ur is None: Ur = math.diag(math.make_complex(math.cos(theta), math.sin(theta))) @@ -220,7 +272,7 @@ def U(self, cutoffs: Sequence[int]): # return total unitary with indexes reordered according to MM convention return math.transpose( Ur, - list(range(0, 2 * num_modes, 2)) + list(range(1, 2 * num_modes, 2)), + list(range(0, 2 * self.num_modes, 2)) + list(range(1, 2 * self.num_modes, 2)), ) @@ -367,6 +419,22 @@ def __init__( self.is_gaussian = True self.short_name = "BS" + def U(self, cutoffs: Optional[List[int]]): + r"""Returns the symplectic transformation matrix for the beam splitter. + + Args: + cutoffs (List[int]): the list of cutoff dimensions for each mode + in the order (out_0, out_1, in_0, in_1). + + Returns: + array[complex]: the unitary tensor of the beamsplitter + """ + return fock.beamsplitter( + self.theta.value, + self.phi.value, + tuple(cutoffs), + ) + @property def X_matrix(self): return gaussian.beam_splitter_symplectic(self.theta.value, self.phi.value) diff --git a/mrmustard/lab/states.py b/mrmustard/lab/states.py index 7d08ff140..ebb9db76e 100644 --- a/mrmustard/lab/states.py +++ b/mrmustard/lab/states.py @@ -81,7 +81,7 @@ class Coherent(Parametrized, State): x_bounds (float or None, float or None): The bounds of the x-displacement. y_bounds (float or None, float or None): The bounds of the y-displacement. modes (optional List[int]): The modes of the coherent state. - cutoffs (Sequence[int], default=None): set to force the cutoff dimensions of the state + cutoffs (Sequence[int], default=None): set to force the fock cutoffs of the state normalize (bool, default False): whether to normalize the leftover state when projecting onto ``Coherent`` """ @@ -486,22 +486,29 @@ def __init__( def _preferred_projection(self, other: State, mode_indices: Sequence[int]): r"""Preferred method to perform a projection onto this state (rather than the default one). - E.g. ``ket << Fock(1, modes=[3])`` is equivalent to ``ket[:,:,:,1]`` if ``ket`` has 4 modes - E.g. ``dm << Fock(1, modes=[1])`` is equivalent to ``dm[:,1,:,1]`` if ``dm`` has 2 modes + E.g. ``ket << Fock([1], modes=[3])`` is equivalent to ``ket[:,:,:,1]`` if ``ket`` has 4 modes + E.g. ``dm << Fock([1], modes=[1])`` is equivalent to ``dm[:,1,:,1]`` if ``dm`` has 2 modes Args: other: the state to project onto this state mode_indices: the indices of the modes of other that we want to project onto self """ getitem = [] + cutoffs = [] used = 0 for i, _ in enumerate(other.modes): if i in mode_indices: getitem.append(self._n[used]) + cutoffs.append(self._n[used] + 1) used += 1 else: getitem.append(slice(None)) - output = other.fock[tuple(getitem)] if other.is_pure else other.fock[tuple(getitem) * 2] + cutoffs.append(other.cutoffs[i]) + output = ( + other.ket(cutoffs)[tuple(getitem)] + if other.is_hilbert_vector + else other.dm(cutoffs)[tuple(getitem) * 2] + ) if self._normalize: return fock.normalize(output, is_dm=other.is_mixed) return output diff --git a/mrmustard/math/lattice/strategies/__init__.py b/mrmustard/math/lattice/strategies/__init__.py new file mode 100644 index 000000000..66a96b326 --- /dev/null +++ b/mrmustard/math/lattice/strategies/__init__.py @@ -0,0 +1,19 @@ +# 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 .beamsplitter import * +from .binomial import * +from .displacement import * +from .squeezer import * +from .vanilla import * diff --git a/mrmustard/math/lattice/strategies/beamsplitter.py b/mrmustard/math/lattice/strategies/beamsplitter.py new file mode 100644 index 000000000..2597ccfd5 --- /dev/null +++ b/mrmustard/math/lattice/strategies/beamsplitter.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. + + +# Recurrencies for Fock-Bargmann amplitudes put together a strategy for +# enumerating the indices in a specific order and functions for calculating +# which neighbours to use in the calculation of the amplitude at a given index. +# In summary, they return the value of the amplitude at the given index by following +# a recipe made of two parts. The function to recompute A and b is determined by +# which neighbours are used. + +"""Fock-Bargmann recurrence relation steps optimized for beamsplitter.""" + +import numpy as np +from numba import njit + +from mrmustard.math.lattice import steps +from mrmustard.typing import ComplexMatrix, ComplexTensor, ComplexVector + +SQRT = np.sqrt(np.arange(100000)) + +__all__ = ["beamsplitter", "beamsplitter_vjp"] + + +@njit +def beamsplitter( + shape: tuple[int, int, int, int], theta: float, phi: float, dtype=np.complex128 +) -> ComplexTensor: # pragma: no cover + r"""Calculates the Fock representation of the beamsplitter. + It takes advantage of input-output particle conservation (m+n=p+q) + to avoid one for loop. Inspired from the original implementation in + the walrus by @ziofil. Here is how the parameters are used in the + code (see eq. 73-75 in https://arxiv.org/abs/2004.11002): + + A = [[0,V],[v^T,0]] # BS bargmann matrix + V = [[ct, -st expm], # BS unitary + [st exp, ct]] + + Args: + shape (tuple[int, int, int, int]): shape of the Fock representation + theta (float): beamsplitter angle + phi (float): beamsplitter phase + dtype (np.dtype): data type of the Fock representation + + Returns: + array (ComplexTensor): The Fock representation of the gate + """ + ct = np.cos(theta) + st = np.sin(theta) * np.exp(1j * phi) + stc = np.conj(st) + + M, N, P, Q = shape + G = np.zeros(shape, dtype=dtype) + G[0, 0, 0, 0] = 1.0 + + # rank 3 + for m in range(M): + for n in range(N - m): + p = m + n + if 0 < p < P: + G[m, n, p, 0] = ( + ct * SQRT[m] / SQRT[p] * G[m - 1, n, p - 1, 0] + + st * SQRT[n] / SQRT[p] * G[m, n - 1, p - 1, 0] + ) + + # rank 4 + for m in range(M): + for n in range(N): + for p in range(P): + q = m + n - p + if 0 < q < Q: + G[m, n, p, q] = ( + -stc * SQRT[m] / SQRT[q] * G[m - 1, n, p, q - 1] + + ct * SQRT[n] / SQRT[q] * G[m, n - 1, p, q - 1] + ) + return G + + +@njit +def beamsplitter_vjp( + G: ComplexTensor, + dLdG: ComplexTensor, + theta: float, + phi: float, +) -> tuple[ComplexMatrix, ComplexVector, complex]: # pragma: no cover + r"""Beamsplitter gradients with respect to theta and phi. + This function could return dL/dA, dL/db, dL/dc like its vanilla counterpart, + but it is more efficient to include this chain rule step in the numba function, + since we can. + + We use these derivatives of the BS unitary: + + dVdt = [[-st, -ct exp], + [ct exp, -st]] + dVdphi = [[0, -i ct expm], + [i ct exp, 0]] + + Args: + G (np.ndarray): Tensor result of the forward pass + dLdG (np.ndarray): gradient of the loss with respect to the output tensor + theta (float): beamsplitter angle + phi (float): beamsplitter phase + + Returns: + tuple[float, float]: dL/dtheta, dL/dphi + """ + M, N, P, Q = G.shape + + # init gradients + dA = np.zeros((4, 4), dtype=np.complex128) + db = np.zeros(4, dtype=np.complex128) + dLdA = np.zeros_like(dA) + dLdb = np.zeros_like(db) + + # rank 3 + for m in range(M): + for n in range(N - m): + p = m + n + if 0 < p < P: + dA, db = steps.vanilla_step_grad(G, (m, n, p, 0), dA, db) + dLdA += dA * dLdG[m, n, p, 0] + dLdb += db * dLdG[m, n, p, 0] + + # rank 4 + for m in range(M): + for n in range(N): + for p in range(P): + q = m + n - p + if 0 < q < Q: + dA, db = steps.vanilla_step_grad(G, (m, n, p, q), dA, db) + dLdA += dA * dLdG[m, n, p, q] + dLdb += db * dLdG[m, n, p, q] + + st = np.sin(theta) + ct = np.cos(theta) + e = np.exp(1j * phi) + em = np.exp(-1j * phi) + + # omitting bottom-left block because dLdA should be zero there + dLdtheta = 2 * np.real( + -st * dLdA[0, 2] - ct * em * dLdA[0, 3] + ct * e * dLdA[1, 2] - st * dLdA[1, 3] + ) + dLdphi = 2 * np.real(1j * st * em * dLdA[0, 3] + 1j * st * e * dLdA[1, 2]) + + return dLdtheta, dLdphi diff --git a/mrmustard/math/lattice/strategies.py b/mrmustard/math/lattice/strategies/binomial.py similarity index 63% rename from mrmustard/math/lattice/strategies.py rename to mrmustard/math/lattice/strategies/binomial.py index 6b90631e9..771675d1a 100644 --- a/mrmustard/math/lattice/strategies.py +++ b/mrmustard/math/lattice/strategies/binomial.py @@ -12,7 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -"""Fock-Bargmann strategies.""" +" This module contains binomial strategies " from typing import Optional @@ -20,99 +20,11 @@ from numba import njit, typed, types from mrmustard.math.lattice import paths, steps -from mrmustard.typing import ComplexMatrix, ComplexTensor, ComplexVector, IntVector +from mrmustard.typing import ComplexMatrix, ComplexTensor, ComplexVector +SQRT = np.sqrt(np.arange(100000)) -@njit -def vanilla(shape: tuple[int, ...], A, b, c) -> ComplexTensor: - r"""Vanilla Fock-Bargmann strategy. Fills the tensor by iterating over all indices - in ndindex order. - - Args: - shape (tuple[int, ...]): shape of the output tensor - A (np.ndarray): A matrix of the Fock-Bargmann representation - b (np.ndarray): B vector of the Fock-Bargmann representation - c (complex): vacuum amplitude - - Returns: - np.ndarray: Fock representation of the Gaussian tensor with shape ``shape`` - """ - - # init output tensor - G = np.zeros(shape, dtype=np.complex128) - - # initialize path iterator - path = np.ndindex(shape) - - # write vacuum amplitude - G[next(path)] = c - - # iterate over the rest of the indices - for index in path: - G[index] = steps.vanilla_step(G, A, b, index) - return G - - -@njit -def vanilla_jacobian(G, A, b, c) -> tuple[ComplexTensor, ComplexTensor, ComplexTensor]: - r"""Vanilla Fock-Bargmann strategy gradient. Returns dG/dA, dG/db, dG/dc. - Notice that G is a holomorphic function of A, b, c. This means that there is only - one gradient to care about for each parameter (i.e. not dG/dA.conj() etc). - """ - - # init output tensors - dGdA = np.zeros(G.shape + A.shape, dtype=np.complex128) - dGdb = np.zeros(G.shape + b.shape, dtype=np.complex128) - dGdc = G / c - - # initialize path iterator - path = paths.ndindex_path(G.shape) - - # skip first index - next(path) - - # iterate over the rest of the indices - for index in path: - dGdA, dGdb = steps.vanilla_step_jacobian(G, A, b, index, dGdA, dGdb) - - return dGdA, dGdb, dGdc - - -@njit -def vanilla_vjp(G, c, dLdG) -> tuple[ComplexMatrix, ComplexVector, complex]: - r"""Vanilla Fock-Bargmann strategy gradient. Returns dL/dA, dL/db, dL/dc. - - Args: - G (np.ndarray): Tensor result of the forward pass - c (complex): vacuum amplitude - dLdG (np.ndarray): gradient of the loss with respect to the output tensor - - Returns: - tuple[np.ndarray, np.ndarray, complex]: dL/dA, dL/db, dL/dc - """ - D = G.ndim - - # init gradients - dA = np.zeros((D, D), dtype=np.complex128) # component of dL/dA - db = np.zeros(D, dtype=np.complex128) # component of dL/db - dLdA = np.zeros_like(dA) - dLdb = np.zeros_like(db) - - # initialize path iterator - path = np.ndindex(G.shape) - - # skip first index - next(path) - - # iterate over the rest of the indices - for index in path: - dA, db = steps.vanilla_step_grad(G, index, dA, db) - dLdA += dA * dLdG[index] - dLdb += db * dLdG[index] - - dLdc = np.sum(G * dLdG) / c - - return dLdA, dLdb, dLdc +__all__ = ["binomial", "binomial_dict", "binomial_numba"] def binomial( @@ -222,7 +134,7 @@ def binomial_numba( FP: dict[tuple[tuple[int, ...], int], list[tuple[int, ...]]], max_prob: float = 0.999, global_cutoff: Optional[int] = None, -) -> ComplexTensor: +) -> ComplexTensor: # pragma: no cover r"""Binomial strategy (fill by weight), fully numba version.""" if global_cutoff is None: global_cutoff = sum(local_cutoffs) - len(local_cutoffs) @@ -246,21 +158,3 @@ def binomial_numba( if prob > max_prob: break return G - - -@njit -def wormhole(shape: IntVector) -> IntVector: - r"wormhole strategy, not implemented yet" - raise NotImplementedError("Wormhole strategy not implemented yet") - - -@njit -def diagonal(shape: IntVector) -> IntVector: - r"diagonal strategy, not implemented yet" - raise NotImplementedError("Diagonal strategy not implemented yet") - - -@njit -def dynamic_U(shape: IntVector) -> IntVector: - r"dynamic U strategy, not implemented yet" - raise NotImplementedError("Dynamic strategy not implemented yet") diff --git a/mrmustard/math/lattice/strategies/displacement.py b/mrmustard/math/lattice/strategies/displacement.py new file mode 100644 index 000000000..79c3568f8 --- /dev/null +++ b/mrmustard/math/lattice/strategies/displacement.py @@ -0,0 +1,139 @@ +# 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 strategies for calculating the matrix elements of the displacement gate. " + +import numpy as np +from numba import jit + +__all__ = ["displacement", "laguerre", "grad_displacement", "jacobian_displacement"] + + +@jit(nopython=True) +def displacement(cutoffs, alpha, dtype=np.complex128): # pragma: no cover + r"""Calculates the matrix elements of the displacement gate using a recurrence relation. + Uses the log of the matrix elements to avoid numerical issues and then takes the exponential. + + Args: + alpha (complex): displacement magnitude and angle + cutoffs (tuple[int, int]): Fock ladder output-input cutoffs + dtype (data type): Specifies the data type used for the calculation + + Returns: + array[complex]: matrix representing the displacement operation. + """ + r = np.abs(alpha) + phi = np.angle(alpha) + N, M = cutoffs + flipped = False + if N < M: + N, M = M, N + flipped = True + + D = np.zeros((N, M), dtype=dtype) + rng = np.arange(max(*cutoffs)) + rng[0] = 1 + log_k_fac = np.cumsum(np.log(rng)) + for n_minus_m in range(N): + m_max = min(M, N - n_minus_m) + logL = np.log(laguerre(r**2.0, m_max, n_minus_m)) + for m in range(m_max): + n = n_minus_m + m + sign = 2 * (not (flipped and n > m and n_minus_m % 2)) - 1 + conj = 2 * (not (flipped and n > m)) - 1 + D[n, m] = sign * np.exp( + +0.5 * (log_k_fac[m] - log_k_fac[n]) + + n_minus_m * np.log(r) + - (r**2.0) / 2.0 + + conj * 1j * phi * n_minus_m + + logL[m] + ) + if n < M: + D[m, n] = (-1.0) ** n_minus_m * np.conj(D[n, m]) + return D if not flipped else np.transpose(D) + + +@jit(nopython=True, cache=True) +def laguerre(x, N, alpha, dtype=np.complex128): # pragma: no cover + r"""Returns the N first generalized Laguerre polynomials evaluated at x. + + Args: + x (float): point at which to evaluate the polynomials + N (int): maximum Laguerre polynomial to calculate + alpha (float): continuous parameter for the generalized Laguerre polynomials + """ + L = np.zeros(N, dtype=dtype) + L[0] = 1.0 + if N > 1: + for m in range(0, N - 1): + L[m + 1] = ((2 * m + 1 + alpha - x) * L[m] - (m + alpha) * L[m - 1]) / (m + 1) + return L + + +@jit(nopython=True) +def grad_displacement(T, r, phi): # pragma: no cover + r"""Calculates the gradient of the displacement gate with respect to the magnitude and angle of the displacement. + + Args: + T (array[complex]): array representing the gate + r (float): displacement magnitude + phi (float): displacement angle + + Returns: + tuple[array[complex], array[complex]]: The gradient of the displacement gate with respect to r and phi + """ + cutoff = T.shape[0] + dtype = T.dtype + ei = np.exp(1j * phi) + eic = np.exp(-1j * phi) + alpha = r * ei + alphac = r * eic + sqrt = np.sqrt(np.arange(cutoff, dtype=dtype)) + grad_r = np.zeros((cutoff, cutoff), dtype=dtype) + grad_phi = np.zeros((cutoff, cutoff), dtype=dtype) + + for m in range(cutoff): + for n in range(cutoff): + grad_r[m, n] = -r * T[m, n] + sqrt[m] * ei * T[m - 1, n] - sqrt[n] * eic * T[m, n - 1] + grad_phi[m, n] = ( + sqrt[m] * 1j * alpha * T[m - 1, n] + sqrt[n] * 1j * alphac * T[m, n - 1] + ) + + return grad_r, grad_phi + + +@jit(nopython=True) +def jacobian_displacement(D, alpha): # pragma: no cover + r"""Calculates the jacobian of the displacement gate with respect to the complex displacement + alpha and its conjugate. Both are needed for backprop, as the displacement gate is not a + holomorphic function, as the Fock amplitudes depend on both alpha and on its conjugate. + Each jacobian in this case has the same shape as the array D, as the displacement is a scalar. + + Args: + D (array[complex]): the D(alpha) gate in Fock representation. Batch dimensions are allowed. + alpha (complex): parameter of D(alpha) + + Returns: + 2 array[complex]: The jacobian of the displacement gate with respect to alpha and alphaconj + """ + shape = D.shape[-2:] + alphac = np.conj(alpha) + sqrt = np.sqrt(np.arange(shape[0] + shape[1], dtype=D.dtype)) + jac_alpha = np.zeros(D.shape, dtype=D.dtype) # i.e. dD_dalpha for all m,n + jac_alphac = np.zeros(D.shape, dtype=D.dtype) # i.e. dD_dalphac for all m,n + for m in range(shape[0]): + for n in range(shape[1]): + jac_alpha[m, n] = -0.5 * alphac * D[m, n] + sqrt[m] * D[m - 1, n] + jac_alphac[m, n] = -0.5 * alpha * D[m, n] - sqrt[n] * D[m, n - 1] + return jac_alpha, jac_alphac diff --git a/mrmustard/math/lattice/strategies/squeezer.py b/mrmustard/math/lattice/strategies/squeezer.py new file mode 100644 index 000000000..f388ac718 --- /dev/null +++ b/mrmustard/math/lattice/strategies/squeezer.py @@ -0,0 +1,188 @@ +# 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 strategies for calculating the matrix elements of the squeezing gate. " + +import numpy as np +from numba import njit + +from mrmustard.math.lattice import steps +from mrmustard.typing import ComplexTensor + +SQRT = np.sqrt(np.arange(100000)) + +__all__ = ["squeezer", "squeezer_vjp", "squeezed", "squeezed_vjp"] + + +@njit +def squeezer( + cutoffs: tuple[int, int], r: float, theta: float, dtype=np.complex128 +): # pragma: no cover + r"""Calculates the matrix elements of the squeezing gate using a recurrence relation. + (See eq. 50-52 in https://arxiv.org/abs/2004.11002) + Args: + cutoffs (tuple[int, int]): Fock cutoffs for the output and input modes. + r (float): squeezing magnitude + theta (float): squeezing angle + dtype (data type): data type used for the calculation. + + Returns: + array (ComplexMatrix): matrix representing the squeezing gate. + """ + M, N = cutoffs + S = np.zeros(cutoffs, dtype=dtype) + + eitheta_tanhr = np.exp(1j * theta) * np.tanh(r) + eitheta_tanhr_conj = np.conj(eitheta_tanhr) + sechr = 1.0 / np.cosh(r) + + S[0, 0] = np.sqrt(sechr) + for m in range(2, M, 2): + S[m, 0] = -SQRT[m - 1] / SQRT[m] * eitheta_tanhr * S[m - 2, 0] + + for m in range(M): + for n in range(2 - (m % 2), N, 2): + # for n in range(1, N): + if (m + n) % 2 == 0: + S[m, n] = ( + SQRT[n - 1] / SQRT[n] * eitheta_tanhr_conj * S[m, n - 2] + + SQRT[m] / SQRT[n] * sechr * S[m - 1, n - 1] + ) + return S + + +@njit +def squeezer_vjp( + G: ComplexTensor, + dLdG: ComplexTensor, + r: float, + phi: float, +) -> tuple[float, float]: # pragma: no cover + r"""Squeezing gradients with respect to r and theta. + This function could return dL/dA, dL/db, dL/dc like its vanilla counterpart, + but it is more efficient to include this chain rule step in the numba function, since we can. + + Args: + G (np.ndarray): Tensor result of the forward pass + dLdG (np.ndarray): gradient of the loss with respect to the output tensor + r (float): squeezing magnitude + phi (float): squeezing angle + + Returns: + tuple[float, float]: dL/dr, dL/phi + """ + M, N = G.shape + + # init gradients + dA = np.zeros((2, 2), dtype=np.complex128) # dGdA at an index (of G) + _ = np.zeros(2, dtype=np.complex128) + dLdA = np.zeros_like(dA) + + # first column + for m in range(2, M, 2): + dA, _ = steps.vanilla_step_grad(G, (m, 0), dA, _) + dLdA += dA * dLdG[m, 0] + + # rest of the matrix + for m in range(M): + for n in range(1, N): + if (m + n) % 2 == 0: + dA, _ = steps.vanilla_step_grad(G, (m, n), dA, _) + dLdA += dA * dLdG[m, n] + + dLdC = np.sum(G * dLdG) # np.sqrt(np.cosh(r)) cancels out with 1 / np.sqrt(np.cosh(r)) later + # chain rule + d_sech = -np.tanh(r) / np.cosh(r) + d_tanh = 1.0 / np.cosh(r) ** 2 + tanh = np.tanh(r) + exp = np.exp(1j * phi) + exp_conj = np.exp(-1j * phi) + + dLdr = 2 * np.real( + -dLdA[0, 0] * exp * d_tanh + + dLdA[0, 1] * d_sech + + dLdA[1, 1] * exp_conj * d_tanh + - np.conj(dLdC) * 0.5 * tanh # / np.sqrt(np.cosh(r)) + ) + dLdphi = 2 * np.real(-dLdA[0, 0] * 1j * exp * tanh - dLdA[1, 1] * 1j * exp_conj * tanh) + + return dLdr, dLdphi + + +@njit +def squeezed(cutoff: int, r: float, theta: float, dtype=np.complex128): # pragma: no cover + r"""Calculates the matrix elements of the single-mode squeezed state using recurrence relations. + + Args: + cutoff (int): Fock cutoff for the ket + r (float): squeezing magnitude + theta (float): squeezing angle + dtype (data type): data type used for the calculation. + + Returns: + array (ComplexMatrix): matrix representing the squeezing gate. + """ + S = np.zeros(cutoff, dtype=dtype) + eitheta_tanhr = np.exp(1j * theta) * np.tanh(r) + S[0] = np.sqrt(1.0 / np.cosh(r)) + + for m in range(2, cutoff, 2): + S[m] = SQRT[m - 1] / SQRT[m] * eitheta_tanhr * S[m - 2] + + return S + + +@njit +def squeezed_vjp( + G: ComplexTensor, + dLdG: ComplexTensor, + r: float, + phi: float, +) -> tuple[float, float]: # pragma: no cover + r"""Squeezed state gradients with respect to r and theta. + This function could return dL/dA, dL/db, dL/dc like its vanilla counterpart, + but it is more efficient to include this chain rule step in the numba function, since we can. + + Args: + G (np.ndarray): Tensor result of the forward pass + dLdG (np.ndarray): gradient of the loss with respect to the output tensor + r (float): squeezing magnitude + phi (float): squeezing angle + + Returns: + tuple[float, float]: dL/dr, dL/phi + """ + M = G.shape[0] + + # init gradients + dA = np.zeros((1, 1), dtype=np.complex128) + _ = np.zeros(1, dtype=np.complex128) + dLdA = np.zeros_like(dA) + + # first column + for m in range(2, M, 2): + dA, _ = steps.vanilla_step_grad(G, (m,), dA, _) + dLdA += dA * dLdG[m] + + # chain rule + tanh = np.tanh(r) + d_tanh = 1.0 / np.cosh(r) ** 2 + exp = np.exp(1j * phi) + + dLdC = np.sum(G * dLdG) # np.sqrt(np.cosh(r)) cancels out with 1 / np.sqrt(np.cosh(r)) later + + dLdr = 2 * np.real(-dLdA[0, 0] * exp * d_tanh - np.conj(dLdC) * 0.5 * tanh) + dLdphi = 2 * np.real(-dLdA[0, 0] * 1j * exp * tanh) + + return dLdr, dLdphi diff --git a/mrmustard/math/lattice/strategies/vanilla.py b/mrmustard/math/lattice/strategies/vanilla.py new file mode 100644 index 000000000..6943f98cf --- /dev/null +++ b/mrmustard/math/lattice/strategies/vanilla.py @@ -0,0 +1,117 @@ +# 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 +from numba import njit + +from mrmustard.math.lattice import paths, steps +from mrmustard.typing import ComplexMatrix, ComplexTensor, ComplexVector + +SQRT = np.sqrt(np.arange(100000)) + +__all__ = ["vanilla", "vanilla_jacobian", "vanilla_vjp"] + + +@njit +def vanilla(shape: tuple[int, ...], A, b, c) -> ComplexTensor: # pragma: no cover + r"""Vanilla Fock-Bargmann strategy. Fills the tensor by iterating over all indices + in ndindex order. + + Args: + shape (tuple[int, ...]): shape of the output tensor + A (np.ndarray): A matrix of the Fock-Bargmann representation + b (np.ndarray): B vector of the Fock-Bargmann representation + c (complex): vacuum amplitude + + Returns: + np.ndarray: Fock representation of the Gaussian tensor with shape ``shape`` + """ + + # init output tensor + G = np.zeros(shape, dtype=np.complex128) + + # initialize path iterator + path = np.ndindex(shape) + + # write vacuum amplitude + G[next(path)] = c + + # iterate over the rest of the indices + for index in path: + G[index] = steps.vanilla_step(G, A, b, index) + return G + + +@njit +def vanilla_jacobian( + G, A, b, c +) -> tuple[ComplexTensor, ComplexTensor, ComplexTensor]: # pragma: no cover + r"""Vanilla Fock-Bargmann strategy gradient. Returns dG/dA, dG/db, dG/dc. + Notice that G is a holomorphic function of A, b, c. This means that there is only + one gradient to care about for each parameter (i.e. not dG/dA.conj() etc). + """ + + # init output tensors + dGdA = np.zeros(G.shape + A.shape, dtype=np.complex128) + dGdb = np.zeros(G.shape + b.shape, dtype=np.complex128) + dGdc = G / c + + # initialize path iterator + path = paths.ndindex_path(G.shape) + + # skip first index + next(path) + + # iterate over the rest of the indices + for index in path: + dGdA, dGdb = steps.vanilla_step_jacobian(G, A, b, index, dGdA, dGdb) + + return dGdA, dGdb, dGdc + + +@njit +def vanilla_vjp(G, c, dLdG) -> tuple[ComplexMatrix, ComplexVector, complex]: # pragma: no cover + r"""Vanilla Fock-Bargmann strategy gradient. Returns dL/dA, dL/db, dL/dc. + + Args: + G (np.ndarray): Tensor result of the forward pass + c (complex): vacuum amplitude + dLdG (np.ndarray): gradient of the loss with respect to the output tensor + + Returns: + tuple[np.ndarray, np.ndarray, complex]: dL/dA, dL/db, dL/dc + """ + D = G.ndim + + # init gradients + dA = np.zeros((D, D), dtype=np.complex128) # component of dL/dA + db = np.zeros(D, dtype=np.complex128) # component of dL/db + dLdA = np.zeros_like(dA) + dLdb = np.zeros_like(db) + + # initialize path iterator + path = np.ndindex(G.shape) + + # skip first index + next(path) + + # iterate over the rest of the indices + for index in path: + dA, db = steps.vanilla_step_grad(G, index, dA, db) + dLdA += dA * dLdG[index] + dLdb += db * dLdG[index] + + dLdc = np.sum(G * dLdG) / c + + return dLdA, dLdb, dLdc diff --git a/mrmustard/math/numba/compactFock_1leftoverMode_amps.py b/mrmustard/math/numba/compactFock_1leftoverMode_amps.py index f6751f30a..c0666a8be 100644 --- a/mrmustard/math/numba/compactFock_1leftoverMode_amps.py +++ b/mrmustard/math/numba/compactFock_1leftoverMode_amps.py @@ -3,21 +3,22 @@ This is done by applying the recursion relation in a selective manner. """ -import numpy as np import numba -from numba import njit, int64 +import numpy as np +from numba import int64, njit from numba.cpython.unsafe.tuple import tuple_setitem + from mrmustard.math.numba.compactFock_helperFunctions import ( SQRT, - repeat_twice, construct_dict_params, + repeat_twice, ) @njit def write_block( i, arr_write, write, arr_read_pivot, read_GB, G_in, GB, A, K_i, cutoff_leftoverMode -): +): # pragma: no cover """ Apply the recurrence relation to blocks of Fock amplitudes (of shape cutoff_leftoverMode x cutoff_leftoverMode) This is the coarse-grained version of applying the recurrence relation of mrmustard.math.numba.compactFock_diagonal_amps once. @@ -62,7 +63,9 @@ def write_block( @njit -def read_block(arr_write, idx_write, arr_read, idx_read_tail, cutoff_leftoverMode): +def read_block( + arr_write, idx_write, arr_read, idx_read_tail, cutoff_leftoverMode +): # pragma: no cover """ Read the blocks of Fock amplitudes (of shape cutoff_leftoverMode x cutoff_leftoverMode) that are required to apply the recurrence relation and write them to G_in diff --git a/mrmustard/physics/fock.py b/mrmustard/physics/fock.py index a6a51c693..45be29792 100644 --- a/mrmustard/physics/fock.py +++ b/mrmustard/physics/fock.py @@ -22,11 +22,11 @@ from typing import List, Optional, Sequence, Tuple import numpy as np -from numba import jit from mrmustard import settings from mrmustard.math import Math from mrmustard.math.caching import tensor_int_cache +from mrmustard.math.lattice import strategies from mrmustard.math.mmtensor import MMTensor from mrmustard.math.numba.compactFock_diagonal_amps import fock_representation_diagonal_amps from mrmustard.physics.bargmann import ( @@ -35,7 +35,7 @@ wigner_to_bargmann_rho, wigner_to_bargmann_U, ) -from mrmustard.typing import Matrix, Scalar, Tensor, Vector +from mrmustard.typing import ComplexTensor, Matrix, Scalar, Tensor, Vector math = Math() SQRT = np.sqrt(np.arange(1e6)) @@ -130,7 +130,7 @@ def wigner_to_fock_state( return math.hermite_renormalized_binomial( A, B, C, shape=shape, max_l2=max_prob, global_cutoff=max_photons + 1 ) - return math.hermite_renormalized(A, B, C, shape=shape) + return math.hermite_renormalized(A, B, C, shape=tuple(shape)) def wigner_to_fock_U(X, d, shape): @@ -147,7 +147,7 @@ def wigner_to_fock_U(X, d, shape): Tensor: the fock representation of the unitary transformation """ A, B, C = wigner_to_bargmann_U(X, d) - return math.hermite_renormalized(A, B, C, shape=shape) + return math.hermite_renormalized(A, B, C, shape=tuple(shape)) def wigner_to_fock_Choi(X, Y, d, shape): @@ -165,7 +165,7 @@ def wigner_to_fock_Choi(X, Y, d, shape): Tensor: the fock representation of the Choi matrix """ A, B, C = wigner_to_bargmann_Choi(X, Y, d) - return math.hermite_renormalized(A, B, C, shape=shape) + return math.hermite_renormalized(A, B, C, shape=tuple(shape)) def ket_to_dm(ket: Tensor) -> Tensor: @@ -237,17 +237,18 @@ def dm_to_probs(dm: Tensor) -> Tensor: return math.all_diagonals(dm, real=True) -def U_to_choi(U: Tensor) -> Tensor: +def U_to_choi(U: Tensor, Udual: Optional[Tensor] = None) -> Tensor: r"""Converts a unitary transformation to a Choi tensor. Args: U: the unitary transformation + Udual: the dual unitary transformation (optional, will use conj U if not provided) Returns: Tensor: the Choi tensor. The index order is going to be :math:`[\mathrm{out}_l, \mathrm{in}_l, \mathrm{out}_r, \mathrm{in}_r]` where :math:`\mathrm{in}_l` and :math:`\mathrm{in}_r` are to be contracted with the left and right indices of the density matrix. """ - return math.outer(U, math.conj(U)) + return math.outer(U, Udual or math.conj(U)) def fidelity(state_a, state_b, a_ket: bool, b_ket: bool) -> Scalar: @@ -382,10 +383,11 @@ def apply_kraus_to_ket(kraus, ket, kraus_in_idx, kraus_out_idx=None): # check that there are no repeated indices in kraus_in_idx and kraus_out_idx (separately) validate_contraction_indices(kraus_in_idx, kraus_out_idx, ket.ndim, "kraus") - ket = MMTensor(ket, axis_labels=[f"left_{i}" for i in range(ket.ndim)]) + ket = MMTensor(ket, axis_labels=[f"in_left_{i}" for i in range(ket.ndim)]) kraus = MMTensor( kraus, - axis_labels=[f"out_left_{i}" for i in kraus_out_idx] + [f"left_{i}" for i in kraus_in_idx], + axis_labels=[f"out_left_{i}" for i in kraus_out_idx] + + [f"in_left_{i}" for i in kraus_in_idx], ) # contract the operator with the ket. @@ -452,42 +454,46 @@ def apply_kraus_to_dm(kraus, dm, kraus_in_idx, kraus_out_idx=None): return k_dm_k.transpose(left + right).tensor -def apply_choi_to_dm(choi, dm, choi_in_idx, choi_out_idx=None): +def apply_choi_to_dm( + choi: ComplexTensor, + dm: ComplexTensor, + choi_in_modes: Sequence[int], + choi_out_modes: Sequence[int] = None, +): r"""Applies a choi operator to a density matrix. It assumes that the density matrix is indexed as left_1, ..., left_n, right_1, ..., right_n. - The choi operator has indices that contract with the density matrix (choi_in_idx) and indices that are left over (choi_out_idx). - `choi` will contract choi_in_idx from the left and from the right with the density matrix. + The choi operator has indices that contract with the density matrix (choi_in_modes) and indices that are left over (choi_out_modes). + `choi` will contract choi_in_modes from the left and from the right with the density matrix. Args: choi (array): the choi operator to be applied dm (array): the density matrix to which the choi operator is applied - choi_in_idx (list of ints): the indices of the choi operator that contract with the density matrix - choi_out_idx (list of ints): the indices of the choi operator that re leftover + choi_in_modes (list of ints): the input modes of the choi operator that contract with the density matrix + choi_out_modes (list of ints): the output modes of the choi operator Returns: array: the resulting density matrix """ - if choi_out_idx is None: - choi_out_idx = choi_in_idx - - if not set(choi_in_idx).issubset(range(dm.ndim // 2)): + if choi_out_modes is None: + choi_out_modes = choi_in_modes + if not set(choi_in_modes).issubset(range(dm.ndim // 2)): raise ValueError("choi_in_idx should be a subset of the density matrix indices.") # check that there are no repeated indices in kraus_in_idx and kraus_out_idx (separately) - validate_contraction_indices(choi_in_idx, choi_out_idx, dm.ndim // 2, "choi") + validate_contraction_indices(choi_in_modes, choi_out_modes, dm.ndim // 2, "choi") dm = MMTensor( dm, - axis_labels=[f"left_{i}" for i in range(dm.ndim // 2)] - + [f"right_{i}" for i in range(dm.ndim // 2)], + axis_labels=[f"in_left_{i}" for i in range(dm.ndim // 2)] + + [f"in_right_{i}" for i in range(dm.ndim // 2)], ) choi = MMTensor( choi, - axis_labels=[f"out_left_{i}" for i in choi_out_idx] - + [f"left_{i}" for i in choi_in_idx] - + [f"out_right_{i}" for i in choi_out_idx] - + [f"right_{i}" for i in choi_in_idx], + axis_labels=[f"out_left_{i}" for i in choi_out_modes] + + [f"in_left_{i}" for i in choi_in_modes] + + [f"out_right_{i}" for i in choi_out_modes] + + [f"in_right_{i}" for i in choi_in_modes], ) # contract the choi matrix with the density matrix. @@ -852,101 +858,70 @@ def sample_homodyne( return homodyne_sample, probability_sample -@jit(nopython=True) -def _displacement(r, phi, cutoff, dtype=np.complex128): # pragma: no cover - r"""Calculates the matrix elements of the displacement gate using a recurrence relation. - Uses the log of the matrix elements to avoid numerical issues and then takes the exponential. - - Args: - r (float): displacement magnitude - phi (float): displacement angle - cutoff (int): Fock ladder cutoff - dtype (data type): Specifies the data type used for the calculation +@math.custom_gradient +def displacement(x, y, shape, tol=1e-15): + r"""creates a single mode displacement matrix""" + alpha = math.asnumpy(x) + 1j * math.asnumpy(y) - Returns: - array[complex]: matrix representing the displacement operation. - """ - D = np.zeros((cutoff, cutoff), dtype=dtype) - rng = np.arange(cutoff) - rng[0] = 1 - log_k_fac = np.cumsum(np.log(rng)) - for n_minus_m in range(cutoff): - m_max = cutoff - n_minus_m - logL = np.log(_laguerre(r**2.0, m_max, n_minus_m)) - for m in range(m_max): - n = n_minus_m + m - D[n, m] = np.exp( - 0.5 * (log_k_fac[m] - log_k_fac[n]) - + n_minus_m * np.log(r) - - (r**2.0) / 2.0 - + 1j * phi * n_minus_m - + logL[m] - ) - D[m, n] = (-1.0) ** (n_minus_m) * np.conj(D[n, m]) - return D + if np.sqrt(x * x + y * y) > tol: + gate = strategies.displacement(tuple(shape), alpha) + else: + gate = math.eye(max(shape), dtype="complex128")[: shape[0], : shape[1]] + def grad(dL_dDc): + dD_da, dD_dac = strategies.jacobian_displacement(math.asnumpy(gate), alpha) + dL_dac = np.conj(dL_dDc) * dD_dac + dL_dDc * np.conj(dD_da) + dLdx = 2 * np.real(dL_dac) + dLdy = 2 * np.imag(dL_dac) + return dLdx, dLdy -@jit(nopython=True, cache=True) -def _laguerre(x, N, alpha, dtype=np.complex128): # pragma: no cover - r"""Returns the N first generalized Laguerre polynomials evaluated at x. + return gate, grad - Args: - x (float): point at which to evaluate the polynomials - N (int): maximum Laguerre polynomial to calculate - alpha (float): continuous parameter for the generalized Laguerre polynomials - """ - L = np.zeros(N, dtype=dtype) - L[0] = 1.0 - if N > 1: - for m in range(0, N - 1): - L[m + 1] = ((2 * m + 1 + alpha - x) * L[m] - (m + alpha) * L[m - 1]) / (m + 1) - return L +@math.custom_gradient +def beamsplitter(theta: float, phi: float, cutoffs: Sequence[int]): + r"""creates a beamsplitter tensor with given cutoffs using a numba-based fock lattice strategy""" + bs_unitary = strategies.beamsplitter(tuple(cutoffs), math.asnumpy(theta), math.asnumpy(phi)) + + def vjp(dLdGc): + return strategies.beamsplitter_vjp( + math.asnumpy(bs_unitary), + math.asnumpy(math.conj(dLdGc)), + math.asnumpy(theta), + math.asnumpy(phi), + ) -@jit(nopython=True) -def _grad_displacement(T, r, phi): # pragma: no cover - r"""Calculates the gradients of the displacement gate with respect to the displacement magnitude and angle. + return bs_unitary, vjp - Args: - T (array[complex]): array representing the gate - r (float): displacement magnitude - phi (float): displacement angle - Returns: - tuple[array[complex], array[complex]]: The gradient of the displacement gate with respect to r and phi - """ - cutoff = T.shape[0] - dtype = T.dtype - ei = np.exp(1j * phi) - eic = np.exp(-1j * phi) - alpha = r * ei - alphac = r * eic - sqrt = np.sqrt(np.arange(cutoff, dtype=dtype)) - grad_r = np.zeros((cutoff, cutoff), dtype=dtype) - grad_phi = np.zeros((cutoff, cutoff), dtype=dtype) - - for m in range(cutoff): - for n in range(cutoff): - grad_r[m, n] = -r * T[m, n] + sqrt[m] * ei * T[m - 1, n] - sqrt[n] * eic * T[m, n - 1] - grad_phi[m, n] = ( - sqrt[m] * 1j * alpha * T[m - 1, n] + sqrt[n] * 1j * alphac * T[m, n - 1] - ) +@math.custom_gradient +def squeezer(r, phi, cutoffs): + r"""creates a single mode squeezer matrix using a numba-based fock lattice strategy""" + sq_unitary = strategies.squeezer(tuple(cutoffs), math.asnumpy(r), math.asnumpy(phi)) + + def vjp(dLdGc): + dr, dphi = strategies.squeezer_vjp( + math.asnumpy(sq_unitary), + math.asnumpy(math.conj(dLdGc)), + math.asnumpy(r), + math.asnumpy(phi), + ) + return dr, dphi - return grad_r, grad_phi + return sq_unitary, vjp @math.custom_gradient -def displacement(r, phi, cutoff, tol=1e-15): - """creates a single mode displacement matrix""" - if r > tol: - gate = _displacement(math.asnumpy(r), math.asnumpy(phi), cutoff) - else: - gate = math.eye(cutoff, dtype="complex128") - - def grad(dy): # pragma: no cover - Dr, Dphi = _grad_displacement(math.asnumpy(gate), math.asnumpy(r), math.asnumpy(phi)) - grad_r = math.real(math.sum(dy * math.conj(Dr))) - grad_phi = math.real(math.sum(dy * math.conj(Dphi))) - return grad_r, grad_phi, None +def squeezed(r, phi, cutoffs): + r"""creates a single mode squeezed state using a numba-based fock lattice strategy""" + sq_ket = strategies.squeezed(tuple(cutoffs), math.asnumpy(r), math.asnumpy(phi)) + + def vjp(dLdGc): + return strategies.squeezed_vjp( + math.asnumpy(sq_ket), + math.asnumpy(math.conj(dLdGc)), + math.asnumpy(r), + math.asnumpy(phi), + ) - return gate, grad + return sq_ket, vjp diff --git a/mrmustard/utils/wigner.py b/mrmustard/utils/wigner.py index f6162536a..4941b6d0b 100644 --- a/mrmustard/utils/wigner.py +++ b/mrmustard/utils/wigner.py @@ -14,8 +14,9 @@ """This module contains the calculation of the Wigner function.""" -from numba import njit import numpy as np +from numba import njit + from mrmustard import settings @@ -27,7 +28,7 @@ def wigner_discretized(rho, qvec, pvec, hbar=settings.HBAR): Args: rho (complex array): the density matrix of the state in Fock representation - xvec (array): array of discretized :math:`x` quadrature values + qvec (array): array of discretized :math:`q` quadrature values pvec (array): array of discretized :math:`p` quadrature values hbar (optional float): the value of `\hbar`, defaults to ``settings.HBAR``. @@ -40,7 +41,7 @@ def wigner_discretized(rho, qvec, pvec, hbar=settings.HBAR): P = np.outer(np.ones_like(qvec), pvec) cutoff = rho.shape[-1] - A = (Q + P * 1.0j) / (2 * np.sqrt(hbar / 2)) + A = (Q + P * 1.0j) / np.sqrt(2 * hbar) Wmat = np.zeros((2, cutoff) + A.shape, dtype=np.complex128) diff --git a/tests/test_lab/test_circuit.py b/tests/test_lab/test_circuit.py index b8e9954db..c88068e38 100644 --- a/tests/test_lab/test_circuit.py +++ b/tests/test_lab/test_circuit.py @@ -12,14 +12,11 @@ # See the License for the specific language governing permissions and # limitations under the License. -import numpy as np -import pytest -from hypothesis import given, strategies as st, assume -from hypothesis.extra.numpy import arrays -from mrmustard.physics import gaussian + +from hypothesis import given + from mrmustard.lab import * -from mrmustard import settings -from tests import random +from tests.random import angle, medium_float, n_mode_pure_state, r def test_circuit_placement_SD(): @@ -53,3 +50,12 @@ def test_is_unitary(): assert Ggate(1).is_unitary assert (Ggate(1) >> Ggate(1)).is_unitary assert not (Ggate(2) >> Attenuator([0.1, 0.2])).is_unitary + + +@given( + r=r, phi1=angle, phi2=angle, x=medium_float, y=medium_float, G=n_mode_pure_state(num_modes=1) +) +def test_shift(r, phi1, phi2, x, y, G): + "test that the leftshift/rightshift operator works as expected" + circ = Sgate(r, phi1) >> Dgate(x, y) >> Rgate(phi2) + assert G == (circ << G) >> circ diff --git a/tests/test_lab/test_gates_fock.py b/tests/test_lab/test_gates_fock.py index 663732bdf..53ac46a6b 100644 --- a/tests/test_lab/test_gates_fock.py +++ b/tests/test_lab/test_gates_fock.py @@ -17,6 +17,7 @@ from hypothesis import given from thewalrus.fock_gradients import ( beamsplitter, + displacement, mzgate, squeezing, two_mode_squeezing, @@ -35,6 +36,8 @@ Sgate, ) from mrmustard.lab.states import TMSV, Fock, SqueezedVacuum, State +from mrmustard.math import Math +from mrmustard.math.lattice import strategies from mrmustard.physics import fock from tests.random import ( angle, @@ -47,6 +50,8 @@ two_mode_unitary_gate, ) +math = Math() + @given(state=n_mode_pure_state(num_modes=1), x=medium_float, y=medium_float) def test_Dgate_1mode(state, x, y): @@ -97,7 +102,7 @@ def test_single_mode_fock_equals_gaussian_ket(gate): def test_single_mode_fock_equals_gaussian_ket_dm(gate): """Test same state is obtained via fock representation or phase space for single mode circuits.""" - cutoffs = [70] + cutoffs = (70,) gaussian_state = SqueezedVacuum(-0.1) fock_state = State(ket=gaussian_state.ket(cutoffs)) @@ -110,7 +115,7 @@ def test_single_mode_fock_equals_gaussian_ket_dm(gate): def test_two_mode_fock_equals_gaussian(gate): """Test same state is obtained via fock representation or phase space for two modes circuits.""" - cutoffs = [20, 20] + cutoffs = (20, 20) gaussian_state = TMSV(0.1) >> BSgate(np.pi / 2) >> Attenuator(0.5) fock_state = State(dm=gaussian_state.dm(cutoffs)) @@ -122,12 +127,12 @@ def test_two_mode_fock_equals_gaussian(gate): @pytest.mark.parametrize( "cutoffs,x,y", [ - [[5], 0.3, 0.5], - [[5], 0.0, 0.0], - [[2, 2], [0.1, 0.1], [0.25, -0.2]], - [[3, 3], [0.0, 0.0], [0.0, 0.0]], - [[2, 5, 1], [0.1, 5.0, 1.0], [-0.3, 0.1, 0.0]], - [[3, 3, 3, 3], [0.1, 0.2, 0.3, 0.4], [-0.5, -4, 3.1, 4.2]], + [[5, 5], 0.3, 0.5], + [[5, 5], 0.0, 0.0], + [[2, 2, 2, 2], [0.1, 0.1], [0.25, -0.2]], + [[3, 3, 3, 3], [0.0, 0.0], [0.0, 0.0]], + [[2, 5, 1, 2, 5, 1], [0.1, 5.0, 1.0], [-0.3, 0.1, 0.0]], + [[3, 3, 3, 3, 3, 3, 3, 3], [0.1, 0.2, 0.3, 0.4], [-0.5, -4, 3.1, 4.2]], ], ) def test_fock_representation_displacement(cutoffs, x, y): @@ -139,8 +144,80 @@ def test_fock_representation_displacement(cutoffs, x, y): # compare with the standard way of calculating # transformation unitaries using the Choi isomorphism - X = np.eye(2 * len(cutoffs)) - expected_Ud = fock.wigner_to_fock_U(X, dgate.XYd[-1], cutoffs * 2) + X, _, d = dgate.XYd(allow_none=False) + expected_Ud = fock.wigner_to_fock_U(X, d, cutoffs) + + assert np.allclose(Ud, expected_Ud, atol=1e-5) + + +@given(x1=medium_float, x2=medium_float, y1=medium_float, y2=medium_float) +def test_parallel_displacement(x1, x2, y1, y2): + """Tests that parallel Dgate returns the correct unitary.""" + U12 = Dgate([x1, x2], [y1, y2]).U([2, 7, 2, 7]) + U1 = Dgate(x1, y1).U([2, 2]) + U2 = Dgate(x2, y2).U([7, 7]) + assert np.allclose(U12, np.transpose(np.tensordot(U1, U2, [[], []]), [0, 2, 1, 3])) + + +def test_squeezer_grad(): + """tests fock squeezer gradient against finite differences""" + cutoffs = [5, 5] + r = math.new_variable(0.5, None, "r") + phi = math.new_variable(0.1, None, "phi") + delta = 1e-6 + dUdr = (Sgate(r + delta, phi).U(cutoffs) - Sgate(r - delta, phi).U(cutoffs)) / (2 * delta) + dUdphi = (Sgate(r, phi + delta).U(cutoffs) - Sgate(r, phi - delta).U(cutoffs)) / (2 * delta) + _, (gradr, gradphi) = math.value_and_gradients( + lambda: fock.squeezer(r, phi, cutoffs=cutoffs), [r, phi] + ) + assert np.allclose(gradr, 2 * np.real(np.sum(dUdr))) + assert np.allclose(gradphi, 2 * np.real(np.sum(dUdphi))) + + +def test_displacement_grad(): + """tests fock displacement gradient against finite differences""" + cutoffs = [5, 5] + x = math.new_variable(0.1, None, "x") + y = math.new_variable(0.1, None, "y") + alpha = math.make_complex(x, y).numpy() + delta = 1e-6 + dUdx = (fock.displacement(x + delta, y, cutoffs) - fock.displacement(x - delta, y, cutoffs)) / ( + 2 * delta + ) + dUdy = (fock.displacement(x, y + delta, cutoffs) - fock.displacement(x, y - delta, cutoffs)) / ( + 2 * delta + ) + + D = fock.displacement(x, y, shape=cutoffs) + dD_da, dD_dac = strategies.jacobian_displacement(math.asnumpy(D), alpha) + assert np.allclose(dD_da + dD_dac, dUdx) + assert np.allclose(1j * (dD_da - dD_dac), dUdy) + + +def test_fock_representation_displacement_rectangular(): + """Tests that DGate returns the correct unitary.""" + x, y = 0.3, 0.5 + cutoffs = 5, 10 + # apply gate + dgate = Dgate(x, y) + Ud = dgate.U(cutoffs) + + # compare with tw implementation + expected_Ud = displacement(np.sqrt(x * x + y * y), np.arctan2(y, x), 10)[:5, :10] + + assert np.allclose(Ud, expected_Ud, atol=1e-5) + + +def test_fock_representation_displacement_rectangular2(): + """Tests that DGate returns the correct unitary.""" + x, y = 0.3, 0.5 + cutoffs = 10, 5 + # apply gate + dgate = Dgate(x, y) + Ud = dgate.U(cutoffs) + + # compare with tw implementation + expected_Ud = displacement(np.sqrt(x * x + y * y), np.arctan2(y, x), 10)[:10, :5] assert np.allclose(Ud, expected_Ud, atol=1e-5) @@ -149,36 +226,45 @@ def test_fock_representation_displacement(cutoffs, x, y): def test_fock_representation_squeezing(r, phi): S = Sgate(r=r, phi=phi) expected = squeezing(r=r, theta=phi, cutoff=20) - assert np.allclose(expected, S.U(cutoffs=[20]), atol=1e-5) + assert np.allclose(expected, S.U(cutoffs=[20, 20]), atol=1e-5) + + +@given(r1=r, phi1=angle, r2=r, phi2=angle) +def test_parallel_squeezing(r1, phi1, r2, phi2): + """Tests that two parallel squeezers return the correct unitary.""" + U12 = Sgate([r1, r2], [phi1, phi2]).U([5, 7, 5, 7]) + U1 = Sgate(r1, phi1).U([5, 5]) + U2 = Sgate(r2, phi2).U([7, 7]) + assert np.allclose(U12, np.transpose(np.tensordot(U1, U2, [[], []]), [0, 2, 1, 3])) @given(theta=angle, phi=angle) def test_fock_representation_beamsplitter(theta, phi): BS = BSgate(theta=theta, phi=phi) - expected = beamsplitter(theta=theta, phi=phi, cutoff=20) - assert np.allclose(expected, BS.U(cutoffs=[20, 20]), atol=1e-5) + expected = beamsplitter(theta=theta, phi=phi, cutoff=10) + assert np.allclose(expected, BS.U(cutoffs=[10, 10, 10, 10]), atol=1e-5) @given(r=r, phi=angle) def test_fock_representation_two_mode_squeezing(r, phi): S2 = S2gate(r=r, phi=phi) - expected = two_mode_squeezing(r=r, theta=phi, cutoff=20) - assert np.allclose(expected, S2.U(cutoffs=[20, 20]), atol=1e-5) + expected = two_mode_squeezing(r=r, theta=phi, cutoff=10) + assert np.allclose(expected, S2.U(cutoffs=[10, 10, 10, 10]), atol=1e-5) @given(phi_a=angle, phi_b=angle) def test_fock_representation_mzgate(phi_a, phi_b): MZ = MZgate(phi_a=phi_a, phi_b=phi_b, internal=False) - expected = mzgate(theta=phi_b, phi=phi_a, cutoff=20) - assert np.allclose(expected, MZ.U(cutoffs=[20, 20]), atol=1e-5) + expected = mzgate(theta=phi_b, phi=phi_a, cutoff=10) + assert np.allclose(expected, MZ.U(cutoffs=[10, 10, 10, 10]), atol=1e-5) @pytest.mark.parametrize( "cutoffs,angles,modes", [ - [[5, 4, 3], [np.pi, np.pi / 2, np.pi / 4], None], - [[3, 4], [np.pi / 3, np.pi / 2], [0, 1]], - [[3], np.pi / 6, [0]], + [[5, 4, 3, 5, 4, 3], [np.pi, np.pi / 2, np.pi / 4], None], + [[3, 4, 3, 4], [np.pi / 3, np.pi / 2], [0, 1]], + [[3, 3], np.pi / 6, [0]], ], ) def test_fock_representation_rgate(cutoffs, angles, modes): @@ -190,8 +276,8 @@ def test_fock_representation_rgate(cutoffs, angles, modes): # compare with the standard way of calculating # transformation unitaries using the Choi isomorphism - d = np.zeros(2 * len(cutoffs)) - expected_R = fock.wigner_to_fock_U(rgate.XYd[0], d, cutoffs * 2) + d = np.zeros(len(cutoffs)) + expected_R = fock.wigner_to_fock_U(rgate.X_matrix, d, cutoffs) assert np.allclose(R, expected_R, atol=1e-5) diff --git a/tests/test_lab/test_state.py b/tests/test_lab/test_state.py index 184a019ee..47aeb6356 100644 --- a/tests/test_lab/test_state.py +++ b/tests/test_lab/test_state.py @@ -19,7 +19,7 @@ def test_multiplication_ket(): scaled = 42.0 * G - assert np.allclose(scaled.ket(G.cutoffs), 42.0 * G.ket()) + assert np.allclose(scaled.ket(G.shape), 42.0 * G.ket()) def test_multiplication_dm(): diff --git a/tests/test_math/test_interface.py b/tests/test_math/test_interface.py index ad463f616..c3fbd63e4 100644 --- a/tests/test_math/test_interface.py +++ b/tests/test_math/test_interface.py @@ -16,9 +16,11 @@ Unit tests for the :class:`Math`. """ +import numpy as np import pytest -from mrmustard.math import Math + from mrmustard import settings +from mrmustard.math import Math try: import torch @@ -50,7 +52,14 @@ def test_error_for_wrong_backend(): backend = settings.BACKEND with pytest.raises(ValueError) as exception_info: settings.BACKEND = "unexisting_backend" - assert exception_info.value.args[0] == f"Backend must be either 'tensorflow' or 'torch'" + assert exception_info.value.args[0] == "Backend must be either 'tensorflow' or 'torch'" # set back to initial value to avoid side effects settings.BACKEND = backend + + +def test_hash_tensor(): + """Test hash of a tensor""" + math = Math() + tensor = math.astensor([1, 2, 3]) + assert np.allclose(*[math.hash_tensor(tensor) for _ in range(3)]) diff --git a/tests/test_physics/test_bargmann.py b/tests/test_physics/test_bargmann.py index b10ac2b8f..06154fea8 100644 --- a/tests/test_physics/test_bargmann.py +++ b/tests/test_physics/test_bargmann.py @@ -1,7 +1,12 @@ import numpy as np -from mrmustard.lab import Attenuator, Dgate, Gaussian -from mrmustard.physics.bargmann import wigner_to_bargmann_psi, wigner_to_bargmann_rho +from mrmustard.lab import Attenuator, Dgate, Gaussian, Ggate +from mrmustard.physics.bargmann import ( + wigner_to_bargmann_Choi, + wigner_to_bargmann_psi, + wigner_to_bargmann_rho, + wigner_to_bargmann_U, +) def test_wigner_to_bargmann_psi(): @@ -18,3 +23,19 @@ def test_wigner_to_bargmann_rho(): for x, y in zip(G.bargmann(), wigner_to_bargmann_rho(G.cov, G.means)): assert np.allclose(x, y) + + +def test_wigner_to_bargmann_U(): + """Test that the Bargmann representation of a unitary is correct""" + G = Ggate(2) >> Dgate(0.1, 0.2) + X, _, d = G.XYd(allow_none=False) + for x, y in zip(G.bargmann(), wigner_to_bargmann_U(X, d)): + assert np.allclose(x, y) + + +def test_wigner_to_bargmann_choi(): + """Test that the Bargmann representation of a Choi matrix is correct""" + G = Ggate(2) >> Dgate(0.1, 0.2) >> Attenuator(0.9) + X, Y, d = G.XYd(allow_none=False) + for x, y in zip(G.bargmann(), wigner_to_bargmann_Choi(X, Y, d)): + assert np.allclose(x, y) diff --git a/tests/test_physics/test_fock/test_fock.py b/tests/test_physics/test_fock/test_fock.py index 85d101767..8c673aee8 100644 --- a/tests/test_physics/test_fock/test_fock.py +++ b/tests/test_physics/test_fock/test_fock.py @@ -34,17 +34,8 @@ State, Vacuum, ) -from mrmustard.physics.fock import ( - _displacement, - _grad_displacement, - apply_choi_to_dm, - apply_choi_to_ket, - apply_kraus_to_dm, - apply_kraus_to_ket, - dm_to_ket, - ket_to_dm, - trace, -) +from mrmustard.math.lattice.strategies import displacement, grad_displacement +from mrmustard.physics import fock # helper strategies st_angle = st.floats(min_value=0, max_value=2 * np.pi) @@ -180,23 +171,23 @@ def test_density_matrix(num_modes): def test_dm_to_ket(state): """Tests pure state density matrix conversion to ket""" dm = state.dm() - ket = dm_to_ket(dm) + ket = fock.dm_to_ket(dm) # check if ket is normalized assert np.allclose(np.linalg.norm(ket), 1, atol=1e-4) # check kets are equivalent assert np.allclose(ket, state.ket(), atol=1e-4) - dm_reconstructed = ket_to_dm(ket) + dm_reconstructed = fock.ket_to_dm(ket) # check ket leads to same dm assert np.allclose(dm, dm_reconstructed, atol=1e-15) def test_dm_to_ket_error(): - """Test dm_to_ket raises an error when state is mixed""" + """Test fock.dm_to_ket raises an error when state is mixed""" state = Coherent(x=0.1, y=-0.4, cutoffs=[15]) >> Attenuator(0.5) with pytest.raises(ValueError): - dm_to_ket(state) + fock.dm_to_ket(state) def test_fock_trace_mode1_dm(): @@ -235,30 +226,29 @@ def test_fock_trace_function(): """tests that the Fock state is correctly traced""" state = Vacuum(2) >> Ggate(2) >> Attenuator([0.1, 0.1]) dm = state.dm([3, 20]) - dm_traced = trace(dm, keep=[0]) + dm_traced = fock.trace(dm, keep=[0]) assert np.allclose(dm_traced, State(dm=dm).get_modes(0).dm(), atol=1e-5) def test_dm_choi(): """tests that choi op is correctly applied to a dm""" circ = Ggate(1) >> Attenuator([0.1]) - dm_out = apply_choi_to_dm(circ.choi([10]), Vacuum(1).dm([10]), [0], [0]) + dm_out = fock.apply_choi_to_dm(circ.choi([10, 10, 10, 10]), Vacuum(1).dm([10]), [0], [0]) dm_expected = (Vacuum(1) >> circ).dm([10]) assert np.allclose(dm_out, dm_expected, atol=1e-5) def test_single_mode_choi_application_order(): """Test dual operations output the correct mode ordering""" - s = Attenuator(1.0) << State(dm=SqueezedVacuum(1.0, np.pi / 2).dm([40])) - assert np.allclose(s.dm([10])[:10, :10], SqueezedVacuum(1.0, np.pi / 2).dm([10])) - # NOTE: the [:10,:10] part is not necessary once PR #184 is merged + s = Attenuator(1.0) << State(dm=SqueezedVacuum(1.0, np.pi / 2).dm([40])) # apply identity gate + assert np.allclose(s.dm([10]), SqueezedVacuum(1.0, np.pi / 2).dm([10])) def test_apply_kraus_to_ket_1mode(): """Test that Kraus operators are applied to a ket on the correct indices""" ket = np.random.normal(size=(2, 3, 4)) kraus = np.random.normal(size=(5, 3)) - ket_out = apply_kraus_to_ket(kraus, ket, [1], [1]) + ket_out = fock.apply_kraus_to_ket(kraus, ket, [1], [1]) assert ket_out.shape == (2, 5, 4) @@ -266,7 +256,7 @@ def test_apply_kraus_to_ket_2mode(): """Test that Kraus operators are applied to a ket on the correct indices""" ket = np.random.normal(size=(2, 3, 4)) kraus = np.random.normal(size=(5, 3, 4)) - ket_out = apply_kraus_to_ket(kraus, ket, [1, 2], [1]) + ket_out = fock.apply_kraus_to_ket(kraus, ket, [1, 2], [1]) assert ket_out.shape == (2, 5) @@ -274,7 +264,7 @@ def test_apply_kraus_to_ket_2mode_2(): """Test that Kraus operators are applied to a ket on the correct indices""" ket = np.random.normal(size=(2, 3)) kraus = np.random.normal(size=(5, 4, 3)) - ket_out = apply_kraus_to_ket(kraus, ket, [1], [1, 2]) + ket_out = fock.apply_kraus_to_ket(kraus, ket, [1], [1, 2]) assert ket_out.shape == (2, 5, 4) @@ -282,7 +272,7 @@ def test_apply_kraus_to_dm_1mode(): """Test that Kraus operators are applied to a dm on the correct indices""" dm = np.random.normal(size=(2, 3, 2, 3)) kraus = np.random.normal(size=(5, 3)) - dm_out = apply_kraus_to_dm(kraus, dm, [1], [1]) + dm_out = fock.apply_kraus_to_dm(kraus, dm, [1], [1]) assert dm_out.shape == (2, 5, 2, 5) @@ -290,7 +280,7 @@ def test_apply_kraus_to_dm_2mode(): """Test that Kraus operators are applied to a dm on the correct indices""" dm = np.random.normal(size=(2, 3, 4, 2, 3, 4)) kraus = np.random.normal(size=(5, 3, 4)) - dm_out = apply_kraus_to_dm(kraus, dm, [1, 2], [1]) + dm_out = fock.apply_kraus_to_dm(kraus, dm, [1, 2], [1]) assert dm_out.shape == (2, 5, 2, 5) @@ -298,7 +288,7 @@ def test_apply_kraus_to_dm_2mode_2(): """Test that Kraus operators are applied to a dm on the correct indices""" dm = np.random.normal(size=(2, 3, 4, 2, 3, 4)) kraus = np.random.normal(size=(5, 6, 3)) - dm_out = apply_kraus_to_dm(kraus, dm, [1], [3, 1]) + dm_out = fock.apply_kraus_to_dm(kraus, dm, [1], [3, 1]) assert dm_out.shape == (2, 6, 4, 5, 2, 6, 4, 5) @@ -306,7 +296,7 @@ def test_apply_choi_to_ket_1mode(): """Test that choi operators are applied to a ket on the correct indices""" ket = np.random.normal(size=(3, 5)) choi = np.random.normal(size=(4, 3, 4, 3)) # [out_l, in_l, out_r, in_r] - ket_out = apply_choi_to_ket(choi, ket, [0], [0]) + ket_out = fock.apply_choi_to_ket(choi, ket, [0], [0]) assert ket_out.shape == (4, 5, 4, 5) @@ -314,7 +304,7 @@ def test_apply_choi_to_ket_2mode(): """Test that choi operators are applied to a ket on the correct indices""" ket = np.random.normal(size=(3, 5)) choi = np.random.normal(size=(2, 3, 5, 2, 3, 5)) # [out_l, in_l, out_r, in_r] - ket_out = apply_choi_to_ket(choi, ket, [0, 1], [0]) + ket_out = fock.apply_choi_to_ket(choi, ket, [0, 1], [0]) assert ket_out.shape == (2, 2) @@ -322,82 +312,104 @@ def test_apply_choi_to_dm_1mode(): """Test that choi operators are applied to a dm on the correct indices""" dm = np.random.normal(size=(3, 5, 3, 5)) choi = np.random.normal(size=(4, 3, 4, 3)) # [out_l, in_l, out_r, in_r] - dm_out = apply_choi_to_dm(choi, dm, [0], [0]) + dm_out = fock.apply_choi_to_dm(choi, dm, [0], [0]) assert dm_out.shape == (4, 5, 4, 5) def test_apply_choi_to_dm_2mode(): """Test that choi operators are applied to a dm on the correct indices""" dm = np.random.normal(size=(4, 5, 4, 5)) - choi = np.random.normal(size=(2, 3, 5, 2, 3, 5)) # [out_l, in_l, out_r, in_r] - dm_out = apply_choi_to_dm(choi, dm, [1], [1, 2]) + choi = np.random.normal(size=(2, 3, 5, 2, 3, 5)) # [out_l_1,2, in_l_1, out_r_1,2, in_r_1] + dm_out = fock.apply_choi_to_dm(choi, dm, [1], [1, 2]) assert dm_out.shape == (4, 2, 3, 4, 2, 3) -class TestDisplacement: - def test_grad_displacement(self): - """Tests the value of the analytic gradient for the Dgate against finite differences""" - cutoff = 4 - r = 1.0 - theta = np.pi / 8 - T = _displacement(r, theta, cutoff) - Dr, Dtheta = _grad_displacement(T, r, theta) - - dr = 0.001 - dtheta = 0.001 - Drp = _displacement(r + dr, theta, cutoff) - Drm = _displacement(r - dr, theta, cutoff) - Dthetap = _displacement(r, theta + dtheta, cutoff) - Dthetam = _displacement(r, theta - dtheta, cutoff) - Drapprox = (Drp - Drm) / (2 * dr) - Dthetaapprox = (Dthetap - Dthetam) / (2 * dtheta) - assert np.allclose(Dr, Drapprox, atol=1e-5, rtol=0) - assert np.allclose(Dtheta, Dthetaapprox, atol=1e-5, rtol=0) - - def test_displacement_values(self): - """Tests the correct construction of the single mode displacement operation""" - cutoff = 5 - alpha = 0.3 + 0.5 * 1j - # This data is obtained by using qutip - # np.array(displace(40,alpha).data.todense())[0:5,0:5] - expected = np.array( +def test_displacement_grad(): + """Tests the value of the analytic gradient for the Dgate against finite differences""" + cutoff = 4 + r = 2.0 + theta = np.pi / 8 + T = displacement((cutoff, cutoff), r * np.exp(1j * theta)) + Dr, Dtheta = grad_displacement(T, r, theta) + + dr = 0.001 + dtheta = 0.001 + Drp = displacement((cutoff, cutoff), (r + dr) * np.exp(1j * theta)) + Drm = displacement((cutoff, cutoff), (r - dr) * np.exp(1j * theta)) + Dthetap = displacement((cutoff, cutoff), r * np.exp(1j * (theta + dtheta))) + Dthetam = displacement((cutoff, cutoff), r * np.exp(1j * (theta - dtheta))) + Drapprox = (Drp - Drm) / (2 * dr) + Dthetaapprox = (Dthetap - Dthetam) / (2 * dtheta) + assert np.allclose(Dr, Drapprox, atol=1e-5, rtol=0) + assert np.allclose(Dtheta, Dthetaapprox, atol=1e-5, rtol=0) + + +def test_displacement_values(): + """Tests the correct construction of the single mode displacement operation""" + cutoff = 5 + alpha = 0.3 + 0.5 * 1j + # This data is obtained by using qutip + # np.array(displace(40,alpha).data.todense())[0:5,0:5] + expected = np.array( + [ [ - [ - 0.84366482 + 0.00000000e00j, - -0.25309944 + 4.21832408e-01j, - -0.09544978 - 1.78968334e-01j, - 0.06819609 + 3.44424719e-03j, - -0.01109048 + 1.65323865e-02j, - ], - [ - 0.25309944 + 4.21832408e-01j, - 0.55681878 + 0.00000000e00j, - -0.29708743 + 4.95145724e-01j, - -0.14658716 - 2.74850926e-01j, - 0.12479885 + 6.30297236e-03j, - ], - [ - -0.09544978 + 1.78968334e-01j, - 0.29708743 + 4.95145724e-01j, - 0.31873657 + 0.00000000e00j, - -0.29777767 + 4.96296112e-01j, - -0.18306015 - 3.43237787e-01j, - ], - [ - -0.06819609 + 3.44424719e-03j, - -0.14658716 + 2.74850926e-01j, - 0.29777767 + 4.96296112e-01j, - 0.12389162 + 1.10385981e-17j, - -0.27646677 + 4.60777945e-01j, - ], - [ - -0.01109048 - 1.65323865e-02j, - -0.12479885 + 6.30297236e-03j, - -0.18306015 + 3.43237787e-01j, - 0.27646677 + 4.60777945e-01j, - -0.03277289 + 1.88440656e-17j, - ], - ] - ) - T = _displacement(np.abs(alpha), np.angle(alpha), cutoff) - assert np.allclose(T, expected, atol=1e-5, rtol=0) + 0.84366482 + 0.00000000e00j, + -0.25309944 + 4.21832408e-01j, + -0.09544978 - 1.78968334e-01j, + 0.06819609 + 3.44424719e-03j, + -0.01109048 + 1.65323865e-02j, + ], + [ + 0.25309944 + 4.21832408e-01j, + 0.55681878 + 0.00000000e00j, + -0.29708743 + 4.95145724e-01j, + -0.14658716 - 2.74850926e-01j, + 0.12479885 + 6.30297236e-03j, + ], + [ + -0.09544978 + 1.78968334e-01j, + 0.29708743 + 4.95145724e-01j, + 0.31873657 + 0.00000000e00j, + -0.29777767 + 4.96296112e-01j, + -0.18306015 - 3.43237787e-01j, + ], + [ + -0.06819609 + 3.44424719e-03j, + -0.14658716 + 2.74850926e-01j, + 0.29777767 + 4.96296112e-01j, + 0.12389162 + 1.10385981e-17j, + -0.27646677 + 4.60777945e-01j, + ], + [ + -0.01109048 - 1.65323865e-02j, + -0.12479885 + 6.30297236e-03j, + -0.18306015 + 3.43237787e-01j, + 0.27646677 + 4.60777945e-01j, + -0.03277289 + 1.88440656e-17j, + ], + ] + ) + D = displacement((cutoff, cutoff), alpha) + assert np.allclose(D, expected, atol=1e-5, rtol=0) + + +@given(x=st.floats(-1, 1), y=st.floats(-1, 1)) +def test_number_means(x, y): + assert np.allclose(State(ket=Coherent(x, y).ket([80])).number_means, x * x + y * y) + assert np.allclose(State(dm=Coherent(x, y).dm([80])).number_means, x * x + y * y) + + +@given(x=st.floats(-1, 1), y=st.floats(-1, 1)) +def test_number_variances_coh(x, y): + assert np.allclose(fock.number_variances(Coherent(x, y).ket([80]), False)[0], x * x + y * y) + assert np.allclose(fock.number_variances(Coherent(x, y).dm([80]), True)[0], x * x + y * y) + + +def test_number_variances_fock(): + assert np.allclose(fock.number_variances(Fock(n=1).ket(), False), 0) + assert np.allclose(fock.number_variances(Fock(n=1).dm(), True), 0) + + +def test_normalize_dm(): + dm = np.array([[0.2, 0], [0, 0.2]]) + assert np.allclose(fock.normalize(dm, True), np.array([[0.5, 0], [0, 0.5]])) diff --git a/tests/test_physics/test_gaussian/test_gaussian_utils.py b/tests/test_physics/test_gaussian/test_gaussian_utils.py index 1698f48a9..9b87c64c4 100644 --- a/tests/test_physics/test_gaussian/test_gaussian_utils.py +++ b/tests/test_physics/test_gaussian/test_gaussian_utils.py @@ -13,7 +13,7 @@ # limitations under the License. import numpy as np -from hypothesis import given, strategies as st + from mrmustard import * from mrmustard.physics import gaussian as gp @@ -53,10 +53,30 @@ def test_partition_cov_2modes(): def test_partition_cov_3modes(): pass # TODO - # arr = np.array([[1,2,3,4,5,6], - # [7,8,9,10,11,12], - # [13,14,15,16,17,18], - # [19,20,21,22,23,24], - # [25,26,27,28,29,30], - # [31,32,33,34,35,36]]) - # A,B,AB = gp.partition_cov(gp.math.astensor(arr), Amodes=[0,2]) + +def test_CPTP_with_none_X(): + cov, means = gp.CPTP( + cov=np.eye(2), + means=np.zeros(2), + X=None, + Y=np.zeros((2, 2)), + d=np.zeros(2), + state_modes=[0], + transf_modes=[0], + ) + assert np.allclose(cov, np.eye(2)) + assert np.allclose(means, np.zeros(2)) + + +def test_CPTP_with_none_XYd(): + cov, means = gp.CPTP( + cov=np.eye(2), + means=np.zeros(2), + X=None, + Y=None, + d=None, + state_modes=[0], + transf_modes=[0], + ) + assert np.allclose(cov, np.eye(2)) + assert np.allclose(means, np.zeros(2)) diff --git a/tests/test_physics/test_gaussian/test_symplectics.py b/tests/test_physics/test_gaussian/test_symplectics.py index b8d3bc57c..832f89cc3 100644 --- a/tests/test_physics/test_gaussian/test_symplectics.py +++ b/tests/test_physics/test_gaussian/test_symplectics.py @@ -17,10 +17,11 @@ from hypothesis import strategies as st from thewalrus.symplectic import beam_splitter, expand, rotation, squeezing, two_mode_squeezing -from mrmustard.lab.gates import ( +from mrmustard.lab import ( Amplifier, Attenuator, BSgate, + Coherent, CXgate, CZgate, Dgate, @@ -205,3 +206,9 @@ def test_amplifier_attenuator_on_coherent_coherent(eta, x, y): assert Vacuum(1) >> Dgate(x, y) >> Amplifier(1 / eta) >> Attenuator(eta) == Thermal( ((1 / eta) - 1) * eta ) >> Dgate(x, y) + + +@given(x=st.floats(-2, 2), y=st.floats(-2, 2)) +def test_number_means(x, y): + """Tests that the number means of a displaced state are correct""" + assert np.allclose(Coherent(x, y).number_means, x * x + y * y) diff --git a/tests/test_training/test_opt.py b/tests/test_training/test_opt.py index 38badd3d1..0b3063f95 100644 --- a/tests/test_training/test_opt.py +++ b/tests/test_training/test_opt.py @@ -31,7 +31,7 @@ S2gate, Sgate, ) -from mrmustard.lab.states import DisplacedSqueezed, SqueezedVacuum, Vacuum +from mrmustard.lab.states import DisplacedSqueezed, Gaussian, SqueezedVacuum, Vacuum from mrmustard.math import Math from mrmustard.physics import fidelity from mrmustard.physics.gaussian import trace, von_neumann_entropy @@ -243,7 +243,7 @@ def test_learning_four_mode_Interferometer(): >> BSgate(settings.rng.normal(scale=0.01), modes=[1, 2]) >> BSgate(settings.rng.normal(scale=0.01), modes=[0, 3]) ) - X = math.cast(perturbed.XYd[0], "complex128") + X = math.cast(perturbed.XYd()[0], "complex128") perturbed_U = X[:4, :4] + 1j * X[4:, :4] ops = [ @@ -283,7 +283,7 @@ def test_learning_four_mode_RealInterferometer(): >> BSgate(settings.rng.normal(scale=0.01), modes=[1, 2]) >> BSgate(settings.rng.normal(scale=0.01), modes=[0, 3]) ) - perturbed_O = pertubed.XYd[0][:4, :4] + perturbed_O = pertubed.XYd()[0][:4, :4] ops = [ Sgate( @@ -406,12 +406,54 @@ def test_dgate_optimization(): settings.SEED = 24 dgate = Dgate(x_trainable=True, y_trainable=True) - target_state = DisplacedSqueezed(r=0.0, x=1.0, y=1.0) + target_state = DisplacedSqueezed(r=0.0, x=0.1, y=0.2).ket(cutoffs=[40]) def cost_fn(): state_out = Vacuum(1) >> dgate - - return 1 - fidelity(state_out, target_state) + return -math.abs(math.sum(math.conj(state_out.ket([40])) * target_state)) ** 2 opt = Optimizer() opt.minimize(cost_fn, by_optimizing=[dgate]) + + assert np.allclose(dgate.x.value, 0.1, atol=0.01) + assert np.allclose(dgate.y.value, 0.2, atol=0.01) + + +def test_sgate_optimization(): + """Test that Sgate is optimized correctly.""" + settings.SEED = 25 + + sgate = Sgate(r=0.2, phi=0.1, r_trainable=True, phi_trainable=True) + target_state = SqueezedVacuum(r=0.1, phi=0.2).ket(cutoffs=[40]) + + def cost_fn(): + state_out = Vacuum(1) >> sgate + + return -math.abs(math.sum(math.conj(state_out.ket([40])) * target_state)) ** 2 + + opt = Optimizer() + opt.minimize(cost_fn, by_optimizing=[sgate]) + + assert np.allclose(sgate.r.value, 0.1, atol=0.01) + assert np.allclose(sgate.phi.value, 0.2, atol=0.01) + + +def test_bsgate_optimization(): + """Test that Sgate is optimized correctly.""" + settings.SEED = 25 + + G = Gaussian(2) + + bsgate = BSgate(0.05, 0.1, theta_trainable=True, phi_trainable=True) + target_state = (G >> BSgate(0.1, 0.2)).ket(cutoffs=[40, 40]) + + def cost_fn(): + state_out = G >> bsgate + + return -math.abs(math.sum(math.conj(state_out.ket([40, 40])) * target_state)) ** 2 + + opt = Optimizer() + opt.minimize(cost_fn, by_optimizing=[bsgate]) + + assert np.allclose(bsgate.theta.value, 0.1, atol=0.01) + assert np.allclose(bsgate.phi.value, 0.2, atol=0.01) diff --git a/tests/test_utils/test_wigner.py b/tests/test_utils/test_wigner.py index 8ade9e656..60395d3b8 100644 --- a/tests/test_utils/test_wigner.py +++ b/tests/test_utils/test_wigner.py @@ -14,20 +14,21 @@ """This module contains test for the calculation of the discretized Wigner function.""" -import pytest import numpy as np +import pytest from scipy.stats import multivariate_normal -from mrmustard.utils.wigner import wigner_discretized + +from mrmustard import settings from mrmustard.lab import ( - Vacuum, Coherent, - SqueezedVacuum, - Thermal, DisplacedSqueezed, - Gaussian, Fock, + Gaussian, + SqueezedVacuum, + Thermal, + Vacuum, ) -from mrmustard import settings +from mrmustard.utils.wigner import wigner_discretized def multivariate_normal_pdf(qvec, pvec, means, cov): From ebbdd3c8f35c6259d39d44764b19a514c7229ab4 Mon Sep 17 00:00:00 2001 From: Filippo Miatto Date: Wed, 31 May 2023 14:41:39 -0700 Subject: [PATCH 48/53] bugfix, test and changelog entry (#246) **Context:** When projecting onto Fock the autocutoff doesn't check the photon number of the Fock projection. Thanks to Jacob for spotting it. **Description of the Change:** Now it does. Added a test too. **Benefits:** No unnecessary cutoff errors when projecting onto a Fock state **Possible Drawbacks:** None **Related GitHub Issues:** None --- .github/CHANGELOG.md | 3 +++ mrmustard/lab/states.py | 2 +- tests/test_lab/test_gates_fock.py | 18 ++++++++++++++++++ 3 files changed, 22 insertions(+), 1 deletion(-) diff --git a/.github/CHANGELOG.md b/.github/CHANGELOG.md index 89af599a5..b35fef8ee 100644 --- a/.github/CHANGELOG.md +++ b/.github/CHANGELOG.md @@ -89,6 +89,9 @@ * The displacement of the dual of an operation had the wrong sign [(#239)](https://github.com/XanaduAI/MrMustard/pull/239) +* When projecting a Gaussian state onto a Fock state, the upper limit of the autocutoff now respect the Fock projection. + [(#246)](https://github.com/XanaduAI/MrMustard/pull/246) + ### Documentation ### Contributors diff --git a/mrmustard/lab/states.py b/mrmustard/lab/states.py index ebb9db76e..ea10e1d73 100644 --- a/mrmustard/lab/states.py +++ b/mrmustard/lab/states.py @@ -81,7 +81,7 @@ class Coherent(Parametrized, State): x_bounds (float or None, float or None): The bounds of the x-displacement. y_bounds (float or None, float or None): The bounds of the y-displacement. modes (optional List[int]): The modes of the coherent state. - cutoffs (Sequence[int], default=None): set to force the fock cutoffs of the state + cutoffs (Sequence[int], default=None): set to force the cutoff dimensions of the state normalize (bool, default False): whether to normalize the leftover state when projecting onto ``Coherent`` """ diff --git a/tests/test_lab/test_gates_fock.py b/tests/test_lab/test_gates_fock.py index 53ac46a6b..0a55f5790 100644 --- a/tests/test_lab/test_gates_fock.py +++ b/tests/test_lab/test_gates_fock.py @@ -299,3 +299,21 @@ def test_raise_interferometer_error(): def test_choi_cutoffs(): output = State(dm=Coherent([1.0, 1.0]).dm([5, 8])) >> Attenuator(0.5, modes=[1]) assert output.cutoffs == [5, 8] # cutoffs are respected by the gate + + +def test_measure_with_fock(): + "tests that the autocutoff respects the fock projection cutoff" + cov = np.array( + [ + [1.08341848, 0.26536937, 0.0, 0.0], + [0.26536937, 1.05564949, 0.0, 0.0], + [0.0, 0.0, 0.98356475, -0.24724869], + [0.0, 0.0, -0.24724869, 1.00943755], + ] + ) + + state = State(means=np.zeros(4), cov=cov) + + n_detect = 2 + state_out = state << Fock([n_detect], modes=[1]) + assert np.allclose(state_out.ket(), np.array([0.00757899, 0.0])) From c84ee68fba8b8cf3486e4ee37a0a1ed2198f31b5 Mon Sep 17 00:00:00 2001 From: Filippo Miatto Date: Tue, 20 Jun 2023 09:51:09 -0700 Subject: [PATCH 49/53] Stable BSgate (#248) **Context:** BSgate is unstable for high Fock cutoff. **Description of the Change:** Implements an alternative method to calculate the BS and exposes it like so: ```python bs = BSgate(1.0,2.0) bs.U(cutoffs=[10,10], method='schwinger') # or 'vanilla' (default is settings.DEFAULT_BS_METHOD='vanilla') ``` **Benefits:** Slower but stable method to compute the BS Fock amplitudes to any cutoff --------- Co-authored-by: Ryk <47638463+ryk-wolf@users.noreply.github.com> --- .github/CHANGELOG.md | 3 + mrmustard/__init__.py | 1 + mrmustard/lab/abstract/transformation.py | 38 +++++++---- mrmustard/lab/gates.py | 67 ++++++++++++++----- .../math/lattice/strategies/beamsplitter.py | 50 +++++++++++++- mrmustard/math/lattice/strategies/squeezer.py | 8 +-- mrmustard/physics/fock.py | 29 ++++++-- tests/test_lab/test_gates_fock.py | 25 ++++--- 8 files changed, 170 insertions(+), 51 deletions(-) diff --git a/.github/CHANGELOG.md b/.github/CHANGELOG.md index b35fef8ee..f4b5f9e4d 100644 --- a/.github/CHANGELOG.md +++ b/.github/CHANGELOG.md @@ -53,6 +53,9 @@ * Gaussian transformations support a `bargmann` method for returning the bargmann representation. [(#239)](https://github.com/XanaduAI/MrMustard/pull/239) +* BSGate.U now supports method='vanilla' (default) and 'schwinger' (slower, but stable to any cutoff) + [(#248)](https://github.com/XanaduAI/MrMustard/pull/248) + ### Breaking Changes * The previous `callback` argument to `Optimizer.minimize` is now `callbacks` since we can now pass diff --git a/mrmustard/__init__.py b/mrmustard/__init__.py index 725a29548..f8ed18e69 100644 --- a/mrmustard/__init__.py +++ b/mrmustard/__init__.py @@ -50,6 +50,7 @@ def __init__(self): self.PROGRESSBAR = True self._seed = np.random.randint(0, 2**31 - 1) self.rng = np.random.default_rng(self._seed) + self.DEFAULT_BS_METHOD = "vanilla" # can be 'vanilla' or 'schwinger' @property def SEED(self): diff --git a/mrmustard/lab/abstract/transformation.py b/mrmustard/lab/abstract/transformation.py index c8db2d0bd..8c9e984c1 100644 --- a/mrmustard/lab/abstract/transformation.py +++ b/mrmustard/lab/abstract/transformation.py @@ -19,15 +19,7 @@ from __future__ import annotations -from typing import ( - Callable, - Iterable, - List, - Optional, - Sequence, - Tuple, - Union, -) +from typing import Callable, Iterable, List, Optional, Sequence, Tuple, Union import numpy as np @@ -216,7 +208,7 @@ def XYd( def XYd_dual( self, allow_none: bool = True - ) -> Tuple[Optional[RealMatrix], Optional[RealMatrix], Optional[RealVector]]: + ) -> tuple[Optional[RealMatrix], Optional[RealMatrix], Optional[RealVector]]: r"""Returns the ```(X, Y, d)``` triple of the dual of the current transformation. Override in subclasses if computing ``Xdual``, ``Ydual`` and ``ddual`` together is more efficient. @@ -233,16 +225,34 @@ def U(self, cutoffs: Sequence[int]): if not self.is_unitary: return None X, _, d = self.XYd(allow_none=False) - return fock.wigner_to_fock_U(X, d, shape=tuple(cutoffs)) + if len(cutoffs) == self.num_modes: + shape = tuple(cutoffs) * 2 + elif len(cutoffs) == 2 * self.num_modes: + shape = tuple(cutoffs) + + else: + raise ValueError( + f"Invalid number of cutoffs: {len(cutoffs)} (expected {self.num_modes} or {2*self.num_modes})" + ) + return fock.wigner_to_fock_U(X, d, shape=shape) def choi(self, cutoffs: Sequence[int]): r"""Returns the Choi representation of the transformation.""" + if len(cutoffs) == self.num_modes: + shape = tuple(cutoffs) * 4 + elif len(cutoffs) == 4 * self.num_modes: + shape = tuple(cutoffs) + else: + raise ValueError( + f"Invalid number of cutoffs: {len(cutoffs)} (expected {self.num_modes} or {4*self.num_modes})" + ) if self.is_unitary: - U = self.U(cutoffs[: self.num_modes]) - Udual = self.U(cutoffs[self.num_modes :]) + U = self.U(shape[: self.num_modes]) + Udual = self.U(shape[self.num_modes :]) return fock.U_to_choi(U, Udual) X, Y, d = self.XYd(allow_none=False) - return fock.wigner_to_fock_Choi(X, Y, d, shape=cutoffs) + + return fock.wigner_to_fock_Choi(X, Y, d, shape=shape) def __getitem__(self, items) -> Callable: r"""Sets the modes on which the transformation acts. diff --git a/mrmustard/lab/gates.py b/mrmustard/lab/gates.py index 54e0e236a..515267cd0 100644 --- a/mrmustard/lab/gates.py +++ b/mrmustard/lab/gates.py @@ -99,21 +99,29 @@ def U(self, cutoffs: Sequence[int]): the Laguerre polynomials. Arguments: - cutoffs (Sequence[int]): the Fock basis truncation for each index of U - in the order (out_1, out_2,..., in_1, in_2,...) + cutoffs (Sequence[int]): the Hilbert space dimension cutoff for each mode Returns: - array[complex]: the unitary matrix + Raises: + ValueError: if the length of the cutoffs array is different from N and 2N """ N = self.num_modes x = self.x.value * math.ones(N, dtype=self.x.value.dtype) y = self.y.value * math.ones(N, dtype=self.y.value.dtype) + if len(cutoffs) == N: + shape = tuple(cutoffs) * 2 + elif len(cutoffs) == 2 * N: + shape = tuple(cutoffs) + else: + raise ValueError( + "len(cutoffs) should be either equal to the number of modes or twice the number of modes (for output-input)." + ) if N > 1: # calculate displacement unitary for each mode and concatenate with outer product Ud = None - for idx, out_in in enumerate(zip(cutoffs[:N], cutoffs[N:])): + for idx, out_in in enumerate(zip(shape[:N], shape[N:])): if Ud is None: Ud = fock.displacement(x[idx], y[idx], out_in) else: @@ -125,7 +133,7 @@ def U(self, cutoffs: Sequence[int]): list(range(0, 2 * N, 2)) + list(range(1, 2 * N, 2)), ) else: - return fock.displacement(x[0], y[0], tuple(cutoffs)) + return fock.displacement(x[0], y[0], shape) class Sgate(Parametrized, Transformation): @@ -173,14 +181,20 @@ def __init__( def U(self, cutoffs: Sequence[int]): r"""Returns the unitary representation of the Squeezing gate. Args: - cutoffs (Sequence[int]): cutoff dimension for each mode - in the order (out_1, out_2,..., in_1, in_2,...) + cutoffs (Sequence[int]): the Hilbert space dimension cutoff for each mode Returns: array[complex]: the unitary matrix - """ N = self.num_modes + if len(cutoffs) == N: + shape = tuple(cutoffs) * 2 + elif len(cutoffs) == 2 * N: + shape = tuple(cutoffs) + else: + raise ValueError( + "len(cutoffs) should be either equal to the number of modes or twice the number of modes (for output-input)." + ) # this works both or scalar r/phi and vector r/phi: r = self.r.value * math.ones(N, dtype=self.r.value.dtype) phi = self.phi.value * math.ones(N, dtype=self.phi.value.dtype) @@ -188,18 +202,18 @@ def U(self, cutoffs: Sequence[int]): if N > 1: # calculate squeezing unitary for each mode and concatenate with outer product Us = None - for idx, out_in in enumerate(zip(cutoffs[:N], cutoffs[N:])): + for idx, single_shape in enumerate(zip(shape[:N], shape[N:])): if Us is None: - Us = fock.squeezer(r=r[idx], phi=phi[idx], cutoffs=out_in) + Us = fock.squeezer(r=r[idx], phi=phi[idx], shape=single_shape) else: - U_next = fock.squeezer(r=r[idx], phi=phi[idx], cutoffs=out_in) + U_next = fock.squeezer(r=r[idx], phi=phi[idx], shape=single_shape) Us = math.outer(Us, U_next) return math.transpose( Us, list(range(0, 2 * N, 2)) + list(range(1, 2 * N, 2)), ) else: - return fock.squeezer(r=r[0], phi=phi[0], cutoffs=tuple(cutoffs)) + return fock.squeezer(r=r[0], phi=phi[0], shape=shape) @property def X_matrix(self): @@ -248,7 +262,7 @@ def U(self, cutoffs: Sequence[int], diag_only=False): r"""Returns the unitary representation of the Rotation gate. Args: - cutoffs (Sequence[int]): cutoff dimension for each mode + cutoffs (Sequence[int]): cutoff dimension for each mode. diag_only (bool): if True, only return the diagonal of the unitary matrix. Returns: @@ -256,12 +270,20 @@ def U(self, cutoffs: Sequence[int], diag_only=False): """ if diag_only: raise NotImplementedError("Rgate does not support diag_only=True yet") - + N = self.num_modes + if len(cutoffs) == N: + shape = tuple(cutoffs) * 2 + elif len(cutoffs) == 2 * N: + shape = tuple(cutoffs) + else: + raise ValueError( + "len(cutoffs) should be either equal to the number of modes or twice the number of modes (for output-input)." + ) angles = self.angle.value * math.ones(self.num_modes, dtype=self.angle.value.dtype) # calculate rotation unitary for each mode and concatenate with outer product Ur = None - for idx, cutoff in enumerate(cutoffs[: self.num_modes]): + for idx, cutoff in enumerate(shape[: self.num_modes]): theta = math.arange(cutoff) * angles[idx] if Ur is None: Ur = math.diag(math.make_complex(math.cos(theta), math.sin(theta))) @@ -419,20 +441,31 @@ def __init__( self.is_gaussian = True self.short_name = "BS" - def U(self, cutoffs: Optional[List[int]]): + def U(self, cutoffs: Optional[List[int]], method=None): r"""Returns the symplectic transformation matrix for the beam splitter. Args: cutoffs (List[int]): the list of cutoff dimensions for each mode in the order (out_0, out_1, in_0, in_1). + method (str): the method used to compute the unitary matrix. Options are: + * 'vanilla': uses the standard method + * 'schwinger': slower, but numerically stable + default is set in settings.DEFAULT_BS_METHOD (with 'vanilla' by default) Returns: array[complex]: the unitary tensor of the beamsplitter """ + if len(cutoffs) == 4: + shape = tuple(cutoffs) + elif len(cutoffs) == 2: + shape = tuple(cutoffs) + tuple(cutoffs) + else: + raise ValueError(f"Invalid len(cutoffs): {len(cutoffs)} (should be 2 or 4).") return fock.beamsplitter( self.theta.value, self.phi.value, - tuple(cutoffs), + shape, + method=method or settings.DEFAULT_BS_METHOD, ) @property diff --git a/mrmustard/math/lattice/strategies/beamsplitter.py b/mrmustard/math/lattice/strategies/beamsplitter.py index 2597ccfd5..285ff9901 100644 --- a/mrmustard/math/lattice/strategies/beamsplitter.py +++ b/mrmustard/math/lattice/strategies/beamsplitter.py @@ -30,7 +30,7 @@ SQRT = np.sqrt(np.arange(100000)) -__all__ = ["beamsplitter", "beamsplitter_vjp"] +__all__ = ["beamsplitter", "beamsplitter_vjp", "beamsplitter_schwinger"] @njit @@ -154,3 +154,51 @@ def beamsplitter_vjp( dLdphi = 2 * np.real(1j * st * em * dLdA[0, 3] + 1j * st * e * dLdA[1, 2]) return dLdtheta, dLdphi + + +def beamsplitter_schwinger(shape, theta, phi, max_N=None): + r"""Returns the Fock representation of the beamsplitter up to + the given cutoff for each of the two modes. + + This implementation is in pure python (so it's slower than the numba version), + but it's numerically stable up to arbitrary cutoffs. + + In this implementation we split the two-mode Fock basis into finite subsets spanned + by |m,n> with m+n=const, i.e. {|0,0>}, {|1,0>,|0,1>}, {|2,0>, |1,1>, |0,2>}, etc... + A beamsplitter acts unitariliy in each of these subspaces without mixing them with each other, + i.e. in this basis the beamsplitter would be a block-diagonal matrix. + This means we can construct the BS matrix by first calculating the BS unitaries + in each of these subspaces. + This can be done using the matrix exponential, which is numerically stable. + We couldn't do this in the original basis because it was infinite-dimensional + and we had to truncate it at some point. + + Arguments: + shape (int, int, int, int): The shape of the output tensor. Only shapes of the form (i,k,i,k) are supported. + theta (float): The angle of the beamsplitter. + phi (float): The phase of the beamsplitter. + max_N (int): The maximum total photon number to include in the calculation. + + Returns: + np.ndarray: The beamsplitter in the Fock basis. + """ + c1, c2, c3, c4 = shape + if c1 != c3 or c2 != c4: + raise ValueError("The Schwinger method only supports shapes of the form (i,k,i,k).") + # create output tensor + U = np.zeros(shape, dtype="complex128") + + # loop over subspaces of constant photon number N up to max_N + if max_N is None or max_N > c1 + c2 - 2: + max_N = c1 + c2 - 2 + for N in range(max_N + 1): + # construct the N+1 x N+1 unitary for this subspace + diag = np.exp(1j * phi) * np.sqrt(np.arange(N, 0, -1) * np.arange(1, N + 1, 1)) + iJy = np.diag(diag, k=-1) - np.diag(np.conj(diag), k=1) + E, V = np.linalg.eig(theta * iJy) + block = V @ np.diag(np.exp(E)) @ np.conj(V.T) + # insert the elements of the block into the output tensor + for i in range(max(0, N + 1 - c1), min(N + 1, c1)): + for j in range(max(0, N + 1 - c1), min(N + 1, c2)): + U[N - i, i, N - j, j] = block[i, j] + return U diff --git a/mrmustard/math/lattice/strategies/squeezer.py b/mrmustard/math/lattice/strategies/squeezer.py index f388ac718..a39a7e649 100644 --- a/mrmustard/math/lattice/strategies/squeezer.py +++ b/mrmustard/math/lattice/strategies/squeezer.py @@ -27,12 +27,12 @@ @njit def squeezer( - cutoffs: tuple[int, int], r: float, theta: float, dtype=np.complex128 + shape: tuple[int, int], r: float, theta: float, dtype=np.complex128 ): # pragma: no cover r"""Calculates the matrix elements of the squeezing gate using a recurrence relation. (See eq. 50-52 in https://arxiv.org/abs/2004.11002) Args: - cutoffs (tuple[int, int]): Fock cutoffs for the output and input modes. + shape (tuple[int, int]): Fock cutoffs for the output and input indices. r (float): squeezing magnitude theta (float): squeezing angle dtype (data type): data type used for the calculation. @@ -40,8 +40,8 @@ def squeezer( Returns: array (ComplexMatrix): matrix representing the squeezing gate. """ - M, N = cutoffs - S = np.zeros(cutoffs, dtype=dtype) + M, N = shape + S = np.zeros(shape, dtype=dtype) eitheta_tanhr = np.exp(1j * theta) * np.tanh(r) eitheta_tanhr_conj = np.conj(eitheta_tanhr) diff --git a/mrmustard/physics/fock.py b/mrmustard/physics/fock.py index 45be29792..bb8c97420 100644 --- a/mrmustard/physics/fock.py +++ b/mrmustard/physics/fock.py @@ -879,9 +879,24 @@ def grad(dL_dDc): @math.custom_gradient -def beamsplitter(theta: float, phi: float, cutoffs: Sequence[int]): - r"""creates a beamsplitter tensor with given cutoffs using a numba-based fock lattice strategy""" - bs_unitary = strategies.beamsplitter(tuple(cutoffs), math.asnumpy(theta), math.asnumpy(phi)) +def beamsplitter(theta: float, phi: float, shape: Sequence[int], method: str): + r"""Creates a beamsplitter tensor with given cutoffs using a numba-based fock lattice strategy. + + Args: + theta (float): transmittivity angle of the beamsplitter + phi (float): phase angle of the beamsplitter + cutoffs (int,int): cutoff dimensions of the two modes + """ + if method == "vanilla": + bs_unitary = strategies.beamsplitter(shape, math.asnumpy(theta), math.asnumpy(phi)) + elif method == "schwinger": + bs_unitary = strategies.beamsplitter_schwinger( + shape, math.asnumpy(theta), math.asnumpy(phi) + ) + else: + raise ValueError( + f"Unknown beamsplitter method {method}. Options are 'vanilla' and 'schwinger'." + ) def vjp(dLdGc): return strategies.beamsplitter_vjp( @@ -895,9 +910,9 @@ def vjp(dLdGc): @math.custom_gradient -def squeezer(r, phi, cutoffs): +def squeezer(r, phi, shape): r"""creates a single mode squeezer matrix using a numba-based fock lattice strategy""" - sq_unitary = strategies.squeezer(tuple(cutoffs), math.asnumpy(r), math.asnumpy(phi)) + sq_unitary = strategies.squeezer(shape, math.asnumpy(r), math.asnumpy(phi)) def vjp(dLdGc): dr, dphi = strategies.squeezer_vjp( @@ -912,9 +927,9 @@ def vjp(dLdGc): @math.custom_gradient -def squeezed(r, phi, cutoffs): +def squeezed(r, phi, shape): r"""creates a single mode squeezed state using a numba-based fock lattice strategy""" - sq_ket = strategies.squeezed(tuple(cutoffs), math.asnumpy(r), math.asnumpy(phi)) + sq_ket = strategies.squeezed(shape, math.asnumpy(r), math.asnumpy(phi)) def vjp(dLdGc): return strategies.squeezed_vjp( diff --git a/tests/test_lab/test_gates_fock.py b/tests/test_lab/test_gates_fock.py index 0a55f5790..d9799ed73 100644 --- a/tests/test_lab/test_gates_fock.py +++ b/tests/test_lab/test_gates_fock.py @@ -159,16 +159,16 @@ def test_parallel_displacement(x1, x2, y1, y2): assert np.allclose(U12, np.transpose(np.tensordot(U1, U2, [[], []]), [0, 2, 1, 3])) -def test_squeezer_grad(): +def test_squeezer_grad_against_finite_differences(): """tests fock squeezer gradient against finite differences""" - cutoffs = [5, 5] + cutoffs = (5, 5) r = math.new_variable(0.5, None, "r") phi = math.new_variable(0.1, None, "phi") delta = 1e-6 dUdr = (Sgate(r + delta, phi).U(cutoffs) - Sgate(r - delta, phi).U(cutoffs)) / (2 * delta) dUdphi = (Sgate(r, phi + delta).U(cutoffs) - Sgate(r, phi - delta).U(cutoffs)) / (2 * delta) _, (gradr, gradphi) = math.value_and_gradients( - lambda: fock.squeezer(r, phi, cutoffs=cutoffs), [r, phi] + lambda: fock.squeezer(r, phi, shape=cutoffs), [r, phi] ) assert np.allclose(gradr, 2 * np.real(np.sum(dUdr))) assert np.allclose(gradphi, 2 * np.real(np.sum(dUdphi))) @@ -262,9 +262,9 @@ def test_fock_representation_mzgate(phi_a, phi_b): @pytest.mark.parametrize( "cutoffs,angles,modes", [ - [[5, 4, 3, 5, 4, 3], [np.pi, np.pi / 2, np.pi / 4], None], - [[3, 4, 3, 4], [np.pi / 3, np.pi / 2], [0, 1]], - [[3, 3], np.pi / 6, [0]], + [[5, 4, 3], [np.pi, np.pi / 2, np.pi / 4], None], + [[3, 4], [np.pi / 3, np.pi / 2], [0, 1]], + [[3], np.pi / 6, [0]], ], ) def test_fock_representation_rgate(cutoffs, angles, modes): @@ -276,8 +276,8 @@ def test_fock_representation_rgate(cutoffs, angles, modes): # compare with the standard way of calculating # transformation unitaries using the Choi isomorphism - d = np.zeros(len(cutoffs)) - expected_R = fock.wigner_to_fock_U(rgate.X_matrix, d, cutoffs) + d = np.zeros(len(cutoffs) * 2) + expected_R = fock.wigner_to_fock_U(rgate.X_matrix, d, tuple(cutoffs + cutoffs)) assert np.allclose(R, expected_R, atol=1e-5) @@ -317,3 +317,12 @@ def test_measure_with_fock(): n_detect = 2 state_out = state << Fock([n_detect], modes=[1]) assert np.allclose(state_out.ket(), np.array([0.00757899, 0.0])) + + +@given(theta=angle, phi=angle) +def test_schwinger_bs_equals_vanilla_bs_for_small_cutoffs(theta, phi): + """Tests that the Schwinger boson BS gate is equivalent to the vanilla BS gate for low cutoffs.""" + U_vanilla = BSgate(theta, phi).U([10, 10, 10, 10], method="vanilla") + U_schwinger = BSgate(theta, phi).U([10, 10, 10, 10], method="schwinger") + + assert np.allclose(U_vanilla, U_schwinger, atol=1e-6) From 7bbddbfd5c9feba7879298ccaa5e0a79064a427c Mon Sep 17 00:00:00 2001 From: Robbe De Prins <52749580+rdprins@users.noreply.github.com> Date: Fri, 23 Jun 2023 10:12:43 +0200 Subject: [PATCH 50/53] Compact fock correction (#249) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit **Context:** There was a bug in the algorithms for faster PNR sampling from Gaussian circuits using density matrices. In the case where the cutoff of the first detector was equal to 1, the resulting density matrix was incorrect. **Description of the Change:** The problem was solved by allowing for diagonal pivots to be used when the cutoffs[0]=1, effectively changing line 4 of Algorithm 1 in https://arxiv.org/pdf/2303.08879.pdf from `if diag_1 < C_1 − 1 then` to `if (diag_1 < C_1 − 1) or (C_1==1) then`. --------- Co-authored-by: Robbe De Prins (UGent-imec) --- .github/CHANGELOG.md | 6 +- .../numba/compactFock_1leftoverMode_amps.py | 2 +- .../numba/compactFock_1leftoverMode_grad.py | 2 +- .../math/numba/compactFock_diagonal_amps.py | 2 +- .../math/numba/compactFock_diagonal_grad.py | 2 +- tests/test_math/test_compactFock.py | 85 +++++++++++-------- 6 files changed, 57 insertions(+), 42 deletions(-) diff --git a/.github/CHANGELOG.md b/.github/CHANGELOG.md index f4b5f9e4d..bda9513d9 100644 --- a/.github/CHANGELOG.md +++ b/.github/CHANGELOG.md @@ -95,10 +95,14 @@ * When projecting a Gaussian state onto a Fock state, the upper limit of the autocutoff now respect the Fock projection. [(#246)](https://github.com/XanaduAI/MrMustard/pull/246) +* Fixed a bug for the algorithms that allow faster PNR sampling from Gaussian circuits using density matrices. When the +cutoff of the first detector is equal to 1, the resulting density matrix is now correct. + ### Documentation ### Contributors -[Filippo Miatto](https://github.com/ziofil), [Zeyue Niu](https://github.com/zeyueN) +[Filippo Miatto](https://github.com/ziofil), [Zeyue Niu](https://github.com/zeyueN), +[Robbe De Prins](https://github.com/rdprins) --- diff --git a/mrmustard/math/numba/compactFock_1leftoverMode_amps.py b/mrmustard/math/numba/compactFock_1leftoverMode_amps.py index c0666a8be..959ef5466 100644 --- a/mrmustard/math/numba/compactFock_1leftoverMode_amps.py +++ b/mrmustard/math/numba/compactFock_1leftoverMode_amps.py @@ -261,7 +261,7 @@ def fock_representation_1leftoverMode_amps_NUMBA( for sum_params in range(sum(cutoffs_tail)): for params in dict_params[sum_params]: # diagonal pivots: aa,bb,cc,dd,... - if params[0] < cutoffs_tail[0] - 1: + if (cutoffs_tail[0] == 1) or (params[0] < cutoffs_tail[0] - 1): arr1 = use_diag_pivot( A, B, M - 1, cutoff_leftoverMode, cutoffs_tail, params, arr0, arr1 ) diff --git a/mrmustard/math/numba/compactFock_1leftoverMode_grad.py b/mrmustard/math/numba/compactFock_1leftoverMode_grad.py index 0bb14c6c5..d94db1a4e 100644 --- a/mrmustard/math/numba/compactFock_1leftoverMode_grad.py +++ b/mrmustard/math/numba/compactFock_1leftoverMode_grad.py @@ -655,7 +655,7 @@ def fock_representation_1leftoverMode_grad_NUMBA( for sum_params in range(sum(cutoffs_tail)): for params in dict_params[sum_params]: # diagonal pivots: aa,bb,cc,dd,... - if params[0] < cutoffs_tail[0] - 1: + if (cutoffs_tail[0] == 1) or (params[0] < cutoffs_tail[0] - 1): arr1_dA, arr1_dB = use_diag_pivot_grad( A, B, diff --git a/mrmustard/math/numba/compactFock_diagonal_amps.py b/mrmustard/math/numba/compactFock_diagonal_amps.py index d65e4ca58..d78df379a 100644 --- a/mrmustard/math/numba/compactFock_diagonal_amps.py +++ b/mrmustard/math/numba/compactFock_diagonal_amps.py @@ -142,7 +142,7 @@ def fock_representation_diagonal_amps_NUMBA( for sum_params in range(sum(cutoffs)): for params in dict_params[sum_params]: # diagonal pivots: aa,bb,cc,dd,... - if params[0] < cutoffs[0] - 1: + if (cutoffs[0] == 1) or (params[0] < cutoffs[0] - 1): arr1 = use_diag_pivot(A, B, M, cutoffs, params, arr0, arr1) # off-diagonal pivots: d=0: (a+1)a,bb,cc,dd,... | d=1: 00,(b+1)b,cc,dd | 00,00,(c+1)c,dd | ... for d in range(M): diff --git a/mrmustard/math/numba/compactFock_diagonal_grad.py b/mrmustard/math/numba/compactFock_diagonal_grad.py index 6b767b182..b3aab8fcd 100644 --- a/mrmustard/math/numba/compactFock_diagonal_grad.py +++ b/mrmustard/math/numba/compactFock_diagonal_grad.py @@ -254,7 +254,7 @@ def fock_representation_diagonal_grad_NUMBA( for sum_params in range(sum(cutoffs)): for params in dict_params[sum_params]: # diagonal pivots: aa,bb,cc,dd,... - if params[0] < cutoffs[0] - 1: + if (cutoffs[0] == 1) or (params[0] < cutoffs[0] - 1): arr1_dA, arr1_dB = use_diag_pivot_grad( A, B, M, cutoffs, params, arr0, arr1, arr0_dA, arr1_dA, arr0_dB, arr1_dB ) diff --git a/tests/test_math/test_compactFock.py b/tests/test_math/test_compactFock.py index 2d9b0d828..26a11ce0c 100644 --- a/tests/test_math/test_compactFock.py +++ b/tests/test_math/test_compactFock.py @@ -15,6 +15,15 @@ math = Math() # use methods in math if you want them to be differentiable +def allowed_cutoffs(max_cutoffs): + r"""Generate all cutoffs from (1,)*M to max_cutoffs""" + res = [] + for idx in np.ndindex(max_cutoffs): + cutoffs = np.array(idx) + 1 + res.append(tuple(cutoffs)) + return res + + @st.composite def random_ABC(draw, M): r""" @@ -29,48 +38,50 @@ def random_ABC(draw, M): @given(random_ABC(M=3)) def test_compactFock_diagonal(A_B_G0): """Test getting Fock amplitudes if all modes are detected (math.hermite_renormalized_diagonal)""" - cutoffs = [7, 4, 8] - A, B, G0 = A_B_G0 # Create random state (M mode Gaussian state with displacement) - - # Vanilla MM - G_ref = math.hermite_renormalized( - math.conj(-A), math.conj(B), math.conj(G0), shape=list(cutoffs) * 2 - ).numpy() # note: shape=[C1,C2,C3,...,C1,C2,C3,...] - - # Extract diagonal amplitudes from vanilla MM - ref_diag = np.zeros(cutoffs, dtype=np.complex128) - for inds in np.ndindex(*cutoffs): - inds_expanded = list(inds) + list(inds) # a,b,c,a,b,c - ref_diag[inds] = G_ref[tuple(inds_expanded)] - - # New MM - G_diag = math.hermite_renormalized_diagonal(math.conj(-A), math.conj(B), math.conj(G0), cutoffs) - assert np.allclose(ref_diag, G_diag) + for cutoffs in allowed_cutoffs((7, 7, 7)): + A, B, G0 = A_B_G0 # Create random state (M mode Gaussian state with displacement) + + # Vanilla MM + G_ref = math.hermite_renormalized( + math.conj(-A), math.conj(B), math.conj(G0), shape=list(cutoffs) * 2 + ).numpy() # note: shape=[C1,C2,C3,...,C1,C2,C3,...] + + # Extract diagonal amplitudes from vanilla MM + ref_diag = np.zeros(cutoffs, dtype=np.complex128) + for inds in np.ndindex(*cutoffs): + inds_expanded = list(inds) + list(inds) # a,b,c,a,b,c + ref_diag[inds] = G_ref[tuple(inds_expanded)] + + # New MM + G_diag = math.hermite_renormalized_diagonal( + math.conj(-A), math.conj(B), math.conj(G0), cutoffs + ) + assert np.allclose(ref_diag, G_diag) @given(random_ABC(M=3)) def test_compactFock_1leftover(A_B_G0): """Test getting Fock amplitudes if all but the first mode are detected (math.hermite_renormalized_1leftoverMode)""" - cutoffs = [7, 4, 8] - A, B, G0 = A_B_G0 # Create random state (M mode Gaussian state with displacement) - - # New algorithm - G_leftover = math.hermite_renormalized_1leftoverMode( - math.conj(-A), math.conj(B), math.conj(G0), cutoffs - ) - - # Vanilla MM - G_ref = math.hermite_renormalized( - math.conj(-A), math.conj(B), math.conj(G0), shape=list(cutoffs) * 2 - ).numpy() # note: shape=[C1,C2,C3,...,C1,C2,C3,...] - - # Extract amplitudes of leftover mode from vanilla MM - ref_leftover = np.zeros([cutoffs[0]] * 2 + list(cutoffs)[1:], dtype=np.complex128) - for inds in np.ndindex(*cutoffs[1:]): - ref_leftover[tuple([slice(cutoffs[0]), slice(cutoffs[0])] + list(inds))] = G_ref[ - tuple([slice(cutoffs[0])] + list(inds) + [slice(cutoffs[0])] + list(inds)) - ] - assert np.allclose(ref_leftover, G_leftover) + for cutoffs in allowed_cutoffs((7, 7, 7)): + A, B, G0 = A_B_G0 # Create random state (M mode Gaussian state with displacement) + + # New algorithm + G_leftover = math.hermite_renormalized_1leftoverMode( + math.conj(-A), math.conj(B), math.conj(G0), cutoffs + ) + + # Vanilla MM + G_ref = math.hermite_renormalized( + math.conj(-A), math.conj(B), math.conj(G0), shape=list(cutoffs) * 2 + ).numpy() # note: shape=[C1,C2,C3,...,C1,C2,C3,...] + + # Extract amplitudes of leftover mode from vanilla MM + ref_leftover = np.zeros([cutoffs[0]] * 2 + list(cutoffs)[1:], dtype=np.complex128) + for inds in np.ndindex(*cutoffs[1:]): + ref_leftover[tuple([slice(cutoffs[0]), slice(cutoffs[0])] + list(inds))] = G_ref[ + tuple([slice(cutoffs[0])] + list(inds) + [slice(cutoffs[0])] + list(inds)) + ] + assert np.allclose(ref_leftover, G_leftover) def test_compactFock_diagonal_gradients(): From 73b2af639d465894266dd6e6beb9edd53c152bed Mon Sep 17 00:00:00 2001 From: Filippo Miatto Date: Mon, 26 Jun 2023 23:02:36 -0700 Subject: [PATCH 51/53] Bugfix gradients (#253) **Context:** The `more_strategies` PR introduced several custom gradients, but we never noticed they were broken under some conditions. **Description of the Change:** - Gradients are now cast to the backend array type - This PR also adds the numpy option to the bargmann methods **Benefits:** Gradients work **Possible Drawbacks:** None **Related GitHub Issues:** None --- mrmustard/lab/abstract/state.py | 8 ++++-- mrmustard/lab/abstract/transformation.py | 9 ++++-- mrmustard/lab/gates.py | 14 +++++----- mrmustard/math/tensorflow.py | 8 +++--- mrmustard/physics/fock.py | 20 ++++++++------ tests/test_physics/test_bargmann.py | 12 ++++++++ tests/test_training/test_opt.py | 35 +++++++++++++++++++++++- 7 files changed, 80 insertions(+), 26 deletions(-) diff --git a/mrmustard/lab/abstract/state.py b/mrmustard/lab/abstract/state.py index 9d12c11ef..8eb8c0e01 100644 --- a/mrmustard/lab/abstract/state.py +++ b/mrmustard/lab/abstract/state.py @@ -526,8 +526,10 @@ def __getitem__(self, item) -> State: self._modes = item return self - def bargmann(self) -> Optional[tuple[ComplexMatrix, ComplexVector, complex]]: - r"""Returns the Bargmann representation of the state.""" + def bargmann(self, numpy=False) -> Optional[tuple[ComplexMatrix, ComplexVector, complex]]: + r"""Returns the Bargmann representation of the state. + If numpy=True, returns the numpy arrays instead of the backend arrays. + """ if self.is_gaussian: if self.is_pure: A, B, C = bargmann.wigner_to_bargmann_psi(self.cov, self.means) @@ -535,6 +537,8 @@ def bargmann(self) -> Optional[tuple[ComplexMatrix, ComplexVector, complex]]: A, B, C = bargmann.wigner_to_bargmann_rho(self.cov, self.means) else: return None + if numpy: + return math.asnumpy(A), math.asnumpy(B), math.asnumpy(C) return A, B, C def get_modes(self, item) -> State: diff --git a/mrmustard/lab/abstract/transformation.py b/mrmustard/lab/abstract/transformation.py index 8c9e984c1..281791937 100644 --- a/mrmustard/lab/abstract/transformation.py +++ b/mrmustard/lab/abstract/transformation.py @@ -38,19 +38,22 @@ class Transformation: r"""Base class for all Transformations.""" is_unitary = True # whether the transformation is unitary (True by default) - def bargmann(self): + def bargmann(self, numpy=False): X, Y, d = self.XYd(allow_none=False) if self.is_unitary: - return bargmann.wigner_to_bargmann_U( + A, B, C = bargmann.wigner_to_bargmann_U( X if X is not None else math.identity(d.shape[-1], dtype=d.dtype), d if d is not None else math.zeros(X.shape[-1], dtype=X.dtype), ) else: - return bargmann.wigner_to_bargmann_Choi( + A, B, C = bargmann.wigner_to_bargmann_Choi( X if X is not None else math.identity(d.shape[-1], dtype=d.dtype), Y if Y is not None else math.zeros((d.shape[-1], d.shape[-1]), dtype=d.dtype), d if d is not None else math.zeros(X.shape[-1], dtype=X.dtype), ) + if numpy: + return math.asnumpy(A), math.asnumpy(B), math.asnumpy(C) + return A, B, C def primal(self, state: State) -> State: r"""Applies ``self`` (a ``Transformation``) to other (a ``State``) and returns the transformed state. diff --git a/mrmustard/lab/gates.py b/mrmustard/lab/gates.py index 515267cd0..f497e663c 100644 --- a/mrmustard/lab/gates.py +++ b/mrmustard/lab/gates.py @@ -123,9 +123,9 @@ def U(self, cutoffs: Sequence[int]): Ud = None for idx, out_in in enumerate(zip(shape[:N], shape[N:])): if Ud is None: - Ud = fock.displacement(x[idx], y[idx], out_in) + Ud = fock.displacement(x[idx], y[idx], shape=out_in) else: - U_next = fock.displacement(x[idx], y[idx], out_in) + U_next = fock.displacement(x[idx], y[idx], shape=out_in) Ud = math.outer(Ud, U_next) return math.transpose( @@ -133,7 +133,7 @@ def U(self, cutoffs: Sequence[int]): list(range(0, 2 * N, 2)) + list(range(1, 2 * N, 2)), ) else: - return fock.displacement(x[0], y[0], shape) + return fock.displacement(x[0], y[0], shape=shape) class Sgate(Parametrized, Transformation): @@ -204,16 +204,16 @@ def U(self, cutoffs: Sequence[int]): Us = None for idx, single_shape in enumerate(zip(shape[:N], shape[N:])): if Us is None: - Us = fock.squeezer(r=r[idx], phi=phi[idx], shape=single_shape) + Us = fock.squeezer(r[idx], phi[idx], shape=single_shape) else: - U_next = fock.squeezer(r=r[idx], phi=phi[idx], shape=single_shape) + U_next = fock.squeezer(r[idx], phi[idx], shape=single_shape) Us = math.outer(Us, U_next) return math.transpose( Us, list(range(0, 2 * N, 2)) + list(range(1, 2 * N, 2)), ) else: - return fock.squeezer(r=r[0], phi=phi[0], shape=shape) + return fock.squeezer(r[0], phi[0], shape=shape) @property def X_matrix(self): @@ -464,7 +464,7 @@ def U(self, cutoffs: Optional[List[int]], method=None): return fock.beamsplitter( self.theta.value, self.phi.value, - shape, + shape=shape, method=method or settings.DEFAULT_BS_METHOD, ) diff --git a/mrmustard/math/tensorflow.py b/mrmustard/math/tensorflow.py index 1d07473f0..5be6eba03 100644 --- a/mrmustard/math/tensorflow.py +++ b/mrmustard/math/tensorflow.py @@ -382,7 +382,7 @@ def hermite_renormalized( def grad(dLdGconj): dLdA, dLdB, dLdC = strategies.vanilla_vjp(G, _C, np.conj(dLdGconj)) - return np.conj(dLdA), np.conj(dLdB), np.conj(dLdC) + return self.conj(dLdA), self.conj(dLdB), self.conj(dLdC) return G, grad @@ -425,7 +425,7 @@ def hermite_renormalized_binomial( def grad(dLdGconj): dLdA, dLdB, dLdC = strategies.vanilla_vjp(G, _C, np.conj(dLdGconj)) - return np.conj(dLdA), np.conj(dLdB), np.conj(dLdC) + return self.conj(dLdA), self.conj(dLdB), self.conj(dLdC) return G, grad @@ -573,9 +573,9 @@ def boolean_mask(tensor: tf.Tensor, mask: tf.Tensor) -> Tensor: return tf.boolean_mask(tensor, mask) @staticmethod - def custom_gradient(func): + def custom_gradient(func, *args, **kwargs): """Decorator to define a function with a custom gradient.""" - return tf.custom_gradient(func) + return tf.custom_gradient(func, *args, **kwargs) # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ # Extras (not in the Interface) diff --git a/mrmustard/physics/fock.py b/mrmustard/physics/fock.py index bb8c97420..9fd1e049c 100644 --- a/mrmustard/physics/fock.py +++ b/mrmustard/physics/fock.py @@ -870,12 +870,12 @@ def displacement(x, y, shape, tol=1e-15): def grad(dL_dDc): dD_da, dD_dac = strategies.jacobian_displacement(math.asnumpy(gate), alpha) - dL_dac = np.conj(dL_dDc) * dD_dac + dL_dDc * np.conj(dD_da) + dL_dac = np.sum(np.conj(dL_dDc) * dD_dac + dL_dDc * np.conj(dD_da)) dLdx = 2 * np.real(dL_dac) dLdy = 2 * np.imag(dL_dac) - return dLdx, dLdy + return math.astensor(dLdx, dtype=x.dtype), math.astensor(dLdy, dtype=y.dtype) - return gate, grad + return math.astensor(gate, dtype=gate.dtype.name), grad @math.custom_gradient @@ -899,14 +899,15 @@ def beamsplitter(theta: float, phi: float, shape: Sequence[int], method: str): ) def vjp(dLdGc): - return strategies.beamsplitter_vjp( + dtheta, dphi = strategies.beamsplitter_vjp( math.asnumpy(bs_unitary), math.asnumpy(math.conj(dLdGc)), math.asnumpy(theta), math.asnumpy(phi), ) + return math.astensor(dtheta, dtype=theta.dtype), math.astensor(dphi, dtype=phi.dtype) - return bs_unitary, vjp + return math.astensor(bs_unitary, dtype=bs_unitary.dtype.name), vjp @math.custom_gradient @@ -921,9 +922,9 @@ def vjp(dLdGc): math.asnumpy(r), math.asnumpy(phi), ) - return dr, dphi + return math.astensor(dr, dtype=r.dtype), math.astensor(dphi, phi.dtype) - return sq_unitary, vjp + return math.astensor(sq_unitary, dtype=sq_unitary.dtype.name), vjp @math.custom_gradient @@ -932,11 +933,12 @@ def squeezed(r, phi, shape): sq_ket = strategies.squeezed(shape, math.asnumpy(r), math.asnumpy(phi)) def vjp(dLdGc): - return strategies.squeezed_vjp( + dr, dphi = strategies.squeezed_vjp( math.asnumpy(sq_ket), math.asnumpy(math.conj(dLdGc)), math.asnumpy(r), math.asnumpy(phi), ) + return math.astensor(dr, dtype=r.dtype), math.astensor(dphi, phi.dtype) - return sq_ket, vjp + return math.astensor(sq_ket, dtype=sq_ket.dtype.name), vjp diff --git a/tests/test_physics/test_bargmann.py b/tests/test_physics/test_bargmann.py index 06154fea8..229536b96 100644 --- a/tests/test_physics/test_bargmann.py +++ b/tests/test_physics/test_bargmann.py @@ -39,3 +39,15 @@ def test_wigner_to_bargmann_choi(): X, Y, d = G.XYd(allow_none=False) for x, y in zip(G.bargmann(), wigner_to_bargmann_Choi(X, Y, d)): assert np.allclose(x, y) + + +def test_bargmann_numpy_state(): + """Tests that the numpy option of the bargmann method of State works correctly""" + state = Gaussian(1) + assert all(isinstance(thing, np.ndarray) for thing in state.bargmann(numpy=True)) + + +def test_bargmann_numpy_transformation(): + """Tests that the numpy option of the bargmann method of State works correctly""" + transformation = Ggate(1) + assert all(isinstance(thing, np.ndarray) for thing in transformation.bargmann(numpy=True)) diff --git a/tests/test_training/test_opt.py b/tests/test_training/test_opt.py index 0b3063f95..9db5b0797 100644 --- a/tests/test_training/test_opt.py +++ b/tests/test_training/test_opt.py @@ -31,7 +31,7 @@ S2gate, Sgate, ) -from mrmustard.lab.states import DisplacedSqueezed, Gaussian, SqueezedVacuum, Vacuum +from mrmustard.lab.states import DisplacedSqueezed, Fock, Gaussian, SqueezedVacuum, Vacuum from mrmustard.math import Math from mrmustard.physics import fidelity from mrmustard.physics.gaussian import trace, von_neumann_entropy @@ -457,3 +457,36 @@ def cost_fn(): assert np.allclose(bsgate.theta.value, 0.1, atol=0.01) assert np.allclose(bsgate.phi.value, 0.2, atol=0.01) + + +def test_squeezing_grad_from_fock(): + """Test that the gradient of a squeezing gate is computed from the fock representation.""" + squeezing = Sgate(r=1, r_trainable=True) + + def cost_fn(): + return -(Fock(2) >> squeezing << Vacuum(1)) + + opt = Optimizer(euclidean_lr=0.05) + opt.minimize(cost_fn, by_optimizing=[squeezing], max_steps=100) + + +def test_displacement_grad_from_fock(): + """Test that the gradient of a displacement gate is computed from the fock representation.""" + disp = Dgate(x=1.0, y=1.0, x_trainable=True, y_trainable=True) + + def cost_fn(): + return -(Fock(2) >> disp << Vacuum(1)) + + opt = Optimizer(euclidean_lr=0.05) + opt.minimize(cost_fn, by_optimizing=[disp], max_steps=100) + + +def test_bsgate_grad_from_fock(): + """Test that the gradient of a beamsplitter gate is computed from the fock representation.""" + sq = SqueezedVacuum(r=1.0, r_trainable=True) + + def cost_fn(): + return -((sq & Fock(1)) >> BSgate(0.5) << (Vacuum(1) & Fock(1))) + + opt = Optimizer(euclidean_lr=0.05) + opt.minimize(cost_fn, by_optimizing=[sq], max_steps=100) From 1f5762d926c8a7630534e95e99c8134e4ecada07 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gabriele=20Gull=C3=AC?= <120967042+ggulli@users.noreply.github.com> Date: Tue, 18 Jul 2023 12:20:46 -0400 Subject: [PATCH 52/53] Migration to Poetry (#257) **Context:** This PR is responsible for migrating the dependencies management from `pip` to [`poetry`](https://python-poetry.org/). The goal is also to use Poetry for versioning, adopting as a single source of truth the version defined in the `pyproject.toml` file. **Description of the Change:** - `pyproject.toml` and `poetry.lock`: these two files have been introduced to properly configure and lock the dependencies of the project. The version used by poetry for the lock file is `1.4.0`. - `requirements` files: the requirements previously defined in the different requirements.txt files have been imported in `pyproject.toml`. The files have been removed from the project. The additional dependencies have been imported respectively under the groups `dev` (requirements-dev.txt) and `doc` (doc/requirements.txt). The versions imported are the ones previously defined in the requirements files. The only change introduced is related to a submodule of tensorflow to make it work between ARM/x86_64 architectures. - `setup.py`: the package configurations have been also imported in `pyproject.toml`. No change has been introduced in the different attributes such as authors, classifiers and so on. Probably some of the are out of date. - `_version.py`: some changes introduced to retrieve the package version from `pyproject.toml`. - Documentation: text changes introduced describing where poetry is now required to perform some actions. `.readthedocs.yml` has been also updated to build the documentation using poetry. - `Makefile`: commands to build or install the package updated to use poetry. - devcontainers: `Dockerfile` and script updated to use poetry. - CI: all GitHub Actions workflows have been updated to use Poetry. An additional change has been introduced in those workflows using a third party action to cancel previous runs, they now use a concurrency setting to do so. **Benefits:** - Dependencies are now managed in a centralized location and locked. - Package build can be executed by Poetry. - Package versioning can be managed through Poetry [version commands](https://python-poetry.org/docs/cli/#version). --- .devcontainer/Dockerfile | 2 + .devcontainer/post-install.sh | 7 +- .github/CHANGELOG.md | 5 +- .github/workflows/builds.yml | 36 +- .github/workflows/tests.yml | 28 +- .github/workflows/upload.yml | 24 +- .readthedocs.yml | 13 +- Dockerfile | 50 - Makefile | 12 +- doc/development/development_guide.rst | 8 +- doc/requirements.txt | 9 - mrmustard/_version.py | 19 +- poetry.lock | 3445 +++++++++++++++++++++++++ pyproject.toml | 71 + requirements-dev.txt | 5 - requirements.txt | 9 - setup.py | 70 - 17 files changed, 3605 insertions(+), 208 deletions(-) delete mode 100644 Dockerfile delete mode 100644 doc/requirements.txt create mode 100644 poetry.lock create mode 100644 pyproject.toml delete mode 100644 requirements-dev.txt delete mode 100644 requirements.txt delete mode 100644 setup.py diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile index 12f6a982d..16064a59f 100644 --- a/.devcontainer/Dockerfile +++ b/.devcontainer/Dockerfile @@ -36,6 +36,8 @@ RUN sh -c "$(wget -nv -O- https://raw.githubusercontent.com/ohmyzsh/ohmyzsh/mast # upgrade pip and install package manager RUN python -m pip install --no-cache-dir --upgrade pip +RUN pip install --no-cache-dir poetry==1.4.0 +RUN poetry config virtualenvs.create false ### TEAR DOWN IMAGE SETUP ### # switch back to dialog for any ad-hoc use of apt-get diff --git a/.devcontainer/post-install.sh b/.devcontainer/post-install.sh index c7cbf8aab..53bc587fa 100755 --- a/.devcontainer/post-install.sh +++ b/.devcontainer/post-install.sh @@ -1,8 +1,3 @@ #! /bin/zsh -pip install --no-cache-dir -r requirements.txt -pip install --no-cache-dir -r requirements-dev.txt -pip install --no-cache-dir -r doc/requirements.txt -pip install ray -pip install pylint -pip install -e . +poetry install --all-extras --with dev,doc diff --git a/.github/CHANGELOG.md b/.github/CHANGELOG.md index bda9513d9..7c5bdc20f 100644 --- a/.github/CHANGELOG.md +++ b/.github/CHANGELOG.md @@ -84,6 +84,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 @@ -102,7 +105,7 @@ cutoff of the first detector is equal to 1, the resulting density matrix is now ### Contributors [Filippo Miatto](https://github.com/ziofil), [Zeyue Niu](https://github.com/zeyueN), -[Robbe De Prins](https://github.com/rdprins) +[Robbe De Prins](https://github.com/rdprins), [Gabriele Gullì](https://github.com/ggulli) --- diff --git a/.github/workflows/builds.yml b/.github/workflows/builds.yml index f92a611a9..90887d6ab 100644 --- a/.github/workflows/builds.yml +++ b/.github/workflows/builds.yml @@ -1,4 +1,4 @@ -name: builds +name: Build package on: push: branches: @@ -12,37 +12,37 @@ jobs: runs-on: ubuntu-latest env: HYPOTHESIS_PROFILE: ci - strategy: fail-fast: false matrix: python-version: ['3.9', '3.10'] + concurrency: + group: ${{ github.workflow }}-${{ github.ref }}-${{ matrix.python-version }} + cancel-in-progress: true steps: - - name: Cancel Previous Runs - uses: styfle/cancel-workflow-action@0.11.0 - with: - access_token: ${{ github.token }} - - - uses: actions/checkout@v3 + - name: Checkout repo + uses: actions/checkout@v3 - - name: Set up Python + - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v4 with: python-version: ${{ matrix.python-version }} - name: Build and install Mr Mustard run: | - python -m pip install --upgrade pip wheel - python setup.py bdist_wheel + python -m pip install --no-cache-dir --upgrade pip + pip install --no-cache-dir poetry==1.4.0 + poetry config virtualenvs.create false + poetry build pip install dist/mrmustard*.whl + # Move to 'src' to properly test only installed package + # https://docs.pytest.org/en/7.1.x/explanation/goodpractices.html#tests-outside-application-code + mkdir src + mv mrmustard src - - name: Install test dependencies - run: | - pip install -r requirements.txt - pip install -r requirements-dev.txt - pip install wheel pytest pytest-cov pytest-mock hypothesis --upgrade + - name: Install only test dependencies + run: poetry install --no-root --extras "ray" --with dev - name: Run tests - run: | - python -m pytest tests -p no:warnings --tb=native + run: python -m pytest tests -p no:warnings --tb=native diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 3c2d9b47e..ad0ff8caf 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -8,38 +8,34 @@ on: - '.github/workflows/tests.yml' - 'mrmustard/**' - 'tests/**' - - 'setup.py' - - 'requirements*' + - 'pyproject.toml' + - 'poetry.lock' - 'pytest.ini' jobs: pytest: runs-on: ubuntu-latest + concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true env: HYPOTHESIS_PROFILE: ci steps: - - name: Cancel Previous Runs - uses: styfle/cancel-workflow-action@0.11.0 - with: - access_token: ${{ github.token }} - - - uses: actions/checkout@v3 + - name: Checkout repo + uses: actions/checkout@v3 - - name: Setup python ${{ matrix.python-version }} + - name: Setup python uses: actions/setup-python@v4 with: python-version: '3.9' - name: Install dependencies run: | - python -m pip install --upgrade pip - pip install -r requirements.txt - pip install -r requirements-dev.txt - pip install wheel pytest pytest-cov --upgrade - pip install .[ray] - # python setup.py bdist_wheel - # pip install dist/*.whl + python -m pip install --no-cache-dir --upgrade pip + pip install --no-cache-dir poetry==1.4.0 + poetry config virtualenvs.create false + poetry install --extras "ray" --with dev - name: Run tests run: python -m pytest tests --cov=mrmustard --cov-report=term-missing --cov-report=xml -p no:warnings --tb=native diff --git a/.github/workflows/upload.yml b/.github/workflows/upload.yml index e52e7ba79..2545e285c 100644 --- a/.github/workflows/upload.yml +++ b/.github/workflows/upload.yml @@ -10,7 +10,8 @@ jobs: HYPOTHESIS_PROFILE: ci steps: - - uses: actions/checkout@v3 + - name: Checkout repo + uses: actions/checkout@v3 - name: Set up Python uses: actions/setup-python@v4 @@ -19,21 +20,24 @@ jobs: - name: Build and install Mr Mustard run: | - python -m pip install --upgrade pip wheel - python setup.py bdist_wheel + python -m pip install --no-cache-dir --upgrade pip + pip install --no-cache-dir poetry==1.4.0 + poetry config virtualenvs.create false + poetry build pip install dist/mrmustard*.whl + # Move to 'src' to properly test only installed package + # https://docs.pytest.org/en/7.1.x/explanation/goodpractices.html#tests-outside-application-code + mkdir src + mv mrmustard src - - name: Install test dependencies - run: | - pip install -r requirements.txt - pip install wheel pytest pytest-cov pytest-mock hypothesis --upgrade + - name: Install only test dependencies + run: poetry install --no-root --extras "ray" --with dev - name: Run tests - run: | - python -m pytest tests -p no:warnings --tb=native + run: python -m pytest tests -p no:warnings --tb=native - name: Publish - uses: pypa/gh-action-pypi-publish@master + uses: pypa/gh-action-pypi-publish@release/v1 with: user: __token__ password: ${{ secrets.PIPY_TOKEN }} diff --git a/.readthedocs.yml b/.readthedocs.yml index 195e00930..a7dbfa275 100644 --- a/.readthedocs.yml +++ b/.readthedocs.yml @@ -6,6 +6,11 @@ build: os: ubuntu-22.04 tools: python: "3.9" + jobs: + post_install: + - pip install --no-cache-dir poetry==1.4.0 + - poetry config virtualenvs.create false + - poetry install --with doc # Build documentation in the docs/ directory with Sphinx sphinx: @@ -16,9 +21,5 @@ sphinx: # - pdf # Optionally declare the Python requirements required to build your docs -python: - install: - - requirements: doc/requirements.txt - - method: pip - path: . - system_packages: true +# python: +# install: diff --git a/Dockerfile b/Dockerfile deleted file mode 100644 index cf329f62f..000000000 --- a/Dockerfile +++ /dev/null @@ -1,50 +0,0 @@ -FROM python:3.10 - -# Configure apt for setup -ENV DEBIAN_FRONTEND=noninteractive - -WORKDIR /mrmustard -COPY . . - -RUN apt-get update && \ - apt-get -y install --no-install-recommends sudo \ - zsh \ - less \ - curl \ - wget \ - graphviz \ - fonts-powerline \ - locales \ - git \ - && apt-get clean && rm -rf /var/lib/apt/lists/* - -### GIT GLOBAL SETUP ### - -RUN git config --global core.excludesfile /.globalgitignore -RUN touch /.globalgitignore -RUN echo ".notebooks" >> /.globalgitignore -RUN echo "nohup.out" >> /.globalgitignore - -### ZSH TERMINAL SETUP ### - -# generate locale for zsh terminal agnoster theme -RUN echo "en_US.UTF-8 UTF-8" > /etc/locale.gen && /usr/sbin/locale-gen -RUN locale-gen en_US.UTF-8 -# set term to be bash instead of sh -ENV TERM xterm -ENV SHELL /bin/zsh -# install oh-my-zsh -RUN sh -c "$(wget -nv -O- https://raw.githubusercontent.com/ohmyzsh/ohmyzsh/master/tools/install.sh)" - -### PYTHON DEPENDENCIES INTALLATION ### - -# upgrade pip and install package manager -RUN python -m pip install --no-cache-dir --upgrade pip -RUN pip install --no-cache-dir -r requirements.txt -RUN pip install --no-cache-dir -r requirements-dev.txt -RUN pip install --no-cache-dir ray -RUN pip install --no-cache-dir -e . - -### TEAR DOWN IMAGE SETUP ### -# switch back to dialog for any ad-hoc use of apt-get -ENV DEBIAN_FRONTEND=dialog diff --git a/Makefile b/Makefile index d20cad652..fcd55052a 100644 --- a/Makefile +++ b/Makefile @@ -6,6 +6,7 @@ COVERAGE := --cov=mrmustard --cov-report=html:coverage_html_report --cov-append help: @echo "Please use \`make ' where is one of" @echo " install to install Mr Mustard" + @echo " install-all to install Mr Mustard with all extras and optional dependencies" @echo " dist to package the source distribution" @echo " clean to delete all temporary, cache, and build files" @echo " clean-docs to delete all built documentation" @@ -18,11 +19,18 @@ install: ifndef PYTHON3 @echo "To install Mr Mustard you need to have Python 3 installed" endif - $(PYTHON) setup.py install + poetry install + +.PHONY: install-all +install-all: +ifndef PYTHON3 + @echo "To install Mr Mustard you need to have Python 3 installed" +endif + poetry install --all-extras --with dev,doc .PHONY: dist dist: - $(PYTHON) setup.py sdist + poetry build .PHONY : clean clean: diff --git a/doc/development/development_guide.rst b/doc/development/development_guide.rst index b7ffbf419..9711859c9 100644 --- a/doc/development/development_guide.rst +++ b/doc/development/development_guide.rst @@ -66,11 +66,11 @@ Development environment ----------------------- Mr Mustard uses a ``pytest`` suite for testing and ``black`` for formatting. These -dependencies can be installed via ``pip``: +dependencies can be installed via ``poetry``: .. code-block:: bash - pip install -r requirements-dev.txt + poetry install --with dev Software tests -------------- @@ -175,11 +175,11 @@ Documentation ------------- Additional packages are required to build the documentation, as specified in -``doc/requirements.txt``. These packages can be installed using: +``pyproject.toml`` under the group ``doc``. These packages can be installed using: .. code-block:: bash - pip install -r doc/requirements.txt + poetry install --with doc from within the top-level directory. To then build the HTML documentation, run diff --git a/doc/requirements.txt b/doc/requirements.txt deleted file mode 100644 index deb3032e8..000000000 --- a/doc/requirements.txt +++ /dev/null @@ -1,9 +0,0 @@ -sphinx -docutils -m2r2 -sphinx-autodoc-typehints -sphinx-copybutton -sphinx-automodapi -sphinxcontrib-bibtex -mistune==0.8.4 -xanadu-sphinx-theme==0.1.0 \ No newline at end of file diff --git a/mrmustard/_version.py b/mrmustard/_version.py index 4df8b481b..c9955a3aa 100644 --- a/mrmustard/_version.py +++ b/mrmustard/_version.py @@ -13,7 +13,22 @@ # limitations under the License. """Version information. - Version number (major.minor.patch[-label]) + +Version number retrieved from pyproject.toml file """ +from pathlib import Path +import tomli + + +def _get_project_root(): + """Compute and return root dir""" + return Path(__file__).parent.parent + + +def _get_project_version(): + """Parse 'pyproject.toml' and return current version""" + with open(f"{_get_project_root()}/pyproject.toml", mode="rb") as pyproject: + return tomli.load(pyproject)["tool"]["poetry"]["version"] + -__version__ = "0.5.0-dev" +__version__ = str(_get_project_version()) diff --git a/poetry.lock b/poetry.lock new file mode 100644 index 000000000..df4e08254 --- /dev/null +++ b/poetry.lock @@ -0,0 +1,3445 @@ +# This file is automatically @generated by Poetry 1.4.0 and should not be changed by hand. + +[[package]] +name = "absl-py" +version = "1.4.0" +description = "Abseil Python Common Libraries, see https://github.com/abseil/abseil-py." +category = "main" +optional = false +python-versions = ">=3.6" +files = [ + {file = "absl-py-1.4.0.tar.gz", hash = "sha256:d2c244d01048ba476e7c080bd2c6df5e141d211de80223460d5b3b8a2a58433d"}, + {file = "absl_py-1.4.0-py3-none-any.whl", hash = "sha256:0d3fe606adfa4f7db64792dd4c7aee4ee0c38ab75dfd353b7a83ed3e957fcb47"}, +] + +[[package]] +name = "aiosignal" +version = "1.3.1" +description = "aiosignal: a list of registered asynchronous callbacks" +category = "main" +optional = true +python-versions = ">=3.7" +files = [ + {file = "aiosignal-1.3.1-py3-none-any.whl", hash = "sha256:f8376fb07dd1e86a584e4fcdec80b36b7f81aac666ebc724e2c090300dd83b17"}, + {file = "aiosignal-1.3.1.tar.gz", hash = "sha256:54cd96e15e1649b75d6c87526a6ff0b6c1b0dd3459f43d9ca11d48c339b68cfc"}, +] + +[package.dependencies] +frozenlist = ">=1.1.0" + +[[package]] +name = "alabaster" +version = "0.7.13" +description = "A configurable sidebar-enabled Sphinx theme" +category = "dev" +optional = false +python-versions = ">=3.6" +files = [ + {file = "alabaster-0.7.13-py3-none-any.whl", hash = "sha256:1ee19aca801bbabb5ba3f5f258e4422dfa86f82f3e9cefb0859b283cdd7f62a3"}, + {file = "alabaster-0.7.13.tar.gz", hash = "sha256:a27a4a084d5e690e16e01e03ad2b2e552c61a65469419b907243193de1a84ae2"}, +] + +[[package]] +name = "appdirs" +version = "1.4.4" +description = "A small Python module for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." +category = "dev" +optional = false +python-versions = "*" +files = [ + {file = "appdirs-1.4.4-py2.py3-none-any.whl", hash = "sha256:a841dacd6b99318a741b166adb07e19ee71a274450e68237b4650ca1055ab128"}, + {file = "appdirs-1.4.4.tar.gz", hash = "sha256:7d5d0167b2b1ba821647616af46a749d1c653740dd0d2415100fe26e27afdf41"}, +] + +[[package]] +name = "astroid" +version = "2.7.3" +description = "An abstract syntax tree for Python with inference support." +category = "dev" +optional = false +python-versions = "~=3.6" +files = [ + {file = "astroid-2.7.3-py3-none-any.whl", hash = "sha256:dc1e8b28427d6bbef6b8842b18765ab58f558c42bb80540bd7648c98412af25e"}, + {file = "astroid-2.7.3.tar.gz", hash = "sha256:3b680ce0419b8a771aba6190139a3998d14b413852506d99aff8dc2bf65ee67c"}, +] + +[package.dependencies] +lazy-object-proxy = ">=1.4.0" +setuptools = ">=20.0" +wrapt = ">=1.11,<1.13" + +[[package]] +name = "astunparse" +version = "1.6.3" +description = "An AST unparser for Python" +category = "main" +optional = false +python-versions = "*" +files = [ + {file = "astunparse-1.6.3-py2.py3-none-any.whl", hash = "sha256:c2652417f2c8b5bb325c885ae329bdf3f86424075c4fd1a128674bc6fba4b8e8"}, + {file = "astunparse-1.6.3.tar.gz", hash = "sha256:5ad93a8456f0d084c3456d059fd9a92cce667963232cbf763eac3bc5b7940872"}, +] + +[package.dependencies] +six = ">=1.6.1,<2.0" +wheel = ">=0.23.0,<1.0" + +[[package]] +name = "atomicwrites" +version = "1.4.1" +description = "Atomic file writes." +category = "dev" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +files = [ + {file = "atomicwrites-1.4.1.tar.gz", hash = "sha256:81b2c9071a49367a7f770170e5eec8cb66567cfbbc8c73d20ce5ca4a8d71cf11"}, +] + +[[package]] +name = "attrs" +version = "23.1.0" +description = "Classes Without Boilerplate" +category = "main" +optional = false +python-versions = ">=3.7" +files = [ + {file = "attrs-23.1.0-py3-none-any.whl", hash = "sha256:1f28b4522cdc2fb4256ac1a020c78acf9cba2c6b461ccd2c126f3aa8e8335d04"}, + {file = "attrs-23.1.0.tar.gz", hash = "sha256:6279836d581513a26f1bf235f9acd333bc9115683f14f7e8fae46c98fc50e015"}, +] + +[package.extras] +cov = ["attrs[tests]", "coverage[toml] (>=5.3)"] +dev = ["attrs[docs,tests]", "pre-commit"] +docs = ["furo", "myst-parser", "sphinx", "sphinx-notfound-page", "sphinxcontrib-towncrier", "towncrier", "zope-interface"] +tests = ["attrs[tests-no-zope]", "zope-interface"] +tests-no-zope = ["cloudpickle", "hypothesis", "mypy (>=1.1.1)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-xdist[psutil]"] + +[[package]] +name = "babel" +version = "2.12.1" +description = "Internationalization utilities" +category = "dev" +optional = false +python-versions = ">=3.7" +files = [ + {file = "Babel-2.12.1-py3-none-any.whl", hash = "sha256:b4246fb7677d3b98f501a39d43396d3cafdc8eadb045f4a31be01863f655c610"}, + {file = "Babel-2.12.1.tar.gz", hash = "sha256:cc2d99999cd01d44420ae725a21c9e3711b3aadc7976d6147f622d8581963455"}, +] + +[[package]] +name = "black" +version = "23.7.0" +description = "The uncompromising code formatter." +category = "dev" +optional = false +python-versions = ">=3.8" +files = [ + {file = "black-23.7.0-cp310-cp310-macosx_10_16_arm64.whl", hash = "sha256:5c4bc552ab52f6c1c506ccae05681fab58c3f72d59ae6e6639e8885e94fe2587"}, + {file = "black-23.7.0-cp310-cp310-macosx_10_16_universal2.whl", hash = "sha256:552513d5cd5694590d7ef6f46e1767a4df9af168d449ff767b13b084c020e63f"}, + {file = "black-23.7.0-cp310-cp310-macosx_10_16_x86_64.whl", hash = "sha256:86cee259349b4448adb4ef9b204bb4467aae74a386bce85d56ba4f5dc0da27be"}, + {file = "black-23.7.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:501387a9edcb75d7ae8a4412bb8749900386eaef258f1aefab18adddea1936bc"}, + {file = "black-23.7.0-cp310-cp310-win_amd64.whl", hash = "sha256:fb074d8b213749fa1d077d630db0d5f8cc3b2ae63587ad4116e8a436e9bbe995"}, + {file = "black-23.7.0-cp311-cp311-macosx_10_16_arm64.whl", hash = "sha256:b5b0ee6d96b345a8b420100b7d71ebfdd19fab5e8301aff48ec270042cd40ac2"}, + {file = "black-23.7.0-cp311-cp311-macosx_10_16_universal2.whl", hash = "sha256:893695a76b140881531062d48476ebe4a48f5d1e9388177e175d76234ca247cd"}, + {file = "black-23.7.0-cp311-cp311-macosx_10_16_x86_64.whl", hash = "sha256:c333286dc3ddca6fdff74670b911cccedacb4ef0a60b34e491b8a67c833b343a"}, + {file = "black-23.7.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:831d8f54c3a8c8cf55f64d0422ee875eecac26f5f649fb6c1df65316b67c8926"}, + {file = "black-23.7.0-cp311-cp311-win_amd64.whl", hash = "sha256:7f3bf2dec7d541b4619b8ce526bda74a6b0bffc480a163fed32eb8b3c9aed8ad"}, + {file = "black-23.7.0-cp38-cp38-macosx_10_16_arm64.whl", hash = "sha256:f9062af71c59c004cd519e2fb8f5d25d39e46d3af011b41ab43b9c74e27e236f"}, + {file = "black-23.7.0-cp38-cp38-macosx_10_16_universal2.whl", hash = "sha256:01ede61aac8c154b55f35301fac3e730baf0c9cf8120f65a9cd61a81cfb4a0c3"}, + {file = "black-23.7.0-cp38-cp38-macosx_10_16_x86_64.whl", hash = "sha256:327a8c2550ddc573b51e2c352adb88143464bb9d92c10416feb86b0f5aee5ff6"}, + {file = "black-23.7.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6d1c6022b86f83b632d06f2b02774134def5d4d4f1dac8bef16d90cda18ba28a"}, + {file = "black-23.7.0-cp38-cp38-win_amd64.whl", hash = "sha256:27eb7a0c71604d5de083757fbdb245b1a4fae60e9596514c6ec497eb63f95320"}, + {file = "black-23.7.0-cp39-cp39-macosx_10_16_arm64.whl", hash = "sha256:8417dbd2f57b5701492cd46edcecc4f9208dc75529bcf76c514864e48da867d9"}, + {file = "black-23.7.0-cp39-cp39-macosx_10_16_universal2.whl", hash = "sha256:47e56d83aad53ca140da0af87678fb38e44fd6bc0af71eebab2d1f59b1acf1d3"}, + {file = "black-23.7.0-cp39-cp39-macosx_10_16_x86_64.whl", hash = "sha256:25cc308838fe71f7065df53aedd20327969d05671bac95b38fdf37ebe70ac087"}, + {file = "black-23.7.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:642496b675095d423f9b8448243336f8ec71c9d4d57ec17bf795b67f08132a91"}, + {file = "black-23.7.0-cp39-cp39-win_amd64.whl", hash = "sha256:ad0014efc7acf0bd745792bd0d8857413652979200ab924fbf239062adc12491"}, + {file = "black-23.7.0-py3-none-any.whl", hash = "sha256:9fd59d418c60c0348505f2ddf9609c1e1de8e7493eab96198fc89d9f865e7a96"}, + {file = "black-23.7.0.tar.gz", hash = "sha256:022a582720b0d9480ed82576c920a8c1dde97cc38ff11d8d8859b3bd6ca9eedb"}, +] + +[package.dependencies] +click = ">=8.0.0" +mypy-extensions = ">=0.4.3" +packaging = ">=22.0" +pathspec = ">=0.9.0" +platformdirs = ">=2" +tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} +typing-extensions = {version = ">=3.10.0.0", markers = "python_version < \"3.10\""} + +[package.extras] +colorama = ["colorama (>=0.4.3)"] +d = ["aiohttp (>=3.7.4)"] +jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"] +uvloop = ["uvloop (>=0.15.2)"] + +[[package]] +name = "cachetools" +version = "5.3.1" +description = "Extensible memoizing collections and decorators" +category = "main" +optional = false +python-versions = ">=3.7" +files = [ + {file = "cachetools-5.3.1-py3-none-any.whl", hash = "sha256:95ef631eeaea14ba2e36f06437f36463aac3a096799e876ee55e5cdccb102590"}, + {file = "cachetools-5.3.1.tar.gz", hash = "sha256:dce83f2d9b4e1f732a8cd44af8e8fab2dbe46201467fc98b3ef8f269092bf62b"}, +] + +[[package]] +name = "certifi" +version = "2023.5.7" +description = "Python package for providing Mozilla's CA Bundle." +category = "main" +optional = false +python-versions = ">=3.6" +files = [ + {file = "certifi-2023.5.7-py3-none-any.whl", hash = "sha256:c6c2e98f5c7869efca1f8916fed228dd91539f9f1b444c314c06eef02980c716"}, + {file = "certifi-2023.5.7.tar.gz", hash = "sha256:0f0d56dc5a6ad56fd4ba36484d6cc34451e1c6548c61daad8c320169f91eddc7"}, +] + +[[package]] +name = "charset-normalizer" +version = "3.2.0" +description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." +category = "main" +optional = false +python-versions = ">=3.7.0" +files = [ + {file = "charset-normalizer-3.2.0.tar.gz", hash = "sha256:3bb3d25a8e6c0aedd251753a79ae98a093c7e7b471faa3aa9a93a81431987ace"}, + {file = "charset_normalizer-3.2.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:0b87549028f680ca955556e3bd57013ab47474c3124dc069faa0b6545b6c9710"}, + {file = "charset_normalizer-3.2.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:7c70087bfee18a42b4040bb9ec1ca15a08242cf5867c58726530bdf3945672ed"}, + {file = "charset_normalizer-3.2.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a103b3a7069b62f5d4890ae1b8f0597618f628b286b03d4bc9195230b154bfa9"}, + {file = "charset_normalizer-3.2.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:94aea8eff76ee6d1cdacb07dd2123a68283cb5569e0250feab1240058f53b623"}, + {file = "charset_normalizer-3.2.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:db901e2ac34c931d73054d9797383d0f8009991e723dab15109740a63e7f902a"}, + {file = "charset_normalizer-3.2.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b0dac0ff919ba34d4df1b6131f59ce95b08b9065233446be7e459f95554c0dc8"}, + {file = "charset_normalizer-3.2.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:193cbc708ea3aca45e7221ae58f0fd63f933753a9bfb498a3b474878f12caaad"}, + {file = "charset_normalizer-3.2.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:09393e1b2a9461950b1c9a45d5fd251dc7c6f228acab64da1c9c0165d9c7765c"}, + {file = "charset_normalizer-3.2.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:baacc6aee0b2ef6f3d308e197b5d7a81c0e70b06beae1f1fcacffdbd124fe0e3"}, + {file = "charset_normalizer-3.2.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:bf420121d4c8dce6b889f0e8e4ec0ca34b7f40186203f06a946fa0276ba54029"}, + {file = "charset_normalizer-3.2.0-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:c04a46716adde8d927adb9457bbe39cf473e1e2c2f5d0a16ceb837e5d841ad4f"}, + {file = "charset_normalizer-3.2.0-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:aaf63899c94de41fe3cf934601b0f7ccb6b428c6e4eeb80da72c58eab077b19a"}, + {file = "charset_normalizer-3.2.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:d62e51710986674142526ab9f78663ca2b0726066ae26b78b22e0f5e571238dd"}, + {file = "charset_normalizer-3.2.0-cp310-cp310-win32.whl", hash = "sha256:04e57ab9fbf9607b77f7d057974694b4f6b142da9ed4a199859d9d4d5c63fe96"}, + {file = "charset_normalizer-3.2.0-cp310-cp310-win_amd64.whl", hash = "sha256:48021783bdf96e3d6de03a6e39a1171ed5bd7e8bb93fc84cc649d11490f87cea"}, + {file = "charset_normalizer-3.2.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:4957669ef390f0e6719db3613ab3a7631e68424604a7b448f079bee145da6e09"}, + {file = "charset_normalizer-3.2.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:46fb8c61d794b78ec7134a715a3e564aafc8f6b5e338417cb19fe9f57a5a9bf2"}, + {file = "charset_normalizer-3.2.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f779d3ad205f108d14e99bb3859aa7dd8e9c68874617c72354d7ecaec2a054ac"}, + {file = "charset_normalizer-3.2.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f25c229a6ba38a35ae6e25ca1264621cc25d4d38dca2942a7fce0b67a4efe918"}, + {file = "charset_normalizer-3.2.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2efb1bd13885392adfda4614c33d3b68dee4921fd0ac1d3988f8cbb7d589e72a"}, + {file = "charset_normalizer-3.2.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1f30b48dd7fa1474554b0b0f3fdfdd4c13b5c737a3c6284d3cdc424ec0ffff3a"}, + {file = "charset_normalizer-3.2.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:246de67b99b6851627d945db38147d1b209a899311b1305dd84916f2b88526c6"}, + {file = "charset_normalizer-3.2.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9bd9b3b31adcb054116447ea22caa61a285d92e94d710aa5ec97992ff5eb7cf3"}, + {file = "charset_normalizer-3.2.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:8c2f5e83493748286002f9369f3e6607c565a6a90425a3a1fef5ae32a36d749d"}, + {file = "charset_normalizer-3.2.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:3170c9399da12c9dc66366e9d14da8bf7147e1e9d9ea566067bbce7bb74bd9c2"}, + {file = "charset_normalizer-3.2.0-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:7a4826ad2bd6b07ca615c74ab91f32f6c96d08f6fcc3902ceeedaec8cdc3bcd6"}, + {file = "charset_normalizer-3.2.0-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:3b1613dd5aee995ec6d4c69f00378bbd07614702a315a2cf6c1d21461fe17c23"}, + {file = "charset_normalizer-3.2.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:9e608aafdb55eb9f255034709e20d5a83b6d60c054df0802fa9c9883d0a937aa"}, + {file = "charset_normalizer-3.2.0-cp311-cp311-win32.whl", hash = "sha256:f2a1d0fd4242bd8643ce6f98927cf9c04540af6efa92323e9d3124f57727bfc1"}, + {file = "charset_normalizer-3.2.0-cp311-cp311-win_amd64.whl", hash = "sha256:681eb3d7e02e3c3655d1b16059fbfb605ac464c834a0c629048a30fad2b27489"}, + {file = "charset_normalizer-3.2.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:c57921cda3a80d0f2b8aec7e25c8aa14479ea92b5b51b6876d975d925a2ea346"}, + {file = "charset_normalizer-3.2.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:41b25eaa7d15909cf3ac4c96088c1f266a9a93ec44f87f1d13d4a0e86c81b982"}, + {file = "charset_normalizer-3.2.0-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f058f6963fd82eb143c692cecdc89e075fa0828db2e5b291070485390b2f1c9c"}, + {file = "charset_normalizer-3.2.0-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a7647ebdfb9682b7bb97e2a5e7cb6ae735b1c25008a70b906aecca294ee96cf4"}, + {file = "charset_normalizer-3.2.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eef9df1eefada2c09a5e7a40991b9fc6ac6ef20b1372abd48d2794a316dc0449"}, + {file = "charset_normalizer-3.2.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e03b8895a6990c9ab2cdcd0f2fe44088ca1c65ae592b8f795c3294af00a461c3"}, + {file = "charset_normalizer-3.2.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:ee4006268ed33370957f55bf2e6f4d263eaf4dc3cfc473d1d90baff6ed36ce4a"}, + {file = "charset_normalizer-3.2.0-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:c4983bf937209c57240cff65906b18bb35e64ae872da6a0db937d7b4af845dd7"}, + {file = "charset_normalizer-3.2.0-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:3bb7fda7260735efe66d5107fb7e6af6a7c04c7fce9b2514e04b7a74b06bf5dd"}, + {file = "charset_normalizer-3.2.0-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:72814c01533f51d68702802d74f77ea026b5ec52793c791e2da806a3844a46c3"}, + {file = "charset_normalizer-3.2.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:70c610f6cbe4b9fce272c407dd9d07e33e6bf7b4aa1b7ffb6f6ded8e634e3592"}, + {file = "charset_normalizer-3.2.0-cp37-cp37m-win32.whl", hash = "sha256:a401b4598e5d3f4a9a811f3daf42ee2291790c7f9d74b18d75d6e21dda98a1a1"}, + {file = "charset_normalizer-3.2.0-cp37-cp37m-win_amd64.whl", hash = "sha256:c0b21078a4b56965e2b12f247467b234734491897e99c1d51cee628da9786959"}, + {file = "charset_normalizer-3.2.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:95eb302ff792e12aba9a8b8f8474ab229a83c103d74a750ec0bd1c1eea32e669"}, + {file = "charset_normalizer-3.2.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1a100c6d595a7f316f1b6f01d20815d916e75ff98c27a01ae817439ea7726329"}, + {file = "charset_normalizer-3.2.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:6339d047dab2780cc6220f46306628e04d9750f02f983ddb37439ca47ced7149"}, + {file = "charset_normalizer-3.2.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e4b749b9cc6ee664a3300bb3a273c1ca8068c46be705b6c31cf5d276f8628a94"}, + {file = "charset_normalizer-3.2.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a38856a971c602f98472050165cea2cdc97709240373041b69030be15047691f"}, + {file = "charset_normalizer-3.2.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f87f746ee241d30d6ed93969de31e5ffd09a2961a051e60ae6bddde9ec3583aa"}, + {file = "charset_normalizer-3.2.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:89f1b185a01fe560bc8ae5f619e924407efca2191b56ce749ec84982fc59a32a"}, + {file = "charset_normalizer-3.2.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e1c8a2f4c69e08e89632defbfabec2feb8a8d99edc9f89ce33c4b9e36ab63037"}, + {file = "charset_normalizer-3.2.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:2f4ac36d8e2b4cc1aa71df3dd84ff8efbe3bfb97ac41242fbcfc053c67434f46"}, + {file = "charset_normalizer-3.2.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:a386ebe437176aab38c041de1260cd3ea459c6ce5263594399880bbc398225b2"}, + {file = "charset_normalizer-3.2.0-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:ccd16eb18a849fd8dcb23e23380e2f0a354e8daa0c984b8a732d9cfaba3a776d"}, + {file = "charset_normalizer-3.2.0-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:e6a5bf2cba5ae1bb80b154ed68a3cfa2fa00fde979a7f50d6598d3e17d9ac20c"}, + {file = "charset_normalizer-3.2.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:45de3f87179c1823e6d9e32156fb14c1927fcc9aba21433f088fdfb555b77c10"}, + {file = "charset_normalizer-3.2.0-cp38-cp38-win32.whl", hash = "sha256:1000fba1057b92a65daec275aec30586c3de2401ccdcd41f8a5c1e2c87078706"}, + {file = "charset_normalizer-3.2.0-cp38-cp38-win_amd64.whl", hash = "sha256:8b2c760cfc7042b27ebdb4a43a4453bd829a5742503599144d54a032c5dc7e9e"}, + {file = "charset_normalizer-3.2.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:855eafa5d5a2034b4621c74925d89c5efef61418570e5ef9b37717d9c796419c"}, + {file = "charset_normalizer-3.2.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:203f0c8871d5a7987be20c72442488a0b8cfd0f43b7973771640fc593f56321f"}, + {file = "charset_normalizer-3.2.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e857a2232ba53ae940d3456f7533ce6ca98b81917d47adc3c7fd55dad8fab858"}, + {file = "charset_normalizer-3.2.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5e86d77b090dbddbe78867a0275cb4df08ea195e660f1f7f13435a4649e954e5"}, + {file = "charset_normalizer-3.2.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c4fb39a81950ec280984b3a44f5bd12819953dc5fa3a7e6fa7a80db5ee853952"}, + {file = "charset_normalizer-3.2.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2dee8e57f052ef5353cf608e0b4c871aee320dd1b87d351c28764fc0ca55f9f4"}, + {file = "charset_normalizer-3.2.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8700f06d0ce6f128de3ccdbc1acaea1ee264d2caa9ca05daaf492fde7c2a7200"}, + {file = "charset_normalizer-3.2.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1920d4ff15ce893210c1f0c0e9d19bfbecb7983c76b33f046c13a8ffbd570252"}, + {file = "charset_normalizer-3.2.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:c1c76a1743432b4b60ab3358c937a3fe1341c828ae6194108a94c69028247f22"}, + {file = "charset_normalizer-3.2.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:f7560358a6811e52e9c4d142d497f1a6e10103d3a6881f18d04dbce3729c0e2c"}, + {file = "charset_normalizer-3.2.0-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:c8063cf17b19661471ecbdb3df1c84f24ad2e389e326ccaf89e3fb2484d8dd7e"}, + {file = "charset_normalizer-3.2.0-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:cd6dbe0238f7743d0efe563ab46294f54f9bc8f4b9bcf57c3c666cc5bc9d1299"}, + {file = "charset_normalizer-3.2.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:1249cbbf3d3b04902ff081ffbb33ce3377fa6e4c7356f759f3cd076cc138d020"}, + {file = "charset_normalizer-3.2.0-cp39-cp39-win32.whl", hash = "sha256:6c409c0deba34f147f77efaa67b8e4bb83d2f11c8806405f76397ae5b8c0d1c9"}, + {file = "charset_normalizer-3.2.0-cp39-cp39-win_amd64.whl", hash = "sha256:7095f6fbfaa55defb6b733cfeb14efaae7a29f0b59d8cf213be4e7ca0b857b80"}, + {file = "charset_normalizer-3.2.0-py3-none-any.whl", hash = "sha256:8e098148dd37b4ce3baca71fb394c81dc5d9c7728c95df695d2dca218edf40e6"}, +] + +[[package]] +name = "click" +version = "8.1.4" +description = "Composable command line interface toolkit" +category = "main" +optional = false +python-versions = ">=3.7" +files = [ + {file = "click-8.1.4-py3-none-any.whl", hash = "sha256:2739815aaa5d2c986a88f1e9230c55e17f0caad3d958a5e13ad0797c166db9e3"}, + {file = "click-8.1.4.tar.gz", hash = "sha256:b97d0c74955da062a7d4ef92fadb583806a585b2ea81958a81bd72726cbb8e37"}, +] + +[package.dependencies] +colorama = {version = "*", markers = "platform_system == \"Windows\""} + +[[package]] +name = "cloudpickle" +version = "2.2.1" +description = "Extended pickling support for Python objects" +category = "main" +optional = false +python-versions = ">=3.6" +files = [ + {file = "cloudpickle-2.2.1-py3-none-any.whl", hash = "sha256:61f594d1f4c295fa5cd9014ceb3a1fc4a70b0de1164b94fbc2d854ccba056f9f"}, + {file = "cloudpickle-2.2.1.tar.gz", hash = "sha256:d89684b8de9e34a2a43b3460fbca07d09d6e25ce858df4d5a44240403b6178f5"}, +] + +[[package]] +name = "colorama" +version = "0.4.6" +description = "Cross-platform colored terminal text." +category = "main" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" +files = [ + {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, + {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, +] + +[[package]] +name = "commonmark" +version = "0.9.1" +description = "Python parser for the CommonMark Markdown spec" +category = "main" +optional = false +python-versions = "*" +files = [ + {file = "commonmark-0.9.1-py2.py3-none-any.whl", hash = "sha256:da2f38c92590f83de410ba1a3cbceafbc74fee9def35f9251ba9a971d6d66fd9"}, + {file = "commonmark-0.9.1.tar.gz", hash = "sha256:452f9dc859be7f06631ddcb328b6919c67984aca654e5fefb3914d54691aed60"}, +] + +[package.extras] +test = ["flake8 (==3.7.8)", "hypothesis (==3.55.3)"] + +[[package]] +name = "coverage" +version = "7.2.7" +description = "Code coverage measurement for Python" +category = "dev" +optional = false +python-versions = ">=3.7" +files = [ + {file = "coverage-7.2.7-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d39b5b4f2a66ccae8b7263ac3c8170994b65266797fb96cbbfd3fb5b23921db8"}, + {file = "coverage-7.2.7-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:6d040ef7c9859bb11dfeb056ff5b3872436e3b5e401817d87a31e1750b9ae2fb"}, + {file = "coverage-7.2.7-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ba90a9563ba44a72fda2e85302c3abc71c5589cea608ca16c22b9804262aaeb6"}, + {file = "coverage-7.2.7-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e7d9405291c6928619403db1d10bd07888888ec1abcbd9748fdaa971d7d661b2"}, + {file = "coverage-7.2.7-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:31563e97dae5598556600466ad9beea39fb04e0229e61c12eaa206e0aa202063"}, + {file = "coverage-7.2.7-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:ebba1cd308ef115925421d3e6a586e655ca5a77b5bf41e02eb0e4562a111f2d1"}, + {file = "coverage-7.2.7-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:cb017fd1b2603ef59e374ba2063f593abe0fc45f2ad9abdde5b4d83bd922a353"}, + {file = "coverage-7.2.7-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:d62a5c7dad11015c66fbb9d881bc4caa5b12f16292f857842d9d1871595f4495"}, + {file = "coverage-7.2.7-cp310-cp310-win32.whl", hash = "sha256:ee57190f24fba796e36bb6d3aa8a8783c643d8fa9760c89f7a98ab5455fbf818"}, + {file = "coverage-7.2.7-cp310-cp310-win_amd64.whl", hash = "sha256:f75f7168ab25dd93110c8a8117a22450c19976afbc44234cbf71481094c1b850"}, + {file = "coverage-7.2.7-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:06a9a2be0b5b576c3f18f1a241f0473575c4a26021b52b2a85263a00f034d51f"}, + {file = "coverage-7.2.7-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5baa06420f837184130752b7c5ea0808762083bf3487b5038d68b012e5937dbe"}, + {file = "coverage-7.2.7-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fdec9e8cbf13a5bf63290fc6013d216a4c7232efb51548594ca3631a7f13c3a3"}, + {file = "coverage-7.2.7-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:52edc1a60c0d34afa421c9c37078817b2e67a392cab17d97283b64c5833f427f"}, + {file = "coverage-7.2.7-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:63426706118b7f5cf6bb6c895dc215d8a418d5952544042c8a2d9fe87fcf09cb"}, + {file = "coverage-7.2.7-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:afb17f84d56068a7c29f5fa37bfd38d5aba69e3304af08ee94da8ed5b0865833"}, + {file = "coverage-7.2.7-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:48c19d2159d433ccc99e729ceae7d5293fbffa0bdb94952d3579983d1c8c9d97"}, + {file = "coverage-7.2.7-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:0e1f928eaf5469c11e886fe0885ad2bf1ec606434e79842a879277895a50942a"}, + {file = "coverage-7.2.7-cp311-cp311-win32.whl", hash = "sha256:33d6d3ea29d5b3a1a632b3c4e4f4ecae24ef170b0b9ee493883f2df10039959a"}, + {file = "coverage-7.2.7-cp311-cp311-win_amd64.whl", hash = "sha256:5b7540161790b2f28143191f5f8ec02fb132660ff175b7747b95dcb77ac26562"}, + {file = "coverage-7.2.7-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:f2f67fe12b22cd130d34d0ef79206061bfb5eda52feb6ce0dba0644e20a03cf4"}, + {file = "coverage-7.2.7-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a342242fe22407f3c17f4b499276a02b01e80f861f1682ad1d95b04018e0c0d4"}, + {file = "coverage-7.2.7-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:171717c7cb6b453aebac9a2ef603699da237f341b38eebfee9be75d27dc38e01"}, + {file = "coverage-7.2.7-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:49969a9f7ffa086d973d91cec8d2e31080436ef0fb4a359cae927e742abfaaa6"}, + {file = "coverage-7.2.7-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:b46517c02ccd08092f4fa99f24c3b83d8f92f739b4657b0f146246a0ca6a831d"}, + {file = "coverage-7.2.7-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:a3d33a6b3eae87ceaefa91ffdc130b5e8536182cd6dfdbfc1aa56b46ff8c86de"}, + {file = "coverage-7.2.7-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:976b9c42fb2a43ebf304fa7d4a310e5f16cc99992f33eced91ef6f908bd8f33d"}, + {file = "coverage-7.2.7-cp312-cp312-win32.whl", hash = "sha256:8de8bb0e5ad103888d65abef8bca41ab93721647590a3f740100cd65c3b00511"}, + {file = "coverage-7.2.7-cp312-cp312-win_amd64.whl", hash = "sha256:9e31cb64d7de6b6f09702bb27c02d1904b3aebfca610c12772452c4e6c21a0d3"}, + {file = "coverage-7.2.7-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:58c2ccc2f00ecb51253cbe5d8d7122a34590fac9646a960d1430d5b15321d95f"}, + {file = "coverage-7.2.7-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d22656368f0e6189e24722214ed8d66b8022db19d182927b9a248a2a8a2f67eb"}, + {file = "coverage-7.2.7-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a895fcc7b15c3fc72beb43cdcbdf0ddb7d2ebc959edac9cef390b0d14f39f8a9"}, + {file = "coverage-7.2.7-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e84606b74eb7de6ff581a7915e2dab7a28a0517fbe1c9239eb227e1354064dcd"}, + {file = "coverage-7.2.7-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:0a5f9e1dbd7fbe30196578ca36f3fba75376fb99888c395c5880b355e2875f8a"}, + {file = "coverage-7.2.7-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:419bfd2caae268623dd469eff96d510a920c90928b60f2073d79f8fe2bbc5959"}, + {file = "coverage-7.2.7-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:2aee274c46590717f38ae5e4650988d1af340fe06167546cc32fe2f58ed05b02"}, + {file = "coverage-7.2.7-cp37-cp37m-win32.whl", hash = "sha256:61b9a528fb348373c433e8966535074b802c7a5d7f23c4f421e6c6e2f1697a6f"}, + {file = "coverage-7.2.7-cp37-cp37m-win_amd64.whl", hash = "sha256:b1c546aca0ca4d028901d825015dc8e4d56aac4b541877690eb76490f1dc8ed0"}, + {file = "coverage-7.2.7-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:54b896376ab563bd38453cecb813c295cf347cf5906e8b41d340b0321a5433e5"}, + {file = "coverage-7.2.7-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:3d376df58cc111dc8e21e3b6e24606b5bb5dee6024f46a5abca99124b2229ef5"}, + {file = "coverage-7.2.7-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5e330fc79bd7207e46c7d7fd2bb4af2963f5f635703925543a70b99574b0fea9"}, + {file = "coverage-7.2.7-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1e9d683426464e4a252bf70c3498756055016f99ddaec3774bf368e76bbe02b6"}, + {file = "coverage-7.2.7-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8d13c64ee2d33eccf7437961b6ea7ad8673e2be040b4f7fd4fd4d4d28d9ccb1e"}, + {file = "coverage-7.2.7-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:b7aa5f8a41217360e600da646004f878250a0d6738bcdc11a0a39928d7dc2050"}, + {file = "coverage-7.2.7-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:8fa03bce9bfbeeef9f3b160a8bed39a221d82308b4152b27d82d8daa7041fee5"}, + {file = "coverage-7.2.7-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:245167dd26180ab4c91d5e1496a30be4cd721a5cf2abf52974f965f10f11419f"}, + {file = "coverage-7.2.7-cp38-cp38-win32.whl", hash = "sha256:d2c2db7fd82e9b72937969bceac4d6ca89660db0a0967614ce2481e81a0b771e"}, + {file = "coverage-7.2.7-cp38-cp38-win_amd64.whl", hash = "sha256:2e07b54284e381531c87f785f613b833569c14ecacdcb85d56b25c4622c16c3c"}, + {file = "coverage-7.2.7-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:537891ae8ce59ef63d0123f7ac9e2ae0fc8b72c7ccbe5296fec45fd68967b6c9"}, + {file = "coverage-7.2.7-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:06fb182e69f33f6cd1d39a6c597294cff3143554b64b9825d1dc69d18cc2fff2"}, + {file = "coverage-7.2.7-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:201e7389591af40950a6480bd9edfa8ed04346ff80002cec1a66cac4549c1ad7"}, + {file = "coverage-7.2.7-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f6951407391b639504e3b3be51b7ba5f3528adbf1a8ac3302b687ecababf929e"}, + {file = "coverage-7.2.7-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6f48351d66575f535669306aa7d6d6f71bc43372473b54a832222803eb956fd1"}, + {file = "coverage-7.2.7-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:b29019c76039dc3c0fd815c41392a044ce555d9bcdd38b0fb60fb4cd8e475ba9"}, + {file = "coverage-7.2.7-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:81c13a1fc7468c40f13420732805a4c38a105d89848b7c10af65a90beff25250"}, + {file = "coverage-7.2.7-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:975d70ab7e3c80a3fe86001d8751f6778905ec723f5b110aed1e450da9d4b7f2"}, + {file = "coverage-7.2.7-cp39-cp39-win32.whl", hash = "sha256:7ee7d9d4822c8acc74a5e26c50604dff824710bc8de424904c0982e25c39c6cb"}, + {file = "coverage-7.2.7-cp39-cp39-win_amd64.whl", hash = "sha256:eb393e5ebc85245347950143969b241d08b52b88a3dc39479822e073a1a8eb27"}, + {file = "coverage-7.2.7-pp37.pp38.pp39-none-any.whl", hash = "sha256:b7b4c971f05e6ae490fef852c218b0e79d4e52f79ef0c8475566584a8fb3e01d"}, + {file = "coverage-7.2.7.tar.gz", hash = "sha256:924d94291ca674905fe9481f12294eb11f2d3d3fd1adb20314ba89e94f44ed59"}, +] + +[package.dependencies] +tomli = {version = "*", optional = true, markers = "python_full_version <= \"3.11.0a6\" and extra == \"toml\""} + +[package.extras] +toml = ["tomli"] + +[[package]] +name = "cycler" +version = "0.11.0" +description = "Composable style cycles" +category = "main" +optional = false +python-versions = ">=3.6" +files = [ + {file = "cycler-0.11.0-py3-none-any.whl", hash = "sha256:3a27e95f763a428a739d2add979fa7494c912a32c17c4c38c4d5f082cad165a3"}, + {file = "cycler-0.11.0.tar.gz", hash = "sha256:9c87405839a19696e837b3b818fed3f5f69f16f1eec1a1ad77e043dcea9c772f"}, +] + +[[package]] +name = "dask" +version = "2023.7.0" +description = "Parallel PyData with Task Scheduling" +category = "main" +optional = false +python-versions = ">=3.9" +files = [ + {file = "dask-2023.7.0-py3-none-any.whl", hash = "sha256:ceb10a806a8a6dca2d4623868687f9e166b4302f9a079e5a297e0780a2cd750d"}, + {file = "dask-2023.7.0.tar.gz", hash = "sha256:83212f085e9f59d6c724f32d4ce1dc1fed5405e868f5bfff701cc54912424c3d"}, +] + +[package.dependencies] +click = ">=8.0" +cloudpickle = ">=1.5.0" +fsspec = ">=2021.09.0" +importlib-metadata = ">=4.13.0" +packaging = ">=20.0" +partd = ">=1.2.0" +pyyaml = ">=5.3.1" +toolz = ">=0.10.0" + +[package.extras] +array = ["numpy (>=1.21)"] +complete = ["dask[array,dataframe,diagnostics,distributed]", "lz4 (>=4.3.2)", "pyarrow (>=7.0)"] +dataframe = ["numpy (>=1.21)", "pandas (>=1.3)"] +diagnostics = ["bokeh (>=2.4.2)", "jinja2 (>=2.10.3)"] +distributed = ["distributed (==2023.7.0)"] +test = ["pandas[test]", "pre-commit", "pytest", "pytest-cov", "pytest-rerunfailures", "pytest-xdist"] + +[[package]] +name = "decorator" +version = "5.1.1" +description = "Decorators for Humans" +category = "main" +optional = false +python-versions = ">=3.5" +files = [ + {file = "decorator-5.1.1-py3-none-any.whl", hash = "sha256:b8c3f85900b9dc423225913c5aace94729fe1fa9763b38939a95226f02d37186"}, + {file = "decorator-5.1.1.tar.gz", hash = "sha256:637996211036b6385ef91435e4fae22989472f9d571faba8927ba8253acbc330"}, +] + +[[package]] +name = "dm-tree" +version = "0.1.8" +description = "Tree is a library for working with nested data structures." +category = "main" +optional = false +python-versions = "*" +files = [ + {file = "dm-tree-0.1.8.tar.gz", hash = "sha256:0fcaabbb14e7980377439e7140bd05552739ca5e515ecb3119f234acee4b9430"}, + {file = "dm_tree-0.1.8-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:35cc164a79336bfcfafb47e5f297898359123bbd3330c1967f0c4994f9cf9f60"}, + {file = "dm_tree-0.1.8-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:39070ba268c0491af9fe7a58644d99e8b4f2cde6e5884ba3380bddc84ed43d5f"}, + {file = "dm_tree-0.1.8-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2869228d9c619074de501a3c10dc7f07c75422f8fab36ecdcb859b6f1b1ec3ef"}, + {file = "dm_tree-0.1.8-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d20f2faa3672b52e5013f4077117bfb99c4cfc0b445d3bde1584c34032b57436"}, + {file = "dm_tree-0.1.8-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5483dca4d7eb1a0d65fe86d3b6a53ae717face83c1f17e0887b1a4a64ae5c410"}, + {file = "dm_tree-0.1.8-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1d7c26e431fc93cc7e0cba867eb000db6a05f6f2b25af11ac4e9dada88fc5bca"}, + {file = "dm_tree-0.1.8-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e4d714371bb08839e4e5e29024fc95832d9affe129825ef38836b143028bd144"}, + {file = "dm_tree-0.1.8-cp310-cp310-win_amd64.whl", hash = "sha256:d40fa4106ca6edc66760246a08f500ec0c85ef55c762fb4a363f6ee739ba02ee"}, + {file = "dm_tree-0.1.8-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:ad16ceba90a56ec47cf45b21856d14962ac314787975ef786efb5e6e9ca75ec7"}, + {file = "dm_tree-0.1.8-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:803bfc53b4659f447ac694dbd04235f94a73ef7c1fd1e0df7c84ac41e0bc963b"}, + {file = "dm_tree-0.1.8-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:378cc8ad93c5fe3590f405a309980721f021c790ca1bdf9b15bb1d59daec57f5"}, + {file = "dm_tree-0.1.8-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1607ce49aa42f010d1e5e616d92ce899d66835d4d8bea49679582435285515de"}, + {file = "dm_tree-0.1.8-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:343a4a4ebaa127451ff971254a4be4084eb4bdc0b2513c32b46f6f728fd03f9e"}, + {file = "dm_tree-0.1.8-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:fa42a605d099ee7d41ba2b5fb75e21423951fd26e5d50583a00471238fb3021d"}, + {file = "dm_tree-0.1.8-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:83b7764de0d855338abefc6e3ee9fe40d301668310aa3baea3f778ff051f4393"}, + {file = "dm_tree-0.1.8-cp311-cp311-win_amd64.whl", hash = "sha256:a5d819c38c03f0bb5b3b3703c60e4b170355a0fc6b5819325bf3d4ceb3ae7e80"}, + {file = "dm_tree-0.1.8-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:8c60a7eadab64c2278861f56bca320b2720f163dca9d7558103c3b77f2416571"}, + {file = "dm_tree-0.1.8-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:af4b3d372f2477dcd89a6e717e4a575ca35ccc20cc4454a8a4b6f8838a00672d"}, + {file = "dm_tree-0.1.8-cp37-cp37m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:de287fabc464b8734be251e46e06aa9aa1001f34198da2b6ce07bd197172b9cb"}, + {file = "dm_tree-0.1.8-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:054b461f8176f4bce7a21f7b1870f873a1ced3bdbe1282c816c550bb43c71fa6"}, + {file = "dm_tree-0.1.8-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2f7915660f59c09068e428613c480150180df1060561fd0d1470684ae7007bd1"}, + {file = "dm_tree-0.1.8-cp37-cp37m-win_amd64.whl", hash = "sha256:b9f89a454e98806b44fe9d40ec9eee61f848388f7e79ac2371a55679bd5a3ac6"}, + {file = "dm_tree-0.1.8-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:0e9620ccf06393eb6b613b5e366469304622d4ea96ae6540b28a33840e6c89cf"}, + {file = "dm_tree-0.1.8-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:b095ba4f8ca1ba19350fd53cf1f8f3eb0bd406aa28af64a6dfc86707b32a810a"}, + {file = "dm_tree-0.1.8-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:b9bd9b9ccb59409d33d51d84b7668010c04c2af7d4a371632874c1ca356cff3d"}, + {file = "dm_tree-0.1.8-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0d3172394079a86c3a759179c65f64c48d1a42b89495fcf38976d11cc3bb952c"}, + {file = "dm_tree-0.1.8-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d1612fcaecd79023dbc6a6ae48d51a80beb5c385d6f3f6d71688e57bc8d07de8"}, + {file = "dm_tree-0.1.8-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c5c8c12e3fda754ef6af94161bacdaeda816d941995fac415d6855c6c386af68"}, + {file = "dm_tree-0.1.8-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:694c3654cfd2a81552c08ec66bb5c4a3d48fa292b9a181880fb081c36c5b9134"}, + {file = "dm_tree-0.1.8-cp38-cp38-win_amd64.whl", hash = "sha256:bb2d109f42190225112da899b9f3d46d0d5f26aef501c61e43529fe9322530b5"}, + {file = "dm_tree-0.1.8-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:d16e1f2a073604cfcc09f7131ae8d534674f43c3aef4c25742eae295bc60d04f"}, + {file = "dm_tree-0.1.8-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:250b692fb75f45f02e2f58fbef9ab338904ef334b90557565621fa251df267cf"}, + {file = "dm_tree-0.1.8-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:81fce77f22a302d7a5968aebdf4efafef4def7ce96528719a354e6990dcd49c7"}, + {file = "dm_tree-0.1.8-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f7ac31b9aecccb2c6e1ab29706f6ded3eba0c2c69c770322c9c685929c3d6afb"}, + {file = "dm_tree-0.1.8-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1fe962015b2fe1282892b28ebe962faed53c7f98d942da9a4625cbf27baef913"}, + {file = "dm_tree-0.1.8-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:28c52cbf4f8b3dbd0beaedf44f69fa85eec5e9dede612e08035e06ada6ec9426"}, + {file = "dm_tree-0.1.8-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:181c35521d480d0365f39300542cb6cd7fd2b77351bb43d7acfda15aef63b317"}, + {file = "dm_tree-0.1.8-cp39-cp39-win_amd64.whl", hash = "sha256:8ed3564abed97c806db122c2d3e1a2b64c74a63debe9903aad795167cc301368"}, +] + +[[package]] +name = "docutils" +version = "0.20.1" +description = "Docutils -- Python Documentation Utilities" +category = "dev" +optional = false +python-versions = ">=3.7" +files = [ + {file = "docutils-0.20.1-py3-none-any.whl", hash = "sha256:96f387a2c5562db4476f09f13bbab2192e764cac08ebbf3a34a95d9b1e4a59d6"}, + {file = "docutils-0.20.1.tar.gz", hash = "sha256:f08a4e276c3a1583a86dce3e34aba3fe04d02bba2dd51ed16106244e8a923e3b"}, +] + +[[package]] +name = "filelock" +version = "3.12.2" +description = "A platform independent file lock." +category = "main" +optional = true +python-versions = ">=3.7" +files = [ + {file = "filelock-3.12.2-py3-none-any.whl", hash = "sha256:cbb791cdea2a72f23da6ac5b5269ab0a0d161e9ef0100e653b69049a7706d1ec"}, + {file = "filelock-3.12.2.tar.gz", hash = "sha256:002740518d8aa59a26b0c76e10fb8c6e15eae825d34b6fdf670333fd7b938d81"}, +] + +[package.extras] +docs = ["furo (>=2023.5.20)", "sphinx (>=7.0.1)", "sphinx-autodoc-typehints (>=1.23,!=1.23.4)"] +testing = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "diff-cover (>=7.5)", "pytest (>=7.3.1)", "pytest-cov (>=4.1)", "pytest-mock (>=3.10)", "pytest-timeout (>=2.1)"] + +[[package]] +name = "flatbuffers" +version = "23.5.26" +description = "The FlatBuffers serialization format for Python" +category = "main" +optional = false +python-versions = "*" +files = [ + {file = "flatbuffers-23.5.26-py2.py3-none-any.whl", hash = "sha256:c0ff356da363087b915fde4b8b45bdda73432fc17cddb3c8157472eab1422ad1"}, + {file = "flatbuffers-23.5.26.tar.gz", hash = "sha256:9ea1144cac05ce5d86e2859f431c6cd5e66cd9c78c558317c7955fb8d4c78d89"}, +] + +[[package]] +name = "fonttools" +version = "4.40.0" +description = "Tools to manipulate font files" +category = "main" +optional = false +python-versions = ">=3.8" +files = [ + {file = "fonttools-4.40.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:b802dcbf9bcff74672f292b2466f6589ab8736ce4dcf36f48eb994c2847c4b30"}, + {file = "fonttools-4.40.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:7f6e3fa3da923063c286320e728ba2270e49c73386e3a711aa680f4b0747d692"}, + {file = "fonttools-4.40.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5fdf60f8a5c6bcce7d024a33f7e4bc7921f5b74e8ea13bccd204f2c8b86f3470"}, + {file = "fonttools-4.40.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:91784e21a1a085fac07c6a407564f4a77feb471b5954c9ee55a4f9165151f6c1"}, + {file = "fonttools-4.40.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:05171f3c546f64d78569f10adc0de72561882352cac39ec7439af12304d8d8c0"}, + {file = "fonttools-4.40.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:7449e5e306f3a930a8944c85d0cbc8429cba13503372a1a40f23124d6fb09b58"}, + {file = "fonttools-4.40.0-cp310-cp310-win32.whl", hash = "sha256:bae8c13abbc2511e9a855d2142c0ab01178dd66b1a665798f357da0d06253e0d"}, + {file = "fonttools-4.40.0-cp310-cp310-win_amd64.whl", hash = "sha256:425b74a608427499b0e45e433c34ddc350820b6f25b7c8761963a08145157a66"}, + {file = "fonttools-4.40.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:00ab569b2a3e591e00425023ade87e8fef90380c1dde61be7691cb524ca5f743"}, + {file = "fonttools-4.40.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:18ea64ac43e94c9e0c23d7a9475f1026be0e25b10dda8f236fc956188761df97"}, + {file = "fonttools-4.40.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:022c4a16b412293e7f1ce21b8bab7a6f9d12c4ffdf171fdc67122baddb973069"}, + {file = "fonttools-4.40.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:530c5d35109f3e0cea2535742d6a3bc99c0786cf0cbd7bb2dc9212387f0d908c"}, + {file = "fonttools-4.40.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:5e00334c66f4e83535384cb5339526d01d02d77f142c23b2f97bd6a4f585497a"}, + {file = "fonttools-4.40.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:eb52c10fda31159c22c7ed85074e05f8b97da8773ea461706c273e31bcbea836"}, + {file = "fonttools-4.40.0-cp311-cp311-win32.whl", hash = "sha256:6a8d71b9a5c884c72741868e845c0e563c5d83dcaf10bb0ceeec3b4b2eb14c67"}, + {file = "fonttools-4.40.0-cp311-cp311-win_amd64.whl", hash = "sha256:15abb3d055c1b2dff9ce376b6c3db10777cb74b37b52b78f61657634fd348a0d"}, + {file = "fonttools-4.40.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:14037c31138fbd21847ad5e5441dfdde003e0a8f3feb5812a1a21fd1c255ffbd"}, + {file = "fonttools-4.40.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:94c915f6716589f78bc00fbc14c5b8de65cfd11ee335d32504f1ef234524cb24"}, + {file = "fonttools-4.40.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:37467cee0f32cada2ec08bc16c9c31f9b53ea54b2f5604bf25a1246b5f50593a"}, + {file = "fonttools-4.40.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:56d4d85f5374b45b08d2f928517d1e313ea71b4847240398decd0ab3ebbca885"}, + {file = "fonttools-4.40.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:8c4305b171b61040b1ee75d18f9baafe58bd3b798d1670078efe2c92436bfb63"}, + {file = "fonttools-4.40.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:a954b90d1473c85a22ecf305761d9fd89da93bbd31dae86e7dea436ad2cb5dc9"}, + {file = "fonttools-4.40.0-cp38-cp38-win32.whl", hash = "sha256:1bc4c5b147be8dbc5df9cc8ac5e93ee914ad030fe2a201cc8f02f499db71011d"}, + {file = "fonttools-4.40.0-cp38-cp38-win_amd64.whl", hash = "sha256:8a917828dbfdb1cbe50cf40eeae6fbf9c41aef9e535649ed8f4982b2ef65c091"}, + {file = "fonttools-4.40.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:882983279bf39afe4e945109772c2ffad2be2c90983d6559af8b75c19845a80a"}, + {file = "fonttools-4.40.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:c55f1b4109dbc3aeb496677b3e636d55ef46dc078c2a5e3f3db4e90f1c6d2907"}, + {file = "fonttools-4.40.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ec468c022d09f1817c691cf884feb1030ef6f1e93e3ea6831b0d8144c06480d1"}, + {file = "fonttools-4.40.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6d5adf4ba114f028fc3f5317a221fd8b0f4ef7a2e5524a2b1e0fd891b093791a"}, + {file = "fonttools-4.40.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:aa83b3f151bc63970f39b2b42a06097c5a22fd7ed9f7ba008e618de4503d3895"}, + {file = "fonttools-4.40.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:97d95b8301b62bdece1af943b88bcb3680fd385f88346a4a899ee145913b414a"}, + {file = "fonttools-4.40.0-cp39-cp39-win32.whl", hash = "sha256:1a003608400dd1cca3e089e8c94973c6b51a4fb1ef00ff6d7641617b9242e637"}, + {file = "fonttools-4.40.0-cp39-cp39-win_amd64.whl", hash = "sha256:7961575221e3da0841c75da53833272c520000d76f7f71274dbf43370f8a1065"}, + {file = "fonttools-4.40.0-py3-none-any.whl", hash = "sha256:200729d12461e2038700d31f0d49ad5a7b55855dec7525074979a06b46f88505"}, + {file = "fonttools-4.40.0.tar.gz", hash = "sha256:337b6e83d7ee73c40ea62407f2ce03b07c3459e213b6f332b94a69923b9e1cb9"}, +] + +[package.extras] +all = ["brotli (>=1.0.1)", "brotlicffi (>=0.8.0)", "fs (>=2.2.0,<3)", "lxml (>=4.0,<5)", "lz4 (>=1.7.4.2)", "matplotlib", "munkres", "scipy", "skia-pathops (>=0.5.0)", "sympy", "uharfbuzz (>=0.23.0)", "unicodedata2 (>=15.0.0)", "xattr", "zopfli (>=0.1.4)"] +graphite = ["lz4 (>=1.7.4.2)"] +interpolatable = ["munkres", "scipy"] +lxml = ["lxml (>=4.0,<5)"] +pathops = ["skia-pathops (>=0.5.0)"] +plot = ["matplotlib"] +repacker = ["uharfbuzz (>=0.23.0)"] +symfont = ["sympy"] +type1 = ["xattr"] +ufo = ["fs (>=2.2.0,<3)"] +unicode = ["unicodedata2 (>=15.0.0)"] +woff = ["brotli (>=1.0.1)", "brotlicffi (>=0.8.0)", "zopfli (>=0.1.4)"] + +[[package]] +name = "frozenlist" +version = "1.3.3" +description = "A list-like structure which implements collections.abc.MutableSequence" +category = "main" +optional = true +python-versions = ">=3.7" +files = [ + {file = "frozenlist-1.3.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:ff8bf625fe85e119553b5383ba0fb6aa3d0ec2ae980295aaefa552374926b3f4"}, + {file = "frozenlist-1.3.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:dfbac4c2dfcc082fcf8d942d1e49b6aa0766c19d3358bd86e2000bf0fa4a9cf0"}, + {file = "frozenlist-1.3.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:b1c63e8d377d039ac769cd0926558bb7068a1f7abb0f003e3717ee003ad85530"}, + {file = "frozenlist-1.3.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7fdfc24dcfce5b48109867c13b4cb15e4660e7bd7661741a391f821f23dfdca7"}, + {file = "frozenlist-1.3.3-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2c926450857408e42f0bbc295e84395722ce74bae69a3b2aa2a65fe22cb14b99"}, + {file = "frozenlist-1.3.3-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1841e200fdafc3d51f974d9d377c079a0694a8f06de2e67b48150328d66d5483"}, + {file = "frozenlist-1.3.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f470c92737afa7d4c3aacc001e335062d582053d4dbe73cda126f2d7031068dd"}, + {file = "frozenlist-1.3.3-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:783263a4eaad7c49983fe4b2e7b53fa9770c136c270d2d4bbb6d2192bf4d9caf"}, + {file = "frozenlist-1.3.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:924620eef691990dfb56dc4709f280f40baee568c794b5c1885800c3ecc69816"}, + {file = "frozenlist-1.3.3-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:ae4dc05c465a08a866b7a1baf360747078b362e6a6dbeb0c57f234db0ef88ae0"}, + {file = "frozenlist-1.3.3-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:bed331fe18f58d844d39ceb398b77d6ac0b010d571cba8267c2e7165806b00ce"}, + {file = "frozenlist-1.3.3-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:02c9ac843e3390826a265e331105efeab489ffaf4dd86384595ee8ce6d35ae7f"}, + {file = "frozenlist-1.3.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:9545a33965d0d377b0bc823dcabf26980e77f1b6a7caa368a365a9497fb09420"}, + {file = "frozenlist-1.3.3-cp310-cp310-win32.whl", hash = "sha256:d5cd3ab21acbdb414bb6c31958d7b06b85eeb40f66463c264a9b343a4e238642"}, + {file = "frozenlist-1.3.3-cp310-cp310-win_amd64.whl", hash = "sha256:b756072364347cb6aa5b60f9bc18e94b2f79632de3b0190253ad770c5df17db1"}, + {file = "frozenlist-1.3.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:b4395e2f8d83fbe0c627b2b696acce67868793d7d9750e90e39592b3626691b7"}, + {file = "frozenlist-1.3.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:14143ae966a6229350021384870458e4777d1eae4c28d1a7aa47f24d030e6678"}, + {file = "frozenlist-1.3.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5d8860749e813a6f65bad8285a0520607c9500caa23fea6ee407e63debcdbef6"}, + {file = "frozenlist-1.3.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:23d16d9f477bb55b6154654e0e74557040575d9d19fe78a161bd33d7d76808e8"}, + {file = "frozenlist-1.3.3-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:eb82dbba47a8318e75f679690190c10a5e1f447fbf9df41cbc4c3afd726d88cb"}, + {file = "frozenlist-1.3.3-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9309869032abb23d196cb4e4db574232abe8b8be1339026f489eeb34a4acfd91"}, + {file = "frozenlist-1.3.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a97b4fe50b5890d36300820abd305694cb865ddb7885049587a5678215782a6b"}, + {file = "frozenlist-1.3.3-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c188512b43542b1e91cadc3c6c915a82a5eb95929134faf7fd109f14f9892ce4"}, + {file = "frozenlist-1.3.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:303e04d422e9b911a09ad499b0368dc551e8c3cd15293c99160c7f1f07b59a48"}, + {file = "frozenlist-1.3.3-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:0771aed7f596c7d73444c847a1c16288937ef988dc04fb9f7be4b2aa91db609d"}, + {file = "frozenlist-1.3.3-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:66080ec69883597e4d026f2f71a231a1ee9887835902dbe6b6467d5a89216cf6"}, + {file = "frozenlist-1.3.3-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:41fe21dc74ad3a779c3d73a2786bdf622ea81234bdd4faf90b8b03cad0c2c0b4"}, + {file = "frozenlist-1.3.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:f20380df709d91525e4bee04746ba612a4df0972c1b8f8e1e8af997e678c7b81"}, + {file = "frozenlist-1.3.3-cp311-cp311-win32.whl", hash = "sha256:f30f1928162e189091cf4d9da2eac617bfe78ef907a761614ff577ef4edfb3c8"}, + {file = "frozenlist-1.3.3-cp311-cp311-win_amd64.whl", hash = "sha256:a6394d7dadd3cfe3f4b3b186e54d5d8504d44f2d58dcc89d693698e8b7132b32"}, + {file = "frozenlist-1.3.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:8df3de3a9ab8325f94f646609a66cbeeede263910c5c0de0101079ad541af332"}, + {file = "frozenlist-1.3.3-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0693c609e9742c66ba4870bcee1ad5ff35462d5ffec18710b4ac89337ff16e27"}, + {file = "frozenlist-1.3.3-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:cd4210baef299717db0a600d7a3cac81d46ef0e007f88c9335db79f8979c0d3d"}, + {file = "frozenlist-1.3.3-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:394c9c242113bfb4b9aa36e2b80a05ffa163a30691c7b5a29eba82e937895d5e"}, + {file = "frozenlist-1.3.3-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6327eb8e419f7d9c38f333cde41b9ae348bec26d840927332f17e887a8dcb70d"}, + {file = "frozenlist-1.3.3-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2e24900aa13212e75e5b366cb9065e78bbf3893d4baab6052d1aca10d46d944c"}, + {file = "frozenlist-1.3.3-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:3843f84a6c465a36559161e6c59dce2f2ac10943040c2fd021cfb70d58c4ad56"}, + {file = "frozenlist-1.3.3-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:84610c1502b2461255b4c9b7d5e9c48052601a8957cd0aea6ec7a7a1e1fb9420"}, + {file = "frozenlist-1.3.3-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:c21b9aa40e08e4f63a2f92ff3748e6b6c84d717d033c7b3438dd3123ee18f70e"}, + {file = "frozenlist-1.3.3-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:efce6ae830831ab6a22b9b4091d411698145cb9b8fc869e1397ccf4b4b6455cb"}, + {file = "frozenlist-1.3.3-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:40de71985e9042ca00b7953c4f41eabc3dc514a2d1ff534027f091bc74416401"}, + {file = "frozenlist-1.3.3-cp37-cp37m-win32.whl", hash = "sha256:180c00c66bde6146a860cbb81b54ee0df350d2daf13ca85b275123bbf85de18a"}, + {file = "frozenlist-1.3.3-cp37-cp37m-win_amd64.whl", hash = "sha256:9bbbcedd75acdfecf2159663b87f1bb5cfc80e7cd99f7ddd9d66eb98b14a8411"}, + {file = "frozenlist-1.3.3-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:034a5c08d36649591be1cbb10e09da9f531034acfe29275fc5454a3b101ce41a"}, + {file = "frozenlist-1.3.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:ba64dc2b3b7b158c6660d49cdb1d872d1d0bf4e42043ad8d5006099479a194e5"}, + {file = "frozenlist-1.3.3-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:47df36a9fe24054b950bbc2db630d508cca3aa27ed0566c0baf661225e52c18e"}, + {file = "frozenlist-1.3.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:008a054b75d77c995ea26629ab3a0c0d7281341f2fa7e1e85fa6153ae29ae99c"}, + {file = "frozenlist-1.3.3-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:841ea19b43d438a80b4de62ac6ab21cfe6827bb8a9dc62b896acc88eaf9cecba"}, + {file = "frozenlist-1.3.3-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e235688f42b36be2b6b06fc37ac2126a73b75fb8d6bc66dd632aa35286238703"}, + {file = "frozenlist-1.3.3-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ca713d4af15bae6e5d79b15c10c8522859a9a89d3b361a50b817c98c2fb402a2"}, + {file = "frozenlist-1.3.3-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9ac5995f2b408017b0be26d4a1d7c61bce106ff3d9e3324374d66b5964325448"}, + {file = "frozenlist-1.3.3-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:a4ae8135b11652b08a8baf07631d3ebfe65a4c87909dbef5fa0cdde440444ee4"}, + {file = "frozenlist-1.3.3-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:4ea42116ceb6bb16dbb7d526e242cb6747b08b7710d9782aa3d6732bd8d27649"}, + {file = "frozenlist-1.3.3-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:810860bb4bdce7557bc0febb84bbd88198b9dbc2022d8eebe5b3590b2ad6c842"}, + {file = "frozenlist-1.3.3-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:ee78feb9d293c323b59a6f2dd441b63339a30edf35abcb51187d2fc26e696d13"}, + {file = "frozenlist-1.3.3-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:0af2e7c87d35b38732e810befb9d797a99279cbb85374d42ea61c1e9d23094b3"}, + {file = "frozenlist-1.3.3-cp38-cp38-win32.whl", hash = "sha256:899c5e1928eec13fd6f6d8dc51be23f0d09c5281e40d9cf4273d188d9feeaf9b"}, + {file = "frozenlist-1.3.3-cp38-cp38-win_amd64.whl", hash = "sha256:7f44e24fa70f6fbc74aeec3e971f60a14dde85da364aa87f15d1be94ae75aeef"}, + {file = "frozenlist-1.3.3-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:2b07ae0c1edaa0a36339ec6cce700f51b14a3fc6545fdd32930d2c83917332cf"}, + {file = "frozenlist-1.3.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:ebb86518203e12e96af765ee89034a1dbb0c3c65052d1b0c19bbbd6af8a145e1"}, + {file = "frozenlist-1.3.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:5cf820485f1b4c91e0417ea0afd41ce5cf5965011b3c22c400f6d144296ccbc0"}, + {file = "frozenlist-1.3.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5c11e43016b9024240212d2a65043b70ed8dfd3b52678a1271972702d990ac6d"}, + {file = "frozenlist-1.3.3-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8fa3c6e3305aa1146b59a09b32b2e04074945ffcfb2f0931836d103a2c38f936"}, + {file = "frozenlist-1.3.3-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:352bd4c8c72d508778cf05ab491f6ef36149f4d0cb3c56b1b4302852255d05d5"}, + {file = "frozenlist-1.3.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:65a5e4d3aa679610ac6e3569e865425b23b372277f89b5ef06cf2cdaf1ebf22b"}, + {file = "frozenlist-1.3.3-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b1e2c1185858d7e10ff045c496bbf90ae752c28b365fef2c09cf0fa309291669"}, + {file = "frozenlist-1.3.3-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:f163d2fd041c630fed01bc48d28c3ed4a3b003c00acd396900e11ee5316b56bb"}, + {file = "frozenlist-1.3.3-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:05cdb16d09a0832eedf770cb7bd1fe57d8cf4eaf5aced29c4e41e3f20b30a784"}, + {file = "frozenlist-1.3.3-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:8bae29d60768bfa8fb92244b74502b18fae55a80eac13c88eb0b496d4268fd2d"}, + {file = "frozenlist-1.3.3-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:eedab4c310c0299961ac285591acd53dc6723a1ebd90a57207c71f6e0c2153ab"}, + {file = "frozenlist-1.3.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:3bbdf44855ed8f0fbcd102ef05ec3012d6a4fd7c7562403f76ce6a52aeffb2b1"}, + {file = "frozenlist-1.3.3-cp39-cp39-win32.whl", hash = "sha256:efa568b885bca461f7c7b9e032655c0c143d305bf01c30caf6db2854a4532b38"}, + {file = "frozenlist-1.3.3-cp39-cp39-win_amd64.whl", hash = "sha256:cfe33efc9cb900a4c46f91a5ceba26d6df370ffddd9ca386eb1d4f0ad97b9ea9"}, + {file = "frozenlist-1.3.3.tar.gz", hash = "sha256:58bcc55721e8a90b88332d6cd441261ebb22342e238296bb330968952fbb3a6a"}, +] + +[[package]] +name = "fsspec" +version = "2023.6.0" +description = "File-system specification" +category = "main" +optional = false +python-versions = ">=3.8" +files = [ + {file = "fsspec-2023.6.0-py3-none-any.whl", hash = "sha256:1cbad1faef3e391fba6dc005ae9b5bdcbf43005c9167ce78c915549c352c869a"}, + {file = "fsspec-2023.6.0.tar.gz", hash = "sha256:d0b2f935446169753e7a5c5c55681c54ea91996cc67be93c39a154fb3a2742af"}, +] + +[package.extras] +abfs = ["adlfs"] +adl = ["adlfs"] +arrow = ["pyarrow (>=1)"] +dask = ["dask", "distributed"] +devel = ["pytest", "pytest-cov"] +dropbox = ["dropbox", "dropboxdrivefs", "requests"] +full = ["adlfs", "aiohttp (!=4.0.0a0,!=4.0.0a1)", "dask", "distributed", "dropbox", "dropboxdrivefs", "fusepy", "gcsfs", "libarchive-c", "ocifs", "panel", "paramiko", "pyarrow (>=1)", "pygit2", "requests", "s3fs", "smbprotocol", "tqdm"] +fuse = ["fusepy"] +gcs = ["gcsfs"] +git = ["pygit2"] +github = ["requests"] +gs = ["gcsfs"] +gui = ["panel"] +hdfs = ["pyarrow (>=1)"] +http = ["aiohttp (!=4.0.0a0,!=4.0.0a1)", "requests"] +libarchive = ["libarchive-c"] +oci = ["ocifs"] +s3 = ["s3fs"] +sftp = ["paramiko"] +smb = ["smbprotocol"] +ssh = ["paramiko"] +tqdm = ["tqdm"] + +[[package]] +name = "gast" +version = "0.4.0" +description = "Python AST that abstracts the underlying Python version" +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +files = [ + {file = "gast-0.4.0-py3-none-any.whl", hash = "sha256:b7adcdd5adbebf1adf17378da5ba3f543684dbec47b1cda1f3997e573cd542c4"}, + {file = "gast-0.4.0.tar.gz", hash = "sha256:40feb7b8b8434785585ab224d1568b857edb18297e5a3047f1ba012bc83b42c1"}, +] + +[[package]] +name = "google-auth" +version = "2.17.3" +description = "Google Authentication Library" +category = "main" +optional = false +python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*" +files = [ + {file = "google-auth-2.17.3.tar.gz", hash = "sha256:ce311e2bc58b130fddf316df57c9b3943c2a7b4f6ec31de9663a9333e4064efc"}, + {file = "google_auth-2.17.3-py2.py3-none-any.whl", hash = "sha256:f586b274d3eb7bd932ea424b1c702a30e0393a2e2bc4ca3eae8263ffd8be229f"}, +] + +[package.dependencies] +cachetools = ">=2.0.0,<6.0" +pyasn1-modules = ">=0.2.1" +rsa = {version = ">=3.1.4,<5", markers = "python_version >= \"3.6\""} +six = ">=1.9.0" + +[package.extras] +aiohttp = ["aiohttp (>=3.6.2,<4.0.0dev)", "requests (>=2.20.0,<3.0.0dev)"] +enterprise-cert = ["cryptography (==36.0.2)", "pyopenssl (==22.0.0)"] +pyopenssl = ["cryptography (>=38.0.3)", "pyopenssl (>=20.0.0)"] +reauth = ["pyu2f (>=0.1.5)"] +requests = ["requests (>=2.20.0,<3.0.0dev)"] + +[[package]] +name = "google-auth-oauthlib" +version = "0.4.6" +description = "Google Authentication Library" +category = "main" +optional = false +python-versions = ">=3.6" +files = [ + {file = "google-auth-oauthlib-0.4.6.tar.gz", hash = "sha256:a90a072f6993f2c327067bf65270046384cda5a8ecb20b94ea9a687f1f233a7a"}, + {file = "google_auth_oauthlib-0.4.6-py2.py3-none-any.whl", hash = "sha256:3f2a6e802eebbb6fb736a370fbf3b055edcb6b52878bf2f26330b5e041316c73"}, +] + +[package.dependencies] +google-auth = ">=1.0.0" +requests-oauthlib = ">=0.7.0" + +[package.extras] +tool = ["click (>=6.0.0)"] + +[[package]] +name = "google-pasta" +version = "0.2.0" +description = "pasta is an AST-based Python refactoring library" +category = "main" +optional = false +python-versions = "*" +files = [ + {file = "google-pasta-0.2.0.tar.gz", hash = "sha256:c9f2c8dfc8f96d0d5808299920721be30c9eec37f2389f28904f454565c8a16e"}, + {file = "google_pasta-0.2.0-py2-none-any.whl", hash = "sha256:4612951da876b1a10fe3960d7226f0c7682cf901e16ac06e473b267a5afa8954"}, + {file = "google_pasta-0.2.0-py3-none-any.whl", hash = "sha256:b32482794a366b5366a32c92a9a9201b107821889935a02b3e51f6b432ea84ed"}, +] + +[package.dependencies] +six = "*" + +[[package]] +name = "grpcio" +version = "1.49.1" +description = "HTTP/2-based RPC framework" +category = "main" +optional = false +python-versions = ">=3.7" +files = [ + {file = "grpcio-1.49.1-cp310-cp310-linux_armv7l.whl", hash = "sha256:fd86040232e805b8e6378b2348c928490ee595b058ce9aaa27ed8e4b0f172b20"}, + {file = "grpcio-1.49.1-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:6fd0c9cede9552bf00f8c5791d257d5bf3790d7057b26c59df08be5e7a1e021d"}, + {file = "grpcio-1.49.1-cp310-cp310-manylinux_2_17_aarch64.whl", hash = "sha256:d0d402e158d4e84e49c158cb5204119d55e1baf363ee98d6cb5dce321c3a065d"}, + {file = "grpcio-1.49.1-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:822ceec743d42a627e64ea266059a62d214c5a3cdfcd0d7fe2b7a8e4e82527c7"}, + {file = "grpcio-1.49.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2106d9c16527f0a85e2eea6e6b91a74fc99579c60dd810d8690843ea02bc0f5f"}, + {file = "grpcio-1.49.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:52dd02b7e7868233c571b49bc38ebd347c3bb1ff8907bb0cb74cb5f00c790afc"}, + {file = "grpcio-1.49.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:120fecba2ec5d14b5a15d11063b39783fda8dc8d24addd83196acb6582cabd9b"}, + {file = "grpcio-1.49.1-cp310-cp310-win32.whl", hash = "sha256:f1a3b88e3c53c1a6e6bed635ec1bbb92201bb6a1f2db186179f7f3f244829788"}, + {file = "grpcio-1.49.1-cp310-cp310-win_amd64.whl", hash = "sha256:a7d0017b92d3850abea87c1bdec6ea41104e71c77bca44c3e17f175c6700af62"}, + {file = "grpcio-1.49.1-cp311-cp311-linux_armv7l.whl", hash = "sha256:9fb17ff8c0d56099ac6ebfa84f670c5a62228d6b5c695cf21c02160c2ac1446b"}, + {file = "grpcio-1.49.1-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:075f2d06e3db6b48a2157a1bcd52d6cbdca980dd18988fe6afdb41795d51625f"}, + {file = "grpcio-1.49.1-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:46d93a1b4572b461a227f1db6b8d35a88952db1c47e5fadcf8b8a2f0e1dd9201"}, + {file = "grpcio-1.49.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dc79b2b37d779ac42341ddef40ad5bf0966a64af412c89fc2b062e3ddabb093f"}, + {file = "grpcio-1.49.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:5f8b3a971c7820ea9878f3fd70086240a36aeee15d1b7e9ecbc2743b0e785568"}, + {file = "grpcio-1.49.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:49b301740cf5bc8fed4fee4c877570189ae3951432d79fa8e524b09353659811"}, + {file = "grpcio-1.49.1-cp311-cp311-win32.whl", hash = "sha256:1c66a25afc6c71d357867b341da594a5587db5849b48f4b7d5908d236bb62ede"}, + {file = "grpcio-1.49.1-cp311-cp311-win_amd64.whl", hash = "sha256:6b6c3a95d27846f4145d6967899b3ab25fffc6ae99544415e1adcacef84842d2"}, + {file = "grpcio-1.49.1-cp37-cp37m-linux_armv7l.whl", hash = "sha256:1cc400c8a2173d1c042997d98a9563e12d9bb3fb6ad36b7f355bc77c7663b8af"}, + {file = "grpcio-1.49.1-cp37-cp37m-macosx_10_10_x86_64.whl", hash = "sha256:34f736bd4d0deae90015c0e383885b431444fe6b6c591dea288173df20603146"}, + {file = "grpcio-1.49.1-cp37-cp37m-manylinux_2_17_aarch64.whl", hash = "sha256:196082b9c89ebf0961dcd77cb114bed8171964c8e3063b9da2fb33536a6938ed"}, + {file = "grpcio-1.49.1-cp37-cp37m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8c9f89c42749890618cd3c2464e1fbf88446e3d2f67f1e334c8e5db2f3272bbd"}, + {file = "grpcio-1.49.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:64419cb8a5b612cdb1550c2fd4acbb7d4fb263556cf4625f25522337e461509e"}, + {file = "grpcio-1.49.1-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:8a5272061826e6164f96e3255405ef6f73b88fd3e8bef464c7d061af8585ac62"}, + {file = "grpcio-1.49.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:ea9d0172445241ad7cb49577314e39d0af2c5267395b3561d7ced5d70458a9f3"}, + {file = "grpcio-1.49.1-cp37-cp37m-win32.whl", hash = "sha256:2070e87d95991473244c72d96d13596c751cb35558e11f5df5414981e7ed2492"}, + {file = "grpcio-1.49.1-cp37-cp37m-win_amd64.whl", hash = "sha256:4fcedcab49baaa9db4a2d240ac81f2d57eb0052b1c6a9501b46b8ae912720fbf"}, + {file = "grpcio-1.49.1-cp38-cp38-linux_armv7l.whl", hash = "sha256:afbb3475cf7f4f7d380c2ca37ee826e51974f3e2665613996a91d6a58583a534"}, + {file = "grpcio-1.49.1-cp38-cp38-macosx_10_10_x86_64.whl", hash = "sha256:a4f9ba141380abde6c3adc1727f21529137a2552002243fa87c41a07e528245c"}, + {file = "grpcio-1.49.1-cp38-cp38-manylinux_2_17_aarch64.whl", hash = "sha256:cf0a1fb18a7204b9c44623dfbd1465b363236ce70c7a4ed30402f9f60d8b743b"}, + {file = "grpcio-1.49.1-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:17bb6fe72784b630728c6cff9c9d10ccc3b6d04e85da6e0a7b27fb1d135fac62"}, + {file = "grpcio-1.49.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:18305d5a082d1593b005a895c10041f833b16788e88b02bb81061f5ebcc465df"}, + {file = "grpcio-1.49.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:b6a1b39e59ac5a3067794a0e498911cf2e37e4b19ee9e9977dc5e7051714f13f"}, + {file = "grpcio-1.49.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:0e20d59aafc086b1cc68400463bddda6e41d3e5ed30851d1e2e0f6a2e7e342d3"}, + {file = "grpcio-1.49.1-cp38-cp38-win32.whl", hash = "sha256:e1e83233d4680863a421f3ee4a7a9b80d33cd27ee9ed7593bc93f6128302d3f2"}, + {file = "grpcio-1.49.1-cp38-cp38-win_amd64.whl", hash = "sha256:221d42c654d2a41fa31323216279c73ed17d92f533bc140a3390cc1bd78bf63c"}, + {file = "grpcio-1.49.1-cp39-cp39-linux_armv7l.whl", hash = "sha256:fa9e6e61391e99708ac87fc3436f6b7b9c6b845dc4639b406e5e61901e1aacde"}, + {file = "grpcio-1.49.1-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:9b449e966ef518ce9c860d21f8afe0b0f055220d95bc710301752ac1db96dd6a"}, + {file = "grpcio-1.49.1-cp39-cp39-manylinux_2_17_aarch64.whl", hash = "sha256:aa34d2ad9f24e47fa9a3172801c676e4037d862247e39030165fe83821a7aafd"}, + {file = "grpcio-1.49.1-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5207f4eed1b775d264fcfe379d8541e1c43b878f2b63c0698f8f5c56c40f3d68"}, + {file = "grpcio-1.49.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0b24a74651438d45619ac67004638856f76cc13d78b7478f2457754cbcb1c8ad"}, + {file = "grpcio-1.49.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:fe763781669790dc8b9618e7e677c839c87eae6cf28b655ee1fa69ae04eea03f"}, + {file = "grpcio-1.49.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:2f2ff7ba0f8f431f32d4b4bc3a3713426949d3533b08466c4ff1b2b475932ca8"}, + {file = "grpcio-1.49.1-cp39-cp39-win32.whl", hash = "sha256:08ff74aec8ff457a89b97152d36cb811dcc1d17cd5a92a65933524e363327394"}, + {file = "grpcio-1.49.1-cp39-cp39-win_amd64.whl", hash = "sha256:274ffbb39717918c514b35176510ae9be06e1d93121e84d50b350861dcb9a705"}, + {file = "grpcio-1.49.1.tar.gz", hash = "sha256:d4725fc9ec8e8822906ae26bb26f5546891aa7fbc3443de970cc556d43a5c99f"}, +] + +[package.dependencies] +six = ">=1.5.2" + +[package.extras] +protobuf = ["grpcio-tools (>=1.49.1)"] + +[[package]] +name = "grpcio" +version = "1.51.3" +description = "HTTP/2-based RPC framework" +category = "main" +optional = false +python-versions = ">=3.7" +files = [ + {file = "grpcio-1.51.3-cp310-cp310-linux_armv7l.whl", hash = "sha256:f601aaeae18dab81930fb8d4f916b0da21e89bb4b5f7367ef793f46b4a76b7b0"}, + {file = "grpcio-1.51.3-cp310-cp310-macosx_12_0_universal2.whl", hash = "sha256:eef0450a4b5ed11feab639bf3eb1b6e23d0efa9b911bf7b06fb60e14f5f8a585"}, + {file = "grpcio-1.51.3-cp310-cp310-manylinux_2_17_aarch64.whl", hash = "sha256:82b0ad8ac825d4bb31bff9f638557c045f4a6d824d84b21e893968286f88246b"}, + {file = "grpcio-1.51.3-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3667c06e37d6cd461afdd51cefe6537702f3d1dc5ff4cac07e88d8b4795dc16f"}, + {file = "grpcio-1.51.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3709048fe0aa23dda09b3e69849a12055790171dab9e399a72ea8f9dfbf9ac80"}, + {file = "grpcio-1.51.3-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:200d69857f9910f7458b39b9bcf83ee4a180591b40146ba9e49314e3a7419313"}, + {file = "grpcio-1.51.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:cd9a5e68e79c5f031500e67793048a90209711e0854a9ddee8a3ce51728de4e5"}, + {file = "grpcio-1.51.3-cp310-cp310-win32.whl", hash = "sha256:6604f614016127ae10969176bbf12eb0e03d2fb3d643f050b3b69e160d144fb4"}, + {file = "grpcio-1.51.3-cp310-cp310-win_amd64.whl", hash = "sha256:e95c7ccd4c5807adef1602005513bf7c7d14e5a41daebcf9d8d30d8bf51b8f81"}, + {file = "grpcio-1.51.3-cp311-cp311-linux_armv7l.whl", hash = "sha256:5e77ee138100f0bb55cbd147840f87ee6241dbd25f09ea7cd8afe7efff323449"}, + {file = "grpcio-1.51.3-cp311-cp311-macosx_10_10_universal2.whl", hash = "sha256:68a7514b754e38e8de9075f7bb4dee919919515ec68628c43a894027e40ddec4"}, + {file = "grpcio-1.51.3-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3c1b9f8afa62ff265d86a4747a2990ec5a96e4efce5d5888f245a682d66eca47"}, + {file = "grpcio-1.51.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8de30f0b417744288cec65ec8cf84b8a57995cf7f1e84ccad2704d93f05d0aae"}, + {file = "grpcio-1.51.3-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:b69c7adc7ed60da1cb1b502853db61f453fc745f940cbcc25eb97c99965d8f41"}, + {file = "grpcio-1.51.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:d81528ffe0e973dc840ec73a4132fd18b8203ad129d7410155d951a0a7e4f5d0"}, + {file = "grpcio-1.51.3-cp311-cp311-win32.whl", hash = "sha256:040eb421613b57c696063abde405916dd830203c184c9000fc8c3b3b3c950325"}, + {file = "grpcio-1.51.3-cp311-cp311-win_amd64.whl", hash = "sha256:2a8e17286c4240137d933b8ca506465472248b4ce0fe46f3404459e708b65b68"}, + {file = "grpcio-1.51.3-cp37-cp37m-linux_armv7l.whl", hash = "sha256:d5cd1389669a847555df54177b911d9ff6f17345b2a6f19388707b7a9f724c88"}, + {file = "grpcio-1.51.3-cp37-cp37m-macosx_10_10_universal2.whl", hash = "sha256:be1bf35ce82cdbcac14e39d5102d8de4079a1c1a6a06b68e41fcd9ef64f9dd28"}, + {file = "grpcio-1.51.3-cp37-cp37m-manylinux_2_17_aarch64.whl", hash = "sha256:5eed34994c095e2bf7194ffac7381c6068b057ef1e69f8f08db77771350a7566"}, + {file = "grpcio-1.51.3-cp37-cp37m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3f9a7d88082b2a17ae7bd3c2354d13bab0453899e0851733f6afa6918373f476"}, + {file = "grpcio-1.51.3-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:36c8abbc5f837111e7bd619612eedc223c290b0903b952ce0c7b00840ea70f14"}, + {file = "grpcio-1.51.3-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:165b05af77e6aecb4210ae7663e25acf234ba78a7c1c157fa5f2efeb0d6ec53c"}, + {file = "grpcio-1.51.3-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:54e36c2ee304ff15f2bfbdc43d2b56c63331c52d818c364e5b5214e5bc2ad9f6"}, + {file = "grpcio-1.51.3-cp37-cp37m-win32.whl", hash = "sha256:cd0daac21d9ef5e033a5100c1d3aa055bbed28bfcf070b12d8058045c4e821b1"}, + {file = "grpcio-1.51.3-cp37-cp37m-win_amd64.whl", hash = "sha256:2fdd6333ce96435408565a9dbbd446212cd5d62e4d26f6a3c0feb1e3c35f1cc8"}, + {file = "grpcio-1.51.3-cp38-cp38-linux_armv7l.whl", hash = "sha256:54b0c29bdd9a3b1e1b61443ab152f060fc719f1c083127ab08d03fac5efd51be"}, + {file = "grpcio-1.51.3-cp38-cp38-macosx_10_10_universal2.whl", hash = "sha256:ffaaf7e93fcb437356b5a4b23bf36e8a3d0221399ff77fd057e4bc77776a24be"}, + {file = "grpcio-1.51.3-cp38-cp38-manylinux_2_17_aarch64.whl", hash = "sha256:eafbe7501a3268d05f2e450e1ddaffb950d842a8620c13ec328b501d25d2e2c3"}, + {file = "grpcio-1.51.3-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:881ecb34feabf31c6b3b9bbbddd1a5b57e69f805041e5a2c6c562a28574f71c4"}, + {file = "grpcio-1.51.3-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e860a3222139b41d430939bbec2ec9c3f6c740938bf7a04471a9a8caaa965a2e"}, + {file = "grpcio-1.51.3-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:49ede0528e9dac7e8a9fe30b16c73b630ddd9a576bf4b675eb6b0c53ee5ca00f"}, + {file = "grpcio-1.51.3-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:6972b009638b40a448d10e1bc18e2223143b8a7aa20d7def0d78dd4af4126d12"}, + {file = "grpcio-1.51.3-cp38-cp38-win32.whl", hash = "sha256:5694448256e3cdfe5bd358f1574a3f2f51afa20cc834713c4b9788d60b7cc646"}, + {file = "grpcio-1.51.3-cp38-cp38-win_amd64.whl", hash = "sha256:3ea4341efe603b049e8c9a5f13c696ca37fcdf8a23ca35f650428ad3606381d9"}, + {file = "grpcio-1.51.3-cp39-cp39-linux_armv7l.whl", hash = "sha256:6c677581ce129f5fa228b8f418cee10bd28dd449f3a544ea73c8ba590ee49d0b"}, + {file = "grpcio-1.51.3-cp39-cp39-macosx_10_10_universal2.whl", hash = "sha256:30e09b5e0531685e176f49679b6a3b190762cc225f4565e55a899f5e14b3aa62"}, + {file = "grpcio-1.51.3-cp39-cp39-manylinux_2_17_aarch64.whl", hash = "sha256:c831f31336e81243f85b6daff3e5e8a123302ce0ea1f2726ad752fd7a59f3aee"}, + {file = "grpcio-1.51.3-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2cd2e4cefb724cab1ba2df4b7535a9980531b9ec51b4dbb5f137a1f3a3754ef0"}, + {file = "grpcio-1.51.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f7a0d0bf44438869d307f85a54f25a896ad6b4b0ca12370f76892ad732928d87"}, + {file = "grpcio-1.51.3-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:c02abd55409bfb293371554adf6a4401197ec2133dd97727c01180889014ba4d"}, + {file = "grpcio-1.51.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:2f8ff75e61e1227ba7a3f16b2eadbcc11d0a54096d52ab75a6b88cfbe56f55d1"}, + {file = "grpcio-1.51.3-cp39-cp39-win32.whl", hash = "sha256:6c99a73a6260bdf844b2e5ddad02dcd530310f80e1fa72c300fa19c1c7496962"}, + {file = "grpcio-1.51.3-cp39-cp39-win_amd64.whl", hash = "sha256:22bdfac4f7f27acdd4da359b5e7e1973dc74bf1ed406729b07d0759fde2f064b"}, + {file = "grpcio-1.51.3.tar.gz", hash = "sha256:be7b2265b7527bb12109a7727581e274170766d5b3c9258d4e466f4872522d7a"}, +] + +[package.extras] +protobuf = ["grpcio-tools (>=1.51.3)"] + +[[package]] +name = "h5py" +version = "3.9.0" +description = "Read and write HDF5 files from Python" +category = "main" +optional = false +python-versions = ">=3.8" +files = [ + {file = "h5py-3.9.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:eb7bdd5e601dd1739698af383be03f3dad0465fe67184ebd5afca770f50df9d6"}, + {file = "h5py-3.9.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:78e44686334cbbf2dd21d9df15823bc38663f27a3061f6a032c68a3e30c47bf7"}, + {file = "h5py-3.9.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f68b41efd110ce9af1cbe6fa8af9f4dcbadace6db972d30828b911949e28fadd"}, + {file = "h5py-3.9.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:12aa556d540f11a2cae53ea7cfb94017353bd271fb3962e1296b342f6550d1b8"}, + {file = "h5py-3.9.0-cp310-cp310-win_amd64.whl", hash = "sha256:d97409e17915798029e297a84124705c8080da901307ea58f29234e09b073ddc"}, + {file = "h5py-3.9.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:551e358db05a874a0f827b22e95b30092f2303edc4b91bb62ad2f10e0236e1a0"}, + {file = "h5py-3.9.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:6822a814b9d8b8363ff102f76ea8d026f0ca25850bb579d85376029ee3e73b93"}, + {file = "h5py-3.9.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:54f01202cdea754ab4227dd27014bdbd561a4bbe4b631424fd812f7c2ce9c6ac"}, + {file = "h5py-3.9.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:64acceaf6aff92af091a4b83f6dee3cf8d3061f924a6bb3a33eb6c4658a8348b"}, + {file = "h5py-3.9.0-cp311-cp311-win_amd64.whl", hash = "sha256:804c7fb42a34c8ab3a3001901c977a5c24d2e9c586a0f3e7c0a389130b4276fc"}, + {file = "h5py-3.9.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:8d9492391ff5c3c80ec30ae2fe82a3f0efd1e750833739c25b0d090e3be1b095"}, + {file = "h5py-3.9.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:9da9e7e63376c32704e37ad4cea2dceae6964cee0d8515185b3ab9cbd6b947bc"}, + {file = "h5py-3.9.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a4e20897c88759cbcbd38fb45b507adc91af3e0f67722aa302d71f02dd44d286"}, + {file = "h5py-3.9.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dbf5225543ca35ce9f61c950b73899a82be7ba60d58340e76d0bd42bf659235a"}, + {file = "h5py-3.9.0-cp38-cp38-win_amd64.whl", hash = "sha256:36408f8c62f50007d14e000f9f3acf77e103b9e932c114cbe52a3089e50ebf94"}, + {file = "h5py-3.9.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:23e74b878bbe1653ab34ca49b83cac85529cd0b36b9d625516c5830cc5ca2eac"}, + {file = "h5py-3.9.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:3f457089c5d524b7998e3649bc63240679b8fb0a3859ea53bbb06841f3d755f1"}, + {file = "h5py-3.9.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a6284061f3214335e1eec883a6ee497dbe7a79f19e6a57fed2dd1f03acd5a8cb"}, + {file = "h5py-3.9.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:95f7a745efd0d56076999b52e8da5fad5d30823bac98b59c68ae75588d09991a"}, + {file = "h5py-3.9.0-cp39-cp39-win_amd64.whl", hash = "sha256:79bbca34696c6f9eeeb36a91776070c49a060b2879828e2c8fa6c58b8ed10dd1"}, + {file = "h5py-3.9.0.tar.gz", hash = "sha256:e604db6521c1e367c6bd7fad239c847f53cc46646f2d2651372d05ae5e95f817"}, +] + +[package.dependencies] +numpy = ">=1.17.3" + +[[package]] +name = "hypothesis" +version = "6.31.6" +description = "A library for property-based testing" +category = "dev" +optional = false +python-versions = ">=3.6" +files = [ + {file = "hypothesis-6.31.6-py3-none-any.whl", hash = "sha256:fbd31da5174f3da8d062017302071967b239a1b397d0e3181a44d43346bc6def"}, + {file = "hypothesis-6.31.6.tar.gz", hash = "sha256:d54be6a80b160ad5ea4209b01a0d72e31d910510ed7142fa9907861911800771"}, +] + +[package.dependencies] +attrs = ">=19.2.0" +sortedcontainers = ">=2.1.0,<3.0.0" + +[package.extras] +all = ["backports.zoneinfo (>=0.2.1)", "black (>=19.10b0)", "click (>=7.0)", "django (>=2.2)", "dpcontracts (>=0.4)", "importlib-metadata (>=3.6)", "importlib-resources (>=3.3.0)", "lark-parser (>=0.6.5)", "libcst (>=0.3.16)", "numpy (>=1.9.0)", "pandas (>=0.25)", "pytest (>=4.6)", "python-dateutil (>=1.4)", "pytz (>=2014.1)", "redis (>=3.0.0)", "rich (>=9.0.0)", "tzdata (>=2021.5)"] +cli = ["black (>=19.10b0)", "click (>=7.0)", "rich (>=9.0.0)"] +codemods = ["libcst (>=0.3.16)"] +dateutil = ["python-dateutil (>=1.4)"] +django = ["django (>=2.2)"] +dpcontracts = ["dpcontracts (>=0.4)"] +ghostwriter = ["black (>=19.10b0)"] +lark = ["lark-parser (>=0.6.5)"] +numpy = ["numpy (>=1.9.0)"] +pandas = ["pandas (>=0.25)"] +pytest = ["pytest (>=4.6)"] +pytz = ["pytz (>=2014.1)"] +redis = ["redis (>=3.0.0)"] +zoneinfo = ["backports.zoneinfo (>=0.2.1)", "importlib-resources (>=3.3.0)", "tzdata (>=2021.5)"] + +[[package]] +name = "idna" +version = "3.4" +description = "Internationalized Domain Names in Applications (IDNA)" +category = "main" +optional = false +python-versions = ">=3.5" +files = [ + {file = "idna-3.4-py3-none-any.whl", hash = "sha256:90b77e79eaa3eba6de819a0c442c0b4ceefc341a7a2ab77d7562bf49f425c5c2"}, + {file = "idna-3.4.tar.gz", hash = "sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4"}, +] + +[[package]] +name = "imagesize" +version = "1.4.1" +description = "Getting image size from png/jpeg/jpeg2000/gif file" +category = "dev" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +files = [ + {file = "imagesize-1.4.1-py2.py3-none-any.whl", hash = "sha256:0d8d18d08f840c19d0ee7ca1fd82490fdc3729b7ac93f49870406ddde8ef8d8b"}, + {file = "imagesize-1.4.1.tar.gz", hash = "sha256:69150444affb9cb0d5cc5a92b3676f0b2fb7cd9ae39e947a5e11a36b4497cd4a"}, +] + +[[package]] +name = "importlib-metadata" +version = "6.8.0" +description = "Read metadata from Python packages" +category = "main" +optional = false +python-versions = ">=3.8" +files = [ + {file = "importlib_metadata-6.8.0-py3-none-any.whl", hash = "sha256:3ebb78df84a805d7698245025b975d9d67053cd94c79245ba4b3eb694abe68bb"}, + {file = "importlib_metadata-6.8.0.tar.gz", hash = "sha256:dbace7892d8c0c4ac1ad096662232f831d4e64f4c4545bd53016a3e9d4654743"}, +] + +[package.dependencies] +zipp = ">=0.5" + +[package.extras] +docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] +perf = ["ipython"] +testing = ["flufl.flake8", "importlib-resources (>=1.3)", "packaging", "pyfakefs", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy (>=0.9.1)", "pytest-perf (>=0.9.2)", "pytest-ruff"] + +[[package]] +name = "iniconfig" +version = "2.0.0" +description = "brain-dead simple config-ini parsing" +category = "dev" +optional = false +python-versions = ">=3.7" +files = [ + {file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"}, + {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, +] + +[[package]] +name = "isort" +version = "5.12.0" +description = "A Python utility / library to sort Python imports." +category = "dev" +optional = false +python-versions = ">=3.8.0" +files = [ + {file = "isort-5.12.0-py3-none-any.whl", hash = "sha256:f84c2818376e66cf843d497486ea8fed8700b340f308f076c6fb1229dff318b6"}, + {file = "isort-5.12.0.tar.gz", hash = "sha256:8bef7dde241278824a6d83f44a544709b065191b95b6e50894bdc722fcba0504"}, +] + +[package.extras] +colors = ["colorama (>=0.4.3)"] +pipfile-deprecated-finder = ["pip-shims (>=0.5.2)", "pipreqs", "requirementslib"] +plugins = ["setuptools"] +requirements-deprecated-finder = ["pip-api", "pipreqs"] + +[[package]] +name = "jinja2" +version = "3.1.2" +description = "A very fast and expressive template engine." +category = "dev" +optional = false +python-versions = ">=3.7" +files = [ + {file = "Jinja2-3.1.2-py3-none-any.whl", hash = "sha256:6088930bfe239f0e6710546ab9c19c9ef35e29792895fed6e6e31a023a182a61"}, + {file = "Jinja2-3.1.2.tar.gz", hash = "sha256:31351a702a408a9e7595a8fc6150fc3f43bb6bf7e319770cbc0db9df9437e852"}, +] + +[package.dependencies] +MarkupSafe = ">=2.0" + +[package.extras] +i18n = ["Babel (>=2.7)"] + +[[package]] +name = "joblib" +version = "1.3.1" +description = "Lightweight pipelining with Python functions" +category = "main" +optional = true +python-versions = ">=3.7" +files = [ + {file = "joblib-1.3.1-py3-none-any.whl", hash = "sha256:89cf0529520e01b3de7ac7b74a8102c90d16d54c64b5dd98cafcd14307fdf915"}, + {file = "joblib-1.3.1.tar.gz", hash = "sha256:1f937906df65329ba98013dc9692fe22a4c5e4a648112de500508b18a21b41e3"}, +] + +[[package]] +name = "jsonschema" +version = "4.18.2" +description = "An implementation of JSON Schema validation for Python" +category = "main" +optional = true +python-versions = ">=3.8" +files = [ + {file = "jsonschema-4.18.2-py3-none-any.whl", hash = "sha256:159fdff1443b4c5ed900d4eeac6b928a3485f4aff5fba6edd1e25cd66bb46b39"}, + {file = "jsonschema-4.18.2.tar.gz", hash = "sha256:af3855bfa30e83b2200a5fe12ab5eb92460e4d3b8e4efd34094aa637f7272a87"}, +] + +[package.dependencies] +attrs = ">=22.2.0" +jsonschema-specifications = ">=2023.03.6" +referencing = ">=0.28.4" +rpds-py = ">=0.7.1" + +[package.extras] +format = ["fqdn", "idna", "isoduration", "jsonpointer (>1.13)", "rfc3339-validator", "rfc3987", "uri-template", "webcolors (>=1.11)"] +format-nongpl = ["fqdn", "idna", "isoduration", "jsonpointer (>1.13)", "rfc3339-validator", "rfc3986-validator (>0.1.0)", "uri-template", "webcolors (>=1.11)"] + +[[package]] +name = "jsonschema-specifications" +version = "2023.6.1" +description = "The JSON Schema meta-schemas and vocabularies, exposed as a Registry" +category = "main" +optional = true +python-versions = ">=3.8" +files = [ + {file = "jsonschema_specifications-2023.6.1-py3-none-any.whl", hash = "sha256:3d2b82663aff01815f744bb5c7887e2121a63399b49b104a3c96145474d091d7"}, + {file = "jsonschema_specifications-2023.6.1.tar.gz", hash = "sha256:ca1c4dd059a9e7b34101cf5b3ab7ff1d18b139f35950d598d629837ef66e8f28"}, +] + +[package.dependencies] +referencing = ">=0.28.0" + +[[package]] +name = "keras" +version = "2.10.0" +description = "Deep learning for humans." +category = "main" +optional = false +python-versions = "*" +files = [ + {file = "keras-2.10.0-py2.py3-none-any.whl", hash = "sha256:26a6e2c2522e7468ddea22710a99b3290493768fc08a39e75d1173a0e3452fdf"}, +] + +[[package]] +name = "keras-preprocessing" +version = "1.1.2" +description = "Easy data preprocessing and data augmentation for deep learning models" +category = "main" +optional = false +python-versions = "*" +files = [ + {file = "Keras_Preprocessing-1.1.2-py2.py3-none-any.whl", hash = "sha256:7b82029b130ff61cc99b55f3bd27427df4838576838c5b2f65940e4fcec99a7b"}, + {file = "Keras_Preprocessing-1.1.2.tar.gz", hash = "sha256:add82567c50c8bc648c14195bf544a5ce7c1f76761536956c3d2978970179ef3"}, +] + +[package.dependencies] +numpy = ">=1.9.1" +six = ">=1.9.0" + +[package.extras] +image = ["Pillow (>=5.2.0)", "scipy (>=0.14)"] +pep8 = ["flake8"] +tests = ["Pillow", "keras", "pandas", "pytest", "pytest-cov", "pytest-xdist", "tensorflow"] + +[[package]] +name = "kiwisolver" +version = "1.4.4" +description = "A fast implementation of the Cassowary constraint solver" +category = "main" +optional = false +python-versions = ">=3.7" +files = [ + {file = "kiwisolver-1.4.4-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:2f5e60fabb7343a836360c4f0919b8cd0d6dbf08ad2ca6b9cf90bf0c76a3c4f6"}, + {file = "kiwisolver-1.4.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:10ee06759482c78bdb864f4109886dff7b8a56529bc1609d4f1112b93fe6423c"}, + {file = "kiwisolver-1.4.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c79ebe8f3676a4c6630fd3f777f3cfecf9289666c84e775a67d1d358578dc2e3"}, + {file = "kiwisolver-1.4.4-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:abbe9fa13da955feb8202e215c4018f4bb57469b1b78c7a4c5c7b93001699938"}, + {file = "kiwisolver-1.4.4-cp310-cp310-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:7577c1987baa3adc4b3c62c33bd1118c3ef5c8ddef36f0f2c950ae0b199e100d"}, + {file = "kiwisolver-1.4.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f8ad8285b01b0d4695102546b342b493b3ccc6781fc28c8c6a1bb63e95d22f09"}, + {file = "kiwisolver-1.4.4-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8ed58b8acf29798b036d347791141767ccf65eee7f26bde03a71c944449e53de"}, + {file = "kiwisolver-1.4.4-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a68b62a02953b9841730db7797422f983935aeefceb1679f0fc85cbfbd311c32"}, + {file = "kiwisolver-1.4.4-cp310-cp310-win32.whl", hash = "sha256:e92a513161077b53447160b9bd8f522edfbed4bd9759e4c18ab05d7ef7e49408"}, + {file = "kiwisolver-1.4.4-cp310-cp310-win_amd64.whl", hash = "sha256:3fe20f63c9ecee44560d0e7f116b3a747a5d7203376abeea292ab3152334d004"}, + {file = "kiwisolver-1.4.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:e0ea21f66820452a3f5d1655f8704a60d66ba1191359b96541eaf457710a5fc6"}, + {file = "kiwisolver-1.4.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:bc9db8a3efb3e403e4ecc6cd9489ea2bac94244f80c78e27c31dcc00d2790ac2"}, + {file = "kiwisolver-1.4.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d5b61785a9ce44e5a4b880272baa7cf6c8f48a5180c3e81c59553ba0cb0821ca"}, + {file = "kiwisolver-1.4.4-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c2dbb44c3f7e6c4d3487b31037b1bdbf424d97687c1747ce4ff2895795c9bf69"}, + {file = "kiwisolver-1.4.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6295ecd49304dcf3bfbfa45d9a081c96509e95f4b9d0eb7ee4ec0530c4a96514"}, + {file = "kiwisolver-1.4.4-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4bd472dbe5e136f96a4b18f295d159d7f26fd399136f5b17b08c4e5f498cd494"}, + {file = "kiwisolver-1.4.4-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bf7d9fce9bcc4752ca4a1b80aabd38f6d19009ea5cbda0e0856983cf6d0023f5"}, + {file = "kiwisolver-1.4.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:78d6601aed50c74e0ef02f4204da1816147a6d3fbdc8b3872d263338a9052c51"}, + {file = "kiwisolver-1.4.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:877272cf6b4b7e94c9614f9b10140e198d2186363728ed0f701c6eee1baec1da"}, + {file = "kiwisolver-1.4.4-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:db608a6757adabb32f1cfe6066e39b3706d8c3aa69bbc353a5b61edad36a5cb4"}, + {file = "kiwisolver-1.4.4-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:5853eb494c71e267912275e5586fe281444eb5e722de4e131cddf9d442615626"}, + {file = "kiwisolver-1.4.4-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:f0a1dbdb5ecbef0d34eb77e56fcb3e95bbd7e50835d9782a45df81cc46949750"}, + {file = "kiwisolver-1.4.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:283dffbf061a4ec60391d51e6155e372a1f7a4f5b15d59c8505339454f8989e4"}, + {file = "kiwisolver-1.4.4-cp311-cp311-win32.whl", hash = "sha256:d06adcfa62a4431d404c31216f0f8ac97397d799cd53800e9d3efc2fbb3cf14e"}, + {file = "kiwisolver-1.4.4-cp311-cp311-win_amd64.whl", hash = "sha256:e7da3fec7408813a7cebc9e4ec55afed2d0fd65c4754bc376bf03498d4e92686"}, + {file = "kiwisolver-1.4.4-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:62ac9cc684da4cf1778d07a89bf5f81b35834cb96ca523d3a7fb32509380cbf6"}, + {file = "kiwisolver-1.4.4-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:41dae968a94b1ef1897cb322b39360a0812661dba7c682aa45098eb8e193dbdf"}, + {file = "kiwisolver-1.4.4-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:02f79693ec433cb4b5f51694e8477ae83b3205768a6fb48ffba60549080e295b"}, + {file = "kiwisolver-1.4.4-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d0611a0a2a518464c05ddd5a3a1a0e856ccc10e67079bb17f265ad19ab3c7597"}, + {file = "kiwisolver-1.4.4-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:db5283d90da4174865d520e7366801a93777201e91e79bacbac6e6927cbceede"}, + {file = "kiwisolver-1.4.4-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:1041feb4cda8708ce73bb4dcb9ce1ccf49d553bf87c3954bdfa46f0c3f77252c"}, + {file = "kiwisolver-1.4.4-cp37-cp37m-win32.whl", hash = "sha256:a553dadda40fef6bfa1456dc4be49b113aa92c2a9a9e8711e955618cd69622e3"}, + {file = "kiwisolver-1.4.4-cp37-cp37m-win_amd64.whl", hash = "sha256:03baab2d6b4a54ddbb43bba1a3a2d1627e82d205c5cf8f4c924dc49284b87166"}, + {file = "kiwisolver-1.4.4-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:841293b17ad704d70c578f1f0013c890e219952169ce8a24ebc063eecf775454"}, + {file = "kiwisolver-1.4.4-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:f4f270de01dd3e129a72efad823da90cc4d6aafb64c410c9033aba70db9f1ff0"}, + {file = "kiwisolver-1.4.4-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:f9f39e2f049db33a908319cf46624a569b36983c7c78318e9726a4cb8923b26c"}, + {file = "kiwisolver-1.4.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c97528e64cb9ebeff9701e7938653a9951922f2a38bd847787d4a8e498cc83ae"}, + {file = "kiwisolver-1.4.4-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1d1573129aa0fd901076e2bfb4275a35f5b7aa60fbfb984499d661ec950320b0"}, + {file = "kiwisolver-1.4.4-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ad881edc7ccb9d65b0224f4e4d05a1e85cf62d73aab798943df6d48ab0cd79a1"}, + {file = "kiwisolver-1.4.4-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b428ef021242344340460fa4c9185d0b1f66fbdbfecc6c63eff4b7c29fad429d"}, + {file = "kiwisolver-1.4.4-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:2e407cb4bd5a13984a6c2c0fe1845e4e41e96f183e5e5cd4d77a857d9693494c"}, + {file = "kiwisolver-1.4.4-cp38-cp38-win32.whl", hash = "sha256:75facbe9606748f43428fc91a43edb46c7ff68889b91fa31f53b58894503a191"}, + {file = "kiwisolver-1.4.4-cp38-cp38-win_amd64.whl", hash = "sha256:5bce61af018b0cb2055e0e72e7d65290d822d3feee430b7b8203d8a855e78766"}, + {file = "kiwisolver-1.4.4-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:8c808594c88a025d4e322d5bb549282c93c8e1ba71b790f539567932722d7bd8"}, + {file = "kiwisolver-1.4.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:f0a71d85ecdd570ded8ac3d1c0f480842f49a40beb423bb8014539a9f32a5897"}, + {file = "kiwisolver-1.4.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:b533558eae785e33e8c148a8d9921692a9fe5aa516efbdff8606e7d87b9d5824"}, + {file = "kiwisolver-1.4.4-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:efda5fc8cc1c61e4f639b8067d118e742b812c930f708e6667a5ce0d13499e29"}, + {file = "kiwisolver-1.4.4-cp39-cp39-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:7c43e1e1206cd421cd92e6b3280d4385d41d7166b3ed577ac20444b6995a445f"}, + {file = "kiwisolver-1.4.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bc8d3bd6c72b2dd9decf16ce70e20abcb3274ba01b4e1c96031e0c4067d1e7cd"}, + {file = "kiwisolver-1.4.4-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4ea39b0ccc4f5d803e3337dd46bcce60b702be4d86fd0b3d7531ef10fd99a1ac"}, + {file = "kiwisolver-1.4.4-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:968f44fdbf6dd757d12920d63b566eeb4d5b395fd2d00d29d7ef00a00582aac9"}, + {file = "kiwisolver-1.4.4-cp39-cp39-win32.whl", hash = "sha256:da7e547706e69e45d95e116e6939488d62174e033b763ab1496b4c29b76fabea"}, + {file = "kiwisolver-1.4.4-cp39-cp39-win_amd64.whl", hash = "sha256:ba59c92039ec0a66103b1d5fe588fa546373587a7d68f5c96f743c3396afc04b"}, + {file = "kiwisolver-1.4.4-pp37-pypy37_pp73-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:91672bacaa030f92fc2f43b620d7b337fd9a5af28b0d6ed3f77afc43c4a64b5a"}, + {file = "kiwisolver-1.4.4-pp37-pypy37_pp73-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:787518a6789009c159453da4d6b683f468ef7a65bbde796bcea803ccf191058d"}, + {file = "kiwisolver-1.4.4-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:da152d8cdcab0e56e4f45eb08b9aea6455845ec83172092f09b0e077ece2cf7a"}, + {file = "kiwisolver-1.4.4-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:ecb1fa0db7bf4cff9dac752abb19505a233c7f16684c5826d1f11ebd9472b871"}, + {file = "kiwisolver-1.4.4-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:28bc5b299f48150b5f822ce68624e445040595a4ac3d59251703779836eceff9"}, + {file = "kiwisolver-1.4.4-pp38-pypy38_pp73-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:81e38381b782cc7e1e46c4e14cd997ee6040768101aefc8fa3c24a4cc58e98f8"}, + {file = "kiwisolver-1.4.4-pp38-pypy38_pp73-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:2a66fdfb34e05b705620dd567f5a03f239a088d5a3f321e7b6ac3239d22aa286"}, + {file = "kiwisolver-1.4.4-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:872b8ca05c40d309ed13eb2e582cab0c5a05e81e987ab9c521bf05ad1d5cf5cb"}, + {file = "kiwisolver-1.4.4-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:70e7c2e7b750585569564e2e5ca9845acfaa5da56ac46df68414f29fea97be9f"}, + {file = "kiwisolver-1.4.4-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:9f85003f5dfa867e86d53fac6f7e6f30c045673fa27b603c397753bebadc3008"}, + {file = "kiwisolver-1.4.4-pp39-pypy39_pp73-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2e307eb9bd99801f82789b44bb45e9f541961831c7311521b13a6c85afc09767"}, + {file = "kiwisolver-1.4.4-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b1792d939ec70abe76f5054d3f36ed5656021dcad1322d1cc996d4e54165cef9"}, + {file = "kiwisolver-1.4.4-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f6cb459eea32a4e2cf18ba5fcece2dbdf496384413bc1bae15583f19e567f3b2"}, + {file = "kiwisolver-1.4.4-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:36dafec3d6d6088d34e2de6b85f9d8e2324eb734162fba59d2ba9ed7a2043d5b"}, + {file = "kiwisolver-1.4.4.tar.gz", hash = "sha256:d41997519fcba4a1e46eb4a2fe31bc12f0ff957b2b81bac28db24744f333e955"}, +] + +[[package]] +name = "latexcodec" +version = "2.0.1" +description = "A lexer and codec to work with LaTeX code in Python." +category = "dev" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +files = [ + {file = "latexcodec-2.0.1-py2.py3-none-any.whl", hash = "sha256:c277a193638dc7683c4c30f6684e3db728a06efb0dc9cf346db8bd0aa6c5d271"}, + {file = "latexcodec-2.0.1.tar.gz", hash = "sha256:2aa2551c373261cefe2ad3a8953a6d6533e68238d180eb4bb91d7964adb3fe9a"}, +] + +[package.dependencies] +six = ">=1.4.1" + +[[package]] +name = "lazy-object-proxy" +version = "1.9.0" +description = "A fast and thorough lazy object proxy." +category = "dev" +optional = false +python-versions = ">=3.7" +files = [ + {file = "lazy-object-proxy-1.9.0.tar.gz", hash = "sha256:659fb5809fa4629b8a1ac5106f669cfc7bef26fbb389dda53b3e010d1ac4ebae"}, + {file = "lazy_object_proxy-1.9.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b40387277b0ed2d0602b8293b94d7257e17d1479e257b4de114ea11a8cb7f2d7"}, + {file = "lazy_object_proxy-1.9.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e8c6cfb338b133fbdbc5cfaa10fe3c6aeea827db80c978dbd13bc9dd8526b7d4"}, + {file = "lazy_object_proxy-1.9.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:721532711daa7db0d8b779b0bb0318fa87af1c10d7fe5e52ef30f8eff254d0cd"}, + {file = "lazy_object_proxy-1.9.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:66a3de4a3ec06cd8af3f61b8e1ec67614fbb7c995d02fa224813cb7afefee701"}, + {file = "lazy_object_proxy-1.9.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:1aa3de4088c89a1b69f8ec0dcc169aa725b0ff017899ac568fe44ddc1396df46"}, + {file = "lazy_object_proxy-1.9.0-cp310-cp310-win32.whl", hash = "sha256:f0705c376533ed2a9e5e97aacdbfe04cecd71e0aa84c7c0595d02ef93b6e4455"}, + {file = "lazy_object_proxy-1.9.0-cp310-cp310-win_amd64.whl", hash = "sha256:ea806fd4c37bf7e7ad82537b0757999264d5f70c45468447bb2b91afdbe73a6e"}, + {file = "lazy_object_proxy-1.9.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:946d27deaff6cf8452ed0dba83ba38839a87f4f7a9732e8f9fd4107b21e6ff07"}, + {file = "lazy_object_proxy-1.9.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:79a31b086e7e68b24b99b23d57723ef7e2c6d81ed21007b6281ebcd1688acb0a"}, + {file = "lazy_object_proxy-1.9.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f699ac1c768270c9e384e4cbd268d6e67aebcfae6cd623b4d7c3bfde5a35db59"}, + {file = "lazy_object_proxy-1.9.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:bfb38f9ffb53b942f2b5954e0f610f1e721ccebe9cce9025a38c8ccf4a5183a4"}, + {file = "lazy_object_proxy-1.9.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:189bbd5d41ae7a498397287c408617fe5c48633e7755287b21d741f7db2706a9"}, + {file = "lazy_object_proxy-1.9.0-cp311-cp311-win32.whl", hash = "sha256:81fc4d08b062b535d95c9ea70dbe8a335c45c04029878e62d744bdced5141586"}, + {file = "lazy_object_proxy-1.9.0-cp311-cp311-win_amd64.whl", hash = "sha256:f2457189d8257dd41ae9b434ba33298aec198e30adf2dcdaaa3a28b9994f6adb"}, + {file = "lazy_object_proxy-1.9.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:d9e25ef10a39e8afe59a5c348a4dbf29b4868ab76269f81ce1674494e2565a6e"}, + {file = "lazy_object_proxy-1.9.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cbf9b082426036e19c6924a9ce90c740a9861e2bdc27a4834fd0a910742ac1e8"}, + {file = "lazy_object_proxy-1.9.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9f5fa4a61ce2438267163891961cfd5e32ec97a2c444e5b842d574251ade27d2"}, + {file = "lazy_object_proxy-1.9.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:8fa02eaab317b1e9e03f69aab1f91e120e7899b392c4fc19807a8278a07a97e8"}, + {file = "lazy_object_proxy-1.9.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:e7c21c95cae3c05c14aafffe2865bbd5e377cfc1348c4f7751d9dc9a48ca4bda"}, + {file = "lazy_object_proxy-1.9.0-cp37-cp37m-win32.whl", hash = "sha256:f12ad7126ae0c98d601a7ee504c1122bcef553d1d5e0c3bfa77b16b3968d2734"}, + {file = "lazy_object_proxy-1.9.0-cp37-cp37m-win_amd64.whl", hash = "sha256:edd20c5a55acb67c7ed471fa2b5fb66cb17f61430b7a6b9c3b4a1e40293b1671"}, + {file = "lazy_object_proxy-1.9.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:2d0daa332786cf3bb49e10dc6a17a52f6a8f9601b4cf5c295a4f85854d61de63"}, + {file = "lazy_object_proxy-1.9.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9cd077f3d04a58e83d04b20e334f678c2b0ff9879b9375ed107d5d07ff160171"}, + {file = "lazy_object_proxy-1.9.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:660c94ea760b3ce47d1855a30984c78327500493d396eac4dfd8bd82041b22be"}, + {file = "lazy_object_proxy-1.9.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:212774e4dfa851e74d393a2370871e174d7ff0ebc980907723bb67d25c8a7c30"}, + {file = "lazy_object_proxy-1.9.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:f0117049dd1d5635bbff65444496c90e0baa48ea405125c088e93d9cf4525b11"}, + {file = "lazy_object_proxy-1.9.0-cp38-cp38-win32.whl", hash = "sha256:0a891e4e41b54fd5b8313b96399f8b0e173bbbfc03c7631f01efbe29bb0bcf82"}, + {file = "lazy_object_proxy-1.9.0-cp38-cp38-win_amd64.whl", hash = "sha256:9990d8e71b9f6488e91ad25f322898c136b008d87bf852ff65391b004da5e17b"}, + {file = "lazy_object_proxy-1.9.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9e7551208b2aded9c1447453ee366f1c4070602b3d932ace044715d89666899b"}, + {file = "lazy_object_proxy-1.9.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5f83ac4d83ef0ab017683d715ed356e30dd48a93746309c8f3517e1287523ef4"}, + {file = "lazy_object_proxy-1.9.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7322c3d6f1766d4ef1e51a465f47955f1e8123caee67dd641e67d539a534d006"}, + {file = "lazy_object_proxy-1.9.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:18b78ec83edbbeb69efdc0e9c1cb41a3b1b1ed11ddd8ded602464c3fc6020494"}, + {file = "lazy_object_proxy-1.9.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:09763491ce220c0299688940f8dc2c5d05fd1f45af1e42e636b2e8b2303e4382"}, + {file = "lazy_object_proxy-1.9.0-cp39-cp39-win32.whl", hash = "sha256:9090d8e53235aa280fc9239a86ae3ea8ac58eff66a705fa6aa2ec4968b95c821"}, + {file = "lazy_object_proxy-1.9.0-cp39-cp39-win_amd64.whl", hash = "sha256:db1c1722726f47e10e0b5fdbf15ac3b8adb58c091d12b3ab713965795036985f"}, +] + +[[package]] +name = "libclang" +version = "16.0.0" +description = "Clang Python Bindings, mirrored from the official LLVM repo: https://github.com/llvm/llvm-project/tree/main/clang/bindings/python, to make the installation process easier." +category = "main" +optional = false +python-versions = "*" +files = [ + {file = "libclang-16.0.0-py2.py3-none-macosx_10_9_x86_64.whl", hash = "sha256:65258a6bb3e7dc31dc9b26f8d42f53c9d3b959643ade291fcd1aef4855303ca6"}, + {file = "libclang-16.0.0-py2.py3-none-macosx_11_0_arm64.whl", hash = "sha256:af55a4aa86fdfe6b2ec68bc8cfe5fdac6c448d591ca7648be86ca17099b41ca8"}, + {file = "libclang-16.0.0-py2.py3-none-manylinux2010_x86_64.whl", hash = "sha256:a043138caaf2cb076ebb060c6281ec95612926645d425c691991fc9df00e8a24"}, + {file = "libclang-16.0.0-py2.py3-none-manylinux2014_aarch64.whl", hash = "sha256:eb59652cb0559c0e71784ff4c8ba24c14644becc907b1446563ecfaa622d523b"}, + {file = "libclang-16.0.0-py2.py3-none-manylinux2014_armv7l.whl", hash = "sha256:7b6686b67a0daa84b4c614bcc119578329fc4fbb52b919565b7376b507c4793b"}, + {file = "libclang-16.0.0-py2.py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:2adce42ae652f312245b8f4eda6f30b4076fb61f7619f2dfd0a0c31dee4c32b9"}, + {file = "libclang-16.0.0-py2.py3-none-win_amd64.whl", hash = "sha256:ee20bf93e3dd330f71fc50cdbf13b92ced0aec8e540be64251db53502a9b33f7"}, + {file = "libclang-16.0.0-py2.py3-none-win_arm64.whl", hash = "sha256:bf4628fc4da7a1dd06a244f9b8e121c5ec68076a763c59d6b13cbb103acc935b"}, +] + +[[package]] +name = "llvmlite" +version = "0.39.1" +description = "lightweight wrapper around basic LLVM functionality" +category = "main" +optional = false +python-versions = ">=3.7" +files = [ + {file = "llvmlite-0.39.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6717c7a6e93c9d2c3d07c07113ec80ae24af45cde536b34363d4bcd9188091d9"}, + {file = "llvmlite-0.39.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ddab526c5a2c4ccb8c9ec4821fcea7606933dc53f510e2a6eebb45a418d3488a"}, + {file = "llvmlite-0.39.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a3f331a323d0f0ada6b10d60182ef06c20a2f01be21699999d204c5750ffd0b4"}, + {file = "llvmlite-0.39.1-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e2c00ff204afa721b0bb9835b5bf1ba7fba210eefcec5552a9e05a63219ba0dc"}, + {file = "llvmlite-0.39.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:16f56eb1eec3cda3a5c526bc3f63594fc24e0c8d219375afeb336f289764c6c7"}, + {file = "llvmlite-0.39.1-cp310-cp310-win32.whl", hash = "sha256:d0bfd18c324549c0fec2c5dc610fd024689de6f27c6cc67e4e24a07541d6e49b"}, + {file = "llvmlite-0.39.1-cp310-cp310-win_amd64.whl", hash = "sha256:7ebf1eb9badc2a397d4f6a6c8717447c81ac011db00064a00408bc83c923c0e4"}, + {file = "llvmlite-0.39.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:6546bed4e02a1c3d53a22a0bced254b3b6894693318b16c16c8e43e29d6befb6"}, + {file = "llvmlite-0.39.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1578f5000fdce513712e99543c50e93758a954297575610f48cb1fd71b27c08a"}, + {file = "llvmlite-0.39.1-cp37-cp37m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3803f11ad5f6f6c3d2b545a303d68d9fabb1d50e06a8d6418e6fcd2d0df00959"}, + {file = "llvmlite-0.39.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:50aea09a2b933dab7c9df92361b1844ad3145bfb8dd2deb9cd8b8917d59306fb"}, + {file = "llvmlite-0.39.1-cp37-cp37m-win32.whl", hash = "sha256:b1a0bbdb274fb683f993198775b957d29a6f07b45d184c571ef2a721ce4388cf"}, + {file = "llvmlite-0.39.1-cp37-cp37m-win_amd64.whl", hash = "sha256:e172c73fccf7d6db4bd6f7de963dedded900d1a5c6778733241d878ba613980e"}, + {file = "llvmlite-0.39.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:e31f4b799d530255aaf0566e3da2df5bfc35d3cd9d6d5a3dcc251663656c27b1"}, + {file = "llvmlite-0.39.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:62c0ea22e0b9dffb020601bb65cb11dd967a095a488be73f07d8867f4e327ca5"}, + {file = "llvmlite-0.39.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9ffc84ade195abd4abcf0bd3b827b9140ae9ef90999429b9ea84d5df69c9058c"}, + {file = "llvmlite-0.39.1-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c0f158e4708dda6367d21cf15afc58de4ebce979c7a1aa2f6b977aae737e2a54"}, + {file = "llvmlite-0.39.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:22d36591cd5d02038912321d9ab8e4668e53ae2211da5523f454e992b5e13c36"}, + {file = "llvmlite-0.39.1-cp38-cp38-win32.whl", hash = "sha256:4c6ebace910410daf0bebda09c1859504fc2f33d122e9a971c4c349c89cca630"}, + {file = "llvmlite-0.39.1-cp38-cp38-win_amd64.whl", hash = "sha256:fb62fc7016b592435d3e3a8f680e3ea8897c3c9e62e6e6cc58011e7a4801439e"}, + {file = "llvmlite-0.39.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:fa9b26939ae553bf30a9f5c4c754db0fb2d2677327f2511e674aa2f5df941789"}, + {file = "llvmlite-0.39.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e4f212c018db951da3e1dc25c2651abc688221934739721f2dad5ff1dd5f90e7"}, + {file = "llvmlite-0.39.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:39dc2160aed36e989610fc403487f11b8764b6650017ff367e45384dff88ffbf"}, + {file = "llvmlite-0.39.1-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1ec3d70b3e507515936e475d9811305f52d049281eaa6c8273448a61c9b5b7e2"}, + {file = "llvmlite-0.39.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:60f8dd1e76f47b3dbdee4b38d9189f3e020d22a173c00f930b52131001d801f9"}, + {file = "llvmlite-0.39.1-cp39-cp39-win32.whl", hash = "sha256:03aee0ccd81735696474dc4f8b6be60774892a2929d6c05d093d17392c237f32"}, + {file = "llvmlite-0.39.1-cp39-cp39-win_amd64.whl", hash = "sha256:3fc14e757bc07a919221f0cbaacb512704ce5774d7fcada793f1996d6bc75f2a"}, + {file = "llvmlite-0.39.1.tar.gz", hash = "sha256:b43abd7c82e805261c425d50335be9a6c4f84264e34d6d6e475207300005d572"}, +] + +[[package]] +name = "locket" +version = "1.0.0" +description = "File-based locks for Python on Linux and Windows" +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +files = [ + {file = "locket-1.0.0-py2.py3-none-any.whl", hash = "sha256:b6c819a722f7b6bd955b80781788e4a66a55628b858d347536b7e81325a3a5e3"}, + {file = "locket-1.0.0.tar.gz", hash = "sha256:5c0d4c052a8bbbf750e056a8e65ccd309086f4f0f18a2eac306a8dfa4112a632"}, +] + +[[package]] +name = "m2r2" +version = "0.3.3.post2" +description = "Markdown and reStructuredText in a single file." +category = "dev" +optional = false +python-versions = ">=3.7" +files = [ + {file = "m2r2-0.3.3.post2-py3-none-any.whl", hash = "sha256:86157721eb6eabcd54d4eea7195890cc58fa6188b8d0abea633383cfbb5e11e3"}, + {file = "m2r2-0.3.3.post2.tar.gz", hash = "sha256:e62bcb0e74b3ce19cda0737a0556b04cf4a43b785072fcef474558f2c1482ca8"}, +] + +[package.dependencies] +docutils = ">=0.19" +mistune = "0.8.4" + +[[package]] +name = "markdown" +version = "3.4.3" +description = "Python implementation of John Gruber's Markdown." +category = "main" +optional = false +python-versions = ">=3.7" +files = [ + {file = "Markdown-3.4.3-py3-none-any.whl", hash = "sha256:065fd4df22da73a625f14890dd77eb8040edcbd68794bcd35943be14490608b2"}, + {file = "Markdown-3.4.3.tar.gz", hash = "sha256:8bf101198e004dc93e84a12a7395e31aac6a9c9942848ae1d99b9d72cf9b3520"}, +] + +[package.dependencies] +importlib-metadata = {version = ">=4.4", markers = "python_version < \"3.10\""} + +[package.extras] +testing = ["coverage", "pyyaml"] + +[[package]] +name = "markupsafe" +version = "2.1.3" +description = "Safely add untrusted strings to HTML/XML markup." +category = "main" +optional = false +python-versions = ">=3.7" +files = [ + {file = "MarkupSafe-2.1.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:cd0f502fe016460680cd20aaa5a76d241d6f35a1c3350c474bac1273803893fa"}, + {file = "MarkupSafe-2.1.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e09031c87a1e51556fdcb46e5bd4f59dfb743061cf93c4d6831bf894f125eb57"}, + {file = "MarkupSafe-2.1.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:68e78619a61ecf91e76aa3e6e8e33fc4894a2bebe93410754bd28fce0a8a4f9f"}, + {file = "MarkupSafe-2.1.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:65c1a9bcdadc6c28eecee2c119465aebff8f7a584dd719facdd9e825ec61ab52"}, + {file = "MarkupSafe-2.1.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:525808b8019e36eb524b8c68acdd63a37e75714eac50e988180b169d64480a00"}, + {file = "MarkupSafe-2.1.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:962f82a3086483f5e5f64dbad880d31038b698494799b097bc59c2edf392fce6"}, + {file = "MarkupSafe-2.1.3-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:aa7bd130efab1c280bed0f45501b7c8795f9fdbeb02e965371bbef3523627779"}, + {file = "MarkupSafe-2.1.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:c9c804664ebe8f83a211cace637506669e7890fec1b4195b505c214e50dd4eb7"}, + {file = "MarkupSafe-2.1.3-cp310-cp310-win32.whl", hash = "sha256:10bbfe99883db80bdbaff2dcf681dfc6533a614f700da1287707e8a5d78a8431"}, + {file = "MarkupSafe-2.1.3-cp310-cp310-win_amd64.whl", hash = "sha256:1577735524cdad32f9f694208aa75e422adba74f1baee7551620e43a3141f559"}, + {file = "MarkupSafe-2.1.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:ad9e82fb8f09ade1c3e1b996a6337afac2b8b9e365f926f5a61aacc71adc5b3c"}, + {file = "MarkupSafe-2.1.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3c0fae6c3be832a0a0473ac912810b2877c8cb9d76ca48de1ed31e1c68386575"}, + {file = "MarkupSafe-2.1.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b076b6226fb84157e3f7c971a47ff3a679d837cf338547532ab866c57930dbee"}, + {file = "MarkupSafe-2.1.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bfce63a9e7834b12b87c64d6b155fdd9b3b96191b6bd334bf37db7ff1fe457f2"}, + {file = "MarkupSafe-2.1.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:338ae27d6b8745585f87218a3f23f1512dbf52c26c28e322dbe54bcede54ccb9"}, + {file = "MarkupSafe-2.1.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:e4dd52d80b8c83fdce44e12478ad2e85c64ea965e75d66dbeafb0a3e77308fcc"}, + {file = "MarkupSafe-2.1.3-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:df0be2b576a7abbf737b1575f048c23fb1d769f267ec4358296f31c2479db8f9"}, + {file = "MarkupSafe-2.1.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:5bbe06f8eeafd38e5d0a4894ffec89378b6c6a625ff57e3028921f8ff59318ac"}, + {file = "MarkupSafe-2.1.3-cp311-cp311-win32.whl", hash = "sha256:dd15ff04ffd7e05ffcb7fe79f1b98041b8ea30ae9234aed2a9168b5797c3effb"}, + {file = "MarkupSafe-2.1.3-cp311-cp311-win_amd64.whl", hash = "sha256:134da1eca9ec0ae528110ccc9e48041e0828d79f24121a1a146161103c76e686"}, + {file = "MarkupSafe-2.1.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:8e254ae696c88d98da6555f5ace2279cf7cd5b3f52be2b5cf97feafe883b58d2"}, + {file = "MarkupSafe-2.1.3-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cb0932dc158471523c9637e807d9bfb93e06a95cbf010f1a38b98623b929ef2b"}, + {file = "MarkupSafe-2.1.3-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9402b03f1a1b4dc4c19845e5c749e3ab82d5078d16a2a4c2cd2df62d57bb0707"}, + {file = "MarkupSafe-2.1.3-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ca379055a47383d02a5400cb0d110cef0a776fc644cda797db0c5696cfd7e18e"}, + {file = "MarkupSafe-2.1.3-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:b7ff0f54cb4ff66dd38bebd335a38e2c22c41a8ee45aa608efc890ac3e3931bc"}, + {file = "MarkupSafe-2.1.3-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:c011a4149cfbcf9f03994ec2edffcb8b1dc2d2aede7ca243746df97a5d41ce48"}, + {file = "MarkupSafe-2.1.3-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:56d9f2ecac662ca1611d183feb03a3fa4406469dafe241673d521dd5ae92a155"}, + {file = "MarkupSafe-2.1.3-cp37-cp37m-win32.whl", hash = "sha256:8758846a7e80910096950b67071243da3e5a20ed2546e6392603c096778d48e0"}, + {file = "MarkupSafe-2.1.3-cp37-cp37m-win_amd64.whl", hash = "sha256:787003c0ddb00500e49a10f2844fac87aa6ce977b90b0feaaf9de23c22508b24"}, + {file = "MarkupSafe-2.1.3-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:2ef12179d3a291be237280175b542c07a36e7f60718296278d8593d21ca937d4"}, + {file = "MarkupSafe-2.1.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:2c1b19b3aaacc6e57b7e25710ff571c24d6c3613a45e905b1fde04d691b98ee0"}, + {file = "MarkupSafe-2.1.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8afafd99945ead6e075b973fefa56379c5b5c53fd8937dad92c662da5d8fd5ee"}, + {file = "MarkupSafe-2.1.3-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8c41976a29d078bb235fea9b2ecd3da465df42a562910f9022f1a03107bd02be"}, + {file = "MarkupSafe-2.1.3-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d080e0a5eb2529460b30190fcfcc4199bd7f827663f858a226a81bc27beaa97e"}, + {file = "MarkupSafe-2.1.3-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:69c0f17e9f5a7afdf2cc9fb2d1ce6aabdb3bafb7f38017c0b77862bcec2bbad8"}, + {file = "MarkupSafe-2.1.3-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:504b320cd4b7eff6f968eddf81127112db685e81f7e36e75f9f84f0df46041c3"}, + {file = "MarkupSafe-2.1.3-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:42de32b22b6b804f42c5d98be4f7e5e977ecdd9ee9b660fda1a3edf03b11792d"}, + {file = "MarkupSafe-2.1.3-cp38-cp38-win32.whl", hash = "sha256:ceb01949af7121f9fc39f7d27f91be8546f3fb112c608bc4029aef0bab86a2a5"}, + {file = "MarkupSafe-2.1.3-cp38-cp38-win_amd64.whl", hash = "sha256:1b40069d487e7edb2676d3fbdb2b0829ffa2cd63a2ec26c4938b2d34391b4ecc"}, + {file = "MarkupSafe-2.1.3-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:8023faf4e01efadfa183e863fefde0046de576c6f14659e8782065bcece22198"}, + {file = "MarkupSafe-2.1.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:6b2b56950d93e41f33b4223ead100ea0fe11f8e6ee5f641eb753ce4b77a7042b"}, + {file = "MarkupSafe-2.1.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9dcdfd0eaf283af041973bff14a2e143b8bd64e069f4c383416ecd79a81aab58"}, + {file = "MarkupSafe-2.1.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:05fb21170423db021895e1ea1e1f3ab3adb85d1c2333cbc2310f2a26bc77272e"}, + {file = "MarkupSafe-2.1.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:282c2cb35b5b673bbcadb33a585408104df04f14b2d9b01d4c345a3b92861c2c"}, + {file = "MarkupSafe-2.1.3-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:ab4a0df41e7c16a1392727727e7998a467472d0ad65f3ad5e6e765015df08636"}, + {file = "MarkupSafe-2.1.3-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:7ef3cb2ebbf91e330e3bb937efada0edd9003683db6b57bb108c4001f37a02ea"}, + {file = "MarkupSafe-2.1.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:0a4e4a1aff6c7ac4cd55792abf96c915634c2b97e3cc1c7129578aa68ebd754e"}, + {file = "MarkupSafe-2.1.3-cp39-cp39-win32.whl", hash = "sha256:fec21693218efe39aa7f8599346e90c705afa52c5b31ae019b2e57e8f6542bb2"}, + {file = "MarkupSafe-2.1.3-cp39-cp39-win_amd64.whl", hash = "sha256:3fd4abcb888d15a94f32b75d8fd18ee162ca0c064f35b11134be77050296d6ba"}, + {file = "MarkupSafe-2.1.3.tar.gz", hash = "sha256:af598ed32d6ae86f1b747b82783958b1a4ab8f617b06fe68795c7f026abbdcad"}, +] + +[[package]] +name = "matplotlib" +version = "3.5.0" +description = "Python plotting package" +category = "main" +optional = false +python-versions = ">=3.7" +files = [ + {file = "matplotlib-3.5.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:4b018ea6f26424a0852eb60eb406420d9f0d34f65736ea7bbfbb104946a66d86"}, + {file = "matplotlib-3.5.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:a07ff2565da72a7b384a9e000b15b6b8270d81370af8a3531a16f6fbcee023cc"}, + {file = "matplotlib-3.5.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2eea16883aa7724c95eea0eb473ab585c6cf66f0e28f7f13e63deb38f4fd6d0f"}, + {file = "matplotlib-3.5.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0e020a42f3338823a393dd2f80e39a2c07b9f941dfe2c778eb104eeb33d60bb5"}, + {file = "matplotlib-3.5.0-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9bac8eb1eccef540d7f4e844b6313d9f7722efd48c07e1b4bfec1056132127fd"}, + {file = "matplotlib-3.5.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7a7cb59ebd63a8ac4542ec1c61dd08724f82ec3aa7bb6b4b9e212d43c611ce3d"}, + {file = "matplotlib-3.5.0-cp310-cp310-win32.whl", hash = "sha256:6e0e6b2111165522ad336705499b1f968c34a9e84d05d498ee5af0b5697d1efe"}, + {file = "matplotlib-3.5.0-cp310-cp310-win_amd64.whl", hash = "sha256:ff5d9fe518ad2de14ce82ab906b6ab5c2b0c7f4f984400ff8a7a905daa580a0a"}, + {file = "matplotlib-3.5.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:66b172610db0ececebebb09d146f54205f87c7b841454e408fba854764f91bdd"}, + {file = "matplotlib-3.5.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ee3d9ff16d749a9aa521bd7d86f0dbf256b2d2ac8ce31b19e4d2c86d2f2ff0b6"}, + {file = "matplotlib-3.5.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:970aa97297537540369d05fe0fd1bb952593f9ab696c9b427c06990a83e2418b"}, + {file = "matplotlib-3.5.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:153a0cf6a6ff4f406a0600d2034710c49988bacc6313d193b32716f98a697580"}, + {file = "matplotlib-3.5.0-cp37-cp37m-win32.whl", hash = "sha256:6db02c5605f063b67780f4d5753476b6a4944343284aa4e93c5e8ff6e9ec7f76"}, + {file = "matplotlib-3.5.0-cp37-cp37m-win_amd64.whl", hash = "sha256:df0042cab69f4d246f4cb8fc297770ac4ae6ec2983f61836b04a117722037dcd"}, + {file = "matplotlib-3.5.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:a7bf8b05c214d32fb7ca7c001fde70b9b426378e897b0adbf77b85ea3569d56a"}, + {file = "matplotlib-3.5.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:0abf8b51cc6d3ba34d1b15b26e329f23879848a0cf1216954c1f432ffc7e1af7"}, + {file = "matplotlib-3.5.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:13930a0c9bec0fd25f43c448b047a21af1353328b946f044a8fc3be077c6b1a8"}, + {file = "matplotlib-3.5.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:18f6e52386300db5cc4d1e9019ad9da2e80658bab018834d963ebb0aa5355095"}, + {file = "matplotlib-3.5.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ba107add08e12600b072cf3c47aaa1ab85dd4d3c48107a5d3377d1bf80f8b235"}, + {file = "matplotlib-3.5.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:2089b9014792dcc87bb1d620cde847913338abf7d957ef05587382b0cb76d44e"}, + {file = "matplotlib-3.5.0-cp38-cp38-win32.whl", hash = "sha256:f23fbf70d2e80f4e03a83fc1206a8306d9bc50482fee4239f10676ce7e470c83"}, + {file = "matplotlib-3.5.0-cp38-cp38-win_amd64.whl", hash = "sha256:71a1851111f23f82fc43d2b6b2bfdd3f760579a664ebc939576fe21cc6133d01"}, + {file = "matplotlib-3.5.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:d092b7ba63182d2dd427904e3eb58dd5c46ec67c5968de14a4b5007010a3a4cc"}, + {file = "matplotlib-3.5.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:ac17a7e7b06ee426a4989f0b7f24ab1a592e39cdf56353a90f4e998bc0bf44d6"}, + {file = "matplotlib-3.5.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a5b62d1805cc83d755972033c05cea78a1e177a159fc84da5c9c4ab6303ccbd9"}, + {file = "matplotlib-3.5.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:666d717a4798eb9c5d3ae83fe80c7bc6ed696b93e879cb01cb24a74155c73612"}, + {file = "matplotlib-3.5.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:65f877882b7ddede7090c7d87be27a0f4720fe7fc6fddd4409c06e1aa0f1ae8d"}, + {file = "matplotlib-3.5.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:7baf23adb698d8c6ca7339c9dde00931bc47b2dd82fa912827fef9f93db77f5e"}, + {file = "matplotlib-3.5.0-cp39-cp39-win32.whl", hash = "sha256:b3b687e905da32e5f2e5f16efa713f5d1fcd9fb8b8c697895de35c91fedeb086"}, + {file = "matplotlib-3.5.0-cp39-cp39-win_amd64.whl", hash = "sha256:a6cef5b31e27c31253c0f852b629a38d550ae66ec6850129c49d872f9ee428cb"}, + {file = "matplotlib-3.5.0-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:a0dcaf5648cecddc328e81a0421821a1f65a1d517b20746c94a1f0f5c36fb51a"}, + {file = "matplotlib-3.5.0-pp37-pypy37_pp73-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:b5e439d9e55d645f2a4dca63e2f66d68fe974c405053b132d61c7e98c25dfeb2"}, + {file = "matplotlib-3.5.0-pp37-pypy37_pp73-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:dc8c5c23e7056e126275dbf29efba817b3d94196690930d0968873ac3a94ab82"}, + {file = "matplotlib-3.5.0-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:a0ea10faa3bab0714d3a19c7e0921279a68d57552414d6eceaea99f97d7735db"}, + {file = "matplotlib-3.5.0.tar.gz", hash = "sha256:38892a254420d95594285077276162a5e9e9c30b6da08bdc2a4d53331ad9a6fa"}, +] + +[package.dependencies] +cycler = ">=0.10" +fonttools = ">=4.22.0" +kiwisolver = ">=1.0.1" +numpy = ">=1.17" +packaging = ">=20.0" +pillow = ">=6.2.0" +pyparsing = ">=2.2.1" +python-dateutil = ">=2.7" +setuptools-scm = ">=4" + +[[package]] +name = "mccabe" +version = "0.6.1" +description = "McCabe checker, plugin for flake8" +category = "dev" +optional = false +python-versions = "*" +files = [ + {file = "mccabe-0.6.1-py2.py3-none-any.whl", hash = "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42"}, + {file = "mccabe-0.6.1.tar.gz", hash = "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f"}, +] + +[[package]] +name = "mistune" +version = "0.8.4" +description = "The fastest markdown parser in pure Python" +category = "dev" +optional = false +python-versions = "*" +files = [ + {file = "mistune-0.8.4-py2.py3-none-any.whl", hash = "sha256:88a1051873018da288eee8538d476dffe1262495144b33ecb586c4ab266bb8d4"}, + {file = "mistune-0.8.4.tar.gz", hash = "sha256:59a3429db53c50b5c6bcc8a07f8848cb00d7dc8bdb431a4ab41920d201d4756e"}, +] + +[[package]] +name = "mpmath" +version = "1.3.0" +description = "Python library for arbitrary-precision floating-point arithmetic" +category = "main" +optional = false +python-versions = "*" +files = [ + {file = "mpmath-1.3.0-py3-none-any.whl", hash = "sha256:a0b2b9fe80bbcd81a6647ff13108738cfb482d481d826cc0e02f5b35e5c88d2c"}, + {file = "mpmath-1.3.0.tar.gz", hash = "sha256:7a28eb2a9774d00c7bc92411c19a89209d5da7c4c9a9e227be8330a23a25b91f"}, +] + +[package.extras] +develop = ["codecov", "pycodestyle", "pytest (>=4.6)", "pytest-cov", "wheel"] +docs = ["sphinx"] +gmpy = ["gmpy2 (>=2.1.0a4)"] +tests = ["pytest (>=4.6)"] + +[[package]] +name = "msgpack" +version = "1.0.5" +description = "MessagePack serializer" +category = "main" +optional = true +python-versions = "*" +files = [ + {file = "msgpack-1.0.5-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:525228efd79bb831cf6830a732e2e80bc1b05436b086d4264814b4b2955b2fa9"}, + {file = "msgpack-1.0.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:4f8d8b3bf1ff2672567d6b5c725a1b347fe838b912772aa8ae2bf70338d5a198"}, + {file = "msgpack-1.0.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:cdc793c50be3f01106245a61b739328f7dccc2c648b501e237f0699fe1395b81"}, + {file = "msgpack-1.0.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5cb47c21a8a65b165ce29f2bec852790cbc04936f502966768e4aae9fa763cb7"}, + {file = "msgpack-1.0.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e42b9594cc3bf4d838d67d6ed62b9e59e201862a25e9a157019e171fbe672dd3"}, + {file = "msgpack-1.0.5-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:55b56a24893105dc52c1253649b60f475f36b3aa0fc66115bffafb624d7cb30b"}, + {file = "msgpack-1.0.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:1967f6129fc50a43bfe0951c35acbb729be89a55d849fab7686004da85103f1c"}, + {file = "msgpack-1.0.5-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:20a97bf595a232c3ee6d57ddaadd5453d174a52594bf9c21d10407e2a2d9b3bd"}, + {file = "msgpack-1.0.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:d25dd59bbbbb996eacf7be6b4ad082ed7eacc4e8f3d2df1ba43822da9bfa122a"}, + {file = "msgpack-1.0.5-cp310-cp310-win32.whl", hash = "sha256:382b2c77589331f2cb80b67cc058c00f225e19827dbc818d700f61513ab47bea"}, + {file = "msgpack-1.0.5-cp310-cp310-win_amd64.whl", hash = "sha256:4867aa2df9e2a5fa5f76d7d5565d25ec76e84c106b55509e78c1ede0f152659a"}, + {file = "msgpack-1.0.5-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:9f5ae84c5c8a857ec44dc180a8b0cc08238e021f57abdf51a8182e915e6299f0"}, + {file = "msgpack-1.0.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:9e6ca5d5699bcd89ae605c150aee83b5321f2115695e741b99618f4856c50898"}, + {file = "msgpack-1.0.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5494ea30d517a3576749cad32fa27f7585c65f5f38309c88c6d137877fa28a5a"}, + {file = "msgpack-1.0.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1ab2f3331cb1b54165976a9d976cb251a83183631c88076613c6c780f0d6e45a"}, + {file = "msgpack-1.0.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:28592e20bbb1620848256ebc105fc420436af59515793ed27d5c77a217477705"}, + {file = "msgpack-1.0.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fe5c63197c55bce6385d9aee16c4d0641684628f63ace85f73571e65ad1c1e8d"}, + {file = "msgpack-1.0.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ed40e926fa2f297e8a653c954b732f125ef97bdd4c889f243182299de27e2aa9"}, + {file = "msgpack-1.0.5-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:b2de4c1c0538dcb7010902a2b97f4e00fc4ddf2c8cda9749af0e594d3b7fa3d7"}, + {file = "msgpack-1.0.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:bf22a83f973b50f9d38e55c6aade04c41ddda19b00c4ebc558930d78eecc64ed"}, + {file = "msgpack-1.0.5-cp311-cp311-win32.whl", hash = "sha256:c396e2cc213d12ce017b686e0f53497f94f8ba2b24799c25d913d46c08ec422c"}, + {file = "msgpack-1.0.5-cp311-cp311-win_amd64.whl", hash = "sha256:6c4c68d87497f66f96d50142a2b73b97972130d93677ce930718f68828b382e2"}, + {file = "msgpack-1.0.5-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:a2b031c2e9b9af485d5e3c4520f4220d74f4d222a5b8dc8c1a3ab9448ca79c57"}, + {file = "msgpack-1.0.5-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4f837b93669ce4336e24d08286c38761132bc7ab29782727f8557e1eb21b2080"}, + {file = "msgpack-1.0.5-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b1d46dfe3832660f53b13b925d4e0fa1432b00f5f7210eb3ad3bb9a13c6204a6"}, + {file = "msgpack-1.0.5-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:366c9a7b9057e1547f4ad51d8facad8b406bab69c7d72c0eb6f529cf76d4b85f"}, + {file = "msgpack-1.0.5-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:4c075728a1095efd0634a7dccb06204919a2f67d1893b6aa8e00497258bf926c"}, + {file = "msgpack-1.0.5-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:f933bbda5a3ee63b8834179096923b094b76f0c7a73c1cfe8f07ad608c58844b"}, + {file = "msgpack-1.0.5-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:36961b0568c36027c76e2ae3ca1132e35123dcec0706c4b7992683cc26c1320c"}, + {file = "msgpack-1.0.5-cp36-cp36m-win32.whl", hash = "sha256:b5ef2f015b95f912c2fcab19c36814963b5463f1fb9049846994b007962743e9"}, + {file = "msgpack-1.0.5-cp36-cp36m-win_amd64.whl", hash = "sha256:288e32b47e67f7b171f86b030e527e302c91bd3f40fd9033483f2cacc37f327a"}, + {file = "msgpack-1.0.5-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:137850656634abddfb88236008339fdaba3178f4751b28f270d2ebe77a563b6c"}, + {file = "msgpack-1.0.5-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0c05a4a96585525916b109bb85f8cb6511db1c6f5b9d9cbcbc940dc6b4be944b"}, + {file = "msgpack-1.0.5-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:56a62ec00b636583e5cb6ad313bbed36bb7ead5fa3a3e38938503142c72cba4f"}, + {file = "msgpack-1.0.5-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ef8108f8dedf204bb7b42994abf93882da1159728a2d4c5e82012edd92c9da9f"}, + {file = "msgpack-1.0.5-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:1835c84d65f46900920b3708f5ba829fb19b1096c1800ad60bae8418652a951d"}, + {file = "msgpack-1.0.5-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:e57916ef1bd0fee4f21c4600e9d1da352d8816b52a599c46460e93a6e9f17086"}, + {file = "msgpack-1.0.5-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:17358523b85973e5f242ad74aa4712b7ee560715562554aa2134d96e7aa4cbbf"}, + {file = "msgpack-1.0.5-cp37-cp37m-win32.whl", hash = "sha256:cb5aaa8c17760909ec6cb15e744c3ebc2ca8918e727216e79607b7bbce9c8f77"}, + {file = "msgpack-1.0.5-cp37-cp37m-win_amd64.whl", hash = "sha256:ab31e908d8424d55601ad7075e471b7d0140d4d3dd3272daf39c5c19d936bd82"}, + {file = "msgpack-1.0.5-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:b72d0698f86e8d9ddf9442bdedec15b71df3598199ba33322d9711a19f08145c"}, + {file = "msgpack-1.0.5-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:379026812e49258016dd84ad79ac8446922234d498058ae1d415f04b522d5b2d"}, + {file = "msgpack-1.0.5-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:332360ff25469c346a1c5e47cbe2a725517919892eda5cfaffe6046656f0b7bb"}, + {file = "msgpack-1.0.5-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:476a8fe8fae289fdf273d6d2a6cb6e35b5a58541693e8f9f019bfe990a51e4ba"}, + {file = "msgpack-1.0.5-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a9985b214f33311df47e274eb788a5893a761d025e2b92c723ba4c63936b69b1"}, + {file = "msgpack-1.0.5-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:48296af57cdb1d885843afd73c4656be5c76c0c6328db3440c9601a98f303d87"}, + {file = "msgpack-1.0.5-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:addab7e2e1fcc04bd08e4eb631c2a90960c340e40dfc4a5e24d2ff0d5a3b3edb"}, + {file = "msgpack-1.0.5-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:916723458c25dfb77ff07f4c66aed34e47503b2eb3188b3adbec8d8aa6e00f48"}, + {file = "msgpack-1.0.5-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:821c7e677cc6acf0fd3f7ac664c98803827ae6de594a9f99563e48c5a2f27eb0"}, + {file = "msgpack-1.0.5-cp38-cp38-win32.whl", hash = "sha256:1c0f7c47f0087ffda62961d425e4407961a7ffd2aa004c81b9c07d9269512f6e"}, + {file = "msgpack-1.0.5-cp38-cp38-win_amd64.whl", hash = "sha256:bae7de2026cbfe3782c8b78b0db9cbfc5455e079f1937cb0ab8d133496ac55e1"}, + {file = "msgpack-1.0.5-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:20c784e66b613c7f16f632e7b5e8a1651aa5702463d61394671ba07b2fc9e025"}, + {file = "msgpack-1.0.5-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:266fa4202c0eb94d26822d9bfd7af25d1e2c088927fe8de9033d929dd5ba24c5"}, + {file = "msgpack-1.0.5-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:18334484eafc2b1aa47a6d42427da7fa8f2ab3d60b674120bce7a895a0a85bdd"}, + {file = "msgpack-1.0.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:57e1f3528bd95cc44684beda696f74d3aaa8a5e58c816214b9046512240ef437"}, + {file = "msgpack-1.0.5-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:586d0d636f9a628ddc6a17bfd45aa5b5efaf1606d2b60fa5d87b8986326e933f"}, + {file = "msgpack-1.0.5-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a740fa0e4087a734455f0fc3abf5e746004c9da72fbd541e9b113013c8dc3282"}, + {file = "msgpack-1.0.5-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:3055b0455e45810820db1f29d900bf39466df96ddca11dfa6d074fa47054376d"}, + {file = "msgpack-1.0.5-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:a61215eac016f391129a013c9e46f3ab308db5f5ec9f25811e811f96962599a8"}, + {file = "msgpack-1.0.5-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:362d9655cd369b08fda06b6657a303eb7172d5279997abe094512e919cf74b11"}, + {file = "msgpack-1.0.5-cp39-cp39-win32.whl", hash = "sha256:ac9dd47af78cae935901a9a500104e2dea2e253207c924cc95de149606dc43cc"}, + {file = "msgpack-1.0.5-cp39-cp39-win_amd64.whl", hash = "sha256:06f5174b5f8ed0ed919da0e62cbd4ffde676a374aba4020034da05fab67b9164"}, + {file = "msgpack-1.0.5.tar.gz", hash = "sha256:c075544284eadc5cddc70f4757331d99dcbc16b2bbd4849d15f8aae4cf36d31c"}, +] + +[[package]] +name = "mypy-extensions" +version = "1.0.0" +description = "Type system extensions for programs checked with the mypy type checker." +category = "dev" +optional = false +python-versions = ">=3.5" +files = [ + {file = "mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d"}, + {file = "mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"}, +] + +[[package]] +name = "numba" +version = "0.56.4" +description = "compiling Python code using LLVM" +category = "main" +optional = false +python-versions = ">=3.7" +files = [ + {file = "numba-0.56.4-cp310-cp310-macosx_10_14_x86_64.whl", hash = "sha256:9f62672145f8669ec08762895fe85f4cf0ead08ce3164667f2b94b2f62ab23c3"}, + {file = "numba-0.56.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c602d015478b7958408d788ba00a50272649c5186ea8baa6cf71d4a1c761bba1"}, + {file = "numba-0.56.4-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:85dbaed7a05ff96492b69a8900c5ba605551afb9b27774f7f10511095451137c"}, + {file = "numba-0.56.4-cp310-cp310-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:f4cfc3a19d1e26448032049c79fc60331b104f694cf570a9e94f4e2c9d0932bb"}, + {file = "numba-0.56.4-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:4e08e203b163ace08bad500b0c16f6092b1eb34fd1fce4feaf31a67a3a5ecf3b"}, + {file = "numba-0.56.4-cp310-cp310-win32.whl", hash = "sha256:0611e6d3eebe4cb903f1a836ffdb2bda8d18482bcd0a0dcc56e79e2aa3fefef5"}, + {file = "numba-0.56.4-cp310-cp310-win_amd64.whl", hash = "sha256:fbfb45e7b297749029cb28694abf437a78695a100e7c2033983d69f0ba2698d4"}, + {file = "numba-0.56.4-cp37-cp37m-macosx_10_14_x86_64.whl", hash = "sha256:3cb1a07a082a61df80a468f232e452d818f5ae254b40c26390054e4e868556e0"}, + {file = "numba-0.56.4-cp37-cp37m-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d69ad934e13c15684e7887100a8f5f0f61d7a8e57e0fd29d9993210089a5b531"}, + {file = "numba-0.56.4-cp37-cp37m-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:dbcc847bac2d225265d054993a7f910fda66e73d6662fe7156452cac0325b073"}, + {file = "numba-0.56.4-cp37-cp37m-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8a95ca9cc77ea4571081f6594e08bd272b66060634b8324e99cd1843020364f9"}, + {file = "numba-0.56.4-cp37-cp37m-win32.whl", hash = "sha256:fcdf84ba3ed8124eb7234adfbb8792f311991cbf8aed1cad4b1b1a7ee08380c1"}, + {file = "numba-0.56.4-cp37-cp37m-win_amd64.whl", hash = "sha256:42f9e1be942b215df7e6cc9948cf9c15bb8170acc8286c063a9e57994ef82fd1"}, + {file = "numba-0.56.4-cp38-cp38-macosx_10_14_x86_64.whl", hash = "sha256:553da2ce74e8862e18a72a209ed3b6d2924403bdd0fb341fa891c6455545ba7c"}, + {file = "numba-0.56.4-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:4373da9757049db7c90591e9ec55a2e97b2b36ba7ae3bf9c956a513374077470"}, + {file = "numba-0.56.4-cp38-cp38-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:3a993349b90569518739009d8f4b523dfedd7e0049e6838c0e17435c3e70dcc4"}, + {file = "numba-0.56.4-cp38-cp38-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:720886b852a2d62619ae3900fe71f1852c62db4f287d0c275a60219e1643fc04"}, + {file = "numba-0.56.4-cp38-cp38-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:e64d338b504c9394a4a34942df4627e1e6cb07396ee3b49fe7b8d6420aa5104f"}, + {file = "numba-0.56.4-cp38-cp38-win32.whl", hash = "sha256:03fe94cd31e96185cce2fae005334a8cc712fc2ba7756e52dff8c9400718173f"}, + {file = "numba-0.56.4-cp38-cp38-win_amd64.whl", hash = "sha256:91f021145a8081f881996818474ef737800bcc613ffb1e618a655725a0f9e246"}, + {file = "numba-0.56.4-cp39-cp39-macosx_10_14_x86_64.whl", hash = "sha256:d0ae9270a7a5cc0ede63cd234b4ff1ce166c7a749b91dbbf45e0000c56d3eade"}, + {file = "numba-0.56.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c75e8a5f810ce80a0cfad6e74ee94f9fde9b40c81312949bf356b7304ef20740"}, + {file = "numba-0.56.4-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:a12ef323c0f2101529d455cfde7f4135eaa147bad17afe10b48634f796d96abd"}, + {file = "numba-0.56.4-cp39-cp39-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:03634579d10a6129181129de293dd6b5eaabee86881369d24d63f8fe352dd6cb"}, + {file = "numba-0.56.4-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0240f9026b015e336069329839208ebd70ec34ae5bfbf402e4fcc8e06197528e"}, + {file = "numba-0.56.4-cp39-cp39-win32.whl", hash = "sha256:14dbbabf6ffcd96ee2ac827389afa59a70ffa9f089576500434c34abf9b054a4"}, + {file = "numba-0.56.4-cp39-cp39-win_amd64.whl", hash = "sha256:0da583c532cd72feefd8e551435747e0e0fbb3c0530357e6845fcc11e38d6aea"}, + {file = "numba-0.56.4.tar.gz", hash = "sha256:32d9fef412c81483d7efe0ceb6cf4d3310fde8b624a9cecca00f790573ac96ee"}, +] + +[package.dependencies] +llvmlite = ">=0.39.0dev0,<0.40" +numpy = ">=1.18,<1.24" +setuptools = "*" + +[[package]] +name = "numpy" +version = "1.23.5" +description = "NumPy is the fundamental package for array computing with Python." +category = "main" +optional = false +python-versions = ">=3.8" +files = [ + {file = "numpy-1.23.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:9c88793f78fca17da0145455f0d7826bcb9f37da4764af27ac945488116efe63"}, + {file = "numpy-1.23.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e9f4c4e51567b616be64e05d517c79a8a22f3606499941d97bb76f2ca59f982d"}, + {file = "numpy-1.23.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7903ba8ab592b82014713c491f6c5d3a1cde5b4a3bf116404e08f5b52f6daf43"}, + {file = "numpy-1.23.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5e05b1c973a9f858c74367553e236f287e749465f773328c8ef31abe18f691e1"}, + {file = "numpy-1.23.5-cp310-cp310-win32.whl", hash = "sha256:522e26bbf6377e4d76403826ed689c295b0b238f46c28a7251ab94716da0b280"}, + {file = "numpy-1.23.5-cp310-cp310-win_amd64.whl", hash = "sha256:dbee87b469018961d1ad79b1a5d50c0ae850000b639bcb1b694e9981083243b6"}, + {file = "numpy-1.23.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ce571367b6dfe60af04e04a1834ca2dc5f46004ac1cc756fb95319f64c095a96"}, + {file = "numpy-1.23.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:56e454c7833e94ec9769fa0f86e6ff8e42ee38ce0ce1fa4cbb747ea7e06d56aa"}, + {file = "numpy-1.23.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5039f55555e1eab31124a5768898c9e22c25a65c1e0037f4d7c495a45778c9f2"}, + {file = "numpy-1.23.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:58f545efd1108e647604a1b5aa809591ccd2540f468a880bedb97247e72db387"}, + {file = "numpy-1.23.5-cp311-cp311-win32.whl", hash = "sha256:b2a9ab7c279c91974f756c84c365a669a887efa287365a8e2c418f8b3ba73fb0"}, + {file = "numpy-1.23.5-cp311-cp311-win_amd64.whl", hash = "sha256:0cbe9848fad08baf71de1a39e12d1b6310f1d5b2d0ea4de051058e6e1076852d"}, + {file = "numpy-1.23.5-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:f063b69b090c9d918f9df0a12116029e274daf0181df392839661c4c7ec9018a"}, + {file = "numpy-1.23.5-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:0aaee12d8883552fadfc41e96b4c82ee7d794949e2a7c3b3a7201e968c7ecab9"}, + {file = "numpy-1.23.5-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:92c8c1e89a1f5028a4c6d9e3ccbe311b6ba53694811269b992c0b224269e2398"}, + {file = "numpy-1.23.5-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d208a0f8729f3fb790ed18a003f3a57895b989b40ea4dce4717e9cf4af62c6bb"}, + {file = "numpy-1.23.5-cp38-cp38-win32.whl", hash = "sha256:06005a2ef6014e9956c09ba07654f9837d9e26696a0470e42beedadb78c11b07"}, + {file = "numpy-1.23.5-cp38-cp38-win_amd64.whl", hash = "sha256:ca51fcfcc5f9354c45f400059e88bc09215fb71a48d3768fb80e357f3b457e1e"}, + {file = "numpy-1.23.5-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:8969bfd28e85c81f3f94eb4a66bc2cf1dbdc5c18efc320af34bffc54d6b1e38f"}, + {file = "numpy-1.23.5-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a7ac231a08bb37f852849bbb387a20a57574a97cfc7b6cabb488a4fc8be176de"}, + {file = "numpy-1.23.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bf837dc63ba5c06dc8797c398db1e223a466c7ece27a1f7b5232ba3466aafe3d"}, + {file = "numpy-1.23.5-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:33161613d2269025873025b33e879825ec7b1d831317e68f4f2f0f84ed14c719"}, + {file = "numpy-1.23.5-cp39-cp39-win32.whl", hash = "sha256:af1da88f6bc3d2338ebbf0e22fe487821ea4d8e89053e25fa59d1d79786e7481"}, + {file = "numpy-1.23.5-cp39-cp39-win_amd64.whl", hash = "sha256:09b7847f7e83ca37c6e627682f145856de331049013853f344f37b0c9690e3df"}, + {file = "numpy-1.23.5-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:abdde9f795cf292fb9651ed48185503a2ff29be87770c3b8e2a14b0cd7aa16f8"}, + {file = "numpy-1.23.5-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f9a909a8bae284d46bbfdefbdd4a262ba19d3bc9921b1e76126b1d21c3c34135"}, + {file = "numpy-1.23.5-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:01dd17cbb340bf0fc23981e52e1d18a9d4050792e8fb8363cecbf066a84b827d"}, + {file = "numpy-1.23.5.tar.gz", hash = "sha256:1b1766d6f397c18153d40015ddfc79ddb715cabadc04d2d228d4e5a8bc4ded1a"}, +] + +[[package]] +name = "oauthlib" +version = "3.2.2" +description = "A generic, spec-compliant, thorough implementation of the OAuth request-signing logic" +category = "main" +optional = false +python-versions = ">=3.6" +files = [ + {file = "oauthlib-3.2.2-py3-none-any.whl", hash = "sha256:8139f29aac13e25d502680e9e19963e83f16838d48a0d71c287fe40e7067fbca"}, + {file = "oauthlib-3.2.2.tar.gz", hash = "sha256:9859c40929662bec5d64f34d01c99e093149682a3f38915dc0655d5a633dd918"}, +] + +[package.extras] +rsa = ["cryptography (>=3.0.0)"] +signals = ["blinker (>=1.4.0)"] +signedtoken = ["cryptography (>=3.0.0)", "pyjwt (>=2.0.0,<3)"] + +[[package]] +name = "opt-einsum" +version = "3.3.0" +description = "Optimizing numpys einsum function" +category = "main" +optional = false +python-versions = ">=3.5" +files = [ + {file = "opt_einsum-3.3.0-py3-none-any.whl", hash = "sha256:2455e59e3947d3c275477df7f5205b30635e266fe6dc300e3d9f9646bfcea147"}, + {file = "opt_einsum-3.3.0.tar.gz", hash = "sha256:59f6475f77bbc37dcf7cd748519c0ec60722e91e63ca114e68821c0c54a46549"}, +] + +[package.dependencies] +numpy = ">=1.7" + +[package.extras] +docs = ["numpydoc", "sphinx (==1.2.3)", "sphinx-rtd-theme", "sphinxcontrib-napoleon"] +tests = ["pytest", "pytest-cov", "pytest-pep8"] + +[[package]] +name = "packaging" +version = "23.1" +description = "Core utilities for Python packages" +category = "main" +optional = false +python-versions = ">=3.7" +files = [ + {file = "packaging-23.1-py3-none-any.whl", hash = "sha256:994793af429502c4ea2ebf6bf664629d07c1a9fe974af92966e4b8d2df7edc61"}, + {file = "packaging-23.1.tar.gz", hash = "sha256:a392980d2b6cffa644431898be54b0045151319d1e7ec34f0cfed48767dd334f"}, +] + +[[package]] +name = "pandas" +version = "2.0.3" +description = "Powerful data structures for data analysis, time series, and statistics" +category = "main" +optional = true +python-versions = ">=3.8" +files = [ + {file = "pandas-2.0.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e4c7c9f27a4185304c7caf96dc7d91bc60bc162221152de697c98eb0b2648dd8"}, + {file = "pandas-2.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f167beed68918d62bffb6ec64f2e1d8a7d297a038f86d4aed056b9493fca407f"}, + {file = "pandas-2.0.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ce0c6f76a0f1ba361551f3e6dceaff06bde7514a374aa43e33b588ec10420183"}, + {file = "pandas-2.0.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba619e410a21d8c387a1ea6e8a0e49bb42216474436245718d7f2e88a2f8d7c0"}, + {file = "pandas-2.0.3-cp310-cp310-win32.whl", hash = "sha256:3ef285093b4fe5058eefd756100a367f27029913760773c8bf1d2d8bebe5d210"}, + {file = "pandas-2.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:9ee1a69328d5c36c98d8e74db06f4ad518a1840e8ccb94a4ba86920986bb617e"}, + {file = "pandas-2.0.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b084b91d8d66ab19f5bb3256cbd5ea661848338301940e17f4492b2ce0801fe8"}, + {file = "pandas-2.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:37673e3bdf1551b95bf5d4ce372b37770f9529743d2498032439371fc7b7eb26"}, + {file = "pandas-2.0.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b9cb1e14fdb546396b7e1b923ffaeeac24e4cedd14266c3497216dd4448e4f2d"}, + {file = "pandas-2.0.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d9cd88488cceb7635aebb84809d087468eb33551097d600c6dad13602029c2df"}, + {file = "pandas-2.0.3-cp311-cp311-win32.whl", hash = "sha256:694888a81198786f0e164ee3a581df7d505024fbb1f15202fc7db88a71d84ebd"}, + {file = "pandas-2.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:6a21ab5c89dcbd57f78d0ae16630b090eec626360085a4148693def5452d8a6b"}, + {file = "pandas-2.0.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:9e4da0d45e7f34c069fe4d522359df7d23badf83abc1d1cef398895822d11061"}, + {file = "pandas-2.0.3-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:32fca2ee1b0d93dd71d979726b12b61faa06aeb93cf77468776287f41ff8fdc5"}, + {file = "pandas-2.0.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:258d3624b3ae734490e4d63c430256e716f488c4fcb7c8e9bde2d3aa46c29089"}, + {file = "pandas-2.0.3-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9eae3dc34fa1aa7772dd3fc60270d13ced7346fcbcfee017d3132ec625e23bb0"}, + {file = "pandas-2.0.3-cp38-cp38-win32.whl", hash = "sha256:f3421a7afb1a43f7e38e82e844e2bca9a6d793d66c1a7f9f0ff39a795bbc5e02"}, + {file = "pandas-2.0.3-cp38-cp38-win_amd64.whl", hash = "sha256:69d7f3884c95da3a31ef82b7618af5710dba95bb885ffab339aad925c3e8ce78"}, + {file = "pandas-2.0.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:5247fb1ba347c1261cbbf0fcfba4a3121fbb4029d95d9ef4dc45406620b25c8b"}, + {file = "pandas-2.0.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:81af086f4543c9d8bb128328b5d32e9986e0c84d3ee673a2ac6fb57fd14f755e"}, + {file = "pandas-2.0.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1994c789bf12a7c5098277fb43836ce090f1073858c10f9220998ac74f37c69b"}, + {file = "pandas-2.0.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5ec591c48e29226bcbb316e0c1e9423622bc7a4eaf1ef7c3c9fa1a3981f89641"}, + {file = "pandas-2.0.3-cp39-cp39-win32.whl", hash = "sha256:04dbdbaf2e4d46ca8da896e1805bc04eb85caa9a82e259e8eed00254d5e0c682"}, + {file = "pandas-2.0.3-cp39-cp39-win_amd64.whl", hash = "sha256:1168574b036cd8b93abc746171c9b4f1b83467438a5e45909fed645cf8692dbc"}, + {file = "pandas-2.0.3.tar.gz", hash = "sha256:c02f372a88e0d17f36d3093a644c73cfc1788e876a7c4bcb4020a77512e2043c"}, +] + +[package.dependencies] +numpy = [ + {version = ">=1.20.3", markers = "python_version < \"3.10\""}, + {version = ">=1.21.0", markers = "python_version >= \"3.10\""}, +] +python-dateutil = ">=2.8.2" +pytz = ">=2020.1" +tzdata = ">=2022.1" + +[package.extras] +all = ["PyQt5 (>=5.15.1)", "SQLAlchemy (>=1.4.16)", "beautifulsoup4 (>=4.9.3)", "bottleneck (>=1.3.2)", "brotlipy (>=0.7.0)", "fastparquet (>=0.6.3)", "fsspec (>=2021.07.0)", "gcsfs (>=2021.07.0)", "html5lib (>=1.1)", "hypothesis (>=6.34.2)", "jinja2 (>=3.0.0)", "lxml (>=4.6.3)", "matplotlib (>=3.6.1)", "numba (>=0.53.1)", "numexpr (>=2.7.3)", "odfpy (>=1.4.1)", "openpyxl (>=3.0.7)", "pandas-gbq (>=0.15.0)", "psycopg2 (>=2.8.6)", "pyarrow (>=7.0.0)", "pymysql (>=1.0.2)", "pyreadstat (>=1.1.2)", "pytest (>=7.3.2)", "pytest-asyncio (>=0.17.0)", "pytest-xdist (>=2.2.0)", "python-snappy (>=0.6.0)", "pyxlsb (>=1.0.8)", "qtpy (>=2.2.0)", "s3fs (>=2021.08.0)", "scipy (>=1.7.1)", "tables (>=3.6.1)", "tabulate (>=0.8.9)", "xarray (>=0.21.0)", "xlrd (>=2.0.1)", "xlsxwriter (>=1.4.3)", "zstandard (>=0.15.2)"] +aws = ["s3fs (>=2021.08.0)"] +clipboard = ["PyQt5 (>=5.15.1)", "qtpy (>=2.2.0)"] +compression = ["brotlipy (>=0.7.0)", "python-snappy (>=0.6.0)", "zstandard (>=0.15.2)"] +computation = ["scipy (>=1.7.1)", "xarray (>=0.21.0)"] +excel = ["odfpy (>=1.4.1)", "openpyxl (>=3.0.7)", "pyxlsb (>=1.0.8)", "xlrd (>=2.0.1)", "xlsxwriter (>=1.4.3)"] +feather = ["pyarrow (>=7.0.0)"] +fss = ["fsspec (>=2021.07.0)"] +gcp = ["gcsfs (>=2021.07.0)", "pandas-gbq (>=0.15.0)"] +hdf5 = ["tables (>=3.6.1)"] +html = ["beautifulsoup4 (>=4.9.3)", "html5lib (>=1.1)", "lxml (>=4.6.3)"] +mysql = ["SQLAlchemy (>=1.4.16)", "pymysql (>=1.0.2)"] +output-formatting = ["jinja2 (>=3.0.0)", "tabulate (>=0.8.9)"] +parquet = ["pyarrow (>=7.0.0)"] +performance = ["bottleneck (>=1.3.2)", "numba (>=0.53.1)", "numexpr (>=2.7.1)"] +plot = ["matplotlib (>=3.6.1)"] +postgresql = ["SQLAlchemy (>=1.4.16)", "psycopg2 (>=2.8.6)"] +spss = ["pyreadstat (>=1.1.2)"] +sql-other = ["SQLAlchemy (>=1.4.16)"] +test = ["hypothesis (>=6.34.2)", "pytest (>=7.3.2)", "pytest-asyncio (>=0.17.0)", "pytest-xdist (>=2.2.0)"] +xml = ["lxml (>=4.6.3)"] + +[[package]] +name = "partd" +version = "1.4.0" +description = "Appendable key-value storage" +category = "main" +optional = false +python-versions = ">=3.7" +files = [ + {file = "partd-1.4.0-py3-none-any.whl", hash = "sha256:7a63529348cf0dff14b986db641cd1b83c16b5cb9fc647c2851779db03282ef8"}, + {file = "partd-1.4.0.tar.gz", hash = "sha256:aa0ff35dbbcc807ae374db56332f4c1b39b46f67bf2975f5151e0b4186aed0d5"}, +] + +[package.dependencies] +locket = "*" +toolz = "*" + +[package.extras] +complete = ["blosc", "numpy (>=1.9.0)", "pandas (>=0.19.0)", "pyzmq"] + +[[package]] +name = "pathspec" +version = "0.11.1" +description = "Utility library for gitignore style pattern matching of file paths." +category = "dev" +optional = false +python-versions = ">=3.7" +files = [ + {file = "pathspec-0.11.1-py3-none-any.whl", hash = "sha256:d8af70af76652554bd134c22b3e8a1cc46ed7d91edcdd721ef1a0c51a84a5293"}, + {file = "pathspec-0.11.1.tar.gz", hash = "sha256:2798de800fa92780e33acca925945e9a19a133b715067cf165b8866c15a31687"}, +] + +[[package]] +name = "pillow" +version = "10.0.0" +description = "Python Imaging Library (Fork)" +category = "main" +optional = false +python-versions = ">=3.8" +files = [ + {file = "Pillow-10.0.0-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:1f62406a884ae75fb2f818694469519fb685cc7eaff05d3451a9ebe55c646891"}, + {file = "Pillow-10.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d5db32e2a6ccbb3d34d87c87b432959e0db29755727afb37290e10f6e8e62614"}, + {file = "Pillow-10.0.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:edf4392b77bdc81f36e92d3a07a5cd072f90253197f4a52a55a8cec48a12483b"}, + {file = "Pillow-10.0.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:520f2a520dc040512699f20fa1c363eed506e94248d71f85412b625026f6142c"}, + {file = "Pillow-10.0.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:8c11160913e3dd06c8ffdb5f233a4f254cb449f4dfc0f8f4549eda9e542c93d1"}, + {file = "Pillow-10.0.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:a74ba0c356aaa3bb8e3eb79606a87669e7ec6444be352870623025d75a14a2bf"}, + {file = "Pillow-10.0.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:d5d0dae4cfd56969d23d94dc8e89fb6a217be461c69090768227beb8ed28c0a3"}, + {file = "Pillow-10.0.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:22c10cc517668d44b211717fd9775799ccec4124b9a7f7b3635fc5386e584992"}, + {file = "Pillow-10.0.0-cp310-cp310-win_amd64.whl", hash = "sha256:dffe31a7f47b603318c609f378ebcd57f1554a3a6a8effbc59c3c69f804296de"}, + {file = "Pillow-10.0.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:9fb218c8a12e51d7ead2a7c9e101a04982237d4855716af2e9499306728fb485"}, + {file = "Pillow-10.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d35e3c8d9b1268cbf5d3670285feb3528f6680420eafe35cccc686b73c1e330f"}, + {file = "Pillow-10.0.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3ed64f9ca2f0a95411e88a4efbd7a29e5ce2cea36072c53dd9d26d9c76f753b3"}, + {file = "Pillow-10.0.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0b6eb5502f45a60a3f411c63187db83a3d3107887ad0d036c13ce836f8a36f1d"}, + {file = "Pillow-10.0.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:c1fbe7621c167ecaa38ad29643d77a9ce7311583761abf7836e1510c580bf3dd"}, + {file = "Pillow-10.0.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:cd25d2a9d2b36fcb318882481367956d2cf91329f6892fe5d385c346c0649629"}, + {file = "Pillow-10.0.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:3b08d4cc24f471b2c8ca24ec060abf4bebc6b144cb89cba638c720546b1cf538"}, + {file = "Pillow-10.0.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:d737a602fbd82afd892ca746392401b634e278cb65d55c4b7a8f48e9ef8d008d"}, + {file = "Pillow-10.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:3a82c40d706d9aa9734289740ce26460a11aeec2d9c79b7af87bb35f0073c12f"}, + {file = "Pillow-10.0.0-cp311-cp311-win_arm64.whl", hash = "sha256:bc2ec7c7b5d66b8ec9ce9f720dbb5fa4bace0f545acd34870eff4a369b44bf37"}, + {file = "Pillow-10.0.0-cp312-cp312-macosx_10_10_x86_64.whl", hash = "sha256:d80cf684b541685fccdd84c485b31ce73fc5c9b5d7523bf1394ce134a60c6883"}, + {file = "Pillow-10.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:76de421f9c326da8f43d690110f0e79fe3ad1e54be811545d7d91898b4c8493e"}, + {file = "Pillow-10.0.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:81ff539a12457809666fef6624684c008e00ff6bf455b4b89fd00a140eecd640"}, + {file = "Pillow-10.0.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ce543ed15570eedbb85df19b0a1a7314a9c8141a36ce089c0a894adbfccb4568"}, + {file = "Pillow-10.0.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:685ac03cc4ed5ebc15ad5c23bc555d68a87777586d970c2c3e216619a5476223"}, + {file = "Pillow-10.0.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:d72e2ecc68a942e8cf9739619b7f408cc7b272b279b56b2c83c6123fcfa5cdff"}, + {file = "Pillow-10.0.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:d50b6aec14bc737742ca96e85d6d0a5f9bfbded018264b3b70ff9d8c33485551"}, + {file = "Pillow-10.0.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:00e65f5e822decd501e374b0650146063fbb30a7264b4d2744bdd7b913e0cab5"}, + {file = "Pillow-10.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:f31f9fdbfecb042d046f9d91270a0ba28368a723302786c0009ee9b9f1f60199"}, + {file = "Pillow-10.0.0-cp312-cp312-win_arm64.whl", hash = "sha256:1ce91b6ec08d866b14413d3f0bbdea7e24dfdc8e59f562bb77bc3fe60b6144ca"}, + {file = "Pillow-10.0.0-cp38-cp38-macosx_10_10_x86_64.whl", hash = "sha256:349930d6e9c685c089284b013478d6f76e3a534e36ddfa912cde493f235372f3"}, + {file = "Pillow-10.0.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:3a684105f7c32488f7153905a4e3015a3b6c7182e106fe3c37fbb5ef3e6994c3"}, + {file = "Pillow-10.0.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b4f69b3700201b80bb82c3a97d5e9254084f6dd5fb5b16fc1a7b974260f89f43"}, + {file = "Pillow-10.0.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3f07ea8d2f827d7d2a49ecf1639ec02d75ffd1b88dcc5b3a61bbb37a8759ad8d"}, + {file = "Pillow-10.0.0-cp38-cp38-manylinux_2_28_aarch64.whl", hash = "sha256:040586f7d37b34547153fa383f7f9aed68b738992380ac911447bb78f2abe530"}, + {file = "Pillow-10.0.0-cp38-cp38-manylinux_2_28_x86_64.whl", hash = "sha256:f88a0b92277de8e3ca715a0d79d68dc82807457dae3ab8699c758f07c20b3c51"}, + {file = "Pillow-10.0.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:c7cf14a27b0d6adfaebb3ae4153f1e516df54e47e42dcc073d7b3d76111a8d86"}, + {file = "Pillow-10.0.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:3400aae60685b06bb96f99a21e1ada7bc7a413d5f49bce739828ecd9391bb8f7"}, + {file = "Pillow-10.0.0-cp38-cp38-win_amd64.whl", hash = "sha256:dbc02381779d412145331789b40cc7b11fdf449e5d94f6bc0b080db0a56ea3f0"}, + {file = "Pillow-10.0.0-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:9211e7ad69d7c9401cfc0e23d49b69ca65ddd898976d660a2fa5904e3d7a9baa"}, + {file = "Pillow-10.0.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:faaf07ea35355b01a35cb442dd950d8f1bb5b040a7787791a535de13db15ed90"}, + {file = "Pillow-10.0.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c9f72a021fbb792ce98306ffb0c348b3c9cb967dce0f12a49aa4c3d3fdefa967"}, + {file = "Pillow-10.0.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9f7c16705f44e0504a3a2a14197c1f0b32a95731d251777dcb060aa83022cb2d"}, + {file = "Pillow-10.0.0-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:76edb0a1fa2b4745fb0c99fb9fb98f8b180a1bbceb8be49b087e0b21867e77d3"}, + {file = "Pillow-10.0.0-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:368ab3dfb5f49e312231b6f27b8820c823652b7cd29cfbd34090565a015e99ba"}, + {file = "Pillow-10.0.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:608bfdee0d57cf297d32bcbb3c728dc1da0907519d1784962c5f0c68bb93e5a3"}, + {file = "Pillow-10.0.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5c6e3df6bdd396749bafd45314871b3d0af81ff935b2d188385e970052091017"}, + {file = "Pillow-10.0.0-cp39-cp39-win_amd64.whl", hash = "sha256:7be600823e4c8631b74e4a0d38384c73f680e6105a7d3c6824fcf226c178c7e6"}, + {file = "Pillow-10.0.0-pp310-pypy310_pp73-macosx_10_10_x86_64.whl", hash = "sha256:92be919bbc9f7d09f7ae343c38f5bb21c973d2576c1d45600fce4b74bafa7ac0"}, + {file = "Pillow-10.0.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f8182b523b2289f7c415f589118228d30ac8c355baa2f3194ced084dac2dbba"}, + {file = "Pillow-10.0.0-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:38250a349b6b390ee6047a62c086d3817ac69022c127f8a5dc058c31ccef17f3"}, + {file = "Pillow-10.0.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:88af2003543cc40c80f6fca01411892ec52b11021b3dc22ec3bc9d5afd1c5334"}, + {file = "Pillow-10.0.0-pp39-pypy39_pp73-macosx_10_10_x86_64.whl", hash = "sha256:c189af0545965fa8d3b9613cfdb0cd37f9d71349e0f7750e1fd704648d475ed2"}, + {file = "Pillow-10.0.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ce7b031a6fc11365970e6a5686d7ba8c63e4c1cf1ea143811acbb524295eabed"}, + {file = "Pillow-10.0.0-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:db24668940f82321e746773a4bc617bfac06ec831e5c88b643f91f122a785684"}, + {file = "Pillow-10.0.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:efe8c0681042536e0d06c11f48cebe759707c9e9abf880ee213541c5b46c5bf3"}, + {file = "Pillow-10.0.0.tar.gz", hash = "sha256:9c82b5b3e043c7af0d95792d0d20ccf68f61a1fec6b3530e718b688422727396"}, +] + +[package.extras] +docs = ["furo", "olefile", "sphinx (>=2.4)", "sphinx-copybutton", "sphinx-inline-tabs", "sphinx-removed-in", "sphinxext-opengraph"] +tests = ["check-manifest", "coverage", "defusedxml", "markdown2", "olefile", "packaging", "pyroma", "pytest", "pytest-cov", "pytest-timeout"] + +[[package]] +name = "platformdirs" +version = "3.8.1" +description = "A small Python package for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." +category = "dev" +optional = false +python-versions = ">=3.7" +files = [ + {file = "platformdirs-3.8.1-py3-none-any.whl", hash = "sha256:cec7b889196b9144d088e4c57d9ceef7374f6c39694ad1577a0aab50d27ea28c"}, + {file = "platformdirs-3.8.1.tar.gz", hash = "sha256:f87ca4fcff7d2b0f81c6a748a77973d7af0f4d526f98f308477c3c436c74d528"}, +] + +[package.extras] +docs = ["furo (>=2023.5.20)", "proselint (>=0.13)", "sphinx (>=7.0.1)", "sphinx-autodoc-typehints (>=1.23,!=1.23.4)"] +test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=7.3.1)", "pytest-cov (>=4.1)", "pytest-mock (>=3.10)"] + +[[package]] +name = "pluggy" +version = "1.2.0" +description = "plugin and hook calling mechanisms for python" +category = "dev" +optional = false +python-versions = ">=3.7" +files = [ + {file = "pluggy-1.2.0-py3-none-any.whl", hash = "sha256:c2fd55a7d7a3863cba1a013e4e2414658b1d07b6bc57b3919e0c63c9abb99849"}, + {file = "pluggy-1.2.0.tar.gz", hash = "sha256:d12f0c4b579b15f5e054301bb226ee85eeeba08ffec228092f8defbaa3a4c4b3"}, +] + +[package.extras] +dev = ["pre-commit", "tox"] +testing = ["pytest", "pytest-benchmark"] + +[[package]] +name = "protobuf" +version = "3.19.6" +description = "Protocol Buffers" +category = "main" +optional = false +python-versions = ">=3.5" +files = [ + {file = "protobuf-3.19.6-cp310-cp310-manylinux2014_aarch64.whl", hash = "sha256:010be24d5a44be7b0613750ab40bc8b8cedc796db468eae6c779b395f50d1fa1"}, + {file = "protobuf-3.19.6-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:11478547958c2dfea921920617eb457bc26867b0d1aa065ab05f35080c5d9eb6"}, + {file = "protobuf-3.19.6-cp310-cp310-win32.whl", hash = "sha256:559670e006e3173308c9254d63facb2c03865818f22204037ab76f7a0ff70b5f"}, + {file = "protobuf-3.19.6-cp310-cp310-win_amd64.whl", hash = "sha256:347b393d4dd06fb93a77620781e11c058b3b0a5289262f094379ada2920a3730"}, + {file = "protobuf-3.19.6-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:a8ce5ae0de28b51dff886fb922012dad885e66176663950cb2344c0439ecb473"}, + {file = "protobuf-3.19.6-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:90b0d02163c4e67279ddb6dc25e063db0130fc299aefabb5d481053509fae5c8"}, + {file = "protobuf-3.19.6-cp36-cp36m-win32.whl", hash = "sha256:30f5370d50295b246eaa0296533403961f7e64b03ea12265d6dfce3a391d8992"}, + {file = "protobuf-3.19.6-cp36-cp36m-win_amd64.whl", hash = "sha256:0c0714b025ec057b5a7600cb66ce7c693815f897cfda6d6efb58201c472e3437"}, + {file = "protobuf-3.19.6-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:5057c64052a1f1dd7d4450e9aac25af6bf36cfbfb3a1cd89d16393a036c49157"}, + {file = "protobuf-3.19.6-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:bb6776bd18f01ffe9920e78e03a8676530a5d6c5911934c6a1ac6eb78973ecb6"}, + {file = "protobuf-3.19.6-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:84a04134866861b11556a82dd91ea6daf1f4925746b992f277b84013a7cc1229"}, + {file = "protobuf-3.19.6-cp37-cp37m-win32.whl", hash = "sha256:4bc98de3cdccfb5cd769620d5785b92c662b6bfad03a202b83799b6ed3fa1fa7"}, + {file = "protobuf-3.19.6-cp37-cp37m-win_amd64.whl", hash = "sha256:aa3b82ca1f24ab5326dcf4ea00fcbda703e986b22f3d27541654f749564d778b"}, + {file = "protobuf-3.19.6-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:2b2d2913bcda0e0ec9a784d194bc490f5dc3d9d71d322d070b11a0ade32ff6ba"}, + {file = "protobuf-3.19.6-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:d0b635cefebd7a8a0f92020562dead912f81f401af7e71f16bf9506ff3bdbb38"}, + {file = "protobuf-3.19.6-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7a552af4dc34793803f4e735aabe97ffc45962dfd3a237bdde242bff5a3de684"}, + {file = "protobuf-3.19.6-cp38-cp38-win32.whl", hash = "sha256:0469bc66160180165e4e29de7f445e57a34ab68f49357392c5b2f54c656ab25e"}, + {file = "protobuf-3.19.6-cp38-cp38-win_amd64.whl", hash = "sha256:91d5f1e139ff92c37e0ff07f391101df77e55ebb97f46bbc1535298d72019462"}, + {file = "protobuf-3.19.6-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:c0ccd3f940fe7f3b35a261b1dd1b4fc850c8fde9f74207015431f174be5976b3"}, + {file = "protobuf-3.19.6-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:30a15015d86b9c3b8d6bf78d5b8c7749f2512c29f168ca259c9d7727604d0e39"}, + {file = "protobuf-3.19.6-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:878b4cd080a21ddda6ac6d1e163403ec6eea2e206cf225982ae04567d39be7b0"}, + {file = "protobuf-3.19.6-cp39-cp39-win32.whl", hash = "sha256:5a0d7539a1b1fb7e76bf5faa0b44b30f812758e989e59c40f77a7dab320e79b9"}, + {file = "protobuf-3.19.6-cp39-cp39-win_amd64.whl", hash = "sha256:bbf5cea5048272e1c60d235c7bd12ce1b14b8a16e76917f371c718bd3005f045"}, + {file = "protobuf-3.19.6-py2.py3-none-any.whl", hash = "sha256:14082457dc02be946f60b15aad35e9f5c69e738f80ebbc0900a19bc83734a5a4"}, + {file = "protobuf-3.19.6.tar.gz", hash = "sha256:5f5540d57a43042389e87661c6eaa50f47c19c6176e8cf1c4f287aeefeccb5c4"}, +] + +[[package]] +name = "py" +version = "1.11.0" +description = "library with cross-python path, ini-parsing, io, code, log facilities" +category = "dev" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +files = [ + {file = "py-1.11.0-py2.py3-none-any.whl", hash = "sha256:607c53218732647dff4acdfcd50cb62615cedf612e72d1724fb1a0cc6405b378"}, + {file = "py-1.11.0.tar.gz", hash = "sha256:51c75c4126074b472f746a24399ad32f6053d1b34b68d2fa41e558e6f4a98719"}, +] + +[[package]] +name = "pyaml" +version = "23.7.0" +description = "PyYAML-based module to produce a bit more pretty and readable YAML-serialized data" +category = "main" +optional = true +python-versions = ">=3.8" +files = [ + {file = "pyaml-23.7.0-py3-none-any.whl", hash = "sha256:0a37018282545ccc31faecbe138fda4d89e236af04d691cfb5af00cd60089345"}, + {file = "pyaml-23.7.0.tar.gz", hash = "sha256:0c510bbb8938309400e0b1e47ac16fd90e56d652805a93417128786718f33546"}, +] + +[package.dependencies] +PyYAML = "*" + +[package.extras] +anchors = ["unidecode"] + +[[package]] +name = "pyarrow" +version = "12.0.1" +description = "Python library for Apache Arrow" +category = "main" +optional = true +python-versions = ">=3.7" +files = [ + {file = "pyarrow-12.0.1-cp310-cp310-macosx_10_14_x86_64.whl", hash = "sha256:6d288029a94a9bb5407ceebdd7110ba398a00412c5b0155ee9813a40d246c5df"}, + {file = "pyarrow-12.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:345e1828efdbd9aa4d4de7d5676778aba384a2c3add896d995b23d368e60e5af"}, + {file = "pyarrow-12.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8d6009fdf8986332b2169314da482baed47ac053311c8934ac6651e614deacd6"}, + {file = "pyarrow-12.0.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2d3c4cbbf81e6dd23fe921bc91dc4619ea3b79bc58ef10bce0f49bdafb103daf"}, + {file = "pyarrow-12.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:cdacf515ec276709ac8042c7d9bd5be83b4f5f39c6c037a17a60d7ebfd92c890"}, + {file = "pyarrow-12.0.1-cp311-cp311-macosx_10_14_x86_64.whl", hash = "sha256:749be7fd2ff260683f9cc739cb862fb11be376de965a2a8ccbf2693b098db6c7"}, + {file = "pyarrow-12.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:6895b5fb74289d055c43db3af0de6e16b07586c45763cb5e558d38b86a91e3a7"}, + {file = "pyarrow-12.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1887bdae17ec3b4c046fcf19951e71b6a619f39fa674f9881216173566c8f718"}, + {file = "pyarrow-12.0.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e2c9cb8eeabbadf5fcfc3d1ddea616c7ce893db2ce4dcef0ac13b099ad7ca082"}, + {file = "pyarrow-12.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:ce4aebdf412bd0eeb800d8e47db854f9f9f7e2f5a0220440acf219ddfddd4f63"}, + {file = "pyarrow-12.0.1-cp37-cp37m-macosx_10_14_x86_64.whl", hash = "sha256:e0d8730c7f6e893f6db5d5b86eda42c0a130842d101992b581e2138e4d5663d3"}, + {file = "pyarrow-12.0.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:43364daec02f69fec89d2315f7fbfbeec956e0d991cbbef471681bd77875c40f"}, + {file = "pyarrow-12.0.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:051f9f5ccf585f12d7de836e50965b3c235542cc896959320d9776ab93f3b33d"}, + {file = "pyarrow-12.0.1-cp37-cp37m-win_amd64.whl", hash = "sha256:be2757e9275875d2a9c6e6052ac7957fbbfc7bc7370e4a036a9b893e96fedaba"}, + {file = "pyarrow-12.0.1-cp38-cp38-macosx_10_14_x86_64.whl", hash = "sha256:cf812306d66f40f69e684300f7af5111c11f6e0d89d6b733e05a3de44961529d"}, + {file = "pyarrow-12.0.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:459a1c0ed2d68671188b2118c63bac91eaef6fc150c77ddd8a583e3c795737bf"}, + {file = "pyarrow-12.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:85e705e33eaf666bbe508a16fd5ba27ca061e177916b7a317ba5a51bee43384c"}, + {file = "pyarrow-12.0.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9120c3eb2b1f6f516a3b7a9714ed860882d9ef98c4b17edcdc91d95b7528db60"}, + {file = "pyarrow-12.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:c780f4dc40460015d80fcd6a6140de80b615349ed68ef9adb653fe351778c9b3"}, + {file = "pyarrow-12.0.1-cp39-cp39-macosx_10_14_x86_64.whl", hash = "sha256:a3c63124fc26bf5f95f508f5d04e1ece8cc23a8b0af2a1e6ab2b1ec3fdc91b24"}, + {file = "pyarrow-12.0.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:b13329f79fa4472324f8d32dc1b1216616d09bd1e77cfb13104dec5463632c36"}, + {file = "pyarrow-12.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bb656150d3d12ec1396f6dde542db1675a95c0cc8366d507347b0beed96e87ca"}, + {file = "pyarrow-12.0.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6251e38470da97a5b2e00de5c6a049149f7b2bd62f12fa5dbb9ac674119ba71a"}, + {file = "pyarrow-12.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:3de26da901216149ce086920547dfff5cd22818c9eab67ebc41e863a5883bac7"}, + {file = "pyarrow-12.0.1.tar.gz", hash = "sha256:cce317fc96e5b71107bf1f9f184d5e54e2bd14bbf3f9a3d62819961f0af86fec"}, +] + +[package.dependencies] +numpy = ">=1.16.6" + +[[package]] +name = "pyasn1" +version = "0.5.0" +description = "Pure-Python implementation of ASN.1 types and DER/BER/CER codecs (X.208)" +category = "main" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,>=2.7" +files = [ + {file = "pyasn1-0.5.0-py2.py3-none-any.whl", hash = "sha256:87a2121042a1ac9358cabcaf1d07680ff97ee6404333bacca15f76aa8ad01a57"}, + {file = "pyasn1-0.5.0.tar.gz", hash = "sha256:97b7290ca68e62a832558ec3976f15cbf911bf5d7c7039d8b861c2a0ece69fde"}, +] + +[[package]] +name = "pyasn1-modules" +version = "0.3.0" +description = "A collection of ASN.1-based protocols modules" +category = "main" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,>=2.7" +files = [ + {file = "pyasn1_modules-0.3.0-py2.py3-none-any.whl", hash = "sha256:d3ccd6ed470d9ffbc716be08bd90efbd44d0734bc9303818f7336070984a162d"}, + {file = "pyasn1_modules-0.3.0.tar.gz", hash = "sha256:5bd01446b736eb9d31512a30d46c1ac3395d676c6f3cafa4c03eb54b9925631c"}, +] + +[package.dependencies] +pyasn1 = ">=0.4.6,<0.6.0" + +[[package]] +name = "pybtex" +version = "0.24.0" +description = "A BibTeX-compatible bibliography processor in Python" +category = "dev" +optional = false +python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*" +files = [ + {file = "pybtex-0.24.0-py2.py3-none-any.whl", hash = "sha256:e1e0c8c69998452fea90e9179aa2a98ab103f3eed894405b7264e517cc2fcc0f"}, + {file = "pybtex-0.24.0.tar.gz", hash = "sha256:818eae35b61733e5c007c3fcd2cfb75ed1bc8b4173c1f70b56cc4c0802d34755"}, +] + +[package.dependencies] +latexcodec = ">=1.0.4" +PyYAML = ">=3.01" +six = "*" + +[package.extras] +test = ["pytest"] + +[[package]] +name = "pybtex-docutils" +version = "1.0.2" +description = "A docutils backend for pybtex." +category = "dev" +optional = false +python-versions = ">=3.6" +files = [ + {file = "pybtex-docutils-1.0.2.tar.gz", hash = "sha256:43aa353b6d498fd5ac30f0073a98e332d061d34fe619d3d50d1761f8fd4aa016"}, + {file = "pybtex_docutils-1.0.2-py3-none-any.whl", hash = "sha256:6f9e3c25a37bcaac8c4f69513272706ec6253bb708a93d8b4b173f43915ba239"}, +] + +[package.dependencies] +docutils = ">=0.8" +pybtex = ">=0.16" + +[[package]] +name = "pygments" +version = "2.15.1" +description = "Pygments is a syntax highlighting package written in Python." +category = "main" +optional = false +python-versions = ">=3.7" +files = [ + {file = "Pygments-2.15.1-py3-none-any.whl", hash = "sha256:db2db3deb4b4179f399a09054b023b6a586b76499d36965813c71aa8ed7b5fd1"}, + {file = "Pygments-2.15.1.tar.gz", hash = "sha256:8ace4d3c1dd481894b2005f560ead0f9f19ee64fe983366be1a21e171d12775c"}, +] + +[package.extras] +plugins = ["importlib-metadata"] + +[[package]] +name = "pylint" +version = "2.10.0" +description = "python code static checker" +category = "dev" +optional = false +python-versions = "~=3.6" +files = [ + {file = "pylint-2.10.0-py3-none-any.whl", hash = "sha256:2d01c6de5ea20443e3f7ed8ae285f75b2d4da92e840f10118ddb7da18a1e09df"}, + {file = "pylint-2.10.0.tar.gz", hash = "sha256:dcf4a5dd7bc98c68790323f783d792423c1946e7a4a195e44d7b2c2d386f457d"}, +] + +[package.dependencies] +appdirs = ">=1.4.0" +astroid = ">=2.7.2,<2.8" +colorama = {version = "*", markers = "sys_platform == \"win32\""} +isort = ">=4.2.5,<6" +mccabe = ">=0.6,<0.7" +toml = ">=0.7.1" + +[[package]] +name = "pyparsing" +version = "3.1.0" +description = "pyparsing module - Classes and methods to define and execute parsing grammars" +category = "main" +optional = false +python-versions = ">=3.6.8" +files = [ + {file = "pyparsing-3.1.0-py3-none-any.whl", hash = "sha256:d554a96d1a7d3ddaf7183104485bc19fd80543ad6ac5bdb6426719d766fb06c1"}, + {file = "pyparsing-3.1.0.tar.gz", hash = "sha256:edb662d6fe322d6e990b1594b5feaeadf806803359e3d4d42f11e295e588f0ea"}, +] + +[package.extras] +diagrams = ["jinja2", "railroad-diagrams"] + +[[package]] +name = "pytest" +version = "6.2.5" +description = "pytest: simple powerful testing with Python" +category = "dev" +optional = false +python-versions = ">=3.6" +files = [ + {file = "pytest-6.2.5-py3-none-any.whl", hash = "sha256:7310f8d27bc79ced999e760ca304d69f6ba6c6649c0b60fb0e04a4a77cacc134"}, + {file = "pytest-6.2.5.tar.gz", hash = "sha256:131b36680866a76e6781d13f101efb86cf674ebb9762eb70d3082b6f29889e89"}, +] + +[package.dependencies] +atomicwrites = {version = ">=1.0", markers = "sys_platform == \"win32\""} +attrs = ">=19.2.0" +colorama = {version = "*", markers = "sys_platform == \"win32\""} +iniconfig = "*" +packaging = "*" +pluggy = ">=0.12,<2.0" +py = ">=1.8.2" +toml = "*" + +[package.extras] +testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "requests", "xmlschema"] + +[[package]] +name = "pytest-cov" +version = "3.0.0" +description = "Pytest plugin for measuring coverage." +category = "dev" +optional = false +python-versions = ">=3.6" +files = [ + {file = "pytest-cov-3.0.0.tar.gz", hash = "sha256:e7f0f5b1617d2210a2cabc266dfe2f4c75a8d32fb89eafb7ad9d06f6d076d470"}, + {file = "pytest_cov-3.0.0-py3-none-any.whl", hash = "sha256:578d5d15ac4a25e5f961c938b85a05b09fdaae9deef3bb6de9a6e766622ca7a6"}, +] + +[package.dependencies] +coverage = {version = ">=5.2.1", extras = ["toml"]} +pytest = ">=4.6" + +[package.extras] +testing = ["fields", "hunter", "process-tests", "pytest-xdist", "six", "virtualenv"] + +[[package]] +name = "python-dateutil" +version = "2.8.2" +description = "Extensions to the standard Python datetime module" +category = "main" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" +files = [ + {file = "python-dateutil-2.8.2.tar.gz", hash = "sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86"}, + {file = "python_dateutil-2.8.2-py2.py3-none-any.whl", hash = "sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9"}, +] + +[package.dependencies] +six = ">=1.5" + +[[package]] +name = "pytz" +version = "2023.3" +description = "World timezone definitions, modern and historical" +category = "main" +optional = true +python-versions = "*" +files = [ + {file = "pytz-2023.3-py2.py3-none-any.whl", hash = "sha256:a151b3abb88eda1d4e34a9814df37de2a80e301e68ba0fd856fb9b46bfbbbffb"}, + {file = "pytz-2023.3.tar.gz", hash = "sha256:1d8ce29db189191fb55338ee6d0387d82ab59f3d00eac103412d64e0ebd0c588"}, +] + +[[package]] +name = "pyyaml" +version = "6.0" +description = "YAML parser and emitter for Python" +category = "main" +optional = false +python-versions = ">=3.6" +files = [ + {file = "PyYAML-6.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d4db7c7aef085872ef65a8fd7d6d09a14ae91f691dec3e87ee5ee0539d516f53"}, + {file = "PyYAML-6.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9df7ed3b3d2e0ecfe09e14741b857df43adb5a3ddadc919a2d94fbdf78fea53c"}, + {file = "PyYAML-6.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:77f396e6ef4c73fdc33a9157446466f1cff553d979bd00ecb64385760c6babdc"}, + {file = "PyYAML-6.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a80a78046a72361de73f8f395f1f1e49f956c6be882eed58505a15f3e430962b"}, + {file = "PyYAML-6.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:f84fbc98b019fef2ee9a1cb3ce93e3187a6df0b2538a651bfb890254ba9f90b5"}, + {file = "PyYAML-6.0-cp310-cp310-win32.whl", hash = "sha256:2cd5df3de48857ed0544b34e2d40e9fac445930039f3cfe4bcc592a1f836d513"}, + {file = "PyYAML-6.0-cp310-cp310-win_amd64.whl", hash = "sha256:daf496c58a8c52083df09b80c860005194014c3698698d1a57cbcfa182142a3a"}, + {file = "PyYAML-6.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d4b0ba9512519522b118090257be113b9468d804b19d63c71dbcf4a48fa32358"}, + {file = "PyYAML-6.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:81957921f441d50af23654aa6c5e5eaf9b06aba7f0a19c18a538dc7ef291c5a1"}, + {file = "PyYAML-6.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:afa17f5bc4d1b10afd4466fd3a44dc0e245382deca5b3c353d8b757f9e3ecb8d"}, + {file = "PyYAML-6.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dbad0e9d368bb989f4515da330b88a057617d16b6a8245084f1b05400f24609f"}, + {file = "PyYAML-6.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:432557aa2c09802be39460360ddffd48156e30721f5e8d917f01d31694216782"}, + {file = "PyYAML-6.0-cp311-cp311-win32.whl", hash = "sha256:bfaef573a63ba8923503d27530362590ff4f576c626d86a9fed95822a8255fd7"}, + {file = "PyYAML-6.0-cp311-cp311-win_amd64.whl", hash = "sha256:01b45c0191e6d66c470b6cf1b9531a771a83c1c4208272ead47a3ae4f2f603bf"}, + {file = "PyYAML-6.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:897b80890765f037df3403d22bab41627ca8811ae55e9a722fd0392850ec4d86"}, + {file = "PyYAML-6.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:50602afada6d6cbfad699b0c7bb50d5ccffa7e46a3d738092afddc1f9758427f"}, + {file = "PyYAML-6.0-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:48c346915c114f5fdb3ead70312bd042a953a8ce5c7106d5bfb1a5254e47da92"}, + {file = "PyYAML-6.0-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:98c4d36e99714e55cfbaaee6dd5badbc9a1ec339ebfc3b1f52e293aee6bb71a4"}, + {file = "PyYAML-6.0-cp36-cp36m-win32.whl", hash = "sha256:0283c35a6a9fbf047493e3a0ce8d79ef5030852c51e9d911a27badfde0605293"}, + {file = "PyYAML-6.0-cp36-cp36m-win_amd64.whl", hash = "sha256:07751360502caac1c067a8132d150cf3d61339af5691fe9e87803040dbc5db57"}, + {file = "PyYAML-6.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:819b3830a1543db06c4d4b865e70ded25be52a2e0631ccd2f6a47a2822f2fd7c"}, + {file = "PyYAML-6.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:473f9edb243cb1935ab5a084eb238d842fb8f404ed2193a915d1784b5a6b5fc0"}, + {file = "PyYAML-6.0-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0ce82d761c532fe4ec3f87fc45688bdd3a4c1dc5e0b4a19814b9009a29baefd4"}, + {file = "PyYAML-6.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:231710d57adfd809ef5d34183b8ed1eeae3f76459c18fb4a0b373ad56bedcdd9"}, + {file = "PyYAML-6.0-cp37-cp37m-win32.whl", hash = "sha256:c5687b8d43cf58545ade1fe3e055f70eac7a5a1a0bf42824308d868289a95737"}, + {file = "PyYAML-6.0-cp37-cp37m-win_amd64.whl", hash = "sha256:d15a181d1ecd0d4270dc32edb46f7cb7733c7c508857278d3d378d14d606db2d"}, + {file = "PyYAML-6.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:0b4624f379dab24d3725ffde76559cff63d9ec94e1736b556dacdfebe5ab6d4b"}, + {file = "PyYAML-6.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:213c60cd50106436cc818accf5baa1aba61c0189ff610f64f4a3e8c6726218ba"}, + {file = "PyYAML-6.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9fa600030013c4de8165339db93d182b9431076eb98eb40ee068700c9c813e34"}, + {file = "PyYAML-6.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:277a0ef2981ca40581a47093e9e2d13b3f1fbbeffae064c1d21bfceba2030287"}, + {file = "PyYAML-6.0-cp38-cp38-win32.whl", hash = "sha256:d4eccecf9adf6fbcc6861a38015c2a64f38b9d94838ac1810a9023a0609e1b78"}, + {file = "PyYAML-6.0-cp38-cp38-win_amd64.whl", hash = "sha256:1e4747bc279b4f613a09eb64bba2ba602d8a6664c6ce6396a4d0cd413a50ce07"}, + {file = "PyYAML-6.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:055d937d65826939cb044fc8c9b08889e8c743fdc6a32b33e2390f66013e449b"}, + {file = "PyYAML-6.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e61ceaab6f49fb8bdfaa0f92c4b57bcfbea54c09277b1b4f7ac376bfb7a7c174"}, + {file = "PyYAML-6.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d67d839ede4ed1b28a4e8909735fc992a923cdb84e618544973d7dfc71540803"}, + {file = "PyYAML-6.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cba8c411ef271aa037d7357a2bc8f9ee8b58b9965831d9e51baf703280dc73d3"}, + {file = "PyYAML-6.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:40527857252b61eacd1d9af500c3337ba8deb8fc298940291486c465c8b46ec0"}, + {file = "PyYAML-6.0-cp39-cp39-win32.whl", hash = "sha256:b5b9eccad747aabaaffbc6064800670f0c297e52c12754eb1d976c57e4f74dcb"}, + {file = "PyYAML-6.0-cp39-cp39-win_amd64.whl", hash = "sha256:b3d267842bf12586ba6c734f89d1f5b871df0273157918b0ccefa29deb05c21c"}, + {file = "PyYAML-6.0.tar.gz", hash = "sha256:68fb519c14306fec9720a2a5b45bc9f0c8d1b9c72adf45c37baedfcd949c35a2"}, +] + +[[package]] +name = "ray" +version = "2.5.0" +description = "Ray provides a simple, universal API for building distributed applications." +category = "main" +optional = true +python-versions = "*" +files = [ + {file = "ray-2.5.0-cp310-cp310-macosx_10_15_universal2.whl", hash = "sha256:d1bebc874e896880c1215f4c1a11697ada49fa1595d6d99d7c5b4dc03030df36"}, + {file = "ray-2.5.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c0285df2d24cacc36ca64b7852178a9bf37e3fc88545752fc2b46c27396965c1"}, + {file = "ray-2.5.0-cp310-cp310-manylinux2014_aarch64.whl", hash = "sha256:38935d46c2597c1d1f113e1c8f88e2716c67052c480de5b2a0265e0a1a5ce88f"}, + {file = "ray-2.5.0-cp310-cp310-manylinux2014_x86_64.whl", hash = "sha256:d53a07c9a9dbc134945a26980f557e9ff0f591bf8cabed1a6ebf921768d1c8bd"}, + {file = "ray-2.5.0-cp310-cp310-win_amd64.whl", hash = "sha256:ef26ba24461dad98365b48ef01e27e70bc9737f4cf4734115804153d7d9195dc"}, + {file = "ray-2.5.0-cp311-cp311-manylinux2014_aarch64.whl", hash = "sha256:d714175a5000ca91f82646a9b72521118bb6d2db5568e1b7ae9ceb64769716b6"}, + {file = "ray-2.5.0-cp311-cp311-manylinux2014_x86_64.whl", hash = "sha256:0cde929e63497ed5f1c8626e5ccf7595ef6acaf1e7e270ad7c12f8e1c7695244"}, + {file = "ray-2.5.0-cp37-cp37m-macosx_10_15_x86_64.whl", hash = "sha256:7e5512abf62c05c9ff90b1c89a4e0f2e45ee00e73f816eb8265e3ebd92fe4064"}, + {file = "ray-2.5.0-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:3bf36beb213f89c0eb1ec5ac6ffddc8f53e616be745167f00ca017abd8672a2d"}, + {file = "ray-2.5.0-cp37-cp37m-manylinux2014_x86_64.whl", hash = "sha256:59c2448b07f45d9a9d8e594bb5337bd35a5fea04e42cb4211a3346c2c0d066b0"}, + {file = "ray-2.5.0-cp37-cp37m-win_amd64.whl", hash = "sha256:63008dd659d9ef25b0e20f0e1a285e8266e0af68b1178bca1b6ae43e49a68104"}, + {file = "ray-2.5.0-cp38-cp38-macosx_10_15_x86_64.whl", hash = "sha256:e9464e93d6b72e0da69b9c5ab0501cc40f2db14801e22c6b97fa4e8039647892"}, + {file = "ray-2.5.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:7dc00fac119bfa1c2f8ac456d50a728346d6f2722fb7a21bf70841fc7476c285"}, + {file = "ray-2.5.0-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:d76051519bd4ae39fda4a87536978cafdebf2843c1c29a9f734c503d8ea676cd"}, + {file = "ray-2.5.0-cp38-cp38-manylinux2014_x86_64.whl", hash = "sha256:9a8e06dc5d4201129c28b6768a971c474b82a23935b2e40461ffc7f1c2f4942a"}, + {file = "ray-2.5.0-cp38-cp38-win_amd64.whl", hash = "sha256:849014b62ca50ff106b7a5d41430346e2762b1c4c803673af076209925b8f912"}, + {file = "ray-2.5.0-cp39-cp39-macosx_10_15_x86_64.whl", hash = "sha256:a1b52c12a3349d8e37357df31438b6f1b12c7719ef41bdf5089fc7e78e8ab212"}, + {file = "ray-2.5.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:25f3d50c27c4c4756259d093d152381c6604bb96684a0cf43c55ddcc2eb73f79"}, + {file = "ray-2.5.0-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:1cb4f6ef9cfdb69d2ae582f357e977527944390e2f5cbbf51efd8252ed4c9a11"}, + {file = "ray-2.5.0-cp39-cp39-manylinux2014_x86_64.whl", hash = "sha256:662cff303c086369a29283badcd7445b7f911874d8407b2c589b1ccbf6028d2e"}, + {file = "ray-2.5.0-cp39-cp39-win_amd64.whl", hash = "sha256:a2cea10981dad7cfd187edf5e225a667eb114269afc5f2321b52113ef2d86123"}, +] + +[package.dependencies] +aiosignal = "*" +attrs = "*" +click = ">=7.0" +filelock = "*" +frozenlist = "*" +grpcio = [ + {version = ">=1.32.0,<=1.51.3", markers = "python_version < \"3.10\" and sys_platform != \"darwin\""}, + {version = ">=1.32.0,<=1.49.1", markers = "python_version < \"3.10\" and sys_platform == \"darwin\""}, + {version = ">=1.42.0,<=1.51.3", markers = "python_version >= \"3.10\" and sys_platform != \"darwin\""}, + {version = ">=1.42.0,<=1.49.1", markers = "python_version >= \"3.10\" and sys_platform == \"darwin\""}, +] +jsonschema = "*" +msgpack = ">=1.0.0,<2.0.0" +numpy = {version = ">=1.19.3", markers = "python_version >= \"3.9\""} +packaging = "*" +pandas = {version = "*", optional = true, markers = "extra == \"tune\""} +protobuf = ">=3.15.3,<3.19.5 || >3.19.5" +pyarrow = {version = ">=6.0.1", optional = true, markers = "extra == \"tune\""} +pyyaml = "*" +requests = "*" +tensorboardX = {version = ">=1.9", optional = true, markers = "extra == \"tune\""} + +[package.extras] +air = ["aiohttp (>=3.7)", "aiohttp-cors", "aiorwlock", "colorful", "fastapi", "fsspec", "gpustat (>=1.0.0)", "numpy (>=1.20)", "opencensus", "pandas", "pandas (>=1.3)", "prometheus-client (>=0.7.1)", "py-spy (>=0.2.0)", "pyarrow (>=6.0.1)", "pydantic", "requests", "smart-open", "starlette", "tensorboardX (>=1.9)", "uvicorn", "virtualenv (>=20.0.24,<20.21.1)"] +all = ["aiohttp (>=3.7)", "aiohttp-cors", "aiorwlock", "colorful", "dm-tree", "fastapi", "fsspec", "gpustat (>=1.0.0)", "gymnasium (==0.26.3)", "kubernetes", "lz4", "numpy (>=1.20)", "opencensus", "opentelemetry-api", "opentelemetry-exporter-otlp", "opentelemetry-sdk", "pandas", "pandas (>=1.3)", "prometheus-client (>=0.7.1)", "py-spy (>=0.2.0)", "pyarrow (>=6.0.1)", "pydantic", "pyyaml", "ray-cpp (==2.5.0)", "requests", "rich", "scikit-image", "scipy", "smart-open", "starlette", "tensorboardX (>=1.9)", "typer", "urllib3", "uvicorn", "virtualenv (>=20.0.24,<20.21.1)"] +cpp = ["ray-cpp (==2.5.0)"] +data = ["fsspec", "numpy (>=1.20)", "pandas (>=1.3)", "pyarrow (>=6.0.1)"] +default = ["aiohttp (>=3.7)", "aiohttp-cors", "colorful", "gpustat (>=1.0.0)", "opencensus", "prometheus-client (>=0.7.1)", "py-spy (>=0.2.0)", "pydantic", "requests", "smart-open", "virtualenv (>=20.0.24,<20.21.1)"] +k8s = ["kubernetes", "urllib3"] +observability = ["opentelemetry-api", "opentelemetry-exporter-otlp", "opentelemetry-sdk"] +rllib = ["dm-tree", "gymnasium (==0.26.3)", "lz4", "pandas", "pyarrow (>=6.0.1)", "pyyaml", "requests", "rich", "scikit-image", "scipy", "tensorboardX (>=1.9)", "typer"] +serve = ["aiohttp (>=3.7)", "aiohttp-cors", "aiorwlock", "colorful", "fastapi", "gpustat (>=1.0.0)", "opencensus", "prometheus-client (>=0.7.1)", "py-spy (>=0.2.0)", "pydantic", "requests", "smart-open", "starlette", "uvicorn", "virtualenv (>=20.0.24,<20.21.1)"] +train = ["pandas", "pyarrow (>=6.0.1)", "requests", "tensorboardX (>=1.9)"] +tune = ["pandas", "pyarrow (>=6.0.1)", "requests", "tensorboardX (>=1.9)"] + +[[package]] +name = "referencing" +version = "0.29.1" +description = "JSON Referencing + Python" +category = "main" +optional = true +python-versions = ">=3.8" +files = [ + {file = "referencing-0.29.1-py3-none-any.whl", hash = "sha256:d3c8f323ee1480095da44d55917cfb8278d73d6b4d5f677e3e40eb21314ac67f"}, + {file = "referencing-0.29.1.tar.gz", hash = "sha256:90cb53782d550ba28d2166ef3f55731f38397def8832baac5d45235f1995e35e"}, +] + +[package.dependencies] +attrs = ">=22.2.0" +rpds-py = ">=0.7.0" + +[[package]] +name = "requests" +version = "2.31.0" +description = "Python HTTP for Humans." +category = "main" +optional = false +python-versions = ">=3.7" +files = [ + {file = "requests-2.31.0-py3-none-any.whl", hash = "sha256:58cd2187c01e70e6e26505bca751777aa9f2ee0b7f4300988b709f44e013003f"}, + {file = "requests-2.31.0.tar.gz", hash = "sha256:942c5a758f98d790eaed1a29cb6eefc7ffb0d1cf7af05c3d2791656dbd6ad1e1"}, +] + +[package.dependencies] +certifi = ">=2017.4.17" +charset-normalizer = ">=2,<4" +idna = ">=2.5,<4" +urllib3 = ">=1.21.1,<3" + +[package.extras] +socks = ["PySocks (>=1.5.6,!=1.5.7)"] +use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] + +[[package]] +name = "requests-oauthlib" +version = "1.3.1" +description = "OAuthlib authentication support for Requests." +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +files = [ + {file = "requests-oauthlib-1.3.1.tar.gz", hash = "sha256:75beac4a47881eeb94d5ea5d6ad31ef88856affe2332b9aafb52c6452ccf0d7a"}, + {file = "requests_oauthlib-1.3.1-py2.py3-none-any.whl", hash = "sha256:2577c501a2fb8d05a304c09d090d6e47c306fef15809d102b327cf8364bddab5"}, +] + +[package.dependencies] +oauthlib = ">=3.0.0" +requests = ">=2.0.0" + +[package.extras] +rsa = ["oauthlib[signedtoken] (>=3.0.0)"] + +[[package]] +name = "rich" +version = "10.15.1" +description = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal" +category = "main" +optional = false +python-versions = ">=3.6.2,<4.0.0" +files = [ + {file = "rich-10.15.1-py3-none-any.whl", hash = "sha256:a59fb2721c52c5061ac65f318c0afb709e098b1ab6ce5813ea38982654c4b6ee"}, + {file = "rich-10.15.1.tar.gz", hash = "sha256:93d0ea3c35ecfd8703dbe52b76885e224ad8d68c7766c921c726b14b22a57b7d"}, +] + +[package.dependencies] +colorama = ">=0.4.0,<0.5.0" +commonmark = ">=0.9.0,<0.10.0" +pygments = ">=2.6.0,<3.0.0" + +[package.extras] +jupyter = ["ipywidgets (>=7.5.1,<8.0.0)"] + +[[package]] +name = "rpds-py" +version = "0.8.10" +description = "Python bindings to Rust's persistent data structures (rpds)" +category = "main" +optional = true +python-versions = ">=3.8" +files = [ + {file = "rpds_py-0.8.10-cp310-cp310-macosx_10_7_x86_64.whl", hash = "sha256:93d06cccae15b3836247319eee7b6f1fdcd6c10dabb4e6d350d27bd0bdca2711"}, + {file = "rpds_py-0.8.10-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:3816a890a6a9e9f1de250afa12ca71c9a7a62f2b715a29af6aaee3aea112c181"}, + {file = "rpds_py-0.8.10-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a7c6304b894546b5a6bdc0fe15761fa53fe87d28527a7142dae8de3c663853e1"}, + {file = "rpds_py-0.8.10-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ad3bfb44c8840fb4be719dc58e229f435e227fbfbe133dc33f34981ff622a8f8"}, + {file = "rpds_py-0.8.10-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:14f1c356712f66653b777ecd8819804781b23dbbac4eade4366b94944c9e78ad"}, + {file = "rpds_py-0.8.10-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:82bb361cae4d0a627006dadd69dc2f36b7ad5dc1367af9d02e296ec565248b5b"}, + {file = "rpds_py-0.8.10-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b2e3c4f2a8e3da47f850d7ea0d7d56720f0f091d66add889056098c4b2fd576c"}, + {file = "rpds_py-0.8.10-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:15a90d0ac11b4499171067ae40a220d1ca3cb685ec0acc356d8f3800e07e4cb8"}, + {file = "rpds_py-0.8.10-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:70bb9c8004b97b4ef7ae56a2aa56dfaa74734a0987c78e7e85f00004ab9bf2d0"}, + {file = "rpds_py-0.8.10-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:d64f9f88d5203274a002b54442cafc9c7a1abff2a238f3e767b70aadf919b451"}, + {file = "rpds_py-0.8.10-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:ccbbd276642788c4376fbe8d4e6c50f0fb4972ce09ecb051509062915891cbf0"}, + {file = "rpds_py-0.8.10-cp310-none-win32.whl", hash = "sha256:fafc0049add8043ad07ab5382ee80d80ed7e3699847f26c9a5cf4d3714d96a84"}, + {file = "rpds_py-0.8.10-cp310-none-win_amd64.whl", hash = "sha256:915031002c86a5add7c6fd4beb601b2415e8a1c956590a5f91d825858e92fe6e"}, + {file = "rpds_py-0.8.10-cp311-cp311-macosx_10_7_x86_64.whl", hash = "sha256:84eb541a44f7a18f07a6bfc48b95240739e93defe1fdfb4f2a295f37837945d7"}, + {file = "rpds_py-0.8.10-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f59996d0550894affaad8743e97b9b9c98f638b221fac12909210ec3d9294786"}, + {file = "rpds_py-0.8.10-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f9adb5664b78fcfcd830000416c8cc69853ef43cb084d645b3f1f0296edd9bae"}, + {file = "rpds_py-0.8.10-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f96f3f98fbff7af29e9edf9a6584f3c1382e7788783d07ba3721790625caa43e"}, + {file = "rpds_py-0.8.10-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:376b8de737401050bd12810003d207e824380be58810c031f10ec563ff6aef3d"}, + {file = "rpds_py-0.8.10-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5d1c2bc319428d50b3e0fa6b673ab8cc7fa2755a92898db3a594cbc4eeb6d1f7"}, + {file = "rpds_py-0.8.10-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:73a1e48430f418f0ac3dfd87860e4cc0d33ad6c0f589099a298cb53724db1169"}, + {file = "rpds_py-0.8.10-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:134ec8f14ca7dbc6d9ae34dac632cdd60939fe3734b5d287a69683c037c51acb"}, + {file = "rpds_py-0.8.10-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:4b519bac7c09444dd85280fd60f28c6dde4389c88dddf4279ba9b630aca3bbbe"}, + {file = "rpds_py-0.8.10-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:9cd57981d9fab04fc74438d82460f057a2419974d69a96b06a440822d693b3c0"}, + {file = "rpds_py-0.8.10-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:69d089c026f6a8b9d64a06ff67dc3be196707b699d7f6ca930c25f00cf5e30d8"}, + {file = "rpds_py-0.8.10-cp311-none-win32.whl", hash = "sha256:220bdcad2d2936f674650d304e20ac480a3ce88a40fe56cd084b5780f1d104d9"}, + {file = "rpds_py-0.8.10-cp311-none-win_amd64.whl", hash = "sha256:6c6a0225b8501d881b32ebf3f5807a08ad3685b5eb5f0a6bfffd3a6e039b2055"}, + {file = "rpds_py-0.8.10-cp312-cp312-macosx_10_7_x86_64.whl", hash = "sha256:e3d0cd3dff0e7638a7b5390f3a53057c4e347f4ef122ee84ed93fc2fb7ea4aa2"}, + {file = "rpds_py-0.8.10-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:d77dff3a5aa5eedcc3da0ebd10ff8e4969bc9541aa3333a8d41715b429e99f47"}, + {file = "rpds_py-0.8.10-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:41c89a366eae49ad9e65ed443a8f94aee762931a1e3723749d72aeac80f5ef2f"}, + {file = "rpds_py-0.8.10-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3793c21494bad1373da517001d0849eea322e9a049a0e4789e50d8d1329df8e7"}, + {file = "rpds_py-0.8.10-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:805a5f3f05d186c5d50de2e26f765ba7896d0cc1ac5b14ffc36fae36df5d2f10"}, + {file = "rpds_py-0.8.10-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b01b39ad5411563031ea3977bbbc7324d82b088e802339e6296f082f78f6115c"}, + {file = "rpds_py-0.8.10-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f3f1e860be21f3e83011116a65e7310486300e08d9a3028e73e8d13bb6c77292"}, + {file = "rpds_py-0.8.10-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a13c8e56c46474cd5958d525ce6a9996727a83d9335684e41f5192c83deb6c58"}, + {file = "rpds_py-0.8.10-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:93d99f957a300d7a4ced41615c45aeb0343bb8f067c42b770b505de67a132346"}, + {file = "rpds_py-0.8.10-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:148b0b38d719c0760e31ce9285a9872972bdd7774969a4154f40c980e5beaca7"}, + {file = "rpds_py-0.8.10-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3cc5e5b5514796f45f03a568981971b12a3570f3de2e76114f7dc18d4b60a3c4"}, + {file = "rpds_py-0.8.10-cp38-cp38-macosx_10_7_x86_64.whl", hash = "sha256:e8e24b210a4deb5a7744971f8f77393005bae7f873568e37dfd9effe808be7f7"}, + {file = "rpds_py-0.8.10-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:b41941583adce4242af003d2a8337b066ba6148ca435f295f31ac6d9e4ea2722"}, + {file = "rpds_py-0.8.10-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3c490204e16bca4f835dba8467869fe7295cdeaa096e4c5a7af97f3454a97991"}, + {file = "rpds_py-0.8.10-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:1ee45cd1d84beed6cbebc839fd85c2e70a3a1325c8cfd16b62c96e2ffb565eca"}, + {file = "rpds_py-0.8.10-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4a8ca409f1252e1220bf09c57290b76cae2f14723746215a1e0506472ebd7bdf"}, + {file = "rpds_py-0.8.10-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:96b293c0498c70162effb13100624c5863797d99df75f2f647438bd10cbf73e4"}, + {file = "rpds_py-0.8.10-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b4627520a02fccbd324b33c7a83e5d7906ec746e1083a9ac93c41ac7d15548c7"}, + {file = "rpds_py-0.8.10-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e39d7ab0c18ac99955b36cd19f43926450baba21e3250f053e0704d6ffd76873"}, + {file = "rpds_py-0.8.10-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:ba9f1d1ebe4b63801977cec7401f2d41e888128ae40b5441270d43140efcad52"}, + {file = "rpds_py-0.8.10-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:802f42200d8caf7f25bbb2a6464cbd83e69d600151b7e3b49f49a47fa56b0a38"}, + {file = "rpds_py-0.8.10-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:d19db6ba816e7f59fc806c690918da80a7d186f00247048cd833acdab9b4847b"}, + {file = "rpds_py-0.8.10-cp38-none-win32.whl", hash = "sha256:7947e6e2c2ad68b1c12ee797d15e5f8d0db36331200b0346871492784083b0c6"}, + {file = "rpds_py-0.8.10-cp38-none-win_amd64.whl", hash = "sha256:fa326b3505d5784436d9433b7980171ab2375535d93dd63fbcd20af2b5ca1bb6"}, + {file = "rpds_py-0.8.10-cp39-cp39-macosx_10_7_x86_64.whl", hash = "sha256:7b38a9ac96eeb6613e7f312cd0014de64c3f07000e8bf0004ad6ec153bac46f8"}, + {file = "rpds_py-0.8.10-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c4d42e83ddbf3445e6514f0aff96dca511421ed0392d9977d3990d9f1ba6753c"}, + {file = "rpds_py-0.8.10-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1b21575031478609db6dbd1f0465e739fe0e7f424a8e7e87610a6c7f68b4eb16"}, + {file = "rpds_py-0.8.10-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:574868858a7ff6011192c023a5289158ed20e3f3b94b54f97210a773f2f22921"}, + {file = "rpds_py-0.8.10-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ae40f4a70a1f40939d66ecbaf8e7edc144fded190c4a45898a8cfe19d8fc85ea"}, + {file = "rpds_py-0.8.10-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:37f7ee4dc86db7af3bac6d2a2cedbecb8e57ce4ed081f6464510e537589f8b1e"}, + {file = "rpds_py-0.8.10-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:695f642a3a5dbd4ad2ffbbacf784716ecd87f1b7a460843b9ddf965ccaeafff4"}, + {file = "rpds_py-0.8.10-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f43ab4cb04bde6109eb2555528a64dfd8a265cc6a9920a67dcbde13ef53a46c8"}, + {file = "rpds_py-0.8.10-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:a11ab0d97be374efd04f640c04fe5c2d3dabc6dfb998954ea946ee3aec97056d"}, + {file = "rpds_py-0.8.10-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:92cf5b3ee60eef41f41e1a2cabca466846fb22f37fc580ffbcb934d1bcab225a"}, + {file = "rpds_py-0.8.10-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:ceaac0c603bf5ac2f505a78b2dcab78d3e6b706be6596c8364b64cc613d208d2"}, + {file = "rpds_py-0.8.10-cp39-none-win32.whl", hash = "sha256:dd4f16e57c12c0ae17606c53d1b57d8d1c8792efe3f065a37cb3341340599d49"}, + {file = "rpds_py-0.8.10-cp39-none-win_amd64.whl", hash = "sha256:c03a435d26c3999c2a8642cecad5d1c4d10c961817536af52035f6f4ee2f5dd0"}, + {file = "rpds_py-0.8.10-pp310-pypy310_pp73-macosx_10_7_x86_64.whl", hash = "sha256:0da53292edafecba5e1d8c1218f99babf2ed0bf1c791d83c0ab5c29b57223068"}, + {file = "rpds_py-0.8.10-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:7d20a8ed227683401cc508e7be58cba90cc97f784ea8b039c8cd01111e6043e0"}, + {file = "rpds_py-0.8.10-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:97cab733d303252f7c2f7052bf021a3469d764fc2b65e6dbef5af3cbf89d4892"}, + {file = "rpds_py-0.8.10-pp310-pypy310_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8c398fda6df361a30935ab4c4bccb7f7a3daef2964ca237f607c90e9f3fdf66f"}, + {file = "rpds_py-0.8.10-pp310-pypy310_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2eb4b08c45f8f8d8254cdbfacd3fc5d6b415d64487fb30d7380b0d0569837bf1"}, + {file = "rpds_py-0.8.10-pp310-pypy310_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e7dfb1cbb895810fa2b892b68153c17716c6abaa22c7dc2b2f6dcf3364932a1c"}, + {file = "rpds_py-0.8.10-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:89c92b74e8bf6f53a6f4995fd52f4bd510c12f103ee62c99e22bc9e05d45583c"}, + {file = "rpds_py-0.8.10-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e9c0683cb35a9b5881b41bc01d5568ffc667910d9dbc632a1fba4e7d59e98773"}, + {file = "rpds_py-0.8.10-pp310-pypy310_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:0eeb2731708207d0fe2619afe6c4dc8cb9798f7de052da891de5f19c0006c315"}, + {file = "rpds_py-0.8.10-pp310-pypy310_pp73-musllinux_1_2_i686.whl", hash = "sha256:7495010b658ec5b52835f21d8c8b1a7e52e194c50f095d4223c0b96c3da704b1"}, + {file = "rpds_py-0.8.10-pp310-pypy310_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:c72ebc22e70e04126158c46ba56b85372bc4d54d00d296be060b0db1671638a4"}, + {file = "rpds_py-0.8.10-pp38-pypy38_pp73-macosx_10_7_x86_64.whl", hash = "sha256:2cd3045e7f6375dda64ed7db1c5136826facb0159ea982f77d9cf6125025bd34"}, + {file = "rpds_py-0.8.10-pp38-pypy38_pp73-macosx_11_0_arm64.whl", hash = "sha256:2418cf17d653d24ffb8b75e81f9f60b7ba1b009a23298a433a4720b2a0a17017"}, + {file = "rpds_py-0.8.10-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1a2edf8173ac0c7a19da21bc68818be1321998528b5e3f748d6ee90c0ba2a1fd"}, + {file = "rpds_py-0.8.10-pp38-pypy38_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7f29b8c55fd3a2bc48e485e37c4e2df3317f43b5cc6c4b6631c33726f52ffbb3"}, + {file = "rpds_py-0.8.10-pp38-pypy38_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9a7d20c1cf8d7b3960c5072c265ec47b3f72a0c608a9a6ee0103189b4f28d531"}, + {file = "rpds_py-0.8.10-pp38-pypy38_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:521fc8861a86ae54359edf53a15a05fabc10593cea7b3357574132f8427a5e5a"}, + {file = "rpds_py-0.8.10-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d5c191713e98e7c28800233f039a32a42c1a4f9a001a8a0f2448b07391881036"}, + {file = "rpds_py-0.8.10-pp38-pypy38_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:083df0fafe199371206111583c686c985dddaf95ab3ee8e7b24f1fda54515d09"}, + {file = "rpds_py-0.8.10-pp38-pypy38_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:ed41f3f49507936a6fe7003985ea2574daccfef999775525d79eb67344e23767"}, + {file = "rpds_py-0.8.10-pp38-pypy38_pp73-musllinux_1_2_i686.whl", hash = "sha256:2614c2732bf45de5c7f9e9e54e18bc78693fa2f635ae58d2895b7965e470378c"}, + {file = "rpds_py-0.8.10-pp38-pypy38_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:c60528671d9d467009a6ec284582179f6b88651e83367d0ab54cb739021cd7de"}, + {file = "rpds_py-0.8.10-pp39-pypy39_pp73-macosx_10_7_x86_64.whl", hash = "sha256:ee744fca8d1ea822480a2a4e7c5f2e1950745477143668f0b523769426060f29"}, + {file = "rpds_py-0.8.10-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:a38b9f526d0d6cbdaa37808c400e3d9f9473ac4ff64d33d9163fd05d243dbd9b"}, + {file = "rpds_py-0.8.10-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:60e0e86e870350e03b3e25f9b1dd2c6cc72d2b5f24e070249418320a6f9097b7"}, + {file = "rpds_py-0.8.10-pp39-pypy39_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f53f55a8852f0e49b0fc76f2412045d6ad9d5772251dea8f55ea45021616e7d5"}, + {file = "rpds_py-0.8.10-pp39-pypy39_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c493365d3fad241d52f096e4995475a60a80f4eba4d3ff89b713bc65c2ca9615"}, + {file = "rpds_py-0.8.10-pp39-pypy39_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:300eb606e6b94a7a26f11c8cc8ee59e295c6649bd927f91e1dbd37a4c89430b6"}, + {file = "rpds_py-0.8.10-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5a665f6f1a87614d1c3039baf44109094926dedf785e346d8b0a728e9cabd27a"}, + {file = "rpds_py-0.8.10-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:927d784648211447201d4c6f1babddb7971abad922b32257ab74de2f2750fad0"}, + {file = "rpds_py-0.8.10-pp39-pypy39_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:c200b30dd573afa83847bed7e3041aa36a8145221bf0cfdfaa62d974d720805c"}, + {file = "rpds_py-0.8.10-pp39-pypy39_pp73-musllinux_1_2_i686.whl", hash = "sha256:08166467258fd0240a1256fce272f689f2360227ee41c72aeea103e9e4f63d2b"}, + {file = "rpds_py-0.8.10-pp39-pypy39_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:996cc95830de9bc22b183661d95559ec6b3cd900ad7bc9154c4cbf5be0c9b734"}, + {file = "rpds_py-0.8.10.tar.gz", hash = "sha256:13e643ce8ad502a0263397362fb887594b49cf84bf518d6038c16f235f2bcea4"}, +] + +[[package]] +name = "rsa" +version = "4.9" +description = "Pure-Python RSA implementation" +category = "main" +optional = false +python-versions = ">=3.6,<4" +files = [ + {file = "rsa-4.9-py3-none-any.whl", hash = "sha256:90260d9058e514786967344d0ef75fa8727eed8a7d2e43ce9f4bcf1b536174f7"}, + {file = "rsa-4.9.tar.gz", hash = "sha256:e38464a49c6c85d7f1351b0126661487a7e0a14a50f1675ec50eb34d4f20ef21"}, +] + +[package.dependencies] +pyasn1 = ">=0.1.3" + +[[package]] +name = "scikit-learn" +version = "1.3.0" +description = "A set of python modules for machine learning and data mining" +category = "main" +optional = true +python-versions = ">=3.8" +files = [ + {file = "scikit-learn-1.3.0.tar.gz", hash = "sha256:8be549886f5eda46436b6e555b0e4873b4f10aa21c07df45c4bc1735afbccd7a"}, + {file = "scikit_learn-1.3.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:981287869e576d42c682cf7ca96af0c6ac544ed9316328fd0d9292795c742cf5"}, + {file = "scikit_learn-1.3.0-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:436aaaae2c916ad16631142488e4c82f4296af2404f480e031d866863425d2a2"}, + {file = "scikit_learn-1.3.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c7e28d8fa47a0b30ae1bd7a079519dd852764e31708a7804da6cb6f8b36e3630"}, + {file = "scikit_learn-1.3.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ae80c08834a473d08a204d966982a62e11c976228d306a2648c575e3ead12111"}, + {file = "scikit_learn-1.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:552fd1b6ee22900cf1780d7386a554bb96949e9a359999177cf30211e6b20df6"}, + {file = "scikit_learn-1.3.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:79970a6d759eb00a62266a31e2637d07d2d28446fca8079cf9afa7c07b0427f8"}, + {file = "scikit_learn-1.3.0-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:850a00b559e636b23901aabbe79b73dc604b4e4248ba9e2d6e72f95063765603"}, + {file = "scikit_learn-1.3.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ee04835fb016e8062ee9fe9074aef9b82e430504e420bff51e3e5fffe72750ca"}, + {file = "scikit_learn-1.3.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9d953531f5d9f00c90c34fa3b7d7cfb43ecff4c605dac9e4255a20b114a27369"}, + {file = "scikit_learn-1.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:151ac2bf65ccf363664a689b8beafc9e6aae36263db114b4ca06fbbbf827444a"}, + {file = "scikit_learn-1.3.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:6a885a9edc9c0a341cab27ec4f8a6c58b35f3d449c9d2503a6fd23e06bbd4f6a"}, + {file = "scikit_learn-1.3.0-cp38-cp38-macosx_12_0_arm64.whl", hash = "sha256:9877af9c6d1b15486e18a94101b742e9d0d2f343d35a634e337411ddb57783f3"}, + {file = "scikit_learn-1.3.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c470f53cea065ff3d588050955c492793bb50c19a92923490d18fcb637f6383a"}, + {file = "scikit_learn-1.3.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd6e2d7389542eae01077a1ee0318c4fec20c66c957f45c7aac0c6eb0fe3c612"}, + {file = "scikit_learn-1.3.0-cp38-cp38-win_amd64.whl", hash = "sha256:3a11936adbc379a6061ea32fa03338d4ca7248d86dd507c81e13af428a5bc1db"}, + {file = "scikit_learn-1.3.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:998d38fcec96584deee1e79cd127469b3ad6fefd1ea6c2dfc54e8db367eb396b"}, + {file = "scikit_learn-1.3.0-cp39-cp39-macosx_12_0_arm64.whl", hash = "sha256:ded35e810438a527e17623ac6deae3b360134345b7c598175ab7741720d7ffa7"}, + {file = "scikit_learn-1.3.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0e8102d5036e28d08ab47166b48c8d5e5810704daecf3a476a4282d562be9a28"}, + {file = "scikit_learn-1.3.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7617164951c422747e7c32be4afa15d75ad8044f42e7d70d3e2e0429a50e6718"}, + {file = "scikit_learn-1.3.0-cp39-cp39-win_amd64.whl", hash = "sha256:1d54fb9e6038284548072df22fd34777e434153f7ffac72c8596f2d6987110dd"}, +] + +[package.dependencies] +joblib = ">=1.1.1" +numpy = ">=1.17.3" +scipy = ">=1.5.0" +threadpoolctl = ">=2.0.0" + +[package.extras] +benchmark = ["matplotlib (>=3.1.3)", "memory-profiler (>=0.57.0)", "pandas (>=1.0.5)"] +docs = ["Pillow (>=7.1.2)", "matplotlib (>=3.1.3)", "memory-profiler (>=0.57.0)", "numpydoc (>=1.2.0)", "pandas (>=1.0.5)", "plotly (>=5.14.0)", "pooch (>=1.6.0)", "scikit-image (>=0.16.2)", "seaborn (>=0.9.0)", "sphinx (>=6.0.0)", "sphinx-copybutton (>=0.5.2)", "sphinx-gallery (>=0.10.1)", "sphinx-prompt (>=1.3.0)", "sphinxext-opengraph (>=0.4.2)"] +examples = ["matplotlib (>=3.1.3)", "pandas (>=1.0.5)", "plotly (>=5.14.0)", "pooch (>=1.6.0)", "scikit-image (>=0.16.2)", "seaborn (>=0.9.0)"] +tests = ["black (>=23.3.0)", "matplotlib (>=3.1.3)", "mypy (>=1.3)", "numpydoc (>=1.2.0)", "pandas (>=1.0.5)", "pooch (>=1.6.0)", "pyamg (>=4.0.0)", "pytest (>=7.1.2)", "pytest-cov (>=2.9.0)", "ruff (>=0.0.272)", "scikit-image (>=0.16.2)"] + +[[package]] +name = "scikit-optimize" +version = "0.9.0" +description = "Sequential model-based optimization toolbox." +category = "main" +optional = true +python-versions = "*" +files = [ + {file = "scikit-optimize-0.9.0.tar.gz", hash = "sha256:77d8c9e64947fc9f5cc05bbc6aed7b8a9907871ae26fe11997fd67be90f26008"}, + {file = "scikit_optimize-0.9.0-py2.py3-none-any.whl", hash = "sha256:5a439a18232381fad4bda78e914b616416720708e67f123498d14bd2842d861a"}, +] + +[package.dependencies] +joblib = ">=0.11" +numpy = ">=1.13.3" +pyaml = ">=16.9" +scikit-learn = ">=0.20.0" +scipy = ">=0.19.1" + +[package.extras] +plots = ["matplotlib (>=2.0.0)"] + +[[package]] +name = "scipy" +version = "1.8.0" +description = "SciPy: Scientific Library for Python" +category = "main" +optional = false +python-versions = ">=3.8,<3.11" +files = [ + {file = "scipy-1.8.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:87b01c7d5761e8a266a0fbdb9d88dcba0910d63c1c671bdb4d99d29f469e9e03"}, + {file = "scipy-1.8.0-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:ae3e327da323d82e918e593460e23babdce40d7ab21490ddf9fc06dec6b91a18"}, + {file = "scipy-1.8.0-cp310-cp310-macosx_12_0_universal2.macosx_10_9_x86_64.whl", hash = "sha256:16e09ef68b352d73befa8bcaf3ebe25d3941fe1a58c82909d5589856e6bc8174"}, + {file = "scipy-1.8.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c17a1878d00a5dd2797ccd73623ceca9d02375328f6218ee6d921e1325e61aff"}, + {file = "scipy-1.8.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:937d28722f13302febde29847bbe554b89073fbb924a30475e5ed7b028898b5f"}, + {file = "scipy-1.8.0-cp310-cp310-win_amd64.whl", hash = "sha256:8f4d059a97b29c91afad46b1737274cb282357a305a80bdd9e8adf3b0ca6a3f0"}, + {file = "scipy-1.8.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:38aa39b6724cb65271e469013aeb6f2ce66fd44f093e241c28a9c6bc64fd79ed"}, + {file = "scipy-1.8.0-cp38-cp38-macosx_12_0_arm64.whl", hash = "sha256:559a8a4c03a5ba9fe3232f39ed24f86457e4f3f6c0abbeae1fb945029f092720"}, + {file = "scipy-1.8.0-cp38-cp38-macosx_12_0_universal2.macosx_10_9_x86_64.whl", hash = "sha256:f4a6d3b9f9797eb2d43938ac2c5d96d02aed17ef170c8b38f11798717523ddba"}, + {file = "scipy-1.8.0-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:92b2c2af4183ed09afb595709a8ef5783b2baf7f41e26ece24e1329c109691a7"}, + {file = "scipy-1.8.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a279e27c7f4566ef18bab1b1e2c37d168e365080974758d107e7d237d3f0f484"}, + {file = "scipy-1.8.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ad5be4039147c808e64f99c0e8a9641eb5d2fa079ff5894dcd8240e94e347af4"}, + {file = "scipy-1.8.0-cp38-cp38-win32.whl", hash = "sha256:3d9dd6c8b93a22bf9a3a52d1327aca7e092b1299fb3afc4f89e8eba381be7b59"}, + {file = "scipy-1.8.0-cp38-cp38-win_amd64.whl", hash = "sha256:5e73343c5e0d413c1f937302b2e04fb07872f5843041bcfd50699aef6e95e399"}, + {file = "scipy-1.8.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:de2e80ee1d925984c2504812a310841c241791c5279352be4707cdcd7c255039"}, + {file = "scipy-1.8.0-cp39-cp39-macosx_12_0_arm64.whl", hash = "sha256:c2bae431d127bf0b1da81fc24e4bba0a84d058e3a96b9dd6475dfcb3c5e8761e"}, + {file = "scipy-1.8.0-cp39-cp39-macosx_12_0_universal2.macosx_10_9_x86_64.whl", hash = "sha256:723b9f878095ed994756fa4ee3060c450e2db0139c5ba248ee3f9628bd64e735"}, + {file = "scipy-1.8.0-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:011d4386b53b933142f58a652aa0f149c9b9242abd4f900b9f4ea5fbafc86b89"}, + {file = "scipy-1.8.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e6f0cd9c0bd374ef834ee1e0f0999678d49dcc400ea6209113d81528958f97c7"}, + {file = "scipy-1.8.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f3720d0124aced49f6f2198a6900304411dbbeed12f56951d7c66ebef05e3df6"}, + {file = "scipy-1.8.0-cp39-cp39-win32.whl", hash = "sha256:3d573228c10a3a8c32b9037be982e6440e411b443a6267b067cac72f690b8d56"}, + {file = "scipy-1.8.0-cp39-cp39-win_amd64.whl", hash = "sha256:bb7088e89cd751acf66195d2f00cf009a1ea113f3019664032d9075b1e727b6c"}, + {file = "scipy-1.8.0.tar.gz", hash = "sha256:31d4f2d6b724bc9a98e527b5849b8a7e589bf1ea630c33aa563eda912c9ff0bd"}, +] + +[package.dependencies] +numpy = ">=1.17.3,<1.25.0" + +[[package]] +name = "setuptools" +version = "68.0.0" +description = "Easily download, build, install, upgrade, and uninstall Python packages" +category = "main" +optional = false +python-versions = ">=3.7" +files = [ + {file = "setuptools-68.0.0-py3-none-any.whl", hash = "sha256:11e52c67415a381d10d6b462ced9cfb97066179f0e871399e006c4ab101fc85f"}, + {file = "setuptools-68.0.0.tar.gz", hash = "sha256:baf1fdb41c6da4cd2eae722e135500da913332ab3f2f5c7d33af9b492acb5235"}, +] + +[package.extras] +docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-hoverxref (<2)", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (==0.8.3)", "sphinx-reredirects", "sphinxcontrib-towncrier"] +testing = ["build[virtualenv]", "filelock (>=3.4.0)", "flake8-2020", "ini2toml[lite] (>=0.9)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pip (>=19.1)", "pip-run (>=8.8)", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-mypy (>=0.9.1)", "pytest-perf", "pytest-ruff", "pytest-timeout", "pytest-xdist", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"] +testing-integration = ["build[virtualenv]", "filelock (>=3.4.0)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pytest", "pytest-enabler", "pytest-xdist", "tomli", "virtualenv (>=13.0.0)", "wheel"] + +[[package]] +name = "setuptools-scm" +version = "7.1.0" +description = "the blessed package to manage your versions by scm tags" +category = "main" +optional = false +python-versions = ">=3.7" +files = [ + {file = "setuptools_scm-7.1.0-py3-none-any.whl", hash = "sha256:73988b6d848709e2af142aa48c986ea29592bbcfca5375678064708205253d8e"}, + {file = "setuptools_scm-7.1.0.tar.gz", hash = "sha256:6c508345a771aad7d56ebff0e70628bf2b0ec7573762be9960214730de278f27"}, +] + +[package.dependencies] +packaging = ">=20.0" +setuptools = "*" +tomli = {version = ">=1.0.0", markers = "python_version < \"3.11\""} +typing-extensions = "*" + +[package.extras] +test = ["pytest (>=6.2)", "virtualenv (>20)"] +toml = ["setuptools (>=42)"] + +[[package]] +name = "six" +version = "1.16.0" +description = "Python 2 and 3 compatibility utilities" +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" +files = [ + {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, + {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, +] + +[[package]] +name = "snowballstemmer" +version = "2.2.0" +description = "This package provides 29 stemmers for 28 languages generated from Snowball algorithms." +category = "dev" +optional = false +python-versions = "*" +files = [ + {file = "snowballstemmer-2.2.0-py2.py3-none-any.whl", hash = "sha256:c8e1716e83cc398ae16824e5572ae04e0d9fc2c6b985fb0f900f5f0c96ecba1a"}, + {file = "snowballstemmer-2.2.0.tar.gz", hash = "sha256:09b16deb8547d3412ad7b590689584cd0fe25ec8db3be37788be3810cbf19cb1"}, +] + +[[package]] +name = "sortedcontainers" +version = "2.4.0" +description = "Sorted Containers -- Sorted List, Sorted Dict, Sorted Set" +category = "dev" +optional = false +python-versions = "*" +files = [ + {file = "sortedcontainers-2.4.0-py2.py3-none-any.whl", hash = "sha256:a163dcaede0f1c021485e957a39245190e74249897e2ae4b2aa38595db237ee0"}, + {file = "sortedcontainers-2.4.0.tar.gz", hash = "sha256:25caa5a06cc30b6b83d11423433f65d1f9d76c4c6a0c90e3379eaa43b9bfdb88"}, +] + +[[package]] +name = "sphinx" +version = "7.0.1" +description = "Python documentation generator" +category = "dev" +optional = false +python-versions = ">=3.8" +files = [ + {file = "Sphinx-7.0.1.tar.gz", hash = "sha256:61e025f788c5977d9412587e733733a289e2b9fdc2fef8868ddfbfc4ccfe881d"}, + {file = "sphinx-7.0.1-py3-none-any.whl", hash = "sha256:60c5e04756c1709a98845ed27a2eed7a556af3993afb66e77fec48189f742616"}, +] + +[package.dependencies] +alabaster = ">=0.7,<0.8" +babel = ">=2.9" +colorama = {version = ">=0.4.5", markers = "sys_platform == \"win32\""} +docutils = ">=0.18.1,<0.21" +imagesize = ">=1.3" +importlib-metadata = {version = ">=4.8", markers = "python_version < \"3.10\""} +Jinja2 = ">=3.0" +packaging = ">=21.0" +Pygments = ">=2.13" +requests = ">=2.25.0" +snowballstemmer = ">=2.0" +sphinxcontrib-applehelp = "*" +sphinxcontrib-devhelp = "*" +sphinxcontrib-htmlhelp = ">=2.0.0" +sphinxcontrib-jsmath = "*" +sphinxcontrib-qthelp = "*" +sphinxcontrib-serializinghtml = ">=1.1.5" + +[package.extras] +docs = ["sphinxcontrib-websupport"] +lint = ["docutils-stubs", "flake8 (>=3.5.0)", "flake8-simplify", "isort", "mypy (>=0.990)", "ruff", "sphinx-lint", "types-requests"] +test = ["cython", "filelock", "html5lib", "pytest (>=4.6)"] + +[[package]] +name = "sphinx-autodoc-typehints" +version = "1.23.3" +description = "Type hints (PEP 484) support for the Sphinx autodoc extension" +category = "dev" +optional = false +python-versions = ">=3.8" +files = [ + {file = "sphinx_autodoc_typehints-1.23.3-py3-none-any.whl", hash = "sha256:ec913d93e915b4dae6a8758cd95baecc70ed77fcdfe050601fc6b12afd0fc059"}, + {file = "sphinx_autodoc_typehints-1.23.3.tar.gz", hash = "sha256:8fb8dfc4b010505c850f4ef395fc80222ebfd8fd1b40149f8862f2396f623760"}, +] + +[package.dependencies] +sphinx = ">=7.0.1" + +[package.extras] +docs = ["furo (>=2023.5.20)", "sphinx (>=7.0.1)"] +numpy = ["nptyping (>=2.5)"] +testing = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "diff-cover (>=7.5)", "pytest (>=7.3.1)", "pytest-cov (>=4.1)", "sphobjinv (>=2.3.1)", "typing-extensions (>=4.6.3)"] +type-comment = ["typed-ast (>=1.5.4)"] + +[[package]] +name = "sphinx-automodapi" +version = "0.15.0" +description = "Sphinx extension for auto-generating API documentation for entire modules" +category = "dev" +optional = false +python-versions = ">=3.7" +files = [ + {file = "sphinx-automodapi-0.15.0.tar.gz", hash = "sha256:fd5871e054df7f3e299dde959afffa849f4d01c6eac274c366b06472afcb06aa"}, + {file = "sphinx_automodapi-0.15.0-py3-none-any.whl", hash = "sha256:06848f261fb127b25d35f27c2c4fddb041e76498733da064504f8077cbd27bec"}, +] + +[package.dependencies] +sphinx = ">=2" + +[package.extras] +test = ["codecov", "coverage", "cython", "pytest", "pytest-cov"] + +[[package]] +name = "sphinx-copybutton" +version = "0.5.2" +description = "Add a copy button to each of your code cells." +category = "dev" +optional = false +python-versions = ">=3.7" +files = [ + {file = "sphinx-copybutton-0.5.2.tar.gz", hash = "sha256:4cf17c82fb9646d1bc9ca92ac280813a3b605d8c421225fd9913154103ee1fbd"}, + {file = "sphinx_copybutton-0.5.2-py3-none-any.whl", hash = "sha256:fb543fd386d917746c9a2c50360c7905b605726b9355cd26e9974857afeae06e"}, +] + +[package.dependencies] +sphinx = ">=1.8" + +[package.extras] +code-style = ["pre-commit (==2.12.1)"] +rtd = ["ipython", "myst-nb", "sphinx", "sphinx-book-theme", "sphinx-examples"] + +[[package]] +name = "sphinx-gallery" +version = "0.13.0" +description = "A `Sphinx `_ extension that builds an HTML gallery of examples from any set of Python scripts." +category = "dev" +optional = false +python-versions = ">=3.7" +files = [ + {file = "sphinx-gallery-0.13.0.tar.gz", hash = "sha256:4756f92e079128b08cbc7a57922cc904b3d442b1abfa73ec6471ad24f3c5b4b2"}, + {file = "sphinx_gallery-0.13.0-py3-none-any.whl", hash = "sha256:5bedfa4998b4158d5affc7d1df6796e4b1e834b16680001dac992af1304d8ed9"}, +] + +[package.dependencies] +sphinx = ">=4" + +[[package]] +name = "sphinxcontrib-applehelp" +version = "1.0.4" +description = "sphinxcontrib-applehelp is a Sphinx extension which outputs Apple help books" +category = "dev" +optional = false +python-versions = ">=3.8" +files = [ + {file = "sphinxcontrib-applehelp-1.0.4.tar.gz", hash = "sha256:828f867945bbe39817c210a1abfd1bc4895c8b73fcaade56d45357a348a07d7e"}, + {file = "sphinxcontrib_applehelp-1.0.4-py3-none-any.whl", hash = "sha256:29d341f67fb0f6f586b23ad80e072c8e6ad0b48417db2bde114a4c9746feb228"}, +] + +[package.extras] +lint = ["docutils-stubs", "flake8", "mypy"] +test = ["pytest"] + +[[package]] +name = "sphinxcontrib-bibtex" +version = "2.5.0" +description = "Sphinx extension for BibTeX style citations." +category = "dev" +optional = false +python-versions = ">=3.6" +files = [ + {file = "sphinxcontrib-bibtex-2.5.0.tar.gz", hash = "sha256:71b42e5db0e2e284f243875326bf9936aa9a763282277d75048826fef5b00eaa"}, + {file = "sphinxcontrib_bibtex-2.5.0-py3-none-any.whl", hash = "sha256:748f726eaca6efff7731012103417ef130ecdcc09501b4d0c54283bf5f059f76"}, +] + +[package.dependencies] +docutils = ">=0.8" +importlib-metadata = {version = ">=3.6", markers = "python_version < \"3.10\""} +pybtex = ">=0.24" +pybtex-docutils = ">=1.0.0" +Sphinx = ">=2.1" + +[[package]] +name = "sphinxcontrib-devhelp" +version = "1.0.2" +description = "sphinxcontrib-devhelp is a sphinx extension which outputs Devhelp document." +category = "dev" +optional = false +python-versions = ">=3.5" +files = [ + {file = "sphinxcontrib-devhelp-1.0.2.tar.gz", hash = "sha256:ff7f1afa7b9642e7060379360a67e9c41e8f3121f2ce9164266f61b9f4b338e4"}, + {file = "sphinxcontrib_devhelp-1.0.2-py2.py3-none-any.whl", hash = "sha256:8165223f9a335cc1af7ffe1ed31d2871f325254c0423bc0c4c7cd1c1e4734a2e"}, +] + +[package.extras] +lint = ["docutils-stubs", "flake8", "mypy"] +test = ["pytest"] + +[[package]] +name = "sphinxcontrib-htmlhelp" +version = "2.0.1" +description = "sphinxcontrib-htmlhelp is a sphinx extension which renders HTML help files" +category = "dev" +optional = false +python-versions = ">=3.8" +files = [ + {file = "sphinxcontrib-htmlhelp-2.0.1.tar.gz", hash = "sha256:0cbdd302815330058422b98a113195c9249825d681e18f11e8b1f78a2f11efff"}, + {file = "sphinxcontrib_htmlhelp-2.0.1-py3-none-any.whl", hash = "sha256:c38cb46dccf316c79de6e5515e1770414b797162b23cd3d06e67020e1d2a6903"}, +] + +[package.extras] +lint = ["docutils-stubs", "flake8", "mypy"] +test = ["html5lib", "pytest"] + +[[package]] +name = "sphinxcontrib-jsmath" +version = "1.0.1" +description = "A sphinx extension which renders display math in HTML via JavaScript" +category = "dev" +optional = false +python-versions = ">=3.5" +files = [ + {file = "sphinxcontrib-jsmath-1.0.1.tar.gz", hash = "sha256:a9925e4a4587247ed2191a22df5f6970656cb8ca2bd6284309578f2153e0c4b8"}, + {file = "sphinxcontrib_jsmath-1.0.1-py2.py3-none-any.whl", hash = "sha256:2ec2eaebfb78f3f2078e73666b1415417a116cc848b72e5172e596c871103178"}, +] + +[package.extras] +test = ["flake8", "mypy", "pytest"] + +[[package]] +name = "sphinxcontrib-qthelp" +version = "1.0.3" +description = "sphinxcontrib-qthelp is a sphinx extension which outputs QtHelp document." +category = "dev" +optional = false +python-versions = ">=3.5" +files = [ + {file = "sphinxcontrib-qthelp-1.0.3.tar.gz", hash = "sha256:4c33767ee058b70dba89a6fc5c1892c0d57a54be67ddd3e7875a18d14cba5a72"}, + {file = "sphinxcontrib_qthelp-1.0.3-py2.py3-none-any.whl", hash = "sha256:bd9fc24bcb748a8d51fd4ecaade681350aa63009a347a8c14e637895444dfab6"}, +] + +[package.extras] +lint = ["docutils-stubs", "flake8", "mypy"] +test = ["pytest"] + +[[package]] +name = "sphinxcontrib-serializinghtml" +version = "1.1.5" +description = "sphinxcontrib-serializinghtml is a sphinx extension which outputs \"serialized\" HTML files (json and pickle)." +category = "dev" +optional = false +python-versions = ">=3.5" +files = [ + {file = "sphinxcontrib-serializinghtml-1.1.5.tar.gz", hash = "sha256:aa5f6de5dfdf809ef505c4895e51ef5c9eac17d0f287933eb49ec495280b6952"}, + {file = "sphinxcontrib_serializinghtml-1.1.5-py2.py3-none-any.whl", hash = "sha256:352a9a00ae864471d3a7ead8d7d79f5fc0b57e8b3f95e9867eb9eb28999b92fd"}, +] + +[package.extras] +lint = ["docutils-stubs", "flake8", "mypy"] +test = ["pytest"] + +[[package]] +name = "sympy" +version = "1.12" +description = "Computer algebra system (CAS) in Python" +category = "main" +optional = false +python-versions = ">=3.8" +files = [ + {file = "sympy-1.12-py3-none-any.whl", hash = "sha256:c3588cd4295d0c0f603d0f2ae780587e64e2efeedb3521e46b9bb1d08d184fa5"}, + {file = "sympy-1.12.tar.gz", hash = "sha256:ebf595c8dac3e0fdc4152c51878b498396ec7f30e7a914d6071e674d49420fb8"}, +] + +[package.dependencies] +mpmath = ">=0.19" + +[[package]] +name = "tensorboard" +version = "2.10.1" +description = "TensorBoard lets you watch Tensors Flow" +category = "main" +optional = false +python-versions = ">=3.6" +files = [ + {file = "tensorboard-2.10.1-py3-none-any.whl", hash = "sha256:fb9222c1750e2fa35ef170d998a1e229f626eeced3004494a8849c88c15d8c1c"}, +] + +[package.dependencies] +absl-py = ">=0.4" +google-auth = ">=1.6.3,<3" +google-auth-oauthlib = ">=0.4.1,<0.5" +grpcio = ">=1.24.3" +markdown = ">=2.6.8" +numpy = ">=1.12.0" +protobuf = ">=3.9.2,<3.20" +requests = ">=2.21.0,<3" +setuptools = ">=41.0.0" +tensorboard-data-server = ">=0.6.0,<0.7.0" +tensorboard-plugin-wit = ">=1.6.0" +werkzeug = ">=1.0.1" +wheel = ">=0.26" + +[[package]] +name = "tensorboard-data-server" +version = "0.6.1" +description = "Fast data loading for TensorBoard" +category = "main" +optional = false +python-versions = ">=3.6" +files = [ + {file = "tensorboard_data_server-0.6.1-py3-none-any.whl", hash = "sha256:809fe9887682d35c1f7d1f54f0f40f98bb1f771b14265b453ca051e2ce58fca7"}, + {file = "tensorboard_data_server-0.6.1-py3-none-macosx_10_9_x86_64.whl", hash = "sha256:fa8cef9be4fcae2f2363c88176638baf2da19c5ec90addb49b1cde05c95c88ee"}, + {file = "tensorboard_data_server-0.6.1-py3-none-manylinux2010_x86_64.whl", hash = "sha256:d8237580755e58eff68d1f3abefb5b1e39ae5c8b127cc40920f9c4fb33f4b98a"}, +] + +[[package]] +name = "tensorboard-plugin-wit" +version = "1.8.1" +description = "What-If Tool TensorBoard plugin." +category = "main" +optional = false +python-versions = "*" +files = [ + {file = "tensorboard_plugin_wit-1.8.1-py3-none-any.whl", hash = "sha256:ff26bdd583d155aa951ee3b152b3d0cffae8005dc697f72b44a8e8c2a77a8cbe"}, +] + +[[package]] +name = "tensorboardx" +version = "2.6" +description = "TensorBoardX lets you watch Tensors Flow without Tensorflow" +category = "main" +optional = true +python-versions = "*" +files = [ + {file = "tensorboardX-2.6-py2.py3-none-any.whl", hash = "sha256:24a7cd076488de1e9d15ef25371b8ebf90c4f8f622af2477c611198f03f4a606"}, + {file = "tensorboardX-2.6.tar.gz", hash = "sha256:d4c036964dd2deb075a1909832b276daa383eab3f9db519ad90b99f5aea06b0c"}, +] + +[package.dependencies] +numpy = "*" +packaging = "*" +protobuf = ">=3.8.0,<4" + +[[package]] +name = "tensorflow" +version = "2.10.0" +description = "TensorFlow is an open source machine learning framework for everyone." +category = "main" +optional = false +python-versions = ">=3.7" +files = [ + {file = "tensorflow-2.10.0-cp310-cp310-macosx_10_14_x86_64.whl", hash = "sha256:60d5b4fbbb7a1304d96352372fa032e861e98bb3f23aced7ce53bc475a2df97d"}, + {file = "tensorflow-2.10.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:25e1e898bc1df521af9a8bfe0e511124379a6414083234ec67c6ab212ad12b2f"}, + {file = "tensorflow-2.10.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e129114dc529e63af9c419b5917b3407d0d26a4c8b73e114f601a175a7eb0477"}, + {file = "tensorflow-2.10.0-cp310-cp310-win_amd64.whl", hash = "sha256:0a3b58d90fadb5bdf81a964bea73bb89019a9d1e9ac12de75375c8f65e0d7570"}, + {file = "tensorflow-2.10.0-cp37-cp37m-macosx_10_14_x86_64.whl", hash = "sha256:0701da16a3d6d34763cd9ced6467cee24c02c9abf0d1a48ba59ea5a8d0421cec"}, + {file = "tensorflow-2.10.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:64cc999ae83ddd891083141d3e5d718e3d799501a1b56c544f2ca648a8396c3e"}, + {file = "tensorflow-2.10.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d9f711c5ff04333355c83eb96ca2e1db57c9663c6fa01d68b5953a040a602a3c"}, + {file = "tensorflow-2.10.0-cp37-cp37m-win_amd64.whl", hash = "sha256:9f4677e9ab7104e73710a94ff5d2ed4b335378dcd2ac7402a68c31802a680911"}, + {file = "tensorflow-2.10.0-cp38-cp38-macosx_10_14_x86_64.whl", hash = "sha256:8773858cbf37aaad444b07605d29f5b2d8f7cd1ecbf1cce2777931b96884589c"}, + {file = "tensorflow-2.10.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5806d4645bce5eb415863d757b5f056364b9d1cfa2c34f711f69d46cac605eee"}, + {file = "tensorflow-2.10.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e85f89bc23c62d4243fad70bac902f00a234b33da8b91e2967eeef0f4b75b1e3"}, + {file = "tensorflow-2.10.0-cp38-cp38-win_amd64.whl", hash = "sha256:d9b19b5120c0b393d9e2fc72561cfa3a454ef7f1ac649d8ad0dcc98817a086a4"}, + {file = "tensorflow-2.10.0-cp39-cp39-macosx_10_14_x86_64.whl", hash = "sha256:4b542af76d93c43e9d24dcb69888793831e434dc781c9533ee07f928fce84a15"}, + {file = "tensorflow-2.10.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c588a1f34d9db51ea856aff07da9aa877c1d1d109336eee2c3bbb16dabd3f605"}, + {file = "tensorflow-2.10.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:487918f4074685e213ba247387faab34933df76939134008441cb9d3e2c95cab"}, + {file = "tensorflow-2.10.0-cp39-cp39-win_amd64.whl", hash = "sha256:741a74278f471dc21991a6c7dc802d454d42fd39515900c6363b8c38a898fb0f"}, +] + +[package.dependencies] +absl-py = ">=1.0.0" +astunparse = ">=1.6.0" +flatbuffers = ">=2.0" +gast = ">=0.2.1,<=0.4.0" +google-pasta = ">=0.1.1" +grpcio = ">=1.24.3,<2.0" +h5py = ">=2.9.0" +keras = ">=2.10.0,<2.11" +keras-preprocessing = ">=1.1.1" +libclang = ">=13.0.0" +numpy = ">=1.20" +opt-einsum = ">=2.3.2" +packaging = "*" +protobuf = ">=3.9.2,<3.20" +setuptools = "*" +six = ">=1.12.0" +tensorboard = ">=2.10,<2.11" +tensorflow-estimator = ">=2.10.0,<2.11" +tensorflow-io-gcs-filesystem = ">=0.23.1" +termcolor = ">=1.1.0" +typing-extensions = ">=3.6.6" +wrapt = ">=1.11.0" + +[[package]] +name = "tensorflow-cpu-aws" +version = "2.10.0" +description = "TensorFlow is an open source machine learning framework for everyone." +category = "main" +optional = false +python-versions = ">=3.7" +files = [ + {file = "tensorflow_cpu_aws-2.10.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b11a7004e078b17d1a472cd5a091a0efd14672294d98ab76b851ddc15faee92c"}, + {file = "tensorflow_cpu_aws-2.10.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8c8e76c48e75d0b353239fcb039c3abbfa8ee6fe2660badb6b094c59e222714e"}, + {file = "tensorflow_cpu_aws-2.10.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2fe56d8506dae3891af6b62f26f16597e8416f9c6b788fe5dd98b602136f6cf6"}, + {file = "tensorflow_cpu_aws-2.10.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:112fa013271dc3282f7c821b138126f2504892703d85d5686198eb348fa6f6d2"}, +] + +[package.dependencies] +absl-py = ">=1.0.0" +astunparse = ">=1.6.0" +flatbuffers = ">=2.0" +gast = ">=0.2.1,<=0.4.0" +google-pasta = ">=0.1.1" +grpcio = ">=1.24.3,<2.0" +h5py = ">=2.9.0" +keras = ">=2.10.0,<2.11" +keras-preprocessing = ">=1.1.1" +libclang = ">=13.0.0" +numpy = ">=1.20" +opt-einsum = ">=2.3.2" +packaging = "*" +protobuf = ">=3.9.2,<3.20" +setuptools = "*" +six = ">=1.12.0" +tensorboard = ">=2.10,<2.11" +tensorflow-estimator = ">=2.10.0,<2.11" +tensorflow-io-gcs-filesystem = ">=0.23.1" +termcolor = ">=1.1.0" +typing-extensions = ">=3.6.6" +wrapt = ">=1.11.0" + +[[package]] +name = "tensorflow-estimator" +version = "2.10.0" +description = "TensorFlow Estimator." +category = "main" +optional = false +python-versions = ">=3.7" +files = [ + {file = "tensorflow_estimator-2.10.0-py2.py3-none-any.whl", hash = "sha256:f324ea17cd57f16e33bf188711d5077e6b2e5f5a12c328d6e01a07b23888edcd"}, +] + +[[package]] +name = "tensorflow-io-gcs-filesystem" +version = "0.32.0" +description = "TensorFlow IO" +category = "main" +optional = false +python-versions = ">=3.7, <3.12" +files = [ + {file = "tensorflow_io_gcs_filesystem-0.32.0-cp310-cp310-macosx_10_14_x86_64.whl", hash = "sha256:74a7e25e83d4117a7ebb09a3f247553a5497393ab48c3ee0cf0d17b405026817"}, + {file = "tensorflow_io_gcs_filesystem-0.32.0-cp310-cp310-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:045d51bba586390d0545fcd8a18727d62b175eb142f6f4c6d719d39de40774cd"}, + {file = "tensorflow_io_gcs_filesystem-0.32.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:db682e9a510c27dd35710ba5a2c62c371e25b727741b2fe3a920355fa501e947"}, + {file = "tensorflow_io_gcs_filesystem-0.32.0-cp311-cp311-macosx_10_14_x86_64.whl", hash = "sha256:7f15fd22e592661b10de317be2f42a0f84be7bfc5e6a565fcfcb04b60d625b78"}, + {file = "tensorflow_io_gcs_filesystem-0.32.0-cp311-cp311-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:336d9b3fe6b55aea149c4f6aa1fd6ffaf27d4e5c37e55a182340b47caba38846"}, + {file = "tensorflow_io_gcs_filesystem-0.32.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:842f5f09cd756bdb3b4d0b5571b3a6f72fd534d42da938b9acf0ef462995eada"}, + {file = "tensorflow_io_gcs_filesystem-0.32.0-cp37-cp37m-macosx_10_14_x86_64.whl", hash = "sha256:1ce80e1555d6ee88dda67feddf366cc8b30252b5837a7a17303df7b06a71fc2e"}, + {file = "tensorflow_io_gcs_filesystem-0.32.0-cp37-cp37m-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:05e65d3cb6c93a7929b384d86c6369c63cbbab8a770440a3d95e094878403f9f"}, + {file = "tensorflow_io_gcs_filesystem-0.32.0-cp38-cp38-macosx_10_14_x86_64.whl", hash = "sha256:21de7dcc06eb1e7de3c022b0072d90ba35ef886578149663437aa7a6fb5bf6b3"}, + {file = "tensorflow_io_gcs_filesystem-0.32.0-cp38-cp38-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:79fdd02103b8ae9f8b89af41f744c013fa1caaea709de19833917795e3063857"}, + {file = "tensorflow_io_gcs_filesystem-0.32.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5635df0bbe40f971dc1b946e3372744b0bdfda45c38ffcd28ef53a32bb8da4da"}, + {file = "tensorflow_io_gcs_filesystem-0.32.0-cp39-cp39-macosx_10_14_x86_64.whl", hash = "sha256:122be149e5f6a030f5c2901be0cc3cb07619232f7b03889e2cdf3da1c0d4f92f"}, + {file = "tensorflow_io_gcs_filesystem-0.32.0-cp39-cp39-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:8214cdf85bea694160f9035ff395221c1e25e119784ccb4c104919b1f5dec84e"}, + {file = "tensorflow_io_gcs_filesystem-0.32.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:28202492d904a6e280cf27560791e87ac1c7566000db82065d63a70c27008af2"}, +] + +[package.extras] +tensorflow = ["tensorflow (>=2.12.0,<2.13.0)"] +tensorflow-aarch64 = ["tensorflow-aarch64 (>=2.12.0,<2.13.0)"] +tensorflow-cpu = ["tensorflow-cpu (>=2.12.0,<2.13.0)"] +tensorflow-gpu = ["tensorflow-gpu (>=2.12.0,<2.13.0)"] +tensorflow-rocm = ["tensorflow-rocm (>=2.12.0,<2.13.0)"] + +[[package]] +name = "tensorflow-probability" +version = "0.18.0" +description = "Probabilistic modeling and statistical inference in TensorFlow" +category = "main" +optional = false +python-versions = "*" +files = [ + {file = "tensorflow_probability-0.18.0-py2.py3-none-any.whl", hash = "sha256:80d57e7792c78586f12255ab4a7c359b7e86bc06d487fd629119d9e650624a18"}, +] + +[package.dependencies] +absl-py = "*" +cloudpickle = ">=1.3" +decorator = "*" +dm-tree = "*" +gast = ">=0.3.2" +numpy = ">=1.13.3" +six = ">=1.10.0" + +[package.extras] +jax = ["jax", "jaxlib"] +tfds = ["tensorflow-datasets (>=2.2.0)"] + +[[package]] +name = "termcolor" +version = "2.3.0" +description = "ANSI color formatting for output in terminal" +category = "main" +optional = false +python-versions = ">=3.7" +files = [ + {file = "termcolor-2.3.0-py3-none-any.whl", hash = "sha256:3afb05607b89aed0ffe25202399ee0867ad4d3cb4180d98aaf8eefa6a5f7d475"}, + {file = "termcolor-2.3.0.tar.gz", hash = "sha256:b5b08f68937f138fe92f6c089b99f1e2da0ae56c52b78bf7075fd95420fd9a5a"}, +] + +[package.extras] +tests = ["pytest", "pytest-cov"] + +[[package]] +name = "thewalrus" +version = "0.19.0" +description = "Open source library for hafnian calculation" +category = "main" +optional = false +python-versions = "*" +files = [ + {file = "thewalrus-0.19.0-py3-none-any.whl", hash = "sha256:07b6e2969bf5405a2df736c442b1500857438bbd2afc2053b8b600b8b0c67f97"}, + {file = "thewalrus-0.19.0.tar.gz", hash = "sha256:06ff07a14cd8cd4650d9c82b8bb8301ef9a58dcdd4bafb14841768ccf80c98b9"}, +] + +[package.dependencies] +dask = {version = "*", extras = ["delayed"]} +numba = ">=0.49.1" +numpy = ">=1.19.2" +scipy = ">=1.2.1" +sympy = ">=1.5.1" + +[[package]] +name = "threadpoolctl" +version = "3.1.0" +description = "threadpoolctl" +category = "main" +optional = true +python-versions = ">=3.6" +files = [ + {file = "threadpoolctl-3.1.0-py3-none-any.whl", hash = "sha256:8b99adda265feb6773280df41eece7b2e6561b772d21ffd52e372f999024907b"}, + {file = "threadpoolctl-3.1.0.tar.gz", hash = "sha256:a335baacfaa4400ae1f0d8e3a58d6674d2f8828e3716bb2802c44955ad391380"}, +] + +[[package]] +name = "toml" +version = "0.10.2" +description = "Python Library for Tom's Obvious, Minimal Language" +category = "dev" +optional = false +python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" +files = [ + {file = "toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"}, + {file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"}, +] + +[[package]] +name = "tomli" +version = "2.0.1" +description = "A lil' TOML parser" +category = "main" +optional = false +python-versions = ">=3.7" +files = [ + {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"}, + {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, +] + +[[package]] +name = "toolz" +version = "0.12.0" +description = "List processing tools and functional utilities" +category = "main" +optional = false +python-versions = ">=3.5" +files = [ + {file = "toolz-0.12.0-py3-none-any.whl", hash = "sha256:2059bd4148deb1884bb0eb770a3cde70e7f954cfbbdc2285f1f2de01fd21eb6f"}, + {file = "toolz-0.12.0.tar.gz", hash = "sha256:88c570861c440ee3f2f6037c4654613228ff40c93a6c25e0eba70d17282c6194"}, +] + +[[package]] +name = "tqdm" +version = "4.65.0" +description = "Fast, Extensible Progress Meter" +category = "main" +optional = false +python-versions = ">=3.7" +files = [ + {file = "tqdm-4.65.0-py3-none-any.whl", hash = "sha256:c4f53a17fe37e132815abceec022631be8ffe1b9381c2e6e30aa70edc99e9671"}, + {file = "tqdm-4.65.0.tar.gz", hash = "sha256:1871fb68a86b8fb3b59ca4cdd3dcccbc7e6d613eeed31f4c332531977b89beb5"}, +] + +[package.dependencies] +colorama = {version = "*", markers = "platform_system == \"Windows\""} + +[package.extras] +dev = ["py-make (>=0.1.0)", "twine", "wheel"] +notebook = ["ipywidgets (>=6)"] +slack = ["slack-sdk"] +telegram = ["requests"] + +[[package]] +name = "typing-extensions" +version = "4.7.1" +description = "Backported and Experimental Type Hints for Python 3.7+" +category = "main" +optional = false +python-versions = ">=3.7" +files = [ + {file = "typing_extensions-4.7.1-py3-none-any.whl", hash = "sha256:440d5dd3af93b060174bf433bccd69b0babc3b15b1a8dca43789fd7f61514b36"}, + {file = "typing_extensions-4.7.1.tar.gz", hash = "sha256:b75ddc264f0ba5615db7ba217daeb99701ad295353c45f9e95963337ceeeffb2"}, +] + +[[package]] +name = "tzdata" +version = "2023.3" +description = "Provider of IANA time zone data" +category = "main" +optional = true +python-versions = ">=2" +files = [ + {file = "tzdata-2023.3-py2.py3-none-any.whl", hash = "sha256:7e65763eef3120314099b6939b5546db7adce1e7d6f2e179e3df563c70511eda"}, + {file = "tzdata-2023.3.tar.gz", hash = "sha256:11ef1e08e54acb0d4f95bdb1be05da659673de4acbd21bf9c69e94cc5e907a3a"}, +] + +[[package]] +name = "urllib3" +version = "2.0.3" +description = "HTTP library with thread-safe connection pooling, file post, and more." +category = "main" +optional = false +python-versions = ">=3.7" +files = [ + {file = "urllib3-2.0.3-py3-none-any.whl", hash = "sha256:48e7fafa40319d358848e1bc6809b208340fafe2096f1725d05d67443d0483d1"}, + {file = "urllib3-2.0.3.tar.gz", hash = "sha256:bee28b5e56addb8226c96f7f13ac28cb4c301dd5ea8a6ca179c0b9835e032825"}, +] + +[package.extras] +brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)"] +secure = ["certifi", "cryptography (>=1.9)", "idna (>=2.0.0)", "pyopenssl (>=17.1.0)", "urllib3-secure-extra"] +socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"] +zstd = ["zstandard (>=0.18.0)"] + +[[package]] +name = "werkzeug" +version = "2.3.6" +description = "The comprehensive WSGI web application library." +category = "main" +optional = false +python-versions = ">=3.8" +files = [ + {file = "Werkzeug-2.3.6-py3-none-any.whl", hash = "sha256:935539fa1413afbb9195b24880778422ed620c0fc09670945185cce4d91a8890"}, + {file = "Werkzeug-2.3.6.tar.gz", hash = "sha256:98c774df2f91b05550078891dee5f0eb0cb797a522c757a2452b9cee5b202330"}, +] + +[package.dependencies] +MarkupSafe = ">=2.1.1" + +[package.extras] +watchdog = ["watchdog (>=2.3)"] + +[[package]] +name = "wheel" +version = "0.40.0" +description = "A built-package format for Python" +category = "main" +optional = false +python-versions = ">=3.7" +files = [ + {file = "wheel-0.40.0-py3-none-any.whl", hash = "sha256:d236b20e7cb522daf2390fa84c55eea81c5c30190f90f29ae2ca1ad8355bf247"}, + {file = "wheel-0.40.0.tar.gz", hash = "sha256:cd1196f3faee2b31968d626e1731c94f99cbdb67cf5a46e4f5656cbee7738873"}, +] + +[package.extras] +test = ["pytest (>=6.0.0)"] + +[[package]] +name = "wrapt" +version = "1.12.1" +description = "Module for decorators, wrappers and monkey patching." +category = "main" +optional = false +python-versions = "*" +files = [ + {file = "wrapt-1.12.1.tar.gz", hash = "sha256:b62ffa81fb85f4332a4f609cab4ac40709470da05643a082ec1eb88e6d9b97d7"}, +] + +[[package]] +name = "xanadu-sphinx-theme" +version = "0.1.0" +description = "Sphinx theme for Xanadu open-source Python packages" +category = "dev" +optional = false +python-versions = "*" +files = [ + {file = "xanadu-sphinx-theme-0.1.0.tar.gz", hash = "sha256:ce3c825cebf4b52a025330a907e95ab1a452e71f91c904fab7e38eccbe0d3eda"}, + {file = "xanadu_sphinx_theme-0.1.0-py3-none-any.whl", hash = "sha256:fedba15959e5abcdbf8f4ea34370ebf3bef32498eede9a7e7f14cc4ce0560b5b"}, +] + +[package.dependencies] +pillow = "*" +sphinx = "*" +sphinx-gallery = "*" + +[[package]] +name = "zipp" +version = "3.16.0" +description = "Backport of pathlib-compatible object wrapper for zip files" +category = "main" +optional = false +python-versions = ">=3.8" +files = [ + {file = "zipp-3.16.0-py3-none-any.whl", hash = "sha256:5dadc3ad0a1f825fe42ce1bce0f2fc5a13af2e6b2d386af5b0ff295bc0a287d3"}, + {file = "zipp-3.16.0.tar.gz", hash = "sha256:1876cb065531855bbe83b6c489dcf69ecc28f1068d8e95959fe8bbc77774c941"}, +] + +[package.extras] +docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] +testing = ["big-O", "jaraco.functools", "jaraco.itertools", "more-itertools", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy (>=0.9.1)", "pytest-ruff"] + +[extras] +ray = ["ray", "scikit-optimize"] + +[metadata] +lock-version = "2.0" +python-versions = ">=3.9,<3.11" +content-hash = "4bc682389a785eeb023c5eae11a3800b7e79cc3405421797fcf1bc5d6b19ef89" diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 000000000..367cc6067 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,71 @@ +[tool.poetry] +name = "mrmustard" +version = "0.5.0-dev" +description = "Differentiable quantum Gaussian circuits" +authors = ["Xanadu "] +license = "Apache License 2.0" +readme = "README.md" +include = ["pyproject.toml"] +classifiers = [ + "Development Status :: 4 - Beta", + "Environment :: Console", + "Intended Audience :: Science/Research", + "License :: OSI Approved :: Apache Software License", + "Natural Language :: English", + "Operating System :: POSIX", + "Operating System :: MacOS :: MacOS X", + "Operating System :: POSIX :: Linux", + "Operating System :: Microsoft :: Windows", + "Programming Language :: Python", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3 :: Only", + "Topic :: Scientific/Engineering :: Physics", +] + +[tool.poetry.dependencies] +python = ">=3.9,<3.11" +numpy = "1.23.5" +scipy = "1.8.0" +numba = "0.56.4" +thewalrus = "0.19.0" +tensorflow = "<=2.10.1" +tensorflow-cpu-aws = { version = "2.10.0", markers = "platform_machine == 'aarch64' or platform_machine == 'arm64'" } +tensorflow-probability = "0.18.0" +rich = "10.15.1" +matplotlib = "3.5.0" +tqdm = "^4.65.0" +ray = { version = "2.5.0", extras = ["tune"], optional = true } +scikit-optimize = { version = "^0.9.0", optional = true } + +[tool.poetry.extras] +ray = ["ray", "scikit-optimize"] + +[tool.poetry.group.dev] +optional = true + +[tool.poetry.group.dev.dependencies] +pytest = "6.2.5" +pytest-cov ="3.0.0" +hypothesis = "6.31.6" +pylint = "2.10.0" +black = ">=22.1.0" + +[tool.poetry.group.doc] +optional = true + +[tool.poetry.group.doc.dependencies] +sphinx = "^7.0.1" +docutils = "^0.20.1" +m2r2 = "^0.3.3.post2" +sphinx-autodoc-typehints = "^1.23.3" +sphinx-copybutton = "^0.5.2" +sphinx-automodapi = "^0.15.0" +sphinxcontrib-bibtex = "^2.5.0" +mistune = "0.8.4" +xanadu-sphinx-theme = "0.1.0" + +[build-system] +requires = ["poetry-core"] +build-backend = "poetry.core.masonry.api" diff --git a/requirements-dev.txt b/requirements-dev.txt deleted file mode 100644 index 5ec2222b6..000000000 --- a/requirements-dev.txt +++ /dev/null @@ -1,5 +0,0 @@ -pytest==6.2.5 -pytest-cov==3.0.0 -hypothesis==6.31.6 -pylint==2.10.0 -black>=22.1.0 diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index 46cd82df7..000000000 --- a/requirements.txt +++ /dev/null @@ -1,9 +0,0 @@ -numpy==1.23.5 -scipy==1.8.0 -numba==0.56.4 -thewalrus==0.19.0 -tensorflow==2.10.1; sys_platform != "darwin" -tensorflow_macos==2.10.0; sys_platform == "darwin" -tensorflow-probability==0.18.0 -rich==10.15.1 -matplotlib==3.5.0 diff --git a/setup.py b/setup.py deleted file mode 100644 index 77ae75209..000000000 --- a/setup.py +++ /dev/null @@ -1,70 +0,0 @@ -# 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. -import platform - -from setuptools import find_packages, setup - -with open("mrmustard/_version.py") as f: - version = f.readlines()[-1].split()[-1].strip("\"'") - -requirements = [ - "numpy", - "scipy", - "numba", - "thewalrus>=0.17.0", - "tensorflow<=2.10.1" if platform.system() != "Darwin" else "tensorflow_macos<=2.10.0", - "tensorflow-probability<=0.18.0", - "rich", - "tqdm", - "matplotlib", -] - -extra_requirements = { - "ray": ["ray[tune]", "scikit-optimize"], -} - -info = { - "name": "mrmustard", - "version": version, - "description": "Differentiable quantum Gaussian circuits", - "url": "https://github.com/XanaduAI/mrmustard", - "author": "Xanadu", - "author_email": "filippo@xanadu.ai", - "license": "Apache License 2.0", - "packages": find_packages(where="."), - "install_requires": requirements, - "extras_require": extra_requirements, - "long_description": open("README.md", encoding="utf-8").read(), - "long_description_content_type": "text/markdown", -} - -classifiers = [ - "Development Status :: 4 - Beta", - "Environment :: Console", - "Intended Audience :: Science/Research", - "License :: OSI Approved :: Apache Software License", - "Natural Language :: English", - "Operating System :: POSIX", - "Operating System :: MacOS :: MacOS X", - "Operating System :: POSIX :: Linux", - "Operating System :: Microsoft :: Windows", - "Programming Language :: Python", - "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.9", - "Programming Language :: Python :: 3.10", - "Programming Language :: Python :: 3 :: Only", - "Topic :: Scientific/Engineering :: Physics", -] - -setup(classifiers=classifiers, **(info)) From fcb9512da48303aeaa666d33e3fd35a7f43d39ca Mon Sep 17 00:00:00 2001 From: Filippo Miatto Date: Wed, 26 Jul 2023 14:45:39 -0700 Subject: [PATCH 53/53] Release v050 (#264) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # Release 0.5.0 ### New features * Optimization callback functionalities has been improved. A dedicated `Callback` class is added which is able to access the optimizer, the cost function, the parameters as well as gradients, during the optimization. In addition, multiple callbacks can be specified. This opens up the endless possiblities of customizing the the optimization progress with schedulers, trackers, heuristics, tricks, etc. [(#219)](https://github.com/XanaduAI/MrMustard/pull/219) * Tensorboard-based optimization tracking is added as a builtin `Callback` class: `TensorboardCallback`. It can automatically track costs as well as all trainable parameters during optimization in realtime. Tensorboard can be most conveniently viewed from VScode. [(#219)](https://github.com/XanaduAI/MrMustard/pull/219) ```python import numpy as np from mrmustard.training import Optimizer, TensorboardCallback def cost_fn(): ... def as_dB(cost): delta = np.sqrt(np.log(1 / (abs(cost) ** 2)) / (2 * np.pi)) cost_dB = -10 * np.log10(delta**2) return cost_dB tb_cb = TensorboardCallback(cost_converter=as_dB, track_grads=True) opt = Optimizer(euclidean_lr = 0.001); opt.minimize(cost_fn, max_steps=200, by_optimizing=[...], callbacks=tb_cb) # Logs will be stored in `tb_cb.logdir` which defaults to `./tb_logdir/...` but can be customized. # VScode can be used to open the Tensorboard frontend for live monitoring. # Or, in command line: `tensorboard --logdir={tb_cb.logdir}` and open link in browser. ``` * Gaussian states support a `bargmann` method for returning the bargmann representation. [(#235)](https://github.com/XanaduAI/MrMustard/pull/235) * The `ket` method of `State` now supports new keyword arguments `max_prob` and `max_photons`. Use them to speed-up the filling of a ket array up to a certain probability or *total* photon number. [(#235)](https://github.com/XanaduAI/MrMustard/pull/235) ```python from mrmustard.lab import Gaussian # Fills the ket array up to 99% probability or up to the |0,3>, |1,2>, |2,1>, |3,0> subspace, whichever is reached first. # The array has the autocutoff shape, unless the cutoffs are specified explicitly. ket = Gaussian(2).ket(max_prob=0.99, max_photons=3) ``` * Gaussian transformations support a `bargmann` method for returning the bargmann representation. [(#239)](https://github.com/XanaduAI/MrMustard/pull/239) * BSGate.U now supports method='vanilla' (default) and 'schwinger' (slower, but stable to any cutoff) [(#248)](https://github.com/XanaduAI/MrMustard/pull/248) ### Breaking Changes * The previous `callback` argument to `Optimizer.minimize` is now `callbacks` since we can now pass multiple callbacks to it. [(#219)](https://github.com/XanaduAI/MrMustard/pull/219) * The `opt_history` attribute of `Optimizer` does not have the placeholder at the beginning anymore. [(#235)](https://github.com/XanaduAI/MrMustard/pull/235) ### Improvements * The math module now has a submodule `lattice` for constructing recurrence relation strategies in the Fock lattice. There are a few predefined strategies in `mrmustard.math.lattice.strategies`. [(#235)](https://github.com/XanaduAI/MrMustard/pull/235) * Gradients in the Fock lattice are now computed using the vector-jacobian product. This saves a lot of memory and speeds up the optimization process by roughly 4x. [(#235)](https://github.com/XanaduAI/MrMustard/pull/235) * Tests of the compact_fock module now use hypothesis. [(#235)](https://github.com/XanaduAI/MrMustard/pull/235) * Faster implementation of the fock representation of `BSgate`, `Sgate` and `SqueezedVacuum`, ranging from 5x to 50x. [(#239)](https://github.com/XanaduAI/MrMustard/pull/239) * 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 [(#235)](https://github.com/XanaduAI/MrMustard/pull/235) * The displacement of the dual of an operation had the wrong sign [(#239)](https://github.com/XanaduAI/MrMustard/pull/239) * When projecting a Gaussian state onto a Fock state, the upper limit of the autocutoff now respect the Fock projection. [(#246)](https://github.com/XanaduAI/MrMustard/pull/246) * Fixed a bug for the algorithms that allow faster PNR sampling from Gaussian circuits using density matrices. When the cutoff of the first detector is equal to 1, the resulting density matrix is now correct. ### Documentation ### Contributors [Filippo Miatto](https://github.com/ziofil), [Zeyue Niu](https://github.com/zeyueN), [Robbe De Prins](https://github.com/rdprins), [Gabriele Gullì](https://github.com/ggulli), [Richard A. Wolf](https://github.com/ryk-wolf) --- .github/CHANGELOG.md | 7 ++++--- pyproject.toml | 2 +- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/.github/CHANGELOG.md b/.github/CHANGELOG.md index 7c5bdc20f..e137e8b9d 100644 --- a/.github/CHANGELOG.md +++ b/.github/CHANGELOG.md @@ -1,4 +1,4 @@ -# Release 0.5.0 (development release) +# Release 0.5.0 ### New features @@ -8,7 +8,7 @@ of customizing the the optimization progress with schedulers, trackers, heuristics, tricks, etc. [(#219)](https://github.com/XanaduAI/MrMustard/pull/219) -* Tensorboard based optimization tracking is added as a builtin `Callback` class: `TensorboardCallback`. +* Tensorboard-based optimization tracking is added as a builtin `Callback` class: `TensorboardCallback`. It can automatically track costs as well as all trainable parameters during optimization in realtime. Tensorboard can be most conveniently viewed from VScode. [(#219)](https://github.com/XanaduAI/MrMustard/pull/219) @@ -105,7 +105,8 @@ cutoff of the first detector is equal to 1, the resulting density matrix is now ### Contributors [Filippo Miatto](https://github.com/ziofil), [Zeyue Niu](https://github.com/zeyueN), -[Robbe De Prins](https://github.com/rdprins), [Gabriele Gullì](https://github.com/ggulli) +[Robbe De Prins](https://github.com/rdprins), [Gabriele Gullì](https://github.com/ggulli), +[Richard A. Wolf](https://github.com/ryk-wolf) --- diff --git a/pyproject.toml b/pyproject.toml index 367cc6067..1262f969a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "mrmustard" -version = "0.5.0-dev" +version = "0.5.0" description = "Differentiable quantum Gaussian circuits" authors = ["Xanadu "] license = "Apache License 2.0"