diff --git a/src/pyfmi/fmi_algorithm_drivers.py b/src/pyfmi/fmi_algorithm_drivers.py index 6b04d18a..c480f1f7 100644 --- a/src/pyfmi/fmi_algorithm_drivers.py +++ b/src/pyfmi/fmi_algorithm_drivers.py @@ -374,11 +374,22 @@ def __init__(self, solver_options = self.solver_options) number_of_diagnostics_variables = len(_diagnostics_vars) - #See if there is an time event at start time + #Check for events at start time if isinstance(self.model, FMUModelME1): event_info = self.model.get_event_info() if event_info.upcomingTimeEvent and event_info.nextEventTime == model.time: self.model.event_update() + elif isinstance(self.model, (FMUModelME2, CoupledFMUModelME2, FMUModelME3)): + if not self.options['initialize']: + self.model.enter_event_mode() + self.model.event_update() + self.model.enter_continuous_time_mode() + else: + event_info = self.model.get_event_info() + if event_info.nextEventTimeDefined and abs(event_info.nextEventTime - model.time) <= 1e-14: + self.model.enter_event_mode() + self.model.event_update() + self.model.enter_continuous_time_mode() if abs(start_time - model.time) > 1e-14: 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)) diff --git a/src/pyfmi/test_util.pyx b/src/pyfmi/test_util.pyx index 6e5350bb..4300c89c 100644 --- a/src/pyfmi/test_util.pyx +++ b/src/pyfmi/test_util.pyx @@ -396,6 +396,9 @@ class Dummy_FMUModelME2(_ForTestingFMUModelME2): def event_update(self, *args, **kwargs): pass + def enter_event_mode(self, *args, **kwargs): + pass + def enter_continuous_time_mode(self, *args, **kwargs): pass diff --git a/tests/test_fmi2.py b/tests/test_fmi2.py index 1264bba2..c42d7bf1 100644 --- a/tests/test_fmi2.py +++ b/tests/test_fmi2.py @@ -1126,4 +1126,41 @@ def test_no_state_fmu_eval_failure_caught(fmu_path): fmu = load_fmu(fmu_path) expected_err = "The right-hand side function had repeated recoverable errors" with pytest.raises(CVodeError, match = re.escape(expected_err)): - fmu.simulate() \ No newline at end of file + fmu.simulate() + +def test_consecutive_simulation_with_initialize_false_time_events(): + """Test initialize=False continuation with a time event at start time. + + After set_fmu_state + advancing model.time to a time-event boundary, + the fix must process the pending event before resuming integration. + Regression test for FMI2/ME2 time-event processing on continuation. + """ + fmu = load_fmu(REFERENCE_FMU_FMI2_PATH / "Stair.fmu") + + # Simulate to a point before the first time event (t=1.0) + res = fmu.simulate(0, 0.8, options={"ncp": 0}) + assert fmu.get('counter')[0] == 1 + assert fmu.get_event_info().nextEventTime == 1.0 + + # Save, restore, then advance model time to the event boundary + state = fmu.get_fmu_state() + fmu.set_fmu_state(state) + fmu.time = 1.0 + assert fmu.time == 1.0 + assert fmu.get_event_info().nextEventTimeDefined + assert fmu.get_event_info().nextEventTime == 1.0 + + # Continue with initialize=False - the fix must process the pending + # time event at t=1.0 before the solver integrates from t=1.0 onward. + # Use ExplicitEuler because CVode's built-in time-event detection masks + # the bug: CVode catches past-due events during its initial step, while + # ExplicitEuler skips them, revealing the omission. + fmu.simulate(1.001, 2.001, options={ + "ncp": 0, "initialize": False, "solver": "ExplicitEuler" + }) + assert fmu.get('counter')[0] == 3, ( + "Expected counter=3 (start=1 + event at 1.0 + event at 2.0), " + "got %d. Time events at continuation start were missed." + % fmu.get('counter')[0] + ) + fmu.free_fmu_state(state) \ No newline at end of file diff --git a/tests/test_fmi3_sim.py b/tests/test_fmi3_sim.py index 8f0aa5c3..dbd2507d 100644 --- a/tests/test_fmi3_sim.py +++ b/tests/test_fmi3_sim.py @@ -353,3 +353,40 @@ def test_dynamic_diagnostics_no_time_per_step_should_not_set_cpu_time(self): res = model.simulate(options = opts) assert "@Diagnostics.cpu_time" not in res.keys() + + def test_consecutive_simulation_with_initialize_false_time_events(self): + """Test initialize=False continuation with a time event at start time. + + After set_fmu_state + advancing model.time to a time-event boundary, + the fix must process the pending event before resuming integration. + Regression test for FMI3 time-event processing on continuation. + """ + fmu = load_fmu(FMI3_REF_FMU_PATH / "Stair.fmu") + + # Simulate to a point before the first time event (t=1.0) + res = fmu.simulate(0, 0.8, options={"ncp": 0}) + assert fmu.get('counter')[0] == 1 + assert fmu.get_event_info().nextEventTime == 1.0 + + # Save, restore, then advance model time to the event boundary + state = fmu.get_fmu_state() + fmu.set_fmu_state(state) + fmu.time = 1.0 + assert fmu.time == 1.0 + assert fmu.get_event_info().nextEventTimeDefined + assert fmu.get_event_info().nextEventTime == 1.0 + + # Continue with initialize=False - the fix must process the pending + # time event at t=1.0 before the solver integrates from t=1.0 onward. + # Use ExplicitEuler because CVode's built-in time-event detection masks + # the bug: CVode catches past-due events during its initial step, while + # ExplicitEuler skips them, revealing the omission. + fmu.simulate(1.001, 2.001, options={ + "ncp": 0, "initialize": False, "solver": "ExplicitEuler" + }) + assert fmu.get('counter')[0] == 3, ( + "Expected counter=3 (start=1 + event at 1.0 + event at 2.0), " + "got %d. Time events at continuation start were missed." + % fmu.get('counter')[0] + ) + fmu.free_fmu_state(state)