Skip to content

Commit 998b0f7

Browse files
syntronadeas31
andauthored
(A008) [ModelicaSystem] getContinuous() / getOutputs() (#418)
* [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 * [ModelicaSystem] use KeyError in getOutputsFinal() * [ModelicaSystem] use KeyError in getContinuousFinal() * [ModelicaSystem] fix docstring of getContinuous() * [test_ModelicaSystem.py] needed changes due to update of output / continuous data handling --------- Co-authored-by: Adeel Asghar <adeel.asghar@liu.se>
1 parent 800ad2b commit 998b0f7

2 files changed

Lines changed: 220 additions & 93 deletions

File tree

OMPython/ModelicaSystem.py

Lines changed: 194 additions & 80 deletions
Original file line numberDiff line numberDiff line change
@@ -325,11 +325,8 @@ def __init__(
325325
self._quantities: list[dict[str, Any]] = []
326326
self._params: dict[str, str] = {} # even numerical values are stored as str
327327
self._inputs: dict[str, list[tuple[float, float]]] = {}
328-
# _outputs values are str before simulate(), but they can be
329-
# np.float64 after simulate().
330-
self._outputs: dict[str, Any] = {}
331-
# same for _continuous
332-
self._continuous: dict[str, Any] = {}
328+
self._outputs: dict[str, np.float64] = {} # numpy.float64 as it allows to define None values
329+
self._continuous: dict[str, np.float64] = {} # numpy.float64 as it allows to define None values
333330
self._simulate_options: dict[str, str] = {}
334331
self._override_variables: dict[str, str] = {}
335332
self._simulate_options_override: dict[str, str] = {}
@@ -629,11 +626,11 @@ def _xmlparse(self, xml_file: OMCPath):
629626
else:
630627
self._params[scalar["name"]] = scalar["start"]
631628
if scalar["variability"] == "continuous":
632-
self._continuous[scalar["name"]] = scalar["start"]
629+
self._continuous[scalar["name"]] = np.float64(scalar["start"])
633630
if scalar["causality"] == "input":
634631
self._inputs[scalar["name"]] = scalar["start"]
635632
if scalar["causality"] == "output":
636-
self._outputs[scalar["name"]] = scalar["start"]
633+
self._outputs[scalar["name"]] = np.float64(scalar["start"])
637634

638635
self._quantities.append(scalar)
639636

@@ -694,15 +691,104 @@ def getQuantities(self, names: Optional[str | list[str]] = None) -> list[dict]:
694691

695692
raise ModelicaSystemError("Unhandled input for getQuantities()")
696693

694+
def getContinuousInitial(
695+
self,
696+
names: Optional[str | list[str]] = None,
697+
) -> dict[str, np.float64] | list[np.float64]:
698+
"""
699+
Get (initial) values of continuous signals.
700+
701+
Args:
702+
names: Either None (default), a string with the continuous signal
703+
name, or a list of signal name strings.
704+
Returns:
705+
If `names` is None, a dict in the format
706+
{signal_name: signal_value} is returned.
707+
If `names` is a string, a single element list [signal_value] is
708+
returned.
709+
If `names` is a list, a list with one value for each signal name
710+
in names is returned: [signal1_value, signal2_value, ...].
711+
712+
Examples:
713+
>>> mod.getContinuousInitial()
714+
{'x': '1.0', 'der(x)': None, 'y': '-0.4'}
715+
>>> mod.getContinuousInitial("y")
716+
['-0.4']
717+
>>> mod.getContinuousInitial(["y","x"])
718+
['-0.4', '1.0']
719+
"""
720+
if names is None:
721+
return self._continuous
722+
if isinstance(names, str):
723+
return [self._continuous[names]]
724+
if isinstance(names, list):
725+
return [self._continuous[x] for x in names]
726+
727+
raise ModelicaSystemError("Unhandled input for getContinousInitial()")
728+
729+
def getContinuousFinal(
730+
self,
731+
names: Optional[str | list[str]] = None,
732+
) -> dict[str, np.float64] | list[np.float64]:
733+
"""
734+
Get (final) values of continuous signals (at stopTime).
735+
736+
Args:
737+
names: Either None (default), a string with the continuous signal
738+
name, or a list of signal name strings.
739+
Returns:
740+
If `names` is None, a dict in the format
741+
{signal_name: signal_value} is returned.
742+
If `names` is a string, a single element list [signal_value] is
743+
returned.
744+
If `names` is a list, a list with one value for each signal name
745+
in names is returned: [signal1_value, signal2_value, ...].
746+
747+
Examples:
748+
>>> mod.getContinuousFinal()
749+
{'x': np.float64(0.68), 'der(x)': np.float64(-0.24), 'y': np.float64(-0.24)}
750+
>>> mod.getContinuousFinal("x")
751+
[np.float64(0.68)]
752+
>>> mod.getContinuousFinal(["y","x"])
753+
[np.float64(-0.24), np.float64(0.68)]
754+
"""
755+
if not self._simulated:
756+
raise ModelicaSystemError("Please use getContinuousInitial() before the simulation was started!")
757+
758+
def get_continuous_solution(name_list: list[str]) -> None:
759+
for name in name_list:
760+
if name in self._continuous:
761+
value = self.getSolutions(name)
762+
self._continuous[name] = np.float64(value[0][-1])
763+
else:
764+
raise KeyError(f"{names} is not continuous")
765+
766+
if names is None:
767+
get_continuous_solution(name_list=list(self._continuous.keys()))
768+
return self._continuous
769+
770+
if isinstance(names, str):
771+
get_continuous_solution(name_list=[names])
772+
return [self._continuous[names]]
773+
774+
if isinstance(names, list):
775+
get_continuous_solution(name_list=names)
776+
values = []
777+
for name in names:
778+
values.append(self._continuous[name])
779+
return values
780+
781+
raise ModelicaSystemError("Unhandled input for getContinousFinal()")
782+
697783
def getContinuous(
698784
self,
699785
names: Optional[str | list[str]] = None,
700-
) -> dict[str, str | numbers.Real] | list[str | numbers.Real]:
786+
) -> dict[str, np.float64] | list[np.float64]:
701787
"""Get values of continuous signals.
702788
703-
If called before simulate(), the initial values are returned as
704-
strings (or None). If called after simulate(), the final values (at
705-
stopTime) are returned as numpy.float64.
789+
If called before simulate(), the initial values are returned.
790+
If called after simulate(), the final values (at stopTime) are returned.
791+
The return format is always numpy.float64.
706792
707793
Args:
708794
names: Either None (default), a string with the continuous signal
@@ -729,45 +815,13 @@ def getContinuous(
729815
{'x': np.float64(0.68), 'der(x)': np.float64(-0.24), 'y': np.float64(-0.24)}
730816
>>> mod.getContinuous("x")
731817
[np.float64(0.68)]
732-
>>> mod.getOutputs(["y","x"])
818+
>>> mod.getContinuous(["y","x"])
733819
[np.float64(-0.24), np.float64(0.68)]
734820
"""
735821
if not self._simulated:
736-
if names is None:
737-
return self._continuous
738-
if isinstance(names, str):
739-
return [self._continuous[names]]
740-
if isinstance(names, list):
741-
return [self._continuous[x] for x in names]
742-
743-
if names is None:
744-
for name in self._continuous:
745-
try:
746-
value = self.getSolutions(name)
747-
self._continuous[name] = value[0][-1]
748-
except (OMCSessionException, ModelicaSystemError) as ex:
749-
raise ModelicaSystemError(f"{name} could not be computed") from ex
750-
return self._continuous
751-
752-
if isinstance(names, str):
753-
if names in self._continuous:
754-
value = self.getSolutions(names)
755-
self._continuous[names] = value[0][-1]
756-
return [self._continuous[names]]
757-
raise ModelicaSystemError(f"{names} is not continuous")
758-
759-
if isinstance(names, list):
760-
valuelist = []
761-
for name in names:
762-
if name in self._continuous:
763-
value = self.getSolutions(name)
764-
self._continuous[name] = value[0][-1]
765-
valuelist.append(value[0][-1])
766-
else:
767-
raise ModelicaSystemError(f"{name} is not continuous")
768-
return valuelist
822+
return self.getContinuousInitial(names=names)
769823

770-
raise ModelicaSystemError("Unhandled input for getContinous()")
824+
return self.getContinuousFinal(names=names)
771825

772826
def getParameters(
773827
self,
@@ -840,15 +894,103 @@ def getInputs(
840894

841895
raise ModelicaSystemError("Unhandled input for getInputs()")
842896

897+
def getOutputsInitial(
898+
self,
899+
names: Optional[str | list[str]] = None,
900+
) -> dict[str, np.float64] | list[np.float64]:
901+
"""
902+
Get (initial) values of output signals.
903+
904+
Args:
905+
names: Either None (default), a string with the output name,
906+
or a list of output name strings.
907+
Returns:
908+
If `names` is None, a dict in the format
909+
{output_name: output_value} is returned.
910+
If `names` is a string, a single element list [output_value] is
911+
returned.
912+
If `names` is a list, a list with one value for each output name
913+
in names is returned: [output1_value, output2_value, ...].
914+
915+
Examples:
916+
>>> mod.getOutputsInitial()
917+
{'out1': '-0.4', 'out2': '1.2'}
918+
>>> mod.getOutputsInitial("out1")
919+
['-0.4']
920+
>>> mod.getOutputsInitial(["out1","out2"])
921+
['-0.4', '1.2']
922+
"""
923+
if names is None:
924+
return self._outputs
925+
if isinstance(names, str):
926+
return [self._outputs[names]]
927+
if isinstance(names, list):
928+
return [self._outputs[x] for x in names]
929+
930+
raise ModelicaSystemError("Unhandled input for getOutputsInitial()")
931+
932+
def getOutputsFinal(
933+
self,
934+
names: Optional[str | list[str]] = None,
935+
) -> dict[str, np.float64] | list[np.float64]:
936+
"""Get (final) values of output signals (at stopTime).
937+
938+
Args:
939+
names: Either None (default), a string with the output name,
940+
or a list of output name strings.
941+
Returns:
942+
If `names` is None, a dict in the format
943+
{output_name: output_value} is returned.
944+
If `names` is a string, a single element list [output_value] is
945+
returned.
946+
If `names` is a list, a list with one value for each output name
947+
in names is returned: [output1_value, output2_value, ...].
948+
949+
Examples:
950+
>>> mod.getOutputsFinal()
951+
{'out1': np.float64(-0.1234), 'out2': np.float64(2.1)}
952+
>>> mod.getOutputsFinal("out1")
953+
[np.float64(-0.1234)]
954+
>>> mod.getOutputsFinal(["out1","out2"])
955+
[np.float64(-0.1234), np.float64(2.1)]
956+
"""
957+
if not self._simulated:
958+
raise ModelicaSystemError("Please use getOuputsInitial() before the simulation was started!")
959+
960+
def get_outputs_solution(name_list: list[str]) -> None:
961+
for name in name_list:
962+
if name in self._outputs:
963+
value = self.getSolutions(name)
964+
self._outputs[name] = np.float64(value[0][-1])
965+
else:
966+
raise KeyError(f"{names} is not a valid output")
967+
968+
if names is None:
969+
get_outputs_solution(name_list=list(self._outputs.keys()))
970+
return self._outputs
971+
972+
if isinstance(names, str):
973+
get_outputs_solution(name_list=[names])
974+
return [self._outputs[names]]
975+
976+
if isinstance(names, list):
977+
get_outputs_solution(name_list=names)
978+
values = []
979+
for name in names:
980+
values.append(self._outputs[name])
981+
return values
982+
983+
raise ModelicaSystemError("Unhandled input for getOutputs()")
984+
843985
def getOutputs(
844986
self,
845987
names: Optional[str | list[str]] = None,
846-
) -> dict[str, str | numbers.Real] | list[str | numbers.Real]:
988+
) -> dict[str, np.float64] | list[np.float64]:
847989
"""Get values of output signals.
848990
849-
If called before simulate(), the initial values are returned as
850-
strings. If called after simulate(), the final values (at stopTime)
851-
are returned as numpy.float64.
991+
If called before simulate(), the initial values are returned.
992+
If called after simulate(), the final values (at stopTime) are returned.
993+
The return format is always numpy.float64.
852994
853995
Args:
854996
names: Either None (default), a string with the output name,
@@ -879,37 +1021,9 @@ def getOutputs(
8791021
[np.float64(-0.1234), np.float64(2.1)]
8801022
"""
8811023
if not self._simulated:
882-
if names is None:
883-
return self._outputs
884-
if isinstance(names, str):
885-
return [self._outputs[names]]
886-
return [self._outputs[x] for x in names]
887-
888-
if names is None:
889-
for name in self._outputs:
890-
value = self.getSolutions(name)
891-
self._outputs[name] = value[0][-1]
892-
return self._outputs
1024+
return self.getOutputsInitial(names=names)
8931025

894-
if isinstance(names, str):
895-
if names in self._outputs:
896-
value = self.getSolutions(names)
897-
self._outputs[names] = value[0][-1]
898-
return [self._outputs[names]]
899-
raise KeyError(names)
900-
901-
if isinstance(names, list):
902-
valuelist = []
903-
for name in names:
904-
if name in self._outputs:
905-
value = self.getSolutions(name)
906-
self._outputs[name] = value[0][-1]
907-
valuelist.append(value[0][-1])
908-
else:
909-
raise KeyError(name)
910-
return valuelist
911-
912-
raise ModelicaSystemError("Unhandled input for getOutputs()")
1026+
return self.getOutputsFinal(names=names)
9131027

9141028
def getSimulationOptions(
9151029
self,

tests/test_ModelicaSystem.py

Lines changed: 26 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -345,20 +345,33 @@ def test_getters(tmp_path):
345345
with pytest.raises(KeyError):
346346
mod.getInputs("thisInputDoesNotExist")
347347
# getOutputs before simulate()
348-
assert mod.getOutputs() == {'y': '-0.4'}
349-
assert mod.getOutputs("y") == ["-0.4"]
350-
assert mod.getOutputs(["y", "y"]) == ["-0.4", "-0.4"]
348+
output = mod.getOutputs()
349+
assert len(output) == 1
350+
assert 'y' in output.keys()
351+
assert np.isclose(output['y'], -0.4)
352+
assert np.isclose(mod.getOutputs("y"), -0.4)
353+
output = mod.getOutputs(["y", "y"])
354+
assert len(output) == 2
355+
assert np.isclose(output[0], -0.4)
356+
assert np.isclose(output[1], -0.4)
351357
with pytest.raises(KeyError):
352358
mod.getOutputs("thisOutputDoesNotExist")
353359

354360
# getContinuous before simulate():
355-
assert mod.getContinuous() == {
356-
'x': '1.0',
357-
'der(x)': None,
358-
'y': '-0.4'
359-
}
360-
assert mod.getContinuous("y") == ['-0.4']
361-
assert mod.getContinuous(["y", "x"]) == ['-0.4', '1.0']
361+
continuous = mod.getContinuous()
362+
assert len(continuous) == 3
363+
assert 'x' in continuous.keys()
364+
assert np.isclose(continuous['x'], 1.0)
365+
assert 'der(x)' in continuous.keys()
366+
assert np.isnan(continuous['der(x)'])
367+
assert 'y' in continuous.keys()
368+
assert np.isclose(continuous['y'], -0.4)
369+
continuous = mod.getContinuous('y')
370+
assert np.isclose(continuous, -0.4)
371+
continuous = mod.getContinuous(['y', 'x'])
372+
assert np.isclose(continuous[0], -0.4)
373+
assert np.isclose(continuous[1], 1.0)
374+
362375
with pytest.raises(KeyError):
363376
mod.getContinuous("a") # a is a parameter
364377

@@ -381,9 +394,9 @@ def test_getters(tmp_path):
381394
mod.getOutputs("thisOutputDoesNotExist")
382395

383396
# getContinuous after simulate() should return values at end of simulation:
384-
with pytest.raises(OMPython.ModelicaSystemError):
397+
with pytest.raises(KeyError):
385398
mod.getContinuous("a") # a is a parameter
386-
with pytest.raises(OMPython.ModelicaSystemError):
399+
with pytest.raises(KeyError):
387400
mod.getContinuous(["x", "a", "y"]) # a is a parameter
388401
d = mod.getContinuous()
389402
assert d.keys() == {"x", "der(x)", "y"}
@@ -393,7 +406,7 @@ def test_getters(tmp_path):
393406
assert mod.getContinuous("x") == [d["x"]]
394407
assert mod.getContinuous(["y", "x"]) == [d["y"], d["x"]]
395408

396-
with pytest.raises(OMPython.ModelicaSystemError):
409+
with pytest.raises(KeyError):
397410
mod.getContinuous("a") # a is a parameter
398411

399412
with pytest.raises(OMPython.ModelicaSystemError):

0 commit comments

Comments
 (0)