Skip to content

Commit a92579b

Browse files
authored
Merge branch 'master' into ModelicaSystemCmd_spelling_fix
2 parents 558fab6 + a351732 commit a92579b

2 files changed

Lines changed: 84 additions & 42 deletions

File tree

OMPython/ModelicaSystem.py

Lines changed: 48 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,6 @@
3333
"""
3434

3535
import ast
36-
import csv
3736
from dataclasses import dataclass
3837
import logging
3938
import numbers
@@ -361,8 +360,7 @@ def __init__(
361360
self._override_variables: dict[str, str] = {}
362361
self._simulate_options_override: dict[str, str] = {}
363362
self._linearization_options = {'startTime': 0.0, 'stopTime': 1.0, 'stepSize': 0.002, 'tolerance': 1e-8}
364-
self._optimization_options = {'startTime': 0.0, 'stopTime': 1.0, 'numberOfIntervals': 500, 'stepSize': 0.002,
365-
'tolerance': 1e-8}
363+
self._optimization_options = self._linearization_options | {'numberOfIntervals': 500}
366364
self._linearized_inputs: list[str] = [] # linearization input list
367365
self._linearized_outputs: list[str] = [] # linearization output list
368366
self._linearized_states: list[str] = [] # linearization states list
@@ -383,7 +381,6 @@ def __init__(
383381
if not isinstance(lmodel, list):
384382
raise ModelicaSystemError(f"Invalid input type for lmodel: {type(lmodel)} - list expected!")
385383

386-
self._xml_file = None
387384
self._lmodel = lmodel # may be needed if model is derived from other model
388385
self._model_name = modelName # Model class name
389386
self._file_name = pathlib.Path(fileName).resolve() if fileName is not None else None # Model file/package name
@@ -480,8 +477,8 @@ def buildModel(self, variableFilter: Optional[str] = None):
480477
buildModelResult = self._requestApi("buildModel", self._model_name, properties=varFilter)
481478
logger.debug("OM model build result: %s", buildModelResult)
482479

483-
self._xml_file = pathlib.Path(buildModelResult[0]).parent / buildModelResult[1]
484-
self._xmlparse()
480+
xml_file = pathlib.Path(buildModelResult[0]).parent / buildModelResult[1]
481+
self._xmlparse(xml_file=xml_file)
485482

486483
def sendExpression(self, expr: str, parsed: bool = True):
487484
try:
@@ -507,30 +504,42 @@ def _requestApi(self, apiName, entity=None, properties=None): # 2
507504

508505
return self.sendExpression(exp)
509506

510-
def _xmlparse(self):
511-
if not self._xml_file.is_file():
512-
raise ModelicaSystemError(f"XML file not generated: {self._xml_file}")
507+
def _xmlparse(self, xml_file: pathlib.Path):
508+
if not xml_file.is_file():
509+
raise ModelicaSystemError(f"XML file not generated: {xml_file}")
513510

514-
tree = ET.parse(self._xml_file)
511+
xml_content = xml_file.read_text()
512+
tree = ET.ElementTree(ET.fromstring(xml_content))
515513
rootCQ = tree.getroot()
516514
for attr in rootCQ.iter('DefaultExperiment'):
517515
for key in ("startTime", "stopTime", "stepSize", "tolerance",
518516
"solver", "outputFormat"):
519-
self._simulate_options[key] = attr.get(key)
517+
self._simulate_options[key] = str(attr.get(key))
520518

521519
for sv in rootCQ.iter('ScalarVariable'):
522-
scalar = {}
523-
for key in ("name", "description", "variability", "causality", "alias"):
524-
scalar[key] = sv.get(key)
525-
scalar["changeable"] = sv.get('isValueChangeable')
526-
scalar["aliasvariable"] = sv.get('aliasVariable')
520+
translations = {
521+
"alias": "alias",
522+
"aliasvariable": "aliasVariable",
523+
"causality": "causality",
524+
"changeable": "isValueChangeable",
525+
"description": "description",
526+
"name": "name",
527+
"variability": "variability",
528+
}
529+
530+
scalar: dict[str, Any] = {}
531+
for key_dst, key_src in translations.items():
532+
val = sv.get(key_src)
533+
scalar[key_dst] = None if val is None else str(val)
534+
527535
ch = list(sv)
528536
for att in ch:
529537
scalar["start"] = att.get('start')
530538
scalar["min"] = att.get('min')
531539
scalar["max"] = att.get('max')
532540
scalar["unit"] = att.get('unit')
533541

542+
# save parameters in the corresponding class variables
534543
if scalar["variability"] == "parameter":
535544
if scalar["name"] in self._override_variables:
536545
self._params[scalar["name"]] = self._override_variables[scalar["name"]]
@@ -954,16 +963,17 @@ def simulate_cmd(
954963
if simargs:
955964
om_cmd.args_set(args=simargs)
956965

957-
overrideFile = self._tempdir / f"{self._model_name}_override.txt"
958966
if self._override_variables or self._simulate_options_override:
959-
tmpdict = self._override_variables.copy()
960-
tmpdict.update(self._simulate_options_override)
961-
# write to override file
962-
with open(file=overrideFile, mode="w", encoding="utf-8") as fh:
963-
for key, value in tmpdict.items():
964-
fh.write(f"{key}={value}\n")
967+
override_file = result_file.parent / f"{result_file.stem}_override.txt"
968+
969+
override_content = (
970+
"\n".join([f"{key}={value}" for key, value in self._override_variables.items()])
971+
+ "\n".join([f"{key}={value}" for key, value in self._simulate_options_override.items()])
972+
+ "\n"
973+
)
965974

966-
om_cmd.arg_set(key="overrideFile", val=overrideFile.as_posix())
975+
override_file.write_text(override_content)
976+
om_cmd.arg_set(key="overrideFile", val=override_file.as_posix())
967977

968978
if self._inputs: # if model has input quantities
969979
for key in self._inputs:
@@ -1083,9 +1093,9 @@ def getSolutions(self, varList: Optional[str | list[str]] = None, resultfile: Op
10831093
else:
10841094
result_file = pathlib.Path(resultfile)
10851095

1086-
# check for result file exits
1096+
# check if the result file exits
10871097
if not result_file.is_file():
1088-
raise ModelicaSystemError(f"Result file does not exist {result_file}")
1098+
raise ModelicaSystemError(f"Result file does not exist {result_file.as_posix()}")
10891099

10901100
# get absolute path
10911101
result_file = result_file.absolute()
@@ -1402,7 +1412,7 @@ def _createCSVData(self, csvfile: Optional[pathlib.Path] = None) -> pathlib.Path
14021412
interpolated_inputs[signal_name] = np.interp(
14031413
all_times,
14041414
signal[:, 0], # times
1405-
signal[:, 1] # values
1415+
signal[:, 1], # values
14061416
)
14071417

14081418
# Write CSV file
@@ -1414,16 +1424,17 @@ def _createCSVData(self, csvfile: Optional[pathlib.Path] = None) -> pathlib.Path
14141424
row = [
14151425
t, # time
14161426
*(interpolated_inputs[name][i] for name in input_names), # input values
1417-
0 # trailing 'end' column
1427+
0, # trailing 'end' column
14181428
]
14191429
csv_rows.append(row)
14201430

14211431
if csvfile is None:
14221432
csvfile = self._tempdir / f'{self._model_name}.csv'
14231433

1424-
with open(file=csvfile, mode="w", encoding="utf-8", newline="") as fh:
1425-
writer = csv.writer(fh)
1426-
writer.writerows(csv_rows)
1434+
# basic definition of a CSV file using csv_rows as input
1435+
csv_content = "\n".join([",".join(map(str, row)) for row in csv_rows]) + "\n"
1436+
1437+
csvfile.write_text(csv_content)
14271438

14281439
return csvfile
14291440

@@ -1535,7 +1546,8 @@ def linearize(self, lintime: Optional[float] = None, simflags: Optional[str] = N
15351546
compatibility, because linearize() used to return `[A, B, C, D]`.
15361547
"""
15371548

