From 820b086ac80d1625f02c5df79f34063f14b0ff9b Mon Sep 17 00:00:00 2001 From: petermeisrimelmodelon Date: Mon, 13 Jul 2026 14:03:43 +0000 Subject: [PATCH 1/2] Adding support for CS output derivatives --- src/pyfmi/fmi3.pxd | 1 + src/pyfmi/fmi3.pyx | 71 ++++++++++++++++++++++++++++++++++++++ src/pyfmi/fmil3_import.pxd | 4 +++ tests/test_fmi3.py | 33 ++++++++++++++++++ 4 files changed, 109 insertions(+) diff --git a/src/pyfmi/fmi3.pxd b/src/pyfmi/fmi3.pxd index 088bdb17..b7acdd9f 100644 --- a/src/pyfmi/fmi3.pxd +++ b/src/pyfmi/fmi3.pxd @@ -189,6 +189,7 @@ cdef class FMUModelCS3(FMUModelBase3): cpdef _set_time(self, FMIL3.fmi3_float64_t t) cpdef FMIL3.fmi3_status_t do_step(self, FMIL3.fmi3_float64_t current_t, FMIL3.fmi3_float64_t step_size, new_step=*) + cdef FMIL3.fmi3_status_t _get_output_derivatives(self, np.ndarray value_refs, np.ndarray values, np.ndarray orders) cdef class _WorkerClass3: cdef int _dim diff --git a/src/pyfmi/fmi3.pyx b/src/pyfmi/fmi3.pyx index 2fff7df1..b8208173 100644 --- a/src/pyfmi/fmi3.pyx +++ b/src/pyfmi/fmi3.pyx @@ -3968,6 +3968,77 @@ cdef class FMUModelCS3(FMUModelBase3): return status + def get_output_derivatives(self, variables, FMIL3.fmi3_int32_t order): + """ + Returns the output derivatives for the specified variables. The + order specifies the nth-derivative. + + Parameters:: + + variables -- + The variables for which the output derivatives + should be returned. + + order -- + The derivative order. + + Returns:: + + The derivatives of the specified order. + """ + cdef FMIL3.fmi3_status_t status + cdef unsigned int max_output_derivative + cdef FMIL.size_t nref + cdef np.ndarray[FMIL3.fmi3_float64_t, ndim=1, mode='c'] values + cdef np.ndarray[FMIL3.fmi3_value_reference_t, ndim=1, mode='c'] value_refs + cdef np.ndarray[FMIL3.fmi3_int32_t, ndim=1, mode='c'] orders + + max_output_derivative = FMIL3.fmi3_import_get_capability(self._fmu, FMIL3.fmi3_cs_maxOutputDerivativeOrder) + + if order < 1 or order > max_output_derivative: + raise FMUException("The order must be greater than zero and below the maximum output derivative support of the FMU (%d)."%max_output_derivative) + + if isinstance(variables, str): + nref = 1 + value_refs = np.array([0], dtype=np.uint32, ndmin=1).ravel() + orders = np.array([order], dtype=np.int32) + value_refs[0] = self.get_variable_valueref(variables) + elif isinstance(variables, list) and np.prod([int(isinstance(v, str)) for v in variables]): #prod equals 0 or 1 + nref = len(variables) + value_refs = np.array([0]*nref, dtype=np.uint32, ndmin=1).ravel() + orders = np.array([0]*nref, dtype=np.int32) + for i in range(nref): + value_refs[i] = self.get_variable_valueref(variables[i]) + orders[i] = order + else: + raise FMUException("The variables must either be a string or a list of strings") + + values = np.array([0.0]*nref, dtype=float, ndmin=1) + + status = self._get_output_derivatives(value_refs, values, orders) + + if status != 0: + raise FMUException('Failed to get the output derivatives.') + + return values + + cdef FMIL3.fmi3_status_t _get_output_derivatives(self, np.ndarray[FMIL3.fmi3_value_reference_t, ndim=1, mode="c"] value_refs, + np.ndarray[FMIL3.fmi3_float64_t, ndim=1, mode="c"] values, + np.ndarray[FMIL3.fmi3_int32_t, ndim=1, mode="c"] orders): + cdef FMIL3.fmi3_status_t status + + if not (np.size(values) >= np.size(value_refs) and np.size(orders) >= np.size(value_refs)): + raise FMUException('Failed to get the output derivatives. Fatal dimension mismatch') + + self._log_handler.capi_start_callback(self._max_log_size_msg_sent, self._current_log_size) + status = FMIL3.fmi3_import_get_output_derivatives(self._fmu, + value_refs.data, np.size(value_refs), + orders.data, + values.data, np.size(values)) + self._log_handler.capi_end_callback(self._max_log_size_msg_sent, self._current_log_size) + + return status + def simulate(self, start_time="Default", final_time="Default", diff --git a/src/pyfmi/fmil3_import.pxd b/src/pyfmi/fmil3_import.pxd index 7913e499..9dfdbb8f 100644 --- a/src/pyfmi/fmil3_import.pxd +++ b/src/pyfmi/fmil3_import.pxd @@ -362,6 +362,10 @@ cdef extern from 'fmilib.h': fmi3_value_reference_t*, size_t, fmi3_float64_t*, size_t, fmi3_float64_t*, size_t) + fmi3_status_t fmi3_import_get_output_derivatives(fmi3_import_t*, + fmi3_value_reference_t*, size_t, + fmi3_int32_t*, + fmi3_float64_t*, size_t) # Misc fmi3_status_t fmi3_import_update_discrete_states( diff --git a/tests/test_fmi3.py b/tests/test_fmi3.py index 739123fc..4593e3b0 100644 --- a/tests/test_fmi3.py +++ b/tests/test_fmi3.py @@ -1631,6 +1631,39 @@ def test_do_step_terminated_resets(self, fmi3_cs_stair): assert not fmi3_cs_stair.do_step_terminated + @pytest.mark.parametrize("order", [1, 2]) + def test_get_output_derivatives_order_too_high(self, order, fmi3_cs_feedthrough): + """get_output_derivatives should raise if the order exceeds the FMU's + maxOutputDerivativeOrder. None of the reference FMUs declare support for + output derivatives, so any positive order is out of range.""" + fmi3_cs_feedthrough.initialize() + assert fmi3_cs_feedthrough.get_capability_flags()["maxOutputDerivativeOrder"] == 0 + + msg = "The order must be greater than zero and below the maximum output " \ + "derivative support of the FMU (0)." + with pytest.raises(FMUException, match = re.escape(msg)): + fmi3_cs_feedthrough.get_output_derivatives("Float64_continuous_output", order) + + @pytest.mark.parametrize("order", [0, -1]) + def test_get_output_derivatives_order_too_low(self, order, fmi3_cs_feedthrough): + """get_output_derivatives should raise for a non-positive order.""" + fmi3_cs_feedthrough.initialize() + + msg = "The order must be greater than zero and below the maximum output " \ + "derivative support of the FMU (0)." + with pytest.raises(FMUException, match = re.escape(msg)): + fmi3_cs_feedthrough.get_output_derivatives("Float64_continuous_output", order) + + def test_get_output_derivatives_list_input(self, fmi3_cs_feedthrough): + """A list of variables is accepted; the order is still validated per the + FMU's maxOutputDerivativeOrder (0 for the reference FMUs).""" + fmi3_cs_feedthrough.initialize() + + msg = "The order must be greater than zero and below the maximum output " \ + "derivative support of the FMU (0)." + with pytest.raises(FMUException, match = re.escape(msg)): + fmi3_cs_feedthrough.get_output_derivatives(["Float64_continuous_output"], 1) + class TestFMI3SE: # TODO: Unsupported for now pass From 30738a618893a3a98074aeff9fc806603e1117c1 Mon Sep 17 00:00:00 2001 From: petermeisrimelmodelon Date: Wed, 12 Aug 2026 15:19:27 +0000 Subject: [PATCH 2/2] review fixes --- src/pyfmi/fmi3.pyx | 21 +++++++-------------- 1 file changed, 7 insertions(+), 14 deletions(-) diff --git a/src/pyfmi/fmi3.pyx b/src/pyfmi/fmi3.pyx index b8208173..7987f4c0 100644 --- a/src/pyfmi/fmi3.pyx +++ b/src/pyfmi/fmi3.pyx @@ -3998,21 +3998,14 @@ cdef class FMUModelCS3(FMUModelBase3): if order < 1 or order > max_output_derivative: raise FMUException("The order must be greater than zero and below the maximum output derivative support of the FMU (%d)."%max_output_derivative) - if isinstance(variables, str): - nref = 1 - value_refs = np.array([0], dtype=np.uint32, ndmin=1).ravel() - orders = np.array([order], dtype=np.int32) - value_refs[0] = self.get_variable_valueref(variables) - elif isinstance(variables, list) and np.prod([int(isinstance(v, str)) for v in variables]): #prod equals 0 or 1 - nref = len(variables) - value_refs = np.array([0]*nref, dtype=np.uint32, ndmin=1).ravel() - orders = np.array([0]*nref, dtype=np.int32) - for i in range(nref): - value_refs[i] = self.get_variable_valueref(variables[i]) - orders[i] = order - else: - raise FMUException("The variables must either be a string or a list of strings") + if not isinstance(variables, (str, list)) or not all(isinstance(v, str) for v in variables): + raise FMUException("The variables must either be a string or a list of strings.") + if isinstance(variables, str): + variables = [variables] + nref = len(variables) + value_refs = np.array([self.get_variable_valueref(v) for v in variables], dtype=np.uint32, ndmin=1).ravel() + orders = np.array([order]*nref, dtype=np.int32) values = np.array([0.0]*nref, dtype=float, ndmin=1) status = self._get_output_derivatives(value_refs, values, orders)