From ac2cceb7d804805bb31cf062e332ebb97fbed8c1 Mon Sep 17 00:00:00 2001 From: syntron Date: Mon, 7 Jul 2025 20:43:02 +0200 Subject: [PATCH 1/8] [ModelicaSystem] fix rebase fallout 2 --- OMPython/ModelicaSystem.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/OMPython/ModelicaSystem.py b/OMPython/ModelicaSystem.py index d4105ea3b..880284e4c 100644 --- a/OMPython/ModelicaSystem.py +++ b/OMPython/ModelicaSystem.py @@ -1459,6 +1459,8 @@ def setInputs( else: raise ModelicaSystemError(f"Data cannot be evaluated for {repr(key)}: {repr(val)}") + self._has_inputs = True + return True def _createCSVData(self, csvfile: Optional[pathlib.Path] = None) -> pathlib.Path: From 5adca7c82f440dc774da6a70002a53577fd8019b Mon Sep 17 00:00:00 2001 From: syntron Date: Sat, 12 Jul 2025 01:03:53 +0200 Subject: [PATCH 2/8] [test_ModelicaSystem] fix test_customBuildDirectory() --- tests/test_ModelicaSystem.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_ModelicaSystem.py b/tests/test_ModelicaSystem.py index 8e9b8a8e9..e782489e7 100644 --- a/tests/test_ModelicaSystem.py +++ b/tests/test_ModelicaSystem.py @@ -105,7 +105,7 @@ def test_customBuildDirectory(tmp_path, model_firstorder): tmpdir = tmp_path / "tmpdir1" tmpdir.mkdir() m = OMPython.ModelicaSystem(filePath, "M", customBuildDirectory=tmpdir) - assert m.getWorkDirectory().resolve() == tmpdir.resolve() + assert pathlib.Path(m.getWorkDirectory()).resolve() == tmpdir.resolve() result_file = tmpdir / "a.mat" assert not result_file.exists() m.simulate(resultfile="a.mat") From e613127f64a1d35dd244109d457793ff5ee671ce Mon Sep 17 00:00:00 2001 From: syntron Date: Tue, 22 Jul 2025 20:37:00 +0200 Subject: [PATCH 3/8] [ModelicaSystem] fix blank lines (flake8) --- OMPython/ModelicaSystem.py | 1 - 1 file changed, 1 deletion(-) diff --git a/OMPython/ModelicaSystem.py b/OMPython/ModelicaSystem.py index 880284e4c..20d317738 100644 --- a/OMPython/ModelicaSystem.py +++ b/OMPython/ModelicaSystem.py @@ -1630,7 +1630,6 @@ def linearize( * `result = linearize(); A = result[0]` mostly just for backwards compatibility, because linearize() used to return `[A, B, C, D]`. """ - if len(self._quantities) == 0: # if self._quantities has no content, the xml file was not parsed; see self._xmlparse() raise ModelicaSystemError( From e7f1bfbabe4bfdc9b7df4b3278dea0b84e5f25ff Mon Sep 17 00:00:00 2001 From: syntron Date: Wed, 6 Aug 2025 21:08:17 +0200 Subject: [PATCH 4/8] [test_optimization] fix due to OMCPath usage --- tests/test_optimization.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/test_optimization.py b/tests/test_optimization.py index b41643971..908cfd62a 100644 --- a/tests/test_optimization.py +++ b/tests/test_optimization.py @@ -47,7 +47,9 @@ def test_optimization_example(tmp_path): r = mod.optimize() # it is necessary to specify resultfile, otherwise it wouldn't find it. - time, f, v = mod.getSolutions(["time", "f", "v"], resultfile=r["resultFile"]) + resultfile_str = r["resultFile"] + resultfile_omcpath = mod._getconn.omcpath(resultfile_str) + time, f, v = mod.getSolutions(["time", "f", "v"], resultfile=resultfile_omcpath.as_posix()) assert np.isclose(f[0], 10) assert np.isclose(f[-1], -10) From c587ab218ff323cb11e183b86040c0c4ce0bd354 Mon Sep 17 00:00:00 2001 From: syntron Date: Wed, 6 Aug 2025 21:09:06 +0200 Subject: [PATCH 5/8] [test_FMIExport] fix due to OMCPath usage --- tests/test_FMIExport.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tests/test_FMIExport.py b/tests/test_FMIExport.py index f47b87ae2..b8305b312 100644 --- a/tests/test_FMIExport.py +++ b/tests/test_FMIExport.py @@ -1,12 +1,13 @@ import OMPython import shutil import os +import pathlib def test_CauerLowPassAnalog(): mod = OMPython.ModelicaSystem(modelName="Modelica.Electrical.Analog.Examples.CauerLowPassAnalog", lmodel=["Modelica"]) - tmp = mod.getWorkDirectory() + tmp = pathlib.Path(mod.getWorkDirectory()) try: fmu = mod.convertMo2Fmu(fileNamePrefix="CauerLowPassAnalog") assert os.path.exists(fmu) @@ -16,7 +17,7 @@ def test_CauerLowPassAnalog(): def test_DrumBoiler(): mod = OMPython.ModelicaSystem(modelName="Modelica.Fluid.Examples.DrumBoiler.DrumBoiler", lmodel=["Modelica"]) - tmp = mod.getWorkDirectory() + tmp = pathlib.Path(mod.getWorkDirectory()) try: fmu = mod.convertMo2Fmu(fileNamePrefix="DrumBoiler") assert os.path.exists(fmu) From bbf9759aec7966765b36c0798a58c54735761887 Mon Sep 17 00:00:00 2001 From: syntron Date: Sat, 9 Aug 2025 20:29:49 +0200 Subject: [PATCH 6/8] [ModelicaSystem] improve definition of getSolution * allow different ways to define the path --- OMPython/ModelicaSystem.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/OMPython/ModelicaSystem.py b/OMPython/ModelicaSystem.py index 20d317738..cb727b627 100644 --- a/OMPython/ModelicaSystem.py +++ b/OMPython/ModelicaSystem.py @@ -1132,7 +1132,11 @@ def simulate( self._simulated = True - def getSolutions(self, varList: Optional[str | list[str]] = None, resultfile: Optional[str] = None) -> tuple[str] | np.ndarray: + def getSolutions( + self, + varList: Optional[str | list[str]] = None, + resultfile: Optional[str | os.PathLike] = None, + ) -> tuple[str] | np.ndarray: """Extract simulation results from a result data file. Args: From 94d5381b52518574a4581e86b716740fdb58837b Mon Sep 17 00:00:00 2001 From: syntron Date: Fri, 11 Jul 2025 22:52:19 +0200 Subject: [PATCH 7/8] [ModelicaSystem] use OMCPath for nearly all file system interactions remove pathlib - use OMCPath and (for type hints) os.PathLike --- OMPython/ModelicaSystem.py | 75 +++++++++++++++++++++----------------- 1 file changed, 42 insertions(+), 33 deletions(-) diff --git a/OMPython/ModelicaSystem.py b/OMPython/ModelicaSystem.py index cb727b627..5b7d3ff99 100644 --- a/OMPython/ModelicaSystem.py +++ b/OMPython/ModelicaSystem.py @@ -38,17 +38,15 @@ import numbers import numpy as np import os -import pathlib import platform import re import subprocess -import tempfile import textwrap from typing import Optional, Any import warnings import xml.etree.ElementTree as ET -from OMPython.OMCSession import OMCSessionException, OMCSessionZMQ, OMCProcessLocal +from OMPython.OMCSession import OMCSessionException, OMCSessionZMQ, OMCProcessLocal, OMCPath # define logger using the current module name as ID logger = logging.getLogger(__name__) @@ -114,8 +112,8 @@ def __getitem__(self, index: int): class ModelicaSystemCmd: """A compiled model executable.""" - def __init__(self, runpath: pathlib.Path, modelname: str, timeout: Optional[float] = None) -> None: - self._runpath = pathlib.Path(runpath).resolve().absolute() + def __init__(self, runpath: OMCPath, modelname: str, timeout: Optional[float] = None) -> None: + self._runpath = runpath self._model_name = modelname self._timeout = timeout @@ -229,7 +227,7 @@ def args_set( for arg in args: self.arg_set(key=arg, val=args[arg]) - def get_exe(self) -> pathlib.Path: + def get_exe(self) -> OMCPath: """Get the path to the compiled model executable.""" if platform.system() == "Windows": path_exe = self._runpath / f"{self._model_name}.exe" @@ -349,7 +347,7 @@ def parse_simflags(simflags: str) -> dict[str, Optional[str | dict[str, Any] | n class ModelicaSystem: def __init__( self, - fileName: Optional[str | os.PathLike | pathlib.Path] = None, + fileName: Optional[str | os.PathLike] = None, modelName: Optional[str] = None, lmodel: Optional[list[str | tuple[str, str]]] = None, commandLineOptions: Optional[list[str]] = None, @@ -446,15 +444,25 @@ def __init__( self._lmodel = lmodel # may be needed if model is derived from other model self._model_name = modelName # Model class name - self._file_name = pathlib.Path(fileName).resolve() if fileName is not None else None # Model file/package name + if fileName is not None: + file_name = self._getconn.omcpath(fileName).resolve() + else: + file_name = None + self._file_name: Optional[OMCPath] = file_name # Model file/package name self._simulated = False # True if the model has already been simulated - self._result_file: Optional[pathlib.Path] = None # for storing result file + self._result_file: Optional[OMCPath] = None # for storing result file self._variable_filter = variableFilter if self._file_name is not None and not self._file_name.is_file(): # if file does not exist raise IOError(f"{self._file_name} does not exist!") - self._work_dir: pathlib.Path = self.setWorkDirectory(customBuildDirectory) + # set default command Line Options for linearization as + # linearize() will use the simulation executable and runtime + # flag -l to perform linearization + self.setCommandLineOptions("--linearizationDumpLanguage=python") + self.setCommandLineOptions("--generateSymbolicLinearization") + + self._work_dir: OMCPath = self.setWorkDirectory(customBuildDirectory) if self._file_name is not None: self._loadLibrary(lmodel=self._lmodel) @@ -474,7 +482,7 @@ def setCommandLineOptions(self, commandLineOptions: str): exp = f'setCommandLineOptions("{commandLineOptions}")' self.sendExpression(exp) - def _loadFile(self, fileName: pathlib.Path): + def _loadFile(self, fileName: OMCPath): # load file self.sendExpression(f'loadFile("{fileName.as_posix()}")') @@ -502,17 +510,17 @@ def _loadLibrary(self, lmodel: list): '1)["Modelica"]\n' '2)[("Modelica","3.2.3"), "PowerSystems"]\n') - def setWorkDirectory(self, customBuildDirectory: Optional[str | os.PathLike] = None) -> pathlib.Path: + def setWorkDirectory(self, customBuildDirectory: Optional[str | os.PathLike] = None) -> OMCPath: """ Define the work directory for the ModelicaSystem / OpenModelica session. The model is build within this directory. If no directory is defined a unique temporary directory is created. """ if customBuildDirectory is not None: - workdir = pathlib.Path(customBuildDirectory).absolute() + workdir = self._getconn.omcpath(customBuildDirectory).absolute() if not workdir.is_dir(): raise IOError(f"Provided work directory does not exists: {customBuildDirectory}!") else: - workdir = pathlib.Path(tempfile.mkdtemp()).absolute() + workdir = self._getconn.omcpath_tempdir().absolute() if not workdir.is_dir(): raise IOError(f"{workdir} could not be created") @@ -525,7 +533,7 @@ def setWorkDirectory(self, customBuildDirectory: Optional[str | os.PathLike] = N # ... and also return the defined path return workdir - def getWorkDirectory(self) -> pathlib.Path: + def getWorkDirectory(self) -> OMCPath: """ Return the defined working directory for this ModelicaSystem / OpenModelica session. """ @@ -546,7 +554,7 @@ def buildModel(self, variableFilter: Optional[str] = None): buildModelResult = self._requestApi(apiName="buildModel", entity=self._model_name, properties=var_filter) logger.debug("OM model build result: %s", buildModelResult) - xml_file = pathlib.Path(buildModelResult[0]).parent / buildModelResult[1] + xml_file = self._getconn.omcpath(buildModelResult[0]).parent / buildModelResult[1] self._xmlparse(xml_file=xml_file) def sendExpression(self, expr: str, parsed: bool = True) -> Any: @@ -578,7 +586,7 @@ def _requestApi( return self.sendExpression(exp) - def _xmlparse(self, xml_file: pathlib.Path): + def _xmlparse(self, xml_file: OMCPath): if not xml_file.is_file(): raise ModelicaSystemError(f"XML file not generated: {xml_file}") @@ -998,7 +1006,7 @@ def getOptimizationOptions(self, names: Optional[str | list[str]] = None) -> dic def simulate_cmd( self, - result_file: pathlib.Path, + result_file: OMCPath, simflags: Optional[str] = None, simargs: Optional[dict[str, Optional[str | dict[str, Any] | numbers.Number]]] = None, timeout: Optional[float] = None, @@ -1103,10 +1111,13 @@ def simulate( # default result file generated by OM self._result_file = self.getWorkDirectory() / f"{self._model_name}_res.mat" elif os.path.exists(resultfile): - self._result_file = pathlib.Path(resultfile) + self._result_file = self._getconn.omcpath(resultfile) else: self._result_file = self.getWorkDirectory() / resultfile + if not isinstance(self._result_file, OMCPath): + raise ModelicaSystemError(f"Invalid result file path: {self._result_file} - must be an OMCPath object!") + om_cmd = self.simulate_cmd( result_file=self._result_file, simflags=simflags, @@ -1124,7 +1135,7 @@ def simulate( # check for an empty (=> 0B) result file which indicates a crash of the model executable # see: https://github.com/OpenModelica/OMPython/issues/261 # https://github.com/OpenModelica/OpenModelica/issues/13829 - if self._result_file.stat().st_size == 0: + if self._result_file.size() == 0: self._result_file.unlink() raise ModelicaSystemError("Empty result file - this indicates a crash of the model executable!") @@ -1173,7 +1184,7 @@ def getSolutions( raise ModelicaSystemError("No result file found. Run simulate() first.") result_file = self._result_file else: - result_file = pathlib.Path(resultfile) + result_file = self._getconn.omcpath(resultfile) # check if the result file exits if not result_file.is_file(): @@ -1463,11 +1474,9 @@ def setInputs( else: raise ModelicaSystemError(f"Data cannot be evaluated for {repr(key)}: {repr(val)}") - self._has_inputs = True - return True - def _createCSVData(self, csvfile: Optional[pathlib.Path] = None) -> pathlib.Path: + def _createCSVData(self, csvfile: Optional[OMCPath] = None) -> OMCPath: """ Create a csv file with inputs for the simulation/optimization of the model. If csvfile is provided as argument, this file is used; else a generic file name is created. @@ -1647,15 +1656,15 @@ def linearize( timeout=timeout, ) - overrideLinearFile = self.getWorkDirectory() / f'{self._model_name}_override_linear.txt' - - with open(file=overrideLinearFile, mode="w", encoding="utf-8") as fh: - for key1, value1 in self._override_variables.items(): - fh.write(f"{key1}={value1}\n") - for key2, value2 in self._linearization_options.items(): - fh.write(f"{key2}={value2}\n") + override_content = ( + "\n".join([f"{key}={value}" for key, value in self._override_variables.items()]) + + "\n".join([f"{key}={value}" for key, value in self._linearization_options.items()]) + + "\n" + ) + override_file = self.getWorkDirectory() / f'{self._model_name}_override_linear.txt' + override_file.write_text(override_content) - om_cmd.arg_set(key="overrideFile", val=overrideLinearFile.as_posix()) + om_cmd.arg_set(key="overrideFile", val=override_file.as_posix()) if self._inputs: for key in self._inputs: @@ -1683,7 +1692,7 @@ def linearize( returncode = om_cmd.run() if returncode != 0: raise ModelicaSystemError(f"Linearize failed with return code: {returncode}") - if not linear_file.exists(): + if not linear_file.is_file(): raise ModelicaSystemError(f"Linearization failed: {linear_file} not found!") self._simulated = True From e88fe207d06e4c2433b465a0e2acbe59f822c0e2 Mon Sep 17 00:00:00 2001 From: syntron Date: Sat, 9 Aug 2025 20:41:33 +0200 Subject: [PATCH 8/8] [ModelicaSystem] improve result file handling in simulate() --- OMPython/ModelicaSystem.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/OMPython/ModelicaSystem.py b/OMPython/ModelicaSystem.py index 5b7d3ff99..693baf423 100644 --- a/OMPython/ModelicaSystem.py +++ b/OMPython/ModelicaSystem.py @@ -1110,10 +1110,12 @@ def simulate( if resultfile is None: # default result file generated by OM self._result_file = self.getWorkDirectory() / f"{self._model_name}_res.mat" - elif os.path.exists(resultfile): - self._result_file = self._getconn.omcpath(resultfile) + elif isinstance(resultfile, OMCPath): + self._result_file = resultfile else: - self._result_file = self.getWorkDirectory() / resultfile + self._result_file = self._getconn.omcpath(resultfile) + if not self._result_file.is_absolute(): + self._result_file = self.getWorkDirectory() / resultfile if not isinstance(self._result_file, OMCPath): raise ModelicaSystemError(f"Invalid result file path: {self._result_file} - must be an OMCPath object!")