From c12c2774ee9b2e09ccbcb579bd50f7405e58ebab Mon Sep 17 00:00:00 2001 From: syntron Date: Sat, 28 Jun 2025 20:44:15 +0200 Subject: [PATCH 01/67] [OMCPath] add class --- OMPython/OMCSession.py | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/OMPython/OMCSession.py b/OMPython/OMCSession.py index ac99dc05..41a96a67 100644 --- a/OMPython/OMCSession.py +++ b/OMPython/OMCSession.py @@ -268,6 +268,31 @@ def getClassNames(self, className=None, recursive=False, qualified=False, sort=F return self._ask(question='getClassNames', opt=opt) +class OMCPath(pathlib.PurePosixPath): + """ + Implementation of a basic Path object which uses OMC as backend. The connection to OMC is provided via a + OMCSessionZMQ session object. + """ + + def __init__(self, *path, session: OMCSessionZMQ): + super().__init__(*path) + self._session = session + + def with_segments(self, *pathsegments): + # overwrite this function of PurePosixPath to ensure session is set + return type(self)(*pathsegments, session=self._session) + + # TODO: implement needed methods from pathlib._abc.PathBase: + # is_dir() + # is_file() + # read_text() + binary()? + # write_text() + binary()? + # unlink() + # resolve() + # ... more ... + # ??? test if local (write OMC => READ local and the other way) and use shortcuts ??? + + class OMCSessionZMQ: def __init__( @@ -322,6 +347,9 @@ def __del__(self): self.omc_zmq = None + def omcpath(self, *path) -> OMCPath: + return OMCPath(*path, session=self) + 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) From 908a239db424e2ddcf41409cc1db6a276c380a01 Mon Sep 17 00:00:00 2001 From: syntron Date: Fri, 27 Jun 2025 22:53:08 +0200 Subject: [PATCH 02/67] [OMCPath] add implementation using OMC via sendExpression() --- OMPython/OMCSession.py | 25 ++++++++++++++++++++----- 1 file changed, 20 insertions(+), 5 deletions(-) diff --git a/OMPython/OMCSession.py b/OMPython/OMCSession.py index 41a96a67..fb865ba4 100644 --- a/OMPython/OMCSession.py +++ b/OMPython/OMCSession.py @@ -282,12 +282,27 @@ def with_segments(self, *pathsegments): # overwrite this function of PurePosixPath to ensure session is set return type(self)(*pathsegments, session=self._session) + def is_file(self) -> bool: + return self._session.sendExpression(f'regularFileExists("{self.as_posix()}")') + + def is_dir(self) -> bool: + return self._session.sendExpression(f'directoryExists("{self.as_posix()}")') + + def read_text(self) -> str: + return self._session.sendExpression(f'readFile("{self.as_posix()}")') + + def write_text(self, data: str) -> bool: + return self._session.sendExpression(f'writeFile("{self.as_posix()}", "{data}", false)') + + def unlink(self) -> bool: + return self._session.sendExpression(f'deleteFile("{self.as_posix()}")') + # TODO: implement needed methods from pathlib._abc.PathBase: - # is_dir() - # is_file() - # read_text() + binary()? - # write_text() + binary()? - # unlink() + # OK - is_dir() + # OK - is_file() + # OK - read_text() + binary()? + # OK - write_text() + binary()? + # OK - unlink() # resolve() # ... more ... # ??? test if local (write OMC => READ local and the other way) and use shortcuts ??? From 00d016f8227425a88692baefb615b6029239f64b Mon Sep 17 00:00:00 2001 From: syntron Date: Sat, 28 Jun 2025 20:47:05 +0200 Subject: [PATCH 03/67] [OMCPath] add pytest (only docker at the moment) --- tests/test_OMCPath.py | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) create mode 100644 tests/test_OMCPath.py diff --git a/tests/test_OMCPath.py b/tests/test_OMCPath.py new file mode 100644 index 00000000..106f1cc7 --- /dev/null +++ b/tests/test_OMCPath.py @@ -0,0 +1,26 @@ +import OMPython + + +def test_OMCPath_docker(): + omcp = OMPython.OMCProcessDocker(docker="openmodelica/openmodelica:v1.25.0-minimal") + om = OMPython.OMCSessionZMQ(omc_process=omcp) + assert om.sendExpression("getVersion()") == "OpenModelica 1.25.0" + + p1 = om.omcpath('/tmp') + assert str(p1) == "/tmp" + p2 = p1 / 'test.txt' + assert str(p2) == "/tmp/test.txt" + assert p2.write_text('test') + assert p2.read_text() == "test" + assert p2.is_file() + assert p2.parent.is_dir() + assert p2.unlink() + assert p2.is_file() == False + + del omcp + del om + + +if __name__ == '__main__': + test_OMCPath_docker() + print('DONE') \ No newline at end of file From a449445a6112d2253b75a7b18bbbeb10379b5bfc Mon Sep 17 00:00:00 2001 From: syntron Date: Sat, 28 Jun 2025 20:47:15 +0200 Subject: [PATCH 04/67] [OMCPath] TODO items --- OMPython/OMCSession.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/OMPython/OMCSession.py b/OMPython/OMCSession.py index fb865ba4..a03b7f57 100644 --- a/OMPython/OMCSession.py +++ b/OMPython/OMCSession.py @@ -274,6 +274,10 @@ class OMCPath(pathlib.PurePosixPath): OMCSessionZMQ session object. """ + # TODO: need to handle PurePosixPath and PureWindowsPath + # PureOMCPath => OMCPathPosix(PureOMCPath, PurePosixPath) + # => OMCPathWindows(PureOMCPath, PureWindowsPath) + def __init__(self, *path, session: OMCSessionZMQ): super().__init__(*path) self._session = session @@ -363,6 +367,8 @@ def __del__(self): self.omc_zmq = None def omcpath(self, *path) -> OMCPath: + # TODO: need to handle PurePosixPath and PureWindowsPath + # define it here based on the backend (omc_process) used? return OMCPath(*path, session=self) def execute(self, command: str): From 17c9ee80cfec9ee0c9fb6dc01867a7dda9725859 Mon Sep 17 00:00:00 2001 From: syntron Date: Wed, 2 Jul 2025 22:34:46 +0200 Subject: [PATCH 05/67] [test_OMCPath] mypy fix --- tests/test_OMCPath.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_OMCPath.py b/tests/test_OMCPath.py index 106f1cc7..bae41714 100644 --- a/tests/test_OMCPath.py +++ b/tests/test_OMCPath.py @@ -15,7 +15,7 @@ def test_OMCPath_docker(): assert p2.is_file() assert p2.parent.is_dir() assert p2.unlink() - assert p2.is_file() == False + assert p2.is_file() is False del omcp del om From 942635e00ef5ad888086ddacdd82a435d3c4895d Mon Sep 17 00:00:00 2001 From: syntron Date: Wed, 2 Jul 2025 22:52:38 +0200 Subject: [PATCH 06/67] [test_OMCPath] fix end of file --- tests/test_OMCPath.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_OMCPath.py b/tests/test_OMCPath.py index bae41714..c0249df6 100644 --- a/tests/test_OMCPath.py +++ b/tests/test_OMCPath.py @@ -23,4 +23,4 @@ def test_OMCPath_docker(): if __name__ == '__main__': test_OMCPath_docker() - print('DONE') \ No newline at end of file + print('DONE') From 11b252403ce92f44350d2a19f71373935180617d Mon Sep 17 00:00:00 2001 From: syntron Date: Thu, 3 Jul 2025 09:21:35 +0200 Subject: [PATCH 07/67] [test_OMCPath] define test using OMCSessionZMQ() locally --- tests/test_OMCPath.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/tests/test_OMCPath.py b/tests/test_OMCPath.py index c0249df6..9621b54b 100644 --- a/tests/test_OMCPath.py +++ b/tests/test_OMCPath.py @@ -1,6 +1,8 @@ import OMPython +import pytest +@pytest.mark.skip(reason="This test would fail (no docker on github)") def test_OMCPath_docker(): omcp = OMPython.OMCProcessDocker(docker="openmodelica/openmodelica:v1.25.0-minimal") om = OMPython.OMCSessionZMQ(omc_process=omcp) @@ -21,6 +23,23 @@ def test_OMCPath_docker(): del om +def test_OMCPath_local(): + om = OMPython.OMCSessionZMQ() + + p1 = om.omcpath('/tmp') + assert str(p1) == "/tmp" + p2 = p1 / 'test.txt' + assert str(p2) == "/tmp/test.txt" + assert p2.write_text('test') + assert p2.read_text() == "test" + assert p2.is_file() + assert p2.parent.is_dir() + assert p2.unlink() + assert p2.is_file() is False + + del om + + if __name__ == '__main__': test_OMCPath_docker() print('DONE') From 05b3c4d284ef8fe083bac47697e14ed6bfce5947 Mon Sep 17 00:00:00 2001 From: syntron Date: Sun, 6 Jul 2025 21:54:01 +0200 Subject: [PATCH 08/67] add TODO - need to check Python versions * not working: 3.10 * working: 3.12 --- OMPython/OMCSession.py | 1 + 1 file changed, 1 insertion(+) diff --git a/OMPython/OMCSession.py b/OMPython/OMCSession.py index a03b7f57..f0505383 100644 --- a/OMPython/OMCSession.py +++ b/OMPython/OMCSession.py @@ -277,6 +277,7 @@ class OMCPath(pathlib.PurePosixPath): # TODO: need to handle PurePosixPath and PureWindowsPath # PureOMCPath => OMCPathPosix(PureOMCPath, PurePosixPath) # => OMCPathWindows(PureOMCPath, PureWindowsPath) + # TODO: only working for Python 3.12+ (not working for 3.10!; 3.11?) def __init__(self, *path, session: OMCSessionZMQ): super().__init__(*path) From 79e118ac90167c0a96ee2a471d27502316d27248 Mon Sep 17 00:00:00 2001 From: syntron Date: Sun, 6 Jul 2025 21:54:24 +0200 Subject: [PATCH 09/67] [test_OMCPath] activate docker based on test_docker --- tests/test_OMCPath.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/tests/test_OMCPath.py b/tests/test_OMCPath.py index 9621b54b..bef2b473 100644 --- a/tests/test_OMCPath.py +++ b/tests/test_OMCPath.py @@ -1,8 +1,14 @@ +import sys import OMPython import pytest +skip_on_windows = pytest.mark.skipif( + sys.platform.startswith("win"), + reason="OpenModelica Docker image is Linux-only; skipping on Windows.", +) -@pytest.mark.skip(reason="This test would fail (no docker on github)") + +@skip_on_windows def test_OMCPath_docker(): omcp = OMPython.OMCProcessDocker(docker="openmodelica/openmodelica:v1.25.0-minimal") om = OMPython.OMCSessionZMQ(omc_process=omcp) From e0cb45393fc95f27e91b063d76e672d1782fe1cd Mon Sep 17 00:00:00 2001 From: syntron Date: Fri, 11 Jul 2025 21:18:56 +0200 Subject: [PATCH 10/67] [OMCPath] add more functionality and docstrings --- OMPython/OMCSession.py | 125 +++++++++++++++++++++++++++++++++++++++-- 1 file changed, 120 insertions(+), 5 deletions(-) diff --git a/OMPython/OMCSession.py b/OMPython/OMCSession.py index f0505383..d8a17af7 100644 --- a/OMPython/OMCSession.py +++ b/OMPython/OMCSession.py @@ -284,23 +284,138 @@ def __init__(self, *path, session: OMCSessionZMQ): self._session = session def with_segments(self, *pathsegments): - # overwrite this function of PurePosixPath to ensure session is set + """ + Create a new OMCPath object with the given path segments. + + The original definition of Path is overridden to ensure session is set. + """ return type(self)(*pathsegments, session=self._session) def is_file(self) -> bool: + """ + Check if the path is a regular file. + """ return self._session.sendExpression(f'regularFileExists("{self.as_posix()}")') def is_dir(self) -> bool: + """ + Check if the path is a directory. + """ return self._session.sendExpression(f'directoryExists("{self.as_posix()}")') - def read_text(self) -> str: + def read_text(self, encoding=None, errors=None) -> str: + """ + Read the content of the file represented by this path as text. + + The additional arguments `encoding` and `errors` are only defined for compatibility with Path() definitions. + """ return self._session.sendExpression(f'readFile("{self.as_posix()}")') - def write_text(self, data: str) -> bool: + def write_text(self, data: str, encoding=None, errors=None, newline=None) -> bool: + """ + Write text data to the file represented by this path. + + The additional arguments `encoding`, `errors`, and `newline` are only defined for compatibility with Path() + definitions. + """ + if not isinstance(data, str): + raise TypeError('data must be str, not %s' % + data.__class__.__name__) + return self._session.sendExpression(f'writeFile("{self.as_posix()}", "{data}", false)') - def unlink(self) -> bool: - return self._session.sendExpression(f'deleteFile("{self.as_posix()}")') + def mkdir(self, mode=0o777, parents=False, exist_ok=False): + """ + Create a directory at the path represented by this OMCPath object. + + The additional arguments `mode`, and `parents` are only defined for compatibility with Path() definitions. + """ + 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()}")') + + def cwd(self): + """ + Returns the current working directory as an OMCPath object. + """ + cwd_str = self._session.sendExpression('cd()') + return OMCPath(cwd_str, session=self._session) + + def unlink(self, missing_ok: bool = False) -> bool: + """ + Unlink (delete) the file or directory represented by this path. + """ + res = self._session.sendExpression(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!") + return res + + def resolve(self, strict: bool = False) -> OMCPath: + """ + Resolve the path to an absolute path. This is done based on available OMC functions. + """ + if strict and not (self.is_file() or self.is_dir()): + raise OMCSessionException(f"Path {self.as_posix()} does not exist!") + + if self.is_file(): + omcpath = self._omc_resolve(self.parent.as_posix()) / self.name + elif self.is_dir(): + omcpath = self._omc_resolve(self.as_posix()) + else: + raise OMCSessionException(f"Path {self.as_posix()} is neither a file nor a directory!") + + return omcpath + + def _omc_resolve(self, pathstr: str) -> OMCPath: + """ + 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)') + + try: + result = self._session.sendExpression(expression) + result_parts = result.split('\n') + pathstr_resolved = result_parts[1] + pathstr_resolved = pathstr_resolved[1:-1] # remove quotes + + omcpath_resolved = self._session.omcpath(pathstr_resolved) + except OMCSessionException as ex: + raise OMCSessionException(f"OMCPath resolve failed for {pathstr}!") from ex + + if not omcpath_resolved.is_file() and not omcpath_resolved.is_dir(): + raise OMCSessionException(f"OMCPath resolve failed for {pathstr} - path does not exist!") + + return omcpath_resolved + + def absolute(self) -> OMCPath: + """ + Resolve the path to an absolute path. This is done by calling resolve() as it is the best we can do + using OMC functions. + """ + return self.resolve(strict=True) + + def exists(self) -> bool: + """ + Semi replacement for pathlib.Path.exists(). + """ + return self.is_file() or self.is_dir() + + def size(self) -> int: + """ + Get the size of the file in bytes - this is a extra function and the best we can do using OMC. + """ + 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()}")') + if res[0]: + return int(res[1]) + + raise OMCSessionException(f"Error reading file size for path {self.as_posix()}!") # TODO: implement needed methods from pathlib._abc.PathBase: # OK - is_dir() From 2b864420ba2a25675ce5d5981030aa86fe65010c Mon Sep 17 00:00:00 2001 From: syntron Date: Fri, 11 Jul 2025 21:19:51 +0200 Subject: [PATCH 11/67] [OMCPath] remove TODO entries --- OMPython/OMCSession.py | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/OMPython/OMCSession.py b/OMPython/OMCSession.py index d8a17af7..3cd38475 100644 --- a/OMPython/OMCSession.py +++ b/OMPython/OMCSession.py @@ -274,11 +274,6 @@ class OMCPath(pathlib.PurePosixPath): OMCSessionZMQ session object. """ - # TODO: need to handle PurePosixPath and PureWindowsPath - # PureOMCPath => OMCPathPosix(PureOMCPath, PurePosixPath) - # => OMCPathWindows(PureOMCPath, PureWindowsPath) - # TODO: only working for Python 3.12+ (not working for 3.10!; 3.11?) - def __init__(self, *path, session: OMCSessionZMQ): super().__init__(*path) self._session = session @@ -417,15 +412,6 @@ def size(self) -> int: raise OMCSessionException(f"Error reading file size for path {self.as_posix()}!") - # TODO: implement needed methods from pathlib._abc.PathBase: - # OK - is_dir() - # OK - is_file() - # OK - read_text() + binary()? - # OK - write_text() + binary()? - # OK - unlink() - # resolve() - # ... more ... - # ??? test if local (write OMC => READ local and the other way) and use shortcuts ??? class OMCSessionZMQ: From a9fb9f9a703dee3d178b4ae0cebf1248d38e3a18 Mon Sep 17 00:00:00 2001 From: syntron Date: Fri, 11 Jul 2025 21:22:17 +0200 Subject: [PATCH 12/67] [OMCPath] define limited compatibility for Python < 3.12 * use modified pathlib.Path as OMCPath --- OMPython/OMCSession.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/OMPython/OMCSession.py b/OMPython/OMCSession.py index 3cd38475..966709bc 100644 --- a/OMPython/OMCSession.py +++ b/OMPython/OMCSession.py @@ -413,6 +413,15 @@ def size(self) -> int: raise OMCSessionException(f"Error reading file size for path {self.as_posix()}!") +if sys.version_info < (3, 12): + class OMCPathCompatibility(pathlib.Path): + + def size(self) -> int: + return self.stat().st_size + + + OMCPath = OMCPathCompatibility + class OMCSessionZMQ: From fcb8571a6812a9c58d4643730fd6588ef472ec30 Mon Sep 17 00:00:00 2001 From: syntron Date: Fri, 11 Jul 2025 21:23:24 +0200 Subject: [PATCH 13/67] [OMCSEssionZMQ] use OMCpath --- OMPython/OMCSession.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/OMPython/OMCSession.py b/OMPython/OMCSession.py index 966709bc..a6541eae 100644 --- a/OMPython/OMCSession.py +++ b/OMPython/OMCSession.py @@ -478,8 +478,15 @@ def __del__(self): self.omc_zmq = None def omcpath(self, *path) -> OMCPath: - # TODO: need to handle PurePosixPath and PureWindowsPath - # define it here based on the backend (omc_process) used? + """ + Create an OMCPath object based on the given path segments and the current OMC session. + """ + + # fallback solution for Python < 3.12; a modified pathlib.Path object is used as OMCPath replacement + if sys.version_info < (3, 12): + # noinspection PyArgumentList + return OMCPath(*path) + return OMCPath(*path, session=self) def execute(self, command: str): From 51b916091759f899f929fc537f256b990a5c0151 Mon Sep 17 00:00:00 2001 From: syntron Date: Fri, 11 Jul 2025 21:28:31 +0200 Subject: [PATCH 14/67] [OMCSessionZMQ] create a tempdir using omcpath_tempdir() --- OMPython/OMCSession.py | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/OMPython/OMCSession.py b/OMPython/OMCSession.py index a6541eae..d8a73a3b 100644 --- a/OMPython/OMCSession.py +++ b/OMPython/OMCSession.py @@ -489,6 +489,30 @@ def omcpath(self, *path) -> OMCPath: return OMCPath(*path, session=self) + def omcpath_tempdir(self) -> OMCPath: + """ + Get a temporary directory using OMC. + """ + names = [str(uuid.uuid4()) for _ in range(100)] + + tempdir_str = self.sendExpression("getTempDirectoryPath()") + tempdir_base = self.omcpath(tempdir_str) + tempdir: Optional[OMCPath] = None + for name in names: + # create a unique temporary directory name + tempdir = tempdir_base / name + + if tempdir.exists(): + continue + + tempdir.mkdir(parents=True, exist_ok=False) + break + + if tempdir is None or not tempdir.is_dir(): + raise OMCSessionException("Cannot create a temporary directory!") + + return tempdir + 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) From dd9afedf0d0922e4ef64faa75d255ade4eef9453 Mon Sep 17 00:00:00 2001 From: syntron Date: Fri, 11 Jul 2025 21:34:24 +0200 Subject: [PATCH 15/67] [OMCPath] fix mypy --- OMPython/OMCSession.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/OMPython/OMCSession.py b/OMPython/OMCSession.py index d8a73a3b..2250d21d 100644 --- a/OMPython/OMCSession.py +++ b/OMPython/OMCSession.py @@ -419,8 +419,7 @@ class OMCPathCompatibility(pathlib.Path): def size(self) -> int: return self.stat().st_size - - OMCPath = OMCPathCompatibility + OMCPath = OMCPathCompatibility # noqa: F811 class OMCSessionZMQ: From dc0393eafb3f397fec575bbe8f1a14da30f58fdd Mon Sep 17 00:00:00 2001 From: syntron Date: Fri, 11 Jul 2025 21:38:27 +0200 Subject: [PATCH 16/67] [OMCPath] add warning message for Python < 3.12 --- OMPython/OMCSession.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/OMPython/OMCSession.py b/OMPython/OMCSession.py index 2250d21d..8b3a3b28 100644 --- a/OMPython/OMCSession.py +++ b/OMPython/OMCSession.py @@ -414,6 +414,12 @@ def size(self) -> int: if sys.version_info < (3, 12): + warnings.warn( + message="Python < 3.12 - using a limited compatibility class as OMCPath replacement.", + category=DeprecationWarning, + stacklevel=1, + ) + class OMCPathCompatibility(pathlib.Path): def size(self) -> int: From 6ff54d3a0abfc46ed2bb26e15852a4a0d8a379f9 Mon Sep 17 00:00:00 2001 From: syntron Date: Fri, 11 Jul 2025 22:20:23 +0200 Subject: [PATCH 17/67] [OMCPath] try to make mypy happy ... --- OMPython/OMCSession.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/OMPython/OMCSession.py b/OMPython/OMCSession.py index 8b3a3b28..3107320d 100644 --- a/OMPython/OMCSession.py +++ b/OMPython/OMCSession.py @@ -268,7 +268,7 @@ def getClassNames(self, className=None, recursive=False, qualified=False, sort=F return self._ask(question='getClassNames', opt=opt) -class OMCPath(pathlib.PurePosixPath): +class OMCPathReal(pathlib.PurePosixPath): """ Implementation of a basic Path object which uses OMC as backend. The connection to OMC is provided via a OMCSessionZMQ session object. @@ -420,13 +420,16 @@ def size(self) -> int: stacklevel=1, ) - class OMCPathCompatibility(pathlib.Path): + class OMCPathCompatibility(pathlib.PosixPath): def size(self) -> int: return self.stat().st_size OMCPath = OMCPathCompatibility # noqa: F811 +else: + OMCPath = OMCPathReal + class OMCSessionZMQ: @@ -491,8 +494,8 @@ def omcpath(self, *path) -> OMCPath: if sys.version_info < (3, 12): # noinspection PyArgumentList return OMCPath(*path) - - return OMCPath(*path, session=self) + else: + return OMCPath(*path, session=self) def omcpath_tempdir(self) -> OMCPath: """ From c4323b36c5e3296bf7a35f1a0919d34d8c13c0be Mon Sep 17 00:00:00 2001 From: syntron Date: Fri, 11 Jul 2025 21:17:00 +0200 Subject: [PATCH 18/67] [test_OMCPath] only for Python >= 3.12 --- tests/test_OMCPath.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/tests/test_OMCPath.py b/tests/test_OMCPath.py index bef2b473..8cc2c88c 100644 --- a/tests/test_OMCPath.py +++ b/tests/test_OMCPath.py @@ -7,8 +7,14 @@ reason="OpenModelica Docker image is Linux-only; skipping on Windows.", ) +skip_python_older_312 = pytest.mark.skipif( + sys.version_info < (3, 12), + reason="OMCPath only working for Python >= 3.12 (definition of pathlib.PurePath).", +) + @skip_on_windows +@skip_python_older_312 def test_OMCPath_docker(): omcp = OMPython.OMCProcessDocker(docker="openmodelica/openmodelica:v1.25.0-minimal") om = OMPython.OMCSessionZMQ(omc_process=omcp) @@ -29,6 +35,7 @@ def test_OMCPath_docker(): del om +@skip_python_older_312 def test_OMCPath_local(): om = OMPython.OMCSessionZMQ() From ec552dd8d3a1498de01fb191743150b9386c8998 Mon Sep 17 00:00:00 2001 From: syntron Date: Fri, 11 Jul 2025 21:17:35 +0200 Subject: [PATCH 19/67] [test_OMCPath] update test --- tests/test_OMCPath.py | 30 ++++++++++++++++++++++-------- 1 file changed, 22 insertions(+), 8 deletions(-) diff --git a/tests/test_OMCPath.py b/tests/test_OMCPath.py index 8cc2c88c..67f1316c 100644 --- a/tests/test_OMCPath.py +++ b/tests/test_OMCPath.py @@ -20,11 +20,16 @@ def test_OMCPath_docker(): om = OMPython.OMCSessionZMQ(omc_process=omcp) assert om.sendExpression("getVersion()") == "OpenModelica 1.25.0" - p1 = om.omcpath('/tmp') - assert str(p1) == "/tmp" - p2 = p1 / 'test.txt' - assert str(p2) == "/tmp/test.txt" + tempdir = '/tmp' + + p1 = om.omcpath(tempdir).resolve().absolute() + assert str(p1) == tempdir + p2 = p1 / '..' / p1.name / 'test.txt' + assert p2.is_file() is False assert p2.write_text('test') + assert p2.is_file() + p2 = p2.resolve().absolute() + assert str(p2) == f"{tempdir}/test.txt" assert p2.read_text() == "test" assert p2.is_file() assert p2.parent.is_dir() @@ -39,11 +44,20 @@ def test_OMCPath_docker(): def test_OMCPath_local(): om = OMPython.OMCSessionZMQ() - p1 = om.omcpath('/tmp') - assert str(p1) == "/tmp" - p2 = p1 / 'test.txt' - assert str(p2) == "/tmp/test.txt" + # use different tempdir for Windows and Linux + if sys.platform.startswith("win"): + tempdir = 'C:/temp' + else: + tempdir = '/tmp' + + p1 = om.omcpath(tempdir).resolve().absolute() + assert str(p1) == tempdir + p2 = p1 / '..' / p1.name / 'test.txt' + assert p2.is_file() is False assert p2.write_text('test') + assert p2.is_file() + p2 = p2.resolve().absolute() + assert str(p2) == f"{tempdir}/test.txt" assert p2.read_text() == "test" assert p2.is_file() assert p2.parent.is_dir() From 3b99f2fd181dcd2680acaf1e9859fc93c7e7ef46 Mon Sep 17 00:00:00 2001 From: syntron Date: Sat, 12 Jul 2025 12:53:28 +0200 Subject: [PATCH 20/67] [OMCPath._omc_resolve] use sendExpression() with parsed=False * this is scripting output and, thus, it cannot be parsed --- OMPython/OMCSession.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/OMPython/OMCSession.py b/OMPython/OMCSession.py index 3107320d..a012ae87 100644 --- a/OMPython/OMCSession.py +++ b/OMPython/OMCSession.py @@ -372,7 +372,7 @@ def _omc_resolve(self, pathstr: str) -> OMCPath: 'cd(omcpath_cwd)') try: - result = self._session.sendExpression(expression) + result = self._session.sendExpression(command=expression, parsed=False) result_parts = result.split('\n') pathstr_resolved = result_parts[1] pathstr_resolved = pathstr_resolved[1:-1] # remove quotes From a54b796475f8631f30ab210c3b3a9665f3782b80 Mon Sep 17 00:00:00 2001 From: syntron Date: Sat, 12 Jul 2025 15:42:45 +0200 Subject: [PATCH 21/67] [test_OMCPath] cleanup; use the same code for local OMC and docker based OMC --- tests/test_OMCPath.py | 27 +++++++-------------------- 1 file changed, 7 insertions(+), 20 deletions(-) diff --git a/tests/test_OMCPath.py b/tests/test_OMCPath.py index 67f1316c..8d323ddf 100644 --- a/tests/test_OMCPath.py +++ b/tests/test_OMCPath.py @@ -22,19 +22,7 @@ def test_OMCPath_docker(): tempdir = '/tmp' - p1 = om.omcpath(tempdir).resolve().absolute() - assert str(p1) == tempdir - p2 = p1 / '..' / p1.name / 'test.txt' - assert p2.is_file() is False - assert p2.write_text('test') - assert p2.is_file() - p2 = p2.resolve().absolute() - assert str(p2) == f"{tempdir}/test.txt" - assert p2.read_text() == "test" - assert p2.is_file() - assert p2.parent.is_dir() - assert p2.unlink() - assert p2.is_file() is False + _run_OMCPath_checks(tempdir, om) del omcp del om @@ -50,6 +38,12 @@ def test_OMCPath_local(): else: tempdir = '/tmp' + _run_OMCPath_checks(tempdir, om) + + del om + + +def _run_OMCPath_checks(tempdir: str, om: OMPython.OMCSessionZMQ): p1 = om.omcpath(tempdir).resolve().absolute() assert str(p1) == tempdir p2 = p1 / '..' / p1.name / 'test.txt' @@ -63,10 +57,3 @@ def test_OMCPath_local(): assert p2.parent.is_dir() assert p2.unlink() assert p2.is_file() is False - - del om - - -if __name__ == '__main__': - test_OMCPath_docker() - print('DONE') From 02c40bc64cabba415bdc87cb8e8c18d10c424df6 Mon Sep 17 00:00:00 2001 From: syntron Date: Sat, 12 Jul 2025 15:45:00 +0200 Subject: [PATCH 22/67] [test_OMCPath] define test for WSL --- tests/test_OMCPath.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/tests/test_OMCPath.py b/tests/test_OMCPath.py index 8d323ddf..ea87f53c 100644 --- a/tests/test_OMCPath.py +++ b/tests/test_OMCPath.py @@ -43,6 +43,23 @@ def test_OMCPath_local(): del om +@pytest.mark.skip(reason="Not able to run WSL on github") +def test_OMCPath_WSL(): + omcp = OMPython.OMCProcessWSL( + wsl_omc='omc', + wsl_user='omc', + timeout=30.0, + ) + om = OMPython.OMCSessionZMQ(omc_process=omcp) + + tempdir = '/tmp' + + _run_OMCPath_checks(tempdir, om) + + del omcp + del om + + def _run_OMCPath_checks(tempdir: str, om: OMPython.OMCSessionZMQ): p1 = om.omcpath(tempdir).resolve().absolute() assert str(p1) == tempdir From 4feccf2c64e724823873a5e7fa693117b5a84fc8 Mon Sep 17 00:00:00 2001 From: syntron Date: Sat, 12 Jul 2025 15:57:02 +0200 Subject: [PATCH 23/67] [test_OMCPath] use omcpath_tempdir() instead of hard-coded tempdir definition --- tests/test_OMCPath.py | 23 ++++++----------------- 1 file changed, 6 insertions(+), 17 deletions(-) diff --git a/tests/test_OMCPath.py b/tests/test_OMCPath.py index ea87f53c..45689622 100644 --- a/tests/test_OMCPath.py +++ b/tests/test_OMCPath.py @@ -20,9 +20,7 @@ def test_OMCPath_docker(): om = OMPython.OMCSessionZMQ(omc_process=omcp) assert om.sendExpression("getVersion()") == "OpenModelica 1.25.0" - tempdir = '/tmp' - - _run_OMCPath_checks(tempdir, om) + _run_OMCPath_checks(om) del omcp del om @@ -32,13 +30,7 @@ def test_OMCPath_docker(): def test_OMCPath_local(): om = OMPython.OMCSessionZMQ() - # use different tempdir for Windows and Linux - if sys.platform.startswith("win"): - tempdir = 'C:/temp' - else: - tempdir = '/tmp' - - _run_OMCPath_checks(tempdir, om) + _run_OMCPath_checks(om) del om @@ -52,23 +44,20 @@ def test_OMCPath_WSL(): ) om = OMPython.OMCSessionZMQ(omc_process=omcp) - tempdir = '/tmp' - - _run_OMCPath_checks(tempdir, om) + _run_OMCPath_checks(om) del omcp del om -def _run_OMCPath_checks(tempdir: str, om: OMPython.OMCSessionZMQ): - p1 = om.omcpath(tempdir).resolve().absolute() - assert str(p1) == tempdir +def _run_OMCPath_checks(om: OMPython.OMCSessionZMQ): + p1 = om.omcpath_tempdir() p2 = p1 / '..' / p1.name / 'test.txt' assert p2.is_file() is False assert p2.write_text('test') assert p2.is_file() p2 = p2.resolve().absolute() - assert str(p2) == f"{tempdir}/test.txt" + assert str(p2) == f"{str(p1)}/test.txt" assert p2.read_text() == "test" assert p2.is_file() assert p2.parent.is_dir() From 362580737387494d1b0b4b3d5569a968c810e1a1 Mon Sep 17 00:00:00 2001 From: syntron Date: Tue, 15 Jul 2025 21:46:39 +0200 Subject: [PATCH 24/67] [OMCPath] spelling fix see commit ID: aa74b367f0fa35b81905d646bbf1beefd3a89595 - [OMCPath] add more functionality and docstrings --- OMPython/OMCSession.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/OMPython/OMCSession.py b/OMPython/OMCSession.py index a012ae87..afb2b8cd 100644 --- a/OMPython/OMCSession.py +++ b/OMPython/OMCSession.py @@ -401,7 +401,7 @@ def exists(self) -> bool: def size(self) -> int: """ - Get the size of the file in bytes - this is a extra function and the best we can do using OMC. + Get the size of the file in bytes - this is an extra function and the best we can do using OMC. """ if not self.is_file(): raise OMCSessionException(f"Path {self.as_posix()} is not a file!") From 930cff1426b0cce5a16cb6bc2a42a08ac4fff7bf Mon Sep 17 00:00:00 2001 From: syntron Date: Wed, 16 Jul 2025 21:08:03 +0200 Subject: [PATCH 25/67] [OMCPath] implementation version 3 * differentiate between * Python >= 3.12 uses OMCPath based on OMC for filesystem operation * Python < 3.12 uses a pathlib.Path based implementation which is limited to OMCProcessLocal --- OMPython/OMCSession.py | 81 ++++++++++++++++++++++++++++++------------ 1 file changed, 58 insertions(+), 23 deletions(-) diff --git a/OMPython/OMCSession.py b/OMPython/OMCSession.py index afb2b8cd..9be3dbf9 100644 --- a/OMPython/OMCSession.py +++ b/OMPython/OMCSession.py @@ -274,7 +274,7 @@ class OMCPathReal(pathlib.PurePosixPath): OMCSessionZMQ session object. """ - def __init__(self, *path, session: OMCSessionZMQ): + def __init__(self, *path, session: OMCSessionZMQ) -> None: super().__init__(*path) self._session = session @@ -286,27 +286,28 @@ def with_segments(self, *pathsegments): """ return type(self)(*pathsegments, session=self._session) - def is_file(self) -> bool: + 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()}")') - def is_dir(self) -> bool: + def is_dir(self, *, follow_symlinks=True) -> bool: """ Check if the path is a directory. """ return self._session.sendExpression(f'directoryExists("{self.as_posix()}")') - def read_text(self, encoding=None, errors=None) -> str: + def read_text(self, encoding=None, errors=None, newline=None) -> str: """ Read the content of the file represented by this path as text. - The additional arguments `encoding` and `errors` are only defined for compatibility with Path() definitions. + The additional arguments `encoding`, `errors` and `newline` are only defined for compatibility with Path() + definition. """ return self._session.sendExpression(f'readFile("{self.as_posix()}")') - def write_text(self, data: str, encoding=None, errors=None, newline=None) -> bool: + def write_text(self, data: str, encoding=None, errors=None, newline=None): """ Write text data to the file represented by this path. @@ -337,16 +338,15 @@ def cwd(self): cwd_str = self._session.sendExpression('cd()') return OMCPath(cwd_str, session=self._session) - def unlink(self, missing_ok: bool = False) -> bool: + 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()}")') if not res and not missing_ok: raise FileNotFoundError(f"Cannot delete file {self.as_posix()} - it does not exists!") - return res - def resolve(self, strict: bool = False) -> OMCPath: + def resolve(self, strict: bool = False): """ Resolve the path to an absolute path. This is done based on available OMC functions. """ @@ -362,7 +362,7 @@ def resolve(self, strict: bool = False) -> OMCPath: return omcpath - def _omc_resolve(self, pathstr: str) -> OMCPath: + def _omc_resolve(self, pathstr: str): """ Internal function to resolve the path of the OMCPath object using OMC functions *WITHOUT* changing the cwd within OMC. @@ -386,7 +386,7 @@ def _omc_resolve(self, pathstr: str) -> OMCPath: return omcpath_resolved - def absolute(self) -> OMCPath: + def absolute(self): """ Resolve the path to an absolute path. This is done by calling resolve() as it is the best we can do using OMC functions. @@ -414,18 +414,42 @@ def size(self) -> int: if sys.version_info < (3, 12): - warnings.warn( - message="Python < 3.12 - using a limited compatibility class as OMCPath replacement.", - category=DeprecationWarning, - stacklevel=1, - ) - class OMCPathCompatibility(pathlib.PosixPath): + class OMCPathCompatibility: + """ + Compatibility class for OMCPath in Python < 3.12. This allows to run all code which uses OMCPath (mainly + ModelicaSystem) on these Python versions. There is one remaining limitation: only OMCProcessLocal will work as + OMCPathCompatibility is based on the standard pathlib.Path implementation. + """ + + # modified copy of pathlib.Path.__new__() definition + def __new__(cls, *args, **kwargs): + logger.warning("Python < 3.12 - using a limited version of class OMCPath.") + + if cls is OMCPathCompatibility: + cls = OMCPathCompatibilityWindows if os.name == 'nt' else OMCPathCompatibilityPosix + self = cls._from_parts(args) + if not self._flavour.is_supported: + raise NotImplementedError("cannot instantiate %r on your system" + % (cls.__name__,)) + return self def size(self) -> int: + """ + Needed compatibility function to have the same interface as OMCPathReal + """ return self.stat().st_size - OMCPath = OMCPathCompatibility # noqa: F811 + + class OMCPathCompatibilityPosix(pathlib.PosixPath, OMCPathCompatibility): + pass + + + class OMCPathCompatibilityWindows(pathlib.WindowsPath, OMCPathCompatibility): + pass + + + OMCPath = OMCPathCompatibility else: OMCPath = OMCPathReal @@ -492,19 +516,30 @@ def omcpath(self, *path) -> OMCPath: # fallback solution for Python < 3.12; a modified pathlib.Path object is used as OMCPath replacement if sys.version_info < (3, 12): - # noinspection PyArgumentList - return OMCPath(*path) + if isinstance(self.omc_process, OMCProcessLocal): + # noinspection PyArgumentList + return OMCPath(*path) + else: + raise OMCSessionException("OMCPath is supported for Python < 3.12 only if OMCProcessLocal is used!") else: return OMCPath(*path, session=self) - def omcpath_tempdir(self) -> OMCPath: + def omcpath_tempdir(self, tempdir_base: Optional[OMCPath] = None) -> OMCPath: """ Get a temporary directory using OMC. """ + # fallback solution for Python < 3.12; a modified pathlib.Path object is used as OMCPath replacement + if sys.version_info < (3, 12): + tempdir_str = tempfile.gettempdir() + # noinspection PyArgumentList + return OMCPath(tempdir_str) + names = [str(uuid.uuid4()) for _ in range(100)] - tempdir_str = self.sendExpression("getTempDirectoryPath()") - tempdir_base = self.omcpath(tempdir_str) + if tempdir_base is None: + tempdir_str = self.sendExpression("getTempDirectoryPath()") + tempdir_base = self.omcpath(tempdir_str) + tempdir: Optional[OMCPath] = None for name in names: # create a unique temporary directory name From 1de0e3b4be5a5c20aa5d4d0e23c5c449f7115aef Mon Sep 17 00:00:00 2001 From: syntron Date: Wed, 16 Jul 2025 23:20:37 +0200 Subject: [PATCH 26/67] [OMCSession*] fix flake8 (PyCharm likes the empty lines) --- OMPython/OMCSession.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/OMPython/OMCSession.py b/OMPython/OMCSession.py index 9be3dbf9..ca82abb5 100644 --- a/OMPython/OMCSession.py +++ b/OMPython/OMCSession.py @@ -440,15 +440,12 @@ def size(self) -> int: """ return self.stat().st_size - class OMCPathCompatibilityPosix(pathlib.PosixPath, OMCPathCompatibility): pass - class OMCPathCompatibilityWindows(pathlib.WindowsPath, OMCPathCompatibility): pass - OMCPath = OMCPathCompatibility else: From 8f38def82f2d4590bc8fdf47f8395946d6ffc33f Mon Sep 17 00:00:00 2001 From: syntron Date: Wed, 16 Jul 2025 23:28:54 +0200 Subject: [PATCH 27/67] [OMCSessionZMQ] more generic definiton for omcpath_tempdir() --- OMPython/OMCSession.py | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/OMPython/OMCSession.py b/OMPython/OMCSession.py index ca82abb5..4c10ecbd 100644 --- a/OMPython/OMCSession.py +++ b/OMPython/OMCSession.py @@ -523,18 +523,17 @@ def omcpath(self, *path) -> OMCPath: def omcpath_tempdir(self, tempdir_base: Optional[OMCPath] = None) -> OMCPath: """ - Get a temporary directory using OMC. + Get a temporary directory using OMC. It is our own implementation as non-local usage relies on OMC to run all + filesystem related access. """ - # fallback solution for Python < 3.12; a modified pathlib.Path object is used as OMCPath replacement - if sys.version_info < (3, 12): - tempdir_str = tempfile.gettempdir() - # noinspection PyArgumentList - return OMCPath(tempdir_str) - names = [str(uuid.uuid4()) for _ in range(100)] if tempdir_base is None: - tempdir_str = self.sendExpression("getTempDirectoryPath()") + # fallback solution for Python < 3.12; a modified pathlib.Path object is used as OMCPath replacement + if sys.version_info < (3, 12): + tempdir_str = tempfile.gettempdir() + else: + tempdir_str = self.sendExpression("getTempDirectoryPath()") tempdir_base = self.omcpath(tempdir_str) tempdir: Optional[OMCPath] = None From 9dc161b1aa3f23b4c17dd0caddd843d89fd7be1b Mon Sep 17 00:00:00 2001 From: syntron Date: Wed, 16 Jul 2025 23:35:44 +0200 Subject: [PATCH 28/67] [OMCPathCompatibility] mypy on github ... --- OMPython/OMCSession.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/OMPython/OMCSession.py b/OMPython/OMCSession.py index 4c10ecbd..744068e3 100644 --- a/OMPython/OMCSession.py +++ b/OMPython/OMCSession.py @@ -415,7 +415,7 @@ def size(self) -> int: if sys.version_info < (3, 12): - class OMCPathCompatibility: + class OMCPathCompatibility(pathlib.Path): """ Compatibility class for OMCPath in Python < 3.12. This allows to run all code which uses OMCPath (mainly ModelicaSystem) on these Python versions. There is one remaining limitation: only OMCProcessLocal will work as From fd906cc023c939237893489f5a5203679629923a Mon Sep 17 00:00:00 2001 From: syntron Date: Wed, 16 Jul 2025 23:47:23 +0200 Subject: [PATCH 29/67] [OMCPathCompatibility] improve log messages --- OMPython/OMCSession.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/OMPython/OMCSession.py b/OMPython/OMCSession.py index 744068e3..1398aa50 100644 --- a/OMPython/OMCSession.py +++ b/OMPython/OMCSession.py @@ -424,7 +424,8 @@ class OMCPathCompatibility(pathlib.Path): # modified copy of pathlib.Path.__new__() definition def __new__(cls, *args, **kwargs): - logger.warning("Python < 3.12 - using a limited version of class OMCPath.") + logger.warning("Python < 3.12 - using a version of class OMCPath " + "based on pathlib.Path for local usage only.") if cls is OMCPathCompatibility: cls = OMCPathCompatibilityWindows if os.name == 'nt' else OMCPathCompatibilityPosix From 90f8c98490797bcb7abe0661138ecb2bf90ff540 Mon Sep 17 00:00:00 2001 From: syntron Date: Wed, 16 Jul 2025 23:18:33 +0200 Subject: [PATCH 30/67] [test_OMCPath] update --- tests/test_OMCPath.py | 57 ++++++++++++++++++++++++++----------------- 1 file changed, 35 insertions(+), 22 deletions(-) diff --git a/tests/test_OMCPath.py b/tests/test_OMCPath.py index 45689622..449108ac 100644 --- a/tests/test_OMCPath.py +++ b/tests/test_OMCPath.py @@ -9,34 +9,43 @@ skip_python_older_312 = pytest.mark.skipif( sys.version_info < (3, 12), - reason="OMCPath only working for Python >= 3.12 (definition of pathlib.PurePath).", + reason="OMCPath(non-local) only working for Python >= 3.12.", ) -@skip_on_windows -@skip_python_older_312 -def test_OMCPath_docker(): - omcp = OMPython.OMCProcessDocker(docker="openmodelica/openmodelica:v1.25.0-minimal") - om = OMPython.OMCSessionZMQ(omc_process=omcp) - assert om.sendExpression("getVersion()") == "OpenModelica 1.25.0" +def test_OMCPath_OMCSessionZMQ(): + om = OMPython.OMCSessionZMQ() + + _run_OMCPath_checks(om) + + del om + + +def test_OMCPath_OMCProcessLocal(): + omp = OMPython.OMCProcessLocal() + om = OMPython.OMCSessionZMQ(omc_process=omp) _run_OMCPath_checks(om) - del omcp del om +@skip_on_windows @skip_python_older_312 -def test_OMCPath_local(): - om = OMPython.OMCSessionZMQ() +def test_OMCPath_OMCProcessDocker(): + omcp = OMPython.OMCProcessDocker(docker="openmodelica/openmodelica:v1.25.0-minimal") + om = OMPython.OMCSessionZMQ(omc_process=omcp) + assert om.sendExpression("getVersion()") == "OpenModelica 1.25.0" _run_OMCPath_checks(om) + del omcp del om @pytest.mark.skip(reason="Not able to run WSL on github") -def test_OMCPath_WSL(): +@skip_python_older_312 +def test_OMCPath_OMCProcessWSL(): omcp = OMPython.OMCProcessWSL( wsl_omc='omc', wsl_user='omc', @@ -52,14 +61,18 @@ def test_OMCPath_WSL(): def _run_OMCPath_checks(om: OMPython.OMCSessionZMQ): p1 = om.omcpath_tempdir() - p2 = p1 / '..' / p1.name / 'test.txt' - assert p2.is_file() is False - assert p2.write_text('test') - assert p2.is_file() - p2 = p2.resolve().absolute() - assert str(p2) == f"{str(p1)}/test.txt" - assert p2.read_text() == "test" - assert p2.is_file() - assert p2.parent.is_dir() - assert p2.unlink() - assert p2.is_file() is False + p2 = p1 / 'test' + p2.mkdir() + assert p2.is_dir() + p3 = p2 / '..' / p2.name / 'test.txt' + assert p3.is_file() is False + assert p3.write_text('test') + assert p3.is_file() + assert p3.size() > 0 + p3 = p3.resolve().absolute() + assert str(p3) == str((p2 / 'test.txt').resolve().absolute()) + assert p3.read_text() == "test" + assert p3.is_file() + assert p3.parent.is_dir() + assert p3.unlink() is None + assert p3.is_file() is False From b2a9190bc8cfb85796f0c95c93eb4da7485fcff2 Mon Sep 17 00:00:00 2001 From: syntron Date: Thu, 24 Jul 2025 20:55:01 +0200 Subject: [PATCH 31/67] [OMCPathReal] align exists() to the definition used in pathlib --- OMPython/OMCSession.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/OMPython/OMCSession.py b/OMPython/OMCSession.py index 1398aa50..d5badb4b 100644 --- a/OMPython/OMCSession.py +++ b/OMPython/OMCSession.py @@ -393,7 +393,7 @@ def absolute(self): """ return self.resolve(strict=True) - def exists(self) -> bool: + def exists(self, follow_symlinks=True) -> bool: """ Semi replacement for pathlib.Path.exists(). """ From b11bfa98761e93119a9448a1343694d4ba27952f Mon Sep 17 00:00:00 2001 From: syntron Date: Sat, 26 Jul 2025 15:47:20 +0200 Subject: [PATCH 32/67] [test_OMCPath] fix error error: "unlink" of "OMCPathReal" does not return a value (it only ever returns None) [func-returns-value] --- tests/test_OMCPath.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_OMCPath.py b/tests/test_OMCPath.py index 449108ac..b8e937f3 100644 --- a/tests/test_OMCPath.py +++ b/tests/test_OMCPath.py @@ -74,5 +74,5 @@ def _run_OMCPath_checks(om: OMPython.OMCSessionZMQ): assert p3.read_text() == "test" assert p3.is_file() assert p3.parent.is_dir() - assert p3.unlink() is None + p3.unlink() assert p3.is_file() is False From 26e73f6f31ebdc31c67eacb3a5c7343e0c7ad249 Mon Sep 17 00:00:00 2001 From: syntron Date: Thu, 24 Jul 2025 21:00:21 +0200 Subject: [PATCH 33/67] [ModelicaSystem] update handling of work directory * use as input str or os.PathLike; the later covers all pathlib objects * rename _tempdir to _work_dir * rename setTempDirectory() => setWorkDirectory() * setWorkDirectory() sets the work dir and also returns its path * use setWorkDirectory() within code; this allows to add special handling to the function if needed --- OMPython/ModelicaSystem.py | 67 +++++++++++++++++++++++--------------- 1 file changed, 41 insertions(+), 26 deletions(-) diff --git a/OMPython/ModelicaSystem.py b/OMPython/ModelicaSystem.py index ae480dde..dad6450b 100644 --- a/OMPython/ModelicaSystem.py +++ b/OMPython/ModelicaSystem.py @@ -301,7 +301,7 @@ def __init__( lmodel: Optional[list[str | tuple[str, str]]] = None, commandLineOptions: Optional[str] = None, variableFilter: Optional[str] = None, - customBuildDirectory: Optional[str | os.PathLike | pathlib.Path] = None, + customBuildDirectory: Optional[str | os.PathLike] = None, omhome: Optional[str] = None, omc_process: Optional[OMCProcessLocal] = None, build: bool = True, @@ -400,7 +400,7 @@ def __init__( self.setCommandLineOptions("--linearizationDumpLanguage=python") self.setCommandLineOptions("--generateSymbolicLinearization") - self._tempdir = self.setTempDirectory(customBuildDirectory) + self._work_dir: pathlib.Path = self.setWorkDirectory(customBuildDirectory) if self._file_name is not None: self._loadLibrary(lmodel=self._lmodel) @@ -448,25 +448,34 @@ def _loadLibrary(self, lmodel: list): '1)["Modelica"]\n' '2)[("Modelica","3.2.3"), "PowerSystems"]\n') - def setTempDirectory(self, customBuildDirectory: Optional[str | os.PathLike | pathlib.Path] = None) -> pathlib.Path: - # create a unique temp directory for each session and build the model in that directory + def setWorkDirectory(self, customBuildDirectory: Optional[str | os.PathLike] = None) -> pathlib.Path: + """ + Define the work directory for the ModelicaSystem / OpenModelica session. The model is build within this + directory. If no directory is defined a unique temporary directory is created. + """ if customBuildDirectory is not None: - if not os.path.exists(customBuildDirectory): - raise IOError(f"{customBuildDirectory} does not exist") - tempdir = pathlib.Path(customBuildDirectory).absolute() + workdir = pathlib.Path(customBuildDirectory).absolute() + if not workdir.is_dir(): + raise IOError(f"Provided work directory does not exists: {customBuildDirectory}!") else: - tempdir = pathlib.Path(tempfile.mkdtemp()).absolute() - if not tempdir.is_dir(): - raise IOError(f"{tempdir} could not be created") + workdir = pathlib.Path(tempfile.mkdtemp()).absolute() + if not workdir.is_dir(): + raise IOError(f"{workdir} could not be created") - logger.info("Define tempdir as %s", tempdir) - exp = f'cd("{tempdir.as_posix()}")' + logger.info("Define work dir as %s", workdir) + exp = f'cd("{workdir.as_posix()}")' self.sendExpression(exp) - return tempdir + # set the class variable _tempdir ... + self._work_dir = workdir + # ... and also return the defined path + return workdir def getWorkDirectory(self) -> pathlib.Path: - return self._tempdir + """ + Return the defined working directory for this ModelicaSystem / OpenModelica session. + """ + return self._work_dir def buildModel(self, variableFilter: Optional[str] = None): if variableFilter is not None: @@ -942,7 +951,11 @@ def simulate_cmd( An instance if ModelicaSystemCmd to run the requested simulation. """ - om_cmd = ModelicaSystemCmd(runpath=self._tempdir, modelname=self._model_name, timeout=timeout) + om_cmd = ModelicaSystemCmd( + runpath=self.getWorkDirectory(), + modelname=self._model_name, + timeout=timeout, + ) # always define the result file to use om_cmd.arg_set(key="r", val=result_file.as_posix()) @@ -954,7 +967,7 @@ def simulate_cmd( if simargs: om_cmd.args_set(args=simargs) - overrideFile = self._tempdir / f"{self._model_name}_override.txt" + overrideFile = self.getWorkDirectory() / f"{self._model_name}_override.txt" if self._override_variables or self._simulate_options_override: tmpdict = self._override_variables.copy() tmpdict.update(self._simulate_options_override) @@ -1013,11 +1026,11 @@ def simulate( if resultfile is None: # default result file generated by OM - self._result_file = self._tempdir / f"{self._model_name}_res.mat" + self._result_file = self.getWorkDirectory() / f"{self._model_name}_res.mat" elif os.path.exists(resultfile): self._result_file = pathlib.Path(resultfile) else: - self._result_file = self._tempdir / resultfile + self._result_file = self.getWorkDirectory() / resultfile om_cmd = self.simulate_cmd( result_file=self._result_file, @@ -1419,7 +1432,7 @@ def _createCSVData(self, csvfile: Optional[pathlib.Path] = None) -> pathlib.Path csv_rows.append(row) if csvfile is None: - csvfile = self._tempdir / f'{self._model_name}.csv' + csvfile = self.getWorkDirectory() / f'{self._model_name}.csv' with open(file=csvfile, mode="w", encoding="utf-8", newline="") as fh: writer = csv.writer(fh) @@ -1541,9 +1554,13 @@ def linearize(self, lintime: Optional[float] = None, simflags: Optional[str] = N "use ModelicaSystem() to build the model first" ) - om_cmd = ModelicaSystemCmd(runpath=self._tempdir, modelname=self._model_name, timeout=timeout) + om_cmd = ModelicaSystemCmd( + runpath=self.getWorkDirectory(), + modelname=self._model_name, + timeout=timeout, + ) - overrideLinearFile = self._tempdir / f'{self._model_name}_override_linear.txt' + overrideLinearFile = self.getWorkDirectory() / f'{self._model_name}_override_linear.txt' with open(file=overrideLinearFile, mode="w", encoding="utf-8") as fh: for key, value in self._override_variables.items(): @@ -1573,19 +1590,17 @@ def linearize(self, lintime: Optional[float] = None, simflags: Optional[str] = N om_cmd.args_set(args=simargs) # the file create by the model executable which contains the matrix and linear inputs, outputs and states - linear_file = self._tempdir / "linearized_model.py" - + linear_file = self.getWorkDirectory() / "linearized_model.py" linear_file.unlink(missing_ok=True) returncode = om_cmd.run() if returncode != 0: raise ModelicaSystemError(f"Linearize failed with return code: {returncode}") - - self._simulated = True - if not linear_file.exists(): raise ModelicaSystemError(f"Linearization failed: {linear_file} not found!") + self._simulated = True + # extract data from the python file with the linearized model using the ast module - this allows to get the # needed information without executing the created code linear_data = {} From a9442b509ae60665368f4db7f6312b0cc0d60356 Mon Sep 17 00:00:00 2001 From: syntron Date: Wed, 9 Jul 2025 20:26:04 +0200 Subject: [PATCH 34/67] [ModelicaSystem] update handling of xml_file --- OMPython/ModelicaSystem.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/OMPython/ModelicaSystem.py b/OMPython/ModelicaSystem.py index ae480dde..bf0c22a0 100644 --- a/OMPython/ModelicaSystem.py +++ b/OMPython/ModelicaSystem.py @@ -480,8 +480,10 @@ def buildModel(self, variableFilter: Optional[str] = None): buildModelResult = self._requestApi("buildModel", self._model_name, properties=varFilter) logger.debug("OM model build result: %s", buildModelResult) - self._xml_file = pathlib.Path(buildModelResult[0]).parent / buildModelResult[1] - self._xmlparse() + xml_file = pathlib.Path(buildModelResult[0]).parent / buildModelResult[1] + self._xmlparse(xml_file=xml_file) + # TODO: remove _xml_file?! + self._xml_file = xml_file def sendExpression(self, expr: str, parsed: bool = True): try: @@ -507,11 +509,11 @@ def _requestApi(self, apiName, entity=None, properties=None): # 2 return self.sendExpression(exp) - def _xmlparse(self): - if not self._xml_file.is_file(): - raise ModelicaSystemError(f"XML file not generated: {self._xml_file}") + def _xmlparse(self, xml_file: pathlib.Path): + if not xml_file.is_file(): + raise ModelicaSystemError(f"XML file not generated: {xml_file}") - tree = ET.parse(self._xml_file) + tree = ET.parse(xml_file) rootCQ = tree.getroot() for attr in rootCQ.iter('DefaultExperiment'): for key in ("startTime", "stopTime", "stepSize", "tolerance", From c4fabc5c5705d7443e7463deb0ea298697dca01c Mon Sep 17 00:00:00 2001 From: syntron Date: Wed, 9 Jul 2025 20:26:42 +0200 Subject: [PATCH 35/67] [ModelicaSystem] replace ET.parse() with ET.ElementTree(ET.fromstring()) read the file content and work on this string see: https://stackoverflow.com/questions/647071/python-xml-elementtree-from-a-string-source --- OMPython/ModelicaSystem.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/OMPython/ModelicaSystem.py b/OMPython/ModelicaSystem.py index bf0c22a0..454bc472 100644 --- a/OMPython/ModelicaSystem.py +++ b/OMPython/ModelicaSystem.py @@ -513,7 +513,8 @@ def _xmlparse(self, xml_file: pathlib.Path): if not xml_file.is_file(): raise ModelicaSystemError(f"XML file not generated: {xml_file}") - tree = ET.parse(xml_file) + xml_content = xml_file.read_text() + tree = ET.ElementTree(ET.fromstring(xml_content)) rootCQ = tree.getroot() for attr in rootCQ.iter('DefaultExperiment'): for key in ("startTime", "stopTime", "stepSize", "tolerance", From 3133f61ea308fc6eb58690ba6f8f2d8da7102118 Mon Sep 17 00:00:00 2001 From: syntron Date: Thu, 10 Jul 2025 09:59:04 +0200 Subject: [PATCH 36/67] [ModelicaSystem._xmlparse] mypy fixes & cleanup --- OMPython/ModelicaSystem.py | 23 +++++++++++++++++------ 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/OMPython/ModelicaSystem.py b/OMPython/ModelicaSystem.py index 454bc472..3fc47bea 100644 --- a/OMPython/ModelicaSystem.py +++ b/OMPython/ModelicaSystem.py @@ -519,14 +519,24 @@ def _xmlparse(self, xml_file: pathlib.Path): for attr in rootCQ.iter('DefaultExperiment'): for key in ("startTime", "stopTime", "stepSize", "tolerance", "solver", "outputFormat"): - self._simulate_options[key] = attr.get(key) + self._simulate_options[key] = str(attr.get(key)) for sv in rootCQ.iter('ScalarVariable'): - scalar = {} - for key in ("name", "description", "variability", "causality", "alias"): - scalar[key] = sv.get(key) - scalar["changeable"] = sv.get('isValueChangeable') - scalar["aliasvariable"] = sv.get('aliasVariable') + translations = { + "alias": "alias", + "aliasvariable": "aliasVariable", + "causality": "causality", + "changeable": "isValueChangeable", + "description": "description", + "name": "name", + "variability": "variability", + } + + scalar: dict[str, Any] = {} + for key_dst, key_src in translations.items(): + val = sv.get(key_src) + scalar[key_dst] = None if val is None else str(val) + ch = list(sv) for att in ch: scalar["start"] = att.get('start') @@ -534,6 +544,7 @@ def _xmlparse(self, xml_file: pathlib.Path): scalar["max"] = att.get('max') scalar["unit"] = att.get('unit') + # save parameters in the corresponding class variables if scalar["variability"] == "parameter": if scalar["name"] in self._override_variables: self._params[scalar["name"]] = self._override_variables[scalar["name"]] From 33ea4607da95359f031e972cf3d33a0396a2301e Mon Sep 17 00:00:00 2001 From: syntron Date: Thu, 10 Jul 2025 10:03:53 +0200 Subject: [PATCH 37/67] [ModelicaSystem] remove class variable _xml_file --- OMPython/ModelicaSystem.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/OMPython/ModelicaSystem.py b/OMPython/ModelicaSystem.py index 3fc47bea..ccaaedfa 100644 --- a/OMPython/ModelicaSystem.py +++ b/OMPython/ModelicaSystem.py @@ -383,7 +383,6 @@ def __init__( if not isinstance(lmodel, list): raise ModelicaSystemError(f"Invalid input type for lmodel: {type(lmodel)} - list expected!") - self._xml_file = None self._lmodel = lmodel # may be needed if model is derived from other model self._model_name = modelName # Model class name self._file_name = pathlib.Path(fileName).resolve() if fileName is not None else None # Model file/package name @@ -482,8 +481,6 @@ def buildModel(self, variableFilter: Optional[str] = None): xml_file = pathlib.Path(buildModelResult[0]).parent / buildModelResult[1] self._xmlparse(xml_file=xml_file) - # TODO: remove _xml_file?! - self._xml_file = xml_file def sendExpression(self, expr: str, parsed: bool = True): try: @@ -1549,7 +1546,8 @@ def linearize(self, lintime: Optional[float] = None, simflags: Optional[str] = N compatibility, because linearize() used to return `[A, B, C, D]`. """ - if self._xml_file is None: + if len(self._quantities) == 0: + # if self._quantities has no content, the xml file was not parsed; see self._xmlparse() raise ModelicaSystemError( "Linearization cannot be performed as the model is not build, " "use ModelicaSystem() to build the model first" From fa1f4536006475239019fcebdcc5c0f5eecbb9a8 Mon Sep 17 00:00:00 2001 From: syntron Date: Sat, 12 Jul 2025 12:03:40 +0200 Subject: [PATCH 38/67] [ModelicaSystem] fix mypy warning - value can have different types in this code (int or str) --- OMPython/ModelicaSystem.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/OMPython/ModelicaSystem.py b/OMPython/ModelicaSystem.py index ccaaedfa..16205bbc 100644 --- a/OMPython/ModelicaSystem.py +++ b/OMPython/ModelicaSystem.py @@ -1558,10 +1558,10 @@ def linearize(self, lintime: Optional[float] = None, simflags: Optional[str] = N overrideLinearFile = self._tempdir / f'{self._model_name}_override_linear.txt' with open(file=overrideLinearFile, mode="w", encoding="utf-8") as fh: - for key, value in self._override_variables.items(): - fh.write(f"{key}={value}\n") - for key, value in self._linearization_options.items(): - fh.write(f"{key}={value}\n") + for key1, value1 in self._override_variables.items(): + fh.write(f"{key1}={value1}\n") + for key2, value2 in self._linearization_options.items(): + fh.write(f"{key2}={value2}\n") om_cmd.arg_set(key="overrideFile", val=overrideLinearFile.as_posix()) From 250870c9b4832b712acd48907de409446ff6b870 Mon Sep 17 00:00:00 2001 From: syntron Date: Sat, 26 Jul 2025 15:20:49 +0200 Subject: [PATCH 39/67] [ModelicaSystem] do not use package csv background: if OMCPath will be used, it is not available --- OMPython/ModelicaSystem.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/OMPython/ModelicaSystem.py b/OMPython/ModelicaSystem.py index ae480dde..63fe926f 100644 --- a/OMPython/ModelicaSystem.py +++ b/OMPython/ModelicaSystem.py @@ -33,7 +33,6 @@ """ import ast -import csv from dataclasses import dataclass import logging import numbers @@ -1421,9 +1420,10 @@ def _createCSVData(self, csvfile: Optional[pathlib.Path] = None) -> pathlib.Path if csvfile is None: csvfile = self._tempdir / f'{self._model_name}.csv' - with open(file=csvfile, mode="w", encoding="utf-8", newline="") as fh: - writer = csv.writer(fh) - writer.writerows(csv_rows) + # basic definition of a CSV file using csv_rows as input + csv_content = "\n".join([",".join(map(str, row)) for row in csv_rows]) + "\n" + + csvfile.write_text(csv_content) return csvfile From 66b9df31ae0df1db07b7c76729bbbf69cb2f076e Mon Sep 17 00:00:00 2001 From: syntron Date: Sat, 26 Jul 2025 15:22:22 +0200 Subject: [PATCH 40/67] [ModelicaSystem] create override file using pathlib.Path.write_text() background: open() is not available if OMCPath is used --- OMPython/ModelicaSystem.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/OMPython/ModelicaSystem.py b/OMPython/ModelicaSystem.py index 63fe926f..0ee51414 100644 --- a/OMPython/ModelicaSystem.py +++ b/OMPython/ModelicaSystem.py @@ -957,11 +957,9 @@ def simulate_cmd( if self._override_variables or self._simulate_options_override: tmpdict = self._override_variables.copy() tmpdict.update(self._simulate_options_override) - # write to override file - with open(file=overrideFile, mode="w", encoding="utf-8") as fh: - for key, value in tmpdict.items(): - fh.write(f"{key}={value}\n") + override_content = "\n".join([f"{key}={value}" for key, value in tmpdict.items()]) + "\n" + overrideFile.write_text(override_content) om_cmd.arg_set(key="overrideFile", val=overrideFile.as_posix()) if self._inputs: # if model has input quantities From a676435c441607bdace3e45da73654f19cd1a5dc Mon Sep 17 00:00:00 2001 From: syntron Date: Tue, 5 Aug 2025 19:29:24 +0200 Subject: [PATCH 41/67] [ModelicaSystem] update handling of override file * define file name based on result file name & Path * simplify code --- OMPython/ModelicaSystem.py | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/OMPython/ModelicaSystem.py b/OMPython/ModelicaSystem.py index 0ee51414..16fb4bd4 100644 --- a/OMPython/ModelicaSystem.py +++ b/OMPython/ModelicaSystem.py @@ -953,14 +953,17 @@ def simulate_cmd( if simargs: om_cmd.args_set(args=simargs) - overrideFile = self._tempdir / f"{self._model_name}_override.txt" if self._override_variables or self._simulate_options_override: - tmpdict = self._override_variables.copy() - tmpdict.update(self._simulate_options_override) + override_file = result_file.parent / f"{result_file.stem}_override.txt" - override_content = "\n".join([f"{key}={value}" for key, value in tmpdict.items()]) + "\n" - overrideFile.write_text(override_content) - om_cmd.arg_set(key="overrideFile", val=overrideFile.as_posix()) + 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()) if self._inputs: # if model has input quantities for key in self._inputs: From 38674e0309f77aee1695ff3a368a3172ce786b48 Mon Sep 17 00:00:00 2001 From: syntron Date: Thu, 24 Jul 2025 21:18:46 +0200 Subject: [PATCH 42/67] [OMCSessionRunData] add new class to store all information about a model executable --- OMPython/OMCSession.py | 43 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/OMPython/OMCSession.py b/OMPython/OMCSession.py index ac99dc05..f6a300d2 100644 --- a/OMPython/OMCSession.py +++ b/OMPython/OMCSession.py @@ -34,6 +34,7 @@ CONDITIONS OF OSMC-PL. """ +import dataclasses import io import json import logging @@ -268,6 +269,48 @@ def getClassNames(self, className=None, recursive=False, qualified=False, sort=F return self._ask(question='getClassNames', opt=opt) +@dataclasses.dataclass +class OMCSessionRunData: + # TODO: rename OMCExcecutableModelData + """ + Data class to store the command line data for running a model executable in the OMC environment. + + All data should be defined for the environment, where OMC is running (local, docker or WSL) + """ + # cmd_path is the expected working directory + cmd_path: str + cmd_model_name: str + # command line arguments for the model executable + cmd_args: list[str] + # result file with the simulation output + cmd_result_path: str + + # command prefix data (as list of strings); needed for docker or WSL + cmd_prefix: Optional[list[str]] = None + # cmd_model_executable is build out of cmd_path and cmd_model_name; this is mainly needed on Windows (add *.exe) + 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 + + def get_cmd(self) -> list[str]: + """ + Get the command line to run the model executable in the environment defined by the OMCProcess definition. + """ + + if self.cmd_model_executable is None: + raise OMCSessionException("No model file defined for the model executable!") + + cmdl = [] if self.cmd_prefix is None else self.cmd_prefix + cmdl += [self.cmd_model_executable] + self.cmd_args + + return cmdl + + class OMCSessionZMQ: def __init__( From 0abdaf926fad8a2d30a4d5d085b88d7ecfcdede2 Mon Sep 17 00:00:00 2001 From: syntron Date: Thu, 24 Jul 2025 21:33:12 +0200 Subject: [PATCH 43/67] [OMCSessionRunData] use class to move run of model executable to OMSessionZMQ --- OMPython/ModelicaSystem.py | 120 ++++++++++++++----------------------- OMPython/OMCSession.py | 120 ++++++++++++++++++++++++++++++++++++- 2 files changed, 165 insertions(+), 75 deletions(-) diff --git a/OMPython/ModelicaSystem.py b/OMPython/ModelicaSystem.py index ae480dde..d4a0ab86 100644 --- a/OMPython/ModelicaSystem.py +++ b/OMPython/ModelicaSystem.py @@ -40,16 +40,13 @@ import numpy as np import os import pathlib -import platform -import re -import subprocess import tempfile import textwrap from typing import Optional, Any import warnings import xml.etree.ElementTree as ET -from OMPython.OMCSession import OMCSessionException, OMCSessionZMQ, OMCProcessLocal +from OMPython.OMCSession import OMCSessionException, OMCSessionRunData, OMCSessionZMQ, OMCProcessLocal # define logger using the current module name as ID logger = logging.getLogger(__name__) @@ -115,7 +112,14 @@ def __getitem__(self, index: int): class ModelicaSystemCmd: """A compiled model executable.""" - def __init__(self, runpath: pathlib.Path, modelname: str, timeout: Optional[float] = None) -> None: + def __init__( + self, + session: OMCSessionZMQ, + runpath: pathlib.Path, + modelname: str, + timeout: Optional[float] = None, + ) -> None: + self._session = session self._runpath = pathlib.Path(runpath).resolve().absolute() self._model_name = modelname self._timeout = timeout @@ -176,27 +180,12 @@ def args_set(self, args: dict[str, Optional[str | dict[str, str]]]) -> None: for arg in args: self.arg_set(key=arg, val=args[arg]) - def get_exe(self) -> pathlib.Path: - """Get the path to the compiled model executable.""" - if platform.system() == "Windows": - path_exe = self._runpath / f"{self._model_name}.exe" - else: - path_exe = self._runpath / self._model_name - - if not path_exe.exists(): - raise ModelicaSystemError(f"Application file path not found: {path_exe}") - - return path_exe - - def get_cmd(self) -> list: - """Get a list with the path to the executable and all command line args. - - This can later be used as an argument for subprocess.run(). + def get_cmd_args(self) -> list[str]: + """ + Get a list with the command arguments for the model executable. """ - path_exe = self.get_exe() - - cmdl = [path_exe.as_posix()] + cmdl = [] for key in self._args: if self._args[key] is None: cmdl.append(f"-{key}") @@ -205,54 +194,26 @@ def get_cmd(self) -> list: return cmdl - def run(self) -> int: - """Run the requested simulation. - - Returns - ------- - Subprocess return code (0 on success). + def definition(self) -> OMCSessionRunData: """ + Define all needed data to run the model executable. The data is stored in an OMCSessionRunData object. + """ + # ensure that a result filename is provided + result_file = self.arg_get('r') + if not isinstance(result_file, str): + result_file = (self._runpath / f"{self._model_name}.mat").as_posix() + + omc_run_data = OMCSessionRunData( + cmd_path=self._runpath.as_posix(), + cmd_model_name=self._model_name, + cmd_args=self.get_cmd_args(), + cmd_result_path=result_file, + cmd_timeout=self._timeout, + ) - cmdl: list = self.get_cmd() - - logger.debug("Run OM command %s in %s", repr(cmdl), self._runpath.as_posix()) - - if platform.system() == "Windows": - path_dll = "" - - # set the process environment from the generated .bat file in windows which should have all the dependencies - path_bat = self._runpath / f"{self._model_name}.bat" - if not path_bat.exists(): - raise ModelicaSystemError("Batch file (*.bat) does not exist " + str(path_bat)) - - with open(file=path_bat, mode='r', encoding='utf-8') as fh: - for line in fh: - match = re.match(r"^SET PATH=([^%]*)", line, re.IGNORECASE) - if match: - path_dll = match.group(1).strip(';') # Remove any trailing semicolons - my_env = os.environ.copy() - 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(cmdl, capture_output=True, text=True, env=my_env, cwd=self._runpath, - timeout=self._timeout, check=True) - stdout = cmdres.stdout.strip() - stderr = cmdres.stderr.strip() - returncode = cmdres.returncode - - logger.debug("OM output for command %s:\n%s", repr(cmdl), stdout) - - if stderr: - raise ModelicaSystemError(f"Error running command {repr(cmdl)}: {stderr}") - except subprocess.TimeoutExpired as ex: - raise ModelicaSystemError(f"Timeout running command {repr(cmdl)}") from ex - except subprocess.CalledProcessError as ex: - raise ModelicaSystemError(f"Error running command {repr(cmdl)}") from ex + omc_run_data_updated = self._session.omc_run_data_update(omc_run_data=omc_run_data) - return returncode + return omc_run_data_updated @staticmethod def parse_simflags(simflags: str) -> dict[str, Optional[str | dict[str, str]]]: @@ -942,7 +903,12 @@ def simulate_cmd( An instance if ModelicaSystemCmd to run the requested simulation. """ - om_cmd = ModelicaSystemCmd(runpath=self._tempdir, modelname=self._model_name, timeout=timeout) + om_cmd = ModelicaSystemCmd( + session=self._getconn, + runpath=self.getWorkDirectory(), + modelname=self._model_name, + timeout=timeout, + ) # always define the result file to use om_cmd.arg_set(key="r", val=result_file.as_posix()) @@ -1030,7 +996,8 @@ def simulate( if self._result_file.is_file(): self._result_file.unlink() # ... run simulation ... - returncode = om_cmd.run() + cmd_definition = om_cmd.definition() + returncode = self._getconn.run_model_executable(cmd_run_data=cmd_definition) # and check returncode *AND* resultfile if returncode != 0 and self._result_file.is_file(): # check for an empty (=> 0B) result file which indicates a crash of the model executable @@ -1541,7 +1508,12 @@ def linearize(self, lintime: Optional[float] = None, simflags: Optional[str] = N "use ModelicaSystem() to build the model first" ) - om_cmd = ModelicaSystemCmd(runpath=self._tempdir, modelname=self._model_name, timeout=timeout) + om_cmd = ModelicaSystemCmd( + session=self._getconn, + runpath=self._tempdir, + modelname=self._model_name, + timeout=timeout, + ) overrideLinearFile = self._tempdir / f'{self._model_name}_override_linear.txt' @@ -1574,10 +1546,10 @@ def linearize(self, lintime: Optional[float] = None, simflags: Optional[str] = N # the file create by the model executable which contains the matrix and linear inputs, outputs and states linear_file = self._tempdir / "linearized_model.py" - linear_file.unlink(missing_ok=True) - returncode = om_cmd.run() + cmd_definition = om_cmd.definition() + returncode = self._getconn.run_model_executable(cmd_run_data=cmd_definition) if returncode != 0: raise ModelicaSystemError(f"Linearize failed with return code: {returncode}") diff --git a/OMPython/OMCSession.py b/OMPython/OMCSession.py index f6a300d2..7a0ff7f9 100644 --- a/OMPython/OMCSession.py +++ b/OMPython/OMCSession.py @@ -34,12 +34,14 @@ CONDITIONS OF OSMC-PL. """ +import abc import dataclasses import io import json import logging import os import pathlib +import platform import psutil import pyparsing import re @@ -365,6 +367,53 @@ def __del__(self): self.omc_zmq = None + def omc_run_data_update(self, omc_run_data: OMCSessionRunData) -> OMCSessionRunData: + """ + Modify data based on the selected OMCProcess implementation. + + Needs to be implemented in the subclasses. + """ + return self.omc_process.omc_run_data_update(omc_run_data=omc_run_data) + + @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. + """ + + my_env = os.environ.copy() + if isinstance(cmd_run_data.cmd_library_path, str): + my_env["PATH"] = cmd_run_data.cmd_library_path + os.pathsep + my_env["PATH"] + + cmdl = cmd_run_data.get_cmd() + + logger.debug("Run OM command %s in %s", repr(cmdl), cmd_run_data.cmd_path) + try: + cmdres = subprocess.run( + cmdl, + capture_output=True, + text=True, + env=my_env, + cwd=cmd_run_data.cmd_cwd_local, + timeout=cmd_run_data.cmd_timeout, + check=True, + ) + stdout = cmdres.stdout.strip() + stderr = cmdres.stderr.strip() + returncode = cmdres.returncode + + logger.debug("OM output for command %s:\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 + 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) @@ -468,7 +517,7 @@ def sendExpression(self, command: str, parsed: bool = True) -> Any: raise OMCSessionException("Cannot parse OMC result") from ex -class OMCProcess: +class OMCProcess(metaclass=abc.ABCMeta): def __init__( self, @@ -550,6 +599,15 @@ def _get_portfile_path(self) -> Optional[pathlib.Path]: return portfile_path + @abc.abstractmethod + def omc_run_data_update(self, omc_run_data: OMCSessionRunData) -> OMCSessionRunData: + """ + Update the OMCSessionRunData object based on the selected OMCProcess implementation. + + Needs to be implemented in the subclasses. + """ + raise NotImplementedError("This method must be implemented in subclasses!") + class OMCProcessPort(OMCProcess): @@ -560,6 +618,12 @@ def __init__( super().__init__() self._omc_port = omc_port + def omc_run_data_update(self, omc_run_data: OMCSessionRunData) -> OMCSessionRunData: + """ + Update the OMCSessionRunData object based on the selected OMCProcess implementation. + """ + raise OMCSessionException("OMCProcessPort does not support omc_run_data_update()!") + class OMCProcessLocal(OMCProcess): @@ -641,6 +705,48 @@ def _omc_port_get(self) -> str: return port + def omc_run_data_update(self, omc_run_data: OMCSessionRunData) -> OMCSessionRunData: + """ + Update the OMCSessionRunData object based on the selected OMCProcess implementation. + """ + # create a copy of the data + omc_run_data_copy = dataclasses.replace(omc_run_data) + + # as this is the local implementation, pathlib.Path can be used + cmd_path = pathlib.Path(omc_run_data_copy.cmd_path) + + if platform.system() == "Windows": + path_dll = "" + + # set the process environment from the generated .bat file in windows which should have all the dependencies + path_bat = cmd_path / f"{omc_run_data.cmd_model_name}.bat" + if not path_bat.is_file(): + raise OMCSessionException("Batch file (*.bat) does not exist " + str(path_bat)) + + content = path_bat.read_text(encoding='utf-8') + for line in content.splitlines(): + match = re.match(r"^SET PATH=([^%]*)", line, re.IGNORECASE) + if match: + path_dll = match.group(1).strip(';') # Remove any trailing semicolons + my_env = os.environ.copy() + my_env["PATH"] = path_dll + os.pathsep + my_env["PATH"] + + omc_run_data_copy.cmd_library_path = path_dll + + cmd_model_executable = cmd_path / f"{omc_run_data_copy.cmd_model_name}.exe" + else: + # for Linux the paths to the needed libraries should be included in the executable (using rpath) + cmd_model_executable = cmd_path / omc_run_data_copy.cmd_model_name + + if not cmd_model_executable.is_file(): + raise OMCSessionException(f"Application file path not found: {cmd_model_executable}") + omc_run_data_copy.cmd_model_executable = cmd_model_executable.as_posix() + + # define local(!) working directory + omc_run_data_copy.cmd_cwd_local = omc_run_data.cmd_path + + return omc_run_data_copy + class OMCProcessDockerHelper(OMCProcess): @@ -747,6 +853,12 @@ def get_docker_container_id(self) -> str: return self._dockerCid + def omc_run_data_update(self, omc_run_data: OMCSessionRunData) -> OMCSessionRunData: + """ + Update the OMCSessionRunData object based on the selected OMCProcess implementation. + """ + raise OMCSessionException("OMCProcessDocker* does not support omc_run_data_update()!") + class OMCProcessDocker(OMCProcessDockerHelper): @@ -1052,3 +1164,9 @@ def _omc_port_get(self) -> str: f"pid={self._omc_process.pid if isinstance(self._omc_process, subprocess.Popen) else '?'}") return port + + def omc_run_data_update(self, omc_run_data: OMCSessionRunData) -> OMCSessionRunData: + """ + Update the OMCSessionRunData object based on the selected OMCProcess implementation. + """ + raise OMCSessionException("OMCProcessWSL does not support omc_run_data_update()!") From 427ed71666caab231c379f1d9b0fa030d47917c3 Mon Sep 17 00:00:00 2001 From: syntron Date: Fri, 25 Jul 2025 17:28:31 +0200 Subject: [PATCH 44/67] [test_ModelicaSystemCmd] fix test --- tests/test_ModelicaSystemCmd.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/tests/test_ModelicaSystemCmd.py b/tests/test_ModelicaSystemCmd.py index 3b28699c..362d92bc 100644 --- a/tests/test_ModelicaSystemCmd.py +++ b/tests/test_ModelicaSystemCmd.py @@ -18,7 +18,11 @@ def model_firstorder(tmp_path): @pytest.fixture def mscmd_firstorder(model_firstorder): mod = OMPython.ModelicaSystem(fileName=model_firstorder.as_posix(), modelName="M") - mscmd = OMPython.ModelicaSystemCmd(runpath=mod.getWorkDirectory(), modelname=mod._model_name) + mscmd = OMPython.ModelicaSystemCmd( + session=mod._getconn, + runpath=mod.getWorkDirectory(), + modelname=mod._model_name, + ) return mscmd From 7cd2cde9a13eaca755e1afc413cec998e6968f24 Mon Sep 17 00:00:00 2001 From: syntron Date: Fri, 25 Jul 2025 18:26:45 +0200 Subject: [PATCH 45/67] [OMCSessionRunData] add to __init__ --- OMPython/__init__.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/OMPython/__init__.py b/OMPython/__init__.py index 1da0a0a3..6144f1c2 100644 --- a/OMPython/__init__.py +++ b/OMPython/__init__.py @@ -37,7 +37,7 @@ """ from OMPython.ModelicaSystem import LinearizationResult, ModelicaSystem, ModelicaSystemCmd, ModelicaSystemError -from OMPython.OMCSession import (OMCSessionCmd, OMCSessionException, OMCSessionZMQ, +from OMPython.OMCSession import (OMCSessionCmd, OMCSessionException, OMCSessionRunData, OMCSessionZMQ, OMCProcessPort, OMCProcessLocal, OMCProcessDocker, OMCProcessDockerContainer, OMCProcessWSL) @@ -50,6 +50,7 @@ 'OMCSessionCmd', 'OMCSessionException', + 'OMCSessionRunData', 'OMCSessionZMQ', 'OMCProcessPort', 'OMCProcessLocal', From ec38c09b46ae889e9df298e55fa0677638172d5b Mon Sep 17 00:00:00 2001 From: syntron Date: Tue, 12 Aug 2025 22:45:05 +0200 Subject: [PATCH 46/67] [test_ModelicaSystemCmd] fix test (again) --- tests/test_ModelicaSystemCmd.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tests/test_ModelicaSystemCmd.py b/tests/test_ModelicaSystemCmd.py index 362d92bc..dfb8da91 100644 --- a/tests/test_ModelicaSystemCmd.py +++ b/tests/test_ModelicaSystemCmd.py @@ -36,8 +36,7 @@ def test_simflags(mscmd_firstorder): with pytest.deprecated_call(): mscmd.args_set(args=mscmd.parse_simflags(simflags="-noEventEmit -noRestart -override=a=1,x=3")) - assert mscmd.get_cmd() == [ - mscmd.get_exe().as_posix(), + assert mscmd.get_cmd_args() == [ '-noEventEmit', '-override=b=2,a=1,x=3', '-noRestart', From 646399f8bf09c9f34045fc6efdc90c285952f886 Mon Sep 17 00:00:00 2001 From: syntron Date: Wed, 6 Aug 2025 21:04:12 +0200 Subject: [PATCH 47/67] [OMCProcess*] use pathlib --- OMPython/OMCSession.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/OMPython/OMCSession.py b/OMPython/OMCSession.py index ac99dc05..5dc1dc8e 100644 --- a/OMPython/OMCSession.py +++ b/OMPython/OMCSession.py @@ -523,7 +523,7 @@ class OMCProcessLocal(OMCProcess): def __init__( self, timeout: float = 10.00, - omhome: Optional[str] = None, + omhome: Optional[str | os.PathLike] = None, ) -> None: super().__init__(timeout=timeout) @@ -536,7 +536,7 @@ def __init__( self._omc_port = self._omc_port_get() @staticmethod - def _omc_home_get(omhome: Optional[str] = None) -> pathlib.Path: + def _omc_home_get(omhome: Optional[str | os.PathLike] = None) -> pathlib.Path: # use the provided path if omhome is not None: return pathlib.Path(omhome) @@ -605,7 +605,7 @@ def __init__( self, timeout: float = 10.00, dockerExtraArgs: Optional[list] = None, - dockerOpenModelicaPath: str = "omc", + dockerOpenModelicaPath: str | os.PathLike = "omc", dockerNetwork: Optional[str] = None, port: Optional[int] = None, ) -> None: @@ -615,7 +615,7 @@ def __init__( dockerExtraArgs = [] self._dockerExtraArgs = dockerExtraArgs - self._dockerOpenModelicaPath = dockerOpenModelicaPath + self._dockerOpenModelicaPath = pathlib.PurePosixPath(dockerOpenModelicaPath) self._dockerNetwork = dockerNetwork self._interactivePort = port @@ -712,7 +712,7 @@ def __init__( timeout: float = 10.00, docker: Optional[str] = None, dockerExtraArgs: Optional[list] = None, - dockerOpenModelicaPath: str = "omc", + dockerOpenModelicaPath: str | os.PathLike = "omc", dockerNetwork: Optional[str] = None, port: Optional[int] = None, ) -> None: @@ -796,7 +796,7 @@ def _docker_omc_cmd( ] + self._dockerExtraArgs + dockerNetworkStr - + [self._docker, self._dockerOpenModelicaPath] + + [self._docker, self._dockerOpenModelicaPath.as_posix()] + omc_path_and_args_list + extraFlags) @@ -853,7 +853,7 @@ def __init__( timeout: float = 10.00, dockerContainer: Optional[str] = None, dockerExtraArgs: Optional[list] = None, - dockerOpenModelicaPath: str = "omc", + dockerOpenModelicaPath: str | os.PathLike = "omc", dockerNetwork: Optional[str] = None, port: Optional[int] = None, ) -> None: @@ -904,7 +904,7 @@ def _docker_omc_cmd(self, omc_path_and_args_list) -> list: "--user", str(self._getuid()), ] + self._dockerExtraArgs - + [self._dockerCid, self._dockerOpenModelicaPath] + + [self._dockerCid, self._dockerOpenModelicaPath.as_posix()] + omc_path_and_args_list + extraFlags) From 4384fa522011aa997e5be3b424186801960c7b7d Mon Sep 17 00:00:00 2001 From: syntron Date: Tue, 5 Aug 2025 19:29:24 +0200 Subject: [PATCH 48/67] [ModelicaSystem] update handling of override file * define file name based on result file name & Path * simplify code --- OMPython/ModelicaSystem.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/OMPython/ModelicaSystem.py b/OMPython/ModelicaSystem.py index 1244a66e..e782fe15 100644 --- a/OMPython/ModelicaSystem.py +++ b/OMPython/ModelicaSystem.py @@ -978,7 +978,7 @@ def simulate_cmd( om_cmd.args_set(args=simargs) if self._override_variables or self._simulate_options_override: - override_file = self.getWorkDirectory() / f"{result_file.stem}_override.txt" + override_file = result_file.parent / f"{result_file.stem}_override.txt" override_content = ( "\n".join([f"{key}={value}" for key, value in self._override_variables.items()]) From 6617ab2826c2b6bd06ab5c984707968b768902f7 Mon Sep 17 00:00:00 2001 From: syntron Date: Mon, 7 Jul 2025 20:43:02 +0200 Subject: [PATCH 49/67] [ModelicaSystem] fix rebase fallout 2 --- OMPython/ModelicaSystem.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/OMPython/ModelicaSystem.py b/OMPython/ModelicaSystem.py index e782fe15..e14ea547 100644 --- a/OMPython/ModelicaSystem.py +++ b/OMPython/ModelicaSystem.py @@ -1395,6 +1395,8 @@ def setInputs( else: raise ModelicaSystemError(f"Data cannot be evaluated for {repr(key)}: {repr(val)}") + self._has_inputs = True + return True def _createCSVData(self, csvfile: Optional[pathlib.Path] = None) -> pathlib.Path: From 3022e658687c7634d29fdcdb0711ca8134eaf02c Mon Sep 17 00:00:00 2001 From: syntron Date: Sat, 12 Jul 2025 01:03:53 +0200 Subject: [PATCH 50/67] [test_ModelicaSystem] fix test_customBuildDirectory() --- tests/test_ModelicaSystem.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_ModelicaSystem.py b/tests/test_ModelicaSystem.py index 8e9b8a8e..e782489e 100644 --- a/tests/test_ModelicaSystem.py +++ b/tests/test_ModelicaSystem.py @@ -105,7 +105,7 @@ def test_customBuildDirectory(tmp_path, model_firstorder): tmpdir = tmp_path / "tmpdir1" tmpdir.mkdir() m = OMPython.ModelicaSystem(filePath, "M", customBuildDirectory=tmpdir) - assert m.getWorkDirectory().resolve() == tmpdir.resolve() + assert pathlib.Path(m.getWorkDirectory()).resolve() == tmpdir.resolve() result_file = tmpdir / "a.mat" assert not result_file.exists() m.simulate(resultfile="a.mat") From daa3cafbfe35735bb75682df5d5fe7d715c6fd19 Mon Sep 17 00:00:00 2001 From: syntron Date: Tue, 22 Jul 2025 20:37:00 +0200 Subject: [PATCH 51/67] [ModelicaSystem] fix blank lines (flake8) --- OMPython/ModelicaSystem.py | 1 - 1 file changed, 1 deletion(-) diff --git a/OMPython/ModelicaSystem.py b/OMPython/ModelicaSystem.py index e14ea547..f8187809 100644 --- a/OMPython/ModelicaSystem.py +++ b/OMPython/ModelicaSystem.py @@ -1561,7 +1561,6 @@ def linearize(self, lintime: Optional[float] = None, simflags: Optional[str] = N * `result = linearize(); A = result[0]` mostly just for backwards compatibility, because linearize() used to return `[A, B, C, D]`. """ - if len(self._quantities) == 0: # if self._quantities has no content, the xml file was not parsed; see self._xmlparse() raise ModelicaSystemError( From f627daf95c86785150eb4db9fe7b6cf199edd34f Mon Sep 17 00:00:00 2001 From: syntron Date: Wed, 6 Aug 2025 21:08:17 +0200 Subject: [PATCH 52/67] [test_optimization] fix due to OMCPath usage --- tests/test_optimization.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/test_optimization.py b/tests/test_optimization.py index b4164397..908cfd62 100644 --- a/tests/test_optimization.py +++ b/tests/test_optimization.py @@ -47,7 +47,9 @@ def test_optimization_example(tmp_path): r = mod.optimize() # it is necessary to specify resultfile, otherwise it wouldn't find it. - time, f, v = mod.getSolutions(["time", "f", "v"], resultfile=r["resultFile"]) + resultfile_str = r["resultFile"] + resultfile_omcpath = mod._getconn.omcpath(resultfile_str) + time, f, v = mod.getSolutions(["time", "f", "v"], resultfile=resultfile_omcpath.as_posix()) assert np.isclose(f[0], 10) assert np.isclose(f[-1], -10) From 47dd9955b8708daaa5128f2b218f0518dd8cc843 Mon Sep 17 00:00:00 2001 From: syntron Date: Wed, 6 Aug 2025 21:09:06 +0200 Subject: [PATCH 53/67] [test_FMIExport] fix due to OMCPath usage --- tests/test_FMIExport.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tests/test_FMIExport.py b/tests/test_FMIExport.py index f47b87ae..b8305b31 100644 --- a/tests/test_FMIExport.py +++ b/tests/test_FMIExport.py @@ -1,12 +1,13 @@ import OMPython import shutil import os +import pathlib def test_CauerLowPassAnalog(): mod = OMPython.ModelicaSystem(modelName="Modelica.Electrical.Analog.Examples.CauerLowPassAnalog", lmodel=["Modelica"]) - tmp = mod.getWorkDirectory() + tmp = pathlib.Path(mod.getWorkDirectory()) try: fmu = mod.convertMo2Fmu(fileNamePrefix="CauerLowPassAnalog") assert os.path.exists(fmu) @@ -16,7 +17,7 @@ def test_CauerLowPassAnalog(): def test_DrumBoiler(): mod = OMPython.ModelicaSystem(modelName="Modelica.Fluid.Examples.DrumBoiler.DrumBoiler", lmodel=["Modelica"]) - tmp = mod.getWorkDirectory() + tmp = pathlib.Path(mod.getWorkDirectory()) try: fmu = mod.convertMo2Fmu(fileNamePrefix="DrumBoiler") assert os.path.exists(fmu) From d42753e09fec44620782194e7c0d77ac7b1002f3 Mon Sep 17 00:00:00 2001 From: syntron Date: Sat, 9 Aug 2025 20:29:49 +0200 Subject: [PATCH 54/67] [ModelicaSystem] improve definition of getSolution * allow different ways to define the path --- OMPython/ModelicaSystem.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/OMPython/ModelicaSystem.py b/OMPython/ModelicaSystem.py index f8187809..9699bc35 100644 --- a/OMPython/ModelicaSystem.py +++ b/OMPython/ModelicaSystem.py @@ -1068,7 +1068,11 @@ def simulate( self._simulated = True - def getSolutions(self, varList: Optional[str | list[str]] = None, resultfile: Optional[str] = None) -> tuple[str] | np.ndarray: + def getSolutions( + self, + varList: Optional[str | list[str]] = None, + resultfile: Optional[str | os.PathLike] = None, + ) -> tuple[str] | np.ndarray: """Extract simulation results from a result data file. Args: From 229d6246de8ffa9702d237d3e6afbdfa4cdc9f81 Mon Sep 17 00:00:00 2001 From: syntron Date: Fri, 11 Jul 2025 22:52:19 +0200 Subject: [PATCH 55/67] [ModelicaSystem] use OMCPath for nearly all file system interactions remove pathlib - use OMCPath and (for type hints) os.PathLike --- OMPython/ModelicaSystem.py | 69 ++++++++++++++++++++------------------ 1 file changed, 36 insertions(+), 33 deletions(-) diff --git a/OMPython/ModelicaSystem.py b/OMPython/ModelicaSystem.py index 9699bc35..442a7548 100644 --- a/OMPython/ModelicaSystem.py +++ b/OMPython/ModelicaSystem.py @@ -38,17 +38,15 @@ import numbers import numpy as np import os -import pathlib import platform import re import subprocess -import tempfile import textwrap from typing import Optional, Any import warnings import xml.etree.ElementTree as ET -from OMPython.OMCSession import OMCSessionException, OMCSessionZMQ, OMCProcessLocal +from OMPython.OMCSession import OMCSessionException, OMCSessionZMQ, OMCProcessLocal, OMCPath # define logger using the current module name as ID logger = logging.getLogger(__name__) @@ -114,8 +112,8 @@ def __getitem__(self, index: int): class ModelicaSystemCmd: """A compiled model executable.""" - def __init__(self, runpath: pathlib.Path, modelname: str, timeout: Optional[float] = None) -> None: - self._runpath = pathlib.Path(runpath).resolve().absolute() + def __init__(self, runpath: OMCPath, modelname: str, timeout: Optional[float] = None) -> None: + self._runpath = runpath self._model_name = modelname self._timeout = timeout self._args: dict[str, str | None] = {} @@ -175,7 +173,7 @@ def args_set(self, args: dict[str, Optional[str | dict[str, str]]]) -> None: for arg in args: self.arg_set(key=arg, val=args[arg]) - def get_exe(self) -> pathlib.Path: + def get_exe(self) -> OMCPath: """Get the path to the compiled model executable.""" if platform.system() == "Windows": path_exe = self._runpath / f"{self._model_name}.exe" @@ -295,7 +293,7 @@ def parse_simflags(simflags: str) -> dict[str, Optional[str | dict[str, str]]]: class ModelicaSystem: def __init__( self, - fileName: Optional[str | os.PathLike | pathlib.Path] = None, + fileName: Optional[str | os.PathLike] = None, modelName: Optional[str] = None, lmodel: Optional[list[str | tuple[str, str]]] = None, commandLineOptions: Optional[str] = None, @@ -384,9 +382,13 @@ def __init__( self._lmodel = lmodel # may be needed if model is derived from other model self._model_name = modelName # Model class name - self._file_name = pathlib.Path(fileName).resolve() if fileName is not None else None # Model file/package name + if fileName is not None: + file_name = self._getconn.omcpath(fileName).resolve() + else: + file_name = None + self._file_name: Optional[OMCPath] = file_name # Model file/package name self._simulated = False # True if the model has already been simulated - self._result_file: Optional[pathlib.Path] = None # for storing result file + self._result_file: Optional[OMCPath] = None # for storing result file self._variable_filter = variableFilter if self._file_name is not None and not self._file_name.is_file(): # if file does not exist @@ -398,7 +400,7 @@ def __init__( self.setCommandLineOptions("--linearizationDumpLanguage=python") self.setCommandLineOptions("--generateSymbolicLinearization") - self._work_dir: pathlib.Path = self.setWorkDirectory(customBuildDirectory) + self._work_dir: OMCPath = self.setWorkDirectory(customBuildDirectory) if self._file_name is not None: self._loadLibrary(lmodel=self._lmodel) @@ -418,7 +420,7 @@ def setCommandLineOptions(self, commandLineOptions: Optional[str] = None): exp = f'setCommandLineOptions("{commandLineOptions}")' self.sendExpression(exp) - def _loadFile(self, fileName: pathlib.Path): + def _loadFile(self, fileName: OMCPath): # load file self.sendExpression(f'loadFile("{fileName.as_posix()}")') @@ -446,17 +448,17 @@ def _loadLibrary(self, lmodel: list): '1)["Modelica"]\n' '2)[("Modelica","3.2.3"), "PowerSystems"]\n') - def setWorkDirectory(self, customBuildDirectory: Optional[str | os.PathLike] = None) -> pathlib.Path: + def setWorkDirectory(self, customBuildDirectory: Optional[str | os.PathLike] = None) -> OMCPath: """ Define the work directory for the ModelicaSystem / OpenModelica session. The model is build within this directory. If no directory is defined a unique temporary directory is created. """ if customBuildDirectory is not None: - workdir = pathlib.Path(customBuildDirectory).absolute() + workdir = self._getconn.omcpath(customBuildDirectory).absolute() if not workdir.is_dir(): raise IOError(f"Provided work directory does not exists: {customBuildDirectory}!") else: - workdir = pathlib.Path(tempfile.mkdtemp()).absolute() + workdir = self._getconn.omcpath_tempdir().absolute() if not workdir.is_dir(): raise IOError(f"{workdir} could not be created") @@ -469,7 +471,7 @@ def setWorkDirectory(self, customBuildDirectory: Optional[str | os.PathLike] = N # ... and also return the defined path return workdir - def getWorkDirectory(self) -> pathlib.Path: + def getWorkDirectory(self) -> OMCPath: """ Return the defined working directory for this ModelicaSystem / OpenModelica session. """ @@ -487,7 +489,7 @@ def buildModel(self, variableFilter: Optional[str] = None): buildModelResult = self._requestApi("buildModel", self._model_name, properties=varFilter) logger.debug("OM model build result: %s", buildModelResult) - xml_file = pathlib.Path(buildModelResult[0]).parent / buildModelResult[1] + xml_file = self._getconn.omcpath(buildModelResult[0]).parent / buildModelResult[1] self._xmlparse(xml_file=xml_file) def sendExpression(self, expr: str, parsed: bool = True): @@ -514,7 +516,7 @@ def _requestApi(self, apiName, entity=None, properties=None): # 2 return self.sendExpression(exp) - def _xmlparse(self, xml_file: pathlib.Path): + def _xmlparse(self, xml_file: OMCPath): if not xml_file.is_file(): raise ModelicaSystemError(f"XML file not generated: {xml_file}") @@ -934,7 +936,7 @@ def getOptimizationOptions(self, names: Optional[str | list[str]] = None) -> dic def simulate_cmd( self, - result_file: pathlib.Path, + result_file: OMCPath, simflags: Optional[str] = None, simargs: Optional[dict[str, Optional[str | dict[str, str]]]] = None, timeout: Optional[float] = None, @@ -1039,10 +1041,13 @@ def simulate( # default result file generated by OM self._result_file = self.getWorkDirectory() / f"{self._model_name}_res.mat" elif os.path.exists(resultfile): - self._result_file = pathlib.Path(resultfile) + self._result_file = self._getconn.omcpath(resultfile) else: self._result_file = self.getWorkDirectory() / resultfile + if not isinstance(self._result_file, OMCPath): + raise ModelicaSystemError(f"Invalid result file path: {self._result_file} - must be an OMCPath object!") + om_cmd = self.simulate_cmd( result_file=self._result_file, simflags=simflags, @@ -1060,7 +1065,7 @@ def simulate( # check for an empty (=> 0B) result file which indicates a crash of the model executable # see: https://github.com/OpenModelica/OMPython/issues/261 # https://github.com/OpenModelica/OpenModelica/issues/13829 - if self._result_file.stat().st_size == 0: + if self._result_file.size() == 0: self._result_file.unlink() raise ModelicaSystemError("Empty result file - this indicates a crash of the model executable!") @@ -1109,7 +1114,7 @@ def getSolutions( raise ModelicaSystemError("No result file found. Run simulate() first.") result_file = self._result_file else: - result_file = pathlib.Path(resultfile) + result_file = self._getconn.omcpath(resultfile) # check for result file exits if not result_file.is_file(): @@ -1399,11 +1404,9 @@ def setInputs( else: raise ModelicaSystemError(f"Data cannot be evaluated for {repr(key)}: {repr(val)}") - self._has_inputs = True - return True - def _createCSVData(self, csvfile: Optional[pathlib.Path] = None) -> pathlib.Path: + def _createCSVData(self, csvfile: Optional[OMCPath] = None) -> OMCPath: """ Create a csv file with inputs for the simulation/optimization of the model. If csvfile is provided as argument, this file is used; else a generic file name is created. @@ -1578,15 +1581,15 @@ def linearize(self, lintime: Optional[float] = None, simflags: Optional[str] = N timeout=timeout, ) - overrideLinearFile = self.getWorkDirectory() / f'{self._model_name}_override_linear.txt' - - with open(file=overrideLinearFile, mode="w", encoding="utf-8") as fh: - for key1, value1 in self._override_variables.items(): - fh.write(f"{key1}={value1}\n") - for key2, value2 in self._linearization_options.items(): - fh.write(f"{key2}={value2}\n") + override_content = ( + "\n".join([f"{key}={value}" for key, value in self._override_variables.items()]) + + "\n".join([f"{key}={value}" for key, value in self._linearization_options.items()]) + + "\n" + ) + override_file = self.getWorkDirectory() / f'{self._model_name}_override_linear.txt' + override_file.write_text(override_content) - om_cmd.arg_set(key="overrideFile", val=overrideLinearFile.as_posix()) + om_cmd.arg_set(key="overrideFile", val=override_file.as_posix()) if self._inputs: for key in self._inputs: @@ -1614,7 +1617,7 @@ def linearize(self, lintime: Optional[float] = None, simflags: Optional[str] = N returncode = om_cmd.run() if returncode != 0: raise ModelicaSystemError(f"Linearize failed with return code: {returncode}") - if not linear_file.exists(): + if not linear_file.is_file(): raise ModelicaSystemError(f"Linearization failed: {linear_file} not found!") self._simulated = True From 087db3db2af7ab6b432f9a8ae46dbb36afa0c2e2 Mon Sep 17 00:00:00 2001 From: syntron Date: Sat, 9 Aug 2025 20:41:33 +0200 Subject: [PATCH 56/67] [ModelicaSystem] improve result file handling in simulate() --- OMPython/ModelicaSystem.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/OMPython/ModelicaSystem.py b/OMPython/ModelicaSystem.py index 442a7548..17652828 100644 --- a/OMPython/ModelicaSystem.py +++ b/OMPython/ModelicaSystem.py @@ -1040,10 +1040,12 @@ def simulate( if resultfile is None: # default result file generated by OM self._result_file = self.getWorkDirectory() / f"{self._model_name}_res.mat" - elif os.path.exists(resultfile): - self._result_file = self._getconn.omcpath(resultfile) + elif isinstance(resultfile, OMCPath): + self._result_file = resultfile else: - self._result_file = self.getWorkDirectory() / resultfile + self._result_file = self._getconn.omcpath(resultfile) + if not self._result_file.is_absolute(): + self._result_file = self.getWorkDirectory() / resultfile if not isinstance(self._result_file, OMCPath): raise ModelicaSystemError(f"Invalid result file path: {self._result_file} - must be an OMCPath object!") From 9695f9d25455c728770fe663bcf862e139855891 Mon Sep 17 00:00:00 2001 From: syntron Date: Sat, 12 Jul 2025 00:29:55 +0200 Subject: [PATCH 57/67] [ModelicaSystemCmd] use OMCPath for file system interactions --- OMPython/OMCSession.py | 23 +++++++++++++---------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/OMPython/OMCSession.py b/OMPython/OMCSession.py index 299a1d10..39bd20b7 100644 --- a/OMPython/OMCSession.py +++ b/OMPython/OMCSession.py @@ -458,18 +458,18 @@ class OMCPathCompatibilityWindows(pathlib.WindowsPath, OMCPathCompatibility): @dataclasses.dataclass class OMCSessionRunData: - # TODO: rename OMCExcecutableModelData """ Data class to store the command line data for running a model executable in the OMC environment. All data should be defined for the environment, where OMC is running (local, docker or WSL) """ - # cmd_path is the expected working directory + # cmd_path is based on the selected OMCProcess definition cmd_path: str cmd_model_name: str # command line arguments for the model executable cmd_args: list[str] # result file with the simulation output + # cmd_result_path is based on the selected OMCProcess definition cmd_result_path: str # command prefix data (as list of strings); needed for docker or WSL @@ -598,13 +598,13 @@ def omcpath_tempdir(self, tempdir_base: Optional[OMCPath] = None) -> OMCPath: return tempdir - def omc_run_data_update(self, omc_run_data: OMCSessionRunData) -> OMCSessionRunData: + def omc_run_data_update(self, omc_run_data: OMCSessionRunData, session: OMCSessionZMQ) -> OMCSessionRunData: """ Modify data based on the selected OMCProcess implementation. Needs to be implemented in the subclasses. """ - return self.omc_process.omc_run_data_update(omc_run_data=omc_run_data) + return self.omc_process.omc_run_data_update(omc_run_data=omc_run_data, session=self) @staticmethod def run_model_executable(cmd_run_data: OMCSessionRunData) -> int: @@ -652,8 +652,11 @@ def execute(self, command: str): return self.sendExpression(command, parsed=False) def sendExpression(self, command: str, parsed: bool = True) -> Any: + """ + Send an expression to the OMC server and return the result. + """ if self.omc_zmq is None: - raise OMCSessionException("No OMC running. Create a new instance of OMCSessionZMQ!") + raise OMCSessionException("No OMC running. Create a new instance of OMCProcess!") logger.debug("sendExpression(%r, parsed=%r)", command, parsed) @@ -831,7 +834,7 @@ def _get_portfile_path(self) -> Optional[pathlib.Path]: return portfile_path @abc.abstractmethod - def omc_run_data_update(self, omc_run_data: OMCSessionRunData) -> OMCSessionRunData: + def omc_run_data_update(self, omc_run_data: OMCSessionRunData, session: OMCSessionZMQ) -> OMCSessionRunData: """ Update the OMCSessionRunData object based on the selected OMCProcess implementation. @@ -849,7 +852,7 @@ def __init__( super().__init__() self._omc_port = omc_port - def omc_run_data_update(self, omc_run_data: OMCSessionRunData) -> OMCSessionRunData: + def omc_run_data_update(self, omc_run_data: OMCSessionRunData, session: OMCSessionZMQ) -> OMCSessionRunData: """ Update the OMCSessionRunData object based on the selected OMCProcess implementation. """ @@ -936,7 +939,7 @@ def _omc_port_get(self) -> str: return port - def omc_run_data_update(self, omc_run_data: OMCSessionRunData) -> OMCSessionRunData: + def omc_run_data_update(self, omc_run_data: OMCSessionRunData, session: OMCSessionZMQ) -> OMCSessionRunData: """ Update the OMCSessionRunData object based on the selected OMCProcess implementation. """ @@ -1084,7 +1087,7 @@ def get_docker_container_id(self) -> str: return self._dockerCid - def omc_run_data_update(self, omc_run_data: OMCSessionRunData) -> OMCSessionRunData: + def omc_run_data_update(self, omc_run_data: OMCSessionRunData, session: OMCSessionZMQ) -> OMCSessionRunData: """ Update the OMCSessionRunData object based on the selected OMCProcess implementation. """ @@ -1396,7 +1399,7 @@ def _omc_port_get(self) -> str: return port - def omc_run_data_update(self, omc_run_data: OMCSessionRunData) -> OMCSessionRunData: + def omc_run_data_update(self, omc_run_data: OMCSessionRunData, session: OMCSessionZMQ) -> OMCSessionRunData: """ Update the OMCSessionRunData object based on the selected OMCProcess implementation. """ From 197c2a43dfad017980e8c7d94b3440e3125694d7 Mon Sep 17 00:00:00 2001 From: syntron Date: Sat, 12 Jul 2025 00:43:11 +0200 Subject: [PATCH 58/67] [OMCProcessDockerHelper] implement omc_run_data_update() --- OMPython/OMCSession.py | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/OMPython/OMCSession.py b/OMPython/OMCSession.py index 39bd20b7..3e72822b 100644 --- a/OMPython/OMCSession.py +++ b/OMPython/OMCSession.py @@ -1091,7 +1091,24 @@ def omc_run_data_update(self, omc_run_data: OMCSessionRunData, session: OMCSessi """ Update the OMCSessionRunData object based on the selected OMCProcess implementation. """ - raise OMCSessionException("OMCProcessDocker* does not support omc_run_data_update()!") + omc_run_data_copy = dataclasses.replace(omc_run_data) + + omc_run_data_copy.cmd_prefix = ( + [ + "docker", "exec", + "--user", str(self._getuid()), + ] + + self._dockerExtraArgs + + [self._dockerCid] + ) + + cmd_path = session.omcpath(omc_run_data_copy.cmd_path) + cmd_model_executable = cmd_path / omc_run_data_copy.cmd_model_name + if not cmd_model_executable.is_file(): + raise OMCSessionException(f"Application file path not found: {cmd_model_executable}") + omc_run_data_copy.cmd_model_executable = cmd_model_executable.as_posix() + + return omc_run_data_copy class OMCProcessDocker(OMCProcessDockerHelper): From ed73392edb3e92ec04bad3cdf17427af5fe8069a Mon Sep 17 00:00:00 2001 From: syntron Date: Sat, 12 Jul 2025 00:43:25 +0200 Subject: [PATCH 59/67] [OMCProcessWSL] implement omc_run_data_update() --- OMPython/OMCSession.py | 37 ++++++++++++++++++++++++++----------- 1 file changed, 26 insertions(+), 11 deletions(-) diff --git a/OMPython/OMCSession.py b/OMPython/OMCSession.py index 3e72822b..755c6625 100644 --- a/OMPython/OMCSession.py +++ b/OMPython/OMCSession.py @@ -1353,25 +1353,30 @@ def __init__( super().__init__(timeout=timeout) - # get wsl base command - self._wsl_cmd = ['wsl'] - if isinstance(wsl_distribution, str): - self._wsl_cmd += ['--distribution', wsl_distribution] - if isinstance(wsl_user, str): - self._wsl_cmd += ['--user', wsl_user] - self._wsl_cmd += ['--'] - # where to find OpenModelica self._wsl_omc = wsl_omc + # store WSL distribution and user + self._wsl_distribution = wsl_distribution + self._wsl_user = wsl_user # start up omc executable, which is waiting for the ZMQ connection self._omc_process = self._omc_process_get() # connect to the running omc instance using ZMQ self._omc_port = self._omc_port_get() + def _wsl_cmd(self) -> list[str]: # get wsl base command + wsl_cmd = ['wsl'] + if isinstance(self._wsl_distribution, str): + wsl_cmd += ['--distribution', self._wsl_distribution] + if isinstance(self._wsl_user, str): + wsl_cmd += ['--user', self._wsl_user] + wsl_cmd += ['--'] + + return wsl_cmd + def _omc_process_get(self) -> subprocess.Popen: my_env = os.environ.copy() - omc_command = self._wsl_cmd + [ + omc_command = self._wsl_cmd() + [ self._wsl_omc, "--locale=C", "--interactive=zmq", @@ -1394,7 +1399,7 @@ def _omc_port_get(self) -> str: omc_portfile_path = self._get_portfile_path() if omc_portfile_path is not None: output = subprocess.check_output( - args=self._wsl_cmd + ["cat", omc_portfile_path.as_posix()], + args=self._wsl_cmd() + ["cat", omc_portfile_path.as_posix()], stderr=subprocess.DEVNULL, ) port = output.decode().strip() @@ -1420,4 +1425,14 @@ def omc_run_data_update(self, omc_run_data: OMCSessionRunData, session: OMCSessi """ Update the OMCSessionRunData object based on the selected OMCProcess implementation. """ - raise OMCSessionException("OMCProcessWSL does not support omc_run_data_update()!") + omc_run_data_copy = dataclasses.replace(omc_run_data) + + omc_run_data_copy.cmd_prefix = self._wsl_cmd() + + cmd_path = session.omcpath(omc_run_data_copy.cmd_path) + cmd_model_executable = cmd_path / omc_run_data_copy.cmd_model_name + if not cmd_model_executable.is_file(): + raise OMCSessionException(f"Application file path not found: {cmd_model_executable}") + omc_run_data_copy.cmd_model_executable = cmd_model_executable.as_posix() + + return omc_run_data_copy From 6a998ba6c730ed1a668acb9c6074d3795ad3570a Mon Sep 17 00:00:00 2001 From: syntron Date: Sat, 12 Jul 2025 12:57:34 +0200 Subject: [PATCH 60/67] [OMCProcessDockerHelper] define work directory in docker --- OMPython/OMCSession.py | 1 + 1 file changed, 1 insertion(+) diff --git a/OMPython/OMCSession.py b/OMPython/OMCSession.py index 755c6625..6e49d242 100644 --- a/OMPython/OMCSession.py +++ b/OMPython/OMCSession.py @@ -1097,6 +1097,7 @@ def omc_run_data_update(self, omc_run_data: OMCSessionRunData, session: OMCSessi [ "docker", "exec", "--user", str(self._getuid()), + "--workdir", omc_run_data_copy.cmd_path, ] + self._dockerExtraArgs + [self._dockerCid] From a0e6582fc055c4e246a1796514423409c0e9a984 Mon Sep 17 00:00:00 2001 From: syntron Date: Sat, 12 Jul 2025 12:58:41 +0200 Subject: [PATCH 61/67] [OMCProcessWSL] define work directory for WSL --- OMPython/OMCSession.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/OMPython/OMCSession.py b/OMPython/OMCSession.py index 6e49d242..bdecdd19 100644 --- a/OMPython/OMCSession.py +++ b/OMPython/OMCSession.py @@ -1364,12 +1364,15 @@ def __init__( # connect to the running omc instance using ZMQ self._omc_port = self._omc_port_get() - def _wsl_cmd(self) -> list[str]: # get wsl base command + def _wsl_cmd(self, wsl_cwd: Optional[str] = None) -> list[str]: + # get wsl base command wsl_cmd = ['wsl'] if isinstance(self._wsl_distribution, str): wsl_cmd += ['--distribution', self._wsl_distribution] if isinstance(self._wsl_user, str): wsl_cmd += ['--user', self._wsl_user] + if isinstance(wsl_cwd, str): + wsl_cmd += ['--cd', wsl_cwd] wsl_cmd += ['--'] return wsl_cmd @@ -1428,7 +1431,7 @@ def omc_run_data_update(self, omc_run_data: OMCSessionRunData, session: OMCSessi """ omc_run_data_copy = dataclasses.replace(omc_run_data) - omc_run_data_copy.cmd_prefix = self._wsl_cmd() + omc_run_data_copy.cmd_prefix = self._wsl_cmd(wsl_cwd=omc_run_data.cmd_path) cmd_path = session.omcpath(omc_run_data_copy.cmd_path) cmd_model_executable = cmd_path / omc_run_data_copy.cmd_model_name From 9556ff799a7a4e78aefe993ab70cfd0d0485b244 Mon Sep 17 00:00:00 2001 From: syntron Date: Sat, 12 Jul 2025 12:59:57 +0200 Subject: [PATCH 62/67] [OMCSessionRunData] update docstring and comments --- OMPython/OMCSession.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/OMPython/OMCSession.py b/OMPython/OMCSession.py index bdecdd19..1d043613 100644 --- a/OMPython/OMCSession.py +++ b/OMPython/OMCSession.py @@ -463,13 +463,12 @@ class OMCSessionRunData: All data should be defined for the environment, where OMC is running (local, docker or WSL) """ - # cmd_path is based on the selected OMCProcess definition + # cmd_path is the expected working directory cmd_path: str cmd_model_name: str # command line arguments for the model executable cmd_args: list[str] # result file with the simulation output - # cmd_result_path is based on the selected OMCProcess definition cmd_result_path: str # command prefix data (as list of strings); needed for docker or WSL From c14b7fdb5fed01325c134d44e08714925d67974d Mon Sep 17 00:00:00 2001 From: syntron Date: Sat, 12 Jul 2025 13:01:27 +0200 Subject: [PATCH 63/67] [ModelicaSystem] allow for non local execution, i.e. docker or WSL --- OMPython/ModelicaSystem.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/OMPython/ModelicaSystem.py b/OMPython/ModelicaSystem.py index 64f4344e..8ed2a93f 100644 --- a/OMPython/ModelicaSystem.py +++ b/OMPython/ModelicaSystem.py @@ -43,7 +43,8 @@ import warnings import xml.etree.ElementTree as ET -from OMPython.OMCSession import OMCSessionException, OMCSessionRunData, OMCSessionZMQ, OMCProcessLocal, OMCPath +from OMPython.OMCSession import (OMCSessionException, OMCSessionRunData, OMCSessionZMQ, + OMCProcess, OMCPath) # define logger using the current module name as ID logger = logging.getLogger(__name__) @@ -261,7 +262,7 @@ def __init__( variableFilter: Optional[str] = None, customBuildDirectory: Optional[str | os.PathLike] = None, omhome: Optional[str] = None, - omc_process: Optional[OMCProcessLocal] = None, + omc_process: Optional[OMCProcess] = None, build: bool = True, ) -> None: """Initialize, load and build a model. @@ -326,8 +327,6 @@ def __init__( self._linearized_states: list[str] = [] # linearization states list if omc_process is not None: - if not isinstance(omc_process, OMCProcessLocal): - raise ModelicaSystemError("Invalid (local) omc process definition provided!") self._getconn = OMCSessionZMQ(omc_process=omc_process) else: self._getconn = OMCSessionZMQ(omhome=omhome) From 358e28692fe48a965547eb19dbdbd12651efb63e Mon Sep 17 00:00:00 2001 From: syntron Date: Sat, 12 Jul 2025 13:42:05 +0200 Subject: [PATCH 64/67] [test_ModelicaSystem] include test of ModelicaSystem using docker --- tests/test_ModelicaSystem.py | 46 +++++++++++++++++++++++++++++++++--- 1 file changed, 43 insertions(+), 3 deletions(-) diff --git a/tests/test_ModelicaSystem.py b/tests/test_ModelicaSystem.py index e782489e..4b4f8c51 100644 --- a/tests/test_ModelicaSystem.py +++ b/tests/test_ModelicaSystem.py @@ -2,20 +2,36 @@ import os import pathlib import pytest +import sys import tempfile import numpy as np +skip_on_windows = pytest.mark.skipif( + sys.platform.startswith("win"), + reason="OpenModelica Docker image is Linux-only; skipping on Windows.", +) + +skip_python_older_312 = pytest.mark.skipif( + sys.version_info < (3, 12), + reason="OMCPath(non-local) only working for Python >= 3.12.", +) + @pytest.fixture -def model_firstorder(tmp_path): - mod = tmp_path / "M.mo" - mod.write_text("""model M +def model_firstorder_content(): + return ("""model M Real x(start = 1, fixed = true); parameter Real a = -1; equation der(x) = x*a; end M; """) + + +@pytest.fixture +def model_firstorder(tmp_path, model_firstorder_content): + mod = tmp_path / "M.mo" + mod.write_text(model_firstorder_content) return mod @@ -112,9 +128,33 @@ def test_customBuildDirectory(tmp_path, model_firstorder): assert result_file.is_file() +@skip_on_windows +@skip_python_older_312 +def test_getSolutions_docker(model_firstorder_content): + omcp = OMPython.OMCProcessDocker(docker="openmodelica/openmodelica:v1.25.0-minimal") + omc = OMPython.OMCSessionZMQ(omc_process=omcp) + + modelpath = omc.omcpath_tempdir() / 'M.mo' + modelpath.write_text(model_firstorder_content) + + file_path = pathlib.Path(modelpath) + mod = OMPython.ModelicaSystem( + fileName=file_path, + modelName="M", + omc_process=omc.omc_process, + ) + + _run_getSolutions(mod) + + def test_getSolutions(model_firstorder): filePath = model_firstorder.as_posix() mod = OMPython.ModelicaSystem(filePath, "M") + + _run_getSolutions(mod) + + +def _run_getSolutions(mod): x0 = 1 a = -1 tau = -1 / a From c95323007c0e2555337e5d4c73c338156f49599c Mon Sep 17 00:00:00 2001 From: syntron Date: Sun, 27 Jul 2025 00:32:02 +0200 Subject: [PATCH 65/67] [OMCSessionZMQ] no session for omc_run_data_update() --- OMPython/OMCSession.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/OMPython/OMCSession.py b/OMPython/OMCSession.py index 1d043613..0383871b 100644 --- a/OMPython/OMCSession.py +++ b/OMPython/OMCSession.py @@ -597,7 +597,7 @@ def omcpath_tempdir(self, tempdir_base: Optional[OMCPath] = None) -> OMCPath: return tempdir - def omc_run_data_update(self, omc_run_data: OMCSessionRunData, session: OMCSessionZMQ) -> OMCSessionRunData: + def omc_run_data_update(self, omc_run_data: OMCSessionRunData) -> OMCSessionRunData: """ Modify data based on the selected OMCProcess implementation. From 6b50282533760f71048a2ece6c5da5a62e9a1352 Mon Sep 17 00:00:00 2001 From: syntron Date: Sun, 27 Jul 2025 00:42:01 +0200 Subject: [PATCH 66/67] [OMCProcess] remove session argument for OMCProcess.omc_run_data_update() * no dependency loop OMCsessionZMQ => OMCProcess* => OMCSessionZMQ * check if model executable exists will be handled via ModelicaSystemCmd --- OMPython/OMCSession.py | 20 ++++++++------------ 1 file changed, 8 insertions(+), 12 deletions(-) diff --git a/OMPython/OMCSession.py b/OMPython/OMCSession.py index 0383871b..df505f80 100644 --- a/OMPython/OMCSession.py +++ b/OMPython/OMCSession.py @@ -603,7 +603,7 @@ def omc_run_data_update(self, omc_run_data: OMCSessionRunData) -> OMCSessionRunD Needs to be implemented in the subclasses. """ - return self.omc_process.omc_run_data_update(omc_run_data=omc_run_data, session=self) + return self.omc_process.omc_run_data_update(omc_run_data=omc_run_data) @staticmethod def run_model_executable(cmd_run_data: OMCSessionRunData) -> int: @@ -833,7 +833,7 @@ def _get_portfile_path(self) -> Optional[pathlib.Path]: return portfile_path @abc.abstractmethod - def omc_run_data_update(self, omc_run_data: OMCSessionRunData, session: OMCSessionZMQ) -> OMCSessionRunData: + def omc_run_data_update(self, omc_run_data: OMCSessionRunData) -> OMCSessionRunData: """ Update the OMCSessionRunData object based on the selected OMCProcess implementation. @@ -851,7 +851,7 @@ def __init__( super().__init__() self._omc_port = omc_port - def omc_run_data_update(self, omc_run_data: OMCSessionRunData, session: OMCSessionZMQ) -> OMCSessionRunData: + def omc_run_data_update(self, omc_run_data: OMCSessionRunData) -> OMCSessionRunData: """ Update the OMCSessionRunData object based on the selected OMCProcess implementation. """ @@ -938,7 +938,7 @@ def _omc_port_get(self) -> str: return port - def omc_run_data_update(self, omc_run_data: OMCSessionRunData, session: OMCSessionZMQ) -> OMCSessionRunData: + def omc_run_data_update(self, omc_run_data: OMCSessionRunData) -> OMCSessionRunData: """ Update the OMCSessionRunData object based on the selected OMCProcess implementation. """ @@ -1086,7 +1086,7 @@ def get_docker_container_id(self) -> str: return self._dockerCid - def omc_run_data_update(self, omc_run_data: OMCSessionRunData, session: OMCSessionZMQ) -> OMCSessionRunData: + def omc_run_data_update(self, omc_run_data: OMCSessionRunData) -> OMCSessionRunData: """ Update the OMCSessionRunData object based on the selected OMCProcess implementation. """ @@ -1102,10 +1102,8 @@ def omc_run_data_update(self, omc_run_data: OMCSessionRunData, session: OMCSessi + [self._dockerCid] ) - cmd_path = session.omcpath(omc_run_data_copy.cmd_path) + cmd_path = pathlib.PurePosixPath(omc_run_data_copy.cmd_path) cmd_model_executable = cmd_path / omc_run_data_copy.cmd_model_name - if not cmd_model_executable.is_file(): - raise OMCSessionException(f"Application file path not found: {cmd_model_executable}") omc_run_data_copy.cmd_model_executable = cmd_model_executable.as_posix() return omc_run_data_copy @@ -1424,7 +1422,7 @@ def _omc_port_get(self) -> str: return port - def omc_run_data_update(self, omc_run_data: OMCSessionRunData, session: OMCSessionZMQ) -> OMCSessionRunData: + def omc_run_data_update(self, omc_run_data: OMCSessionRunData) -> OMCSessionRunData: """ Update the OMCSessionRunData object based on the selected OMCProcess implementation. """ @@ -1432,10 +1430,8 @@ def omc_run_data_update(self, omc_run_data: OMCSessionRunData, session: OMCSessi omc_run_data_copy.cmd_prefix = self._wsl_cmd(wsl_cwd=omc_run_data.cmd_path) - cmd_path = session.omcpath(omc_run_data_copy.cmd_path) + cmd_path = pathlib.PurePosixPath(omc_run_data_copy.cmd_path) cmd_model_executable = cmd_path / omc_run_data_copy.cmd_model_name - if not cmd_model_executable.is_file(): - raise OMCSessionException(f"Application file path not found: {cmd_model_executable}") omc_run_data_copy.cmd_model_executable = cmd_model_executable.as_posix() return omc_run_data_copy From eea4a5dd89cfdd0d42258366e4e2819d3e8fb4b3 Mon Sep 17 00:00:00 2001 From: syntron Date: Sun, 3 Aug 2025 22:23:02 +0200 Subject: [PATCH 67/67] [ModelicaSystem.buildModel] check if executable exists via ModelicaSystemCmd --- OMPython/ModelicaSystem.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/OMPython/ModelicaSystem.py b/OMPython/ModelicaSystem.py index 8ed2a93f..ee5db62d 100644 --- a/OMPython/ModelicaSystem.py +++ b/OMPython/ModelicaSystem.py @@ -449,6 +449,20 @@ def buildModel(self, variableFilter: Optional[str] = None): buildModelResult = self._requestApi("buildModel", self._model_name, properties=varFilter) logger.debug("OM model build result: %s", buildModelResult) + # check if the executable exists ... + om_cmd = ModelicaSystemCmd( + session=self._getconn, + runpath=self.getWorkDirectory(), + modelname=self._model_name, + timeout=5.0, + ) + # ... by running it - output help for command help + om_cmd.arg_set(key="help", val="help") + cmd_definition = om_cmd.definition() + returncode = self._getconn.run_model_executable(cmd_run_data=cmd_definition) + if returncode != 0: + raise ModelicaSystemError("Model executable not working!") + xml_file = self._getconn.omcpath(buildModelResult[0]).parent / buildModelResult[1] self._xmlparse(xml_file=xml_file)