From ba103d7176cee635160d34741a92edc7d7e82469 Mon Sep 17 00:00:00 2001 From: syntron Date: Thu, 24 Apr 2025 20:59:31 +0200 Subject: [PATCH 01/56] [ModelicaSystem] simplify subprocess.Popen() => use subprocess.run() --- OMPython/ModelicaSystem.py | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/OMPython/ModelicaSystem.py b/OMPython/ModelicaSystem.py index fbaa5876..1a50c4de 100644 --- a/OMPython/ModelicaSystem.py +++ b/OMPython/ModelicaSystem.py @@ -310,18 +310,13 @@ def _run_cmd(self, cmd: list): my_env = None try: - p = subprocess.Popen(cmd, env=my_env, stdout=subprocess.PIPE, - stderr=subprocess.PIPE, cwd=self.tempdir) - stdout, stderr = p.communicate() - - stdout = stdout.decode('ascii').strip() - stderr = stderr.decode('ascii').strip() - if stderr: + cmdres = subprocess.run(cmd, capture_output=True, text=True, env=my_env, cwd=self.tempdir) + stdout = cmdres.stdout.strip() + stderr = cmdres.stderr.strip() + if cmdres.returncode != 0 or stderr: raise ModelicaSystemError(f"Error running command {cmd}: {stderr}") if self._verbose and stdout: logger.info("OM output for command %s:\n%s", cmd, stdout) - p.wait() - p.terminate() except Exception as e: raise ModelicaSystemError(f"Exception {type(e)} running command {cmd}: {e}") From 78e7ae241ceb02a78d5d0b9c6494a848f5a200d5 Mon Sep 17 00:00:00 2001 From: syntron Date: Thu, 24 Apr 2025 21:00:45 +0200 Subject: [PATCH 02/56] [ModelicaSystem] add timeout to subprocess.run() in _run_cmd() --- OMPython/ModelicaSystem.py | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/OMPython/ModelicaSystem.py b/OMPython/ModelicaSystem.py index 1a50c4de..4ff15ae5 100644 --- a/OMPython/ModelicaSystem.py +++ b/OMPython/ModelicaSystem.py @@ -287,7 +287,7 @@ def setTempDirectory(self, customBuildDirectory): def getWorkDirectory(self): return self.tempdir - def _run_cmd(self, cmd: list): + 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": @@ -310,13 +310,16 @@ def _run_cmd(self, cmd: list): my_env = None try: - cmdres = subprocess.run(cmd, capture_output=True, text=True, env=my_env, cwd=self.tempdir) + 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() if cmdres.returncode != 0 or stderr: raise ModelicaSystemError(f"Error running command {cmd}: {stderr}") if self._verbose and stdout: logger.info("OM output for command %s:\n%s", cmd, stdout) + except subprocess.TimeoutExpired: + raise ModelicaSystemError(f"Timeout running command {repr(cmd)}") except Exception as e: raise ModelicaSystemError(f"Exception {type(e)} running command {cmd}: {e}") @@ -661,7 +664,7 @@ def get_exe_file(self) -> pathlib.Path: else: return pathlib.Path(self.tempdir) / self.modelName - def simulate(self, resultfile=None, simflags=None): # 11 + def simulate(self, resultfile=None, simflags=None, timeout: Optional[int] = None): # 11 """ This method simulates model according to the simulation options. usage @@ -724,7 +727,7 @@ def simulate(self, resultfile=None, simflags=None): # 11 cmd = exe_file.as_posix() + override + csvinput + r + simflags cmd = cmd.split(" ") - self._run_cmd(cmd=cmd) + self._run_cmd(cmd=cmd, timeout=timeout) self.simulationFlag = True # to extract simulation results @@ -1041,7 +1044,8 @@ def optimize(self): # 21 return optimizeResult - def linearize(self, lintime: Optional[float] = None, simflags: Optional[str] = None) -> LinearizationResult: + def linearize(self, lintime: Optional[float] = None, simflags: Optional[str] = None, + timeout: Optional[int] = None) -> LinearizationResult: """Linearize the model according to linearOptions. Args: @@ -1102,7 +1106,7 @@ def linearize(self, lintime: Optional[float] = None, simflags: Optional[str] = N else: cmd = exe_file.as_posix() + linruntime + override + csvinput + simflags cmd = cmd.split(' ') - self._run_cmd(cmd=cmd) + self._run_cmd(cmd=cmd, timeout=timeout) # code to get the matrix and linear inputs, outputs and states linearFile = pathlib.Path(self.tempdir) / "linearized_model.py" From b93f12107c014d6c3c62a21b64db57a6ff3cf116 Mon Sep 17 00:00:00 2001 From: syntron Date: Fri, 25 Apr 2025 22:47:15 +0200 Subject: [PATCH 03/56] [ModelicaSystem._run_cmd()] differentiate between OM error and nonzero return code --- OMPython/ModelicaSystem.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/OMPython/ModelicaSystem.py b/OMPython/ModelicaSystem.py index 4ff15ae5..b139627b 100644 --- a/OMPython/ModelicaSystem.py +++ b/OMPython/ModelicaSystem.py @@ -314,7 +314,9 @@ def _run_cmd(self, cmd: list, timeout: Optional[int] = None): timeout=timeout) stdout = cmdres.stdout.strip() stderr = cmdres.stderr.strip() - if cmdres.returncode != 0 or stderr: + if cmdres.returncode != 0: + raise ModelicaSystemError(f"Error running command {cmd}: nonzero return code") + if stderr: raise ModelicaSystemError(f"Error running command {cmd}: {stderr}") if self._verbose and stdout: logger.info("OM output for command %s:\n%s", cmd, stdout) From 2b9221623a7f233667059ba93a6355cd9cabb1a2 Mon Sep 17 00:00:00 2001 From: syntron Date: Fri, 25 Apr 2025 23:15:34 +0200 Subject: [PATCH 04/56] [ModelicaSystem.linearize()] fix docstring / add description for timeout --- OMPython/ModelicaSystem.py | 1 + 1 file changed, 1 insertion(+) diff --git a/OMPython/ModelicaSystem.py b/OMPython/ModelicaSystem.py index b139627b..1a870afd 100644 --- a/OMPython/ModelicaSystem.py +++ b/OMPython/ModelicaSystem.py @@ -1054,6 +1054,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. + timeout: Possible timeout for the execution of OM. Returns: A LinearizationResult object is returned. This allows several From 64b72f32717ba00e8522ca3826a40a6586f69139 Mon Sep 17 00:00:00 2001 From: syntron Date: Wed, 23 Apr 2025 19:38:54 +0200 Subject: [PATCH 05/56] [ModelicaSystem] simplify code in xmlparse() --- OMPython/ModelicaSystem.py | 16 ++++------------ 1 file changed, 4 insertions(+), 12 deletions(-) diff --git a/OMPython/ModelicaSystem.py b/OMPython/ModelicaSystem.py index fbaa5876..3e920a8f 100644 --- a/OMPython/ModelicaSystem.py +++ b/OMPython/ModelicaSystem.py @@ -395,19 +395,11 @@ def xmlparse(self): scalar["changeable"] = sv.get('isValueChangeable') scalar["aliasvariable"] = sv.get('aliasVariable') ch = list(sv) - start = None - min = None - max = None - unit = None for att in ch: - start = att.get('start') - min = att.get('min') - max = att.get('max') - unit = att.get('unit') - scalar["start"] = start - scalar["min"] = min - scalar["max"] = max - scalar["unit"] = unit + scalar["start"] = att.get('start') + scalar["min"] = att.get('min') + scalar["max"] = att.get('max') + scalar["unit"] = att.get('unit') if scalar["variability"] == "parameter": if scalar["name"] in self.overridevariables: From d9643e5ddce7c531ff97e2201e0ffc96c980af8d Mon Sep 17 00:00:00 2001 From: syntron Date: Wed, 23 Apr 2025 20:29:03 +0200 Subject: [PATCH 06/56] [ModelicaSystem] add ModelicaSystemError() for unhandled final else cases there are a lot of functions which check the type of the input; this is done by if ... elif ... - however, invalid input is not catched, i.e. there is *NO* return value define (would be None) if the input is not matching any of the if branches --- OMPython/ModelicaSystem.py | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/OMPython/ModelicaSystem.py b/OMPython/ModelicaSystem.py index 3e920a8f..1dcde994 100644 --- a/OMPython/ModelicaSystem.py +++ b/OMPython/ModelicaSystem.py @@ -430,6 +430,8 @@ def getQuantities(self, names=None): # 3 elif isinstance(names, list): return [x for y in names for x in self.quantitiesList if x["name"] == y] + raise ModelicaSystemError("Unhandled input for getQuantities()") + def getContinuous(self, names=None): # 4 """ This method returns dict. The key is continuous names and value is corresponding continuous value. @@ -474,6 +476,8 @@ def getContinuous(self, names=None): # 4 raise ModelicaSystemError(f"OM error: {i} is not continuous") return valuelist + raise ModelicaSystemError("Unhandled input for getContinous()") + def getParameters(self, names: Optional[str | list[str]] = None) -> dict[str, str] | list[str]: # 5 """Get parameter values. @@ -503,6 +507,8 @@ def getParameters(self, names: Optional[str | list[str]] = None) -> dict[str, st elif isinstance(names, list): return ([self.paramlist.get(x, "NotExist") for x in names]) + raise ModelicaSystemError("Unhandled input for getParameters()") + def getInputs(self, names: Optional[str | list[str]] = None) -> dict | list: # 6 """Get input values. @@ -537,6 +543,8 @@ def getInputs(self, names: Optional[str | list[str]] = None) -> dict | list: # elif isinstance(names, list): return ([self.inputlist.get(x, "NotExist") for x in names]) + raise ModelicaSystemError("Unhandled input for getInputs()") + def getOutputs(self, names: Optional[str | list[str]] = None): # 7 """Get output values. @@ -605,6 +613,8 @@ def getOutputs(self, names: Optional[str | list[str]] = None): # 7 return (i, "is not Output") return valuelist + raise ModelicaSystemError("Unhandled input for getOutputs()") + def getSimulationOptions(self, names=None): # 8 """ This method returns dict. The key is simulation option names and value is corresponding simulation option value. @@ -621,6 +631,8 @@ def getSimulationOptions(self, names=None): # 8 elif isinstance(names, list): return ([self.simulateOptions.get(x, "NotExist") for x in names]) + raise ModelicaSystemError("Unhandled input for getSimulationOptions()") + def getLinearizationOptions(self, names=None): # 9 """ This method returns dict. The key is linearize option names and value is corresponding linearize option value. @@ -637,6 +649,8 @@ def getLinearizationOptions(self, names=None): # 9 elif isinstance(names, list): return ([self.linearOptions.get(x, "NotExist") for x in names]) + raise ModelicaSystemError("Unhandled input for getLinearizationOptions()") + def getOptimizationOptions(self, names=None): # 10 """ usage: @@ -651,6 +665,8 @@ def getOptimizationOptions(self, names=None): # 10 elif isinstance(names, list): return ([self.optimizeOptions.get(x, "NotExist") for x in names]) + raise ModelicaSystemError("Unhandled input for getOptimizationOptions()") + def get_exe_file(self) -> pathlib.Path: """Get path to model executable.""" if platform.system() == "Windows": @@ -773,12 +789,16 @@ def getSolutions(self, varList=None, resultfile=None): # 12 self.sendExpression("closeSimulationResultFile()") return npRes + raise ModelicaSystemError("Unhandled input for getSolutions()") + def strip_space(self, name): if isinstance(name, str): return name.replace(" ", "") elif isinstance(name, list): return [x.replace(" ", "") for x in name] + raise ModelicaSystemError("Unhandled input for strip_space()") + def setMethodHelper(self, args1, args2, args3, args4=None): """ Helper function for setParameter(),setContinuous(),setSimulationOptions(),setLinearizationOption(),setOptimizationOption() From 41f9876ec43bc961dff0bd5fb5bf96bb68ef0297 Mon Sep 17 00:00:00 2001 From: syntron Date: Wed, 23 Apr 2025 20:33:46 +0200 Subject: [PATCH 07/56] [ModelicaSystem.getSolution()] do not try to continue on error but fail Rule: fail early, fail hard - tell the user that something is wrong! In this case, the user ask for the solution but could get None (= plain 'return') - this would case hard to track errors later (if verbose==False and raiseerrors==False) --- OMPython/ModelicaSystem.py | 20 ++++++++------------ 1 file changed, 8 insertions(+), 12 deletions(-) diff --git a/OMPython/ModelicaSystem.py b/OMPython/ModelicaSystem.py index 1dcde994..32a801a6 100644 --- a/OMPython/ModelicaSystem.py +++ b/OMPython/ModelicaSystem.py @@ -760,29 +760,24 @@ def getSolutions(self, varList=None, resultfile=None): # 12 # check for result file exits if not os.path.exists(resFile): - errstr = f"Error: Result file does not exist {resFile}" - self._raise_error(errstr=errstr) - return + raise ModelicaSystemError(f"Result file does not exist {resFile}") resultVars = self.sendExpression(f'readSimulationResultVars("{resFile}")') self.sendExpression("closeSimulationResultFile()") if varList is None: return resultVars elif isinstance(varList, str): if varList not in resultVars and varList != "time": - self._raise_error(errstr=f'!!! {varList} does not exist') - return + raise ModelicaSystemError(f"Requested data {repr(varList)} does not exist") res = self.sendExpression(f'readSimulationResult("{resFile}", {{{varList}}})') npRes = np.array(res) self.sendExpression("closeSimulationResultFile()") return npRes elif isinstance(varList, list): - # varList, = varList - for v in varList: - if v == "time": + for var in varList: + if var == "time": continue - if v not in resultVars: - self._raise_error(errstr=f'!!! {v} does not exist') - return + if var not in resultVars: + raise ModelicaSystemError(f"Requested data {repr(var)} does not exist") variables = ",".join(varList) res = self.sendExpression(f'readSimulationResult("{resFile}",{{{variables}}})') npRes = np.array(res) @@ -823,7 +818,8 @@ def apply_single(args1): return True else: - self._raise_error(errstr=f'"{value[0]}" is not a {args3} variable') + raise ModelicaSystemError("Unhandled case in setMethodHelper.apply_single() - " + f"{repr(value[0])} is not a {repr(args3)} variable") result = [] if isinstance(args1, str): From d0665486628be7242b3d010ad6360bcc60ba1aa1 Mon Sep 17 00:00:00 2001 From: syntron Date: Wed, 23 Apr 2025 20:36:06 +0200 Subject: [PATCH 08/56] [ModelicaSystem] remove redundant parentheses (type hint by PyCharm) --- OMPython/ModelicaSystem.py | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/OMPython/ModelicaSystem.py b/OMPython/ModelicaSystem.py index 32a801a6..7ef2965e 100644 --- a/OMPython/ModelicaSystem.py +++ b/OMPython/ModelicaSystem.py @@ -505,7 +505,7 @@ def getParameters(self, names: Optional[str | list[str]] = None) -> dict[str, st elif isinstance(names, str): return [self.paramlist.get(names, "NotExist")] elif isinstance(names, list): - return ([self.paramlist.get(x, "NotExist") for x in names]) + return [self.paramlist.get(x, "NotExist") for x in names] raise ModelicaSystemError("Unhandled input for getParameters()") @@ -541,7 +541,7 @@ def getInputs(self, names: Optional[str | list[str]] = None) -> dict | list: # elif isinstance(names, str): return [self.inputlist.get(names, "NotExist")] elif isinstance(names, list): - return ([self.inputlist.get(x, "NotExist") for x in names]) + return [self.inputlist.get(x, "NotExist") for x in names] raise ModelicaSystemError("Unhandled input for getInputs()") @@ -588,7 +588,7 @@ def getOutputs(self, names: Optional[str | list[str]] = None): # 7 elif isinstance(names, str): return [self.outputlist.get(names, "NotExist")] else: - return ([self.outputlist.get(x, "NotExist") for x in names]) + return [self.outputlist.get(x, "NotExist") for x in names] else: if names is None: for i in self.outputlist: @@ -601,7 +601,7 @@ def getOutputs(self, names: Optional[str | list[str]] = None): # 7 self.outputlist[names] = value[0][-1] return [self.outputlist.get(names)] else: - return (names, " is not Output") + return names, " is not Output" elif isinstance(names, list): valuelist = [] for i in names: @@ -610,7 +610,7 @@ def getOutputs(self, names: Optional[str | list[str]] = None): # 7 self.outputlist[i] = value[0][-1] valuelist.append(value[0][-1]) else: - return (i, "is not Output") + return i, "is not Output" return valuelist raise ModelicaSystemError("Unhandled input for getOutputs()") @@ -629,7 +629,7 @@ def getSimulationOptions(self, names=None): # 8 elif isinstance(names, str): return [self.simulateOptions.get(names, "NotExist")] elif isinstance(names, list): - return ([self.simulateOptions.get(x, "NotExist") for x in names]) + return [self.simulateOptions.get(x, "NotExist") for x in names] raise ModelicaSystemError("Unhandled input for getSimulationOptions()") @@ -647,7 +647,7 @@ def getLinearizationOptions(self, names=None): # 9 elif isinstance(names, str): return [self.linearOptions.get(names, "NotExist")] elif isinstance(names, list): - return ([self.linearOptions.get(x, "NotExist") for x in names]) + return [self.linearOptions.get(x, "NotExist") for x in names] raise ModelicaSystemError("Unhandled input for getLinearizationOptions()") @@ -663,7 +663,7 @@ def getOptimizationOptions(self, names=None): # 10 elif isinstance(names, str): return [self.optimizeOptions.get(names, "NotExist")] elif isinstance(names, list): - return ([self.optimizeOptions.get(x, "NotExist") for x in names]) + return [self.optimizeOptions.get(x, "NotExist") for x in names] raise ModelicaSystemError("Unhandled input for getOptimizationOptions()") @@ -855,7 +855,7 @@ def setParameters(self, pvals): # 14 def isParameterChangeable(self, name, value): q = self.getQuantities(name) - if (q[0]["changeable"] == "false"): + if q[0]["changeable"] == "false": if self._verbose: logger.info("setParameters() failed : It is not possible to set " f'the following signal "{name}", It seems to be structural, final, ' @@ -924,10 +924,10 @@ def setInputs(self, name): # 15 value = var.split("=") if value[0] in self.inputlist: tmpvalue = eval(value[1]) - if (isinstance(tmpvalue, int) or isinstance(tmpvalue, float)): + if isinstance(tmpvalue, int) or isinstance(tmpvalue, float): self.inputlist[value[0]] = [(float(self.simulateOptions["startTime"]), float(value[1])), (float(self.simulateOptions["stopTime"]), float(value[1]))] - elif (isinstance(tmpvalue, list)): + elif isinstance(tmpvalue, list): self.checkValidInputs(tmpvalue) self.inputlist[value[0]] = tmpvalue self.inputFlag = True From 87a58b716ff3495ffbb6330ae51f182dffae3ee5 Mon Sep 17 00:00:00 2001 From: syntron Date: Thu, 24 Apr 2025 21:01:56 +0200 Subject: [PATCH 09/56] [OMCSessionException] add exception handling for OMCSession* --- OMPython/OMCSession.py | 4 ++++ OMPython/__init__.py | 3 ++- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/OMPython/OMCSession.py b/OMPython/OMCSession.py index 63fbba00..0f38023e 100644 --- a/OMPython/OMCSession.py +++ b/OMPython/OMCSession.py @@ -75,6 +75,10 @@ def wait(self, timeout): return self.process.wait(timeout=timeout) +class OMCSessionException(Exception): + pass + + class OMCSessionBase(metaclass=abc.ABCMeta): def __init__(self, readonly=False): diff --git a/OMPython/__init__.py b/OMPython/__init__.py index 29eaca99..eee36acc 100644 --- a/OMPython/__init__.py +++ b/OMPython/__init__.py @@ -36,7 +36,7 @@ CONDITIONS OF OSMC-PL. """ -from OMPython.OMCSession import OMCSessionBase, OMCSessionZMQ +from OMPython.OMCSession import OMCSessionBase, OMCSessionZMQ, OMCSessionException from OMPython.ModelicaSystem import ModelicaSystem, ModelicaSystemError, LinearizationResult # global names imported if import 'from OMPython import *' is used @@ -45,6 +45,7 @@ 'ModelicaSystemError', 'LinearizationResult', + 'OMCSessionException', 'OMCSessionZMQ', 'OMCSessionBase', ] From e60986084b07fdcc7aaf31235669327abec41637 Mon Sep 17 00:00:00 2001 From: syntron Date: Thu, 24 Apr 2025 21:21:31 +0200 Subject: [PATCH 10/56] [OMCSessionZMQ] use specific exceptions instead of generic Exception where possible --- OMPython/OMCSession.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/OMPython/OMCSession.py b/OMPython/OMCSession.py index 0f38023e..e51b5fe7 100644 --- a/OMPython/OMCSession.py +++ b/OMPython/OMCSession.py @@ -373,9 +373,10 @@ def __del__(self): self._omc_log_file.close() try: self._omc_process.wait(timeout=2.0) - except Exception: + except subprocess.TimeoutExpired: if self._omc_process: - logger.warning("OMC did not exit after being sent the quit() command; killing the process with pid=%s", self._omc_process.pid) + logger.warning("OMC did not exit after being sent the quit() command; " + "killing the process with pid=%s", self._omc_process.pid) self._omc_process.kill() self._omc_process.wait() @@ -405,14 +406,14 @@ def _start_omc_process(self, timeout): try: with open(self._dockerCidFile, "r") as fin: self._dockerCid = fin.read().strip() - except Exception: + except IOError: pass if self._dockerCid: break time.sleep(timeout / 40.0) try: os.remove(self._dockerCidFile) - except Exception: + except FileNotFoundError: pass if self._dockerCid is None: logger.error("Docker did not start. Log-file says:\n%s" % (open(self._omc_log_file.name).read())) @@ -520,9 +521,10 @@ def _connect_to_omc(self, timeout): while True: if self._dockerCid: try: - self._port = subprocess.check_output(["docker", "exec", self._dockerCid, "cat", self._port_file], stderr=subprocess.DEVNULL).decode().strip() + self._port = subprocess.check_output(["docker", "exec", self._dockerCid, "cat", self._port_file], + stderr=subprocess.DEVNULL).decode().strip() break - except Exception: + except subprocess.CalledProcessError: pass else: if os.path.isfile(self._port_file): From 029321439fac8f5479d6589b71ef050687d52ccd Mon Sep 17 00:00:00 2001 From: syntron Date: Fri, 25 Apr 2025 22:01:22 +0200 Subject: [PATCH 11/56] [OMCSessionBase] use OMCSessionException instead of generic Exception --- OMPython/OMCSession.py | 33 ++++++++++++++++++--------------- 1 file changed, 18 insertions(+), 15 deletions(-) diff --git a/OMPython/OMCSession.py b/OMPython/OMCSession.py index e51b5fe7..8883ffa1 100644 --- a/OMPython/OMCSession.py +++ b/OMPython/OMCSession.py @@ -126,7 +126,7 @@ def ask(self, question, opt=None, parsed=True): try: res = self.sendExpression(expression, parsed=parsed) - except Exception: + except OMCSessionException: logger.error("OMC failed: %s, %s, parsed=%s", question, opt, parsed) raise @@ -368,7 +368,7 @@ def __init__(self, readonly=False, timeout=10.00, def __del__(self): try: self.sendExpression("quit()") - except Exception: + except OMCSessionException: pass self._omc_log_file.close() try: @@ -417,7 +417,8 @@ def _start_omc_process(self, timeout): pass if self._dockerCid is None: logger.error("Docker did not start. Log-file says:\n%s" % (open(self._omc_log_file.name).read())) - raise Exception("Docker did not start (timeout=%f might be too short especially if you did not docker pull the image before this command)." % timeout) + raise OMCSessionException("Docker did not start (timeout=%f might be too short especially if you did " + "not docker pull the image before this command)." % timeout) dockerTop = None if self._docker or self._dockerContainer: @@ -434,17 +435,16 @@ def _start_omc_process(self, timeout): try: self._omc_process = DummyPopen(int(columns[1])) except psutil.NoSuchProcess: - raise Exception( - f"Could not find PID {dockerTop} - is this a docker instance spawned without --pid=host?\n" - f"Log-file says:\n{open(self._omc_log_file.name).read()}") + raise OMCSessionException( + f"Could not find PID {dockerTop} - is this a docker instance spawned " + f"without --pid=host?\nLog-file says:\n{open(self._omc_log_file.name).read()}") break if self._omc_process is not None: break time.sleep(timeout / 40.0) if self._omc_process is None: - - raise Exception("Docker top did not contain omc process %s:\n%s\nLog-file says:\n%s" - % (self._random_string, dockerTop, open(self._omc_log_file.name).read())) + raise OMCSessionException("Docker top did not contain omc process %s:\n%s\nLog-file says:\n%s" + % (self._random_string, dockerTop, open(self._omc_log_file.name).read())) return self._omc_process def _getuid(self): @@ -465,7 +465,9 @@ def _set_omc_command(self, omc_path_and_args_list): if (self._docker or self._dockerContainer) and sys.platform == "win32": extraFlags = ["-d=zmqDangerousAcceptConnectionsFromAnywhere"] if not self._interactivePort: - raise Exception("docker on Windows requires knowing which port to connect to. For dockerContainer=..., the container needs to have already manually exposed this port when it was started (-p 127.0.0.1:n:n) or you get an error later.") + raise OMCSessionException("docker on Windows requires knowing which port to connect to. For " + "dockerContainer=..., the container needs to have already manually exposed " + "this port when it was started (-p 127.0.0.1:n:n) or you get an error later.") else: extraFlags = [] if self._docker: @@ -478,7 +480,7 @@ def _set_omc_command(self, omc_path_and_args_list): dockerNetworkStr = [] extraFlags = ["-d=zmqDangerousAcceptConnectionsFromAnywhere"] else: - raise Exception('dockerNetwork was set to %s, but only \"host\" or \"separate\" is allowed') + raise OMCSessionException('dockerNetwork was set to %s, but only \"host\" or \"separate\" is allowed') self._dockerCidFile = self._omc_log_file.name + ".docker.cid" omcCommand = ["docker", "run", "--cidfile", self._dockerCidFile, "--rm", "--env", "USER=%s" % self._currentUser, "--user", str(self._getuid())] + self._dockerExtraArgs + dockerNetworkStr + [self._docker, self._dockerOpenModelicaPath] elif self._dockerContainer: @@ -508,7 +510,7 @@ def _get_omhome(self, omhome: str = None): if path_to_omc is not None: return pathlib.Path(path_to_omc).parents[1] - raise ValueError("Cannot find OpenModelica executable, please install from openmodelica.org") + raise OMCSessionException("Cannot find OpenModelica executable, please install from openmodelica.org") def _get_omc_path(self) -> pathlib.Path: return self.omhome / "bin" / "omc" @@ -539,7 +541,8 @@ def _connect_to_omc(self, timeout): name = self._omc_log_file.name self._omc_log_file.close() logger.error("OMC Server did not start. Please start it! Log-file says:\n%s" % open(name).read()) - raise Exception(f"OMC Server did not start (timeout={timeout}). Could not open file {self._port_file}") + raise OMCSessionException(f"OMC Server did not start (timeout={timeout}). " + "Could not open file {self._port_file}") time.sleep(timeout / 80.0) self._port = self._port.replace("0.0.0.0", self._serverIPAddress) @@ -555,7 +558,7 @@ def _connect_to_omc(self, timeout): def sendExpression(self, command, parsed=True): p = self._omc_process.poll() # check if process is running if p is not None: - raise Exception("Process Exited, No connection with OMC. Create a new instance of OMCSessionZMQ") + raise OMCSessionException("Process Exited, No connection with OMC. Create a new instance of OMCSessionZMQ!") attempts = 0 while True: @@ -569,7 +572,7 @@ def sendExpression(self, command, parsed=True): self._omc_log_file.seek(0) log = self._omc_log_file.read() self._omc_log_file.close() - raise Exception(f"No connection with OMC (timeout={self._timeout}). Log-file says: \n{log}") + raise OMCSessionException(f"No connection with OMC (timeout={self._timeout}). Log-file says: \n{log}") time.sleep(self._timeout / 50.0) if command == "quit()": self._omc.close() From 0135cdca35d9a1ec612c6553fe3edc359106efd0 Mon Sep 17 00:00:00 2001 From: syntron Date: Thu, 24 Apr 2025 20:36:08 +0200 Subject: [PATCH 12/56] [OMParser] use a single entry point for OMParser --- OMPython/OMCSession.py | 11 +++-------- OMPython/OMParser.py | 12 ++++++++++++ 2 files changed, 15 insertions(+), 8 deletions(-) diff --git a/OMPython/OMCSession.py b/OMPython/OMCSession.py index 63fbba00..43de2f9c 100644 --- a/OMPython/OMCSession.py +++ b/OMPython/OMCSession.py @@ -52,7 +52,7 @@ # TODO: replace this with the new parser from OMPython import OMTypedParser -from OMPython import OMParser +from OMPython.OMParser import om_parser_basic # define logger using the current module name as ID @@ -81,9 +81,6 @@ def __init__(self, readonly=False): self._readonly = readonly self._omc_cache = {} - def clearOMParserResult(self): - OMParser.result = {} - def execute(self, command): warnings.warn("This function is depreciated and will be removed in future versions; " "please use sendExpression() instead", DeprecationWarning, stacklevel=1) @@ -246,8 +243,7 @@ def getComponentModifierValue(self, className, componentName): logger.warning('OMTypedParser error: %s', ex.message) result = self.ask('getComponentModifierValue', f'{className}, {componentName}', parsed=False) try: - answer = OMParser.check_for_values(result) - OMParser.result = {} + answer = om_parser_basic(result) return answer[2:] except (TypeError, UnboundLocalError) as ex: logger.warning('OMParser error: %s', ex) @@ -264,8 +260,7 @@ def getExtendsModifierValue(self, className, extendsName, modifierName): logger.warning('OMTypedParser error: %s', ex.message) result = self.ask('getExtendsModifierValue', f'{className}, {extendsName}, {modifierName}', parsed=False) try: - answer = OMParser.check_for_values(result) - OMParser.result = {} + answer = om_parser_basic(result) return answer[2:] except (TypeError, UnboundLocalError) as ex: logger.warning('OMParser error: %s', ex) diff --git a/OMPython/OMParser.py b/OMPython/OMParser.py index f1708947..c9bb1796 100644 --- a/OMPython/OMParser.py +++ b/OMPython/OMParser.py @@ -892,3 +892,15 @@ def check_for_values(string): check_for_values(next_set) return result + + +# TODO: hack to be able to use one entry point wich also resets the (global) variable results +# this should be checked such that the content of this file can be used as class with correct handling of +# variable usage +def om_parser_basic(string: str): + result_return = check_for_values(string=string) + + global result + result = {} + + return result_return From e0ff3edabf29bb808eb13c68faac538cbeb815b6 Mon Sep 17 00:00:00 2001 From: syntron Date: Thu, 24 Apr 2025 20:38:40 +0200 Subject: [PATCH 13/56] [OMTypedParser] use a single entry point for OMTypedParser --- OMPython/OMCSession.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/OMPython/OMCSession.py b/OMPython/OMCSession.py index 43de2f9c..029569fe 100644 --- a/OMPython/OMCSession.py +++ b/OMPython/OMCSession.py @@ -51,7 +51,7 @@ import warnings # TODO: replace this with the new parser -from OMPython import OMTypedParser +from OMPython.OMTypedParser import parseString as om_parser_typed from OMPython.OMParser import om_parser_basic @@ -567,7 +567,7 @@ def sendExpression(self, command, parsed=True): else: result = self._omc.recv_string() if parsed is True: - answer = OMTypedParser.parseString(result) + answer = om_parser_typed(result) return answer else: return result From 8d642d54df73a636e8c4b9c83d7e288421022162 Mon Sep 17 00:00:00 2001 From: syntron Date: Thu, 24 Apr 2025 20:45:43 +0200 Subject: [PATCH 14/56] [OMCSessionBase] fix exception handling for pyparsing.ParseException * fix ex.message => ex.msg --- OMPython/OMCSession.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/OMPython/OMCSession.py b/OMPython/OMCSession.py index 029569fe..f96f0ccd 100644 --- a/OMPython/OMCSession.py +++ b/OMPython/OMCSession.py @@ -194,7 +194,7 @@ def getClassComment(self, className): return self.ask('getClassComment', className) except pyparsing.ParseException as ex: logger.warning("Method 'getClassComment' failed for %s", className) - logger.warning('OMTypedParser error: %s', ex.message) + logger.warning('OMTypedParser error: %s', ex.msg) return 'No description available' def getNthComponent(self, className, comp_id): @@ -229,7 +229,7 @@ def getParameterValue(self, className, parameterName): try: return self.ask('getParameterValue', f'{className}, {parameterName}') except pyparsing.ParseException as ex: - logger.warning('OMTypedParser error: %s', ex.message) + logger.warning('OMTypedParser error: %s', ex.msg) return "" def getComponentModifierNames(self, className, componentName): @@ -240,7 +240,7 @@ def getComponentModifierValue(self, className, componentName): # FIXME: OMPython exception UnboundLocalError exception for 'Modelica.Fluid.Machines.ControlledPump' return self.ask('getComponentModifierValue', f'{className}, {componentName}') except pyparsing.ParseException as ex: - logger.warning('OMTypedParser error: %s', ex.message) + logger.warning('OMTypedParser error: %s', ex.msg) result = self.ask('getComponentModifierValue', f'{className}, {componentName}', parsed=False) try: answer = om_parser_basic(result) @@ -257,7 +257,7 @@ def getExtendsModifierValue(self, className, extendsName, modifierName): # FIXME: OMPython exception UnboundLocalError exception for 'Modelica.Fluid.Machines.ControlledPump' return self.ask('getExtendsModifierValue', f'{className}, {extendsName}, {modifierName}') except pyparsing.ParseException as ex: - logger.warning('OMTypedParser error: %s', ex.message) + logger.warning('OMTypedParser error: %s', ex.msg) result = self.ask('getExtendsModifierValue', f'{className}, {extendsName}, {modifierName}', parsed=False) try: answer = om_parser_basic(result) From f16e486a1924c8de222df0fd8ec7cc981fdce973 Mon Sep 17 00:00:00 2001 From: syntron Date: Thu, 24 Apr 2025 20:53:18 +0200 Subject: [PATCH 15/56] [OMCSessionBase] simplify the two use cases of OMParser.om_parse_basic() --- OMPython/OMCSession.py | 46 ++++++++++++++++++++---------------------- 1 file changed, 22 insertions(+), 24 deletions(-) diff --git a/OMPython/OMCSession.py b/OMPython/OMCSession.py index f96f0ccd..9c495f13 100644 --- a/OMPython/OMCSession.py +++ b/OMPython/OMCSession.py @@ -128,6 +128,24 @@ def ask(self, question, opt=None, parsed=True): return res + def _ask_with_fallback(self, question: str, opt: str = None): + """ + Version of ask() with OMTypedParser as fallback for OMParser which is used in ask() => sendExpression() if + parsed is set to True. + """ + try: + # FIXME: OMPython exception UnboundLocalError exception for 'Modelica.Fluid.Machines.ControlledPump' + return self.ask(question=question, opt=opt) + except pyparsing.ParseException as ex: + logger.warning('OMTypedParser error: %s', ex.msg) + result = self.ask(question=question, opt=opt, parsed=False) + try: + answer = om_parser_basic(result) + return answer[2:] + except (TypeError, UnboundLocalError) as ex: + logger.warning('OMParser error: %s', ex) + return result + # TODO: Open Modelica Compiler API functions. Would be nice to generate these. def loadFile(self, filename): return self.ask('loadFile', f'"{filename}"') @@ -236,35 +254,15 @@ def getComponentModifierNames(self, className, componentName): return self.ask('getComponentModifierNames', f'{className}, {componentName}') def getComponentModifierValue(self, className, componentName): - try: - # FIXME: OMPython exception UnboundLocalError exception for 'Modelica.Fluid.Machines.ControlledPump' - return self.ask('getComponentModifierValue', f'{className}, {componentName}') - except pyparsing.ParseException as ex: - logger.warning('OMTypedParser error: %s', ex.msg) - result = self.ask('getComponentModifierValue', f'{className}, {componentName}', parsed=False) - try: - answer = om_parser_basic(result) - return answer[2:] - except (TypeError, UnboundLocalError) as ex: - logger.warning('OMParser error: %s', ex) - return result + return self._ask_with_fallback(question='getComponentModifierValue', + opt=f'{className}, {componentName}') def getExtendsModifierNames(self, className, componentName): return self.ask('getExtendsModifierNames', f'{className}, {componentName}') def getExtendsModifierValue(self, className, extendsName, modifierName): - try: - # FIXME: OMPython exception UnboundLocalError exception for 'Modelica.Fluid.Machines.ControlledPump' - return self.ask('getExtendsModifierValue', f'{className}, {extendsName}, {modifierName}') - except pyparsing.ParseException as ex: - logger.warning('OMTypedParser error: %s', ex.msg) - result = self.ask('getExtendsModifierValue', f'{className}, {extendsName}, {modifierName}', parsed=False) - try: - answer = om_parser_basic(result) - return answer[2:] - except (TypeError, UnboundLocalError) as ex: - logger.warning('OMParser error: %s', ex) - return result + return self._ask_with_fallback(question='getExtendsModifierValue', + opt=f'{className}, {extendsName}, {modifierName}') def getNthComponentModification(self, className, comp_id): # FIXME: OMPython exception Results KeyError exception From 4c666129011ccee1b08a712318cc968fd0b09fec Mon Sep 17 00:00:00 2001 From: syntron Date: Thu, 24 Apr 2025 21:20:35 +0200 Subject: [PATCH 16/56] [DummyPopen] remove redundant parentheses --- OMPython/OMCSession.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/OMPython/OMCSession.py b/OMPython/OMCSession.py index 2f8518ff..5e7a421c 100644 --- a/OMPython/OMCSession.py +++ b/OMPython/OMCSession.py @@ -59,7 +59,7 @@ logger = logging.getLogger(__name__) -class DummyPopen(): +class DummyPopen: def __init__(self, pid): self.pid = pid self.process = psutil.Process(pid) From 30dbf355b64e758a7dc4933c904f20bcd28584de Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Sluka?= Date: Sat, 12 Apr 2025 15:49:54 +0200 Subject: [PATCH 17/56] Fix two spaces in simflags causing error --- OMPython/ModelicaSystem.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/OMPython/ModelicaSystem.py b/OMPython/ModelicaSystem.py index 9f3402c1..3954b0c8 100644 --- a/OMPython/ModelicaSystem.py +++ b/OMPython/ModelicaSystem.py @@ -736,7 +736,7 @@ def simulate(self, resultfile=None, simflags=None, timeout: Optional[int] = None raise Exception(f"Error: Application file path not found: {exe_file}") cmd = exe_file.as_posix() + override + csvinput + r + simflags - cmd = cmd.split(" ") + cmd = [s for s in cmd.split(' ') if s] self._run_cmd(cmd=cmd, timeout=timeout) self.simulationFlag = True @@ -1116,7 +1116,7 @@ def linearize(self, lintime: Optional[float] = None, simflags: Optional[str] = N raise Exception(f"Error: Application file path not found: {exe_file}") else: cmd = exe_file.as_posix() + linruntime + override + csvinput + simflags - cmd = cmd.split(' ') + cmd = [s for s in cmd.split(' ') if s] self._run_cmd(cmd=cmd, timeout=timeout) # code to get the matrix and linear inputs, outputs and states From ebe6961d68059e6361dcca8bb3bfb343df97200b Mon Sep 17 00:00:00 2001 From: syntron Date: Sat, 26 Apr 2025 21:34:50 +0200 Subject: [PATCH 18/56] [ModelicaSystem] remove not needed variables --- OMPython/ModelicaSystem.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/OMPython/ModelicaSystem.py b/OMPython/ModelicaSystem.py index 3954b0c8..90e9a087 100644 --- a/OMPython/ModelicaSystem.py +++ b/OMPython/ModelicaSystem.py @@ -183,10 +183,6 @@ def __init__( else: self.getconn = OMCSessionZMQ(omhome=omhome) - # needed for properly deleting the session - self._omc_log_file = self.getconn._omc_log_file - self._omc_process = self.getconn._omc_process - # set commandLineOptions if provided by users self.setCommandLineOptions(commandLineOptions=commandLineOptions) From 882b127a7a64e850db1b947fbfdb7a52a62b99d6 Mon Sep 17 00:00:00 2001 From: syntron Date: Sat, 26 Apr 2025 21:36:54 +0200 Subject: [PATCH 19/56] [ModelicaSystem] csvFile --- OMPython/ModelicaSystem.py | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/OMPython/ModelicaSystem.py b/OMPython/ModelicaSystem.py index 90e9a087..c4547b5e 100644 --- a/OMPython/ModelicaSystem.py +++ b/OMPython/ModelicaSystem.py @@ -722,8 +722,8 @@ def simulate(self, resultfile=None, simflags=None, timeout: Optional[int] = None errstr = f"!!! stopTime not matched for Input {i}" self._raise_error(errstr=errstr) return - self.createCSVData() # create csv file - csvinput = " -csvInput=" + self.csvFile + self.csvFile = self.createCSVData() # create csv file + csvinput = " -csvInput=" + self.csvFile.as_posix() else: csvinput = "" @@ -944,7 +944,7 @@ def checkValidInputs(self, name): else: ModelicaSystemError('Error!!! Value must be in tuple format') - def createCSVData(self) -> None: + def createCSVData(self) -> pathlib.Path: start_time: float = float(self.simulateOptions["startTime"]) stop_time: float = float(self.simulateOptions["stopTime"]) @@ -985,12 +985,14 @@ def createCSVData(self) -> None: ] csv_rows.append(row) - self.csvFile: str = (pathlib.Path(self.tempdir) / f'{self.modelName}.csv').as_posix() + csvFile = pathlib.Path(self.tempdir) / f'{self.modelName}.csv' - with open(self.csvFile, "w", newline="") as f: + with open(csvFile, "w", newline="") as f: writer = csv.writer(f) writer.writerows(csv_rows) + return csvFile + # to convert Modelica model to FMU def convertMo2Fmu(self, version="2.0", fmuType="me_cs", fileNamePrefix="", includeResources=True): # 19 """ @@ -1093,8 +1095,8 @@ def linearize(self, lintime: Optional[float] = None, simflags: Optional[str] = N for l in tupleList: if l[0] < float(self.simulateOptions["startTime"]): raise ModelicaSystemError('Input time value is less than simulation startTime') - self.createCSVData() - csvinput = " -csvInput=" + self.csvFile + self.csvFile = self.createCSVData() + csvinput = " -csvInput=" + self.csvFile.as_posix() else: csvinput = "" From 3662fd5558b78020de790b35dea5034825cf6c38 Mon Sep 17 00:00:00 2001 From: syntron Date: Sat, 26 Apr 2025 21:37:24 +0200 Subject: [PATCH 20/56] [ModelicaSystem] tempdir --- OMPython/ModelicaSystem.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/OMPython/ModelicaSystem.py b/OMPython/ModelicaSystem.py index c4547b5e..b178f903 100644 --- a/OMPython/ModelicaSystem.py +++ b/OMPython/ModelicaSystem.py @@ -277,7 +277,7 @@ def setTempDirectory(self, customBuildDirectory): raise IOError(self.tempdir, " cannot be created") logger.info("Define tempdir as %s", self.tempdir) - exp = f'cd("{pathlib.Path(self.tempdir).as_posix()}")' + exp = f'cd("{pathlib.Path(self.tempdir).absolute().as_posix()}")' self.sendExpression(exp) def getWorkDirectory(self): From 52df1d8fa10d6499f9661e59b518922679de6045 Mon Sep 17 00:00:00 2001 From: syntron Date: Sat, 26 Apr 2025 21:37:46 +0200 Subject: [PATCH 21/56] [ModelicaSystem] batFilePath --- OMPython/ModelicaSystem.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/OMPython/ModelicaSystem.py b/OMPython/ModelicaSystem.py index b178f903..84277343 100644 --- a/OMPython/ModelicaSystem.py +++ b/OMPython/ModelicaSystem.py @@ -292,7 +292,7 @@ def _run_cmd(self, cmd: list, timeout: Optional[int] = None): # 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 " + batFilePath) + ModelicaSystemError("Batch file (*.bat) does not exist " + str(batFilePath)) with open(batFilePath, 'r') as file: for line in file: From 3878503cf9a9b68be33e98fcc1f0401d18f46061 Mon Sep 17 00:00:00 2001 From: syntron Date: Sat, 26 Apr 2025 21:39:04 +0200 Subject: [PATCH 22/56] [ModelicaSystem] fileName / lmodel --- OMPython/ModelicaSystem.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/OMPython/ModelicaSystem.py b/OMPython/ModelicaSystem.py index 84277343..64c329b3 100644 --- a/OMPython/ModelicaSystem.py +++ b/OMPython/ModelicaSystem.py @@ -202,7 +202,7 @@ def __init__( self._raiseerrors = raiseerrors - if fileName is not None and not self.fileName.is_file(): # if file does not exist + if self.fileName is not None and not self.fileName.is_file(): # if file does not exist raise IOError(f"File Error: {self.fileName} does not exist!!!") # set default command Line Options for linearization as @@ -213,13 +213,13 @@ def __init__( self.setTempDirectory(customBuildDirectory) - if fileName is not None: - self.loadLibrary() - self.loadFile() + if self.fileName is not None: + self.loadLibrary(lmodel=self.lmodel) + self.loadFile(fileName=self.fileName) # allow directly loading models from MSL without fileName - if fileName is None and modelName is not None: - self.loadLibrary() + elif fileName is None and modelName is not None: + self.loadLibrary(lmodel=self.lmodel) self.buildModel(variableFilter) @@ -231,17 +231,17 @@ def setCommandLineOptions(self, commandLineOptions: str): if not self.sendExpression(exp): self._check_error() - def loadFile(self): + def loadFile(self, fileName: pathlib.Path): # load file - loadMsg = self.sendExpression(f'loadFile("{self.fileName.as_posix()}")') + loadMsg = self.sendExpression(f'loadFile("{fileName.as_posix()}")') # Show notification or warnings to the user when verbose=True OR if some error occurred i.e., not result if self._verbose or not loadMsg: self._check_error() # for loading file/package, loading model and building model - def loadLibrary(self): + def loadLibrary(self, lmodel: list): # load Modelica standard libraries or Modelica files if needed - for element in self.lmodel: + for element in lmodel: if element is not None: if isinstance(element, str): if element.endswith(".mo"): From 1ee22a678a44f2b6b76e6030d4c13e6800e63672 Mon Sep 17 00:00:00 2001 From: syntron Date: Mon, 28 Apr 2025 08:52:27 +0200 Subject: [PATCH 23/56] [tests] fix definition of lmodel - should be a list --- 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 15e94227..0d6d0ff9 100644 --- a/tests/test_FMIExport.py +++ b/tests/test_FMIExport.py @@ -14,7 +14,8 @@ def __del__(self): def testCauerLowPassAnalog(self): print("testing Cauer") - mod = OMPython.ModelicaSystem(modelName="Modelica.Electrical.Analog.Examples.CauerLowPassAnalog", lmodel="Modelica") + mod = OMPython.ModelicaSystem(modelName="Modelica.Electrical.Analog.Examples.CauerLowPassAnalog", + lmodel=["Modelica"]) self.tmp = mod.getWorkDirectory() fmu = mod.convertMo2Fmu(fileNamePrefix="CauerLowPassAnalog") @@ -22,7 +23,7 @@ def testCauerLowPassAnalog(self): def testDrumBoiler(self): print("testing DrumBoiler") - mod = OMPython.ModelicaSystem(modelName="Modelica.Fluid.Examples.DrumBoiler.DrumBoiler", lmodel="Modelica") + mod = OMPython.ModelicaSystem(modelName="Modelica.Fluid.Examples.DrumBoiler.DrumBoiler", lmodel=["Modelica"]) self.tmp = mod.getWorkDirectory() fmu = mod.convertMo2Fmu(fileNamePrefix="DrumBoiler") From 9a50b883b24f387555b7b02bec0ddd8884c8f382 Mon Sep 17 00:00:00 2001 From: syntron Date: Sat, 26 Apr 2025 21:39:23 +0200 Subject: [PATCH 24/56] [ModelicaSystem] resultfile --- OMPython/ModelicaSystem.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/OMPython/ModelicaSystem.py b/OMPython/ModelicaSystem.py index 64c329b3..a24619f0 100644 --- a/OMPython/ModelicaSystem.py +++ b/OMPython/ModelicaSystem.py @@ -679,14 +679,14 @@ def simulate(self, resultfile=None, simflags=None, timeout: Optional[int] = None >>> simulate(simflags="-noEventEmit -noRestart -override=e=0.3,g=10") # set runtime simulation flags """ if resultfile is None: - r = "" + # default result file generated by OM self.resultfile = (pathlib.Path(self.tempdir) / f"{self.modelName}_res.mat").as_posix() + elif os.path.exists(resultfile): + self.resultfile = resultfile else: - if os.path.exists(resultfile): - self.resultfile = resultfile - else: - self.resultfile = (pathlib.Path(self.tempdir) / resultfile).as_posix() - r = " -r=" + self.resultfile + self.resultfile = (pathlib.Path(self.tempdir) / resultfile).as_posix() + # always define the resultfile to use + resultfileflag = " -r=" + self.resultfile # allow runtime simulation flags from user input if simflags is None: @@ -731,7 +731,7 @@ def simulate(self, resultfile=None, simflags=None, timeout: Optional[int] = None if not exe_file.exists(): raise Exception(f"Error: Application file path not found: {exe_file}") - cmd = exe_file.as_posix() + override + csvinput + r + simflags + 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 From 498eebe76a508ea5749205676f4dc6b4cd398461 Mon Sep 17 00:00:00 2001 From: syntron Date: Sat, 26 Apr 2025 21:54:27 +0200 Subject: [PATCH 25/56] [ModelicaSystem] check session --- OMPython/ModelicaSystem.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/OMPython/ModelicaSystem.py b/OMPython/ModelicaSystem.py index a24619f0..25059308 100644 --- a/OMPython/ModelicaSystem.py +++ b/OMPython/ModelicaSystem.py @@ -179,6 +179,8 @@ def __init__( self._verbose = verbose if session is not None: + if not isinstance(session, OMCSessionZMQ): + raise ModelicaSystemError("Invalid session data provided!") self.getconn = session else: self.getconn = OMCSessionZMQ(omhome=omhome) From be1fa5751e6095b63aa9b115e6b84dfdc30d44ae Mon Sep 17 00:00:00 2001 From: syntron Date: Thu, 24 Apr 2025 20:14:24 +0200 Subject: [PATCH 26/56] [ModelicaSystem] remove _check_error() --- OMPython/ModelicaSystem.py | 31 ++++++++----------------------- 1 file changed, 8 insertions(+), 23 deletions(-) diff --git a/OMPython/ModelicaSystem.py b/OMPython/ModelicaSystem.py index 25059308..b8d54a2b 100644 --- a/OMPython/ModelicaSystem.py +++ b/OMPython/ModelicaSystem.py @@ -230,15 +230,11 @@ def setCommandLineOptions(self, commandLineOptions: str): if commandLineOptions is None: return exp = f'setCommandLineOptions("{commandLineOptions}")' - if not self.sendExpression(exp): - self._check_error() + self.sendExpression(exp) def loadFile(self, fileName: pathlib.Path): # load file - loadMsg = self.sendExpression(f'loadFile("{fileName.as_posix()}")') - # Show notification or warnings to the user when verbose=True OR if some error occurred i.e., not result - if self._verbose or not loadMsg: - self._check_error() + self.sendExpression(f'loadFile("{fileName.as_posix()}")') # for loading file/package, loading model and building model def loadLibrary(self, lmodel: list): @@ -250,22 +246,19 @@ def loadLibrary(self, lmodel: list): apiCall = "loadFile" else: apiCall = "loadModel" - result = self.requestApi(apiCall, element) + self.requestApi(apiCall, element) elif isinstance(element, tuple): if not element[1]: - libname = f"loadModel({element[0]})" + expr_load_lib = f"loadModel({element[0]})" else: - libname = f'loadModel({element[0]}, {{"{element[1]}"}})' - result = self.sendExpression(libname) + expr_load_lib = f'loadModel({element[0]}, {{"{element[1]}"}})' + self.sendExpression(expr_load_lib) else: raise ModelicaSystemError("loadLibrary() failed, Unknown type detected: " f"{element} is of type {type(element)}, " "The following patterns are supported:\n" '1)["Modelica"]\n' '2)[("Modelica","3.2.3"), "PowerSystems"]\n') - # Show notification or warnings to the user when verbose=True OR if some error occurred i.e., not result - if self._verbose or not result: - self._check_error() def setTempDirectory(self, customBuildDirectory): # create a unique temp directory for each session and build the model in that directory @@ -323,12 +316,6 @@ def _run_cmd(self, cmd: list, timeout: Optional[int] = None): except Exception as e: raise ModelicaSystemError(f"Exception {type(e)} running command {cmd}: {e}") - def _check_error(self): - errstr = self.sendExpression("getErrorString()") - if not errstr: - return - self._raise_error(errstr=errstr) - def _raise_error(self, errstr: str): if self._raiseerrors: raise ModelicaSystemError(f"OM error: {errstr}") @@ -347,7 +334,6 @@ def buildModel(self, variableFilter=None): buildModelResult = self.requestApi("buildModel", self.modelName, properties=varFilter) if self._verbose: logger.info("OM model build result: %s", buildModelResult) - self._check_error() self.xmlFile = pathlib.Path(buildModelResult[0]).parent / buildModelResult[1] self.xmlparse() @@ -1017,7 +1003,7 @@ def convertMo2Fmu(self, version="2.0", fmuType="me_cs", fileNamePrefix=" Date: Sat, 26 Apr 2025 22:12:25 +0200 Subject: [PATCH 27/56] [ModelicaSystem] static strip_space() --- OMPython/ModelicaSystem.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/OMPython/ModelicaSystem.py b/OMPython/ModelicaSystem.py index b8d54a2b..408f93ff 100644 --- a/OMPython/ModelicaSystem.py +++ b/OMPython/ModelicaSystem.py @@ -770,7 +770,8 @@ def getSolutions(self, varList=None, resultfile=None): # 12 raise ModelicaSystemError("Unhandled input for getSolutions()") - def strip_space(self, name): + @staticmethod + def _strip_space(name): if isinstance(name, str): return name.replace(" ", "") elif isinstance(name, list): @@ -787,7 +788,7 @@ def setMethodHelper(self, args1, args2, args3, args4=None): args4 - dict() which stores the new override variables list, """ def apply_single(args1): - args1 = self.strip_space(args1) + args1 = self._strip_space(args1) value = args1.split("=") if value[0] in args2: if args3 == "parameter" and self.isParameterChangeable(value[0], value[1]): @@ -811,7 +812,7 @@ def apply_single(args1): elif isinstance(args1, list): result = [] - args1 = self.strip_space(args1) + args1 = self._strip_space(args1) for var in args1: result.append(apply_single(var)) @@ -888,7 +889,7 @@ def setInputs(self, name): # 15 >>> setInputs(["Name1=value1","Name2=value2"]) """ if isinstance(name, str): - name = self.strip_space(name) + name = self._strip_space(name) value = name.split("=") if value[0] in self.inputlist: tmpvalue = eval(value[1]) @@ -903,7 +904,7 @@ def setInputs(self, name): # 15 errstr = value[0] + " is not an input" self._raise_error(errstr=errstr) elif isinstance(name, list): - name = self.strip_space(name) + name = self._strip_space(name) for var in name: value = var.split("=") if value[0] in self.inputlist: From 27a149979ec4108480e643ec6369167e77d9b31a Mon Sep 17 00:00:00 2001 From: syntron Date: Sat, 26 Apr 2025 22:42:40 +0200 Subject: [PATCH 28/56] [ModelicaSystem] exception handling --- OMPython/ModelicaSystem.py | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/OMPython/ModelicaSystem.py b/OMPython/ModelicaSystem.py index 408f93ff..0a7bacba 100644 --- a/OMPython/ModelicaSystem.py +++ b/OMPython/ModelicaSystem.py @@ -158,7 +158,7 @@ def __init__( mod = ModelicaSystem("ModelicaModel.mo", "modelName", [("Modelica","3.2.3"), "PowerSystems"]) """ if fileName is None and modelName is None and not lmodel: # all None - raise Exception("Cannot create ModelicaSystem object without any arguments") + raise ModelicaSystemError("Cannot create ModelicaSystem object without any arguments") self.quantitiesList = [] self.paramlist = {} @@ -205,7 +205,7 @@ def __init__( self._raiseerrors = raiseerrors if self.fileName is not None and not self.fileName.is_file(): # if file does not exist - raise IOError(f"File Error: {self.fileName} does not exist!!!") + raise IOError(f"{self.fileName} does not exist!") # set default command Line Options for linearization as # linearize() will use the simulation executable and runtime @@ -313,8 +313,8 @@ def _run_cmd(self, cmd: list, timeout: Optional[int] = None): logger.info("OM output for command %s:\n%s", cmd, stdout) except subprocess.TimeoutExpired: raise ModelicaSystemError(f"Timeout running command {repr(cmd)}") - except Exception as e: - raise ModelicaSystemError(f"Exception {type(e)} running command {cmd}: {e}") + except Exception as ex: + raise ModelicaSystemError(f"Error running command {cmd}") from ex def _raise_error(self, errstr: str): if self._raiseerrors: @@ -437,8 +437,8 @@ def getContinuous(self, names=None): # 4 try: value = self.getSolutions(i) self.continuouslist[i] = value[0][-1] - except Exception: - raise ModelicaSystemError(f"OM error: {i} could not be computed") + except OMCSessionException as ex: + raise ModelicaSystemError(f"{i} could not be computed") from ex return self.continuouslist elif isinstance(names, str): @@ -447,7 +447,7 @@ def getContinuous(self, names=None): # 4 self.continuouslist[names] = value[0][-1] return [self.continuouslist.get(names)] else: - raise ModelicaSystemError(f"OM error: {names} is not continuous") + raise ModelicaSystemError(f"{names} is not continuous") elif isinstance(names, list): valuelist = [] @@ -457,7 +457,7 @@ def getContinuous(self, names=None): # 4 self.continuouslist[i] = value[0][-1] valuelist.append(value[0][-1]) else: - raise ModelicaSystemError(f"OM error: {i} is not continuous") + raise ModelicaSystemError(f"{i} is not continuous") return valuelist raise ModelicaSystemError("Unhandled input for getContinous()") @@ -717,7 +717,7 @@ def simulate(self, resultfile=None, simflags=None, timeout: Optional[int] = None exe_file = self.get_exe_file() if not exe_file.exists(): - raise Exception(f"Error: Application file path not found: {exe_file}") + raise ModelicaSystemError(f"Application file path not found: {exe_file}") cmd = exe_file.as_posix() + override + csvinput + resultfileflag + simflags cmd = [s for s in cmd.split(' ') if s] @@ -1099,7 +1099,7 @@ def linearize(self, lintime: Optional[float] = None, simflags: Optional[str] = N simflags = " " + simflags if not exe_file.exists(): - raise Exception(f"Error: Application file path not found: {exe_file}") + 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] @@ -1129,8 +1129,8 @@ def linearize(self, lintime: Optional[float] = None, simflags: Optional[str] = N self.linearstates = stateVars return LinearizationResult(n, m, p, A, B, C, D, x0, u0, stateVars, inputVars, outputVars) - except ModuleNotFoundError: - raise Exception("ModuleNotFoundError: No module named 'linearized_model'") + except ModuleNotFoundError as ex: + raise ModelicaSystemError("No module named 'linearized_model'") from ex def getLinearInputs(self): """ From 2bc107ac40e7d74054bf4b4182e1bf05b8635d32 Mon Sep 17 00:00:00 2001 From: syntron Date: Mon, 28 Apr 2025 15:53:17 +0200 Subject: [PATCH 29/56] [ModelicaSystem] remove _raise_error() --- OMPython/ModelicaSystem.py | 36 +++++++----------------------------- 1 file changed, 7 insertions(+), 29 deletions(-) diff --git a/OMPython/ModelicaSystem.py b/OMPython/ModelicaSystem.py index 0a7bacba..5b2b883e 100644 --- a/OMPython/ModelicaSystem.py +++ b/OMPython/ModelicaSystem.py @@ -117,7 +117,6 @@ def __init__( variableFilter: Optional[str] = None, customBuildDirectory: Optional[str | os.PathLike] = None, verbose: bool = True, - raiseerrors: bool = False, omhome: Optional[str] = None, session: Optional[OMCSessionBase] = None ): @@ -145,8 +144,6 @@ def __init__( files like the model executable. If left unspecified, a tmp directory will be created. verbose: If True, enable verbose logging. - raiseerrors: If True, raise exceptions instead of just logging - OpenModelica errors. omhome: OPENMODELICAHOME value to be used when creating the OMC session. session: OMC session to be used. If unspecified, a new session @@ -202,8 +199,6 @@ def __init__( self.resultfile = "" # for storing result file self.variableFilter = variableFilter - self._raiseerrors = raiseerrors - if self.fileName is not None and not self.fileName.is_file(): # if file does not exist raise IOError(f"{self.fileName} does not exist!") @@ -316,12 +311,6 @@ def _run_cmd(self, cmd: list, timeout: Optional[int] = None): except Exception as ex: raise ModelicaSystemError(f"Error running command {cmd}") from ex - def _raise_error(self, errstr: str): - if self._raiseerrors: - raise ModelicaSystemError(f"OM error: {errstr}") - else: - logger.error(errstr) - def buildModel(self, variableFilter=None): if variableFilter is not None: self.variableFilter = variableFilter @@ -353,17 +342,12 @@ def requestApi(self, apiName, entity=None, properties=None): # 2 exp = f'{apiName}({entity})' else: exp = f'{apiName}()' - try: - res = self.sendExpression(exp) - except Exception as e: - self._raise_error(errstr=f"Exception {type(e)} raised: {e}") - res = None - return res + + return self.sendExpression(exp) def xmlparse(self): if not self.xmlFile.exists(): - self._raise_error(errstr=f"XML file not generated: {self.xmlFile}") - return + ModelicaSystemError(f"XML file not generated: {self.xmlFile}") tree = ET.parse(self.xmlFile) rootCQ = tree.getroot() @@ -703,13 +687,9 @@ def simulate(self, resultfile=None, simflags=None, timeout: Optional[int] = None self.inputlist[i] = [(float(self.simulateOptions["startTime"]), 0.0), (float(self.simulateOptions["stopTime"]), 0.0)] if float(self.simulateOptions["startTime"]) != val[0][0]: - errstr = f"!!! startTime not matched for Input {i}" - self._raise_error(errstr=errstr) - return + raise ModelicaSystemError(f"startTime not matched for Input {i}!") if float(self.simulateOptions["stopTime"]) != val[-1][0]: - errstr = f"!!! stopTime not matched for Input {i}" - self._raise_error(errstr=errstr) - return + raise ModelicaSystemError(f"stopTime not matched for Input {i}!") self.csvFile = self.createCSVData() # create csv file csvinput = " -csvInput=" + self.csvFile.as_posix() else: @@ -901,8 +881,7 @@ def setInputs(self, name): # 15 self.inputlist[value[0]] = tmpvalue self.inputFlag = True else: - errstr = value[0] + " is not an input" - self._raise_error(errstr=errstr) + raise ModelicaSystemError(f"{value[0]} is not an input") elif isinstance(name, list): name = self._strip_space(name) for var in name: @@ -917,8 +896,7 @@ def setInputs(self, name): # 15 self.inputlist[value[0]] = tmpvalue self.inputFlag = True else: - errstr = value[0] + " is not an input" - self._raise_error(errstr=errstr) + raise ModelicaSystemError(f"{value[0]} is not an input!") def checkValidInputs(self, name): if name != sorted(name, key=lambda x: x[0]): From 6864a4663e593ea903709eace7586473b6a52c3f Mon Sep 17 00:00:00 2001 From: syntron Date: Mon, 28 Apr 2025 08:51:51 +0200 Subject: [PATCH 30/56] [tests] remove raiseerror=True --- tests/test_ModelicaSystem.py | 15 +++++++-------- tests/test_linearization.py | 2 +- tests/test_optimization.py | 3 +-- 3 files changed, 9 insertions(+), 11 deletions(-) diff --git a/tests/test_ModelicaSystem.py b/tests/test_ModelicaSystem.py index 5704c950..9a0ce1c9 100644 --- a/tests/test_ModelicaSystem.py +++ b/tests/test_ModelicaSystem.py @@ -35,7 +35,7 @@ def worker(): def test_setParameters(self): omc = OMPython.OMCSessionZMQ() model_path = omc.sendExpression("getInstallationDirectoryPath()") + "/share/doc/omc/testmodels/" - mod = OMPython.ModelicaSystem(model_path + "BouncingBall.mo", "BouncingBall", raiseerrors=True) + mod = OMPython.ModelicaSystem(model_path + "BouncingBall.mo", "BouncingBall") # method 1 mod.setParameters("e=1.234") @@ -59,7 +59,7 @@ def test_setParameters(self): def test_setSimulationOptions(self): omc = OMPython.OMCSessionZMQ() model_path = omc.sendExpression("getInstallationDirectoryPath()") + "/share/doc/omc/testmodels/" - mod = OMPython.ModelicaSystem(model_path + "BouncingBall.mo", "BouncingBall", raiseerrors=True) + mod = OMPython.ModelicaSystem(model_path + "BouncingBall.mo", "BouncingBall") # method 1 mod.setSimulationOptions("stopTime=1.234") @@ -89,7 +89,7 @@ def test_relative_path(self): model_relative = str(model_file) assert "/" not in model_relative - mod = OMPython.ModelicaSystem(model_relative, "M", raiseerrors=True) + mod = OMPython.ModelicaSystem(model_relative, "M") assert float(mod.getParameters("a")[0]) == -1 finally: # clean up the temporary file @@ -99,8 +99,7 @@ def test_customBuildDirectory(self): filePath = (self.tmp / "M.mo").as_posix() tmpdir = self.tmp / "tmpdir1" tmpdir.mkdir() - m = OMPython.ModelicaSystem(filePath, "M", raiseerrors=True, - customBuildDirectory=tmpdir) + m = OMPython.ModelicaSystem(filePath, "M", customBuildDirectory=tmpdir) assert pathlib.Path(m.getWorkDirectory()).resolve() == tmpdir.resolve() result_file = tmpdir / "a.mat" assert not result_file.exists() @@ -109,7 +108,7 @@ def test_customBuildDirectory(self): def test_getSolutions(self): filePath = (self.tmp / "M.mo").as_posix() - mod = OMPython.ModelicaSystem(filePath, "M", raiseerrors=True) + mod = OMPython.ModelicaSystem(filePath, "M") x0 = 1 a = -1 tau = -1 / a @@ -145,7 +144,7 @@ def test_getters(self): y = der(x); end M_getters; """) - mod = OMPython.ModelicaSystem(model_file.as_posix(), "M_getters", raiseerrors=True) + mod = OMPython.ModelicaSystem(model_file.as_posix(), "M_getters") q = mod.getQuantities() assert isinstance(q, list) @@ -323,7 +322,7 @@ def test_simulate_inputs(self): y = x; end M_input; """) - mod = OMPython.ModelicaSystem(model_file.as_posix(), "M_input", raiseerrors=True) + mod = OMPython.ModelicaSystem(model_file.as_posix(), "M_input") mod.setSimulationOptions("stopTime=1.0") diff --git a/tests/test_linearization.py b/tests/test_linearization.py index 2cc49fed..bf759fde 100644 --- a/tests/test_linearization.py +++ b/tests/test_linearization.py @@ -61,7 +61,7 @@ def test_getters(self): y2 = phi + u1; end Pendulum; """) - mod = OMPython.ModelicaSystem(model_file.as_posix(), "Pendulum", ["Modelica"], raiseerrors=True) + mod = OMPython.ModelicaSystem(model_file.as_posix(), "Pendulum", ["Modelica"]) d = mod.getLinearizationOptions() assert isinstance(d, dict) diff --git a/tests/test_optimization.py b/tests/test_optimization.py index ae93d0b8..283df062 100644 --- a/tests/test_optimization.py +++ b/tests/test_optimization.py @@ -45,8 +45,7 @@ def test_example(self): end BangBang2021; """) - mod = OMPython.ModelicaSystem(model_file.as_posix(), "BangBang2021", - raiseerrors=True) + mod = OMPython.ModelicaSystem(model_file.as_posix(), "BangBang2021") mod.setOptimizationOptions(["numberOfIntervals=16", "stopTime=1", "stepSize=0.001", "tolerance=1e-8"]) From a4a62e534712246c1ed7d8018bf871165b23240c Mon Sep 17 00:00:00 2001 From: syntron Date: Sat, 26 Apr 2025 22:54:50 +0200 Subject: [PATCH 31/56] [ModelicaSystem] remove verbose - use logger.debug --- OMPython/ModelicaSystem.py | 22 ++++++++-------------- 1 file changed, 8 insertions(+), 14 deletions(-) diff --git a/OMPython/ModelicaSystem.py b/OMPython/ModelicaSystem.py index 5b2b883e..f6f14a6c 100644 --- a/OMPython/ModelicaSystem.py +++ b/OMPython/ModelicaSystem.py @@ -116,7 +116,6 @@ def __init__( commandLineOptions: Optional[str] = None, variableFilter: Optional[str] = None, customBuildDirectory: Optional[str | os.PathLike] = None, - verbose: bool = True, omhome: Optional[str] = None, session: Optional[OMCSessionBase] = None ): @@ -143,7 +142,6 @@ def __init__( customBuildDirectory: Path to a directory to be used for temporary files like the model executable. If left unspecified, a tmp directory will be created. - verbose: If True, enable verbose logging. omhome: OPENMODELICAHOME value to be used when creating the OMC session. session: OMC session to be used. If unspecified, a new session @@ -173,8 +171,6 @@ def __init__( self.linearstates = [] # linearization states list self.tempdir = "" - self._verbose = verbose - if session is not None: if not isinstance(session, OMCSessionZMQ): raise ModelicaSystemError("Invalid session data provided!") @@ -300,12 +296,13 @@ def _run_cmd(self, cmd: list, timeout: Optional[int] = None): 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}: nonzero return code") if stderr: raise ModelicaSystemError(f"Error running command {cmd}: {stderr}") - if self._verbose and stdout: - logger.info("OM output for command %s:\n%s", cmd, stdout) except subprocess.TimeoutExpired: raise ModelicaSystemError(f"Timeout running command {repr(cmd)}") except Exception as ex: @@ -321,8 +318,7 @@ def buildModel(self, variableFilter=None): varFilter = 'variableFilter=".*"' logger.debug("varFilter=%s", varFilter) buildModelResult = self.requestApi("buildModel", self.modelName, properties=varFilter) - if self._verbose: - logger.info("OM model build result: %s", buildModelResult) + logger.debug("OM model build result: %s", buildModelResult) self.xmlFile = pathlib.Path(buildModelResult[0]).parent / buildModelResult[1] self.xmlparse() @@ -821,12 +817,10 @@ def setParameters(self, pvals): # 14 def isParameterChangeable(self, name, value): q = self.getQuantities(name) if q[0]["changeable"] == "false": - if self._verbose: - logger.info("setParameters() failed : It is not possible to set " - f'the following signal "{name}", It seems to be structural, final, ' - "protected or evaluated or has a non-constant binding, use sendExpression(" - f"setParameterValue({self.modelName}, {name}, {value}), " - "parsed=false) and rebuild the model using buildModel() API") + logger.verbose(f"setParameters() failed : It is not possible to set the following signal {repr(name)}. " + "It seems to be structural, final, protected or evaluated or has a non-constant binding, " + f"use sendExpression(\"setParameterValue({self.modelName}, {name}, {value})\", " + "parsed=False) and rebuild the model using buildModel() API") return False return True From 9363b775c84fc8cf960ddcd8415966cb2ea80afc Mon Sep 17 00:00:00 2001 From: syntron Date: Mon, 28 Apr 2025 08:52:58 +0200 Subject: [PATCH 32/56] [ModelicaSystem] check that lmodel is defined as list --- OMPython/ModelicaSystem.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/OMPython/ModelicaSystem.py b/OMPython/ModelicaSystem.py index f6f14a6c..0b6917bd 100644 --- a/OMPython/ModelicaSystem.py +++ b/OMPython/ModelicaSystem.py @@ -184,6 +184,9 @@ def __init__( if lmodel is None: lmodel = [] + if not isinstance(lmodel, list): + raise ModelicaSystemError(f"Invalid input type for lmodel: {type(lmodel)} - list expected!") + self.xmlFile = None self.lmodel = lmodel # may be needed if model is derived from other model self.modelName = modelName # Model class name From 07ed38b92449c866ba810755e364e63abd8a30a0 Mon Sep 17 00:00:00 2001 From: syntron Date: Mon, 28 Apr 2025 14:31:04 +0200 Subject: [PATCH 33/56] [OMCSessionZMQ] use pathlib.Path() --- OMPython/OMCSession.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/OMPython/OMCSession.py b/OMPython/OMCSession.py index 2f8518ff..72a0fa16 100644 --- a/OMPython/OMCSession.py +++ b/OMPython/OMCSession.py @@ -320,7 +320,7 @@ def __init__(self, readonly=False, timeout=10.00, self._serverIPAddress = "127.0.0.1" self._interactivePort = None # FIXME: this code is not well written... need to be refactored - self._temp_dir = tempfile.gettempdir() + self._temp_dir = pathlib.Path(tempfile.gettempdir()) # generate a random string for this session self._random_string = uuid.uuid4().hex # omc log file @@ -345,7 +345,7 @@ def __init__(self, readonly=False, timeout=10.00, self._dockerNetwork = dockerNetwork self._create_omc_log_file("port") self._timeout = timeout - self._port_file = os.path.join("/tmp" if docker else self._temp_dir, self._port_file).replace("\\", "/") + self._port_file = ((pathlib.Path("/tmp") if docker else self._temp_dir) / self._port_file).as_posix() self._interactivePort = port # set omc executable path and args self._set_omc_command([ @@ -379,7 +379,7 @@ def _create_omc_log_file(self, suffix): else: log_filename = f"openmodelica.{self._currentUser}.{suffix}.{self._random_string}.log" # this file must be closed in the destructor - self._omc_log_file = open(pathlib.Path(self._temp_dir) / log_filename, "w+") + self._omc_log_file = open(self._temp_dir / log_filename, "w+") def _start_omc_process(self, timeout): if sys.platform == 'win32': From f07fa7f1aed503625932fc74d68c678e31f5d415 Mon Sep 17 00:00:00 2001 From: syntron Date: Fri, 25 Apr 2025 22:34:57 +0200 Subject: [PATCH 34/56] [OMCSessionBase.ask()] define opt as list --- OMPython/OMCSession.py | 117 ++++++++++++++++++++--------------------- 1 file changed, 58 insertions(+), 59 deletions(-) diff --git a/OMPython/OMCSession.py b/OMPython/OMCSession.py index 72a0fa16..5a4d7fdd 100644 --- a/OMPython/OMCSession.py +++ b/OMPython/OMCSession.py @@ -45,6 +45,7 @@ import sys import tempfile import time +from typing import Optional import uuid import pyparsing import zmq @@ -106,43 +107,45 @@ def sendExpression(self, command, parsed=True): """ pass - def ask(self, question, opt=None, parsed=True): - p = (question, opt, parsed) + def _ask(self, question: str, opt: Optional[list[str]] = None, parsed: Optional[bool] = True): + + if opt is None: + expression = question + elif isinstance(opt, list): + expression = f"{question}({','.join(opt)})" + else: + raise OMCSessionException(f"Invalid definition of options for {repr(question)}: {repr(opt)}") + + p = (expression, parsed) if self._readonly and question != 'getErrorString': # can use cache if readonly if p in self._omc_cache: return self._omc_cache[p] - if opt: - expression = f'{question}({opt})' - else: - expression = question - - logger.debug('OMC ask: %s - parsed: %s', expression, parsed) + logger.debug('OMC ask: %s (parsed=%s)', expression, parsed) try: res = self.sendExpression(expression, parsed=parsed) - except OMCSessionException: - logger.error("OMC failed: %s, %s, parsed=%s", question, opt, parsed) - raise + except OMCSessionException as ex: + raise OMCSessionException("OMC ask failed: %s (parsed=%s)", expression, parsed) from ex # save response self._omc_cache[p] = res return res - def _ask_with_fallback(self, question: str, opt: str = None): + def _ask_with_fallback(self, question: str, opt: Optional[list[str]] = None): """ Version of ask() with OMTypedParser as fallback for OMParser which is used in ask() => sendExpression() if parsed is set to True. """ try: # FIXME: OMPython exception UnboundLocalError exception for 'Modelica.Fluid.Machines.ControlledPump' - return self.ask(question=question, opt=opt) + return self._ask(question=question, opt=opt) except pyparsing.ParseException as ex: logger.warning('OMTypedParser error: %s', ex.msg) - result = self.ask(question=question, opt=opt, parsed=False) + result = self._ask(question=question, opt=opt, parsed=False) try: answer = om_parser_basic(result) return answer[2:] @@ -152,68 +155,68 @@ def _ask_with_fallback(self, question: str, opt: str = None): # TODO: Open Modelica Compiler API functions. Would be nice to generate these. def loadFile(self, filename): - return self.ask('loadFile', f'"{filename}"') + return self._ask(question='loadFile', opt=[f'"{filename}"']) def loadModel(self, className): - return self.ask('loadModel', className) + return self._ask(question='loadModel', opt=[className]) def isModel(self, className): - return self.ask('isModel', className) + return self._ask(question='isModel', opt=[className]) def isPackage(self, className): - return self.ask('isPackage', className) + return self._ask(question='isPackage', opt=[className]) def isPrimitive(self, className): - return self.ask('isPrimitive', className) + return self._ask(question='isPrimitive', opt=[className]) def isConnector(self, className): - return self.ask('isConnector', className) + return self._ask(question='isConnector', opt=[className]) def isRecord(self, className): - return self.ask('isRecord', className) + return self._ask(question='isRecord', opt=[className]) def isBlock(self, className): - return self.ask('isBlock', className) + return self._ask(question='isBlock', opt=[className]) def isType(self, className): - return self.ask('isType', className) + return self._ask(question='isType', opt=[className]) def isFunction(self, className): - return self.ask('isFunction', className) + return self._ask(question='isFunction', opt=[className]) def isClass(self, className): - return self.ask('isClass', className) + return self._ask(question='isClass', opt=[className]) def isParameter(self, className): - return self.ask('isParameter', className) + return self._ask(question='isParameter', opt=[className]) def isConstant(self, className): - return self.ask('isConstant', className) + return self._ask(question='isConstant', opt=[className]) def isProtected(self, className): - return self.ask('isProtected', className) + return self._ask(question='isProtected', opt=[className]) def getPackages(self, className="AllLoadedClasses"): - return self.ask('getPackages', className) + return self._ask(question='getPackages', opt=[className]) def getClassRestriction(self, className): - return self.ask('getClassRestriction', className) + return self._ask(question='getClassRestriction', opt=[className]) def getDerivedClassModifierNames(self, className): - return self.ask('getDerivedClassModifierNames', className) + return self._ask(question='getDerivedClassModifierNames', opt=[className]) def getDerivedClassModifierValue(self, className, modifierName): - return self.ask('getDerivedClassModifierValue', f'{className}, {modifierName}') + return self._ask(question='getDerivedClassModifierValue', opt=[className, modifierName]) def typeNameStrings(self, className): - return self.ask('typeNameStrings', className) + return self._ask(question='typeNameStrings', opt=[className]) def getComponents(self, className): - return self.ask('getComponents', className) + return self._ask(question='getComponents', opt=[className]) def getClassComment(self, className): try: - return self.ask('getClassComment', className) + return self._ask(question='getClassComment', opt=[className]) except pyparsing.ParseException as ex: logger.warning("Method 'getClassComment' failed for %s", className) logger.warning('OMTypedParser error: %s', ex.msg) @@ -221,27 +224,27 @@ def getClassComment(self, className): def getNthComponent(self, className, comp_id): """ returns with (type, name, description) """ - return self.ask('getNthComponent', f'{className}, {comp_id}') + return self._ask(question='getNthComponent', opt=[className, comp_id]) def getNthComponentAnnotation(self, className, comp_id): - return self.ask('getNthComponentAnnotation', f'{className}, {comp_id}') + return self._ask(question='getNthComponentAnnotation', opt=[className, comp_id]) def getImportCount(self, className): - return self.ask('getImportCount', className) + return self._ask(question='getImportCount', opt=[className]) def getNthImport(self, className, importNumber): # [Path, id, kind] - return self.ask('getNthImport', f'{className}, {importNumber}') + return self._ask(question='getNthImport', opt=[className, importNumber]) def getInheritanceCount(self, className): - return self.ask('getInheritanceCount', className) + return self._ask(question='getInheritanceCount', opt=[className]) def getNthInheritedClass(self, className, inheritanceDepth): - return self.ask('getNthInheritedClass', f'{className}, {inheritanceDepth}') + return self._ask(question='getNthInheritedClass', opt=[className, inheritanceDepth]) def getParameterNames(self, className): try: - return self.ask('getParameterNames', className) + return self._ask(question='getParameterNames', opt=[className]) except KeyError as ex: logger.warning('OMPython error: %s', ex) # FIXME: OMC returns with a different structure for empty parameter set @@ -249,31 +252,29 @@ def getParameterNames(self, className): def getParameterValue(self, className, parameterName): try: - return self.ask('getParameterValue', f'{className}, {parameterName}') + return self._ask(question='getParameterValue', opt=[className, parameterName]) except pyparsing.ParseException as ex: logger.warning('OMTypedParser error: %s', ex.msg) return "" def getComponentModifierNames(self, className, componentName): - return self.ask('getComponentModifierNames', f'{className}, {componentName}') + return self._ask(question='getComponentModifierNames', opt=[className, componentName]) def getComponentModifierValue(self, className, componentName): - return self._ask_with_fallback(question='getComponentModifierValue', - opt=f'{className}, {componentName}') + return self._ask_with_fallback(question='getComponentModifierValue', opt=[className, componentName]) def getExtendsModifierNames(self, className, componentName): - return self.ask('getExtendsModifierNames', f'{className}, {componentName}') + return self._ask(question='getExtendsModifierNames', opt=[className, componentName]) def getExtendsModifierValue(self, className, extendsName, modifierName): - return self._ask_with_fallback(question='getExtendsModifierValue', - opt=f'{className}, {extendsName}, {modifierName}') + return self._ask_with_fallback(question='getExtendsModifierValue', opt=[className, extendsName, modifierName]) def getNthComponentModification(self, className, comp_id): # FIXME: OMPython exception Results KeyError exception # get {$Code(....)} field # \{\$Code\((\S*\s*)*\)\} - value = self.ask('getNthComponentModification', f'{className}, {comp_id}', parsed=False) + value = self._ask(question='getNthComponentModification', opt=[className, comp_id], parsed=False) value = value.replace("{$Code(", "") return value[:-3] # return self.re_Code.findall(value) @@ -289,15 +290,13 @@ def getNthComponentModification(self, className, comp_id): # end getClassNames; def getClassNames(self, className=None, recursive=False, qualified=False, sort=False, builtin=False, showProtected=False): - value = self.ask( - 'getClassNames', - (f'{className}, ' if className else '') + - f'recursive={str(recursive).lower()}, ' - f'qualified={str(qualified).lower()}, ' - f'sort={str(sort).lower()}, ' - f'builtin={str(builtin).lower()}, ' - f'showProtected={str(showProtected).lower()}' - ) + value = self._ask(question='getClassNames', + opt=[className] if className else [] + [f'recursive={str(recursive).lower()}', + f'qualified={str(qualified).lower()}', + f'sort={str(sort).lower()}', + f'builtin={str(builtin).lower()}', + f'showProtected={str(showProtected).lower()}'] + ) return value From 2b51501dcdf99a6b43cd5810b10449876cb337b1 Mon Sep 17 00:00:00 2001 From: syntron Date: Sat, 26 Apr 2025 22:38:49 +0200 Subject: [PATCH 35/56] rename [OMCSessionBase] => [OMCSessionCmd]; remove dependencies --- OMPython/ModelicaSystem.py | 4 ++-- OMPython/OMCSession.py | 31 +++++++++---------------------- OMPython/__init__.py | 4 ++-- 3 files changed, 13 insertions(+), 26 deletions(-) diff --git a/OMPython/ModelicaSystem.py b/OMPython/ModelicaSystem.py index 9f3402c1..c8061c1e 100644 --- a/OMPython/ModelicaSystem.py +++ b/OMPython/ModelicaSystem.py @@ -46,7 +46,7 @@ from dataclasses import dataclass from typing import Optional -from OMPython.OMCSession import OMCSessionBase, OMCSessionZMQ +from OMPython.OMCSession import OMCSessionZMQ # define logger using the current module name as ID logger = logging.getLogger(__name__) @@ -119,7 +119,7 @@ def __init__( verbose: bool = True, raiseerrors: bool = False, omhome: Optional[str] = None, - session: Optional[OMCSessionBase] = None + session: Optional[OMCSessionZMQ] = None ): """Initialize, load and build a model. diff --git a/OMPython/OMCSession.py b/OMPython/OMCSession.py index 5a4d7fdd..ef69a564 100644 --- a/OMPython/OMCSession.py +++ b/OMPython/OMCSession.py @@ -3,6 +3,8 @@ Definition of an OMC session. """ +from __future__ import annotations + __license__ = """ This file is part of OpenModelica. @@ -33,7 +35,6 @@ """ import shutil -import abc import getpass import logging import json @@ -80,9 +81,12 @@ class OMCSessionException(Exception): pass -class OMCSessionBase(metaclass=abc.ABCMeta): +class OMCSessionCmd: - def __init__(self, readonly=False): + def __init__(self, session: OMCSessionZMQ, readonly: Optional[bool] = False): + if not isinstance(session, OMCSessionZMQ): + raise OMCSessionException("Invalid session definition!") + self._session = session self._readonly = readonly self._omc_cache = {} @@ -92,21 +96,6 @@ def execute(self, command): return self.sendExpression(command, parsed=False) - @abc.abstractmethod - def sendExpression(self, command, parsed=True): - """ - Sends an expression to the OpenModelica. The return type is parsed as if the - expression was part of the typed OpenModelica API (see ModelicaBuiltin.mo). - * Integer and Real are returned as Python numbers - * Strings, enumerations, and typenames are returned as Python strings - * Arrays, tuples, and MetaModelica lists are returned as tuples - * Records are returned as dicts (the name of the record is lost) - * Booleans are returned as True or False - * NONE() is returned as None - * SOME(value) is returned as value - """ - pass - def _ask(self, question: str, opt: Optional[list[str]] = None, parsed: Optional[bool] = True): if opt is None: @@ -126,7 +115,7 @@ def _ask(self, question: str, opt: Optional[list[str]] = None, parsed: Optional[ logger.debug('OMC ask: %s (parsed=%s)', expression, parsed) try: - res = self.sendExpression(expression, parsed=parsed) + res = self._session.sendExpression(expression, parsed=parsed) except OMCSessionException as ex: raise OMCSessionException("OMC ask failed: %s (parsed=%s)", expression, parsed) from ex @@ -300,7 +289,7 @@ def getClassNames(self, className=None, recursive=False, qualified=False, sort=F return value -class OMCSessionZMQ(OMCSessionBase): +class OMCSessionZMQ: def __init__(self, readonly=False, timeout=10.00, docker=None, dockerContainer=None, dockerExtraArgs=None, dockerOpenModelicaPath="omc", @@ -308,8 +297,6 @@ def __init__(self, readonly=False, timeout=10.00, if dockerExtraArgs is None: dockerExtraArgs = [] - super().__init__(readonly=readonly) - self.omhome = self._get_omhome(omhome=omhome) self._omc_process = None diff --git a/OMPython/__init__.py b/OMPython/__init__.py index eee36acc..0d4ab686 100644 --- a/OMPython/__init__.py +++ b/OMPython/__init__.py @@ -36,7 +36,7 @@ CONDITIONS OF OSMC-PL. """ -from OMPython.OMCSession import OMCSessionBase, OMCSessionZMQ, OMCSessionException +from OMPython.OMCSession import OMCSessionCmd, OMCSessionZMQ, OMCSessionException from OMPython.ModelicaSystem import ModelicaSystem, ModelicaSystemError, LinearizationResult # global names imported if import 'from OMPython import *' is used @@ -47,5 +47,5 @@ 'OMCSessionException', 'OMCSessionZMQ', - 'OMCSessionBase', + 'OMCSessionCmd', ] From 822013b49b5958611f76ed59e017cc9ddb0b3cc0 Mon Sep 17 00:00:00 2001 From: syntron Date: Mon, 28 Apr 2025 13:43:40 +0200 Subject: [PATCH 36/56] [OMCSessionZMQ] remove unused argument readonly --- OMPython/OMCSession.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/OMPython/OMCSession.py b/OMPython/OMCSession.py index ef69a564..73ebd845 100644 --- a/OMPython/OMCSession.py +++ b/OMPython/OMCSession.py @@ -291,7 +291,7 @@ def getClassNames(self, className=None, recursive=False, qualified=False, sort=F class OMCSessionZMQ: - def __init__(self, readonly=False, timeout=10.00, + def __init__(self, timeout=10.00, docker=None, dockerContainer=None, dockerExtraArgs=None, dockerOpenModelicaPath="omc", dockerNetwork=None, port=None, omhome: str = None): if dockerExtraArgs is None: From 8b205451f63503317c7ad9314a5d4fa605d94e27 Mon Sep 17 00:00:00 2001 From: syntron Date: Mon, 28 Apr 2025 09:01:46 +0200 Subject: [PATCH 37/56] [OMCSessionCmd] restore test_ZMQ - move execute() back into OMCSessionZMQ --- OMPython/OMCSession.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/OMPython/OMCSession.py b/OMPython/OMCSession.py index 73ebd845..1cc6f7aa 100644 --- a/OMPython/OMCSession.py +++ b/OMPython/OMCSession.py @@ -90,12 +90,6 @@ def __init__(self, session: OMCSessionZMQ, readonly: Optional[bool] = False): self._readonly = readonly self._omc_cache = {} - def execute(self, command): - warnings.warn("This function is depreciated and will be removed in future versions; " - "please use sendExpression() instead", DeprecationWarning, stacklevel=1) - - return self.sendExpression(command, parsed=False) - def _ask(self, question: str, opt: Optional[list[str]] = None, parsed: Optional[bool] = True): if opt is None: @@ -534,6 +528,12 @@ def _connect_to_omc(self, timeout): self._omc.setsockopt(zmq.IMMEDIATE, True) # Queue messages only to completed connections self._omc.connect(self._port) + def execute(self, command): + warnings.warn("This function is depreciated and will be removed in future versions; " + "please use sendExpression() instead", DeprecationWarning, stacklevel=1) + + return self.sendExpression(command, parsed=False) + def sendExpression(self, command, parsed=True): p = self._omc_process.poll() # check if process is running if p is not None: From ca41560493fa491d71374af97b5b3124b2a0d2fc Mon Sep 17 00:00:00 2001 From: syntron Date: Mon, 28 Apr 2025 13:54:46 +0200 Subject: [PATCH 38/56] [OMCSessionCmd] make sendExpression() available --- OMPython/OMCSession.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/OMPython/OMCSession.py b/OMPython/OMCSession.py index 1cc6f7aa..e40d5da1 100644 --- a/OMPython/OMCSession.py +++ b/OMPython/OMCSession.py @@ -90,6 +90,9 @@ def __init__(self, session: OMCSessionZMQ, readonly: Optional[bool] = False): self._readonly = readonly self._omc_cache = {} + def sendExpression(self, command, parsed=True): + return self._session.sendExpression(command=command, parsed=parsed) + def _ask(self, question: str, opt: Optional[list[str]] = None, parsed: Optional[bool] = True): if opt is None: From b06d28ab61968749e8c62f2ea982445896b81a90 Mon Sep 17 00:00:00 2001 From: syntron Date: Mon, 28 Apr 2025 16:40:48 +0200 Subject: [PATCH 39/56] [tests] new test for OMCSessionCmd --- tests/test_OMSessionCmd.py | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 tests/test_OMSessionCmd.py diff --git a/tests/test_OMSessionCmd.py b/tests/test_OMSessionCmd.py new file mode 100644 index 00000000..96fd667a --- /dev/null +++ b/tests/test_OMSessionCmd.py @@ -0,0 +1,22 @@ +import OMPython +import unittest + + +class OMCSessionCmdTester(unittest.TestCase): + def __init__(self, *args, **kwargs): + super(OMCSessionCmdTester, self).__init__(*args, **kwargs) + + def test_isPackage(self): + omczmq = OMPython.OMCSessionZMQ() + omccmd = OMPython.OMCSessionCmd(session=omczmq) + assert not omccmd.isPackage('Modelica') + + def test_isPackage2(self): + mod = OMPython.ModelicaSystem(modelName="Modelica.Electrical.Analog.Examples.CauerLowPassAnalog", + lmodel=["Modelica"]) + omccmd = OMPython.OMCSessionCmd(session=mod.getconn) + assert omccmd.isPackage('Modelica') + + +if __name__ == '__main__': + unittest.main() From 7c1575bab78bfc09b6f163aabd33e65e4775f39c Mon Sep 17 00:00:00 2001 From: syntron Date: Sat, 26 Apr 2025 21:51:02 +0200 Subject: [PATCH 40/56] [OMCSessionZMQ] allways check for errors if using sendExpression() needs the preparation / additional changes in OMCSession* and ModelicaSystem --- OMPython/OMCSession.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/OMPython/OMCSession.py b/OMPython/OMCSession.py index e0c37d65..13905ab8 100644 --- a/OMPython/OMCSession.py +++ b/OMPython/OMCSession.py @@ -562,6 +562,17 @@ def sendExpression(self, command, parsed=True): return None else: result = self._omc.recv_string() + + # allways check for error + self._omc.send_string("getErrorString()", flags=zmq.NOBLOCK) + error_raw = self._omc.recv_string() + error_str = om_parser_typed(error_raw) + if error_str: + if "Error" in error_str: + raise OMCSessionException(f"OM Error for 'sendExpression({command}, {parsed})': {error_str}") + else: + logger.warning(f"[OM]: {error_str}") + if parsed is True: answer = om_parser_typed(result) return answer From fb239c234d6540b3fd745b86392365cfb7f7fded Mon Sep 17 00:00:00 2001 From: syntron Date: Tue, 29 Apr 2025 20:48:30 +0200 Subject: [PATCH 41/56] [OMCSession*] move logging into OMCSessionZMQ.sendExpression() --- OMPython/OMCSession.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/OMPython/OMCSession.py b/OMPython/OMCSession.py index 13905ab8..90e60dae 100644 --- a/OMPython/OMCSession.py +++ b/OMPython/OMCSession.py @@ -109,8 +109,6 @@ def _ask(self, question: str, opt: Optional[list[str]] = None, parsed: Optional[ if p in self._omc_cache: return self._omc_cache[p] - logger.debug('OMC ask: %s (parsed=%s)', expression, parsed) - try: res = self._session.sendExpression(expression, parsed=parsed) except OMCSessionException as ex: @@ -542,6 +540,8 @@ def sendExpression(self, command, parsed=True): if p is not None: raise OMCSessionException("Process Exited, No connection with OMC. Create a new instance of OMCSessionZMQ!") + logger.debug("sendExpression(%r, parsed=%r)", command, parsed) + attempts = 0 while True: try: From 02a79890e67ceddee53ec46db4fd49494d021922 Mon Sep 17 00:00:00 2001 From: syntron Date: Mon, 28 Apr 2025 15:53:23 +0200 Subject: [PATCH 42/56] [ModelicaSystem] exception handling for sendExpression() --- OMPython/ModelicaSystem.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/OMPython/ModelicaSystem.py b/OMPython/ModelicaSystem.py index 1a077b1b..0ecd94e9 100644 --- a/OMPython/ModelicaSystem.py +++ b/OMPython/ModelicaSystem.py @@ -46,7 +46,7 @@ from dataclasses import dataclass from typing import Optional -from OMPython.OMCSession import OMCSessionZMQ +from OMPython.OMCSession import OMCSessionZMQ, OMCSessionException # define logger using the current module name as ID logger = logging.getLogger(__name__) @@ -327,8 +327,14 @@ def buildModel(self, variableFilter=None): self.xmlparse() def sendExpression(self, expr, parsed=True): - logger.debug("sendExpression(%r, %r)", expr, parsed) - return self.getconn.sendExpression(expr, parsed) + try: + retval = self.getconn.sendExpression(expr, parsed) + except OMCSessionException as ex: + raise ModelicaSystemError(f"Error executing {repr(expr)}") from ex + + logger.debug(f"Result of executing {repr(expr)}: {repr(retval)}") + + return retval # request to OMC def requestApi(self, apiName, entity=None, properties=None): # 2 From 3dd1ba8d14eca491a801b53cc44d0a545ca055e5 Mon Sep 17 00:00:00 2001 From: syntron Date: Tue, 29 Apr 2025 20:50:45 +0200 Subject: [PATCH 43/56] [ModelicaSystem] remove last call to getErrorString() this is handled in OMCSessionZMQ.sendExpression() --- OMPython/ModelicaSystem.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/OMPython/ModelicaSystem.py b/OMPython/ModelicaSystem.py index 0ecd94e9..cdd7f1cd 100644 --- a/OMPython/ModelicaSystem.py +++ b/OMPython/ModelicaSystem.py @@ -1094,8 +1094,7 @@ def linearize(self, lintime: Optional[float] = None, simflags: Optional[str] = N linearFile = pathlib.Path(f'linear_{self.modelName}.py') if not linearFile.exists(): - errormsg = self.sendExpression("getErrorString()") - raise ModelicaSystemError(f"Linearization failed: {linearFile} not found: {errormsg}") + raise ModelicaSystemError(f"Linearization failed: {linearFile} not found!") # this function is called from the generated python code linearized_model.py at runtime, # to improve the performance by directly reading the matrices A, B, C and D from the julia code and avoid building the linearized modelica model From 6547f14c1c76071a34d55a10d7eaec1c29de7533 Mon Sep 17 00:00:00 2001 From: syntron Date: Sat, 26 Apr 2025 21:43:23 +0200 Subject: [PATCH 44/56] [ModelicaSystemCmd] draft --- OMPython/ModelicaSystem.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/OMPython/ModelicaSystem.py b/OMPython/ModelicaSystem.py index cdd7f1cd..b506f5b0 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 b9c04b24fdf44d7233b4d9684d147f1782feaad1 Mon Sep 17 00:00:00 2001 From: syntron Date: Mon, 28 Apr 2025 09:24:25 +0200 Subject: [PATCH 45/56] [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 b506f5b0..d35b9ba0 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, OMCSessionException @@ -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}: nonzero return code") + 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: @@ -284,45 +349,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(): - 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}: nonzero return code") - 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 @@ -652,21 +678,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() @@ -675,13 +700,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: @@ -691,9 +729,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: @@ -708,18 +745,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 @@ -1034,13 +1063,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: @@ -1057,6 +1088,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: @@ -1065,8 +1098,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() @@ -1077,26 +1109,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 5cc7fa5fb00cc9d63ebd9e4ecc3a996ed0885180 Mon Sep 17 00:00:00 2001 From: syntron Date: Mon, 28 Apr 2025 13:15:51 +0200 Subject: [PATCH 46/56] [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 d35b9ba0..82b06e56 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 @@ -687,7 +693,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) @@ -700,7 +706,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? @@ -711,6 +717,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 @@ -730,7 +739,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: @@ -746,7 +755,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() @@ -1071,7 +1080,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: @@ -1098,7 +1107,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() @@ -1109,9 +1118,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? @@ -1122,6 +1131,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 e680f1f3476e9e24eb79581e5337f10386b77248 Mon Sep 17 00:00:00 2001 From: syntron Date: Mon, 28 Apr 2025 13:20:50 +0200 Subject: [PATCH 47/56] [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 82b06e56..60008b29 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__( @@ -709,23 +729,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) @@ -1123,22 +1128,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 066fc03c26bb60ce02242ba73008acc48efd5942 Mon Sep 17 00:00:00 2001 From: syntron Date: Mon, 28 Apr 2025 13:35:39 +0200 Subject: [PATCH 48/56] [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 60008b29..4ef6e1a6 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}: nonzero return code") + raise ModelicaSystemError(f"Error running command {cmdl}: nonzero return code") 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) @@ -716,7 +778,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 @@ -1102,7 +1164,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 e23dfeceae0ea4939d3e3ba04d0d7b982c2c572f Mon Sep 17 00:00:00 2001 From: syntron Date: Mon, 28 Apr 2025 14:04:50 +0200 Subject: [PATCH 49/56] [ModelicaSystemCmd] simplify --- OMPython/ModelicaSystem.py | 33 +++++++++------------------------ 1 file changed, 9 insertions(+), 24 deletions(-) diff --git a/OMPython/ModelicaSystem.py b/OMPython/ModelicaSystem.py index 4ef6e1a6..47a17eff 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 4b0fabaf28a53fc57aaed0c599fcc953e5a95d58 Mon Sep 17 00:00:00 2001 From: syntron Date: Mon, 28 Apr 2025 13:42:02 +0200 Subject: [PATCH 50/56] [__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 2bd6754a5b2c89894df8b17780ee3f269b3a8540 Mon Sep 17 00:00:00 2001 From: syntron Date: Mon, 28 Apr 2025 19:07:27 +0200 Subject: [PATCH 51/56] [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 47a17eff..28236079 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 308eb3b701ae7cabad088d50d7fefbe61de847de Mon Sep 17 00:00:00 2001 From: syntron Date: Mon, 28 Apr 2025 21:37:49 +0200 Subject: [PATCH 52/56] [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 28236079..e50fae74 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 6bfecef4005b8f8471d4c1390d3d0fb1aa4d14af Mon Sep 17 00:00:00 2001 From: syntron Date: Mon, 28 Apr 2025 14:38:08 +0200 Subject: [PATCH 53/56] [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 e50fae74..a7b36822 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 ): @@ -371,7 +371,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): @@ -409,7 +408,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) @@ -456,22 +455,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): @@ -815,15 +816,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) @@ -834,7 +835,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) @@ -1109,7 +1110,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) @@ -1201,9 +1202,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(): @@ -1236,7 +1237,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 9a0ce1c9..ac1d0f50 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 2d0c3d0547535fe08f77c343e0141353d76a3153 Mon Sep 17 00:00:00 2001 From: syntron Date: Mon, 28 Apr 2025 21:38:32 +0200 Subject: [PATCH 54/56] [ModelicaSystemCmd] do *NOT* raise error if returncode != 0 could be a simulation which stoped before the final time ... --- OMPython/ModelicaSystem.py | 28 ++++++++++++++++++---------- 1 file changed, 18 insertions(+), 10 deletions(-) diff --git a/OMPython/ModelicaSystem.py b/OMPython/ModelicaSystem.py index a7b36822..b39e677a 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}: nonzero return code") 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: @@ -396,7 +395,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 @@ -820,13 +819,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: @@ -862,7 +861,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 @@ -878,7 +886,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 From d6fd226254bd7bdf38fdb45322e3fe27ab9cad19 Mon Sep 17 00:00:00 2001 From: syntron Date: Mon, 28 Apr 2025 21:58:53 +0200 Subject: [PATCH 55/56] [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 ae5d6c450b542685f2af52d8fb6c2cad8bfc3050 Mon Sep 17 00:00:00 2001 From: syntron Date: Tue, 29 Apr 2025 21:14:41 +0200 Subject: [PATCH 56/56] [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 b39e677a..6888ba43 100644 --- a/OMPython/ModelicaSystem.py +++ b/OMPython/ModelicaSystem.py @@ -1233,7 +1233,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: