Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
58 commits
Select commit Hold shift + click to select a range
ac2cceb
[ModelicaSystem] fix rebase fallout 2
syntron Jul 7, 2025
5adca7c
[test_ModelicaSystem] fix test_customBuildDirectory()
syntron Jul 11, 2025
e613127
[ModelicaSystem] fix blank lines (flake8)
syntron Jul 22, 2025
e7f1bfb
[test_optimization] fix due to OMCPath usage
syntron Aug 6, 2025
c587ab2
[test_FMIExport] fix due to OMCPath usage
syntron Aug 6, 2025
bbf9759
[ModelicaSystem] improve definition of getSolution
syntron Aug 9, 2025
94d5381
[ModelicaSystem] use OMCPath for nearly all file system interactions
syntron Jul 11, 2025
e88fe20
[ModelicaSystem] improve result file handling in simulate()
syntron Aug 9, 2025
c611d1d
[OMCProcess*] use pathlib
syntron Aug 6, 2025
a6bbdba
[OMCSessionRunData] add new class to store all information about a mo…
syntron Jul 24, 2025
659183c
[OMCSessionRunData] use class to move run of model executable to OMSe…
syntron Jul 24, 2025
091144b
[test_ModelicaSystemCmd] fix test
syntron Jul 25, 2025
fc54173
[OMCSessionRunData] add to __init__
syntron Jul 25, 2025
1caea3a
[test_ModelicaSystemCmd] fix test (again)
syntron Aug 12, 2025
2859dc9
[ModelicaSystemDoE] add class
syntron Jun 24, 2025
e4fc5e0
[__init__] add class ModelicaSystemDoE
syntron Jun 24, 2025
8f74dfc
[test_ModelicaSystemDoE] add test
syntron Jun 24, 2025
ce6168d
[ModelicaSystemDoE] add docstrings
syntron Jun 25, 2025
3fd5bbd
[ModelicaSystemDoE] define dict keys as constants
syntron Jun 25, 2025
5b5a317
[ModelicaSystemDoE] build model after all structural parameters are d…
syntron Jun 25, 2025
5459566
[ModelicaSystemDoE] cleanup prepare() / rename variables
syntron Jun 25, 2025
10a9e40
[ModelicaSystemDoE] cleanup simulate() / rename variables
syntron Jun 25, 2025
7b1e06f
[ModelicaSystemDoE] cleanup get_solutions() / rename variables
syntron Jun 25, 2025
0c09b29
[test_ModelicaSystemDoE] update test
syntron Jun 25, 2025
7ade513
[ModelicaSystemDoE] add example to show the usage
syntron Jun 25, 2025
7381add
add pandas as new dependency (use in ModelicaSystemDoE)
syntron Jun 25, 2025
3e2995a
[test_ModelicaSystemDoE] fix mypy
syntron Jun 25, 2025
f1d866c
add pandas to requirements in pyproject.toml
syntron Jun 25, 2025
0752f3a
[ModelicaSystemDoE] rename class constants
syntron Jun 25, 2025
7d42ea2
[ModelicaSystemDoE] remove dependency on pandas
syntron Jun 28, 2025
55aa6e1
[ModelicaSystemDoE.simulate] fix percent of tasks left
syntron Jun 28, 2025
c333ee2
[ModelicaSystemDoE.prepare] do not convert all non-structural paramet…
syntron Jun 28, 2025
55712b1
[ModelicaSystemDoE] update set parameter expressions for str and bool
syntron Jun 28, 2025
af4252d
[ModelicaSystemDoE] rename class constants
syntron Jun 28, 2025
9a6dd90
[ModelicaSystemDoE] fix bool comparison
syntron Jun 28, 2025
42e3df6
[ModelicaSystemDoE] remove unused code
syntron Jun 28, 2025
68af6c7
[ModelicaSystemDoE] fix rebase fallout
syntron Jul 9, 2025
2c0f632
[ModelicaSystemDoE] fix rebase fallout
syntron Oct 15, 2025
4a6e413
Merge branch 'OMCProcess_pathlib' into ModelicaSystemCmd_use_OMCPath
syntron Oct 30, 2025
32a5f10
Merge branch 'OMCSession_executable' into ModelicaSystemCmd_use_OMCPath
syntron Oct 30, 2025
997b237
[ModelicaSystemCmd] use OMCPath for file system interactions
syntron Jul 11, 2025
ffeee42
[OMCProcessDockerHelper] implement omc_run_data_update()
syntron Jul 11, 2025
8fd30a2
[OMCProcessWSL] implement omc_run_data_update()
syntron Jul 11, 2025
d60f3a0
[OMCProcessDockerHelper] define work directory in docker
syntron Jul 12, 2025
8bf42f2
[OMCProcessWSL] define work directory for WSL
syntron Jul 12, 2025
9b3d28f
[OMCSessionRunData] update docstring and comments
syntron Jul 12, 2025
cd35090
[test_ModelicaSystem] include test of ModelicaSystem using docker
syntron Jul 12, 2025
7dd8e87
[OMCSessionZMQ] no session for omc_run_data_update()
syntron Jul 26, 2025
4331df1
[OMCProcess] remove session argument for OMCProcess.omc_run_data_upda…
syntron Jul 26, 2025
1d55f4e
[ModelicaSystem.buildModel] check if executable exists via ModelicaSy…
syntron Aug 3, 2025
297d2f5
[ModelicaSystem*] rebase cleanup
syntron Oct 30, 2025
f0b3105
Merge branch 'ModelicaSystemDoE' into ModelicaSystemDoE_use_OMCPath
syntron Oct 30, 2025
b9b4189
[ModelicaSystemDoE] rename variables & cleanup
syntron Aug 6, 2025
5bb7799
[ModelicaSystemDoE] update variable handling / remove variables not n…
syntron Aug 7, 2025
c1f6a9d
[test_ModelicaSystemDoE] update test definition for local/docker/WSL
syntron Aug 7, 2025
11106d6
[ModelicaSystemDoe] do not limit to OMCProcessLocal
syntron Aug 9, 2025
788a197
[ModelicaSystem] rebase fix
syntron Oct 30, 2025
8a4901d
[__init__] rewrite - make it easier to modify imports
syntron Oct 15, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
603 changes: 496 additions & 107 deletions OMPython/ModelicaSystem.py

