Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Pcvl 833 use heralds at first step of feed forward #499

Open
wants to merge 3 commits into
base: release/0.12.1
Choose a base branch
from
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
4 changes: 4 additions & 0 deletions perceval/components/feed_forward_configurator.py
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,8 @@ class FFCircuitProvider(AFFConfigurator):
"""

def __init__(self, m: int, offset: int, default_circuit: ACircuit, name: str = None):
assert not isinstance(default_circuit, AFFConfigurator), \
"Can't add directly a Feed-forward configurator to a configurator (use a Processor)"
super().__init__(m, offset, default_circuit, name)
self._map: dict[BasicState, ACircuit] = {}

Expand All @@ -136,6 +138,8 @@ def circuit_map(self, circuit_map: dict[BasicState, ACircuit]):
def add_configuration(self, state, circuit: ACircuit) -> FFCircuitProvider:
state = BasicState(state)
assert state.m == self.m, f"Incorrect number of modes for state {state} (expected {self.m})"
assert not isinstance(circuit, AFFConfigurator), \
"Can't add directly a Feed-forward configurator to a configurator (use a Processor)"
if not self._blocked_circuit_size:
self._max_circuit_size = max(self._max_circuit_size, circuit.m)
else:
Expand Down
52 changes: 28 additions & 24 deletions perceval/simulators/feed_forward_simulator.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@
# SOFTWARE.
from __future__ import annotations

from sympy.combinatorics import Permutation

from perceval.components import Processor, AComponent, Barrier, PERM, IDetector, Herald, PortLocation, Source
from perceval.utils import NoiseModel, BasicState, BSDistribution, SVDistribution, StateVector, PostSelect, get_logger, \
partial_progress_callable
Expand All @@ -53,9 +55,6 @@ def __init__(self, backend: AStrongSimulationBackend):
self._noise_model = None
self._source = None

def do_postprocess(self, doit: bool):
self._postprocess = doit

def set_circuit(self, circuit: Processor | list[tuple[tuple, AComponent]]):
if isinstance(circuit, Processor):
self._components = circuit.components
Expand Down Expand Up @@ -94,7 +93,7 @@ def _probs_svd(self,
progress_callback: callable = None) -> tuple[BSDistribution, float]:

# 1: Find all the FFConfigurators that can be simulated without measuring more modes
considered_config, measured_modes = self._find_next_simulation_layer()
considered_config, measured_modes, unsafe_modes = self._find_next_simulation_layer()

# 2: Launch a simulation with the default circuits
components = self._components.copy()
Expand All @@ -108,7 +107,12 @@ def _probs_svd(self,
components[i] = (circ_r[0], config.default_circuit)

# We can't reject any state at this moment since we need all possible measured states
sim, new_input_state, new_detectors, default_proc = self._init_simulator(input_state, components, detectors)
# Except for heralds on safe modes (i.e. not subject to feed-forward anywhere)
new_heralds = {r: v for r, v in self._heralds.items() if r not in unsafe_modes} if self._heralds is not None else None
# TODO: in theory, if we can split the Postselect keeping only the safe modes,
# it can be even faster by removing more impossible measures (thus not simulating them)
sim, new_input_state, new_detectors, default_proc = self._init_simulator(input_state, components, detectors,
new_heralds=new_heralds)

# Estimation of possible measures: n for each measured mode
n = input_state.n if isinstance(input_state, BasicState) else input_state.n_max
Expand Down Expand Up @@ -162,8 +166,8 @@ def _probs_svd(self,

new_heralds = {i: state[i] for i in measured_modes}
sim, new_input_state, new_detectors, _ = self._init_simulator(input_state, components, detectors,
filter_states=True,
new_heralds=new_heralds)
filter_states=True,
new_heralds=new_heralds)

new_prog_cb = partial_progress_callable(prog_cb, j / len(default_res), (j + 1) / len(default_res))
sub_res = sim.probs_svd(new_input_state, new_detectors, new_prog_cb)
Expand All @@ -185,24 +189,28 @@ def _probs_svd(self,
res.normalize()
return res, global_perf

def _find_next_simulation_layer(self) -> tuple[list[tuple[int, AFFConfigurator]], list[int]]:
def _find_next_simulation_layer(self) -> tuple[list[tuple[int, AFFConfigurator]], list[int], set[int]]:
"""
:return: The list containing the tuples with the index in the component list
of the configuration independent FFConfigurators and their instances, and the list of the associated measured modes
of the configuration independent FFConfigurators and their instances,
the list of the associated measured modes,
and the list of modes that are touched at anytime by feed-forward configurators (including after the layer)
"""
# We can add a configurator as long as the measured mode don't come from a configurable circuit
feed_forwarded_modes: set[int] = set()
measured_modes = set()
res = []
lock_res = False

for i, (r, c) in enumerate(self._components):
if isinstance(c, AFFConfigurator):
if any(r0 in feed_forwarded_modes for r0 in r):
return res, list(measured_modes)
if not lock_res and any(r0 in feed_forwarded_modes for r0 in r):
lock_res = True

feed_forwarded_modes.update(c.config_modes(r))
res.append((i, c))
measured_modes.update(r)
if not lock_res:
res.append((i, c))
measured_modes.update(r)

elif isinstance(c, Barrier):
continue
Expand All @@ -221,7 +229,7 @@ def _find_next_simulation_layer(self) -> tuple[list[tuple[int, AFFConfigurator]]
elif any(new_mode in feed_forwarded_modes for new_mode in r):
feed_forwarded_modes.update(r)

return res, list(measured_modes)
return res, list(measured_modes), feed_forwarded_modes

def _init_simulator(self, input_state: SVDistribution,
components: list[tuple[tuple, AComponent | Processor]],
Expand Down Expand Up @@ -252,13 +260,13 @@ def _init_simulator(self, input_state: SVDistribution,
# Now the Processor has only the heralds that were possibly added by adding Processors as input, all at the end
heralded_dist = proc.generate_noisy_heralds()
if len(heralded_dist):
input_state *= heralded_dist
input_state = input_state * heralded_dist

if filter_states:
if new_heralds is not None:
for r, v in new_heralds.items():
proc.add_port(r, Herald(v), PortLocation.OUTPUT)

if new_heralds is not None:
for r, v in new_heralds.items():
proc.add_port(r, Herald(v), PortLocation.OUTPUT)
if filter_states:

if self._heralds is not None:
for r, v in self._heralds.items():
Expand All @@ -267,17 +275,13 @@ def _init_simulator(self, input_state: SVDistribution,
if self._postselect is not None:
proc.set_postselection(self._postselect)

proc.min_detected_photons_filter(self._min_detected_photons_filter)
proc.min_detected_photons_filter(self._min_detected_photons_filter if filter_states else 0)

from .simulator_factory import SimulatorFactory # Avoids a circular import

sim = SimulatorFactory.build(proc)
if self._precision is not None:
sim.set_precision(self._precision)
if filter_states:
sim.do_postprocess(self._postprocess)
else:
sim.do_postprocess(False)
sim.set_silent(True)
return sim, input_state, detectors + proc.detectors[m:], proc

Expand Down
27 changes: 11 additions & 16 deletions perceval/simulators/simulator.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,10 +67,6 @@ def __init__(self, backend: AStrongSimulationBackend):
self._logical_perf: float = 1
self._rel_precision: float = 1e-6 # Precision relative to the highest probability of interest in probs_svd
self._keep_heralds = True
self._postprocess = True

def do_postprocess(self, doit: bool):
self._postprocess = doit

@property
def precision(self):
Expand Down Expand Up @@ -244,9 +240,7 @@ def probs(self, input_state: BasicState) -> BSDistribution:
input_list = input_state.separate_state(keep_annotations=False)
self._evolve_cache(set(input_list))
result = self._merge_probability_dist(input_list)
if self._postprocess:
result, self._logical_perf = post_select_distribution(
result, self._postselect, self._heralds, self._keep_heralds)
result, self._logical_perf = post_select_distribution(result, self._postselect, self._heralds, self._keep_heralds)
return result

@dispatch(StateVector)
Expand Down Expand Up @@ -319,7 +313,8 @@ def _probs_svd_generic(self, input_dist, p_threshold, progress_callback: Callabl
exec_request = progress_callback((idx + 1) / len(decomposed_input), 'probs')
if exec_request is not None and 'cancel_requested' in exec_request and exec_request['cancel_requested']:
raise RuntimeError("Cancel requested")
res.normalize()
if len(res):
res.normalize()
return res, physical_perf

def _probs_svd_fast(self, input_dist, p_threshold, progress_callback: Callable = None):
Expand Down Expand Up @@ -401,7 +396,8 @@ def _probs_svd_fast(self, input_dist, p_threshold, progress_callback: Callable =
"""
if self._logical_perf > 0 and physical_perf > 0:
self._logical_perf = 1 - (1 - self._logical_perf) / physical_perf
res.normalize()
if len(res):
res.normalize()
return res, physical_perf

