Skip to content

Commit

Permalink
Simplified set/get_variable_value interface, so that the special OSP …
Browse files Browse the repository at this point in the history
…set_initial call is not in general needed. Other improvements and re-testing
  • Loading branch information
eisDNV committed Dec 30, 2024
1 parent 60d804b commit 0f0861b
Show file tree
Hide file tree
Showing 8 changed files with 129 additions and 81 deletions.
63 changes: 27 additions & 36 deletions src/sim_explorer/case.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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."""
Expand All @@ -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
Expand Down Expand Up @@ -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)

Expand Down Expand Up @@ -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
Expand All @@ -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)}}:
Expand Down
2 changes: 1 addition & 1 deletion src/sim_explorer/json5.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion src/sim_explorer/simulator_interface.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
7 changes: 2 additions & 5 deletions src/sim_explorer/utils/misc.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
"""
Expand All @@ -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
76 changes: 44 additions & 32 deletions src/sim_explorer/utils/osp.py
Original file line number Diff line number Diff line change
@@ -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


# ==========================================
Expand Down Expand Up @@ -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",
{
Expand All @@ -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")
Expand Down Expand Up @@ -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).
Expand Down Expand Up @@ -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"<OspSystemStructure> 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
structure.update(connections)

return structure
17 changes: 17 additions & 0 deletions tests/data/MobileCrane/crane_table.js5
Original file line number Diff line number Diff line change
@@ -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"],
],
}
30 changes: 30 additions & 0 deletions tests/data/MobileCrane/crane_table.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
<OspSystemStructure xmlns="http://opensimulationplatform.com/MSMI/OSPSystemStructure" version="0.1">
<StartTime>0.0</StartTime>
<BaseStepSize>0.01</BaseStepSize>
<Simulators>
<Simulator name="simpleTable" source="../SimpleTable/SimpleTable.fmu" stepSize="0.01">
<InitialValues>
<InitialValue variable="interpolate">
<Boolean value="True" />
</InitialValue>
</InitialValues>
</Simulator>
<Simulator name="mobileCrane" source="MobileCrane.fmu" stepSize="0.01">
<InitialValues>
<InitialValue variable="pedestal.pedestalMass">
<Real value="5000.0" />
</InitialValue>
<InitialValue variable="boom.boom[0]">
<Real value="20.0" />
</InitialValue>
</InitialValues>
</Simulator>
</Simulators>
<Functions />
<Connections>
<VariableConnection>
<Variable simulator="simpleTable" name="outputs[0]" />
<Variable simulator="mobileCrane" name="pedestal.angularVelocity" />
</VariableConnection>
</Connections>
</OspSystemStructure>
13 changes: 7 additions & 6 deletions tests/test_osp_systemstructure.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from pathlib import Path

import pytest
from libcosimpy.CosimEnums import (
CosimVariableCausality,
CosimVariableType,
Expand Down Expand Up @@ -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()

0 comments on commit 0f0861b

Please sign in to comment.