Skip to content

Commit

Permalink
Release v0.13.0
Browse files Browse the repository at this point in the history
- Defining the `Result` class (#506 )
- Defining the `QutipEmulator` class (#519 )
  • Loading branch information
a-corni authored May 23, 2023
2 parents b7adbc0 + 35cbbe2 commit 34f9ea8
Show file tree
Hide file tree
Showing 25 changed files with 1,234 additions and 492 deletions.
2 changes: 1 addition & 1 deletion VERSION.txt
Original file line number Diff line number Diff line change
@@ -1 +1 @@
0.12.0
0.13.0
26 changes: 18 additions & 8 deletions pulser-core/pulser/devices/_device_datacls.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,8 @@ class BaseDevice(ABC):
max_radial_distance: The furthest away an atom can be from the center
of the array (in μm).
min_atom_distance: The closest together two atoms can be (in μm).
interaction_coeff_xy: :math:`C_3/\hbar` (in :math:`\mu m^3 / \mu s`),
interaction_coeff_xy: :math:`C_3/\hbar`
(in :math:`rad \cdot \mu s^{-1} \cdot \mu m^3`),
which sets the van der Waals interaction strength between atoms in
different Rydberg states. Needed only if there is a Microwave
channel in the device. If unsure, 3700.0 is a good default value.
Expand Down Expand Up @@ -208,7 +209,12 @@ def supported_bases(self) -> set[str]:

@property
def interaction_coeff(self) -> float:
r""":math:`C_6/\hbar` coefficient of chosen Rydberg level."""
r"""The interaction coefficient for the chosen Rydberg level.
Corresponds to :math:`C_6/\hbar` (in units of
:math:`rad \cdot \mu s^{-1} \cdot \mu m^6`)
for the interaction term of the Ising hamiltonian.
"""
return float(c6_dict[self.rydberg_level])

def __repr__(self) -> str:
Expand Down Expand Up @@ -432,9 +438,11 @@ class Device(BaseDevice):
max_radial_distance: The furthest away an atom can be from the center
of the array (in μm).
min_atom_distance: The closest together two atoms can be (in μm).
interaction_coeff_xy: :math:`C_3/\hbar` (in :math:`\mu m^3 / \mu s`),
interaction_coeff_xy: :math:`C_3/\hbar`
(in :math:`rad \cdot \mu s^{-1} \cdot \mu m^3`),
which sets the van der Waals interaction strength between atoms in
different Rydberg states.
different Rydberg states. Needed only if there is a Microwave
channel in the device. If unsure, 3700.0 is a good default value.
supports_slm_mask: Whether the device supports the SLM mask feature.
max_layout_filling: The largest fraction of a layout that can be filled
with atoms.
Expand Down Expand Up @@ -567,9 +575,11 @@ class VirtualDevice(BaseDevice):
max_radial_distance: The furthest away an atom can be from the center
of the array (in μm).
min_atom_distance: The closest together two atoms can be (in μm).
interaction_coeff_xy: :math:`C_3/\hbar` (in :math:`\mu m^3 / \mu s`),
interaction_coeff_xy: :math:`C_3/\hbar`
(in :math:`rad \cdot \mu s^{-1} \cdot \mu m^3`),
which sets the van der Waals interaction strength between atoms in
different Rydberg states.
different Rydberg states. Needed only if there is a Microwave
channel in the device. If unsure, 3700.0 is a good default value.
supports_slm_mask: Whether the device supports the SLM mask feature.
max_layout_filling: The largest fraction of a layout that can be filled
with atoms.
Expand All @@ -587,9 +597,9 @@ def _optional_parameters(self) -> tuple[str, ...]:
return ("max_atom_num", "max_radial_distance")

def change_rydberg_level(self, ryd_lvl: int) -> None:
"""Changes the Rydberg level used in the Device.
r"""Changes the Rydberg level used in the Device.
Find the :math:`C_6` coefficient matching the Rydberg level on
Find the :math:`C_6/\hbar` coefficient matching the Rydberg level on
`this page <https://github.com/pasqal-io/Pulser/blob/develop/
pulser-core/pulser/devices/interaction_coefficients/C6_coeffs.json>`_
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,14 @@
# 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.
"""C_6/hbar (in um^6 / us`), coeffs for Rydberg levels between 50 and 100."""
"""C_6/hbar (in rad/µs x µm^6), for Rydberg levels between 50 and 100.
The values were calculated using ARC_ and double checked with
PairInteraction_.
.. _ARC: https://arc-alkali-rydberg-calculator.readthedocs.io/
.. _PairInteraction: https://www.pairinteraction.org/
"""

import json
from pathlib import PurePath
Expand Down
10 changes: 8 additions & 2 deletions pulser-core/pulser/json/abstract_repr/serializer.py
Original file line number Diff line number Diff line change
Expand Up @@ -94,13 +94,19 @@ def abstract_repr(name: str, *args: Any, **kwargs: Any) -> dict[str, Any]:


def serialize_abstract_sequence(
seq: Sequence, seq_name: str = "pulser-exported", **defaults: Any
seq: Sequence,
seq_name: str = "pulser-exported",
json_dumps_options: dict[str, Any] = {},
**defaults: Any,
) -> str:
"""Serializes the Sequence into an abstract JSON object.
Keyword Args:
seq_name (str): A name for the sequence. If not defined, defaults
to "pulser-exported".
json_dumps_options: A mapping between optional parameters of
``json.dumps()`` (as string) and their value (parameter cannot
be "cls").
defaults: The default values for all the variables declared in this
Sequence instance, indexed by the name given upon declaration.
Check ``Sequence.declared_variables`` to see all the variables.
Expand Down Expand Up @@ -286,4 +292,4 @@ def get_all_args(
else:
raise AbstractReprError(f"Unknown call '{call.name}'.")

return json.dumps(res, cls=AbstractReprEncoder)
return json.dumps(res, cls=AbstractReprEncoder, **json_dumps_options)
15 changes: 2 additions & 13 deletions pulser-core/pulser/parametrized/paramobj.py
Original file line number Diff line number Diff line change
Expand Up @@ -235,13 +235,13 @@ def class_to_dict(cls: Callable) -> dict[str, Any]:
return obj_to_dict(self, cls_dict, *args, **self.kwargs)

def _to_abstract_repr(self) -> dict[str, Any]:
op_name = self.cls.__name__
if isinstance(self.cls, Parametrized):
raise ValueError(
"Serialization of calls to parametrized objects is not "
"supported."
)
elif (
op_name = self.cls.__name__
if (
self.args # If it is a classmethod the first arg will be the class
and hasattr(self.args[0], op_name)
and inspect.isfunction(self.cls)
Expand Down Expand Up @@ -344,17 +344,6 @@ def __call__(self, *args: Any, **kwargs: Any) -> ParamObj:
)
return obj

def __getattr__(self, name: str) -> ParamObj:
if hasattr(self.cls, name):
warnings.warn(
"Serialization of 'getattr' calls to parametrized objects "
"is not supported, so this object can't be serialized.",
stacklevel=2,
)
return ParamObj(getattr, self, name)
else:
raise AttributeError(f"No attribute named '{name}' in {self}.")

def __str__(self) -> str:
args = [str(a) for a in self.args]
kwargs = [f"{key}={str(value)}" for key, value in self.kwargs.items()]
Expand Down
157 changes: 157 additions & 0 deletions pulser-core/pulser/result.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
# Copyright 2023 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.
"""Classes to store measurement results."""
from __future__ import annotations

from abc import ABC, abstractmethod
from collections import Counter
from dataclasses import dataclass
from typing import Any

import matplotlib.pyplot as plt
import numpy as np

from pulser.register import QubitId


@dataclass
class Result(ABC):
"""Base class for storing the result of a sequence run."""

atom_order: tuple[QubitId, ...]
meas_basis: str

@property
def sampling_dist(self) -> dict[str, float]:
"""Sampling distribution of the measured bitstring.
Args:
atom_order: The order of the atoms in the bitstrings that
represent the measured states.
meas_basis: The measurement basis.
"""
n = self._size
return {
np.binary_repr(ind, width=n): prob
for ind, prob in enumerate(self._weights())
if prob != 0
}

@property
@abstractmethod
def sampling_errors(self) -> dict[str, float]:
"""The sampling error associated to each bitstring's sampling rate.
Uses the standard error of the mean as a quantifier for sampling error.
"""
pass

@property
def _size(self) -> int:
return len(self.atom_order)

@abstractmethod
def _weights(self) -> np.ndarray:
"""The sampling rate for every state in an ordered array."""
pass

def get_samples(self, n_samples: int) -> Counter[str]:
"""Takes multiple samples from the sampling distribution.
Args:
n_samples: Number of samples to return.
Returns:
Samples of bitstrings corresponding to measured quantum states.
"""
dist = np.random.multinomial(n_samples, self._weights())
return Counter(
{
np.binary_repr(i, self._size): dist[i]
for i in np.nonzero(dist)[0]
}
)

def get_state(self) -> Any:
"""Gets the quantum state associated with the result.
Can only be defined for emulation results that don't resort to
sampling a quantum state (which is the case for certain types of
noise).
"""
raise NotImplementedError(
f"`{self.__class__.__name__}.get_state()` is not implemented."
)

def plot_histogram(
self,
min_rate: float = 0.001,
max_n_bitstrings: int | None = None,
show: bool = True,
) -> None:
"""Plots the result in an histogram.
Args:
min_rate: The minimum sampling rate a bitstring must have to be
displayed.
max_n_bitstrings: An optional limit on the number of bitrstrings
displayed.
show: Whether or not to call `plt.show()` before returning.
"""
# TODO: Add error bars
probs = np.array(
Counter(self.sampling_dist).most_common(max_n_bitstrings),
dtype=object,
)
probs = probs[probs[:, 1] >= min_rate]
plt.bar(probs[:, 0], probs[:, 1])
plt.xticks(rotation="vertical")
plt.ylabel("Probabilites")
if show:
plt.show()


@dataclass
class SampledResult(Result):
"""Represents the result of a run from a series of samples.
Args:
atom_order: The order of the atoms in the bitstrings that
represent the measured states.
meas_basis: The measurement basis.
bitstring_counts: The number of times each bitstring was
measured.
"""

bitstring_counts: dict[str, int]

def __post_init__(self) -> None:
self.n_samples = sum(self.bitstring_counts.values())

@property
def sampling_errors(self) -> dict[str, float]:
"""The sampling error associated to each bitstring's sampling rate.
Uses the standard error of the mean as a quantifier for sampling error.
"""
return {
bitstr: np.sqrt(p * (1 - p) / self.n_samples)
for bitstr, p in self.sampling_dist.items()
}

def _weights(self) -> np.ndarray:
weights = np.zeros(2**self._size)
for bitstr, counts in self.bitstring_counts.items():
weights[int(bitstr, base=2)] = counts / self.n_samples
return weights / sum(weights)
8 changes: 7 additions & 1 deletion pulser-core/pulser/sampler/sampler.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,12 +40,18 @@ def sample(
)
samples_list.append(samples)

optionals = {}
optionals: dict = dict()
if seq._slm_mask_targets and seq._slm_mask_time:
optionals["_slm_mask"] = _SlmMask(
seq._slm_mask_targets,
seq._slm_mask_time[1],
)
if seq._in_xy:
optionals["_magnetic_field"] = seq.magnetic_field
if hasattr(seq, "_measurement"):
# Has attribute measurement because sequence can't be parametrized
optionals["_measurement"] = seq._measurement

return SequenceSamples(
list(seq.declared_channels.keys()),
samples_list,
Expand Down
30 changes: 24 additions & 6 deletions pulser-core/pulser/sampler/samples.py
Original file line number Diff line number Diff line change
Expand Up @@ -292,6 +292,8 @@ class SequenceSamples:
samples_list: list[ChannelSamples]
_ch_objs: dict[str, Channel]
_slm_mask: _SlmMask = field(default_factory=_SlmMask)
_magnetic_field: np.ndarray | None = None
_measurement: str | None = None

@property
def channel_samples(self) -> dict[str, ChannelSamples]:
Expand All @@ -303,6 +305,7 @@ def max_duration(self) -> int:
"""The maximum duration among the channel samples."""
return max(samples.duration for samples in self.samples_list)

@property
def used_bases(self) -> set[str]:
"""The bases with non-zero pulses."""
return {
Expand All @@ -313,6 +316,26 @@ def used_bases(self) -> set[str]:
if not ch_samples.is_empty()
}

@property
def _in_xy(self) -> bool:
"""Checks if the sequence is in XY mode."""
bases = {ch_obj.basis for ch_obj in self._ch_objs.values()}
in_xy = False
if "XY" in bases:
assert bases == {"XY"}
in_xy = True
return in_xy

def extend_duration(self, new_duration: int) -> SequenceSamples:
"""Extend the duration of each samples to a new duration."""
return replace(
self,
samples_list=[
sample.extend_duration(new_duration)
for sample in self.samples_list
],
)

def to_nested_dict(self, all_local: bool = False) -> dict:
"""Format in the nested dictionary form.
Expand All @@ -327,12 +350,7 @@ def to_nested_dict(self, all_local: bool = False) -> dict:
addressing ('Global' or 'Local'), the targeted basis
and, in the 'Local' case, the targeted qubit.
"""
bases = {ch_obj.basis for ch_obj in self._ch_objs.values()}
in_xy = False
if "XY" in bases:
assert bases == {"XY"}
in_xy = True
d = _prepare_dict(self.max_duration, in_xy=in_xy)
d = _prepare_dict(self.max_duration, in_xy=self._in_xy)
for chname, samples in zip(self.channels, self.samples_list):
cs = (
samples.extend_duration(self.max_duration)
Expand Down
Loading

0 comments on commit 34f9ea8

Please sign in to comment.