Skip to content

Commit b2d268c

Browse files
committed
[OMCSessionRunData] use class to move run of model executable to OMSessionZMQ
1 parent 8bdb86a commit b2d268c

2 files changed

Lines changed: 155 additions & 72 deletions

File tree

OMPython/ModelicaSystem.py

Lines changed: 36 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -39,16 +39,13 @@
3939
import numpy as np
4040
import os
4141
import pathlib
42-
import platform
43-
import re
44-
import subprocess
4542
import tempfile
4643
import textwrap
4744
from typing import Optional, Any
4845
import warnings
4946
import xml.etree.ElementTree as ET
5047

51-
from OMPython.OMCSession import OMCSessionException, OMCSessionZMQ, OMCProcessLocal
48+
from OMPython.OMCSession import OMCSessionException, OMCSessionRunData, OMCSessionZMQ, OMCProcessLocal
5249

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

117-
def __init__(self, runpath: pathlib.Path, modelname: str, timeout: Optional[float] = None) -> None:
114+
def __init__(
115+
self,
116+
session: OMCSessionZMQ,
117+
runpath: pathlib.Path,
118+
modelname: str,
119+
timeout: Optional[float] = None,
120+
) -> None:
121+
self._session = session
118122
self._runpath = pathlib.Path(runpath).resolve().absolute()
119123
self._model_name = modelname
120124
self._timeout = timeout
@@ -229,27 +233,12 @@ def args_set(
229233
for arg in args:
230234
self.arg_set(key=arg, val=args[arg])
231235

232-
def get_exe(self) -> pathlib.Path:
233-
"""Get the path to the compiled model executable."""
234-
if platform.system() == "Windows":
235-
path_exe = self._runpath / f"{self._model_name}.exe"
236-
else:
237-
path_exe = self._runpath / self._model_name
238-
239-
if not path_exe.exists():
240-
raise ModelicaSystemError(f"Application file path not found: {path_exe}")
241-
242-
return path_exe
243-
244-
def get_cmd(self) -> list:
245-
"""Get a list with the path to the executable and all command line args.
246-
247-
This can later be used as an argument for subprocess.run().
236+
def get_cmd_args(self) -> list[str]:
237+
"""
238+
Get a list with the command arguments for the model executable.
248239
"""
249240

250-
path_exe = self.get_exe()
251-
252-
cmdl = [path_exe.as_posix()]
241+
cmdl = []
253242
for key in sorted(self._args):
254243
if self._args[key] is None:
255244
cmdl.append(f"-{key}")
@@ -258,54 +247,26 @@ def get_cmd(self) -> list:
258247

259248
return cmdl
260249

261-
def run(self) -> int:
262-
"""Run the requested simulation.
263-
264-
Returns
265-
-------
266-
Subprocess return code (0 on success).
250+
def definition(self) -> OMCSessionRunData:
267251
"""
252+
Define all needed data to run the model executable. The data is stored in an OMCSessionRunData object.
253+
"""
254+
# ensure that a result filename is provided
255+
result_file = self.arg_get('r')
256+
if not isinstance(result_file, str):
257+
result_file = (self._runpath / f"{self._model_name}.mat").as_posix()
258+
259+
omc_run_data = OMCSessionRunData(
260+
cmd_path=self._runpath.as_posix(),
261+
cmd_model_name=self._model_name,
262+
cmd_args=self.get_cmd_args(),
263+
cmd_result_path=result_file,
264+
cmd_timeout=self._timeout,
265+
)
268266

269-
cmdl: list = self.get_cmd()
270-
271-
logger.debug("Run OM command %s in %s", repr(cmdl), self._runpath.as_posix())
272-
273-
if platform.system() == "Windows":
274-
path_dll = ""
275-
276-
# set the process environment from the generated .bat file in windows which should have all the dependencies
277-
path_bat = self._runpath / f"{self._model_name}.bat"
278-
if not path_bat.exists():
279-
raise ModelicaSystemError("Batch file (*.bat) does not exist " + str(path_bat))
280-
281-
with open(file=path_bat, mode='r', encoding='utf-8') as fh:
282-
for line in fh:
283-
match = re.match(r"^SET PATH=([^%]*)", line, re.IGNORECASE)
284-
if match:
285-
path_dll = match.group(1).strip(';') # Remove any trailing semicolons
286-
my_env = os.environ.copy()
287-
my_env["PATH"] = path_dll + os.pathsep + my_env["PATH"]
288-
else:
289-
# TODO: how to handle path to resources of external libraries for any system not Windows?
290-
my_env = None
291-
292-
try:
293-
cmdres = subprocess.run(cmdl, capture_output=True, text=True, env=my_env, cwd=self._runpath,
294-
timeout=self._timeout, check=True)
295-
stdout = cmdres.stdout.strip()
296-
stderr = cmdres.stderr.strip()
297-
returncode = cmdres.returncode
298-
299-
logger.debug("OM output for command %s:\n%s", repr(cmdl), stdout)
300-
301-
if stderr:
302-
raise ModelicaSystemError(f"Error running command {repr(cmdl)}: {stderr}")
303-
except subprocess.TimeoutExpired as ex:
304-
raise ModelicaSystemError(f"Timeout running command {repr(cmdl)}") from ex
305-
except subprocess.CalledProcessError as ex:
306-
raise ModelicaSystemError(f"Error running command {repr(cmdl)}") from ex
267+
omc_run_data_updated = self._session.omc_run_data_update(omc_run_data=omc_run_data)
307268

308-
return returncode
269+
return omc_run_data_updated
309270

310271
@staticmethod
311272
def parse_simflags(simflags: str) -> dict[str, Optional[str | dict[str, Any] | numbers.Number]]:
@@ -1026,6 +987,7 @@ def simulate_cmd(
1026987
"""
1027988

1028989
om_cmd = ModelicaSystemCmd(
990+
session=self._getconn,
1029991
runpath=self.getWorkDirectory(),
1030992
modelname=self._model_name,
1031993
timeout=timeout,
@@ -1118,7 +1080,8 @@ def simulate(
11181080
if self._result_file.is_file():
11191081
self._result_file.unlink()
11201082
# ... run simulation ...
1121-
returncode = om_cmd.run()
1083+
cmd_definition = om_cmd.definition()
1084+
returncode = self._getconn.run_model_executable(cmd_run_data=cmd_definition)
11221085
# and check returncode *AND* resultfile
11231086
if returncode != 0 and self._result_file.is_file():
11241087
# check for an empty (=> 0B) result file which indicates a crash of the model executable
@@ -1637,6 +1600,7 @@ def linearize(
16371600
)
16381601

16391602
om_cmd = ModelicaSystemCmd(
1603+
session=self._getconn,
16401604
runpath=self.getWorkDirectory(),
16411605
modelname=self._model_name,
16421606
timeout=timeout,
@@ -1675,7 +1639,8 @@ def linearize(
16751639
linear_file = self.getWorkDirectory() / "linearized_model.py"
16761640
linear_file.unlink(missing_ok=True)
16771641

1678-
returncode = om_cmd.run()
1642+
cmd_definition = om_cmd.definition()
1643+
returncode = self._getconn.run_model_executable(cmd_run_data=cmd_definition)
16791644
if returncode != 0:
16801645
raise ModelicaSystemError(f"Linearize failed with return code: {returncode}")
16811646
if not linear_file.exists():

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
@@ -368,6 +370,53 @@ def __del__(self):
368370

369371
self.omc_zmq = None
370372

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

473522

474-
class OMCProcess:
523+
class OMCProcess(metaclass=abc.ABCMeta):
475524

476525
def __init__(
477526
self,
@@ -559,6 +608,15 @@ def _get_portfile_path(self) -> Optional[pathlib.Path]:
559608

560609
return portfile_path
561610

611+
@abc.abstractmethod
612+
def omc_run_data_update(self, omc_run_data: OMCSessionRunData) -> OMCSessionRunData:
613+
"""
614+
Update the OMCSessionRunData object based on the selected OMCProcess implementation.
615+
616+
Needs to be implemented in the subclasses.
617+
"""
618+
raise NotImplementedError("This method must be implemented in subclasses!")
619+
562620

563621
class OMCProcessPort(OMCProcess):
564622
"""
@@ -572,6 +630,12 @@ def __init__(
572630
super().__init__()
573631
self._omc_port = omc_port
574632

633+
def omc_run_data_update(self, omc_run_data: OMCSessionRunData) -> OMCSessionRunData:
634+
"""
635+
Update the OMCSessionRunData object based on the selected OMCProcess implementation.
636+
"""
637+
raise OMCSessionException("OMCProcessPort does not support omc_run_data_update()!")
638+
575639

576640
class OMCProcessLocal(OMCProcess):
577641
"""
@@ -656,6 +720,48 @@ def _omc_port_get(self) -> str:
656720

657721
return port
658722

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

660766
class OMCProcessDockerHelper(OMCProcess):
661767
"""
@@ -771,6 +877,12 @@ def get_docker_container_id(self) -> str:
771877

772878
return self._dockerCid
773879

880+
def omc_run_data_update(self, omc_run_data: OMCSessionRunData) -> OMCSessionRunData:
881+
"""
882+
Update the OMCSessionRunData object based on the selected OMCProcess implementation.
883+
"""
884+
raise OMCSessionException("OMCProcessDocker* does not support omc_run_data_update()!")
885+
774886

775887
class OMCProcessDocker(OMCProcessDockerHelper):
776888
"""
@@ -1085,3 +1197,9 @@ def _omc_port_get(self) -> str:
10851197
f"pid={self._omc_process.pid if isinstance(self._omc_process, subprocess.Popen) else '?'}")
10861198

10871199
return port
1200+
1201+
def omc_run_data_update(self, omc_run_data: OMCSessionRunData) -> OMCSessionRunData:
1202+
"""
1203+
Update the OMCSessionRunData object based on the selected OMCProcess implementation.
1204+
"""
1205+
raise OMCSessionException("OMCProcessWSL does not support omc_run_data_update()!")

0 commit comments

Comments
 (0)