From 429fe877667e23c251297a8389b6a00835436a66 Mon Sep 17 00:00:00 2001 From: syntron Date: Sat, 27 Dec 2025 21:00:13 +0100 Subject: [PATCH 01/37] [ModelicaSystem] move import re Pylint is recommenting to order the imports in several sections alphabetically: (1) python standard library (2) third party packages (3) current package --- OMPython/ModelicaSystem.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/OMPython/ModelicaSystem.py b/OMPython/ModelicaSystem.py index 9db3da33..20e6df10 100644 --- a/OMPython/ModelicaSystem.py +++ b/OMPython/ModelicaSystem.py @@ -11,6 +11,7 @@ import os import pathlib import queue +import re import textwrap import threading from typing import Any, cast, Optional @@ -19,8 +20,6 @@ import numpy as np -import re - from OMPython.OMCSession import ( OMCSessionException, OMCSessionRunData, From c1e597a5cb0618e024b335f77d54ec9353a3c5ed Mon Sep 17 00:00:00 2001 From: syntron Date: Sat, 27 Dec 2025 21:35:22 +0100 Subject: [PATCH 02/37] [ModelicaSystem] fix pylint message OMPython/ModelicaSystem.py:1787:16: W0612: Unused variable 'key' (unused-variable) => replace items() by values() --- OMPython/ModelicaSystem.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/OMPython/ModelicaSystem.py b/OMPython/ModelicaSystem.py index 9db3da33..4a323540 100644 --- a/OMPython/ModelicaSystem.py +++ b/OMPython/ModelicaSystem.py @@ -1797,7 +1797,7 @@ def linearize( om_cmd.arg_set(key="overrideFile", val=override_file.as_posix()) if self._inputs: - for key, data in self._inputs.items(): + for data in self._inputs.values(): if data is not None: for value in data: if value[0] < float(self._simulate_options["startTime"]): From e588845b28864c723f91b126992e599f824e1162 Mon Sep 17 00:00:00 2001 From: syntron Date: Sat, 27 Dec 2025 21:43:59 +0100 Subject: [PATCH 03/37] [ModelicaSystem] improve lintime checks --- OMPython/ModelicaSystem.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/OMPython/ModelicaSystem.py b/OMPython/ModelicaSystem.py index 9db3da33..e7e1cf19 100644 --- a/OMPython/ModelicaSystem.py +++ b/OMPython/ModelicaSystem.py @@ -1805,7 +1805,14 @@ def linearize( csvfile = self._createCSVData() om_cmd.arg_set(key="csvInput", val=csvfile.as_posix()) - om_cmd.arg_set(key="l", val=str(lintime or self._linearization_options["stopTime"])) + if lintime is None: + lintime = float(self._linearization_options["stopTime"]) + if (float(self._linearization_options["startTime"]) > lintime + or float(self._linearization_options["stopTime"]) < lintime): + raise ModelicaSystemError(f"Invalid linearisation time: {lintime=}; " + f"expected value: {self._linearization_options['startTime']} " + f"<= lintime <= {self._linearization_options['stopTime']}") + om_cmd.arg_set(key="l", val=str(lintime)) # allow runtime simulation flags from user input if simflags is not None: From d5ee23205d998d83478f13a1c7a58b689fe73c19 Mon Sep 17 00:00:00 2001 From: syntron Date: Sun, 23 Nov 2025 13:25:50 +0100 Subject: [PATCH 04/37] [unittests] use new definitions / remove OMCSessionZMQ --- tests/test_ArrayDimension.py | 16 ++++----- tests/test_FMIRegression.py | 12 +++---- tests/test_ModelicaSystem.py | 19 +++++----- tests/test_ModelicaSystemDoE.py | 7 ++-- tests/test_OMCPath.py | 43 ++++++++-------------- tests/test_OMSessionCmd.py | 4 +-- tests/test_ZMQ.py | 63 ++++++++++++++++----------------- tests/test_docker.py | 24 +++++-------- 8 files changed, 82 insertions(+), 106 deletions(-) diff --git a/tests/test_ArrayDimension.py b/tests/test_ArrayDimension.py index 13b3c11b..6e80d53f 100644 --- a/tests/test_ArrayDimension.py +++ b/tests/test_ArrayDimension.py @@ -2,18 +2,18 @@ def test_ArrayDimension(tmp_path): - omc = OMPython.OMCSessionZMQ() + omcs = OMPython.OMCSessionLocal() - omc.sendExpression(f'cd("{tmp_path.as_posix()}")') + omcs.sendExpression(f'cd("{tmp_path.as_posix()}")') - omc.sendExpression('loadString("model A Integer x[5+1,1+6]; end A;")') - omc.sendExpression("getErrorString()") + omcs.sendExpression('loadString("model A Integer x[5+1,1+6]; end A;")') + omcs.sendExpression("getErrorString()") - result = omc.sendExpression("getComponents(A)") + result = omcs.sendExpression("getComponents(A)") assert result[0][-1] == (6, 7), "array dimension does not match" - omc.sendExpression('loadString("model A Integer y = 5; Integer x[y+1,1+9]; end A;")') - omc.sendExpression("getErrorString()") + omcs.sendExpression('loadString("model A Integer y = 5; Integer x[y+1,1+9]; end A;")') + omcs.sendExpression("getErrorString()") - result = omc.sendExpression("getComponents(A)") + result = omcs.sendExpression("getComponents(A)") assert result[-1][-1] == ('y+1', 10), "array dimension does not match" diff --git a/tests/test_FMIRegression.py b/tests/test_FMIRegression.py index b61b8d49..8a91c514 100644 --- a/tests/test_FMIRegression.py +++ b/tests/test_FMIRegression.py @@ -7,21 +7,21 @@ def buildModelFMU(modelName): - omc = OMPython.OMCSessionZMQ() + omcs = OMPython.OMCSessionLocal() tempdir = pathlib.Path(tempfile.mkdtemp()) try: - omc.sendExpression(f'cd("{tempdir.as_posix()}")') + omcs.sendExpression(f'cd("{tempdir.as_posix()}")') - omc.sendExpression("loadModel(Modelica)") - omc.sendExpression("getErrorString()") + omcs.sendExpression("loadModel(Modelica)") + omcs.sendExpression("getErrorString()") fileNamePrefix = modelName.split(".")[-1] exp = f'buildModelFMU({modelName}, fileNamePrefix="{fileNamePrefix}")' - fmu = omc.sendExpression(exp) + fmu = omcs.sendExpression(exp) assert os.path.exists(fmu) finally: - del omc + del omcs shutil.rmtree(tempdir, ignore_errors=True) diff --git a/tests/test_ModelicaSystem.py b/tests/test_ModelicaSystem.py index dcc55d0b..dd0321ec 100644 --- a/tests/test_ModelicaSystem.py +++ b/tests/test_ModelicaSystem.py @@ -47,14 +47,15 @@ def worker(): ) mod.simulate() mod.convertMo2Fmu(fmuType="me") + for _ in range(10): worker() def test_setParameters(): - omc = OMPython.OMCSessionZMQ() - model_path_str = omc.sendExpression("getInstallationDirectoryPath()") + "/share/doc/omc/testmodels" - model_path = omc.omcpath(model_path_str) + omcs = OMPython.OMCSessionLocal() + model_path_str = omcs.sendExpression("getInstallationDirectoryPath()") + "/share/doc/omc/testmodels" + model_path = omcs.omcpath(model_path_str) mod = OMPython.ModelicaSystem() mod.model( model_file=model_path / "BouncingBall.mo", @@ -87,9 +88,9 @@ def test_setParameters(): def test_setSimulationOptions(): - omc = OMPython.OMCSessionZMQ() - model_path_str = omc.sendExpression("getInstallationDirectoryPath()") + "/share/doc/omc/testmodels" - model_path = omc.omcpath(model_path_str) + omcs = OMPython.OMCSessionLocal() + model_path_str = omcs.sendExpression("getInstallationDirectoryPath()") + "/share/doc/omc/testmodels" + model_path = omcs.omcpath(model_path_str) mod = OMPython.ModelicaSystem() mod.model( model_file=model_path / "BouncingBall.mo", @@ -155,11 +156,9 @@ def test_customBuildDirectory(tmp_path, model_firstorder): @skip_on_windows @skip_python_older_312 def test_getSolutions_docker(model_firstorder): - omcp = OMPython.OMCSessionDocker(docker="openmodelica/openmodelica:v1.25.0-minimal") - omc = OMPython.OMCSessionZMQ(omc_process=omcp) - + omcs = OMPython.OMCSessionDocker(docker="openmodelica/openmodelica:v1.25.0-minimal") mod = OMPython.ModelicaSystem( - session=omc.omc_process, + session=omcs, ) mod.model( model_file=model_firstorder, diff --git a/tests/test_ModelicaSystemDoE.py b/tests/test_ModelicaSystemDoE.py index 79c6e62d..0e8d6caa 100644 --- a/tests/test_ModelicaSystemDoE.py +++ b/tests/test_ModelicaSystemDoE.py @@ -69,15 +69,14 @@ def test_ModelicaSystemDoE_local(tmp_path, model_doe, param_doe): @skip_on_windows @skip_python_older_312 def test_ModelicaSystemDoE_docker(tmp_path, model_doe, param_doe): - omcp = OMPython.OMCSessionDocker(docker="openmodelica/openmodelica:v1.25.0-minimal") - omc = OMPython.OMCSessionZMQ(omc_process=omcp) - assert omc.sendExpression("getVersion()") == "OpenModelica 1.25.0" + omcs = OMPython.OMCSessionDocker(docker="openmodelica/openmodelica:v1.25.0-minimal") + assert omcs.sendExpression("getVersion()") == "OpenModelica 1.25.0" doe_mod = OMPython.ModelicaSystemDoE( model_file=model_doe, model_name="M", parameters=param_doe, - session=omcp, + session=omcs, simargs={"override": {'stopTime': 1.0}}, ) diff --git a/tests/test_OMCPath.py b/tests/test_OMCPath.py index b37e7c63..2ea8b8c8 100644 --- a/tests/test_OMCPath.py +++ b/tests/test_OMCPath.py @@ -15,54 +15,41 @@ ) -def test_OMCPath_OMCSessionZMQ(): - om = OMPython.OMCSessionZMQ() - - _run_OMCPath_checks(om) - - del om - - def test_OMCPath_OMCProcessLocal(): - omp = OMPython.OMCSessionLocal() - om = OMPython.OMCSessionZMQ(omc_process=omp) + omcs = OMPython.OMCSessionLocal() - _run_OMCPath_checks(om) + _run_OMCPath_checks(omcs) - del om + del omcs @skip_on_windows @skip_python_older_312 def test_OMCPath_OMCProcessDocker(): - omcp = OMPython.OMCSessionDocker(docker="openmodelica/openmodelica:v1.25.0-minimal") - om = OMPython.OMCSessionZMQ(omc_process=omcp) - assert om.sendExpression("getVersion()") == "OpenModelica 1.25.0" + omcs = OMPython.OMCSessionDocker(docker="openmodelica/openmodelica:v1.25.0-minimal") + assert omcs.sendExpression("getVersion()") == "OpenModelica 1.25.0" - _run_OMCPath_checks(om) + _run_OMCPath_checks(omcs) - del omcp - del om + del omcs @pytest.mark.skip(reason="Not able to run WSL on github") @skip_python_older_312 def test_OMCPath_OMCProcessWSL(): - omcp = OMPython.OMCSessionWSL( + omcs = OMPython.OMCSessionWSL( wsl_omc='omc', wsl_user='omc', timeout=30.0, ) - om = OMPython.OMCSessionZMQ(omc_process=omcp) - _run_OMCPath_checks(om) + _run_OMCPath_checks(omcs) - del omcp - del om + del omcs -def _run_OMCPath_checks(om: OMPython.OMCSessionZMQ): - p1 = om.omcpath_tempdir() +def _run_OMCPath_checks(omcs: OMPython.OMCSession): + p1 = omcs.omcpath_tempdir() p2 = p1 / 'test' p2.mkdir() assert p2.is_dir() @@ -81,14 +68,14 @@ def _run_OMCPath_checks(om: OMPython.OMCSessionZMQ): def test_OMCPath_write_file(tmpdir): - om = OMPython.OMCSessionZMQ() + omcs = OMPython.OMCSessionLocal() data = "abc # \\t # \" # \\n # xyz" - p1 = om.omcpath_tempdir() + p1 = omcs.omcpath_tempdir() p2 = p1 / 'test.txt' p2.write_text(data=data) assert data == p2.read_text() - del om + del omcs diff --git a/tests/test_OMSessionCmd.py b/tests/test_OMSessionCmd.py index bff4afde..d3997ecf 100644 --- a/tests/test_OMSessionCmd.py +++ b/tests/test_OMSessionCmd.py @@ -2,8 +2,8 @@ def test_isPackage(): - omczmq = OMPython.OMCSessionZMQ() - omccmd = OMPython.OMCSessionCmd(session=omczmq.omc_process) + omcs = OMPython.OMCSessionLocal() + omccmd = OMPython.OMCSessionCmd(session=omcs) assert not omccmd.isPackage('Modelica') diff --git a/tests/test_ZMQ.py b/tests/test_ZMQ.py index ba101560..1302a79d 100644 --- a/tests/test_ZMQ.py +++ b/tests/test_ZMQ.py @@ -14,58 +14,55 @@ def model_time_str(): @pytest.fixture -def om(tmp_path): +def omcs(tmp_path): origDir = pathlib.Path.cwd() os.chdir(tmp_path) - om = OMPython.OMCSessionZMQ() + omcs = OMPython.OMCSessionLocal() os.chdir(origDir) - return om + return omcs -def testHelloWorld(om): - assert om.sendExpression('"HelloWorld!"') == "HelloWorld!" +def testHelloWorld(omcs): + assert omcs.sendExpression('"HelloWorld!"') == "HelloWorld!" -def test_Translate(om, model_time_str): - assert om.sendExpression(model_time_str) == ("M",) - assert om.sendExpression('translateModel(M)') is True +def test_Translate(omcs, model_time_str): + assert omcs.sendExpression(model_time_str) == ("M",) + assert omcs.sendExpression('translateModel(M)') is True -def test_Simulate(om, model_time_str): - assert om.sendExpression(f'loadString("{model_time_str}")') is True - om.sendExpression('res:=simulate(M, stopTime=2.0)') - assert om.sendExpression('res.resultFile') +def test_Simulate(omcs, model_time_str): + assert omcs.sendExpression(f'loadString("{model_time_str}")') is True + omcs.sendExpression('res:=simulate(M, stopTime=2.0)') + assert omcs.sendExpression('res.resultFile') -def test_execute(om): +def test_execute(omcs): with pytest.deprecated_call(): - assert om.execute('"HelloWorld!"') == '"HelloWorld!"\n' - assert om.sendExpression('"HelloWorld!"', parsed=False) == '"HelloWorld!"\n' - assert om.sendExpression('"HelloWorld!"', parsed=True) == 'HelloWorld!' + assert omcs.execute('"HelloWorld!"') == '"HelloWorld!"\n' + assert omcs.sendExpression('"HelloWorld!"', parsed=False) == '"HelloWorld!"\n' + assert omcs.sendExpression('"HelloWorld!"', parsed=True) == 'HelloWorld!' -def test_omcprocessport_execute(om): - port = om.omc_process.get_port() - omcp = OMPython.OMCSessionPort(omc_port=port) +def test_omcprocessport_execute(omcs): + port = omcs.get_port() + omcs2 = OMPython.OMCSessionPort(omc_port=port) # run 1 - om1 = OMPython.OMCSessionZMQ(omc_process=omcp) - assert om1.sendExpression('"HelloWorld!"', parsed=False) == '"HelloWorld!"\n' + assert omcs.sendExpression('"HelloWorld!"', parsed=False) == '"HelloWorld!"\n' # run 2 - om2 = OMPython.OMCSessionZMQ(omc_process=omcp) - assert om2.sendExpression('"HelloWorld!"', parsed=False) == '"HelloWorld!"\n' + assert omcs2.sendExpression('"HelloWorld!"', parsed=False) == '"HelloWorld!"\n' - del om1 - del om2 + del omcs2 -def test_omcprocessport_simulate(om, model_time_str): - port = om.omc_process.get_port() - omcp = OMPython.OMCSessionPort(omc_port=port) +def test_omcprocessport_simulate(omcs, model_time_str): + port = omcs.get_port() + omcs2 = OMPython.OMCSessionPort(omc_port=port) - om = OMPython.OMCSessionZMQ(omc_process=omcp) - assert om.sendExpression(f'loadString("{model_time_str}")') is True - om.sendExpression('res:=simulate(M, stopTime=2.0)') - assert om.sendExpression('res.resultFile') != "" - del om + assert omcs2.sendExpression(f'loadString("{model_time_str}")') is True + omcs2.sendExpression('res:=simulate(M, stopTime=2.0)') + assert omcs2.sendExpression('res.resultFile') != "" + + del omcs2 diff --git a/tests/test_docker.py b/tests/test_docker.py index 025c48e3..f1973599 100644 --- a/tests/test_docker.py +++ b/tests/test_docker.py @@ -10,23 +10,17 @@ @skip_on_windows def test_docker(): - omcp = OMPython.OMCSessionDocker(docker="openmodelica/openmodelica:v1.25.0-minimal") - om = OMPython.OMCSessionZMQ(omc_process=omcp) - assert om.sendExpression("getVersion()") == "OpenModelica 1.25.0" + omcs = OMPython.OMCSessionDocker(docker="openmodelica/openmodelica:v1.25.0-minimal") + assert omcs.sendExpression("getVersion()") == "OpenModelica 1.25.0" - omcpInner = OMPython.OMCSessionDockerContainer(dockerContainer=omcp.get_docker_container_id()) - omInner = OMPython.OMCSessionZMQ(omc_process=omcpInner) - assert omInner.sendExpression("getVersion()") == "OpenModelica 1.25.0" + omcsInner = OMPython.OMCSessionDockerContainer(dockerContainer=omcs.get_docker_container_id()) + assert omcsInner.sendExpression("getVersion()") == "OpenModelica 1.25.0" - omcp2 = OMPython.OMCSessionDocker(docker="openmodelica/openmodelica:v1.25.0-minimal", port=11111) - om2 = OMPython.OMCSessionZMQ(omc_process=omcp2) - assert om2.sendExpression("getVersion()") == "OpenModelica 1.25.0" + omcs2 = OMPython.OMCSessionDocker(docker="openmodelica/openmodelica:v1.25.0-minimal", port=11111) + assert omcs2.sendExpression("getVersion()") == "OpenModelica 1.25.0" - del omcp2 - del om2 + del omcs2 - del omcpInner - del omInner + del omcsInner - del omcp - del om + del omcs From 5cc2554e266b6a76363360354eaa6eab192f7470 Mon Sep 17 00:00:00 2001 From: syntron Date: Thu, 27 Nov 2025 09:04:49 +0100 Subject: [PATCH 05/37] use keyword arguments if possible (FKA100 - flake8-force-keyword-arguments) --- OMPython/ModelicaSystem.py | 14 ++++++++------ OMPython/OMCSession.py | 6 ++++-- 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/OMPython/ModelicaSystem.py b/OMPython/ModelicaSystem.py index 9db3da33..deb1c8ac 100644 --- a/OMPython/ModelicaSystem.py +++ b/OMPython/ModelicaSystem.py @@ -263,8 +263,10 @@ def parse_simflags(simflags: str) -> dict[str, Optional[str | dict[str, Any] | n The return data can be used as input for self.args_set(). """ - warnings.warn("The argument 'simflags' is depreciated and will be removed in future versions; " - "please use 'simargs' instead", DeprecationWarning, stacklevel=2) + warnings.warn(message="The argument 'simflags' is depreciated and will be removed in future versions; " + "please use 'simargs' instead", + category=DeprecationWarning, + stacklevel=2) simargs: dict[str, Optional[str | dict[str, Any] | numbers.Number]] = {} @@ -559,7 +561,7 @@ def buildModel(self, variableFilter: Optional[str] = None): def sendExpression(self, expr: str, parsed: bool = True) -> Any: try: - retval = self._session.sendExpression(expr, parsed) + retval = self._session.sendExpression(command=expr, parsed=parsed) except OMCSessionException as ex: raise ModelicaSystemError(f"Error executing {repr(expr)}: {ex}") from ex @@ -1605,9 +1607,9 @@ def _createCSVData(self, csvfile: Optional[OMCPath] = None) -> OMCPath: for signal_name, signal_values in inputs.items(): signal = np.array(signal_values) interpolated_inputs[signal_name] = np.interp( - all_times, - signal[:, 0], # times - signal[:, 1], # values + x=all_times, + xp=signal[:, 0], # times + fp=signal[:, 1], # values ) # Write CSV file diff --git a/OMPython/OMCSession.py b/OMPython/OMCSession.py index 861f2a3a..47c3568f 100644 --- a/OMPython/OMCSession.py +++ b/OMPython/OMCSession.py @@ -818,8 +818,10 @@ def run_model_executable(cmd_run_data: OMCSessionRunData) -> int: return returncode def execute(self, command: str): - warnings.warn("This function is depreciated and will be removed in future versions; " - "please use sendExpression() instead", DeprecationWarning, stacklevel=2) + warnings.warn(message="This function is depreciated and will be removed in future versions; " + "please use sendExpression() instead", + category=DeprecationWarning, + stacklevel=2) return self.sendExpression(command, parsed=False) From a32dac819000c24063c595d1befc76cdba97a485 Mon Sep 17 00:00:00 2001 From: syntron Date: Mon, 15 Dec 2025 21:42:37 +0100 Subject: [PATCH 06/37] [OMCSession] improve log messages for model simulation using OM executable * use check=False => no CalledProcessError exception; possibility to handle the error code * error code needs to be checked (compare == 0) by the caller! * improve log messages; print returncode if it is != 0 with stdout --- OMPython/OMCSession.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/OMPython/OMCSession.py b/OMPython/OMCSession.py index 861f2a3a..9efbc879 100644 --- a/OMPython/OMCSession.py +++ b/OMPython/OMCSession.py @@ -800,20 +800,23 @@ def run_model_executable(cmd_run_data: OMCSessionRunData) -> int: env=my_env, cwd=cmd_run_data.cmd_cwd_local, timeout=cmd_run_data.cmd_timeout, - check=True, + check=False, ) stdout = cmdres.stdout.strip() stderr = cmdres.stderr.strip() returncode = cmdres.returncode - logger.debug("OM output for command %s:\n%s", repr(cmdl), stdout) + if returncode != 0: + logger.warning("OM executable run %s with returncode=%d and stdout:\n%s", + repr(cmdl), returncode, stdout) + else: + logger.debug("OM executable run %s with stdout:\n%s", repr(cmdl), stdout) if stderr: raise OMCSessionException(f"Error running model executable {repr(cmdl)}: {stderr}") + except subprocess.TimeoutExpired as ex: raise OMCSessionException(f"Timeout running model executable {repr(cmdl)}") from ex - except subprocess.CalledProcessError as ex: - raise OMCSessionException(f"Error running model executable {repr(cmdl)}") from ex return returncode From 7298ac972eada2fe55925a56540900fb8369c678 Mon Sep 17 00:00:00 2001 From: syntron Date: Mon, 15 Dec 2025 21:46:06 +0100 Subject: [PATCH 07/37] [ModelicaSystem] improve handling of model simulation * ensure a message if logged if returncode != 0 --- OMPython/ModelicaSystem.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/OMPython/ModelicaSystem.py b/OMPython/ModelicaSystem.py index 9db3da33..3d5cfa0d 100644 --- a/OMPython/ModelicaSystem.py +++ b/OMPython/ModelicaSystem.py @@ -1168,15 +1168,15 @@ def simulate( cmd_definition = om_cmd.definition() returncode = self._session.run_model_executable(cmd_run_data=cmd_definition) # and check returncode *AND* resultfile - if returncode != 0 and self._result_file.is_file(): + if returncode != 0: # check for an empty (=> 0B) result file which indicates a crash of the model executable # see: https://github.com/OpenModelica/OMPython/issues/261 # https://github.com/OpenModelica/OpenModelica/issues/13829 - if self._result_file.size() == 0: + if self._result_file.is_file() and self._result_file.size() == 0: self._result_file.unlink() raise ModelicaSystemError("Empty result file - this indicates a crash of the model executable!") - logger.warning(f"Return code = {returncode} but result file exists!") + logger.warning(f"Return code = {returncode} but result file was created!") self._simulated = True From 4f65db9498063e0d68bc03c159973d2e17b8e847 Mon Sep 17 00:00:00 2001 From: syntron Date: Tue, 16 Dec 2025 21:17:23 +0100 Subject: [PATCH 08/37] [ModelicaSystemDoE] fix exception handling * self.session().run_model_executable() will raise OMCSessionException! --- OMPython/ModelicaSystem.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/OMPython/ModelicaSystem.py b/OMPython/ModelicaSystem.py index 3d5cfa0d..195920fa 100644 --- a/OMPython/ModelicaSystem.py +++ b/OMPython/ModelicaSystem.py @@ -2178,9 +2178,9 @@ def worker(worker_id, task_queue): try: returncode = self.get_session().run_model_executable(cmd_run_data=cmd_definition) logger.info(f"[Worker {worker_id}] Simulation {resultpath.name} " - f"finished with return code: {returncode}") - except ModelicaSystemError as ex: - logger.warning(f"Simulation error for {resultpath.name}: {ex}") + f"finished with return code {returncode}") + except OMCSessionException as ex: + logger.warning(f"Error executing {repr(cmd_definition.get_cmd())}: {ex}") # Mark the task as done task_queue.task_done() From 9a3a3f0ebf564ecd2c4f31cb9e59e02e1dd34ed9 Mon Sep 17 00:00:00 2001 From: syntron Date: Thu, 27 Nov 2025 10:24:57 +0100 Subject: [PATCH 09/37] add OMCPath to the public interface --- OMPython/__init__.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/OMPython/__init__.py b/OMPython/__init__.py index de861736..5f189c7f 100644 --- a/OMPython/__init__.py +++ b/OMPython/__init__.py @@ -15,6 +15,7 @@ ModelicaSystemError, ) from OMPython.OMCSession import ( + OMCPath, OMCSessionCmd, OMCSessionException, OMCSessionRunData, @@ -34,6 +35,8 @@ 'ModelicaSystemDoE', 'ModelicaSystemError', + 'OMCPath', + 'OMCSessionCmd', 'OMCSessionException', 'OMCSessionRunData', From a46532d9ac8d724d4bea14ad98356bd92e84b9c3 Mon Sep 17 00:00:00 2001 From: syntron Date: Mon, 29 Dec 2025 14:25:17 +0100 Subject: [PATCH 10/37] [OMCSession] fix pylint: W0706: The except handler raises immediately (try-except-raise) --- OMPython/OMCSession.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/OMPython/OMCSession.py b/OMPython/OMCSession.py index 861f2a3a..c39c91d1 100644 --- a/OMPython/OMCSession.py +++ b/OMPython/OMCSession.py @@ -171,8 +171,6 @@ def getClassComment(self, className): logger.warning("Method 'getClassComment(%s)' failed; OMTypedParser error: %s", className, ex.msg) return 'No description available' - except OMCSessionException: - raise def getNthComponent(self, className, comp_id): """ returns with (type, name, description) """ @@ -201,8 +199,6 @@ def getParameterNames(self, className): logger.warning('OMPython error: %s', ex) # FIXME: OMC returns with a different structure for empty parameter set return [] - except OMCSessionException: - raise def getParameterValue(self, className, parameterName): try: @@ -211,8 +207,6 @@ def getParameterValue(self, className, parameterName): logger.warning("Method 'getParameterValue(%s, %s)' failed; OMTypedParser error: %s", className, parameterName, ex.msg) return "" - except OMCSessionException: - raise def getComponentModifierNames(self, className, componentName): return self._ask(question='getComponentModifierNames', opt=[className, componentName]) From 237e5cbaa68784395770dc7e58eb5034e99d08ed Mon Sep 17 00:00:00 2001 From: syntron Date: Sat, 27 Dec 2025 21:07:00 +0100 Subject: [PATCH 11/37] [ModelicaSystem] parse OM version in __init__() --- OMPython/ModelicaSystem.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/OMPython/ModelicaSystem.py b/OMPython/ModelicaSystem.py index 20e6df10..e6ca53bb 100644 --- a/OMPython/ModelicaSystem.py +++ b/OMPython/ModelicaSystem.py @@ -350,7 +350,8 @@ def __init__( self._session = OMCSessionLocal(omhome=omhome) # get OpenModelica version - self._version = self._session.sendExpression("getVersion()", parsed=True) + version_str = self._session.sendExpression("getVersion()", parsed=True) + self._version = self._parse_om_version(version=version_str) # set commandLineOptions using default values or the user defined list if command_line_options is None: # set default command line options to improve the performance of linearization and to avoid recompilation if @@ -1022,11 +1023,12 @@ def getOptimizationOptions( raise ModelicaSystemError("Unhandled input for getOptimizationOptions()") - def parse_om_version(self, version: str) -> tuple[int, int, int]: + def _parse_om_version(self, version: str) -> tuple[int, int, int]: match = re.search(r"v?(\d+)\.(\d+)\.(\d+)", version) if not match: raise ValueError(f"Version not found in: {version}") major, minor, patch = map(int, match.groups()) + return major, minor, patch def simulate_cmd( @@ -1078,8 +1080,7 @@ def simulate_cmd( # simulation options are not read from override file from version >= 1.26.0, # pass them to simulation executable directly as individual arguments # see https://github.com/OpenModelica/OpenModelica/pull/14813 - major, minor, patch = self.parse_om_version(self._version) - if (major, minor, patch) >= (1, 26, 0): + if self._version >= Version("1.26.0"): for key, opt_value in self._simulate_options_override.items(): om_cmd.arg_set(key=key, val=str(opt_value)) override_content = ( @@ -1775,8 +1776,7 @@ def linearize( ) # See comment in simulate_cmd regarding override file and OM version - major, minor, patch = self.parse_om_version(self._version) - if (major, minor, patch) >= (1, 26, 0): + if self._version >= Version("1.26.0"): for key, opt_value in self._linearization_options.items(): om_cmd.arg_set(key=key, val=str(opt_value)) override_content = ( From 1f3ec8704e9fb904c648545390029a79f641b432 Mon Sep 17 00:00:00 2001 From: syntron Date: Sat, 27 Dec 2025 21:21:05 +0100 Subject: [PATCH 12/37] [ModelicaSystem] simplify processing of override data Would it make sense to combine this code and the code in linearize() in one new function? def _process_override_data(self, om_cmd, sim_override, file_override) -> None: The code could: (1) check the version; set command line parameters as needed (2) create the content of the override file (3) create the overwrite file and set it as command line parameter The advantage would be, that the modified code is not in two places but only in one ... --- OMPython/ModelicaSystem.py | 80 +++++++++++++++++++------------------- 1 file changed, 39 insertions(+), 41 deletions(-) diff --git a/OMPython/ModelicaSystem.py b/OMPython/ModelicaSystem.py index e6ca53bb..2f7b6bd4 100644 --- a/OMPython/ModelicaSystem.py +++ b/OMPython/ModelicaSystem.py @@ -1031,6 +1031,33 @@ def _parse_om_version(self, version: str) -> tuple[int, int, int]: return major, minor, patch + def _process_override_data( + self, + om_cmd: ModelicaSystemCmd, + override_file: OMCPath, + override_var: dict[str, str], + override_sim: dict[str, str], + ) -> None: + if not override_var and not override_sim: + return + + override_content = "" + if override_var: + override_content += "\n".join([f"{key}={value}" for key, value in override_var.items()]) + "\n" + + # simulation options are not read from override file from version >= 1.26.0, + # pass them to simulation executable directly as individual arguments + # see https://github.com/OpenModelica/OpenModelica/pull/14813 + if override_sim: + if self._version >= (1, 26, 0): + for key, opt_value in override_sim.items(): + om_cmd.arg_set(key=key, val=str(opt_value)) + else: + override_content += "\n".join([f"{key}={value}" for key, value in override_sim.items()]) + "\n" + + override_file.write_text(override_content) + om_cmd.arg_set(key="overrideFile", val=override_file.as_posix()) + def simulate_cmd( self, result_file: OMCPath, @@ -1074,28 +1101,12 @@ def simulate_cmd( if simargs: om_cmd.args_set(args=simargs) - if self._override_variables or self._simulate_options_override: - override_file = result_file.parent / f"{result_file.stem}_override.txt" - - # simulation options are not read from override file from version >= 1.26.0, - # pass them to simulation executable directly as individual arguments - # see https://github.com/OpenModelica/OpenModelica/pull/14813 - if self._version >= Version("1.26.0"): - for key, opt_value in self._simulate_options_override.items(): - om_cmd.arg_set(key=key, val=str(opt_value)) - override_content = ( - "\n".join([f"{key}={value}" for key, value in self._override_variables.items()]) - + "\n" - ) - else: - override_content = ( - "\n".join([f"{key}={value}" for key, value in self._override_variables.items()]) - + "\n".join([f"{key}={value}" for key, value in self._simulate_options_override.items()]) - + "\n" - ) - - override_file.write_text(override_content) - om_cmd.arg_set(key="overrideFile", val=override_file.as_posix()) + self._process_override_data( + om_cmd=om_cmd, + override_file=result_file.parent / f"{result_file.stem}_override.txt", + override_var=self._override_variables, + override_sim=self._simulate_options_override, + ) if self._inputs: # if model has input quantities for key, val in self._inputs.items(): @@ -1775,25 +1786,12 @@ def linearize( modelname=self._model_name, ) - # See comment in simulate_cmd regarding override file and OM version - if self._version >= Version("1.26.0"): - for key, opt_value in self._linearization_options.items(): - om_cmd.arg_set(key=key, val=str(opt_value)) - override_content = ( - "\n".join([f"{key}={value}" for key, value in self._override_variables.items()]) - + "\n" - ) - else: - override_content = ( - "\n".join([f"{key}={value}" for key, value in self._override_variables.items()]) - + "\n".join([f"{key}={value}" for key, value in self._linearization_options.items()]) - + "\n" - ) - - override_file = self.getWorkDirectory() / f'{self._model_name}_override_linear.txt' - override_file.write_text(override_content) - - om_cmd.arg_set(key="overrideFile", val=override_file.as_posix()) + self._process_override_data( + om_cmd=om_cmd, + override_file=self.getWorkDirectory() / f'{self._model_name}_override_linear.txt', + override_var=self._override_variables, + override_sim=self._linearization_options, + ) if self._inputs: for key, data in self._inputs.items(): From cd3a9b9a3aa90fb870cf7b26b00a7255a303b2db Mon Sep 17 00:00:00 2001 From: syntron Date: Sat, 27 Dec 2025 21:32:21 +0100 Subject: [PATCH 13/37] [ModelicaSystem] define _linearization_options and _optimization_options as dict[str, str] * after OMC is run, the values will be string anyway * simplify code / align on one common definition for these dicts --- OMPython/ModelicaSystem.py | 32 +++++++++++++++----------------- 1 file changed, 15 insertions(+), 17 deletions(-) diff --git a/OMPython/ModelicaSystem.py b/OMPython/ModelicaSystem.py index 2f7b6bd4..3b25845d 100644 --- a/OMPython/ModelicaSystem.py +++ b/OMPython/ModelicaSystem.py @@ -331,14 +331,14 @@ def __init__( self._simulate_options: dict[str, str] = {} self._override_variables: dict[str, str] = {} self._simulate_options_override: dict[str, str] = {} - self._linearization_options: dict[str, str | float] = { - 'startTime': 0.0, - 'stopTime': 1.0, - 'stepSize': 0.002, - 'tolerance': 1e-8, + self._linearization_options: dict[str, str] = { + 'startTime': str(0.0), + 'stopTime': str(1.0), + 'stepSize': str(0.002), + 'tolerance': str(1e-8), } self._optimization_options = self._linearization_options | { - 'numberOfIntervals': 500, + 'numberOfIntervals': str(500), } self._linearized_inputs: list[str] = [] # linearization input list self._linearized_outputs: list[str] = [] # linearization output list @@ -950,7 +950,7 @@ def getSimulationOptions( def getLinearizationOptions( self, names: Optional[str | list[str]] = None, - ) -> dict[str, str | float] | list[str | float]: + ) -> dict[str, str] | list[str]: """Get simulation options used for linearization. Args: @@ -964,17 +964,16 @@ def getLinearizationOptions( returned. If `names` is a list, a list with one value for each option name in names is returned: [option1_value, option2_value, ...]. - Some option values are returned as float when first initialized, - but always as strings after setLinearizationOptions is used to - change them. + + The option values are always returned as strings. Examples: >>> mod.getLinearizationOptions() - {'startTime': 0.0, 'stopTime': 1.0, 'stepSize': 0.002, 'tolerance': 1e-08} + {'startTime': '0.0', 'stopTime': '1.0', 'stepSize': '0.002', 'tolerance': '1e-08'} >>> mod.getLinearizationOptions("stopTime") - [1.0] + ['1.0'] >>> mod.getLinearizationOptions(["tolerance", "stopTime"]) - [1e-08, 1.0] + ['1e-08', '1.0'] """ if names is None: return self._linearization_options @@ -988,7 +987,7 @@ def getLinearizationOptions( def getOptimizationOptions( self, names: Optional[str | list[str]] = None, - ) -> dict[str, str | float] | list[str | float]: + ) -> dict[str, str] | list[str]: """Get simulation options used for optimization. Args: @@ -1002,9 +1001,8 @@ def getOptimizationOptions( returned. If `names` is a list, a list with one value for each option name in names is returned: [option1_value, option2_value, ...]. - Some option values are returned as float when first initialized, - but always as strings after setOptimizationOptions is used to - change them. + + The option values are always returned as string. Examples: >>> mod.getOptimizationOptions() From 843cb2c0225fb915601dd270ab92cdbd04b5d6b6 Mon Sep 17 00:00:00 2001 From: syntron Date: Sat, 27 Dec 2025 22:08:50 +0100 Subject: [PATCH 14/37] [ModelicaSystem] simplify call to sendExpression() --- OMPython/ModelicaSystem.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/OMPython/ModelicaSystem.py b/OMPython/ModelicaSystem.py index 3b25845d..a1643348 100644 --- a/OMPython/ModelicaSystem.py +++ b/OMPython/ModelicaSystem.py @@ -350,7 +350,7 @@ def __init__( self._session = OMCSessionLocal(omhome=omhome) # get OpenModelica version - version_str = self._session.sendExpression("getVersion()", parsed=True) + version_str = self.sendExpression(expr="getVersion()") self._version = self._parse_om_version(version=version_str) # set commandLineOptions using default values or the user defined list if command_line_options is None: From 052f61e3369530f1f967d2c7781b77eefa54ae1b Mon Sep 17 00:00:00 2001 From: syntron Date: Sat, 27 Dec 2025 22:50:51 +0100 Subject: [PATCH 15/37] [ModelicaSystem] check for dict content using len() == 0 --- OMPython/ModelicaSystem.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/OMPython/ModelicaSystem.py b/OMPython/ModelicaSystem.py index a1643348..f5530b80 100644 --- a/OMPython/ModelicaSystem.py +++ b/OMPython/ModelicaSystem.py @@ -1036,7 +1036,7 @@ def _process_override_data( override_var: dict[str, str], override_sim: dict[str, str], ) -> None: - if not override_var and not override_sim: + if len(override_var) == 0 and len(override_sim) == 0: return override_content = "" From 27d894b356662f08d4726b35ea21bc2e103fea19 Mon Sep 17 00:00:00 2001 From: syntron Date: Sun, 28 Dec 2025 23:41:33 +0100 Subject: [PATCH 16/37] [ModelicaSystem] fix overwrite file (write only if there is content) --- OMPython/ModelicaSystem.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/OMPython/ModelicaSystem.py b/OMPython/ModelicaSystem.py index f5530b80..bf8ca91e 100644 --- a/OMPython/ModelicaSystem.py +++ b/OMPython/ModelicaSystem.py @@ -1053,8 +1053,9 @@ def _process_override_data( else: override_content += "\n".join([f"{key}={value}" for key, value in override_sim.items()]) + "\n" - override_file.write_text(override_content) - om_cmd.arg_set(key="overrideFile", val=override_file.as_posix()) + if override_content: + override_file.write_text(override_content) + om_cmd.arg_set(key="overrideFile", val=override_file.as_posix()) def simulate_cmd( self, From 86af54f44e2f0bb27af1d8f932cf2665d597d53f Mon Sep 17 00:00:00 2001 From: syntron Date: Mon, 5 Jan 2026 20:45:57 +0100 Subject: [PATCH 17/37] [ModelicaSystem] add docstring for _process_override_data() --- OMPython/ModelicaSystem.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/OMPython/ModelicaSystem.py b/OMPython/ModelicaSystem.py index bf8ca91e..707ed95a 100644 --- a/OMPython/ModelicaSystem.py +++ b/OMPython/ModelicaSystem.py @@ -1036,6 +1036,11 @@ def _process_override_data( override_var: dict[str, str], override_sim: dict[str, str], ) -> None: + """ + Define the override parameters. As the definition of simulation specific override parameter changes with OM + 1.26.0, version specific code is needed. Please keep in mind, that this will fail if OMC is not used to run the + model executable. + """ if len(override_var) == 0 and len(override_sim) == 0: return From 319cf9d2b26015428f17ca1fd8f2916bf90716c5 Mon Sep 17 00:00:00 2001 From: syntron Date: Thu, 27 Nov 2025 10:23:47 +0100 Subject: [PATCH 18/37] update README.md - replace OMCSessionZMQ with OMCSessionLocal --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index ff3888e7..5c7db4b6 100644 --- a/README.md +++ b/README.md @@ -40,8 +40,8 @@ help(OMPython) ``` ```python -from OMPython import OMCSessionZMQ -omc = OMCSessionZMQ() +from OMPython import OMCSessionLocal +omc = OMCSessionLocal() omc.sendExpression("getVersion()") ``` From f08ea8be028cc2e6efe72c30c5d9d9185d90784f Mon Sep 17 00:00:00 2001 From: syntron Date: Sun, 23 Nov 2025 17:37:14 +0100 Subject: [PATCH 19/37] [OMCSessionPort] add missing function / catch possible errors OMCSessionPort is a limited version as we do not know how OMC is run. --- OMPython/OMCSession.py | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/OMPython/OMCSession.py b/OMPython/OMCSession.py index 861f2a3a..d0bf9135 100644 --- a/OMPython/OMCSession.py +++ b/OMPython/OMCSession.py @@ -1022,11 +1022,27 @@ def __init__( super().__init__() self._omc_port = omc_port + @staticmethod + def run_model_executable(cmd_run_data: OMCSessionRunData) -> int: + """ + Run the command defined in cmd_run_data. This class is defined as static method such that there is no need to + keep instances of over classes around. + """ + raise OMCSessionException(f"({self.__class__.__name__}) does not support run_model_executable()!") + + def get_log(self) -> str: + """ + Get the log file content of the OMC session. + """ + log = f"No log available if OMC session is defined by port ({self.__class__.__name__})" + + return log + def omc_run_data_update(self, omc_run_data: OMCSessionRunData) -> OMCSessionRunData: """ Update the OMCSessionRunData object based on the selected OMCSession implementation. """ - raise OMCSessionException("OMCSessionPort does not support omc_run_data_update()!") + raise OMCSessionException(f"({self.__class__.__name__}) does not support omc_run_data_update()!") class OMCSessionLocal(OMCSession): From eae5e65dc83d4b3053a767eab3af524d7cacef70 Mon Sep 17 00:00:00 2001 From: syntron Date: Wed, 26 Nov 2025 20:42:37 +0100 Subject: [PATCH 20/37] [OMCSessionPort] fix exception message --- OMPython/OMCSession.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/OMPython/OMCSession.py b/OMPython/OMCSession.py index d0bf9135..ffcfadf2 100644 --- a/OMPython/OMCSession.py +++ b/OMPython/OMCSession.py @@ -1028,7 +1028,7 @@ def run_model_executable(cmd_run_data: OMCSessionRunData) -> int: Run the command defined in cmd_run_data. This class is defined as static method such that there is no need to keep instances of over classes around. """ - raise OMCSessionException(f"({self.__class__.__name__}) does not support run_model_executable()!") + raise OMCSessionException("OMCSessionPort does not support run_model_executable()!") def get_log(self) -> str: """ From 1a3762b6ce5b853858a57937e32a95cb651b5a7f Mon Sep 17 00:00:00 2001 From: syntron Date: Sat, 8 Nov 2025 11:36:27 +0100 Subject: [PATCH 21/37] reorder imports in __init__.py --- OMPython/__init__.py | 22 +++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/OMPython/__init__.py b/OMPython/__init__.py index de861736..d6912847 100644 --- a/OMPython/__init__.py +++ b/OMPython/__init__.py @@ -15,32 +15,36 @@ ModelicaSystemError, ) from OMPython.OMCSession import ( + OMCPath, OMCSessionCmd, - OMCSessionException, - OMCSessionRunData, - OMCSessionZMQ, - OMCSessionPort, - OMCSessionLocal, OMCSessionDocker, OMCSessionDockerContainer, + OMCSessionException, + OMCSessionLocal, + OMCSessionPort, + OMCSessionRunData, OMCSessionWSL, + OMCSessionZMQ, ) # global names imported if import 'from OMPython import *' is used __all__ = [ 'LinearizationResult', + 'ModelicaSystem', 'ModelicaSystemCmd', 'ModelicaSystemDoE', 'ModelicaSystemError', + 'OMCPath', + 'OMCSessionCmd', + 'OMCSessionDocker', + 'OMCSessionDockerContainer', 'OMCSessionException', - 'OMCSessionRunData', - 'OMCSessionZMQ', 'OMCSessionPort', 'OMCSessionLocal', - 'OMCSessionDocker', - 'OMCSessionDockerContainer', + 'OMCSessionRunData', 'OMCSessionWSL', + 'OMCSessionZMQ', ] From a2005ad24c1b71045a2e366f8e7db2f9f2a53256 Mon Sep 17 00:00:00 2001 From: syntron Date: Sun, 23 Nov 2025 17:37:29 +0100 Subject: [PATCH 22/37] [OMCSession] improve logging --- OMPython/OMCSession.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/OMPython/OMCSession.py b/OMPython/OMCSession.py index 861f2a3a..769e4ec2 100644 --- a/OMPython/OMCSession.py +++ b/OMPython/OMCSession.py @@ -702,8 +702,8 @@ def __del__(self): if isinstance(self._omc_zmq, zmq.Socket): try: self.sendExpression("quit()") - except OMCSessionException: - pass + except OMCSessionException as exc: + logger.warning(f"Exception on sending 'quit()' to OMC: {exc}! Continue nevertheless ...") finally: self._omc_zmq = None @@ -720,7 +720,7 @@ def __del__(self): self._omc_process.wait(timeout=2.0) except subprocess.TimeoutExpired: if self._omc_process: - logger.warning("OMC did not exit after being sent the quit() command; " + 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() From 12eef6907e5e8cac22f95d0073081fa84c8c851e Mon Sep 17 00:00:00 2001 From: syntron Date: Wed, 26 Nov 2025 20:21:33 +0100 Subject: [PATCH 23/37] [OMCSession*] define set_timeout() --- OMPython/OMCSession.py | 28 ++++++++++++++++++---------- 1 file changed, 18 insertions(+), 10 deletions(-) diff --git a/OMPython/OMCSession.py b/OMPython/OMCSession.py index 861f2a3a..46590c06 100644 --- a/OMPython/OMCSession.py +++ b/OMPython/OMCSession.py @@ -488,8 +488,6 @@ class OMCSessionRunData: cmd_model_executable: Optional[str] = None # additional library search path; this is mainly needed if OMCProcessLocal is run on Windows cmd_library_path: Optional[str] = None - # command timeout - cmd_timeout: Optional[float] = 10.0 # working directory to be used on the *local* system cmd_cwd_local: Optional[str] = None @@ -564,13 +562,12 @@ def omc_run_data_update(self, omc_run_data: OMCSessionRunData) -> OMCSessionRunD """ return self.omc_process.omc_run_data_update(omc_run_data=omc_run_data) - @staticmethod - def run_model_executable(cmd_run_data: OMCSessionRunData) -> int: + def run_model_executable(self, cmd_run_data: OMCSessionRunData) -> int: """ Run the command defined in cmd_run_data. This class is defined as static method such that there is no need to keep instances of over classes around. """ - return OMCSession.run_model_executable(cmd_run_data=cmd_run_data) + return self.omc_process.run_model_executable(cmd_run_data=cmd_run_data) def execute(self, command: str): return self.omc_process.execute(command=command) @@ -727,6 +724,19 @@ def __del__(self): finally: self._omc_process = None + def set_timeout(self, timeout: Optional[float] = None) -> float: + """ + Set the timeout to be used for OMC communication (OMCSession). + + The defined value is set and the current value is returned. If None is provided as argument, nothing is changed. + """ + retval = self._timeout + if timeout is not None: + if timeout <= 0.0: + raise OMCSessionException(f"Invalid timeout value: {timeout}!") + self._timeout = timeout + return retval + @staticmethod def escape_str(value: str) -> str: """ @@ -778,11 +788,9 @@ def omcpath_tempdir(self, tempdir_base: Optional[OMCPath] = None) -> OMCPath: return tempdir - @staticmethod - def run_model_executable(cmd_run_data: OMCSessionRunData) -> int: + def run_model_executable(self, cmd_run_data: OMCSessionRunData) -> int: """ - Run the command defined in cmd_run_data. This class is defined as static method such that there is no need to - keep instances of over classes around. + Run the command defined in cmd_run_data. """ my_env = os.environ.copy() @@ -799,7 +807,7 @@ def run_model_executable(cmd_run_data: OMCSessionRunData) -> int: text=True, env=my_env, cwd=cmd_run_data.cmd_cwd_local, - timeout=cmd_run_data.cmd_timeout, + timeout=self._timeout, check=True, ) stdout = cmdres.stdout.strip() From fb568de409dc295904cadbf316ed782aeb0fae42 Mon Sep 17 00:00:00 2001 From: syntron Date: Tue, 25 Nov 2025 22:26:36 +0100 Subject: [PATCH 24/37] [OMCSession*] align all usages of timeout to the same structure --- OMPython/OMCSession.py | 113 +++++++++++++++++++++-------------------- 1 file changed, 58 insertions(+), 55 deletions(-) diff --git a/OMPython/OMCSession.py b/OMPython/OMCSession.py index 46590c06..32ba5b95 100644 --- a/OMPython/OMCSession.py +++ b/OMPython/OMCSession.py @@ -839,34 +839,32 @@ def sendExpression(self, command: str, parsed: bool = True) -> Any: Caller should only check for OMCSessionException. """ - # this is needed if the class is not fully initialized or in the process of deletion - if hasattr(self, '_timeout'): - timeout = self._timeout - else: - timeout = 1.0 - if self._omc_zmq is None: raise OMCSessionException("No OMC running. Please create a new instance of OMCSession!") logger.debug("sendExpression(%r, parsed=%r)", command, parsed) + MAX_RETRIES = 50 attempts = 0 - while True: + while attempts < MAX_RETRIES: + attempts += 1 + try: self._omc_zmq.send_string(str(command), flags=zmq.NOBLOCK) break except zmq.error.Again: pass - attempts += 1 - if attempts >= 50: - # in the deletion process, the content is cleared. Thus, any access to a class attribute must be checked - try: - log_content = self.get_log() - except OMCSessionException: - log_content = 'log not available' - raise OMCSessionException(f"No connection with OMC (timeout={timeout}). " - f"Log-file says: \n{log_content}") - time.sleep(timeout / 50.0) + time.sleep(self._timeout / MAX_RETRIES) + else: + # in the deletion process, the content is cleared. Thus, any access to a class attribute must be checked + try: + log_content = self.get_log() + except OMCSessionException: + log_content = 'log not available' + + logger.error(f"Docker did not start. Log-file says:\n{log_content}") + raise OMCSessionException(f"No connection with OMC (timeout={self._timeout}).") + if command == "quit()": self._omc_zmq.close() self._omc_zmq = None @@ -1095,25 +1093,23 @@ def _omc_port_get(self) -> str: port = None # See if the omc server is running + MAX_RETRIES = 80 attempts = 0 - while True: - omc_portfile_path = self._get_portfile_path() + while attempts < MAX_RETRIES: + attempts += 1 + omc_portfile_path = self._get_portfile_path() if omc_portfile_path is not None and omc_portfile_path.is_file(): # Read the port file with open(file=omc_portfile_path, mode='r', encoding="utf-8") as f_p: port = f_p.readline() break - if port is not None: break - - attempts += 1 - if attempts == 80.0: - raise OMCSessionException(f"OMC Server did not start (timeout={self._timeout}). " - f"Could not open file {omc_portfile_path}. " - f"Log-file says:\n{self.get_log()}") - time.sleep(self._timeout / 80.0) + time.sleep(self._timeout / MAX_RETRIES) + else: + logger.error(f"Docker did not start. Log-file says:\n{self.get_log()}") + raise OMCSessionException(f"OMC Server did not start (timeout={self._timeout}).") logger.info(f"Local OMC Server is up and running at ZMQ port {port} " f"pid={self._omc_process.pid if isinstance(self._omc_process, subprocess.Popen) else '?'}") @@ -1195,7 +1191,11 @@ def _docker_process_get(self, docker_cid: str) -> Optional[DockerPopen]: raise NotImplementedError("Docker not supported on win32!") docker_process = None - for _ in range(0, 40): + MAX_RETRIES = 40 + attempts = 0 + while attempts < MAX_RETRIES: + attempts += 1 + docker_top = subprocess.check_output(["docker", "top", docker_cid]).decode().strip() docker_process = None for line in docker_top.split("\n"): @@ -1206,10 +1206,12 @@ def _docker_process_get(self, docker_cid: str) -> Optional[DockerPopen]: except psutil.NoSuchProcess as ex: raise OMCSessionException(f"Could not find PID {docker_top} - " "is this a docker instance spawned without --pid=host?") from ex - if docker_process is not None: break - time.sleep(self._timeout / 40.0) + time.sleep(self._timeout / MAX_RETRIES) + else: + logger.error(f"Docker did not start. Log-file says:\n{self.get_log()}") + raise OMCSessionException(f"Docker based OMC Server did not start (timeout={self._timeout}).") return docker_process @@ -1231,8 +1233,11 @@ def _omc_port_get(self) -> str: raise OMCSessionException(f"Invalid docker container ID: {self._docker_container_id}") # See if the omc server is running + MAX_RETRIES = 80 attempts = 0 - while True: + while attempts < MAX_RETRIES: + attempts += 1 + omc_portfile_path = self._get_portfile_path() if omc_portfile_path is not None: try: @@ -1243,16 +1248,12 @@ def _omc_port_get(self) -> str: port = output.decode().strip() except subprocess.CalledProcessError: pass - if port is not None: break - - attempts += 1 - if attempts == 80.0: - raise OMCSessionException(f"Docker based OMC Server did not start (timeout={self._timeout}). " - f"Could not open port file {omc_portfile_path}. " - f"Log-file says:\n{self.get_log()}") - time.sleep(self._timeout / 80.0) + time.sleep(self._timeout / MAX_RETRIES) + else: + logger.error(f"Docker did not start. Log-file says:\n{self.get_log()}") + raise OMCSessionException(f"Docker based OMC Server did not start (timeout={self._timeout}).") logger.info(f"Docker based OMC Server is up and running at port {port}") @@ -1420,25 +1421,28 @@ def _docker_omc_start(self) -> Tuple[subprocess.Popen, DockerPopen, str]: raise OMCSessionException(f"Invalid content for docker container ID file path: {docker_cid_file}") docker_cid = None - for _ in range(0, 40): + MAX_RETRIES = 40 + attempts = 0 + while attempts < MAX_RETRIES: + attempts += 1 + try: with open(file=docker_cid_file, mode="r", encoding="utf-8") as fh: docker_cid = fh.read().strip() except IOError: pass - if docker_cid: + if docker_cid is not None: break - time.sleep(self._timeout / 40.0) - - if docker_cid is None: + time.sleep(self._timeout / MAX_RETRIES) + else: logger.error(f"Docker did not start. Log-file says:\n{self.get_log()}") raise OMCSessionException(f"Docker did not start (timeout={self._timeout} might be too short " "especially if you did not docker pull the image before this command).") docker_process = self._docker_process_get(docker_cid=docker_cid) if docker_process is None: - raise OMCSessionException(f"Docker top did not contain omc process {self._random_string}. " - f"Log-file says:\n{self.get_log()}") + logger.error(f"Docker did not start. Log-file says:\n{self.get_log()}") + raise OMCSessionException(f"Docker top did not contain omc process {self._random_string}.") return omc_process, docker_process, docker_cid @@ -1594,8 +1598,11 @@ def _omc_port_get(self) -> str: port = None # See if the omc server is running + MAX_RETRIES = 80 attempts = 0 - while True: + while attempts < MAX_RETRIES: + attempts += 1 + try: omc_portfile_path = self._get_portfile_path() if omc_portfile_path is not None: @@ -1606,16 +1613,12 @@ def _omc_port_get(self) -> str: port = output.decode().strip() except subprocess.CalledProcessError: pass - if port is not None: break - - attempts += 1 - if attempts == 80.0: - raise OMCSessionException(f"WSL based OMC Server did not start (timeout={self._timeout}). " - f"Could not open port file {omc_portfile_path}. " - f"Log-file says:\n{self.get_log()}") - time.sleep(self._timeout / 80.0) + time.sleep(self._timeout / MAX_RETRIES) + else: + logger.error(f"Docker did not start. Log-file says:\n{self.get_log()}") + raise OMCSessionException(f"WSL based OMC Server did not start (timeout={self._timeout}).") logger.info(f"WSL based OMC Server is up and running at ZMQ port {port} " f"pid={self._omc_process.pid if isinstance(self._omc_process, subprocess.Popen) else '?'}") From 702208d78d76ac91462459260665a9c01954b990 Mon Sep 17 00:00:00 2001 From: syntron Date: Wed, 26 Nov 2025 19:38:48 +0100 Subject: [PATCH 25/37] [OMCSession*] simplify code for timeout loops --- OMPython/OMCSession.py | 73 +++++++++++++++++++++--------------------- 1 file changed, 37 insertions(+), 36 deletions(-) diff --git a/OMPython/OMCSession.py b/OMPython/OMCSession.py index 32ba5b95..b670e928 100644 --- a/OMPython/OMCSession.py +++ b/OMPython/OMCSession.py @@ -724,6 +724,31 @@ def __del__(self): finally: self._omc_process = None + def _timeout_loop( + self, + timeout: Optional[float] = None, + timestep: float = 0.1, + ): + """ + Helper (using yield) for while loops to check OMC startup / response. The loop is executed as long as True is + returned, i.e. the first False will stop the while loop. + """ + + if timeout is None: + timeout = self._timeout + if timeout <= 0: + raise OMCSessionException(f"Invalid timeout: {timeout}") + + timer = 0.0 + yield True + while True: + timer += timestep + if timer > timeout: + break + time.sleep(timestep) + yield True + yield False + def set_timeout(self, timeout: Optional[float] = None) -> float: """ Set the timeout to be used for OMC communication (OMCSession). @@ -844,17 +869,13 @@ def sendExpression(self, command: str, parsed: bool = True) -> Any: logger.debug("sendExpression(%r, parsed=%r)", command, parsed) - MAX_RETRIES = 50 - attempts = 0 - while attempts < MAX_RETRIES: - attempts += 1 - + loop = self._timeout_loop(timestep=0.05) + while next(loop): try: self._omc_zmq.send_string(str(command), flags=zmq.NOBLOCK) break except zmq.error.Again: pass - time.sleep(self._timeout / MAX_RETRIES) else: # in the deletion process, the content is cleared. Thus, any access to a class attribute must be checked try: @@ -1093,11 +1114,8 @@ def _omc_port_get(self) -> str: port = None # See if the omc server is running - MAX_RETRIES = 80 - attempts = 0 - while attempts < MAX_RETRIES: - attempts += 1 - + loop = self._timeout_loop(timestep=0.1) + while next(loop): omc_portfile_path = self._get_portfile_path() if omc_portfile_path is not None and omc_portfile_path.is_file(): # Read the port file @@ -1106,7 +1124,6 @@ def _omc_port_get(self) -> str: break if port is not None: break - time.sleep(self._timeout / MAX_RETRIES) else: logger.error(f"Docker did not start. Log-file says:\n{self.get_log()}") raise OMCSessionException(f"OMC Server did not start (timeout={self._timeout}).") @@ -1191,11 +1208,8 @@ def _docker_process_get(self, docker_cid: str) -> Optional[DockerPopen]: raise NotImplementedError("Docker not supported on win32!") docker_process = None - MAX_RETRIES = 40 - attempts = 0 - while attempts < MAX_RETRIES: - attempts += 1 - + loop = self._timeout_loop(timestep=0.2) + while next(loop): docker_top = subprocess.check_output(["docker", "top", docker_cid]).decode().strip() docker_process = None for line in docker_top.split("\n"): @@ -1208,7 +1222,6 @@ def _docker_process_get(self, docker_cid: str) -> Optional[DockerPopen]: "is this a docker instance spawned without --pid=host?") from ex if docker_process is not None: break - time.sleep(self._timeout / MAX_RETRIES) else: logger.error(f"Docker did not start. Log-file says:\n{self.get_log()}") raise OMCSessionException(f"Docker based OMC Server did not start (timeout={self._timeout}).") @@ -1233,11 +1246,8 @@ def _omc_port_get(self) -> str: raise OMCSessionException(f"Invalid docker container ID: {self._docker_container_id}") # See if the omc server is running - MAX_RETRIES = 80 - attempts = 0 - while attempts < MAX_RETRIES: - attempts += 1 - + loop = self._timeout_loop(timestep=0.1) + while next(loop): omc_portfile_path = self._get_portfile_path() if omc_portfile_path is not None: try: @@ -1250,7 +1260,6 @@ def _omc_port_get(self) -> str: pass if port is not None: break - time.sleep(self._timeout / MAX_RETRIES) else: logger.error(f"Docker did not start. Log-file says:\n{self.get_log()}") raise OMCSessionException(f"Docker based OMC Server did not start (timeout={self._timeout}).") @@ -1421,11 +1430,8 @@ def _docker_omc_start(self) -> Tuple[subprocess.Popen, DockerPopen, str]: raise OMCSessionException(f"Invalid content for docker container ID file path: {docker_cid_file}") docker_cid = None - MAX_RETRIES = 40 - attempts = 0 - while attempts < MAX_RETRIES: - attempts += 1 - + loop = self._timeout_loop(timestep=0.1) + while next(loop): try: with open(file=docker_cid_file, mode="r", encoding="utf-8") as fh: docker_cid = fh.read().strip() @@ -1433,7 +1439,6 @@ def _docker_omc_start(self) -> Tuple[subprocess.Popen, DockerPopen, str]: pass if docker_cid is not None: break - time.sleep(self._timeout / MAX_RETRIES) else: logger.error(f"Docker did not start. Log-file says:\n{self.get_log()}") raise OMCSessionException(f"Docker did not start (timeout={self._timeout} might be too short " @@ -1598,11 +1603,8 @@ def _omc_port_get(self) -> str: port = None # See if the omc server is running - MAX_RETRIES = 80 - attempts = 0 - while attempts < MAX_RETRIES: - attempts += 1 - + loop = self._timeout_loop(timestep=0.1) + while next(loop): try: omc_portfile_path = self._get_portfile_path() if omc_portfile_path is not None: @@ -1615,7 +1617,6 @@ def _omc_port_get(self) -> str: pass if port is not None: break - time.sleep(self._timeout / MAX_RETRIES) else: logger.error(f"Docker did not start. Log-file says:\n{self.get_log()}") raise OMCSessionException(f"WSL based OMC Server did not start (timeout={self._timeout}).") From cce234bd44f34b5964ea9a98ecdb8e0bd93891f1 Mon Sep 17 00:00:00 2001 From: syntron Date: Thu, 27 Nov 2025 10:21:54 +0100 Subject: [PATCH 26/37] [OMCSession] fix definiton of _timeout variable - use set_timeout() checks --- OMPython/OMCSession.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/OMPython/OMCSession.py b/OMPython/OMCSession.py index b670e928..396fb9e4 100644 --- a/OMPython/OMCSession.py +++ b/OMPython/OMCSession.py @@ -648,7 +648,9 @@ def __init__( """ # store variables - self._timeout = timeout + # set_timeout() is used to define the value of _timeout as it includes additional checks + self._timeout: float + self.set_timeout(timeout=timeout) # generate a random string for this instance of OMC self._random_string = uuid.uuid4().hex # get a temporary directory From ae4711a30a4b15cf65ad2db60ab1aa93d6c37c7b Mon Sep 17 00:00:00 2001 From: syntron Date: Thu, 27 Nov 2025 10:08:25 +0100 Subject: [PATCH 27/37] [OMCSession*] some additional cleanup (mypy / flake8) * remove not needed variable definitions * fix if condition for bool --- OMPython/OMCSession.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/OMPython/OMCSession.py b/OMPython/OMCSession.py index 396fb9e4..c487b758 100644 --- a/OMPython/OMCSession.py +++ b/OMPython/OMCSession.py @@ -983,7 +983,7 @@ def sendExpression(self, command: str, parsed: bool = True) -> Any: raise OMCSessionException(f"OMC error occurred for 'sendExpression({command}, {parsed}):\n" f"{msg_long_str}") - if parsed is False: + if not parsed: return result try: @@ -1209,7 +1209,6 @@ def _docker_process_get(self, docker_cid: str) -> Optional[DockerPopen]: if sys.platform == 'win32': raise NotImplementedError("Docker not supported on win32!") - docker_process = None loop = self._timeout_loop(timestep=0.2) while next(loop): docker_top = subprocess.check_output(["docker", "top", docker_cid]).decode().strip() @@ -1601,7 +1600,6 @@ def _omc_process_get(self) -> subprocess.Popen: return omc_process def _omc_port_get(self) -> str: - omc_portfile_path: Optional[pathlib.Path] = None port = None # See if the omc server is running From 252af40d3862a9e3a9042407a7731f281356f95c Mon Sep 17 00:00:00 2001 From: syntron Date: Thu, 27 Nov 2025 21:19:13 +0100 Subject: [PATCH 28/37] [OMCSession] move call to set_timeout() to __post_init__ --- OMPython/OMCSession.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/OMPython/OMCSession.py b/OMPython/OMCSession.py index c487b758..e1d1f123 100644 --- a/OMPython/OMCSession.py +++ b/OMPython/OMCSession.py @@ -648,9 +648,7 @@ def __init__( """ # store variables - # set_timeout() is used to define the value of _timeout as it includes additional checks - self._timeout: float - self.set_timeout(timeout=timeout) + self._timeout = timeout # generate a random string for this instance of OMC self._random_string = uuid.uuid4().hex # get a temporary directory @@ -684,6 +682,9 @@ def __post_init__(self) -> None: """ Create the connection to the OMC server using ZeroMQ. """ + # set_timeout() is used to define the value of _timeout as it includes additional checks + self.set_timeout(timeout=self._timeout) + port = self.get_port() if not isinstance(port, str): raise OMCSessionException(f"Invalid content for port: {port}") From 7f3241002f1e3db67fddde28b9418ba82aae3502 Mon Sep 17 00:00:00 2001 From: syntron Date: Sun, 28 Dec 2025 23:57:27 +0100 Subject: [PATCH 29/37] update test matrix python-version: ['3.10', '3.12', '3.14'] os: ['ubuntu-latest', 'windows-latest'] omc-version: ['1.25.0', 'stable', 'nightly'] --- .github/workflows/Test.yml | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/.github/workflows/Test.yml b/.github/workflows/Test.yml index 3601cb84..03042753 100644 --- a/.github/workflows/Test.yml +++ b/.github/workflows/Test.yml @@ -14,9 +14,19 @@ jobs: timeout-minutes: 30 strategy: matrix: - python-version: ['3.10', '3.12', '3.13'] + # test for: + # * Python 3.10 - oldest supported version + # * Python 3.12 - changes in OMCSession / OMCPath + # * Python 3.14 - latest Python version + python-version: ['3.10', '3.12', '3.14'] + # * Linux using ubuntu-latest + # * Windows using windows-latest os: ['ubuntu-latest', 'windows-latest'] - omc-version: ['stable', 'nightly'] + # * OM 1.25.0 - before changing definition of simulation overrides + # * OM stable - latest stable version + # * OM nightly - latest nightly build + omc-version: ['1.25.0', 'stable', 'nightly'] + # => total of 12 runs for each test steps: - uses: actions/checkout@v6 From 4626128d4f3988ccc457149081c187a8d965ca26 Mon Sep 17 00:00:00 2001 From: syntron Date: Sat, 24 Jan 2026 12:46:47 +0100 Subject: [PATCH 30/37] [__init__] fix imports - include OMCSession --- OMPython/__init__.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/OMPython/__init__.py b/OMPython/__init__.py index de861736..bc8aefbd 100644 --- a/OMPython/__init__.py +++ b/OMPython/__init__.py @@ -15,6 +15,7 @@ ModelicaSystemError, ) from OMPython.OMCSession import ( + OMCSession, OMCSessionCmd, OMCSessionException, OMCSessionRunData, @@ -34,6 +35,7 @@ 'ModelicaSystemDoE', 'ModelicaSystemError', + 'OMCSession', 'OMCSessionCmd', 'OMCSessionException', 'OMCSessionRunData', From 5d1c38d38734b8a9f88d5003f9b4cc349a8cfc21 Mon Sep 17 00:00:00 2001 From: syntron Date: Sat, 24 Jan 2026 14:30:27 +0100 Subject: [PATCH 31/37] [OMCSession] align definition of sendExpression() - use expr (was: command) the following classes are not changed - tehse are obsolete: - OMCSessionZMQ - OMCSessionCmd --- OMPython/ModelicaSystem.py | 44 ++++++++++++++++++------------------ OMPython/OMCSession.py | 46 +++++++++++++++++++------------------- 2 files changed, 45 insertions(+), 45 deletions(-) diff --git a/OMPython/ModelicaSystem.py b/OMPython/ModelicaSystem.py index 9db3da33..40e37024 100644 --- a/OMPython/ModelicaSystem.py +++ b/OMPython/ModelicaSystem.py @@ -466,12 +466,12 @@ def set_command_line_options(self, command_line_option: str): """ Set the provided command line option via OMC setCommandLineOptions(). """ - exp = f'setCommandLineOptions("{command_line_option}")' - self.sendExpression(exp) + expr = f'setCommandLineOptions("{command_line_option}")' + self.sendExpression(expr=expr) def _loadFile(self, fileName: OMCPath): # load file - self.sendExpression(f'loadFile("{fileName.as_posix()}")') + self.sendExpression(expr=f'loadFile("{fileName.as_posix()}")') # for loading file/package, loading model and building model def _loadLibrary(self, libraries: list): @@ -489,7 +489,7 @@ def _loadLibrary(self, libraries: list): expr_load_lib = f"loadModel({element[0]})" else: expr_load_lib = f'loadModel({element[0]}, {{"{element[1]}"}})' - self.sendExpression(expr_load_lib) + self.sendExpression(expr=expr_load_lib) else: raise ModelicaSystemError("loadLibrary() failed, Unknown type detected: " f"{element} is of type {type(element)}, " @@ -512,8 +512,8 @@ def setWorkDirectory(self, work_directory: Optional[str | os.PathLike] = None) - raise IOError(f"{workdir} could not be created") logger.info("Define work dir as %s", workdir) - exp = f'cd("{workdir.as_posix()}")' - self.sendExpression(exp) + expr = f'cd("{workdir.as_posix()}")' + self.sendExpression(expr=expr) # set the class variable _work_dir ... self._work_dir = workdir @@ -559,7 +559,7 @@ def buildModel(self, variableFilter: Optional[str] = None): def sendExpression(self, expr: str, parsed: bool = True) -> Any: try: - retval = self._session.sendExpression(expr, parsed) + retval = self._session.sendExpression(expr=expr, parsed=parsed) except OMCSessionException as ex: raise ModelicaSystemError(f"Error executing {repr(expr)}: {ex}") from ex @@ -575,16 +575,16 @@ def _requestApi( properties: Optional[str] = None, ) -> Any: if entity is not None and properties is not None: - exp = f'{apiName}({entity}, {properties})' + expr = f'{apiName}({entity}, {properties})' elif entity is not None and properties is None: if apiName in ("loadFile", "importFMU"): - exp = f'{apiName}("{entity}")' + expr = f'{apiName}("{entity}")' else: - exp = f'{apiName}({entity})' + expr = f'{apiName}({entity})' else: - exp = f'{apiName}()' + expr = f'{apiName}()' - return self.sendExpression(exp) + return self.sendExpression(expr=expr) def _xmlparse(self, xml_file: OMCPath): if not xml_file.is_file(): @@ -1258,8 +1258,8 @@ def getSolutions( # get absolute path result_file = result_file.absolute() - result_vars = self.sendExpression(f'readSimulationResultVars("{result_file.as_posix()}")') - self.sendExpression("closeSimulationResultFile()") + result_vars = self.sendExpression(expr=f'readSimulationResultVars("{result_file.as_posix()}")') + self.sendExpression(expr="closeSimulationResultFile()") if varList is None: return result_vars @@ -1276,9 +1276,9 @@ def getSolutions( if var not in result_vars: raise ModelicaSystemError(f"Requested data {repr(var)} does not exist") variables = ",".join(var_list_checked) - res = self.sendExpression(f'readSimulationResult("{result_file.as_posix()}",{{{variables}}})') + res = self.sendExpression(expr=f'readSimulationResult("{result_file.as_posix()}",{{{variables}}})') np_res = np.array(res) - self.sendExpression("closeSimulationResultFile()") + self.sendExpression(expr="closeSimulationResultFile()") return np_res @staticmethod @@ -1378,7 +1378,7 @@ def _set_method_helper( "structural, final, protected, evaluated or has a non-constant binding. " "Use sendExpression(...) and rebuild the model using buildModel() API; " "command to set the parameter before rebuilding the model: " - "sendExpression(\"setParameterValue(" + "sendExpression(expr=\"setParameterValue(" f"{self._model_name}, {key}, {val if val is not None else ''}" ")\").") @@ -2051,16 +2051,16 @@ def prepare(self) -> int: pk_value = pc_structure[idx_structure] if isinstance(pk_value, str): pk_value_str = self.get_session().escape_str(pk_value) - expression = f"setParameterValue({self._model_name}, {pk_structure}, \"{pk_value_str}\")" + expr = f"setParameterValue({self._model_name}, {pk_structure}, \"{pk_value_str}\")" elif isinstance(pk_value, bool): pk_value_bool_str = "true" if pk_value else "false" - expression = f"setParameterValue({self._model_name}, {pk_structure}, {pk_value_bool_str});" + expr = f"setParameterValue({self._model_name}, {pk_structure}, {pk_value_bool_str});" else: - expression = f"setParameterValue({self._model_name}, {pk_structure}, {pk_value})" - res = self._mod.sendExpression(expression) + expr = f"setParameterValue({self._model_name}, {pk_structure}, {pk_value})" + res = self._mod.sendExpression(expr=expr) if not res: raise ModelicaSystemError(f"Cannot set structural parameter {self._model_name}.{pk_structure} " - f"to {pk_value} using {repr(expression)}") + f"to {pk_value} using {repr(expr)}") self._mod.buildModel() diff --git a/OMPython/OMCSession.py b/OMPython/OMCSession.py index 861f2a3a..326cdb2a 100644 --- a/OMPython/OMCSession.py +++ b/OMPython/OMCSession.py @@ -280,13 +280,13 @@ def is_file(self, *, follow_symlinks=True) -> bool: """ Check if the path is a regular file. """ - return self._session.sendExpression(f'regularFileExists("{self.as_posix()}")') + return self._session.sendExpression(expr=f'regularFileExists("{self.as_posix()}")') def is_dir(self, *, follow_symlinks=True) -> bool: """ Check if the path is a directory. """ - return self._session.sendExpression(f'directoryExists("{self.as_posix()}")') + return self._session.sendExpression(expr=f'directoryExists("{self.as_posix()}")') def is_absolute(self): """ @@ -304,7 +304,7 @@ def read_text(self, encoding=None, errors=None, newline=None) -> str: The additional arguments `encoding`, `errors` and `newline` are only defined for compatibility with Path() definition. """ - return self._session.sendExpression(f'readFile("{self.as_posix()}")') + return self._session.sendExpression(expr=f'readFile("{self.as_posix()}")') def write_text(self, data: str, encoding=None, errors=None, newline=None): """ @@ -317,7 +317,7 @@ def write_text(self, data: str, encoding=None, errors=None, newline=None): raise TypeError(f"data must be str, not {data.__class__.__name__}") data_omc = self._session.escape_str(data) - self._session.sendExpression(f'writeFile("{self.as_posix()}", "{data_omc}", false);') + self._session.sendExpression(expr=f'writeFile("{self.as_posix()}", "{data_omc}", false);') return len(data) @@ -330,20 +330,20 @@ def mkdir(self, mode=0o777, parents=False, exist_ok=False): if self.is_dir() and not exist_ok: raise FileExistsError(f"Directory {self.as_posix()} already exists!") - return self._session.sendExpression(f'mkdir("{self.as_posix()}")') + return self._session.sendExpression(expr=f'mkdir("{self.as_posix()}")') def cwd(self): """ Returns the current working directory as an OMCPath object. """ - cwd_str = self._session.sendExpression('cd()') + cwd_str = self._session.sendExpression(expr='cd()') return OMCPath(cwd_str, session=self._session) def unlink(self, missing_ok: bool = False) -> None: """ Unlink (delete) the file or directory represented by this path. """ - res = self._session.sendExpression(f'deleteFile("{self.as_posix()}")') + res = self._session.sendExpression(expr=f'deleteFile("{self.as_posix()}")') if not res and not missing_ok: raise FileNotFoundError(f"Cannot delete file {self.as_posix()} - it does not exists!") @@ -373,12 +373,12 @@ def _omc_resolve(self, pathstr: str) -> str: Internal function to resolve the path of the OMCPath object using OMC functions *WITHOUT* changing the cwd within OMC. """ - expression = ('omcpath_cwd := cd(); ' - f'omcpath_check := cd("{pathstr}"); ' # check requested pathstring - 'cd(omcpath_cwd)') + expr = ('omcpath_cwd := cd(); ' + f'omcpath_check := cd("{pathstr}"); ' # check requested pathstring + 'cd(omcpath_cwd)') try: - result = self._session.sendExpression(command=expression, parsed=False) + result = self._session.sendExpression(expr=expr, parsed=False) result_parts = result.split('\n') pathstr_resolved = result_parts[1] pathstr_resolved = pathstr_resolved[1:-1] # remove quotes @@ -407,7 +407,7 @@ def size(self) -> int: if not self.is_file(): raise OMCSessionException(f"Path {self.as_posix()} is not a file!") - res = self._session.sendExpression(f'stat("{self.as_posix()}")') + res = self._session.sendExpression(expr=f'stat("{self.as_posix()}")') if res[0]: return int(res[1]) @@ -582,7 +582,7 @@ def sendExpression(self, command: str, parsed: bool = True) -> Any: The complete error handling of the OMC result is done within this method using '"getMessagesStringInternal()'. Caller should only check for OMCSessionException. """ - return self.omc_process.sendExpression(command=command, parsed=parsed) + return self.omc_process.sendExpression(expr=command, parsed=parsed) class PostInitCaller(type): @@ -701,7 +701,7 @@ def __post_init__(self) -> None: def __del__(self): if isinstance(self._omc_zmq, zmq.Socket): try: - self.sendExpression("quit()") + self.sendExpression(expr="quit()") except OMCSessionException: pass finally: @@ -759,7 +759,7 @@ def omcpath_tempdir(self, tempdir_base: Optional[OMCPath] = None) -> OMCPath: if sys.version_info < (3, 12): tempdir_str = tempfile.gettempdir() else: - tempdir_str = self.sendExpression("getTempDirectoryPath()") + tempdir_str = self.sendExpression(expr="getTempDirectoryPath()") tempdir_base = self.omcpath(tempdir_str) tempdir: Optional[OMCPath] = None @@ -823,7 +823,7 @@ def execute(self, command: str): return self.sendExpression(command, parsed=False) - def sendExpression(self, command: str, parsed: bool = True) -> Any: + def sendExpression(self, expr: str, parsed: bool = True) -> Any: """ Send an expression to the OMC server and return the result. @@ -840,12 +840,12 @@ def sendExpression(self, command: str, parsed: bool = True) -> Any: if self._omc_zmq is None: raise OMCSessionException("No OMC running. Please create a new instance of OMCSession!") - logger.debug("sendExpression(%r, parsed=%r)", command, parsed) + logger.debug("sendExpression(expr='%r', parsed=%r)", str(expr), parsed) attempts = 0 while True: try: - self._omc_zmq.send_string(str(command), flags=zmq.NOBLOCK) + self._omc_zmq.send_string(str(expr), flags=zmq.NOBLOCK) break except zmq.error.Again: pass @@ -859,7 +859,7 @@ def sendExpression(self, command: str, parsed: bool = True) -> Any: raise OMCSessionException(f"No connection with OMC (timeout={timeout}). " f"Log-file says: \n{log_content}") time.sleep(timeout / 50.0) - if command == "quit()": + if expr == "quit()": self._omc_zmq.close() self._omc_zmq = None return None @@ -869,13 +869,13 @@ def sendExpression(self, command: str, parsed: bool = True) -> Any: if result.startswith('Error occurred building AST'): raise OMCSessionException(f"OMC error: {result}") - if command == "getErrorString()": + if expr == "getErrorString()": # no error handling if 'getErrorString()' is called if parsed: logger.warning("Result of 'getErrorString()' cannot be parsed!") return result - if command == "getMessagesStringInternal()": + if expr == "getMessagesStringInternal()": # no error handling if 'getMessagesStringInternal()' is called if parsed: logger.warning("Result of 'getMessagesStringInternal()' cannot be parsed!") @@ -929,7 +929,7 @@ def sendExpression(self, command: str, parsed: bool = True) -> Any: log_level = log_raw[0][8] log_id = log_raw[0][9] - msg_short = (f"[OMC log for 'sendExpression({command}, {parsed})']: " + msg_short = (f"[OMC log for 'sendExpression(expr={expr}, parsed={parsed})']: " f"[{log_kind}:{log_level}:{log_id}] {log_message}") # response according to the used log level @@ -951,7 +951,7 @@ def sendExpression(self, command: str, parsed: bool = True) -> Any: msg_long_list.append(msg_long) if has_error: msg_long_str = '\n'.join(f"{idx:02d}: {msg}" for idx, msg in enumerate(msg_long_list)) - raise OMCSessionException(f"OMC error occurred for 'sendExpression({command}, {parsed}):\n" + raise OMCSessionException(f"OMC error occurred for 'sendExpression(expr={expr}, parsed={parsed}):\n" f"{msg_long_str}") if parsed is False: From 4e814877ec548b508122a16493bbcdb6714b7fc6 Mon Sep 17 00:00:00 2001 From: syntron Date: Sat, 24 Jan 2026 14:35:23 +0100 Subject: [PATCH 32/37] [OMCSession] fix log message --- OMPython/OMCSession.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/OMPython/OMCSession.py b/OMPython/OMCSession.py index e1d1f123..38f45b8d 100644 --- a/OMPython/OMCSession.py +++ b/OMPython/OMCSession.py @@ -886,7 +886,7 @@ def sendExpression(self, command: str, parsed: bool = True) -> Any: except OMCSessionException: log_content = 'log not available' - logger.error(f"Docker did not start. Log-file says:\n{log_content}") + logger.error(f"OMC did not start. Log-file says:\n{log_content}") raise OMCSessionException(f"No connection with OMC (timeout={self._timeout}).") if command == "quit()": From 64e4265eb6f7348c1c3707f4b25676cc9f094f61 Mon Sep 17 00:00:00 2001 From: syntron Date: Tue, 17 Jun 2025 08:47:13 +0200 Subject: [PATCH 33/37] [OMCSessionZMQ] remove depreciated function execute() --- OMPython/OMCSession.py | 12 ------------ tests/test_ZMQ.py | 8 +++----- 2 files changed, 3 insertions(+), 17 deletions(-) diff --git a/OMPython/OMCSession.py b/OMPython/OMCSession.py index fd77a5a2..253ed913 100644 --- a/OMPython/OMCSession.py +++ b/OMPython/OMCSession.py @@ -22,7 +22,6 @@ import time from typing import Any, Optional, Tuple import uuid -import warnings import zmq import psutil @@ -563,9 +562,6 @@ def run_model_executable(self, cmd_run_data: OMCSessionRunData) -> int: """ return self.omc_process.run_model_executable(cmd_run_data=cmd_run_data) - def execute(self, command: str): - return self.omc_process.execute(command=command) - def sendExpression(self, command: str, parsed: bool = True) -> Any: """ Send an expression to the OMC server and return the result. @@ -850,14 +846,6 @@ def run_model_executable(self, cmd_run_data: OMCSessionRunData) -> int: return returncode - def execute(self, command: str): - warnings.warn(message="This function is depreciated and will be removed in future versions; " - "please use sendExpression() instead", - category=DeprecationWarning, - stacklevel=2) - - return self.sendExpression(command, parsed=False) - def sendExpression(self, expr: str, parsed: bool = True) -> Any: """ Send an expression to the OMC server and return the result. diff --git a/tests/test_ZMQ.py b/tests/test_ZMQ.py index 1302a79d..dba77dd8 100644 --- a/tests/test_ZMQ.py +++ b/tests/test_ZMQ.py @@ -37,11 +37,9 @@ def test_Simulate(omcs, model_time_str): assert omcs.sendExpression('res.resultFile') -def test_execute(omcs): - with pytest.deprecated_call(): - assert omcs.execute('"HelloWorld!"') == '"HelloWorld!"\n' - assert omcs.sendExpression('"HelloWorld!"', parsed=False) == '"HelloWorld!"\n' - assert omcs.sendExpression('"HelloWorld!"', parsed=True) == 'HelloWorld!' +def test_sendExpression(om): + assert om.sendExpression('"HelloWorld!"', parsed=False) == '"HelloWorld!"\n' + assert om.sendExpression('"HelloWorld!"', parsed=True) == 'HelloWorld!' def test_omcprocessport_execute(omcs): From aa1692cd38dad0ac86da3de8053669c14a696776 Mon Sep 17 00:00:00 2001 From: syntron Date: Tue, 17 Jun 2025 08:48:59 +0200 Subject: [PATCH 34/37] [ModelicaSystemCmd] remove depreciated simflags --- OMPython/ModelicaSystem.py | 66 +-------------------------------- tests/test_ModelicaSystemCmd.py | 2 - 2 files changed, 2 insertions(+), 66 deletions(-) diff --git a/OMPython/ModelicaSystem.py b/OMPython/ModelicaSystem.py index 04bb1050..60e9ecee 100644 --- a/OMPython/ModelicaSystem.py +++ b/OMPython/ModelicaSystem.py @@ -255,46 +255,6 @@ def definition(self) -> OMCSessionRunData: return omc_run_data_updated - @staticmethod - def parse_simflags(simflags: str) -> dict[str, Optional[str | dict[str, Any] | numbers.Number]]: - """ - Parse a simflag definition; this is deprecated! - - The return data can be used as input for self.args_set(). - """ - warnings.warn(message="The argument 'simflags' is depreciated and will be removed in future versions; " - "please use 'simargs' instead", - category=DeprecationWarning, - stacklevel=2) - - simargs: dict[str, Optional[str | dict[str, Any] | numbers.Number]] = {} - - 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 - elif parts[0] == 'override': - override = '='.join(parts[1:]) - - override_dict = {} - for item in override.split(','): - kv = item.split('=') - if not 0 < len(kv) < 3: - raise ModelicaSystemError(f"Invalid value for '-override': {override}") - if kv[0]: - try: - override_dict[kv[0]] = kv[1] - except (KeyError, IndexError) as ex: - raise ModelicaSystemError(f"Invalid value for '-override': {override}") from ex - - simargs[parts[0]] = override_dict - - return simargs - class ModelicaSystem: """ @@ -1067,7 +1027,6 @@ def _process_override_data( def simulate_cmd( self, result_file: OMCPath, - simflags: Optional[str] = None, simargs: Optional[dict[str, Optional[str | dict[str, Any] | numbers.Number]]] = None, ) -> ModelicaSystemCmd: """ @@ -1080,12 +1039,6 @@ def simulate_cmd( However, if only non-structural parameters are used, it is possible to reuse an existing instance of ModelicaSystem to create several version ModelicaSystemCmd to run the model using different settings. - Parameters - ---------- - result_file - simflags - simargs - Returns ------- An instance if ModelicaSystemCmd to run the requested simulation. @@ -1100,11 +1053,7 @@ def simulate_cmd( # always define the result file to use om_cmd.arg_set(key="r", val=result_file.as_posix()) - # allow runtime simulation flags from user input - if simflags is not None: - om_cmd.args_set(args=om_cmd.parse_simflags(simflags=simflags)) - - if simargs: + if simargs is not None: om_cmd.args_set(args=simargs) self._process_override_data( @@ -1137,7 +1086,6 @@ def simulate_cmd( def simulate( self, resultfile: Optional[str | os.PathLike] = None, - simflags: Optional[str] = None, simargs: Optional[dict[str, Optional[str | dict[str, Any] | numbers.Number]]] = None, ) -> None: """Simulate the model according to simulation options. @@ -1146,8 +1094,6 @@ def simulate( Args: resultfile: Path to a custom result file - simflags: String of extra command line flags for the model binary. - This argument is deprecated, use simargs instead. simargs: Dict with simulation runtime flags. Examples: @@ -1174,7 +1120,6 @@ def simulate( om_cmd = self.simulate_cmd( result_file=self._result_file, - simflags=simflags, simargs=simargs, ) @@ -1757,7 +1702,6 @@ def optimize(self) -> dict[str, Any]: def linearize( self, lintime: Optional[float] = None, - simflags: Optional[str] = None, simargs: Optional[dict[str, Optional[str | dict[str, Any] | numbers.Number]]] = None, ) -> LinearizationResult: """Linearize the model according to linearization options. @@ -1766,8 +1710,6 @@ def linearize( Args: lintime: Override "stopTime" value. - simflags: String of extra command line flags for the model binary. - This argument is deprecated, use simargs instead. simargs: A dict with command line flags and possible options; example: "simargs={'csvInput': 'a.csv'}" Returns: @@ -1817,11 +1759,7 @@ def linearize( f"<= lintime <= {self._linearization_options['stopTime']}") om_cmd.arg_set(key="l", val=str(lintime)) - # allow runtime simulation flags from user input - if simflags is not None: - om_cmd.args_set(args=om_cmd.parse_simflags(simflags=simflags)) - - if simargs: + if simargs is not None: om_cmd.args_set(args=simargs) # the file create by the model executable which contains the matrix and linear inputs, outputs and states diff --git a/tests/test_ModelicaSystemCmd.py b/tests/test_ModelicaSystemCmd.py index 2480aad9..bf0d22a4 100644 --- a/tests/test_ModelicaSystemCmd.py +++ b/tests/test_ModelicaSystemCmd.py @@ -38,8 +38,6 @@ def test_simflags(mscmd_firstorder): "noEventEmit": None, "override": {'b': 2} }) - with pytest.deprecated_call(): - mscmd.args_set(args=mscmd.parse_simflags(simflags="-noEventEmit -noRestart -override=a=1,x=3")) assert mscmd.get_cmd_args() == [ '-noEventEmit', From d491caa8d6fc39e4ee4e381ea97735a2e826cd4f Mon Sep 17 00:00:00 2001 From: syntron Date: Fri, 31 Oct 2025 17:00:49 +0100 Subject: [PATCH 35/37] [test_ModelicaSystemCmd] update test_simflags --- tests/test_ModelicaSystemCmd.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/tests/test_ModelicaSystemCmd.py b/tests/test_ModelicaSystemCmd.py index bf0d22a4..341fce42 100644 --- a/tests/test_ModelicaSystemCmd.py +++ b/tests/test_ModelicaSystemCmd.py @@ -36,13 +36,12 @@ def test_simflags(mscmd_firstorder): mscmd.args_set({ "noEventEmit": None, - "override": {'b': 2} + "override": {'b': 2, 'a': 4}, }) assert mscmd.get_cmd_args() == [ '-noEventEmit', - '-noRestart', - '-override=a=1,b=2,x=3', + '-override=a=4,b=2', ] mscmd.args_set({ @@ -51,6 +50,5 @@ def test_simflags(mscmd_firstorder): assert mscmd.get_cmd_args() == [ '-noEventEmit', - '-noRestart', - '-override=a=1,x=3', + '-override=a=4', ] From cec873cedf98231f806c31b1cfa400015aca5de8 Mon Sep 17 00:00:00 2001 From: syntron Date: Sat, 29 Nov 2025 23:22:39 +0100 Subject: [PATCH 36/37] fix test_ZMQ --- tests/test_ZMQ.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/test_ZMQ.py b/tests/test_ZMQ.py index dba77dd8..f4ebf105 100644 --- a/tests/test_ZMQ.py +++ b/tests/test_ZMQ.py @@ -37,9 +37,9 @@ def test_Simulate(omcs, model_time_str): assert omcs.sendExpression('res.resultFile') -def test_sendExpression(om): - assert om.sendExpression('"HelloWorld!"', parsed=False) == '"HelloWorld!"\n' - assert om.sendExpression('"HelloWorld!"', parsed=True) == 'HelloWorld!' +def test_sendExpression(omcs): + assert omcs.sendExpression('"HelloWorld!"', parsed=False) == '"HelloWorld!"\n' + assert omcs.sendExpression('"HelloWorld!"', parsed=True) == 'HelloWorld!' def test_omcprocessport_execute(omcs): From 035a549be27aba916ecc9bcda1a1017f52054ae7 Mon Sep 17 00:00:00 2001 From: syntron Date: Sun, 30 Nov 2025 12:01:13 +0100 Subject: [PATCH 37/37] keep 'import warnings' --- OMPython/OMCSession.py | 1 + 1 file changed, 1 insertion(+) diff --git a/OMPython/OMCSession.py b/OMPython/OMCSession.py index 253ed913..1e905010 100644 --- a/OMPython/OMCSession.py +++ b/OMPython/OMCSession.py @@ -22,6 +22,7 @@ import time from typing import Any, Optional, Tuple import uuid +import warnings import zmq import psutil