From ba422107809cd52b6023be04646dbd32458127f7 Mon Sep 17 00:00:00 2001 From: syntron Date: Sat, 28 Jun 2025 21:03:17 +0200 Subject: [PATCH 1/5] [ModelicaSystemCmd] improve arg_set() * fix override values (string/bool/numbers) --- OMPython/ModelicaSystem.py | 30 ++++++++++++++++++++++-------- 1 file changed, 22 insertions(+), 8 deletions(-) diff --git a/OMPython/ModelicaSystem.py b/OMPython/ModelicaSystem.py index ae480dde..ba3ad6d1 100644 --- a/OMPython/ModelicaSystem.py +++ b/OMPython/ModelicaSystem.py @@ -131,23 +131,37 @@ def arg_set(self, key: str, val: Optional[str | dict] = None) -> None: key : str val : str, None """ + + def override2str(value: Any) -> str: + """ + Convert a value for 'override' to a string taking into account differences between Modelica and Python. + """ + if isinstance(value, str): + return f"\"{value.strip()}\"" + if isinstance(value, bool): + return 'true' if value else 'false' + if isinstance(value, numbers.Number): + return str(value) + raise ModelicaSystemError(f"Invalid value type: {type(value)} for key {key}") + if not isinstance(key, str): raise ModelicaSystemError(f"Invalid argument key: {repr(key)} (type: {type(key)})") key = key.strip() - if val is None: - argval = None - elif isinstance(val, str): - argval = val.strip() - elif isinstance(val, numbers.Number): - argval = str(val) - elif key == 'override' and isinstance(val, dict): + + if key == 'override' and isinstance(val, dict): for okey in val: if not isinstance(okey, str) or not isinstance(val[okey], (str, numbers.Number)): raise ModelicaSystemError("Invalid argument for 'override': " f"{repr(okey)} = {repr(val[okey])}") self._arg_override[okey] = val[okey] - argval = ','.join([f"{okey}={str(self._arg_override[okey])}" for okey in self._arg_override]) + argval = ','.join([f"{okey}={override2str(self._arg_override[okey])}" for okey in self._arg_override]) + elif val is None: + argval = None + elif isinstance(val, str): + argval = val.strip() + elif isinstance(val, numbers.Number): + argval = str(val) else: raise ModelicaSystemError(f"Invalid argument value for {repr(key)}: {repr(val)} (type: {type(val)})") From 69654e7694e953260bf5fc9b25f91cfceca6077c Mon Sep 17 00:00:00 2001 From: syntron Date: Tue, 8 Jul 2025 13:45:21 +0200 Subject: [PATCH 2/5] [ModelicaSystemCmd] improve arg_set() - improve log message --- OMPython/ModelicaSystem.py | 35 +++++++++++++++++------------------ 1 file changed, 17 insertions(+), 18 deletions(-) diff --git a/OMPython/ModelicaSystem.py b/OMPython/ModelicaSystem.py index ba3ad6d1..1e0af338 100644 --- a/OMPython/ModelicaSystem.py +++ b/OMPython/ModelicaSystem.py @@ -126,23 +126,26 @@ def arg_set(self, key: str, val: Optional[str | dict] = None) -> None: """ Set one argument for the executable model. - Parameters - ---------- - key : str - val : str, None + Args: + key: identifier / argument name to be used for the call of the model executable. + val: value for the given key; None for no value and for key == 'override' a dictionary can be used which + indicates variables to override """ - def override2str(value: Any) -> str: + def override2str(okey: str, oval: Any) -> str: """ Convert a value for 'override' to a string taking into account differences between Modelica and Python. """ - if isinstance(value, str): - return f"\"{value.strip()}\"" - if isinstance(value, bool): - return 'true' if value else 'false' - if isinstance(value, numbers.Number): - return str(value) - raise ModelicaSystemError(f"Invalid value type: {type(value)} for key {key}") + if isinstance(oval, str): + oval_str = f"\"{oval.strip()}\"" + elif isinstance(oval, bool): + oval_str = 'true' if oval else 'false' + elif isinstance(oval, numbers.Number): + oval_str = str(oval) + else: + raise ModelicaSystemError(f"Invalid value for override key {okey}: {type(oval)}") + + return f"{okey}={oval_str}" if not isinstance(key, str): raise ModelicaSystemError(f"Invalid argument key: {repr(key)} (type: {type(key)})") @@ -150,12 +153,12 @@ def override2str(value: Any) -> str: if key == 'override' and isinstance(val, dict): for okey in val: - if not isinstance(okey, str) or not isinstance(val[okey], (str, numbers.Number)): + if not isinstance(okey, str) or not isinstance(val[okey], (str, bool, numbers.Number)): raise ModelicaSystemError("Invalid argument for 'override': " f"{repr(okey)} = {repr(val[okey])}") self._arg_override[okey] = val[okey] - argval = ','.join([f"{okey}={override2str(self._arg_override[okey])}" for okey in self._arg_override]) + argval = ','.join([override2str(okey=okey, oval=oval) for okey, oval in self._arg_override.items()]) elif val is None: argval = None elif isinstance(val, str): @@ -182,10 +185,6 @@ def arg_get(self, key: str) -> Optional[str | dict]: def args_set(self, args: dict[str, Optional[str | dict[str, str]]]) -> None: """ Define arguments for the model executable. - - Parameters - ---------- - args : dict[str, Optional[str | dict[str, str]]] """ for arg in args: self.arg_set(key=arg, val=args[arg]) From 6d5bfe7966a2cc2c7f2e9607b518bebca5082927 Mon Sep 17 00:00:00 2001 From: syntron Date: Thu, 7 Aug 2025 22:01:36 +0200 Subject: [PATCH 3/5] [ModelicaSystemCmd] update handling of (override) args * sort args for a defined output * update type hints --- OMPython/ModelicaSystem.py | 85 +++++++++++++++++++++++++++++--------- 1 file changed, 65 insertions(+), 20 deletions(-) diff --git a/OMPython/ModelicaSystem.py b/OMPython/ModelicaSystem.py index 1e0af338..73959196 100644 --- a/OMPython/ModelicaSystem.py +++ b/OMPython/ModelicaSystem.py @@ -119,10 +119,18 @@ def __init__(self, runpath: pathlib.Path, modelname: str, timeout: Optional[floa self._runpath = pathlib.Path(runpath).resolve().absolute() self._model_name = modelname self._timeout = timeout + + # dictionaries of command line arguments for the model executable self._args: dict[str, str | None] = {} + # 'override' argument needs special handling, as it is a dict on its own saved as dict elements following the + # structure: 'key' => 'key=value' self._arg_override: dict[str, str] = {} - def arg_set(self, key: str, val: Optional[str | dict] = None) -> None: + def arg_set( + self, + key: str, + val: Optional[str | dict[str, Any] | numbers.Number] = None, + ) -> None: """ Set one argument for the executable model. @@ -132,12 +140,24 @@ def arg_set(self, key: str, val: Optional[str | dict] = None) -> None: indicates variables to override """ - def override2str(okey: str, oval: Any) -> str: + def override2str( + okey: str, + oval: str | bool | numbers.Number, + ) -> str: """ Convert a value for 'override' to a string taking into account differences between Modelica and Python. """ + # check oval for any string representations of numbers (or bool) and convert these to Python representations + if isinstance(oval, str): + try: + oval_evaluated = ast.literal_eval(oval) + if isinstance(oval_evaluated, (numbers.Number, bool)): + oval = oval_evaluated + except (ValueError, SyntaxError): + pass + if isinstance(oval, str): - oval_str = f"\"{oval.strip()}\"" + oval_str = oval.strip() elif isinstance(oval, bool): oval_str = 'true' if oval else 'false' elif isinstance(oval, numbers.Number): @@ -151,14 +171,32 @@ def override2str(okey: str, oval: Any) -> str: raise ModelicaSystemError(f"Invalid argument key: {repr(key)} (type: {type(key)})") key = key.strip() - if key == 'override' and isinstance(val, dict): - for okey in val: - if not isinstance(okey, str) or not isinstance(val[okey], (str, bool, numbers.Number)): - raise ModelicaSystemError("Invalid argument for 'override': " - f"{repr(okey)} = {repr(val[okey])}") - self._arg_override[okey] = val[okey] + if isinstance(val, dict): + if key != 'override': + raise ModelicaSystemError("Dictionary input only possible for key 'override'!") + + for okey, oval in val.items(): + if not isinstance(okey, str): + raise ModelicaSystemError("Invalid key for argument 'override': " + f"{repr(okey)} (type: {type(okey)})") + + if not isinstance(oval, (str, bool, numbers.Number, type(None))): + raise ModelicaSystemError(f"Invalid input for 'override'.{repr(okey)}: " + f"{repr(oval)} (type: {type(oval)})") + + if okey in self._arg_override: + if oval is None: + logger.info(f"Remove model executable override argument: {repr(self._arg_override[okey])}") + del self._arg_override[okey] + continue - argval = ','.join([override2str(okey=okey, oval=oval) for okey, oval in self._arg_override.items()]) + logger.info(f"Update model executable override argument: {repr(okey)} = {repr(oval)} " + f"(was: {repr(self._arg_override[okey])})") + + if oval is not None: + self._arg_override[okey] = override2str(okey=okey, oval=oval) + + argval = ','.join(sorted(self._arg_override.values())) elif val is None: argval = None elif isinstance(val, str): @@ -173,7 +211,7 @@ def override2str(okey: str, oval: Any) -> str: f"(was: {repr(self._args[key])})") self._args[key] = argval - def arg_get(self, key: str) -> Optional[str | dict]: + def arg_get(self, key: str) -> Optional[str | dict[str, str | bool | numbers.Number]]: """ Return the value for the given key """ @@ -182,7 +220,10 @@ def arg_get(self, key: str) -> Optional[str | dict]: return None - def args_set(self, args: dict[str, Optional[str | dict[str, str]]]) -> None: + def args_set( + self, + args: dict[str, Optional[str | dict[str, Any] | numbers.Number]], + ) -> None: """ Define arguments for the model executable. """ @@ -210,7 +251,7 @@ def get_cmd(self) -> list: path_exe = self.get_exe() cmdl = [path_exe.as_posix()] - for key in self._args: + for key in sorted(self._args): if self._args[key] is None: cmdl.append(f"-{key}") else: @@ -268,7 +309,7 @@ def run(self) -> int: return returncode @staticmethod - def parse_simflags(simflags: str) -> dict[str, Optional[str | dict[str, str]]]: + def parse_simflags(simflags: str) -> dict[str, Optional[str | dict[str, Any] | numbers.Number]]: """ Parse a simflag definition; this is deprecated! @@ -277,7 +318,7 @@ def parse_simflags(simflags: str) -> dict[str, Optional[str | dict[str, str]]]: warnings.warn("The argument 'simflags' is depreciated and will be removed in future versions; " "please use 'simargs' instead", DeprecationWarning, stacklevel=2) - simargs: dict[str, Optional[str | dict[str, str]]] = {} + simargs: dict[str, Optional[str | dict[str, Any] | numbers.Number]] = {} args = [s for s in simflags.split(' ') if s] for arg in args: @@ -930,7 +971,7 @@ def simulate_cmd( self, result_file: pathlib.Path, simflags: Optional[str] = None, - simargs: Optional[dict[str, Optional[str | dict[str, str]]]] = None, + simargs: Optional[dict[str, Optional[str | dict[str, Any] | numbers.Number]]] = None, timeout: Optional[float] = None, ) -> ModelicaSystemCmd: """ @@ -1003,7 +1044,7 @@ def simulate( self, resultfile: Optional[str] = None, simflags: Optional[str] = None, - simargs: Optional[dict[str, Optional[str | dict[str, str]]]] = None, + simargs: Optional[dict[str, Optional[str | dict[str, Any] | numbers.Number]]] = None, timeout: Optional[float] = None, ) -> None: """Simulate the model according to simulation options. @@ -1524,9 +1565,13 @@ def optimize(self) -> dict[str, Any]: return optimizeResult - def linearize(self, lintime: Optional[float] = None, simflags: Optional[str] = None, - simargs: Optional[dict[str, Optional[str | dict[str, str]]]] = None, - timeout: Optional[float] = None) -> LinearizationResult: + def linearize( + self, + lintime: Optional[float] = None, + simflags: Optional[str] = None, + simargs: Optional[dict[str, Optional[str | dict[str, Any] | numbers.Number]]] = None, + timeout: Optional[float] = None, + ) -> LinearizationResult: """Linearize the model according to linearization options. See setLinearizationOptions. From 2a45a2bf0d35f5766509a6927e59756855f3c54c Mon Sep 17 00:00:00 2001 From: syntron Date: Thu, 7 Aug 2025 21:43:27 +0200 Subject: [PATCH 4/5] [test_ModelicaSystemCmd] update test due to sort / test remove of override entry --- tests/test_ModelicaSystemCmd.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/tests/test_ModelicaSystemCmd.py b/tests/test_ModelicaSystemCmd.py index 3b28699c..b08e8ef3 100644 --- a/tests/test_ModelicaSystemCmd.py +++ b/tests/test_ModelicaSystemCmd.py @@ -35,6 +35,16 @@ def test_simflags(mscmd_firstorder): assert mscmd.get_cmd() == [ mscmd.get_exe().as_posix(), '-noEventEmit', - '-override=b=2,a=1,x=3', '-noRestart', + '-override=a=1,b=2,x=3', + ] + + mscmd.args_set({ + "override": {'b': None}, + }) + + assert mscmd.get_cmd_args() == [ + '-noEventEmit', + '-noRestart', + '-override=a=1,x=3', ] From ac6d126ba6b79eba5e72e48a79b7f0b5c2374a27 Mon Sep 17 00:00:00 2001 From: syntron Date: Fri, 15 Aug 2025 19:23:52 +0200 Subject: [PATCH 5/5] [test_ModelicaSystemCmd] fix rebase fallout --- tests/test_ModelicaSystemCmd.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/test_ModelicaSystemCmd.py b/tests/test_ModelicaSystemCmd.py index b08e8ef3..3544a1bd 100644 --- a/tests/test_ModelicaSystemCmd.py +++ b/tests/test_ModelicaSystemCmd.py @@ -43,7 +43,8 @@ def test_simflags(mscmd_firstorder): "override": {'b': None}, }) - assert mscmd.get_cmd_args() == [ + assert mscmd.get_cmd() == [ + mscmd.get_exe().as_posix(), '-noEventEmit', '-noRestart', '-override=a=1,x=3',