Skip to content

Commit

Permalink
Deprecate legacy serializer + Improve error messages (#585)
Browse files Browse the repository at this point in the history
* Improve serialization error messages

* Deprecate the legacy serializer

* Update serialization tutorial

* UT updates

* Import sorting

* Add reminder to remove legacy serializer

* Fix typo in tutorial

* Fix typo in tutorial text
  • Loading branch information
HGSilveri authored Sep 25, 2023
1 parent 9e05982 commit 3e40319
Show file tree
Hide file tree
Showing 6 changed files with 340 additions and 203 deletions.
101 changes: 96 additions & 5 deletions pulser-core/pulser/sequence/sequence.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
overload,
)

import jsonschema
import matplotlib.pyplot as plt
import numpy as np
from numpy.typing import ArrayLike
Expand All @@ -48,6 +49,7 @@
)
from pulser.json.abstract_repr.serializer import serialize_abstract_sequence
from pulser.json.coders import PulserDecoder, PulserEncoder
from pulser.json.exceptions import AbstractReprError
from pulser.json.utils import obj_to_dict
from pulser.parametrized import Parametrized, Variable
from pulser.parametrized.variable import VariableItem
Expand Down Expand Up @@ -1560,6 +1562,39 @@ def build(
def serialize(self, **kwargs: Any) -> str:
"""Serializes the Sequence into a JSON formatted string.
Other Parameters:
kwargs: Valid keyword-arguments for ``json.dumps()``, except for
``cls``.
Returns:
The sequence encoded in a JSON formatted string.
Warning:
This method has been deprecated and is scheduled for removal
in Pulser v1.0.0. For sequence serialization and deserialization,
use ``Sequence.to_abstract_repr()`` and
``Sequence.from_abstract_repr()`` instead.
See Also:
``json.dumps``: Built-in function for serialization to a JSON
formatted string.
"""
with warnings.catch_warnings():
warnings.simplefilter("always")
warnings.warn(
DeprecationWarning(
"`Sequence.serialize()` and `Sequence.deserialize()` have "
"been deprecated and will be removed in Pulser v1.0.0. "
"Use `Sequence.to_abstract_repr()` and "
"`Sequence.from_abstract_repr()` instead."
)
)

return self._serialize(**kwargs)

def _serialize(self, **kwargs: Any) -> str:
"""Serializes the Sequence into a JSON formatted string.
Other Parameters:
kwargs: Valid keyword-arguments for ``json.dumps()``, except for
``cls``.
Expand Down Expand Up @@ -1599,16 +1634,62 @@ def to_abstract_repr(
Returns:
str: The sequence encoded as an abstract JSON object.
"""
try:
return serialize_abstract_sequence(
self, seq_name, json_dumps_options, **defaults
)
except jsonschema.exceptions.ValidationError as e:
if self.is_parametrized():
raise AbstractReprError(
"The serialization of the parametrized sequence failed, "
"potentially due to an error that only appears at build "
"time. Check that no errors appear when building with "
"`Sequence.build()` or when providing the `defaults` to "
"`Sequence.to_abstract_repr()`."
) from e
raise e # pragma: no cover

@staticmethod
def deserialize(obj: str, **kwargs: Any) -> Sequence:
"""Deserializes a JSON formatted string.
Args:
obj: The JSON formatted string to deserialize, coming from
the serialization of a ``Sequence`` through
``Sequence.serialize()``.
Other Parameters:
kwargs: Valid keyword-arguments for ``json.loads()``, except for
``cls`` and ``object_hook``.
Returns:
The deserialized Sequence object.
Warning:
This method has been deprecated and is scheduled for removal
in Pulser v1.0.0. For sequence serialization and deserialization,
use ``Sequence.to_abstract_repr()`` and
``Sequence.from_abstract_repr()`` instead.
See Also:
``serialize``
``json.loads``: Built-in function for deserialization from a JSON
formatted string.
"""
return serialize_abstract_sequence(
self, seq_name, json_dumps_options, **defaults
)
with warnings.catch_warnings():
warnings.simplefilter("always")
warnings.warn(
DeprecationWarning(
"`Sequence.serialize()` and `Sequence.deserialize()` have "
"been deprecated and will be removed in Pulser v1.0.0. "
"Use `Sequence.to_abstract_repr()` and "
"`Sequence.from_abstract_repr()` instead."
)
)
return Sequence._deserialize(obj, **kwargs)

@staticmethod
def deserialize(obj: str, **kwargs: Any) -> Sequence:
def _deserialize(obj: str, **kwargs: Any) -> Sequence:
"""Deserializes a JSON formatted string.
Args:
Expand All @@ -1627,6 +1708,11 @@ def deserialize(obj: str, **kwargs: Any) -> Sequence:
``json.loads``: Built-in function for deserialization from a JSON
formatted string.
"""
if not isinstance(obj, str):
raise TypeError(
"The serialized sequence must be given as a string. "
f"Instead, got object of type {type(obj)}."
)
if "Sequence" not in obj:
raise ValueError(
"The given JSON formatted string does not encode a Sequence."
Expand All @@ -1645,6 +1731,11 @@ def from_abstract_repr(obj_str: str) -> Sequence:
Returns:
Sequence: The Pulser sequence.
"""
if not isinstance(obj_str, str):
raise TypeError(
"The serialized sequence must be given as a string. "
f"Instead, got object of type {type(obj_str)}."
)
return deserialize_abstract_sequence(obj_str)

@seq_decorators.screen
Expand Down
28 changes: 28 additions & 0 deletions tests/test_abstract_repr.py
Original file line number Diff line number Diff line change
Expand Up @@ -819,6 +819,23 @@ def test_mappable_reg_with_local_ops(
getattr(seq, op)(*args)
seq.to_abstract_repr()

def test_parametrized_fails_validation(self):
seq_ = Sequence(Register.square(1, prefix="q"), MockDevice)
vars = seq_.declare_variable("vars", dtype=int, size=2)
seq_.declare_channel("ryd", "rydberg_global")
seq_.delay(vars, "ryd") # vars has size 2, the build will fail
with pytest.raises(
AbstractReprError,
match=re.escape(
"The serialization of the parametrized sequence failed, "
"potentially due to an error that only appears at build "
"time. Check that no errors appear when building with "
"`Sequence.build()` or when providing the `defaults` to "
"`Sequence.to_abstract_repr()`."
),
):
seq_.to_abstract_repr()

@pytest.mark.parametrize("is_empty", [True, False])
def test_dmm_slm_mask(self, triangular_lattice, is_empty):
mask = {"q0", "q2", "q4", "q5"}
Expand Down Expand Up @@ -1965,3 +1982,14 @@ def test_legacy_device(self, device):
)
seq = Sequence.from_abstract_repr(json.dumps(s))
assert seq.device == device

def test_bad_type(self):
s = _get_serialized_seq()
with pytest.raises(
TypeError,
match=re.escape(
"The serialized sequence must be given as a string. "
f"Instead, got object of type {dict}."
),
):
Sequence.from_abstract_repr(s)
56 changes: 47 additions & 9 deletions tests/test_json.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,12 @@
# See the License for the specific language governing permissions and
# limitations under the License.
import json
import re

import numpy as np
import pytest

import pulser
from pulser import Register, Register3D, Sequence
from pulser.devices import Chadoq2, MockDevice
from pulser.json.coders import PulserDecoder, PulserEncoder
Expand Down Expand Up @@ -136,7 +138,7 @@ def test_mappable_register():
assert seq.is_register_mappable()
mapped_seq = seq.build(qubits={"q0": 2, "q1": 1})
assert not mapped_seq.is_register_mappable()
new_mapped_seq = Sequence.deserialize(mapped_seq.serialize())
new_mapped_seq = Sequence._deserialize(mapped_seq._serialize())
assert not new_mapped_seq.is_register_mappable()


Expand All @@ -154,8 +156,15 @@ def test_rare_cases(patch_plt_show):
s = encode(wf())
s = encode(wf)

with pytest.raises(
TypeError,
match="The serialized sequence must be given as a string. "
f"Instead, got object of type {dict}.",
):
wf_ = Sequence._deserialize(json.loads(s))

with pytest.raises(ValueError, match="not encode a Sequence"):
wf_ = Sequence.deserialize(s)
wf_ = Sequence._deserialize(s)

wf_ = decode(s)
seq._variables["var"]._assign(-10)
Expand Down Expand Up @@ -208,31 +217,43 @@ def test_sequence_module():
# Check that the sequence module is backwards compatible after refactoring
seq = Sequence(Register.square(2), Chadoq2)

obj_dict = json.loads(seq.serialize())
obj_dict = json.loads(seq._serialize())
assert obj_dict["__module__"] == "pulser.sequence"

# Defensively check that the standard format runs
Sequence.deserialize(seq.serialize())
Sequence._deserialize(seq._serialize())

# Use module being used in v0.7.0-0.7.2.0
obj_dict["__module__"] == "pulser.sequence.sequence"

# Check that it also works
s = json.dumps(obj_dict)
Sequence.deserialize(s)
Sequence._deserialize(s)


def test_type_error():
s = Sequence(Register.square(1), MockDevice)._serialize()
with pytest.raises(
TypeError,
match=re.escape(
"The serialized sequence must be given as a string. "
f"Instead, got object of type {dict}."
),
):
Sequence._deserialize(json.loads(s))


def test_deprecation():
def test_deprecated_device_args():
seq = Sequence(Register.square(1), MockDevice)

seq_dict = json.loads(seq.serialize())
seq_dict = json.loads(seq._serialize())
dev_dict = seq_dict["__kwargs__"]["device"]

assert "_channels" not in dev_dict["__kwargs__"]
dev_dict["__kwargs__"]["_channels"] = []

s = json.dumps(seq_dict)
new_seq = Sequence.deserialize(s)
new_seq = Sequence._deserialize(s)
assert new_seq.device == MockDevice

ids = dev_dict["__kwargs__"].pop("channel_ids")
Expand All @@ -241,5 +262,22 @@ def test_deprecation():

assert seq_dict["__kwargs__"]["device"] == dev_dict
s = json.dumps(seq_dict)
new_seq = Sequence.deserialize(s)
new_seq = Sequence._deserialize(s)
assert new_seq.device == MockDevice


def test_deprecation_warning():
msg = re.escape(
"`Sequence.serialize()` and `Sequence.deserialize()` have "
"been deprecated and will be removed in Pulser v1.0.0. "
"Use `Sequence.to_abstract_repr()` and "
"`Sequence.from_abstract_repr()` instead."
)
seq = Sequence(Register.square(1), MockDevice)
with pytest.warns(DeprecationWarning, match=msg):
s = seq.serialize()

with pytest.warns(DeprecationWarning, match=msg):
Sequence.deserialize(s)

assert pulser.__version__ < "1.0", "Remove legacy serializer methods"
8 changes: 4 additions & 4 deletions tests/test_paramseq.py
Original file line number Diff line number Diff line change
Expand Up @@ -200,12 +200,12 @@ def test_build():
assert seq.current_phase_ref("q0") == 0.0
assert seq._measurement == "ground-rydberg"

s = sb.serialize()
sb_ = Sequence.deserialize(s)
s = sb._serialize()
sb_ = Sequence._deserialize(s)
assert str(sb) == str(sb_)

s2 = sb_.serialize()
sb_2 = Sequence.deserialize(s2)
s2 = sb_._serialize()
sb_2 = Sequence._deserialize(s2)
assert str(sb) == str(sb_2)


Expand Down
12 changes: 6 additions & 6 deletions tests/test_sequence.py
Original file line number Diff line number Diff line change
Expand Up @@ -305,8 +305,8 @@ def test_magnetic_field(reg):
with pytest.raises(ValueError, match="can only be set on an empty seq"):
seq3.set_magnetic_field()

seq3_str = seq3.serialize()
seq3_ = Sequence.deserialize(seq3_str)
seq3_str = seq3._serialize()
seq3_ = Sequence._deserialize(seq3_str)
assert seq3_._in_xy
assert str(seq3) == str(seq3_)
assert np.all(seq3_.magnetic_field == np.array((1.0, 0.0, 0.0)))
Expand Down Expand Up @@ -1290,9 +1290,9 @@ def test_sequence(reg, device, patch_plt_show):
seq.draw(draw_phase_area=True)
seq.draw(draw_phase_curve=True)

s = seq.serialize()
s = seq._serialize()
assert json.loads(s)["__version__"] == pulser.__version__
seq_ = Sequence.deserialize(s)
seq_ = Sequence._deserialize(s)
assert str(seq) == str(seq_)


Expand Down Expand Up @@ -1409,8 +1409,8 @@ def test_slm_mask_in_xy(reg, patch_plt_show):
seq_xy5.add(Pulse.ConstantPulse(200, var, 0, 0), "ch")
assert seq_xy5.is_parametrized()
seq_xy5.config_slm_mask(targets)
seq_xy5_str = seq_xy5.serialize()
seq_xy5_ = Sequence.deserialize(seq_xy5_str)
seq_xy5_str = seq_xy5._serialize()
seq_xy5_ = Sequence._deserialize(seq_xy5_str)
assert str(seq_xy5) == str(seq_xy5_)

# Check drawing method
Expand Down
Loading

0 comments on commit 3e40319

Please sign in to comment.