From 0f0861bd6e10fd67058b3c1763ef684263bfab74 Mon Sep 17 00:00:00 2001 From: Eisinger Date: Mon, 30 Dec 2024 23:23:40 +0100 Subject: [PATCH] Simplified set/get_variable_value interface, so that the special OSP set_initial call is not in general needed. Other improvements and re-testing --- src/sim_explorer/case.py | 63 +++++++++----------- src/sim_explorer/json5.py | 2 +- src/sim_explorer/simulator_interface.py | 2 +- src/sim_explorer/utils/misc.py | 7 +-- src/sim_explorer/utils/osp.py | 76 ++++++++++++++----------- tests/data/MobileCrane/crane_table.js5 | 17 ++++++ tests/data/MobileCrane/crane_table.xml | 30 ++++++++++ tests/test_osp_systemstructure.py | 13 +++-- 8 files changed, 129 insertions(+), 81 deletions(-) create mode 100644 tests/data/MobileCrane/crane_table.js5 create mode 100644 tests/data/MobileCrane/crane_table.xml diff --git a/src/sim_explorer/case.py b/src/sim_explorer/case.py index 1ddb245..b5838a8 100644 --- a/src/sim_explorer/case.py +++ b/src/sim_explorer/case.py @@ -167,7 +167,12 @@ def _add_action(self, typ: str, action: Callable, args: tuple, at_time: float): else: raise AssertionError(f"Unknown typ {typ} in _add_action") assert isinstance(at_time, (float, int)), f"Actions require a defined time as float. Found {at_time}" - if at_time in dct: + if typ == "set" and at_time <= self.special["startTime"] and action != self.cases.simulator.set_initial: + # isinstance( self.simulator, SystemInterfaceOSP)): + action = self.cases.simulator.set_initial # OSP requires a special initial value function + for i in range(len(args[2])): # set_initial takes only single variables + self._add_action(typ, action, (args[0], args[1], args[2][i], args[3][i]), at_time) + elif at_time in dct: for i, act in enumerate(dct[at_time]): if act.func.__name__ == action.__name__ and all(act.args[k] == args[k] for k in range(2)): # the type of action, the model id and the variable type match @@ -404,36 +409,21 @@ def read_spec_item(self, key: str, value: Any | None = None): else: # set actions assert value is not None, f"Variable {key}: Value needed for 'set' actions." assert at_time_type in ("set"), f"Unknown @time type {at_time_type} for case '{self.name}'" - if at_time_arg <= self.special["startTime"]: # False: #?? set_initial() does so far not work??# - # SimulatorInterface.default_initial(cvar_info["causality"], cvar_info["variability"]) < 3: - assert at_time_arg <= self.special["startTime"], f"Initial settings at time {at_time_arg}?" - for inst in cvar_info["instances"]: # ask simulator to provide function to set variables: - _inst = self.cases.simulator.component_id_from_name(inst) - if not self.cases.simulator.allowed_action("set", _inst, tuple(var_refs), 0): - raise AssertionError(self.cases.simulator.message) from None - for ref, val in zip(var_refs, var_vals, strict=False): - self._add_action( - at_time_type, - self.cases.simulator.set_initial, - (_inst, cvar_info["type"], ref, val), - at_time_arg * self.cases.timefac, - ) - else: - for inst in cvar_info["instances"]: # ask simulator to provide function to set variables: - _inst = self.cases.simulator.component_id_from_name(inst) - if not self.cases.simulator.allowed_action("set", _inst, tuple(var_refs), at_time_arg): - raise AssertionError(self.cases.simulator.message) from None - self._add_action( - at_time_type, - self.cases.simulator.set_variable_value, - ( - _inst, - cvar_info["type"], - tuple(var_refs), - tuple(var_vals), - ), - at_time_arg * self.cases.timefac, - ) + for inst in cvar_info["instances"]: # ask simulator to provide function to set variables: + _inst = self.cases.simulator.component_id_from_name(inst) + if not self.cases.simulator.allowed_action("set", _inst, tuple(var_refs), at_time_arg): + raise AssertionError(self.cases.simulator.message) from None + self._add_action( + at_time_type, + self.cases.simulator.set_variable_value, + ( + _inst, + cvar_info["type"], + tuple(var_refs), + tuple(var_vals), + ), + at_time_arg * self.cases.timefac, + ) def list_cases(self, as_name: bool = True, flat: bool = False) -> list[str] | list[Case]: """List this case and all sub-cases recursively, as name or case objects.""" @@ -455,7 +445,7 @@ def _ensure_specials(self, special: dict[str, Any]) -> dict[str, Any]: def get_from_config(element: str, default: float | None = None): if isinstance(self.cases.simulator.sysconfig, Path): - info = from_xml(self.cases.simulator.sysconfig, sub=None, xpath=".//{*}" + element) + info = from_xml(self.cases.simulator.sysconfig, sub=None).findall(".//{*}" + element) if not len(info): return default txt = info[0].text @@ -548,7 +538,7 @@ def do_actions(_t: float, _a, _iter, time: int, record: bool = True): self.res.add(time / self.cases.timefac, a.args[0], a.args[1], a.args[2], a()) self.cases.simulator.reset() - + if dump is not None: self.res.save(dump) @@ -607,7 +597,8 @@ def __init__(self, spec: str | Path, simulator: SimulatorInterface | None = None self.file = Path(spec) # everything relative to the folder of this file! assert self.file.exists(), f"Cases spec file {spec} not found" self.js = Json5(spec) - log_level = CosimLogLevel[self.js.jspath("$.header.logLevel") or "FATAL"] + # del log_level = CosimLogLevel[self.js.jspath("$.header.logLevel") or "FATAL"] + log_level = self.js.jspath("$.header.logLevel") or "fatal" if simulator is None: modelfile = self.js.jspath("$.header.modelFile", str) or "OspSystemStructure.xml" path = self.file.parent / modelfile @@ -617,13 +608,13 @@ def __init__(self, spec: str | Path, simulator: SimulatorInterface | None = None system=path, name=self.js.jspath("$.header.name", str) or "", description=self.js.jspath("$.header.description", str) or "", - log_level=log_level, + log_level=log_level.upper(), ) except Exception as err: raise AssertionError(f"'modelFile' needed from spec: {err}") from err else: self.simulator = simulator # SimulatorInterface( simulator = simulator) - log_output_level(log_level) + log_output_level(CosimLogLevel[log_level.upper()]) self.timefac = self._get_time_unit() * 1e9 # internally OSP uses pico-seconds as integer! # read the 'variables' section and generate dict { alias : { (instances), (variables)}}: diff --git a/src/sim_explorer/json5.py b/src/sim_explorer/json5.py index 4f32cbb..4c421df 100644 --- a/src/sim_explorer/json5.py +++ b/src/sim_explorer/json5.py @@ -421,7 +421,7 @@ def _value(self): elif isinstance(v, str) and not len(v): # might be empty due to trailing ',' return "" - if q2 >= 0: # explicitly quoted values are treated as strings! + if q2 >= 0: # explicitly quoted values are treated as strings! return str(v) try: return int(v) # type: ignore diff --git a/src/sim_explorer/simulator_interface.py b/src/sim_explorer/simulator_interface.py index 9de7e30..2f6a30f 100644 --- a/src/sim_explorer/simulator_interface.py +++ b/src/sim_explorer/simulator_interface.py @@ -79,7 +79,7 @@ def __init__( name: str | None = None, description: str = "", simulator: CosimExecution | None = None, - log_level: str = 'fatal', + log_level: str = "fatal", fmus_available: bool = False, ): self.name = name # overwrite if the system includes that diff --git a/src/sim_explorer/utils/misc.py b/src/sim_explorer/utils/misc.py index 8b5802b..d8574c4 100644 --- a/src/sim_explorer/utils/misc.py +++ b/src/sim_explorer/utils/misc.py @@ -19,7 +19,7 @@ def match_with_wildcard(findtxt: str, matchtxt: str) -> bool: return m is not None -def from_xml(file: Path, sub: str | None = None, xpath: str | None = None) -> ET.Element | list[ET.Element]: +def from_xml(file: Path, sub: str | None = None) -> ET.Element: """Retrieve the Element root from a zipped file (retrieve sub), or an xml file (sub unused). If xpath is provided only the xpath matching element (using findall) is returned. """ @@ -40,7 +40,4 @@ def from_xml(file: Path, sub: str | None = None, xpath: str | None = None) -> ET except ET.ParseError as err: raise Exception(f"File '{file}' does not seem to be a proper xml file") from err - if xpath is None: - return et - else: - return et.findall(xpath) + return et diff --git a/src/sim_explorer/utils/osp.py b/src/sim_explorer/utils/osp.py index 1b4acb4..fe8a9a6 100644 --- a/src/sim_explorer/utils/osp.py +++ b/src/sim_explorer/utils/osp.py @@ -1,8 +1,10 @@ import xml.etree.ElementTree as ET # noqa: N817 from pathlib import Path +from component_model.utils.xml import read_xml + from sim_explorer.json5 import Json5 -from component_model.utils.xml import read_xml +from typing import Any # ========================================== @@ -76,6 +78,7 @@ def make_initial_value(var: str, val: bool | int | float | str): _simulators = ET.Element("Simulators") if simulators is not None: for m, props in simulators.items(): + print("COMPONENT", m, props) simulator = ET.Element( "Simulator", { @@ -84,10 +87,12 @@ def make_initial_value(var: str, val: bool | int | float | str): "stepSize": str(props.get("stepSize", base_step)), }, ) - if "initialValues" in props: - initial = ET.SubElement(simulator, "InitialValues") - for var, value in props["initialValues"].items(): + initial = ET.Element("InitialValues") + for var, value in props.items(): + if var not in ("source", "stepSize"): initial.append(make_initial_value(var, value)) + if len(initial): + simulator.append(initial) _simulators.append(simulator) # print(f"Model {m}: {simulator}. Length {len(simulators)}") # ET.ElementTree(simulators).write("Test.xml") @@ -180,6 +185,7 @@ def make_connection(main: str, sub1: str, attr1: dict, sub2: str, attr2: dict): tree.write(file, encoding="utf-8") return file + def osp_system_structure_from_js5(file: Path, dest: Path | None = None): """Make a OspSystemStructure file from a js5 specification. The js5 specification is closely related to the make_osp_systemStructure() function (and uses it). @@ -207,46 +213,52 @@ def osp_system_structure_from_js5(file: Path, dest: Path | None = None): return ss -def read_system_structure_xml( file: Path): + +def read_system_structure_xml(file: Path) -> dict[str, Any]: """Read the system structure in xml format and return as js5 dict, similar to ..._from_js5.""" - def type_value( typ:str, value:str): - return {'Real': float, 'Integer': int, 'Boolean': bool, 'String': str}[typ]( value) - el = read_xml( file) + + def type_value(el: ET.Element): + typ = el.tag.split("}")[1] + value = el.get("value") + return {"Real": float, "Integer": int, "Boolean": bool, "String": str}[typ](value) + + el = read_xml(file) assert el.tag.endswith("OspSystemStructure"), f" expected. Found {el.tag}" ns = el.tag.split("{")[1].split("}")[0] bss = el.find(".//BaseStepSize") or 0.01 header = { - "xmlns" : ns, - "version" : el.get( "version", "'0.1'"), + "xmlns": ns, + "version": el.get("version", "'0.1'"), "StartTime": el.find(".//StartTime") or 0.0, - "Algorithm": el.find(".//Algorithm") or 'fixedStep', + "Algorithm": el.find(".//Algorithm") or "fixedStep", "BaseStepSize": bss, - } - - simulators : dict = {} + } + + simulators: dict = {} for sim in el.findall(".//{*}Simulator"): props = { - "source" : sim.get('source'), - "stepSize" : sim.get('stepSize', bss), - } + "source": sim.get("source"), + "stepSize": sim.get("stepSize", bss), + } for initial in sim.findall(".//{*}InitialValue"): - props.update( { initial.get('variable') : type_value( initial[0].tag.split('}')[1], initial[0].get('value'))}) - simulators.update( {sim.get('name') : props}) - - structure = {"header" : header, "Simulators" : simulators} - connections = {} + props.update({str(initial.get("variable")): type_value(initial[0])}) + assert "name" in sim.attrib, f"Required attribute 'name' not found in {sim.tag}, attrib:{sim.attrib}" + simulators.update({sim.get("name"): props}) + + structure = {"header": header, "Simulators": simulators} + connections: dict = {} for c in ("Variable", "Signal", "Group", "SignalGroup"): - cons = [] - for con in el.findall(".//{*}"+c+"Connection"): + cons: list = [] + for con in el.findall(".//{*}" + c + "Connection"): assert len(con) == 2, f"Two sub-elements expected. Found {len(con)}" - props = [] + _con: list = [] for i in range(2): for p in con[i].attrib.values(): - props.append( p) - cons.append( props) - if len( cons): - connections.update( {"Connections"+c : cons}) + _con.append(p) + cons.append(_con) + if len(cons): + connections.update({"Connections" + c: cons}) if len(connections): - structure.update( connections) - - return structure \ No newline at end of file + structure.update(connections) + + return structure diff --git a/tests/data/MobileCrane/crane_table.js5 b/tests/data/MobileCrane/crane_table.js5 new file mode 100644 index 0000000..57ed315 --- /dev/null +++ b/tests/data/MobileCrane/crane_table.js5 @@ -0,0 +1,17 @@ +{ +header : { + xmlns : "http://opensimulationplatform.com/MSMI/OSPSystemStructure", + version : "0.1", + StartTime : 0.0, + Algorithm : "fixedStep", + BaseStepSize : 0.01, + }, +Simulators : { + simpleTable : {source: "../SimpleTable/SimpleTable.fmu", interpolate: True}, + mobileCrane : {source: "MobileCrane.fmu" stepSize: 0.01, + pedestal.pedestalMass: 5000.0, boom.boom[0]: 20.0}, + }, +ConnectionsVariable : [ + ["simpleTable", "outputs[0]", "mobileCrane", "pedestal.angularVelocity"], + ], +} \ No newline at end of file diff --git a/tests/data/MobileCrane/crane_table.xml b/tests/data/MobileCrane/crane_table.xml new file mode 100644 index 0000000..fc009e9 --- /dev/null +++ b/tests/data/MobileCrane/crane_table.xml @@ -0,0 +1,30 @@ + + 0.0 + 0.01 + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/tests/test_osp_systemstructure.py b/tests/test_osp_systemstructure.py index e5ebc5e..b82b2ec 100644 --- a/tests/test_osp_systemstructure.py +++ b/tests/test_osp_systemstructure.py @@ -1,5 +1,6 @@ from pathlib import Path +import pytest from libcosimpy.CosimEnums import ( CosimVariableCausality, CosimVariableType, @@ -40,15 +41,15 @@ def test_osp_structure(): def test_system_structure_from_js5(): - osp_system_structure_from_js5(Path(__file__).parent / "data" / "crane_table.js5") + osp_system_structure_from_js5(Path(__file__).parent / "data" / "MobileCrane" / "crane_table.js5") if __name__ == "__main__": - # retcode = pytest.main(["-rA", "-v", __file__]) - # assert retcode == 0, f"Non-zero return code {retcode}" - import os + retcode = pytest.main(["-rA", "-v", __file__]) + assert retcode == 0, f"Non-zero return code {retcode}" + # import os - os.chdir(Path(__file__).parent / "test_working_directory") - test_system_structure() + # os.chdir(Path(__file__).parent / "test_working_directory") + # test_system_structure() # test_osp_structure() # test_system_structure_from_js5()