Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions perceval/runtime/error_mitigation/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
73 changes: 73 additions & 0 deletions perceval/runtime/error_mitigation/detector_balancing.py
Original file line number Diff line number Diff line change
@@ -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
2 changes: 1 addition & 1 deletion perceval/runtime/remote_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
2 changes: 1 addition & 1 deletion perceval/utils/bsdistribution.py
Original file line number Diff line number Diff line change
Expand Up @@ -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).
Expand Down
33 changes: 32 additions & 1 deletion perceval/utils/noise_model.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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
Expand All @@ -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) ?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, but this should be done before replacing invalid values by 1

@Benoit-F-Q Benoit-F-Q Jul 23, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We need precise specs on this.

Setting [ .1, .1 ], I'd imagined that modes 0 and 1 have 90% loss and other modes are perfect

The "median scaled to 100%" thing seems specific to perf <-> noise methods

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, you're right
In that case, something here should ensure that the values are not greater than 1

else:
self._loss_ratios = []

def __deepcopy__(self, memo):
return NoiseModel(**self.__dict__())
Expand All @@ -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:
Expand All @@ -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,
Expand Down
115 changes: 115 additions & 0 deletions tests/runtime/error_mitigation/test_detector_balancing.py
Original file line number Diff line number Diff line change
@@ -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.]

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We'll need to decide where to put this


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)
Loading