Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Add the classes for EOM mode configuration #417

Merged
merged 7 commits into from
Nov 28, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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