diff --git a/OMPython/ModelicaSystem.py b/OMPython/ModelicaSystem.py index 96fbfaf6..067fed24 100644 --- a/OMPython/ModelicaSystem.py +++ b/OMPython/ModelicaSystem.py @@ -8,6 +8,7 @@ import pathlib import platform from typing import Any, Optional +import warnings import numpy as np @@ -28,10 +29,15 @@ ModelicaDoEOMC, ) +from OMPython.compatibility_v400 import ( + depreciated_class, +) + # define logger using the current module name as ID logger = logging.getLogger(__name__) +@depreciated_class(msg="Please use class ModelicaSystemOMC instead!") class ModelicaSystem(ModelicaSystemOMC): """ Compatibility class. @@ -67,58 +73,197 @@ def __init__( def setCommandLineOptions(self, commandLineOptions: str): super().set_command_line_options(command_line_option=commandLineOptions) - def setContinuous( # type: ignore[override] + def _set_compatibility_helper( + self, + pkey: str, + args: Any, + kwargs: dict[str, Any], + ) -> dict[str, Any]: + input_args = [] + if len(args) == 1: + input_args.append(args[0]) + elif pkey in kwargs: + input_args.append(kwargs[pkey]) + + # the code below is based on _prepare_input_data2() + + def prepare_str(str_in: str) -> dict[str, str]: + str_in = str_in.replace(" ", "") + key_val_list: list[str] = str_in.split("=") + if len(key_val_list) != 2: + raise ModelicaSystemError(f"Invalid 'key=value' pair: {str_in}") + if len(key_val_list[0]) == 0: + raise ModelicaSystemError(f"Empty key: {str_in}") + + input_data_from_str: dict[str, str] = {str(key_val_list[0]): str(key_val_list[1])} + + return input_data_from_str + + input_data: dict[str, str] = {} + + if input_args is None: + return input_data + + for input_arg in input_args: + if isinstance(input_arg, str): + warnings.warn(message="The definition of values to set should use a dictionary, " + "i.e. {'key1': 'val1', 'key2': 'val2', ...}. Please convert all cases which " + "use a string ('key=val') or list ['key1=val1', 'key2=val2', ...]", + category=DeprecationWarning, + stacklevel=3) + input_data = input_data | prepare_str(input_arg) + elif isinstance(input_arg, list): + warnings.warn(message="The definition of values to set should use a dictionary, " + "i.e. {'key1': 'val1', 'key2': 'val2', ...}. Please convert all cases which " + "use a string ('key=val') or list ['key1=val1', 'key2=val2', ...]", + category=DeprecationWarning, + stacklevel=3) + + for item in input_arg: + if not isinstance(item, str): + raise ModelicaSystemError(f"Invalid input data type for set*() function: {type(item)}!") + input_data = input_data | prepare_str(item) + elif isinstance(input_arg, dict): + input_arg_str: dict[str, str] = {} + for key, val in input_arg.items(): + if not isinstance(key, str) or len(key) == 0: + raise ModelicaSystemError(f"Invalid key for set*() functions: {repr(key)}") + input_arg_str[key] = str(val).replace(' ', '') + input_data = input_data | input_arg_str + else: + raise ModelicaSystemError(f"Invalid input data type for set*() function: {type(input_arg)}!") + + return input_data + + def setContinuous( self, - cvals: str | list[str] | dict[str, Any], + *args: Any, + **kwargs: dict[str, Any], ) -> bool: - if isinstance(cvals, dict): - return super().setContinuous(**cvals) - raise ModelicaSystemError("Only dict input supported for setContinuous()") + """ + Compatibility wrapper for setContinuous() from OMPython v4.0.0 + + Original definition: - def setParameters( # type: ignore[override] + ``` + def setContinuous( + self, + cvals: str | list[str] | dict[str, Any], + ) -> bool: + ``` + """ + param = self._set_compatibility_helper(pkey='cvals', args=args, kwargs=kwargs) + return super().setContinuous(**param) + + def setParameters( self, - pvals: str | list[str] | dict[str, Any], + *args: Any, + **kwargs: dict[str, Any], ) -> bool: - if isinstance(pvals, dict): - return super().setParameters(**pvals) - raise ModelicaSystemError("Only dict input supported for setParameters()") + """ + Compatibility wrapper for setParameters() from OMPython v4.0.0 + + Original definition: - def setOptimizationOptions( # type: ignore[override] + ``` + def setParameters( + self, + pvals: str | list[str] | dict[str, Any], + ) -> bool: + ``` + """ + param = self._set_compatibility_helper(pkey='pvals', args=args, kwargs=kwargs) + return super().setParameters(**param) + + def setOptimizationOptions( self, - optimizationOptions: str | list[str] | dict[str, Any], + *args: Any, + **kwargs: dict[str, Any], ) -> bool: - if isinstance(optimizationOptions, dict): - return super().setOptimizationOptions(**optimizationOptions) - raise ModelicaSystemError("Only dict input supported for setOptimizationOptions()") + """ + Compatibility wrapper for setOptimizationOptions() from OMPython v4.0.0 + + Original definition: - def setInputs( # type: ignore[override] + ``` + def setOptimizationOptions( + self, + optimizationOptions: str | list[str] | dict[str, Any], + ) -> bool: + ``` + """ + param = self._set_compatibility_helper(pkey='optimizationOptions', args=args, kwargs=kwargs) + return super().setOptimizationOptions(**param) + + def setInputs( self, - name: str | list[str] | dict[str, Any], + *args: Any, + **kwargs: dict[str, Any], ) -> bool: - if isinstance(name, dict): - return super().setInputs(**name) - raise ModelicaSystemError("Only dict input supported for setInputs()") + """ + Compatibility wrapper for setInputs() from OMPython v4.0.0 + + Original definition: - def setSimulationOptions( # type: ignore[override] + ``` + def setInputs( + self, + name: str | list[str] | dict[str, Any], + ) -> bool: + ``` + """ + param = self._set_compatibility_helper(pkey='name', args=args, kwargs=kwargs) + return super().setInputs(**param) + + def setSimulationOptions( self, - simOptions: str | list[str] | dict[str, Any], + *args: Any, + **kwargs: dict[str, Any], ) -> bool: - if isinstance(simOptions, dict): - return super().setSimulationOptions(**simOptions) - raise ModelicaSystemError("Only dict input supported for setSimulationOptions()") + """ + Compatibility wrapper for setSimulationOptions() from OMPython v4.0.0 - def setLinearizationOptions( # type: ignore[override] + Original definition: + + ``` + def setSimulationOptions( + self, + simOptions: str | list[str] | dict[str, Any], + ) -> bool: + ``` + """ + param = self._set_compatibility_helper(pkey='simOptions', args=args, kwargs=kwargs) + return super().setSimulationOptions(**param) + + def setLinearizationOptions( self, - linearizationOptions: str | list[str] | dict[str, Any], + *args: Any, + **kwargs: dict[str, Any], ) -> bool: - if isinstance(linearizationOptions, dict): - return super().setLinearizationOptions(**linearizationOptions) - raise ModelicaSystemError("Only dict input supported for setLinearizationOptions()") + """ + Compatibility wrapper for setLinearizationOptions() from OMPython v4.0.0 + + Original definition: + + ``` + def setLinearizationOptions( + self, + linearizationOptions: str | list[str] | dict[str, Any], + ) -> bool: + ``` + """ + param = self._set_compatibility_helper(pkey='linearizationOptions', args=args, kwargs=kwargs) + return super().setLinearizationOptions(**param) def getContinuous( self, names: Optional[str | list[str]] = None, ): + """ + Compatibility wrapper for getContinuous() from OMPython v4.0.0 + + If no model simulation was run (self._simulated == False), the return value should be converted to str. + """ retval = super().getContinuous(names=names) if self._simulated: return retval @@ -140,12 +285,17 @@ def getContinuous( retval3.append(str(val)) return retval3 - raise ModelExecutionException("Invalid data!") + raise ModelicaSystemError("Invalid data!") def getOutputs( self, names: Optional[str | list[str]] = None, ): + """ + Compatibility wrapper for getOutputs() from OMPython v4.0.0 + + If no model simulation was run (self._simulated == False), the return value should be converted to str. + """ retval = super().getOutputs(names=names) if self._simulated: return retval @@ -167,18 +317,20 @@ def getOutputs( retval3.append(str(val)) return retval3 - raise ModelExecutionException("Invalid data!") + raise ModelicaSystemError("Invalid data!") +@depreciated_class(msg="Please use class ModelicaDoEOMC instead!") class ModelicaSystemDoE(ModelicaDoEOMC): """ Compatibility class. """ +@depreciated_class(msg="Please use class ModelExecutionConfig instead!") class ModelicaSystemCmd(ModelExecutionConfig): """ - Compatibility class; in the new version it is renamed as ModelExecutionCmd. + Compatibility class; in the new version it is renamed as ModelExecutionConfig. """ def __init__( @@ -209,7 +361,8 @@ def get_exe(self) -> pathlib.Path: return path_exe def get_cmd(self) -> list: - """Get a list with the path to the executable and all command line args. + """ + Get a list with the path to the executable and all command line args. This can later be used as an argument for subprocess.run(). """ @@ -218,6 +371,10 @@ def get_cmd(self) -> list: return cmdl - def run(self): + def run(self) -> int: cmd_definition = self.definition() - return cmd_definition.run() + try: + returncode = cmd_definition.run() + except ModelExecutionException as exc: + raise ModelicaSystemError(f"Cannot execute model: {exc}") from exc + return returncode diff --git a/OMPython/OMCSession.py b/OMPython/OMCSession.py index 38a5c94a..abc84ae7 100644 --- a/OMPython/OMCSession.py +++ b/OMPython/OMCSession.py @@ -7,7 +7,6 @@ import logging from typing import Any, Optional -import warnings import pyparsing @@ -17,7 +16,6 @@ OMSessionException, ) from OMPython.om_session_omc import ( - DockerPopen, OMCSessionABC, OMCSessionDocker, OMCSessionDockerContainer, @@ -26,30 +24,28 @@ OMCSessionWSL, ) +from OMPython.compatibility_v400 import ( + depreciated_class, +) # define logger using the current module name as ID logger = logging.getLogger(__name__) +@depreciated_class(msg="Please use class OMSessionException instead!") class OMCSessionException(OMSessionException): """ Just a compatibility layer ... """ +@depreciated_class(msg="Please use OMCSession*.sendExpression(...) instead!") class OMCSessionCmd: """ Implementation of Open Modelica Compiler API functions. Depreciated! """ def __init__(self, session: OMSessionABC, readonly: bool = False): - warnings.warn( - message="The class OMCSessionCMD is depreciated and will be removed in future versions; " - "please use OMCSession*.sendExpression(...) instead!", - category=DeprecationWarning, - stacklevel=2, - ) - if not isinstance(session, OMSessionABC): raise OMCSessionException("Invalid OMC process definition!") self._session = session @@ -228,6 +224,7 @@ def getClassNames(self, className=None, recursive=False, qualified=False, sort=F return self._ask(question='getClassNames', opt=opt) +@depreciated_class(msg="Please use OMCSession* classes instead!") class OMCSessionZMQ(OMSessionABC): """ This class is a compatibility layer for the new schema using OMCSession* classes. @@ -242,11 +239,6 @@ def __init__( """ Initialisation for OMCSessionZMQ """ - warnings.warn(message="The class OMCSessionZMQ is depreciated and will be removed in future versions; " - "please use OMCProcess* classes instead!", - category=DeprecationWarning, - stacklevel=2) - if omc_process is None: omc_process = OMCSessionLocal(omhome=omhome, timeout=timeout) elif not isinstance(omc_process, OMCSessionABC): @@ -308,9 +300,36 @@ def set_workdir(self, workdir: OMPathABC) -> None: return self.omc_process.set_workdir(workdir=workdir) -DummyPopen = DockerPopen -OMCProcessLocal = OMCSessionLocal -OMCProcessPort = OMCSessionPort -OMCProcessDocker = OMCSessionDocker -OMCProcessDockerContainer = OMCSessionDockerContainer -OMCProcessWSL = OMCSessionWSL +@depreciated_class(msg="Please use class OMCSessionLocal instead!") +class OMCProcessLocal(OMCSessionLocal): + """ + Just a wrapper class; OMCProcessLocal => OMCSessionLocal + """ + + +@depreciated_class(msg="Please use class OMCSessionPort instead!") +class OMCProcessPort(OMCSessionPort): + """ + Just a wrapper class; OMCProcessPort => OMCSessionPort + """ + + +@depreciated_class(msg="Please use class OMCSessionDocker instead!") +class OMCProcessDocker(OMCSessionDocker): + """ + Just a wrapper class; OMCProcessDocker => OMCSessionDocker + """ + + +@depreciated_class(msg="Please use class OMCSessionDockerContainer instead!") +class OMCProcessDockerContainer(OMCSessionDockerContainer): + """ + Just a wrapper class; OMCProcessDockerContainer => OMCSessionDockerContainer + """ + + +@depreciated_class(msg="Please use class OMCSessionWSL instead!") +class OMCProcessWSL(OMCSessionWSL): + """ + Just a wrapper class; OMCProcessWSL => OMCSessionWSL + """ diff --git a/OMPython/__init__.py b/OMPython/__init__.py index 1ea0ed8a..f3526da9 100644 --- a/OMPython/__init__.py +++ b/OMPython/__init__.py @@ -89,7 +89,6 @@ 'ModelicaDoEOMC', 'ModelicaDoERunner', 'ModelicaSystemABC', - 'ModelicaSystemDoE', 'ModelicaSystemError', 'ModelicaSystemOMC', 'ModelicaSystemRunner', @@ -112,8 +111,8 @@ 'ModelicaSystemCmd', 'ModelicaSystem', + 'ModelicaSystemDoE', - 'OMCSessionABC', 'OMCSessionCmd', 'OMCSessionException', diff --git a/OMPython/compatibility_v400.py b/OMPython/compatibility_v400.py new file mode 100644 index 00000000..61fa27a8 --- /dev/null +++ b/OMPython/compatibility_v400.py @@ -0,0 +1,39 @@ +# -*- coding: utf-8 -*- +""" +Helper functions for compatibility with OMPython v4.0.0 +""" +import warnings +from typing import Optional + + +def depreciated_class(msg: Optional[str] = None): + """ + Decorator for depreciated / compatibility classes. + """ + + def depreciated(cls): + """ + Helper functions to do the decoration part. + """ + + class Wrapper(cls): + """ + Wrapper to define the depreciation message. + """ + + def __init__(self, *args, **kwargs): + message = f"The class {cls.__name__} is depreciated and will be removed in future versions!" + if msg is not None: + message += f" {msg}" + + warnings.warn( + message=message, + category=DeprecationWarning, + stacklevel=3, + ) + + super().__init__(*args, **kwargs) + + return Wrapper + + return depreciated diff --git a/OMPython/modelica_doe_abc.py b/OMPython/modelica_doe_abc.py index 0ab3add9..062f8833 100644 --- a/OMPython/modelica_doe_abc.py +++ b/OMPython/modelica_doe_abc.py @@ -14,6 +14,7 @@ from OMPython.model_execution import ( ModelExecutionRun, + ModelExecutionException, ) from OMPython.om_session_abc import ( OMPathABC, @@ -209,7 +210,7 @@ def prepare(self) -> int: } ) - self._mod.setParameters(sim_param_non_structural) + self._mod.setParameters(**sim_param_non_structural) mscmd = self._mod.simulate_cmd( result_file=resultfile, ) @@ -310,8 +311,8 @@ def worker(worker_id, task_queue): returncode = cmd_definition.run() logger.info(f"[Worker {worker_id}] Simulation {resultpath.name} " f"finished with return code: {returncode}") - except ModelicaSystemError as ex: - logger.warning(f"Simulation error for {resultpath.name}: {ex}") + except ModelExecutionException as exc: + logger.warning(f"Simulation error for {resultpath.name}: {exc}") # Mark the task as done task_queue.task_done() diff --git a/OMPython/modelica_system_abc.py b/OMPython/modelica_system_abc.py index 0f04e4df..f3e02ddf 100644 --- a/OMPython/modelica_system_abc.py +++ b/OMPython/modelica_system_abc.py @@ -11,13 +11,13 @@ import os import re from typing import Any, Optional -import warnings import xml.etree.ElementTree as ET import numpy as np from OMPython.model_execution import ( ModelExecutionConfig, + ModelExecutionException, ) from OMPython.om_session_abc import ( OMPathABC, @@ -200,7 +200,10 @@ def check_model_executable(self): # ... by running it - output help for command help om_cmd.arg_set(key="help", val="help") cmd_definition = om_cmd.definition() - returncode = cmd_definition.run() + try: + returncode = cmd_definition.run() + except ModelExecutionException as exc: + raise ModelicaSystemError(f"Cannot execute model: {exc}") from exc if returncode != 0: raise ModelicaSystemError("Model executable not working!") @@ -736,7 +739,10 @@ def simulate( self._result_file.unlink() # ... run simulation ... cmd_definition = om_cmd.definition() - returncode = cmd_definition.run() + try: + returncode = cmd_definition.run() + except ModelExecutionException as exc: + raise ModelicaSystemError(f"Cannot execute model: {exc}") from exc # and check returncode *AND* resultfile if returncode != 0 and self._result_file.is_file(): # check for an empty (=> 0B) result file which indicates a crash of the model executable @@ -752,49 +758,13 @@ def simulate( @staticmethod def _prepare_input_data( - input_args: Any, input_kwargs: dict[str, Any], ) -> dict[str, str]: """ Convert raw input to a structured dictionary {'key1': 'value1', 'key2': 'value2'}. """ - - def prepare_str(str_in: str) -> dict[str, str]: - str_in = str_in.replace(" ", "") - key_val_list: list[str] = str_in.split("=") - if len(key_val_list) != 2: - raise ModelicaSystemError(f"Invalid 'key=value' pair: {str_in}") - - input_data_from_str: dict[str, str] = {key_val_list[0]: key_val_list[1]} - - return input_data_from_str - input_data: dict[str, str] = {} - for input_arg in input_args: - if isinstance(input_arg, str): - warnings.warn(message="The definition of values to set should use a dictionary, " - "i.e. {'key1': 'val1', 'key2': 'val2', ...}. Please convert all cases which " - "use a string ('key=val') or list ['key1=val1', 'key2=val2', ...]", - category=DeprecationWarning, - stacklevel=3) - input_data = input_data | prepare_str(input_arg) - elif isinstance(input_arg, list): - warnings.warn(message="The definition of values to set should use a dictionary, " - "i.e. {'key1': 'val1', 'key2': 'val2', ...}. Please convert all cases which " - "use a string ('key=val') or list ['key1=val1', 'key2=val2', ...]", - category=DeprecationWarning, - stacklevel=3) - - for item in input_arg: - if not isinstance(item, str): - raise ModelicaSystemError(f"Invalid input data type for set*() function: {type(item)}!") - input_data = input_data | prepare_str(item) - elif isinstance(input_arg, dict): - input_data = input_data | input_arg - else: - raise ModelicaSystemError(f"Invalid input data type for set*() function: {type(input_arg)}!") - if len(input_kwargs): for key, val in input_kwargs.items(): # ensure all values are strings to align it on one type: dict[str, str] @@ -872,21 +842,17 @@ def isParameterChangeable( def setContinuous( self, - *args: Any, **kwargs: dict[str, Any], ) -> bool: """ - This method is used to set continuous values. It can be called: - with a sequence of continuous name and assigning corresponding values as arguments as show in the example below: - usage - >>> setContinuous("Name=value") # depreciated - >>> setContinuous(["Name1=value1","Name2=value2"]) # depreciated + This method is used to set continuous values. + usage: >>> setContinuous(Name1="value1", Name2="value2") >>> param = {"Name1": "value1", "Name2": "value2"} >>> setContinuous(**param) """ - inputdata = self._prepare_input_data(input_args=args, input_kwargs=kwargs) + inputdata = self._prepare_input_data(input_kwargs=kwargs) return self._set_method_helper( inputdata=inputdata, @@ -896,21 +862,17 @@ def setContinuous( def setParameters( self, - *args: Any, **kwargs: dict[str, Any], ) -> bool: """ - This method is used to set parameter values. It can be called: - with a sequence of parameter name and assigning corresponding value as arguments as show in the example below: - usage - >>> setParameters("Name=value") # depreciated - >>> setParameters(["Name1=value1","Name2=value2"]) # depreciated + This method is used to set parameter values + usage: >>> setParameters(Name1="value1", Name2="value2") >>> param = {"Name1": "value1", "Name2": "value2"} >>> setParameters(**param) """ - inputdata = self._prepare_input_data(input_args=args, input_kwargs=kwargs) + inputdata = self._prepare_input_data(input_kwargs=kwargs) return self._set_method_helper( inputdata=inputdata, @@ -920,22 +882,17 @@ def setParameters( def setSimulationOptions( self, - *args: Any, **kwargs: dict[str, Any], ) -> bool: """ - This method is used to set simulation options. It can be called: - with a sequence of simulation options name and assigning corresponding values as arguments as show in the - example below: - usage - >>> setSimulationOptions("Name=value") # depreciated - >>> setSimulationOptions(["Name1=value1","Name2=value2"]) # depreciated + This method is used to set simulation options. + usage: >>> setSimulationOptions(Name1="value1", Name2="value2") >>> param = {"Name1": "value1", "Name2": "value2"} >>> setSimulationOptions(**param) """ - inputdata = self._prepare_input_data(input_args=args, input_kwargs=kwargs) + inputdata = self._prepare_input_data(input_kwargs=kwargs) return self._set_method_helper( inputdata=inputdata, @@ -945,22 +902,17 @@ def setSimulationOptions( def setLinearizationOptions( self, - *args: Any, **kwargs: dict[str, Any], ) -> bool: """ - This method is used to set linearization options. It can be called: - with a sequence of linearization options name and assigning corresponding value as arguments as show in the - example below - usage - >>> setLinearizationOptions("Name=value") # depreciated - >>> setLinearizationOptions(["Name1=value1","Name2=value2"]) # depreciated + This method is used to set linearization options. + usage: >>> setLinearizationOptions(Name1="value1", Name2="value2") >>> param = {"Name1": "value1", "Name2": "value2"} >>> setLinearizationOptions(**param) """ - inputdata = self._prepare_input_data(input_args=args, input_kwargs=kwargs) + inputdata = self._prepare_input_data(input_kwargs=kwargs) return self._set_method_helper( inputdata=inputdata, @@ -970,22 +922,17 @@ def setLinearizationOptions( def setOptimizationOptions( self, - *args: Any, **kwargs: dict[str, Any], ) -> bool: """ - This method is used to set optimization options. It can be called: - with a sequence of optimization options name and assigning corresponding values as arguments as show in the - example below: - usage - >>> setOptimizationOptions("Name=value") # depreciated - >>> setOptimizationOptions(["Name1=value1","Name2=value2"]) # depreciated + This method is used to set optimization options. + usage: >>> setOptimizationOptions(Name1="value1", Name2="value2") >>> param = {"Name1": "value1", "Name2": "value2"} >>> setOptimizationOptions(**param) """ - inputdata = self._prepare_input_data(input_args=args, input_kwargs=kwargs) + inputdata = self._prepare_input_data(input_kwargs=kwargs) return self._set_method_helper( inputdata=inputdata, @@ -999,19 +946,17 @@ def setInputs( **kwargs: dict[str, Any], ) -> bool: """ - This method is used to set input values. It can be called with a sequence of input name and assigning - corresponding values as arguments as show in the example below. Compared to other set*() methods this is a - special case as value could be a list of tuples - these are converted to a string in _prepare_input_data() - and restored here via ast.literal_eval(). + This method is used to set input values. - >>> setInputs("Name=value") # depreciated - >>> setInputs(["Name1=value1","Name2=value2"]) # depreciated + Compared to other set*() methods this is a special case as value could be a list of tuples - these are + converted to a string in _prepare_input_data() and restored here via ast.literal_eval(). + usage: >>> setInputs(Name1="value1", Name2="value2") >>> param = {"Name1": "value1", "Name2": "value2"} >>> setInputs(**param) """ - inputdata = self._prepare_input_data(input_args=args, input_kwargs=kwargs) + inputdata = self._prepare_input_data(input_kwargs=kwargs) for key, val in inputdata.items(): if key not in self._inputs: @@ -1021,7 +966,6 @@ def setInputs( raise ModelicaSystemError(f"Invalid data in input for {repr(key)}: {repr(val)}") val_evaluated = ast.literal_eval(val) - if isinstance(val_evaluated, (int, float)): self._inputs[key] = [(float(self._simulate_options["startTime"]), float(val)), (float(self._simulate_options["stopTime"]), float(val))] @@ -1029,19 +973,30 @@ def setInputs( if not all(isinstance(item, tuple) for item in val_evaluated): raise ModelicaSystemError("Value for setInput() must be in tuple format; " f"got {repr(val_evaluated)}") - if val_evaluated != sorted(val_evaluated, key=lambda x: x[0]): - raise ModelicaSystemError("Time value should be in increasing order; " - f"got {repr(val_evaluated)}") + val_evaluated_checked: list[tuple[float, float]] = [] for item in val_evaluated: - if item[0] < float(self._simulate_options["startTime"]): - raise ModelicaSystemError(f"Time value in {repr(item)} of {repr(val_evaluated)} is less " - "than the simulation start time") if len(item) != 2: raise ModelicaSystemError(f"Value {repr(item)} of {repr(val_evaluated)} " "is in incorrect format!") - self._inputs[key] = val_evaluated + try: + val_evaluated_checked.append((float(item[0]), float(item[1]))) + except (ValueError, TypeError) as exc: + raise ModelicaSystemError("All elements of the input for setInput() should be convertible to " + "type Tuple[float, float] - " + f"found [{repr(item[0])}, {repr(item[1])}] with types " + f"[{type(item[0])}, {type(item[1])}]!") from exc + + if item[0] < float(self._simulate_options["startTime"]): + raise ModelicaSystemError(f"Time value in {repr(item)} of {repr(val_evaluated)} is less " + "than the simulation start time") + + if val_evaluated_checked != sorted(val_evaluated_checked, key=lambda x: x[0]): + raise ModelicaSystemError("Time value should be in increasing order; " + f"got {repr(val_evaluated_checked)}") + + self._inputs[key] = val_evaluated_checked else: raise ModelicaSystemError(f"Data cannot be evaluated for {repr(key)}: {repr(val)}") @@ -1180,7 +1135,10 @@ def linearize( linear_file.unlink(missing_ok=True) cmd_definition = om_cmd.definition() - returncode = cmd_definition.run() + try: + returncode = cmd_definition.run() + except ModelExecutionException as exc: + raise ModelicaSystemError(f"Cannot execute model: {exc}") from exc if returncode != 0: raise ModelicaSystemError(f"Linearize failed with return code: {returncode}") if not linear_file.is_file(): diff --git a/tests/test_ModelicaSystemOMC.py b/tests/test_ModelicaSystemOMC.py index c63b92e1..bff63315 100644 --- a/tests/test_ModelicaSystemOMC.py +++ b/tests/test_ModelicaSystemOMC.py @@ -64,9 +64,8 @@ def test_setParameters(): model_name="BouncingBall", ) - # method 1 (test depreciated variants) - mod.setParameters("e=1.234") - mod.setParameters(["g=321.0"]) + mod.setParameters(e=1.234) + mod.setParameters(g=321.0) assert mod.getParameters("e") == ["1.234"] assert mod.getParameters("g") == ["321.0"] assert mod.getParameters() == { @@ -76,7 +75,6 @@ def test_setParameters(): with pytest.raises(KeyError): mod.getParameters("thisParameterDoesNotExist") - # method 2 (new style) pvals = {"e": 21.3, "g": 0.12} mod.setParameters(**pvals) assert mod.getParameters() == { @@ -439,6 +437,14 @@ def test_simulate_inputs(tmp_path): simOptions = {"stopTime": 1.0} mod.setSimulationOptions(**simOptions) + # check invalid inputs + # * 'None' cannot be converted to float + with pytest.raises(OMPython.ModelicaSystemError): + mod.setInputs(u1=[(0.0, None), (0.5, 1)]) + # * 'abc' cannot be converted to float + with pytest.raises(OMPython.ModelicaSystemError): + mod.setInputs(u1=[(0.0, 0.0), ("abc", 1)]) + # integrate zero (no setInputs call) - it should default to None -> 0 assert mod.getInputs() == { "u1": None, diff --git a/tests_v400/test_ModelicaSystem.py b/tests_v400/test_ModelicaSystem.py index c55e95fc..aa713af0 100644 --- a/tests_v400/test_ModelicaSystem.py +++ b/tests_v400/test_ModelicaSystem.py @@ -35,7 +35,7 @@ def test_setParameters(): mod = OMPython.ModelicaSystem(model_path + "BouncingBall.mo", "BouncingBall") # method 1 - mod.setParameters(pvals={"e": 1.234}) + mod.setParameters(pvals="e=1.234") mod.setParameters(pvals={"g": 321.0}) assert mod.getParameters("e") == ["1.234"] assert mod.getParameters("g") == ["321.0"]