From ab6ab52cc2c6de483871badb9c5a8d07026aacee Mon Sep 17 00:00:00 2001 From: syntron Date: Mon, 23 Jun 2025 09:07:48 +0200 Subject: [PATCH 01/43] [ModelicaSystem] remove class variable csvFile; define name based on resultfile in simulate() reason: * variable not needed / used as class variable * using name based on resultfile allows to run the same model executable several times --- OMPython/ModelicaSystem.py | 27 ++++++++++++++++++--------- 1 file changed, 18 insertions(+), 9 deletions(-) diff --git a/OMPython/ModelicaSystem.py b/OMPython/ModelicaSystem.py index e9c247b3..310ccb52 100644 --- a/OMPython/ModelicaSystem.py +++ b/OMPython/ModelicaSystem.py @@ -419,7 +419,6 @@ def __init__( self.inputFlag = False # for model with input quantity self.simulationFlag = False # if the model is simulated? self.outputFlag = False - self.csvFile: Optional[pathlib.Path] = None # for storing inputs condition self.resultfile: Optional[pathlib.Path] = None # for storing result file self.variableFilter = variableFilter @@ -910,6 +909,9 @@ def simulate(self, resultfile: Optional[str] = None, simflags: Optional[str] = N om_cmd.arg_set(key="overrideFile", val=overrideFile.as_posix()) if self.inputFlag: # if model has input quantities + # csvfile is based on name used for result file + csvfile = resultfile.parent / f"{resultfile.stem}.csv" + for i in self.inputlist: val = self.inputlist[i] if val is None: @@ -921,9 +923,11 @@ def simulate(self, resultfile: Optional[str] = None, simflags: Optional[str] = N raise ModelicaSystemError(f"startTime not matched for Input {i}!") if float(self.simulateOptions["stopTime"]) != val[-1][0]: raise ModelicaSystemError(f"stopTime not matched for Input {i}!") - self.csvFile = self.createCSVData() # create csv file - om_cmd.arg_set(key="csvInput", val=self.csvFile.as_posix()) + # write csv file and store the name + csvfile = self.createCSVData(csvfile=csvfile) + + om_cmd.arg_set(key="csvInput", val=csvfile.as_posix()) # delete resultfile ... if self.resultfile.is_file(): @@ -1151,7 +1155,11 @@ def checkValidInputs(self, name): else: raise ModelicaSystemError('Error!!! Value must be in tuple format') - def createCSVData(self) -> pathlib.Path: + def createCSVData(self, csvfile: Optional[pathlib.Path] = None) -> pathlib.Path: + """ + Create a csv file with inputs for the simulation/optimization of the model. If csvfile is provided as argument, + this file is used; else a generic file name is created. + """ start_time: float = float(self.simulateOptions["startTime"]) stop_time: float = float(self.simulateOptions["stopTime"]) @@ -1192,13 +1200,14 @@ def createCSVData(self) -> pathlib.Path: ] csv_rows.append(row) - csvFile = self.tempdir / f'{self.modelName}.csv' + if csvfile is None: + csvfile = self.tempdir / f'{self.modelName}.csv' - with open(file=csvFile, mode="w", encoding="utf-8", newline="") as fh: + with open(file=csvfile, mode="w", encoding="utf-8", newline="") as fh: writer = csv.writer(fh) writer.writerows(csv_rows) - return csvFile + return csvfile # to convert Modelica model to FMU def convertMo2Fmu(self, version="2.0", fmuType="me_cs", fileNamePrefix="", includeResources=True): # 19 @@ -1314,8 +1323,8 @@ def load_module_from_path(module_name, file_path): for l in tupleList: if l[0] < float(self.simulateOptions["startTime"]): raise ModelicaSystemError('Input time value is less than simulation startTime') - self.csvFile = self.createCSVData() - om_cmd.arg_set(key="csvInput", val=self.csvFile.as_posix()) + csvfile = self.createCSVData() + om_cmd.arg_set(key="csvInput", val=csvfile.as_posix()) om_cmd.arg_set(key="l", val=str(lintime or self.linearOptions["stopTime"])) From 047a6538e0675e4f9d66985cfddef35a59b18ff0 Mon Sep 17 00:00:00 2001 From: syntron Date: Mon, 23 Jun 2025 15:51:48 +0200 Subject: [PATCH 02/43] [ModelicaSystem/test_ModelicaSystem] fix test (csvFile no longer a class variable) --- tests/test_ModelicaSystem.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/tests/test_ModelicaSystem.py b/tests/test_ModelicaSystem.py index 156dde03..a943d6a7 100644 --- a/tests/test_ModelicaSystem.py +++ b/tests/test_ModelicaSystem.py @@ -396,12 +396,14 @@ def test_simulate_inputs(tmp_path): "u1=[(0.0, 0), (1.0, 1)]", "u2=[(0.0, 0), (0.25, 0.5), (0.5, 1.0), (1.0, 0)]", ]) - mod.simulate() - assert pathlib.Path(mod.csvFile).read_text() == """time,u1,u2,end + csv_file = mod.createCSVData() + assert pathlib.Path(csv_file).read_text() == """time,u1,u2,end 0.0,0.0,0.0,0 0.25,0.25,0.5,0 0.5,0.5,1.0,0 1.0,1.0,0.0,0 """ + + mod.simulate() y = mod.getSolutions("y")[0] assert np.isclose(y[-1], 1.0) From bb8a8be6728bd8308b4f01d9482cbb15fb883e0d Mon Sep 17 00:00:00 2001 From: syntron Date: Sat, 28 Jun 2025 21:48:16 +0200 Subject: [PATCH 03/43] [ModelicaSystem] fix mypy: use self.resultfile --- OMPython/ModelicaSystem.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/OMPython/ModelicaSystem.py b/OMPython/ModelicaSystem.py index 310ccb52..8f598a7c 100644 --- a/OMPython/ModelicaSystem.py +++ b/OMPython/ModelicaSystem.py @@ -910,7 +910,7 @@ def simulate(self, resultfile: Optional[str] = None, simflags: Optional[str] = N if self.inputFlag: # if model has input quantities # csvfile is based on name used for result file - csvfile = resultfile.parent / f"{resultfile.stem}.csv" + csvfile = self.resultfile.parent / f"{self.resultfile.stem}.csv" for i in self.inputlist: val = self.inputlist[i] From 3e7d9eb265972f5604b530c0e0ad4c1a2a2d6dbf Mon Sep 17 00:00:00 2001 From: syntron Date: Fri, 13 Jun 2025 13:13:51 +0200 Subject: [PATCH 04/43] [ModelicaSystem] split simulate() into two methods (1) create ModelicasystemCmd instance - simulate_cmd() (2) run it - simulate() --- OMPython/ModelicaSystem.py | 67 ++++++++++++++++++++++++++++---------- 1 file changed, 49 insertions(+), 18 deletions(-) diff --git a/OMPython/ModelicaSystem.py b/OMPython/ModelicaSystem.py index e9c247b3..5641b6ce 100644 --- a/OMPython/ModelicaSystem.py +++ b/OMPython/ModelicaSystem.py @@ -867,29 +867,36 @@ def getOptimizationOptions(self, names=None): # 10 raise ModelicaSystemError("Unhandled input for getOptimizationOptions()") - def simulate(self, resultfile: Optional[str] = None, simflags: Optional[str] = None, - simargs: Optional[dict[str, Optional[str | dict[str, str]]]] = None, - timeout: Optional[int] = None): # 11 + def simulate_cmd( + self, + resultfile: pathlib.Path, + simflags: Optional[str] = None, + simargs: Optional[dict[str, Optional[str | dict[str, str]]]] = None, + timeout: Optional[int] = None, + ) -> ModelicaSystemCmd: """ - This method simulates model according to the simulation options. - usage - >>> simulate() - >>> simulate(resultfile="a.mat") - >>> simulate(simflags="-noEventEmit -noRestart -override=e=0.3,g=10") # set runtime simulation flags - >>> simulate(simargs={"noEventEmit": None, "noRestart": None, "override": "e=0.3,g=10"}) # using simargs + This method prepares the simulates model according to the simulation options. It returns an instance of + ModelicaSystemCmd which can be used to run the simulation. + + Due to the tempdir being unique for the ModelicaSystem instance, *NEVER* use this to create several simulations + with the same instance of ModelicaSystem! Restart each simulation process with a new instance of ModelicaSystem. + + Parameters + ---------- + resultfile + simflags + simargs + timeout + + Returns + ------- + An instance if ModelicaSystemCmd to run the requested simulation. """ om_cmd = ModelicaSystemCmd(runpath=self.tempdir, modelname=self.modelName, timeout=timeout) - if resultfile is None: - # default result file generated by OM - self.resultfile = self.tempdir / f"{self.modelName}_res.mat" - elif os.path.exists(resultfile): - self.resultfile = pathlib.Path(resultfile) - else: - self.resultfile = self.tempdir / resultfile - # always define the resultfile to use - om_cmd.arg_set(key="r", val=self.resultfile.as_posix()) + # always define the result file to use + om_cmd.arg_set(key="r", val=resultfile.as_posix()) # allow runtime simulation flags from user input if simflags is not None: @@ -925,6 +932,30 @@ def simulate(self, resultfile: Optional[str] = None, simflags: Optional[str] = N om_cmd.arg_set(key="csvInput", val=self.csvFile.as_posix()) + return om_cmd + + def simulate(self, resultfile: Optional[str] = None, simflags: Optional[str] = None, + simargs: Optional[dict[str, Optional[str | dict[str, str]]]] = None, + timeout: Optional[int] = None): # 11 + """ + This method simulates model according to the simulation options. + usage + >>> simulate() + >>> simulate(resultfile="a.mat") + >>> simulate(simflags="-noEventEmit -noRestart -override=e=0.3,g=10") # set runtime simulation flags + >>> simulate(simargs={"noEventEmit": None, "noRestart": None, "override": "e=0.3,g=10"}) # using simargs + """ + + if resultfile is None: + # default result file generated by OM + self.resultfile = self.tempdir / f"{self.modelName}_res.mat" + elif os.path.exists(resultfile): + self.resultfile = pathlib.Path(resultfile) + else: + self.resultfile = self.tempdir / resultfile + + om_cmd = self.simulate_cmd(resultfile=self.resultfile, simflags=simflags, simargs=simargs, timeout=timeout) + # delete resultfile ... if self.resultfile.is_file(): self.resultfile.unlink() From b57c7e315638e38e07c82c42d8eeb3d378438405 Mon Sep 17 00:00:00 2001 From: syntron Date: Mon, 23 Jun 2025 21:28:24 +0200 Subject: [PATCH 05/43] [ModelicaSystem] improve docstring for simulate_cmd() --- OMPython/ModelicaSystem.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/OMPython/ModelicaSystem.py b/OMPython/ModelicaSystem.py index 5641b6ce..ecce524c 100644 --- a/OMPython/ModelicaSystem.py +++ b/OMPython/ModelicaSystem.py @@ -881,6 +881,9 @@ def simulate_cmd( Due to the tempdir being unique for the ModelicaSystem instance, *NEVER* use this to create several simulations with the same instance of ModelicaSystem! Restart each simulation process with a new instance of ModelicaSystem. + However, if only non-structural parameters are used, it is possible to reuse an existing instance of + ModelicaSystem to create several version ModelicaSystemCmd to run the model using different settings. + Parameters ---------- resultfile From cda6d1c900a4bf28f8a5b4e9a2994ceaaf9fc336 Mon Sep 17 00:00:00 2001 From: syntron Date: Mon, 23 Jun 2025 09:51:50 +0200 Subject: [PATCH 06/43] [ModelicaSystem] add type hints for set*() functions and rename arguments * fix some type hint issues in setInput() * prepare for definition via dictionary replacing 'a=b' and '[a=b, c=d]' style --- OMPython/ModelicaSystem.py | 163 ++++++++++++++++++++++++------------- 1 file changed, 106 insertions(+), 57 deletions(-) diff --git a/OMPython/ModelicaSystem.py b/OMPython/ModelicaSystem.py index e9c247b3..2609afba 100644 --- a/OMPython/ModelicaSystem.py +++ b/OMPython/ModelicaSystem.py @@ -1001,46 +1001,79 @@ def _strip_space(name): raise ModelicaSystemError("Unhandled input for strip_space()") - def setMethodHelper(self, args1, args2, args3, args4=None): + def setMethodHelper( + self, + inputdata: str | list[str] | dict[str, str | int | float], + classdata: dict[str, Any], + datatype: str, + overwritedata: Optional[dict[str, str]] = None, + ) -> bool: """ Helper function for setParameter(),setContinuous(),setSimulationOptions(),setLinearizationOption(),setOptimizationOption() + args1 - string or list of string given by user args2 - dict() containing the values of different variables(eg:, parameter,continuous,simulation parameters) args3 - function name (eg; continuous, parameter, simulation, linearization,optimization) args4 - dict() which stores the new override variables list, """ - def apply_single(args1): - args1 = self._strip_space(args1) - value = args1.split("=") - if value[0] in args2: - if args3 == "parameter" and self.isParameterChangeable(value[0], value[1]): - args2[value[0]] = value[1] - if args4 is not None: - args4[value[0]] = value[1] - elif args3 != "parameter": - args2[value[0]] = value[1] - if args4 is not None: - args4[value[0]] = value[1] + + # TODO: cleanup / data handling / ... + # (1) args1: Optional[str | list[str]] -> convert to dict + # (2) work on dict! inputs: dict[str, str | int | float | ?numbers.number?] + # (3) handle function + # (4) use it also for other functions with such an input, i.e. 'key=value' | ['key=value'] + # (5) include setInputs() + + def apply_single(key_val: str): + key_val_list = key_val.split("=") + if len(key_val_list) != 2: + raise ModelicaSystemError(f"Invalid key = value pair: {key_val}") + + name = self._strip_space(key_val_list[0]) + value = self._strip_space(key_val_list[1]) + + if name in classdata: + if datatype == "parameter" and not self.isParameterChangeable(name): + logger.debug(f"It is not possible to set the parameter {repr(name)}. It seems to be " + "structural, final, protected, evaluated or has a non-constant binding. " + "Use sendExpression(...) and rebuild the model using buildModel() API; example: " + "sendExpression(\"setParameterValue(" + f"{self.modelName}, {name}, {value if value is not None else ''}" + ")\") ") + return False + + classdata[name] = value + if overwritedata is not None: + overwritedata[name] = value return True else: raise ModelicaSystemError("Unhandled case in setMethodHelper.apply_single() - " - f"{repr(value[0])} is not a {repr(args3)} variable") + f"{repr(name)} is not a {repr(datatype)} variable") result = [] - if isinstance(args1, str): - result = [apply_single(args1)] + if isinstance(inputdata, str): + result = [apply_single(inputdata)] - elif isinstance(args1, list): + elif isinstance(inputdata, list): result = [] - args1 = self._strip_space(args1) - for var in args1: + inputdata = self._strip_space(inputdata) + for var in inputdata: result.append(apply_single(var)) return all(result) - def setContinuous(self, cvals): # 13 + def isParameterChangeable( + self, + name: str, + ): + q = self.getQuantities(name) + if q[0]["changeable"] == "false": + return False + return True + + def setContinuous(self, cvals: str | list[str] | dict[str, str | int | float]) -> 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: @@ -1048,9 +1081,13 @@ def setContinuous(self, cvals): # 13 >>> setContinuous("Name=value") >>> setContinuous(["Name1=value1","Name2=value2"]) """ - return self.setMethodHelper(cvals, self.continuouslist, "continuous", self.overridevariables) + return self.setMethodHelper( + inputdata=cvals, + classdata=self.continuouslist, + datatype="continuous", + overwritedata=self.overridevariables) - def setParameters(self, pvals): # 14 + def setParameters(self, pvals: str | list[str] | dict[str, str | int | float]) -> 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: @@ -1058,19 +1095,13 @@ def setParameters(self, pvals): # 14 >>> setParameters("Name=value") >>> setParameters(["Name1=value1","Name2=value2"]) """ - return self.setMethodHelper(pvals, self.paramlist, "parameter", self.overridevariables) - - def isParameterChangeable(self, name, value): - q = self.getQuantities(name) - if q[0]["changeable"] == "false": - logger.debug(f"setParameters() failed : It is not possible to set the following signal {repr(name)}. " - "It seems to be structural, final, protected or evaluated or has a non-constant binding, " - f"use sendExpression(\"setParameterValue({self.modelName}, {name}, {value})\") " - "and rebuild the model using buildModel() API") - return False - return True + return self.setMethodHelper( + inputdata=pvals, + classdata=self.paramlist, + datatype="parameter", + overwritedata=self.overridevariables) - def setSimulationOptions(self, simOptions): # 16 + def setSimulationOptions(self, simOptions: str | list[str] | dict[str, str | int | float]) -> 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: @@ -1078,9 +1109,13 @@ def setSimulationOptions(self, simOptions): # 16 >>> setSimulationOptions("Name=value") >>> setSimulationOptions(["Name1=value1","Name2=value2"]) """ - return self.setMethodHelper(simOptions, self.simulateOptions, "simulation-option", self.simoptionsoverride) + return self.setMethodHelper( + inputdata=simOptions, + classdata=self.simulateOptions, + datatype="simulation-option", + overwritedata=self.simoptionsoverride) - def setLinearizationOptions(self, linearizationOptions): # 18 + def setLinearizationOptions(self, linearizationOptions: str | list[str] | dict[str, str | int | float]) -> 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 @@ -1088,9 +1123,13 @@ def setLinearizationOptions(self, linearizationOptions): # 18 >>> setLinearizationOptions("Name=value") >>> setLinearizationOptions(["Name1=value1","Name2=value2"]) """ - return self.setMethodHelper(linearizationOptions, self.linearOptions, "Linearization-option", None) + return self.setMethodHelper( + inputdata=linearizationOptions, + classdata=self.linearOptions, + datatype="Linearization-option", + overwritedata=None) - def setOptimizationOptions(self, optimizationOptions): # 17 + def setOptimizationOptions(self, optimizationOptions: str | list[str] | dict[str, str | int | float]) -> 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: @@ -1098,9 +1137,13 @@ def setOptimizationOptions(self, optimizationOptions): # 17 >>> setOptimizationOptions("Name=value") >>> setOptimizationOptions(["Name1=value1","Name2=value2"]) """ - return self.setMethodHelper(optimizationOptions, self.optimizeOptions, "optimization-option", None) + return self.setMethodHelper( + inputdata=optimizationOptions, + classdata=self.optimizeOptions, + datatype="optimization-option", + overwritedata=None) - def setInputs(self, name): # 15 + def setInputs(self, name: str | list[str] | dict[str, str | int | float]) -> 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: @@ -1109,34 +1152,40 @@ def setInputs(self, name): # 15 >>> setInputs(["Name1=value1","Name2=value2"]) """ if isinstance(name, str): - name = self._strip_space(name) - value = name.split("=") - if value[0] in self.inputlist: - tmpvalue = eval(value[1]) + name1: str = name + name1 = name1.replace(" ", "") + value1 = name1.split("=") + if value1[0] in self.inputlist: + tmpvalue = eval(value1[1]) if isinstance(tmpvalue, (int, float)): - self.inputlist[value[0]] = [(float(self.simulateOptions["startTime"]), float(value[1])), - (float(self.simulateOptions["stopTime"]), float(value[1]))] + self.inputlist[value1[0]] = [(float(self.simulateOptions["startTime"]), float(value1[1])), + (float(self.simulateOptions["stopTime"]), float(value1[1]))] elif isinstance(tmpvalue, list): self.checkValidInputs(tmpvalue) - self.inputlist[value[0]] = tmpvalue + self.inputlist[value1[0]] = tmpvalue self.inputFlag = True else: - raise ModelicaSystemError(f"{value[0]} is not an input") + raise ModelicaSystemError(f"{value1[0]} is not an input") elif isinstance(name, list): - name = self._strip_space(name) - for var in name: - value = var.split("=") - if value[0] in self.inputlist: - tmpvalue = eval(value[1]) + name_list: list[str] = name + for name2 in name_list: + name2 = name2.replace(" ", "") + value2 = name2.split("=") + if value2[0] in self.inputlist: + tmpvalue = eval(value2[1]) if isinstance(tmpvalue, (int, float)): - self.inputlist[value[0]] = [(float(self.simulateOptions["startTime"]), float(value[1])), - (float(self.simulateOptions["stopTime"]), float(value[1]))] + self.inputlist[value2[0]] = [(float(self.simulateOptions["startTime"]), float(value2[1])), + (float(self.simulateOptions["stopTime"]), float(value2[1]))] elif isinstance(tmpvalue, list): self.checkValidInputs(tmpvalue) - self.inputlist[value[0]] = tmpvalue + self.inputlist[value2[0]] = tmpvalue self.inputFlag = True else: - raise ModelicaSystemError(f"{value[0]} is not an input!") + raise ModelicaSystemError(f"{value2[0]} is not an input!") + elif isinstance(name, dict): + raise NotImplementedError("Must be defined!") + + return True def checkValidInputs(self, name): if name != sorted(name, key=lambda x: x[0]): From c21c58f67d95d2547dea178c58db73728bf7c250 Mon Sep 17 00:00:00 2001 From: syntron Date: Mon, 23 Jun 2025 10:38:58 +0200 Subject: [PATCH 07/43] [ModelicaSystem] add _prepare_inputdata() --- OMPython/ModelicaSystem.py | 44 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/OMPython/ModelicaSystem.py b/OMPython/ModelicaSystem.py index 2609afba..a5503f42 100644 --- a/OMPython/ModelicaSystem.py +++ b/OMPython/ModelicaSystem.py @@ -1001,6 +1001,50 @@ def _strip_space(name): raise ModelicaSystemError("Unhandled input for strip_space()") + def _prepare_inputdata( + self, + rawinput: str | list[str] | dict[str, str | int | float], + ) -> 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}") + + inputdata = {key_val_list[0]: key_val_list[1]} + + return inputdata + + if isinstance(rawinput, 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) + return prepare_str(rawinput) + + if isinstance(rawinput, 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) + + inputdata: dict[str, str] = {} + for item in rawinput: + inputdata |= prepare_str(item) + + return inputdata + + if isinstance(rawinput, dict): + inputdata = {key: str(val) for key, val in rawinput.items()} + + return inputdata + def setMethodHelper( self, inputdata: str | list[str] | dict[str, str | int | float], From bf3a1a4997dc8dc5608f75e7b51d4c368538c392 Mon Sep 17 00:00:00 2001 From: syntron Date: Mon, 23 Jun 2025 10:39:55 +0200 Subject: [PATCH 08/43] [ModelicaSystem] update _set_method_helper() * rename from setMethodHelper() * use _prepare_inputdata() * cleanup code to align with new input as dict[str, str] * setInput() is a special case --- OMPython/ModelicaSystem.py | 161 +++++++++++++++++++------------------ 1 file changed, 81 insertions(+), 80 deletions(-) diff --git a/OMPython/ModelicaSystem.py b/OMPython/ModelicaSystem.py index a5503f42..a6c9b970 100644 --- a/OMPython/ModelicaSystem.py +++ b/OMPython/ModelicaSystem.py @@ -992,17 +992,7 @@ def getSolutions(self, varList=None, resultfile=None): # 12 return np_res @staticmethod - def _strip_space(name): - if isinstance(name, str): - return name.replace(" ", "") - - if isinstance(name, list): - return [x.replace(" ", "") for x in name] - - raise ModelicaSystemError("Unhandled input for strip_space()") - def _prepare_inputdata( - self, rawinput: str | list[str] | dict[str, str | int | float], ) -> dict[str, str]: """ @@ -1041,72 +1031,65 @@ def prepare_str(str_in: str) -> dict[str, str]: return inputdata if isinstance(rawinput, dict): - inputdata = {key: str(val) for key, val in rawinput.items()} + for key, val in rawinput.items(): + str_val = str(val) + if ' ' in key or ' ' in str_val: + raise ModelicaSystemError(f"Spaces not allowed in key/value pairs: {repr(key)} = {repr(val)}!") + inputdata[key] = str_val return inputdata - def setMethodHelper( + def _set_method_helper( self, - inputdata: str | list[str] | dict[str, str | int | float], + inputdata: dict[str, str], classdata: dict[str, Any], datatype: str, overwritedata: Optional[dict[str, str]] = None, ) -> bool: """ - Helper function for setParameter(),setContinuous(),setSimulationOptions(),setLinearizationOption(),setOptimizationOption() - - args1 - string or list of string given by user - args2 - dict() containing the values of different variables(eg:, parameter,continuous,simulation parameters) - args3 - function name (eg; continuous, parameter, simulation, linearization,optimization) - args4 - dict() which stores the new override variables list, - """ - - # TODO: cleanup / data handling / ... - # (1) args1: Optional[str | list[str]] -> convert to dict - # (2) work on dict! inputs: dict[str, str | int | float | ?numbers.number?] - # (3) handle function - # (4) use it also for other functions with such an input, i.e. 'key=value' | ['key=value'] - # (5) include setInputs() - - def apply_single(key_val: str): - key_val_list = key_val.split("=") - if len(key_val_list) != 2: - raise ModelicaSystemError(f"Invalid key = value pair: {key_val}") - - name = self._strip_space(key_val_list[0]) - value = self._strip_space(key_val_list[1]) + Helper function for: + * setParameter() + * setContinuous() + * setSimulationOptions() + * setLinearizationOption() + * setOptimizationOption() + * setInputs() - if name in classdata: - if datatype == "parameter" and not self.isParameterChangeable(name): - logger.debug(f"It is not possible to set the parameter {repr(name)}. It seems to be " + Parameters + ---------- + inputdata + string or list of string given by user + classdata + dict() containing the values of different variables (eg: parameter, continuous, simulation parameters) + datatype + type identifier (eg; continuous, parameter, simulation, linearization, optimization) + overwritedata + dict() which stores the new override variables list, + """ + + inputdata_status: dict[str, bool] = {} + for key, val in inputdata.items(): + status = False + if key in classdata: + if datatype == "parameter" and not self.isParameterChangeable(key): + logger.debug(f"It is not possible to set the parameter {repr(key)}. It seems to be " "structural, final, protected, evaluated or has a non-constant binding. " "Use sendExpression(...) and rebuild the model using buildModel() API; example: " "sendExpression(\"setParameterValue(" - f"{self.modelName}, {name}, {value if value is not None else ''}" + f"{self.modelName}, {key}, {val if val is not None else ''}" ")\") ") - return False - - classdata[name] = value - if overwritedata is not None: - overwritedata[name] = value - - return True - + else: + classdata[key] = val + if overwritedata is not None: + overwritedata[key] = val + status = True else: raise ModelicaSystemError("Unhandled case in setMethodHelper.apply_single() - " - f"{repr(name)} is not a {repr(datatype)} variable") - - result = [] - if isinstance(inputdata, str): - result = [apply_single(inputdata)] + f"{repr(key)} is not a {repr(datatype)} variable") - elif isinstance(inputdata, list): - result = [] - inputdata = self._strip_space(inputdata) - for var in inputdata: - result.append(apply_single(var)) + inputdata_status[key] = status - return all(result) + return all(inputdata_status.values()) def isParameterChangeable( self, @@ -1122,11 +1105,14 @@ def setContinuous(self, cvals: str | list[str] | dict[str, str | int | float]) - 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") - >>> setContinuous(["Name1=value1","Name2=value2"]) + >>> setContinuous("Name=value") # depreciated + >>> setContinuous(["Name1=value1","Name2=value2"]) # depreciated + >>> setContinuous(cvals={"Name1": "value1", "Name2": "value2"}) """ - return self.setMethodHelper( - inputdata=cvals, + inputdata = self._prepare_inputdata(rawinput=cvals) + + return self._set_method_helper( + inputdata=inputdata, classdata=self.continuouslist, datatype="continuous", overwritedata=self.overridevariables) @@ -1136,11 +1122,14 @@ def setParameters(self, pvals: str | list[str] | dict[str, str | int | float]) - 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") - >>> setParameters(["Name1=value1","Name2=value2"]) + >>> setParameters("Name=value") # depreciated + >>> setParameters(["Name1=value1","Name2=value2"]) # depreciated + >>> setParameters(pvals={"Name1": "value1", "Name2": "value2"}) """ - return self.setMethodHelper( - inputdata=pvals, + inputdata = self._prepare_inputdata(rawinput=pvals) + + return self._set_method_helper( + inputdata=inputdata, classdata=self.paramlist, datatype="parameter", overwritedata=self.overridevariables) @@ -1150,11 +1139,14 @@ def setSimulationOptions(self, simOptions: str | list[str] | dict[str, str | int 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") - >>> setSimulationOptions(["Name1=value1","Name2=value2"]) + >>> setSimulationOptions("Name=value") # depreciated + >>> setSimulationOptions(["Name1=value1","Name2=value2"]) # depreciated + >>> setSimulationOptions(simOptions={"Name1": "value1", "Name2": "value2"}) """ - return self.setMethodHelper( - inputdata=simOptions, + inputdata = self._prepare_inputdata(rawinput=simOptions) + + return self._set_method_helper( + inputdata=inputdata, classdata=self.simulateOptions, datatype="simulation-option", overwritedata=self.simoptionsoverride) @@ -1164,11 +1156,14 @@ def setLinearizationOptions(self, linearizationOptions: str | list[str] | dict[s 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") - >>> setLinearizationOptions(["Name1=value1","Name2=value2"]) + >>> setLinearizationOptions("Name=value") # depreciated + >>> setLinearizationOptions(["Name1=value1","Name2=value2"]) # depreciated + >>> setLinearizationOptions(linearizationOtions={"Name1": "value1", "Name2": "value2"}) """ - return self.setMethodHelper( - inputdata=linearizationOptions, + inputdata = self._prepare_inputdata(rawinput=linearizationOptions) + + return self._set_method_helper( + inputdata=inputdata, classdata=self.linearOptions, datatype="Linearization-option", overwritedata=None) @@ -1178,11 +1173,14 @@ def setOptimizationOptions(self, optimizationOptions: str | list[str] | dict[str 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") - >>> setOptimizationOptions(["Name1=value1","Name2=value2"]) + >>> setOptimizationOptions("Name=value") # depreciated + >>> setOptimizationOptions(["Name1=value1","Name2=value2"]) # depreciated + >>> setOptimizationOptions(optimizationOptions={"Name1": "value1", "Name2": "value2"}) """ - return self.setMethodHelper( - inputdata=optimizationOptions, + inputdata = self._prepare_inputdata(rawinput=optimizationOptions) + + return self._set_method_helper( + inputdata=inputdata, classdata=self.optimizeOptions, datatype="optimization-option", overwritedata=None) @@ -1192,9 +1190,12 @@ def setInputs(self, name: str | list[str] | dict[str, str | int | float]) -> boo 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: usage - >>> setInputs("Name=value") - >>> setInputs(["Name1=value1","Name2=value2"]) + >>> setInputs("Name=value") # depreciated + >>> setInputs(["Name1=value1","Name2=value2"]) # depreciated + >>> setInputs(name={"Name1": "value1", "Name2": "value2"}) """ + # inputdata = self._prepare_inputdata(rawinput=name) + if isinstance(name, str): name1: str = name name1 = name1.replace(" ", "") From 0301043fdde223d0c17bd78dd494b320210cf274 Mon Sep 17 00:00:00 2001 From: syntron Date: Mon, 23 Jun 2025 16:15:29 +0200 Subject: [PATCH 09/43] [ModelicaSystem] improve definition of _prepare_inputdata() --- OMPython/ModelicaSystem.py | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/OMPython/ModelicaSystem.py b/OMPython/ModelicaSystem.py index a6c9b970..f22cbf9e 100644 --- a/OMPython/ModelicaSystem.py +++ b/OMPython/ModelicaSystem.py @@ -1005,9 +1005,11 @@ def prepare_str(str_in: str) -> dict[str, str]: if len(key_val_list) != 2: raise ModelicaSystemError(f"Invalid 'key=value' pair: {str_in}") - inputdata = {key_val_list[0]: key_val_list[1]} + input_data_from_str: dict[str, str] = {key_val_list[0]: key_val_list[1]} - return inputdata + return input_data_from_str + + input_data: dict[str, str] = {} if isinstance(rawinput, str): warnings.warn(message="The definition of values to set should use a dictionary, " @@ -1024,20 +1026,21 @@ def prepare_str(str_in: str) -> dict[str, str]: category=DeprecationWarning, stacklevel=3) - inputdata: dict[str, str] = {} for item in rawinput: - inputdata |= prepare_str(item) + input_data |= prepare_str(item) - return inputdata + return input_data if isinstance(rawinput, dict): for key, val in rawinput.items(): str_val = str(val) if ' ' in key or ' ' in str_val: raise ModelicaSystemError(f"Spaces not allowed in key/value pairs: {repr(key)} = {repr(val)}!") - inputdata[key] = str_val + input_data[key] = str_val + + return input_data - return inputdata + raise ModelicaSystemError(f"Invalid type of input: {type(rawinput)}") def _set_method_helper( self, From 341e429c74a04c8b5e0cdf10e51a51044c1d177a Mon Sep 17 00:00:00 2001 From: syntron Date: Mon, 23 Jun 2025 16:16:02 +0200 Subject: [PATCH 10/43] [ModelicaSystem] rename _prepare_inputdata() => _prepare_input_data() --- OMPython/ModelicaSystem.py | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/OMPython/ModelicaSystem.py b/OMPython/ModelicaSystem.py index f22cbf9e..8234618e 100644 --- a/OMPython/ModelicaSystem.py +++ b/OMPython/ModelicaSystem.py @@ -992,8 +992,8 @@ def getSolutions(self, varList=None, resultfile=None): # 12 return np_res @staticmethod - def _prepare_inputdata( - rawinput: str | list[str] | dict[str, str | int | float], + def _prepare_input_data( + raw_input: str | list[str] | dict[str, str | int | float], ) -> dict[str, str]: """ Convert raw input to a structured dictionary {'key1': 'value1', 'key2': 'value2'}. @@ -1011,28 +1011,28 @@ def prepare_str(str_in: str) -> dict[str, str]: input_data: dict[str, str] = {} - if isinstance(rawinput, str): + if isinstance(raw_input, 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) - return prepare_str(rawinput) + return prepare_str(raw_input) - if isinstance(rawinput, list): + if isinstance(raw_input, 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 rawinput: + for item in raw_input: input_data |= prepare_str(item) return input_data - if isinstance(rawinput, dict): - for key, val in rawinput.items(): + if isinstance(raw_input, dict): + for key, val in raw_input.items(): str_val = str(val) if ' ' in key or ' ' in str_val: raise ModelicaSystemError(f"Spaces not allowed in key/value pairs: {repr(key)} = {repr(val)}!") @@ -1040,7 +1040,7 @@ def prepare_str(str_in: str) -> dict[str, str]: return input_data - raise ModelicaSystemError(f"Invalid type of input: {type(rawinput)}") + raise ModelicaSystemError(f"Invalid type of input: {type(raw_input)}") def _set_method_helper( self, @@ -1112,7 +1112,7 @@ def setContinuous(self, cvals: str | list[str] | dict[str, str | int | float]) - >>> setContinuous(["Name1=value1","Name2=value2"]) # depreciated >>> setContinuous(cvals={"Name1": "value1", "Name2": "value2"}) """ - inputdata = self._prepare_inputdata(rawinput=cvals) + inputdata = self._prepare_input_data(raw_input=cvals) return self._set_method_helper( inputdata=inputdata, @@ -1129,7 +1129,7 @@ def setParameters(self, pvals: str | list[str] | dict[str, str | int | float]) - >>> setParameters(["Name1=value1","Name2=value2"]) # depreciated >>> setParameters(pvals={"Name1": "value1", "Name2": "value2"}) """ - inputdata = self._prepare_inputdata(rawinput=pvals) + inputdata = self._prepare_input_data(raw_input=pvals) return self._set_method_helper( inputdata=inputdata, @@ -1146,7 +1146,7 @@ def setSimulationOptions(self, simOptions: str | list[str] | dict[str, str | int >>> setSimulationOptions(["Name1=value1","Name2=value2"]) # depreciated >>> setSimulationOptions(simOptions={"Name1": "value1", "Name2": "value2"}) """ - inputdata = self._prepare_inputdata(rawinput=simOptions) + inputdata = self._prepare_input_data(raw_input=simOptions) return self._set_method_helper( inputdata=inputdata, @@ -1163,7 +1163,7 @@ def setLinearizationOptions(self, linearizationOptions: str | list[str] | dict[s >>> setLinearizationOptions(["Name1=value1","Name2=value2"]) # depreciated >>> setLinearizationOptions(linearizationOtions={"Name1": "value1", "Name2": "value2"}) """ - inputdata = self._prepare_inputdata(rawinput=linearizationOptions) + inputdata = self._prepare_input_data(raw_input=linearizationOptions) return self._set_method_helper( inputdata=inputdata, @@ -1180,7 +1180,7 @@ def setOptimizationOptions(self, optimizationOptions: str | list[str] | dict[str >>> setOptimizationOptions(["Name1=value1","Name2=value2"]) # depreciated >>> setOptimizationOptions(optimizationOptions={"Name1": "value1", "Name2": "value2"}) """ - inputdata = self._prepare_inputdata(rawinput=optimizationOptions) + inputdata = self._prepare_input_data(raw_input=optimizationOptions) return self._set_method_helper( inputdata=inputdata, @@ -1197,7 +1197,7 @@ def setInputs(self, name: str | list[str] | dict[str, str | int | float]) -> boo >>> setInputs(["Name1=value1","Name2=value2"]) # depreciated >>> setInputs(name={"Name1": "value1", "Name2": "value2"}) """ - # inputdata = self._prepare_inputdata(rawinput=name) + # inputdata = self._prepare_input_data(raw_input=name) if isinstance(name, str): name1: str = name From 1cbd1f72d770328b74d387da27a90feee1a1c468 Mon Sep 17 00:00:00 2001 From: syntron Date: Mon, 23 Jun 2025 16:33:58 +0200 Subject: [PATCH 11/43] [ModelicaSystem] update setInput() * replace eval() with ast.literal_eval() as a saver version * use _prepare_input_data() * simplify code --- OMPython/ModelicaSystem.py | 73 ++++++++++++++------------------------ 1 file changed, 27 insertions(+), 46 deletions(-) diff --git a/OMPython/ModelicaSystem.py b/OMPython/ModelicaSystem.py index 8234618e..ca0a8cdd 100644 --- a/OMPython/ModelicaSystem.py +++ b/OMPython/ModelicaSystem.py @@ -32,6 +32,7 @@ CONDITIONS OF OSMC-PL. """ +import ast import csv from dataclasses import dataclass import importlib @@ -1197,57 +1198,37 @@ def setInputs(self, name: str | list[str] | dict[str, str | int | float]) -> boo >>> setInputs(["Name1=value1","Name2=value2"]) # depreciated >>> setInputs(name={"Name1": "value1", "Name2": "value2"}) """ - # inputdata = self._prepare_input_data(raw_input=name) - - if isinstance(name, str): - name1: str = name - name1 = name1.replace(" ", "") - value1 = name1.split("=") - if value1[0] in self.inputlist: - tmpvalue = eval(value1[1]) - if isinstance(tmpvalue, (int, float)): - self.inputlist[value1[0]] = [(float(self.simulateOptions["startTime"]), float(value1[1])), - (float(self.simulateOptions["stopTime"]), float(value1[1]))] - elif isinstance(tmpvalue, list): - self.checkValidInputs(tmpvalue) - self.inputlist[value1[0]] = tmpvalue + inputdata = self._prepare_input_data(raw_input=name) + + for key, val in inputdata.items(): + if key in self.inputlist: + val_evaluated = ast.literal_eval(val) + if isinstance(val_evaluated, (int, float)): + self.inputlist[key] = [(float(self.simulateOptions["startTime"]), float(val)), + (float(self.simulateOptions["stopTime"]), float(val))] + elif isinstance(val_evaluated, list): + 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)}") + + for item in val_evaluated: + if item[0] < float(self.simulateOptions["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.inputlist[key] = val_evaluated self.inputFlag = True else: - raise ModelicaSystemError(f"{value1[0]} is not an input") - elif isinstance(name, list): - name_list: list[str] = name - for name2 in name_list: - name2 = name2.replace(" ", "") - value2 = name2.split("=") - if value2[0] in self.inputlist: - tmpvalue = eval(value2[1]) - if isinstance(tmpvalue, (int, float)): - self.inputlist[value2[0]] = [(float(self.simulateOptions["startTime"]), float(value2[1])), - (float(self.simulateOptions["stopTime"]), float(value2[1]))] - elif isinstance(tmpvalue, list): - self.checkValidInputs(tmpvalue) - self.inputlist[value2[0]] = tmpvalue - self.inputFlag = True - else: - raise ModelicaSystemError(f"{value2[0]} is not an input!") - elif isinstance(name, dict): - raise NotImplementedError("Must be defined!") + raise ModelicaSystemError(f"{key} is not an input") return True - def checkValidInputs(self, name): - if name != sorted(name, key=lambda x: x[0]): - raise ModelicaSystemError('Time value should be in increasing order') - for l in name: - if isinstance(l, tuple): - # if l[0] < float(self.simValuesList[0]): - if l[0] < float(self.simulateOptions["startTime"]): - raise ModelicaSystemError('Input time value is less than simulation startTime') - if len(l) != 2: - raise ModelicaSystemError(f'Value for {l} is in incorrect format!') - else: - raise ModelicaSystemError('Error!!! Value must be in tuple format') - def createCSVData(self) -> pathlib.Path: start_time: float = float(self.simulateOptions["startTime"]) stop_time: float = float(self.simulateOptions["stopTime"]) From 2c87f4ed13cf480b8193611e18a0868d48cc4367 Mon Sep 17 00:00:00 2001 From: syntron Date: Mon, 23 Jun 2025 16:49:52 +0200 Subject: [PATCH 12/43] update tests - use new dict based input for set*() methods --- tests/test_ModelicaSystem.py | 20 ++++++++++---------- tests/test_linearization.py | 2 +- tests/test_optimization.py | 8 +++++--- 3 files changed, 16 insertions(+), 14 deletions(-) diff --git a/tests/test_ModelicaSystem.py b/tests/test_ModelicaSystem.py index 156dde03..9749e5f1 100644 --- a/tests/test_ModelicaSystem.py +++ b/tests/test_ModelicaSystem.py @@ -35,8 +35,8 @@ def test_setParameters(): mod = OMPython.ModelicaSystem(model_path + "BouncingBall.mo", "BouncingBall") # method 1 - mod.setParameters("e=1.234") - mod.setParameters("g=321.0") + mod.setParameters(pvals={"e": 1.234}) + mod.setParameters(pvals={"g": 321.0}) assert mod.getParameters("e") == ["1.234"] assert mod.getParameters("g") == ["321.0"] assert mod.getParameters() == { @@ -47,7 +47,7 @@ def test_setParameters(): mod.getParameters("thisParameterDoesNotExist") # method 2 - mod.setParameters(["e=21.3", "g=0.12"]) + mod.setParameters(pvals={"e": 21.3, "g": 0.12}) assert mod.getParameters() == { "e": "21.3", "g": "0.12", @@ -64,8 +64,8 @@ def test_setSimulationOptions(): mod = OMPython.ModelicaSystem(fileName=model_path + "BouncingBall.mo", modelName="BouncingBall") # method 1 - mod.setSimulationOptions("stopTime=1.234") - mod.setSimulationOptions("tolerance=1.1e-08") + mod.setSimulationOptions(simOptions={"stopTime": 1.234}) + mod.setSimulationOptions(simOptions={"tolerance": 1.1e-08}) assert mod.getSimulationOptions("stopTime") == ["1.234"] assert mod.getSimulationOptions("tolerance") == ["1.1e-08"] assert mod.getSimulationOptions(["tolerance", "stopTime"]) == ["1.1e-08", "1.234"] @@ -77,7 +77,7 @@ def test_setSimulationOptions(): mod.getSimulationOptions("thisOptionDoesNotExist") # method 2 - mod.setSimulationOptions(["stopTime=2.1", "tolerance=1.2e-08"]) + mod.setSimulationOptions(simOptions={"stopTime": 2.1, "tolerance": "1.2e-08"}) d = mod.getSimulationOptions() assert d["stopTime"] == "2.1" assert d["tolerance"] == "1.2e-08" @@ -119,7 +119,7 @@ def test_getSolutions(model_firstorder): a = -1 tau = -1 / a stopTime = 5*tau - mod.setSimulationOptions([f"stopTime={stopTime}", "stepSize=0.1", "tolerance=1e-8"]) + mod.setSimulationOptions(simOptions={"stopTime": stopTime, "stepSize": 0.1, "tolerance": 1e-8}) mod.simulate() x = mod.getSolutions("x") @@ -298,7 +298,7 @@ def test_getters(tmp_path): x0 = 1.0 x_analytical = -b/a + (x0 + b/a) * np.exp(a * stopTime) dx_analytical = (x0 + b/a) * a * np.exp(a * stopTime) - mod.setSimulationOptions(f"stopTime={stopTime}") + mod.setSimulationOptions(simOptions={"stopTime": stopTime}) mod.simulate() # getOutputs after simulate() @@ -327,7 +327,7 @@ def test_getters(tmp_path): mod.getContinuous("a") # a is a parameter with pytest.raises(OMPython.ModelicaSystemError): - mod.setSimulationOptions("thisOptionDoesNotExist=3") + mod.setSimulationOptions(simOptions={"thisOptionDoesNotExist": 3}) def test_simulate_inputs(tmp_path): @@ -345,7 +345,7 @@ def test_simulate_inputs(tmp_path): """) mod = OMPython.ModelicaSystem(fileName=model_file.as_posix(), modelName="M_input") - mod.setSimulationOptions("stopTime=1.0") + mod.setSimulationOptions(simOptions={"stopTime": 1.0}) # integrate zero (no setInputs call) - it should default to None -> 0 assert mod.getInputs() == { diff --git a/tests/test_linearization.py b/tests/test_linearization.py index 2c79190c..baec6202 100644 --- a/tests/test_linearization.py +++ b/tests/test_linearization.py @@ -62,7 +62,7 @@ def test_getters(tmp_path): assert "startTime" in d assert "stopTime" in d assert mod.getLinearizationOptions(["stopTime", "startTime"]) == [d["stopTime"], d["startTime"]] - mod.setLinearizationOptions("stopTime=0.02") + mod.setLinearizationOptions(linearizationOptions={"stopTime": 0.02}) assert mod.getLinearizationOptions("stopTime") == ["0.02"] mod.setInputs(["u1=10", "u2=0"]) diff --git a/tests/test_optimization.py b/tests/test_optimization.py index aa74df79..b4164397 100644 --- a/tests/test_optimization.py +++ b/tests/test_optimization.py @@ -35,13 +35,15 @@ def test_optimization_example(tmp_path): mod = OMPython.ModelicaSystem(fileName=model_file.as_posix(), modelName="BangBang2021") - mod.setOptimizationOptions(["numberOfIntervals=16", "stopTime=1", - "stepSize=0.001", "tolerance=1e-8"]) + mod.setOptimizationOptions(optimizationOptions={"numberOfIntervals": 16, + "stopTime": 1, + "stepSize": 0.001, + "tolerance": 1e-8}) # test the getter assert mod.getOptimizationOptions()["stopTime"] == "1" assert mod.getOptimizationOptions("stopTime") == ["1"] - assert mod.getOptimizationOptions(["tolerance", "stopTime"]) == ["1e-8", "1"] + assert mod.getOptimizationOptions(["tolerance", "stopTime"]) == ["1e-08", "1"] r = mod.optimize() # it is necessary to specify resultfile, otherwise it wouldn't find it. From c498498e0d3c5a8567a50cf9ba28c90b0195ea85 Mon Sep 17 00:00:00 2001 From: syntron Date: Mon, 23 Jun 2025 17:02:05 +0200 Subject: [PATCH 13/43] [ModelicaSystem] add type hint for return value of isParameterChangeable() --- OMPython/ModelicaSystem.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/OMPython/ModelicaSystem.py b/OMPython/ModelicaSystem.py index ca0a8cdd..4c98c01a 100644 --- a/OMPython/ModelicaSystem.py +++ b/OMPython/ModelicaSystem.py @@ -1098,7 +1098,7 @@ def _set_method_helper( def isParameterChangeable( self, name: str, - ): + ) -> bool: q = self.getQuantities(name) if q[0]["changeable"] == "false": return False From 3b0c9d9e5b336c4caa5e980b6b2158a4d2aa52c8 Mon Sep 17 00:00:00 2001 From: syntron Date: Mon, 23 Jun 2025 17:09:46 +0200 Subject: [PATCH 14/43] [ModelicaSystem] fix type hint for _prepare_input_data() - use dict[str, Any] --- OMPython/ModelicaSystem.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/OMPython/ModelicaSystem.py b/OMPython/ModelicaSystem.py index 4c98c01a..ee3f73d1 100644 --- a/OMPython/ModelicaSystem.py +++ b/OMPython/ModelicaSystem.py @@ -994,7 +994,7 @@ def getSolutions(self, varList=None, resultfile=None): # 12 @staticmethod def _prepare_input_data( - raw_input: str | list[str] | dict[str, str | int | float], + raw_input: str | list[str] | dict[str, Any], ) -> dict[str, str]: """ Convert raw input to a structured dictionary {'key1': 'value1', 'key2': 'value2'}. @@ -1104,7 +1104,7 @@ def isParameterChangeable( return False return True - def setContinuous(self, cvals: str | list[str] | dict[str, str | int | float]) -> bool: + def setContinuous(self, cvals: str | list[str] | 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: @@ -1121,7 +1121,7 @@ def setContinuous(self, cvals: str | list[str] | dict[str, str | int | float]) - datatype="continuous", overwritedata=self.overridevariables) - def setParameters(self, pvals: str | list[str] | dict[str, str | int | float]) -> bool: + def setParameters(self, pvals: str | list[str] | 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: @@ -1138,7 +1138,7 @@ def setParameters(self, pvals: str | list[str] | dict[str, str | int | float]) - datatype="parameter", overwritedata=self.overridevariables) - def setSimulationOptions(self, simOptions: str | list[str] | dict[str, str | int | float]) -> bool: + def setSimulationOptions(self, simOptions: str | list[str] | 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: @@ -1155,7 +1155,7 @@ def setSimulationOptions(self, simOptions: str | list[str] | dict[str, str | int datatype="simulation-option", overwritedata=self.simoptionsoverride) - def setLinearizationOptions(self, linearizationOptions: str | list[str] | dict[str, str | int | float]) -> bool: + def setLinearizationOptions(self, linearizationOptions: str | list[str] | 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 @@ -1172,7 +1172,7 @@ def setLinearizationOptions(self, linearizationOptions: str | list[str] | dict[s datatype="Linearization-option", overwritedata=None) - def setOptimizationOptions(self, optimizationOptions: str | list[str] | dict[str, str | int | float]) -> bool: + def setOptimizationOptions(self, optimizationOptions: str | list[str] | 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: @@ -1189,7 +1189,7 @@ def setOptimizationOptions(self, optimizationOptions: str | list[str] | dict[str datatype="optimization-option", overwritedata=None) - def setInputs(self, name: str | list[str] | dict[str, str | int | float]) -> bool: + def setInputs(self, name: str | list[str] | 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: From 241615dd823f9ec9bc7000fbe8b11bf97434d427 Mon Sep 17 00:00:00 2001 From: syntron Date: Mon, 23 Jun 2025 17:14:54 +0200 Subject: [PATCH 15/43] [ModelicaSystem] setInput() - handly input data as list of tuples 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(). --- OMPython/ModelicaSystem.py | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/OMPython/ModelicaSystem.py b/OMPython/ModelicaSystem.py index ee3f73d1..5f01d814 100644 --- a/OMPython/ModelicaSystem.py +++ b/OMPython/ModelicaSystem.py @@ -1034,7 +1034,9 @@ def prepare_str(str_in: str) -> dict[str, str]: if isinstance(raw_input, dict): for key, val in raw_input.items(): - str_val = str(val) + # convert all values to strings to align it on one type: dict[str, str] + # spaces have to be removed as setInput() could take list of tuples as input and spaces would + str_val = str(val).replace(' ', '') if ' ' in key or ' ' in str_val: raise ModelicaSystemError(f"Spaces not allowed in key/value pairs: {repr(key)} = {repr(val)}!") input_data[key] = str_val @@ -1191,9 +1193,11 @@ def setOptimizationOptions(self, optimizationOptions: str | list[str] | dict[str def setInputs(self, name: str | list[str] | 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: - usage + 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(). + >>> setInputs("Name=value") # depreciated >>> setInputs(["Name1=value1","Name2=value2"]) # depreciated >>> setInputs(name={"Name1": "value1", "Name2": "value2"}) @@ -1202,7 +1206,11 @@ def setInputs(self, name: str | list[str] | dict[str, Any]) -> bool: for key, val in inputdata.items(): if key in self.inputlist: + if not isinstance(val, str): + 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.inputlist[key] = [(float(self.simulateOptions["startTime"]), float(val)), (float(self.simulateOptions["stopTime"]), float(val))] From f8c742bf590bf351ebf61fd852f1211d6beae188 Mon Sep 17 00:00:00 2001 From: syntron Date: Mon, 23 Jun 2025 17:15:43 +0200 Subject: [PATCH 16/43] update tests - use new dict based input for setInput() method --- tests/test_ModelicaSystem.py | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/tests/test_ModelicaSystem.py b/tests/test_ModelicaSystem.py index 9749e5f1..ef1dccf8 100644 --- a/tests/test_ModelicaSystem.py +++ b/tests/test_ModelicaSystem.py @@ -357,7 +357,7 @@ def test_simulate_inputs(tmp_path): assert np.isclose(y[-1], 0.0) # integrate a constant - mod.setInputs("u1=2.5") + mod.setInputs(name={"u1": 2.5}) assert mod.getInputs() == { "u1": [ (0.0, 2.5), @@ -370,7 +370,7 @@ def test_simulate_inputs(tmp_path): assert np.isclose(y[-1], 2.5) # now let's integrate the sum of two ramps - mod.setInputs("u1=[(0.0, 0.0), (0.5, 2), (1.0, 0)]") + mod.setInputs(name={"u1": [(0.0, 0.0), (0.5, 2), (1.0, 0)]}) assert mod.getInputs("u1") == [[ (0.0, 0.0), (0.5, 2.0), @@ -383,19 +383,17 @@ def test_simulate_inputs(tmp_path): # let's try some edge cases # unmatched startTime with pytest.raises(OMPython.ModelicaSystemError): - mod.setInputs("u1=[(-0.5, 0.0), (1.0, 1)]") + mod.setInputs(name={"u1": [(-0.5, 0.0), (1.0, 1)]}) mod.simulate() # unmatched stopTime with pytest.raises(OMPython.ModelicaSystemError): - mod.setInputs("u1=[(0.0, 0.0), (0.5, 1)]") + mod.setInputs(name={"u1": [(0.0, 0.0), (0.5, 1)]}) mod.simulate() # Let's use both inputs, but each one with different number of of # samples. This has an effect when generating the csv file. - mod.setInputs([ - "u1=[(0.0, 0), (1.0, 1)]", - "u2=[(0.0, 0), (0.25, 0.5), (0.5, 1.0), (1.0, 0)]", - ]) + mod.setInputs(name={"u1": [(0.0, 0), (1.0, 1)], + "u2": [(0.0, 0), (0.25, 0.5), (0.5, 1.0), (1.0, 0)]}) mod.simulate() assert pathlib.Path(mod.csvFile).read_text() == """time,u1,u2,end 0.0,0.0,0.0,0 From 9ec7edd491da710a264ea3d5a12dc07fc3563802 Mon Sep 17 00:00:00 2001 From: syntron Date: Tue, 24 Jun 2025 21:31:29 +0200 Subject: [PATCH 17/43] [test_linearization] fix setInput() call --- tests/test_linearization.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_linearization.py b/tests/test_linearization.py index baec6202..6af565c6 100644 --- a/tests/test_linearization.py +++ b/tests/test_linearization.py @@ -65,7 +65,7 @@ def test_getters(tmp_path): mod.setLinearizationOptions(linearizationOptions={"stopTime": 0.02}) assert mod.getLinearizationOptions("stopTime") == ["0.02"] - mod.setInputs(["u1=10", "u2=0"]) + mod.setInputs(name={"u1": 10, "u2": 0}) [A, B, C, D] = mod.linearize() g = float(mod.getParameters("g")[0]) l = float(mod.getParameters("l")[0]) From 36ca683ce3ae94dec4ea634c8c8850ccf1924db5 Mon Sep 17 00:00:00 2001 From: syntron Date: Thu, 26 Jun 2025 20:34:19 +0200 Subject: [PATCH 18/43] [ModelicaSystem] simplify _set_method_helper() --- OMPython/ModelicaSystem.py | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/OMPython/ModelicaSystem.py b/OMPython/ModelicaSystem.py index 5f01d814..ae332831 100644 --- a/OMPython/ModelicaSystem.py +++ b/OMPython/ModelicaSystem.py @@ -1075,24 +1075,24 @@ def _set_method_helper( inputdata_status: dict[str, bool] = {} for key, val in inputdata.items(): - status = False - if key in classdata: - if datatype == "parameter" and not self.isParameterChangeable(key): - logger.debug(f"It is not possible to set the parameter {repr(key)}. It seems to be " - "structural, final, protected, evaluated or has a non-constant binding. " - "Use sendExpression(...) and rebuild the model using buildModel() API; example: " - "sendExpression(\"setParameterValue(" - f"{self.modelName}, {key}, {val if val is not None else ''}" - ")\") ") - else: - classdata[key] = val - if overwritedata is not None: - overwritedata[key] = val - status = True - else: + if key not in classdata: raise ModelicaSystemError("Unhandled case in setMethodHelper.apply_single() - " f"{repr(key)} is not a {repr(datatype)} variable") + status = False + if datatype == "parameter" and not self.isParameterChangeable(key): + logger.debug(f"It is not possible to set the parameter {repr(key)}. It seems to be " + "structural, final, protected, evaluated or has a non-constant binding. " + "Use sendExpression(...) and rebuild the model using buildModel() API; example: " + "sendExpression(\"setParameterValue(" + f"{self.modelName}, {key}, {val if val is not None else ''}" + ")\") ") + else: + classdata[key] = val + if overwritedata is not None: + overwritedata[key] = val + status = True + inputdata_status[key] = status return all(inputdata_status.values()) From 817f20dc91992b6368c4fdfcfd8addbc021594b3 Mon Sep 17 00:00:00 2001 From: syntron Date: Thu, 26 Jun 2025 21:14:55 +0200 Subject: [PATCH 19/43] [ModelicaSystem] improve setInputs() - reduce spaces / cleanup --- OMPython/ModelicaSystem.py | 59 ++++++++++++++++++++------------------ 1 file changed, 31 insertions(+), 28 deletions(-) diff --git a/OMPython/ModelicaSystem.py b/OMPython/ModelicaSystem.py index ae332831..34d479bd 100644 --- a/OMPython/ModelicaSystem.py +++ b/OMPython/ModelicaSystem.py @@ -1205,36 +1205,39 @@ def setInputs(self, name: str | list[str] | dict[str, Any]) -> bool: inputdata = self._prepare_input_data(raw_input=name) for key, val in inputdata.items(): - if key in self.inputlist: - if not isinstance(val, str): - 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.inputlist[key] = [(float(self.simulateOptions["startTime"]), float(val)), - (float(self.simulateOptions["stopTime"]), float(val))] - elif isinstance(val_evaluated, list): - 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)}") - - for item in val_evaluated: - if item[0] < float(self.simulateOptions["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.inputlist[key] = val_evaluated - self.inputFlag = True - else: + if key not in self.inputlist: raise ModelicaSystemError(f"{key} is not an input") + if not isinstance(val, str): + 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.inputlist[key] = [(float(self.simulateOptions["startTime"]), float(val)), + (float(self.simulateOptions["stopTime"]), float(val))] + elif isinstance(val_evaluated, list): + 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)}") + + for item in val_evaluated: + if item[0] < float(self.simulateOptions["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.inputlist[key] = val_evaluated + else: + raise ModelicaSystemError(f"Data cannot be evaluated for {repr(key)}: {repr(val)}") + + self.inputFlag = True + return True def createCSVData(self) -> pathlib.Path: From 86c5cdd656f989a63b967680b58f31f4157a2d50 Mon Sep 17 00:00:00 2001 From: syntron Date: Tue, 24 Jun 2025 16:31:42 +0200 Subject: [PATCH 20/43] [ModelicaSystemDoE] add class --- OMPython/ModelicaSystem.py | 236 ++++++++++++++++++++++++++++++++++++- 1 file changed, 235 insertions(+), 1 deletion(-) diff --git a/OMPython/ModelicaSystem.py b/OMPython/ModelicaSystem.py index 5dfceba6..feccbfb8 100644 --- a/OMPython/ModelicaSystem.py +++ b/OMPython/ModelicaSystem.py @@ -36,17 +36,21 @@ import csv from dataclasses import dataclass import importlib +import itertools import logging import numbers import numpy as np import os +import pandas as pd import pathlib import platform +import queue import re import subprocess import tempfile import textwrap -from typing import Optional, Any +import threading +from typing import Any, Optional import warnings import xml.etree.ElementTree as ET @@ -1514,3 +1518,233 @@ def getLinearStates(self): >>> getLinearStates() """ return self.linearstates + + +class ModelicaSystemDoE: + def __init__( + self, + fileName: Optional[str | os.PathLike | pathlib.Path] = None, + modelName: Optional[str] = None, + lmodel: Optional[list[str | tuple[str, str]]] = None, + commandLineOptions: Optional[str] = None, + variableFilter: Optional[str] = None, + customBuildDirectory: Optional[str | os.PathLike | pathlib.Path] = None, + omhome: Optional[str] = None, + + simargs: Optional[dict[str, Optional[str | dict[str, str]]]] = None, + timeout: Optional[int] = None, + + resultpath: Optional[pathlib.Path] = None, + parameters: Optional[dict[str, list[str] | list[int] | list[float]]] = None, + ) -> None: + self._lmodel = lmodel + self._modelName = modelName + self._fileName = fileName + + self._CommandLineOptions = commandLineOptions + self._variableFilter = variableFilter + self._customBuildDirectory = customBuildDirectory + self._omhome = omhome + + # reference for the model; not used for any simulations but to evaluate parameters, etc. + self._mod = ModelicaSystem( + fileName=self._fileName, + modelName=self._modelName, + lmodel=self._lmodel, + commandLineOptions=self._CommandLineOptions, + variableFilter=self._variableFilter, + customBuildDirectory=self._customBuildDirectory, + omhome=self._omhome, + ) + + self._simargs = simargs + self._timeout = timeout + + if isinstance(resultpath, pathlib.Path): + self._resultpath = resultpath + else: + self._resultpath = pathlib.Path('.') + + if isinstance(parameters, dict): + self._parameters = parameters + else: + self._parameters = {} + + self._sim_df: Optional[pd.DataFrame] = None + self._sim_task_query: queue.Queue = queue.Queue() + + def prepare(self) -> int: + + param_structure = {} + param_simple = {} + for param_name in self._parameters.keys(): + changeable = self._mod.isParameterChangeable(name=param_name) + logger.info(f"Parameter {repr(param_name)} is changeable? {changeable}") + + if changeable: + param_simple[param_name] = self._parameters[param_name] + else: + param_structure[param_name] = self._parameters[param_name] + + param_structure_combinations = list(itertools.product(*param_structure.values())) + param_simple_combinations = list(itertools.product(*param_simple.values())) + + df_entries: list[pd.DataFrame] = [] + for idx_pc_structure, pc_structure in enumerate(param_structure_combinations): + mod_structure = ModelicaSystem( + fileName=self._fileName, + modelName=self._modelName, + lmodel=self._lmodel, + commandLineOptions=self._CommandLineOptions, + variableFilter=self._variableFilter, + customBuildDirectory=self._customBuildDirectory, + omhome=self._omhome, + ) + + sim_args_structure = {} + for idx_structure, pk_structure in enumerate(param_structure.keys()): + sim_args_structure[pk_structure] = pc_structure[idx_structure] + + pk_value = pc_structure[idx_structure] + if isinstance(pk_value, str): + expression = f"setParameterValue({self._modelName}, {pk_structure}, $Code(=\"{pk_value}\"))" + elif isinstance(pk_value, bool): + pk_value_bool_str = "true" if pk_value else "false" + expression = f"setParameterValue({self._modelName}, {pk_structure}, $Code(={pk_value_bool_str}));" + else: + expression = f"setParameterValue({self._modelName}, {pk_structure}, {pk_value})" + mod_structure.sendExpression(expression) + + for idx_pc_simple, pc_simple in enumerate(param_simple_combinations): + sim_args_simple = {} + for idx_simple, pk_simple in enumerate(param_simple.keys()): + sim_args_simple[pk_simple] = str(pc_simple[idx_simple]) + + resfilename = f"DOE_{idx_pc_structure:09d}_{idx_pc_simple:09d}.mat" + logger.info(f"use result file {repr(resfilename)} " + f"for structural parameters: {sim_args_structure} " + f"and simple parameters: {sim_args_simple}") + resultfile = self._resultpath / resfilename + + df_data = ( + { + 'ID structure': idx_pc_structure, + 'ID simple': idx_pc_simple, + 'resulfilename': resfilename, + 'structural parameters ID': idx_pc_structure, + } + | sim_args_structure + | { + 'non-structural parameters ID': idx_pc_simple, + } + | sim_args_simple + | { + 'results available': False, + } + ) + + df_entries.append(pd.DataFrame.from_dict(df_data)) + + cmd = mod_structure.simulate_cmd( + resultfile=resultfile.absolute().resolve(), + simargs={"override": sim_args_simple}, + ) + + self._sim_task_query.put(cmd) + + self._sim_df = pd.concat(df_entries, ignore_index=True) + + logger.info(f"Prepared {self._sim_df.shape[0]} simulation definitions for the defined DoE.") + + return self._sim_df.shape[0] + + def get_doe(self) -> Optional[pd.DataFrame]: + return self._sim_df + + def simulate(self, num_workers: int = 3) -> None: + + sim_count_total = self._sim_task_query.qsize() + + def worker(worker_id, task_queue): + while True: + sim_data = {} + try: + # Get the next task from the queue + cmd: ModelicaSystemCmd = task_queue.get(block=False) + except queue.Empty: + logger.info(f"[Worker {worker_id}] No more simulations to run.") + break + + resultfile = cmd.arg_get(key='r') + resultpath = pathlib.Path(resultfile) + + logger.info(f"[Worker {worker_id}] Performing task: {resultpath.name}") + + try: + sim_data['sim'].run() + except ModelicaSystemError as ex: + logger.warning(f"Simulation error for {resultpath.name}: {ex}") + + # Mark the task as done + task_queue.task_done() + + sim_count_done = sim_count_total - self._sim_task_query.qsize() + logger.info(f"[Worker {worker_id}] Task completed: {resultpath.name} " + f"({sim_count_done}/{sim_count_total} = " + f"{sim_count_done / sim_count_total * 100:.2f}% of tasks left)") + + logger.info(f"Start simulations for DoE with {sim_count_total} simulations " + f"using {num_workers} workers ...") + + # Create and start worker threads + threads = [] + for i in range(num_workers): + thread = threading.Thread(target=worker, args=(i, self._sim_task_query)) + thread.start() + threads.append(thread) + + # Wait for all threads to complete + for thread in threads: + thread.join() + + for idx, row in self._sim_df.to_dict('index').items(): + resultfilename = row['resultfilename'] + resultfile = self._resultpath / resultfilename + + if resultfile.exists(): + self._sim_df.loc[idx, 'results available'] = True + + sim_total = self._sim_df.shape[0] + sim_done = self._sim_df['results available'].sum() + logger.info(f"All workers finished ({sim_done} of {sim_total} simulations with a result file).") + + def get_solutions( + self, + var_list: Optional[list] = None, + ) -> Optional[tuple[str] | dict[str, pd.DataFrame | str]]: + if self._sim_df is None: + return None + + if self._sim_df.shape[0] == 0 or self._sim_df['results available'].sum() == 0: + raise ModelicaSystemError("No result files available - all simulations did fail?") + + if var_list is None: + resultfilename = self._sim_df['resultfilename'].values[0] + resultfile = self._resultpath / resultfilename + return self._mod.getSolutions(resultfile=resultfile) + + sol_dict: dict[str, pd.DataFrame | str] = {} + for row in self._sim_df.to_dict('records'): + resultfilename = row['resultfilename'] + resultfile = self._resultpath / resultfilename + + try: + sol = self._mod.getSolutions(varList=var_list, resultfile=resultfile) + sol_data = {var: sol[idx] for idx, var in var_list} + sol_df = pd.DataFrame(sol_data) + sol_dict[resultfilename] = sol_df + except ModelicaSystemError as ex: + logger.warning(f"No solution for {resultfilename}: {ex}") + sol_dict[resultfilename] = str(ex) + + return sol_dict From a072d8df190df25dd89e2c33ac5d4d29c80e4e67 Mon Sep 17 00:00:00 2001 From: syntron Date: Tue, 24 Jun 2025 16:32:04 +0200 Subject: [PATCH 21/43] [__init__] add class ModelicaSystemDoE --- OMPython/__init__.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/OMPython/__init__.py b/OMPython/__init__.py index 1da0a0a3..8a0584fd 100644 --- a/OMPython/__init__.py +++ b/OMPython/__init__.py @@ -36,7 +36,8 @@ CONDITIONS OF OSMC-PL. """ -from OMPython.ModelicaSystem import LinearizationResult, ModelicaSystem, ModelicaSystemCmd, ModelicaSystemError +from OMPython.ModelicaSystem import (LinearizationResult, ModelicaSystem, ModelicaSystemCmd, ModelicaSystemDoE, + ModelicaSystemError) from OMPython.OMCSession import (OMCSessionCmd, OMCSessionException, OMCSessionZMQ, OMCProcessPort, OMCProcessLocal, OMCProcessDocker, OMCProcessDockerContainer, OMCProcessWSL) @@ -46,6 +47,7 @@ 'LinearizationResult', 'ModelicaSystem', 'ModelicaSystemCmd', + 'ModelicaSystemDoE', 'ModelicaSystemError', 'OMCSessionCmd', From 8b08591f32c7bc6add6ca3d3857825afc587cfc8 Mon Sep 17 00:00:00 2001 From: syntron Date: Tue, 24 Jun 2025 16:32:57 +0200 Subject: [PATCH 22/43] [test_ModelicaSystemDoE] add test --- tests/test_ModelicaSystemDoE.py | 58 +++++++++++++++++++++++++++++++++ 1 file changed, 58 insertions(+) create mode 100644 tests/test_ModelicaSystemDoE.py diff --git a/tests/test_ModelicaSystemDoE.py b/tests/test_ModelicaSystemDoE.py new file mode 100644 index 00000000..a49ef111 --- /dev/null +++ b/tests/test_ModelicaSystemDoE.py @@ -0,0 +1,58 @@ +import OMPython +import pandas as pd +import pathlib +import pytest + +@pytest.fixture +def model_doe(tmp_path: pathlib.Path) -> pathlib.Path: + # see: https://trac.openmodelica.org/OpenModelica/ticket/4052 + mod = tmp_path / "M.mo" + mod.write_text(f""" +model M + parameter Integer p=1; + parameter Integer q=1; + parameter Real a = -1; + parameter Real b = -1; + Real x[p]; + Real y[q]; +equation + der(x) = a * fill(1.0, p); + der(y) = b * fill(1.0, q); +end M; +""") + return mod + + +@pytest.fixture +def param_doe() -> dict[str, list]: + param = { + # structural + 'p': [1, 2], + 'q': [3, 4], + # simple + 'a': [5, 6], + 'b': [7, 8], + } + return param + + +def test_ModelicaSystemDoE(tmp_path, model_doe, param_doe): + + tmpdir = tmp_path / 'DoE' + tmpdir.mkdir(exist_ok=True) + + mod_doe = OMPython.ModelicaSystemDoE( + fileName=model_doe.as_posix(), + modelName="M", + parameters=param_doe, + resultpath=tmpdir, + ) + mod_doe.prepare() + df_doe = mod_doe.get_doe() + assert isinstance(df_doe, pd.DataFrame) + assert df_doe.shape[0] == 16 + assert df_doe['results available'].sum() == 16 + + mod_doe.simulate() + sol = mod_doe.get_solutions(var_list=['x[1]', 'y[1]']) + assert len(sol) == 16 From 6a20e44560f3cf5e4feea673a704505456e08e6b Mon Sep 17 00:00:00 2001 From: syntron Date: Wed, 25 Jun 2025 21:01:25 +0200 Subject: [PATCH 23/43] [ModelicaSystemDoE] add docstrings --- OMPython/ModelicaSystem.py | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/OMPython/ModelicaSystem.py b/OMPython/ModelicaSystem.py index feccbfb8..78334d14 100644 --- a/OMPython/ModelicaSystem.py +++ b/OMPython/ModelicaSystem.py @@ -1521,6 +1521,10 @@ def getLinearStates(self): class ModelicaSystemDoE: + """ + Class to run DoEs based on a (Open)Modelica model using ModelicaSystem + """ + def __init__( self, fileName: Optional[str | os.PathLike | pathlib.Path] = None, @@ -1537,6 +1541,11 @@ def __init__( resultpath: Optional[pathlib.Path] = None, parameters: Optional[dict[str, list[str] | list[int] | list[float]]] = None, ) -> None: + """ + Initialisation of ModelicaSystemDoE. The parameters are based on: ModelicaSystem.__init__() and + ModelicaSystem.simulate(). Additionally, the path to store the result files is needed (= resultpath) as well as + a list of parameters to vary for the Doe (= parameters). All possible combinations are considered. + """ self._lmodel = lmodel self._modelName = modelName self._fileName = fileName @@ -1574,6 +1583,12 @@ def __init__( self._sim_task_query: queue.Queue = queue.Queue() def prepare(self) -> int: + """ + Prepare the DoE by evaluating the parameters. Each structural parameter requires a new instance of + ModelicaSystem while the non-structural parameters can just be set on the executable. + + The return value is the number of simulation defined. + """ param_structure = {} param_simple = {} @@ -1659,9 +1674,17 @@ def prepare(self) -> int: return self._sim_df.shape[0] def get_doe(self) -> Optional[pd.DataFrame]: + """ + Get the defined Doe as a poandas dataframe. + """ return self._sim_df def simulate(self, num_workers: int = 3) -> None: + """ + Simulate the DoE using the defined number of workers. + + Returns True if all simulations were done successfully, else False. + """ sim_count_total = self._sim_task_query.qsize() @@ -1722,6 +1745,15 @@ def get_solutions( self, var_list: Optional[list] = None, ) -> Optional[tuple[str] | dict[str, pd.DataFrame | str]]: + """ + Get all solutions of the DoE run. The following return values are possible: + + * None, if there no simulation was run + + * A list of variables if val_list == None + + * The Solutions as dict[str, pd.DataFrame] if a value list (== val_list) is defined. + """ if self._sim_df is None: return None From d794f8d4842a4366c9c21db9824b584204652e05 Mon Sep 17 00:00:00 2001 From: syntron Date: Wed, 25 Jun 2025 21:03:59 +0200 Subject: [PATCH 24/43] [ModelicaSystemDoE] define dict keys as constants --- OMPython/ModelicaSystem.py | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/OMPython/ModelicaSystem.py b/OMPython/ModelicaSystem.py index 78334d14..c638589f 100644 --- a/OMPython/ModelicaSystem.py +++ b/OMPython/ModelicaSystem.py @@ -1525,6 +1525,9 @@ class ModelicaSystemDoE: Class to run DoEs based on a (Open)Modelica model using ModelicaSystem """ + DF_COLUMNS_RESULTFILENAME: str = 'resultfilename' + DF_COLUMNS_RESULTS_AVAILABLE: str = 'results available' + def __init__( self, fileName: Optional[str | os.PathLike | pathlib.Path] = None, @@ -1645,7 +1648,7 @@ def prepare(self) -> int: { 'ID structure': idx_pc_structure, 'ID simple': idx_pc_simple, - 'resulfilename': resfilename, + self.DF_COLUMNS_RESULTFILENAME: resfilename, 'structural parameters ID': idx_pc_structure, } | sim_args_structure @@ -1654,7 +1657,7 @@ def prepare(self) -> int: } | sim_args_simple | { - 'results available': False, + self.DF_COLUMNS_RESULTS_AVAILABLE: False, } ) @@ -1731,14 +1734,15 @@ def worker(worker_id, task_queue): thread.join() for idx, row in self._sim_df.to_dict('index').items(): - resultfilename = row['resultfilename'] + resultfilename = row[self.DF_COLUMNS_RESULTFILENAME] resultfile = self._resultpath / resultfilename if resultfile.exists(): - self._sim_df.loc[idx, 'results available'] = True + mask = self._sim_df[self.DF_COLUMNS_RESULTFILENAME] == resultfilename + self._sim_df.loc[mask, self.DF_COLUMNS_RESULTS_AVAILABLE] = True + sim_done = self._sim_df[self.DF_COLUMNS_RESULTS_AVAILABLE].sum() sim_total = self._sim_df.shape[0] - sim_done = self._sim_df['results available'].sum() logger.info(f"All workers finished ({sim_done} of {sim_total} simulations with a result file).") def get_solutions( @@ -1757,17 +1761,17 @@ def get_solutions( if self._sim_df is None: return None - if self._sim_df.shape[0] == 0 or self._sim_df['results available'].sum() == 0: + if self._sim_df.shape[0] == 0 or self._sim_df[self.DF_COLUMNS_RESULTS_AVAILABLE].sum() == 0: raise ModelicaSystemError("No result files available - all simulations did fail?") if var_list is None: - resultfilename = self._sim_df['resultfilename'].values[0] + resultfilename = self._sim_df[self.DF_COLUMNS_RESULTFILENAME].values[0] resultfile = self._resultpath / resultfilename return self._mod.getSolutions(resultfile=resultfile) sol_dict: dict[str, pd.DataFrame | str] = {} for row in self._sim_df.to_dict('records'): - resultfilename = row['resultfilename'] + resultfilename = row[self.DF_COLUMNS_RESULTFILENAME] resultfile = self._resultpath / resultfilename try: From 7848282a1557e8b9ec85c58bcf0c362305ec35d3 Mon Sep 17 00:00:00 2001 From: syntron Date: Wed, 25 Jun 2025 21:04:34 +0200 Subject: [PATCH 25/43] [ModelicaSystemDoE] build model after all structural parameters are defined --- OMPython/ModelicaSystem.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/OMPython/ModelicaSystem.py b/OMPython/ModelicaSystem.py index c638589f..7b8bef53 100644 --- a/OMPython/ModelicaSystem.py +++ b/OMPython/ModelicaSystem.py @@ -1617,6 +1617,7 @@ def prepare(self) -> int: variableFilter=self._variableFilter, customBuildDirectory=self._customBuildDirectory, omhome=self._omhome, + build=False, ) sim_args_structure = {} @@ -1633,6 +1634,8 @@ def prepare(self) -> int: expression = f"setParameterValue({self._modelName}, {pk_structure}, {pk_value})" mod_structure.sendExpression(expression) + mod_structure.buildModel(variableFilter=self._variableFilter) + for idx_pc_simple, pc_simple in enumerate(param_simple_combinations): sim_args_simple = {} for idx_simple, pk_simple in enumerate(param_simple.keys()): From 9a9c7f1bdf97a42681a3a942d131844a0465ca35 Mon Sep 17 00:00:00 2001 From: syntron Date: Wed, 25 Jun 2025 21:05:30 +0200 Subject: [PATCH 26/43] [ModelicaSystemDoE] cleanup prepare() / rename variables --- OMPython/ModelicaSystem.py | 32 +++++++++++++++++++------------- 1 file changed, 19 insertions(+), 13 deletions(-) diff --git a/OMPython/ModelicaSystem.py b/OMPython/ModelicaSystem.py index 7b8bef53..18975b84 100644 --- a/OMPython/ModelicaSystem.py +++ b/OMPython/ModelicaSystem.py @@ -1620,9 +1620,9 @@ def prepare(self) -> int: build=False, ) - sim_args_structure = {} + sim_param_structure = {} for idx_structure, pk_structure in enumerate(param_structure.keys()): - sim_args_structure[pk_structure] = pc_structure[idx_structure] + sim_param_structure[pk_structure] = pc_structure[idx_structure] pk_value = pc_structure[idx_structure] if isinstance(pk_value, str): @@ -1632,19 +1632,22 @@ def prepare(self) -> int: expression = f"setParameterValue({self._modelName}, {pk_structure}, $Code(={pk_value_bool_str}));" else: expression = f"setParameterValue({self._modelName}, {pk_structure}, {pk_value})" - mod_structure.sendExpression(expression) + res = mod_structure.sendExpression(expression) + if not res: + raise ModelicaSystemError(f"Cannot set structural parameter {self._modelName}.{pk_structure} " + f"to {pk_value} using {repr(expression)}") mod_structure.buildModel(variableFilter=self._variableFilter) for idx_pc_simple, pc_simple in enumerate(param_simple_combinations): - sim_args_simple = {} + sim_param_simple = {} for idx_simple, pk_simple in enumerate(param_simple.keys()): - sim_args_simple[pk_simple] = str(pc_simple[idx_simple]) + sim_param_simple[pk_simple] = str(pc_simple[idx_simple]) resfilename = f"DOE_{idx_pc_structure:09d}_{idx_pc_simple:09d}.mat" logger.info(f"use result file {repr(resfilename)} " - f"for structural parameters: {sim_args_structure} " - f"and simple parameters: {sim_args_simple}") + f"for structural parameters: {sim_param_structure} " + f"and simple parameters: {sim_param_simple}") resultfile = self._resultpath / resfilename df_data = ( @@ -1654,24 +1657,27 @@ def prepare(self) -> int: self.DF_COLUMNS_RESULTFILENAME: resfilename, 'structural parameters ID': idx_pc_structure, } - | sim_args_structure + | sim_param_structure | { 'non-structural parameters ID': idx_pc_simple, } - | sim_args_simple + | sim_param_simple | { self.DF_COLUMNS_RESULTS_AVAILABLE: False, } ) - df_entries.append(pd.DataFrame.from_dict(df_data)) + df_entries.append(pd.DataFrame(data=df_data, index=[0])) - cmd = mod_structure.simulate_cmd( + mscmd = mod_structure.simulate_cmd( resultfile=resultfile.absolute().resolve(), - simargs={"override": sim_args_simple}, + timeout=self._timeout, ) + if self._simargs is not None: + mscmd.args_set(args=self._simargs) + mscmd.args_set(args={"override": sim_param_simple}) - self._sim_task_query.put(cmd) + self._sim_task_query.put(mscmd) self._sim_df = pd.concat(df_entries, ignore_index=True) From 4639bf9d32a11e1a45b5cff79b0f1e93c47ea5c4 Mon Sep 17 00:00:00 2001 From: syntron Date: Wed, 25 Jun 2025 21:06:26 +0200 Subject: [PATCH 27/43] [ModelicaSystemDoE] cleanup simulate() / rename variables --- OMPython/ModelicaSystem.py | 29 +++++++++++++++++++---------- 1 file changed, 19 insertions(+), 10 deletions(-) diff --git a/OMPython/ModelicaSystem.py b/OMPython/ModelicaSystem.py index 18975b84..89ff5c32 100644 --- a/OMPython/ModelicaSystem.py +++ b/OMPython/ModelicaSystem.py @@ -1691,44 +1691,53 @@ def get_doe(self) -> Optional[pd.DataFrame]: """ return self._sim_df - def simulate(self, num_workers: int = 3) -> None: + def simulate( + self, + num_workers: int = 3, + ) -> bool: """ Simulate the DoE using the defined number of workers. Returns True if all simulations were done successfully, else False. """ - sim_count_total = self._sim_task_query.qsize() + sim_query_total = self._sim_task_query.qsize() + if not isinstance(self._sim_df, pd.DataFrame): + raise ModelicaSystemError("Missing Doe Summary!") + sim_df_total = self._sim_df.shape[0] def worker(worker_id, task_queue): while True: - sim_data = {} + mscmd: Optional[ModelicaSystemCmd] = None try: # Get the next task from the queue - cmd: ModelicaSystemCmd = task_queue.get(block=False) + mscmd = task_queue.get(block=False) except queue.Empty: logger.info(f"[Worker {worker_id}] No more simulations to run.") break - resultfile = cmd.arg_get(key='r') + if mscmd is None: + raise ModelicaSystemError("Missing simulation definition!") + + resultfile = mscmd.arg_get(key='r') resultpath = pathlib.Path(resultfile) logger.info(f"[Worker {worker_id}] Performing task: {resultpath.name}") try: - sim_data['sim'].run() + mscmd.run() except ModelicaSystemError as ex: logger.warning(f"Simulation error for {resultpath.name}: {ex}") # Mark the task as done task_queue.task_done() - sim_count_done = sim_count_total - self._sim_task_query.qsize() + sim_query_done = sim_query_total - self._sim_task_query.qsize() logger.info(f"[Worker {worker_id}] Task completed: {resultpath.name} " - f"({sim_count_done}/{sim_count_total} = " - f"{sim_count_done / sim_count_total * 100:.2f}% of tasks left)") + f"({sim_query_done}/{sim_query_total} = " + f"{sim_query_done / sim_query_total * 100:.2f}% of tasks left)") - logger.info(f"Start simulations for DoE with {sim_count_total} simulations " + logger.info(f"Start simulations for DoE with {sim_query_total} simulations " f"using {num_workers} workers ...") # Create and start worker threads From 682171ff7420e5b18f3e4f82e55a64e558d4dded Mon Sep 17 00:00:00 2001 From: syntron Date: Wed, 25 Jun 2025 21:06:58 +0200 Subject: [PATCH 28/43] [ModelicaSystemDoE] cleanup get_solutions() / rename variables --- OMPython/ModelicaSystem.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/OMPython/ModelicaSystem.py b/OMPython/ModelicaSystem.py index 89ff5c32..3f1db4ee 100644 --- a/OMPython/ModelicaSystem.py +++ b/OMPython/ModelicaSystem.py @@ -1751,7 +1751,7 @@ def worker(worker_id, task_queue): for thread in threads: thread.join() - for idx, row in self._sim_df.to_dict('index').items(): + for row in self._sim_df.to_dict('records'): resultfilename = row[self.DF_COLUMNS_RESULTFILENAME] resultfile = self._resultpath / resultfilename @@ -1759,9 +1759,10 @@ def worker(worker_id, task_queue): mask = self._sim_df[self.DF_COLUMNS_RESULTFILENAME] == resultfilename self._sim_df.loc[mask, self.DF_COLUMNS_RESULTS_AVAILABLE] = True - sim_done = self._sim_df[self.DF_COLUMNS_RESULTS_AVAILABLE].sum() - sim_total = self._sim_df.shape[0] - logger.info(f"All workers finished ({sim_done} of {sim_total} simulations with a result file).") + sim_df_done = self._sim_df[self.DF_COLUMNS_RESULTS_AVAILABLE].sum() + logger.info(f"All workers finished ({sim_df_done} of {sim_df_total} simulations with a result file).") + + return sim_df_total == sim_df_done def get_solutions( self, From 642471133738a8536a93b1052222ac997880ebb9 Mon Sep 17 00:00:00 2001 From: syntron Date: Wed, 25 Jun 2025 21:07:14 +0200 Subject: [PATCH 29/43] [test_ModelicaSystemDoE] update test --- tests/test_ModelicaSystemDoE.py | 27 +++++++++++++++++++++++---- 1 file changed, 23 insertions(+), 4 deletions(-) diff --git a/tests/test_ModelicaSystemDoE.py b/tests/test_ModelicaSystemDoE.py index a49ef111..90b36c51 100644 --- a/tests/test_ModelicaSystemDoE.py +++ b/tests/test_ModelicaSystemDoE.py @@ -1,3 +1,4 @@ +import numpy as np import OMPython import pandas as pd import pathlib @@ -37,7 +38,6 @@ def param_doe() -> dict[str, list]: def test_ModelicaSystemDoE(tmp_path, model_doe, param_doe): - tmpdir = tmp_path / 'DoE' tmpdir.mkdir(exist_ok=True) @@ -46,13 +46,32 @@ def test_ModelicaSystemDoE(tmp_path, model_doe, param_doe): modelName="M", parameters=param_doe, resultpath=tmpdir, + simargs={"override": {'stopTime': 1.0}}, ) mod_doe.prepare() df_doe = mod_doe.get_doe() assert isinstance(df_doe, pd.DataFrame) assert df_doe.shape[0] == 16 - assert df_doe['results available'].sum() == 16 + assert df_doe['results available'].sum() == 0 mod_doe.simulate() - sol = mod_doe.get_solutions(var_list=['x[1]', 'y[1]']) - assert len(sol) == 16 + assert df_doe['results available'].sum() == 16 + + for row in df_doe.to_dict('records'): + resultfilename = row[mod_doe.DF_COLUMNS_RESULTFILENAME] + resultfile = mod_doe._resultpath / resultfilename + + var_dict = { + # simple / non-structural parameters + 'a': float(row['a']), + 'b': float(row['b']), + # structural parameters + 'p': float(row['p']), + 'q': float(row['q']), + # variables using the structural parameters + f"x[{row['p']}]": float(row['a']), + f"y[{row['p']}]": float(row['b']), + } + sol = mod_doe._mod.getSolutions(resultfile=resultfile.as_posix(), varList=list(var_dict.keys())) + + assert np.isclose(sol[:, -1], np.array(list(var_dict.values()))).all() From 73b87fb37be83d4b9f1fca75f3cf1ec6a338388e Mon Sep 17 00:00:00 2001 From: syntron Date: Wed, 25 Jun 2025 21:46:28 +0200 Subject: [PATCH 30/43] [ModelicaSystemDoE] add example to show the usage --- OMPython/ModelicaSystem.py | 56 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 56 insertions(+) diff --git a/OMPython/ModelicaSystem.py b/OMPython/ModelicaSystem.py index 3f1db4ee..c80a37ac 100644 --- a/OMPython/ModelicaSystem.py +++ b/OMPython/ModelicaSystem.py @@ -1523,6 +1523,62 @@ def getLinearStates(self): class ModelicaSystemDoE: """ Class to run DoEs based on a (Open)Modelica model using ModelicaSystem + + Example + ------- + ``` + import OMPython + import pathlib + + + def run_doe(): + mypath = pathlib.Path('.') + + model = mypath / "M.mo" + model.write_text( + " model M\n" + " parameter Integer p=1;\n" + " parameter Integer q=1;\n" + " parameter Real a = -1;\n" + " parameter Real b = -1;\n" + " Real x[p];\n" + " Real y[q];\n" + " equation\n" + " der(x) = a * fill(1.0, p);\n" + " der(y) = b * fill(1.0, q);\n" + " end M;\n" + ) + + param = { + # structural + 'p': [1, 2], + 'q': [3, 4], + # simple + 'a': [5, 6], + 'b': [7, 8], + } + + resdir = mypath / 'DoE' + resdir.mkdir(exist_ok=True) + + mod_doe = OMPython.ModelicaSystemDoE( + fileName=model.as_posix(), + modelName="M", + parameters=param, + resultpath=resdir, + simargs={"override": {'stopTime': 1.0}}, + ) + mod_doe.prepare() + df_doe = mod_doe.get_doe() + mod_doe.simulate() + var_list = mod_doe.get_solutions() + sol_dict = mod_doe.get_solutions(var_list=var_list) + + + if __name__ == "__main__": + run_doe() + ``` + """ DF_COLUMNS_RESULTFILENAME: str = 'resultfilename' From 53a94da10fc2584cd351a09cf1934c015f1c8fd3 Mon Sep 17 00:00:00 2001 From: syntron Date: Wed, 25 Jun 2025 21:49:36 +0200 Subject: [PATCH 31/43] add pandas as new dependency (use in ModelicaSystemDoE) --- .pre-commit-config.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 48f9ac64..d853e371 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -35,5 +35,6 @@ repos: additional_dependencies: - pyparsing - types-psutil + - pandas-stubs - pyzmq - numpy From 9a3b7a7169c229a8ad873329ba4045d867dbb835 Mon Sep 17 00:00:00 2001 From: syntron Date: Wed, 25 Jun 2025 22:07:20 +0200 Subject: [PATCH 32/43] [test_ModelicaSystemDoE] fix mypy --- tests/test_ModelicaSystemDoE.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/test_ModelicaSystemDoE.py b/tests/test_ModelicaSystemDoE.py index 90b36c51..288db522 100644 --- a/tests/test_ModelicaSystemDoE.py +++ b/tests/test_ModelicaSystemDoE.py @@ -4,11 +4,12 @@ import pathlib import pytest + @pytest.fixture def model_doe(tmp_path: pathlib.Path) -> pathlib.Path: # see: https://trac.openmodelica.org/OpenModelica/ticket/4052 mod = tmp_path / "M.mo" - mod.write_text(f""" + mod.write_text(""" model M parameter Integer p=1; parameter Integer q=1; From d2a1afc0915e27c9f0d540091fac2a1ab209fec2 Mon Sep 17 00:00:00 2001 From: syntron Date: Wed, 25 Jun 2025 22:11:28 +0200 Subject: [PATCH 33/43] add pandas to requirements in pyproject.toml --- pyproject.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/pyproject.toml b/pyproject.toml index 0abafd0c..d529f93b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -17,6 +17,7 @@ license = "BSD-3-Clause OR LicenseRef-OSMC-PL-1.2 OR GPL-3.0-only" requires-python = ">=3.10" dependencies = [ "numpy", + "pandas", "psutil", "pyparsing", "pyzmq", From ae68c429357f77097ed290954d627d870be83bea Mon Sep 17 00:00:00 2001 From: syntron Date: Wed, 25 Jun 2025 22:43:07 +0200 Subject: [PATCH 34/43] [ModelicaSystemDoE] rename class constants --- OMPython/ModelicaSystem.py | 22 +++++++++++----------- tests/test_ModelicaSystemDoE.py | 2 +- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/OMPython/ModelicaSystem.py b/OMPython/ModelicaSystem.py index c80a37ac..8376fc78 100644 --- a/OMPython/ModelicaSystem.py +++ b/OMPython/ModelicaSystem.py @@ -1581,8 +1581,8 @@ def run_doe(): """ - DF_COLUMNS_RESULTFILENAME: str = 'resultfilename' - DF_COLUMNS_RESULTS_AVAILABLE: str = 'results available' + DF_COLUMNS_RESULT_FILENAME: str = 'result filename' + DF_COLUMNS_RESULT_AVAILABLE: str = 'result available' def __init__( self, @@ -1710,7 +1710,7 @@ def prepare(self) -> int: { 'ID structure': idx_pc_structure, 'ID simple': idx_pc_simple, - self.DF_COLUMNS_RESULTFILENAME: resfilename, + self.DF_COLUMNS_RESULT_FILENAME: resfilename, 'structural parameters ID': idx_pc_structure, } | sim_param_structure @@ -1719,7 +1719,7 @@ def prepare(self) -> int: } | sim_param_simple | { - self.DF_COLUMNS_RESULTS_AVAILABLE: False, + self.DF_COLUMNS_RESULT_AVAILABLE: False, } ) @@ -1808,14 +1808,14 @@ def worker(worker_id, task_queue): thread.join() for row in self._sim_df.to_dict('records'): - resultfilename = row[self.DF_COLUMNS_RESULTFILENAME] + resultfilename = row[self.DF_COLUMNS_RESULT_FILENAME] resultfile = self._resultpath / resultfilename if resultfile.exists(): - mask = self._sim_df[self.DF_COLUMNS_RESULTFILENAME] == resultfilename - self._sim_df.loc[mask, self.DF_COLUMNS_RESULTS_AVAILABLE] = True + mask = self._sim_df[self.DF_COLUMNS_RESULT_FILENAME] == resultfilename + self._sim_df.loc[mask, self.DF_COLUMNS_RESULT_AVAILABLE] = True - sim_df_done = self._sim_df[self.DF_COLUMNS_RESULTS_AVAILABLE].sum() + sim_df_done = self._sim_df[self.DF_COLUMNS_RESULT_AVAILABLE].sum() logger.info(f"All workers finished ({sim_df_done} of {sim_df_total} simulations with a result file).") return sim_df_total == sim_df_done @@ -1836,17 +1836,17 @@ def get_solutions( if self._sim_df is None: return None - if self._sim_df.shape[0] == 0 or self._sim_df[self.DF_COLUMNS_RESULTS_AVAILABLE].sum() == 0: + if self._sim_df.shape[0] == 0 or self._sim_df[self.DF_COLUMNS_RESULT_AVAILABLE].sum() == 0: raise ModelicaSystemError("No result files available - all simulations did fail?") if var_list is None: - resultfilename = self._sim_df[self.DF_COLUMNS_RESULTFILENAME].values[0] + resultfilename = self._sim_df[self.DF_COLUMNS_RESULT_FILENAME].values[0] resultfile = self._resultpath / resultfilename return self._mod.getSolutions(resultfile=resultfile) sol_dict: dict[str, pd.DataFrame | str] = {} for row in self._sim_df.to_dict('records'): - resultfilename = row[self.DF_COLUMNS_RESULTFILENAME] + resultfilename = row[self.DF_COLUMNS_RESULT_FILENAME] resultfile = self._resultpath / resultfilename try: diff --git a/tests/test_ModelicaSystemDoE.py b/tests/test_ModelicaSystemDoE.py index 288db522..7561dbf1 100644 --- a/tests/test_ModelicaSystemDoE.py +++ b/tests/test_ModelicaSystemDoE.py @@ -59,7 +59,7 @@ def test_ModelicaSystemDoE(tmp_path, model_doe, param_doe): assert df_doe['results available'].sum() == 16 for row in df_doe.to_dict('records'): - resultfilename = row[mod_doe.DF_COLUMNS_RESULTFILENAME] + resultfilename = row[mod_doe.DF_COLUMNS_RESULT_FILENAME] resultfile = mod_doe._resultpath / resultfilename var_dict = { From 1e7e48c6b8458211b852a2164d48841974b900a9 Mon Sep 17 00:00:00 2001 From: syntron Date: Sat, 28 Jun 2025 21:02:01 +0200 Subject: [PATCH 35/43] [ModelicaSystemDoE] remove dependency on pandas * no need to add aditional requirements * hint how to use poandas in the docstrings * update test to match code changes --- .pre-commit-config.yaml | 1 - OMPython/ModelicaSystem.py | 127 ++++++++++++++++++++------------ pyproject.toml | 1 - tests/test_ModelicaSystemDoE.py | 33 +++++---- 4 files changed, 97 insertions(+), 65 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index d853e371..48f9ac64 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -35,6 +35,5 @@ repos: additional_dependencies: - pyparsing - types-psutil - - pandas-stubs - pyzmq - numpy diff --git a/OMPython/ModelicaSystem.py b/OMPython/ModelicaSystem.py index 8376fc78..7f1aa4c4 100644 --- a/OMPython/ModelicaSystem.py +++ b/OMPython/ModelicaSystem.py @@ -41,7 +41,6 @@ import numbers import numpy as np import os -import pandas as pd import pathlib import platform import queue @@ -1561,18 +1560,19 @@ def run_doe(): resdir = mypath / 'DoE' resdir.mkdir(exist_ok=True) - mod_doe = OMPython.ModelicaSystemDoE( + doe_mod = OMPython.ModelicaSystemDoE( fileName=model.as_posix(), modelName="M", parameters=param, resultpath=resdir, simargs={"override": {'stopTime': 1.0}}, ) - mod_doe.prepare() - df_doe = mod_doe.get_doe() - mod_doe.simulate() - var_list = mod_doe.get_solutions() - sol_dict = mod_doe.get_solutions(var_list=var_list) + doe_mod.prepare() + doe_dict = doe_mod.get_doe() + doe_mod.simulate() + doe_sol = doe_mod.get_solutions() + + # ... work with doe_df and doe_sol ... if __name__ == "__main__": @@ -1638,7 +1638,7 @@ def __init__( else: self._parameters = {} - self._sim_df: Optional[pd.DataFrame] = None + self._sim_dict: Optional[dict[str, dict[str, Any]]] = None self._sim_task_query: queue.Queue = queue.Queue() def prepare(self) -> int: @@ -1663,7 +1663,7 @@ def prepare(self) -> int: param_structure_combinations = list(itertools.product(*param_structure.values())) param_simple_combinations = list(itertools.product(*param_simple.values())) - df_entries: list[pd.DataFrame] = [] + self._sim_dict = {} for idx_pc_structure, pc_structure in enumerate(param_structure_combinations): mod_structure = ModelicaSystem( fileName=self._fileName, @@ -1709,13 +1709,10 @@ def prepare(self) -> int: df_data = ( { 'ID structure': idx_pc_structure, - 'ID simple': idx_pc_simple, - self.DF_COLUMNS_RESULT_FILENAME: resfilename, - 'structural parameters ID': idx_pc_structure, } | sim_param_structure | { - 'non-structural parameters ID': idx_pc_simple, + 'ID non-structure': idx_pc_simple, } | sim_param_simple | { @@ -1723,7 +1720,7 @@ def prepare(self) -> int: } ) - df_entries.append(pd.DataFrame(data=df_data, index=[0])) + self._sim_dict[resfilename] = df_data mscmd = mod_structure.simulate_cmd( resultfile=resultfile.absolute().resolve(), @@ -1735,17 +1732,26 @@ def prepare(self) -> int: self._sim_task_query.put(mscmd) - self._sim_df = pd.concat(df_entries, ignore_index=True) - - logger.info(f"Prepared {self._sim_df.shape[0]} simulation definitions for the defined DoE.") + logger.info(f"Prepared {self._sim_task_query.qsize()} simulation definitions for the defined DoE.") - return self._sim_df.shape[0] + return self._sim_task_query.qsize() - def get_doe(self) -> Optional[pd.DataFrame]: + def get_doe(self) -> Optional[dict[str, dict[str, Any]]]: """ - Get the defined Doe as a poandas dataframe. + Get the defined DoE as a dict, where each key is the result filename and the value is a dict of simulation + settings including structural and non-structural parameters. + + The following code snippet can be used to convert the data to a pandas dataframe: + + ``` + import pandas as pd + + doe_dict = doe_mod.get_doe() + doe_df = pd.DataFrame.from_dict(data=doe_dict, orient='index') + ``` + """ - return self._sim_df + return self._sim_dict def simulate( self, @@ -1758,9 +1764,9 @@ def simulate( """ sim_query_total = self._sim_task_query.qsize() - if not isinstance(self._sim_df, pd.DataFrame): + if not isinstance(self._sim_dict, dict) or len(self._sim_dict) == 0: raise ModelicaSystemError("Missing Doe Summary!") - sim_df_total = self._sim_df.shape[0] + sim_dict_total = len(self._sim_dict) def worker(worker_id, task_queue): while True: @@ -1807,55 +1813,78 @@ def worker(worker_id, task_queue): for thread in threads: thread.join() - for row in self._sim_df.to_dict('records'): - resultfilename = row[self.DF_COLUMNS_RESULT_FILENAME] + sim_dict_done = 0 + for resultfilename in self._sim_dict: resultfile = self._resultpath / resultfilename - if resultfile.exists(): - mask = self._sim_df[self.DF_COLUMNS_RESULT_FILENAME] == resultfilename - self._sim_df.loc[mask, self.DF_COLUMNS_RESULT_AVAILABLE] = True + # include check for an empty (=> 0B) result file which indicates a crash of the model executable + # see: https://github.com/OpenModelica/OMPython/issues/261 + # https://github.com/OpenModelica/OpenModelica/issues/13829 + if resultfile.is_file() and resultfile.stat().st_size > 0: + self._sim_dict[resultfilename][self.DF_COLUMNS_RESULTS_AVAILABLE] = True + sim_dict_done += 1 - sim_df_done = self._sim_df[self.DF_COLUMNS_RESULT_AVAILABLE].sum() - logger.info(f"All workers finished ({sim_df_done} of {sim_df_total} simulations with a result file).") + logger.info(f"All workers finished ({sim_dict_done} of {sim_dict_total} simulations with a result file).") - return sim_df_total == sim_df_done + return sim_dict_total == sim_dict_done def get_solutions( self, var_list: Optional[list] = None, - ) -> Optional[tuple[str] | dict[str, pd.DataFrame | str]]: + ) -> Optional[tuple[str] | dict[str, dict[str, np.ndarray]]]: """ Get all solutions of the DoE run. The following return values are possible: - * None, if there no simulation was run - * A list of variables if val_list == None * The Solutions as dict[str, pd.DataFrame] if a value list (== val_list) is defined. + + The following code snippet can be used to convert the solution data for each run to a pandas dataframe: + + ``` + import pandas as pd + + doe_sol = doe_mod.get_solutions() + for key in doe_sol: + data = doe_sol[key]['data'] + if data: + doe_sol[key]['df'] = pd.DataFrame.from_dict(data=data) + else: + doe_sol[key]['df'] = None + ``` + """ - if self._sim_df is None: + if not isinstance(self._sim_dict, dict): return None - if self._sim_df.shape[0] == 0 or self._sim_df[self.DF_COLUMNS_RESULT_AVAILABLE].sum() == 0: + if len(self._sim_dict) == 0: raise ModelicaSystemError("No result files available - all simulations did fail?") - if var_list is None: - resultfilename = self._sim_df[self.DF_COLUMNS_RESULT_FILENAME].values[0] + sol_dict: dict[str, dict[str, Any]] = {} + for resultfilename in self._sim_dict: resultfile = self._resultpath / resultfilename - return self._mod.getSolutions(resultfile=resultfile) - sol_dict: dict[str, pd.DataFrame | str] = {} - for row in self._sim_df.to_dict('records'): - resultfilename = row[self.DF_COLUMNS_RESULT_FILENAME] - resultfile = self._resultpath / resultfilename + sol_dict[resultfilename] = {} + + if self._sim_dict[resultfilename][self.DF_COLUMNS_RESULTS_AVAILABLE] != True: + sol_dict[resultfilename]['msg'] = 'No result file available!' + sol_dict[resultfilename]['data'] = {} + continue + + if var_list is None: + var_list_row = list(self._mod.getSolutions(resultfile=resultfile)) + else: + var_list_row = var_list try: - sol = self._mod.getSolutions(varList=var_list, resultfile=resultfile) - sol_data = {var: sol[idx] for idx, var in var_list} - sol_df = pd.DataFrame(sol_data) - sol_dict[resultfilename] = sol_df + sol = self._mod.getSolutions(varList=var_list_row, resultfile=resultfile) + sol_data = {var: sol[idx] for idx, var in enumerate(var_list_row)} + sol_dict[resultfilename]['msg'] = 'Simulation available' + sol_dict[resultfilename]['data'] = sol_data except ModelicaSystemError as ex: - logger.warning(f"No solution for {resultfilename}: {ex}") - sol_dict[resultfilename] = str(ex) + msg = f"Error reading solution for {resultfilename}: {ex}" + logger.warning(msg) + sol_dict[resultfilename]['msg'] = msg + sol_dict[resultfilename]['data'] = {} return sol_dict diff --git a/pyproject.toml b/pyproject.toml index d529f93b..0abafd0c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -17,7 +17,6 @@ license = "BSD-3-Clause OR LicenseRef-OSMC-PL-1.2 OR GPL-3.0-only" requires-python = ">=3.10" dependencies = [ "numpy", - "pandas", "psutil", "pyparsing", "pyzmq", diff --git a/tests/test_ModelicaSystemDoE.py b/tests/test_ModelicaSystemDoE.py index 7561dbf1..40fed90d 100644 --- a/tests/test_ModelicaSystemDoE.py +++ b/tests/test_ModelicaSystemDoE.py @@ -1,6 +1,5 @@ import numpy as np import OMPython -import pandas as pd import pathlib import pytest @@ -42,25 +41,30 @@ def test_ModelicaSystemDoE(tmp_path, model_doe, param_doe): tmpdir = tmp_path / 'DoE' tmpdir.mkdir(exist_ok=True) - mod_doe = OMPython.ModelicaSystemDoE( + doe_mod = OMPython.ModelicaSystemDoE( fileName=model_doe.as_posix(), modelName="M", parameters=param_doe, resultpath=tmpdir, simargs={"override": {'stopTime': 1.0}}, ) - mod_doe.prepare() - df_doe = mod_doe.get_doe() - assert isinstance(df_doe, pd.DataFrame) - assert df_doe.shape[0] == 16 - assert df_doe['results available'].sum() == 0 + doe_count = doe_mod.prepare() + assert doe_count == 16 - mod_doe.simulate() - assert df_doe['results available'].sum() == 16 + doe_dict = doe_mod.get_doe() + assert isinstance(doe_dict, dict) + assert len(doe_dict.keys()) == 16 - for row in df_doe.to_dict('records'): - resultfilename = row[mod_doe.DF_COLUMNS_RESULT_FILENAME] - resultfile = mod_doe._resultpath / resultfilename + doe_status = doe_mod.simulate() + assert doe_status is True + + doe_sol = doe_mod.get_solutions() + + for resultfilename in doe_dict: + row = doe_dict[resultfilename] + + assert resultfilename in doe_sol + sol = doe_sol[resultfilename] var_dict = { # simple / non-structural parameters @@ -73,6 +77,7 @@ def test_ModelicaSystemDoE(tmp_path, model_doe, param_doe): f"x[{row['p']}]": float(row['a']), f"y[{row['p']}]": float(row['b']), } - sol = mod_doe._mod.getSolutions(resultfile=resultfile.as_posix(), varList=list(var_dict.keys())) - assert np.isclose(sol[:, -1], np.array(list(var_dict.values()))).all() + for var in var_dict: + assert var in sol['data'] + assert np.isclose(sol['data'][var][-1], var_dict[var]) From 81b4840aded506d2d0875eec99ea0c7fab335f02 Mon Sep 17 00:00:00 2001 From: syntron Date: Sat, 28 Jun 2025 21:05:04 +0200 Subject: [PATCH 36/43] [ModelicaSystemDoE.simulate] fix percent of tasks left --- OMPython/ModelicaSystem.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/OMPython/ModelicaSystem.py b/OMPython/ModelicaSystem.py index 7f1aa4c4..7fe50f61 100644 --- a/OMPython/ModelicaSystem.py +++ b/OMPython/ModelicaSystem.py @@ -1796,8 +1796,8 @@ def worker(worker_id, task_queue): sim_query_done = sim_query_total - self._sim_task_query.qsize() logger.info(f"[Worker {worker_id}] Task completed: {resultpath.name} " - f"({sim_query_done}/{sim_query_total} = " - f"{sim_query_done / sim_query_total * 100:.2f}% of tasks left)") + f"({sim_query_total - sim_query_done}/{sim_query_total} = " + f"{(sim_query_total - sim_query_done) / sim_query_total * 100:.2f}% of tasks left)") logger.info(f"Start simulations for DoE with {sim_query_total} simulations " f"using {num_workers} workers ...") From 05e5f1883f875290806b635b8c85d34f7485944a Mon Sep 17 00:00:00 2001 From: syntron Date: Sat, 28 Jun 2025 21:05:34 +0200 Subject: [PATCH 37/43] [ModelicaSystemDoE.prepare] do not convert all non-structural parameters to string --- OMPython/ModelicaSystem.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/OMPython/ModelicaSystem.py b/OMPython/ModelicaSystem.py index 7fe50f61..fbe8ff68 100644 --- a/OMPython/ModelicaSystem.py +++ b/OMPython/ModelicaSystem.py @@ -1698,7 +1698,7 @@ def prepare(self) -> int: for idx_pc_simple, pc_simple in enumerate(param_simple_combinations): sim_param_simple = {} for idx_simple, pk_simple in enumerate(param_simple.keys()): - sim_param_simple[pk_simple] = str(pc_simple[idx_simple]) + sim_param_simple[pk_simple] = pc_simple[idx_simple] resfilename = f"DOE_{idx_pc_structure:09d}_{idx_pc_simple:09d}.mat" logger.info(f"use result file {repr(resfilename)} " From 9c411d8afda5f6386f4858df572ff8386bcefbc2 Mon Sep 17 00:00:00 2001 From: syntron Date: Sat, 28 Jun 2025 21:05:59 +0200 Subject: [PATCH 38/43] [ModelicaSystemDoE] update set parameter expressions for str and bool --- OMPython/ModelicaSystem.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/OMPython/ModelicaSystem.py b/OMPython/ModelicaSystem.py index fbe8ff68..ca46f9bf 100644 --- a/OMPython/ModelicaSystem.py +++ b/OMPython/ModelicaSystem.py @@ -1682,10 +1682,10 @@ def prepare(self) -> int: pk_value = pc_structure[idx_structure] if isinstance(pk_value, str): - expression = f"setParameterValue({self._modelName}, {pk_structure}, $Code(=\"{pk_value}\"))" + expression = f"setParameterValue({self._modelName}, {pk_structure}, \"{pk_value}\")" elif isinstance(pk_value, bool): pk_value_bool_str = "true" if pk_value else "false" - expression = f"setParameterValue({self._modelName}, {pk_structure}, $Code(={pk_value_bool_str}));" + expression = f"setParameterValue({self._modelName}, {pk_structure}, {pk_value_bool_str});" else: expression = f"setParameterValue({self._modelName}, {pk_structure}, {pk_value})" res = mod_structure.sendExpression(expression) From dea325db8a360bd0caf4bab90f9a026de4ff4422 Mon Sep 17 00:00:00 2001 From: syntron Date: Sat, 28 Jun 2025 21:10:17 +0200 Subject: [PATCH 39/43] [ModelicaSystemDoE] rename class constants --- OMPython/ModelicaSystem.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/OMPython/ModelicaSystem.py b/OMPython/ModelicaSystem.py index ca46f9bf..e209b90a 100644 --- a/OMPython/ModelicaSystem.py +++ b/OMPython/ModelicaSystem.py @@ -1581,8 +1581,8 @@ def run_doe(): """ - DF_COLUMNS_RESULT_FILENAME: str = 'result filename' - DF_COLUMNS_RESULT_AVAILABLE: str = 'result available' + DICT_RESULT_FILENAME: str = 'result filename' + DICT_RESULT_AVAILABLE: str = 'result available' def __init__( self, @@ -1716,7 +1716,7 @@ def prepare(self) -> int: } | sim_param_simple | { - self.DF_COLUMNS_RESULT_AVAILABLE: False, + self.DICT_RESULT_AVAILABLE: False, } ) @@ -1821,7 +1821,7 @@ def worker(worker_id, task_queue): # see: https://github.com/OpenModelica/OMPython/issues/261 # https://github.com/OpenModelica/OpenModelica/issues/13829 if resultfile.is_file() and resultfile.stat().st_size > 0: - self._sim_dict[resultfilename][self.DF_COLUMNS_RESULTS_AVAILABLE] = True + self._sim_dict[resultfilename][self.DICT_RESULT_AVAILABLE] = True sim_dict_done += 1 logger.info(f"All workers finished ({sim_dict_done} of {sim_dict_total} simulations with a result file).") @@ -1866,7 +1866,7 @@ def get_solutions( sol_dict[resultfilename] = {} - if self._sim_dict[resultfilename][self.DF_COLUMNS_RESULTS_AVAILABLE] != True: + if self._sim_dict[resultfilename][self.DICT_RESULT_AVAILABLE] != True: sol_dict[resultfilename]['msg'] = 'No result file available!' sol_dict[resultfilename]['data'] = {} continue From a15945ae6eb63de67a2d0bbe7d1615a76bb06f23 Mon Sep 17 00:00:00 2001 From: syntron Date: Sat, 28 Jun 2025 21:12:10 +0200 Subject: [PATCH 40/43] [ModelicaSystemDoE] fix bool comparison --- OMPython/ModelicaSystem.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/OMPython/ModelicaSystem.py b/OMPython/ModelicaSystem.py index e209b90a..7036bcd2 100644 --- a/OMPython/ModelicaSystem.py +++ b/OMPython/ModelicaSystem.py @@ -1866,7 +1866,7 @@ def get_solutions( sol_dict[resultfilename] = {} - if self._sim_dict[resultfilename][self.DICT_RESULT_AVAILABLE] != True: + if not self._sim_dict[resultfilename][self.DICT_RESULT_AVAILABLE]: sol_dict[resultfilename]['msg'] = 'No result file available!' sol_dict[resultfilename]['data'] = {} continue From 1e5f03fc6f0cc28c66c80f86b4632b37c57c1d85 Mon Sep 17 00:00:00 2001 From: syntron Date: Sat, 28 Jun 2025 21:13:17 +0200 Subject: [PATCH 41/43] [ModelicaSystemDoE] remove unused code --- OMPython/ModelicaSystem.py | 1 - 1 file changed, 1 deletion(-) diff --git a/OMPython/ModelicaSystem.py b/OMPython/ModelicaSystem.py index 7036bcd2..5fb90bb0 100644 --- a/OMPython/ModelicaSystem.py +++ b/OMPython/ModelicaSystem.py @@ -1770,7 +1770,6 @@ def simulate( def worker(worker_id, task_queue): while True: - mscmd: Optional[ModelicaSystemCmd] = None try: # Get the next task from the queue mscmd = task_queue.get(block=False) From 42f571ad0487fd03428d0b17ceeca17b56b36b0b Mon Sep 17 00:00:00 2001 From: syntron Date: Wed, 2 Jul 2025 22:07:59 +0200 Subject: [PATCH 42/43] [ModelicaSystem] fix updated definition of resultfile --- OMPython/ModelicaSystem.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/OMPython/ModelicaSystem.py b/OMPython/ModelicaSystem.py index 5fb90bb0..83bcadc4 100644 --- a/OMPython/ModelicaSystem.py +++ b/OMPython/ModelicaSystem.py @@ -924,7 +924,7 @@ def simulate_cmd( if self.inputFlag: # if model has input quantities # csvfile is based on name used for result file - csvfile = self.resultfile.parent / f"{self.resultfile.stem}.csv" + csvfile = resultfile.parent / f"{resultfile.stem}.csv" for i in self.inputlist: val = self.inputlist[i] From 36730bf33d898574dee65546183d1057f10f035d Mon Sep 17 00:00:00 2001 From: syntron Date: Wed, 2 Jul 2025 22:40:59 +0200 Subject: [PATCH 43/43] [ModelicaSystemCmd] fix mypy warning in ModelicaSystemDoE on usage of args_set() --- OMPython/ModelicaSystem.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/OMPython/ModelicaSystem.py b/OMPython/ModelicaSystem.py index 83bcadc4..d41782b6 100644 --- a/OMPython/ModelicaSystem.py +++ b/OMPython/ModelicaSystem.py @@ -137,7 +137,7 @@ def __init__(self, runpath: pathlib.Path, modelname: str, timeout: Optional[int] self._args: dict[str, str | None] = {} 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]] = None) -> None: """ Set one argument for the executable model. @@ -180,7 +180,7 @@ 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]]]) -> None: """ Define arguments for the model executable.