1538-
if self._xml_file is None:
1549+
if len(self._quantities) == 0:
1550+
# if self._quantities has no content, the xml file was not parsed; see self._xmlparse()
15391551
raise ModelicaSystemError(
15401552
"Linearization cannot be performed as the model is not build, "
15411553
"use ModelicaSystem() to build the model first"
@@ -1546,10 +1558,10 @@ def linearize(self, lintime: Optional[float] = None, simflags: Optional[str] = N
15461558
overrideLinearFile = self._tempdir / f'{self._model_name}_override_linear.txt'
15471559

15481560
with open(file=overrideLinearFile, mode="w", encoding="utf-8") as fh:
1549-
for key, value in self._override_variables.items():
1550-
fh.write(f"{key}={value}\n")
1551-
for key, value in self._linearization_options.items():
1552-
fh.write(f"{key}={value}\n")
1561+
for key1, value1 in self._override_variables.items():
1562+
fh.write(f"{key1}={value1}\n")
1563+
for key2, value2 in self._linearization_options.items():
1564+
fh.write(f"{key2}={value2}\n")
15531565

15541566
om_cmd.arg_set(key="overrideFile", val=overrideLinearFile.as_posix())
15551567

OMPython/OMCSession.py

Lines changed: 36 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -483,11 +483,17 @@ def __del__(self):
483483
self._omc_process = None
484484

485485
def get_port(self) -> Optional[str]:
486+
"""
487+
Get the port to connect to the OMC process.
488+
"""
486489
if not isinstance(self._omc_port, str):
487490
raise OMCSessionException(f"Invalid port to connect to OMC process: {self._omc_port}")
488491
return self._omc_port
489492

490493
def get_log(self) -> str:
494+
"""
495+
Get the log file content of the OMC session.
496+
"""
491497
if self._omc_loghandle is None:
492498
raise OMCSessionException("Log file not available!")
493499

@@ -509,6 +515,9 @@ def _get_portfile_path(self) -> Optional[pathlib.Path]:
509515

510516

511517
class OMCProcessPort(OMCProcess):
518+
"""
519+
OMCProcess implementation which uses a port to connect to an already running OMC server.
520+
"""
512521

513522
def __init__(
514523
self,
@@ -519,6 +528,9 @@ def __init__(
519528

520529

521530
class OMCProcessLocal(OMCProcess):
531+
"""
532+
OMCProcess implementation which runs the OMC server locally on the machine (Linux / Windows).
533+
"""
522534

523535
def __init__(
524536
self,
@@ -600,6 +612,9 @@ def _omc_port_get(self) -> str:
600612

601613

602614
class OMCProcessDockerHelper(OMCProcess):
615+
"""
616+
Base class for OMCProcess implementations which run the OMC server in a Docker container.
617+
"""
603618

604619
def __init__(
605620
self,
@@ -692,20 +707,29 @@ def _omc_port_get(self) -> str:
692707
return port
693708

694709
def get_server_address(self) -> Optional[str]:
710+
"""
711+
Get the server address of the OMC server running in a Docker container.
712+
"""
695713
if self._dockerNetwork == "separate" and isinstance(self._dockerCid, str):
696714
output = subprocess.check_output(["docker", "inspect", self._dockerCid]).decode().strip()
697715
return json.loads(output)[0]["NetworkSettings"]["IPAddress"]
698716

699717
return None
700718

701719
def get_docker_container_id(self) -> str:
720+
"""
721+
Get the Docker container ID of the Docker container with the OMC server.
722+
"""
702723
if not isinstance(self._dockerCid, str):
703724
raise OMCSessionException(f"Invalid docker container ID: {self._dockerCid}!")
704725

705726
return self._dockerCid
706727

707728

708729
class OMCProcessDocker(OMCProcessDockerHelper):
730+
"""
731+
OMC process running in a Docker container.
732+
"""
709733

710734
def __init__(
711735
self,
@@ -764,9 +788,8 @@ def _docker_omc_cmd(
764788
if sys.platform == "win32":
765789
extraFlags = ["-d=zmqDangerousAcceptConnectionsFromAnywhere"]
766790
if not self._interactivePort:
767-
raise OMCSessionException("docker on Windows requires knowing which port to connect to. For "
768-
"dockerContainer=..., the container needs to have already manually exposed "
769-
"this port when it was started (-p 127.0.0.1:n:n) or you get an error later.")
791+
raise OMCSessionException("docker on Windows requires knowing which port to connect to - "
792+
"please set the interactivePort argument")
770793

771794
if sys.platform == "win32":
772795
if isinstance(self._interactivePort, str):
@@ -847,6 +870,9 @@ def _docker_omc_start(self) -> Tuple[subprocess.Popen, DummyPopen, str]:
847870

848871

849872
class OMCProcessDockerContainer(OMCProcessDockerHelper):
873+
"""
874+
OMC process running in a Docker container (by container ID).
875+
"""
850876

851877
def __init__(
852878
self,
@@ -892,9 +918,10 @@ def _docker_omc_cmd(self, omc_path_and_args_list) -> list:
892918
if sys.platform == "win32":
893919
extraFlags = ["-d=zmqDangerousAcceptConnectionsFromAnywhere"]
894920
if not self._interactivePort:
895-
raise OMCSessionException("docker on Windows requires knowing which port to connect to. For "
896-
"dockerContainer=..., the container needs to have already manually exposed "
897-
"this port when it was started (-p 127.0.0.1:n:n) or you get an error later.")
921+
raise OMCSessionException("Docker on Windows requires knowing which port to connect to - "
922+
"Please set the interactivePort argument. Furthermore, the container needs "
923+
"to have already manually exposed this port when it was started "
924+
"(-p 127.0.0.1:n:n) or you get an error later.")
898925

899926
if isinstance(self._interactivePort, int):
900927
extraFlags = extraFlags + [f"--interactivePort={int(self._interactivePort)}"]
@@ -936,6 +963,9 @@ def _docker_omc_start(self) -> Tuple[subprocess.Popen, DummyPopen]:
936963

937964

938965
class OMCProcessWSL(OMCProcess):
966+
"""
967+
OMC process running in Windows Subsystem for Linux (WSL).
968+
"""
939969

940970
def __init__(
941971
self,

0 commit comments

Comments
 (0)