Skip to content

Commit

Permalink
Add the classes for EOM mode configuration (#417)
Browse files Browse the repository at this point in the history
* Creating the `pulser.channels` module

* Create the EOM related classes

* Splitting channels.py into two files

* Comments for detuning_off UT

* Documentation updates

* Adding comments to the EOMConfig class
  • Loading branch information
HGSilveri authored Nov 28, 2022
1 parent 890b3ff commit c437beb
Show file tree
Hide file tree
Showing 17 changed files with 622 additions and 101 deletions.
19 changes: 17 additions & 2 deletions docs/source/apidoc/core.rst
Original file line number Diff line number Diff line change
Expand Up @@ -98,12 +98,27 @@ which when associated with a :class:`pulser.Sequence` condition its development.


Channels
^^^^^^^^^^^
.. automodule:: pulser.channels
---------------------

Base Channel
^^^^^^^^^^^^^^^
.. automodule:: pulser.channels.base_channel
:members:


Available Channels
^^^^^^^^^^^^^^^^^^^^^^^
.. automodule:: pulser.channels.channels
:members:
:show-inheritance:


EOM Mode Configuration
^^^^^^^^^^^^^^^^^^^^^^^^
.. automodule:: pulser.channels.eom
:members:
:show-inheritance:

Sampler
------------------
.. automodule:: pulser.sampler.sampler
Expand Down
16 changes: 16 additions & 0 deletions pulser-core/pulser/channels/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# Copyright 2022 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.
"""The various hardware channel types."""

from pulser.channels.channels import Microwave, Raman, Rydberg
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Copyright 2020 Pulser Development Team
# Copyright 2022 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.
Expand All @@ -11,20 +11,21 @@
# 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.
"""The various hardware channel types."""
"""Defines the Channel ABC."""

from __future__ import annotations

import warnings
from abc import ABC, abstractmethod
from dataclasses import asdict, dataclass
from dataclasses import dataclass, field, fields
from sys import version_info
from typing import Any, Optional, cast

import numpy as np
from numpy.typing import ArrayLike
from scipy.fft import fft, fftfreq, ifft

from pulser.channels.eom import MODBW_TO_TR, BaseEOM
from pulser.json.utils import obj_to_dict
from pulser.pulse import Pulse

Expand All @@ -43,11 +44,6 @@
# Warnings of adjusted waveform duration appear just once
warnings.filterwarnings("once", "A duration of")

# Conversion factor from modulation bandwith to rise time
# For more info, see https://tinyurl.com/bdeumc8k
MODBW_TO_TR = 0.48

ADDRESSING = Literal["Global", "Local"]
CH_TYPE = Literal["Rydberg", "Raman", "Microwave"]
BASIS = Literal["ground-rydberg", "digital", "XY"]

Expand Down Expand Up @@ -83,7 +79,7 @@ class Channel(ABC):
call ``Rydberg.Global(...)``.
"""

addressing: ADDRESSING
addressing: Literal["Global", "Local"]
max_abs_detuning: Optional[float]
max_amp: Optional[float]
phase_jump_time: int = 0
Expand All @@ -94,6 +90,7 @@ class Channel(ABC):
min_duration: int = 1 # ns
max_duration: Optional[int] = int(1e8) # ns
mod_bandwidth: Optional[float] = None # MHz
eom_config: Optional[BaseEOM] = field(init=False, default=None)

@property
def name(self) -> CH_TYPE:
Expand All @@ -116,7 +113,7 @@ def __post_init__(self) -> None:
internal_param_value_pairs = [
("name", CH_TYPE),
("basis", BASIS),
("addressing", ADDRESSING),
("addressing", Literal["Global", "Local"]),
]
for param, type_options in internal_param_value_pairs:
value = getattr(self, param)
Expand Down Expand Up @@ -210,6 +207,10 @@ def is_virtual(self) -> bool:
"""Whether the channel is virtual (i.e. partially defined)."""
return bool(self._undefined_fields())

def supports_eom(self) -> bool:
"""Whether the channel supports EOM mode operation."""
return hasattr(self, "eom_config") and self.eom_config is not None

def _undefined_fields(self) -> list[str]:
optional = [
"max_amp",
Expand Down Expand Up @@ -366,48 +367,62 @@ def validate_pulse(self, pulse: Pulse) -> None:
)

def modulate(
self, input_samples: np.ndarray, keep_ends: bool = False
self,
input_samples: np.ndarray,
keep_ends: bool = False,
eom: bool = False,
) -> np.ndarray:
"""Modulates the input according to the channel's modulation bandwidth.
Args:
input_samples: The samples to modulate.
keep_ends: Assume the end values of the samples were kept
constant (i.e. there is no ramp from zero on the ends).
eom: Whether to calculate the modulation using the EOM
bandwidth.
Returns:
The modulated output signal.
"""
if not self.mod_bandwidth:
if eom:
if not self.supports_eom():
raise TypeError(f"The channel {self} does not have an EOM.")
eom_config = cast(BaseEOM, self.eom_config)
mod_bandwidth = eom_config.mod_bandwidth
rise_time = eom_config.rise_time

elif not self.mod_bandwidth:
warnings.warn(
f"No modulation bandwidth defined for channel '{self}',"
" 'Channel.modulate()' returns the 'input_samples' unchanged.",
stacklevel=2,
)
return input_samples
else:
mod_bandwidth = self.mod_bandwidth
rise_time = self.rise_time

# The cutoff frequency (fc) and the modulation transfer function
# are defined in https://tinyurl.com/bdeumc8k
fc = self.mod_bandwidth * 1e-3 / np.sqrt(np.log(2))
fc = mod_bandwidth * 1e-3 / np.sqrt(np.log(2))
if keep_ends:
samples = np.pad(input_samples, 2 * self.rise_time, mode="edge")
samples = np.pad(input_samples, 2 * rise_time, mode="edge")
else:
samples = np.pad(input_samples, self.rise_time)
samples = np.pad(input_samples, rise_time)
freqs = fftfreq(samples.size)
modulation = np.exp(-(freqs**2) / fc**2)
mod_samples = ifft(fft(samples) * modulation).real
if keep_ends:
# Cut off the extra ends
return cast(
np.ndarray, mod_samples[self.rise_time : -self.rise_time]
)
return cast(np.ndarray, mod_samples[rise_time:-rise_time])
return cast(np.ndarray, mod_samples)

def calc_modulation_buffer(
self,
input_samples: ArrayLike,
mod_samples: ArrayLike,
max_allowed_diff: float = 1e-2,
eom: bool = False,
) -> tuple[int, int]:
"""Calculates the minimal buffers needed around a modulated waveform.
Expand All @@ -417,17 +432,23 @@ def calc_modulation_buffer(
``len(input_samples) + 2 * self.rise_time``.
max_allowed_diff: The maximum allowed difference between
the input and modulated samples at the end points.
eom: Whether to calculate the modulation buffers with the EOM
bandwidth.
Returns:
The minimum buffer times at the start and end of
the samples, in ns.
"""
if not self.mod_bandwidth:
raise TypeError(
f"The channel {self} doesn't have a modulation bandwidth."
)

tr = self.rise_time
if eom:
if not self.supports_eom():
raise TypeError(f"The channel {self} does not have an EOM.")
tr = cast(BaseEOM, self.eom_config).rise_time
else:
if not self.mod_bandwidth:
raise TypeError(
f"The channel {self} doesn't have a modulation bandwidth."
)
tr = self.rise_time
samples = np.pad(input_samples, tr)
diffs = np.abs(samples - mod_samples) <= max_allowed_diff
try:
Expand Down Expand Up @@ -475,46 +496,7 @@ def __repr__(self) -> str:
return self.name + config

def _to_dict(self) -> dict[str, Any]:
return obj_to_dict(self, **asdict(self))


@dataclass(init=True, repr=False, frozen=True)
class Raman(Channel):
"""Raman beam channel.
Channel targeting the transition between the hyperfine ground states, in
which the 'digital' basis is encoded. See base class.
"""

@property
def basis(self) -> Literal["digital"]:
"""The addressed basis name."""
return "digital"


@dataclass(init=True, repr=False, frozen=True)
class Rydberg(Channel):
"""Rydberg beam channel.
Channel targeting the transition between the ground and rydberg states,
thus enconding the 'ground-rydberg' basis. See base class.
"""

@property
def basis(self) -> Literal["ground-rydberg"]:
"""The addressed basis name."""
return "ground-rydberg"


@dataclass(init=True, repr=False, frozen=True)
class Microwave(Channel):
"""Microwave adressing channel.
Channel targeting the transition between two rydberg states, thus encoding
the 'XY' basis. See base class.
"""

@property
def basis(self) -> Literal["XY"]:
"""The addressed basis name."""
return "XY"
params = {
f.name: getattr(self, f.name) for f in fields(self) if f.init
}
return obj_to_dict(self, _module="pulser.channels", **params)
80 changes: 80 additions & 0 deletions pulser-core/pulser/channels/channels.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
# 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.
"""The Channel subclasses."""

from __future__ import annotations

from dataclasses import dataclass
from typing import Optional

from pulser.channels.base_channel import Channel, Literal
from pulser.channels.eom import RydbergEOM


@dataclass(init=True, repr=False, frozen=True)
class Raman(Channel):
"""Raman beam channel.
Channel targeting the transition between the hyperfine ground states, in
which the 'digital' basis is encoded. See base class.
"""

@property
def basis(self) -> Literal["digital"]:
"""The addressed basis name."""
return "digital"


@dataclass(init=True, repr=False, frozen=True)
class Rydberg(Channel):
"""Rydberg beam channel.
Channel targeting the transition between the ground and rydberg states,
thus enconding the 'ground-rydberg' basis. See base class.
"""

eom_config: Optional[RydbergEOM] = None

def __post_init__(self) -> None:
super().__post_init__()
if self.eom_config is not None:
if not isinstance(self.eom_config, RydbergEOM):
raise TypeError(
"When defined, 'eom_config' must be a valid 'RydbergEOM'"
f" instance, not {type(self.eom_config)}."
)
if self.mod_bandwidth is None:
raise ValueError(
"'eom_config' can't be defined in a Channel without a "
"modulation bandwidth."
)

@property
def basis(self) -> Literal["ground-rydberg"]:
"""The addressed basis name."""
return "ground-rydberg"


@dataclass(init=True, repr=False, frozen=True)
class Microwave(Channel):
"""Microwave adressing channel.
Channel targeting the transition between two rydberg states, thus encoding
the 'XY' basis. See base class.
"""

@property
def basis(self) -> Literal["XY"]:
"""The addressed basis name."""
return "XY"
Loading

0 comments on commit c437beb

Please sign in to comment.