Skip to content

Commit 659183c

Browse files
committed
[OMCSessionRunData] use class to move run of model executable to OMSessionZMQ
1 parent a6bbdba commit 659183c

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
@@ -599,6 +601,53 @@ def omcpath_tempdir(self, tempdir_base: Optional[OMCPath] = None) -> OMCPath:
599601

600602
return tempdir
601603

604+
def omc_run_data_update(self, omc_run_data: OMCSessionRunData) -> OMCSessionRunData:
605+
"""
606+
Modify data based on the selected OMCProcess implementation.
607+
608+
Needs to be implemented in the subclasses.
609+
"""
610+
return self.omc_process.omc_run_data_update(omc_run_data=omc_run_data)
611+
612+
@staticmethod
613+
def run_model_executable(cmd_run_data: OMCSessionRunData) -> int:
614+
"""
615+
Run the command defined in cmd_run_data. This class is defined as static method such that there is no need to
616+
keep instances of over classes around.
617+
"""
618+
619+
my_env = os.environ.copy()
620+
if isinstance(cmd_run_data.cmd_library_path, str):
621+
my_env["PATH"] = cmd_run_data.cmd_library_path + os.pathsep + my_env["PATH"]
622+
623+
cmdl = cmd_run_data.get_cmd()
624+
625+
logger.debug("Run OM command %s in %s", repr(cmdl), cmd_run_data.cmd_path)
626+
try:
627+
cmdres = subprocess.run(
628+
cmdl,
629+
capture_output=True,
630+
text=True,
631+
env=my_env,
632+
cwd=cmd_run_data.cmd_cwd_local,
633+
timeout=cmd_run_data.cmd_timeout,
634+
check=True,
635+
)
636+
stdout = cmdres.stdout.strip()
637+
stderr = cmdres.stderr.strip()
638+
returncode = cmdres.returncode
639+
640+
logger.debug("OM output for command %s:\n%s", repr(cmdl), stdout)
641+
642+
if stderr:
643+
raise OMCSessionException(f"Error running model executable {repr(cmdl)}: {stderr}")
644+
except subprocess.TimeoutExpired as ex:
645+
raise OMCSessionException(f"Timeout running model executable {repr(cmdl)}") from ex
646+
except subprocess.CalledProcessError as ex:
647+
raise OMCSessionException(f"Error running model executable {repr(cmdl)}") from ex
648+
649+
return returncode
650+
602651
def execute(self, command: str):
603652
warnings.warn("This function is depreciated and will be removed in future versions; "
604653
"please use sendExpression() instead", DeprecationWarning, stacklevel=2)
@@ -702,7 +751,7 @@ def sendExpression(self, command: str, parsed: bool = True) -> Any:
702751
raise OMCSessionException("Cannot parse OMC result") from ex
703752

704753

705-
class OMCProcess:
754+
class OMCProcess(metaclass=abc.ABCMeta):
706755

707756
def __init__(
708757
self,
@@ -790,6 +839,15 @@ def _get_portfile_path(self) -> Optional[pathlib.Path]:
790839

791840
return portfile_path
792841

842+
@abc.abstractmethod
843+
def omc_run_data_update(self, omc_run_data: OMCSessionRunData) -> OMCSessionRunData:
844+
"""
845+
Update the OMCSessionRunData object based on the selected OMCProcess implementation.
846+
847+
Needs to be implemented in the subclasses.
848+
"""
849+
raise NotImplementedError("This method must be implemented in subclasses!")
850+
793851

794852
class OMCProcessPort(OMCProcess):
795853
"""
@@ -803,6 +861,12 @@ def __init__(
803861
super().__init__()
804862
self._omc_port = omc_port
805863

864+
def omc_run_data_update(self, omc_run_data: OMCSessionRunData) -> OMCSessionRunData:
865+
"""
866+
Update the OMCSessionRunData object based on the selected OMCProcess implementation.
867+
"""
868+
raise OMCSessionException("OMCProcessPort does not support omc_run_data_update()!")
869+
806870

807871
class OMCProcessLocal(OMCProcess):
808872
"""
@@ -887,6 +951,48 @@ def _omc_port_get(self) -> str:
887951

888952
return port
889953

954+
def omc_run_data_update(self, omc_run_data: OMCSessionRunData) -> OMCSessionRunData:
955+
"""
956+
Update the OMCSessionRunData object based on the selected OMCProcess implementation.
957+
"""
958+
# create a copy of the data
959+
omc_run_data_copy = dataclasses.replace(omc_run_data)
960+
961+
# as this is the local implementation, pathlib.Path can be used
962+
cmd_path = pathlib.Path(omc_run_data_copy.cmd_path)
963+
964+
if platform.system() == "Windows":
965+
path_dll = ""
966+
967+
# set the process environment from the generated .bat file in windows which should have all the dependencies
968+
path_bat = cmd_path / f"{omc_run_data.cmd_model_name}.bat"
969+
if not path_bat.is_file():
970+
raise OMCSessionException("Batch file (*.bat) does not exist " + str(path_bat))
971+
972+
content = path_bat.read_text(encoding='utf-8')
973+
for line in content.splitlines():
974+
match = re.match(r"^SET PATH=([^%]*)", line, re.IGNORECASE)
975+
if match:
976+
path_dll = match.group(1).strip(';') # Remove any trailing semicolons
977+
my_env = os.environ.copy()
978+
my_env["PATH"] = path_dll + os.pathsep + my_env["PATH"]
979+
980+
omc_run_data_copy.cmd_library_path = path_dll
981+
982+
cmd_model_executable = cmd_path / f"{omc_run_data_copy.cmd_model_name}.exe"
983+
else:
984+
# for Linux the paths to the needed libraries should be included in the executable (using rpath)
985+
cmd_model_executable = cmd_path / omc_run_data_copy.cmd_model_name
986+
987+
if not cmd_model_executable.is_file():
988+
raise OMCSessionException(f"Application file path not found: {cmd_model_executable}")
989+
omc_run_data_copy.cmd_model_executable = cmd_model_executable.as_posix()
990+
991+
# define local(!) working directory
992+
omc_run_data_copy.cmd_cwd_local = omc_run_data.cmd_path
993+
994+
return omc_run_data_copy
995+
890996

891997
class OMCProcessDockerHelper(OMCProcess):
892998
"""
@@ -1002,6 +1108,12 @@ def get_docker_container_id(self) -> str:
10021108

10031109
return self._dockerCid
10041110

1111+
def omc_run_data_update(self, omc_run_data: OMCSessionRunData) -> OMCSessionRunData:
1112+
"""
1113+
Update the OMCSessionRunData object based on the selected OMCProcess implementation.
1114+
"""
1115+
raise OMCSessionException("OMCProcessDocker* does not support omc_run_data_update()!")
1116+
10051117

10061118
class OMCProcessDocker(OMCProcessDockerHelper):
10071119
"""
@@ -1316,3 +1428,9 @@ def _omc_port_get(self) -> str:
13161428
f"pid={self._omc_process.pid if isinstance(self._omc_process, subprocess.Popen) else '?'}")
13171429

13181430
return port
1431+
1432+
def omc_run_data_update(self, omc_run_data: OMCSessionRunData) -> OMCSessionRunData:
1433+
"""
1434+
Update the OMCSessionRunData object based on the selected OMCProcess implementation.
1435+
"""
1436+
raise OMCSessionException("OMCProcessWSL does not support omc_run_data_update()!")

0 commit comments

Comments
 (0)