Skip to content

Commit e4788b3

Browse files
committed
[OMCSessionRunData] use class to move run of model executable to OMSessionZMQ
1 parent e94f822 commit e4788b3

2 files changed

Lines changed: 165 additions & 74 deletions

File tree

OMPython/ModelicaSystem.py

Lines changed: 46 additions & 73 deletions
Original file line numberDiff line numberDiff line change
@@ -40,16 +40,13 @@
4040
import numpy as np
4141
import os
4242
import pathlib
43-
import platform
44-
import re
45-
import subprocess
4643
import tempfile
4744
import textwrap
4845
from typing import Optional, Any
4946
import warnings
5047
import xml.etree.ElementTree as ET
5148

52-
from OMPython.OMCSession import OMCSessionException, OMCSessionZMQ, OMCProcessLocal
49+
from OMPython.OMCSession import OMCSessionException, OMCSessionRunData, OMCSessionZMQ, OMCProcessLocal
5350

5451
# define logger using the current module name as ID
5552
logger = logging.getLogger(__name__)
@@ -115,7 +112,14 @@ def __getitem__(self, index: int):
115112
class ModelicaSystemCmd:
116113
"""A compiled model executable."""
117114

118-
def __init__(self, runpath: pathlib.Path, modelname: str, timeout: Optional[float] = None) -> None:
115+
def __init__(
116+
self,
117+
session: OMCSessionZMQ,
118+
runpath: pathlib.Path,
119+
modelname: str,
120+
timeout: Optional[float] = None,
121+
) -> None:
122+
self._session = session
119123
self._runpath = pathlib.Path(runpath).resolve().absolute()
120124
self._model_name = modelname
121125
self._timeout = timeout
@@ -176,27 +180,12 @@ def args_set(self, args: dict[str, Optional[str | dict[str, str]]]) -> None:
176180
for arg in args:
177181
self.arg_set(key=arg, val=args[arg])
178182

179-
def get_exe(self) -> pathlib.Path:
180-
"""Get the path to the compiled model executable."""
181-
if platform.system() == "Windows":
182-
path_exe = self._runpath / f"{self._model_name}.exe"
183-
else:
184-
path_exe = self._runpath / self._model_name
185-
186-
if not path_exe.exists():
187-
raise ModelicaSystemError(f"Application file path not found: {path_exe}")
188-
189-
return path_exe
190-
191-
def get_cmd(self) -> list:
192-
"""Get a list with the path to the executable and all command line args.
193-
194-
This can later be used as an argument for subprocess.run().
183+
def get_cmd_args(self) -> list[str]:
184+
"""
185+
Get a list with the command arguments for the model executable.
195186
"""
196187

197-
path_exe = self.get_exe()
198-
199-
cmdl = [path_exe.as_posix()]
188+
cmdl = []
200189
for key in self._args:
201190
if self._args[key] is None:
202191
cmdl.append(f"-{key}")
@@ -205,54 +194,26 @@ def get_cmd(self) -> list:
205194

206195
return cmdl
207196

208-
def run(self) -> int:
209-
"""Run the requested simulation.
210-
211-
Returns
212-
-------
213-
Subprocess return code (0 on success).
197+
def definition(self) -> OMCSessionRunData:
214198
"""
199+
Define all needed data to run the model executable. The data is stored in an OMCSessionRunData object.
200+
"""
201+
# ensure that a result filename is provided
202+
result_file = self.arg_get('r')
203+
if not isinstance(result_file, str):
204+
result_file = (self._runpath / f"{self._model_name}.mat").as_posix()
205+
206+
omc_run_data = OMCSessionRunData(
207+
cmd_path=self._runpath.as_posix(),
208+
cmd_model_name=self._model_name,
209+
cmd_args=self.get_cmd_args(),
210+
cmd_result_path=result_file,
211+
cmd_timeout=self._timeout,
212+
)
215213

