diff --git a/pulser-core/pulser/json/abstract_repr/deserializer.py b/pulser-core/pulser/json/abstract_repr/deserializer.py index fcf4b14c5..e76f1a900 100644 --- a/pulser-core/pulser/json/abstract_repr/deserializer.py +++ b/pulser-core/pulser/json/abstract_repr/deserializer.py @@ -16,7 +16,7 @@ import dataclasses import json -from typing import TYPE_CHECKING, Any, Type, Union, cast, overload +from typing import TYPE_CHECKING, Any, Literal, Type, Union, cast, overload import jsonschema import jsonschema.exceptions @@ -57,7 +57,7 @@ if TYPE_CHECKING: from pulser.noise_model import NoiseModel - from pulser.register.base_register import BaseRegister + from pulser.register import Register, Register3D from pulser.sequence import Sequence @@ -384,7 +384,7 @@ def _deserialize_layout(layout_obj: dict[str, Any]) -> RegisterLayout: def _deserialize_register( qubits: list[dict[str, Any]], layout: RegisterLayout | None -) -> BaseRegister: +) -> Register: coords = [(q["x"], q["y"]) for q in qubits] qubit_ids = [q["name"] for q in qubits] if layout: @@ -392,7 +392,20 @@ def _deserialize_register( reg = layout.define_register(*trap_ids, qubit_ids=qubit_ids) else: reg = pulser.Register(dict(zip(qubit_ids, coords))) - return reg + return cast(pulser.Register, reg) + + +def _deserialize_register3d( + qubits: list[dict[str, Any]], layout: RegisterLayout | None +) -> Register3D: + coords = [(q["x"], q["y"], q["z"]) for q in qubits] + qubit_ids = [q["name"] for q in qubits] + if layout: + trap_ids = layout.get_traps_from_coordinates(*coords) + reg = layout.define_register(*trap_ids, qubit_ids=qubit_ids) + else: + reg = pulser.Register3D(dict(zip(qubit_ids, coords))) + return cast(pulser.Register3D, reg) def _deserialize_noise_model(noise_model_obj: dict[str, Any]) -> NoiseModel: @@ -495,11 +508,14 @@ def deserialize_abstract_sequence(obj_str: str) -> Sequence: layout = _deserialize_layout(obj["layout"]) if "layout" in obj else None # Register - reg: Union[BaseRegister, MappableRegister] + reg: Register | Register3D | MappableRegister qubits = obj["register"] if {"name", "x", "y"} == qubits[0].keys(): - # Regular register + # Regular 2D register reg = _deserialize_register(qubits, layout) + elif {"name", "x", "y", "z"} == qubits[0].keys(): + # Regular 3D register + reg = _deserialize_register3d(qubits, layout) else: # Mappable register assert ( @@ -589,20 +605,59 @@ def deserialize_abstract_layout(obj_str: str) -> RegisterLayout: return _deserialize_layout(json.loads(obj_str)) -def deserialize_abstract_register(obj_str: str) -> BaseRegister: +@overload +def deserialize_abstract_register( + obj_str: str, expected_dim: Literal[2] +) -> Register: + pass + + +@overload +def deserialize_abstract_register( + obj_str: str, expected_dim: Literal[3] +) -> Register3D: + pass + + +@overload +def deserialize_abstract_register(obj_str: str) -> Register | Register3D: + pass + + +def deserialize_abstract_register( + obj_str: str, expected_dim: Literal[None, 2, 3] = None +) -> Register | Register3D: """Deserialize a register from an abstract JSON object. Args: - obj_str: the JSON string representing the register encoded + obj_str: The JSON string representing the register encoded in the abstract JSON format. + expected_dim: If defined, ensures the register is of the + specified dimensionality. Returns: The Register instance. """ + if expected_dim not in (None, 2, 3): + raise ValueError( + "When specified, 'expected_dim' must be 2 or 3, " + f"not {expected_dim!s}." + ) validate_abstract_repr(obj_str, "register") obj = json.loads(obj_str) layout = _deserialize_layout(obj["layout"]) if "layout" in obj else None - return _deserialize_register(qubits=obj["register"], layout=layout) + qubits = obj["register"] + dim_ = len(set(qubits[0]) - {"name"}) + # These conditions should be enforced by the schema + assert dim_ == 2 or dim_ == 3 + assert layout is None or layout.dimensionality == dim_ + if expected_dim is not None and expected_dim != dim_: + raise ValueError( + f"The provided register must be in {expected_dim}D, not {dim_}D." + ) + if dim_ == 3: + return _deserialize_register3d(qubits=qubits, layout=layout) + return _deserialize_register(qubits=qubits, layout=layout) def deserialize_abstract_noise_model(obj_str: str) -> NoiseModel: diff --git a/pulser-core/pulser/json/abstract_repr/schemas/device-schema.json b/pulser-core/pulser/json/abstract_repr/schemas/device-schema.json index 88349bd87..b70192f2a 100644 --- a/pulser-core/pulser/json/abstract_repr/schemas/device-schema.json +++ b/pulser-core/pulser/json/abstract_repr/schemas/device-schema.json @@ -421,7 +421,7 @@ "type": "boolean" }, "red_shift_coeff": { - "description": "The weight coefficient of the blue beam's contribution to the lightshift.", + "description": "The weight coefficient of the red beam's contribution to the lightshift.", "type": "number" } }, @@ -755,7 +755,7 @@ "type": "boolean" }, "red_shift_coeff": { - "description": "The weight coefficient of the blue beam's contribution to the lightshift.", + "description": "The weight coefficient of the red beam's contribution to the lightshift.", "type": "number" } }, @@ -1103,7 +1103,7 @@ "type": "boolean" }, "red_shift_coeff": { - "description": "The weight coefficient of the blue beam's contribution to the lightshift.", + "description": "The weight coefficient of the red beam's contribution to the lightshift.", "type": "number" } }, @@ -1410,7 +1410,7 @@ "type": "boolean" }, "red_shift_coeff": { - "description": "The weight coefficient of the blue beam's contribution to the lightshift.", + "description": "The weight coefficient of the red beam's contribution to the lightshift.", "type": "number" } }, diff --git a/pulser-core/pulser/json/abstract_repr/schemas/layout-schema.json b/pulser-core/pulser/json/abstract_repr/schemas/layout-schema.json index 500576f77..01899461f 100644 --- a/pulser-core/pulser/json/abstract_repr/schemas/layout-schema.json +++ b/pulser-core/pulser/json/abstract_repr/schemas/layout-schema.json @@ -4,8 +4,18 @@ "$schema": "http://json-schema.org/draft-07/schema#", "definitions": { "Layout": { + "anyOf": [ + { + "$ref": "#/definitions/Layout2D" + }, + { + "$ref": "#/definitions/Layout3D" + } + ], + "description": "Layout with the positions of the traps. A selection of up to 50% of these traps makes up the Register." + }, + "Layout2D": { "additionalProperties": false, - "description": "Layout with the positions of the traps. A selection of up to 50% of these traps makes up the Register.", "properties": { "coordinates": { "description": "The trap coordinates in µm.", @@ -28,6 +38,31 @@ "coordinates" ], "type": "object" + }, + "Layout3D": { + "additionalProperties": false, + "properties": { + "coordinates": { + "description": "The trap coordinates in µm.", + "items": { + "items": { + "type": "number" + }, + "maxItems": 3, + "minItems": 3, + "type": "array" + }, + "type": "array" + }, + "slug": { + "description": "An optional name for the layout.", + "type": "string" + } + }, + "required": [ + "coordinates" + ], + "type": "object" } } } diff --git a/pulser-core/pulser/json/abstract_repr/schemas/register-schema.json b/pulser-core/pulser/json/abstract_repr/schemas/register-schema.json index 0595f5f81..f67e41fea 100644 --- a/pulser-core/pulser/json/abstract_repr/schemas/register-schema.json +++ b/pulser-core/pulser/json/abstract_repr/schemas/register-schema.json @@ -26,15 +26,103 @@ ], "type": "object" }, + "Atom3D": { + "additionalProperties": false, + "properties": { + "name": { + "$ref": "#/definitions/QubitId", + "description": "Name of the atom." + }, + "x": { + "description": "x-position in µm", + "type": "number" + }, + "y": { + "description": "y-position in µm", + "type": "number" + }, + "z": { + "description": "z-position in µm", + "type": "number" + } + }, + "required": [ + "name", + "x", + "y", + "z" + ], + "type": "object" + }, + "Layout2D": { + "additionalProperties": false, + "properties": { + "coordinates": { + "description": "The trap coordinates in µm.", + "items": { + "items": { + "type": "number" + }, + "maxItems": 2, + "minItems": 2, + "type": "array" + }, + "type": "array" + }, + "slug": { + "description": "An optional name for the layout.", + "type": "string" + } + }, + "required": [ + "coordinates" + ], + "type": "object" + }, + "Layout3D": { + "additionalProperties": false, + "properties": { + "coordinates": { + "description": "The trap coordinates in µm.", + "items": { + "items": { + "type": "number" + }, + "maxItems": 3, + "minItems": 3, + "type": "array" + }, + "type": "array" + }, + "slug": { + "description": "An optional name for the layout.", + "type": "string" + } + }, + "required": [ + "coordinates" + ], + "type": "object" + }, "QubitId": { "description": "Name for a qubit.", "type": "string" }, "Register": { + "anyOf": [ + { + "$ref": "#/definitions/Register2D" + }, + { + "$ref": "#/definitions/Register3D" + } + ] + }, + "Register2D": { "additionalProperties": false, "properties": { "layout": { - "$ref": "layout-schema.json", + "$ref": "#/definitions/Layout2D", "description": "The trap layout underlying the register." }, "register": { @@ -49,6 +137,26 @@ "register" ], "type": "object" + }, + "Register3D": { + "additionalProperties": false, + "properties": { + "layout": { + "$ref": "#/definitions/Layout3D", + "description": "The trap layout underlying the register." + }, + "register": { + "description": "A 3D register containing a set of atoms.", + "items": { + "$ref": "#/definitions/Atom3D" + }, + "type": "array" + } + }, + "required": [ + "register" + ], + "type": "object" } } } diff --git a/pulser-core/pulser/json/abstract_repr/schemas/sequence-schema.json b/pulser-core/pulser/json/abstract_repr/schemas/sequence-schema.json index 706ef3a55..48838461a 100644 --- a/pulser-core/pulser/json/abstract_repr/schemas/sequence-schema.json +++ b/pulser-core/pulser/json/abstract_repr/schemas/sequence-schema.json @@ -26,6 +26,34 @@ ], "type": "object" }, + "Atom3D": { + "additionalProperties": false, + "properties": { + "name": { + "$ref": "#/definitions/QubitId", + "description": "Name of the atom." + }, + "x": { + "description": "x-position in µm", + "type": "number" + }, + "y": { + "description": "y-position in µm", + "type": "number" + }, + "z": { + "description": "z-position in µm", + "type": "number" + } + }, + "required": [ + "name", + "x", + "y", + "z" + ], + "type": "object" + }, "Basis": { "description": "The two-level-system basis addressable by a given channel.", "enum": [ @@ -352,6 +380,56 @@ ], "type": "object" }, + "Layout2D": { + "additionalProperties": false, + "properties": { + "coordinates": { + "description": "The trap coordinates in µm.", + "items": { + "items": { + "type": "number" + }, + "maxItems": 2, + "minItems": 2, + "type": "array" + }, + "type": "array" + }, + "slug": { + "description": "An optional name for the layout.", + "type": "string" + } + }, + "required": [ + "coordinates" + ], + "type": "object" + }, + "Layout3D": { + "additionalProperties": false, + "properties": { + "coordinates": { + "description": "The trap coordinates in µm.", + "items": { + "items": { + "type": "number" + }, + "maxItems": 3, + "minItems": 3, + "type": "array" + }, + "type": "array" + }, + "slug": { + "description": "An optional name for the layout.", + "type": "string" + } + }, + "required": [ + "coordinates" + ], + "type": "object" + }, "MappableQubit": { "additionalProperties": false, "properties": { @@ -860,7 +938,7 @@ "description": "A valid device in which to execute the Sequence" }, "layout": { - "$ref": "layout-schema.json", + "$ref": "#/definitions/Layout2D", "description": "The trap layout underlying the register." }, "magnetic_field": { @@ -932,6 +1010,103 @@ ], "type": "object" }, + { + "additionalProperties": false, + "properties": { + "$schema": { + "type": "string" + }, + "channels": { + "additionalProperties": { + "$ref": "#/definitions/ChannelId" + }, + "description": "Channels declared in this Sequence.", + "type": "object" + }, + "device": { + "anyOf": [ + { + "$ref": "#/definitions/HardcodedDevice" + }, + { + "$ref": "device-schema.json" + } + ], + "description": "A valid device in which to execute the Sequence" + }, + "layout": { + "$ref": "#/definitions/Layout3D", + "description": "The trap layout underlying the register." + }, + "magnetic_field": { + "description": "The magnetic field components in x, y and z (in Gauss)", + "items": { + "type": "number" + }, + "maxItems": 3, + "minItems": 3, + "type": "array" + }, + "measurement": { + "anyOf": [ + { + "$ref": "#/definitions/Basis" + }, + { + "type": "null" + } + ], + "description": "Type of measurement to perform after all pulses are executed" + }, + "name": { + "description": "User-assigned sequence name. Can be autogenerated on export if not provided.", + "type": "string" + }, + "operations": { + "description": "Sequence of pulses, delays and target changes, performed in specified order.", + "items": { + "$ref": "#/definitions/Operation" + }, + "type": "array" + }, + "register": { + "description": "A 3D register containing a set of atoms.", + "items": { + "$ref": "#/definitions/Atom3D" + }, + "type": "array" + }, + "slm_mask_targets": { + "description": "The qubits to mask during the first global pulse of the sequence.", + "items": { + "$ref": "#/definitions/QubitId" + }, + "type": "array" + }, + "variables": { + "additionalProperties": { + "$ref": "#/definitions/Variable" + }, + "description": "Variables and expressions that can be used in expressions or parametrized values.", + "type": "object" + }, + "version": { + "const": "1", + "type": "string" + } + }, + "required": [ + "channels", + "device", + "measurement", + "name", + "operations", + "register", + "variables", + "version" + ], + "type": "object" + }, { "additionalProperties": false, "properties": { @@ -1184,4 +1359,4 @@ "type": "object" } } -} \ No newline at end of file +} diff --git a/pulser-core/pulser/json/abstract_repr/serializer.py b/pulser-core/pulser/json/abstract_repr/serializer.py index 5ebf2cd3f..925bc6180 100644 --- a/pulser-core/pulser/json/abstract_repr/serializer.py +++ b/pulser-core/pulser/json/abstract_repr/serializer.py @@ -50,7 +50,7 @@ def default(self, o: Any) -> dict[str, Any] | list | int: return list(o) elif isinstance(o, complex): return dict(real=o.real, imag=o.imag) - else: + else: # pragma: no cover return cast(dict, json.JSONEncoder.default(self, o)) diff --git a/pulser-core/pulser/register/base_register.py b/pulser-core/pulser/register/base_register.py index c49ed7432..eb03c597f 100644 --- a/pulser-core/pulser/register/base_register.py +++ b/pulser-core/pulser/register/base_register.py @@ -15,6 +15,7 @@ from __future__ import annotations +import json from abc import ABC, abstractmethod from collections.abc import Iterable, Mapping from collections.abc import Sequence as abcSequence @@ -32,6 +33,8 @@ import numpy as np from numpy.typing import ArrayLike +from pulser.json.abstract_repr.serializer import AbstractReprEncoder +from pulser.json.abstract_repr.validation import validate_abstract_repr from pulser.json.utils import obj_to_dict from pulser.register._coordinates import CoordsCollection from pulser.register.weight_maps import DetuningMap @@ -289,3 +292,16 @@ def coords_hex_hash(self) -> str: the '0x' prefix (unlike what is returned by 'hex()'). """ return self._safe_hash().hex() + + @abstractmethod + def _to_abstract_repr(self) -> list[dict[str, Union[QubitId, float]]]: + pass + + def to_abstract_repr(self) -> str: + """Serializes the register into an abstract JSON object.""" + abstr_reg: dict[str, Any] = dict(register=self._to_abstract_repr()) + if self.layout is not None: + abstr_reg["layout"] = self.layout + abstr_reg_str = json.dumps(abstr_reg, cls=AbstractReprEncoder) + validate_abstract_repr(abstr_reg_str, "register") + return abstr_reg_str diff --git a/pulser-core/pulser/register/register.py b/pulser-core/pulser/register/register.py index 260608e0b..db6abd4c0 100644 --- a/pulser-core/pulser/register/register.py +++ b/pulser-core/pulser/register/register.py @@ -15,10 +15,9 @@ from __future__ import annotations -import json import warnings from collections.abc import Mapping -from typing import Any, Optional, Union, cast +from typing import Any, Optional, Union import matplotlib.pyplot as plt import numpy as np @@ -30,8 +29,6 @@ from pulser.json.abstract_repr.deserializer import ( deserialize_abstract_register, ) -from pulser.json.abstract_repr.serializer import AbstractReprEncoder -from pulser.json.abstract_repr.validation import validate_abstract_repr from pulser.json.utils import stringify_qubit_ids from pulser.register._reg_drawer import RegDrawer from pulser.register.base_register import BaseRegister, QubitId @@ -422,15 +419,6 @@ def _to_abstract_repr(self) -> list[dict[str, Union[QubitId, float]]]: for name, (x, y) in zip(names, self._coords) ] - def to_abstract_repr(self) -> str: - """Serializes the register into an abstract JSON object.""" - abstr_reg: dict[str, Any] = dict(register=self._to_abstract_repr()) - if self.layout is not None: - abstr_reg["layout"] = self.layout - abstr_reg_str = json.dumps(abstr_reg, cls=AbstractReprEncoder) - validate_abstract_repr(abstr_reg_str, "register") - return abstr_reg_str - @staticmethod def from_abstract_repr(obj_str: str) -> Register: """Deserialize a register from an abstract JSON object. @@ -444,4 +432,4 @@ def from_abstract_repr(obj_str: str) -> Register: "The serialized register must be given as a string. " f"Instead, got object of type {type(obj_str)}." ) - return cast(Register, deserialize_abstract_register(obj_str)) + return deserialize_abstract_register(obj_str, expected_dim=2) diff --git a/pulser-core/pulser/register/register3d.py b/pulser-core/pulser/register/register3d.py index 29ed78b18..831c64b75 100644 --- a/pulser-core/pulser/register/register3d.py +++ b/pulser-core/pulser/register/register3d.py @@ -22,6 +22,10 @@ import numpy as np from numpy.typing import ArrayLike +from pulser.json.abstract_repr.deserializer import ( + deserialize_abstract_register, +) +from pulser.json.utils import stringify_qubit_ids from pulser.register._reg_drawer import RegDrawer from pulser.register.base_register import BaseRegister, QubitId from pulser.register.register import Register @@ -240,3 +244,25 @@ def draw( def _to_dict(self) -> dict[str, Any]: return super()._to_dict() + + def _to_abstract_repr(self) -> list[dict[str, QubitId | float]]: + names = stringify_qubit_ids(self._ids) + return [ + {"name": name, "x": x, "y": y, "z": z} + for name, (x, y, z) in zip(names, self._coords) + ] + + @staticmethod + def from_abstract_repr(obj_str: str) -> Register3D: + """Deserialize a 3D register from an abstract JSON object. + + Args: + obj_str (str): the JSON string representing the register encoded + in the abstract JSON format. + """ + if not isinstance(obj_str, str): + raise TypeError( + "The serialized register must be given as a string. " + f"Instead, got object of type {type(obj_str)}." + ) + return deserialize_abstract_register(obj_str, expected_dim=3) diff --git a/tests/test_abstract_repr.py b/tests/test_abstract_repr.py index 8f42e9d3d..3608bb766 100644 --- a/tests/test_abstract_repr.py +++ b/tests/test_abstract_repr.py @@ -39,6 +39,7 @@ ) from pulser.json.abstract_repr.deserializer import ( VARIABLE_TYPE_MAP, + deserialize_abstract_register, deserialize_device, ) from pulser.json.abstract_repr.serializer import ( @@ -93,6 +94,7 @@ RegisterLayout([[0, 0], [1, 1]]), TriangularLatticeLayout(10, 10), RegisterLayout([[10, 0], [1, 10]], slug="foo"), + RegisterLayout([[0.0, 1.0, 2.0], [-0.4, 1.6, 35.0]]), ], ) def test_layout(layout: RegisterLayout): @@ -107,10 +109,10 @@ def test_layout(layout: RegisterLayout): RegisterLayout.from_abstract_repr(ser_layout_obj) # Check the validation catches invalid entries - with pytest.raises( - jsonschema.exceptions.ValidationError, match="is too long" - ): - ser_layout_obj["coordinates"].append([0, 0, 0]) + with pytest.raises(jsonschema.exceptions.ValidationError): + ser_layout_obj["coordinates"].append( + [0, 0, 0] if layout.dimensionality == 2 else [0, 0] + ) RegisterLayout.from_abstract_repr(json.dumps(ser_layout_obj)) @@ -119,9 +121,11 @@ def test_layout(layout: RegisterLayout): [ Register.from_coordinates(np.array([[0, 0], [1, 1]]), prefix="q"), TriangularLatticeLayout(10, 10).define_register(*[1, 2, 3]), + Register3D(dict(q0=(0, 0, 0), q1=(1, 2, 3))), + RegisterLayout([[0, 0, 0], [1, 1, 1]]).define_register(1), ], ) -def test_register(reg: Register): +def test_register(reg: Register | Register3D): ser_reg_str = reg.to_abstract_repr() ser_reg_obj = json.loads(ser_reg_str) if reg.layout: @@ -131,18 +135,42 @@ def test_register(reg: Register): else: assert "layout" not in ser_reg_obj - re_reg = Register.from_abstract_repr(ser_reg_str) + re_reg = type(reg).from_abstract_repr(ser_reg_str) assert reg == re_reg with pytest.raises(TypeError, match="must be given as a string"): - Register.from_abstract_repr(ser_reg_obj) + type(reg).from_abstract_repr(ser_reg_obj) - # Check the validation catches invalid entries - with pytest.raises( - jsonschema.exceptions.ValidationError, match="'z' was unexpected" - ): - ser_reg_obj["register"].append(dict(name="q10", x=10, y=0, z=1)) - Register.from_abstract_repr(json.dumps(ser_reg_obj)) + with pytest.raises(ValueError, match="must be 2 or 3, not 1"): + deserialize_abstract_register( # type: ignore + ser_reg_str, + expected_dim=1, + ) + + # Without expected_dim, the deserializer returns the right type + re_reg2 = deserialize_abstract_register(ser_reg_str) + assert type(reg) is type(re_reg2) + assert re_reg == re_reg2 + + if reg.dimensionality == 2: + # A 2D register can't be deserialized as a 3D register + with pytest.raises(ValueError, match="must be in 3D, not 2D"): + Register3D.from_abstract_repr(ser_reg_str) + + # Check the validation catches invalid entries + with pytest.raises(jsonschema.exceptions.ValidationError): + ser_reg_obj["register"].append(dict(name="q10", x=10, y=0, z=1)) + Register.from_abstract_repr(json.dumps(ser_reg_obj)) + else: + assert reg.dimensionality == 3 + # A 3D register can't be deserialized as a 2D register + with pytest.raises(ValueError, match="must be in 2D, not 3D"): + Register.from_abstract_repr(ser_reg_str) + + # Check the validation catches invalid entries + with pytest.raises(jsonschema.exceptions.ValidationError): + ser_reg_obj["register"].append(dict(name="q10", x=10, y=0)) + Register.from_abstract_repr(json.dumps(ser_reg_obj)) @pytest.mark.parametrize( @@ -597,10 +625,6 @@ def test_values(self, abstract): assert abstract["measurement"] == "digital" def test_exceptions(self, sequence): - with pytest.raises(TypeError, match="not JSON serializable"): - Sequence( - Register3D.cubic(2, prefix="q"), MockDevice - ).to_abstract_repr() with pytest.raises( ValueError, match="No signature found for 'FakeWaveform'" @@ -1298,10 +1322,54 @@ def test_deserialize_register(self, layout_coords): # Check register assert len(seq.register.qubits) == len(s["register"]) + assert seq.register.dimensionality == 2 + assert isinstance(seq.register, Register) + for q in s["register"]: + assert q["name"] in seq.qubit_info + assert seq.qubit_info[q["name"]][0] == q["x"] + assert seq.qubit_info[q["name"]][1] == q["y"] + + # Check layout + if layout_coords is not None: + assert seq.register.layout == reg_layout + q_coords = list(seq.qubit_info.values()) + assert seq.register._layout_info.trap_ids == tuple( + reg_layout.get_traps_from_coordinates(*q_coords) + ) + assert reg_layout.dimensionality == 2 + else: + assert "layout" not in s + assert seq.register.layout is None + + @pytest.mark.parametrize( + "layout_coords", [None, np.array([(0, 0, 0), (1, 2, 3)])] + ) + def test_deserialize_register3D(self, layout_coords): + custom_fields = { + "device": json.loads(MockDevice.to_abstract_repr()), + "register": [ + {"name": "q0", "x": 1.0, "y": 2.0, "z": 3.0}, + ], + } + if layout_coords is not None: + reg_layout = RegisterLayout(layout_coords) + custom_fields["layout"] = { + "coordinates": reg_layout.coords.tolist() + } + + s = _get_serialized_seq(**custom_fields) + _check_roundtrip(s) + seq = Sequence.from_abstract_repr(json.dumps(s)) + + # Check register + assert len(seq.register.qubits) == len(s["register"]) + assert seq.register.dimensionality == 3 + assert isinstance(seq.register, Register3D) for q in s["register"]: assert q["name"] in seq.qubit_info assert seq.qubit_info[q["name"]][0] == q["x"] assert seq.qubit_info[q["name"]][1] == q["y"] + assert seq.qubit_info[q["name"]][2] == q["z"] # Check layout if layout_coords is not None: @@ -1310,6 +1378,7 @@ def test_deserialize_register(self, layout_coords): assert seq.register._layout_info.trap_ids == tuple( reg_layout.get_traps_from_coordinates(*q_coords) ) + assert reg_layout.dimensionality == 3 else: assert "layout" not in s assert seq.register.layout is None