diff --git a/modelitool/corrai_connector.py b/modelitool/corrai_connector.py index fdf77fa..59d2d41 100644 --- a/modelitool/corrai_connector.py +++ b/modelitool/corrai_connector.py @@ -99,7 +99,7 @@ def function(self, x_dict): param[Parameter.NAME]: x_dict[param[Parameter.NAME]] for param in self.param_list } - self.om_model._set_param_dict(temp_dict) + self.om_model.set_param_dict(temp_dict) res = self.om_model.simulate() function_results = {} diff --git a/modelitool/simulate.py b/modelitool/simulate.py index 63caf14..47be737 100644 --- a/modelitool/simulate.py +++ b/modelitool/simulate.py @@ -1,5 +1,6 @@ import os import tempfile +import warnings from pathlib import Path import pandas as pd @@ -15,7 +16,6 @@ def __init__( self, model_path: Path | str, simulation_options: dict[str, float | str | int] = None, - x: pd.DataFrame = None, output_list: list[str] = None, simulation_path: Path = None, x_combitimetable_name: str = None, @@ -33,9 +33,6 @@ def __init__( - simulation_options (dict[str, float | str | int], optional): Options for the simulation. May include values for "startTime", "stopTime", "stepSize", "tolerance", "solver", "outputFormat". - - x (pd.DataFrame, optional): Input data for the simulation. Index shall - be a DatetimeIndex or integers. Columns must match the combi time table - used to specify boundary conditions in the Modelica System. - output_list (list[str], optional): List of output variables. Default will output all available variables. - simulation_path (Path, optional): Path to run the simulation and @@ -57,7 +54,7 @@ def __init__( if not os.path.exists(self._simulation_path): os.mkdir(simulation_path) - self._x = x if x is not None else pd.DataFrame() + self._x = pd.DataFrame() self.output_list = output_list self.omc = OMCSessionZMQ() self.omc.sendExpression(f'cd("{self._simulation_path.as_posix()}")') @@ -84,14 +81,16 @@ def simulate( ) -> pd.DataFrame: """ Runs the simulation with the provided parameters, simulation options and - boundariy conditions. + boundary conditions. - parameter_dict (dict, optional): Dictionary of parameters. - - simulation_options (dict, optional): Will update simulation options if it - had been given at the init phase. May include values for "startTime", - "stopTime", "stepSize", "tolerance", "solver", "outputFormat". + - simulation_options (dict, optional): May include values for "startTime", + "stopTime", "stepSize", "tolerance", "solver", "outputFormat". Can + also include 'x' with a DataFrame for boundary conditions. - x (pd.DataFrame, optional): Input data for the simulation. Index shall - be a DatetimeIndex or integers. Columns must match the combi time table - used to specify boundary conditions in the Modelica System. + be a DatetimeIndex or integers. Columns must match the combitimetable + used to specify boundary conditions in the Modelica System. If 'x' is + provided both in simulation_options and as a direct parameter, the one + provided as direct parameter will be used. - verbose (bool, optional): If True, prints simulation progress. Defaults to True. - simflags (str, optional): Additional simulation flags. @@ -102,9 +101,17 @@ def simulate( """ if parameter_dict is not None: - self._set_param_dict(parameter_dict) + self.set_param_dict(parameter_dict) if simulation_options is not None: + if x is not None and "x" in simulation_options: + warnings.warn( + "Boundary file 'x' specified both in simulation_options and as a " + "direct parameter. The 'x' provided in simulate() will be used.", + UserWarning, + stacklevel=2, + ) + self._set_simulation_options(simulation_options) if x is not None: @@ -172,28 +179,32 @@ def get_parameters(self): return self.model.getParameters() def _set_simulation_options(self, simulation_options): - self.model.setSimulationOptions( - [ - f'startTime={simulation_options["startTime"]}', - f'stopTime={simulation_options["stopTime"]}', - f'stepSize={simulation_options["stepSize"]}', - f'tolerance={simulation_options["tolerance"]}', - f'solver={simulation_options["solver"]}', - f'outputFormat={simulation_options["outputFormat"]}', - ] - ) + standard_options = { + "startTime": simulation_options.get("startTime"), + "stopTime": simulation_options.get("stopTime"), + "stepSize": simulation_options.get("stepSize"), + "tolerance": simulation_options.get("tolerance"), + "solver": simulation_options.get("solver"), + "outputFormat": simulation_options.get("outputFormat"), + } + + options = [f"{k}={v}" for k, v in standard_options.items() if v is not None] + self.model.setSimulationOptions(options) self.simulation_options = simulation_options + if "x" in simulation_options: + self._set_x(simulation_options["x"]) + def _set_x(self, df: pd.DataFrame): """Sets the input data for the simulation and updates the corresponding file.""" if not self._x.equals(df): new_bounds_path = self._simulation_path / "boundaries.txt" df_to_combitimetable(df, new_bounds_path) full_path = (self._simulation_path / "boundaries.txt").resolve().as_posix() - self._set_param_dict({f"{self.x_combitimetable_name}.fileName": full_path}) + self.set_param_dict({f"{self.x_combitimetable_name}.fileName": full_path}) self._x = df - def _set_param_dict(self, param_dict): + def set_param_dict(self, param_dict): self.model.setParameters([f"{item}={val}" for item, val in param_dict.items()]) diff --git a/tests/test_simulate.py b/tests/test_simulate.py index 117d295..0bd5a96 100644 --- a/tests/test_simulate.py +++ b/tests/test_simulate.py @@ -42,7 +42,7 @@ def test_set_param_dict(self, simul): "y.k": 2.0, } - simul._set_param_dict(test_dict) + simul.set_param_dict(test_dict) for key in test_dict.keys(): assert float(test_dict[key]) == float(simul.model.getParameters()[key]) @@ -100,19 +100,68 @@ def test_set_boundaries_df(self): "outputFormat": "mat", } - x = pd.DataFrame( + x_options = pd.DataFrame( {"Boundaries.y[1]": [10, 20, 30], "Boundaries.y[2]": [3, 4, 5]}, index=pd.date_range("2009-07-13 00:00:00", periods=3, freq="h"), ) + x_direct = pd.DataFrame( + {"Boundaries.y[1]": [100, 200, 300], "Boundaries.y[2]": [30, 40, 50]}, + index=pd.date_range("2009-07-13 00:00:00", periods=3, freq="h"), + ) + + simu = OMModel( + model_path="TestLib.boundary_test", + package_path=PACKAGE_DIR / "package.mo", + lmodel=["Modelica"], + ) + + simulation_options_with_x = simulation_options.copy() + simulation_options_with_x["x"] = x_options + res1 = simu.simulate(simulation_options=simulation_options_with_x) + res1 = res1.loc[:, ["Boundaries.y[1]", "Boundaries.y[2]"]] + np.testing.assert_allclose(x_options.to_numpy(), res1.to_numpy()) + assert np.all( + [x_options.index[i] == res1.index[i] for i in range(len(x_options.index))] + ) + assert np.all( + [ + x_options.columns[i] == res1.columns[i] + for i in range(len(x_options.columns)) + ] + ) simu = OMModel( model_path="TestLib.boundary_test", package_path=PACKAGE_DIR / "package.mo", lmodel=["Modelica"], ) + res2 = simu.simulate(simulation_options=simulation_options, x=x_direct) + res2 = res2.loc[:, ["Boundaries.y[1]", "Boundaries.y[2]"]] + np.testing.assert_allclose(x_direct.to_numpy(), res2.to_numpy()) + assert np.all( + [x_direct.index[i] == res2.index[i] for i in range(len(x_direct.index))] + ) + assert np.all( + [ + x_direct.columns[i] == res2.columns[i] + for i in range(len(x_direct.columns)) + ] + ) - res = simu.simulate(simulation_options=simulation_options, x=x) - res = res.loc[:, ["Boundaries.y[1]", "Boundaries.y[2]"]] - assert np.all([x.index[i] == res.index[i] for i in range(len(x.index))]) - np.testing.assert_allclose(x.to_numpy(), res.to_numpy()) - assert np.all([x.columns[i] == res.columns[i] for i in range(len(x.columns))]) + simu = OMModel( + model_path="TestLib.boundary_test", + package_path=PACKAGE_DIR / "package.mo", + lmodel=["Modelica"], + ) + with pytest.warns( + UserWarning, + match="Boundary file 'x' specified both in simulation_options and as a " + "direct parameter", + ): + res3 = simu.simulate( + simulation_options=simulation_options_with_x, x=x_direct + ) + res3 = res3.loc[:, ["Boundaries.y[1]", "Boundaries.y[2]"]] + np.testing.assert_allclose(x_direct.to_numpy(), res3.to_numpy()) + with pytest.raises(AssertionError): + np.testing.assert_allclose(x_options.to_numpy(), res3.to_numpy()) diff --git a/tutorials/Modelica models Handling.ipynb b/tutorials/Modelica models Handling.ipynb index de73e50..9435763 100644 --- a/tutorials/Modelica models Handling.ipynb +++ b/tutorials/Modelica models Handling.ipynb @@ -77,7 +77,7 @@ "source": [ "# 2. Set boundary file\n", "## Option A: load csv file\n", - "Let's load measurement data on python. We can use this dataframe directly in our simulator class to define boundary conditions." + "Let's load measurement data on python. We can use this dataframe to define boundary conditions of our model." ] }, { @@ -151,133 +151,139 @@ }, { "cell_type": "markdown", - "id": "f2dedef9-dc04-430f-8fe8-754c39ab7105", - "metadata": {}, - "source": [ - "#### Set up simulation options \n", - "\n", - "Before loading the modelica file, we need to specify the simulation running options. In Modelica, startTime and stopTime correspond to the number\n", - "of seconds since the beginning of the year. \n", - "\n", - "The values can be found in the file created earlier using df_to_combitimetable . Another way is to use the index of the DataFrame we just created.\n", - "The modelitool function modelitool.combitabconvert.datetime_to_seconds\n", - "helps you convert datetime index in seconds.\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "b26a8f6e-2f1a-41ed-a74e-dc9a41435110", + "id": "0ea8c4b4-2eab-429c-a67d-743aaa47a5bd", "metadata": {}, - "outputs": [], "source": [ - "from modelitool.combitabconvert import datetime_to_seconds" + "To avoid loading all ouptut from modelica model, let's first define a list of output that will be included in the dataframe output for any simulation." ] }, { "cell_type": "code", "execution_count": null, - "id": "a7472557-a5af-49bf-8ffc-08f30741e4c9", + "id": "64149508-369a-4a8c-8928-6c71090b4428", "metadata": {}, "outputs": [], "source": [ - "simulation_df = reference_df.loc[\"2018-03-22\":\"2018-03-23\"]\n", - "second_index = datetime_to_seconds(simulation_df.index)" + "output_list = [\n", + " \"T_coat_ins.T\",\n", + " \"T_ins_ins.T\",\n", + " \"Tw_out.T\"\n", + "]" ] }, { "cell_type": "markdown", - "id": "d4d76fff-d5c5-45b3-805b-255ee67e6ddc", + "id": "b092bb4236cc85f3", "metadata": {}, "source": [ - "- stepSize is the simulation timestep size. In this case it's 5 min or\n", - "300 sec.\n", - "- tolerance and solver are related to solver configuration\n", - "do not change if you don't need to.\n", - "- outputFormat can be either csv or mat. csv will enable faster data handling during sensitivity analyses and optimizations." + "Now, we can load the *om file.\n", + "\n", + "The `OMModel` class is used to load and simulate Modelica models. It requires the following parameters:\n", + "\n", + "- `model_path`: Path to the Modelica model file (*.mo) or model name if already loaded in OpenModelica\n", + "- `package_path` (optional): Path to additional Modelica packages required by the model\n", + "- `simulation_options` (optional): Dictionary containing simulation settings like:\n", + " - `startTime`: Start time in seconds\n", + " - `stopTime`: Stop time in seconds\n", + " - `stepSize`: Time step for the simulation\n", + " - `tolerance`: Numerical tolerance for the solver\n", + " - `solver`: Solver to use (e.g. \"dassl\")\n", + " - `outputFormat`: \"mat\" or \"csv\" for results format\n", + " - `x`: Boundary conditions as a DataFrame (optional)\n", + "- `output_list` (optional): List of variables to include in simulation results\n", + "- `lmodel` (optional): List of required Modelica libraries (e.g. [\"Modelica\"])" ] }, { "cell_type": "code", "execution_count": null, - "id": "4cbdd9d0-c377-484f-a4bd-d73fe3e741e2", + "id": "3264057e-66ef-41c6-b75a-6efd28748f8c", "metadata": {}, "outputs": [], "source": [ - "simulation_df" + "from modelitool.simulate import OMModel" ] }, { "cell_type": "code", "execution_count": null, - "id": "604aa9ed-b37b-4e61-b96e-a6dfdad42ca7", + "id": "8d9bfb90-3f07-49e9-9d7f-314ec3a07fc1", "metadata": {}, "outputs": [], "source": [ - "simulation_opt = {\n", - " \"startTime\": second_index[0],\n", - " \"stopTime\": second_index[-1],\n", - " \"stepSize\": 300,\n", - " \"tolerance\": 1e-06,\n", - " \"solver\": \"dassl\",\n", - " \"outputFormat\": \"csv\"\n", - "}" + "simu_OM = OMModel(\n", + " model_path=Path(TUTORIAL_DIR) / \"resources/etics_v0.mo\",\n", + " output_list=output_list,\n", + " lmodel=[\"Modelica\"],\n", + ")" ] }, { "cell_type": "markdown", - "id": "0ea8c4b4-2eab-429c-a67d-743aaa47a5bd", + "id": "766241a0-95b8-4916-9206-1ca240b2f361", "metadata": {}, "source": [ - "Finally, we can define a list of output that will be included in the dataframe output for any simulation." + "#### Set up simulation options \n", + "\n", + "As they were not specified when instantiating OMModel, simulation running options (if different from the one provided by the modelica model) should be defined.\n", + "\n", + "In Modelica, startTime and stopTime correspond to the number\n", + "of seconds since the beginning of the year. \n", + "\n", + "The values can be found in the file created earlier using df_to_combitimetable . Another way is to use the index of the DataFrame we just created.\n", + "The modelitool function modelitool.combitabconvert.datetime_to_seconds\n", + "helps you convert datetime index in seconds.\n" ] }, { "cell_type": "code", "execution_count": null, - "id": "64149508-369a-4a8c-8928-6c71090b4428", + "id": "b26a8f6e-2f1a-41ed-a74e-dc9a41435110", "metadata": {}, "outputs": [], "source": [ - "output_list = [\n", - " \"T_coat_ins.T\",\n", - " \"T_ins_ins.T\",\n", - " \"Tw_out.T\"\n", - "]" + "from modelitool.combitabconvert import datetime_to_seconds" ] }, { - "cell_type": "markdown", - "id": "b092bb4236cc85f3", + "cell_type": "code", + "execution_count": null, + "id": "a7472557-a5af-49bf-8ffc-08f30741e4c9", "metadata": {}, + "outputs": [], "source": [ - "Now let's load the *om file: \n" + "simulation_df = reference_df.loc[\"2018-03-22\":\"2018-03-23\"]\n", + "second_index = datetime_to_seconds(simulation_df.index)" ] }, { - "cell_type": "code", - "execution_count": null, - "id": "3264057e-66ef-41c6-b75a-6efd28748f8c", + "cell_type": "markdown", + "id": "9d771fd7-bdde-4b90-9d3e-699d3f488099", "metadata": {}, - "outputs": [], "source": [ - "from modelitool.simulate import OMModel" + "- stepSize is the simulation timestep size. In this case it's 5 min or\n", + "300 sec.\n", + "- tolerance and solver are related to solver configuration\n", + "do not change if you don't need to.\n", + "- outputFormat can be either csv or mat. csv will enable faster data handling during sensitivity analyses and optimizations.\n", + "- x: as the boundary conditions. If not given here, it can still be provided in method `simulate`." ] }, { "cell_type": "code", "execution_count": null, - "id": "8d9bfb90-3f07-49e9-9d7f-314ec3a07fc1", + "id": "604aa9ed-b37b-4e61-b96e-a6dfdad42ca7", "metadata": {}, "outputs": [], "source": [ - "simu_OM = OMModel(\n", - " model_path=Path(TUTORIAL_DIR) / \"resources/etics_v0.mo\",\n", - " simulation_options=simulation_opt,\n", - " x=simulation_df,\n", - " output_list=output_list,\n", - " lmodel=[\"Modelica\"],\n", - ")" + "simulation_opt = {\n", + " \"startTime\": second_index[0],\n", + " \"stopTime\": second_index[-1],\n", + " \"stepSize\": 300,\n", + " \"tolerance\": 1e-06,\n", + " \"solver\": \"dassl\",\n", + " \"outputFormat\": \"csv\"\n", + "}" ] }, { @@ -293,7 +299,7 @@ "id": "02e59be3-e4f4-44f0-adcd-66a43d200146", "metadata": {}, "source": [ - "Set the initial and parameter values in a dictionary." + "Set the initial and parameter values in a dictionary. They can either be set before simluation (with `set_param_dict()` method, or when using method `simulate()`. Each change of paramter value overwrite the previous one. " ] }, { @@ -317,12 +323,14 @@ "id": "65fd55a9-959f-4bef-9ee2-14d0c617b75b", "metadata": {}, "source": [ - "Simulation flags can be specified in simulate() method. Overview of possible simulation flags can be found here: https://openmodelica.org/doc/OpenModelicaUsersGuide/latest/simulationflags.html. Note that the simulation flag override cannot be used, as it was already used in class OMModel with simulation_options.\n", + "Simulation flags can also be specified in simulate() method. Overview of possible simulation flags can be found here: https://openmodelica.org/doc/OpenModelicaUsersGuide/latest/simulationflags.html. Note that the simulation flag override cannot be used, as it was already used in class OMModel with simulation_options.\n", "\n", - "If x boundary conditions is not specified or do not\n", + "If x boundary conditions do not\n", " have a DateTime index (seconds int), a year can be specified to convert\n", " int seconds index to a datetime index. If simulation spans overs several\n", - " years, it shall be the year when it begins." + " years, it shall be the year when it begins.\n", + "\n", + "The output of the `simulate()` method is a dataframe, containing the outputs listed in output_list." ] }, { @@ -335,26 +343,10 @@ "init_res_OM = simu_OM.simulate(\n", " simflags = \"-initialStepSize=60 -maxStepSize=3600 -w -lv=LOG_STATS\",\n", " parameter_dict=parameter_dict_OM,\n", + " x=reference_df,\n", " year=2024,\n", - ")" - ] - }, - { - "cell_type": "markdown", - "id": "70cfed9f-467b-44b2-b80d-459dc02288ae", - "metadata": {}, - "source": [ - "Results are displayed in a dataframe:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "4820b4f3-c446-4f65-869b-aee86ac96806", - "metadata": {}, - "outputs": [], - "source": [ - "init_res_OM" + ")\n", + "init_res_OM.head()" ] }, {