216-
cmdl: list = self.get_cmd()
217-
218-
logger.debug("Run OM command %s in %s", repr(cmdl), self._runpath.as_posix())
219-
220-
if platform.system() == "Windows":
221-
path_dll = ""
222-
223-
# set the process environment from the generated .bat file in windows which should have all the dependencies
224-
path_bat = self._runpath / f"{self._model_name}.bat"
225-
if not path_bat.exists():
226-
raise ModelicaSystemError("Batch file (*.bat) does not exist " + str(path_bat))
227-
228-
with open(file=path_bat, mode='r', encoding='utf-8') as fh:
229-
for line in fh:
230-
match = re.match(r"^SET PATH=([^%]*)", line, re.IGNORECASE)
231-
if match:
232-
path_dll = match.group(1).strip(';') # Remove any trailing semicolons
233-
my_env = os.environ.copy()
234-
my_env["PATH"] = path_dll + os.pathsep + my_env["PATH"]
235-
else:
236-
# TODO: how to handle path to resources of external libraries for any system not Windows?
237-
my_env = None
238-
239-
try:
240-
cmdres = subprocess.run(cmdl, capture_output=True, text=True, env=my_env, cwd=self._runpath,
241-
timeout=self._timeout, check=True)
242-
stdout = cmdres.stdout.strip()
243-
stderr = cmdres.stderr.strip()
244-
returncode = cmdres.returncode
245-
246-
logger.debug("OM output for command %s:\n%s", repr(cmdl), stdout)
247-
248-
if stderr:
249-
raise ModelicaSystemError(f"Error running command {repr(cmdl)}: {stderr}")
250-
except subprocess.TimeoutExpired as ex:
251-
raise ModelicaSystemError(f"Timeout running command {repr(cmdl)}") from ex
252-
except subprocess.CalledProcessError as ex:
253-
raise ModelicaSystemError(f"Error running command {repr(cmdl)}") from ex
214+
omc_run_data_updated = self._session.omc_run_data_update(omc_run_data)
254215

255-
return returncode
216+
return omc_run_data_updated
256217

