Skip to content

Commit

Permalink
Release v0.12.0
Browse files Browse the repository at this point in the history
Main changes:
- Switching from pasqal-sdk to pasqal-cloud (#510)
- Impose qubit order in MappableRegister (#507) 
- FIX: Serialization of parametrized pulses with constant parameters (#514)
- Drop support for Python 3.7 (#513)
  • Loading branch information
HGSilveri authored May 2, 2023
2 parents 43ce7f8 + 02f890a commit b7adbc0
Show file tree
Hide file tree
Showing 37 changed files with 382 additions and 332 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ jobs:
strategy:
fail-fast: false
matrix:
python-version: ["3.7", "3.11"]
python-version: ["3.8", "3.11"]
steps:
- name: Check out Pulser
uses: actions/checkout@v3
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ jobs:
fail-fast: false
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
python-version: ["3.7", "3.8", "3.9", "3.10", "3.11"]
python-version: ["3.8", "3.9", "3.10", "3.11"]
steps:
- name: Check out Pulser
uses: actions/checkout@v3
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ jobs:
fail-fast: false
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
python-version: ["3.7", "3.8", "3.9", "3.10", "3.11"]
python-version: ["3.8", "3.9", "3.10", "3.11"]
steps:
- name: Check out Pulser
uses: actions/checkout@v3
Expand Down
3 changes: 2 additions & 1 deletion .mypy.ini
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@ disallow_untyped_defs = True
# 3rd-party libs without type hints nor stubs
[mypy-matplotlib.*,scipy.*,qutip.*,jsonschema.*]
follow_imports = silent
ignore_missing_imports = true
ignore_missing_imports = True

[mypy-tests.*]
disable_error_code = annotation-unchecked
disallow_untyped_defs = False
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ For a comprehensive overview of Pulser, check out [Pulser's white paper](https:/

**Note**: *Pulser v0.6 introduced a split of the ``pulser`` package that prevents it from being correctly upgraded. If you have an older version of ``pulser`` installed and wish to upgrade, make sure to uninstall it first by running ``pip uninstall pulser``.*

To install the latest release of ``pulser``, have Python 3.7.0 or higher installed, then use ``pip``:
To install the latest release of ``pulser``, have Python 3.8 or higher installed, then use ``pip``:

```bash
pip install pulser
Expand Down
2 changes: 1 addition & 1 deletion VERSION.txt
Original file line number Diff line number Diff line change
@@ -1 +1 @@
0.11.1
0.12.0
2 changes: 1 addition & 1 deletion dev_requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ black[jupyter] ~= 23.1
flake8
flake8-docstrings
isort
mypy == 0.921
mypy == 1.2
pytest
pytest-cov

Expand Down
2 changes: 1 addition & 1 deletion docs/source/installation.rst
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ Installation

before proceeding to any of the steps below.

To install the latest release of ``pulser``, have Python 3.7.0 or higher
To install the latest release of ``pulser``, have Python 3.8 or higher
installed, then use ``pip``: ::

pip install pulser
Expand Down
17 changes: 2 additions & 15 deletions pulser-core/pulser/channels/base_channel.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,7 @@
import warnings
from abc import ABC, abstractmethod
from dataclasses import dataclass, field, fields
from sys import version_info
from typing import Any, Optional, Type, TypeVar, cast
from typing import Any, Literal, Optional, Type, TypeVar, cast

import numpy as np
from numpy.typing import ArrayLike
Expand All @@ -29,25 +28,13 @@
from pulser.json.utils import obj_to_dict
from pulser.pulse import Pulse

if version_info[:2] >= (3, 8): # pragma: no cover
from typing import Literal
else: # pragma: no cover
try:
from typing_extensions import Literal # type: ignore
except ImportError:
raise ImportError(
"Using pulser with Python version 3.7 requires the"
" `typing_extensions` module. Install it by running"
" `pip install typing-extensions`."
)

# Warnings of adjusted waveform duration appear just once
warnings.filterwarnings("once", "A duration of")

ChannelType = TypeVar("ChannelType", bound="Channel")


@dataclass(init=True, repr=False, frozen=True) # type: ignore[misc]
@dataclass(init=True, repr=False, frozen=True)
class Channel(ABC):
"""Base class of a hardware channel.
Expand Down
22 changes: 6 additions & 16 deletions pulser-core/pulser/devices/_device_datacls.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,7 @@
from abc import ABC, abstractmethod
from collections import Counter
from dataclasses import dataclass, field, fields
from sys import version_info
from typing import Any, cast
from typing import Any, Literal, cast, get_args

import numpy as np
from scipy.spatial.distance import pdist, squareform
Expand All @@ -32,22 +31,10 @@
from pulser.register.mappable_reg import MappableRegister
from pulser.register.register_layout import COORD_PRECISION, RegisterLayout

if version_info[:2] >= (3, 8): # pragma: no cover
from typing import Literal, get_args
else: # pragma: no cover
try:
from typing_extensions import Literal, get_args # type: ignore
except ImportError:
raise ImportError(
"Using pulser with Python version 3.7 requires the"
" `typing_extensions` module. Install it by running"
" `pip install typing-extensions`."
)

DIMENSIONS = Literal[2, 3]


@dataclass(frozen=True, repr=False) # type: ignore[misc]
@dataclass(frozen=True, repr=False)
class BaseDevice(ABC):
r"""Base class of a neutral-atom device.
Expand Down Expand Up @@ -236,7 +223,10 @@ def rydberg_blockade_radius(self, rabi_frequency: float) -> float:
Returns:
The rydberg blockade radius, in μm.
"""
return (self.interaction_coeff / rabi_frequency) ** (1 / 6)
# mypy can't guarantee that float**float is a float, so we need to cast
return cast(
float, (self.interaction_coeff / rabi_frequency) ** (1 / 6)
)

def rabi_from_blockade(self, blockade_radius: float) -> float:
"""The maximum Rabi frequency value to enforce a given blockade radius.
Expand Down
44 changes: 36 additions & 8 deletions pulser-core/pulser/json/abstract_repr/deserializer.py
Original file line number Diff line number Diff line change
Expand Up @@ -219,14 +219,42 @@ def _deserialize_operation(seq: Sequence, op: dict, vars: dict) -> None:
*[_deserialize_parameter(t, vars) for t in op["targets"]],
)
elif op["op"] == "pulse":
pulse = Pulse(
amplitude=_deserialize_waveform(op["amplitude"], vars),
detuning=_deserialize_waveform(op["detuning"], vars),
phase=_deserialize_parameter(op["phase"], vars),
post_phase_shift=_deserialize_parameter(
op["post_phase_shift"], vars
),
)
phase = _deserialize_parameter(op["phase"], vars)
post_phase_shift = _deserialize_parameter(op["post_phase_shift"], vars)

# A waveform with a duration of 0 means the pulse was created with one
# of Pulse's class methods (ConstantAmplitude or ConstantDetuning) and
# the Pulse is parametrized
if (
op["amplitude"].get("duration") == 0
and op["amplitude"].get("kind") == "constant"
):
pulse = Pulse.ConstantAmplitude(
amplitude=_deserialize_parameter(
op["amplitude"]["value"], vars
),
detuning=_deserialize_waveform(op["detuning"], vars),
phase=phase,
post_phase_shift=post_phase_shift,
)
elif (
op["detuning"].get("duration") == 0
and op["detuning"].get("kind") == "constant"
):
pulse = Pulse.ConstantDetuning(
amplitude=_deserialize_waveform(op["amplitude"], vars),
detuning=_deserialize_parameter(op["detuning"]["value"], vars),
phase=phase,
post_phase_shift=post_phase_shift,
)
else:
pulse = Pulse(
amplitude=_deserialize_waveform(op["amplitude"], vars),
detuning=_deserialize_waveform(op["detuning"], vars),
phase=phase,
post_phase_shift=post_phase_shift,
)

seq.add(
pulse=pulse,
channel=op["channel"],
Expand Down
11 changes: 3 additions & 8 deletions pulser-core/pulser/json/abstract_repr/serializer.py
Original file line number Diff line number Diff line change
Expand Up @@ -153,14 +153,9 @@ def convert_targets(
og_dim = target_array.ndim
if og_dim == 0:
target_array = target_array[np.newaxis]
try:
indices = seq.register.find_indices(target_array.tolist())
# RuntimeError raised when calling seq.register for a MappableRegister
except RuntimeError:
raise NotImplementedError(
"Serialization of sequences with local operations and"
" a mappable register is currently not supported."
)
indices = seq.get_register(include_mappable=True).find_indices(
target_array.tolist()
)
return indices[0] if og_dim == 0 else indices

def get_kwarg_default(call_name: str, kwarg_name: str) -> Any:
Expand Down
29 changes: 14 additions & 15 deletions pulser-core/pulser/parametrized/paramobj.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,6 @@
class OpSupport:
"""Methods for supporting operators on parametrized objects."""

# TODO: Make operator methods' args pos-only when python 3.7 is dropped
# Unary operators
def __neg__(self) -> ParamObj:
return ParamObj(operator.neg, self)
Expand Down Expand Up @@ -93,46 +92,46 @@ def tan(self) -> ParamObj:
return ParamObj(np.tan, self)

# Binary operators
def __add__(self, other: Union[int, float]) -> ParamObj:
def __add__(self, other: Union[int, float], /) -> ParamObj:
return ParamObj(operator.add, self, other)

def __radd__(self, other: Union[int, float]) -> ParamObj:
def __radd__(self, other: Union[int, float], /) -> ParamObj:
return ParamObj(operator.add, other, self)

def __sub__(self, other: Union[int, float]) -> ParamObj:
def __sub__(self, other: Union[int, float], /) -> ParamObj:
return ParamObj(operator.sub, self, other)

def __rsub__(self, other: Union[int, float]) -> ParamObj:
def __rsub__(self, other: Union[int, float], /) -> ParamObj:
return ParamObj(operator.sub, other, self)

def __mul__(self, other: Union[int, float]) -> ParamObj:
def __mul__(self, other: Union[int, float], /) -> ParamObj:
return ParamObj(operator.mul, self, other)

def __rmul__(self, other: Union[int, float]) -> ParamObj:
def __rmul__(self, other: Union[int, float], /) -> ParamObj:
return ParamObj(operator.mul, other, self)

def __truediv__(self, other: Union[int, float]) -> ParamObj:
def __truediv__(self, other: Union[int, float], /) -> ParamObj:
return ParamObj(operator.truediv, self, other)

def __rtruediv__(self, other: Union[int, float]) -> ParamObj:
def __rtruediv__(self, other: Union[int, float], /) -> ParamObj:
return ParamObj(operator.truediv, other, self)

def __floordiv__(self, other: Union[int, float]) -> ParamObj:
def __floordiv__(self, other: Union[int, float], /) -> ParamObj:
return (self / other).__floor__()

def __rfloordiv__(self, other: Union[int, float]) -> ParamObj:
def __rfloordiv__(self, other: Union[int, float], /) -> ParamObj:
return (other / self).__floor__()

def __pow__(self, other: Union[int, float]) -> ParamObj:
def __pow__(self, other: Union[int, float], /) -> ParamObj:
return ParamObj(operator.pow, self, other)

def __rpow__(self, other: Union[int, float]) -> ParamObj:
def __rpow__(self, other: Union[int, float], /) -> ParamObj:
return ParamObj(operator.pow, other, self)

def __mod__(self, other: Union[int, float]) -> ParamObj:
def __mod__(self, other: Union[int, float], /) -> ParamObj:
return ParamObj(operator.mod, self, other)

def __rmod__(self, other: Union[int, float]) -> ParamObj:
def __rmod__(self, other: Union[int, float], /) -> ParamObj:
return ParamObj(operator.mod, other, self)


Expand Down
38 changes: 16 additions & 22 deletions pulser-core/pulser/register/mappable_reg.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,12 @@ def build_register(self, qubits: Mapping[QubitId, int]) -> BaseRegister:
raise ValueError(
"All qubits must be labeled with pre-declared qubit IDs."
)
elif set(chosen_ids) != set(self.qubit_ids[: len(chosen_ids)]):
raise ValueError(
f"To declare {len(qubits.keys())} qubits, 'qubits' should "
f"contain the first {len(qubits.keys())} elements of the "
"'qubit_ids'."
)
register_ordered_qubits = {
id: qubits[id] for id in self._qubit_ids if id in chosen_ids
}
Expand All @@ -80,10 +86,8 @@ def build_register(self, qubits: Mapping[QubitId, int]) -> BaseRegister:
qubit_ids=tuple(register_ordered_qubits.keys()),
)

def find_indices(
self, chosen_ids: set[QubitId], id_list: abcSequence[QubitId]
) -> list[int]:
"""Computes indices of qubits according to a register mapping.
def find_indices(self, id_list: abcSequence[QubitId]) -> list[int]:
"""Computes indices of qubits.
This can especially be useful when building a Pulser Sequence
with a parameter denoting qubits.
Expand All @@ -92,41 +96,31 @@ def find_indices(
Let ``reg`` be a mappable register with qubit Ids "a", "b", "c"
and "d".
>>> qubit_map = dict(b=1, a=2, d=0)
>>> reg.find_indices(
>>> qubit_map.keys(),
>>> ["a", "b", "d", "a"])
>>> reg.find_indices(["a", "b", "d", "a"])
It returns ``[0, 1, 2, 0]``, following the qubits order of the
mappable register, but keeping only the chosen ones.
It returns ``[0, 1, 3, 0]``, following the qubits order of the
mappable register (defined by qubit_ids).
Then, it is possible to use these indices when building a
sequence, typically to instanciate an array of variables
that can be provided as an argument to ``target_index``
and ``phase_shift_index``.
``qubit_map`` should be provided when building the sequence,
to tell how to instantiate the register from the mappable register.
When building a sequence and declaring N qubits, their ids should
refer to the first N elements of qubit_id.
Args:
chosen_ids: IDs of the qubits that are chosen to
map the MappableRegister
id_list: IDs of the qubits to denote.
Returns:
Indices of the qubits to denote, only valid for the
given mapping.
"""
if not chosen_ids <= set(self._qubit_ids):
raise ValueError(
"Chosen IDs must be selected among pre-declared qubit IDs."
)
if not set(id_list) <= chosen_ids:
if not set(id_list) <= set(self._qubit_ids):
raise ValueError(
"The IDs list must be selected among the chosen IDs."
"The IDs list must be selected among pre-declared qubit IDs."
)
ordered_ids = [id for id in self.qubit_ids if id in chosen_ids]
return [ordered_ids.index(id) for id in id_list]
return [self.qubit_ids.index(id) for id in id_list]

def _to_dict(self) -> dict[str, Any]:
return obj_to_dict(self, self._layout, *self._qubit_ids)
Expand Down
6 changes: 3 additions & 3 deletions pulser-core/pulser/register/register.py
Original file line number Diff line number Diff line change
Expand Up @@ -211,8 +211,8 @@ def max_connectivity(
cls,
n_qubits: int,
device: pulser.devices._device_datacls.BaseDevice,
spacing: float = None,
prefix: str = None,
spacing: float | None = None,
prefix: str | None = None,
) -> Register:
"""Initializes the register with maximum connectivity for a device.
Expand Down Expand Up @@ -296,7 +296,7 @@ def draw(
draw_graph: bool = True,
draw_half_radius: bool = False,
qubit_colors: Mapping[QubitId, str] = dict(),
fig_name: str = None,
fig_name: str | None = None,
kwargs_savefig: dict = {},
custom_ax: Optional[Axes] = None,
show: bool = True,
Expand Down
2 changes: 1 addition & 1 deletion pulser-core/pulser/register/register3d.py
Original file line number Diff line number Diff line change
Expand Up @@ -180,7 +180,7 @@ def draw(
draw_half_radius: bool = False,
qubit_colors: Mapping[QubitId, str] = dict(),
projection: bool = False,
fig_name: str = None,
fig_name: str | None = None,
kwargs_savefig: dict = {},
) -> None:
"""Draws the entire register.
Expand Down
Loading

0 comments on commit b7adbc0

Please sign in to comment.