Skip to content

Commit eb12130

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

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
@@ -38,15 +38,12 @@
3838
import numbers
3939
import numpy as np
4040
import os
41-
import platform
42-
import re
43-
import subprocess
4441
import textwrap
4542
from typing import Optional, Any
4643
import warnings
4744
import xml.etree.ElementTree as ET
4845

49-
from OMPython.OMCSession import OMCSessionException, OMCSessionZMQ, OMCProcessLocal, OMCPath
46+
from OMPython.OMCSession import OMCSessionException, OMCSessionRunData, OMCSessionZMQ, OMCProcessLocal, OMCPath
5047

5148
# define logger using the current module name as ID
5249
logger = logging.getLogger(__name__)
@@ -112,7 +109,14 @@ def __getitem__(self, index: int):
112109
class ModelicaSystemCmd:
113110
"""A compiled model executable."""
114111

115-
def __init__(self, runpath: OMCPath, modelname: str, timeout: Optional[float] = None) -> None:
112+
def __init__(
113+
self,
114+
session: OMCSessionZMQ,
115+
runpath: OMCPath,
116+
modelname: str,
117+
timeout: Optional[float] = None,
118+
) -> None:
119+
self._session = session
116120
self._runpath = runpath
117121
self._model_name = modelname
118122
self._timeout = timeout
@@ -227,27 +231,12 @@ def args_set(
227231
for arg in args:
228232
self.arg_set(key=arg, val=args[arg])
229233

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

248-
path_exe = self.get_exe()
249-
250-
cmdl = [path_exe.as_posix()]
239+
cmdl = []
251240
for key in sorted(self._args):
252241
if self._args[key] is None:
253242
cmdl.append(f"-{key}")
@@ -256,54 +245,26 @@ def get_cmd(self) -> list:
256245

257246
return cmdl
258247

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

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

306-
return returncode
267+
return omc_run_data_updated
307268

308269
@staticmethod
309270
def parse_simflags(simflags: str) -> dict[str, Optional[str | dict[str, Any] | numbers.Number]]:
@@ -1031,6 +992,7 @@ def simulate_cmd(
1031992
"""
1032993

1033994
om_cmd = ModelicaSystemCmd(
995+
session=self._getconn,
1034996
runpath=self.getWorkDirectory(),
1035997
modelname=self._model_name,
1036998
timeout=timeout,
@@ -1127,7 +1089,8 @@ def simulate(
11271089
if self._result_file.is_file():
11281090
self._result_file.unlink()
11291091
# ... run simulation ...
1130-
returncode = om_cmd.run()
1092+
cmd_definition = om_cmd.definition()
1093+
returncode = self._getconn.run_model_executable(cmd_run_data=cmd_definition)
11311094
# and check returncode *AND* resultfile
11321095
if returncode != 0 and self._result_file.is_file():
11331096
# check for an empty (=> 0B) result file which indicates a crash of the model executable
@@ -1679,6 +1642,7 @@ def linearize(
16791642
)
16801643

16811644
om_cmd = ModelicaSystemCmd(
1645+
session=self._getconn,
16821646
runpath=self.getWorkDirectory(),
16831647
modelname=self._model_name,
16841648
timeout=timeout,
@@ -1716,7 +1680,8 @@ def linearize(
17161680
linear_file = self.getWorkDirectory() / "linearized_model.py"
17171681
linear_file.unlink(missing_ok=True)
17181682

1719-
returncode = om_cmd.run()
1683+
cmd_definition = om_cmd.definition()
1684+
returncode = self._getconn.run_model_executable(cmd_run_data=cmd_definition)
17201685
if returncode != 0:
17211686
raise ModelicaSystemError(f"Linearize failed with return code: {returncode}")
17221687
if not linear_file.is_file():

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)
@@ -703,7 +752,7 @@ def sendExpression(self, command: str, parsed: bool = True) -> Any:
703752
raise OMCSessionException("Cannot parse OMC result") from ex
704753

705754

706-
class OMCProcess:
755+
class OMCProcess(metaclass=abc.ABCMeta):
707756

708757
def __init__(
709758
self,
@@ -791,6 +840,15 @@ def _get_portfile_path(self) -> Optional[pathlib.Path]:
791840

792841
return portfile_path
793842

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

795853
class OMCProcessPort(OMCProcess):
796854
"""
@@ -804,6 +862,12 @@ def __init__(
804862
super().__init__()
805863
self._omc_port = omc_port
806864

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

808872
class OMCProcessLocal(OMCProcess):
809873
"""
@@ -888,6 +952,48 @@ def _omc_port_get(self) -> str:
888952

889953
return port
890954

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

892998
class OMCProcessDockerHelper(OMCProcess):
893999
"""
@@ -1003,6 +1109,12 @@ def get_docker_container_id(self) -> str:
10031109

10041110
return self._dockerCid
10051111

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

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

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

0 commit comments

Comments
 (0)