Skip to content

Commit 58d6912

Browse files
committed
[ModelExecution*] move classes into model_execution.py
1 parent f44ad1a commit 58d6912

File tree

4 files changed

+360
-339
lines changed

4 files changed

+360
-339
lines changed

OMPython/ModelicaSystem.py

Lines changed: 4 additions & 249 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

@@ -95,253 +97,6 @@ def __getitem__(self, index: int):
9597
return {0: self.A, 1: self.B, 2: self.C, 3: self.D}[index]
9698

9799

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

OMPython/OMCSession.py

Lines changed: 0 additions & 85 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
@@ -812,90 +811,6 @@ def size(self) -> int:
812811
OMPathRunnerBash = _OMPathRunnerBash
813812

814813

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

0 commit comments

Comments
 (0)