{ "cells": [ { "cell_type": "markdown", "id": "fe85b323", "metadata": {}, "source": [ "(integration_model_class_template_label)=\n", "\n", "# Workflow integration using model classes \n", "\n", "If you want to integrate tespy models in workflows, use the optimization\n", "API of tespy or couple your models with other software it is very advisable to\n", "create model classes, that control some of the interaction with the tespy model\n", "and the user for you. Here we show, how this can be done.\n", "\n", "```{tip}\n", "This page is just to give you an idea on how you can set up such kind of\n", "classes and what you can do with them. Note, that it is not feature complete\n", "and the capabilities/methods will always somehow have to be adapted to your \n", "requirements.\n", "```\n", "\n", "## Models in workflows\n", "\n", "If you are using your models in any kind of workflow or integration with other\n", "software, then\n", "\n", "- your model(s) will be likely be executed more than once\n", "- you will retrieve specific results or run processing routines frequently\n", "- you want to avoid model crashes or at least want them handled in some way\n", " without leaving side effects\n", "- you may let users not knowing what is behind the model enter input data\n", "- there might be the need for various tespy models doing similar things\n", " - various topologies\n", " - various working fluids\n", " - ...\n", "\n", "## A possible solution using classes\n", "\n", "We will present a possible solution to this problem, that has proved to be\n", "quite efficient. However, this is not universal truth, this is what has been\n", "working well. There might be better solutions, if you have any, please\n", "suggest them or give some feedback to improve this suggestion. It is greatly\n", "appreciated!\n", "\n", "### Structure\n", "\n", "The structure of the classes would look like this:\n", "\n", "- there is one parent class which handles\n", "\n", " - setup of model instances\n", " - inputting and retrieving data from the model instances\n", " - solving models in design or offdesign mode (we will show design only here)\n", " - postprocessing result, e.g. plotting TQ diagrams of heat exchangers or \n", " cycle diagrams of the process\n", "\n", "- there are child classes which create the concrete tespy model\n", "\n", " - these set up the Network and create an initial stable solution\n", " - create mappings between external input parameters and the internals\n", "\n", "This structure will help to nicely organize model input and output and \n", "automatically handle the solving of the models. In context of parameter input\n", "and output, the following structure works well:\n", "\n", "- use a dictionary of inputs, where the keys are mapped to specific parameters\n", " in the model, e.g. \"evaporator_pinch\" mapped to the parameter **td_pinch** of\n", " the **component** **evaporator** \n", "- internally an in-between layer of nested dictionaries is created which then\n", " is used to set parameters to the model or retrieve results, e.g.\n", " \n", " - user specifies {code}`{\"evaporator_pinch\": 10}`\n", " - in the parameter lookup we find:\n", " {code}`\"evaporator_pinch\": [\"Components\", \"evaporator\", \"td_pinch\"]`\n", " - this creates {code}`{\"Components\": {\"evaporator\": {\"td_pinch\": 10}}}`\n", "\n", "- the reason for this is, that it integrates nicely with the optimization API\n", "\n", "For the solving we have two methods, one that solves design case and one that\n", "solves offdesign case. The purpose is to:\n", "\n", "- solve the model\n", "- check if the solve was successful with the {code}`status` attribute of the\n", " network\n", "- handle potential errors in the solving or if the status is not 0\n", "\n", " - e.g handle what happens with converged simulation violating physical\n", " limits (status = 1)\n", " - e.g handle what happens with non-converged simulation (status = 2) or\n", " unexpected linear dependency (status = 3)\n", " - e.g handle what happens with crashed simulation (status = 99)\n", "\n", " ## Template\n", "\n", " With this set up, we can build our template:\n" ] }, { "cell_type": "code", "execution_count": null, "id": "408849c7", "metadata": { "execution": { "iopub.execute_input": "2025-10-15T15:11:59.485892Z", "iopub.status.busy": "2025-10-15T15:11:59.485380Z", "iopub.status.idle": "2025-10-15T15:12:01.502169Z", "shell.execute_reply": "2025-10-15T15:12:01.500679Z" } }, "outputs": [], "source": [ "from tespy.tools.helpers import merge_dicts\n", "from tespy.networks import Network\n", "import matplotlib.pyplot as plt\n", "from fluprodia import FluidPropertyDiagram\n", "\n", "\n", "DIAGRAMS = {}\n", "\n", "\n", "class ModelTemplate():\n", "\n", " def __init__(self) -> None:\n", " self.parameter_lookup = self._parameter_lookup()\n", " self._create_network()\n", "\n", " def _create_network(self) -> None:\n", " self.nw = Network()\n", " self.nw.units.set_defaults(\n", " **{\"temperature\": \"°C\", \"pressure\": \"bar\"}\n", " )\n", "\n", " def _parameter_lookup(self) -> dict:\n", " return {}\n", "\n", " def _map_parameter(self, parameter: str) -> tuple:\n", " return self.parameter_lookup[parameter]\n", "\n", " def _map_to_input_dict(self, **kwargs) -> dict:\n", " input_dict = {}\n", " for param, value in kwargs.items():\n", " if param not in self.parameter_lookup:\n", " msg = (\n", " f\"The parameter {param} is not mapped to any input of the \"\n", " \"model. The following parameters are available:\\n\"\n", " f\"{', '.join(self.parameter_lookup)}.\"\n", " )\n", " raise KeyError(msg)\n", " key = self._map_parameter(param)\n", " input_dict = merge_dicts(\n", " input_dict,\n", " {key[0]: {key[1]: {key[2]: value}}}\n", " )\n", " return input_dict\n", "\n", " def get_parameter(self, parameter: str) -> float:\n", " mapped = self._map_parameter(parameter)\n", " if mapped[0] == \"Connections\":\n", " return self.nw.get_conn(mapped[1]).get_attr(mapped[2]).val\n", "\n", " elif mapped[0] == \"Components\":\n", " return self.nw.get_comp(mapped[1]).get_attr(mapped[2]).val\n", "\n", " def set_parameters(self, **kwargs) -> None:\n", " input_dict = self._map_to_input_dict(**kwargs)\n", " if \"Connections\" in input_dict:\n", " for c, params in input_dict[\"Connections\"].items():\n", " self.nw.get_conn(c).set_attr(**params)\n", "\n", " if \"Components\" in input_dict:\n", " for c, params in input_dict[\"Components\"].items():\n", " self.nw.get_comp(c).set_attr(**params)\n", "\n", " def solve_model(self, **kwargs) -> None:\n", " self.set_parameters(**kwargs)\n", "\n", " self._solved = False\n", " self.nw.solve(\"design\")\n", "\n", " if self.nw.status == 0:\n", " self._solved = True\n", " # is not required in this example, but could lead to handling some\n", " # stuff\n", " elif self.nw.status == 1:\n", " self._solved = False\n", " elif self.nw.status in [2, 3, 99]:\n", " # in this case model is very likely corrupted!!\n", " # fix it by running a presolve using the stable solution\n", " self._solved = False\n", " self.nw.solve(\"design\", init_only=True, init_path=self._stable_solution)\n", "\n", " def plot_Ts_diagram(self) -> plt.Figure:\n", " fig, ax = plt.subplots(1)\n", " return fig, ax" ] }, { "cell_type": "markdown", "id": "5b72dbda", "metadata": {}, "source": [ "## Concrete model\n", "\n", "For example, let us build a model that implements an Organic Rankine Cycle\n", "with internal heat recuperator. We now only need to define the\n", "{code}`_parameter_lookup` method for the concrete model and set up the model in\n", "the {code}`_create_network` method. \n", "\n", "Next to setting up the structure of the model, that method has one main\n", "purpose: Make an initial (or multiple) solves, that guarantee a stable solution\n", "to start with for the future. For this reason, we start with a specification\n", "that is very simple to solve (pressure and temperatures given), solve the model\n", "and only then switch inputs to more usable ones (pinches, efficiencies etc.). \n", "We solve the model again and save the solution to a file, from where we can\n", "always reload the solution to initialize a future model run, when the starting\n", "values of a model run become corrupted (e.g. because of unexpected model\n", "crashes or model non-convergence)." ] }, { "cell_type": "code", "execution_count": null, "id": "4f9138b2", "metadata": {}, "outputs": [], "source": [ "from tespy.components import Turbine, MovingBoundaryHeatExchanger, CycleCloser, Pump, Source, Sink, Generator, PowerBus, PowerSink, Motor\n", "from tespy.connections import Connection, PowerConnection\n", "from tespy.tools import get_plotting_data\n", "\n", "\n", "class ORCModel(ModelTemplate):\n", "\n", " def _parameter_lookup(self) -> dict:\n", " return {\n", " \"evaporator_pinch\": [\"Components\", \"evaporator\", \"td_pinch\"],\n", " \"condenser_pinch\": [\"Components\", \"condenser\", \"td_pinch\"],\n", " \"turbine__efficiency\": [\"Components\", \"turbine\", \"eta_s\"],\n", " \"net_power\": [\"Connections\", \"e5\", \"E\"],\n", " \"T_source\": [\"Connections\", \"a1\", \"T\"],\n", " \"T_outflow\": [\"Connections\", \"a3\", \"T\"],\n", " \"m_source\": [\"Connections\", \"a1\", \"m\"]\n", " }\n", "\n", " def _create_network(self) -> None:\n", "\n", " super()._create_network()\n", "\n", " turbine = Turbine(\"turbine\")\n", " recuperator = MovingBoundaryHeatExchanger(\"recuperator\")\n", " condenser = MovingBoundaryHeatExchanger(\"condenser\")\n", " pump = Pump(\"pump\")\n", " preheater = MovingBoundaryHeatExchanger(\"preheater\")\n", " evaporator = MovingBoundaryHeatExchanger(\"evaporator\")\n", " cc = CycleCloser(\"cc\")\n", "\n", " heat_source = Source(\"heat source\")\n", " heat_outflow = Sink(\"heat outflow\")\n", "\n", " air_source = Source(\"air source\")\n", " air_sink = Sink(\"air sink\")\n", "\n", " a1 = Connection(heat_source, \"out1\", evaporator, \"in1\", label=\"a1\")\n", " a2 = Connection(evaporator, \"out1\", preheater, \"in1\", label=\"a2\")\n", " a3 = Connection(preheater, \"out1\", heat_outflow, \"in1\", label=\"a3\")\n", "\n", " b1 = Connection(cc, \"out1\", turbine, \"in1\", label=\"b1\")\n", " b2 = Connection(turbine, \"out1\", recuperator, \"in1\", label=\"b2\")\n", " b3 = Connection(recuperator, \"out1\", condenser, \"in1\", label=\"b3\")\n", " b4 = Connection(condenser, \"out1\", pump, \"in1\", label=\"b4\")\n", " b5 = Connection(pump, \"out1\", recuperator, \"in2\", label=\"b5\")\n", " b6 = Connection(recuperator, \"out2\", preheater, \"in2\", label=\"b6\")\n", " b7 = Connection(preheater, \"out2\", evaporator, \"in2\", label=\"b7\")\n", " b8 = Connection(evaporator, \"out2\", cc, \"in1\", label=\"b8\")\n", "\n", " c1 = Connection(air_source, \"out1\", condenser, \"in2\", label=\"c1\")\n", " c2 = Connection(condenser, \"out2\", air_sink, \"in1\", label=\"c2\")\n", "\n", " self.nw.add_conns(a1, a2, a3, b1, b2, b3, b4, b5, b6, b7, b8, c1, c2)\n", "\n", " generator = Generator(\"generator\")\n", " motor = Motor(\"motor\")\n", " power_bus = PowerBus(\"bus\", num_in=1, num_out=2)\n", " grid = PowerSink(\"grid\")\n", "\n", " e1 = PowerConnection(turbine, \"power\", generator, \"power_in\", label=\"e1\")\n", " e2 = PowerConnection(generator, \"power_out\", power_bus, \"power_in1\", label=\"e2\")\n", " e3 = PowerConnection(power_bus, \"power_out1\", motor, \"power_in\", label=\"e3\")\n", " e4 = PowerConnection(motor, \"power_out\", pump, \"power\", label=\"e4\")\n", " e5 = PowerConnection(power_bus, \"power_out2\", grid, \"power\", label=\"e5\")\n", "\n", " self.nw.add_conns(e1, e2, e3, e4, e5)\n", "\n", " generator.set_attr(eta=0.98)\n", " motor.set_attr(eta=0.98)\n", "\n", " a1.set_attr(fluid={\"air\": 1}, T=200, p=1, m=10)\n", " a2.set_attr(T=155)\n", "\n", " b1.set_attr(fluid={\"Isopentane\": 1}, x=1, T=150)\n", "\n", " b3.set_attr(td_dew=10, T_dew=30)\n", " b4.set_attr(td_bubble=5)\n", " b7.set_attr(td_bubble=5)\n", "\n", " c1.set_attr(fluid={\"air\": 1}, T=10, p=1)\n", " c2.set_attr(T=20)\n", "\n", " recuperator.set_attr(dp1=0, dp2=0)\n", " condenser.set_attr(dp1=0, dp2=0)\n", " preheater.set_attr(dp1=0, dp2=0)\n", " evaporator.set_attr(dp1=0, dp2=0)\n", "\n", " turbine.set_attr(eta_s=0.8)\n", " pump.set_attr(eta_s=0.7)\n", "\n", " self.nw.solve(\"design\")\n", "\n", " b3.set_attr(T_dew=None)\n", " condenser.set_attr(td_pinch=5)\n", "\n", " a2.set_attr(T=None)\n", " evaporator.set_attr(td_pinch=10)\n", "\n", " self.nw.solve(\"design\")\n", " self._stable_solution = \"stable_solution.json\"\n", " self.nw.save(self._stable_solution)\n", "\n", " def plot_Ts_diagram(self):\n", " fig, ax = super().plot_Ts_diagram()\n", "\n", " if \"Isopentane\" not in DIAGRAMS:\n", " # if not already available we will create one\n", " diagram = FluidPropertyDiagram(\"Isopentane\")\n", " # these are hard coded right now, but could be flexible of course\n", " diagram.set_unit_system(**{\"T\": \"°C\", \"p\": \"bar\"})\n", " diagram.set_isolines_subcritical(0, 250)\n", " diagram.calc_isolines()\n", " DIAGRAMS[\"Isopentane\"] = diagram\n", "\n", " diagram = DIAGRAMS[\"Isopentane\"]\n", " processes, points = get_plotting_data(self.nw, \"b1\")\n", "\n", " processes = {\n", " key: diagram.calc_individual_isoline(**value)\n", " for key, value in processes.items()\n", " if value is not None\n", "\n", " }\n", "\n", " diagram.draw_isolines(fig, ax, \"Ts\", -200, 1750, 0, 250)\n", "\n", " for label, values in processes.items():\n", " _ = ax.plot(values[\"s\"], values[\"T\"], label=label, color=\"tab:red\")\n", "\n", " for label, point in points.items():\n", " _ = ax.scatter(point[\"s\"], point[\"T\"], label=label, color=\"tab:red\")\n", "\n", " return fig, ax" ] }, { "cell_type": "markdown", "id": "3f2f5bf1", "metadata": {}, "source": [ "## Make use of the model\n", "\n", "With the model ready to use, we can set up an instance. We see two times\n", "solving iterations." ] }, { "cell_type": "code", "execution_count": null, "id": "37c8af7c", "metadata": { "execution": { "iopub.execute_input": "2025-10-15T15:12:01.506263Z", "iopub.status.busy": "2025-10-15T15:12:01.505892Z", "iopub.status.idle": "2025-10-15T15:12:02.006773Z", "shell.execute_reply": "2025-10-15T15:12:02.005856Z" } }, "outputs": [], "source": [ "model = ORCModel()" ] }, { "cell_type": "markdown", "id": "1e1c9aba", "metadata": {}, "source": [ "And then we can resolve it easily with updated input parameters:" ] }, { "cell_type": "code", "execution_count": null, "id": "613205a2", "metadata": { "execution": { "iopub.execute_input": "2025-10-15T15:12:02.011177Z", "iopub.status.busy": "2025-10-15T15:12:02.010703Z", "iopub.status.idle": "2025-10-15T15:12:02.178082Z", "shell.execute_reply": "2025-10-15T15:12:02.177550Z" } }, "outputs": [], "source": [ "model.solve_model(\n", " **{\"T_source\": 225}\n", ")" ] }, { "cell_type": "markdown", "id": "7cfdf7fb", "metadata": {}, "source": [ "And we can retrieve some results:" ] }, { "cell_type": "code", "execution_count": null, "id": "e2c74726", "metadata": {}, "outputs": [], "source": [ "model.get_parameter(\"T_outflow\")" ] }, { "cell_type": "code", "execution_count": null, "id": "36aa0917", "metadata": {}, "outputs": [], "source": [ "fig, ax = model.plot_Ts_diagram()" ] } ], "metadata": { "kernelspec": { "display_name": "tespy", "language": "python", "name": "python3" }, "language_info": { "codemirror_mode": { "name": "ipython", "version": 3 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.12.3" } }, "nbformat": 4, "nbformat_minor": 5 }