Skip to content

Commit ae77187

Browse files
committed
(C001) Runner definition
[OMCSession] add *Runner related classes for OMPath and OMSession [ModelicaSystem] add ModelicaSystemRunner [test_ModelicaSystemRunner] add test case for ModelicaSystemRunner [ModelicaSystem] add ModelicaDoERunner [test_ModelicaDoERunner] add test case for ModelicaDoERunner [OMCSession] move OMCPathRunner* into the if clause [OMSessionRunner] fix usage of sendExpression() [__init__] add missing definitions for *Runner classes [ModelicaDoERunner] fix definition; allow all variations of ModelicaSystem* [test_ModelicaDoERunner] fix definition; test ModelicaSystem(OCM|Runner) [ModelicaDoEABC] add get_resultpath()
1 parent 7626599 commit ae77187

File tree

5 files changed

+513
-0
lines changed

5 files changed

+513
-0
lines changed

OMPython/ModelicaSystem.py

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
OMPathABC,
3232

3333
OMSessionABC,
34+
OMSessionRunner,
3435
)
3536

3637
# define logger using the current module name as ID
@@ -2228,6 +2229,12 @@ def get_session(self) -> OMSessionABC:
22282229
"""
22292230
return self._mod.get_session()
22302231

2232+
def get_resultpath(self) -> OMPathABC:
2233+
"""
2234+
Get the path there the result data is saved.
2235+
"""
2236+
return self._resultpath
2237+
22312238
def prepare(self) -> int:
22322239
"""
22332240
Prepare the DoE by evaluating the parameters. Each structural parameter requires a new instance of
@@ -2579,3 +2586,98 @@ class ModelicaSystemDoE(ModelicaDoEOMC):
25792586
"""
25802587
Compatibility class.
25812588
"""
2589+
2590+
2591+
class ModelicaSystemRunner(ModelicaSystemABC):
2592+
"""
2593+
Class to simulate a Modelica model using a pre-compiled model binary.
2594+
"""
2595+
2596+
def __init__(
2597+
self,
2598+
work_directory: Optional[str | os.PathLike] = None,
2599+
session: Optional[OMSessionABC] = None,
2600+
) -> None:
2601+
if session is None:
2602+
session = OMSessionRunner()
2603+
2604+
if not isinstance(session, OMSessionRunner):
2605+
raise ModelicaSystemError("Only working if OMCsessionDummy is used!")
2606+
2607+
super().__init__(
2608+
work_directory=work_directory,
2609+
session=session,
2610+
)
2611+
2612+
def setup(
2613+
self,
2614+
model_name: Optional[str] = None,
2615+
variable_filter: Optional[str] = None,
2616+
) -> None:
2617+
"""
2618+
Needed definitions to set up the runner class. This class expects the model (defined by model_name) to exists
2619+
within the working directory. At least two files are needed:
2620+
2621+
* model executable (as '<model_name>' or '<model_name>.exe'; in case of Windows additional '<model_name>.bat'
2622+
is expected to evaluate the path to needed dlls
2623+
* the model initialization file (as '<model_name>_init.xml')
2624+
"""
2625+
2626+
if self._model_name is not None:
2627+
raise ModelicaSystemError("Can not reuse this instance of ModelicaSystem "
2628+
f"defined for {repr(self._model_name)}!")
2629+
2630+
if model_name is None or not isinstance(model_name, str):
2631+
raise ModelicaSystemError("A model name must be provided!")
2632+
2633+
# set variables
2634+
self._model_name = model_name # Model class name
2635+
self._variable_filter = variable_filter
2636+
2637+
# test if the model can be executed
2638+
self.check_model_executable()
2639+
2640+
# read XML file
2641+
xml_file = self._session.omcpath(self.getWorkDirectory()) / f"{self._model_name}_init.xml"
2642+
self._xmlparse(xml_file=xml_file)
2643+
2644+
2645+
class ModelicaDoERunner(ModelicaDoEABC):
2646+
"""
2647+
Class to run DoEs based on a (Open)Modelica model using ModelicaSystemRunner
2648+
2649+
The example is the same as defined for ModelicaDoEABC
2650+
"""
2651+
2652+
def __init__(
2653+
self,
2654+
# ModelicaSystem definition to use
2655+
mod: ModelicaSystemABC,
2656+
# simulation specific input
2657+
# TODO: add more settings (simulation options, input options, ...)
2658+
simargs: Optional[dict[str, Optional[str | dict[str, str] | numbers.Number]]] = None,
2659+
# DoE specific inputs
2660+
resultpath: Optional[str | os.PathLike] = None,
2661+
parameters: Optional[dict[str, list[str] | list[int] | list[float]]] = None,
2662+
) -> None:
2663+
if not isinstance(mod, ModelicaSystemABC):
2664+
raise ModelicaSystemError(f"Invalid definition for ModelicaSystem*: {type(mod)}!")
2665+
2666+
super().__init__(
2667+
mod=mod,
2668+
simargs=simargs,
2669+
resultpath=resultpath,
2670+
parameters=parameters,
2671+
)
2672+
2673+
def _prepare_structure_parameters(
2674+
self,
2675+
idx_pc_structure: int,
2676+
pc_structure: Tuple,
2677+
param_structure: dict[str, list[str] | list[int] | list[float]],
2678+
) -> dict[str, str | int | float]:
2679+
if len(param_structure.keys()) > 0:
2680+
raise ModelicaSystemError(f"{self.__class__.__name__} can not handle structure parameters as it uses a "
2681+
"pre-compiled binary of model.")
2682+
2683+
return {}

