Skip to content

Commit 05adda9

Browse files
committed
fix: handle state events for ME2/ME3 on consecutive simulations with initialize=False
When continuing a simulation with initialize=False (e.g. after set_fmu_state() + input changes), the FMU must process potential state events before resuming integration. The FMI spec requires calling fmi2NewDiscreteStates / fmi3UpdateDiscreteStates after fmi2SetFMUstate since restored state + new inputs may trigger events. Previously, time-event checks at start time only covered ME1 and used unreliable event_info flags. PR modelon-community#109's unconditional event_update() was too aggressive because it ran on the initialize=True path too, where event_update was already called during init, violating the FMI state machine. This fix: - For initialize=False with ME2/ME3: enter_event_mode + event_update + enter_continuous_time_mode to process pending events (e.g. after set_fmu_state + input changes) - For initialize=True with ME2/ME3: extend the existing time-event check (equivalent to the ME1 check that was already in place) with proper mode transition wrappers - ME1 behavior unchanged - Add enter_event_mode() no-op override to Dummy_FMUModelME2 to prevent segfault when algorithm driver calls enter_event_mode() on test stubs Fixes modelon-community#159 Fixes modelon-community#72 Closes modelon-community#109 Tests added: - test_fmi2.py::test_consecutive_simulation_with_initialize_false - test_fmi3_sim.py::TestSimulation::test_consecutive_simulation_with_initialize_false
1 parent 67d63ff commit 05adda9

4 files changed

Lines changed: 50 additions & 2 deletions

File tree

‎src/pyfmi/fmi_algorithm_drivers.py‎

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -374,11 +374,22 @@ def __init__(self,
374374
solver_options = self.solver_options)
375375
number_of_diagnostics_variables = len(_diagnostics_vars)
376376

377-
#See if there is an time event at start time
377+
#Check for events at start time
378378
if isinstance(self.model, FMUModelME1):
379379
event_info = self.model.get_event_info()
380380
if event_info.upcomingTimeEvent and event_info.nextEventTime == model.time:
381381
self.model.event_update()
382+
elif isinstance(self.model, (FMUModelME2, CoupledFMUModelME2, FMUModelME3)):
383+
if not self.options['initialize']:
384+
self.model.enter_event_mode()
385+
self.model.event_update()
386+
self.model.enter_continuous_time_mode()
387+
else:
388+
event_info = self.model.get_event_info()
389+
if event_info.nextEventTimeDefined and abs(event_info.nextEventTime - model.time) <= 1e-14:
390+
self.model.enter_event_mode()
391+
self.model.event_update()
392+
self.model.enter_continuous_time_mode()
382393

383394
if abs(start_time - model.time) > 1e-14:
384395
logging_module.warning('The simulation start time (%f) and the current time in the model (%f) is different. Is the simulation start time correctly set?'%(start_time, model.time))

‎src/pyfmi/test_util.pyx‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -396,6 +396,9 @@ class Dummy_FMUModelME2(_ForTestingFMUModelME2):
396396
def event_update(self, *args, **kwargs):
397397
pass
398398

399+
def enter_event_mode(self, *args, **kwargs):
400+
pass
401+
399402
def enter_continuous_time_mode(self, *args, **kwargs):
400403
pass
401404

‎tests/test_fmi2.py‎

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1126,4 +1126,21 @@ def test_no_state_fmu_eval_failure_caught(fmu_path):
11261126
fmu = load_fmu(fmu_path)
11271127
expected_err = "The right-hand side function had repeated recoverable errors"
11281128
with pytest.raises(CVodeError, match = re.escape(expected_err)):
1129-
fmu.simulate()
1129+
fmu.simulate()
1130+
1131+
def test_consecutive_simulation_with_initialize_false():
1132+
"""Test that consecutive simulate() calls with initialize=False work.
1133+
1134+
The initialize=False path invokes enter_event_mode + event_update +
1135+
enter_continuous_time_mode before resuming integration. Regression
1136+
test to ensure this does not crash or corrupt the FMU state.
1137+
"""
1138+
fmu = load_fmu(REFERENCE_FMU_FMI2_PATH / "Feedthrough.fmu")
1139+
1140+
fmu.set('Int32_input', 42)
1141+
fmu.simulate(0, 0.1, options={"ncp": 1})
1142+
assert fmu.get('Int32_output')[0] == 42
1143+
1144+
# Continue without re-initializing — exercises the fix
1145+
fmu.simulate(0.1, 0.2, options={"ncp": 1, "initialize": False})
1146+
assert fmu.get('Int32_output')[0] == 42

‎tests/test_fmi3_sim.py‎

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -353,3 +353,20 @@ def test_dynamic_diagnostics_no_time_per_step_should_not_set_cpu_time(self):
353353
res = model.simulate(options = opts)
354354

355355
assert "@Diagnostics.cpu_time" not in res.keys()
356+
357+
def test_consecutive_simulation_with_initialize_false(self):
358+
"""Test that consecutive simulate() calls with initialize=False work.
359+
360+
The initialize=False path invokes enter_event_mode + event_update +
361+
enter_continuous_time_mode before resuming integration. Regression
362+
test to ensure this does not crash or corrupt the FMU state.
363+
"""
364+
fmu = load_fmu(FMI3_REF_FMU_PATH / "Feedthrough.fmu")
365+
366+
fmu.set('Int32_input', 42)
367+
fmu.simulate(0, 0.1, options={"ncp": 1})
368+
assert fmu.get('Int32_output')[0] == 42
369+
370+
# Continue without re-initializing — exercises the fix
371+
fmu.simulate(0.1, 0.2, options={"ncp": 1, "initialize": False})
372+
assert fmu.get('Int32_output')[0] == 42

0 commit comments

Comments
 (0)