diff --git a/perceval/components/feed_forward_configurator.py b/perceval/components/feed_forward_configurator.py index a8ab1d92..92464ae5 100644 --- a/perceval/components/feed_forward_configurator.py +++ b/perceval/components/feed_forward_configurator.py @@ -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] = {} @@ -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: diff --git a/perceval/simulators/feed_forward_simulator.py b/perceval/simulators/feed_forward_simulator.py index 2b1fe0db..33c58682 100644 --- a/perceval/simulators/feed_forward_simulator.py +++ b/perceval/simulators/feed_forward_simulator.py @@ -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 @@ -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 @@ -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() @@ -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 @@ -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) @@ -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 @@ -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]], @@ -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(): @@ -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 diff --git a/perceval/simulators/simulator.py b/perceval/simulators/simulator.py index 5f3ae054..ce19c40a 100644 --- a/perceval/simulators/simulator.py +++ b/perceval/simulators/simulator.py @@ -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): @@ -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) @@ -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): @@ -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]: @@ -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, @@ -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} diff --git a/perceval/simulators/simulator_interface.py b/perceval/simulators/simulator_interface.py index b670a98d..55188528 100644 --- a/perceval/simulators/simulator_interface.py +++ b/perceval/simulators/simulator_interface.py @@ -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 @@ -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 diff --git a/perceval/simulators/stepper.py b/perceval/simulators/stepper.py index ea09b1bc..ca790008 100644 --- a/perceval/simulators/stepper.py +++ b/perceval/simulators/stepper.py @@ -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 diff --git a/tests/test_ff_simulator.py b/tests/test_ff_simulator.py index a7a95b35..d2dee68c 100644 --- a/tests/test_ff_simulator.py +++ b/tests/test_ff_simulator.py @@ -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 + })) diff --git a/tests/test_processor.py b/tests/test_processor.py index 785e08f9..91c745e5 100644 --- a/tests/test_processor.py +++ b/tests/test_processor.py @@ -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()