diff --git a/perceval/runtime/error_mitigation/__init__.py b/perceval/runtime/error_mitigation/__init__.py index 077c009a1..bfe6617b3 100644 --- a/perceval/runtime/error_mitigation/__init__.py +++ b/perceval/runtime/error_mitigation/__init__.py @@ -30,3 +30,4 @@ from .loss_mitigation import photon_recycling, PhotonRecycling from .abstract_mitigation import AbstractMitigation from .compilation_averaging import CompilationAveraging +from .detector_balancing import DetectorBalancing diff --git a/perceval/runtime/error_mitigation/detector_balancing.py b/perceval/runtime/error_mitigation/detector_balancing.py new file mode 100644 index 000000000..3db4350c6 --- /dev/null +++ b/perceval/runtime/error_mitigation/detector_balancing.py @@ -0,0 +1,73 @@ +# MIT License +# +# Copyright (c) 2022 Quandela +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# As a special exception, the copyright holders of exqalibur library give you +# permission to combine exqalibur with code included in the standard release of +# Perceval under the MIT license (or modified versions of such code). You may +# copy and distribute such a combined system following the terms of the MIT +# license for both exqalibur and Perceval. This exception for the usage of +# exqalibur is limited to the python bindings used by Perceval. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +from copy import deepcopy, copy +import math +import warnings + +from perceval.utils.logging import get_logger + +from .abstract_mitigation import AbstractMitigation +from ..computation import Computation + +from perceval.utils import NoiseModel +from perceval.utils.constants import KEY_RESULTS + +class DetectorBalancing(AbstractMitigation): + # Note: we do not know if it behaves correctly with Feef-Forward + + APPLY_MIN_PHOTONS = False + APPLY_LOGICAL_SELECTION = False + + def __init__(self): + """ + A mitigation process that adjusts the probabilities of each output state based on the output + loss and number of photons in each mode. + """ + + def extend_computation(self, computation: Computation, noise: NoiseModel) -> list[Computation]: + comp = deepcopy(computation) + comp.command.name = "probs" + return [comp] + + def _parse_results(self, computation: Computation, results: list[dict], noise: NoiseModel) -> dict: + ratios = noise.loss_ratios + if len(ratios) < computation.experiment.m: + get_logger().warn( + "Not enough loss ratio for DetectorBalancing: " + "defaulting missing ones to 1." + ) + ratios.extend([1.] * (computation.experiment.m - len(ratios))) + + res = copy(results[0]) # We are going to modify this to keep custom fields as much as we can + for k in res[KEY_RESULTS].keys(): + res[KEY_RESULTS][k] /= math.prod([ratios[k.photon2mode(i)] for i in range(k.n)]) + res[KEY_RESULTS].normalize() + + return res diff --git a/perceval/runtime/remote_config.py b/perceval/runtime/remote_config.py index c8b703140..42a0a2b63 100644 --- a/perceval/runtime/remote_config.py +++ b/perceval/runtime/remote_config.py @@ -53,7 +53,7 @@ class RemoteConfig: _token = None _url = None - def __init__(self, persistent_data: PersistentData = PersistentData()): + def __init__(self, persistent_data: PersistentData = PersistentData()): # !! default value is evaluated during file load, so test_remote_config_env_var_vs_cache cannot mock it !! self._persistent_data = persistent_data def _get_remote_config(self, key) -> str | dict[str, str] | None: diff --git a/perceval/utils/bsdistribution.py b/perceval/utils/bsdistribution.py index 31b361001..6c595e81c 100644 --- a/perceval/utils/bsdistribution.py +++ b/perceval/utils/bsdistribution.py @@ -36,7 +36,7 @@ import exqalibur -class BSDistribution(): +class BSDistribution(Mapping): """ Basic state distribution holding measured states (i.e. perfect Fock states), of the same size (number of modes). diff --git a/perceval/utils/noise_model.py b/perceval/utils/noise_model.py index 1475e4d17..badfbc004 100644 --- a/perceval/utils/noise_model.py +++ b/perceval/utils/noise_model.py @@ -26,6 +26,11 @@ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. +import math +import warnings + +from perceval.utils.logging import get_logger + from ._validated_params import AValidatedParam, ValidatedBool, ValidatedFloat from math import pi @@ -70,7 +75,10 @@ def __init__(self, g2_distinguishable: bool = None, transmittance: float = None, phase_imprecision: float = None, - phase_error: float = None + phase_error: float = None, + loss_ratios: list[float] = None + , transmitance_ratios_input: list[float] = None + , transmitance_ratios_output: list[float] = None ): self.brightness = brightness self.indistinguishability = indistinguishability @@ -79,6 +87,25 @@ def __init__(self, self.transmittance = transmittance self.phase_imprecision = phase_imprecision self.phase_error = phase_error + self.loss_ratios = loss_ratios + + @property + def loss_ratios(self) -> list[float] | None: + return self._loss_ratios + + @loss_ratios.setter + def loss_ratios(self, values: list[float] | None): + if values: + valids = [math.isfinite(v) and v > 0. for v in values] + if not all(valids): + get_logger().warn( + "Calibrated detector loss ratios contain non-positive or non-finite " + "values. Replacing invalid entries with 1.0." + ) + self._loss_ratios = [ v if valid else 1. for v, valid in zip(values, valids) ] + #TODO: divide by max(valid_ratios) ? + else: + self._loss_ratios = [] def __deepcopy__(self, memo): return NoiseModel(**self.__dict__()) @@ -97,6 +124,8 @@ def __dict__(self) -> dict: v = getattr(self, attr) if v != cls.__dict__[attr].default_value: res[attr] = v + if self.loss_ratios: + res["loss_ratio"] = self.loss_ratios return res def __eq__(self, other) -> bool: @@ -116,10 +145,12 @@ def perf_dict_to_noise(perfs: dict[str, float]) -> NoiseModel: nm.indistinguishability = perfs[INDISTINGUISHABILITY_KEY] / 100 if G2_KEY in perfs: nm.g2 = perfs[G2_KEY] / 100 + #TODO loss ratios return nm def noise_to_perf_dict(nm: NoiseModel) -> dict: + #TODO loss ratios return { INDISTINGUISHABILITY_KEY: nm.indistinguishability * 100, TRANSMITTANCE_KEY: nm.brightness * nm.transmittance * 100, diff --git a/tests/runtime/error_mitigation/test_detector_balancing.py b/tests/runtime/error_mitigation/test_detector_balancing.py new file mode 100644 index 000000000..e2e589c02 --- /dev/null +++ b/tests/runtime/error_mitigation/test_detector_balancing.py @@ -0,0 +1,115 @@ +# MIT License +# +# Copyright (c) 2022 Quandela +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# As a special exception, the copyright holders of exqalibur library give you +# permission to combine exqalibur with code included in the standard release of +# Perceval under the MIT license (or modified versions of such code). You may +# copy and distribute such a combined system following the terms of the MIT +# license for both exqalibur and Perceval. This exception for the usage of +# exqalibur is limited to the python bindings used by Perceval. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +from copy import copy +import random + +import pytest +from exqalibur import FockState +from exqalibur.exqalibur import PostSelect + +from perceval import DetectorBalancing, Computation, CommandFactory, Experiment, NoiseModel, Command, BSCount, \ + apply_min_photons, apply_post_select +from perceval.runtime.simulated_computer import SimulatedComputer +from perceval.utils.bsdistribution import BSDistribution +from perceval.utils.constants import KEY_SHOTS_USED +from perceval.utils.states import BasicState + +def prepare_test(): + raw_results = BSDistribution({FockState([0, 1]): 1, + FockState([1, 0]): 1, + FockState([1, 1]): 1, + FockState([2, 0]): 1, + FockState([0, 2]): 1}) + raw_results.normalize() + + shots_used = 1_000_000 + min_photons = 2 + post_select = PostSelect("[1] >= 1") + heralds = {} + + raw_results, phys_perf = apply_min_photons(raw_results, min_photons) + raw_results, log_perf = apply_post_select(raw_results, post_select, heralds, True) + + sub_results = [] + + sub_results.append({"results": copy(raw_results), + "physical_perf": phys_perf, + "logical_perf": log_perf, + "global_perf": phys_perf * log_perf, + KEY_SHOTS_USED: shots_used}) + + expected = {"results": raw_results, + "physical_perf": phys_perf, + "logical_perf": log_perf, + "global_perf": phys_perf * log_perf, + KEY_SHOTS_USED: shots_used} + + return expected, sub_results + + +def test_computation_extension(): + computation = Computation(CommandFactory.samples, Experiment()) + averaging = DetectorBalancing() + comp_list = averaging.extend_computation(computation, NoiseModel()) + + assert len(comp_list) == 1 + + assert all(comp.command.name == "probs" for comp in comp_list) + + +def test_recombination(): + expected, sub_results = prepare_test() + noise = NoiseModel() + noise.loss_ratios = [1., 1.] + + averaging = DetectorBalancing() + + computation = Computation(CommandFactory.probs, Experiment(2)) + computation.add_params(max_shots = 50000, max_samples = 10000) + + res = averaging.parse_results(computation, sub_results, noise) + + assert res == expected + + +def test_run_through(): + computer = SimulatedComputer("SLOS") + computer.mitigations = [ DetectorBalancing() ] + noise = NoiseModel() + noise.loss_ratios = [1., .5] + computer.noise = noise + + e = Experiment(2) + e.min_detected_photons_filter(1) + e.with_input(BasicState([1, 0])) + computation = Computation(CommandFactory.probs, e) + computation.add_params(max_shots = 50000, max_samples = 10000) + + computer.execute(computation)