Skip to content

Commit

Permalink
Added support for initializing FMI3 ME FMUs
Browse files Browse the repository at this point in the history
  • Loading branch information
modelonrobinandersson committed Feb 19, 2025
1 parent 8a46013 commit 732ba1f
Show file tree
Hide file tree
Showing 6 changed files with 198 additions and 41 deletions.
10 changes: 5 additions & 5 deletions src/pyfmi/fmi2.pxd
Original file line number Diff line number Diff line change
Expand Up @@ -169,12 +169,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
Expand Down
13 changes: 6 additions & 7 deletions src/pyfmi/fmi2.pyx
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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
Expand Down
6 changes: 4 additions & 2 deletions src/pyfmi/fmi3.pxd
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down
166 changes: 145 additions & 21 deletions src/pyfmi/fmi3.pyx
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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):
Expand Down Expand Up @@ -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.")

Expand All @@ -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::
Expand All @@ -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_string_t>FMIL3.fmi3_import_get_version(self._fmu)
self._log_handler.capi_end_callback(self._max_log_size_msg_sent, self._current_log_size)
Expand All @@ -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):
"""
Expand Down Expand Up @@ -380,21 +400,14 @@ 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(
self._fmu,
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)

Expand All @@ -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,
Expand Down
29 changes: 23 additions & 6 deletions src/pyfmi/fmil3_import.pxd
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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)
Expand Down
15 changes: 15 additions & 0 deletions tests/test_fmi3.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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"])
Expand Down

0 comments on commit 732ba1f

Please sign in to comment.