Skip to content

Commit a4c8378

Browse files
committed
[ModelicaSystemCmd] use OMCPath for file system interactions
1 parent f1695a9 commit a4c8378

5 files changed

Lines changed: 180 additions & 59 deletions

File tree

OMPython/ModelicaSystem.py

Lines changed: 64 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -39,15 +39,13 @@
3939
import numpy as np
4040
import os
4141
import pathlib
42-
import platform
43-
import re
4442
import subprocess
4543
import textwrap
4644
from typing import Optional, Any
4745
import warnings
4846
import xml.etree.ElementTree as ET
4947

50-
from OMPython.OMCSession import OMCSessionException, OMCSessionZMQ, OMCProcessLocal, OMCPath
48+
from OMPython.OMCSession import OMCSessionException, OMCSessionZMQ, OMCProcessLocal, OMCPath, OMCSessionRunData
5149

5250
# define logger using the current module name as ID
5351
logger = logging.getLogger(__name__)
@@ -113,7 +111,14 @@ def __getitem__(self, index: int):
113111
class ModelicaSystemCmd:
114112
"""A compiled model executable."""
115113

116-
def __init__(self, runpath: pathlib.Path, modelname: str, timeout: Optional[float] = None) -> None:
114+
def __init__(
115+
self,
116+
session: OMCSessionZMQ,
117+
runpath: OMCPath,
118+
modelname: str,
119+
timeout: Optional[float] = None,
120+
) -> None:
121+
self._session = session
117122
self._runpath = pathlib.Path(runpath).resolve().absolute()
118123
self._model_name = modelname
119124
self._timeout = timeout
@@ -174,27 +179,12 @@ def args_set(self, args: dict[str, Optional[str | dict[str, str]]]) -> None:
174179
for arg in args:
175180
self.arg_set(key=arg, val=args[arg])
176181

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

195-
path_exe = self.get_exe()
196-
197-
cmdl = [path_exe.as_posix()]
187+
cmdl = []
198188
for key in self._args:
199189
if self._args[key] is None:
200190
cmdl.append(f"-{key}")
@@ -203,40 +193,49 @@ def get_cmd(self) -> list:
203193

204194
return cmdl
205195

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

214-
cmdl: list = self.get_cmd()
215-
216-
logger.debug("Run OM command %s in %s", repr(cmdl), self._runpath.as_posix())
213+
return omc_run_data
217214

218-
if platform.system() == "Windows":
219-
path_dll = ""
215+
@staticmethod
216+
def run_cmd(cmd_run_data: OMCSessionRunData) -> int:
217+
"""
218+
Run the command defined in cmd_run_data. This class is defined as static method such that there is no need to
219+
keep instances of over classes around.
220+
"""
220221

221-
# set the process environment from the generated .bat file in windows which should have all the dependencies
222-
path_bat = self._runpath / f"{self._model_name}.bat"
223-
if not path_bat.exists():
224-
raise ModelicaSystemError("Batch file (*.bat) does not exist " + str(path_bat))
222+
my_env = os.environ.copy()
223+
if isinstance(cmd_run_data.cmd_library_path, str):
224+
my_env["PATH"] = cmd_run_data.cmd_library_path + os.pathsep + my_env["PATH"]
225225

226-
with open(file=path_bat, mode='r', encoding='utf-8') as fh:
227-
for line in fh:
228-
match = re.match(r"^SET PATH=([^%]*)", line, re.IGNORECASE)
229-
if match:
230-
path_dll = match.group(1).strip(';') # Remove any trailing semicolons
231-
my_env = os.environ.copy()
232-
my_env["PATH"] = path_dll + os.pathsep + my_env["PATH"]
233-
else:
234-
# TODO: how to handle path to resources of external libraries for any system not Windows?
235-
my_env = None
226+
cmdl = cmd_run_data.get_cmd()
236227

