From 2c728a1601d1930c1ee2c657b594de22031cf606 Mon Sep 17 00:00:00 2001 From: syntron Date: Sun, 18 Jan 2026 19:17:48 +0100 Subject: [PATCH 1/4] [ModelExecution*] create classes to handle model execution * rename ModelicaSystemCmd => ModelExecutionCmd * rename OMCSessionRunData => ModelExecutionData * create class ModelExecutionException * move some code: * OMCSession.omc_run_data_update() => merge into ModelExecutionCmd.define() * OMCSession.run_model_executable() => ModelExecutionData.run() --- OMPython/ModelicaSystem.py | 152 ++++++++++++++------- OMPython/OMCSession.py | 273 +++++++++++++------------------------ OMPython/__init__.py | 14 +- 3 files changed, 204 insertions(+), 235 deletions(-) diff --git a/OMPython/ModelicaSystem.py b/OMPython/ModelicaSystem.py index 03c24b51..b352176b 100644 --- a/OMPython/ModelicaSystem.py +++ b/OMPython/ModelicaSystem.py @@ -21,8 +21,10 @@ import numpy as np from OMPython.OMCSession import ( + ModelExecutionData, + ModelExecutionException, + OMCSessionException, - OMCSessionRunData, OMCSession, OMCSessionLocal, OMCPath, @@ -34,7 +36,7 @@ class ModelicaSystemError(Exception): """ - Exception used in ModelicaSystem and ModelicaSystemCmd classes. + Exception used in ModelicaSystem classes. """ @@ -89,7 +91,7 @@ def __getitem__(self, index: int): return {0: self.A, 1: self.B, 2: self.C, 3: self.D}[index] -class ModelicaSystemCmd: +class ModelExecutionCmd: """ All information about a compiled model executable. This should include data about all structured parameters, i.e. parameters which need a recompilation of the model. All non-structured parameters can be easily changed without @@ -98,16 +100,22 @@ class ModelicaSystemCmd: def __init__( self, - session: OMCSession, - runpath: OMCPath, - modelname: Optional[str] = None, + runpath: os.PathLike, + cmd_prefix: list[str], + cmd_local: bool = False, + cmd_windows: bool = False, + timeout: float = 10.0, + model_name: Optional[str] = None, ) -> None: - if modelname is None: - raise ModelicaSystemError("Missing model name!") + if model_name is None: + raise ModelExecutionException("Missing model name!") - self._session = session - self._runpath = runpath - self._model_name = modelname + self._cmd_local = cmd_local + self._cmd_windows = cmd_windows + self._cmd_prefix = cmd_prefix + self._runpath = pathlib.PurePosixPath(runpath) + self._model_name = model_name + self._timeout = timeout # dictionaries of command line arguments for the model executable self._args: dict[str, str | None] = {} @@ -152,26 +160,26 @@ def override2str( elif isinstance(oval, numbers.Number): oval_str = str(oval) else: - raise ModelicaSystemError(f"Invalid value for override key {okey}: {type(oval)}") + raise ModelExecutionException(f"Invalid value for override key {okey}: {type(oval)}") return f"{okey}={oval_str}" if not isinstance(key, str): - raise ModelicaSystemError(f"Invalid argument key: {repr(key)} (type: {type(key)})") + raise ModelExecutionException(f"Invalid argument key: {repr(key)} (type: {type(key)})") key = key.strip() if isinstance(val, dict): if key != 'override': - raise ModelicaSystemError("Dictionary input only possible for key 'override'!") + raise ModelExecutionException("Dictionary input only possible for key 'override'!") for okey, oval in val.items(): if not isinstance(okey, str): - raise ModelicaSystemError("Invalid key for argument 'override': " - f"{repr(okey)} (type: {type(okey)})") + raise ModelExecutionException("Invalid key for argument 'override': " + f"{repr(okey)} (type: {type(okey)})") if not isinstance(oval, (str, bool, numbers.Number, type(None))): - raise ModelicaSystemError(f"Invalid input for 'override'.{repr(okey)}: " - f"{repr(oval)} (type: {type(oval)})") + raise ModelExecutionException(f"Invalid input for 'override'.{repr(okey)}: " + f"{repr(oval)} (type: {type(oval)})") if okey in self._arg_override: if oval is None: @@ -193,7 +201,7 @@ def override2str( elif isinstance(val, numbers.Number): argval = str(val) else: - raise ModelicaSystemError(f"Invalid argument value for {repr(key)}: {repr(val)} (type: {type(val)})") + raise ModelExecutionException(f"Invalid argument value for {repr(key)}: {repr(val)} (type: {type(val)})") if key in self._args: logger.warning(f"Override model executable argument: {repr(key)} = {repr(argval)} " @@ -233,7 +241,7 @@ def get_cmd_args(self) -> list[str]: return cmdl - def definition(self) -> OMCSessionRunData: + def definition(self) -> ModelExecutionData: """ Define all needed data to run the model executable. The data is stored in an OMCSessionRunData object. """ @@ -242,18 +250,50 @@ def definition(self) -> OMCSessionRunData: 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(), + # as this is the local implementation, pathlib.Path can be used + cmd_path = self._runpath + + cmd_library_path = None + if self._cmd_local and self._cmd_windows: + cmd_library_path = "" + + # set the process environment from the generated .bat file in windows which should have all the dependencies + # for this pathlib.PurePosixPath() must be converted to a pathlib.Path() object, i.e. WindowsPath + path_bat = pathlib.Path(cmd_path) / f"{self._model_name}.bat" + if not path_bat.is_file(): + raise ModelExecutionException("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(pattern=r"^SET PATH=([^%]*)", string=line, flags=re.IGNORECASE) + if match: + cmd_library_path = match.group(1).strip(';') # Remove any trailing semicolons + my_env = os.environ.copy() + my_env["PATH"] = cmd_library_path + os.pathsep + my_env["PATH"] + + cmd_model_executable = cmd_path / f"{self._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 / self._model_name + + # define local(!) working directory + cmd_cwd_local = None + if self._cmd_local: + cmd_cwd_local = cmd_path.as_posix() + + omc_run_data = ModelExecutionData( + cmd_path=cmd_path.as_posix(), cmd_model_name=self._model_name, cmd_args=self.get_cmd_args(), - cmd_result_path=result_file, + cmd_result_file=result_file, + cmd_prefix=self._cmd_prefix, + cmd_library_path=cmd_library_path, + cmd_model_executable=cmd_model_executable.as_posix(), + cmd_cwd_local=cmd_cwd_local, + cmd_timeout=self._timeout, ) - omc_run_data_updated = self._session.omc_run_data_update( - omc_run_data=omc_run_data, - ) - - return omc_run_data_updated + return omc_run_data @staticmethod def parse_simflags(simflags: str) -> dict[str, Optional[str | dict[str, Any] | numbers.Number]]: @@ -262,17 +302,19 @@ def parse_simflags(simflags: str) -> dict[str, Optional[str | dict[str, Any] | n The return data can be used as input for self.args_set(). """ - warnings.warn(message="The argument 'simflags' is depreciated and will be removed in future versions; " - "please use 'simargs' instead", - category=DeprecationWarning, - stacklevel=2) + warnings.warn( + message="The argument 'simflags' is depreciated and will be removed in future versions; " + "please use 'simargs' instead", + category=DeprecationWarning, + stacklevel=2, + ) simargs: dict[str, Optional[str | dict[str, Any] | numbers.Number]] = {} args = [s for s in simflags.split(' ') if s] for arg in args: if arg[0] != '-': - raise ModelicaSystemError(f"Invalid simulation flag: {arg}") + raise ModelExecutionException(f"Invalid simulation flag: {arg}") arg = arg[1:] parts = arg.split('=') if len(parts) == 1: @@ -284,12 +326,12 @@ def parse_simflags(simflags: str) -> dict[str, Optional[str | dict[str, Any] | n for item in override.split(','): kv = item.split('=') if not 0 < len(kv) < 3: - raise ModelicaSystemError(f"Invalid value for '-override': {override}") + raise ModelExecutionException(f"Invalid value for '-override': {override}") if kv[0]: try: override_dict[kv[0]] = kv[1] except (KeyError, IndexError) as ex: - raise ModelicaSystemError(f"Invalid value for '-override': {override}") from ex + raise ModelExecutionException(f"Invalid value for '-override': {override}") from ex simargs[parts[0]] = override_dict @@ -544,15 +586,17 @@ def buildModel(self, variableFilter: Optional[str] = None): logger.debug("OM model build result: %s", build_model_result) # check if the executable exists ... - om_cmd = ModelicaSystemCmd( - session=self._session, + om_cmd = ModelExecutionCmd( runpath=self.getWorkDirectory(), - modelname=self._model_name, + cmd_local=self._session.model_execution_local, + cmd_windows=self._session.model_execution_windows, + cmd_prefix=self._session.model_execution_prefix(cwd=self.getWorkDirectory()), + model_name=self._model_name, ) # ... by running it - output help for command help om_cmd.arg_set(key="help", val="help") cmd_definition = om_cmd.definition() - returncode = self._session.run_model_executable(cmd_run_data=cmd_definition) + returncode = cmd_definition.run() if returncode != 0: raise ModelicaSystemError("Model executable not working!") @@ -1069,7 +1113,7 @@ def simulate_cmd( result_file: OMCPath, simflags: Optional[str] = None, simargs: Optional[dict[str, Optional[str | dict[str, Any] | numbers.Number]]] = None, - ) -> ModelicaSystemCmd: + ) -> ModelExecutionCmd: """ This method prepares the simulates model according to the simulation options. It returns an instance of ModelicaSystemCmd which can be used to run the simulation. @@ -1091,10 +1135,12 @@ def simulate_cmd( An instance if ModelicaSystemCmd to run the requested simulation. """ - om_cmd = ModelicaSystemCmd( - session=self._session, + om_cmd = ModelExecutionCmd( runpath=self.getWorkDirectory(), - modelname=self._model_name, + cmd_local=self._session.model_execution_local, + cmd_windows=self._session.model_execution_windows, + cmd_prefix=self._session.model_execution_prefix(cwd=self.getWorkDirectory()), + model_name=self._model_name, ) # always define the result file to use @@ -1183,7 +1229,7 @@ def simulate( self._result_file.unlink() # ... run simulation ... cmd_definition = om_cmd.definition() - returncode = self._session.run_model_executable(cmd_run_data=cmd_definition) + returncode = cmd_definition.run() # 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 @@ -1786,10 +1832,12 @@ def linearize( "use ModelicaSystem() to build the model first" ) - om_cmd = ModelicaSystemCmd( - session=self._session, + om_cmd = ModelExecutionCmd( runpath=self.getWorkDirectory(), - modelname=self._model_name, + cmd_local=self._session.model_execution_local, + cmd_windows=self._session.model_execution_windows, + cmd_prefix=self._session.model_execution_prefix(cwd=self.getWorkDirectory()), + model_name=self._model_name, ) self._process_override_data( @@ -1829,7 +1877,7 @@ def linearize( linear_file.unlink(missing_ok=True) cmd_definition = om_cmd.definition() - returncode = self._session.run_model_executable(cmd_run_data=cmd_definition) + returncode = cmd_definition.run() if returncode != 0: raise ModelicaSystemError(f"Linearize failed with return code: {returncode}") if not linear_file.is_file(): @@ -2015,7 +2063,7 @@ def __init__( self._parameters = {} self._doe_def: Optional[dict[str, dict[str, Any]]] = None - self._doe_cmd: Optional[dict[str, OMCSessionRunData]] = None + self._doe_cmd: Optional[dict[str, ModelExecutionData]] = None def get_session(self) -> OMCSession: """ @@ -2134,7 +2182,7 @@ def get_doe_definition(self) -> Optional[dict[str, dict[str, Any]]]: """ return self._doe_def - def get_doe_command(self) -> Optional[dict[str, OMCSessionRunData]]: + def get_doe_command(self) -> Optional[dict[str, ModelExecutionData]]: """ Get the definitions of simulations commands to run for this DoE. """ @@ -2180,13 +2228,13 @@ def worker(worker_id, task_queue): if cmd_definition is None: raise ModelicaSystemError("Missing simulation definition!") - resultfile = cmd_definition.cmd_result_path + resultfile = cmd_definition.cmd_result_file resultpath = self.get_session().omcpath(resultfile) logger.info(f"[Worker {worker_id}] Performing task: {resultpath.name}") try: - returncode = self.get_session().run_model_executable(cmd_run_data=cmd_definition) + returncode = cmd_definition.run() logger.info(f"[Worker {worker_id}] Simulation {resultpath.name} " f"finished with return code: {returncode}") except ModelicaSystemError as ex: diff --git a/OMPython/OMCSession.py b/OMPython/OMCSession.py index 6b5b2b3d..7f5af85e 100644 --- a/OMPython/OMCSession.py +++ b/OMPython/OMCSession.py @@ -458,31 +458,38 @@ class OMCPathCompatibilityWindows(pathlib.WindowsPath, OMCPathCompatibility): OMCPath = OMCPathReal +class ModelExecutionException(Exception): + """ + Exception which is raised by ModelException* classes. + """ + + @dataclasses.dataclass -class OMCSessionRunData: +class ModelExecutionData: """ 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) To use this as a definition of an OMC simulation run, it has to be processed within - OMCProcess*.omc_run_data_update(). This defines the attribute cmd_model_executable. + OMCProcess*.self_update(). This defines the attribute cmd_model_executable. """ # cmd_path is the expected working directory cmd_path: str cmd_model_name: str + # command prefix data (as list of strings); needed for docker or WSL + cmd_prefix: list[str] + # cmd_model_executable is build out of cmd_path and cmd_model_name; this is mainly needed on Windows (add *.exe) + cmd_model_executable: str # command line arguments for the model executable cmd_args: list[str] # result file with the simulation output - cmd_result_path: str + cmd_result_file: str + # command timeout + cmd_timeout: float - # 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 - # working directory to be used on the *local* system cmd_cwd_local: Optional[str] = None @@ -491,14 +498,49 @@ 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 + cmdl = self.cmd_prefix + cmdl += [self.cmd_model_executable] + cmdl += self.cmd_args return cmdl + def run(self) -> int: + """ + Run the model execution defined in this class. + """ + + my_env = os.environ.copy() + if isinstance(self.cmd_library_path, str): + my_env["PATH"] = self.cmd_library_path + os.pathsep + my_env["PATH"] + + cmdl = self.get_cmd() + + logger.debug("Run OM command %s in %s", repr(cmdl), self.cmd_path) + try: + cmdres = subprocess.run( + cmdl, + capture_output=True, + text=True, + env=my_env, + cwd=self.cmd_cwd_local, + timeout=self.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 ModelExecutionException(f"Error running model executable {repr(cmdl)}: {stderr}") + except subprocess.TimeoutExpired as ex: + raise ModelExecutionException(f"Timeout running model executable {repr(cmdl)}") from ex + except subprocess.CalledProcessError as ex: + raise ModelExecutionException(f"Error running model executable {repr(cmdl)}") from ex + + return returncode + class OMCSessionZMQ: """ @@ -548,21 +590,6 @@ def omcpath_tempdir(self, tempdir_base: Optional[OMCPath] = None) -> OMCPath: """ return self.omc_process.omcpath_tempdir(tempdir_base=tempdir_base) - 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) - - def run_model_executable(self, cmd_run_data: OMCSessionRunData) -> int: - """ - Run the command defined in cmd_run_data. This class is defined as static method such that there is no need to - keep instances of over classes around. - """ - return self.omc_process.run_model_executable(cmd_run_data=cmd_run_data) - def execute(self, command: str): return self.omc_process.execute(command=command) @@ -641,6 +668,10 @@ def __init__( Initialisation for OMCSession """ + # some helper data + self.model_execution_windows = platform.system() == "Windows" + self.model_execution_local = False + # store variables self._timeout = timeout # generate a random string for this instance of OMC @@ -766,6 +797,13 @@ def escape_str(value: str) -> str: """ return value.replace("\\", "\\\\").replace('"', '\\"') + def model_execution_prefix(self, cwd: Optional[OMCPath] = None) -> list[str]: + """ + Helper function which returns a command prefix needed for docker and WSL. It defaults to an empty list. + """ + + return [] + def omcpath(self, *path) -> OMCPath: """ Create an OMCPath object based on the given path segments and the current OMCSession* class. @@ -784,7 +822,6 @@ def omcpath_tempdir(self, tempdir_base: Optional[OMCPath] = None) -> OMCPath: Get a temporary directory using OMC. It is our own implementation as non-local usage relies on OMC to run all filesystem related access. """ - names = [str(uuid.uuid4()) for _ in range(100)] if tempdir_base is None: # fallback solution for Python < 3.12; a modified pathlib.Path object is used as OMCPath replacement @@ -794,6 +831,12 @@ def omcpath_tempdir(self, tempdir_base: Optional[OMCPath] = None) -> OMCPath: tempdir_str = self.sendExpression(expr="getTempDirectoryPath()") tempdir_base = self.omcpath(tempdir_str) + return self._tempdir(tempdir_base=tempdir_base) + + @staticmethod + def _tempdir(tempdir_base: OMCPath) -> OMCPath: + names = [str(uuid.uuid4()) for _ in range(100)] + tempdir: Optional[OMCPath] = None for name in names: # create a unique temporary directory name @@ -810,43 +853,6 @@ def omcpath_tempdir(self, tempdir_base: Optional[OMCPath] = None) -> OMCPath: return tempdir - def run_model_executable(self, cmd_run_data: OMCSessionRunData) -> int: - """ - Run the command defined in cmd_run_data. - """ - - 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=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 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(message="This function is depreciated and will be removed in future versions; " "please use sendExpression() instead", @@ -1023,18 +1029,6 @@ 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 OMCSession implementation. - - The main point is the definition of OMCSessionRunData.cmd_model_executable which contains the specific command - to run depending on the selected system. - - Needs to be implemented in the subclasses. - """ - raise NotImplementedError("This method must be implemented in subclasses!") - class OMCSessionPort(OMCSession): """ @@ -1048,28 +1042,6 @@ def __init__( super().__init__() self._omc_port = omc_port - @staticmethod - def run_model_executable(cmd_run_data: OMCSessionRunData) -> int: - """ - Run the command defined in cmd_run_data. This class is defined as static method such that there is no need to - keep instances of over classes around. - """ - raise OMCSessionException("OMCSessionPort does not support run_model_executable()!") - - def get_log(self) -> str: - """ - Get the log file content of the OMC session. - """ - log = f"No log available if OMC session is defined by port ({self.__class__.__name__})" - - return log - - def omc_run_data_update(self, omc_run_data: OMCSessionRunData) -> OMCSessionRunData: - """ - Update the OMCSessionRunData object based on the selected OMCSession implementation. - """ - raise OMCSessionException(f"({self.__class__.__name__}) does not support omc_run_data_update()!") - class OMCSessionLocal(OMCSession): """ @@ -1084,6 +1056,8 @@ def __init__( super().__init__(timeout=timeout) + self.model_execution_local = True + # where to find OpenModelica self._omhome = self._omc_home_get(omhome=omhome) # start up omc executable, which is waiting for the ZMQ connection @@ -1149,48 +1123,6 @@ 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 OMCSession 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 OMCSessionDockerHelper(OMCSession): """ @@ -1303,27 +1235,21 @@ def get_docker_container_id(self) -> str: return self._docker_container_id - def omc_run_data_update(self, omc_run_data: OMCSessionRunData) -> OMCSessionRunData: + def model_execution_prefix(self, cwd: Optional[OMCPath] = None) -> list[str]: """ - Update the OMCSessionRunData object based on the selected OMCSession implementation. + Helper function which returns a command prefix needed for docker and WSL. It defaults to an empty list. """ - 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._docker_extra_args - + [self._docker_container_id] - ) - - 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() + docker_cmd = [ + "docker", "exec", + "--user", str(self._getuid()), + ] + if isinstance(cwd, OMCPath): + docker_cmd += ["--workdir", cwd.as_posix()] + docker_cmd += self._docker_extra_args + if isinstance(self._docker_container_id, str): + docker_cmd += [self._docker_container_id] - return omc_run_data_copy + return docker_cmd class OMCSessionDocker(OMCSessionDockerHelper): @@ -1586,15 +1512,18 @@ def __init__( # 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]: + def model_execution_prefix(self, cwd: Optional[OMCPath] = None) -> list[str]: + """ + Helper function which returns a command prefix needed for docker and WSL. It defaults to an empty list. + """ # 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] + if isinstance(cwd, OMCPath): + wsl_cmd += ['--cd', cwd.as_posix()] wsl_cmd += ['--'] return wsl_cmd @@ -1602,7 +1531,7 @@ def _wsl_cmd(self, wsl_cwd: Optional[str] = None) -> list[str]: def _omc_process_get(self) -> subprocess.Popen: my_env = os.environ.copy() - omc_command = self._wsl_cmd() + [ + omc_command = self.model_execution_prefix() + [ self._wsl_omc, "--locale=C", "--interactive=zmq", @@ -1624,7 +1553,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.model_execution_prefix() + ["cat", omc_portfile_path.as_posix()], stderr=subprocess.DEVNULL, ) port = output.decode().strip() @@ -1641,17 +1570,3 @@ 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 OMCSession 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 diff --git a/OMPython/__init__.py b/OMPython/__init__.py index 59a0ad10..0e85ed1a 100644 --- a/OMPython/__init__.py +++ b/OMPython/__init__.py @@ -10,16 +10,19 @@ from OMPython.ModelicaSystem import ( LinearizationResult, ModelicaSystem, - ModelicaSystemCmd, + ModelExecutionCmd, ModelicaSystemDoE, ModelicaSystemError, ) from OMPython.OMCSession import ( OMCPath, OMCSession, + + ModelExecutionData, + ModelExecutionException, + OMCSessionCmd, OMCSessionException, - OMCSessionRunData, OMCSessionZMQ, OMCSessionPort, OMCSessionLocal, @@ -31,8 +34,12 @@ # global names imported if import 'from OMPython import *' is used __all__ = [ 'LinearizationResult', + + 'ModelExecutionData', + 'ModelExecutionException', + 'ModelicaSystem', - 'ModelicaSystemCmd', + 'ModelExecutionCmd', 'ModelicaSystemDoE', 'ModelicaSystemError', @@ -41,7 +48,6 @@ 'OMCSession', 'OMCSessionCmd', 'OMCSessionException', - 'OMCSessionRunData', 'OMCSessionZMQ', 'OMCSessionPort', 'OMCSessionLocal', From 31f113efdf9016751bceedcb584b665cc9975586 Mon Sep 17 00:00:00 2001 From: syntron Date: Sun, 18 Jan 2026 19:19:06 +0100 Subject: [PATCH 2/4] [test_ModelicaSystemCmd] update unittest --- tests/test_ModelicaSystemCmd.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/tests/test_ModelicaSystemCmd.py b/tests/test_ModelicaSystemCmd.py index 2480aad9..6fa2658f 100644 --- a/tests/test_ModelicaSystemCmd.py +++ b/tests/test_ModelicaSystemCmd.py @@ -23,11 +23,15 @@ def mscmd_firstorder(model_firstorder): model_file=model_firstorder, model_name="M", ) - mscmd = OMPython.ModelicaSystemCmd( - session=mod.get_session(), + + mscmd = OMPython.ModelExecutionCmd( runpath=mod.getWorkDirectory(), - modelname=mod._model_name, + cmd_local=mod.get_session().model_execution_local, + cmd_windows=mod.get_session().model_execution_windows, + cmd_prefix=mod.get_session().model_execution_prefix(cwd=mod.getWorkDirectory()), + model_name=mod._model_name, ) + return mscmd From ee49fa0a5c6b6fcee8393bc1588165e15ec1c133 Mon Sep 17 00:00:00 2001 From: syntron Date: Sat, 24 Jan 2026 14:54:56 +0100 Subject: [PATCH 3/4] [ModelExecutionData] include the original exception if reraised as ModelExecutionException --- OMPython/OMCSession.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/OMPython/OMCSession.py b/OMPython/OMCSession.py index 7f5af85e..6efb5235 100644 --- a/OMPython/OMCSession.py +++ b/OMPython/OMCSession.py @@ -535,9 +535,9 @@ def run(self) -> int: if stderr: raise ModelExecutionException(f"Error running model executable {repr(cmdl)}: {stderr}") except subprocess.TimeoutExpired as ex: - raise ModelExecutionException(f"Timeout running model executable {repr(cmdl)}") from ex + raise ModelExecutionException(f"Timeout running model executable {repr(cmdl)}: {ex}") from ex except subprocess.CalledProcessError as ex: - raise ModelExecutionException(f"Error running model executable {repr(cmdl)}") from ex + raise ModelExecutionException(f"Error running model executable {repr(cmdl)}: {ex}") from ex return returncode From 541cd73133a3c6a86235c8b222b1697858c4abee Mon Sep 17 00:00:00 2001 From: syntron Date: Tue, 17 Feb 2026 19:33:20 +0100 Subject: [PATCH 4/4] [ModelicaSystem] fix usage of ModelicaSystemCmd --- OMPython/ModelicaSystem.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/OMPython/ModelicaSystem.py b/OMPython/ModelicaSystem.py index b352176b..a21cb969 100644 --- a/OMPython/ModelicaSystem.py +++ b/OMPython/ModelicaSystem.py @@ -1077,7 +1077,7 @@ def _parse_om_version(self, version: str) -> tuple[int, int, int]: def _process_override_data( self, - om_cmd: ModelicaSystemCmd, + om_cmd: ModelExecutionCmd, override_file: OMCPath, override_var: dict[str, str], override_sim: dict[str, str],