From 86ebb490f01fc79f2716625a4e182ad8e8711467 Mon Sep 17 00:00:00 2001 From: petermeisrimelmodelon Date: Tue, 17 Mar 2026 10:27:44 +0000 Subject: [PATCH] fix: fixing a bug for the Master algorithm where connections could be initialized incorrectly when FMUs were initialized separately and using step_size_downsampling --- CHANGELOG | 2 ++ src/pyfmi/master.pyx | 11 ++++++----- tests/test_fmi_master.py | 33 +++++++++++++++++++++++++++++++++ 3 files changed, 41 insertions(+), 5 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 1f9b084c..800fbcdc 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -2,6 +2,8 @@ --- PyFMI-FUTURE --- * Fixed a crash with the `Master` algorithm option `block_initialization`. * Fixed a result handling issue for `dynamic_diagnostics = True` and `["_options"]["clock_step"] = False`. + * Fixed an issue for the `Master` algorithm where connection values could be initialized incorrectly, when FMUs + were initialized separately and using `step_size_downsampling_factor`. --- PyFMI-2.20.1 --- * Resolved issue where caching in result handling was too persistent and could prevent automatic garbage collection. diff --git a/src/pyfmi/master.pyx b/src/pyfmi/master.pyx index cc62ae20..531de547 100644 --- a/src/pyfmi/master.pyx +++ b/src/pyfmi/master.pyx @@ -1253,10 +1253,6 @@ cdef class Master: exit_initialization_mode(self.models, self.elapsed_time_init) - #Store the outputs - self.y_prev = self.get_connection_outputs(initialize = True).copy() - self.set_last_y(self.y_prev) - def initialize_result_objects(self, opts): if (opts["result_handling"] == "custom") and (not isinstance(opts["result_handler"], dict)): raise FMUException("'result_handler' option must be a dictionary for 'result_handling' = 'custom'.") @@ -1666,8 +1662,13 @@ cdef class Master: time_stop = timer() print('Elapsed initialization time: ' + str(time_stop-time_start) + ' seconds.') + # Get outputs and update previous values + self.y_prev = self.get_connection_outputs(initialize = True).copy() + self.set_last_y(self.y_prev) + self.y_discrete_prev = self.get_connection_outputs_discrete(initialize = True).copy() + #Store the inputs - self.set_last_us(self.L.dot(self.get_connection_outputs())) + self.set_last_us(self.L.dot(self.y_prev)) #Copy FMU address (used when evaluating in parallel) self.copy_fmu_addresses() diff --git a/tests/test_fmi_master.py b/tests/test_fmi_master.py index 59bb4014..34943656 100644 --- a/tests/test_fmi_master.py +++ b/tests/test_fmi_master.py @@ -843,3 +843,36 @@ def compute_global_D(self): "Actual values may no longer be sensible." with pytest.warns(UserWarning, match = re.escape(msg)): master.simulate(t_start, t_final, options = opts) + + def test_with_manual_initialization(self): + fmu1 = FMUModelCS2(os.path.join(FMI2_REF_FMU_PATH, "Feedthrough.fmu")) + fmu2 = FMUModelCS2(os.path.join(FMI2_REF_FMU_PATH, "Feedthrough.fmu")) + fmu3 = FMUModelCS2(os.path.join(FMI2_REF_FMU_PATH, "Feedthrough.fmu")) + + models = [fmu1, fmu2, fmu3] + connections = [ + (fmu1, "Float64_continuous_output", fmu2, "Float64_continuous_input"), + (fmu2, "Float64_continuous_output", fmu3, "Float64_continuous_input"), + + (fmu1, "Float64_discrete_output", fmu2, "Float64_discrete_input"), + (fmu2, "Float64_discrete_output", fmu3, "Float64_discrete_input"), + ] + master = Master(models, connections) + opts = master.simulate_options() + opts["step_size"] = 0.1 + opts["step_size_downsampling_factor"] = {fmu1: 5, fmu2: 2, fmu3: 1} + + fmu1.initialize() + fmu2.initialize() + fmu3.initialize() + opts["initialize"] = False + + res = master.simulate(options = opts) + + assert set(res[0]["Float64_continuous_output"].tolist()) == set([0.]) + assert set(res[1]["Float64_continuous_output"].tolist()) == set([0.]) + assert set(res[2]["Float64_continuous_output"].tolist()) == set([0.]) + + assert set(res[0]["Float64_discrete_output"].tolist()) == set([0.]) + assert set(res[1]["Float64_discrete_output"].tolist()) == set([0.]) + assert set(res[2]["Float64_discrete_output"].tolist()) == set([0.])