Skip to content

Commit d494b90

Browse files
committed
[ModelExecution*] move classes into model_execution.py
1 parent 226cb05 commit d494b90

4 files changed

Lines changed: 367 additions & 346 deletions

File tree

OMPython/ModelicaSystem.py

Lines changed: 4 additions & 255 deletions
Original file line numberDiff line numberDiff line change
@@ -21,10 +21,12 @@
2121

2222
import numpy as np
2323

24-
from OMPython.OMCSession import (
24+
from OMPython.model_execution import (
25+
ModelExecutionCmd,
2526
ModelExecutionData,
2627
ModelExecutionException,
27-
28+
)
29+
from OMPython.OMCSession import (
2830
OMSessionException,
2931
OMCSessionLocal,
3032

@@ -97,259 +99,6 @@ def __getitem__(self, index: int):
9799
return {0: self.A, 1: self.B, 2: self.C, 3: self.D}[index]
98100

99101

100-
class ModelExecutionCmd:
101-
"""
102-
All information about a compiled model executable. This should include data about all structured parameters, i.e.
103-
parameters which need a recompilation of the model. All non-structured parameters can be easily changed without
104-
the need for recompilation.
105-
"""
106-
107-
def __init__(
108-
self,
109-
runpath: os.PathLike,
110-
cmd_prefix: list[str],
111-
cmd_local: bool = False,
112-
cmd_windows: bool = False,
113-
timeout: Optional[float] = None,
114-
model_name: Optional[str] = None,
115-
) -> None:
116-
if model_name is None:
117-
raise ModelExecutionException("Missing model name!")
118-
119-
self._cmd_local = cmd_local
120-
self._cmd_windows = cmd_windows
121-
self._cmd_prefix = cmd_prefix
122-
self._runpath = pathlib.PurePosixPath(runpath)
123-
self._model_name = model_name
124-
125-
if timeout is None:
126-
# a separate timeout is defined here to allow the use of the class independent of the normal call chain via
127-
# classes derived from OMSession (OMSESSION_TIMEOUT)
128-
self._timeout: float = MODEL_EXECUTION_TIMEOUT
129-
else:
130-
self._timeout = timeout
131-
132-
# dictionaries of command line arguments for the model executable
133-
self._args: dict[str, str | None] = {}
134-
# 'override' argument needs special handling, as it is a dict on its own saved as dict elements following the
135-
# structure: 'key' => 'key=value'
136-
self._arg_override: dict[str, str] = {}
137-
138-
def arg_set(
139-
self,
140-
key: str,
141-
val: Optional[str | dict[str, Any] | numbers.Number] = None,
142-
) -> None:
143-
"""
144-
Set one argument for the executable model.
145-
146-
Args:
147-
key: identifier / argument name to be used for the call of the model executable.
148-
val: value for the given key; None for no value and for key == 'override' a dictionary can be used which
149-
indicates variables to override
150-
"""
151-
152-
def override2str(
153-
orkey: str,
154-
orval: str | bool | numbers.Number,
155-
) -> str:
156-
"""
157-
Convert a value for 'override' to a string taking into account differences between Modelica and Python.
158-
"""
159-
# check oval for any string representations of numbers (or bool) and convert these to Python representations
160-
if isinstance(orval, str):
161-
try:
162-
val_evaluated = ast.literal_eval(orval)
163-
if isinstance(val_evaluated, (numbers.Number, bool)):
164-
orval = val_evaluated
165-
except (ValueError, SyntaxError):
166-
pass
167-
168-
if isinstance(orval, str):
169-
val_str = orval.strip()
170-
elif isinstance(orval, bool):
171-
val_str = 'true' if orval else 'false'
172-
elif isinstance(orval, numbers.Number):
173-
val_str = str(orval)
174-
else:
175-
raise ModelExecutionException(f"Invalid value for override key {orkey}: {type(orval)}")
176-
177-
return f"{orkey}={val_str}"
178-
179-
if not isinstance(key, str):
180-
raise ModelExecutionException(f"Invalid argument key: {repr(key)} (type: {type(key)})")
181-
key = key.strip()
182-
183-
if isinstance(val, dict):
184-
if key != 'override':
185-
raise ModelExecutionException("Dictionary input only possible for key 'override'!")
186-
187-
for okey, oval in val.items():
188-
if not isinstance(okey, str):
189-
raise ModelExecutionException("Invalid key for argument 'override': "
190-
f"{repr(okey)} (type: {type(okey)})")
191-
192-
if not isinstance(oval, (str, bool, numbers.Number, type(None))):
193-
raise ModelExecutionException(f"Invalid input for 'override'.{repr(okey)}: "
194-
f"{repr(oval)} (type: {type(oval)})")
195-
196-
if okey in self._arg_override:
197-
if oval is None:
198-
logger.info(f"Remove model executable override argument: {repr(self._arg_override[okey])}")
199-
del self._arg_override[okey]
200-
continue
201-
202-
logger.info(f"Update model executable override argument: {repr(okey)} = {repr(oval)} "
203-
f"(was: {repr(self._arg_override[okey])})")
204-
205-
if oval is not None:
206-
self._arg_override[okey] = override2str(orkey=okey, orval=oval)
207-
208-
argval = ','.join(sorted(self._arg_override.values()))
209-
elif val is None:
210-
argval = None
211-
elif isinstance(val, str):
212-
argval = val.strip()
213-
elif isinstance(val, numbers.Number):
214-
argval = str(val)
215-
else:
216-
raise ModelExecutionException(f"Invalid argument value for {repr(key)}: {repr(val)} (type: {type(val)})")
217-
218-
if key in self._args:
219-
logger.warning(f"Override model executable argument: {repr(key)} = {repr(argval)} "
220-
f"(was: {repr(self._args[key])})")
221-
self._args[key] = argval
222-
223-
def arg_get(self, key: str) -> Optional[str | dict[str, str | bool | numbers.Number]]:
224-
"""
225-
Return the value for the given key
226-
"""
227-
if key in self._args:
228-
return self._args[key]
229-
230-
return None
231-
232-
def args_set(
233-
self,
234-
args: dict[str, Optional[str | dict[str, Any] | numbers.Number]],
235-
) -> None:
236-
"""
237-
Define arguments for the model executable.
238-
"""
239-
for arg in args:
240-
self.arg_set(key=arg, val=args[arg])
241-
242-
def get_cmd_args(self) -> list[str]:
243-
"""
244-
Get a list with the command arguments for the model executable.
245-
"""
246-
247-
cmdl = []
248-
for key in sorted(self._args):
249-
if self._args[key] is None:
250-
cmdl.append(f"-{key}")
251-
else:
252-
cmdl.append(f"-{key}={self._args[key]}")
253-
254-
return cmdl
255-
256-
def definition(self) -> ModelExecutionData:
257-
"""
258-
Define all needed data to run the model executable. The data is stored in an OMCSessionRunData object.
259-
"""
260-
# ensure that a result filename is provided
261-
result_file = self.arg_get('r')
262-
if not isinstance(result_file, str):
263-
result_file = (self._runpath / f"{self._model_name}.mat").as_posix()
264-
265-
# as this is the local implementation, pathlib.Path can be used
266-
cmd_path = self._runpath
267-
268-
cmd_library_path = None
269-
if self._cmd_local and self._cmd_windows:
270-
cmd_library_path = ""
271-
272-
# set the process environment from the generated .bat file in windows which should have all the dependencies
273-
# for this pathlib.PurePosixPath() must be converted to a pathlib.Path() object, i.e. WindowsPath
274-
path_bat = pathlib.Path(cmd_path) / f"{self._model_name}.bat"
275-
if not path_bat.is_file():
276-
raise ModelExecutionException("Batch file (*.bat) does not exist " + str(path_bat))
277-
278-
content = path_bat.read_text(encoding='utf-8')
279-
for line in content.splitlines():
280-
match = re.match(pattern=r"^SET PATH=([^%]*)", string=line, flags=re.IGNORECASE)
281-
if match:
282-
cmd_library_path = match.group(1).strip(';') # Remove any trailing semicolons
283-
my_env = os.environ.copy()
284-
my_env["PATH"] = cmd_library_path + os.pathsep + my_env["PATH"]
285-
286-
cmd_model_executable = cmd_path / f"{self._model_name}.exe"
287-
else:
288-
# for Linux the paths to the needed libraries should be included in the executable (using rpath)
289-
cmd_model_executable = cmd_path / self._model_name
290-
291-
# define local(!) working directory
292-
cmd_cwd_local = None
293-
if self._cmd_local:
294-
cmd_cwd_local = cmd_path.as_posix()
295-
296-
omc_run_data = ModelExecutionData(
297-
cmd_path=cmd_path.as_posix(),
298-
cmd_model_name=self._model_name,
299-
cmd_args=self.get_cmd_args(),
300-
cmd_result_file=result_file,
301-
cmd_prefix=self._cmd_prefix,
302-
cmd_library_path=cmd_library_path,
303-
cmd_model_executable=cmd_model_executable.as_posix(),
304-
cmd_cwd_local=cmd_cwd_local,
305-
cmd_timeout=self._timeout,
306-
)
307-
308-
return omc_run_data
309-
310-
@staticmethod
311-
def parse_simflags(simflags: str) -> dict[str, Optional[str | dict[str, Any] | numbers.Number]]:
312-
"""
313-
Parse a simflag definition; this is deprecated!
314-
315-
The return data can be used as input for self.args_set().
316-
"""
317-
warnings.warn(
318-
message="The argument 'simflags' is depreciated and will be removed in future versions; "
319-
"please use 'simargs' instead",
320-
category=DeprecationWarning,
321-
stacklevel=2,
322-
)
323-
324-
simargs: dict[str, Optional[str | dict[str, Any] | numbers.Number]] = {}
325-
326-
args = [s for s in simflags.split(' ') if s]
327-
for arg in args:
328-
if arg[0] != '-':
329-
raise ModelExecutionException(f"Invalid simulation flag: {arg}")
330-
arg = arg[1:]
331-
parts = arg.split('=')
332-
if len(parts) == 1:
333-
simargs[parts[0]] = None
334-
elif parts[0] == 'override':
335-
override = '='.join(parts[1:])
336-
337-
override_dict = {}
338-
for item in override.split(','):
339-
kv = item.split('=')
340-
if not 0 < len(kv) < 3:
341-
raise ModelExecutionException(f"Invalid value for '-override': {override}")
342-
if kv[0]:
343-
try:
344-
override_dict[kv[0]] = kv[1]
345-
except (KeyError, IndexError) as ex:
346-
raise ModelExecutionException(f"Invalid value for '-override': {override}") from ex
347-
348-
simargs[parts[0]] = override_dict
349-
350-
return simargs
351-
352-
353102
class ModelicaSystemABC(metaclass=abc.ABCMeta):
354103
"""
355104
Base class to simulate a Modelica models.

OMPython/OMCSession.py

Lines changed: 0 additions & 86 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@
66
from __future__ import annotations
77

88
import abc
9-
import dataclasses
109
import io
1110
import json
1211
import logging
@@ -814,91 +813,6 @@ def size(self) -> int:
814813
OMPathRunnerBash = _OMPathRunnerBash
815814

816815

817-
class ModelExecutionException(Exception):
818-
"""
819-
Exception which is raised by ModelException* classes.
820-
"""
821-
822-
823-
@dataclasses.dataclass
824-
class ModelExecutionData:
825-
"""
826-
Data class to store the command line data for running a model executable in the OMC environment.
827-
828-
All data should be defined for the environment, where OMC is running (local, docker or WSL)
829-
830-
To use this as a definition of an OMC simulation run, it has to be processed within
831-
OMCProcess*.self_update(). This defines the attribute cmd_model_executable.
832-
"""
833-
# cmd_path is the expected working directory
834-
cmd_path: str
835-
cmd_model_name: str
836-
# command prefix data (as list of strings); needed for docker or WSL
837-
cmd_prefix: list[str]
838-
# cmd_model_executable is build out of cmd_path and cmd_model_name; this is mainly needed on Windows (add *.exe)
839-
cmd_model_executable: str
840-
# command line arguments for the model executable
841-
cmd_args: list[str]
842-
# result file with the simulation output
843-
cmd_result_file: str
844-
# command timeout
845-
cmd_timeout: float
846-
847-
# additional library search path; this is mainly needed if OMCProcessLocal is run on Windows
848-
cmd_library_path: Optional[str] = None
849-
# working directory to be used on the *local* system
850-
cmd_cwd_local: Optional[str] = None
851-
852-
def get_cmd(self) -> list[str]:
853-
"""
854-
Get the command line to run the model executable in the environment defined by the OMCProcess definition.
855-
"""
856-
857-
cmdl = self.cmd_prefix
858-
cmdl += [self.cmd_model_executable]
859-
cmdl += self.cmd_args
860-
861-
return cmdl
862-
863-
def run(self) -> int:
864-
"""
865-
Run the model execution defined in this class.
866-
"""
867-
868-
my_env = os.environ.copy()
869-
if isinstance(self.cmd_library_path, str):
870-
my_env["PATH"] = self.cmd_library_path + os.pathsep + my_env["PATH"]
871-
872-
cmdl = self.get_cmd()
873-
874-
logger.debug("Run OM command %s in %s (timeout=%2fs)", repr(cmdl), self.cmd_path, self.cmd_timeout)
875-
try:
876-
cmdres = subprocess.run(
877-
cmdl,
878-
capture_output=True,
879-
text=True,
880-
env=my_env,
881-
cwd=self.cmd_cwd_local,
882-
timeout=self.cmd_timeout,
883-
check=True,
884-
)
885-
stdout = cmdres.stdout.strip()
886-
stderr = cmdres.stderr.strip()
887-
returncode = cmdres.returncode
888-
889-
logger.debug("OM output for command %s:\n%s", repr(cmdl), stdout)
890-
891-
if stderr:
892-
raise ModelExecutionException(f"Error running model executable {repr(cmdl)}: {stderr}")
893-
except subprocess.TimeoutExpired as ex:
894-
raise ModelExecutionException("OMPython timeout running model executable "
895-
f"(timeout={self.cmd_timeout:.2f}s){repr(cmdl)}: {ex}") from ex
896-
except subprocess.CalledProcessError as ex:
897-
raise ModelExecutionException(f"Error running model executable {repr(cmdl)}: {ex}") from ex
898-
899-
return returncode
900-
901-
902816
class PostInitCaller(type):
903817
"""
904818
Metaclass definition to define a new function __post_init__() which is called after all __init__() functions where

0 commit comments

Comments
 (0)