From 84ccc31c23592d0f7443e0816bccc4346cdf19f2 Mon Sep 17 00:00:00 2001 From: syntron Date: Sat, 26 Apr 2025 21:43:23 +0200 Subject: [PATCH 01/21] [ModelicaSystemCmd] draft --- OMPython/ModelicaSystem.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/OMPython/ModelicaSystem.py b/OMPython/ModelicaSystem.py index 47f86d75..5553e179 100644 --- a/OMPython/ModelicaSystem.py +++ b/OMPython/ModelicaSystem.py @@ -107,6 +107,18 @@ def __getitem__(self, index: int): return {0: self.A, 1: self.B, 2: self.C, 3: self.D}[index] +class ModelicaSystemCmd: + + def __init__(self, cmdpath: pathlib.Path, modelname: str): + pass + + def arg_set(self, key, val=None): + pass + + def run(self): + pass + + class ModelicaSystem: def __init__( self, From 0b09234027de682ea8ba450684b3dc2a67540266 Mon Sep 17 00:00:00 2001 From: syntron Date: Mon, 28 Apr 2025 09:24:25 +0200 Subject: [PATCH 02/21] [ModelicaSystemCmd] define and use it - needs cleanup! --- OMPython/ModelicaSystem.py | 212 ++++++++++++++++++++++--------------- 1 file changed, 124 insertions(+), 88 deletions(-) diff --git a/OMPython/ModelicaSystem.py b/OMPython/ModelicaSystem.py index 5553e179..1be4c87d 100644 --- a/OMPython/ModelicaSystem.py +++ b/OMPython/ModelicaSystem.py @@ -45,6 +45,7 @@ import pathlib from dataclasses import dataclass from typing import Optional +import warnings from OMPython.OMCSession import OMCSessionZMQ @@ -109,14 +110,78 @@ def __getitem__(self, index: int): class ModelicaSystemCmd: - def __init__(self, cmdpath: pathlib.Path, modelname: str): - pass + def __init__(self, cmdpath: pathlib.Path, modelname: str, timeout: Optional[int] = None): + self.tempdir = cmdpath + self.modelName = modelname + self._exe_file = self.get_exe_file(tempdir=cmdpath, modelName=modelname) + if not self._exe_file.exists(): + raise ModelicaSystemError(f"Application file path not found: {self._exe_file}") + + self._timeout = timeout + self._args = {} def arg_set(self, key, val=None): - pass + key = key.strip() + if val is not None: + val = val.strip() + self._args[key] = val + + def args_set(self, args: dict): + for arg in args: + self.arg_set(key=arg, val=args[arg]) def run(self): - pass + + cmd = [self._exe_file.as_posix()] + [f"{key}={self._args[key]}" for key in self._args] + self._run_cmd(cmd=cmd, timeout=self._timeout) + + return True + + def _run_cmd(self, cmd: list, timeout: Optional[int] = None): + logger.debug("Run OM command %s in %s", cmd, self.tempdir) + + if platform.system() == "Windows": + dllPath = "" + + # set the process environment from the generated .bat file in windows which should have all the dependencies + batFilePath = pathlib.Path(self.tempdir) / f"{self.modelName}.bat" + if not batFilePath.exists(): + ModelicaSystemError("Batch file (*.bat) does not exist " + str(batFilePath)) + + with open(batFilePath, 'r') as file: + for line in file: + match = re.match(r"^SET PATH=([^%]*)", line, re.IGNORECASE) + if match: + dllPath = match.group(1).strip(';') # Remove any trailing semicolons + my_env = os.environ.copy() + my_env["PATH"] = dllPath + os.pathsep + my_env["PATH"] + else: + # TODO: how to handle path to resources of external libraries for any system not Windows? + my_env = None + + try: + cmdres = subprocess.run(cmd, capture_output=True, text=True, env=my_env, cwd=self.tempdir, + timeout=timeout) + stdout = cmdres.stdout.strip() + stderr = cmdres.stderr.strip() + + logger.debug("OM output for command %s:\n%s", cmd, stdout) + + if cmdres.returncode != 0: + raise ModelicaSystemError(f"Error running command {cmd}: return code = {cmdres.returncode}") + if stderr: + raise ModelicaSystemError(f"Error running command {cmd}: {stderr}") + except subprocess.TimeoutExpired: + raise ModelicaSystemError(f"Timeout running command {repr(cmd)}") + except Exception as ex: + raise ModelicaSystemError(f"Error running command {cmd}") from ex + + def get_exe_file(self, tempdir, modelName) -> pathlib.Path: + """Get path to model executable.""" + if platform.system() == "Windows": + return pathlib.Path(tempdir) / f"{modelName}.exe" + else: + return pathlib.Path(tempdir) / modelName class ModelicaSystem: @@ -289,45 +354,6 @@ def setTempDirectory(self, customBuildDirectory): def getWorkDirectory(self): return self.tempdir - def _run_cmd(self, cmd: list, timeout: Optional[int] = None): - logger.debug("Run OM command %s in %s", cmd, self.tempdir) - - if platform.system() == "Windows": - dllPath = "" - - # set the process environment from the generated .bat file in windows which should have all the dependencies - batFilePath = pathlib.Path(self.tempdir) / f"{self.modelName}.bat" - if not batFilePath.exists(): - raise ModelicaSystemError("Batch file (*.bat) does not exist " + str(batFilePath)) - - with open(batFilePath, 'r') as file: - for line in file: - match = re.match(r"^SET PATH=([^%]*)", line, re.IGNORECASE) - if match: - dllPath = match.group(1).strip(';') # Remove any trailing semicolons - my_env = os.environ.copy() - my_env["PATH"] = dllPath + os.pathsep + my_env["PATH"] - else: - # TODO: how to handle path to resources of external libraries for any system not Windows? - my_env = None - - try: - cmdres = subprocess.run(cmd, capture_output=True, text=True, env=my_env, cwd=self.tempdir, - timeout=timeout) - stdout = cmdres.stdout.strip() - stderr = cmdres.stderr.strip() - - logger.debug("OM output for command %s:\n%s", cmd, stdout) - - if cmdres.returncode != 0: - raise ModelicaSystemError(f"Error running command {cmd}: return code = {cmdres.returncode}") - if stderr: - raise ModelicaSystemError(f"Error running command {cmd}: {stderr}") - except subprocess.TimeoutExpired: - raise ModelicaSystemError(f"Timeout running command {repr(cmd)}") - except Exception as ex: - raise ModelicaSystemError(f"Error running command {cmd}") from ex - def buildModel(self, variableFilter=None): if variableFilter is not None: self.variableFilter = variableFilter @@ -651,21 +677,20 @@ def getOptimizationOptions(self, names=None): # 10 raise ModelicaSystemError("Unhandled input for getOptimizationOptions()") - def get_exe_file(self) -> pathlib.Path: - """Get path to model executable.""" - if platform.system() == "Windows": - return pathlib.Path(self.tempdir) / f"{self.modelName}.exe" - else: - return pathlib.Path(self.tempdir) / self.modelName - - def simulate(self, resultfile=None, simflags=None, timeout: Optional[int] = None): # 11 + def simulate(self, resultfile: Optional[str] = None, simflags: Optional[str] = None, + simargs: Optional[dict[str, str | None]] = None, + timeout: Optional[int] = None): # 11 """ This method simulates model according to the simulation options. usage >>> simulate() >>> simulate(resultfile="a.mat") >>> simulate(simflags="-noEventEmit -noRestart -override=e=0.3,g=10") # set runtime simulation flags + >>> simulate(simargs={"-noEventEmit": None, "-noRestart": None, "-override": "e=0.3,g=10"}) # using simargs """ + + om_cmd = ModelicaSystemCmd(cmdpath=pathlib.Path(self.tempdir), modelname=self.modelName, timeout=timeout) + if resultfile is None: # default result file generated by OM self.resultfile = (pathlib.Path(self.tempdir) / f"{self.modelName}_res.mat").as_posix() @@ -674,13 +699,26 @@ def simulate(self, resultfile=None, simflags=None, timeout: Optional[int] = None else: self.resultfile = (pathlib.Path(self.tempdir) / resultfile).as_posix() # always define the resultfile to use - resultfileflag = " -r=" + self.resultfile + om_cmd.arg_set(key="-r", val=self.resultfile) # allow runtime simulation flags from user input - if simflags is None: - simflags = "" - else: - simflags = " " + simflags + # TODO: merge into ModelicaSystemCmd? + if simflags is not None: + # add old style simulation arguments + warnings.warn("The argument simflags is depreciated and will be removed in future versions; " + "please use simargs instead", DeprecationWarning, stacklevel=1) + + args = [s for s in simflags.split(' ') if s] + for arg in args: + parts = arg.split('=') + if len(parts) == 1: + val = None + else: + val = '='.join(parts[1:]) + om_cmd.arg_set(key=parts[0], val=val) + + if simargs: + om_cmd.args_set(args=simargs) overrideFile = pathlib.Path(self.tempdir) / f"{self.modelName}_override.txt" if self.overridevariables or self.simoptionsoverride: @@ -690,9 +728,8 @@ def simulate(self, resultfile=None, simflags=None, timeout: Optional[int] = None with open(overrideFile, "w") as file: for key, value in tmpdict.items(): file.write(f"{key}={value}\n") - override = " -overrideFile=" + overrideFile.as_posix() - else: - override = "" + + om_cmd.arg_set(key="-overrideFile", val=overrideFile.as_posix()) if self.inputFlag: # if model has input quantities for i in self.inputlist: @@ -707,18 +744,10 @@ def simulate(self, resultfile=None, simflags=None, timeout: Optional[int] = None if float(self.simulateOptions["stopTime"]) != val[-1][0]: raise ModelicaSystemError(f"stopTime not matched for Input {i}!") self.csvFile = self.createCSVData() # create csv file - csvinput = " -csvInput=" + self.csvFile.as_posix() - else: - csvinput = "" - exe_file = self.get_exe_file() - if not exe_file.exists(): - raise ModelicaSystemError(f"Application file path not found: {exe_file}") + om_cmd.arg_set(key="-csvInput", val=self.csvFile.as_posix()) - cmd = exe_file.as_posix() + override + csvinput + resultfileflag + simflags - cmd = [s for s in cmd.split(' ') if s] - self._run_cmd(cmd=cmd, timeout=timeout) - self.simulationFlag = True + self.simulationFlag = om_cmd.run() # to extract simulation results def getSolutions(self, varList=None, resultfile=None): # 12 @@ -1033,13 +1062,15 @@ def optimize(self): # 21 return optimizeResult def linearize(self, lintime: Optional[float] = None, simflags: Optional[str] = None, + simargs: Optional[dict[str, str | None]] = None, timeout: Optional[int] = None) -> LinearizationResult: """Linearize the model according to linearOptions. Args: lintime: Override linearOptions["stopTime"] value. simflags: A string of extra command line flags for the model - binary. + binary. - depreciated in favor of simargs + simargs: A dict with command line flags and possible options timeout: Possible timeout for the execution of OM. Returns: @@ -1056,6 +1087,8 @@ def linearize(self, lintime: Optional[float] = None, simflags: Optional[str] = N raise IOError("Linearization cannot be performed as the model is not build, " "use ModelicaSystem() to build the model first") + om_cmd = ModelicaSystemCmd(cmdpath=pathlib.Path(self.tempdir), modelname=self.modelName, timeout=timeout) + overrideLinearFile = pathlib.Path(self.tempdir) / f'{self.modelName}_override_linear.txt' with open(overrideLinearFile, "w") as file: @@ -1064,8 +1097,7 @@ def linearize(self, lintime: Optional[float] = None, simflags: Optional[str] = N for key, value in self.linearOptions.items(): file.write(f"{key}={value}\n") - override = " -overrideFile=" + overrideLinearFile.as_posix() - logger.debug(f"overwrite = {override}") + om_cmd.arg_set(key="-overrideFile", val=overrideLinearFile.as_posix()) if self.inputFlag: nameVal = self.getInputs() @@ -1076,26 +1108,30 @@ def linearize(self, lintime: Optional[float] = None, simflags: Optional[str] = N if l[0] < float(self.simulateOptions["startTime"]): raise ModelicaSystemError('Input time value is less than simulation startTime') self.csvFile = self.createCSVData() - csvinput = " -csvInput=" + self.csvFile.as_posix() - else: - csvinput = "" + om_cmd.arg_set(key="-csvInput", val=self.csvFile.as_posix()) - # prepare the linearization runtime command - exe_file = self.get_exe_file() + om_cmd.arg_set(key="-l", val=f"{lintime or self.linearOptions["stopTime"]}") - linruntime = f' -l={lintime or self.linearOptions["stopTime"]}' + # allow runtime simulation flags from user input + # TODO: merge into ModelicaSystemCmd? + if simflags is not None: + # add old style simulation arguments + warnings.warn("The argument simflags is depreciated and will be removed in future versions; " + "please use simargs instead", DeprecationWarning, stacklevel=1) + + args = [s for s in simflags.split(' ') if s] + for arg in args: + parts = arg.split('=') + if len(parts) == 1: + val = None + else: + val = '='.join(parts[1:]) + om_cmd.arg_set(key=parts[0], val=val) - if simflags is None: - simflags = "" - else: - simflags = " " + simflags + if simargs: + om_cmd.args_set(args=simargs) - if not exe_file.exists(): - raise ModelicaSystemError(f"Application file path not found: {exe_file}") - else: - cmd = exe_file.as_posix() + linruntime + override + csvinput + simflags - cmd = [s for s in cmd.split(' ') if s] - self._run_cmd(cmd=cmd, timeout=timeout) + self.simulationFlag = om_cmd.run() # code to get the matrix and linear inputs, outputs and states linearFile = pathlib.Path(self.tempdir) / "linearized_model.py" From ed7a160bb36adc64cb08ddca136f0e15e33220c4 Mon Sep 17 00:00:00 2001 From: syntron Date: Mon, 28 Apr 2025 13:15:51 +0200 Subject: [PATCH 03/21] [ModelicaSystemCmd] update handling of simargs --- OMPython/ModelicaSystem.py | 31 +++++++++++++++++++++---------- 1 file changed, 21 insertions(+), 10 deletions(-) diff --git a/OMPython/ModelicaSystem.py b/OMPython/ModelicaSystem.py index 1be4c87d..3d635cbb 100644 --- a/OMPython/ModelicaSystem.py +++ b/OMPython/ModelicaSystem.py @@ -132,8 +132,14 @@ def args_set(self, args: dict): def run(self): - cmd = [self._exe_file.as_posix()] + [f"{key}={self._args[key]}" for key in self._args] - self._run_cmd(cmd=cmd, timeout=self._timeout) + cmdl = [self._exe_file.as_posix()] + for key in self._args: + if self._args[key] is None: + cmdl.append(f"-{key}") + else: + cmdl.append(f"-{key}={self._args[key]}") + + self._run_cmd(cmd=cmdl, timeout=self._timeout) return True @@ -686,7 +692,7 @@ def simulate(self, resultfile: Optional[str] = None, simflags: Optional[str] = N >>> simulate() >>> simulate(resultfile="a.mat") >>> simulate(simflags="-noEventEmit -noRestart -override=e=0.3,g=10") # set runtime simulation flags - >>> simulate(simargs={"-noEventEmit": None, "-noRestart": None, "-override": "e=0.3,g=10"}) # using simargs + >>> simulate(simargs={"noEventEmit": None, "noRestart": None, "override": "e=0.3,g=10"}) # using simargs """ om_cmd = ModelicaSystemCmd(cmdpath=pathlib.Path(self.tempdir), modelname=self.modelName, timeout=timeout) @@ -699,7 +705,7 @@ def simulate(self, resultfile: Optional[str] = None, simflags: Optional[str] = N else: self.resultfile = (pathlib.Path(self.tempdir) / resultfile).as_posix() # always define the resultfile to use - om_cmd.arg_set(key="-r", val=self.resultfile) + om_cmd.arg_set(key="r", val=self.resultfile) # allow runtime simulation flags from user input # TODO: merge into ModelicaSystemCmd? @@ -710,6 +716,9 @@ def simulate(self, resultfile: Optional[str] = None, simflags: Optional[str] = N args = [s for s in simflags.split(' ') if s] for arg in args: + if arg[0] != '-': + raise ModelicaSystemError(f"Invalid simulation flag: {arg}") + arg = arg[1:] parts = arg.split('=') if len(parts) == 1: val = None @@ -729,7 +738,7 @@ def simulate(self, resultfile: Optional[str] = None, simflags: Optional[str] = N for key, value in tmpdict.items(): file.write(f"{key}={value}\n") - om_cmd.arg_set(key="-overrideFile", val=overrideFile.as_posix()) + om_cmd.arg_set(key="overrideFile", val=overrideFile.as_posix()) if self.inputFlag: # if model has input quantities for i in self.inputlist: @@ -745,7 +754,7 @@ def simulate(self, resultfile: Optional[str] = None, simflags: Optional[str] = N raise ModelicaSystemError(f"stopTime not matched for Input {i}!") self.csvFile = self.createCSVData() # create csv file - om_cmd.arg_set(key="-csvInput", val=self.csvFile.as_posix()) + om_cmd.arg_set(key="csvInput", val=self.csvFile.as_posix()) self.simulationFlag = om_cmd.run() @@ -1070,7 +1079,7 @@ def linearize(self, lintime: Optional[float] = None, simflags: Optional[str] = N lintime: Override linearOptions["stopTime"] value. simflags: A string of extra command line flags for the model binary. - depreciated in favor of simargs - simargs: A dict with command line flags and possible options + simargs: A dict with command line flags and possible options; example: "simargs={'csvInput': 'a.csv'}" timeout: Possible timeout for the execution of OM. Returns: @@ -1097,7 +1106,7 @@ def linearize(self, lintime: Optional[float] = None, simflags: Optional[str] = N for key, value in self.linearOptions.items(): file.write(f"{key}={value}\n") - om_cmd.arg_set(key="-overrideFile", val=overrideLinearFile.as_posix()) + om_cmd.arg_set(key="overrideFile", val=overrideLinearFile.as_posix()) if self.inputFlag: nameVal = self.getInputs() @@ -1108,9 +1117,9 @@ def linearize(self, lintime: Optional[float] = None, simflags: Optional[str] = N if l[0] < float(self.simulateOptions["startTime"]): raise ModelicaSystemError('Input time value is less than simulation startTime') self.csvFile = self.createCSVData() - om_cmd.arg_set(key="-csvInput", val=self.csvFile.as_posix()) + om_cmd.arg_set(key="csvInput", val=self.csvFile.as_posix()) - om_cmd.arg_set(key="-l", val=f"{lintime or self.linearOptions["stopTime"]}") + om_cmd.arg_set(key="l", val=f"{lintime or self.linearOptions["stopTime"]}") # allow runtime simulation flags from user input # TODO: merge into ModelicaSystemCmd? @@ -1121,6 +1130,8 @@ def linearize(self, lintime: Optional[float] = None, simflags: Optional[str] = N args = [s for s in simflags.split(' ') if s] for arg in args: + if arg[0] != '-': + raise ModelicaSystemError(f"Invalid simulation flag: {arg}") parts = arg.split('=') if len(parts) == 1: val = None From 086f7b5c676e64beb6f09f55530cf7cd31666a00 Mon Sep 17 00:00:00 2001 From: syntron Date: Mon, 28 Apr 2025 13:20:50 +0200 Subject: [PATCH 04/21] [ModelicaSystemCmd] move handling of simflags info this class --- OMPython/ModelicaSystem.py | 53 ++++++++++++++++---------------------- 1 file changed, 22 insertions(+), 31 deletions(-) diff --git a/OMPython/ModelicaSystem.py b/OMPython/ModelicaSystem.py index 3d635cbb..e1243817 100644 --- a/OMPython/ModelicaSystem.py +++ b/OMPython/ModelicaSystem.py @@ -189,6 +189,26 @@ def get_exe_file(self, tempdir, modelName) -> pathlib.Path: else: return pathlib.Path(tempdir) / modelName + def parse_simflags(self, simflags: str) -> dict: + # add old style simulation arguments + warnings.warn("The argument 'simflags' is depreciated and will be removed in future versions; " + "please use 'simargs' instead", DeprecationWarning, stacklevel=2) + + simargs = {} + + args = [s for s in simflags.split(' ') if s] + for arg in args: + if arg[0] != '-': + raise ModelicaSystemError(f"Invalid simulation flag: {arg}") + arg = arg[1:] + parts = arg.split('=') + if len(parts) == 1: + simargs[parts[0]] = None + else: + simargs[parts[0]] = '='.join(parts[1:]) + + return simargs + class ModelicaSystem: def __init__( @@ -708,23 +728,8 @@ def simulate(self, resultfile: Optional[str] = None, simflags: Optional[str] = N om_cmd.arg_set(key="r", val=self.resultfile) # allow runtime simulation flags from user input - # TODO: merge into ModelicaSystemCmd? if simflags is not None: - # add old style simulation arguments - warnings.warn("The argument simflags is depreciated and will be removed in future versions; " - "please use simargs instead", DeprecationWarning, stacklevel=1) - - args = [s for s in simflags.split(' ') if s] - for arg in args: - if arg[0] != '-': - raise ModelicaSystemError(f"Invalid simulation flag: {arg}") - arg = arg[1:] - parts = arg.split('=') - if len(parts) == 1: - val = None - else: - val = '='.join(parts[1:]) - om_cmd.arg_set(key=parts[0], val=val) + om_cmd.args_set(args=om_cmd.parse_simflags(simflags=simflags)) if simargs: om_cmd.args_set(args=simargs) @@ -1122,22 +1127,8 @@ def linearize(self, lintime: Optional[float] = None, simflags: Optional[str] = N om_cmd.arg_set(key="l", val=f"{lintime or self.linearOptions["stopTime"]}") # allow runtime simulation flags from user input - # TODO: merge into ModelicaSystemCmd? if simflags is not None: - # add old style simulation arguments - warnings.warn("The argument simflags is depreciated and will be removed in future versions; " - "please use simargs instead", DeprecationWarning, stacklevel=1) - - args = [s for s in simflags.split(' ') if s] - for arg in args: - if arg[0] != '-': - raise ModelicaSystemError(f"Invalid simulation flag: {arg}") - parts = arg.split('=') - if len(parts) == 1: - val = None - else: - val = '='.join(parts[1:]) - om_cmd.arg_set(key=parts[0], val=val) + om_cmd.args_set(args=om_cmd.parse_simflags(simflags=simflags)) if simargs: om_cmd.args_set(args=simargs) From e35b645b2376e742b85809e023603df003af4571 Mon Sep 17 00:00:00 2001 From: syntron Date: Mon, 28 Apr 2025 13:35:39 +0200 Subject: [PATCH 05/21] [ModelicaSystemCmd] cleanup / docstrings --- OMPython/ModelicaSystem.py | 134 +++++++++++++++++++++++++++---------- 1 file changed, 98 insertions(+), 36 deletions(-) diff --git a/OMPython/ModelicaSystem.py b/OMPython/ModelicaSystem.py index e1243817..bd6174ff 100644 --- a/OMPython/ModelicaSystem.py +++ b/OMPython/ModelicaSystem.py @@ -109,28 +109,69 @@ def __getitem__(self, index: int): class ModelicaSystemCmd: + """ + Execute a simulation by running the comiled model. + """ - def __init__(self, cmdpath: pathlib.Path, modelname: str, timeout: Optional[int] = None): - self.tempdir = cmdpath - self.modelName = modelname - self._exe_file = self.get_exe_file(tempdir=cmdpath, modelName=modelname) - if not self._exe_file.exists(): - raise ModelicaSystemError(f"Application file path not found: {self._exe_file}") + def __init__(self, runpath: pathlib.Path, modelname: str, timeout: Optional[int] = None) -> None: + """ + Initialisation + Parameters + ---------- + runpath : pathlib.Path + modelname : str + timeout : Optional[int], None + """ + self._runpath = pathlib.Path(runpath).resolve().absolute() + self._modelname = modelname self._timeout = timeout self._args = {} - def arg_set(self, key, val=None): + self._exe_file = self.get_exe_file(tempdir=runpath, modelname=modelname) + if not self._exe_file.exists(): + raise ModelicaSystemError(f"Application file path not found: {self._exe_file}") + + def arg_set(self, key: str, val: str = None) -> None: + """ + Set one argument for the executeable model. + + Parameters + ---------- + key : str + val : str, None + """ + if not isinstance(key, str): + raise ModelicaSystemError(f"Invalid argument key: {repr(key)} (type: {type(key)})") key = key.strip() if val is not None: + if not isinstance(val, str): + raise ModelicaSystemError(f"Invalid argument value for {repr(key)}: {repr(val)} (type: {type(val)})") val = val.strip() + if key in self._args: + logger.warning(f"Overwrite model executable argument: {repr(key)} = {repr(val)} " + f"(was: {repr(self._args[key])})") self._args[key] = val - def args_set(self, args: dict): + def args_set(self, args: dict) -> None: + """ + Define arguments for the model executable. + + Parameters + ---------- + args : dict + """ for arg in args: self.arg_set(key=arg, val=args[arg]) - def run(self): + def run(self) -> bool: + """ + Run the requested simulation + + Returns + ------- + bool + """ cmdl = [self._exe_file.as_posix()] for key in self._args: @@ -139,57 +180,78 @@ def run(self): else: cmdl.append(f"-{key}={self._args[key]}") - self._run_cmd(cmd=cmdl, timeout=self._timeout) - - return True - - def _run_cmd(self, cmd: list, timeout: Optional[int] = None): - logger.debug("Run OM command %s in %s", cmd, self.tempdir) + logger.debug("Run OM command %s in %s", repr(cmdl), self._runpath.as_posix()) if platform.system() == "Windows": - dllPath = "" + path_dll = "" # set the process environment from the generated .bat file in windows which should have all the dependencies - batFilePath = pathlib.Path(self.tempdir) / f"{self.modelName}.bat" - if not batFilePath.exists(): - ModelicaSystemError("Batch file (*.bat) does not exist " + str(batFilePath)) + path_bat = self._runpath / f"{self._modelname}.bat" + if not path_bat.exists(): + ModelicaSystemError("Batch file (*.bat) does not exist " + str(path_bat)) - with open(batFilePath, 'r') as file: + with open(path_bat, 'r') as file: for line in file: match = re.match(r"^SET PATH=([^%]*)", line, re.IGNORECASE) if match: - dllPath = match.group(1).strip(';') # Remove any trailing semicolons + path_dll = match.group(1).strip(';') # Remove any trailing semicolons my_env = os.environ.copy() - my_env["PATH"] = dllPath + os.pathsep + my_env["PATH"] + my_env["PATH"] = path_dll + os.pathsep + my_env["PATH"] else: # TODO: how to handle path to resources of external libraries for any system not Windows? my_env = None try: - cmdres = subprocess.run(cmd, capture_output=True, text=True, env=my_env, cwd=self.tempdir, - timeout=timeout) + cmdres = subprocess.run(cmdl, capture_output=True, text=True, env=my_env, cwd=self._runpath, + timeout=self._timeout) stdout = cmdres.stdout.strip() stderr = cmdres.stderr.strip() - logger.debug("OM output for command %s:\n%s", cmd, stdout) + logger.debug("OM output for command %s:\n%s", cmdl, stdout) if cmdres.returncode != 0: - raise ModelicaSystemError(f"Error running command {cmd}: return code = {cmdres.returncode}") + raise ModelicaSystemError(f"Error running command {cmdl}: return code = {cmdres.returncode}") if stderr: - raise ModelicaSystemError(f"Error running command {cmd}: {stderr}") + raise ModelicaSystemError(f"Error running command {cmdl}: {stderr}") except subprocess.TimeoutExpired: - raise ModelicaSystemError(f"Timeout running command {repr(cmd)}") + raise ModelicaSystemError(f"Timeout running command {repr(cmdl)}") except Exception as ex: - raise ModelicaSystemError(f"Error running command {cmd}") from ex + raise ModelicaSystemError(f"Error running command {cmdl}") from ex + + return True + + @staticmethod + def get_exe_file(tempdir: pathlib.Path, modelname: str) -> pathlib.Path: + """ + Get path to model executable. + + Parameters + ---------- + tempdir : pathlib.Path + modelname : str - def get_exe_file(self, tempdir, modelName) -> pathlib.Path: - """Get path to model executable.""" + Returns + ------- + pathlib.Path + """ if platform.system() == "Windows": - return pathlib.Path(tempdir) / f"{modelName}.exe" + return pathlib.Path(tempdir) / f"{modelname}.exe" else: - return pathlib.Path(tempdir) / modelName + return pathlib.Path(tempdir) / modelname + + @staticmethod + def parse_simflags(simflags: str) -> dict: + """ + Parse a simflag definition; this is depreciated! - def parse_simflags(self, simflags: str) -> dict: + Parameters + ---------- + simflags : str + + Returns + ------- + dict + """ # add old style simulation arguments warnings.warn("The argument 'simflags' is depreciated and will be removed in future versions; " "please use 'simargs' instead", DeprecationWarning, stacklevel=2) @@ -715,7 +777,7 @@ def simulate(self, resultfile: Optional[str] = None, simflags: Optional[str] = N >>> simulate(simargs={"noEventEmit": None, "noRestart": None, "override": "e=0.3,g=10"}) # using simargs """ - om_cmd = ModelicaSystemCmd(cmdpath=pathlib.Path(self.tempdir), modelname=self.modelName, timeout=timeout) + om_cmd = ModelicaSystemCmd(runpath=pathlib.Path(self.tempdir), modelname=self.modelName, timeout=timeout) if resultfile is None: # default result file generated by OM @@ -1101,7 +1163,7 @@ def linearize(self, lintime: Optional[float] = None, simflags: Optional[str] = N raise IOError("Linearization cannot be performed as the model is not build, " "use ModelicaSystem() to build the model first") - om_cmd = ModelicaSystemCmd(cmdpath=pathlib.Path(self.tempdir), modelname=self.modelName, timeout=timeout) + om_cmd = ModelicaSystemCmd(runpath=pathlib.Path(self.tempdir), modelname=self.modelName, timeout=timeout) overrideLinearFile = pathlib.Path(self.tempdir) / f'{self.modelName}_override_linear.txt' From 68268226d9d1d7f64caf11d9d73d6fabf64fe3d5 Mon Sep 17 00:00:00 2001 From: syntron Date: Mon, 28 Apr 2025 14:04:50 +0200 Subject: [PATCH 06/21] [ModelicaSystemCmd] simplify --- OMPython/ModelicaSystem.py | 33 +++++++++------------------------ 1 file changed, 9 insertions(+), 24 deletions(-) diff --git a/OMPython/ModelicaSystem.py b/OMPython/ModelicaSystem.py index bd6174ff..eed67895 100644 --- a/OMPython/ModelicaSystem.py +++ b/OMPython/ModelicaSystem.py @@ -128,10 +128,6 @@ def __init__(self, runpath: pathlib.Path, modelname: str, timeout: Optional[int] self._timeout = timeout self._args = {} - self._exe_file = self.get_exe_file(tempdir=runpath, modelname=modelname) - if not self._exe_file.exists(): - raise ModelicaSystemError(f"Application file path not found: {self._exe_file}") - def arg_set(self, key: str, val: str = None) -> None: """ Set one argument for the executeable model. @@ -173,7 +169,15 @@ def run(self) -> bool: bool """ - cmdl = [self._exe_file.as_posix()] + if platform.system() == "Windows": + path_exe = self._runpath / f"{self._modelname}.exe" + else: + path_exe = self._runpath / self._modelname + + if not path_exe.exists(): + raise ModelicaSystemError(f"Application file path not found: {path_exe}") + + cmdl = [path_exe.as_posix()] for key in self._args: if self._args[key] is None: cmdl.append(f"-{key}") @@ -220,25 +224,6 @@ def run(self) -> bool: return True - @staticmethod - def get_exe_file(tempdir: pathlib.Path, modelname: str) -> pathlib.Path: - """ - Get path to model executable. - - Parameters - ---------- - tempdir : pathlib.Path - modelname : str - - Returns - ------- - pathlib.Path - """ - if platform.system() == "Windows": - return pathlib.Path(tempdir) / f"{modelname}.exe" - else: - return pathlib.Path(tempdir) / modelname - @staticmethod def parse_simflags(simflags: str) -> dict: """ From 43350ee07cb7d609b8bf24bb49d74e91db9ecf0d Mon Sep 17 00:00:00 2001 From: syntron Date: Mon, 28 Apr 2025 13:42:02 +0200 Subject: [PATCH 07/21] [__init__] make ModelicaSystemCmd available --- OMPython/__init__.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/OMPython/__init__.py b/OMPython/__init__.py index 0d4ab686..53368a03 100644 --- a/OMPython/__init__.py +++ b/OMPython/__init__.py @@ -37,11 +37,12 @@ """ from OMPython.OMCSession import OMCSessionCmd, OMCSessionZMQ, OMCSessionException -from OMPython.ModelicaSystem import ModelicaSystem, ModelicaSystemError, LinearizationResult +from OMPython.ModelicaSystem import ModelicaSystem, ModelicaSystemCmd, ModelicaSystemError, LinearizationResult # global names imported if import 'from OMPython import *' is used __all__ = [ 'ModelicaSystem', + 'ModelicaSystemCmd', 'ModelicaSystemError', 'LinearizationResult', From a1e6fd38dd92acd1a931fa850c7e5a3c5f74c57d Mon Sep 17 00:00:00 2001 From: syntron Date: Mon, 28 Apr 2025 19:07:27 +0200 Subject: [PATCH 08/21] [ModelicaSystemCmd] special handling for override in simflags / simargs --- OMPython/ModelicaSystem.py | 45 ++++++++++++++++++++++++++++++-------- 1 file changed, 36 insertions(+), 9 deletions(-) diff --git a/OMPython/ModelicaSystem.py b/OMPython/ModelicaSystem.py index eed67895..31690133 100644 --- a/OMPython/ModelicaSystem.py +++ b/OMPython/ModelicaSystem.py @@ -34,6 +34,7 @@ import csv import logging +import numbers import os import platform import re @@ -128,7 +129,7 @@ def __init__(self, runpath: pathlib.Path, modelname: str, timeout: Optional[int] self._timeout = timeout self._args = {} - def arg_set(self, key: str, val: str = None) -> None: + def arg_set(self, key: str, val: str | dict = None) -> None: """ Set one argument for the executeable model. @@ -140,14 +141,26 @@ def arg_set(self, key: str, val: str = None) -> None: if not isinstance(key, str): raise ModelicaSystemError(f"Invalid argument key: {repr(key)} (type: {type(key)})") key = key.strip() - if val is not None: - if not isinstance(val, str): - raise ModelicaSystemError(f"Invalid argument value for {repr(key)}: {repr(val)} (type: {type(val)})") - val = val.strip() + if val is None: + argval = None + elif isinstance(val, str): + argval = val.strip() + elif isinstance(val, numbers.Number): + argval = str(val) + elif key == 'override' and isinstance(val, dict): + argval = self._args['override'] if 'override' in self._args else {} + for overwrite_key in val: + if not isinstance(overwrite_key, str) or not isinstance(val[overwrite_key], (str, numbers.Number)): + raise ModelicaSystemError("Invalid argument for 'override': " + f"{repr(overwrite_key)} = {repr(val[overwrite_key])}") + argval[overwrite_key] = val[overwrite_key] + else: + raise ModelicaSystemError(f"Invalid argument value for {repr(key)}: {repr(val)} (type: {type(val)})") + if key in self._args: - logger.warning(f"Overwrite model executable argument: {repr(key)} = {repr(val)} " + logger.warning(f"Overwrite model executable argument: {repr(key)} = {repr(argval)} " f"(was: {repr(self._args[key])})") - self._args[key] = val + self._args[key] = argval def args_set(self, args: dict) -> None: """ @@ -181,6 +194,9 @@ def run(self) -> bool: for key in self._args: if self._args[key] is None: cmdl.append(f"-{key}") + elif key == 'override' and isinstance(self._args[key], dict): + valstr = ','.join([f"{valkey}={str(self._args[key][valkey])}" for valkey in self._args[key]]) + cmdl.append(f"-{key}={valstr}") else: cmdl.append(f"-{key}={self._args[key]}") @@ -251,8 +267,19 @@ def parse_simflags(simflags: str) -> dict: parts = arg.split('=') if len(parts) == 1: simargs[parts[0]] = None - else: - simargs[parts[0]] = '='.join(parts[1:]) + elif parts[0] == 'override': + override = '='.join(parts[1:]) + + simargs[parts[0]] = {} + for item in override.split(','): + kv = item.split('=') + if not (0 < len(kv) < 3): + raise ModelicaSystemError(f"Invalide value for '-override': {override}") + if kv[0]: + try: + simargs[parts[0]][kv[0]] = kv[1] + except (KeyError, IndexError) as ex: + raise ModelicaSystemError(f"Invalide value for '-override': {override}") from ex return simargs From c2f2c280ea06aa3f42a97940c05b4445996ee4ea Mon Sep 17 00:00:00 2001 From: syntron Date: Mon, 28 Apr 2025 21:37:49 +0200 Subject: [PATCH 09/21] [ModelicaSystemCmd] split run() - create command in get_cmd() and get_exe() --- OMPython/ModelicaSystem.py | 33 +++++++++++++++++++++++++++++---- 1 file changed, 29 insertions(+), 4 deletions(-) diff --git a/OMPython/ModelicaSystem.py b/OMPython/ModelicaSystem.py index 31690133..4a4ed4cf 100644 --- a/OMPython/ModelicaSystem.py +++ b/OMPython/ModelicaSystem.py @@ -173,15 +173,14 @@ def args_set(self, args: dict) -> None: for arg in args: self.arg_set(key=arg, val=args[arg]) - def run(self) -> bool: + def get_exe(self) -> pathlib.Path: """ - Run the requested simulation + Get the path to the executable / complied model. Returns ------- - bool + pathlib.Path """ - if platform.system() == "Windows": path_exe = self._runpath / f"{self._modelname}.exe" else: @@ -190,6 +189,19 @@ def run(self) -> bool: if not path_exe.exists(): raise ModelicaSystemError(f"Application file path not found: {path_exe}") + return path_exe + + def get_cmd(self) -> list: + """ + Run the requested simulation + + Returns + ------- + list + """ + + path_exe = self.get_exe() + cmdl = [path_exe.as_posix()] for key in self._args: if self._args[key] is None: @@ -200,6 +212,19 @@ def run(self) -> bool: else: cmdl.append(f"-{key}={self._args[key]}") + return cmdl + + def run(self) -> int: + """ + Run the requested simulation + + Returns + ------- + int + """ + + cmdl: list = self.get_cmd() + logger.debug("Run OM command %s in %s", repr(cmdl), self._runpath.as_posix()) if platform.system() == "Windows": From 9d83dd490b7031dd8ed6428185a83ec73bf7b12c Mon Sep 17 00:00:00 2001 From: syntron Date: Mon, 28 Apr 2025 14:38:08 +0200 Subject: [PATCH 10/21] [ModelicaSystem] use pathlib.Path() / simplify --- OMPython/ModelicaSystem.py | 39 ++++++++++++++++++------------------ tests/test_ModelicaSystem.py | 2 +- 2 files changed, 21 insertions(+), 20 deletions(-) diff --git a/OMPython/ModelicaSystem.py b/OMPython/ModelicaSystem.py index 4a4ed4cf..c12e3069 100644 --- a/OMPython/ModelicaSystem.py +++ b/OMPython/ModelicaSystem.py @@ -317,7 +317,7 @@ def __init__( lmodel: Optional[list[str | tuple[str, str]]] = None, commandLineOptions: Optional[str] = None, variableFilter: Optional[str] = None, - customBuildDirectory: Optional[str | os.PathLike] = None, + customBuildDirectory: Optional[str | os.PathLike | pathlib.Path] = None, omhome: Optional[str] = None, session: Optional[OMCSessionZMQ] = None, build: Optional[bool] = True @@ -375,7 +375,6 @@ def __init__( self.linearinputs = [] # linearization input list self.linearoutputs = [] # linearization output list self.linearstates = [] # linearization states list - self.tempdir = "" if session is not None: if not isinstance(session, OMCSessionZMQ): @@ -413,7 +412,7 @@ def __init__( self.setCommandLineOptions("--linearizationDumpLanguage=python") self.setCommandLineOptions("--generateSymbolicLinearization") - self.setTempDirectory(customBuildDirectory) + self.tempdir = self.setTempDirectory(customBuildDirectory) if self.fileName is not None: self.loadLibrary(lmodel=self.lmodel) @@ -461,22 +460,24 @@ def loadLibrary(self, lmodel: list): '1)["Modelica"]\n' '2)[("Modelica","3.2.3"), "PowerSystems"]\n') - def setTempDirectory(self, customBuildDirectory): + def setTempDirectory(self, customBuildDirectory) -> pathlib.Path: # create a unique temp directory for each session and build the model in that directory if customBuildDirectory is not None: if not os.path.exists(customBuildDirectory): raise IOError(customBuildDirectory, " does not exist") - self.tempdir = customBuildDirectory + tempdir = pathlib.Path(customBuildDirectory) else: - self.tempdir = tempfile.mkdtemp() - if not os.path.exists(self.tempdir): - raise IOError(self.tempdir, " cannot be created") + tempdir = pathlib.Path(tempfile.mkdtemp()) + if not tempdir.is_dir(): + raise IOError(tempdir, " cannot be created") - logger.info("Define tempdir as %s", self.tempdir) - exp = f'cd("{pathlib.Path(self.tempdir).absolute().as_posix()}")' + logger.info("Define tempdir as %s", tempdir) + exp = f'cd("{tempdir.absolute().as_posix()}")' self.sendExpression(exp) - def getWorkDirectory(self): + return tempdir + + def getWorkDirectory(self) -> pathlib.Path: return self.tempdir def buildModel(self, variableFilter=None): @@ -814,15 +815,15 @@ def simulate(self, resultfile: Optional[str] = None, simflags: Optional[str] = N >>> simulate(simargs={"noEventEmit": None, "noRestart": None, "override": "e=0.3,g=10"}) # using simargs """ - om_cmd = ModelicaSystemCmd(runpath=pathlib.Path(self.tempdir), modelname=self.modelName, timeout=timeout) + om_cmd = ModelicaSystemCmd(runpath=self.tempdir, modelname=self.modelName, timeout=timeout) if resultfile is None: # default result file generated by OM - self.resultfile = (pathlib.Path(self.tempdir) / f"{self.modelName}_res.mat").as_posix() + self.resultfile = (self.tempdir / f"{self.modelName}_res.mat").as_posix() elif os.path.exists(resultfile): self.resultfile = resultfile else: - self.resultfile = (pathlib.Path(self.tempdir) / resultfile).as_posix() + self.resultfile = (self.tempdir / resultfile).as_posix() # always define the resultfile to use om_cmd.arg_set(key="r", val=self.resultfile) @@ -833,7 +834,7 @@ def simulate(self, resultfile: Optional[str] = None, simflags: Optional[str] = N if simargs: om_cmd.args_set(args=simargs) - overrideFile = pathlib.Path(self.tempdir) / f"{self.modelName}_override.txt" + overrideFile = self.tempdir / f"{self.modelName}_override.txt" if self.overridevariables or self.simoptionsoverride: tmpdict = self.overridevariables.copy() tmpdict.update(self.simoptionsoverride) @@ -1108,7 +1109,7 @@ def createCSVData(self) -> pathlib.Path: ] csv_rows.append(row) - csvFile = pathlib.Path(self.tempdir) / f'{self.modelName}.csv' + csvFile = self.tempdir / f'{self.modelName}.csv' with open(csvFile, "w", newline="") as f: writer = csv.writer(f) @@ -1200,9 +1201,9 @@ def linearize(self, lintime: Optional[float] = None, simflags: Optional[str] = N raise IOError("Linearization cannot be performed as the model is not build, " "use ModelicaSystem() to build the model first") - om_cmd = ModelicaSystemCmd(runpath=pathlib.Path(self.tempdir), modelname=self.modelName, timeout=timeout) + om_cmd = ModelicaSystemCmd(runpath=self.tempdir, modelname=self.modelName, timeout=timeout) - overrideLinearFile = pathlib.Path(self.tempdir) / f'{self.modelName}_override_linear.txt' + overrideLinearFile = self.tempdir / f'{self.modelName}_override_linear.txt' with open(overrideLinearFile, "w") as file: for key, value in self.overridevariables.items(): @@ -1235,7 +1236,7 @@ def linearize(self, lintime: Optional[float] = None, simflags: Optional[str] = N self.simulationFlag = om_cmd.run() # code to get the matrix and linear inputs, outputs and states - linearFile = pathlib.Path(self.tempdir) / "linearized_model.py" + linearFile = self.tempdir / "linearized_model.py" # support older openmodelica versions before OpenModelica v1.16.2 where linearize() generates "linear_modelname.mo" file if not linearFile.exists(): diff --git a/tests/test_ModelicaSystem.py b/tests/test_ModelicaSystem.py index 145aa526..66dfd90d 100644 --- a/tests/test_ModelicaSystem.py +++ b/tests/test_ModelicaSystem.py @@ -100,7 +100,7 @@ def test_customBuildDirectory(self): tmpdir = self.tmp / "tmpdir1" tmpdir.mkdir() m = OMPython.ModelicaSystem(filePath, "M", customBuildDirectory=tmpdir) - assert pathlib.Path(m.getWorkDirectory()).resolve() == tmpdir.resolve() + assert m.getWorkDirectory().resolve() == tmpdir.resolve() result_file = tmpdir / "a.mat" assert not result_file.exists() m.simulate(resultfile="a.mat") From bf21a7aa8fccb7d32e6f172530b167e8e6be8bd1 Mon Sep 17 00:00:00 2001 From: syntron Date: Mon, 28 Apr 2025 21:38:32 +0200 Subject: [PATCH 11/21] [ModelicaSystemCmd] do *NOT* raise error if returncode != 0 could be a simulation which stoped before the final time ... --- OMPython/ModelicaSystem.py | 34 +++++++++++++++++++++++----------- 1 file changed, 23 insertions(+), 11 deletions(-) diff --git a/OMPython/ModelicaSystem.py b/OMPython/ModelicaSystem.py index c12e3069..48eb171e 100644 --- a/OMPython/ModelicaSystem.py +++ b/OMPython/ModelicaSystem.py @@ -251,11 +251,10 @@ def run(self) -> int: timeout=self._timeout) stdout = cmdres.stdout.strip() stderr = cmdres.stderr.strip() + returncode = cmdres.returncode logger.debug("OM output for command %s:\n%s", cmdl, stdout) - if cmdres.returncode != 0: - raise ModelicaSystemError(f"Error running command {cmdl}: return code = {cmdres.returncode}") if stderr: raise ModelicaSystemError(f"Error running command {cmdl}: {stderr}") except subprocess.TimeoutExpired: @@ -263,7 +262,7 @@ def run(self) -> int: except Exception as ex: raise ModelicaSystemError(f"Error running command {cmdl}") from ex - return True + return returncode @staticmethod def parse_simflags(simflags: str) -> dict: @@ -400,7 +399,7 @@ def __init__( self.simulationFlag = False # if the model is simulated? self.outputFlag = False self.csvFile = '' # for storing inputs condition - self.resultfile = "" # for storing result file + self.resultfile = None # for storing result file self.variableFilter = variableFilter if self.fileName is not None and not self.fileName.is_file(): # if file does not exist @@ -819,13 +818,13 @@ def simulate(self, resultfile: Optional[str] = None, simflags: Optional[str] = N if resultfile is None: # default result file generated by OM - self.resultfile = (self.tempdir / f"{self.modelName}_res.mat").as_posix() + self.resultfile = self.tempdir / f"{self.modelName}_res.mat" elif os.path.exists(resultfile): - self.resultfile = resultfile + self.resultfile = pathlib.Path(resultfile) else: - self.resultfile = (self.tempdir / resultfile).as_posix() + self.resultfile = self.tempdir / resultfile # always define the resultfile to use - om_cmd.arg_set(key="r", val=self.resultfile) + om_cmd.arg_set(key="r", val=self.resultfile.as_posix()) # allow runtime simulation flags from user input if simflags is not None: @@ -861,7 +860,16 @@ def simulate(self, resultfile: Optional[str] = None, simflags: Optional[str] = N om_cmd.arg_set(key="csvInput", val=self.csvFile.as_posix()) - self.simulationFlag = om_cmd.run() + # delete resultfile ... + if self.resultfile.is_file(): + self.resultfile.unlink() + # ... run simulation ... + returncode = om_cmd.run() + # and check returncode *AND* resultfile + if returncode != 0 and self.resultfile.is_file(): + logger.warning(f"Return code = {returncode} but result file exists!") + + self.simulationFlag = True # to extract simulation results def getSolutions(self, varList=None, resultfile=None): # 12 @@ -877,7 +885,7 @@ def getSolutions(self, varList=None, resultfile=None): # 12 >>> getSolutions(["Name1","Name2"],resultfile=""c:/a.mat"") """ if resultfile is None: - resFile = self.resultfile + resFile = self.resultfile.as_posix() else: resFile = resultfile @@ -1233,7 +1241,11 @@ def linearize(self, lintime: Optional[float] = None, simflags: Optional[str] = N if simargs: om_cmd.args_set(args=simargs) - self.simulationFlag = om_cmd.run() + returncode = om_cmd.run() + if returncode != 0: + raise ModelicaSystemError(f"Linearize failed with return code: {returncode}") + + self.simulationFlag = True # code to get the matrix and linear inputs, outputs and states linearFile = self.tempdir / "linearized_model.py" From f02d50672fb3d3ded7b1e6a91f57809974a4b87e Mon Sep 17 00:00:00 2001 From: syntron Date: Sat, 3 May 2025 21:28:58 +0200 Subject: [PATCH 12/21] [ModelicaSystemCmd.run()] use repr(cmdl) in log messages / exceptions --- OMPython/ModelicaSystem.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/OMPython/ModelicaSystem.py b/OMPython/ModelicaSystem.py index 48eb171e..440bdefc 100644 --- a/OMPython/ModelicaSystem.py +++ b/OMPython/ModelicaSystem.py @@ -253,14 +253,14 @@ def run(self) -> int: stderr = cmdres.stderr.strip() returncode = cmdres.returncode - logger.debug("OM output for command %s:\n%s", cmdl, stdout) + logger.debug("OM output for command %s:\n%s", repr(cmdl), stdout) if stderr: - raise ModelicaSystemError(f"Error running command {cmdl}: {stderr}") + raise ModelicaSystemError(f"Error running command {repr(cmdl)}: {stderr}") except subprocess.TimeoutExpired: raise ModelicaSystemError(f"Timeout running command {repr(cmdl)}") except Exception as ex: - raise ModelicaSystemError(f"Error running command {cmdl}") from ex + raise ModelicaSystemError(f"Error running command {repr(cmdl)}") from ex return returncode From 59d11c2fbd7abbca7ae0fefe20031740e916b144 Mon Sep 17 00:00:00 2001 From: syntron Date: Mon, 12 May 2025 20:35:26 +0200 Subject: [PATCH 13/21] [ModelicaSystemCmd] fix exception handling * define specific exceptions --- OMPython/ModelicaSystem.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/OMPython/ModelicaSystem.py b/OMPython/ModelicaSystem.py index 440bdefc..96e44618 100644 --- a/OMPython/ModelicaSystem.py +++ b/OMPython/ModelicaSystem.py @@ -259,7 +259,7 @@ def run(self) -> int: raise ModelicaSystemError(f"Error running command {repr(cmdl)}: {stderr}") except subprocess.TimeoutExpired: raise ModelicaSystemError(f"Timeout running command {repr(cmdl)}") - except Exception as ex: + except subprocess.CalledProcessError as ex: raise ModelicaSystemError(f"Error running command {repr(cmdl)}") from ex return returncode From cf5e0b91dc68d198b5cd8112d777b8b2c43b76fd Mon Sep 17 00:00:00 2001 From: syntron Date: Mon, 28 Apr 2025 21:58:53 +0200 Subject: [PATCH 14/21] [tests] for ModelicaSystemCmd --- tests/test_ModelicaSystemCmd.py | 42 +++++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) create mode 100644 tests/test_ModelicaSystemCmd.py diff --git a/tests/test_ModelicaSystemCmd.py b/tests/test_ModelicaSystemCmd.py new file mode 100644 index 00000000..6257a2a6 --- /dev/null +++ b/tests/test_ModelicaSystemCmd.py @@ -0,0 +1,42 @@ +import OMPython +import pathlib +import shutil +import tempfile +import unittest + + +import logging +logger = logging.getLogger(__name__) +logging.basicConfig(level=logging.DEBUG) + + +class ModelicaSystemCmdTester(unittest.TestCase): + def __init__(self, *args, **kwargs): + super(ModelicaSystemCmdTester, self).__init__(*args, **kwargs) + self.tmp = pathlib.Path(tempfile.mkdtemp(prefix='tmpOMPython.tests')) + self.model = self.tmp / "M.mo" + with open(self.model, "w") as fout: + fout.write("""model M + Real x(start = 1, fixed = true); + parameter Real a = -1; +equation + der(x) = x*a; +end M; + """) + self.mod = OMPython.ModelicaSystem(self.model.as_posix(), "M") + + def __del__(self): + shutil.rmtree(self.tmp, ignore_errors=True) + + def test_simflags(self): + mscmd = OMPython.ModelicaSystemCmd(runpath=self.mod.tempdir, modelname=self.mod.modelName) + mscmd.args_set(args={"noEventEmit": None, "noRestart": None, "override": {'b': 2}}) + mscmd.args_set(args=mscmd.parse_simflags(simflags="-noEventEmit -noRestart -override=a=1,x=3")) + + logger.info(mscmd.get_cmd()) + + assert mscmd.get_cmd() == [mscmd.get_exe().as_posix(), '-noEventEmit', '-noRestart', '-override=b=2,a=1,x=3'] + + +if __name__ == '__main__': + unittest.main() From 4aaf76106da51af84a2a07c2ee2500660c1f24e8 Mon Sep 17 00:00:00 2001 From: syntron Date: Tue, 29 Apr 2025 21:14:41 +0200 Subject: [PATCH 15/21] [ModelicaSystem] fix flake8 error ./OMPython/ModelicaSystem.py:1236:72: E999 SyntaxError: f-string: unmatched '[' --- OMPython/ModelicaSystem.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/OMPython/ModelicaSystem.py b/OMPython/ModelicaSystem.py index 96e44618..3d036434 100644 --- a/OMPython/ModelicaSystem.py +++ b/OMPython/ModelicaSystem.py @@ -1232,7 +1232,7 @@ def linearize(self, lintime: Optional[float] = None, simflags: Optional[str] = N self.csvFile = self.createCSVData() om_cmd.arg_set(key="csvInput", val=self.csvFile.as_posix()) - om_cmd.arg_set(key="l", val=f"{lintime or self.linearOptions["stopTime"]}") + om_cmd.arg_set(key="l", val=str(lintime or self.linearOptions["stopTime"])) # allow runtime simulation flags from user input if simflags is not None: From f0f9b2824d718c4f77212d93e2996421fae85d9a Mon Sep 17 00:00:00 2001 From: syntron Date: Thu, 22 May 2025 16:48:10 +0200 Subject: [PATCH 16/21] [ModelicaSystemCmd] spelling fix --- OMPython/ModelicaSystem.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/OMPython/ModelicaSystem.py b/OMPython/ModelicaSystem.py index 530fce91..67df1e89 100644 --- a/OMPython/ModelicaSystem.py +++ b/OMPython/ModelicaSystem.py @@ -132,7 +132,7 @@ def __init__(self, runpath: pathlib.Path, modelname: str, timeout: Optional[int] def arg_set(self, key: str, val: str | dict = None) -> None: """ - Set one argument for the executeable model. + Set one argument for the executable model. Parameters ---------- From ca121888d4ce954045ea931d0807b5340227f1a9 Mon Sep 17 00:00:00 2001 From: syntron Date: Thu, 22 May 2025 16:48:27 +0200 Subject: [PATCH 17/21] [LinearizationResult] spelling fix --- OMPython/ModelicaSystem.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/OMPython/ModelicaSystem.py b/OMPython/ModelicaSystem.py index 67df1e89..883604da 100644 --- a/OMPython/ModelicaSystem.py +++ b/OMPython/ModelicaSystem.py @@ -112,7 +112,7 @@ def __getitem__(self, index: int): class ModelicaSystemCmd: """ - Execute a simulation by running the comiled model. + Execute a simulation by running the compiled model. """ def __init__(self, runpath: pathlib.Path, modelname: str, timeout: Optional[int] = None) -> None: From e33537a67feae1ca14795fb60982d3fb8f22ff7a Mon Sep 17 00:00:00 2001 From: syntron Date: Thu, 22 May 2025 16:48:52 +0200 Subject: [PATCH 18/21] [ModelicaSystemCmd] update definition of arg_set() - use Optional[] --- OMPython/ModelicaSystem.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/OMPython/ModelicaSystem.py b/OMPython/ModelicaSystem.py index 883604da..e5f0b4ad 100644 --- a/OMPython/ModelicaSystem.py +++ b/OMPython/ModelicaSystem.py @@ -130,7 +130,7 @@ def __init__(self, runpath: pathlib.Path, modelname: str, timeout: Optional[int] self._timeout = timeout self._args = {} - def arg_set(self, key: str, val: str | dict = None) -> None: + def arg_set(self, key: str, val: Optional[str | dict] = None) -> None: """ Set one argument for the executable model. From a8d2e31a0dcc42a7549b17fed53aee3121dfef96 Mon Sep 17 00:00:00 2001 From: syntron Date: Thu, 22 May 2025 19:18:41 +0200 Subject: [PATCH 19/21] [ModelicaSystemCmd] fix mypy warnings --- OMPython/ModelicaSystem.py | 29 +++++++++++++++-------------- 1 file changed, 15 insertions(+), 14 deletions(-) diff --git a/OMPython/ModelicaSystem.py b/OMPython/ModelicaSystem.py index e5f0b4ad..51a11233 100644 --- a/OMPython/ModelicaSystem.py +++ b/OMPython/ModelicaSystem.py @@ -128,7 +128,8 @@ def __init__(self, runpath: pathlib.Path, modelname: str, timeout: Optional[int] self._runpath = pathlib.Path(runpath).resolve().absolute() self._modelname = modelname self._timeout = timeout - self._args = {} + self._args: dict[str, str | None] = {} + self._arg_override: dict[str, str] = {} def arg_set(self, key: str, val: Optional[str | dict] = None) -> None: """ @@ -149,12 +150,13 @@ def arg_set(self, key: str, val: Optional[str | dict] = None) -> None: elif isinstance(val, numbers.Number): argval = str(val) elif key == 'override' and isinstance(val, dict): - argval = self._args['override'] if 'override' in self._args else {} - for overwrite_key in val: - if not isinstance(overwrite_key, str) or not isinstance(val[overwrite_key], (str, numbers.Number)): + for okey in val: + if not isinstance(okey, str) or not isinstance(val[okey], (str, numbers.Number)): raise ModelicaSystemError("Invalid argument for 'override': " - f"{repr(overwrite_key)} = {repr(val[overwrite_key])}") - argval[overwrite_key] = val[overwrite_key] + f"{repr(okey)} = {repr(val[okey])}") + self._arg_override[okey] = val[okey] + + argval = ','.join([f"{okey}={str(self._arg_override[okey])}" for okey in self._arg_override]) else: raise ModelicaSystemError(f"Invalid argument value for {repr(key)}: {repr(val)} (type: {type(val)})") @@ -207,9 +209,6 @@ def get_cmd(self) -> list: for key in self._args: if self._args[key] is None: cmdl.append(f"-{key}") - elif key == 'override' and isinstance(self._args[key], dict): - valstr = ','.join([f"{valkey}={str(self._args[key][valkey])}" for valkey in self._args[key]]) - cmdl.append(f"-{key}={valstr}") else: cmdl.append(f"-{key}={self._args[key]}") @@ -282,7 +281,7 @@ def parse_simflags(simflags: str) -> dict: warnings.warn("The argument 'simflags' is depreciated and will be removed in future versions; " "please use 'simargs' instead", DeprecationWarning, stacklevel=2) - simargs = {} + simargs: dict[str, Optional[str | dict[str, str]]] = {} args = [s for s in simflags.split(' ') if s] for arg in args: @@ -295,16 +294,18 @@ def parse_simflags(simflags: str) -> dict: elif parts[0] == 'override': override = '='.join(parts[1:]) - simargs[parts[0]] = {} + override_dict = {} for item in override.split(','): kv = item.split('=') if not (0 < len(kv) < 3): - raise ModelicaSystemError(f"Invalide value for '-override': {override}") + raise ModelicaSystemError(f"Invalid value for '-override': {override}") if kv[0]: try: - simargs[parts[0]][kv[0]] = kv[1] + override_dict[kv[0]] = kv[1] except (KeyError, IndexError) as ex: - raise ModelicaSystemError(f"Invalide value for '-override': {override}") from ex + raise ModelicaSystemError(f"Invalid value for '-override': {override}") from ex + + simargs[parts[0]] = override_dict return simargs From 3a70dffd2f75faf4e0fe79fd50a2de0f43ddaed1 Mon Sep 17 00:00:00 2001 From: syntron Date: Thu, 22 May 2025 21:20:03 +0200 Subject: [PATCH 20/21] [ModelicaSystemCmd] some cleanup of docstring for parse_simplags() --- OMPython/ModelicaSystem.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/OMPython/ModelicaSystem.py b/OMPython/ModelicaSystem.py index 51a11233..36a5c7b7 100644 --- a/OMPython/ModelicaSystem.py +++ b/OMPython/ModelicaSystem.py @@ -269,6 +269,8 @@ def parse_simflags(simflags: str) -> dict: """ Parse a simflag definition; this is depreciated! + The return data can be used as input for self.args_set(). + Parameters ---------- simflags : str @@ -277,7 +279,6 @@ def parse_simflags(simflags: str) -> dict: ------- dict """ - # add old style simulation arguments warnings.warn("The argument 'simflags' is depreciated and will be removed in future versions; " "please use 'simargs' instead", DeprecationWarning, stacklevel=2) From 860d6cf431798c6d2de820abb55107324e7ba45a Mon Sep 17 00:00:00 2001 From: syntron Date: Thu, 22 May 2025 21:25:23 +0200 Subject: [PATCH 21/21] [ModelicaSystemCmd] additional type hints clarifications --- OMPython/ModelicaSystem.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/OMPython/ModelicaSystem.py b/OMPython/ModelicaSystem.py index 36a5c7b7..9e2c46e1 100644 --- a/OMPython/ModelicaSystem.py +++ b/OMPython/ModelicaSystem.py @@ -165,13 +165,13 @@ def arg_set(self, key: str, val: Optional[str | dict] = None) -> None: f"(was: {repr(self._args[key])})") self._args[key] = argval - def args_set(self, args: dict) -> None: + def args_set(self, args: dict[str, Optional[str | dict[str, str]]]) -> None: """ Define arguments for the model executable. Parameters ---------- - args : dict + args : dict[str, Optional[str | dict[str, str]]] """ for arg in args: self.arg_set(key=arg, val=args[arg]) @@ -265,7 +265,7 @@ def run(self) -> int: return returncode @staticmethod - def parse_simflags(simflags: str) -> dict: + def parse_simflags(simflags: str) -> dict[str, Optional[str | dict[str, str]]]: """ Parse a simflag definition; this is depreciated! @@ -812,7 +812,7 @@ def getOptimizationOptions(self, names=None): # 10 raise ModelicaSystemError("Unhandled input for getOptimizationOptions()") def simulate(self, resultfile: Optional[str] = None, simflags: Optional[str] = None, - simargs: Optional[dict[str, str | None]] = None, + simargs: Optional[dict[str, Optional[str | dict[str, str]]]] = None, timeout: Optional[int] = None): # 11 """ This method simulates model according to the simulation options. @@ -1193,7 +1193,7 @@ def optimize(self): # 21 return optimizeResult def linearize(self, lintime: Optional[float] = None, simflags: Optional[str] = None, - simargs: Optional[dict[str, str | None]] = None, + simargs: Optional[dict[str, Optional[str | dict[str, str]]]] = None, timeout: Optional[int] = None) -> LinearizationResult: """Linearize the model according to linearOptions.