From acb7c165e600985800f495ce82d5aac4cfa2204f Mon Sep 17 00:00:00 2001 From: Robin Andersson Date: Wed, 19 Feb 2025 11:04:27 -0500 Subject: [PATCH] Added support for initializing FMI3 ME FMUs --- src/pyfmi/fmi2.pxd | 10 +-- src/pyfmi/fmi2.pyx | 13 ++- src/pyfmi/fmi3.pxd | 6 +- src/pyfmi/fmi3.pyx | 166 ++++++++++++++++++++++++++++++++----- src/pyfmi/fmil3_import.pxd | 29 +++++-- tests/test_fmi3.py | 15 ++++ 6 files changed, 198 insertions(+), 41 deletions(-) diff --git a/src/pyfmi/fmi2.pxd b/src/pyfmi/fmi2.pxd index 3771e2ca..8433a96a 100644 --- a/src/pyfmi/fmi2.pxd +++ b/src/pyfmi/fmi2.pxd @@ -170,12 +170,12 @@ cdef class WorkerClass2: cpdef verify_dimensions(self, int dim) cdef object _load_fmi2_fmu( - fmu, - object log_file_name, - str kind, - int log_level, + fmu, + object log_file_name, + str kind, + int log_level, int allow_unzipped_fmu, - FMIL.fmi_import_context_t* context, + FMIL.fmi_import_context_t* context, bytes fmu_temp_dir, FMIL.jm_callbacks callbacks, list log_data diff --git a/src/pyfmi/fmi2.pyx b/src/pyfmi/fmi2.pyx index 280dce14..f5b31b96 100644 --- a/src/pyfmi/fmi2.pyx +++ b/src/pyfmi/fmi2.pyx @@ -40,7 +40,7 @@ from pyfmi.fmi_base import ( from pyfmi.exceptions import ( FMUException, InvalidBinaryException, - InvalidXMLException, + InvalidXMLException, InvalidVersionException ) from pyfmi.common.core import create_temp_dir @@ -1185,7 +1185,6 @@ cdef class FMUModelBase2(FMI_BASE.ModelBase): self._t = start_time self._last_accepted_time = start_time - self._relative_tolerance = tolerance self._log_handler.capi_start_callback(self._max_log_size_msg_sent, self._current_log_size) status = FMIL2.fmi2_import_setup_experiment(self._fmu, @@ -5172,12 +5171,12 @@ cdef class WorkerClass2: return ret cdef object _load_fmi2_fmu( - fmu, - object log_file_name, - str kind, - int log_level, + fmu, + object log_file_name, + str kind, + int log_level, int allow_unzipped_fmu, - FMIL.fmi_import_context_t* context, + FMIL.fmi_import_context_t* context, bytes fmu_temp_dir, FMIL.jm_callbacks callbacks, list log_data diff --git a/src/pyfmi/fmi3.pxd b/src/pyfmi/fmi3.pxd index 83d866a0..7f895422 100644 --- a/src/pyfmi/fmi3.pxd +++ b/src/pyfmi/fmi3.pxd @@ -31,11 +31,13 @@ cdef class FMUModelBase3(FMI_BASE.ModelBase): cdef FMIL.size_t _nContinuousStates # Internal values - cdef object _fmu_full_path + cdef public float _last_accepted_time cdef public object _enable_logging + cdef object _fmu_full_path + cdef object _modelName + cdef object _t cdef int _allow_unzipped_fmu cdef int _allocated_context, _allocated_dll, _allocated_fmu, _allocated_xml - cdef object _modelName cdef char* _fmu_temp_dir cdef class FMUModelME3(FMUModelBase3): diff --git a/src/pyfmi/fmi3.pyx b/src/pyfmi/fmi3.pyx index 1e3fe795..4a1dfa7a 100644 --- a/src/pyfmi/fmi3.pyx +++ b/src/pyfmi/fmi3.pyx @@ -95,9 +95,13 @@ cdef class FMUModelBase3(FMI_BASE.ModelBase): self._allocated_xml = 0 self._fmu_temp_dir = NULL self._fmu_log_name = NULL + # Used to adjust behavior if FMU is unzipped self._allow_unzipped_fmu = 1 if allow_unzipped_fmu else 0 + # Default values + self._t = None + self._last_accepted_time = 0.0 # Internal values self._enable_logging = False @@ -201,14 +205,17 @@ cdef class FMUModelBase3(FMI_BASE.ModelBase): self._modelName = pyfmi_util.decode(FMIL3.fmi3_import_get_model_name(self._fmu)) - # TODO Check status and error handling? self._log_handler.capi_start_callback(self._max_log_size_msg_sent, self._current_log_size) status = FMIL3.fmi3_import_get_number_of_event_indicators(self._fmu, &self._nEventIndicators) self._log_handler.capi_end_callback(self._max_log_size_msg_sent, self._current_log_size) + if status != FMIL3.fmi3_status_ok: + raise InvalidFMUException("The FMU could not be instantiated, error retrieving number of event indicators.") self._log_handler.capi_start_callback(self._max_log_size_msg_sent, self._current_log_size) status = FMIL3.fmi3_import_get_number_of_continuous_states(self._fmu, &self._nContinuousStates) self._log_handler.capi_end_callback(self._max_log_size_msg_sent, self._current_log_size) + if status != FMIL3.fmi3_status_ok: + raise InvalidFMUException("The FMU could not be instantiated, error retrieving number of continuous states.") # TODO: The code below is identical between FMUModelBase2 and FMUModelBase3, perhaps we can refactor this if not isinstance(log_file_name, str): @@ -259,6 +266,10 @@ cdef class FMUModelBase3(FMI_BASE.ModelBase): if self._log_stream: self._log_stream = None + def reset(self): + """ TODO """ + self._t = None + def _get_fmu_kind(self): raise FMUException("FMUModelBase3 cannot be used directly, use FMUModelME3.") @@ -267,6 +278,15 @@ cdef class FMUModelBase3(FMI_BASE.ModelBase): raise NotImplementedError + def initialize(self, + tolerance_defined=True, + tolerance="Default", + start_time="Default", + stop_time_defined=False, + stop_time="Default" + ): + raise NotImplementedError + def get_fmil_log_level(self): """ Returns:: @@ -280,18 +300,7 @@ cdef class FMUModelBase3(FMI_BASE.ModelBase): raise FMUException('Logging is not enabled') def get_version(self): - """ - Returns the FMI version of the Model which it was generated according. - - Returns:: - - version -- - The version. - - Example:: - - model.get_version() - """ + """ Returns the FMI version of the Model which it was generated according. """ self._log_handler.capi_start_callback(self._max_log_size_msg_sent, self._current_log_size) cdef FMIL3.fmi3_string_t version = FMIL3.fmi3_import_get_version(self._fmu) self._log_handler.capi_end_callback(self._max_log_size_msg_sent, self._current_log_size) @@ -303,6 +312,17 @@ cdef class FMUModelBase3(FMI_BASE.ModelBase): """ return self._modelId + def get_default_experiment_start_time(self): + """ Returns the default experiment start time as defined the XML description. """ + return FMIL3.fmi3_import_get_default_experiment_start(self._fmu) + + def get_default_experiment_stop_time(self): + """ Returns the default experiment stop time as defined the XML description. """ + return FMIL3.fmi3_import_get_default_experiment_stop(self._fmu) + + def get_default_experiment_tolerance(self): + """ Returns the default experiment tolerance as defined in the XML description. """ + return FMIL3.fmi3_import_get_default_experiment_tolerance(self._fmu) cdef class FMUModelME3(FMUModelBase3): """ @@ -380,11 +400,6 @@ cdef class FMUModelME3(FMUModelBase3): log = self._enable_logging vis = visible - #if isinstance(self, FMUModelME3): - # - #else: - # raise FMUException('The instance is not curent an instance of an ME-model or a CS-model. Use load_fmu for correct loading.') - name_as_bytes = pyfmi_util.encode(name) self._log_handler.capi_start_callback(self._max_log_size_msg_sent, self._current_log_size) status = FMIL3.fmi3_import_instantiate_model_exchange( @@ -392,9 +407,7 @@ cdef class FMUModelME3(FMUModelBase3): name_as_bytes, NULL, vis, - log, - NULL, - FMIL3.fmi3_log_forwarding + log ) self._log_handler.capi_end_callback(self._max_log_size_msg_sent, self._current_log_size) @@ -403,6 +416,117 @@ cdef class FMUModelME3(FMUModelBase3): self._allocated_fmu = 1 + + def initialize(self, + tolerance_defined=True, + tolerance="Default", + start_time="Default", + stop_time_defined=False, + stop_time="Default" + ): + """ TODO """ + log_open = self._log_open() + if not log_open and self.get_log_level() > 2: + self._open_log_file() + + try: + self.enter_initialization_mode( + tolerance_defined, + tolerance, + start_time, + stop_time_defined, + stop_time + ) + self.exit_initialization_mode() + except Exception: + if not log_open and self.get_log_level() > 2: + self._close_log_file() + + raise + + if not log_open and self.get_log_level() > 2: + self._close_log_file() + + def enter_initialization_mode(self, + tolerance_defined=True, + tolerance="Default", + start_time="Default", + stop_time_defined=False, + stop_time="Default" + ): + """ + fmi3_import_enter_initialization_mode( + fmi3_import_t* fmu, + fmi3_boolean_t toleranceDefined, + fmi3_float64_t tolerance, + fmi3_float64_t startTime, + fmi3_boolean_t stopTimeDefined, + fmi3_float64_t stopTime); + """ + cdef FMIL3.fmi3_status_t status + + cdef FMIL3.fmi3_boolean_t stop_defined = FMIL3.fmi3_true if stop_time_defined else FMIL3.fmi3_false + cdef FMIL3.fmi3_boolean_t tol_defined = FMIL3.fmi3_true if tolerance_defined else FMIL3.fmi3_false + + if tolerance == "Default": + tolerance = self.get_default_experiment_tolerance() + if start_time == "Default": + start_time = self.get_default_experiment_start_time() + if stop_time == "Default": + stop_time = self.get_default_experiment_stop_time() + + self._t = start_time + self._last_accepted_time = start_time + self._log_handler.capi_start_callback(self._max_log_size_msg_sent, self._current_log_size) + status = FMIL3.fmi3_import_enter_initialization_mode( + self._fmu, + tolerance_defined, + tolerance, + start_time, + stop_time_defined, + stop_time + ) + self._log_handler.capi_end_callback(self._max_log_size_msg_sent, self._current_log_size) + + if status != FMIL3.fmi3_status_ok: + raise FMUException("Failed to enter initialization mode") + + def exit_initialization_mode(self): + """ + fmi3_import_exit_initialization_mode(fmi3_import_t* fmu); + """ + cdef FMIL3.fmi3_status_t status + self._log_handler.capi_start_callback(self._max_log_size_msg_sent, self._current_log_size) + status = FMIL3.fmi3_import_exit_initialization_mode(self._fmu) + self._log_handler.capi_end_callback(self._max_log_size_msg_sent, self._current_log_size) + + if status != FMIL3.fmi3_status_ok: + raise FMUException("Failed to exit initialization mode") + + def enter_continuous_time_mode(self): + """ + fmi3_import_enter_continuous_time_mode(fmi3_import_t* fmu); + """ + cdef FMIL3.fmi3_status_t status + self._log_handler.capi_start_callback(self._max_log_size_msg_sent, self._current_log_size) + status = FMIL3.fmi3_import_enter_continuous_time_mode(self._fmu) + self._log_handler.capi_end_callback(self._max_log_size_msg_sent, self._current_log_size) + + if status != FMIL3.fmi3_status_ok: + raise FMUException("Failed to enter continuous time mode") + + def enter_event_mode(self): + """ + fmi3_import_enter_event_mode(fmi3_import_t* fmu); + """ + cdef FMIL3.fmi3_status_t status + self._log_handler.capi_start_callback(self._max_log_size_msg_sent, self._current_log_size) + status = FMIL3.fmi3_import_enter_event_mode(self._fmu) + self._log_handler.capi_end_callback(self._max_log_size_msg_sent, self._current_log_size) + + if status != FMIL3.fmi3_status_ok: + raise FMUException("Failed to enter event mode") + cdef void _cleanup_on_load_error( FMIL3.fmi3_import_t* fmu_3, FMIL.fmi_import_context_t* context, diff --git a/src/pyfmi/fmil3_import.pxd b/src/pyfmi/fmil3_import.pxd index f0bdf014..ab420ce0 100644 --- a/src/pyfmi/fmil3_import.pxd +++ b/src/pyfmi/fmil3_import.pxd @@ -28,7 +28,13 @@ cdef extern from 'fmilib.h': # FMI VARIABLE TYPE DEFINITIONS ctypedef void* fmi3_instance_environment_t ctypedef char* fmi3_string_t - ctypedef bool fmi3_boolean_t + ctypedef bool fmi3_boolean_t + ctypedef double fmi3_float64_t + + # STRUCTS + ctypedef enum fmi3_boolean_enu_t: + fmi3_true = 1 + fmi3_false = 0 # STATUS cdef enum fmi3_fmu_kind_enu_t: @@ -73,19 +79,30 @@ cdef extern from 'fmilib.h': fmi3_string_t instanceName, fmi3_string_t resourcePath, fmi3_boolean_t visible, - fmi3_boolean_t loggingOn, - fmi3_instance_environment_t instanceEnvironment, - fmi3_log_message_callback_ft logMessage + fmi3_boolean_t loggingOn ) # modes - + fmi3_status_t fmi3_import_enter_initialization_mode( + fmi3_import_t* fmu, + fmi3_boolean_t toleranceDefined, + fmi3_float64_t tolerance, + fmi3_float64_t startTime, + fmi3_boolean_t stopTimeDefined, + fmi3_float64_t stopTime) + fmi3_status_t fmi3_import_exit_initialization_mode(fmi3_import_t* fmu) + fmi3_status_t fmi3_import_enter_event_mode(fmi3_import_t* fmu) + fmi3_status_t fmi3_import_enter_continuous_time_mode(fmi3_import_t* fmu) + fmi3_status_t fmi3_import_enter_event_mode(fmi3_import_t* fmu) # misc char* fmi3_import_get_version(fmi3_import_t*) # setting + fmi3_status_t fmi3_import_set_time(fmi3_import_t *, fmi3_float64_t) # getting - + double fmi3_import_get_default_experiment_start(fmi3_import_t*); + double fmi3_import_get_default_experiment_stop(fmi3_import_t*); + double fmi3_import_get_default_experiment_tolerance(fmi3_import_t*); # save states # FMI HELPER METHODS (3.0) diff --git a/tests/test_fmi3.py b/tests/test_fmi3.py index 5f94ff22..68ac8292 100644 --- a/tests/test_fmi3.py +++ b/tests/test_fmi3.py @@ -48,6 +48,7 @@ def temp_dir_context(tmpdir): class TestFMI3LoadFMU: """Basic unit tests for FMI3 loading via 'load_fmu'.""" + @pytest.mark.parametrize("ref_fmu", [ FMI3_REF_FMU_PATH / "BouncingBall.fmu", FMI3_REF_FMU_PATH / "Dahlquist.fmu", @@ -119,6 +120,20 @@ def test_instantiation(self, tmpdir): log_file = ''.join(contents) assert found_substring, f"Unable to locate substring '{substring_to_find}' in file with contents '{log_file}'" + @pytest.mark.parametrize("ref_fmu", [ + FMI3_REF_FMU_PATH / "BouncingBall.fmu", + FMI3_REF_FMU_PATH / "Dahlquist.fmu", + FMI3_REF_FMU_PATH / "Resource.fmu", + FMI3_REF_FMU_PATH / "StateSpace.fmu", + FMI3_REF_FMU_PATH / "Feedthrough.fmu", + FMI3_REF_FMU_PATH / "Stair.fmu", + FMI3_REF_FMU_PATH / "VanDerPol.fmu", + ]) + def test_load_kind_auto(self, ref_fmu): + """Test initialize all the ME reference FMUs. """ + fmu = load_fmu(ref_fmu) + fmu.initialize() # Should simply pass without any exceptions + class Test_FMI3ME: """Basic unit tests for FMI3 import directly via the FMUModelME3 class.""" @pytest.mark.parametrize("ref_fmu", [FMI3_REF_FMU_PATH / "VanDerPol.fmu"])