From 8bf4185bd98d60e0dd4a34b23b462289196e66e7 Mon Sep 17 00:00:00 2001 From: syntron Date: Tue, 20 Jan 2026 21:29:41 +0100 Subject: [PATCH 1/5] [ModelicaSystem] update handling of outputs and continuous data * store data as numpy.float64 - allows to define None values * split get*() function into Initial values and Final values --- OMPython/ModelicaSystem.py | 272 ++++++++++++++++++++++++++----------- 1 file changed, 193 insertions(+), 79 deletions(-) diff --git a/OMPython/ModelicaSystem.py b/OMPython/ModelicaSystem.py index 03c24b51..766c3c9d 100644 --- a/OMPython/ModelicaSystem.py +++ b/OMPython/ModelicaSystem.py @@ -325,11 +325,8 @@ def __init__( self._quantities: list[dict[str, Any]] = [] self._params: dict[str, str] = {} # even numerical values are stored as str self._inputs: dict[str, list[tuple[float, float]]] = {} - # _outputs values are str before simulate(), but they can be - # np.float64 after simulate(). - self._outputs: dict[str, Any] = {} - # same for _continuous - self._continuous: dict[str, Any] = {} + self._outputs: dict[str, np.float64] = {} # numpy.float64 as it allows to define None values + self._continuous: dict[str, np.float64] = {} # numpy.float64 as it allows to define None values self._simulate_options: dict[str, str] = {} self._override_variables: dict[str, str] = {} self._simulate_options_override: dict[str, str] = {} @@ -630,11 +627,11 @@ def _xmlparse(self, xml_file: OMCPath): else: self._params[scalar["name"]] = scalar["start"] if scalar["variability"] == "continuous": - self._continuous[scalar["name"]] = scalar["start"] + self._continuous[scalar["name"]] = np.float64(scalar["start"]) if scalar["causality"] == "input": self._inputs[scalar["name"]] = scalar["start"] if scalar["causality"] == "output": - self._outputs[scalar["name"]] = scalar["start"] + self._outputs[scalar["name"]] = np.float64(scalar["start"]) self._quantities.append(scalar) @@ -695,15 +692,104 @@ def getQuantities(self, names: Optional[str | list[str]] = None) -> list[dict]: raise ModelicaSystemError("Unhandled input for getQuantities()") + def getContinuousInitial( + self, + names: Optional[str | list[str]] = None, + ) -> dict[str, np.float64] | list[np.float64]: + """ + Get (initial) values of continuous signals. + + Args: + names: Either None (default), a string with the continuous signal + name, or a list of signal name strings. + Returns: + If `names` is None, a dict in the format + {signal_name: signal_value} is returned. + If `names` is a string, a single element list [signal_value] is + returned. + If `names` is a list, a list with one value for each signal name + in names is returned: [signal1_value, signal2_value, ...]. + + Examples: + >>> mod.getContinuousInitial() + {'x': '1.0', 'der(x)': None, 'y': '-0.4'} + >>> mod.getContinuousInitial("y") + ['-0.4'] + >>> mod.getContinuousInitial(["y","x"]) + ['-0.4', '1.0'] + """ + if names is None: + return self._continuous + if isinstance(names, str): + return [self._continuous[names]] + if isinstance(names, list): + return [self._continuous[x] for x in names] + + raise ModelicaSystemError("Unhandled input for getContinousInitial()") + + def getContinuousFinal( + self, + names: Optional[str | list[str]] = None, + ) -> dict[str, np.float64] | list[np.float64]: + """ + Get (final) values of continuous signals (at stopTime). + + Args: + names: Either None (default), a string with the continuous signal + name, or a list of signal name strings. + Returns: + If `names` is None, a dict in the format + {signal_name: signal_value} is returned. + If `names` is a string, a single element list [signal_value] is + returned. + If `names` is a list, a list with one value for each signal name + in names is returned: [signal1_value, signal2_value, ...]. + + Examples: + >>> mod.getContinuousFinal() + {'x': np.float64(0.68), 'der(x)': np.float64(-0.24), 'y': np.float64(-0.24)} + >>> mod.getContinuousFinal("x") + [np.float64(0.68)] + >>> mod.getContinuousFinal(["y","x"]) + [np.float64(-0.24), np.float64(0.68)] + """ + if not self._simulated: + raise ModelicaSystemError("Please use getContinuousInitial() before the simulation was started!") + + def get_continuous_solution(name_list: list[str]) -> None: + for name in name_list: + if name in self._continuous: + value = self.getSolutions(name) + self._continuous[name] = np.float64(value[0][-1]) + else: + raise ModelicaSystemError(f"{names} is not continuous") + + if names is None: + get_continuous_solution(name_list=list(self._continuous.keys())) + return self._continuous + + if isinstance(names, str): + get_continuous_solution(name_list=[names]) + return [self._continuous[names]] + + if isinstance(names, list): + get_continuous_solution(name_list=names) + values = [] + for name in names: + values.append(self._continuous[name]) + return values + + raise ModelicaSystemError("Unhandled input for getContinousFinal()") + def getContinuous( self, names: Optional[str | list[str]] = None, - ) -> dict[str, str | numbers.Real] | list[str | numbers.Real]: + ) -> dict[str, np.float64] | list[np.float64]: """Get values of continuous signals. - If called before simulate(), the initial values are returned as - strings (or None). If called after simulate(), the final values (at - stopTime) are returned as numpy.float64. + If called before simulate(), the initial values are returned. + If called after simulate(), the final values (at stopTime) are returned. + The return format is always numpy.float64. Args: names: Either None (default), a string with the continuous signal @@ -734,41 +820,9 @@ def getContinuous( [np.float64(-0.24), np.float64(0.68)] """ if not self._simulated: - if names is None: - return self._continuous - if isinstance(names, str): - return [self._continuous[names]] - if isinstance(names, list): - return [self._continuous[x] for x in names] - - if names is None: - for name in self._continuous: - try: - value = self.getSolutions(name) - self._continuous[name] = value[0][-1] - except (OMCSessionException, ModelicaSystemError) as ex: - raise ModelicaSystemError(f"{name} could not be computed") from ex - return self._continuous + return self.getContinuousInitial(names=names) - if isinstance(names, str): - if names in self._continuous: - value = self.getSolutions(names) - self._continuous[names] = value[0][-1] - return [self._continuous[names]] - raise ModelicaSystemError(f"{names} is not continuous") - - if isinstance(names, list): - valuelist = [] - for name in names: - if name in self._continuous: - value = self.getSolutions(name) - self._continuous[name] = value[0][-1] - valuelist.append(value[0][-1]) - else: - raise ModelicaSystemError(f"{name} is not continuous") - return valuelist - - raise ModelicaSystemError("Unhandled input for getContinous()") + return self.getContinuousFinal(names=names) def getParameters( self, @@ -841,15 +895,103 @@ def getInputs( raise ModelicaSystemError("Unhandled input for getInputs()") + def getOutputsInitial( + self, + names: Optional[str | list[str]] = None, + ) -> dict[str, np.float64] | list[np.float64]: + """ + Get (initial) values of output signals. + + Args: + names: Either None (default), a string with the output name, + or a list of output name strings. + Returns: + If `names` is None, a dict in the format + {output_name: output_value} is returned. + If `names` is a string, a single element list [output_value] is + returned. + If `names` is a list, a list with one value for each output name + in names is returned: [output1_value, output2_value, ...]. + + Examples: + >>> mod.getOutputsInitial() + {'out1': '-0.4', 'out2': '1.2'} + >>> mod.getOutputsInitial("out1") + ['-0.4'] + >>> mod.getOutputsInitial(["out1","out2"]) + ['-0.4', '1.2'] + """ + if names is None: + return self._outputs + if isinstance(names, str): + return [self._outputs[names]] + if isinstance(names, list): + return [self._outputs[x] for x in names] + + raise ModelicaSystemError("Unhandled input for getOutputsInitial()") + + def getOutputsFinal( + self, + names: Optional[str | list[str]] = None, + ) -> dict[str, np.float64] | list[np.float64]: + """Get (final) values of output signals (at stopTime). + + Args: + names: Either None (default), a string with the output name, + or a list of output name strings. + Returns: + If `names` is None, a dict in the format + {output_name: output_value} is returned. + If `names` is a string, a single element list [output_value] is + returned. + If `names` is a list, a list with one value for each output name + in names is returned: [output1_value, output2_value, ...]. + + Examples: + >>> mod.getOutputsFinal() + {'out1': np.float64(-0.1234), 'out2': np.float64(2.1)} + >>> mod.getOutputsFinal("out1") + [np.float64(-0.1234)] + >>> mod.getOutputsFinal(["out1","out2"]) + [np.float64(-0.1234), np.float64(2.1)] + """ + if not self._simulated: + raise ModelicaSystemError("Please use getOuputsInitial() before the simulation was started!") + + def get_outputs_solution(name_list: list[str]) -> None: + for name in name_list: + if name in self._outputs: + value = self.getSolutions(name) + self._outputs[name] = np.float64(value[0][-1]) + else: + raise ModelicaSystemError(f"{names} is not a valid output") + + if names is None: + get_outputs_solution(name_list=list(self._outputs.keys())) + return self._outputs + + if isinstance(names, str): + get_outputs_solution(name_list=[names]) + return [self._outputs[names]] + + if isinstance(names, list): + get_outputs_solution(name_list=names) + values = [] + for name in names: + values.append(self._outputs[name]) + return values + + raise ModelicaSystemError("Unhandled input for getOutputs()") + def getOutputs( self, names: Optional[str | list[str]] = None, - ) -> dict[str, str | numbers.Real] | list[str | numbers.Real]: + ) -> dict[str, np.float64] | list[np.float64]: """Get values of output signals. - If called before simulate(), the initial values are returned as - strings. If called after simulate(), the final values (at stopTime) - are returned as numpy.float64. + If called before simulate(), the initial values are returned. + If called after simulate(), the final values (at stopTime) are returned. + The return format is always numpy.float64. Args: names: Either None (default), a string with the output name, @@ -880,37 +1022,9 @@ def getOutputs( [np.float64(-0.1234), np.float64(2.1)] """ if not self._simulated: - if names is None: - return self._outputs - if isinstance(names, str): - return [self._outputs[names]] - return [self._outputs[x] for x in names] - - if names is None: - for name in self._outputs: - value = self.getSolutions(name) - self._outputs[name] = value[0][-1] - return self._outputs + return self.getOutputsInitial(names=names) - if isinstance(names, str): - if names in self._outputs: - value = self.getSolutions(names) - self._outputs[names] = value[0][-1] - return [self._outputs[names]] - raise KeyError(names) - - if isinstance(names, list): - valuelist = [] - for name in names: - if name in self._outputs: - value = self.getSolutions(name) - self._outputs[name] = value[0][-1] - valuelist.append(value[0][-1]) - else: - raise KeyError(name) - return valuelist - - raise ModelicaSystemError("Unhandled input for getOutputs()") + return self.getOutputsFinal(names=names) def getSimulationOptions( self, From a9da1e7b49130448abd3e2ba148eb5af14bfa298 Mon Sep 17 00:00:00 2001 From: syntron Date: Wed, 21 Jan 2026 19:09:07 +0100 Subject: [PATCH 2/5] [ModelicaSystem] use KeyError in getOutputsFinal() --- OMPython/ModelicaSystem.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/OMPython/ModelicaSystem.py b/OMPython/ModelicaSystem.py index 766c3c9d..b7d9eadd 100644 --- a/OMPython/ModelicaSystem.py +++ b/OMPython/ModelicaSystem.py @@ -964,7 +964,7 @@ def get_outputs_solution(name_list: list[str]) -> None: value = self.getSolutions(name) self._outputs[name] = np.float64(value[0][-1]) else: - raise ModelicaSystemError(f"{names} is not a valid output") + raise KeyError(f"{names} is not a valid output") if names is None: get_outputs_solution(name_list=list(self._outputs.keys())) From 8583041b897de73afd8fe61b228c9d08684b6ff4 Mon Sep 17 00:00:00 2001 From: syntron Date: Wed, 21 Jan 2026 19:12:28 +0100 Subject: [PATCH 3/5] [ModelicaSystem] use KeyError in getContinuousFinal() --- OMPython/ModelicaSystem.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/OMPython/ModelicaSystem.py b/OMPython/ModelicaSystem.py index b7d9eadd..cbeff951 100644 --- a/OMPython/ModelicaSystem.py +++ b/OMPython/ModelicaSystem.py @@ -762,7 +762,7 @@ def get_continuous_solution(name_list: list[str]) -> None: value = self.getSolutions(name) self._continuous[name] = np.float64(value[0][-1]) else: - raise ModelicaSystemError(f"{names} is not continuous") + raise KeyError(f"{names} is not continuous") if names is None: get_continuous_solution(name_list=list(self._continuous.keys())) From c77c472800a60ed475ce1a0f4811daa0f4b6b939 Mon Sep 17 00:00:00 2001 From: syntron Date: Wed, 21 Jan 2026 19:09:50 +0100 Subject: [PATCH 4/5] [ModelicaSystem] fix docstring of getContinuous() --- OMPython/ModelicaSystem.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/OMPython/ModelicaSystem.py b/OMPython/ModelicaSystem.py index cbeff951..661031a6 100644 --- a/OMPython/ModelicaSystem.py +++ b/OMPython/ModelicaSystem.py @@ -816,7 +816,7 @@ def getContinuous( {'x': np.float64(0.68), 'der(x)': np.float64(-0.24), 'y': np.float64(-0.24)} >>> mod.getContinuous("x") [np.float64(0.68)] - >>> mod.getOutputs(["y","x"]) + >>> mod.getContinuous(["y","x"]) [np.float64(-0.24), np.float64(0.68)] """ if not self._simulated: From c92e50ab0f4faff869223b6599535a6ec8619225 Mon Sep 17 00:00:00 2001 From: syntron Date: Tue, 20 Jan 2026 21:30:43 +0100 Subject: [PATCH 5/5] [test_ModelicaSystem.py] needed changes due to update of output / continuous data handling --- tests/test_ModelicaSystem.py | 39 ++++++++++++++++++++++++------------ 1 file changed, 26 insertions(+), 13 deletions(-) diff --git a/tests/test_ModelicaSystem.py b/tests/test_ModelicaSystem.py index dd0321ec..9bf0a7b9 100644 --- a/tests/test_ModelicaSystem.py +++ b/tests/test_ModelicaSystem.py @@ -345,20 +345,33 @@ def test_getters(tmp_path): with pytest.raises(KeyError): mod.getInputs("thisInputDoesNotExist") # getOutputs before simulate() - assert mod.getOutputs() == {'y': '-0.4'} - assert mod.getOutputs("y") == ["-0.4"] - assert mod.getOutputs(["y", "y"]) == ["-0.4", "-0.4"] + output = mod.getOutputs() + assert len(output) == 1 + assert 'y' in output.keys() + assert np.isclose(output['y'], -0.4) + assert np.isclose(mod.getOutputs("y"), -0.4) + output = mod.getOutputs(["y", "y"]) + assert len(output) == 2 + assert np.isclose(output[0], -0.4) + assert np.isclose(output[1], -0.4) with pytest.raises(KeyError): mod.getOutputs("thisOutputDoesNotExist") # getContinuous before simulate(): - assert mod.getContinuous() == { - 'x': '1.0', - 'der(x)': None, - 'y': '-0.4' - } - assert mod.getContinuous("y") == ['-0.4'] - assert mod.getContinuous(["y", "x"]) == ['-0.4', '1.0'] + continuous = mod.getContinuous() + assert len(continuous) == 3 + assert 'x' in continuous.keys() + assert np.isclose(continuous['x'], 1.0) + assert 'der(x)' in continuous.keys() + assert np.isnan(continuous['der(x)']) + assert 'y' in continuous.keys() + assert np.isclose(continuous['y'], -0.4) + continuous = mod.getContinuous('y') + assert np.isclose(continuous, -0.4) + continuous = mod.getContinuous(['y', 'x']) + assert np.isclose(continuous[0], -0.4) + assert np.isclose(continuous[1], 1.0) + with pytest.raises(KeyError): mod.getContinuous("a") # a is a parameter @@ -381,9 +394,9 @@ def test_getters(tmp_path): mod.getOutputs("thisOutputDoesNotExist") # getContinuous after simulate() should return values at end of simulation: - with pytest.raises(OMPython.ModelicaSystemError): + with pytest.raises(KeyError): mod.getContinuous("a") # a is a parameter - with pytest.raises(OMPython.ModelicaSystemError): + with pytest.raises(KeyError): mod.getContinuous(["x", "a", "y"]) # a is a parameter d = mod.getContinuous() assert d.keys() == {"x", "der(x)", "y"} @@ -393,7 +406,7 @@ def test_getters(tmp_path): assert mod.getContinuous("x") == [d["x"]] assert mod.getContinuous(["y", "x"]) == [d["y"], d["x"]] - with pytest.raises(OMPython.ModelicaSystemError): + with pytest.raises(KeyError): mod.getContinuous("a") # a is a parameter with pytest.raises(OMPython.ModelicaSystemError):