diff --git a/src/accml_lib/core/interfaces/utils/measurement_execution_engine.py b/src/accml_lib/core/interfaces/utils/measurement_execution_engine.py index 61906a4..6db81bf 100644 --- a/src/accml_lib/core/interfaces/utils/measurement_execution_engine.py +++ b/src/accml_lib/core/interfaces/utils/measurement_execution_engine.py @@ -7,7 +7,7 @@ class MeasurementExecutionEngine(metaclass=ABCMeta): @abstractmethod - def execute( + async def execute( self, commands_collection: Sequence[TransactionCommand], *args, **kwargs ) -> str: """ diff --git a/src/accml_lib/core/model/conv.py b/src/accml_lib/core/model/conv.py new file mode 100644 index 0000000..4cdb272 --- /dev/null +++ b/src/accml_lib/core/model/conv.py @@ -0,0 +1,23 @@ +from typing import Any, Dict, Union +import jsons + + +def deserialse_value(value: Dict[str, Union[str, int, float, dict]], **kwargs): + def delegate_to_jsons(obj, **kwargs): + return jsons.dump(obj, **kwargs) + + d = dict(int=int, float=float, dict=delegate_to_jsons) + ins = d[value["type"]] + r = ins(value["value"]) + return r + + +def serialize_value(value, **kwargs) -> dict[str, Any]: + if isinstance(value, int): + return dict(type="int", value=value) + elif isinstance(value, float): + return dict(type="float", value=value) + + d = jsons.dump(value, **kwargs) + assert isinstance(d, dict), "don't know how to serialize {value}" + return dict(type="dict", value=d) diff --git a/src/accml_lib/core/model/jsons_support.py b/src/accml_lib/core/model/jsons_support.py new file mode 100644 index 0000000..1c7134c --- /dev/null +++ b/src/accml_lib/core/model/jsons_support.py @@ -0,0 +1,19 @@ +"""register json serializer / deserialiser support functions +""" +import jsons + +from .utils import command +from .output import result + + +def register_serializers(json_fork): + + command.register_serializer_for_command(json_fork) + result.register_serializer_for_read_together(json_fork) + result.register_serializer_for_single_reading(json_fork) + + +def register_deserializers(json_fork): + + command.register_deserializer_for_command(json_fork) + result.register_deserializer_for_single_reading(json_fork) diff --git a/src/accml_lib/core/model/output/result.py b/src/accml_lib/core/model/output/result.py index 2470977..4593223 100644 --- a/src/accml_lib/core/model/output/result.py +++ b/src/accml_lib/core/model/output/result.py @@ -1,14 +1,18 @@ import datetime -from dataclasses import dataclass +import json +from dataclasses import dataclass, asdict from functools import cached_property -from typing import Sequence +from typing import Sequence, Dict, Union -from ..utils.command import ReadCommand +import jsons + +from ..conv import deserialse_value, serialize_value +from ..utils.command import ReadCommand, Command @dataclass class SingleFloat: - value : float + value: float @dataclass @@ -17,16 +21,24 @@ class SingleReading: e.g. reading from one device or tune of the machine """ + name: str payload: object cmd: ReadCommand @classmethod - def from_jsons(cls, obj): - name = obj["name"] - payload = obj["payload"] - cmd = obj["cmd"] - return cls(name=name, payload=payload, cmd=cmd) + def from_jsons(cls, obj, **kwargs): + cmd = jsons.load(obj["cmd"], ReadCommand, **kwargs) + return cls(name=obj["name"], cmd=cmd, payload=deserialse_value(obj["payload"])) + + def to_jsons(self, **kwargs): + import jsons + + return dict( + name=self.name, + cmd=jsons.dump(self.cmd, **kwargs), + payload=serialize_value(self.payload, **kwargs), + ) @dataclass @@ -34,48 +46,72 @@ class ReadTogether: """ data taken together """ - data : Sequence[SingleReading] + + data: Sequence[SingleReading] + start: datetime.datetime + end: datetime.datetime def get(self, key): - return self._dict[key] + return self._dict[key] @cached_property def _dict(self): - return {d.name : d for d in self.data} + return {d.name: d for d in self.data} + + def to_jsons(self, **kwargs): + import jsons - def to_jsons(self): - return dict(name=self.__class__.__name__, data=self.data) + def conv(obj): + r = jsons.dump(obj, **kwargs) + return r + + return dict(data=conv(self.data), start=conv(self.start), end=conv(self.end)) + + +@dataclass +class ResultOfExecutionStep: + #: the relevant commands reflecting state changes + #: the idea is that with data one can reconstruct + #: in which state the machine or accelerator was + cmds: Sequence[Command] + data: Sequence[ReadTogether] @dataclass class Result: - start: datetime.datetime - end: datetime.datetime #: in the expected view / context of the caller - data: Sequence[ReadTogether] + data: Sequence[ResultOfExecutionStep] #: in the expected view / context as produced by the backend - orig_data: Sequence[ReadTogether] + orig_data: Sequence[ResultOfExecutionStep] + +def register_deserializer_for_single_reading(t_fork): + import jsons + def convert(obj: dict, cls: SingleReading, **kwargs): + assert cls == SingleReading, "only prepared for SingleReading here" + return cls.from_jsons(obj, **kwargs) -def single_reading_deserializer(obj: dict, cls, **kwargs): - assert cls == SingleReading, "only prepared to handle single reading" - return cls.from_jsons(obj) + jsons.set_deserializer(convert, SingleReading, high_prio=True, fork_inst=t_fork) -def dataclass_delegate_to_jsons_method(obj, **kwargs): +def register_serializer_for_single_reading(t_fork): import jsons - r = jsons.dump(obj.to_jsons(), **kwargs) - return r + def convert(obj, cls: SingleReading, **kwargs): + if cls is not None: + assert cls == SingleReading, "only prepared to handle single reading" + return obj.to_jsons(**kwargs) -def register_deserializers_to_json_fork(t_fork): - import jsons - jsons.set_deserializer(single_reading_deserializer, SingleReading, high_prio=True, fork_inst=t_fork) + jsons.set_serializer(convert, SingleReading, high_prio=True, fork_inst=t_fork) -def register_serializers_to_json_fork(t_fork): +def register_serializer_for_read_together(t_fork): import jsons - jsons.set_serializer(dataclass_delegate_to_jsons_method, ReadTogether, high_prio=True, fork_inst=t_fork) + def convert(obj, cls: ReadTogether, **kwargs): + if cls is not None: + assert cls == ReadTogether, "only prepared to handle read to gether" + return obj.to_jsons(**kwargs) + jsons.set_serializer(convert, ReadTogether, high_prio=True, fork_inst=t_fork) diff --git a/src/accml_lib/core/model/utils/command.py b/src/accml_lib/core/model/utils/command.py index 65de672..6597db1 100644 --- a/src/accml_lib/core/model/utils/command.py +++ b/src/accml_lib/core/model/utils/command.py @@ -1,6 +1,8 @@ from dataclasses import dataclass from enum import IntEnum -from typing import Sequence +from typing import Sequence, Any, Union, Dict + +from accml_lib.core.model.conv import deserialse_value, serialize_value class BehaviourOnError(IntEnum): @@ -22,9 +24,33 @@ class Command: #: can be the identifier of a lattice element or a device id: str property: str - value: object + #: the object can not be deserialized need to be more precise here + value: Union[int, float] behaviour_on_error: BehaviourOnError + @classmethod + def from_jsons(cls, d: dict, **kwargs): + """ + Todo: + check if kwargs needs to be passed to some argument + """ + v = deserialse_value(d["value"]) + return cls( + id=d["id"], + property=d["property"], + value=v, + behaviour_on_error=d["behaviour_on_error"], + ) + + def to_jsons(self, **kwargs): + d = dict( + id=self.id, + property=self.property, + value=serialize_value(self.value, **kwargs), + behaviour_on_error=self.behaviour_on_error, + ) + return d + @dataclass class TransactionCommand: @@ -44,4 +70,33 @@ class CommandSequence: commands: Sequence[TransactionCommand] -__all__ = ["BehaviourOnError", "Command", "CommandSequence", "ReadCommand", "TransactionCommand"] +def command_deserializer(obj: dict, cls: Command, **kwargs): + assert cls == Command, "only prepared to handle single reading" + r = cls.from_jsons(obj, **kwargs) + return r + + +def register_deserializer_for_command(t_fork): + import jsons + + jsons.set_deserializer( + command_deserializer, Command, high_prio=True, fork_inst=t_fork + ) + + +def register_serializer_for_command(t_fork): + import jsons + + def conv(ins: Command, **kwargs): + return ins.to_jsons(**kwargs) + + jsons.set_serializer(conv, Command, high_prio=True, fork_inst=t_fork) + + +__all__ = [ + "BehaviourOnError", + "Command", + "CommandSequence", + "ReadCommand", + "TransactionCommand", +] diff --git a/tests/test_core/test_jsons_support.py b/tests/test_core/test_jsons_support.py new file mode 100644 index 0000000..094aea7 --- /dev/null +++ b/tests/test_core/test_jsons_support.py @@ -0,0 +1,148 @@ +"""tests for dedicated jsons serialisers, deserialisers +""" +import datetime + +import jsons +import pytest + +from accml_lib.core.model import jsons_support +from accml_lib.core.model.output.result import ReadTogether, SingleReading +from accml_lib.core.model.output.tune import Tune +from accml_lib.core.model.utils.command import Command, BehaviourOnError, ReadCommand + +jsons_fork = jsons.fork() +jsons_support.register_serializers(jsons_fork) +jsons_support.register_deserializers(jsons_fork) + + +def test_serialize_command_with_int_value(): + cmd = Command( + id="foo", + property="bar", + value=int(42), + behaviour_on_error=BehaviourOnError.stop, + ) + data = jsons.dump(cmd, fork_inst=jsons_fork) + assert data["id"] == "foo" + assert data["property"] == "bar" + assert data["behaviour_on_error"] == BehaviourOnError.stop + v = data["value"] + assert v["type"] == "int" + assert v["value"] == 42 + + ncmd = jsons.load(data, Command, fork_inst=jsons_fork) + assert cmd == ncmd + assert isinstance(ncmd.value, int) + + +def test_serialize_command_with_float_value(): + cmd = Command( + id="box", + property="blue", + value=float(42), + behaviour_on_error=BehaviourOnError.ignore, + ) + data = jsons.dump(cmd, fork_inst=jsons_fork) + assert data["id"] == "box" + assert data["property"] == "blue" + assert data["behaviour_on_error"] == BehaviourOnError.ignore + + v = data["value"] + assert v["type"] == "float" + assert v["value"] == pytest.approx(42, abs=1e-12) + + ncmd = jsons.load(data, Command, fork_inst=jsons_fork) + assert cmd == ncmd + + +def test_serialize_command_with_float_with_decimal_digits(): + import math + + cmd = Command( + id="pi", + property="slightly_off", + value=float(355e0 / 113e0), + behaviour_on_error=BehaviourOnError.roll_back, + ) + data = jsons.dump(cmd, fork_inst=jsons_fork) + assert data["id"] == "pi" + assert data["property"] == "slightly_off" + assert data["behaviour_on_error"] == BehaviourOnError.roll_back + + v = data["value"] + assert v["type"] == "float" + assert v["value"] == pytest.approx(math.pi, abs=1e-6) + assert v["value"] == pytest.approx(355e0 / 113e0, abs=1e-15) + + # Better to check element by element + # double conversion, some digits can be lost + ncmd = jsons.load(data, Command, fork_inst=jsons_fork) + del data + assert ncmd.id == "pi" + assert ncmd.property == "slightly_off" + assert ncmd.behaviour_on_error == BehaviourOnError.roll_back + assert ncmd.value == pytest.approx(355e0 / 113e0, abs=1e-15) + assert isinstance(ncmd.value, float) + + +def test_single_reading(): + sr = SingleReading( + cmd=ReadCommand(id="car", property="wheel"), name="semperit", payload=42 + ) + d = jsons.dump(sr, fork_inst=jsons_fork) + cmd = d["cmd"] + assert cmd["id"] == "car" + assert cmd["property"] == "wheel" + + assert d["name"] == "semperit" + p = d["payload"] + assert p["value"] == 42 + assert p["type"] == "int" + + nsr = jsons.load(d, SingleReading, fork_inst=jsons_fork) + assert nsr == sr + + +def test_single_reading_payload_tune(): + t = Tune(x=1060.1, y=907.00) + sr = SingleReading( + cmd=ReadCommand(id="tune", property="transveral"), name="test", payload=t + ) + d = jsons.dump(sr, fork_inst=jsons_fork) + payload = d["payload"] + assert payload["type"] == "dict" + assert payload["value"]["x"] == pytest.approx(t.x, rel=1e-6, abs=1e-3) + assert payload["value"]["y"] == pytest.approx(t.y, rel=1e-6, abs=1e-3) + nsr = jsons.load(sr, SingleReading) + assert nsr.payload.x == pytest.approx(t.x, rel=1e-6, abs=1e-3) + assert nsr.payload.y == pytest.approx(t.y, rel=1e-6, abs=1e-3) + + +def test_read_together(): + end = datetime.datetime.now().astimezone() + start = end - datetime.timedelta(seconds=2) + datum = ReadTogether( + start=start, + end=end, + data=[ + SingleReading( + cmd=ReadCommand(id="guide", property="computer"), name="foo", payload=42 + ) + ], + ) + d = jsons.dump(datum, fork_inst=jsons_fork) + assert datetime.datetime.fromisoformat(d["start"]) == start + assert datetime.datetime.fromisoformat(d["end"]) == end + + (sr,) = d["data"] + assert sr["name"] == "foo" + p = sr["payload"] + assert p["type"] == "int" + assert p["value"] == 42 + + cmd = sr["cmd"] + assert cmd["id"] == "guide" + assert cmd["property"] == "computer" + + ndatum = jsons.load(d, ReadTogether, fork_inst=jsons_fork) + assert ndatum == datum