From e122aea9e5a16cbd09c437847d3f6c82b00085f7 Mon Sep 17 00:00:00 2001 From: Louis Vignoli Date: Thu, 24 Feb 2022 11:48:14 +0100 Subject: [PATCH 01/47] A first gist of sequence sampler plain dataframe of samples extract samples in the nested dict form A note: basis that are unused by declared channels are arrays full of zeros, while it is simply not initilized in the dict in the simulation module. add test to compare with extraction from simulation module Add various docstrings type annotations sample per qubit, test passes add support for LocalNoises add seed to the amplitude_noise generator fix the doppler noise to use a normal distribution Write test for doppler noise. It fails. It fails probably because of the different seeds in the random generators ? refactor a bit Rename as module sampler Refactor by distinguishing the local and global extraction Remove unused import Refactor the noises to their own modules Add modulation, but no implementation Revert "Refactor the noises to their own modules" This reverts commit 37093330c57a9152604f239e79ec6940a213b432. Use strategies for _TimeSlot extraction Remove functions made useless by refactor to strategy pattern Add doctrings and make some functions private Extend test to local channels Remove dead code Add modulation Refactor a bit: docstrings, order of functions, etc. Fix _TimeSlot grouping by handling delays correctly Tidy the comments refactor the extraction strategies Docstring --- pulser/sampler.py | 314 ++++++++++++++++++++++++++ pulser/tests/test_sequence_sampler.py | 101 +++++++++ 2 files changed, 415 insertions(+) create mode 100644 pulser/sampler.py create mode 100644 pulser/tests/test_sequence_sampler.py diff --git a/pulser/sampler.py b/pulser/sampler.py new file mode 100644 index 000000000..7a30c54d9 --- /dev/null +++ b/pulser/sampler.py @@ -0,0 +1,314 @@ +"""Module _sequence_sampler contains functions to recover sequences' samples. + +One needs samples of a sequence for emulation purposes or for the driving of an +actual QPU. This module contains allows to extract samples from a sequence in a +form of a pandas.DataFrame and a nested dictionary. + + Examples: + + seq = Sequence(...) + samples = get_channel_dataframes() +""" +from __future__ import annotations + +import enum +import functools +import itertools +from dataclasses import dataclass +from typing import Callable, List, Optional, cast + +import numpy as np + +from pulser import Register +from pulser.channels import Channel +from pulser.pulse import Pulse +from pulser.sequence import QubitId, Sequence, _TimeSlot + + +@dataclass +class GlobalSamples: + """Samples gather arrays of values for amplitude, detuning and phase.""" + + amp: np.ndarray + det: np.ndarray + phase: np.ndarray + + +@dataclass +class QubitSamples: + """Gathers samples concerning a single qubit.""" + + amp: np.ndarray + det: np.ndarray + phase: np.ndarray + qubit: QubitId + + @classmethod + def from_global(cls, qubit: QubitId, s: GlobalSamples) -> QubitSamples: + """Construct a QubitSamples from a Samples instance.""" + return cls(amp=s.amp, det=s.det, phase=s.phase, qubit=qubit) + + +LocalNoise = Callable[[QubitSamples], QubitSamples] + + +# It might be better to pass a Sequence rather than a Register. + + +def doppler_noise(reg: Register, std_dev: float, seed: int = 0) -> LocalNoise: + """Generate a LocalNoise modelling the Doppler effect detuning shifts.""" + rng = np.random.default_rng(seed) + errs = rng.normal(0.0, std_dev, size=len(reg.qubit_ids)) + detunings = dict(zip(reg.qubit_ids, errs)) + + def f(s: QubitSamples) -> QubitSamples: + det = s.det + det[np.nonzero(s.det)] += detunings[s.qubit] + return QubitSamples( + amp=s.amp, + det=det, + phase=s.phase, + qubit=s.qubit, + ) + + return f + + +def amplitude_noise( + reg: Register, waist_width: float, seed: int = 0 +) -> LocalNoise: + """Generate a LocalNoise modelling the amplitude profile of laser beams. + + The laser of a global channel has a non-constant amplitude profile in the + register plane. It makes global channels act differently on each qubit, + becoming local. + """ + rng = np.random.default_rng(seed) + + def f(s: QubitSamples) -> QubitSamples: + r = np.linalg.norm(reg.qubits[s.qubit]) + + noise_amp = rng.normal(1.0, 1.0e-3) + noise_amp *= np.exp(-((r / waist_width) ** 2)) + + amp = s.amp + amp[np.nonzero(s.amp)] *= noise_amp + return QubitSamples( + amp=s.amp, + det=s.det, + phase=s.phase, + qubit=s.qubit, + ) + + return f + + +def compose_local_noises(*functions: LocalNoise) -> LocalNoise: + """Helper to compose multiple functions.""" + if functions is None: + return lambda x: x + return functools.reduce( + lambda f, g: lambda x: f(g(x)), functions, lambda x: x + ) + + +def sample( + seq: Sequence, + local_noises: Optional[list[LocalNoise]] = None, + modulation: bool = False, +) -> dict: + """Samples the given Sequence and returns a nested dictionary. + + Args: + seq (Sequence): a pulser.Sequence instance. + local_noise (Optional[list[LocalNoise]]): a list of the noise sources + to account for. + modulation (bool): a flag to account for the modulation of AOM/EOM + before sampling. + + Returns: + A nested dictionnary of the samples of the amplitude, detuning and + phase at every nanoseconds for all channels. + """ + if local_noises is None: + local_noises = [] + + d = _prepare_dict(seq, seq.get_duration()) + for ch_name in seq.declared_channels: + addr = seq.declared_channels[ch_name].addressing + basis = seq.declared_channels[ch_name].basis + + if addr == "Global": + ch_samples = _sample_global_channel(seq, ch_name) + d[addr][basis]["amp"] += ch_samples.amp + d[addr][basis]["det"] += ch_samples.det + d[addr][basis]["phase"] += ch_samples.phase + elif addr == "Local": + samples: list[QubitSamples] = [] + if modulation: + # Gatering the samples of consecutive pulses with same targets. + # These can be safely modulated. + samples = _sample_local_channel( + seq, ch_name, strategy=_group_between_retarget + ) + samples = _modulate(seq.declared_channels[ch_name], samples) + else: + samples = _sample_local_channel(seq, ch_name, _regular) + for s in samples: + if len(local_noises) > 0: + # The noises are applied in the reversed order of the list + noise_func = compose_local_noises(*local_noises) + s = noise_func(s) + d[addr][basis][s.qubit]["amp"] += s.amp + d[addr][basis][s.qubit]["det"] += s.det + d[addr][basis][s.qubit]["phase"] += s.phase + return d + + +def _prepare_dict(seq: Sequence, N: int) -> dict: + """Constructs empty dict of size N. + + Usually N is the duration of seq, but we allow for a longer one, in case of + modulation for example. + """ + + def new_qty_dict() -> dict: + return { + "amp": np.zeros(N), + "det": np.zeros(N), + "phase": np.zeros(N), + } + + def new_qdict() -> dict: + return {qubit: new_qty_dict() for qubit in seq._qids} + + if seq._in_xy: + return { + "Global": {"XY", new_qty_dict()}, + "Local": {"XY": new_qdict()}, + } + else: + return { + "Global": { + basis: new_qty_dict() + for basis in ["ground-rydberg", "digital"] + }, + "Local": { + basis: new_qdict() for basis in ["ground-rydberg", "digital"] + }, + } + + +def _sample_global_channel(seq: Sequence, ch_name: str) -> GlobalSamples: + if ch_name not in seq.declared_channels: + raise ValueError(f"{ch_name} is not declared in the given Sequence") + slots = seq._schedule[ch_name] + return _sample_slots(seq.get_duration(), *slots) + + +def _sample_local_channel( + seq: Sequence, ch_name: str, strategy: TimeSlotExtractionStrategy +) -> list[QubitSamples]: + if ch_name not in seq.declared_channels: + raise ValueError(f"{ch_name} is not declared in the given Sequence") + if seq.declared_channels[ch_name].addressing != "Local": + raise ValueError(f"{ch_name} is no a local channel") + + return strategy(seq.get_duration(), seq._schedule[ch_name]) + + +def _sample_slots(N: int, *slots: _TimeSlot) -> GlobalSamples: + samples = GlobalSamples(np.zeros(N), np.zeros(N), np.zeros(N)) + for s in slots: + if type(s.type) is str: + continue + pulse = cast(Pulse, s.type) + samples.amp[s.ti : s.tf] += pulse.amplitude.samples + samples.det[s.ti : s.tf] += pulse.detuning.samples + samples.phase[s.ti : s.tf] += pulse.phase + + return samples + + +# This strategy type is used mostly for the necessity to extract samples +# differently when taking into account the modulation of AOM/EOM. Still it's +# nice to keep modularity here, to accomodate for future needs. +TimeSlotExtractionStrategy = Callable[ + [int, List[_TimeSlot]], List[QubitSamples] +] + + +def _regular(N: int, ts: list[_TimeSlot]) -> list[QubitSamples]: + return [ + QubitSamples.from_global(q, _sample_slots(N, slot)) + for slot in ts + for q in slot.targets + ] + + +def _group_between_retarget(N: int, ts: list[_TimeSlot]) -> list[QubitSamples]: + qs: list[QubitSamples] = [] + grouped_slots = _consecutive_slots_between_retargets(ts) + for targets, group in grouped_slots: + ss = [ + QubitSamples.from_global(q, _sample_slots(N, *group)) + for q in targets + ] + qs.extend(ss) + return qs + + +class _GroupType(enum.Enum): + PULSE_AND_DELAYS = "pulses_and_delays" + TARGET = "target" + OTHER = "other" + + +def _consecutive_slots_between_retargets( + ts: list[_TimeSlot], +) -> list[tuple[list[QubitId], list[_TimeSlot]]]: + """Filter and group _TimeSlots together. + + Group the input slots by group of consecutive Pulses and delays between two + target operations. + + Returns: + A list of tuples (a, b) where a is the list of common targeted qubits + and b is a list of consecutive _TimeSlot of type Pulse or "delay". All + "target" _TimeSlots are discarded. + """ + grouped_slots: list = [] + + def key_func(x: _TimeSlot) -> _GroupType: + if isinstance(x.type, Pulse) or x.type == "delay": + return _GroupType.PULSE_AND_DELAYS + else: + return _GroupType.OTHER + + for key, group in itertools.groupby(ts, key_func): + g = list(group) + if key != _GroupType.PULSE_AND_DELAYS: + continue + grouped_slots.append((g[0].targets, g)) + + return grouped_slots + + +def _modulate(ch: Channel, samples: list[QubitSamples]) -> list[QubitSamples]: + """Modulate samples according to the hardware specs. + + Additional parameters will probably be needed (keep_end, etc). + """ + modulated_samples: list[QubitSamples] = [] + for s in samples: + + modulated_samples.append( + QubitSamples( + amp=ch.modulate(s.amp), + det=ch.modulate(s.det), + phase=s.phase * np.ones(len(ch.modulate(s.amp))), + qubit=s.qubit, + ) + ) + + return modulated_samples diff --git a/pulser/tests/test_sequence_sampler.py b/pulser/tests/test_sequence_sampler.py new file mode 100644 index 000000000..1398f72db --- /dev/null +++ b/pulser/tests/test_sequence_sampler.py @@ -0,0 +1,101 @@ +import numpy as np +import pytest + +import pulser +from pulser.sampler import doppler_noise, sample +from pulser.devices import MockDevice +from pulser.pulse import Pulse +from pulser.simulation.simconfig import SimConfig +from pulser.waveforms import BlackmanWaveform + +# from deepdiff import DeepDiff + + +def test_sequence_sampler(seq): + """Check against the legacy sample extraction in the simulation module.""" + samples = sample(seq) + sim = pulser.Simulation(seq) + + # Exclude the digital basis, since filled with zero vs empty. + # it's just a way to check the coherence + global_keys = [ + ("Global", basis, qty) + for basis in ["ground-rydberg"] + for qty in ["amp", "det", "phase"] + ] + local_keys = [ + ("Local", basis, qubit, qty) + for basis in ["ground-rydberg"] + for qubit in seq._qids + for qty in ["amp", "det", "phase"] + ] + + for k in global_keys: + np.testing.assert_array_equal( + samples[k[0]][k[1]][k[2]], sim.samples[k[0]][k[1]][k[2]] + ) + + for k in local_keys: + np.testing.assert_array_equal( + samples[k[0]][k[1]][k[2]][k[3]], + sim.samples[k[0]][k[1]][k[2]][k[3]], + ) + + # print(DeepDiff(samples, sim.samples)) + # The deepdiff shows that there is no dict for unused basis in sim.samples, + # where it's a zero dict for _sequence_sampler.sample + + +def test_doppler_noise(seq): + + MASS = 1.45e-25 # kg + KB = 1.38e-23 # J/K + KEFF = 8.7 # µm^-1 + doppler_sigma = KEFF * np.sqrt(KB * 50.0e-6 / MASS) + + local_noises = [doppler_noise(seq.register, doppler_sigma, seed=0)] + samples = sample(seq, local_noises=local_noises) + got = samples["Local"]["ground-rydberg"]["q0"]["det"] + + np.random.seed(0) + sim = pulser.Simulation(seq) + sim.add_config(SimConfig("doppler")) + sim._extract_samples() + want = sim.samples["Local"]["ground-rydberg"]["q0"]["det"] + + np.testing.assert_array_equal(got, want) + + +@pytest.fixture +def seq() -> pulser.Sequence: + reg = pulser.Register.from_coordinates( + np.array([[0.0, 0.0], [2.0, 0.0]]), prefix="q" + ) + seq = pulser.Sequence(reg, MockDevice) + seq.declare_channel("ch0", "rydberg_global") + seq.declare_channel("ch1", "rydberg_local", initial_target="q0") + seq.add( + Pulse.ConstantDetuning(BlackmanWaveform(100, np.pi / 8), 0.0, 0.0), + "ch0", + ) + seq.delay(20, "ch0") + seq.add( + Pulse.ConstantAmplitude(0.0, BlackmanWaveform(100, np.pi / 8), 0.0), + "ch0", + ) + seq.add( + Pulse.ConstantDetuning(BlackmanWaveform(100, np.pi / 8), 0.0, 0.0), + "ch1", + ) + seq.target("q1", "ch1") + seq.add( + Pulse.ConstantAmplitude(1.0, BlackmanWaveform(100, np.pi / 8), 0.0), + "ch1", + ) + seq.target(["q0", "q1"], "ch1") + seq.add( + Pulse.ConstantDetuning(BlackmanWaveform(100, np.pi / 8), 0.0, 0.0), + "ch1", + ) + seq.measure() + return seq From 94fbdb2ceef6073170cef705291e7e2df76a41bb Mon Sep 17 00:00:00 2001 From: Louis Vignoli Date: Mon, 14 Mar 2022 11:38:08 +0100 Subject: [PATCH 02/47] Refactor sampler in independent module --- pulser/sampler/__init__.py | 22 ++++++ pulser/sampler/noises.py | 70 +++++++++++++++++ pulser/{ => sampler}/sampler.py | 103 +------------------------- pulser/sampler/samples.py | 32 ++++++++ pulser/tests/test_sequence_sampler.py | 3 +- 5 files changed, 130 insertions(+), 100 deletions(-) create mode 100644 pulser/sampler/__init__.py create mode 100644 pulser/sampler/noises.py rename pulser/{ => sampler}/sampler.py (71%) create mode 100644 pulser/sampler/samples.py diff --git a/pulser/sampler/__init__.py b/pulser/sampler/__init__.py new file mode 100644 index 000000000..1987e67e3 --- /dev/null +++ b/pulser/sampler/__init__.py @@ -0,0 +1,22 @@ +# Copyright 2020 Pulser Development Team +# +# 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. + +"""Module sampler allow the sampling of pulser sequences. + +Samples of a sequence are needed for plotting and simulation. + + Typical usage: + + sampler.sample(sequence) +""" diff --git a/pulser/sampler/noises.py b/pulser/sampler/noises.py new file mode 100644 index 000000000..bbabd3a13 --- /dev/null +++ b/pulser/sampler/noises.py @@ -0,0 +1,70 @@ +"""Contains the noise models.""" +import functools +from typing import Callable + +import numpy as np +from samples import QubitSamples + +from pulser.register import Register + +LocalNoise = Callable[[QubitSamples], QubitSamples] + + +# It might be better to pass a Sequence rather than a Register. + + +def doppler_noise(reg: Register, std_dev: float, seed: int = 0) -> LocalNoise: + """Generate a LocalNoise modelling the Doppler effect detuning shifts.""" + rng = np.random.default_rng(seed) + errs = rng.normal(0.0, std_dev, size=len(reg.qubit_ids)) + detunings = dict(zip(reg.qubit_ids, errs)) + + def f(s: QubitSamples) -> QubitSamples: + det = s.det + det[np.nonzero(s.det)] += detunings[s.qubit] + return QubitSamples( + amp=s.amp, + det=det, + phase=s.phase, + qubit=s.qubit, + ) + + return f + + +def amplitude_noise( + reg: Register, waist_width: float, seed: int = 0 +) -> LocalNoise: + """Generate a LocalNoise modelling the amplitude profile of laser beams. + + The laser of a global channel has a non-constant amplitude profile in the + register plane. It makes global channels act differently on each qubit, + becoming local. + """ + rng = np.random.default_rng(seed) + + def f(s: QubitSamples) -> QubitSamples: + r = np.linalg.norm(reg.qubits[s.qubit]) + + noise_amp = rng.normal(1.0, 1.0e-3) + noise_amp *= np.exp(-((r / waist_width) ** 2)) + + amp = s.amp + amp[np.nonzero(s.amp)] *= noise_amp + return QubitSamples( + amp=s.amp, + det=s.det, + phase=s.phase, + qubit=s.qubit, + ) + + return f + + +def compose_local_noises(*functions: LocalNoise) -> LocalNoise: + """Helper to compose multiple functions.""" + if functions is None: + return lambda x: x + return functools.reduce( + lambda f, g: lambda x: f(g(x)), functions, lambda x: x + ) diff --git a/pulser/sampler.py b/pulser/sampler/sampler.py similarity index 71% rename from pulser/sampler.py rename to pulser/sampler/sampler.py index 7a30c54d9..6902f8e1e 100644 --- a/pulser/sampler.py +++ b/pulser/sampler/sampler.py @@ -1,117 +1,22 @@ -"""Module _sequence_sampler contains functions to recover sequences' samples. +"""Expose the sample() functions. -One needs samples of a sequence for emulation purposes or for the driving of an -actual QPU. This module contains allows to extract samples from a sequence in a -form of a pandas.DataFrame and a nested dictionary. - - Examples: - - seq = Sequence(...) - samples = get_channel_dataframes() +It contains many helpers. """ from __future__ import annotations import enum -import functools import itertools -from dataclasses import dataclass from typing import Callable, List, Optional, cast import numpy as np -from pulser import Register from pulser.channels import Channel from pulser.pulse import Pulse +from pulser.sampler.noises import LocalNoise, compose_local_noises +from pulser.sampler.samples import GlobalSamples, QubitSamples from pulser.sequence import QubitId, Sequence, _TimeSlot -@dataclass -class GlobalSamples: - """Samples gather arrays of values for amplitude, detuning and phase.""" - - amp: np.ndarray - det: np.ndarray - phase: np.ndarray - - -@dataclass -class QubitSamples: - """Gathers samples concerning a single qubit.""" - - amp: np.ndarray - det: np.ndarray - phase: np.ndarray - qubit: QubitId - - @classmethod - def from_global(cls, qubit: QubitId, s: GlobalSamples) -> QubitSamples: - """Construct a QubitSamples from a Samples instance.""" - return cls(amp=s.amp, det=s.det, phase=s.phase, qubit=qubit) - - -LocalNoise = Callable[[QubitSamples], QubitSamples] - - -# It might be better to pass a Sequence rather than a Register. - - -def doppler_noise(reg: Register, std_dev: float, seed: int = 0) -> LocalNoise: - """Generate a LocalNoise modelling the Doppler effect detuning shifts.""" - rng = np.random.default_rng(seed) - errs = rng.normal(0.0, std_dev, size=len(reg.qubit_ids)) - detunings = dict(zip(reg.qubit_ids, errs)) - - def f(s: QubitSamples) -> QubitSamples: - det = s.det - det[np.nonzero(s.det)] += detunings[s.qubit] - return QubitSamples( - amp=s.amp, - det=det, - phase=s.phase, - qubit=s.qubit, - ) - - return f - - -def amplitude_noise( - reg: Register, waist_width: float, seed: int = 0 -) -> LocalNoise: - """Generate a LocalNoise modelling the amplitude profile of laser beams. - - The laser of a global channel has a non-constant amplitude profile in the - register plane. It makes global channels act differently on each qubit, - becoming local. - """ - rng = np.random.default_rng(seed) - - def f(s: QubitSamples) -> QubitSamples: - r = np.linalg.norm(reg.qubits[s.qubit]) - - noise_amp = rng.normal(1.0, 1.0e-3) - noise_amp *= np.exp(-((r / waist_width) ** 2)) - - amp = s.amp - amp[np.nonzero(s.amp)] *= noise_amp - return QubitSamples( - amp=s.amp, - det=s.det, - phase=s.phase, - qubit=s.qubit, - ) - - return f - - -def compose_local_noises(*functions: LocalNoise) -> LocalNoise: - """Helper to compose multiple functions.""" - if functions is None: - return lambda x: x - return functools.reduce( - lambda f, g: lambda x: f(g(x)), functions, lambda x: x - ) - - def sample( seq: Sequence, local_noises: Optional[list[LocalNoise]] = None, diff --git a/pulser/sampler/samples.py b/pulser/sampler/samples.py new file mode 100644 index 000000000..23e0542df --- /dev/null +++ b/pulser/sampler/samples.py @@ -0,0 +1,32 @@ +"""Defines samples dataclasses.""" +from __future__ import annotations + +from dataclasses import dataclass + +import numpy as np + +from pulser.sequence import QubitId + + +@dataclass +class GlobalSamples: + """Samples gather arrays of values for amplitude, detuning and phase.""" + + amp: np.ndarray + det: np.ndarray + phase: np.ndarray + + +@dataclass +class QubitSamples: + """Gathers samples concerning a single qubit.""" + + amp: np.ndarray + det: np.ndarray + phase: np.ndarray + qubit: QubitId + + @classmethod + def from_global(cls, qubit: QubitId, s: GlobalSamples) -> QubitSamples: + """Construct a QubitSamples from a Samples instance.""" + return cls(amp=s.amp, det=s.det, phase=s.phase, qubit=qubit) diff --git a/pulser/tests/test_sequence_sampler.py b/pulser/tests/test_sequence_sampler.py index 1398f72db..2ad6e2068 100644 --- a/pulser/tests/test_sequence_sampler.py +++ b/pulser/tests/test_sequence_sampler.py @@ -2,7 +2,8 @@ import pytest import pulser -from pulser.sampler import doppler_noise, sample +from pulser.sampler.noises import doppler_noise +from sampler.sampler import sample from pulser.devices import MockDevice from pulser.pulse import Pulse from pulser.simulation.simconfig import SimConfig From f0277572e41fca8696654149087e51f2b966064c Mon Sep 17 00:00:00 2001 From: Louis Vignoli Date: Mon, 14 Mar 2022 11:50:20 +0100 Subject: [PATCH 03/47] Consistent support for noises, SLM and modulation of Global and Local Deprecate temporarily the amplitude_noise as it misses the point It urges for a correct implementation Refactor sample writing Allow local decay with on SLM Add support for noises on global channel Add TODO for global modulation Fix usage of noises.apply() Fix modulation and apply SLM on local channels as well Fix imports --- pulser/sampler/noises.py | 13 ++++++ pulser/sampler/sampler.py | 96 ++++++++++++++++++++++++++++++--------- 2 files changed, 88 insertions(+), 21 deletions(-) diff --git a/pulser/sampler/noises.py b/pulser/sampler/noises.py index bbabd3a13..e53eca91e 100644 --- a/pulser/sampler/noises.py +++ b/pulser/sampler/noises.py @@ -1,4 +1,6 @@ """Contains the noise models.""" +from __future__ import annotations + import functools from typing import Callable @@ -32,6 +34,8 @@ def f(s: QubitSamples) -> QubitSamples: return f +# ! For now "malformed". It should be used only for global channels. The +# ! current LocalNoise type seems not suited for this use. def amplitude_noise( reg: Register, waist_width: float, seed: int = 0 ) -> LocalNoise: @@ -68,3 +72,12 @@ def compose_local_noises(*functions: LocalNoise) -> LocalNoise: return functools.reduce( lambda f, g: lambda x: f(g(x)), functions, lambda x: x ) + + +def apply( + samples: list[QubitSamples], noises: list[LocalNoise] +) -> list[QubitSamples]: + """Apply a list of noises on a list of QubitSamples.""" + tot_noise = compose_local_noises(*noises) + + return [tot_noise(s) for s in samples] diff --git a/pulser/sampler/sampler.py b/pulser/sampler/sampler.py index 6902f8e1e..77efcea3b 100644 --- a/pulser/sampler/sampler.py +++ b/pulser/sampler/sampler.py @@ -10,9 +10,10 @@ import numpy as np +import pulser.sampler.noises as noises from pulser.channels import Channel from pulser.pulse import Pulse -from pulser.sampler.noises import LocalNoise, compose_local_noises +from pulser.sampler.noises import LocalNoise from pulser.sampler.samples import GlobalSamples, QubitSamples from pulser.sequence import QubitId, Sequence, _TimeSlot @@ -20,6 +21,7 @@ def sample( seq: Sequence, local_noises: Optional[list[LocalNoise]] = None, + global_noises: Optional[list[LocalNoise]] = None, modulation: bool = False, ) -> dict: """Samples the given Sequence and returns a nested dictionary. @@ -37,39 +39,79 @@ def sample( """ if local_noises is None: local_noises = [] + if global_noises is None: + global_noises = [] + # The noises are applied in the reversed order of the list d = _prepare_dict(seq, seq.get_duration()) for ch_name in seq.declared_channels: addr = seq.declared_channels[ch_name].addressing basis = seq.declared_channels[ch_name].basis + samples: list[QubitSamples] = [] + if addr == "Global": + # 1. determine if decay + # 2. extract samples + # 3. modulate + # 4. apply noises/SLM + # 5. write samples + + decay = len(seq._slm_mask_targets) > 0 or len(global_noises) > 0 + ch_samples = _sample_global_channel(seq, ch_name) - d[addr][basis]["amp"] += ch_samples.amp - d[addr][basis]["det"] += ch_samples.det - d[addr][basis]["phase"] += ch_samples.phase + if modulation: + ch_samples = _modulate_global( + seq.declared_channels[ch_name], ch_samples + ) + + if not decay: + _write_global_samples(d, basis, ch_samples) + else: + qs = seq._qids - seq._slm_mask_targets + samples = [QubitSamples.from_global(q, ch_samples) for q in qs] + samples = noises.apply(samples, global_noises) + _write_local_samples(d, basis, samples) + elif addr == "Local": - samples: list[QubitSamples] = [] + # 1. determine if modulation + # 2. extract samples (depends on modulation) + # 3. apply noises/SLM + # 4. write samples if modulation: - # Gatering the samples of consecutive pulses with same targets. - # These can be safely modulated. + # gatering the samples of consecutive pulses with same targets + # that can be safely modulated. samples = _sample_local_channel( - seq, ch_name, strategy=_group_between_retarget + seq, ch_name, _group_between_retarget + ) + samples = _modulate_local( + seq.declared_channels[ch_name], samples ) - samples = _modulate(seq.declared_channels[ch_name], samples) else: samples = _sample_local_channel(seq, ch_name, _regular) - for s in samples: - if len(local_noises) > 0: - # The noises are applied in the reversed order of the list - noise_func = compose_local_noises(*local_noises) - s = noise_func(s) - d[addr][basis][s.qubit]["amp"] += s.amp - d[addr][basis][s.qubit]["det"] += s.det - d[addr][basis][s.qubit]["phase"] += s.phase + qs = seq._qids - seq._slm_mask_targets + samples = [s for s in samples if s.qubit in qs] + samples = noises.apply(samples, local_noises) + _write_local_samples(d, basis, samples) + return d +def _write_global_samples(d: dict, basis: str, samples: GlobalSamples) -> None: + d["Global"][basis]["amp"] += samples.amp + d["Global"][basis]["det"] += samples.det + d["Global"][basis]["phase"] += samples.phase + + +def _write_local_samples( + d: dict, basis: str, samples: list[QubitSamples] +) -> None: + for s in samples: + d["Local"][basis][s.qubit]["amp"] += s.amp + d["Local"][basis][s.qubit]["det"] += s.det + d["Local"][basis][s.qubit]["phase"] += s.phase + + def _prepare_dict(seq: Sequence, N: int) -> dict: """Constructs empty dict of size N. @@ -199,14 +241,27 @@ def key_func(x: _TimeSlot) -> _GroupType: return grouped_slots -def _modulate(ch: Channel, samples: list[QubitSamples]) -> list[QubitSamples]: - """Modulate samples according to the hardware specs. +def _modulate_global(ch: Channel, samples: GlobalSamples) -> GlobalSamples: + """Modulate global samples according to the hardware specs. + + Additional parameters will probably be needed (keep_end, etc). + """ + return GlobalSamples( + amp=ch.modulate(samples.amp), + det=ch.modulate(samples.det), + phase=ch.modulate(samples.phase), + ) + + +def _modulate_local( + ch: Channel, samples: list[QubitSamples] +) -> list[QubitSamples]: + """Modulate local samples according to the hardware specs. Additional parameters will probably be needed (keep_end, etc). """ modulated_samples: list[QubitSamples] = [] for s in samples: - modulated_samples.append( QubitSamples( amp=ch.modulate(s.amp), @@ -215,5 +270,4 @@ def _modulate(ch: Channel, samples: list[QubitSamples]) -> list[QubitSamples]: qubit=s.qubit, ) ) - return modulated_samples From 5b5f95b853afc14c863decf6c8d188c31b9b1b06 Mon Sep 17 00:00:00 2001 From: Louis Vignoli Date: Mon, 14 Mar 2022 17:54:38 +0100 Subject: [PATCH 04/47] Refactor the noises module Also fix imports for mypy compliance --- pulser/sampler/noises.py | 55 ++++++++++++--------------- pulser/sampler/sampler.py | 6 +-- pulser/tests/test_sequence_sampler.py | 6 +-- 3 files changed, 30 insertions(+), 37 deletions(-) diff --git a/pulser/sampler/noises.py b/pulser/sampler/noises.py index e53eca91e..2353c9283 100644 --- a/pulser/sampler/noises.py +++ b/pulser/sampler/noises.py @@ -5,40 +5,14 @@ from typing import Callable import numpy as np -from samples import QubitSamples from pulser.register import Register +from pulser.sampler.samples import QubitSamples -LocalNoise = Callable[[QubitSamples], QubitSamples] +NoiseModel = Callable[[QubitSamples], QubitSamples] -# It might be better to pass a Sequence rather than a Register. - - -def doppler_noise(reg: Register, std_dev: float, seed: int = 0) -> LocalNoise: - """Generate a LocalNoise modelling the Doppler effect detuning shifts.""" - rng = np.random.default_rng(seed) - errs = rng.normal(0.0, std_dev, size=len(reg.qubit_ids)) - detunings = dict(zip(reg.qubit_ids, errs)) - - def f(s: QubitSamples) -> QubitSamples: - det = s.det - det[np.nonzero(s.det)] += detunings[s.qubit] - return QubitSamples( - amp=s.amp, - det=det, - phase=s.phase, - qubit=s.qubit, - ) - - return f - - -# ! For now "malformed". It should be used only for global channels. The -# ! current LocalNoise type seems not suited for this use. -def amplitude_noise( - reg: Register, waist_width: float, seed: int = 0 -) -> LocalNoise: +def amplitude(reg: Register, waist_width: float, seed: int = 0) -> NoiseModel: """Generate a LocalNoise modelling the amplitude profile of laser beams. The laser of a global channel has a non-constant amplitude profile in the @@ -65,7 +39,26 @@ def f(s: QubitSamples) -> QubitSamples: return f -def compose_local_noises(*functions: LocalNoise) -> LocalNoise: +def doppler(reg: Register, std_dev: float, seed: int = 0) -> NoiseModel: + """Generate a LocalNoise modelling the Doppler effect detuning shifts.""" + rng = np.random.default_rng(seed) + errs = rng.normal(0.0, std_dev, size=len(reg.qubit_ids)) + detunings = dict(zip(reg.qubit_ids, errs)) + + def f(s: QubitSamples) -> QubitSamples: + det = s.det + det[np.nonzero(s.det)] += detunings[s.qubit] + return QubitSamples( + amp=s.amp, + det=det, + phase=s.phase, + qubit=s.qubit, + ) + + return f + + +def compose_local_noises(*functions: NoiseModel) -> NoiseModel: """Helper to compose multiple functions.""" if functions is None: return lambda x: x @@ -75,7 +68,7 @@ def compose_local_noises(*functions: LocalNoise) -> LocalNoise: def apply( - samples: list[QubitSamples], noises: list[LocalNoise] + samples: list[QubitSamples], noises: list[NoiseModel] ) -> list[QubitSamples]: """Apply a list of noises on a list of QubitSamples.""" tot_noise = compose_local_noises(*noises) diff --git a/pulser/sampler/sampler.py b/pulser/sampler/sampler.py index 77efcea3b..54935edd2 100644 --- a/pulser/sampler/sampler.py +++ b/pulser/sampler/sampler.py @@ -13,16 +13,16 @@ import pulser.sampler.noises as noises from pulser.channels import Channel from pulser.pulse import Pulse -from pulser.sampler.noises import LocalNoise +from pulser.sampler.noises import NoiseModel from pulser.sampler.samples import GlobalSamples, QubitSamples from pulser.sequence import QubitId, Sequence, _TimeSlot def sample( seq: Sequence, - local_noises: Optional[list[LocalNoise]] = None, - global_noises: Optional[list[LocalNoise]] = None, modulation: bool = False, + local_noises: Optional[list[NoiseModel]] = None, + global_noises: Optional[list[NoiseModel]] = None, ) -> dict: """Samples the given Sequence and returns a nested dictionary. diff --git a/pulser/tests/test_sequence_sampler.py b/pulser/tests/test_sequence_sampler.py index 2ad6e2068..dd6219f9b 100644 --- a/pulser/tests/test_sequence_sampler.py +++ b/pulser/tests/test_sequence_sampler.py @@ -2,10 +2,10 @@ import pytest import pulser -from pulser.sampler.noises import doppler_noise -from sampler.sampler import sample from pulser.devices import MockDevice from pulser.pulse import Pulse +from pulser.sampler.noises import doppler +from pulser.sampler.sampler import sample from pulser.simulation.simconfig import SimConfig from pulser.waveforms import BlackmanWaveform @@ -54,7 +54,7 @@ def test_doppler_noise(seq): KEFF = 8.7 # µm^-1 doppler_sigma = KEFF * np.sqrt(KB * 50.0e-6 / MASS) - local_noises = [doppler_noise(seq.register, doppler_sigma, seed=0)] + local_noises = [doppler(seq.register, doppler_sigma, seed=0)] samples = sample(seq, local_noises=local_noises) got = samples["Local"]["ground-rydberg"]["q0"]["det"] From 7459af5e522e3371d9df735b59a2a3c4fce9fed8 Mon Sep 17 00:00:00 2001 From: Louis Vignoli Date: Mon, 14 Mar 2022 18:34:34 +0100 Subject: [PATCH 05/47] Improve docstrings and small refactoring --- pulser/sampler/noises.py | 15 +++- pulser/sampler/sampler.py | 108 +++++++++++++++----------- pulser/sampler/samples.py | 6 +- pulser/tests/test_sequence_sampler.py | 2 +- 4 files changed, 78 insertions(+), 53 deletions(-) diff --git a/pulser/sampler/noises.py b/pulser/sampler/noises.py index 2353c9283..e994a8fdc 100644 --- a/pulser/sampler/noises.py +++ b/pulser/sampler/noises.py @@ -10,10 +10,17 @@ from pulser.sampler.samples import QubitSamples NoiseModel = Callable[[QubitSamples], QubitSamples] +"""A function that apply some noise on a list of QubitSamples. + +A NoiseModel corresponds to a source of noises present in a device which is +relevant when sampling the input pulses. Physical effects contributing to +modifications of the shined amplitude, detuning and phase felt by qubits of the +register are susceptible to be implemented by a NoiseModel. +""" def amplitude(reg: Register, waist_width: float, seed: int = 0) -> NoiseModel: - """Generate a LocalNoise modelling the amplitude profile of laser beams. + """Generate a NoiseModel for the gaussian amplitude profile of laser beams. The laser of a global channel has a non-constant amplitude profile in the register plane. It makes global channels act differently on each qubit, @@ -40,7 +47,7 @@ def f(s: QubitSamples) -> QubitSamples: def doppler(reg: Register, std_dev: float, seed: int = 0) -> NoiseModel: - """Generate a LocalNoise modelling the Doppler effect detuning shifts.""" + """Generate a NoiseModel for the Doppler effect detuning shifts.""" rng = np.random.default_rng(seed) errs = rng.normal(0.0, std_dev, size=len(reg.qubit_ids)) detunings = dict(zip(reg.qubit_ids, errs)) @@ -59,7 +66,7 @@ def f(s: QubitSamples) -> QubitSamples: def compose_local_noises(*functions: NoiseModel) -> NoiseModel: - """Helper to compose multiple functions.""" + """Helper to compose multiple NoiseModel.""" if functions is None: return lambda x: x return functools.reduce( @@ -70,7 +77,7 @@ def compose_local_noises(*functions: NoiseModel) -> NoiseModel: def apply( samples: list[QubitSamples], noises: list[NoiseModel] ) -> list[QubitSamples]: - """Apply a list of noises on a list of QubitSamples.""" + """Apply a list of NoiseModel on a list of QubitSamples.""" tot_noise = compose_local_noises(*noises) return [tot_noise(s) for s in samples] diff --git a/pulser/sampler/sampler.py b/pulser/sampler/sampler.py index 54935edd2..d0651bddf 100644 --- a/pulser/sampler/sampler.py +++ b/pulser/sampler/sampler.py @@ -14,31 +14,33 @@ from pulser.channels import Channel from pulser.pulse import Pulse from pulser.sampler.noises import NoiseModel -from pulser.sampler.samples import GlobalSamples, QubitSamples +from pulser.sampler.samples import QubitSamples, Samples from pulser.sequence import QubitId, Sequence, _TimeSlot def sample( seq: Sequence, modulation: bool = False, - local_noises: Optional[list[NoiseModel]] = None, + common_noises: Optional[list[NoiseModel]] = None, global_noises: Optional[list[NoiseModel]] = None, ) -> dict: """Samples the given Sequence and returns a nested dictionary. Args: seq (Sequence): a pulser.Sequence instance. - local_noise (Optional[list[LocalNoise]]): a list of the noise sources - to account for. modulation (bool): a flag to account for the modulation of AOM/EOM before sampling. + common_noises (Optional[list[LocalNoise]]): a list of the noise sources + for all channels. + global_noises (Optional[list[LocalNoise]]): a list of the noise sources + for global channels. Returns: A nested dictionnary of the samples of the amplitude, detuning and phase at every nanoseconds for all channels. """ - if local_noises is None: - local_noises = [] + if common_noises is None: + common_noises = [] if global_noises is None: global_noises = [] # The noises are applied in the reversed order of the list @@ -48,10 +50,10 @@ def sample( addr = seq.declared_channels[ch_name].addressing basis = seq.declared_channels[ch_name].basis - samples: list[QubitSamples] = [] + ls: list[QubitSamples] = [] if addr == "Global": - # 1. determine if decay + # 1. determine if the global channel decay to a local one # 2. extract samples # 3. modulate # 4. apply noises/SLM @@ -59,19 +61,17 @@ def sample( decay = len(seq._slm_mask_targets) > 0 or len(global_noises) > 0 - ch_samples = _sample_global_channel(seq, ch_name) + gs = _sample_global_channel(seq, ch_name) if modulation: - ch_samples = _modulate_global( - seq.declared_channels[ch_name], ch_samples - ) + gs = _modulate_global(seq.declared_channels[ch_name], gs) if not decay: - _write_global_samples(d, basis, ch_samples) + _write_global_samples(d, basis, gs) else: qs = seq._qids - seq._slm_mask_targets - samples = [QubitSamples.from_global(q, ch_samples) for q in qs] - samples = noises.apply(samples, global_noises) - _write_local_samples(d, basis, samples) + ls = [QubitSamples.from_global(q, gs) for q in qs] + ls = noises.apply(ls, common_noises + global_noises) + _write_local_samples(d, basis, ls) elif addr == "Local": # 1. determine if modulation @@ -79,25 +79,23 @@ def sample( # 3. apply noises/SLM # 4. write samples if modulation: - # gatering the samples of consecutive pulses with same targets + # gatering the samples for consecutive pulses with same targets # that can be safely modulated. - samples = _sample_local_channel( + ls = _sample_local_channel( seq, ch_name, _group_between_retarget ) - samples = _modulate_local( - seq.declared_channels[ch_name], samples - ) + ls = _modulate_local(seq.declared_channels[ch_name], ls) else: - samples = _sample_local_channel(seq, ch_name, _regular) + ls = _sample_local_channel(seq, ch_name, _regular) qs = seq._qids - seq._slm_mask_targets - samples = [s for s in samples if s.qubit in qs] - samples = noises.apply(samples, local_noises) - _write_local_samples(d, basis, samples) + ls = [s for s in ls if s.qubit in qs] + ls = noises.apply(ls, common_noises) + _write_local_samples(d, basis, ls) return d -def _write_global_samples(d: dict, basis: str, samples: GlobalSamples) -> None: +def _write_global_samples(d: dict, basis: str, samples: Samples) -> None: d["Global"][basis]["amp"] += samples.amp d["Global"][basis]["det"] += samples.det d["Global"][basis]["phase"] += samples.phase @@ -146,9 +144,10 @@ def new_qdict() -> dict: } -def _sample_global_channel(seq: Sequence, ch_name: str) -> GlobalSamples: - if ch_name not in seq.declared_channels: - raise ValueError(f"{ch_name} is not declared in the given Sequence") +def _sample_global_channel(seq: Sequence, ch_name: str) -> Samples: + """Compute Samples for a global channel.""" + if seq.declared_channels[ch_name].addressing != "Global": + raise ValueError(f"{ch_name} is no a global channel") slots = seq._schedule[ch_name] return _sample_slots(seq.get_duration(), *slots) @@ -156,16 +155,24 @@ def _sample_global_channel(seq: Sequence, ch_name: str) -> GlobalSamples: def _sample_local_channel( seq: Sequence, ch_name: str, strategy: TimeSlotExtractionStrategy ) -> list[QubitSamples]: - if ch_name not in seq.declared_channels: - raise ValueError(f"{ch_name} is not declared in the given Sequence") + """Compute Samples for a local channel.""" if seq.declared_channels[ch_name].addressing != "Local": raise ValueError(f"{ch_name} is no a local channel") return strategy(seq.get_duration(), seq._schedule[ch_name]) -def _sample_slots(N: int, *slots: _TimeSlot) -> GlobalSamples: - samples = GlobalSamples(np.zeros(N), np.zeros(N), np.zeros(N)) +def _sample_slots(N: int, *slots: _TimeSlot) -> Samples: + """Gather samples of a list of _TimeSlot in a single Samples instance. + + Args: + N (int): the size of the samples arrays. + *slots (tuple[_TimeSlots]): the _TimeSlots to sample + + Returns: + A Samples instance. + """ + samples = Samples(np.zeros(N), np.zeros(N), np.zeros(N)) for s in slots: if type(s.type) is str: continue @@ -177,15 +184,24 @@ def _sample_slots(N: int, *slots: _TimeSlot) -> GlobalSamples: return samples -# This strategy type is used mostly for the necessity to extract samples -# differently when taking into account the modulation of AOM/EOM. Still it's -# nice to keep modularity here, to accomodate for future needs. TimeSlotExtractionStrategy = Callable[ [int, List[_TimeSlot]], List[QubitSamples] ] +"""Extraction strategy of _TimeSlot's of a Channel. + +This strategy type is used mostly for the necessity to extract samples +differently when taking into account the modulation of AOM/EOM. Despite there +is only two cases, wheter it's necessary to modulate a local channel or not, +this patterns is nice and can accomodate for future needs. +""" def _regular(N: int, ts: list[_TimeSlot]) -> list[QubitSamples]: + """No grouping performed. + + Fallback on the extraction procedure for a Global-like channel, and create + QubitSamples for each targeted qubit from the result. + """ return [ QubitSamples.from_global(q, _sample_slots(N, slot)) for slot in ts @@ -195,6 +211,7 @@ def _regular(N: int, ts: list[_TimeSlot]) -> list[QubitSamples]: def _group_between_retarget(N: int, ts: list[_TimeSlot]) -> list[QubitSamples]: qs: list[QubitSamples] = [] + """Group a list of _TimeSlot by consecutive Pulse between retarget ops.""" grouped_slots = _consecutive_slots_between_retargets(ts) for targets, group in grouped_slots: ss = [ @@ -211,6 +228,13 @@ class _GroupType(enum.Enum): OTHER = "other" +def _key_func(x: _TimeSlot) -> _GroupType: + if isinstance(x.type, Pulse) or x.type == "delay": + return _GroupType.PULSE_AND_DELAYS + else: + return _GroupType.OTHER + + def _consecutive_slots_between_retargets( ts: list[_TimeSlot], ) -> list[tuple[list[QubitId], list[_TimeSlot]]]: @@ -226,13 +250,7 @@ def _consecutive_slots_between_retargets( """ grouped_slots: list = [] - def key_func(x: _TimeSlot) -> _GroupType: - if isinstance(x.type, Pulse) or x.type == "delay": - return _GroupType.PULSE_AND_DELAYS - else: - return _GroupType.OTHER - - for key, group in itertools.groupby(ts, key_func): + for key, group in itertools.groupby(ts, _key_func): g = list(group) if key != _GroupType.PULSE_AND_DELAYS: continue @@ -241,12 +259,12 @@ def key_func(x: _TimeSlot) -> _GroupType: return grouped_slots -def _modulate_global(ch: Channel, samples: GlobalSamples) -> GlobalSamples: +def _modulate_global(ch: Channel, samples: Samples) -> Samples: """Modulate global samples according to the hardware specs. Additional parameters will probably be needed (keep_end, etc). """ - return GlobalSamples( + return Samples( amp=ch.modulate(samples.amp), det=ch.modulate(samples.det), phase=ch.modulate(samples.phase), diff --git a/pulser/sampler/samples.py b/pulser/sampler/samples.py index 23e0542df..c550594b6 100644 --- a/pulser/sampler/samples.py +++ b/pulser/sampler/samples.py @@ -9,8 +9,8 @@ @dataclass -class GlobalSamples: - """Samples gather arrays of values for amplitude, detuning and phase.""" +class Samples: + """Gather sampleswith for unspecified qubits.""" amp: np.ndarray det: np.ndarray @@ -27,6 +27,6 @@ class QubitSamples: qubit: QubitId @classmethod - def from_global(cls, qubit: QubitId, s: GlobalSamples) -> QubitSamples: + def from_global(cls, qubit: QubitId, s: Samples) -> QubitSamples: """Construct a QubitSamples from a Samples instance.""" return cls(amp=s.amp, det=s.det, phase=s.phase, qubit=qubit) diff --git a/pulser/tests/test_sequence_sampler.py b/pulser/tests/test_sequence_sampler.py index dd6219f9b..cc68ee0e1 100644 --- a/pulser/tests/test_sequence_sampler.py +++ b/pulser/tests/test_sequence_sampler.py @@ -55,7 +55,7 @@ def test_doppler_noise(seq): doppler_sigma = KEFF * np.sqrt(KB * 50.0e-6 / MASS) local_noises = [doppler(seq.register, doppler_sigma, seed=0)] - samples = sample(seq, local_noises=local_noises) + samples = sample(seq, common_noises=local_noises) got = samples["Local"]["ground-rydberg"]["q0"]["det"] np.random.seed(0) From c0ff5d78e407a4e98de48d902d79a860cd8c4885 Mon Sep 17 00:00:00 2001 From: Louis Vignoli Date: Tue, 15 Mar 2022 08:56:17 +0100 Subject: [PATCH 06/47] Small typos in comments --- pulser/sampler/__init__.py | 2 +- pulser/sampler/sampler.py | 9 ++++----- pulser/sampler/samples.py | 2 +- 3 files changed, 6 insertions(+), 7 deletions(-) diff --git a/pulser/sampler/__init__.py b/pulser/sampler/__init__.py index 1987e67e3..f1d1e022e 100644 --- a/pulser/sampler/__init__.py +++ b/pulser/sampler/__init__.py @@ -12,7 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -"""Module sampler allow the sampling of pulser sequences. +"""Module sampler enables the sampling of pulser sequences. Samples of a sequence are needed for plotting and simulation. diff --git a/pulser/sampler/sampler.py b/pulser/sampler/sampler.py index d0651bddf..2fb8b9148 100644 --- a/pulser/sampler/sampler.py +++ b/pulser/sampler/sampler.py @@ -1,4 +1,4 @@ -"""Expose the sample() functions. +"""Exposes the sample() functions. It contains many helpers. """ @@ -113,8 +113,7 @@ def _write_local_samples( def _prepare_dict(seq: Sequence, N: int) -> dict: """Constructs empty dict of size N. - Usually N is the duration of seq, but we allow for a longer one, in case of - modulation for example. + Usually N is the duration of seq. """ def new_qty_dict() -> dict: @@ -191,8 +190,8 @@ def _sample_slots(N: int, *slots: _TimeSlot) -> Samples: This strategy type is used mostly for the necessity to extract samples differently when taking into account the modulation of AOM/EOM. Despite there -is only two cases, wheter it's necessary to modulate a local channel or not, -this patterns is nice and can accomodate for future needs. +are only two cases, whether it's necessary to modulate a local channel or not, +this pattern can accomodate for future needs. """ diff --git a/pulser/sampler/samples.py b/pulser/sampler/samples.py index c550594b6..b17b0ec60 100644 --- a/pulser/sampler/samples.py +++ b/pulser/sampler/samples.py @@ -10,7 +10,7 @@ @dataclass class Samples: - """Gather sampleswith for unspecified qubits.""" + """Gather samples for unspecified qubits.""" amp: np.ndarray det: np.ndarray From 79fcd26b2f61964bbfa1898540b842fa0c7d7cc0 Mon Sep 17 00:00:00 2001 From: Louis Vignoli Date: Wed, 16 Mar 2022 14:35:07 +0100 Subject: [PATCH 07/47] Fix the decay of global channels to local ones --- pulser/sampler/sampler.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/pulser/sampler/sampler.py b/pulser/sampler/sampler.py index 2fb8b9148..f624592c3 100644 --- a/pulser/sampler/sampler.py +++ b/pulser/sampler/sampler.py @@ -59,7 +59,11 @@ def sample( # 4. apply noises/SLM # 5. write samples - decay = len(seq._slm_mask_targets) > 0 or len(global_noises) > 0 + decay = ( + len(seq._slm_mask_targets) > 0 + or len(global_noises) > 0 + or len(common_noises) > 0 + ) gs = _sample_global_channel(seq, ch_name) if modulation: From d9830317a1bfe3037d84f146872a4095f3c7c9c1 Mon Sep 17 00:00:00 2001 From: Louis Vignoli Date: Wed, 16 Mar 2022 14:53:59 +0100 Subject: [PATCH 08/47] Fix the modulation feature --- pulser/sampler/sampler.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/pulser/sampler/sampler.py b/pulser/sampler/sampler.py index f624592c3..fce78fc12 100644 --- a/pulser/sampler/sampler.py +++ b/pulser/sampler/sampler.py @@ -46,6 +46,10 @@ def sample( # The noises are applied in the reversed order of the list d = _prepare_dict(seq, seq.get_duration()) + if modulation: + max_rt = max([ch.rise_time for ch in seq.declared_channels.values()]) + d = _prepare_dict(seq, seq.get_duration() + 2 * max_rt) + for ch_name in seq.declared_channels: addr = seq.declared_channels[ch_name].addressing basis = seq.declared_channels[ch_name].basis @@ -287,7 +291,7 @@ def _modulate_local( QubitSamples( amp=ch.modulate(s.amp), det=ch.modulate(s.det), - phase=s.phase * np.ones(len(ch.modulate(s.amp))), + phase=ch.modulate(s.phase), qubit=s.qubit, ) ) From 922ffd30837d6de3d98305c1e7d9c2566a01838d Mon Sep 17 00:00:00 2001 From: Louis Vignoli Date: Wed, 16 Mar 2022 16:52:21 +0100 Subject: [PATCH 09/47] Refactor Adds a post_init check for same length of quantities arrays Add documentation, testing and refactoring Refactor the dict construction with modulation Simple comment to clarify the design of sampler.sample() Remove unnecessary value in enum _GroupType Add a diagram for _TimeSlot grouping Fix embedded NoiseModel by using copies Update docstrings with args Expose the sample() function from the __init__ Test the amplitude noise Remove unused commented import Add usage note on doppler noise Add a docstring for the amplitude noise Remove empty line Create a better test for doppler noise Small refactor Mark the doppler test as expected to fail Refactor the control flow of sampler.sample() Refactor the dict exporting Refactor Fix typo Add a docstring Refactor the TimeSlot grouping and strategy --- pulser/sampler/__init__.py | 1 + pulser/sampler/noises.py | 46 ++++-- pulser/sampler/sampler.py | 195 ++++++++++++++------------ pulser/sampler/samples.py | 6 + pulser/tests/test_sequence_sampler.py | 87 +++++++++--- 5 files changed, 221 insertions(+), 114 deletions(-) diff --git a/pulser/sampler/__init__.py b/pulser/sampler/__init__.py index f1d1e022e..cbfbcc3fb 100644 --- a/pulser/sampler/__init__.py +++ b/pulser/sampler/__init__.py @@ -20,3 +20,4 @@ sampler.sample(sequence) """ +from pulser.sampler.sampler import sample diff --git a/pulser/sampler/noises.py b/pulser/sampler/noises.py index e994a8fdc..fd3d0a535 100644 --- a/pulser/sampler/noises.py +++ b/pulser/sampler/noises.py @@ -19,27 +19,35 @@ """ -def amplitude(reg: Register, waist_width: float, seed: int = 0) -> NoiseModel: +def amplitude( + reg: Register, waist_width: float, random: bool = True, seed: int = 0 +) -> NoiseModel: """Generate a NoiseModel for the gaussian amplitude profile of laser beams. The laser of a global channel has a non-constant amplitude profile in the register plane. It makes global channels act differently on each qubit, becoming local. + + Args: + reg (Register): a Pulser register + waist_width (float): the laser waist_width in µm + seed (int): optional, seed for the numpy.random.Generator """ rng = np.random.default_rng(seed) def f(s: QubitSamples) -> QubitSamples: r = np.linalg.norm(reg.qubits[s.qubit]) - noise_amp = rng.normal(1.0, 1.0e-3) + noise_amp = rng.normal(1.0, 1.0e-3) if random else 1.0 noise_amp *= np.exp(-((r / waist_width) ** 2)) - amp = s.amp - amp[np.nonzero(s.amp)] *= noise_amp + amp = s.amp.copy() + amp[np.nonzero(amp)] *= noise_amp + return QubitSamples( - amp=s.amp, - det=s.det, - phase=s.phase, + amp=amp, + det=s.det.copy(), + phase=s.phase.copy(), qubit=s.qubit, ) @@ -47,18 +55,34 @@ def f(s: QubitSamples) -> QubitSamples: def doppler(reg: Register, std_dev: float, seed: int = 0) -> NoiseModel: - """Generate a NoiseModel for the Doppler effect detuning shifts.""" + """Generate a NoiseModel for the Doppler effect detuning shifts. + + Example usage: + + MASS = 1.45e-25 # kg + KB = 1.38e-23 # J/K + KEFF = 8.7 # µm^-1 + sigma = KEFF * np.sqrt(KB * 50.0e-6 / MASS) + doppler_noise = doppler(reg, sigma) + ... + + Args: + reg (Register): a Pulser register + std_dev (float): the standard deviation of the normal distribution used + to sample the random detuning shifts + seed (int): optional, seed for the numpy.random.Generator + """ rng = np.random.default_rng(seed) errs = rng.normal(0.0, std_dev, size=len(reg.qubit_ids)) detunings = dict(zip(reg.qubit_ids, errs)) def f(s: QubitSamples) -> QubitSamples: - det = s.det + det = s.det.copy() det[np.nonzero(s.det)] += detunings[s.qubit] return QubitSamples( - amp=s.amp, + amp=s.amp.copy(), det=det, - phase=s.phase, + phase=s.phase.copy(), qubit=s.qubit, ) diff --git a/pulser/sampler/sampler.py b/pulser/sampler/sampler.py index fce78fc12..05d6eb03b 100644 --- a/pulser/sampler/sampler.py +++ b/pulser/sampler/sampler.py @@ -15,7 +15,7 @@ from pulser.pulse import Pulse from pulser.sampler.noises import NoiseModel from pulser.sampler.samples import QubitSamples, Samples -from pulser.sequence import QubitId, Sequence, _TimeSlot +from pulser.sequence import Sequence, _TimeSlot def sample( @@ -26,6 +26,8 @@ def sample( ) -> dict: """Samples the given Sequence and returns a nested dictionary. + It is intended to be used like the json.dumps() function. + Args: seq (Sequence): a pulser.Sequence instance. modulation (bool): a flag to account for the modulation of AOM/EOM @@ -43,62 +45,60 @@ def sample( common_noises = [] if global_noises is None: global_noises = [] - # The noises are applied in the reversed order of the list - d = _prepare_dict(seq, seq.get_duration()) - if modulation: - max_rt = max([ch.rise_time for ch in seq.declared_channels.values()]) - d = _prepare_dict(seq, seq.get_duration() + 2 * max_rt) + # The idea of this refactor: every channel is local behind the scene A + # global channel is just a convenient abstraction for an ideal case. But as + # soon as we introduce noises it's useless. Hence, the distinction between + # the two should be very thin: no big if diverging branches + # + # 1. determine if the global channel decay to a local one + # 2. extract samples + # 3. modulate + # 4. apply noises/SLM + # 5. write samples + + samples: dict[str, Samples | list[QubitSamples]] = {} + + # First extract to the internal representation + for ch_name, ch in seq.declared_channels.items(): + s: Samples | list[QubitSamples] - for ch_name in seq.declared_channels: addr = seq.declared_channels[ch_name].addressing - basis = seq.declared_channels[ch_name].basis - ls: list[QubitSamples] = [] + ch_noises = list(common_noises) if addr == "Global": - # 1. determine if the global channel decay to a local one - # 2. extract samples - # 3. modulate - # 4. apply noises/SLM - # 5. write samples - decay = ( len(seq._slm_mask_targets) > 0 or len(global_noises) > 0 or len(common_noises) > 0 ) + if decay: + addr = "Local" + ch_noises.extend(global_noises) - gs = _sample_global_channel(seq, ch_name) - if modulation: - gs = _modulate_global(seq.declared_channels[ch_name], gs) - - if not decay: - _write_global_samples(d, basis, gs) - else: - qs = seq._qids - seq._slm_mask_targets - ls = [QubitSamples.from_global(q, gs) for q in qs] - ls = noises.apply(ls, common_noises + global_noises) - _write_local_samples(d, basis, ls) - - elif addr == "Local": - # 1. determine if modulation - # 2. extract samples (depends on modulation) - # 3. apply noises/SLM - # 4. write samples + if addr == "Global": + s = _sample_global_channel(seq, ch_name) if modulation: - # gatering the samples for consecutive pulses with same targets - # that can be safely modulated. - ls = _sample_local_channel( - seq, ch_name, _group_between_retarget - ) - ls = _modulate_local(seq.declared_channels[ch_name], ls) - else: - ls = _sample_local_channel(seq, ch_name, _regular) - qs = seq._qids - seq._slm_mask_targets - ls = [s for s in ls if s.qubit in qs] - ls = noises.apply(ls, common_noises) - _write_local_samples(d, basis, ls) + s = _modulate_global(ch, s) + # No SLM since not decayed + samples[ch_name] = s + continue + + strategy = _group_between_retargets if modulation else _regular + s = _sample_channel(seq, ch_name, strategy) + if modulation: + s = _modulate_local(ch, s) + + unmasked_qubits = seq._qids - seq._slm_mask_targets + s = [x for x in s if x.qubit in unmasked_qubits] # SLM + + s = noises.apply(s, ch_noises) + + samples[ch_name] = s + + # Output: format the samples in the simulation dict form + d = _write_dict(seq, modulation, samples) return d @@ -151,6 +151,27 @@ def new_qdict() -> dict: } +def _write_dict( + seq: Sequence, + modulation: bool, + samples: dict[str, Samples | list[QubitSamples]], +) -> dict: + """Needs to be rewritten: do not need the sequence nor modulation args.""" + N = seq.get_duration() + if modulation: + max_rt = max([ch.rise_time for ch in seq.declared_channels.values()]) + N += 2 * max_rt + d = _prepare_dict(seq, N) + + for ch_name, a in samples.items(): + basis = seq.declared_channels[ch_name].basis + if isinstance(a, Samples): + _write_global_samples(d, basis, a) + else: + _write_local_samples(d, basis, a) + return d + + def _sample_global_channel(seq: Sequence, ch_name: str) -> Samples: """Compute Samples for a global channel.""" if seq.declared_channels[ch_name].addressing != "Global": @@ -159,14 +180,25 @@ def _sample_global_channel(seq: Sequence, ch_name: str) -> Samples: return _sample_slots(seq.get_duration(), *slots) -def _sample_local_channel( +def _sample_channel( seq: Sequence, ch_name: str, strategy: TimeSlotExtractionStrategy ) -> list[QubitSamples]: - """Compute Samples for a local channel.""" - if seq.declared_channels[ch_name].addressing != "Local": - raise ValueError(f"{ch_name} is no a local channel") + """Compute a list of QubitSamples for a channel.""" + qs: list[QubitSamples] = [] + grouped_slots = strategy(seq._schedule[ch_name]) - return strategy(seq.get_duration(), seq._schedule[ch_name]) + for group in grouped_slots: + # Same target in one group, guaranteed by the strategy (this seems + # weird, it's not enforced by the structure,bad design?) + targets = group[0].targets + ss = [ + QubitSamples.from_global( + q, _sample_slots(seq.get_duration(), *group) + ) + for q in targets + ] + qs.extend(ss) + return qs def _sample_slots(N: int, *slots: _TimeSlot) -> Samples: @@ -191,47 +223,27 @@ def _sample_slots(N: int, *slots: _TimeSlot) -> Samples: return samples -TimeSlotExtractionStrategy = Callable[ - [int, List[_TimeSlot]], List[QubitSamples] -] +TimeSlotExtractionStrategy = Callable[[List[_TimeSlot]], List[List[_TimeSlot]]] """Extraction strategy of _TimeSlot's of a Channel. -This strategy type is used mostly for the necessity to extract samples -differently when taking into account the modulation of AOM/EOM. Despite there -are only two cases, whether it's necessary to modulate a local channel or not, -this pattern can accomodate for future needs. -""" - - -def _regular(N: int, ts: list[_TimeSlot]) -> list[QubitSamples]: - """No grouping performed. +It's an alias for functions that returns a list of lists of _TimeSlots. +_TimeSlots in the same group MUST share the same targets. - Fallback on the extraction procedure for a Global-like channel, and create - QubitSamples for each targeted qubit from the result. - """ - return [ - QubitSamples.from_global(q, _sample_slots(N, slot)) - for slot in ts - for q in slot.targets - ] +NOTE: + This strategy type is used mostly for the necessity to extract samples + differently when taking into account the modulation of AOM/EOM. Despite + there are only two cases, whether it's necessary to modulate a local + channel or not, this pattern can accomodate for future needs. +""" -def _group_between_retarget(N: int, ts: list[_TimeSlot]) -> list[QubitSamples]: - qs: list[QubitSamples] = [] - """Group a list of _TimeSlot by consecutive Pulse between retarget ops.""" - grouped_slots = _consecutive_slots_between_retargets(ts) - for targets, group in grouped_slots: - ss = [ - QubitSamples.from_global(q, _sample_slots(N, *group)) - for q in targets - ] - qs.extend(ss) - return qs +def _regular(ts: list[_TimeSlot]) -> list[list[_TimeSlot]]: + """No grouping performed.""" + return [[x] for x in ts] class _GroupType(enum.Enum): PULSE_AND_DELAYS = "pulses_and_delays" - TARGET = "target" OTHER = "other" @@ -242,26 +254,37 @@ def _key_func(x: _TimeSlot) -> _GroupType: return _GroupType.OTHER -def _consecutive_slots_between_retargets( +def _group_between_retargets( ts: list[_TimeSlot], -) -> list[tuple[list[QubitId], list[_TimeSlot]]]: +) -> list[list[_TimeSlot]]: """Filter and group _TimeSlots together. Group the input slots by group of consecutive Pulses and delays between two - target operations. + target operations. Consider the following sequence consisting of pulses A B + C D E F, targeting different qubits: + + .---A---B------.---C--D--E---.----F-- + ^ ^ ^ + | | | + target q0 target q1 target q0 + + It will group the pulses' _TimeSlot's in batches (A B), (C D E) and (F), + returning the following list of tuples: + + [("q0", [A, B]), ("q1", [C, D, E]), ("q0", [F])] Returns: A list of tuples (a, b) where a is the list of common targeted qubits and b is a list of consecutive _TimeSlot of type Pulse or "delay". All "target" _TimeSlots are discarded. """ - grouped_slots: list = [] + grouped_slots: list[list[_TimeSlot]] = [] for key, group in itertools.groupby(ts, _key_func): g = list(group) if key != _GroupType.PULSE_AND_DELAYS: continue - grouped_slots.append((g[0].targets, g)) + grouped_slots.append(g) return grouped_slots diff --git a/pulser/sampler/samples.py b/pulser/sampler/samples.py index b17b0ec60..4ff95d75f 100644 --- a/pulser/sampler/samples.py +++ b/pulser/sampler/samples.py @@ -30,3 +30,9 @@ class QubitSamples: def from_global(cls, qubit: QubitId, s: Samples) -> QubitSamples: """Construct a QubitSamples from a Samples instance.""" return cls(amp=s.amp, det=s.det, phase=s.phase, qubit=qubit) + + def __post_init__(self) -> None: + if not len(self.amp) == len(self.det) == len(self.phase): + raise ValueError( + "ndarrays amp, det and phase must have the same length" + ) diff --git a/pulser/tests/test_sequence_sampler.py b/pulser/tests/test_sequence_sampler.py index cc68ee0e1..e9a008947 100644 --- a/pulser/tests/test_sequence_sampler.py +++ b/pulser/tests/test_sequence_sampler.py @@ -2,14 +2,11 @@ import pytest import pulser +import pulser.sampler.noises as noises from pulser.devices import MockDevice from pulser.pulse import Pulse -from pulser.sampler.noises import doppler -from pulser.sampler.sampler import sample -from pulser.simulation.simconfig import SimConfig -from pulser.waveforms import BlackmanWaveform - -# from deepdiff import DeepDiff +from pulser.sampler import sample +from pulser.waveforms import BlackmanWaveform, ConstantWaveform def test_sequence_sampler(seq): @@ -42,31 +39,87 @@ def test_sequence_sampler(seq): sim.samples[k[0]][k[1]][k[2]][k[3]], ) - # print(DeepDiff(samples, sim.samples)) - # The deepdiff shows that there is no dict for unused basis in sim.samples, - # where it's a zero dict for _sequence_sampler.sample +@pytest.mark.xfail( + reason="Test a different doppler effect than the one implemented." +) +def test_doppler_noise(): + """What is exactly the doppler noise here? + + A constant detuning shift per pulse seems weird. A global shift seems more + reasonable, but how can it be constant during the all sequence? It is not + clear to me here, I find the current implementation in the simulation + module to be unsatisfactory. + + No surprise I make it fail on purpose right now 😅 + """ + N = 100 + det_value = np.pi -def test_doppler_noise(seq): + reg = pulser.Register.from_coordinates(np.array([[0.0, 0.0]]), prefix="q") + seq = pulser.Sequence(reg, MockDevice) + seq.declare_channel("ch0", "rydberg_global") + for _ in range(3): + seq.add( + Pulse.ConstantDetuning(ConstantWaveform(N, 1.0), det_value, 0.0), + "ch0", + ) + seq.delay(100, "ch0") + seq.measure() MASS = 1.45e-25 # kg KB = 1.38e-23 # J/K KEFF = 8.7 # µm^-1 doppler_sigma = KEFF * np.sqrt(KB * 50.0e-6 / MASS) + seed = 42 + rng = np.random.default_rng(seed) + + shifts = rng.normal(0, doppler_sigma, 3) + want = np.zeros(6 * N) + want[0:100] = det_value + shifts[0] + want[200:300] = det_value + shifts[1] + want[400:500] = det_value + shifts[2] - local_noises = [doppler(seq.register, doppler_sigma, seed=0)] + local_noises = [noises.doppler(reg, doppler_sigma, seed=seed)] samples = sample(seq, common_noises=local_noises) got = samples["Local"]["ground-rydberg"]["q0"]["det"] - np.random.seed(0) - sim = pulser.Simulation(seq) - sim.add_config(SimConfig("doppler")) - sim._extract_samples() - want = sim.samples["Local"]["ground-rydberg"]["q0"]["det"] - np.testing.assert_array_equal(got, want) +def test_amplitude_noise(): + N = 100 + amplitude = 1.0 + waist_width = 2.0 # µm + + coords = np.array([[-2.0, 0.0], [0.0, 0.0], [2.0, 0.0]]) + reg = pulser.Register.from_coordinates(coords, prefix="q") + seq = pulser.Sequence(reg, MockDevice) + seq.declare_channel("ch0", "rydberg_global") + seq.add( + Pulse.ConstantAmplitude(amplitude, ConstantWaveform(N, 0.0), 0.0), + "ch0", + ) + seq.measure() + + def expected_samples(vec: np.ndarray) -> np.ndarray: + """Defines the non-noisy effect of a gaussian amplitude profile.""" + r = np.linalg.norm(vec) + a = np.ones(N) + a *= amplitude + a *= np.exp(-((r / waist_width) ** 2)) + return a + + s = sample( + seq, global_noises=[noises.amplitude(reg, waist_width, random=False)] + ) + + for q, coords in reg.qubits.items(): + got = s["Local"]["ground-rydberg"][q]["amp"] + want = expected_samples(coords) + np.testing.assert_equal(got, want) + + @pytest.fixture def seq() -> pulser.Sequence: reg = pulser.Register.from_coordinates( From e7f17985fcd7b8ab2f3a3a79c1369e8ace91464d Mon Sep 17 00:00:00 2001 From: Louis Vignoli Date: Mon, 21 Mar 2022 15:01:53 +0100 Subject: [PATCH 10/47] Simplify the flow of samples.samples() Global and local channels are sampled identically. The export to a dict form like the simulation one handles the distinctions. It introduces an additional dict keeping track of the addressing of each channel: Global, Decayed, Local. Trim dead code, refactor, defensive coding Write global channel from QubitSamples Rename modulation function --- pulser/sampler/sampler.py | 117 +++++++++++++++++--------------------- 1 file changed, 51 insertions(+), 66 deletions(-) diff --git a/pulser/sampler/sampler.py b/pulser/sampler/sampler.py index 05d6eb03b..b60abb402 100644 --- a/pulser/sampler/sampler.py +++ b/pulser/sampler/sampler.py @@ -46,22 +46,17 @@ def sample( if global_noises is None: global_noises = [] - # The idea of this refactor: every channel is local behind the scene A - # global channel is just a convenient abstraction for an ideal case. But as - # soon as we introduce noises it's useless. Hence, the distinction between - # the two should be very thin: no big if diverging branches - # # 1. determine if the global channel decay to a local one # 2. extract samples # 3. modulate # 4. apply noises/SLM # 5. write samples - samples: dict[str, Samples | list[QubitSamples]] = {} + samples: dict[str, list[QubitSamples]] = {} + addrs: dict[str, str] = {} - # First extract to the internal representation for ch_name, ch in seq.declared_channels.items(): - s: Samples | list[QubitSamples] + s: list[QubitSamples] addr = seq.declared_channels[ch_name].addressing @@ -74,21 +69,15 @@ def sample( or len(common_noises) > 0 ) if decay: - addr = "Local" + addr = "Decayed" ch_noises.extend(global_noises) - if addr == "Global": - s = _sample_global_channel(seq, ch_name) - if modulation: - s = _modulate_global(ch, s) - # No SLM since not decayed - samples[ch_name] = s - continue + addrs[ch_name] = addr strategy = _group_between_retargets if modulation else _regular s = _sample_channel(seq, ch_name, strategy) if modulation: - s = _modulate_local(ch, s) + s = _modulate(ch, s) unmasked_qubits = seq._qids - seq._slm_mask_targets s = [x for x in s if x.qubit in unmasked_qubits] # SLM @@ -97,27 +86,12 @@ def sample( samples[ch_name] = s - # Output: format the samples in the simulation dict form - d = _write_dict(seq, modulation, samples) + # format the samples in the simulation dict form + d = _write_dict(seq, samples, addrs) return d -def _write_global_samples(d: dict, basis: str, samples: Samples) -> None: - d["Global"][basis]["amp"] += samples.amp - d["Global"][basis]["det"] += samples.det - d["Global"][basis]["phase"] += samples.phase - - -def _write_local_samples( - d: dict, basis: str, samples: list[QubitSamples] -) -> None: - for s in samples: - d["Local"][basis][s.qubit]["amp"] += s.amp - d["Local"][basis][s.qubit]["det"] += s.det - d["Local"][basis][s.qubit]["phase"] += s.phase - - def _prepare_dict(seq: Sequence, N: int) -> dict: """Constructs empty dict of size N. @@ -153,31 +127,56 @@ def new_qdict() -> dict: def _write_dict( seq: Sequence, - modulation: bool, - samples: dict[str, Samples | list[QubitSamples]], + samples: dict[str, list[QubitSamples]], + addrs: dict[str, str], ) -> dict: - """Needs to be rewritten: do not need the sequence nor modulation args.""" - N = seq.get_duration() - if modulation: - max_rt = max([ch.rise_time for ch in seq.declared_channels.values()]) - N += 2 * max_rt + """Export the given samples to a nested dictionary.""" + # Get the duration + if not _same_duration(samples): # Defensive coding + raise ValueError("All the samples do not share the same duration.") + N = list(samples.values())[0][0].amp.size + d = _prepare_dict(seq, N) for ch_name, a in samples.items(): basis = seq.declared_channels[ch_name].basis - if isinstance(a, Samples): - _write_global_samples(d, basis, a) + addr = addrs[ch_name] + if addr == "Global": + # Take samples on only one qubit and write them + a_qubit = next(iter(seq._qids)) + to_write = [x for x in a if x.qubit == a_qubit] + _write_global_samples(d, basis, to_write) else: _write_local_samples(d, basis, a) return d -def _sample_global_channel(seq: Sequence, ch_name: str) -> Samples: - """Compute Samples for a global channel.""" - if seq.declared_channels[ch_name].addressing != "Global": - raise ValueError(f"{ch_name} is no a global channel") - slots = seq._schedule[ch_name] - return _sample_slots(seq.get_duration(), *slots) +def _write_global_samples( + d: dict, basis: str, samples: list[QubitSamples] +) -> None: + for s in samples: + d["Global"][basis]["amp"] += s.amp + d["Global"][basis]["det"] += s.det + d["Global"][basis]["phase"] += s.phase + + +def _write_local_samples( + d: dict, basis: str, samples: list[QubitSamples] +) -> None: + for s in samples: + d["Local"][basis][s.qubit]["amp"] += s.amp + d["Local"][basis][s.qubit]["det"] += s.det + d["Local"][basis][s.qubit]["phase"] += s.phase + + +def _same_duration(samples: dict[str, list[QubitSamples]]) -> bool: + ds: list[int] = [] + ss: list[QubitSamples] = [] + for s in samples.values(): + ss.extend(s) + for x in ss: + ds.extend((x.amp.size, x.det.size, x.phase.size)) + return ds.count(ds[0]) == len(ds) def _sample_channel( @@ -238,8 +237,8 @@ def _sample_slots(N: int, *slots: _TimeSlot) -> Samples: def _regular(ts: list[_TimeSlot]) -> list[list[_TimeSlot]]: - """No grouping performed.""" - return [[x] for x in ts] + """No grouping performed, return only the pulses.""" + return [[x] for x in ts if isinstance(x.type, Pulse)] class _GroupType(enum.Enum): @@ -289,21 +288,7 @@ def _group_between_retargets( return grouped_slots -def _modulate_global(ch: Channel, samples: Samples) -> Samples: - """Modulate global samples according to the hardware specs. - - Additional parameters will probably be needed (keep_end, etc). - """ - return Samples( - amp=ch.modulate(samples.amp), - det=ch.modulate(samples.det), - phase=ch.modulate(samples.phase), - ) - - -def _modulate_local( - ch: Channel, samples: list[QubitSamples] -) -> list[QubitSamples]: +def _modulate(ch: Channel, samples: list[QubitSamples]) -> list[QubitSamples]: """Modulate local samples according to the hardware specs. Additional parameters will probably be needed (keep_end, etc). From 9269cdd2b5c4b963b9f77a41eaa969dc5da07696 Mon Sep 17 00:00:00 2001 From: Louis Vignoli Date: Mon, 21 Mar 2022 16:30:59 +0100 Subject: [PATCH 11/47] Improve test and get total coverage Table based tests Add a modulation test I don't like it. It is testing the sampling for sure, but still I would like to test against a known output of the modulation. Remove unnecessary if branch functools.reduce got us covered already Extend to digital Fix mypy error because of weird scope of variables Test sequence in XY and fix a typo in the relevant code Test for corner cases and omit defensive coding with pragma no cover --- pulser/sampler/noises.py | 2 - pulser/sampler/sampler.py | 6 +- pulser/tests/test_sequence_sampler.py | 180 +++++++++++++++++++++++++- 3 files changed, 181 insertions(+), 7 deletions(-) diff --git a/pulser/sampler/noises.py b/pulser/sampler/noises.py index fd3d0a535..49a208e24 100644 --- a/pulser/sampler/noises.py +++ b/pulser/sampler/noises.py @@ -91,8 +91,6 @@ def f(s: QubitSamples) -> QubitSamples: def compose_local_noises(*functions: NoiseModel) -> NoiseModel: """Helper to compose multiple NoiseModel.""" - if functions is None: - return lambda x: x return functools.reduce( lambda f, g: lambda x: f(g(x)), functions, lambda x: x ) diff --git a/pulser/sampler/sampler.py b/pulser/sampler/sampler.py index b60abb402..27d1b804b 100644 --- a/pulser/sampler/sampler.py +++ b/pulser/sampler/sampler.py @@ -110,7 +110,7 @@ def new_qdict() -> dict: if seq._in_xy: return { - "Global": {"XY", new_qty_dict()}, + "Global": {"XY": new_qty_dict()}, "Local": {"XY": new_qdict()}, } else: @@ -132,7 +132,7 @@ def _write_dict( ) -> dict: """Export the given samples to a nested dictionary.""" # Get the duration - if not _same_duration(samples): # Defensive coding + if not _same_duration(samples): # pragma: no cover raise ValueError("All the samples do not share the same duration.") N = list(samples.values())[0][0].amp.size @@ -212,7 +212,7 @@ def _sample_slots(N: int, *slots: _TimeSlot) -> Samples: """ samples = Samples(np.zeros(N), np.zeros(N), np.zeros(N)) for s in slots: - if type(s.type) is str: + if type(s.type) is str: # pragma: no cover continue pulse = cast(Pulse, s.type) samples.amp[s.ti : s.tf] += pulse.amplitude.samples diff --git a/pulser/tests/test_sequence_sampler.py b/pulser/tests/test_sequence_sampler.py index e9a008947..7b4811842 100644 --- a/pulser/tests/test_sequence_sampler.py +++ b/pulser/tests/test_sequence_sampler.py @@ -1,12 +1,28 @@ +from __future__ import annotations + +from copy import deepcopy + import numpy as np import pytest import pulser import pulser.sampler.noises as noises -from pulser.devices import MockDevice +from pulser.channels import Rydberg +from pulser.devices import Device, MockDevice from pulser.pulse import Pulse from pulser.sampler import sample -from pulser.waveforms import BlackmanWaveform, ConstantWaveform +from pulser.sampler.samples import QubitSamples +from pulser.waveforms import BlackmanWaveform, ConstantWaveform, RampWaveform + + +def test_corner_cases(): + with pytest.raises(ValueError): + _ = QubitSamples( + amp=np.array([1.0]), + det=np.array([1.0]), + phase=np.array([1.0, 1.0]), + qubit="q0", + ) def test_sequence_sampler(seq): @@ -120,6 +136,69 @@ def expected_samples(vec: np.ndarray) -> np.ndarray: np.testing.assert_equal(got, want) +def test_table_sequence(seqs: list[pulser.Sequence]): + for seq in seqs: + + global_keys = [ + ("Global", basis, qty) + for basis in ["ground-rydberg", "digital"] + for qty in ["amp", "det", "phase"] + ] + local_keys = [ + ("Local", basis, qubit, qty) + for basis in ["ground-rydberg", "digital"] + for qubit in seq._qids + for qty in ["amp", "det", "phase"] + ] + + sim = pulser.Simulation(seq) + want = sim.samples + got = sample(seq) + + for k1 in global_keys: + try: + np.testing.assert_array_equal( + got[k1[0]][k1[1]][k1[2]], want[k1[0]][k1[1]][k1[2]] + ) + except KeyError: + np.testing.assert_array_equal( + got[k1[0]][k1[1]][k1[2]], + np.zeros(len(got[k1[0]][k1[1]][k1[2]])), + ) + + for k2 in local_keys: + try: + np.testing.assert_array_equal( + got[k2[0]][k2[1]][k2[2]][k2[3]], + want[k2[0]][k2[1]][k2[2]][k2[3]], + ) + except KeyError: + np.testing.assert_array_equal( + got[k2[0]][k2[1]][k2[2]][k2[3]], + np.zeros(len(got[k2[0]][k2[1]][k2[2]][k2[3]])), + ) + + +@pytest.fixture +def seqs() -> list[pulser.Sequence]: + seqs: list[pulser.Sequence] = [] + + pulse = Pulse( + BlackmanWaveform(200, np.pi / 2), + RampWaveform(200, -np.pi / 2, np.pi / 2), + 0.0, + ) + + reg = pulser.Register.from_coordinates(np.array([[0.0, 0.0]]), prefix="q") + seq = pulser.Sequence(reg, MockDevice) + seq.declare_channel("ch0", "raman_global") + seq.add(pulse, "ch0") + seq.measure() + seqs.append(deepcopy(seq)) + + return seqs + + @pytest.fixture def seq() -> pulser.Sequence: reg = pulser.Register.from_coordinates( @@ -153,3 +232,100 @@ def seq() -> pulser.Sequence: ) seq.measure() return seq + + +def test_inXY() -> None: + pulse = Pulse( + BlackmanWaveform(200, np.pi / 2), + RampWaveform(200, -np.pi / 2, np.pi / 2), + 0.0, + ) + + reg = pulser.Register.from_coordinates(np.array([[0.0, 0.0]]), prefix="q") + seq = pulser.Sequence(reg, MockDevice) + seq.declare_channel("ch0", "mw_global") + seq.add(pulse, "ch0") + seq.measure(basis="XY") + + sim = pulser.Simulation(seq) + + want = sim.samples + got = sample(seq) + + for qty in ["amp", "det", "phase"]: + np.testing.assert_array_equal( + got["Global"]["XY"][qty], want["Global"]["XY"][qty] + ) + + for q in seq._qids: + for qty in ["amp", "det", "phase"]: + try: + np.testing.assert_array_equal( + got["Local"]["XY"][q][qty], want["Local"]["XY"][q][qty] + ) + except KeyError: + np.testing.assert_array_equal( + got["Local"]["XY"][q][qty], + np.zeros(len(got["Local"]["XY"][q][qty])), + ) + + +def test_modulation(mod_seq: pulser.Sequence) -> None: + """Not the smartest test I have ever written.""" + got = sample(mod_seq, modulation=True)["Global"]["ground-rydberg"]["amp"] + + chan = mod_seq.declared_channels["ch0"] + input = np.pi / 2 / 0.42 * np.blackman(1000) + want = chan.modulate(input) + np.testing.assert_allclose(got, want, atol=1e-2) # 1e-2 ! + + +@pytest.fixture +def mod_seq(mod_device: Device) -> pulser.Sequence: + reg = pulser.Register.from_coordinates(np.array([[0.0, 0.0]]), prefix="q") + seq = pulser.Sequence(reg, mod_device) + seq.declare_channel("ch0", "rydberg_global") + seq.add( + Pulse.ConstantDetuning(BlackmanWaveform(1000, np.pi / 2), 0.0, 0.0), + "ch0", + ) + seq.measure() + return seq + + +@pytest.fixture +def mod_device() -> Device: + return Device( + name="ModDevice", + dimensions=3, + rydberg_level=70, + max_atom_num=2000, + max_radial_distance=1000, + min_atom_distance=1, + _channels=( + ( + "rydberg_global", + Rydberg( + "Global", + 1000, + 200, + clock_period=1, + min_duration=1, + mod_bandwidth=4.0, # MHz + ), + ), + ( + "rydberg_local", + Rydberg( + "Local", + 2 * np.pi * 20, + 2 * np.pi * 10, + max_targets=2, + phase_jump_time=0, + fixed_retarget_t=0, + min_retarget_interval=220, + mod_bandwidth=4.0, + ), + ), + ), + ) From 1220cbd51c709328804e9d08bbe4467e596660e1 Mon Sep 17 00:00:00 2001 From: Louis Vignoli Date: Tue, 22 Mar 2022 11:36:12 +0100 Subject: [PATCH 12/47] Simplify the sampling by removing the Samples dataclass --- pulser/sampler/sampler.py | 44 ++++++++++++++++----------------------- pulser/sampler/samples.py | 14 ------------- 2 files changed, 18 insertions(+), 40 deletions(-) diff --git a/pulser/sampler/sampler.py b/pulser/sampler/sampler.py index 27d1b804b..8f467de59 100644 --- a/pulser/sampler/sampler.py +++ b/pulser/sampler/sampler.py @@ -14,7 +14,7 @@ from pulser.channels import Channel from pulser.pulse import Pulse from pulser.sampler.noises import NoiseModel -from pulser.sampler.samples import QubitSamples, Samples +from pulser.sampler.samples import QubitSamples from pulser.sequence import Sequence, _TimeSlot @@ -187,39 +187,31 @@ def _sample_channel( grouped_slots = strategy(seq._schedule[ch_name]) for group in grouped_slots: - # Same target in one group, guaranteed by the strategy (this seems - # weird, it's not enforced by the structure,bad design?) - targets = group[0].targets - ss = [ - QubitSamples.from_global( - q, _sample_slots(seq.get_duration(), *group) - ) - for q in targets - ] + ss = _sample_slots(seq.get_duration(), *group) qs.extend(ss) return qs -def _sample_slots(N: int, *slots: _TimeSlot) -> Samples: - """Gather samples of a list of _TimeSlot in a single Samples instance. - - Args: - N (int): the size of the samples arrays. - *slots (tuple[_TimeSlots]): the _TimeSlots to sample - - Returns: - A Samples instance. - """ - samples = Samples(np.zeros(N), np.zeros(N), np.zeros(N)) +def _sample_slots(N: int, *slots: _TimeSlot) -> list[QubitSamples]: + """Gather samples of a list of _TimeSlot in a single Samples instance.""" + # Same target in one group, guaranteed by the strategy (this seems + # weird, it's not enforced by the structure,bad design?) + qubits = slots[0].targets + amp, det, phase = np.zeros(N), np.zeros(N), np.zeros(N) for s in slots: if type(s.type) is str: # pragma: no cover continue pulse = cast(Pulse, s.type) - samples.amp[s.ti : s.tf] += pulse.amplitude.samples - samples.det[s.ti : s.tf] += pulse.detuning.samples - samples.phase[s.ti : s.tf] += pulse.phase - - return samples + amp[s.ti : s.tf] += pulse.amplitude.samples + det[s.ti : s.tf] += pulse.detuning.samples + phase[s.ti : s.tf] += pulse.phase + qs = [ + QubitSamples( + amp=amp.copy(), det=det.copy(), phase=phase.copy(), qubit=q + ) + for q in qubits + ] + return qs TimeSlotExtractionStrategy = Callable[[List[_TimeSlot]], List[List[_TimeSlot]]] diff --git a/pulser/sampler/samples.py b/pulser/sampler/samples.py index 4ff95d75f..076178e6d 100644 --- a/pulser/sampler/samples.py +++ b/pulser/sampler/samples.py @@ -8,15 +8,6 @@ from pulser.sequence import QubitId -@dataclass -class Samples: - """Gather samples for unspecified qubits.""" - - amp: np.ndarray - det: np.ndarray - phase: np.ndarray - - @dataclass class QubitSamples: """Gathers samples concerning a single qubit.""" @@ -26,11 +17,6 @@ class QubitSamples: phase: np.ndarray qubit: QubitId - @classmethod - def from_global(cls, qubit: QubitId, s: Samples) -> QubitSamples: - """Construct a QubitSamples from a Samples instance.""" - return cls(amp=s.amp, det=s.det, phase=s.phase, qubit=qubit) - def __post_init__(self) -> None: if not len(self.amp) == len(self.det) == len(self.phase): raise ValueError( From 949cfd05c99590562ccb7aee06c7ebb40e079c06 Mon Sep 17 00:00:00 2001 From: Louis Vignoli Date: Tue, 22 Mar 2022 11:44:23 +0100 Subject: [PATCH 13/47] Fix a docstring --- pulser/sampler/noises.py | 1 + 1 file changed, 1 insertion(+) diff --git a/pulser/sampler/noises.py b/pulser/sampler/noises.py index 49a208e24..15be0a6fc 100644 --- a/pulser/sampler/noises.py +++ b/pulser/sampler/noises.py @@ -31,6 +31,7 @@ def amplitude( Args: reg (Register): a Pulser register waist_width (float): the laser waist_width in µm + random (bool): adds an additional random noise on the amplitude seed (int): optional, seed for the numpy.random.Generator """ rng = np.random.default_rng(seed) From b2f0e3ea990c54fa6c00e783adf7dbda83f89a64 Mon Sep 17 00:00:00 2001 From: Louis Vignoli Date: Wed, 23 Mar 2022 10:45:40 +0100 Subject: [PATCH 14/47] Fix docstrings formatting and content --- pulser/sampler/noises.py | 22 +++++++++++++++------- pulser/sampler/sampler.py | 27 +++++++++++++++------------ 2 files changed, 30 insertions(+), 19 deletions(-) diff --git a/pulser/sampler/noises.py b/pulser/sampler/noises.py index 15be0a6fc..bc3abd05b 100644 --- a/pulser/sampler/noises.py +++ b/pulser/sampler/noises.py @@ -29,10 +29,14 @@ def amplitude( becoming local. Args: - reg (Register): a Pulser register - waist_width (float): the laser waist_width in µm - random (bool): adds an additional random noise on the amplitude - seed (int): optional, seed for the numpy.random.Generator + reg (Register): A Pulser register + waist_width (float): The laser waist_width in µm + random (bool): Adds an additional random noise on the amplitude + seed (int): Optional, seed for the numpy.random.Generator + + Return: + NoiseModel: The function that applies the amplitude noise to some + QubitSamples. """ rng = np.random.default_rng(seed) @@ -68,10 +72,14 @@ def doppler(reg: Register, std_dev: float, seed: int = 0) -> NoiseModel: ... Args: - reg (Register): a Pulser register - std_dev (float): the standard deviation of the normal distribution used + reg (Register): A Pulser register + std_dev (float): The standard deviation of the normal distribution used to sample the random detuning shifts - seed (int): optional, seed for the numpy.random.Generator + seed (int): Optional, seed for the numpy.random.Generator + + Return: + NoiseModel: The function that applies the doppler noise to some + QubitSamples. """ rng = np.random.default_rng(seed) errs = rng.normal(0.0, std_dev, size=len(reg.qubit_ids)) diff --git a/pulser/sampler/sampler.py b/pulser/sampler/sampler.py index 8f467de59..3fc3532cf 100644 --- a/pulser/sampler/sampler.py +++ b/pulser/sampler/sampler.py @@ -29,12 +29,12 @@ def sample( It is intended to be used like the json.dumps() function. Args: - seq (Sequence): a pulser.Sequence instance. - modulation (bool): a flag to account for the modulation of AOM/EOM + seq (Sequence): A pulser.Sequence instance. + modulation (bool): Flag to account for the modulation of AOM/EOM before sampling. - common_noises (Optional[list[LocalNoise]]): a list of the noise sources + common_noises (Optional[list[LocalNoise]]): A list of the noise sources for all channels. - global_noises (Optional[list[LocalNoise]]): a list of the noise sources + global_noises (Optional[list[LocalNoise]]): A list of the noise sources for global channels. Returns: @@ -250,9 +250,9 @@ def _group_between_retargets( ) -> list[list[_TimeSlot]]: """Filter and group _TimeSlots together. - Group the input slots by group of consecutive Pulses and delays between two - target operations. Consider the following sequence consisting of pulses A B - C D E F, targeting different qubits: + Group the input slots by groups of successive Pulses and delays between + two target operations. Consider the following sequence consisting of pulses + A B C D E F, targeting different qubits: .---A---B------.---C--D--E---.----F-- ^ ^ ^ @@ -260,14 +260,17 @@ def _group_between_retargets( target q0 target q1 target q0 It will group the pulses' _TimeSlot's in batches (A B), (C D E) and (F), - returning the following list of tuples: + returning the following list of list of _TimeSlot instances: - [("q0", [A, B]), ("q1", [C, D, E]), ("q0", [F])] + [[A, B], [C, D, E], [F]] + + Args: + ts (list[_TimeSlot]): A list of TimeSlot from a Sequence schedule. Returns: - A list of tuples (a, b) where a is the list of common targeted qubits - and b is a list of consecutive _TimeSlot of type Pulse or "delay". All - "target" _TimeSlots are discarded. + A list of list of _TimeSlot. _TimeSlot instances are successive and + share the same targets. They are of type either Pulse or "delay", all + "target" ones are discarded. """ grouped_slots: list[list[_TimeSlot]] = [] From 8d09f126dc95cc85894d8e402ae070ede1bc3af1 Mon Sep 17 00:00:00 2001 From: Louis Vignoli Date: Wed, 23 Mar 2022 10:55:24 +0100 Subject: [PATCH 15/47] Remove unnecessary nonzero check in amplitude noise --- pulser/sampler/noises.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pulser/sampler/noises.py b/pulser/sampler/noises.py index bc3abd05b..17d1d816c 100644 --- a/pulser/sampler/noises.py +++ b/pulser/sampler/noises.py @@ -47,7 +47,7 @@ def f(s: QubitSamples) -> QubitSamples: noise_amp *= np.exp(-((r / waist_width) ** 2)) amp = s.amp.copy() - amp[np.nonzero(amp)] *= noise_amp + amp *= noise_amp return QubitSamples( amp=amp, From 9f350424da39aebd7d67141b887f5e90ef651775 Mon Sep 17 00:00:00 2001 From: Louis Vignoli Date: Wed, 23 Mar 2022 10:56:55 +0100 Subject: [PATCH 16/47] Fix the noise seed default value: defaults to None --- pulser/sampler/noises.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/pulser/sampler/noises.py b/pulser/sampler/noises.py index 17d1d816c..b97058489 100644 --- a/pulser/sampler/noises.py +++ b/pulser/sampler/noises.py @@ -2,7 +2,7 @@ from __future__ import annotations import functools -from typing import Callable +from typing import Callable, Optional import numpy as np @@ -20,7 +20,10 @@ def amplitude( - reg: Register, waist_width: float, random: bool = True, seed: int = 0 + reg: Register, + waist_width: float, + random: bool = True, + seed: Optional[int] = None, ) -> NoiseModel: """Generate a NoiseModel for the gaussian amplitude profile of laser beams. @@ -59,7 +62,7 @@ def f(s: QubitSamples) -> QubitSamples: return f -def doppler(reg: Register, std_dev: float, seed: int = 0) -> NoiseModel: +def doppler(reg: Register, std_dev: float, seed: Optional[int]) -> NoiseModel: """Generate a NoiseModel for the Doppler effect detuning shifts. Example usage: From 03e8558ed9c22c45488a6f03b5f5b7d9a542aaec Mon Sep 17 00:00:00 2001 From: Louis Vignoli Date: Wed, 23 Mar 2022 11:24:48 +0100 Subject: [PATCH 17/47] Improve docstrings of helper functions in noise module --- pulser/sampler/noises.py | 24 ++++++++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/pulser/sampler/noises.py b/pulser/sampler/noises.py index b97058489..1a2b8030c 100644 --- a/pulser/sampler/noises.py +++ b/pulser/sampler/noises.py @@ -102,7 +102,15 @@ def f(s: QubitSamples) -> QubitSamples: def compose_local_noises(*functions: NoiseModel) -> NoiseModel: - """Helper to compose multiple NoiseModel.""" + """Helper to compose multiple NoiseModel. + + Args: + *functions: a list of functions + + Returns: + The mathematical composition of *functions. The last element is applied + first. If *functions is [f, g, h], it returns f∘g∘h. + """ return functools.reduce( lambda f, g: lambda x: f(g(x)), functions, lambda x: x ) @@ -111,7 +119,19 @@ def compose_local_noises(*functions: NoiseModel) -> NoiseModel: def apply( samples: list[QubitSamples], noises: list[NoiseModel] ) -> list[QubitSamples]: - """Apply a list of NoiseModel on a list of QubitSamples.""" + """Apply a list of NoiseModel on a list of QubitSamples. + + The noises are composed using the compose_local_noises function, such that + the last element is applied first. + + Args: + samples (list[QubitSamples]): A list of QubitSamples. + noises (list[NoiseModel]): A list of NoiseModel. + + Return: + A list of QubitSamples on which each element of noises has been + applied. + """ tot_noise = compose_local_noises(*noises) return [tot_noise(s) for s in samples] From 19af68d676cc78b905fc891b6e17e7a330545324 Mon Sep 17 00:00:00 2001 From: Louis Vignoli Date: Wed, 23 Mar 2022 11:26:33 +0100 Subject: [PATCH 18/47] Apply noises before the SLM masking --- pulser/sampler/sampler.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pulser/sampler/sampler.py b/pulser/sampler/sampler.py index 3fc3532cf..be76b21cf 100644 --- a/pulser/sampler/sampler.py +++ b/pulser/sampler/sampler.py @@ -79,11 +79,11 @@ def sample( if modulation: s = _modulate(ch, s) + s = noises.apply(s, ch_noises) + unmasked_qubits = seq._qids - seq._slm_mask_targets s = [x for x in s if x.qubit in unmasked_qubits] # SLM - s = noises.apply(s, ch_noises) - samples[ch_name] = s # format the samples in the simulation dict form From 14f9c8b0de1e9d550b9e653589fc6df5d96c1f32 Mon Sep 17 00:00:00 2001 From: Louis Vignoli Date: Wed, 23 Mar 2022 11:36:43 +0100 Subject: [PATCH 19/47] Rename misnamed variables --- pulser/sampler/sampler.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/pulser/sampler/sampler.py b/pulser/sampler/sampler.py index be76b21cf..4a5e7d296 100644 --- a/pulser/sampler/sampler.py +++ b/pulser/sampler/sampler.py @@ -138,16 +138,16 @@ def _write_dict( d = _prepare_dict(seq, N) - for ch_name, a in samples.items(): + for ch_name, some_samples in samples.items(): basis = seq.declared_channels[ch_name].basis addr = addrs[ch_name] if addr == "Global": # Take samples on only one qubit and write them a_qubit = next(iter(seq._qids)) - to_write = [x for x in a if x.qubit == a_qubit] + to_write = [x for x in some_samples if x.qubit == a_qubit] _write_global_samples(d, basis, to_write) else: - _write_local_samples(d, basis, a) + _write_local_samples(d, basis, some_samples) return d @@ -170,13 +170,13 @@ def _write_local_samples( def _same_duration(samples: dict[str, list[QubitSamples]]) -> bool: - ds: list[int] = [] - ss: list[QubitSamples] = [] - for s in samples.values(): - ss.extend(s) - for x in ss: - ds.extend((x.amp.size, x.det.size, x.phase.size)) - return ds.count(ds[0]) == len(ds) + durations: list[int] = [] + flatten_samples: list[QubitSamples] = [] + for some_samples in samples.values(): + flatten_samples.extend(some_samples) + for s in flatten_samples: + durations.extend((s.amp.size, s.det.size, s.phase.size)) + return durations.count(durations[0]) == len(durations) def _sample_channel( From 41d175dcb53eba54b19850f63b0e9642a4a4fb64 Mon Sep 17 00:00:00 2001 From: Louis Vignoli Date: Wed, 23 Mar 2022 14:34:45 +0100 Subject: [PATCH 20/47] Change the scope of _key_func() and remove the _GroupType class --- pulser/sampler/sampler.py | 25 ++++++++++--------------- 1 file changed, 10 insertions(+), 15 deletions(-) diff --git a/pulser/sampler/sampler.py b/pulser/sampler/sampler.py index 4a5e7d296..9fad3d7ba 100644 --- a/pulser/sampler/sampler.py +++ b/pulser/sampler/sampler.py @@ -4,7 +4,6 @@ """ from __future__ import annotations -import enum import itertools from typing import Callable, List, Optional, cast @@ -233,18 +232,6 @@ def _regular(ts: list[_TimeSlot]) -> list[list[_TimeSlot]]: return [[x] for x in ts if isinstance(x.type, Pulse)] -class _GroupType(enum.Enum): - PULSE_AND_DELAYS = "pulses_and_delays" - OTHER = "other" - - -def _key_func(x: _TimeSlot) -> _GroupType: - if isinstance(x.type, Pulse) or x.type == "delay": - return _GroupType.PULSE_AND_DELAYS - else: - return _GroupType.OTHER - - def _group_between_retargets( ts: list[_TimeSlot], ) -> list[list[_TimeSlot]]: @@ -272,11 +259,19 @@ def _group_between_retargets( share the same targets. They are of type either Pulse or "delay", all "target" ones are discarded. """ + TO_KEEP = "pulses_and_delays" + + def key_func(x: _TimeSlot) -> str: + if isinstance(x.type, Pulse) or x.type == "delay": + return TO_KEEP + else: + return "other" + grouped_slots: list[list[_TimeSlot]] = [] - for key, group in itertools.groupby(ts, _key_func): + for key, group in itertools.groupby(ts, key_func): g = list(group) - if key != _GroupType.PULSE_AND_DELAYS: + if key != TO_KEEP: continue grouped_slots.append(g) From 4c8b160b12f5b70d3c16ba304002c4c0313e040b Mon Sep 17 00:00:00 2001 From: Louis Vignoli Date: Wed, 23 Mar 2022 14:49:41 +0100 Subject: [PATCH 21/47] Fix dictionary construction with defaultdict --- pulser/sampler/sampler.py | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/pulser/sampler/sampler.py b/pulser/sampler/sampler.py index 9fad3d7ba..f70ce3029 100644 --- a/pulser/sampler/sampler.py +++ b/pulser/sampler/sampler.py @@ -5,6 +5,7 @@ from __future__ import annotations import itertools +from collections import defaultdict from typing import Callable, List, Optional, cast import numpy as np @@ -114,13 +115,8 @@ def new_qdict() -> dict: } else: return { - "Global": { - basis: new_qty_dict() - for basis in ["ground-rydberg", "digital"] - }, - "Local": { - basis: new_qdict() for basis in ["ground-rydberg", "digital"] - }, + "Global": defaultdict(new_qty_dict), + "Local": defaultdict(new_qdict), } From 1af88d0265c3e00511e6b188d3aa70d49087700f Mon Sep 17 00:00:00 2001 From: Louis Vignoli Date: Wed, 23 Mar 2022 15:06:55 +0100 Subject: [PATCH 22/47] Fix misnamed keys in tests --- pulser/tests/test_sequence_sampler.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/pulser/tests/test_sequence_sampler.py b/pulser/tests/test_sequence_sampler.py index 7b4811842..a55417111 100644 --- a/pulser/tests/test_sequence_sampler.py +++ b/pulser/tests/test_sequence_sampler.py @@ -44,15 +44,15 @@ def test_sequence_sampler(seq): for qty in ["amp", "det", "phase"] ] - for k in global_keys: + for addr, basis, qty in global_keys: np.testing.assert_array_equal( - samples[k[0]][k[1]][k[2]], sim.samples[k[0]][k[1]][k[2]] + samples[addr][basis][qty], sim.samples[addr][basis][qty] ) - for k in local_keys: + for addr, basis, qubit, qty in local_keys: np.testing.assert_array_equal( - samples[k[0]][k[1]][k[2]][k[3]], - sim.samples[k[0]][k[1]][k[2]][k[3]], + samples[addr][basis][qubit][qty], + sim.samples[addr][basis][qubit][qty], ) From 8084b6e855f14773399572e50d76912940dae94a Mon Sep 17 00:00:00 2001 From: Louis Vignoli Date: Wed, 23 Mar 2022 15:19:25 +0100 Subject: [PATCH 23/47] Add a match to pytest.raises --- pulser/tests/test_sequence_sampler.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/pulser/tests/test_sequence_sampler.py b/pulser/tests/test_sequence_sampler.py index a55417111..b6f8c641d 100644 --- a/pulser/tests/test_sequence_sampler.py +++ b/pulser/tests/test_sequence_sampler.py @@ -16,7 +16,10 @@ def test_corner_cases(): - with pytest.raises(ValueError): + with pytest.raises( + ValueError, + match=".*must have the same length.*", + ): _ = QubitSamples( amp=np.array([1.0]), det=np.array([1.0]), From ff38f10fca96d6cc96bf69da19cd11da208b5bdc Mon Sep 17 00:00:00 2001 From: Louis Vignoli Date: Wed, 23 Mar 2022 15:25:41 +0100 Subject: [PATCH 24/47] Move the noises to the simulation module Import NoiseModel as noises.NoiseModel to avoid a weird circular import --- pulser/sampler/sampler.py | 7 +++---- pulser/{sampler => simulation}/noises.py | 0 pulser/tests/test_sequence_sampler.py | 2 +- 3 files changed, 4 insertions(+), 5 deletions(-) rename pulser/{sampler => simulation}/noises.py (100%) diff --git a/pulser/sampler/sampler.py b/pulser/sampler/sampler.py index f70ce3029..c5395fe26 100644 --- a/pulser/sampler/sampler.py +++ b/pulser/sampler/sampler.py @@ -10,10 +10,9 @@ import numpy as np -import pulser.sampler.noises as noises +import pulser.simulation.noises as noises from pulser.channels import Channel from pulser.pulse import Pulse -from pulser.sampler.noises import NoiseModel from pulser.sampler.samples import QubitSamples from pulser.sequence import Sequence, _TimeSlot @@ -21,8 +20,8 @@ def sample( seq: Sequence, modulation: bool = False, - common_noises: Optional[list[NoiseModel]] = None, - global_noises: Optional[list[NoiseModel]] = None, + common_noises: Optional[list[noises.NoiseModel]] = None, + global_noises: Optional[list[noises.NoiseModel]] = None, ) -> dict: """Samples the given Sequence and returns a nested dictionary. diff --git a/pulser/sampler/noises.py b/pulser/simulation/noises.py similarity index 100% rename from pulser/sampler/noises.py rename to pulser/simulation/noises.py diff --git a/pulser/tests/test_sequence_sampler.py b/pulser/tests/test_sequence_sampler.py index b6f8c641d..13331bc3d 100644 --- a/pulser/tests/test_sequence_sampler.py +++ b/pulser/tests/test_sequence_sampler.py @@ -6,7 +6,7 @@ import pytest import pulser -import pulser.sampler.noises as noises +import pulser.simulation.noises as noises from pulser.channels import Rydberg from pulser.devices import Device, MockDevice from pulser.pulse import Pulse From 96db04cd00bd36e6ebec24207140dfc2db2548e8 Mon Sep 17 00:00:00 2001 From: Louis Vignoli Date: Wed, 23 Mar 2022 15:27:11 +0100 Subject: [PATCH 25/47] Improve the simulation.noises docstring --- pulser/simulation/noises.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/pulser/simulation/noises.py b/pulser/simulation/noises.py index 1a2b8030c..4a537bdcb 100644 --- a/pulser/simulation/noises.py +++ b/pulser/simulation/noises.py @@ -1,4 +1,9 @@ -"""Contains the noise models.""" +"""Contains noise models. + +For now, only the amplitude and doppler noises are implemented in the form a +NoiseModel, which are the laser-atom interaction related noises relevant at +sampling time. +""" from __future__ import annotations import functools From 7129c952bf18f7658bb5841511d3abeeb74c6f47 Mon Sep 17 00:00:00 2001 From: Louis Vignoli Date: Wed, 23 Mar 2022 15:32:44 +0100 Subject: [PATCH 26/47] Remove noisy helper functions --- pulser/sampler/sampler.py | 28 ++++++++-------------------- 1 file changed, 8 insertions(+), 20 deletions(-) diff --git a/pulser/sampler/sampler.py b/pulser/sampler/sampler.py index c5395fe26..092a3498a 100644 --- a/pulser/sampler/sampler.py +++ b/pulser/sampler/sampler.py @@ -139,30 +139,18 @@ def _write_dict( # Take samples on only one qubit and write them a_qubit = next(iter(seq._qids)) to_write = [x for x in some_samples if x.qubit == a_qubit] - _write_global_samples(d, basis, to_write) + for s in to_write: + d["Global"][basis]["amp"] += s.amp + d["Global"][basis]["det"] += s.det + d["Global"][basis]["phase"] += s.phase else: - _write_local_samples(d, basis, some_samples) + for s in some_samples: + d["Local"][basis][s.qubit]["amp"] += s.amp + d["Local"][basis][s.qubit]["det"] += s.det + d["Local"][basis][s.qubit]["phase"] += s.phase return d -def _write_global_samples( - d: dict, basis: str, samples: list[QubitSamples] -) -> None: - for s in samples: - d["Global"][basis]["amp"] += s.amp - d["Global"][basis]["det"] += s.det - d["Global"][basis]["phase"] += s.phase - - -def _write_local_samples( - d: dict, basis: str, samples: list[QubitSamples] -) -> None: - for s in samples: - d["Local"][basis][s.qubit]["amp"] += s.amp - d["Local"][basis][s.qubit]["det"] += s.det - d["Local"][basis][s.qubit]["phase"] += s.phase - - def _same_duration(samples: dict[str, list[QubitSamples]]) -> bool: durations: list[int] = [] flatten_samples: list[QubitSamples] = [] From 578c78dde9c3fd4631103f82af73fad81b3deae7 Mon Sep 17 00:00:00 2001 From: Louis Vignoli Date: Wed, 23 Mar 2022 16:13:08 +0100 Subject: [PATCH 27/47] Write test case for _write_dict exception It removes the # pragma: no cover. --- pulser/sampler/sampler.py | 2 +- pulser/sampler/samples.py | 2 +- pulser/tests/test_sequence_sampler.py | 15 ++++++++++++++- 3 files changed, 16 insertions(+), 3 deletions(-) diff --git a/pulser/sampler/sampler.py b/pulser/sampler/sampler.py index 092a3498a..73eb58b06 100644 --- a/pulser/sampler/sampler.py +++ b/pulser/sampler/sampler.py @@ -126,7 +126,7 @@ def _write_dict( ) -> dict: """Export the given samples to a nested dictionary.""" # Get the duration - if not _same_duration(samples): # pragma: no cover + if not _same_duration(samples): raise ValueError("All the samples do not share the same duration.") N = list(samples.values())[0][0].amp.size diff --git a/pulser/sampler/samples.py b/pulser/sampler/samples.py index 076178e6d..4072ab064 100644 --- a/pulser/sampler/samples.py +++ b/pulser/sampler/samples.py @@ -20,5 +20,5 @@ class QubitSamples: def __post_init__(self) -> None: if not len(self.amp) == len(self.det) == len(self.phase): raise ValueError( - "ndarrays amp, det and phase must have the same length" + "ndarrays amp, det and phase must have the same length." ) diff --git a/pulser/tests/test_sequence_sampler.py b/pulser/tests/test_sequence_sampler.py index 13331bc3d..c17b00006 100644 --- a/pulser/tests/test_sequence_sampler.py +++ b/pulser/tests/test_sequence_sampler.py @@ -11,6 +11,7 @@ from pulser.devices import Device, MockDevice from pulser.pulse import Pulse from pulser.sampler import sample +from pulser.sampler.sampler import _write_dict from pulser.sampler.samples import QubitSamples from pulser.waveforms import BlackmanWaveform, ConstantWaveform, RampWaveform @@ -18,7 +19,7 @@ def test_corner_cases(): with pytest.raises( ValueError, - match=".*must have the same length.*", + match="ndarrays amp, det and phase must have the same length.", ): _ = QubitSamples( amp=np.array([1.0]), @@ -27,6 +28,18 @@ def test_corner_cases(): qubit="q0", ) + reg = pulser.Register.square(1, prefix="q") + seq = pulser.Sequence(reg, MockDevice) + N, M = 10, 11 + samples_dict = { + "a": [QubitSamples(np.zeros(N), np.zeros(N), np.zeros(N), "q0")], + "b": [QubitSamples(np.zeros(M), np.zeros(M), np.zeros(M), "q0")], + } + with pytest.raises( + ValueError, match="All the samples do not share the same duration." + ): + _write_dict(seq, samples_dict, {}) + def test_sequence_sampler(seq): """Check against the legacy sample extraction in the simulation module.""" From f1b2f23ebf8893b0a670d5fb5938525ef2709b74 Mon Sep 17 00:00:00 2001 From: Louis Vignoli Date: Wed, 23 Mar 2022 16:35:44 +0100 Subject: [PATCH 28/47] Reorder tests --- pulser/tests/test_sequence_sampler.py | 182 +++++++++++++------------- 1 file changed, 93 insertions(+), 89 deletions(-) diff --git a/pulser/tests/test_sequence_sampler.py b/pulser/tests/test_sequence_sampler.py index c17b00006..4c960da76 100644 --- a/pulser/tests/test_sequence_sampler.py +++ b/pulser/tests/test_sequence_sampler.py @@ -72,6 +72,99 @@ def test_sequence_sampler(seq): ) +def test_table_sequence(seqs: list[pulser.Sequence]): + for seq in seqs: + + global_keys = [ + ("Global", basis, qty) + for basis in ["ground-rydberg", "digital"] + for qty in ["amp", "det", "phase"] + ] + local_keys = [ + ("Local", basis, qubit, qty) + for basis in ["ground-rydberg", "digital"] + for qubit in seq._qids + for qty in ["amp", "det", "phase"] + ] + + sim = pulser.Simulation(seq) + want = sim.samples + got = sample(seq) + + for k1 in global_keys: + try: + np.testing.assert_array_equal( + got[k1[0]][k1[1]][k1[2]], want[k1[0]][k1[1]][k1[2]] + ) + except KeyError: + np.testing.assert_array_equal( + got[k1[0]][k1[1]][k1[2]], + np.zeros(len(got[k1[0]][k1[1]][k1[2]])), + ) + + for k2 in local_keys: + try: + np.testing.assert_array_equal( + got[k2[0]][k2[1]][k2[2]][k2[3]], + want[k2[0]][k2[1]][k2[2]][k2[3]], + ) + except KeyError: + np.testing.assert_array_equal( + got[k2[0]][k2[1]][k2[2]][k2[3]], + np.zeros(len(got[k2[0]][k2[1]][k2[2]][k2[3]])), + ) + + +def test_inXY() -> None: + + pulse = Pulse( + BlackmanWaveform(200, np.pi / 2), + RampWaveform(200, -np.pi / 2, np.pi / 2), + 0.0, + ) + + reg = pulser.Register.from_coordinates(np.array([[0.0, 0.0]]), prefix="q") + seq = pulser.Sequence(reg, MockDevice) + seq.declare_channel("ch0", "mw_global") + seq.add(pulse, "ch0") + seq.measure(basis="XY") + + sim = pulser.Simulation(seq) + + want = sim.samples + got = sample(seq) + + for qty in ["amp", "det", "phase"]: + np.testing.assert_array_equal( + got["Global"]["XY"][qty], want["Global"]["XY"][qty] + ) + + for q in seq._qids: + for qty in ["amp", "det", "phase"]: + try: + np.testing.assert_array_equal( + got["Local"]["XY"][q][qty], want["Local"]["XY"][q][qty] + ) + except KeyError: + np.testing.assert_array_equal( + got["Local"]["XY"][q][qty], + np.zeros(len(got["Local"]["XY"][q][qty])), + ) + + +def test_modulation(mod_seq: pulser.Sequence) -> None: + """Test sampling for modulated channels.""" + got = sample(mod_seq, modulation=True)["Global"]["ground-rydberg"]["amp"] + + chan = mod_seq.declared_channels["ch0"] + input = np.pi / 2 / 0.42 * np.blackman(1000) + want = chan.modulate(input) + + np.testing.assert_allclose(got, want, atol=1e-2) + # Equality at 1e-2 only... Why are they not equal? channel.modulate is + # called in both cases. + + @pytest.mark.xfail( reason="Test a different doppler effect than the one implemented." ) @@ -152,49 +245,6 @@ def expected_samples(vec: np.ndarray) -> np.ndarray: np.testing.assert_equal(got, want) -def test_table_sequence(seqs: list[pulser.Sequence]): - for seq in seqs: - - global_keys = [ - ("Global", basis, qty) - for basis in ["ground-rydberg", "digital"] - for qty in ["amp", "det", "phase"] - ] - local_keys = [ - ("Local", basis, qubit, qty) - for basis in ["ground-rydberg", "digital"] - for qubit in seq._qids - for qty in ["amp", "det", "phase"] - ] - - sim = pulser.Simulation(seq) - want = sim.samples - got = sample(seq) - - for k1 in global_keys: - try: - np.testing.assert_array_equal( - got[k1[0]][k1[1]][k1[2]], want[k1[0]][k1[1]][k1[2]] - ) - except KeyError: - np.testing.assert_array_equal( - got[k1[0]][k1[1]][k1[2]], - np.zeros(len(got[k1[0]][k1[1]][k1[2]])), - ) - - for k2 in local_keys: - try: - np.testing.assert_array_equal( - got[k2[0]][k2[1]][k2[2]][k2[3]], - want[k2[0]][k2[1]][k2[2]][k2[3]], - ) - except KeyError: - np.testing.assert_array_equal( - got[k2[0]][k2[1]][k2[2]][k2[3]], - np.zeros(len(got[k2[0]][k2[1]][k2[2]][k2[3]])), - ) - - @pytest.fixture def seqs() -> list[pulser.Sequence]: seqs: list[pulser.Sequence] = [] @@ -250,52 +300,6 @@ def seq() -> pulser.Sequence: return seq -def test_inXY() -> None: - pulse = Pulse( - BlackmanWaveform(200, np.pi / 2), - RampWaveform(200, -np.pi / 2, np.pi / 2), - 0.0, - ) - - reg = pulser.Register.from_coordinates(np.array([[0.0, 0.0]]), prefix="q") - seq = pulser.Sequence(reg, MockDevice) - seq.declare_channel("ch0", "mw_global") - seq.add(pulse, "ch0") - seq.measure(basis="XY") - - sim = pulser.Simulation(seq) - - want = sim.samples - got = sample(seq) - - for qty in ["amp", "det", "phase"]: - np.testing.assert_array_equal( - got["Global"]["XY"][qty], want["Global"]["XY"][qty] - ) - - for q in seq._qids: - for qty in ["amp", "det", "phase"]: - try: - np.testing.assert_array_equal( - got["Local"]["XY"][q][qty], want["Local"]["XY"][q][qty] - ) - except KeyError: - np.testing.assert_array_equal( - got["Local"]["XY"][q][qty], - np.zeros(len(got["Local"]["XY"][q][qty])), - ) - - -def test_modulation(mod_seq: pulser.Sequence) -> None: - """Not the smartest test I have ever written.""" - got = sample(mod_seq, modulation=True)["Global"]["ground-rydberg"]["amp"] - - chan = mod_seq.declared_channels["ch0"] - input = np.pi / 2 / 0.42 * np.blackman(1000) - want = chan.modulate(input) - np.testing.assert_allclose(got, want, atol=1e-2) # 1e-2 ! - - @pytest.fixture def mod_seq(mod_device: Device) -> pulser.Sequence: reg = pulser.Register.from_coordinates(np.array([[0.0, 0.0]]), prefix="q") From 802e86e31ae416e659171e8e236f5f2b3bacef4a Mon Sep 17 00:00:00 2001 From: Louis Vignoli Date: Wed, 23 Mar 2022 16:57:39 +0100 Subject: [PATCH 29/47] Add a more fundamental test --- pulser/tests/test_sequence_sampler.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/pulser/tests/test_sequence_sampler.py b/pulser/tests/test_sequence_sampler.py index 4c960da76..0b57e92da 100644 --- a/pulser/tests/test_sequence_sampler.py +++ b/pulser/tests/test_sequence_sampler.py @@ -41,6 +41,24 @@ def test_corner_cases(): _write_dict(seq, samples_dict, {}) +def test_one_pulse_sampling(): + """Test the sample function on a one-pulse sequence.""" + reg = pulser.Register.square(1, prefix="q") + seq = pulser.Sequence(reg, MockDevice) + seq.declare_channel("ch0", "rydberg_global") + N = 1000 + amp_wf = BlackmanWaveform(N, np.pi) + det_wf = RampWaveform(N, -np.pi / 2, np.pi / 2) + phase = 1.234 + seq.add(Pulse(amp_wf, det_wf, phase), "ch0") + seq.measure() + + got = sample(seq)["Global"]["ground-rydberg"] + want = (amp_wf.samples, det_wf.samples, np.ones(N) * phase) + for i, key in enumerate(["amp", "det", "phase"]): + np.testing.assert_array_equal(got[key], want[i]) + + def test_sequence_sampler(seq): """Check against the legacy sample extraction in the simulation module.""" samples = sample(seq) From 4b36792390a579926c8ae3d774bab8a33daf0049 Mon Sep 17 00:00:00 2001 From: Louis Vignoli Date: Wed, 23 Mar 2022 17:01:38 +0100 Subject: [PATCH 30/47] Reword and add docstrings --- pulser/tests/test_sequence_sampler.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/pulser/tests/test_sequence_sampler.py b/pulser/tests/test_sequence_sampler.py index 0b57e92da..6d6be5f4f 100644 --- a/pulser/tests/test_sequence_sampler.py +++ b/pulser/tests/test_sequence_sampler.py @@ -17,6 +17,7 @@ def test_corner_cases(): + """Test corner cases of helper functions.""" with pytest.raises( ValueError, match="ndarrays amp, det and phase must have the same length.", @@ -91,6 +92,7 @@ def test_sequence_sampler(seq): def test_table_sequence(seqs: list[pulser.Sequence]): + """A table-driven test designed to be extended easily.""" for seq in seqs: global_keys = [ @@ -134,7 +136,7 @@ def test_table_sequence(seqs: list[pulser.Sequence]): def test_inXY() -> None: - + """Test sequence in XY mode.""" pulse = Pulse( BlackmanWaveform(200, np.pi / 2), RampWaveform(200, -np.pi / 2, np.pi / 2), @@ -184,7 +186,8 @@ def test_modulation(mod_seq: pulser.Sequence) -> None: @pytest.mark.xfail( - reason="Test a different doppler effect than the one implemented." + reason="Test a different doppler effect than the one implemented; " + "no surprise it fails." ) def test_doppler_noise(): """What is exactly the doppler noise here? @@ -231,6 +234,7 @@ def test_doppler_noise(): def test_amplitude_noise(): + """Test the noise related to the amplitude profile of global pulses.""" N = 100 amplitude = 1.0 waist_width = 2.0 # µm From d3f207b1af86b08e1f859faf354e38f6ef07229a Mon Sep 17 00:00:00 2001 From: Louis Vignoli Date: Wed, 23 Mar 2022 17:03:30 +0100 Subject: [PATCH 31/47] Fix other misnamed keys in tests --- pulser/tests/test_sequence_sampler.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/pulser/tests/test_sequence_sampler.py b/pulser/tests/test_sequence_sampler.py index 6d6be5f4f..84bc61658 100644 --- a/pulser/tests/test_sequence_sampler.py +++ b/pulser/tests/test_sequence_sampler.py @@ -111,27 +111,27 @@ def test_table_sequence(seqs: list[pulser.Sequence]): want = sim.samples got = sample(seq) - for k1 in global_keys: + for addr, basis, qty in global_keys: try: np.testing.assert_array_equal( - got[k1[0]][k1[1]][k1[2]], want[k1[0]][k1[1]][k1[2]] + got[addr][basis][qty], want[addr][basis][qty] ) except KeyError: np.testing.assert_array_equal( - got[k1[0]][k1[1]][k1[2]], - np.zeros(len(got[k1[0]][k1[1]][k1[2]])), + got[addr][basis][qty], + np.zeros(len(got[addr][basis][qty])), ) - for k2 in local_keys: + for addr, basis, qubit, qty in local_keys: try: np.testing.assert_array_equal( - got[k2[0]][k2[1]][k2[2]][k2[3]], - want[k2[0]][k2[1]][k2[2]][k2[3]], + got[addr][basis][qubit][qty], + want[addr][basis][qubit][qty], ) except KeyError: np.testing.assert_array_equal( - got[k2[0]][k2[1]][k2[2]][k2[3]], - np.zeros(len(got[k2[0]][k2[1]][k2[2]][k2[3]])), + got[addr][basis][qubit][qty], + np.zeros(len(got[addr][basis][qubit][qty])), ) From 85cbdbfc31f4dcb727182d028b3faf9c0370dd5e Mon Sep 17 00:00:00 2001 From: Louis Vignoli Date: Fri, 25 Mar 2022 09:50:34 +0100 Subject: [PATCH 32/47] Fix SLM masking: consider only the seq._mask_times Sequence._slm_mask_time is not a list of times, but a pair with a start and end time of the SLM masking. --- pulser/sampler/sampler.py | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/pulser/sampler/sampler.py b/pulser/sampler/sampler.py index 73eb58b06..0c9b8c96f 100644 --- a/pulser/sampler/sampler.py +++ b/pulser/sampler/sampler.py @@ -61,12 +61,10 @@ def sample( ch_noises = list(common_noises) + slm_on = len(seq._slm_mask_targets) > 0 and len(seq._slm_mask_time) > 0 + if addr == "Global": - decay = ( - len(seq._slm_mask_targets) > 0 - or len(global_noises) > 0 - or len(common_noises) > 0 - ) + decay = slm_on or len(global_noises) > 0 or len(common_noises) > 0 if decay: addr = "Decayed" ch_noises.extend(global_noises) @@ -80,8 +78,12 @@ def sample( s = noises.apply(s, ch_noises) - unmasked_qubits = seq._qids - seq._slm_mask_targets - s = [x for x in s if x.qubit in unmasked_qubits] # SLM + if slm_on: # Update the samples of masked qubits during SLM on times + for i, _ in enumerate(s): + if s[i].qubit in seq._slm_mask_targets: + ti, tf = seq._slm_mask_time[0], seq._slm_mask_time[1] + s[i].amp[ti:tf] = 0.0 + # apply only on amp since it's just a shutter samples[ch_name] = s From a88c31d90b2bc3956b908050231c26fd7d4554ef Mon Sep 17 00:00:00 2001 From: Louis Vignoli Date: Fri, 25 Mar 2022 09:56:05 +0100 Subject: [PATCH 33/47] Change for defaultdict in new_qdict --- pulser/sampler/sampler.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pulser/sampler/sampler.py b/pulser/sampler/sampler.py index 0c9b8c96f..c4986fd13 100644 --- a/pulser/sampler/sampler.py +++ b/pulser/sampler/sampler.py @@ -107,7 +107,7 @@ def new_qty_dict() -> dict: } def new_qdict() -> dict: - return {qubit: new_qty_dict() for qubit in seq._qids} + return defaultdict(new_qty_dict) if seq._in_xy: return { From 97126fca9866d2aba36e06a18b11a85f52f270eb Mon Sep 17 00:00:00 2001 From: Louis Vignoli Date: Fri, 25 Mar 2022 11:49:56 +0100 Subject: [PATCH 34/47] Add a note about performance for sampling of global channels For global channels, we hold the same data in N copies, N the number of qubits of the register. It scales poorly with the size of the register, but it remains manageable for current size of registers. If needed, we should patch this, at the expense of a more complex control flow in the sample() function at least. --- pulser/sampler/sampler.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pulser/sampler/sampler.py b/pulser/sampler/sampler.py index c4986fd13..110935399 100644 --- a/pulser/sampler/sampler.py +++ b/pulser/sampler/sampler.py @@ -1,6 +1,9 @@ """Exposes the sample() functions. It contains many helpers. + +NOTE(perf): it not very efficient to hold the same data for all qubit in global +channels, but i remains manageable for register # with less than 100 qubits. """ from __future__ import annotations From 0d752fd80374f0d48e2eb5dd3c43293e69d2e3f0 Mon Sep 17 00:00:00 2001 From: Louis Vignoli Date: Fri, 25 Mar 2022 14:06:31 +0100 Subject: [PATCH 35/47] Simply testing of sequence --- pulser/tests/test_sequence_sampler.py | 37 ++++++++++----------------- 1 file changed, 13 insertions(+), 24 deletions(-) diff --git a/pulser/tests/test_sequence_sampler.py b/pulser/tests/test_sequence_sampler.py index 84bc61658..9fc64b46b 100644 --- a/pulser/tests/test_sequence_sampler.py +++ b/pulser/tests/test_sequence_sampler.py @@ -65,30 +65,19 @@ def test_sequence_sampler(seq): samples = sample(seq) sim = pulser.Simulation(seq) - # Exclude the digital basis, since filled with zero vs empty. - # it's just a way to check the coherence - global_keys = [ - ("Global", basis, qty) - for basis in ["ground-rydberg"] - for qty in ["amp", "det", "phase"] - ] - local_keys = [ - ("Local", basis, qubit, qty) - for basis in ["ground-rydberg"] - for qubit in seq._qids - for qty in ["amp", "det", "phase"] - ] - - for addr, basis, qty in global_keys: - np.testing.assert_array_equal( - samples[addr][basis][qty], sim.samples[addr][basis][qty] - ) - - for addr, basis, qubit, qty in local_keys: - np.testing.assert_array_equal( - samples[addr][basis][qubit][qty], - sim.samples[addr][basis][qubit][qty], - ) + for basis in sim.samples["Global"]: + for qty in sim.samples["Global"][basis]: + np.testing.assert_array_equal( + samples["Global"][basis][qty], + sim.samples["Global"][basis][qty], + ) + for basis in sim.samples["Local"]: + for qubit in sim.samples["Local"][basis]: + for qty in sim.samples["Local"][basis][qubit]: + np.testing.assert_array_equal( + samples["Local"][basis][qubit][qty], + sim.samples["Local"][basis][qubit][qty], + ) def test_table_sequence(seqs: list[pulser.Sequence]): From 77d97e023e04d4e648eeb408cc66e6456b6917c8 Mon Sep 17 00:00:00 2001 From: Louis Vignoli Date: Fri, 25 Mar 2022 14:26:31 +0100 Subject: [PATCH 36/47] Refactor the testing of sequence with a helper function --- pulser/tests/test_sequence_sampler.py | 72 +++++++-------------------- 1 file changed, 18 insertions(+), 54 deletions(-) diff --git a/pulser/tests/test_sequence_sampler.py b/pulser/tests/test_sequence_sampler.py index 9fc64b46b..16a873156 100644 --- a/pulser/tests/test_sequence_sampler.py +++ b/pulser/tests/test_sequence_sampler.py @@ -60,68 +60,30 @@ def test_one_pulse_sampling(): np.testing.assert_array_equal(got[key], want[i]) -def test_sequence_sampler(seq): +def check_same_samples_as_sim(seq: pulser.Sequence): """Check against the legacy sample extraction in the simulation module.""" - samples = sample(seq) - sim = pulser.Simulation(seq) + got = sample(seq) + want = pulser.Simulation(seq).samples - for basis in sim.samples["Global"]: - for qty in sim.samples["Global"][basis]: + for basis in want["Global"]: + for qty in want["Global"][basis]: np.testing.assert_array_equal( - samples["Global"][basis][qty], - sim.samples["Global"][basis][qty], + got["Global"][basis][qty], + want["Global"][basis][qty], ) - for basis in sim.samples["Local"]: - for qubit in sim.samples["Local"][basis]: - for qty in sim.samples["Local"][basis][qubit]: + for basis in want["Local"]: + for qubit in want["Local"][basis]: + for qty in want["Local"][basis][qubit]: np.testing.assert_array_equal( - samples["Local"][basis][qubit][qty], - sim.samples["Local"][basis][qubit][qty], + got["Local"][basis][qubit][qty], + want["Local"][basis][qubit][qty], ) -def test_table_sequence(seqs: list[pulser.Sequence]): +def test_table_sequence(seqs): """A table-driven test designed to be extended easily.""" for seq in seqs: - - global_keys = [ - ("Global", basis, qty) - for basis in ["ground-rydberg", "digital"] - for qty in ["amp", "det", "phase"] - ] - local_keys = [ - ("Local", basis, qubit, qty) - for basis in ["ground-rydberg", "digital"] - for qubit in seq._qids - for qty in ["amp", "det", "phase"] - ] - - sim = pulser.Simulation(seq) - want = sim.samples - got = sample(seq) - - for addr, basis, qty in global_keys: - try: - np.testing.assert_array_equal( - got[addr][basis][qty], want[addr][basis][qty] - ) - except KeyError: - np.testing.assert_array_equal( - got[addr][basis][qty], - np.zeros(len(got[addr][basis][qty])), - ) - - for addr, basis, qubit, qty in local_keys: - try: - np.testing.assert_array_equal( - got[addr][basis][qubit][qty], - want[addr][basis][qubit][qty], - ) - except KeyError: - np.testing.assert_array_equal( - got[addr][basis][qubit][qty], - np.zeros(len(got[addr][basis][qubit][qty])), - ) + check_same_samples_as_sim(seq) def test_inXY() -> None: @@ -257,7 +219,7 @@ def expected_samples(vec: np.ndarray) -> np.ndarray: @pytest.fixture -def seqs() -> list[pulser.Sequence]: +def seqs(seq_rydberg) -> list[pulser.Sequence]: seqs: list[pulser.Sequence] = [] pulse = Pulse( @@ -273,11 +235,13 @@ def seqs() -> list[pulser.Sequence]: seq.measure() seqs.append(deepcopy(seq)) + seqs.append(seq_rydberg) + return seqs @pytest.fixture -def seq() -> pulser.Sequence: +def seq_rydberg() -> pulser.Sequence: reg = pulser.Register.from_coordinates( np.array([[0.0, 0.0], [2.0, 0.0]]), prefix="q" ) From fe65aa453cafefe3b49fc5f19a30f0247cce7bb1 Mon Sep 17 00:00:00 2001 From: Louis Vignoli Date: Fri, 25 Mar 2022 14:38:44 +0100 Subject: [PATCH 37/47] Fix the testing of blackman modulation --- pulser/tests/test_sequence_sampler.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/pulser/tests/test_sequence_sampler.py b/pulser/tests/test_sequence_sampler.py index 16a873156..a5724c81d 100644 --- a/pulser/tests/test_sequence_sampler.py +++ b/pulser/tests/test_sequence_sampler.py @@ -125,15 +125,15 @@ def test_inXY() -> None: def test_modulation(mod_seq: pulser.Sequence) -> None: """Test sampling for modulated channels.""" - got = sample(mod_seq, modulation=True)["Global"]["ground-rydberg"]["amp"] - + N = mod_seq.get_duration() chan = mod_seq.declared_channels["ch0"] - input = np.pi / 2 / 0.42 * np.blackman(1000) + blackman = np.clip(np.blackman(N), 0, np.inf) + input = (np.pi / 2) / (np.sum(blackman) / N) * blackman + want = chan.modulate(input) + got = sample(mod_seq, modulation=True)["Global"]["ground-rydberg"]["amp"] - np.testing.assert_allclose(got, want, atol=1e-2) - # Equality at 1e-2 only... Why are they not equal? channel.modulate is - # called in both cases. + np.testing.assert_array_equal(got, want) @pytest.mark.xfail( From 1f05e03334e05926e6e1ddf316c5702f2943d3ba Mon Sep 17 00:00:00 2001 From: Louis Vignoli Date: Fri, 25 Mar 2022 14:41:03 +0100 Subject: [PATCH 38/47] Move noise-related tests to an independent testing files --- pulser/tests/test_sampling_noises.py | 93 +++++++++++++++++++++++++++ pulser/tests/test_sequence_sampler.py | 85 +----------------------- 2 files changed, 94 insertions(+), 84 deletions(-) create mode 100644 pulser/tests/test_sampling_noises.py diff --git a/pulser/tests/test_sampling_noises.py b/pulser/tests/test_sampling_noises.py new file mode 100644 index 000000000..2822ea929 --- /dev/null +++ b/pulser/tests/test_sampling_noises.py @@ -0,0 +1,93 @@ +from __future__ import annotations + +import numpy as np +import pytest + +import pulser +import pulser.simulation.noises as noises +from pulser.devices import MockDevice +from pulser.pulse import Pulse +from pulser.sampler import sample +from pulser.waveforms import ConstantWaveform + + +def test_amplitude_noise(): + """Test the noise related to the amplitude profile of global pulses.""" + N = 100 + amplitude = 1.0 + waist_width = 2.0 # µm + + coords = np.array([[-2.0, 0.0], [0.0, 0.0], [2.0, 0.0]]) + reg = pulser.Register.from_coordinates(coords, prefix="q") + seq = pulser.Sequence(reg, MockDevice) + seq.declare_channel("ch0", "rydberg_global") + seq.add( + Pulse.ConstantAmplitude(amplitude, ConstantWaveform(N, 0.0), 0.0), + "ch0", + ) + seq.measure() + + def expected_samples(vec: np.ndarray) -> np.ndarray: + """Defines the non-noisy effect of a gaussian amplitude profile.""" + r = np.linalg.norm(vec) + a = np.ones(N) + a *= amplitude + a *= np.exp(-((r / waist_width) ** 2)) + return a + + s = sample( + seq, global_noises=[noises.amplitude(reg, waist_width, random=False)] + ) + + for q, coords in reg.qubits.items(): + got = s["Local"]["ground-rydberg"][q]["amp"] + want = expected_samples(coords) + np.testing.assert_equal(got, want) + + +@pytest.mark.xfail( + reason="Test a different doppler effect than the one implemented; " + "no surprise it fails." +) +def test_doppler_noise(): + """What is exactly the doppler noise here? + + A constant detuning shift per pulse seems weird. A global shift seems more + reasonable, but how can it be constant during the all sequence? It is not + clear to me here, I find the current implementation in the simulation + module to be unsatisfactory. + + No surprise I make it fail on purpose right now 😅 + """ + N = 100 + det_value = np.pi + + reg = pulser.Register.from_coordinates(np.array([[0.0, 0.0]]), prefix="q") + seq = pulser.Sequence(reg, MockDevice) + seq.declare_channel("ch0", "rydberg_global") + for _ in range(3): + seq.add( + Pulse.ConstantDetuning(ConstantWaveform(N, 1.0), det_value, 0.0), + "ch0", + ) + seq.delay(100, "ch0") + seq.measure() + + MASS = 1.45e-25 # kg + KB = 1.38e-23 # J/K + KEFF = 8.7 # µm^-1 + doppler_sigma = KEFF * np.sqrt(KB * 50.0e-6 / MASS) + seed = 42 + rng = np.random.default_rng(seed) + + shifts = rng.normal(0, doppler_sigma, 3) + want = np.zeros(6 * N) + want[0:100] = det_value + shifts[0] + want[200:300] = det_value + shifts[1] + want[400:500] = det_value + shifts[2] + + local_noises = [noises.doppler(reg, doppler_sigma, seed=seed)] + samples = sample(seq, common_noises=local_noises) + got = samples["Local"]["ground-rydberg"]["q0"]["det"] + + np.testing.assert_array_equal(got, want) diff --git a/pulser/tests/test_sequence_sampler.py b/pulser/tests/test_sequence_sampler.py index a5724c81d..2d873b36b 100644 --- a/pulser/tests/test_sequence_sampler.py +++ b/pulser/tests/test_sequence_sampler.py @@ -6,14 +6,13 @@ import pytest import pulser -import pulser.simulation.noises as noises from pulser.channels import Rydberg from pulser.devices import Device, MockDevice from pulser.pulse import Pulse from pulser.sampler import sample from pulser.sampler.sampler import _write_dict from pulser.sampler.samples import QubitSamples -from pulser.waveforms import BlackmanWaveform, ConstantWaveform, RampWaveform +from pulser.waveforms import BlackmanWaveform, RampWaveform def test_corner_cases(): @@ -136,88 +135,6 @@ def test_modulation(mod_seq: pulser.Sequence) -> None: np.testing.assert_array_equal(got, want) -@pytest.mark.xfail( - reason="Test a different doppler effect than the one implemented; " - "no surprise it fails." -) -def test_doppler_noise(): - """What is exactly the doppler noise here? - - A constant detuning shift per pulse seems weird. A global shift seems more - reasonable, but how can it be constant during the all sequence? It is not - clear to me here, I find the current implementation in the simulation - module to be unsatisfactory. - - No surprise I make it fail on purpose right now 😅 - """ - N = 100 - det_value = np.pi - - reg = pulser.Register.from_coordinates(np.array([[0.0, 0.0]]), prefix="q") - seq = pulser.Sequence(reg, MockDevice) - seq.declare_channel("ch0", "rydberg_global") - for _ in range(3): - seq.add( - Pulse.ConstantDetuning(ConstantWaveform(N, 1.0), det_value, 0.0), - "ch0", - ) - seq.delay(100, "ch0") - seq.measure() - - MASS = 1.45e-25 # kg - KB = 1.38e-23 # J/K - KEFF = 8.7 # µm^-1 - doppler_sigma = KEFF * np.sqrt(KB * 50.0e-6 / MASS) - seed = 42 - rng = np.random.default_rng(seed) - - shifts = rng.normal(0, doppler_sigma, 3) - want = np.zeros(6 * N) - want[0:100] = det_value + shifts[0] - want[200:300] = det_value + shifts[1] - want[400:500] = det_value + shifts[2] - - local_noises = [noises.doppler(reg, doppler_sigma, seed=seed)] - samples = sample(seq, common_noises=local_noises) - got = samples["Local"]["ground-rydberg"]["q0"]["det"] - - np.testing.assert_array_equal(got, want) - - -def test_amplitude_noise(): - """Test the noise related to the amplitude profile of global pulses.""" - N = 100 - amplitude = 1.0 - waist_width = 2.0 # µm - - coords = np.array([[-2.0, 0.0], [0.0, 0.0], [2.0, 0.0]]) - reg = pulser.Register.from_coordinates(coords, prefix="q") - seq = pulser.Sequence(reg, MockDevice) - seq.declare_channel("ch0", "rydberg_global") - seq.add( - Pulse.ConstantAmplitude(amplitude, ConstantWaveform(N, 0.0), 0.0), - "ch0", - ) - seq.measure() - - def expected_samples(vec: np.ndarray) -> np.ndarray: - """Defines the non-noisy effect of a gaussian amplitude profile.""" - r = np.linalg.norm(vec) - a = np.ones(N) - a *= amplitude - a *= np.exp(-((r / waist_width) ** 2)) - return a - - s = sample( - seq, global_noises=[noises.amplitude(reg, waist_width, random=False)] - ) - - for q, coords in reg.qubits.items(): - got = s["Local"]["ground-rydberg"][q]["amp"] - want = expected_samples(coords) - np.testing.assert_equal(got, want) - - @pytest.fixture def seqs(seq_rydberg) -> list[pulser.Sequence]: seqs: list[pulser.Sequence] = [] From 2c8c07a97e77b2f7bc57cbf0a6afbcacf86d9ee8 Mon Sep 17 00:00:00 2001 From: Louis Vignoli Date: Fri, 25 Mar 2022 14:50:38 +0100 Subject: [PATCH 39/47] Fix the unneeded # pragma: no cover in _sample_slots --- pulser/sampler/sampler.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/pulser/sampler/sampler.py b/pulser/sampler/sampler.py index 110935399..64c31e1a0 100644 --- a/pulser/sampler/sampler.py +++ b/pulser/sampler/sampler.py @@ -185,9 +185,8 @@ def _sample_slots(N: int, *slots: _TimeSlot) -> list[QubitSamples]: # weird, it's not enforced by the structure,bad design?) qubits = slots[0].targets amp, det, phase = np.zeros(N), np.zeros(N), np.zeros(N) - for s in slots: - if type(s.type) is str: # pragma: no cover - continue + pulse_slots = [s for s in slots if isinstance(s.type, Pulse)] + for s in pulse_slots: pulse = cast(Pulse, s.type) amp[s.ti : s.tf] += pulse.amplitude.samples det[s.ti : s.tf] += pulse.detuning.samples From 9697a2dd17a0e8343667013cca9c1df333cc6cce Mon Sep 17 00:00:00 2001 From: Louis Vignoli Date: Fri, 25 Mar 2022 15:42:19 +0100 Subject: [PATCH 40/47] Add a Failing test for the SLM, for discussion --- pulser/tests/test_sequence_sampler.py | 41 +++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/pulser/tests/test_sequence_sampler.py b/pulser/tests/test_sequence_sampler.py index 2d873b36b..2206987fe 100644 --- a/pulser/tests/test_sequence_sampler.py +++ b/pulser/tests/test_sequence_sampler.py @@ -1,5 +1,6 @@ from __future__ import annotations +import textwrap from copy import deepcopy import numpy as np @@ -135,6 +136,46 @@ def test_modulation(mod_seq: pulser.Sequence) -> None: np.testing.assert_array_equal(got, want) +slm_reason = textwrap.dedent( + """ + If the SLM is on, Global channels decay to local ones in the + sampler, such that the Global key in the output dict is empty and + all the samples are written in the Local dict. On the contrary, the + simulation module use the Local dict only for the first pulse, and + then write the remaining in the Global dict. + """ +) + + +@pytest.mark.xfail(reason=slm_reason) +def test_SLM(): + q_dict = { + "batman": np.array([-4.0, 0.0]), # sometimes masked + "superman": np.array([4.0, 0.0]), # always unmasked + } + + reg = pulser.Register(q_dict) + seq = pulser.Sequence(reg, MockDevice) + + seq.declare_channel("ch0", "rydberg_global") + seq.config_slm_mask(["batman"]) + + seq.add( + Pulse.ConstantDetuning(BlackmanWaveform(200, np.pi / 2), 0.0, 0.0), + "ch0", + ) + seq.add( + Pulse.ConstantDetuning(BlackmanWaveform(200, np.pi / 2), 0.0, 0.0), + "ch0", + ) + seq.measure() + + print(sample(seq)) + print(pulser.Simulation(seq).samples) + + check_same_samples_as_sim(seq) + + @pytest.fixture def seqs(seq_rydberg) -> list[pulser.Sequence]: seqs: list[pulser.Sequence] = [] From 198c094545b65599aad321f445d8a824ce5bba03 Mon Sep 17 00:00:00 2001 From: Louis Vignoli Date: Mon, 28 Mar 2022 10:15:52 +0200 Subject: [PATCH 41/47] Move the comment on performance --- pulser/sampler/sampler.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/pulser/sampler/sampler.py b/pulser/sampler/sampler.py index 64c31e1a0..2cb19d99b 100644 --- a/pulser/sampler/sampler.py +++ b/pulser/sampler/sampler.py @@ -1,9 +1,6 @@ """Exposes the sample() functions. It contains many helpers. - -NOTE(perf): it not very efficient to hold the same data for all qubit in global -channels, but i remains manageable for register # with less than 100 qubits. """ from __future__ import annotations @@ -53,6 +50,10 @@ def sample( # 3. modulate # 4. apply noises/SLM # 5. write samples + # + # NOTE(perf): it not very efficient to hold copies of the same data for + # every qubits in a global channel, but it remains manageable for registers + # with less than 100 qubits. samples: dict[str, list[QubitSamples]] = {} addrs: dict[str, str] = {} From 373e10fdcf2d1acd8d24252d2379b25ea33a8c0e Mon Sep 17 00:00:00 2001 From: Louis Vignoli Date: Mon, 28 Mar 2022 10:17:43 +0200 Subject: [PATCH 42/47] Make the SLM detectino more idiomatic --- pulser/sampler/sampler.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pulser/sampler/sampler.py b/pulser/sampler/sampler.py index 2cb19d99b..aaf2243ef 100644 --- a/pulser/sampler/sampler.py +++ b/pulser/sampler/sampler.py @@ -65,7 +65,7 @@ def sample( ch_noises = list(common_noises) - slm_on = len(seq._slm_mask_targets) > 0 and len(seq._slm_mask_time) > 0 + slm_on = seq._slm_mask_targets and seq._slm_mask_time if addr == "Global": decay = slm_on or len(global_noises) > 0 or len(common_noises) > 0 From 0720c502947bf7ca5cae8e7590273fb80975761a Mon Sep 17 00:00:00 2001 From: Louis Vignoli Date: Mon, 28 Mar 2022 10:54:33 +0200 Subject: [PATCH 43/47] Simplify the assertions for sequence in XY --- pulser/tests/test_sequence_sampler.py | 23 +---------------------- 1 file changed, 1 insertion(+), 22 deletions(-) diff --git a/pulser/tests/test_sequence_sampler.py b/pulser/tests/test_sequence_sampler.py index 2206987fe..a0a7ca6c1 100644 --- a/pulser/tests/test_sequence_sampler.py +++ b/pulser/tests/test_sequence_sampler.py @@ -93,34 +93,13 @@ def test_inXY() -> None: RampWaveform(200, -np.pi / 2, np.pi / 2), 0.0, ) - reg = pulser.Register.from_coordinates(np.array([[0.0, 0.0]]), prefix="q") seq = pulser.Sequence(reg, MockDevice) seq.declare_channel("ch0", "mw_global") seq.add(pulse, "ch0") seq.measure(basis="XY") - sim = pulser.Simulation(seq) - - want = sim.samples - got = sample(seq) - - for qty in ["amp", "det", "phase"]: - np.testing.assert_array_equal( - got["Global"]["XY"][qty], want["Global"]["XY"][qty] - ) - - for q in seq._qids: - for qty in ["amp", "det", "phase"]: - try: - np.testing.assert_array_equal( - got["Local"]["XY"][q][qty], want["Local"]["XY"][q][qty] - ) - except KeyError: - np.testing.assert_array_equal( - got["Local"]["XY"][q][qty], - np.zeros(len(got["Local"]["XY"][q][qty])), - ) + check_same_samples_as_sim(seq) def test_modulation(mod_seq: pulser.Sequence) -> None: From eeeba301235d6a368e76ee4670c14ab484d39a3f Mon Sep 17 00:00:00 2001 From: Louis Vignoli Date: Mon, 28 Mar 2022 11:33:56 +0200 Subject: [PATCH 44/47] Remove superfluous print statements --- pulser/tests/test_sequence_sampler.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/pulser/tests/test_sequence_sampler.py b/pulser/tests/test_sequence_sampler.py index a0a7ca6c1..79662ab37 100644 --- a/pulser/tests/test_sequence_sampler.py +++ b/pulser/tests/test_sequence_sampler.py @@ -149,8 +149,6 @@ def test_SLM(): ) seq.measure() - print(sample(seq)) - print(pulser.Simulation(seq).samples) check_same_samples_as_sim(seq) From 0201e14a52dcc511cd33ef51b813c789c4396d2d Mon Sep 17 00:00:00 2001 From: Louis Vignoli Date: Mon, 28 Mar 2022 11:35:37 +0200 Subject: [PATCH 45/47] Test SLM sampling alongside the check against simulation --- pulser/tests/test_sequence_sampler.py | 71 +++++++++++++++++++++------ 1 file changed, 57 insertions(+), 14 deletions(-) diff --git a/pulser/tests/test_sequence_sampler.py b/pulser/tests/test_sequence_sampler.py index 79662ab37..767bf4931 100644 --- a/pulser/tests/test_sequence_sampler.py +++ b/pulser/tests/test_sequence_sampler.py @@ -115,19 +115,8 @@ def test_modulation(mod_seq: pulser.Sequence) -> None: np.testing.assert_array_equal(got, want) -slm_reason = textwrap.dedent( - """ - If the SLM is on, Global channels decay to local ones in the - sampler, such that the Global key in the output dict is empty and - all the samples are written in the Local dict. On the contrary, the - simulation module use the Local dict only for the first pulse, and - then write the remaining in the Global dict. - """ -) - - -@pytest.mark.xfail(reason=slm_reason) -def test_SLM(): +@pytest.fixture +def seq_with_SLM() -> pulser.Sequence: q_dict = { "batman": np.array([-4.0, 0.0]), # sometimes masked "superman": np.array([4.0, 0.0]), # always unmasked @@ -148,9 +137,63 @@ def test_SLM(): "ch0", ) seq.measure() + return seq - check_same_samples_as_sim(seq) +def test_SLM_samples(seq_with_SLM): + pulse = Pulse.ConstantDetuning(BlackmanWaveform(200, np.pi / 2), 0.0, 0.0) + a_samples = pulse.amplitude.samples + + def z() -> np.ndarray: + return np.zeros(seq_with_SLM.get_duration()) + + want: dict = { + "Global": {}, + "Local": { + "ground-rydberg": { + "batman": {"amp": z(), "det": z(), "phase": z()}, + "superman": {"amp": z(), "det": z(), "phase": z()}, + } + }, + } + want["Local"]["ground-rydberg"]["batman"]["amp"][200:401] = a_samples + want["Local"]["ground-rydberg"]["superman"]["amp"][0:200] = a_samples + want["Local"]["ground-rydberg"]["superman"]["amp"][200:401] = a_samples + + got = sample(seq_with_SLM) + assert_nested_dict_equality(got, want) + + +slm_reason = textwrap.dedent( + """ +If the SLM is on, Global channels decay to local ones in the +sampler, such that the Global key in the output dict is empty and +all the samples are written in the Local dict. On the contrary, the +simulation module use the Local dict only for the first pulse, and +then write the remaining in the Global dict. +""" +) + + +@pytest.mark.xfail(reason=slm_reason) +def test_SLM_against_simulation(seq_with_SLM): + check_same_samples_as_sim(seq_with_SLM) + + +def assert_nested_dict_equality(got, want: dict) -> None: + for basis in want["Global"]: + for qty in want["Global"][basis]: + np.testing.assert_array_equal( + got["Global"][basis][qty], + want["Global"][basis][qty], + ) + for basis in want["Local"]: + for qubit in want["Local"][basis]: + for qty in want["Local"][basis][qubit]: + np.testing.assert_array_equal( + got["Local"][basis][qubit][qty], + want["Local"][basis][qubit][qty], + ) @pytest.fixture From aefbc9e5a38719125d82af00387fea619d2e1145 Mon Sep 17 00:00:00 2001 From: Louis Vignoli Date: Mon, 28 Mar 2022 11:36:46 +0200 Subject: [PATCH 46/47] Refactor using a helper for nested dicts --- pulser/tests/test_sequence_sampler.py | 14 +------------- 1 file changed, 1 insertion(+), 13 deletions(-) diff --git a/pulser/tests/test_sequence_sampler.py b/pulser/tests/test_sequence_sampler.py index 767bf4931..ae925d5bf 100644 --- a/pulser/tests/test_sequence_sampler.py +++ b/pulser/tests/test_sequence_sampler.py @@ -65,19 +65,7 @@ def check_same_samples_as_sim(seq: pulser.Sequence): got = sample(seq) want = pulser.Simulation(seq).samples - for basis in want["Global"]: - for qty in want["Global"][basis]: - np.testing.assert_array_equal( - got["Global"][basis][qty], - want["Global"][basis][qty], - ) - for basis in want["Local"]: - for qubit in want["Local"][basis]: - for qty in want["Local"][basis][qubit]: - np.testing.assert_array_equal( - got["Local"][basis][qubit][qty], - want["Local"][basis][qubit][qty], - ) + assert_nested_dict_equality(got, want) def test_table_sequence(seqs): From 35d4b75e9a55b96feb683f72395d79181cfcc321 Mon Sep 17 00:00:00 2001 From: Louis Vignoli Date: Mon, 28 Mar 2022 11:44:38 +0200 Subject: [PATCH 47/47] Rearrange the test file --- pulser/tests/test_sequence_sampler.py | 104 ++++++++++++++------------ 1 file changed, 56 insertions(+), 48 deletions(-) diff --git a/pulser/tests/test_sequence_sampler.py b/pulser/tests/test_sequence_sampler.py index ae925d5bf..c170e4da2 100644 --- a/pulser/tests/test_sequence_sampler.py +++ b/pulser/tests/test_sequence_sampler.py @@ -15,31 +15,34 @@ from pulser.sampler.samples import QubitSamples from pulser.waveforms import BlackmanWaveform, RampWaveform +# Helpers -def test_corner_cases(): - """Test corner cases of helper functions.""" - with pytest.raises( - ValueError, - match="ndarrays amp, det and phase must have the same length.", - ): - _ = QubitSamples( - amp=np.array([1.0]), - det=np.array([1.0]), - phase=np.array([1.0, 1.0]), - qubit="q0", - ) - reg = pulser.Register.square(1, prefix="q") - seq = pulser.Sequence(reg, MockDevice) - N, M = 10, 11 - samples_dict = { - "a": [QubitSamples(np.zeros(N), np.zeros(N), np.zeros(N), "q0")], - "b": [QubitSamples(np.zeros(M), np.zeros(M), np.zeros(M), "q0")], - } - with pytest.raises( - ValueError, match="All the samples do not share the same duration." - ): - _write_dict(seq, samples_dict, {}) +def assert_same_samples_as_sim(seq: pulser.Sequence) -> None: + """Check against the legacy sample extraction in the simulation module.""" + got = sample(seq) + want = pulser.Simulation(seq).samples + + assert_nested_dict_equality(got, want) + + +def assert_nested_dict_equality(got, want: dict) -> None: + for basis in want["Global"]: + for qty in want["Global"][basis]: + np.testing.assert_array_equal( + got["Global"][basis][qty], + want["Global"][basis][qty], + ) + for basis in want["Local"]: + for qubit in want["Local"][basis]: + for qty in want["Local"][basis][qubit]: + np.testing.assert_array_equal( + got["Local"][basis][qubit][qty], + want["Local"][basis][qubit][qty], + ) + + +# Tests def test_one_pulse_sampling(): @@ -60,18 +63,10 @@ def test_one_pulse_sampling(): np.testing.assert_array_equal(got[key], want[i]) -def check_same_samples_as_sim(seq: pulser.Sequence): - """Check against the legacy sample extraction in the simulation module.""" - got = sample(seq) - want = pulser.Simulation(seq).samples - - assert_nested_dict_equality(got, want) - - def test_table_sequence(seqs): """A table-driven test designed to be extended easily.""" for seq in seqs: - check_same_samples_as_sim(seq) + assert_same_samples_as_sim(seq) def test_inXY() -> None: @@ -87,7 +82,7 @@ def test_inXY() -> None: seq.add(pulse, "ch0") seq.measure(basis="XY") - check_same_samples_as_sim(seq) + assert_same_samples_as_sim(seq) def test_modulation(mod_seq: pulser.Sequence) -> None: @@ -165,23 +160,36 @@ def z() -> np.ndarray: @pytest.mark.xfail(reason=slm_reason) def test_SLM_against_simulation(seq_with_SLM): - check_same_samples_as_sim(seq_with_SLM) + assert_same_samples_as_sim(seq_with_SLM) -def assert_nested_dict_equality(got, want: dict) -> None: - for basis in want["Global"]: - for qty in want["Global"][basis]: - np.testing.assert_array_equal( - got["Global"][basis][qty], - want["Global"][basis][qty], - ) - for basis in want["Local"]: - for qubit in want["Local"][basis]: - for qty in want["Local"][basis][qubit]: - np.testing.assert_array_equal( - got["Local"][basis][qubit][qty], - want["Local"][basis][qubit][qty], - ) +def test_corner_cases(): + """Test corner cases of helper functions.""" + with pytest.raises( + ValueError, + match="ndarrays amp, det and phase must have the same length.", + ): + _ = QubitSamples( + amp=np.array([1.0]), + det=np.array([1.0]), + phase=np.array([1.0, 1.0]), + qubit="q0", + ) + + reg = pulser.Register.square(1, prefix="q") + seq = pulser.Sequence(reg, MockDevice) + N, M = 10, 11 + samples_dict = { + "a": [QubitSamples(np.zeros(N), np.zeros(N), np.zeros(N), "q0")], + "b": [QubitSamples(np.zeros(M), np.zeros(M), np.zeros(M), "q0")], + } + with pytest.raises( + ValueError, match="All the samples do not share the same duration." + ): + _write_dict(seq, samples_dict, {}) + + +# Fixtures @pytest.fixture