Skip to content

Commit 72d88f5

Browse files
feat: basic FMI3 CS doStep implementation (#415)
1 parent d9d51fb commit 72d88f5

9 files changed

Lines changed: 427 additions & 84 deletions

File tree

src/pyfmi/fmi2.pyx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3700,8 +3700,8 @@ cdef class FMUModelCS2(FMUModelBase2):
37003700
37013701
status --
37023702
The status of function which can be checked against
3703-
FMI_OK, FMI_WARNING. FMI_DISCARD, FMI_ERROR,
3704-
FMI_FATAL,FMI_PENDING...
3703+
FMI_OK, FMI_WARNING, FMI_DISCARD, FMI_ERROR,
3704+
FMI_FATAL, FMI_PENDING.
37053705
37063706
Calls the underlying low-level function fmi2DoStep.
37073707
"""

src/pyfmi/fmi3.pxd

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -183,9 +183,13 @@ cdef class FMUModelME3(FMUModelBase3):
183183
cdef FMIL3.fmi3_status_t _get_nominal_continuous_states_fmil(self, FMIL3.fmi3_float64_t* xnominal, size_t nx)
184184

185185
cdef class FMUModelCS3(FMUModelBase3):
186+
cdef public bool do_step_terminated
187+
cdef FMIL3.fmi3_boolean_t _instantiated_with_early_return
186188
cpdef _get_time(self)
187189
cpdef _set_time(self, FMIL3.fmi3_float64_t t)
188190

191+
cpdef FMIL3.fmi3_status_t do_step(self, FMIL3.fmi3_float64_t current_t, FMIL3.fmi3_float64_t step_size, new_step=*)
192+
189193
cdef class _WorkerClass3:
190194
cdef int _dim
191195

src/pyfmi/fmi3.pyx

Lines changed: 187 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3799,12 +3799,18 @@ cdef class FMUModelCS3(FMUModelBase3):
37993799
FMUModelBase3.__init__(self, fmu, log_file_name, log_level,
38003800
_unzipped_dir, _connect_dll, allow_unzipped_fmu)
38013801

3802+
self.do_step_terminated = False
3803+
38023804
if self.get_capability_flags().get('needsExecutionTool', False):
38033805
raise FMUException("The FMU specifies 'needsExecutionTool=true' which implies that it requires an external execution tool to simulate, this is not supported.")
38043806

38053807
if _connect_dll:
38063808
self.instantiate()
38073809

3810+
def reset(self):
3811+
FMUModelBase3.reset(self)
3812+
self.do_step_terminated = False
3813+
38083814
def _get_fmu_kind(self):
38093815
if self._fmu_kind & FMIL3.fmi3_fmu_kind_cs:
38103816
return FMIL3.fmi3_fmu_kind_cs
@@ -3861,6 +3867,7 @@ cdef class FMUModelCS3(FMUModelBase3):
38613867
if status != FMIL.jm_status_success:
38623868
raise FMUException('Failed to instantiate the model. See the log for possibly more information.')
38633869

3870+
self._instantiated_with_early_return = earlyReturnAllowed
38643871
self._allocated_fmu = 1
38653872

38663873
cpdef _get_time(self):
@@ -3888,6 +3895,173 @@ cdef class FMUModelCS3(FMUModelBase3):
38883895
doc = "Property for accessing the current time of the simulation."
38893896
)
38903897

3898+
cpdef FMIL3.fmi3_status_t do_step(self, FMIL3.fmi3_float64_t current_t, FMIL3.fmi3_float64_t step_size, new_step=True):
3899+
"""
3900+
Performs an integrator step.
3901+
3902+
Parameters::
3903+
3904+
current_t --
3905+
The current communication point (current time) of
3906+
the master.
3907+
3908+
step_size --
3909+
The length of the step to be taken.
3910+
3911+
new_step --
3912+
True the last step was accepted by the master and
3913+
False if not.
3914+
3915+
Returns::
3916+
3917+
status --
3918+
The status of function which can be checked against
3919+
FMI_OK, FMI_WARNING, FMI_DISCARD, FMI_ERROR, FMI_FATAL.
3920+
3921+
Calls the underlying low-level function fmi3DoStep.
3922+
The `do_step_terminated` class attribute tracks the fmi3DoStep return
3923+
for `terminateSimulation`.
3924+
"""
3925+
cdef FMIL3.fmi3_status_t status
3926+
cdef FMIL3.fmi3_boolean_t new_s
3927+
cdef FMIL3.fmi3_boolean_t eventHandlingNeeded
3928+
cdef FMIL3.fmi3_boolean_t terminate
3929+
cdef FMIL3.fmi3_boolean_t earlyReturn
3930+
cdef FMIL3.fmi3_float64_t lastSuccessfulTime
3931+
3932+
if new_step:
3933+
new_s = FMIL3.fmi3_true
3934+
else:
3935+
new_s = FMIL3.fmi3_false
3936+
3937+
log_open = self._log_open()
3938+
if not log_open and self.get_log_level() > 2:
3939+
self._open_log_file()
3940+
3941+
self._log_handler.capi_start_callback(self._max_log_size_msg_sent, self._current_log_size)
3942+
status = FMIL3.fmi3_import_do_step(
3943+
self._fmu,
3944+
current_t,
3945+
step_size,
3946+
new_s,
3947+
&eventHandlingNeeded,
3948+
&terminate,
3949+
&earlyReturn,
3950+
&lastSuccessfulTime
3951+
)
3952+
self._log_handler.capi_end_callback(self._max_log_size_msg_sent, self._current_log_size)
3953+
3954+
if not log_open and self.get_log_level() > 2:
3955+
self._close_log_file()
3956+
3957+
if status != FMIL3.fmi3_status_ok:
3958+
return status
3959+
# On a fully completed step the reached time is current_t + step_size;
3960+
# lastSuccessfulTime is only meaningful when the FMU returns early.
3961+
if terminate:
3962+
self.time = lastSuccessfulTime
3963+
self.do_step_terminated = True
3964+
elif self._instantiated_with_early_return and earlyReturn:
3965+
self.time = lastSuccessfulTime
3966+
else:
3967+
self.time = current_t + step_size
3968+
3969+
return status
3970+
3971+
def simulate(self,
3972+
start_time="Default",
3973+
final_time="Default",
3974+
input=(),
3975+
algorithm='FMICSAlg',
3976+
options={}):
3977+
"""
3978+
Compact function for model simulation.
3979+
3980+
The simulation method depends on which algorithm is used, this can be
3981+
set with the function argument 'algorithm'. Options for the algorithm
3982+
are passed as option classes or as pure dicts. See
3983+
FMUModel.simulate_options for more details.
3984+
3985+
The default algorithm for this function is FMICSAlg.
3986+
3987+
Parameters::
3988+
3989+
start_time --
3990+
Start time for the simulation.
3991+
Default: Start time defined in the default experiment from
3992+
the ModelDescription file.
3993+
3994+
final_time --
3995+
Final time for the simulation.
3996+
Default: Stop time defined in the default experiment from
3997+
the ModelDescription file.
3998+
3999+
input --
4000+
Input signal for the simulation. The input should be a 2-tuple
4001+
consisting of first the names of the input variable(s) and then
4002+
the data matrix.
4003+
Default: Empty tuple.
4004+
4005+
algorithm --
4006+
The algorithm which will be used for the simulation is specified
4007+
by passing the algorithm class as string or class object in this
4008+
argument. 'algorithm' can be any class which implements the
4009+
abstract class AlgorithmBase (found in algorithm_drivers.py). In
4010+
this way it is possible to write own algorithms and use them
4011+
with this function.
4012+
Default: 'FMICSAlg'
4013+
4014+
options --
4015+
The options that should be used in the algorithm. For details on
4016+
the options do:
4017+
4018+
>> myModel = load_fmu(...)
4019+
>> opts = myModel.simulate_options()
4020+
>> opts?
4021+
4022+
Valid values are:
4023+
- A dict which gives AssimuloFMIAlgOptions with
4024+
default values on all options except the ones
4025+
listed in the dict. Empty dict will thus give all
4026+
options with default values.
4027+
- An options object.
4028+
Default: Empty dict
4029+
4030+
Returns::
4031+
4032+
Result object, subclass of common.algorithm_drivers.ResultBase.
4033+
"""
4034+
if start_time == "Default":
4035+
start_time = self.get_default_experiment_start_time()
4036+
if final_time == "Default":
4037+
final_time = self.get_default_experiment_stop_time()
4038+
4039+
return self._exec_simulate_algorithm(start_time,
4040+
final_time,
4041+
input,
4042+
'pyfmi.fmi_algorithm_drivers',
4043+
algorithm,
4044+
options)
4045+
4046+
def simulate_options(self, algorithm='FMICSAlg'):
4047+
"""
4048+
Get an instance of the simulate options class, filled with default
4049+
values. If called without argument then the options class for the
4050+
default simulation algorithm will be returned.
4051+
4052+
Parameters::
4053+
4054+
algorithm --
4055+
The algorithm for which the options class should be fetched.
4056+
Possible values are: 'FMICSAlg'.
4057+
Default: 'FMICSAlg'
4058+
4059+
Returns::
4060+
4061+
Options class for the algorithm specified with default values.
4062+
"""
4063+
return self._default_options('pyfmi.fmi_algorithm_drivers', algorithm)
4064+
38914065
def get_capability_flags(self) -> dict:
38924066
"""
38934067
Returns a dictionary with the capability flags of the FMU.
@@ -3934,6 +4108,19 @@ cdef class FMUModelCS3(FMUModelBase3):
39344108

39354109
return capabilities
39364110

4111+
def _provides_directional_derivatives(self) -> bool:
4112+
"""
4113+
Check capability to provide directional derivatives.
4114+
"""
4115+
return bool(FMIL3.fmi3_import_get_capability(self._fmu, FMIL3.fmi3_cs_providesDirectionalDerivatives))
4116+
4117+
def _supports_get_set_FMU_state(self) -> bool:
4118+
"""
4119+
Check support for getting and setting the FMU-state.
4120+
"""
4121+
return bool(FMIL3.fmi3_import_get_capability(self._fmu, FMIL3.fmi3_cs_canGetAndSetFMUState))
4122+
4123+
39374124
cdef class FMUModelME3(FMUModelBase3):
39384125
"""
39394126
FMI3 ModelExchange model loaded from a dll

src/pyfmi/fmi_algorithm_drivers.py

Lines changed: 34 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -24,9 +24,9 @@
2424
import numpy as np
2525
import scipy.optimize as spopt
2626

27-
from pyfmi.fmi1 import FMUModelME1, FMUModelCS1, FMI_ERROR, FMI_DISCARD, FMI1_LAST_SUCCESSFUL_TIME # TODO
27+
from pyfmi.fmi1 import FMUModelME1, FMUModelCS1, FMI_OK, FMI_ERROR, FMI_DISCARD, FMI1_LAST_SUCCESSFUL_TIME # TODO
2828
from pyfmi.fmi2 import FMUModelME2, FMUModelCS2, FMI2_INPUT, FMI2_LAST_SUCCESSFUL_TIME
29-
from pyfmi.fmi3 import FMUModelME3
29+
from pyfmi.fmi3 import FMUModelME3, FMUModelCS3
3030
from pyfmi.fmi_coupled import CoupledFMUModelME2
3131
from pyfmi.fmi_extended import FMUModelME1Extended
3232
from pyfmi.fmi_util import parameter_estimation_f
@@ -940,19 +940,19 @@ def __init__(self,
940940
if self.options['initialize']:
941941
if isinstance(self.model, (FMUModelCS1, FMUModelME1Extended)):
942942
self.model.initialize(start_time, final_time, stop_time_defined=self.options["stop_time_defined"])
943-
944943
elif isinstance(self.model, FMUModelCS2):
945944
self.model.setup_experiment(start_time=start_time, stop_time_defined=self.options["stop_time_defined"], stop_time=final_time)
946945
self.model.initialize()
947-
946+
elif isinstance(self.model, FMUModelCS3):
947+
self.model.initialize(start_time=start_time, stop_time_defined=self.options["stop_time_defined"], stop_time=final_time)
948948
else:
949949
raise FMUException("Unknown model.")
950950

951951
time_res_init = timer()
952952
self.result_handler.initialize_complete()
953953
time_res_init = timer() - time_res_init
954954

955-
elif self.model.time is None and isinstance(self.model, FMUModelCS2):
955+
elif self.model.time is None and isinstance(self.model, (FMUModelCS2, FMUModelCS3)):
956956
raise FMUException("Setup Experiment has not been called, this has to be called prior to the initialization call.")
957957
elif self.model.time is None:
958958
raise FMUException("The model need to be initialized prior to calling the simulate method if the option 'initialize' is set to False")
@@ -1015,6 +1015,26 @@ def _set_solver_options(self):
10151015
"""
10161016
pass #No solver options
10171017

1018+
def _check_do_step_status_and_terminated(self, status) -> tuple[bool, float]:
1019+
"""Return (true, <termination_time>) if terminated, (False, 0) else.
1020+
Raise exception in case of error returns."""
1021+
if status != FMI_OK:
1022+
if status == FMI_DISCARD and isinstance(self.model, (FMUModelCS1, FMUModelCS2)):
1023+
try:
1024+
if isinstance(self.model, FMUModelCS1):
1025+
last_time = self.model.get_real_status(FMI1_LAST_SUCCESSFUL_TIME)
1026+
else:
1027+
last_time = self.model.get_real_status(FMI2_LAST_SUCCESSFUL_TIME)
1028+
return True, last_time
1029+
except FMUException:
1030+
pass
1031+
else: # status = error || fatal || (discard && FMI3)
1032+
raise FMUException("The simulation failed. See the log for more information. Return flag %d."%status)
1033+
elif isinstance(self.model, FMUModelCS3):
1034+
if self.model.do_step_terminated:
1035+
return True, self.model.time
1036+
return False, 0
1037+
10181038
def solve(self):
10191039
"""
10201040
Runs the simulation.
@@ -1045,28 +1065,16 @@ def solve(self):
10451065
status = self.model.do_step(t,h)
10461066
self.status = status
10471067

1048-
if status != 0:
1049-
1050-
if status == FMI_ERROR:
1051-
raise FMUException("The simulation failed. See the log for more information. Return flag %d."%status)
1068+
terminated, terminated_time = self._check_do_step_status_and_terminated(status)
1069+
if terminated:
1070+
if terminated_time > t: # only store additional point if time advanced
1071+
self.model.time = terminated_time
1072+
final_time = terminated_time
10521073

1053-
elif status == FMI_DISCARD and isinstance(self.model, (FMUModelCS1, FMUModelCS2)):
1054-
1055-
try:
1056-
if isinstance(self.model, FMUModelCS1):
1057-
last_time = self.model.get_real_status(FMI1_LAST_SUCCESSFUL_TIME)
1058-
else:
1059-
last_time = self.model.get_real_status(FMI2_LAST_SUCCESSFUL_TIME)
1060-
if last_time > t: #Solver succeeded in taken a step a little further than the last time
1061-
self.model.time = last_time
1062-
final_time = last_time
1063-
1064-
start_time_point = timer()
1065-
result_handler.integration_point()
1066-
self.timings["storing_result"] += timer() - start_time_point
1067-
except FMUException:
1068-
pass
1069-
break
1074+
start_time_point = timer()
1075+
result_handler.integration_point()
1076+
self.timings["storing_result"] += timer() - start_time_point
1077+
break # stop integration loop
10701078

10711079
final_time = t+h
10721080

src/pyfmi/fmil3_import.pxd

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -385,6 +385,17 @@ cdef extern from 'fmilib.h':
385385
fmi3_status_t fmi3_import_serialize_fmu_state(fmi3_import_t*, fmi3_FMU_state_t, fmi3_byte_t*, size_t)
386386
fmi3_status_t fmi3_import_de_serialize_fmu_state(fmi3_import_t*, fmi3_byte_t*, size_t, fmi3_FMU_state_t*)
387387

388+
# CS CAPI methods
389+
fmi3_status_t fmi3_import_do_step(
390+
fmi3_import_t* fmu,
391+
fmi3_float64_t currentCommunicationPoint,
392+
fmi3_float64_t communicationStepSize,
393+
fmi3_boolean_t noSetFMUStatePriorToCurrentPoint,
394+
fmi3_boolean_t* eventHandlingNeeded,
395+
fmi3_boolean_t* terminate,
396+
fmi3_boolean_t* earlyReturn,
397+
fmi3_float64_t* lastSuccessfulTime)
398+
388399
# FMI HELPER METHODS (3.0)
389400
fmi3_fmu_kind_enu_t fmi3_import_get_fmu_kind(fmi3_import_t*)
390401
char* fmi3_fmu_kind_to_string(fmi3_fmu_kind_enu_t)

0 commit comments

Comments
 (0)