From 49736594c2cef190c20958933987a442eae8c862 Mon Sep 17 00:00:00 2001 From: a_corni Date: Thu, 29 Aug 2024 13:54:06 +0200 Subject: [PATCH 1/3] Add from_abstract_repr to Device and VirtualDevice --- pulser-core/pulser/devices/_device_datacls.py | 44 +++++++++ tests/test_abstract_repr.py | 89 ++++++++++++++++--- 2 files changed, 123 insertions(+), 10 deletions(-) diff --git a/pulser-core/pulser/devices/_device_datacls.py b/pulser-core/pulser/devices/_device_datacls.py index 472f373f..e8af0a46 100644 --- a/pulser-core/pulser/devices/_device_datacls.py +++ b/pulser-core/pulser/devices/_device_datacls.py @@ -23,6 +23,7 @@ import numpy as np from scipy.spatial.distance import pdist, squareform +import pulser.json.abstract_repr as pulser_abstract_repr from pulser.channels.base_channel import Channel, States, get_states_from_bases from pulser.channels.dmm import DMM from pulser.devices.interaction_coefficients import c6_dict @@ -715,6 +716,29 @@ def _to_abstract_repr(self) -> dict[str, Any]: d["is_virtual"] = False return d + @staticmethod + def from_abstract_repr(obj_str: str) -> Device: + """Deserialize a noise model from an abstract JSON object. + + Args: + obj_str (str): the JSON string representing the noise model + encoded in the abstract JSON format. + """ + if not isinstance(obj_str, str): + raise TypeError( + "The serialized Device must be given as a string. " + f"Instead, got object of type {type(obj_str)}." + ) + + # Avoids circular imports + device = pulser_abstract_repr.deserializer.deserialize_device(obj_str) + if not isinstance(device, Device): + raise TypeError( + "The given schema is not related to a Device, but to a" + f" {type(device).__name__}." + ) + return device + @dataclass(frozen=True) class VirtualDevice(BaseDevice): @@ -796,3 +820,23 @@ def _to_abstract_repr(self) -> dict[str, Any]: d = super()._to_abstract_repr() d["is_virtual"] = True return d + + @staticmethod + def from_abstract_repr(obj_str: str) -> VirtualDevice: + """Deserialize a noise model from an abstract JSON object. + + Args: + obj_str (str): the JSON string representing the noise model + encoded in the abstract JSON format. + """ + if not isinstance(obj_str, str): + raise TypeError( + "The serialized VirtualDevice must be given as a string. " + f"Instead, got object of type {type(obj_str)}." + ) + + # Avoids circular imports + device = pulser_abstract_repr.deserializer.deserialize_device(obj_str) + if isinstance(device, Device): + return device.to_virtual() + return device diff --git a/tests/test_abstract_repr.py b/tests/test_abstract_repr.py index f020628d..0188de60 100644 --- a/tests/test_abstract_repr.py +++ b/tests/test_abstract_repr.py @@ -36,6 +36,7 @@ DigitalAnalogDevice, IroiseMVP, MockDevice, + VirtualDevice, ) from pulser.json.abstract_repr.deserializer import ( VARIABLE_TYPE_MAP, @@ -228,27 +229,75 @@ def _roundtrip(abstract_device): def test_exceptions(self, abstract_device): def check_error_raised( - obj_str: str, original_err: Type[Exception], err_msg: str = "" + obj_str: str, + original_err: Type[Exception], + err_msg: str = "", + check_from_abstract_repr=False, ) -> Exception: - with pytest.raises(DeserializeDeviceError) as exc_info: - deserialize_device(obj_str) - - cause = exc_info.value.__cause__ - assert isinstance(cause, original_err) - assert re.search(re.escape(err_msg), str(cause)) is not None + func_to_test = [deserialize_device] + err_raised = [] + if check_from_abstract_repr: + func_to_test += [ + Device.from_abstract_repr, + VirtualDevice.from_abstract_repr, + ] + for func in func_to_test: + with pytest.raises(DeserializeDeviceError) as exc_info: + func(obj_str) + err_raised.append(exc_info) + for exc_info in err_raised: + cause = exc_info.value.__cause__ + assert isinstance(cause, original_err) + assert re.search(re.escape(err_msg), str(cause)) is not None return cause + dev_str = json.dumps(abstract_device) if abstract_device["name"] == "DigitalAnalogDevice": with pytest.warns( DeprecationWarning, match="From v0.18 and onwards" ): - good_device = deserialize_device(json.dumps(abstract_device)) + good_device = deserialize_device(dev_str) + deser_device = type(good_device).from_abstract_repr(dev_str) else: - good_device = deserialize_device(json.dumps(abstract_device)) + good_device = deserialize_device(dev_str) + deser_device = type(good_device).from_abstract_repr(dev_str) + assert good_device == deser_device + + if isinstance(good_device, Device): + if abstract_device["name"] == "DigitalAnalogDevice": + with pytest.warns( + DeprecationWarning, match="From v0.18 and onwards" + ): + deser_device = VirtualDevice.from_abstract_repr(dev_str) + else: + deser_device = VirtualDevice.from_abstract_repr(dev_str) + assert good_device.to_virtual() == deser_device + else: + with pytest.raises( + TypeError, + match="The given schema is not related to a Device, but to " + "a VirtualDevice.", + ): + if abstract_device["name"] == "DigitalAnalogDevice": + with pytest.warns( + DeprecationWarning, match="From v0.18 and onwards" + ): + Device.from_abstract_repr(dev_str) + else: + Device.from_abstract_repr(dev_str) check_error_raised( abstract_device, TypeError, "'obj_str' must be a string" ) + with pytest.raises( + TypeError, match="The serialized Device must be given as a string." + ): + Device.from_abstract_repr(abstract_device) + with pytest.raises( + TypeError, + match="The serialized VirtualDevice must be given as a string.", + ): + VirtualDevice.from_abstract_repr(abstract_device) # JSONDecodeError from json.loads() bad_str = "\ufeff" @@ -257,7 +306,7 @@ def check_error_raised( ) as err: json.loads(bad_str) err_msg = str(err.value) - check_error_raised(bad_str, json.JSONDecodeError, err_msg) + check_error_raised(bad_str, json.JSONDecodeError, err_msg, True) # jsonschema.exceptions.ValidationError from jsonschema invalid_dev = abstract_device.copy() @@ -268,6 +317,7 @@ def check_error_raised( json.dumps(invalid_dev), jsonschema.exceptions.ValidationError, str(err.value), + True, ) # AbstractReprError from invalid RydbergEOM configuration @@ -282,6 +332,7 @@ def check_error_raised( json.dumps(bad_eom_dev), AbstractReprError, "RydbergEOM deserialization failed.", + True, ) assert isinstance(prev_err.__cause__, ValueError) @@ -292,6 +343,7 @@ def check_error_raised( json.dumps(bad_ch_dev1), AbstractReprError, "Channel deserialization failed.", + True, ) assert isinstance(prev_err.__cause__, ValueError) @@ -302,6 +354,7 @@ def check_error_raised( json.dumps(bad_ch_dev2), AbstractReprError, "Channel deserialization failed.", + True, ) assert isinstance(prev_err.__cause__, NotImplementedError) @@ -315,6 +368,7 @@ def check_error_raised( json.dumps(bad_layout_dev), AbstractReprError, "Register layout deserialization failed.", + True, ) assert isinstance(prev_err.__cause__, ValueError) @@ -326,6 +380,7 @@ def check_error_raised( json.dumps(bad_xy_coeff_dev), AbstractReprError, "Device deserialization failed.", + True, ) assert isinstance(prev_err.__cause__, TypeError) @@ -336,6 +391,7 @@ def check_error_raised( json.dumps(bad_dev), AbstractReprError, "Device deserialization failed.", + True, ) assert isinstance(prev_err.__cause__, ValueError) @@ -353,6 +409,18 @@ def test_optional_device_fields(self, og_device, field, value): device = replace(og_device, **{field: value}) dev_str = device.to_abstract_repr() assert device == deserialize_device(dev_str) + assert device == type(og_device).from_abstract_repr(dev_str) + if isinstance(og_device, Device): + assert device.to_virtual() == VirtualDevice.from_abstract_repr( + dev_str + ) + return + with pytest.raises( + TypeError, + match="The given schema is not related to a Device, but to a " + "VirtualDevice.", + ): + Device.from_abstract_repr(dev_str) @pytest.mark.parametrize( "ch_obj", @@ -418,6 +486,7 @@ def test_optional_channel_fields(self, ch_obj): ) dev_str = device.to_abstract_repr() assert device == deserialize_device(dev_str) + assert device == VirtualDevice.from_abstract_repr(dev_str) def validate_schema(instance): From e4ab8e385a55b968d09d8fecd01a72ae06e1723a Mon Sep 17 00:00:00 2001 From: a_corni Date: Fri, 13 Sep 2024 14:22:50 +0200 Subject: [PATCH 2/3] Adding warning section on the behaviour of from_abstract_repr in Device and VirtualDevice --- pulser-core/pulser/devices/_device_datacls.py | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/pulser-core/pulser/devices/_device_datacls.py b/pulser-core/pulser/devices/_device_datacls.py index e8af0a46..886dd2d9 100644 --- a/pulser-core/pulser/devices/_device_datacls.py +++ b/pulser-core/pulser/devices/_device_datacls.py @@ -718,10 +718,14 @@ def _to_abstract_repr(self) -> dict[str, Any]: @staticmethod def from_abstract_repr(obj_str: str) -> Device: - """Deserialize a noise model from an abstract JSON object. + """Deserialize a Device from an abstract JSON object. + + Warning: + Raises an error if the JSON string represents a VirtualDevice. + VirtualDevice.from_abstract_repr should be used for this case. Args: - obj_str (str): the JSON string representing the noise model + obj_str (str): the JSON string representing the Device encoded in the abstract JSON format. """ if not isinstance(obj_str, str): @@ -823,7 +827,11 @@ def _to_abstract_repr(self) -> dict[str, Any]: @staticmethod def from_abstract_repr(obj_str: str) -> VirtualDevice: - """Deserialize a noise model from an abstract JSON object. + """Deserialize a VirtualDevice from an abstract JSON object. + + Warning: + If the JSON string represents a Device, the Device is converted + into a VirtualDevice using the `Device.to_virtual` method. Args: obj_str (str): the JSON string representing the noise model From 30adfbc22cbed1612e6a2cd9408d5558a85ffc65 Mon Sep 17 00:00:00 2001 From: a_corni Date: Fri, 13 Sep 2024 14:38:47 +0200 Subject: [PATCH 3/3] Address review comment in test_abstract_repr, solve typing --- tests/test_abstract_repr.py | 137 +++++++++++++++++++++++++++++------- 1 file changed, 113 insertions(+), 24 deletions(-) diff --git a/tests/test_abstract_repr.py b/tests/test_abstract_repr.py index eef64fd0..33945baa 100644 --- a/tests/test_abstract_repr.py +++ b/tests/test_abstract_repr.py @@ -226,23 +226,14 @@ def check_error_raised( obj_str: str, original_err: Type[Exception], err_msg: str = "", - check_from_abstract_repr=False, + func: Callable = deserialize_device, ) -> Exception: - func_to_test = [deserialize_device] - err_raised = [] - if check_from_abstract_repr: - func_to_test += [ - Device.from_abstract_repr, - VirtualDevice.from_abstract_repr, - ] - for func in func_to_test: - with pytest.raises(DeserializeDeviceError) as exc_info: - func(obj_str) - err_raised.append(exc_info) - for exc_info in err_raised: - cause = exc_info.value.__cause__ - assert isinstance(cause, original_err) - assert re.search(re.escape(err_msg), str(cause)) is not None + with pytest.raises(DeserializeDeviceError) as exc_info: + func(obj_str) + + cause = exc_info.value.__cause__ + assert isinstance(cause, original_err) + assert re.search(re.escape(err_msg), str(cause)) is not None return cause dev_str = json.dumps(abstract_device) @@ -279,7 +270,16 @@ def check_error_raised( ) as err: json.loads(bad_str) err_msg = str(err.value) - check_error_raised(bad_str, json.JSONDecodeError, err_msg, True) + check_error_raised(bad_str, json.JSONDecodeError, err_msg) + check_error_raised( + bad_str, json.JSONDecodeError, err_msg, Device.from_abstract_repr + ) + check_error_raised( + bad_str, + json.JSONDecodeError, + err_msg, + VirtualDevice.from_abstract_repr, + ) # jsonschema.exceptions.ValidationError from jsonschema invalid_dev = abstract_device.copy() @@ -290,7 +290,18 @@ def check_error_raised( json.dumps(invalid_dev), jsonschema.exceptions.ValidationError, str(err.value), - True, + ) + check_error_raised( + json.dumps(invalid_dev), + jsonschema.exceptions.ValidationError, + str(err.value), + Device.from_abstract_repr, + ) + check_error_raised( + json.dumps(invalid_dev), + jsonschema.exceptions.ValidationError, + str(err.value), + VirtualDevice.from_abstract_repr, ) # AbstractReprError from invalid RydbergEOM configuration @@ -305,7 +316,20 @@ def check_error_raised( json.dumps(bad_eom_dev), AbstractReprError, "RydbergEOM deserialization failed.", - True, + Device.from_abstract_repr, + ) + assert isinstance(prev_err.__cause__, ValueError) + prev_err = check_error_raised( + json.dumps(bad_eom_dev), + AbstractReprError, + "RydbergEOM deserialization failed.", + VirtualDevice.from_abstract_repr, + ) + assert isinstance(prev_err.__cause__, ValueError) + prev_err = check_error_raised( + json.dumps(bad_eom_dev), + AbstractReprError, + "RydbergEOM deserialization failed.", ) assert isinstance(prev_err.__cause__, ValueError) @@ -316,7 +340,20 @@ def check_error_raised( json.dumps(bad_ch_dev1), AbstractReprError, "Channel deserialization failed.", - True, + Device.from_abstract_repr, + ) + assert isinstance(prev_err.__cause__, ValueError) + prev_err = check_error_raised( + json.dumps(bad_ch_dev1), + AbstractReprError, + "Channel deserialization failed.", + VirtualDevice.from_abstract_repr, + ) + assert isinstance(prev_err.__cause__, ValueError) + prev_err = check_error_raised( + json.dumps(bad_ch_dev1), + AbstractReprError, + "Channel deserialization failed.", ) assert isinstance(prev_err.__cause__, ValueError) @@ -327,7 +364,20 @@ def check_error_raised( json.dumps(bad_ch_dev2), AbstractReprError, "Channel deserialization failed.", - True, + Device.from_abstract_repr, + ) + assert isinstance(prev_err.__cause__, NotImplementedError) + prev_err = check_error_raised( + json.dumps(bad_ch_dev2), + AbstractReprError, + "Channel deserialization failed.", + VirtualDevice.from_abstract_repr, + ) + assert isinstance(prev_err.__cause__, NotImplementedError) + prev_err = check_error_raised( + json.dumps(bad_ch_dev2), + AbstractReprError, + "Channel deserialization failed.", ) assert isinstance(prev_err.__cause__, NotImplementedError) @@ -341,7 +391,20 @@ def check_error_raised( json.dumps(bad_layout_dev), AbstractReprError, "Register layout deserialization failed.", - True, + Device.from_abstract_repr, + ) + assert isinstance(prev_err.__cause__, ValueError) + prev_err = check_error_raised( + json.dumps(bad_layout_dev), + AbstractReprError, + "Register layout deserialization failed.", + VirtualDevice.from_abstract_repr, + ) + assert isinstance(prev_err.__cause__, ValueError) + prev_err = check_error_raised( + json.dumps(bad_layout_dev), + AbstractReprError, + "Register layout deserialization failed.", ) assert isinstance(prev_err.__cause__, ValueError) @@ -353,7 +416,20 @@ def check_error_raised( json.dumps(bad_xy_coeff_dev), AbstractReprError, "Device deserialization failed.", - True, + Device.from_abstract_repr, + ) + assert isinstance(prev_err.__cause__, TypeError) + prev_err = check_error_raised( + json.dumps(bad_xy_coeff_dev), + AbstractReprError, + "Device deserialization failed.", + VirtualDevice.from_abstract_repr, + ) + assert isinstance(prev_err.__cause__, TypeError) + prev_err = check_error_raised( + json.dumps(bad_xy_coeff_dev), + AbstractReprError, + "Device deserialization failed.", ) assert isinstance(prev_err.__cause__, TypeError) @@ -364,7 +440,20 @@ def check_error_raised( json.dumps(bad_dev), AbstractReprError, "Device deserialization failed.", - True, + Device.from_abstract_repr, + ) + assert isinstance(prev_err.__cause__, ValueError) + prev_err = check_error_raised( + json.dumps(bad_dev), + AbstractReprError, + "Device deserialization failed.", + VirtualDevice.from_abstract_repr, + ) + assert isinstance(prev_err.__cause__, ValueError) + prev_err = check_error_raised( + json.dumps(bad_dev), + AbstractReprError, + "Device deserialization failed.", ) assert isinstance(prev_err.__cause__, ValueError)