def _preprocess_svd(self, svd: SVDistribution) -> tuple[SVDistribution, float, bool, bool]:
Expand Down Expand Up @@ -457,14 +453,12 @@ def probs_svd(self,
return {'results': res, 'physical_perf': 1, 'logical_perf': 1}

if detectors:
min_photons = self._min_detected_photons_filter if self._postprocess else 0
res, phys_perf = simulate_detectors(res, detectors, min_photons)
res, phys_perf = simulate_detectors(res, detectors, self._min_detected_photons_filter)
physical_perf *= phys_perf

if self._postprocess:
res, logical_perf_contrib = post_select_distribution(res, self._postselect, self._heralds,
self._keep_heralds)
self._logical_perf *= logical_perf_contrib
res, logical_perf_contrib = post_select_distribution(res, self._postselect, self._heralds, self._keep_heralds)
self._logical_perf *= logical_perf_contrib

self.log_resources(sys._getframe().f_code.co_name, {'n': input_dist.n_max})
return {'results': res,
'physical_perf': physical_perf,
Expand Down Expand Up @@ -602,7 +596,8 @@ def evolve_svd(self,
if exec_request is not None and 'cancel_requested' in exec_request and exec_request['cancel_requested']:
raise RuntimeError("Cancel requested")
self._logical_perf = intermediary_logical_perf
new_svd.normalize()
if len(new_svd):
new_svd.normalize()
return {'results': new_svd,
'physical_perf': physical_perf,
'logical_perf': self._logical_perf}
Expand Down
7 changes: 0 additions & 7 deletions perceval/simulators/simulator_interface.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,10 +43,6 @@ def __init__(self):
def set_silent(self, silent: bool):
self._silent = silent

@abstractmethod
def do_postprocess(self, doit: bool):
pass

@abstractmethod
def set_circuit(self, circuit):
pass
Expand Down Expand Up @@ -111,9 +107,6 @@ def set_selection(self,
if heralds is not None:
self._heralds = heralds

def do_postprocess(self, doit: bool):
self._simulator.do_postprocess(doit)

@abstractmethod
def _prepare_input(self, input_state):
pass
Expand Down
3 changes: 0 additions & 3 deletions perceval/simulators/stepper.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,9 +56,6 @@ def __init__(self, backend: AStrongSimulationBackend = None):
self._C = None
self._postprocess = True

def do_postprocess(self, doit: bool):
self._postprocess = doit

def _clear_cache(self):
self._result_dict = defaultdict(lambda: {'_set': set()})
self._compiled_input = None
Expand Down
31 changes: 31 additions & 0 deletions tests/test_ff_simulator.py
Original file line number Diff line number Diff line change
Expand Up @@ -326,3 +326,34 @@ def test_with_annotated_state_vector():
BasicState([0, 2, 0, 0, 1]): .375,
BasicState([1, 1, 0, 1, 0]): .25
}))


def test_config_with_config():
proc = Processor("SLOS", 8)

# Note: please don't do this, this is just to test an edge case
cnot_proc = Processor("SLOS", 4)
cnot_proc.add(0, PERM([1, 0]))
cnot_proc.add(0, Detector.pnr())
cnot_proc.add(1, Detector.pnr())
cnot_proc.add(0, cnot)

double_not = FFCircuitProvider(2, 0, Circuit(2)).add_configuration([0, 1], cnot_proc)

proc.add(0, BS())
proc.add(0, Detector.pnr())
proc.add(1, Detector.pnr())
proc.add(0, double_not)
proc.add(4, Detector.pnr())
proc.add(5, Detector.pnr())
proc.add(4, cnot)

proc.min_detected_photons_filter(4)
proc.with_input(BasicState([1, 0, 1, 0, 1, 0, 1, 0]))

sampler = Sampler(proc)

assert sampler.probs()["results"] == pytest.approx(BSDistribution({
BasicState([1, 0, 1, 0, 1, 0, 1, 0]): .5,
BasicState([0, 1, 0, 1, 0, 1, 0, 1]): .5
}))
2 changes: 1 addition & 1 deletion tests/test_processor.py
Original file line number Diff line number Diff line change
Expand Up @@ -277,7 +277,7 @@ def test_empty_output(mock_warn):
p.min_detected_photons_filter(2)
p.with_input(BasicState([0, 1, 0]))

with LogChecker(mock_warn, expected_log_number=2): # Normalize is called twice
with LogChecker(mock_warn, expected_log_number=1): # Normalize is called once
res = p.probs()["results"]
assert res == BSDistribution()

Expand Down
Loading