diff --git a/pulser-core/pulser/sequence/sequence.py b/pulser-core/pulser/sequence/sequence.py index 39bf5533..81eae094 100644 --- a/pulser-core/pulser/sequence/sequence.py +++ b/pulser-core/pulser/sequence/sequence.py @@ -33,6 +33,7 @@ overload, ) +import jsonschema import matplotlib.pyplot as plt import numpy as np from numpy.typing import ArrayLike @@ -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 @@ -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``. @@ -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: @@ -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." @@ -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 diff --git a/tests/test_abstract_repr.py b/tests/test_abstract_repr.py index e7d8392c..cf4e5cee 100644 --- a/tests/test_abstract_repr.py +++ b/tests/test_abstract_repr.py @@ -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"} @@ -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) diff --git a/tests/test_json.py b/tests/test_json.py index c8a4f16e2..661021fb 100644 --- a/tests/test_json.py +++ b/tests/test_json.py @@ -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 @@ -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() @@ -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) @@ -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") @@ -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" diff --git a/tests/test_paramseq.py b/tests/test_paramseq.py index 58c7e4b4..e311426b 100644 --- a/tests/test_paramseq.py +++ b/tests/test_paramseq.py @@ -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) diff --git a/tests/test_sequence.py b/tests/test_sequence.py index 3f560b49..7f48fb38 100644 --- a/tests/test_sequence.py +++ b/tests/test_sequence.py @@ -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))) @@ -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_) @@ -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 diff --git a/tutorials/advanced_features/Serialization.ipynb b/tutorials/advanced_features/Serialization.ipynb index 8548c57b..16ab34a1 100644 --- a/tutorials/advanced_features/Serialization.ipynb +++ b/tutorials/advanced_features/Serialization.ipynb @@ -1,182 +1,162 @@ { - "cells": [ - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# JSON Serialization" - ] + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# JSON Serialization" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import numpy as np\n", + "from pulser import Pulse, Sequence, Register\n", + "from pulser.waveforms import BlackmanWaveform\n", + "from pulser.devices import Chadoq2" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Often times, it is useful to import/export a given `Sequence` between different locations. To enable this, the `Sequence` object supports **serialization** and **deserialization** into JSON-formatted strings. This will work for any given `Sequence`. Take for example, this sequence that creates the Bell state $|\\Phi^+\\rangle = \\frac{|00\\rangle + |11\\rangle}{\\sqrt{2}}$:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "scrolled": true + }, + "outputs": [], + "source": [ + "qubits = {\"control\": (-2, 0), \"target\": (2, 0)}\n", + "reg = Register(qubits)\n", + "\n", + "seq = Sequence(reg, Chadoq2)\n", + "pulse_time = seq.declare_variable(\"pulse_time\", dtype=int)\n", + "seq.declare_channel(\"digital\", \"raman_local\", initial_target=\"control\")\n", + "seq.declare_channel(\"rydberg\", \"rydberg_local\", initial_target=\"control\")\n", + "\n", + "half_pi_wf = BlackmanWaveform(pulse_time, area=np.pi / 2)\n", + "\n", + "ry = Pulse.ConstantDetuning(amplitude=half_pi_wf, detuning=0, phase=-np.pi / 2)\n", + "ry_dag = Pulse.ConstantDetuning(\n", + " amplitude=half_pi_wf, detuning=0, phase=np.pi / 2\n", + ")\n", + "\n", + "seq.add(ry, \"digital\")\n", + "seq.target(\"target\", \"digital\")\n", + "seq.add(ry_dag, \"digital\")\n", + "\n", + "pi_wf = BlackmanWaveform(pulse_time, np.pi)\n", + "pi_pulse = Pulse.ConstantDetuning(pi_wf, 0, 0)\n", + "\n", + "max_val = Chadoq2.rabi_from_blockade(9)\n", + "two_pi_wf = BlackmanWaveform.from_max_val(max_val, 2 * np.pi)\n", + "two_pi_pulse = Pulse.ConstantDetuning(two_pi_wf, 0, 0)\n", + "\n", + "seq.align(\"digital\", \"rydberg\")\n", + "seq.add(pi_pulse, \"rydberg\")\n", + "seq.target(\"target\", \"rydberg\")\n", + "seq.add(two_pi_pulse, \"rydberg\")\n", + "seq.target(\"control\", \"rydberg\")\n", + "seq.add(pi_pulse, \"rydberg\")\n", + "\n", + "seq.align(\"digital\", \"rydberg\")\n", + "seq.add(ry, \"digital\")\n", + "seq.measure(\"digital\")\n", + "seq1 = seq.build(pulse_time=200)\n", + "seq1.draw()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Serialize" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "To serialize, use `Sequence.to_abstract_repr()`:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "s_readable = seq.to_abstract_repr(\n", + " json_dumps_options={\"indent\": 1},\n", + " seq_name=\"Sequence_with_defaults\",\n", + ")\n", + "print(s_readable[:350], \"...\")" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We can note that it is possible to provide optional parameters of `json.dumps` such as `indent` via a dictionnary in the argument `json_dumps_options`.\n", + "\n", + "Providing optional arguments to `to_abstract_repr` defines default parameters in the JSON object (like the name of the sequence `seq_name`). This does not change the `Sequence` object in itself, as we'll see in the following part about deserialization." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Deserialize" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The generated strings contain all the necessary information for recreating the original sequence elsewhere (it could, for example, be saved to a file and then imported). To recover the sequence `seq` from `s_readable` (converted into JSON using `Sequence.to_abstract_repr()`), one should use `Sequence.from_abstract_repr()`. " + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "recovered_seq = Sequence.from_abstract_repr(s_readable)\n", + "recovered_seq.build(pulse_time=200).draw()" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.8.5" + } }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "import numpy as np\n", - "from pulser import Pulse, Sequence, Register\n", - "from pulser.waveforms import BlackmanWaveform\n", - "from pulser.devices import Chadoq2" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Often times, it is useful to import/export a given `Sequence` between different locations. To enable this, the `Sequence` object supports **serialization** and **deserialization** into JSON-formatted strings. This will work for any given `Sequence`. Take for example, this sequence that creates the Bell state $|\\Phi^+\\rangle = \\frac{|00\\rangle + |11\\rangle}{\\sqrt{2}}$:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "scrolled": true - }, - "outputs": [], - "source": [ - "qubits = {\"control\": (-2, 0), \"target\": (2, 0)}\n", - "reg = Register(qubits)\n", - "\n", - "seq = Sequence(reg, Chadoq2)\n", - "pulse_time = seq.declare_variable(\"pulse_time\", dtype=int)\n", - "seq.declare_channel(\"digital\", \"raman_local\", initial_target=\"control\")\n", - "seq.declare_channel(\"rydberg\", \"rydberg_local\", initial_target=\"control\")\n", - "\n", - "half_pi_wf = BlackmanWaveform(pulse_time, area=np.pi / 2)\n", - "\n", - "ry = Pulse.ConstantDetuning(amplitude=half_pi_wf, detuning=0, phase=-np.pi / 2)\n", - "ry_dag = Pulse.ConstantDetuning(\n", - " amplitude=half_pi_wf, detuning=0, phase=np.pi / 2\n", - ")\n", - "\n", - "seq.add(ry, \"digital\")\n", - "seq.target(\"target\", \"digital\")\n", - "seq.add(ry_dag, \"digital\")\n", - "\n", - "pi_wf = BlackmanWaveform(pulse_time, np.pi)\n", - "pi_pulse = Pulse.ConstantDetuning(pi_wf, 0, 0)\n", - "\n", - "max_val = Chadoq2.rabi_from_blockade(8)\n", - "two_pi_wf = BlackmanWaveform.from_max_val(max_val, 2 * np.pi)\n", - "two_pi_pulse = Pulse.ConstantDetuning(two_pi_wf, 0, 0)\n", - "\n", - "seq.align(\"digital\", \"rydberg\")\n", - "seq.add(pi_pulse, \"rydberg\")\n", - "seq.target(\"target\", \"rydberg\")\n", - "seq.add(two_pi_pulse, \"rydberg\")\n", - "seq.target(\"control\", \"rydberg\")\n", - "seq.add(pi_pulse, \"rydberg\")\n", - "\n", - "seq.align(\"digital\", \"rydberg\")\n", - "seq.add(ry, \"digital\")\n", - "seq.measure(\"digital\")\n", - "seq1 = seq.build(pulse_time=200)\n", - "seq1.draw()" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Serialize" - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "metadata": {}, - "source": [ - "The former version of converting a `Sequence` into a JSON-formatted string was to use the `serialize` method. It is still supported, but a new method named `to_abstract_repr` should be favored to perform serialization. Let's compare both serialization methods:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "s = seq.serialize(indent=1)\n", - "print(s[:350], \"...\")" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "s_readable = seq.to_abstract_repr(\n", - " json_dumps_options={\"indent\": 1},\n", - " seq_name=\"Sequence_with_defaults\",\n", - ")\n", - "print(s_readable[:350], \"...\")" - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "metadata": {}, - "source": [ - "We can note that it is possible to provide optional parameters of `json.dumps` such as `indent` to both methods. In `serialize`, they should be provided as optional arguments whereas for `to_abstract_repr` they should be defined as a dictionnary in the argument `json_dumps_options`.\n", - "\n", - "Providing optional arguments to `to_abstract_repr` defines default parameters in the JSON object (like the name of the sequence `seq_name`). This does not change the `Sequence` object in itself, as will be seen in the following part about deserialization." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Deserialize" - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "metadata": {}, - "source": [ - "The generated strings contain all the necessary information for recreating the original sequence elsewhere (it could, for example, be saved to a file and then imported). In the case of strings obtained from `Sequence.serialize`, one could recover the sequence `seq` by calling `Sequence.deserialize`. This method is still supported. However, to recover the sequence `seq` from `s_readable` (converted into JSON using `Sequence.to_abstract_repr`), one should use `Sequence.from_abstract_repr`. " - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "recovered_seq = Sequence.deserialize(s)\n", - "recovered_seq.build(pulse_time=200).draw()" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "recovered_seq_from_readable = Sequence.from_abstract_repr(s_readable)\n", - "recovered_seq_from_readable.build(pulse_time=200).draw()" - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Python 3", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.8.15" - } - }, - "nbformat": 4, - "nbformat_minor": 4 + "nbformat": 4, + "nbformat_minor": 4 }