Skip to content

Commit 59d0e53

Browse files
authored
Merge branch 'master' into ModelicaSystemCmd_arg_set
2 parents ac6d126 + c44b088 commit 59d0e53

2 files changed

Lines changed: 139 additions & 76 deletions

File tree

OMPython/ModelicaSystem.py

Lines changed: 103 additions & 70 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
@@ -207,7 +206,7 @@ def override2str(
207206
raise ModelicaSystemError(f"Invalid argument value for {repr(key)}: {repr(val)} (type: {type(val)})")
208207

209208
if key in self._args:
210-
logger.warning(f"Overwrite model executable argument: {repr(key)} = {repr(argval)} "
209+
logger.warning(f"Override model executable argument: {repr(key)} = {repr(argval)} "
211210
f"(was: {repr(self._args[key])})")
212211
self._args[key] = argval
213212

@@ -355,7 +354,7 @@ def __init__(
355354
lmodel: Optional[list[str | tuple[str, str]]] = None,
356355
commandLineOptions: Optional[str] = None,
357356
variableFilter: Optional[str] = None,
358-
customBuildDirectory: Optional[str | os.PathLike | pathlib.Path] = None,
357+
customBuildDirectory: Optional[str | os.PathLike] = None,
359358
omhome: Optional[str] = None,
360359
omc_process: Optional[OMCProcessLocal] = None,
361360
build: bool = True,
@@ -415,8 +414,7 @@ def __init__(
415414
self._override_variables: dict[str, str] = {}
416415
self._simulate_options_override: dict[str, str] = {}
417416
self._linearization_options = {'startTime': 0.0, 'stopTime': 1.0, 'stepSize': 0.002, 'tolerance': 1e-8}
418-
self._optimization_options = {'startTime': 0.0, 'stopTime': 1.0, 'numberOfIntervals': 500, 'stepSize': 0.002,
419-
'tolerance': 1e-8}
417+
self._optimization_options = self._linearization_options | {'numberOfIntervals': 500}
420418
self._linearized_inputs: list[str] = [] # linearization input list
421419
self._linearized_outputs: list[str] = [] # linearization output list
422420
self._linearized_states: list[str] = [] # linearization states list
@@ -437,7 +435,6 @@ def __init__(
437435
if not isinstance(lmodel, list):
438436
raise ModelicaSystemError(f"Invalid input type for lmodel: {type(lmodel)} - list expected!")
439437

440-
self._xml_file = None
441438
self._lmodel = lmodel # may be needed if model is derived from other model
442439
self._model_name = modelName # Model class name
443440
self._file_name = pathlib.Path(fileName).resolve() if fileName is not None else None # Model file/package name
@@ -454,7 +451,7 @@ def __init__(
454451
self.setCommandLineOptions("--linearizationDumpLanguage=python")
455452
self.setCommandLineOptions("--generateSymbolicLinearization")
456453

457-
self._tempdir = self.setTempDirectory(customBuildDirectory)
454+
self._work_dir: pathlib.Path = self.setWorkDirectory(customBuildDirectory)
458455

459456
if self._file_name is not None:
460457
self._loadLibrary(lmodel=self._lmodel)
@@ -488,7 +485,7 @@ def _loadLibrary(self, lmodel: list):
488485
apiCall = "loadFile"
489486
else:
490487
apiCall = "loadModel"
491-
self._requestApi(apiCall, element)
488+
self._requestApi(apiName=apiCall, entity=element)
492489
elif isinstance(element, tuple):
493490
if not element[1]:
494491
expr_load_lib = f"loadModel({element[0]})"
@@ -502,25 +499,34 @@ def _loadLibrary(self, lmodel: list):
502499
'1)["Modelica"]\n'
503500
'2)[("Modelica","3.2.3"), "PowerSystems"]\n')
504501

505-
def setTempDirectory(self, customBuildDirectory: Optional[str | os.PathLike | pathlib.Path] = None) -> pathlib.Path:
506-
# create a unique temp directory for each session and build the model in that directory
502+
def setWorkDirectory(self, customBuildDirectory: Optional[str | os.PathLike] = None) -> pathlib.Path:
503+
"""
504+
Define the work directory for the ModelicaSystem / OpenModelica session. The model is build within this
505+
directory. If no directory is defined a unique temporary directory is created.
506+
"""
507507
if customBuildDirectory is not None:
508-
if not os.path.exists(customBuildDirectory):
509-
raise IOError(f"{customBuildDirectory} does not exist")
510-
tempdir = pathlib.Path(customBuildDirectory).absolute()
508+
workdir = pathlib.Path(customBuildDirectory).absolute()
509+
if not workdir.is_dir():
510+
raise IOError(f"Provided work directory does not exists: {customBuildDirectory}!")
511511
else:
512-
tempdir = pathlib.Path(tempfile.mkdtemp()).absolute()
513-
if not tempdir.is_dir():
514-
raise IOError(f"{tempdir} could not be created")
512+
workdir = pathlib.Path(tempfile.mkdtemp()).absolute()
513+
if not workdir.is_dir():
514+
raise IOError(f"{workdir} could not be created")
515515

516-
logger.info("Define tempdir as %s", tempdir)
517-
exp = f'cd("{tempdir.as_posix()}")'
516+
logger.info("Define work dir as %s", workdir)
517+
exp = f'cd("{workdir.as_posix()}")'
518518
self.sendExpression(exp)
519519

520-
return tempdir
520+
# set the class variable _work_dir ...
521+
self._work_dir = workdir
522+
# ... and also return the defined path
523+
return workdir
521524

522525
def getWorkDirectory(self) -> pathlib.Path:
523-
return self._tempdir
526+
"""
527+
Return the defined working directory for this ModelicaSystem / OpenModelica session.
528+
"""
529+
return self._work_dir
524530

525531
def buildModel(self, variableFilter: Optional[str] = None):
526532
if variableFilter is not None:
@@ -531,13 +537,13 @@ def buildModel(self, variableFilter: Optional[str] = None):
531537
else:
532538
varFilter = 'variableFilter=".*"'
533539

534-
buildModelResult = self._requestApi("buildModel", self._model_name, properties=varFilter)
540+
buildModelResult = self._requestApi(apiName="buildModel", entity=self._model_name, properties=varFilter)
535541
logger.debug("OM model build result: %s", buildModelResult)
536542

537-
self._xml_file = pathlib.Path(buildModelResult[0]).parent / buildModelResult[1]
538-
self._xmlparse()
543+
xml_file = pathlib.Path(buildModelResult[0]).parent / buildModelResult[1]
544+
self._xmlparse(xml_file=xml_file)
539545

540-
def sendExpression(self, expr: str, parsed: bool = True):
546+
def sendExpression(self, expr: str, parsed: bool = True) -> Any:
541547
try:
542548
retval = self._getconn.sendExpression(expr, parsed)
543549
except OMCSessionException as ex:
@@ -548,7 +554,12 @@ def sendExpression(self, expr: str, parsed: bool = True):
548554
return retval
549555

550556
# request to OMC
551-
def _requestApi(self, apiName, entity=None, properties=None): # 2
557+
def _requestApi(
558+
self,
559+
apiName: str,
560+
entity: Optional[str] = None,
561+
properties: Optional[str] = None,
562+
) -> Any:
552563
if entity is not None and properties is not None:
553564
exp = f'{apiName}({entity}, {properties})'
554565
elif entity is not None and properties is None:
@@ -561,30 +572,42 @@ def _requestApi(self, apiName, entity=None, properties=None): # 2
561572

562573
return self.sendExpression(exp)
563574

564-
def _xmlparse(self):
565-
if not self._xml_file.is_file():
566-
raise ModelicaSystemError(f"XML file not generated: {self._xml_file}")
575+
def _xmlparse(self, xml_file: pathlib.Path):
576+
if not xml_file.is_file():
577+
raise ModelicaSystemError(f"XML file not generated: {xml_file}")
567578

568-
tree = ET.parse(self._xml_file)
579+
xml_content = xml_file.read_text()
580+
tree = ET.ElementTree(ET.fromstring(xml_content))
569581
rootCQ = tree.getroot()
570582
for attr in rootCQ.iter('DefaultExperiment'):
571583
for key in ("startTime", "stopTime", "stepSize", "tolerance",
572584
"solver", "outputFormat"):
573-
self._simulate_options[key] = attr.get(key)
585+
self._simulate_options[key] = str(attr.get(key))
574586

575587
for sv in rootCQ.iter('ScalarVariable'):
576-
scalar = {}
577-
for key in ("name", "description", "variability", "causality", "alias"):
578-
scalar[key] = sv.get(key)
579-
scalar["changeable"] = sv.get('isValueChangeable')
580-
scalar["aliasvariable"] = sv.get('aliasVariable')
588+
translations = {
589+
"alias": "alias",
590+
"aliasvariable": "aliasVariable",
591+
"causality": "causality",
592+
"changeable": "isValueChangeable",
593+
"description": "description",
594+
"name": "name",
595+
"variability": "variability",
596+
}
597+
598+
scalar: dict[str, Any] = {}
599+
for key_dst, key_src in translations.items():
600+
val = sv.get(key_src)
601+
scalar[key_dst] = None if val is None else str(val)
602+
581603
ch = list(sv)
582604
for att in ch:
583605
scalar["start"] = att.get('start')
584606
scalar["min"] = att.get('min')
585607
scalar["max"] = att.get('max')
586608
scalar["unit"] = att.get('unit')
587609

610+
# save parameters in the corresponding class variables
588611
if scalar["variability"] == "parameter":
589612
if scalar["name"] in self._override_variables:
590613
self._params[scalar["name"]] = self._override_variables[scalar["name"]]
@@ -996,7 +1019,11 @@ def simulate_cmd(
9961019
An instance if ModelicaSystemCmd to run the requested simulation.
9971020
"""
9981021

999-
om_cmd = ModelicaSystemCmd(runpath=self._tempdir, modelname=self._model_name, timeout=timeout)
1022+
om_cmd = ModelicaSystemCmd(
1023+
runpath=self.getWorkDirectory(),
1024+
modelname=self._model_name,
1025+
timeout=timeout,
1026+
)
10001027

10011028
# always define the result file to use
10021029
om_cmd.arg_set(key="r", val=result_file.as_posix())
@@ -1008,16 +1035,17 @@ def simulate_cmd(
10081035
if simargs:
10091036
om_cmd.args_set(args=simargs)
10101037

1011-
overrideFile = self._tempdir / f"{self._model_name}_override.txt"
10121038
if self._override_variables or self._simulate_options_override:
1013-
tmpdict = self._override_variables.copy()
1014-
tmpdict.update(self._simulate_options_override)
1015-
# write to override file
1016-
with open(file=overrideFile, mode="w", encoding="utf-8") as fh:
1017-
for key, value in tmpdict.items():
1018-
fh.write(f"{key}={value}\n")
1039+
override_file = result_file.parent / f"{result_file.stem}_override.txt"
1040+
1041+
override_content = (
1042+
"\n".join([f"{key}={value}" for key, value in self._override_variables.items()])
1043+
+ "\n".join([f"{key}={value}" for key, value in self._simulate_options_override.items()])
1044+
+ "\n"
1045+
)
10191046

1020-
om_cmd.arg_set(key="overrideFile", val=overrideFile.as_posix())
1047+
override_file.write_text(override_content)
1048+
om_cmd.arg_set(key="overrideFile", val=override_file.as_posix())
10211049

10221050
if self._inputs: # if model has input quantities
10231051
for key in self._inputs:
@@ -1067,11 +1095,11 @@ def simulate(
10671095

10681096
if resultfile is None:
10691097
# default result file generated by OM
1070-
self._result_file = self._tempdir / f"{self._model_name}_res.mat"
1098+
self._result_file = self.getWorkDirectory() / f"{self._model_name}_res.mat"
10711099
elif os.path.exists(resultfile):
10721100
self._result_file = pathlib.Path(resultfile)
10731101
else:
1074-
self._result_file = self._tempdir / resultfile
1102+
self._result_file = self.getWorkDirectory() / resultfile
10751103

10761104
om_cmd = self.simulate_cmd(
10771105
result_file=self._result_file,
@@ -1137,9 +1165,9 @@ def getSolutions(self, varList: Optional[str | list[str]] = None, resultfile: Op
11371165
else:
11381166
result_file = pathlib.Path(resultfile)
11391167

1140-
# check for result file exits
1168+
# check if the result file exits
11411169
if not result_file.is_file():
1142-
raise ModelicaSystemError(f"Result file does not exist {result_file}")
1170+
raise ModelicaSystemError(f"Result file does not exist {result_file.as_posix()}")
11431171

11441172
# get absolute path
11451173
result_file = result_file.absolute()
@@ -1456,7 +1484,7 @@ def _createCSVData(self, csvfile: Optional[pathlib.Path] = None) -> pathlib.Path
14561484
interpolated_inputs[signal_name] = np.interp(
14571485
all_times,
14581486
signal[:, 0], # times
1459-
signal[:, 1] # values
1487+
signal[:, 1], # values
14601488
)
14611489

14621490
# Write CSV file
@@ -1468,16 +1496,17 @@ def _createCSVData(self, csvfile: Optional[pathlib.Path] = None) -> pathlib.Path
14681496
row = [
14691497
t, # time
14701498
*(interpolated_inputs[name][i] for name in input_names), # input values
1471-
0 # trailing 'end' column
1499+
0, # trailing 'end' column
14721500
]
14731501
csv_rows.append(row)
14741502

14751503
if csvfile is None:
1476-
csvfile = self._tempdir / f'{self._model_name}.csv'
1504+
csvfile = self.getWorkDirectory() / f'{self._model_name}.csv'
14771505

1478-
with open(file=csvfile, mode="w", encoding="utf-8", newline="") as fh:
1479-
writer = csv.writer(fh)
1480-
writer.writerows(csv_rows)
1506+
# basic definition of a CSV file using csv_rows as input
1507+
csv_content = "\n".join([",".join(map(str, row)) for row in csv_rows]) + "\n"
1508+
1509+
csvfile.write_text(csv_content)
14811510

14821511
return csvfile
14831512

@@ -1505,8 +1534,9 @@ def convertMo2Fmu(self, version: str = "2.0", fmuType: str = "me_cs",
15051534
includeResourcesStr = "true"
15061535
else:
15071536
includeResourcesStr = "false"
1508-
properties = f'version="{version}", fmuType="{fmuType}", fileNamePrefix="{fileNamePrefix}", includeResources={includeResourcesStr}'
1509-
fmu = self._requestApi('buildModelFMU', self._model_name, properties)
1537+
properties = (f'version="{version}", fmuType="{fmuType}", '
1538+
f'fileNamePrefix="{fileNamePrefix}", includeResources={includeResourcesStr}')
1539+
fmu = self._requestApi(apiName='buildModelFMU', entity=self._model_name, properties=properties)
15101540

15111541
# report proper error message
15121542
if not os.path.exists(fmu):
@@ -1523,7 +1553,7 @@ def convertFmu2Mo(self, fmuName): # 20
15231553
>>> convertFmu2Mo("c:/BouncingBall.Fmu")
15241554
"""
15251555

1526-
fileName = self._requestApi('importFMU', fmuName)
1556+
fileName = self._requestApi(apiName='importFMU', entity=fmuName)
15271557

15281558
# report proper error message
15291559
if not os.path.exists(fileName):
@@ -1561,7 +1591,7 @@ def optimize(self) -> dict[str, Any]:
15611591
cName = self._model_name
15621592
properties = ','.join(f"{key}={val}" for key, val in self._optimization_options.items())
15631593
self.setCommandLineOptions("-g=Optimica")
1564-
optimizeResult = self._requestApi('optimize', cName, properties)
1594+
optimizeResult = self._requestApi(apiName='optimize', entity=cName, properties=properties)
15651595

15661596
return optimizeResult
15671597

@@ -1593,21 +1623,26 @@ def linearize(
15931623
compatibility, because linearize() used to return `[A, B, C, D]`.
15941624
"""
15951625

1596-
if self._xml_file is None:
1626+
if len(self._quantities) == 0:
1627+
# if self._quantities has no content, the xml file was not parsed; see self._xmlparse()
15971628
raise ModelicaSystemError(
15981629
"Linearization cannot be performed as the model is not build, "
15991630
"use ModelicaSystem() to build the model first"
16001631
)
16011632

1602-
om_cmd = ModelicaSystemCmd(runpath=self._tempdir, modelname=self._model_name, timeout=timeout)
1633+
om_cmd = ModelicaSystemCmd(
1634+
runpath=self.getWorkDirectory(),
1635+
modelname=self._model_name,
1636+
timeout=timeout,
1637+
)
16031638

1604-
overrideLinearFile = self._tempdir / f'{self._model_name}_override_linear.txt'
1639+
overrideLinearFile = self.getWorkDirectory() / f'{self._model_name}_override_linear.txt'
16051640

16061641
with open(file=overrideLinearFile, mode="w", encoding="utf-8") as fh:
1607-
for key, value in self._override_variables.items():
1608-
fh.write(f"{key}={value}\n")
1609-
for key, value in self._linearization_options.items():
1610-
fh.write(f"{key}={value}\n")
1642+
for key1, value1 in self._override_variables.items():
1643+
fh.write(f"{key1}={value1}\n")
1644+
for key2, value2 in self._linearization_options.items():
1645+
fh.write(f"{key2}={value2}\n")
16111646

16121647
om_cmd.arg_set(key="overrideFile", val=overrideLinearFile.as_posix())
16131648

@@ -1631,19 +1666,17 @@ def linearize(
16311666
om_cmd.args_set(args=simargs)
16321667

16331668
# the file create by the model executable which contains the matrix and linear inputs, outputs and states
1634-
linear_file = self._tempdir / "linearized_model.py"
1635-
1669+
linear_file = self.getWorkDirectory() / "linearized_model.py"
16361670
linear_file.unlink(missing_ok=True)
16371671

16381672
returncode = om_cmd.run()
16391673
if returncode != 0:
16401674
raise ModelicaSystemError(f"Linearize failed with return code: {returncode}")
1641-
1642-
self._simulated = True
1643-
16441675
if not linear_file.exists():
16451676
raise ModelicaSystemError(f"Linearization failed: {linear_file} not found!")
16461677

1678+
self._simulated = True
1679+
16471680
# extract data from the python file with the linearized model using the ast module - this allows to get the
16481681
# needed information without executing the created code
16491682
linear_data = {}

0 commit comments

Comments
 (0)