257218
@staticmethod
258219
def parse_simflags(simflags: str) -> dict[str, Optional[str | dict[str, str]]]:
@@ -943,7 +904,12 @@ def simulate_cmd(
943904
An instance if ModelicaSystemCmd to run the requested simulation.
944905
"""
945906

946-
om_cmd = ModelicaSystemCmd(runpath=self._tempdir, modelname=self._model_name, timeout=timeout)
907+
om_cmd = ModelicaSystemCmd(
908+
session=self._getconn,
909+
runpath=self.getWorkDirectory(),
910+
modelname=self._model_name,
911+
timeout=timeout,
912+
)
947913

948914
# always define the result file to use
949915
om_cmd.arg_set(key="r", val=result_file.as_posix())
@@ -1033,7 +999,8 @@ def simulate(
1033999
if self._result_file.is_file():
10341000
self._result_file.unlink()
10351001
# ... run simulation ...
1036-
returncode = om_cmd.run()
1002+
cmd_definition = om_cmd.definition()
1003+
returncode = self._getconn.run_model_executable(cmd_run_data=cmd_definition)
10371004
# and check returncode *AND* resultfile
10381005
if returncode != 0 and self._result_file.is_file():
10391006
# check for an empty (=> 0B) result file which indicates a crash of the model executable
@@ -1452,7 +1419,12 @@ def load_module_from_path(module_name, file_path):
14521419
"use ModelicaSystem() to build the model first"
14531420
)
14541421

1455-
om_cmd = ModelicaSystemCmd(runpath=self._tempdir, modelname=self._model_name, timeout=timeout)
1422+
om_cmd = ModelicaSystemCmd(
1423+
session=self._getconn,
1424+
runpath=self._tempdir,
1425+
modelname=self._model_name,
1426+
timeout=timeout,
1427+
)
14561428

14571429
overrideLinearFile = self._tempdir / f'{self._model_name}_override_linear.txt'
14581430

@@ -1484,7 +1456,8 @@ def load_module_from_path(module_name, file_path):
14841456
if simargs:
14851457
om_cmd.args_set(args=simargs)
14861458

1487-
returncode = om_cmd.run()
1459+
cmd_definition = om_cmd.definition()
1460+
returncode = self._getconn.run_model_executable(cmd_run_data=cmd_definition)
14881461
if returncode != 0:
14891462
raise ModelicaSystemError(f"Linearize failed with return code: {returncode}")
14901463

OMPython/OMCSession.py

Lines changed: 119 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,12 +34,14 @@
3434
CONDITIONS OF OSMC-PL.
3535
"""
3636

37+
import abc
3738
import dataclasses
3839
import io
3940
import json
4041
import logging
4142
import os
4243
import pathlib
44+
import platform
4345
import psutil
4446
import pyparsing
4547
import re
@@ -365,6 +367,53 @@ def __del__(self):
365367

366368
self.omc_zmq = None
367369

370+
def omc_run_data_update(self, omc_run_data: OMCSessionRunData) -> OMCSessionRunData:
371+
"""
372+
Modify data based on the selected OMCProcess implementation.
373+
374+
Needs to be implemented in the subclasses.
375+
"""
376+
return self.omc_process.omc_run_data_update(omc_run_data=omc_run_data)
377+
378+
@staticmethod
379+
def run_model_executable(cmd_run_data: OMCSessionRunData) -> int:
380+
"""
381+
Run the command defined in cmd_run_data. This class is defined as static method such that there is no need to
382+
keep instances of over classes around.
383+
"""
384+
385+
my_env = os.environ.copy()
386+
if isinstance(cmd_run_data.cmd_library_path, str):
387+
my_env["PATH"] = cmd_run_data.cmd_library_path + os.pathsep + my_env["PATH"]
388+
389+
cmdl = cmd_run_data.get_cmd()
390+
391+
logger.debug("Run OM command %s in %s", repr(cmdl), cmd_run_data.cmd_path)
392+
try:
393+
cmdres = subprocess.run(
394+
cmdl,
395+
capture_output=True,
396+
text=True,
397+
env=my_env,
398+
cwd=cmd_run_data.cmd_cwd_local,
399+
timeout=cmd_run_data.cmd_timeout,
400+
check=True,
401+
)
402+
stdout = cmdres.stdout.strip()
403+
stderr = cmdres.stderr.strip()
404+
returncode = cmdres.returncode
405+
406+
logger.debug("OM output for command %s:\n%s", repr(cmdl), stdout)
407+
408+
if stderr:
409+
raise OMCSessionException(f"Error running model executable {repr(cmdl)}: {stderr}")
410+
except subprocess.TimeoutExpired as ex:
411+
raise OMCSessionException(f"Timeout running model executable {repr(cmdl)}") from ex
412+
except subprocess.CalledProcessError as ex:
413+
raise OMCSessionException(f"Error running model executable {repr(cmdl)}") from ex
414+
415+
return returncode
416+
368417
def execute(self, command: str):
369418
warnings.warn("This function is depreciated and will be removed in future versions; "
370419
"please use sendExpression() instead", DeprecationWarning, stacklevel=2)
@@ -468,7 +517,7 @@ def sendExpression(self, command: str, parsed: bool = True) -> Any:
468517
raise OMCSessionException("Cannot parse OMC result") from ex
469518

470519

471-
class OMCProcess:
520+
class OMCProcess(metaclass=abc.ABCMeta):
472521

473522
def __init__(
474523
self,
@@ -550,6 +599,15 @@ def _get_portfile_path(self) -> Optional[pathlib.Path]:
550599

551600
return portfile_path
552601

602+
@abc.abstractmethod
603+
def omc_run_data_update(self, omc_run_data: OMCSessionRunData) -> OMCSessionRunData:
604+
"""
605+
Update the OMCSessionRunData object based on the selected OMCProcess implementation.
606+
607+
Needs to be implemented in the subclasses.
608+
"""
609+
raise NotImplementedError("This method must be implemented in subclasses!")
610+
553611

554612
class OMCProcessPort(OMCProcess):
555613

@@ -560,6 +618,12 @@ def __init__(
560618
super().__init__()
561619
self._omc_port = omc_port
562620

621+
def omc_run_data_update(self, omc_run_data: OMCSessionRunData) -> OMCSessionRunData:
622+
"""
623+
Update the OMCSessionRunData object based on the selected OMCProcess implementation.
624+
"""
625+
raise OMCSessionException("OMCProcessPort does not support omc_run_data_update()!")
626+
563627

564628
class OMCProcessLocal(OMCProcess):
565629

@@ -641,6 +705,48 @@ def _omc_port_get(self) -> str:
641705

642706
return port
643707

708+
def omc_run_data_update(self, omc_run_data: OMCSessionRunData) -> OMCSessionRunData:
709+
"""
710+
Update the OMCSessionRunData object based on the selected OMCProcess implementation.
711+
"""
712+
# create a copy of the data
713+
omc_run_data_copy = dataclasses.replace(omc_run_data)
714+
715+
# as this is the local implementation, pathlib.Path can be used
716+
cmd_path = pathlib.Path(omc_run_data_copy.cmd_path)
717+
718+
if platform.system() == "Windows":
719+
path_dll = ""
720+
721+
# set the process environment from the generated .bat file in windows which should have all the dependencies
722+
path_bat = cmd_path / f"{omc_run_data.cmd_model_name}.bat"
723+
if not path_bat.is_file():
724+
raise OMCSessionException("Batch file (*.bat) does not exist " + str(path_bat))
725+
726+
content = path_bat.read_text(encoding='utf-8')
727+
for line in content.splitlines():
728+
match = re.match(r"^SET PATH=([^%]*)", line, re.IGNORECASE)
729+
if match:
730+
path_dll = match.group(1).strip(';') # Remove any trailing semicolons
731+
my_env = os.environ.copy()
732+
my_env["PATH"] = path_dll + os.pathsep + my_env["PATH"]
733+
734+
omc_run_data_copy.cmd_library_path = path_dll
735+
736+
cmd_model_executable = cmd_path / f"{omc_run_data_copy.cmd_model_name}.exe"
737+
else:
738+
# for Linux the paths to the needed libraries should be included in the executable (using rpath)
739+
cmd_model_executable = cmd_path / omc_run_data_copy.cmd_model_name
740+
741+
if not cmd_model_executable.is_file():
742+
raise OMCSessionException(f"Application file path not found: {cmd_model_executable}")
743+
omc_run_data_copy.cmd_model_executable = cmd_model_executable.as_posix()
744+
745+
# define local(!) working directory
746+
omc_run_data_copy.cmd_cwd_local = omc_run_data.cmd_path
747+
748+
return omc_run_data_copy
749+
644750

645751
class OMCProcessDockerHelper(OMCProcess):
646752

@@ -747,6 +853,12 @@ def get_docker_container_id(self) -> str:
747853

748854
return self._dockerCid
749855

856+
def omc_run_data_update(self, omc_run_data: OMCSessionRunData) -> OMCSessionRunData:
857+
"""
858+
Update the OMCSessionRunData object based on the selected OMCProcess implementation.
859+
"""
860+
raise OMCSessionException("OMCProcessDocker* does not support omc_run_data_update()!")
861+
750862

751863
class OMCProcessDocker(OMCProcessDockerHelper):
752864

@@ -1052,3 +1164,9 @@ def _omc_port_get(self) -> str:
10521164
f"pid={self._omc_process.pid if isinstance(self._omc_process, subprocess.Popen) else '?'}")
10531165

10541166
return port
1167+
1168+
def omc_run_data_update(self, omc_run_data: OMCSessionRunData) -> OMCSessionRunData:
1169+
"""
1170+
Update the OMCSessionRunData object based on the selected OMCProcess implementation.
1171+
"""
1172+
raise OMCSessionException("OMCProcessWSL does not support omc_run_data_update()!")

0 commit comments

Comments
 (0)