OMPython/OMCSession.py

Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -289,6 +289,8 @@ class OMPathCompatibilityWindows(pathlib.WindowsPath, OMPathCompatibility):
289289

290290
OMPathABC = OMPathCompatibility
291291
OMCPath = OMPathCompatibility
292+
OMPathRunnerABC = OMPathCompatibility
293+
OMPathRunnerLocal = OMPathCompatibility
292294
else:
293295
class OMPathABC(pathlib.PurePosixPath, metaclass=abc.ABCMeta):
294296
"""
@@ -514,7 +516,95 @@ def size(self) -> int:
514516

515517
raise OMCSessionException(f"Error reading file size for path {self.as_posix()}!")
516518

519+
class OMPathRunnerABC(OMPathABC, metaclass=abc.ABCMeta):
520+
"""
521+
Base function for OMPath definitions *without* OMC server
522+
"""
523+
524+
def _path(self) -> pathlib.Path:
525+
return pathlib.Path(self.as_posix())
526+
527+
class _OMPathRunnerLocal(OMPathRunnerABC):
528+
"""
529+
Implementation of OMPathBase which does not use the session data at all. Thus, this implementation can run
530+
locally without any usage of OMC.
531+
532+
This class is based on OMPathBase and, therefore, on pathlib.PurePosixPath. This is working well, but it is not
533+
the correct implementation on Windows systems. To get a valid Windows representation of the path, use the
534+
conversion via pathlib.Path(<OMCPathDummy>.as_posix()).
535+
"""
536+
537+
def is_file(self) -> bool:
538+
"""
539+
Check if the path is a regular file.
540+
"""
541+
return self._path().is_file()
542+
543+
def is_dir(self) -> bool:
544+
"""
545+
Check if the path is a directory.
546+
"""
547+
return self._path().is_dir()
548+
549+
def is_absolute(self):
550+
"""
551+
Check if the path is an absolute path.
552+
"""
553+
return self._path().is_absolute()
554+
555+
def read_text(self) -> str:
556+
"""
557+
Read the content of the file represented by this path as text.
558+
"""
559+
return self._path().read_text(encoding='utf-8')
560+
561+
def write_text(self, data: str):
562+
"""
563+
Write text data to the file represented by this path.
564+
"""
565+
return self._path().write_text(data=data, encoding='utf-8')
566+
567+
def mkdir(self, parents: bool = True, exist_ok: bool = False):
568+
"""
569+
Create a directory at the path represented by this class.
570+
571+
The argument parents with default value True exists to ensure compatibility with the fallback solution for
572+
Python < 3.12. In this case, pathlib.Path is used directly and this option ensures, that missing parent
573+
directories are also created.
574+
"""
575+
return self._path().mkdir(parents=parents, exist_ok=exist_ok)
576+
577+
def cwd(self):
578+
"""
579+
Returns the current working directory as an OMPathBase object.
580+
"""
581+
return self._path().cwd()
582+
583+
def unlink(self, missing_ok: bool = False) -> None:
584+
"""
585+
Unlink (delete) the file or directory represented by this path.
586+
"""
587+
return self._path().unlink(missing_ok=missing_ok)
588+
589+
def resolve(self, strict: bool = False):
590+
"""
591+
Resolve the path to an absolute path. This is done based on available OMC functions.
592+
"""
593+
path_resolved = self._path().resolve(strict=strict)
594+
return type(self)(path_resolved, session=self._session)
595+
596+
def size(self) -> int:
597+
"""
598+
Get the size of the file in bytes - implementation baseon on pathlib.Path.
599+
"""
600+
if not self.is_file():
601+
raise OMCSessionException(f"Path {self.as_posix()} is not a file!")
602+
603+
path = self._path()
604+
return path.stat().st_size
605+
517606
OMCPath = _OMCPath
607+
OMPathRunnerLocal = _OMPathRunnerLocal
518608

519609

520610
class ModelExecutionException(Exception):
@@ -1735,3 +1825,61 @@ def _omc_port_get(self) -> str:
17351825
f"pid={self._omc_process.pid if isinstance(self._omc_process, subprocess.Popen) else '?'}")
17361826

17371827
return port
1828+
1829+
1830+
class OMSessionRunner(OMSessionABC):
1831+
"""
1832+
Implementation based on OMSessionABC without any use of an OMC server.
1833+
"""
1834+
1835+
def __init__(
1836+
self,
1837+
timeout: float = 10.00,
1838+
version: str = "1.27.0"
1839+
) -> None:
1840+
super().__init__(timeout=timeout)
1841+
self.model_execution_local = True
1842+
self._version = version
1843+
1844+
def __post_init__(self) -> None:
1845+
"""
1846+
No connection to an OMC server is created by this class!
1847+
"""
1848+
1849+
def model_execution_prefix(self, cwd: Optional[OMPathABC] = None) -> list[str]:
1850+
"""
1851+
Helper function which returns a command prefix.
1852+
"""
1853+
return []
1854+
1855+
def get_version(self) -> str:
1856+
"""
1857+
We can not provide an OM version as we are not link to an OMC server. Thus, the provided version string is used
1858+
directly.
1859+
"""
1860+
return self._version
1861+
1862+
def set_workdir(self, workdir: OMPathABC) -> None:
1863+
"""
1864+
Set the workdir for this session.
1865+
"""
1866+
os.chdir(workdir.as_posix())
1867+
1868+
def omcpath(self, *path) -> OMPathABC:
1869+
"""
1870+
Create an OMCPath object based on the given path segments and the current OMCSession* class.
1871+
"""
1872+
return OMPathRunnerLocal(*path, session=self)
1873+
1874+
def omcpath_tempdir(self, tempdir_base: Optional[OMPathABC] = None) -> OMPathABC:
1875+
"""
1876+
Get a temporary directory without using OMC.
1877+
"""
1878+
if tempdir_base is None:
1879+
tempdir_str = tempfile.gettempdir()
1880+
tempdir_base = self.omcpath(tempdir_str)
1881+
1882+
return self._tempdir(tempdir_base=tempdir_base)
1883+
1884+
def sendExpression(self, expr: str, parsed: bool = True) -> Any:
1885+
raise OMCSessionException(f"{self.__class__.__name__} does not uses an OMC server!")

OMPython/__init__.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,13 +19,17 @@
1919
ModelicaSystemDoE,
2020
ModelicaDoEOMC,
2121
ModelicaSystemError,
22+
ModelicaSystemRunner,
23+
ModelicaDoERunner,
2224

2325
doe_get_solutions,
2426
)
2527
from OMPython.OMCSession import (
2628
OMPathABC,
2729
OMCPath,
2830

31+
OMSessionRunner,
32+
2933
OMCSessionABC,
3034

3135
ModelExecutionData,
@@ -55,9 +59,14 @@
5559
'ModelicaDoEOMC',
5660
'ModelicaSystemError',
5761

62+
'ModelicaSystemRunner',
63+
'ModelicaDoERunner',
64+
5865
'OMPathABC',
5966
'OMCPath',
6067

68+
'OMSessionRunner',
69+
6170
'OMCSessionABC',
6271

6372
'doe_get_solutions',

0 commit comments

Comments
 (0)