228+
logger.debug("Run OM command %s in %s", repr(cmdl), cmd_run_data.cmd_path)
237229
try:
238-
cmdres = subprocess.run(cmdl, capture_output=True, text=True, env=my_env, cwd=self._runpath,
239-
timeout=self._timeout, check=True)
230+
cmdres = subprocess.run(
231+
cmdl,
232+
capture_output=True,
233+
text=True,
234+
env=my_env,
235+
cwd=cmd_run_data.cmd_path,
236+
timeout=cmd_run_data.cmd_timeout,
237+
check=True,
238+
)
240239
stdout = cmdres.stdout.strip()
241240
stderr = cmdres.stderr.strip()
242241
returncode = cmdres.returncode
@@ -252,6 +251,17 @@ def run(self) -> int:
252251

253252
return returncode
254253

254+
def run(self) -> int:
255+
"""Run the requested simulation.
256+
257+
Returns
258+
-------
259+
Subprocess return code (0 on success).
260+
"""
261+
cmd_run_data = self.run_def()
262+
cmd_run_data = self._session.omc_run_data_update(cmd_run_data, session=self._session)
263+
return self.run_cmd(cmd_run_data=cmd_run_data)
264+
255265
@staticmethod
256266
def parse_simflags(simflags: str) -> dict[str, Optional[str | dict[str, str]]]:
257267
"""
@@ -952,7 +962,8 @@ def simulate_cmd(
952962
"""
953963

954964
om_cmd = ModelicaSystemCmd(
955-
runpath=pathlib.Path(self.getWorkDirectory()),
965+
session=self._getconn,
966+
runpath=self.getWorkDirectory(),
956967
modelname=self._model_name,
957968
timeout=timeout,
958969
)
@@ -1562,7 +1573,8 @@ def linearize(self, lintime: Optional[float] = None, simflags: Optional[str] = N
15621573
)
15631574

15641575
om_cmd = ModelicaSystemCmd(
1565-
runpath=pathlib.Path(self.getWorkDirectory()),
1576+
session=self._getconn,
1577+
runpath=self.getWorkDirectory(),
15661578
modelname=self._model_name,
15671579
timeout=timeout,
15681580
)

OMPython/OMCSession.py

