From 13a881cead11beed1c19f878bbc99803631684f0 Mon Sep 17 00:00:00 2001 From: Lars Ivar Hatledal Date: Sat, 15 Feb 2020 18:39:07 +0100 Subject: [PATCH 1/7] Use str key for state and add integration test --- pythonfmu/fmi2slave.py | 10 +++++----- pythonfmu/tests/test_integration.py | 31 +++++++++++++++++++++++++++++ 2 files changed, 36 insertions(+), 5 deletions(-) diff --git a/pythonfmu/fmi2slave.py b/pythonfmu/fmi2slave.py index 8e573642..9e1c690a 100644 --- a/pythonfmu/fmi2slave.py +++ b/pythonfmu/fmi2slave.py @@ -251,12 +251,12 @@ def set_string(self, vrs: List[int], values: List[str]): f"Variable with valueReference={vr} is not of type String!" ) - def _get_fmu_state(self) -> Dict[int, Any]: + def _get_fmu_state(self) -> Dict[str, Any]: state = dict() for var in self.vars.values(): - state[var.value_reference] = self.get_value(var.name) + state[var.name] = self.get_value(var.name) return state - def _set_fmu_state(self, state: Dict[int, Any]): - for vr, value in state.items(): - self.set_value(self.vars[vr].name, value) + def _set_fmu_state(self, state: Dict[str, Any]): + for name, value in state.items(): + self.set_value(name, value) diff --git a/pythonfmu/tests/test_integration.py b/pythonfmu/tests/test_integration.py index 76a10f6e..82caeda9 100644 --- a/pythonfmu/tests/test_integration.py +++ b/pythonfmu/tests/test_integration.py @@ -50,6 +50,37 @@ def test_integration_reset(tmp_path): assert read == pytest.approx(initial_value, rel=1e-7) +@pytest.mark.integration +def test_integration_state(tmp_path): + script_file = Path(__file__).parent / DEMO + + FmuBuilder.build_FMU(script_file, dest=tmp_path, needsExecutionTool="false", canGetAndSetFMUstate="true") + + fmu = tmp_path / "PythonSlave.fmu" + assert fmu.exists() + + vr = 5 # realOut + dt = 0.1 + t = 0.0 + + def step(model): + nonlocal t + model.do_step(t, dt, True) + t += dt + + model = pyfmi.load_fmu(str(fmu)) + step(model) + state = model.get_fmu_state() + assert model.get_real([vr])[0] == pytest.approx(dt, rel=1e-7) + step(model) + assert model.get_real([vr])[0] == pytest.approx(dt * 2, rel=1e-7) + model.set_fmu_state(state) + assert model.get_real([vr])[0] == pytest.approx(dt, rel=1e-7) + step(model) + assert model.get_real([vr])[0] == pytest.approx(dt * 3, rel=1e-7) + model.free_fmu_state(state) + + @pytest.mark.integration def test_integration_get(tmp_path): From cdf18ad43cdc8b239c622adb5b2814d905492751 Mon Sep 17 00:00:00 2001 From: Lars Ivar Hatledal Date: Sat, 15 Feb 2020 21:48:18 +0100 Subject: [PATCH 2/7] Support serialization of state --- pythonfmu/fmi2slave.py | 14 ++-- .../pythonfmu-export/cpp/PySlaveInstance.cpp | 26 +++++++- .../pythonfmu-export/cpp/fmi_functions.cpp | 64 ++++++++++++------- .../headers/cppfmu/cppfmu_cs.hpp | 6 +- .../headers/pythonfmu/PySlaveInstance.hpp | 4 ++ 5 files changed, 84 insertions(+), 30 deletions(-) diff --git a/pythonfmu/fmi2slave.py b/pythonfmu/fmi2slave.py index 9e1c690a..4c1bab60 100644 --- a/pythonfmu/fmi2slave.py +++ b/pythonfmu/fmi2slave.py @@ -1,4 +1,5 @@ """Define the abstract facade class.""" +import json import datetime from abc import ABC, abstractmethod from collections import OrderedDict, namedtuple @@ -251,12 +252,17 @@ def set_string(self, vrs: List[int], values: List[str]): f"Variable with valueReference={vr} is not of type String!" ) - def _get_fmu_state(self) -> Dict[str, Any]: + def store_custom_state(self, state: Dict[str, Any]): + pass + + def _get_fmu_state(self) -> bytes: state = dict() for var in self.vars.values(): state[var.name] = self.get_value(var.name) - return state + self.store_custom_state(state) + return json.dumps(state).encode("utf-8") - def _set_fmu_state(self, state: Dict[str, Any]): - for name, value in state.items(): + def _set_fmu_state(self, state: bytes): + py_state: Dict[str, Any] = json.loads(state.decode("utf-8")) + for name, value in py_state.items(): self.set_value(name, value) diff --git a/pythonfmu/pythonfmu-export/cpp/PySlaveInstance.cpp b/pythonfmu/pythonfmu-export/cpp/PySlaveInstance.cpp index c651cadd..2abed528 100644 --- a/pythonfmu/pythonfmu-export/cpp/PySlaveInstance.cpp +++ b/pythonfmu/pythonfmu-export/cpp/PySlaveInstance.cpp @@ -307,13 +307,37 @@ void PySlaveInstance::FreeFMUstate(fmi2FMUstate& state) Py_XDECREF(f); } +size_t PySlaveInstance::SerializedFMUstateSize(const fmi2FMUstate& state) +{ + auto f = reinterpret_cast(state); + return PyBytes_Size(f); +} + +void PySlaveInstance::SerializeFMUstate(const fmi2FMUstate& state, fmi2Byte* bytes, size_t size) +{ + auto f = reinterpret_cast(state); + char* c = PyBytes_AsString(f); + for (int i = 0; i < size; i++) { + bytes[i] = c[i]; + } +} + +void PySlaveInstance::DeSerializeFMUstate(const fmi2Byte bytes[], size_t size, fmi2FMUstate& state) +{ + PyObject* pyState = PyBytes_FromStringAndSize(bytes, size); + if (pyState == nullptr) { + handle_py_exception("[DeSerializeFMUstate] PyBytes_FromStringAndSize"); + } + state = reinterpret_cast(pyState); +} + + PySlaveInstance::~PySlaveInstance() { Py_XDECREF(pClass_); Py_XDECREF(pInstance_); } - } // namespace pythonfmu std::unique_ptr pyState = nullptr; diff --git a/pythonfmu/pythonfmu-export/cpp/fmi_functions.cpp b/pythonfmu/pythonfmu-export/cpp/fmi_functions.cpp index aee71170..e2beafe8 100644 --- a/pythonfmu/pythonfmu-export/cpp/fmi_functions.cpp +++ b/pythonfmu/pythonfmu-export/cpp/fmi_functions.cpp @@ -424,40 +424,58 @@ fmi2Status fmi2FreeFMUstate( fmi2Status fmi2SerializedFMUstateSize( fmi2Component c, - fmi2FMUstate, - size_t*) + fmi2FMUstate state, + size_t* size) { - reinterpret_cast(c)->logger.Log( - fmi2Error, - "cppfmu", - "FMI function not supported: fmi2SerializedFMUstateSize"); - return fmi2Error; + const auto component = reinterpret_cast(c); + try { + *size = component->slave->SerializedFMUstateSize(state); + return fmi2OK; + } catch (const cppfmu::FatalError& e) { + component->logger.Log(fmi2Fatal, "", e.what()); + return fmi2Fatal; + } catch (const std::exception& e) { + component->logger.Log(fmi2Error, "", e.what()); + return fmi2Error; + } } fmi2Status fmi2SerializeFMUstate( fmi2Component c, - fmi2FMUstate, - fmi2Byte[], - size_t) + fmi2FMUstate state, + fmi2Byte bytes[], + size_t size) { - reinterpret_cast(c)->logger.Log( - fmi2Error, - "cppfmu", - "FMI function not supported: fmi2SerializeFMUstate"); - return fmi2Error; + const auto component = reinterpret_cast(c); + try { + component->slave->SerializeFMUstate(state, bytes, size); + return fmi2OK; + } catch (const cppfmu::FatalError& e) { + component->logger.Log(fmi2Fatal, "", e.what()); + return fmi2Fatal; + } catch (const std::exception& e) { + component->logger.Log(fmi2Error, "", e.what()); + return fmi2Error; + } } fmi2Status fmi2DeSerializeFMUstate( fmi2Component c, - const fmi2Byte[], - size_t, - fmi2FMUstate*) + const fmi2Byte bytes[], + size_t size, + fmi2FMUstate* state) { - reinterpret_cast(c)->logger.Log( - fmi2Error, - "cppfmu", - "FMI function not supported: fmi2DeSerializeFMUstate"); - return fmi2Error; + const auto component = reinterpret_cast(c); + try { + component->slave->DeSerializeFMUstate(bytes, size, *state); + return fmi2OK; + } catch (const cppfmu::FatalError& e) { + component->logger.Log(fmi2Fatal, "", e.what()); + return fmi2Fatal; + } catch (const std::exception& e) { + component->logger.Log(fmi2Error, "", e.what()); + return fmi2Error; + } } diff --git a/pythonfmu/pythonfmu-export/headers/cppfmu/cppfmu_cs.hpp b/pythonfmu/pythonfmu-export/headers/cppfmu/cppfmu_cs.hpp index e022c989..1e0ff4cd 100644 --- a/pythonfmu/pythonfmu-export/headers/cppfmu/cppfmu_cs.hpp +++ b/pythonfmu/pythonfmu-export/headers/cppfmu/cppfmu_cs.hpp @@ -112,11 +112,13 @@ class SlaveInstance FMIReal& endOfStep) = 0; virtual void GetFMUstate(fmi2FMUstate& state) = 0; - virtual void SetFMUstate(const fmi2FMUstate& state) = 0; - virtual void FreeFMUstate(fmi2FMUstate& state) = 0; + virtual size_t SerializedFMUstateSize(const fmi2FMUstate& state) = 0; + virtual void SerializeFMUstate(const fmi2FMUstate& state, fmi2Byte bytes[], size_t size) = 0; + virtual void DeSerializeFMUstate(const fmi2Byte bytes[], size_t size, fmi2FMUstate& state) = 0; + // The instance is destroyed in fmi2FreeInstance()/fmiFreeSlaveInstance(). virtual ~SlaveInstance() CPPFMU_NOEXCEPT; }; diff --git a/pythonfmu/pythonfmu-export/headers/pythonfmu/PySlaveInstance.hpp b/pythonfmu/pythonfmu-export/headers/pythonfmu/PySlaveInstance.hpp index cb749d2d..b619b14b 100644 --- a/pythonfmu/pythonfmu-export/headers/pythonfmu/PySlaveInstance.hpp +++ b/pythonfmu/pythonfmu-export/headers/pythonfmu/PySlaveInstance.hpp @@ -39,6 +39,10 @@ class PySlaveInstance : public cppfmu::SlaveInstance void SetFMUstate(const fmi2FMUstate& state) override; void FreeFMUstate(fmi2FMUstate& state) override; + size_t SerializedFMUstateSize(const fmi2FMUstate& state) override; + void SerializeFMUstate(const fmi2FMUstate& state, fmi2Byte bytes[], size_t size) override; + void DeSerializeFMUstate(const fmi2Byte bytes[], size_t size, fmi2FMUstate& state) override; + ~PySlaveInstance() override; private: From fb7de36bfcdb70d4e029ce247f94d452745e116d Mon Sep 17 00:00:00 2001 From: Lars Ivar Hatledal Date: Sun, 16 Feb 2020 17:10:50 +0100 Subject: [PATCH 3/7] Remove store_custom_state for now --- pythonfmu/fmi2slave.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/pythonfmu/fmi2slave.py b/pythonfmu/fmi2slave.py index 4c1bab60..bd772b91 100644 --- a/pythonfmu/fmi2slave.py +++ b/pythonfmu/fmi2slave.py @@ -252,14 +252,10 @@ def set_string(self, vrs: List[int], values: List[str]): f"Variable with valueReference={vr} is not of type String!" ) - def store_custom_state(self, state: Dict[str, Any]): - pass - def _get_fmu_state(self) -> bytes: state = dict() for var in self.vars.values(): state[var.name] = self.get_value(var.name) - self.store_custom_state(state) return json.dumps(state).encode("utf-8") def _set_fmu_state(self, state: bytes): From 69cc532349f36115eb0df5061c158df686c78dca Mon Sep 17 00:00:00 2001 From: Lars Ivar Hatledal Date: Sun, 16 Feb 2020 19:23:02 +0100 Subject: [PATCH 4/7] Separate logic for get/set and serialize/deserialise state --- pythonfmu/fmi2slave.py | 17 +++++++---- .../pythonfmu-export/cpp/PySlaveInstance.cpp | 29 ++++++++++++++----- 2 files changed, 34 insertions(+), 12 deletions(-) diff --git a/pythonfmu/fmi2slave.py b/pythonfmu/fmi2slave.py index fccf52dc..df1360db 100644 --- a/pythonfmu/fmi2slave.py +++ b/pythonfmu/fmi2slave.py @@ -251,13 +251,20 @@ def set_string(self, vrs: List[int], values: List[str]): f"Variable with valueReference={vr} is not of type String!" ) - def _get_fmu_state(self) -> bytes: + def _get_fmu_state(self) -> Dict[str, any]: state = dict() for var in self.vars.values(): state[var.name] = self.get_value(var.name) - return json.dumps(state).encode("utf-8") + return state - def _set_fmu_state(self, state: bytes): - py_state: Dict[str, Any] = json.loads(state.decode("utf-8")) - for name, value in py_state.items(): + def _set_fmu_state(self, state: Dict[str, any]): + for name, value in state.items(): self.set_value(name, value) + + @staticmethod + def _fmu_state_to_bytes(state: Dict[str, Any]) -> bytes: + return json.dumps(state).encode("utf-8") + + @staticmethod + def _fmu_state_from_bytes(state: bytes) -> Dict[str, Any]: + return json.loads(state.decode("utf-8")) diff --git a/pythonfmu/pythonfmu-export/cpp/PySlaveInstance.cpp b/pythonfmu/pythonfmu-export/cpp/PySlaveInstance.cpp index 2abed528..56ba7ca0 100644 --- a/pythonfmu/pythonfmu-export/cpp/PySlaveInstance.cpp +++ b/pythonfmu/pythonfmu-export/cpp/PySlaveInstance.cpp @@ -309,29 +309,44 @@ void PySlaveInstance::FreeFMUstate(fmi2FMUstate& state) size_t PySlaveInstance::SerializedFMUstateSize(const fmi2FMUstate& state) { - auto f = reinterpret_cast(state); - return PyBytes_Size(f); + auto pyState = reinterpret_cast(state); + PyObject* pyStateBytes = PyObject_CallMethod(pClass_, "_fmu_state_to_bytes", "(O)", pyState); + auto size = PyBytes_Size(pyStateBytes); + Py_DECREF(pyStateBytes); + return size; } void PySlaveInstance::SerializeFMUstate(const fmi2FMUstate& state, fmi2Byte* bytes, size_t size) { - auto f = reinterpret_cast(state); - char* c = PyBytes_AsString(f); + auto pyState = reinterpret_cast(state); + PyObject* pyStateBytes = PyObject_CallMethod(pClass_, "_fmu_state_to_bytes", "(O)", pyState); + if (pyStateBytes == nullptr) { + handle_py_exception("[SerializeFMUstate] PyObject_CallMethod"); + } + char* c = PyBytes_AsString(pyStateBytes); + if (c == nullptr) { + handle_py_exception("[SerializeFMUstate] PyBytes_AsString"); + } for (int i = 0; i < size; i++) { bytes[i] = c[i]; } + Py_DECREF(pyStateBytes); } void PySlaveInstance::DeSerializeFMUstate(const fmi2Byte bytes[], size_t size, fmi2FMUstate& state) { - PyObject* pyState = PyBytes_FromStringAndSize(bytes, size); - if (pyState == nullptr) { + PyObject* pyStateBytes = PyBytes_FromStringAndSize(bytes, size); + if (pyStateBytes == nullptr) { handle_py_exception("[DeSerializeFMUstate] PyBytes_FromStringAndSize"); } + PyObject* pyState = PyObject_CallMethod(pClass_, "_fmu_state_from_bytes", "(O)", pyStateBytes); + if (pyState == nullptr) { + handle_py_exception("[DeSerializeFMUstate] PyObject_CallMethod"); + } state = reinterpret_cast(pyState); + Py_DECREF(pyStateBytes); } - PySlaveInstance::~PySlaveInstance() { Py_XDECREF(pClass_); From 1aeb4bcb43c3325864b0a42a0d700b0e8f6c7092 Mon Sep 17 00:00:00 2001 From: Lars Ivar Hatledal Date: Sun, 16 Feb 2020 19:24:41 +0100 Subject: [PATCH 5/7] Exception logic --- pythonfmu/pythonfmu-export/cpp/PySlaveInstance.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pythonfmu/pythonfmu-export/cpp/PySlaveInstance.cpp b/pythonfmu/pythonfmu-export/cpp/PySlaveInstance.cpp index 56ba7ca0..5b77e95e 100644 --- a/pythonfmu/pythonfmu-export/cpp/PySlaveInstance.cpp +++ b/pythonfmu/pythonfmu-export/cpp/PySlaveInstance.cpp @@ -311,6 +311,9 @@ size_t PySlaveInstance::SerializedFMUstateSize(const fmi2FMUstate& state) { auto pyState = reinterpret_cast(state); PyObject* pyStateBytes = PyObject_CallMethod(pClass_, "_fmu_state_to_bytes", "(O)", pyState); + if (pyStateBytes == nullptr) { + handle_py_exception("[SerializedFMUstateSize] PyObject_CallMethod"); + } auto size = PyBytes_Size(pyStateBytes); Py_DECREF(pyStateBytes); return size; From 9311e531672afb5ae776c78de385531c788a5932 Mon Sep 17 00:00:00 2001 From: Lars Ivar Hatledal Date: Mon, 17 Feb 2020 07:56:27 +0100 Subject: [PATCH 6/7] Update pythonfmu/fmi2slave.py MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Frédéric Collonval --- pythonfmu/fmi2slave.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pythonfmu/fmi2slave.py b/pythonfmu/fmi2slave.py index df1360db..65cc473a 100644 --- a/pythonfmu/fmi2slave.py +++ b/pythonfmu/fmi2slave.py @@ -257,7 +257,7 @@ def _get_fmu_state(self) -> Dict[str, any]: state[var.name] = self.get_value(var.name) return state - def _set_fmu_state(self, state: Dict[str, any]): + def _set_fmu_state(self, state: Dict[str, Any]): for name, value in state.items(): self.set_value(name, value) From ba81758bb8baea4ed4b92385b8a9b2ff1cdc3a43 Mon Sep 17 00:00:00 2001 From: Lars Ivar Hatledal Date: Mon, 17 Feb 2020 07:56:43 +0100 Subject: [PATCH 7/7] Update pythonfmu/fmi2slave.py MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Frédéric Collonval --- pythonfmu/fmi2slave.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pythonfmu/fmi2slave.py b/pythonfmu/fmi2slave.py index 65cc473a..9245dc51 100644 --- a/pythonfmu/fmi2slave.py +++ b/pythonfmu/fmi2slave.py @@ -251,7 +251,7 @@ def set_string(self, vrs: List[int], values: List[str]): f"Variable with valueReference={vr} is not of type String!" ) - def _get_fmu_state(self) -> Dict[str, any]: + def _get_fmu_state(self) -> Dict[str, Any]: state = dict() for var in self.vars.values(): state[var.name] = self.get_value(var.name)