From d6918e144cf42229b87699c44e9fc1a1859837cd Mon Sep 17 00:00:00 2001 From: Stephen Smith Date: Fri, 25 Jul 2025 14:09:25 +0100 Subject: [PATCH 1/2] event model excange --- examples/bouncingBall_me.py | 75 +++++++++++++++++++ pythonfmu3/fmi3slave.py | 15 +++- pythonfmu3/modelexchange.py | 6 +- .../pythonfmu-export/src/cppfmu/cppfmu_cs.hpp | 1 + .../src/cppfmu/fmi_functions.cpp | 12 ++- .../src/pythonfmu/PySlaveInstance.cpp | 27 ++++++- 6 files changed, 131 insertions(+), 5 deletions(-) create mode 100644 examples/bouncingBall_me.py diff --git a/examples/bouncingBall_me.py b/examples/bouncingBall_me.py new file mode 100644 index 0000000..e8083aa --- /dev/null +++ b/examples/bouncingBall_me.py @@ -0,0 +1,75 @@ +from pythonfmu3 import Fmi3Causality, Fmi3Variability, Fmi3SlaveBase, ModelExchange, Fmi3Status, Float64, Fmi3Initial, Unit, Float64Type, Fmi3StepResult + +from typing import List + + +EVENT_EPS = 1e-12 + +class BouncingBall(Fmi3SlaveBase, ModelExchange): + + def __init__(self, **kwargs): + super().__init__(**kwargs) + + self.author = "..." + self.description = "Bouncing Ball" + + self.time = 0.0 + self.h = 1.0 + self.v = 0.0 + self.derh = 0.0 + self.derv = 0.0 + self.g = -9.81 + self.e = 0.7 + self.v_min = 0.1 + + # define units + unit1 = Unit(name="m", m=1) + unit2 = Unit(name="m/s", m=1, s=-1) + unit3 = Unit(name="m/s2", m=1, s=-2) + self.register_units([unit1, unit2, unit3]) + + + self.register_variable(Float64("time", causality=Fmi3Causality.independent, variability=Fmi3Variability.continuous)) + + self.register_variable(Float64("ball.h", causality=Fmi3Causality.output, start=1, variability=Fmi3Variability.continuous, initial=Fmi3Initial.exact, unit=unit1.name), + nested=False, has_event_indicator=True) + self.register_variable(Float64("ball.derh", causality=Fmi3Causality.local, variability=Fmi3Variability.continuous, derivative=1, unit=unit2.name), + nested=False) + self.register_variable(Float64("ball.v", causality=Fmi3Causality.output, start=0, variability=Fmi3Variability.continuous, initial=Fmi3Initial.exact, unit=unit2.name), + nested=False) + self.register_variable(Float64("ball.derv", causality=Fmi3Causality.local, variability=Fmi3Variability.continuous, derivative=3, unit=unit3.name), + nested=False) + + self.register_variable(Float64("g", causality=Fmi3Causality.parameter, variability=Fmi3Variability.fixed, unit=unit3.name)) + self.register_variable(Float64("e", causality=Fmi3Causality.parameter, variability=Fmi3Variability.tunable)) + self.register_variable(Float64("v_min", variability=Fmi3Variability.constant, start=0.1)) + + + + def get_continuous_state_derivatives(self) -> List[float]: + self.derh = self.v + self.derv = self.g + + def get_event_indicators(self) -> List[float]: + z = [self.h] + if self.h > -EVENT_EPS and self.h <=0 and self.v > 0: + z[0] = -EVENT_EPS + + return [self.h - 0.0] + + def update_discrete_states(self): + discrete_states_need_update = False + terminate_simulation = False + nominals_of_continuous_states_changed = False + values_of_continuous_states_changed = False + next_event_time_defined = False + next_event_time = 0.0 + + if self.h <= 0 and self.v < 0: + self.v = -self.e * self.v + if abs(self.v) < self.v_min: + self.v = 0.0 + terminate_simulation = True + discrete_states_need_update = True + + return Fmi3Status.ok, discrete_states_need_update, terminate_simulation diff --git a/pythonfmu3/fmi3slave.py b/pythonfmu3/fmi3slave.py index eb22919..ee180f4 100644 --- a/pythonfmu3/fmi3slave.py +++ b/pythonfmu3/fmi3slave.py @@ -50,6 +50,7 @@ class Fmi3SlaveBase(object): def __init__(self, **kwargs): self.vars = OrderedDict() + self.event_indicators: List[int] = [] self.instance_name = kwargs["instance_name"] self.resources = kwargs.get("resources", None) self.visible = kwargs.get("visible", False) @@ -184,7 +185,8 @@ def to_xml(self, model_options: Dict[str, str] = dict()) -> Element: for v in initial_unknown: SubElement(structure, "InitialUnknown", attrib=dict(valueReference=str(v.value_reference))) - + for v in self.event_indicators: + SubElement(structure, "EventIndicator", attrib=dict(valueReference=str(v))) return root @@ -206,7 +208,7 @@ def __apply_start_value(self, var: ModelVariable): raise Exception(f"Unsupported type {type(var)}!") var.start = refs if len(getattr(var, "dimensions", [])) > 0 else refs[0] - def register_variable(self, var: ModelVariable, nested: bool = True, var_type: Any = None): + def register_variable(self, var: ModelVariable, nested: bool = True, var_type: Any = None, has_event_indicator: bool = False): """Register a variable as FMU interface. Args: @@ -238,6 +240,12 @@ def register_variable(self, var: ModelVariable, nested: bool = True, var_type: A if var_type: self.type_definitions[var_type.name] = var_type var.declared_type = var_type.name + + if has_event_indicator: + self.register_event_indicator(var.value_reference) + + def register_event_indicator(self, vr): + self.event_indicators.append(vr) def setup_experiment(self, start_time: float): pass @@ -416,6 +424,9 @@ def _set_fmu_state(self, state: Dict[str, Any]): if v.setter is not None: v.setter(value) + def get_number_of_event_indicators(self) -> int: + return len(self.event_indicators) + def set_continuous_states(self, values: List[float]): offset = 0 continuous_state_derivatives = list( diff --git a/pythonfmu3/modelexchange.py b/pythonfmu3/modelexchange.py index 16ddbec..85bb809 100644 --- a/pythonfmu3/modelexchange.py +++ b/pythonfmu3/modelexchange.py @@ -20,4 +20,8 @@ class ModelExchange(ABC, metaclass=RequireTimeMeta): @abstractmethod def get_continuous_state_derivatives(self): """Return the continuous state derivatives of the model.""" - pass \ No newline at end of file + pass + + def get_event_indicators(self): + """Return the event indicators of the model.""" + return [] \ No newline at end of file diff --git a/pythonfmu3/pythonfmu-export/src/cppfmu/cppfmu_cs.hpp b/pythonfmu3/pythonfmu-export/src/cppfmu/cppfmu_cs.hpp index 5297d26..6b83fb4 100644 --- a/pythonfmu3/pythonfmu-export/src/cppfmu/cppfmu_cs.hpp +++ b/pythonfmu3/pythonfmu-export/src/cppfmu/cppfmu_cs.hpp @@ -141,6 +141,7 @@ class SlaveInstance virtual void SetTime(cppfmu::FMIFloat64 time) = 0; virtual void GetNumberOfContinuousStates(std::size_t& nStates) const = 0; virtual void GetNumberOfEventIndicators(std::size_t& nEventIndicators) const = 0; + virtual void GetEventIndicators(cppfmu::FMIFloat64* eventIndicators, std::size_t nEventIndicators) const = 0; virtual void UpdateDiscreteStates( cppfmu::FMIBoolean* discreteStatesNeedUpdate, cppfmu::FMIBoolean* terminateSimulation, diff --git a/pythonfmu3/pythonfmu-export/src/cppfmu/fmi_functions.cpp b/pythonfmu3/pythonfmu-export/src/cppfmu/fmi_functions.cpp index 0e0a6eb..9501db3 100644 --- a/pythonfmu3/pythonfmu-export/src/cppfmu/fmi_functions.cpp +++ b/pythonfmu3/pythonfmu-export/src/cppfmu/fmi_functions.cpp @@ -622,7 +622,17 @@ fmi3Status fmi3GetEventIndicators( fmi3Float64 eventIndicators[], size_t nEventIndicators) { - return fmi3OK; + const auto component = reinterpret_cast(c); + try { + component->slave->GetEventIndicators(eventIndicators, nEventIndicators); + return fmi3OK; + } catch (const cppfmu::FatalError& e) { + component->logger.Log(fmi3Fatal, "", e.what()); + return fmi3Fatal; + } catch (const std::exception& e) { + component->logger.Log(fmi3Error, "", e.what()); + return fmi3Error; + } } fmi3Status fmi3GetNominalsOfContinuousStates( diff --git a/pythonfmu3/pythonfmu-export/src/pythonfmu/PySlaveInstance.cpp b/pythonfmu3/pythonfmu-export/src/pythonfmu/PySlaveInstance.cpp index 077f0d7..3206368 100644 --- a/pythonfmu3/pythonfmu-export/src/pythonfmu/PySlaveInstance.cpp +++ b/pythonfmu3/pythonfmu-export/src/pythonfmu/PySlaveInstance.cpp @@ -602,9 +602,34 @@ void PySlaveInstance::GetNumberOfContinuousStates(std::size_t& nStates) const void PySlaveInstance::GetNumberOfEventIndicators(std::size_t& nIndicators) const { - nIndicators= 0u; + py_safe_run([this, &nIndicators](PyGILState_STATE gilState) { + auto f = PyObject_CallMethod(pInstance_, "get_number_of_event_indicators", nullptr); + if (f == nullptr) { + handle_py_exception("[getNumberOfEventIndicators] PyObject_CallMethod", gilState); + } + nIndicators = static_cast(PyLong_AsLong(f)); + Py_DECREF(f); + clearLogBuffer(); + }); +} + +void PySlaveInstance::GetEventIndicators(cppfmu::FMIFloat64* eventIndicators, std::size_t nIndicators) const +{ + py_safe_run([this, &eventIndicators, nIndicators](PyGILState_STATE gilState) { + auto f = PyObject_CallMethod(pInstance_, "get_event_indicators", nullptr); + if (f == nullptr) { + handle_py_exception("[getEventIndicators] PyObject_CallMethod", gilState); + } + // Assuming f is a list of floats + for (std::size_t i = 0; i < nIndicators; i++) { + PyObject* item = PyList_GetItem(f, i); + eventIndicators[i] = static_cast(PyFloat_AsDouble(item)); + } + clearLogBuffer(); + }); } + void PySlaveInstance::SetTime(cppfmu::FMIFloat64 time) { py_safe_run([this, time](PyGILState_STATE gilState) { From feb56cda2b637261543cf6059747ce5fea27dd22 Mon Sep 17 00:00:00 2001 From: Stephen Smith Date: Mon, 28 Jul 2025 11:08:49 +0100 Subject: [PATCH 2/2] import model exchange implementation --- examples/bouncingBall_me.py | 30 ++++----- pythonfmu3/__init__.py | 2 +- pythonfmu3/modelexchange.py | 23 ++++++- .../src/pythonfmu/PySlaveInstance.cpp | 65 +++++++++++++++---- .../src/pythonfmu/PySlaveInstance.hpp | 1 + 5 files changed, 91 insertions(+), 30 deletions(-) diff --git a/examples/bouncingBall_me.py b/examples/bouncingBall_me.py index e8083aa..e98e7c7 100644 --- a/examples/bouncingBall_me.py +++ b/examples/bouncingBall_me.py @@ -1,7 +1,8 @@ -from pythonfmu3 import Fmi3Causality, Fmi3Variability, Fmi3SlaveBase, ModelExchange, Fmi3Status, Float64, Fmi3Initial, Unit, Float64Type, Fmi3StepResult +from pythonfmu3 import Fmi3Causality, Fmi3Variability, Fmi3SlaveBase, ModelExchange, Fmi3Status, Float64, Fmi3Initial, Unit, Fmi3UpdateDiscreteStatesResult from typing import List +import sys EVENT_EPS = 1e-12 @@ -49,27 +50,26 @@ def __init__(self, **kwargs): def get_continuous_state_derivatives(self) -> List[float]: self.derh = self.v self.derv = self.g + return [self.derh, self.derv] def get_event_indicators(self) -> List[float]: z = [self.h] if self.h > -EVENT_EPS and self.h <=0 and self.v > 0: z[0] = -EVENT_EPS - return [self.h - 0.0] + return z def update_discrete_states(self): - discrete_states_need_update = False - terminate_simulation = False - nominals_of_continuous_states_changed = False - values_of_continuous_states_changed = False - next_event_time_defined = False - next_event_time = 0.0 + fdsr = Fmi3UpdateDiscreteStatesResult() if self.h <= 0 and self.v < 0: - self.v = -self.e * self.v - if abs(self.v) < self.v_min: - self.v = 0.0 - terminate_simulation = True - discrete_states_need_update = True - - return Fmi3Status.ok, discrete_states_need_update, terminate_simulation + self.h = sys.float_info.min + self.v = -self.v * self.e + + if self.v < self.v_min: + self.v = 0.0; + self.g = 0.0; + + fdsr.valuesOfContinuousStatesChanged = True + + return fdsr diff --git a/pythonfmu3/__init__.py b/pythonfmu3/__init__.py index 6ca2b51..d5e7048 100644 --- a/pythonfmu3/__init__.py +++ b/pythonfmu3/__init__.py @@ -1,7 +1,7 @@ from ._version import __version__ from .builder import FmuBuilder from .cosimulation import CoSimulation -from .modelexchange import ModelExchange +from .modelexchange import ModelExchange, Fmi3UpdateDiscreteStatesResult from .enums import Fmi3Causality, Fmi3Initial, Fmi3Status, Fmi3Variability from .fmi3slave import Fmi3Slave, Fmi3SlaveBase, Fmi3StepResult from .variables import Boolean, Enumeration, Int32, Int64, UInt64, Float64, String, Dimension diff --git a/pythonfmu3/modelexchange.py b/pythonfmu3/modelexchange.py index 85bb809..03eb987 100644 --- a/pythonfmu3/modelexchange.py +++ b/pythonfmu3/modelexchange.py @@ -1,4 +1,14 @@ from abc import ABC, ABCMeta, abstractmethod +from dataclasses import dataclass + +@dataclass +class Fmi3UpdateDiscreteStatesResult: + discreteStateNeedsUpdate: bool = False + terminateSimulation: bool = False + nominalsOfContinuousStatesChanged: bool = False + valuesOfContinuousStatesChanged: bool = False + nextEventTimeDefined: bool = False + nextEventTime: float = 0.0 class RequireTimeMeta(ABCMeta): def __init__(cls, name, bases, namespace): @@ -15,6 +25,13 @@ def new_init(self, *args, **kwargs): class ModelExchange(ABC, metaclass=RequireTimeMeta): """ Classes derived from ModelExchange must define a 'self.time' member variable. + + Required methods to override: + - `get_continuous_state_derivatives`: Must return a list of continuous state derivatives. + + Optional methods: + - `get_event_indicators`: Should return a list of event indicators. + - `update_discrete_states`: Signify converged solution at current super-dense time instant. """ @abstractmethod @@ -24,4 +41,8 @@ def get_continuous_state_derivatives(self): def get_event_indicators(self): """Return the event indicators of the model.""" - return [] \ No newline at end of file + return [] + + def update_discrete_states(self): + """Update the discrete states of the model.""" + return Fmi3UpdateDiscreteStatesResult() \ No newline at end of file diff --git a/pythonfmu3/pythonfmu-export/src/pythonfmu/PySlaveInstance.cpp b/pythonfmu3/pythonfmu-export/src/pythonfmu/PySlaveInstance.cpp index 3206368..0ec897f 100644 --- a/pythonfmu3/pythonfmu-export/src/pythonfmu/PySlaveInstance.cpp +++ b/pythonfmu3/pythonfmu-export/src/pythonfmu/PySlaveInstance.cpp @@ -542,6 +542,7 @@ void PySlaveInstance::GetContinuousStates(cppfmu::FMIFloat64* continuousStates, PyObject* item = PyList_GetItem(f, i); continuousStates[i] = static_cast(PyFloat_AsDouble(item)); } + Py_DECREF(f); clearLogBuffer(); }); } @@ -551,6 +552,7 @@ void PySlaveInstance::GetContinuousStateDerivatives(cppfmu::FMIFloat64* continuo py_safe_run([this, &continuousStateDerivatives, nStates](PyGILState_STATE gilState) { auto f = PyObject_CallMethod(pInstance_, "get_continuous_state_derivatives", nullptr); if (f == nullptr) { + PyErr_Print(); handle_py_exception("[get_continuous_state_derivatives] PyObject_CallMethod", gilState); } // Assuming f is a list of floats @@ -558,6 +560,7 @@ void PySlaveInstance::GetContinuousStateDerivatives(cppfmu::FMIFloat64* continuo PyObject* item = PyList_GetItem(f, i); continuousStateDerivatives[i] = static_cast(PyFloat_AsDouble(item)); } + Py_DECREF(f); clearLogBuffer(); }); } @@ -603,13 +606,13 @@ void PySlaveInstance::GetNumberOfContinuousStates(std::size_t& nStates) const void PySlaveInstance::GetNumberOfEventIndicators(std::size_t& nIndicators) const { py_safe_run([this, &nIndicators](PyGILState_STATE gilState) { - auto f = PyObject_CallMethod(pInstance_, "get_number_of_event_indicators", nullptr); - if (f == nullptr) { - handle_py_exception("[getNumberOfEventIndicators] PyObject_CallMethod", gilState); - } - nIndicators = static_cast(PyLong_AsLong(f)); - Py_DECREF(f); - clearLogBuffer(); + auto f = PyObject_CallMethod(pInstance_, "get_number_of_event_indicators", nullptr); + if (f == nullptr) { + handle_py_exception("[getNumberOfEventIndicators] PyObject_CallMethod", gilState); + } + nIndicators = static_cast(PyLong_AsLong(f)); + Py_DECREF(f); + clearLogBuffer(); }); } @@ -625,8 +628,10 @@ void PySlaveInstance::GetEventIndicators(cppfmu::FMIFloat64* eventIndicators, st PyObject* item = PyList_GetItem(f, i); eventIndicators[i] = static_cast(PyFloat_AsDouble(item)); } + Py_DECREF(f); clearLogBuffer(); }); + } @@ -650,12 +655,46 @@ void PySlaveInstance::UpdateDiscreteStates( cppfmu::FMIBoolean* nextEventTimeDefined, cppfmu::FMIFloat64* nextEventTime) { - *discreteStatesNeedUpdate = false; - *terminateSimulation = false; - *nominalContinuousStatesChanged = false; - *valuesOfContinuousStatesChanged = false; - *nextEventTimeDefined = false; - *nextEventTime = 0.0; + py_safe_run([this, discreteStatesNeedUpdate, terminateSimulation, nominalContinuousStatesChanged, valuesOfContinuousStatesChanged, nextEventTimeDefined, nextEventTime](PyGILState_STATE gilState) { + auto f = PyObject_CallMethod(pInstance_, "update_discrete_states", nullptr); + if (f == nullptr) { + handle_py_exception("[updateDiscreteStates] PyObject_CallMethod", gilState); + } + + if (PyObject_HasAttrString(f, "discreteStatesNeedUpdate")) { + PyObject * pyDiscreteStatesNeedUpdate = PyObject_GetAttrString(f, "discreteStatesNeedUpdate"); + *discreteStatesNeedUpdate = static_cast(PyObject_IsTrue(pyDiscreteStatesNeedUpdate)); + Py_DECREF(pyDiscreteStatesNeedUpdate); + } + if (PyObject_HasAttrString(f, "terminateSimulation")) { + PyObject* pyTerminateSimulation = PyObject_GetAttrString(f, "terminateSimulation"); + *terminateSimulation = static_cast(PyObject_IsTrue(pyTerminateSimulation)); + Py_DECREF(pyTerminateSimulation); + } + if (PyObject_HasAttrString(f, "nominalContinuousStatesChanged")) { + PyObject* pyNominalContinuousStatesChanged = PyObject_GetAttrString(f, "nominalContinuousStatesChanged"); + *nominalContinuousStatesChanged = static_cast(PyObject_IsTrue(pyNominalContinuousStatesChanged)); + Py_DECREF(pyNominalContinuousStatesChanged); + } + if (PyObject_HasAttrString(f, "valuesOfContinuousStatesChanged")) { + PyObject* pyValuesOfContinuousStatesChanged = PyObject_GetAttrString(f, "valuesOfContinuousStatesChanged"); + *valuesOfContinuousStatesChanged = static_cast(PyObject_IsTrue(pyValuesOfContinuousStatesChanged)); + Py_DECREF(pyValuesOfContinuousStatesChanged); + } + if (PyObject_HasAttrString(f, "nextEventTimeDefined")) { + PyObject* pyNextEventTimeDefined = PyObject_GetAttrString(f, "nextEventTimeDefined"); + *nextEventTimeDefined = static_cast(PyObject_IsTrue(pyNextEventTimeDefined)); + Py_DECREF(pyNextEventTimeDefined); + } + if (nextEventTimeDefined && *nextEventTimeDefined) { + PyObject* pyNextEventTime = PyObject_GetAttrString(f, "nextEventTime"); + *nextEventTime = PyFloat_AsDouble(pyNextEventTime); + Py_DECREF(pyNextEventTime); + } + + Py_DECREF(f); + clearLogBuffer(); + }); } void PySlaveInstance::GetFMUstate(fmi3FMUState& state) diff --git a/pythonfmu3/pythonfmu-export/src/pythonfmu/PySlaveInstance.hpp b/pythonfmu3/pythonfmu-export/src/pythonfmu/PySlaveInstance.hpp index c6f5162..8f7c204 100644 --- a/pythonfmu3/pythonfmu-export/src/pythonfmu/PySlaveInstance.hpp +++ b/pythonfmu3/pythonfmu-export/src/pythonfmu/PySlaveInstance.hpp @@ -57,6 +57,7 @@ class PySlaveInstance : public cppfmu::SlaveInstance void GetNumberOfEventIndicators(std::size_t& nEventIndicators) const override; void GetContinuousStateDerivatives(cppfmu::FMIFloat64* derivatives, std::size_t nStates) const override; void GetNominalsOfContinuousStates(cppfmu::FMIFloat64* nominalContinuousStates, std::size_t nStates) const override; + void GetEventIndicators(cppfmu::FMIFloat64* eventIndicators, std::size_t nEventIndicators) const override; void SetContinuousStates(const cppfmu::FMIFloat64* continuousStates, std::size_t nStates) override; void SetTime(cppfmu::FMIFloat64 time) override; void UpdateDiscreteStates(