Lines changed: 104 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,11 +34,14 @@
3434
CONDITIONS OF OSMC-PL.
3535
"""
3636

37+
import abc
38+
import dataclasses
3739
import io
3840
import json
3941
import logging
4042
import os
4143
import pathlib
44+
import platform
4245
import psutil
4346
import pyparsing
4447
import re
@@ -431,6 +434,43 @@ def size(self) -> int:
431434
OMCPath = OMCPathReal
432435

433436

437+
@dataclasses.dataclass
438+
class OMCSessionRunData:
439+
"""
440+
Data class to store the command line data for running a model executable in the OMC environment.
441+
"""
442+
# cmd_path is based on the selected OMCProcess definition
443+
cmd_path: str
444+
cmd_model_name: str
445+
# command line arguments for the model executable
446+
cmd_args: list[str]
447+
# result file with the simulation output
448+
# cmd_result_path is based on the selected OMCProcess definition
449+
cmd_result_path: str
450+
451+
# command prefix data (as list of strings); needed for docker or WSL
452+
cmd_prefix: Optional[list[str]] = None
453+
# cmd_model_executable is build out of cmd_path and cmd_model_name; this is mainly needed on Windows (add *.exe)
454+
cmd_model_executable: Optional[str] = None
455+
# additional library search path; this is mainly needed if OMCProcessLocal is run on Windows
456+
cmd_library_path: Optional[str] = None
457+
# command timeout
458+
cmd_timeout: Optional[float] = 10.0
459+
460+
def get_cmd(self) -> list[str]:
461+
"""
462+
Get the command line to run the model executable in the environment defined by the OMCProcess definition.
463+
"""
464+
465+
if self.cmd_model_executable is None:
466+
raise OMCSessionException("No model file defined for the model executable!")
467+
468+
cmdl = [] if self.cmd_prefix is None else self.cmd_prefix
469+
cmdl += [self.cmd_model_executable] + self.cmd_args
470+
471+
return cmdl
472+
473+
434474
class OMCSessionZMQ:
435475

436476
def __init__(
@@ -521,15 +561,26 @@ def omcpath_tempdir(self) -> OMCPath:
521561

522562
return tempdir
523563

564+
def omc_run_data_update(self, omc_run_data: OMCSessionRunData, session: OMCSessionZMQ) -> OMCSessionRunData:
565+
"""
566+
Modify data based on the selected OMCProcess implementation.
567+
568+
Needs to be implemented in the subclasses.
569+
"""
570+
return self.omc_process.omc_run_data_update(omc_run_data=omc_run_data, session=session)
571+
524572
def execute(self, command: str):
525573
warnings.warn("This function is depreciated and will be removed in future versions; "
526574
"please use sendExpression() instead", DeprecationWarning, stacklevel=2)
527575

528576
return self.sendExpression(command, parsed=False)
529577

530578
def sendExpression(self, command: str, parsed: bool = True) -> Any:
579+
"""
580+
Send an expression to the OMC server and return the result.
581+
"""
531582
if self.omc_zmq is None:
532-
raise OMCSessionException("No OMC running. Create a new instance of OMCSessionZMQ!")
583+
raise OMCSessionException("No OMC running. Create a new instance of OMCProcess!")
533584

534585
logger.debug("sendExpression(%r, parsed=%r)", command, parsed)
535586

@@ -624,7 +675,7 @@ def sendExpression(self, command: str, parsed: bool = True) -> Any:
624675
raise OMCSessionException("Cannot parse OMC result") from ex
625676

626677

627-
class OMCProcess:
678+
class OMCProcess(metaclass=abc.ABCMeta):
628679

629680
def __init__(
630681
self,
@@ -706,6 +757,15 @@ def _get_portfile_path(self) -> Optional[pathlib.Path]:
706757

707758
return portfile_path
708759

760+
@abc.abstractmethod
761+
def omc_run_data_update(self, omc_run_data: OMCSessionRunData, session: OMCSessionZMQ) -> OMCSessionRunData:
762+
"""
763+
Modify data based on the selected OMCProcess implementation.
764+
765+
Needs to be implemented in the subclasses.
766+
"""
767+
raise NotImplementedError("This method must be implemented in subclasses!")
768+
709769

710770
class OMCProcessPort(OMCProcess):
711771

@@ -716,6 +776,9 @@ def __init__(
716776
super().__init__()
717777
self._omc_port = omc_port
718778

779+
def omc_run_data_update(self, omc_run_data: OMCSessionRunData, session: OMCSessionZMQ) -> OMCSessionRunData:
780+
raise OMCSessionException("OMCProcessPort does not support omc_run_data_update()!")
781+
719782

720783
class OMCProcessLocal(OMCProcess):
721784

@@ -797,6 +860,39 @@ def _omc_port_get(self) -> str:
797860

798861
return port
799862

863+
def omc_run_data_update(self, omc_run_data: OMCSessionRunData, session: OMCSessionZMQ) -> OMCSessionRunData:
864+
omc_run_data_copy = dataclasses.replace(omc_run_data)
865+
866+
cmd_path = session.omcpath(omc_run_data_copy.cmd_path)
867+
868+
if platform.system() == "Windows":
869+
path_dll = ""
870+
871+
# set the process environment from the generated .bat file in windows which should have all the dependencies
872+
path_bat = cmd_path / f"{omc_run_data.cmd_model_name}.bat"
873+
if not path_bat.is_file():
874+
raise OMCSessionException("Batch file (*.bat) does not exist " + str(path_bat))
875+
876+
content = path_bat.read_text(encoding='utf-8')
877+
for line in content.splitlines():
878+
match = re.match(r"^SET PATH=([^%]*)", line, re.IGNORECASE)
879+
if match:
880+
path_dll = match.group(1).strip(';') # Remove any trailing semicolons
881+
my_env = os.environ.copy()
882+
my_env["PATH"] = path_dll + os.pathsep + my_env["PATH"]
883+
884+
omc_run_data_copy.cmd_library_path = path_dll
885+
886+
cmd_model_executable = cmd_path / f"{omc_run_data_copy.cmd_model_name}.exe"
887+
else:
888+
cmd_model_executable = cmd_path / omc_run_data_copy.cmd_model_name
889+
890+
if not cmd_model_executable.is_file():
891+
raise OMCSessionException(f"Application file path not found: {cmd_model_executable}")
892+
omc_run_data_copy.cmd_model_executable = cmd_model_executable.as_posix()
893+
894+
return omc_run_data_copy
895+
800896

801897
class OMCProcessDockerHelper(OMCProcess):
802898

@@ -903,6 +999,9 @@ def get_docker_container_id(self) -> str:
903999

9041000
return self._dockerCid
9051001

1002+
def omc_run_data_update(self, omc_run_data: OMCSessionRunData, session: OMCSessionZMQ) -> OMCSessionRunData:
1003+
raise OMCSessionException("OMCProcessDocker(Container) does not support omc_run_data_update()!")
1004+
9061005

9071006
class OMCProcessDocker(OMCProcessDockerHelper):
9081007

@@ -1208,3 +1307,6 @@ def _omc_port_get(self) -> str:
12081307
f"pid={self._omc_process.pid if isinstance(self._omc_process, subprocess.Popen) else '?'}")
12091308

12101309
return port
1310+
1311+
def omc_run_data_update(self, omc_run_data: OMCSessionRunData, session: OMCSessionZMQ) -> OMCSessionRunData:
1312+
raise OMCSessionException("OMCProcessWSL does not support omc_run_data_update()!")

tests/test_FMIExport.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
11
import OMPython
22
import shutil
33
import os
4+
import pathlib
45

56

67
def test_CauerLowPassAnalog():
78
mod = OMPython.ModelicaSystem(modelName="Modelica.Electrical.Analog.Examples.CauerLowPassAnalog",
89
lmodel=["Modelica"])
9-
tmp = mod.getWorkDirectory()
10+
# TODO [OMCPath]: need to work using OMCPath
11+
tmp = pathlib.Path(mod.getWorkDirectory())
1012
try:
1113
fmu = mod.convertMo2Fmu(fileNamePrefix="CauerLowPassAnalog")
1214
assert os.path.exists(fmu)
@@ -16,7 +18,8 @@ def test_CauerLowPassAnalog():
1618

1719
def test_DrumBoiler():
1820
mod = OMPython.ModelicaSystem(modelName="Modelica.Fluid.Examples.DrumBoiler.DrumBoiler", lmodel=["Modelica"])
19-
tmp = mod.getWorkDirectory()
21+
# TODO [OMCPath]: need to work using OMCPath
22+
tmp = pathlib.Path(mod.getWorkDirectory())
2023
try:
2124
fmu = mod.convertMo2Fmu(fileNamePrefix="DrumBoiler")
2225
assert os.path.exists(fmu)

tests/test_ModelicaSystem.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,7 @@ def test_customBuildDirectory(tmp_path, model_firstorder):
105105
tmpdir = tmp_path / "tmpdir1"
106106
tmpdir.mkdir()
107107
m = OMPython.ModelicaSystem(filePath, "M", customBuildDirectory=tmpdir)
108+
# TODO [OMCPath]: need to work using OMCPath
108109
assert pathlib.Path(m.getWorkDirectory()).resolve() == tmpdir.resolve()
109110
result_file = tmpdir / "a.mat"
110111
assert not result_file.exists()

0 commit comments

Comments
 (0)