Large diffs are not rendered by default.

235 changes: 215 additions & 20 deletions OMPython/OMCSession.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,11 +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
Expand Down Expand Up @@ -456,6 +459,47 @@ class OMCPathCompatibilityWindows(pathlib.WindowsPath, OMCPathCompatibility):
OMCPath = OMCPathReal


@dataclasses.dataclass
class OMCSessionRunData:
"""
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__(
Expand Down Expand Up @@ -556,15 +600,65 @@ def omcpath_tempdir(self, tempdir_base: Optional[OMCPath] = None) -> OMCPath:

return tempdir

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)

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)

Expand Down Expand Up @@ -659,7 +753,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,
Expand Down Expand Up @@ -747,6 +841,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):
"""
Expand All @@ -760,6 +863,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):
"""
Expand All @@ -769,7 +878,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)
Expand All @@ -782,7 +891,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)
Expand Down Expand Up @@ -844,6 +953,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):
"""
Expand All @@ -854,7 +1005,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:
Expand All @@ -864,7 +1015,7 @@ def __init__(
dockerExtraArgs = []

self._dockerExtraArgs = dockerExtraArgs
self._dockerOpenModelicaPath = dockerOpenModelicaPath
self._dockerOpenModelicaPath = pathlib.PurePosixPath(dockerOpenModelicaPath)
self._dockerNetwork = dockerNetwork

self._interactivePort = port
Expand Down Expand Up @@ -959,6 +1110,28 @@ 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.
"""
omc_run_data_copy = dataclasses.replace(omc_run_data)

omc_run_data_copy.cmd_prefix = (
[
"docker", "exec",
"--user", str(self._getuid()),
"--workdir", omc_run_data_copy.cmd_path,
]
+ self._dockerExtraArgs
+ [self._dockerCid]
)

cmd_path = pathlib.PurePosixPath(omc_run_data_copy.cmd_path)
cmd_model_executable = cmd_path / omc_run_data_copy.cmd_model_name
omc_run_data_copy.cmd_model_executable = cmd_model_executable.as_posix()

return omc_run_data_copy


class OMCProcessDocker(OMCProcessDockerHelper):
"""
Expand All @@ -970,7 +1143,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:
Expand Down Expand Up @@ -1053,7 +1226,7 @@ def _docker_omc_cmd(
]
+ self._dockerExtraArgs
+ dockerNetworkStr
+ [self._docker, self._dockerOpenModelicaPath]
+ [self._docker, self._dockerOpenModelicaPath.as_posix()]
+ omc_path_and_args_list
+ extraFlags)

Expand Down Expand Up @@ -1113,7 +1286,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:
Expand Down Expand Up @@ -1165,7 +1338,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)

Expand Down Expand Up @@ -1211,25 +1384,33 @@ 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, 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

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",
Expand All @@ -1252,7 +1433,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()
Expand All @@ -1273,3 +1454,17 @@ 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.
"""
omc_run_data_copy = dataclasses.replace(omc_run_data)

omc_run_data_copy.cmd_prefix = self._wsl_cmd(wsl_cwd=omc_run_data.cmd_path)

cmd_path = pathlib.PurePosixPath(omc_run_data_copy.cmd_path)
cmd_model_executable = cmd_path / omc_run_data_copy.cmd_model_name
omc_run_data_copy.cmd_model_executable = cmd_model_executable.as_posix()

return omc_run_data_copy
Loading