Skip to content

Commit 78c9d8f

Browse files
authored
Merge branch 'master' into ModelicaSystem_requestApi
2 parents 2b13ba2 + 7616030 commit 78c9d8f

2 files changed

Lines changed: 125 additions & 68 deletions

File tree

OMPython/ModelicaSystem.py

Lines changed: 89 additions & 62 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
@@ -152,7 +151,7 @@ def arg_set(self, key: str, val: Optional[str | dict] = None) -> None:
152151
raise ModelicaSystemError(f"Invalid argument value for {repr(key)}: {repr(val)} (type: {type(val)})")
153152

154153
if key in self._args:
155-
logger.warning(f"Overwrite model executable argument: {repr(key)} = {repr(argval)} "
154+
logger.warning(f"Override model executable argument: {repr(key)} = {repr(argval)} "
156155
f"(was: {repr(self._args[key])})")
157156
self._args[key] = argval
158157

@@ -301,7 +300,7 @@ def __init__(
301300
lmodel: Optional[list[str | tuple[str, str]]] = None,
302301
commandLineOptions: Optional[str] = None,
303302
variableFilter: Optional[str] = None,
304-
customBuildDirectory: Optional[str | os.PathLike | pathlib.Path] = None,
303+
customBuildDirectory: Optional[str | os.PathLike] = None,
305304
omhome: Optional[str] = None,
306305
omc_process: Optional[OMCProcessLocal] = None,
307306
build: bool = True,
@@ -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
@@ -400,7 +397,7 @@ def __init__(
400397
self.setCommandLineOptions("--linearizationDumpLanguage=python")
401398
self.setCommandLineOptions("--generateSymbolicLinearization")
402399

403-
self._tempdir = self.setTempDirectory(customBuildDirectory)
400+
self._work_dir: pathlib.Path = self.setWorkDirectory(customBuildDirectory)
404401

405402
if self._file_name is not None:
406403
self._loadLibrary(lmodel=self._lmodel)
@@ -448,25 +445,34 @@ def _loadLibrary(self, lmodel: list):
448445
'1)["Modelica"]\n'
449446
'2)[("Modelica","3.2.3"), "PowerSystems"]\n')
450447

451-
def setTempDirectory(self, customBuildDirectory: Optional[str | os.PathLike | pathlib.Path] = None) -> pathlib.Path:
452-
# create a unique temp directory for each session and build the model in that directory
448+
def setWorkDirectory(self, customBuildDirectory: Optional[str | os.PathLike] = None) -> pathlib.Path:
449+
"""
450+
Define the work directory for the ModelicaSystem / OpenModelica session. The model is build within this
451+
directory. If no directory is defined a unique temporary directory is created.
452+
"""
453453
if customBuildDirectory is not None:
454-
if not os.path.exists(customBuildDirectory):
455-
raise IOError(f"{customBuildDirectory} does not exist")
456-
tempdir = pathlib.Path(customBuildDirectory).absolute()
454+
workdir = pathlib.Path(customBuildDirectory).absolute()
455+
if not workdir.is_dir():
456+
raise IOError(f"Provided work directory does not exists: {customBuildDirectory}!")
457457
else:
458-
tempdir = pathlib.Path(tempfile.mkdtemp()).absolute()
459-
if not tempdir.is_dir():
460-
raise IOError(f"{tempdir} could not be created")
458+
workdir = pathlib.Path(tempfile.mkdtemp()).absolute()
459+
if not workdir.is_dir():
460+
raise IOError(f"{workdir} could not be created")
461461

462-
logger.info("Define tempdir as %s", tempdir)
463-
exp = f'cd("{tempdir.as_posix()}")'
462+
logger.info("Define work dir as %s", workdir)
463+
exp = f'cd("{workdir.as_posix()}")'
464464
self.sendExpression(exp)
465465

466-
return tempdir
466+
# set the class variable _work_dir ...
467+
self._work_dir = workdir
468+
# ... and also return the defined path
469+
return workdir
467470

468471
def getWorkDirectory(self) -> pathlib.Path:
469-
return self._tempdir
472+
"""
473+
Return the defined working directory for this ModelicaSystem / OpenModelica session.
474+
"""
475+
return self._work_dir
470476

471477
def buildModel(self, variableFilter: Optional[str] = None):
472478
if variableFilter is not None:
@@ -480,8 +486,8 @@ def buildModel(self, variableFilter: Optional[str] = None):
480486
buildModelResult = self._requestApi(apiName="buildModel", entity=self._model_name, properties=varFilter)
481487
logger.debug("OM model build result: %s", buildModelResult)
482488

483-
self._xml_file = pathlib.Path(buildModelResult[0]).parent / buildModelResult[1]
484-
self._xmlparse()
489+
xml_file = pathlib.Path(buildModelResult[0]).parent / buildModelResult[1]
490+
self._xmlparse(xml_file=xml_file)
485491

486492
def sendExpression(self, expr: str, parsed: bool = True) -> Any:
487493
try:
@@ -512,30 +518,42 @@ def _requestApi(
512518

513519
return self.sendExpression(exp)
514520

515-
def _xmlparse(self):
516-
if not self._xml_file.is_file():
517-
raise ModelicaSystemError(f"XML file not generated: {self._xml_file}")
521+
def _xmlparse(self, xml_file: pathlib.Path):
522+
if not xml_file.is_file():
523+
raise ModelicaSystemError(f"XML file not generated: {xml_file}")
518524

519-
tree = ET.parse(self._xml_file)
525+
xml_content = xml_file.read_text()
526+
tree = ET.ElementTree(ET.fromstring(xml_content))
520527
rootCQ = tree.getroot()
521528
for attr in rootCQ.iter('DefaultExperiment'):
522529
for key in ("startTime", "stopTime", "stepSize", "tolerance",
523530
"solver", "outputFormat"):
524-
self._simulate_options[key] = attr.get(key)
531+
self._simulate_options[key] = str(attr.get(key))
525532

526533
for sv in rootCQ.iter('ScalarVariable'):
527-
scalar = {}
528-
for key in ("name", "description", "variability", "causality", "alias"):
529-
scalar[key] = sv.get(key)
530-
scalar["changeable"] = sv.get('isValueChangeable')
531-
scalar["aliasvariable"] = sv.get('aliasVariable')
534+
translations = {
535+
"alias": "alias",
536+
"aliasvariable": "aliasVariable",
537+
"causality": "causality",
538+
"changeable": "isValueChangeable",
539+
"description": "description",
540+
"name": "name",
541+
"variability": "variability",
542+
}
543+
544+
scalar: dict[str, Any] = {}
545+
for key_dst, key_src in translations.items():
546+
val = sv.get(key_src)
547+
scalar[key_dst] = None if val is None else str(val)
548+
532549
ch = list(sv)
533550
for att in ch:
534551
scalar["start"] = att.get('start')
535552
scalar["min"] = att.get('min')
536553
scalar["max"] = att.get('max')
537554
scalar["unit"] = att.get('unit')
538555

556+
# save parameters in the corresponding class variables
539557
if scalar["variability"] == "parameter":
540558
if scalar["name"] in self._override_variables:
541559
self._params[scalar["name"]] = self._override_variables[scalar["name"]]
@@ -947,7 +965,11 @@ def simulate_cmd(
947965
An instance if ModelicaSystemCmd to run the requested simulation.
948966
"""
949967

950-
om_cmd = ModelicaSystemCmd(runpath=self._tempdir, modelname=self._model_name, timeout=timeout)
968+
om_cmd = ModelicaSystemCmd(
969+
runpath=self.getWorkDirectory(),
970+
modelname=self._model_name,
971+
timeout=timeout,
972+
)
951973

952974
# always define the result file to use
953975
om_cmd.arg_set(key="r", val=result_file.as_posix())
@@ -959,16 +981,17 @@ def simulate_cmd(
959981
if simargs:
960982
om_cmd.args_set(args=simargs)
961983

962-
overrideFile = self._tempdir / f"{self._model_name}_override.txt"
963984
if self._override_variables or self._simulate_options_override:
964-
tmpdict = self._override_variables.copy()
965-
tmpdict.update(self._simulate_options_override)
966-
# write to override file
967-
with open(file=overrideFile, mode="w", encoding="utf-8") as fh:
968-
for key, value in tmpdict.items():
969-
fh.write(f"{key}={value}\n")
985+
override_file = result_file.parent / f"{result_file.stem}_override.txt"
986+
987+
override_content = (
988+
"\n".join([f"{key}={value}" for key, value in self._override_variables.items()])
989+
+ "\n".join([f"{key}={value}" for key, value in self._simulate_options_override.items()])
990+
+ "\n"
991+
)
970992

971-
om_cmd.arg_set(key="overrideFile", val=overrideFile.as_posix())
993+
override_file.write_text(override_content)
994+
om_cmd.arg_set(key="overrideFile", val=override_file.as_posix())
972995

973996
if self._inputs: # if model has input quantities
974997
for key in self._inputs:
@@ -1018,11 +1041,11 @@ def simulate(
10181041

10191042
if resultfile is None:
10201043
# default result file generated by OM
1021-
self._result_file = self._tempdir / f"{self._model_name}_res.mat"
1044+
self._result_file = self.getWorkDirectory() / f"{self._model_name}_res.mat"
10221045
elif os.path.exists(resultfile):
10231046
self._result_file = pathlib.Path(resultfile)
10241047
else:
1025-
self._result_file = self._tempdir / resultfile
1048+
self._result_file = self.getWorkDirectory() / resultfile
10261049

10271050
om_cmd = self.simulate_cmd(
10281051
result_file=self._result_file,
@@ -1088,9 +1111,9 @@ def getSolutions(self, varList: Optional[str | list[str]] = None, resultfile: Op
10881111
else:
10891112
result_file = pathlib.Path(resultfile)
10901113

1091-
# check for result file exits
1114+
# check if the result file exits
10921115
if not result_file.is_file():
1093-
raise ModelicaSystemError(f"Result file does not exist {result_file}")
1116+
raise ModelicaSystemError(f"Result file does not exist {result_file.as_posix()}")
10941117

10951118
# get absolute path
10961119
result_file = result_file.absolute()
@@ -1407,7 +1430,7 @@ def _createCSVData(self, csvfile: Optional[pathlib.Path] = None) -> pathlib.Path
14071430
interpolated_inputs[signal_name] = np.interp(
14081431
all_times,
14091432
signal[:, 0], # times
1410-
signal[:, 1] # values
1433+
signal[:, 1], # values
14111434
)
14121435

14131436
# Write CSV file
@@ -1419,16 +1442,17 @@ def _createCSVData(self, csvfile: Optional[pathlib.Path] = None) -> pathlib.Path
14191442
row = [
14201443
t, # time
14211444
*(interpolated_inputs[name][i] for name in input_names), # input values
1422-
0 # trailing 'end' column
1445+
0, # trailing 'end' column
14231446
]
14241447
csv_rows.append(row)
14251448

14261449
if csvfile is None:
1427-
csvfile = self._tempdir / f'{self._model_name}.csv'
1450+
csvfile = self.getWorkDirectory() / f'{self._model_name}.csv'
14281451

1429-
with open(file=csvfile, mode="w", encoding="utf-8", newline="") as fh:
1430-
writer = csv.writer(fh)
1431-
writer.writerows(csv_rows)
1452+
# basic definition of a CSV file using csv_rows as input
1453+
csv_content = "\n".join([",".join(map(str, row)) for row in csv_rows]) + "\n"
1454+
1455+
csvfile.write_text(csv_content)
14321456

14331457
return csvfile
14341458

@@ -1541,21 +1565,26 @@ def linearize(self, lintime: Optional[float] = None, simflags: Optional[str] = N
15411565
compatibility, because linearize() used to return `[A, B, C, D]`.
15421566
"""
15431567

1544-
if self._xml_file is None:
1568+
if len(self._quantities) == 0:
1569+
# if self._quantities has no content, the xml file was not parsed; see self._xmlparse()
15451570
raise ModelicaSystemError(
15461571
"Linearization cannot be performed as the model is not build, "
15471572
"use ModelicaSystem() to build the model first"
15481573
)
15491574

1550-
om_cmd = ModelicaSystemCmd(runpath=self._tempdir, modelname=self._model_name, timeout=timeout)
1575+
om_cmd = ModelicaSystemCmd(
1576+
runpath=self.getWorkDirectory(),
1577+
modelname=self._model_name,
1578+
timeout=timeout,
1579+
)
15511580

1552-
overrideLinearFile = self._tempdir / f'{self._model_name}_override_linear.txt'
1581+
overrideLinearFile = self.getWorkDirectory() / f'{self._model_name}_override_linear.txt'
15531582

15541583
with open(file=overrideLinearFile, mode="w", encoding="utf-8") as fh:
1555-
for key, value in self._override_variables.items():
1556-
fh.write(f"{key}={value}\n")
1557-
for key, value in self._linearization_options.items():
1558-
fh.write(f"{key}={value}\n")
1584+
for key1, value1 in self._override_variables.items():
1585+
fh.write(f"{key1}={value1}\n")
1586+
for key2, value2 in self._linearization_options.items():
1587+
fh.write(f"{key2}={value2}\n")
15591588

15601589
om_cmd.arg_set(key="overrideFile", val=overrideLinearFile.as_posix())
15611590

@@ -1579,19 +1608,17 @@ def linearize(self, lintime: Optional[float] = None, simflags: Optional[str] = N
15791608
om_cmd.args_set(args=simargs)
15801609

15811610
# the file create by the model executable which contains the matrix and linear inputs, outputs and states
1582-
linear_file = self._tempdir / "linearized_model.py"
1583-
1611+
linear_file = self.getWorkDirectory() / "linearized_model.py"
15841612
linear_file.unlink(missing_ok=True)
15851613

15861614
returncode = om_cmd.run()
15871615
if returncode != 0:
15881616
raise ModelicaSystemError(f"Linearize failed with return code: {returncode}")
1589-
1590-
self._simulated = True
1591-
15921617
if not linear_file.exists():
15931618
raise ModelicaSystemError(f"Linearization failed: {linear_file} not found!")
15941619

1620+
self._simulated = True
1621+
15951622
# extract data from the python file with the linearized model using the ast module - this allows to get the
15961623
# needed information without executing the created code
15971624
linear_data = {}

0 commit comments

Comments
 (0)