{ "cells": [ { "cell_type": "markdown", "id": "49245ef8", "metadata": {}, "source": [ "(model_sco2_label)=\n", "\n", "# Supercritical CO2 recompression cycle\n", "\n", "This is a model of a supercritical CO2 Brayton cycle with recompression and two\n", "recuperators providing 100 MW net power. The model is based on the model used\n", "validate the exergy analysis originally published in {cite}`Witte2022`. It is\n", "validated against the data of Penkuhn and Tsatsaronis {cite}`Penkuhn2018`. The\n", "original model is available in the\n", "[sCO2_exergy repository](https://github.com/fwitte/sCO2_exergy).\n", "\n", "```{image} /_static/images/tutorials/sco2/flowsheet.svg\n", ":align: center\n", ":class: only-light\n", ":width: 90%\n", "```\n", "\n", "```{image} /_static/images/tutorials/sco2/flowsheet_darkmode.svg\n", ":align: center\n", ":class: only-dark\n", ":width: 90%\n", "```\n", "\n", "On top of the model validating the data reported, the ModelTemplate is used to\n", "implement \n", "\n", "- a log(*p*)-*h* diagram of the process,\n", "- run sensitivity analyses on some parameters, and\n", "- apply a multi-objective optimization of efficiency and recuperator UA" ] }, { "cell_type": "markdown", "id": "e55af4fd", "metadata": {}, "source": [ "## Imports\n", "\n", "Next to the tespy imports we need the `FluidPropertyDiagram` from\n", "[fluprodia](https://fluprodia.readthedocs.io) and the `NSGA2` algorithm for\n", "multi-objective optimization from [pymoo](https://pymoo.org)." ] }, { "cell_type": "code", "execution_count": null, "id": "83b9c793", "metadata": {}, "outputs": [], "source": [ "import os\n", "import warnings\n", "\n", "import matplotlib.pyplot as plt\n", "import numpy as np\n", "import pandas as pd\n", "from fluprodia import FluidPropertyDiagram\n", "from pymoo.algorithms.moo.nsga2 import NSGA2\n", "\n", "from tespy.components import (\n", " Compressor, CycleCloser, Generator, HeatSource, Merge, Motor,\n", " PowerBus, PowerSink, SectionedHeatExchanger, SimpleHeatExchanger,\n", " Splitter, Turbine\n", ")\n", "from tespy.connections import Connection, HeatConnection, PowerConnection, Ref\n", "from tespy.models.template import ModelTemplate\n", "from tespy.networks import Network\n" ] }, { "cell_type": "markdown", "id": "966b949d", "metadata": {}, "source": [ "## The `SCO2Model` class\n", "\n", "The model class inherits from \n", "{py:class}`ModelTemplate `, which\n", "implements input and output parameter handling, solving, plotting diagrams and\n", "running sensitivity analyses and optimization.\n", "\n", "We need to implement these three methods:\n", "\n", "- `_create_network` to build the topology and apply the initial boundary\n", " conditions specifications.\n", "- `_parameter_lookup` to map names onto parameters in the network.\n", "- `solve_model` to let the optimizer and sensitivity analysis know, how to\n", " solve a model.\n", "\n", "`create_diagram` is overridden because the base class isolines do not go beyond\n", "200 °C while this cycle reaches 600 °C at 250 bar." ] }, { "cell_type": "code", "execution_count": null, "id": "73c3082e", "metadata": {}, "outputs": [], "source": [ "class SCO2Model(ModelTemplate):\n", " \"\"\"Supercritical CO2 power cycle with recompression and recuperators.\"\"\"\n", "\n", " # specification of ambient state\n", " pamb = 1.01325 # bar\n", " Tamb = 15 # °C\n", "\n", " def _create_network(self):\n", " self.nw = Network(iterinfo=False)\n", " self.nw.units.set_defaults(\n", " temperature=\"°C\",\n", " pressure=\"bar\",\n", " pressure_difference=\"bar\",\n", " enthalpy=\"kJ / kg\",\n", " entropy=\"kJ / kgK\",\n", " power=\"MW\",\n", " heat=\"MW\",\n", " heat_transfer_coefficient=\"MW/K\"\n", " )\n", "\n", " # components definition\n", " closer = CycleCloser(\"Cycle closer\")\n", "\n", " cp1 = Compressor(\"Compressor 1\")\n", " cp2 = Compressor(\"Compressor 2\")\n", "\n", " rec1 = SectionedHeatExchanger(\"Recuperator 1\", num_sections=10)\n", " rec2 = SectionedHeatExchanger(\"Recuperator 2\", num_sections=10)\n", "\n", " cooler = SimpleHeatExchanger(\"Water cooler\")\n", " heater = SimpleHeatExchanger(\"Heater\")\n", "\n", " turb = Turbine(\"Turbine\")\n", "\n", " sp1 = Splitter(\"Splitter 1\")\n", " m1 = Merge(\"Merge 1\")\n", "\n", " # connections definition\n", " # power cycle\n", " c1 = Connection(cooler, \"out1\", cp1, \"in1\", label=\"01\")\n", " c2 = Connection(cp1, \"out1\", rec1, \"in2\", label=\"02\")\n", " c3 = Connection(rec2, \"out2\", heater, \"in1\", label=\"03\")\n", "\n", " c0 = Connection(heater, \"out1\", closer, \"in1\", label=\"00\")\n", " c4 = Connection(closer, \"out1\", turb, \"in1\", label=\"04\")\n", " c5 = Connection(turb, \"out1\", rec2, \"in1\", label=\"05\")\n", " c6 = Connection(sp1, \"out1\", cooler, \"in1\", label=\"06\")\n", "\n", " c10 = Connection(sp1, \"out2\", cp2, \"in1\", label=\"10\")\n", " c11 = Connection(cp2, \"out1\", m1, \"in2\", label=\"11\")\n", " c12 = Connection(rec1, \"out2\", m1, \"in1\", label=\"12\")\n", " c13 = Connection(m1, \"out1\", rec2, \"in2\", label=\"13\")\n", "\n", " c14 = Connection(rec2, \"out1\", rec1, \"in1\", label=\"14\")\n", " c15 = Connection(rec1, \"out1\", sp1, \"in1\", label=\"15\")\n", "\n", " # add connections to network\n", " self.nw.add_conns(c0, c1, c2, c3, c4, c5, c6, c10, c11, c12, c13, c14, c15)\n", "\n", " # power system\n", " grid = PowerSink(\"grid\")\n", " distribution = PowerBus(\"distribution\", num_in=1, num_out=3)\n", " motor1 = Motor(\"motor 1\")\n", " motor2 = Motor(\"motor 2\")\n", " generator = Generator(\"generator\")\n", "\n", " e1 = PowerConnection(distribution, \"power_out3\", grid, \"power\", label=\"e1\")\n", "\n", " e2 = PowerConnection(distribution, \"power_out1\", motor1, \"power_in\", label=\"e2\")\n", " e3 = PowerConnection(motor1, \"power_out\", cp1, \"power\", label=\"e3\")\n", "\n", " e4 = PowerConnection(distribution, \"power_out2\", motor2, \"power_in\", label=\"e4\")\n", " e5 = PowerConnection(motor2, \"power_out\", cp2, \"power\", label=\"e5\")\n", "\n", " e6 = PowerConnection(turb, \"power\", generator, \"power_in\", label=\"e6\")\n", " e7 = PowerConnection(generator, \"power_out\", distribution, \"power_in1\", label=\"e7\")\n", "\n", " self.nw.add_conns(e1, e2, e3, e4, e5, e6, e7)\n", "\n", " # heat input\n", " heatsource = HeatSource(\"heatsource\")\n", " h1 = HeatConnection(heatsource, \"heat\", heater, \"heat\", label=\"h1\")\n", "\n", " self.nw.add_conns(h1)\n", "\n", " motor1.set_attr(eta=0.97 * 0.98)\n", " motor2.set_attr(eta=0.97 * 0.98)\n", " generator.set_attr(eta=0.99 * 0.99)\n", "\n", " # connection parameters\n", " c1.set_attr(T=35, p=75, fluid={\"CO2\": 1})\n", " c2.set_attr(p=258.4)\n", " c3.set_attr(p=257)\n", " c4.set_attr(T=600, p=250)\n", " c5.set_attr(p=77.95)\n", " c6.set_attr(p=75.15)\n", " c11.set_attr(p=257.51, T=Ref(c12, 1, 0))\n", " c14.set_attr(p=76.94)\n", "\n", " # component parameters\n", " turb.set_attr(eta_s=0.9)\n", " cp1.set_attr(eta_s=0.85)\n", " cp2.set_attr(eta_s=0.85)\n", " rec1.set_attr(ttd_l=5)\n", " rec2.set_attr(ttd_l=5)\n", "\n", " # net power output\n", " e1.set_attr(E=100)\n", "\n", " def _parameter_lookup(self):\n", " return {\n", " \"net power\": [\"Connections\", \"e1\", \"E\"],\n", " \"heat input\": [\"Connections\", \"h1\", \"E\"],\n", " \"turbine inlet temperature\": [\"Connections\", \"04\", \"T\"],\n", " \"turbine inlet pressure\": [\"Connections\", \"04\", \"p\"],\n", " \"compressor inlet temperature\": [\"Connections\", \"01\", \"T\"],\n", " \"compressor inlet pressure\": [\"Connections\", \"01\", \"p\"],\n", " \"turbine efficiency\": [\"Components\", \"Turbine\", \"eta_s\"],\n", " \"recuperator 1 pinch\": [\"Components\", \"Recuperator 1\", \"td_pinch\"],\n", " \"recuperator 2 pinch\": [\"Components\", \"Recuperator 2\", \"td_pinch\"],\n", " \"recuperator 1 ttd_l\": [\"Components\", \"Recuperator 1\", \"ttd_l\"],\n", " \"recuperator 2 ttd_l\": [\"Components\", \"Recuperator 2\", \"ttd_l\"],\n", " \"recuperator 1 UA\": [\"Components\", \"Recuperator 1\", \"UA\"],\n", " \"recuperator 2 UA\": [\"Components\", \"Recuperator 2\", \"UA\"],\n", " \"recuperator UA\": {\"get\": self._get_recuperator_total_UA},\n", " \"T recompressed in\": [\"Connections\", \"11\", \"T\"],\n", " \"T recuperated in\": [\"Connections\", \"12\", \"T\"],\n", " \"thermal efficiency\": {\"get\": self._calc_thermal_efficiency},\n", " \"split ratio\": {\"set\": self._set_split_ratio, \"get\": self._get_split_ratio},\n", " \"status\": {\"get\": self._get_status}\n", " }\n", "\n", " def _get_recuperator_total_UA(self):\n", " return (\n", " self.get_parameter(\"recuperator 1 UA\")\n", " + self.get_parameter(\"recuperator 2 UA\")\n", " )\n", "\n", " def _get_status(self):\n", " return self.nw.status\n", "\n", " def _calc_thermal_efficiency(self):\n", " return (\n", " self.nw.get_conn(\"e1\").E.val_SI\n", " / self.nw.get_conn(\"h1\").E.val_SI\n", " )\n", "\n", " def _set_split_ratio(self, value):\n", " m = None\n", " c10, c15 = self.nw.get_conn([\"10\", \"15\"])\n", " if value is not None:\n", " m=Ref(c15, value, 0)\n", " c10.set_attr(m=m)\n", "\n", " def _get_split_ratio(self):\n", " c10, c15 = self.nw.get_conn([\"10\", \"15\"])\n", " return c10.m.val / c15.m.val\n", "\n", " def create_diagram(self, fluid_name):\n", " \"\"\"Fluid property diagram with isolines suited for the cycle.\n", "\n", " The default isoline range of the base class ends at 200 °C and\n", " slightly above the critical pressure. The cycle however reaches\n", " turbine inlet temperatures of 600 °C and pressures of more than\n", " 250 bar, so the temperature range and the pressure isolines are\n", " extended (including the internal iteration bound p_max, which the\n", " isotherms and isochores are calculated up to).\n", " \"\"\"\n", " diagram = FluidPropertyDiagram(fluid_name)\n", " diagram.set_unit_system(self.nw.units)\n", " diagram.set_isolines_subcritical(-20, 650)\n", " diagram.set_isolines(\n", " p=np.array([1, 5, 10, 25, 50, 100, 150, 200, 250, 300])\n", " )\n", " diagram.p_max = 300e5\n", " diagram.calc_isolines()\n", " return diagram\n", "\n", " def solve_model(self, **kwargs):\n", " self.solve_model_design(**kwargs)\n", "\n", " def solve_design(self):\n", " self.nw.solve(mode=\"design\")\n", " self.nw.assert_convergence()\n", "\n", " def run_exergy_analysis(self):\n", " from exerpy import ExergyAnalysis\n", " E_F = {\"inputs\": [\"h1\"], \"outputs\": []}\n", " E_P = {\"inputs\": [\"e1\"], \"outputs\": []}\n", " self._ean = ExergyAnalysis.from_tespy(\n", " self.nw, self.Tamb + 273.15, self.pamb * 1e5\n", " )\n", " # the cooler rejects heat to the ambient without a material cooling\n", " # stream; it is a dissipative component (as in the publication), its\n", " # exergy input is destroyed rather than delivered as a product\n", " self._ean.components[\"Water cooler\"].dissipative = True\n", " self._ean.analyse(E_F=E_F, E_P=E_P)\n", " return self._ean\n" ] }, { "cell_type": "markdown", "id": "d1fd4782", "metadata": {}, "source": [ "## Design simulation\n", "\n", "Instantiating the class builds the network once, `solve_model` runs the design\n", "simulation. The net power output is imposed at 100 MW." ] }, { "cell_type": "code", "execution_count": null, "id": "7fd57f09", "metadata": {}, "outputs": [], "source": [ "model = SCO2Model()\n", "model.solve_model()\n", "\n", "indicators = [\n", " \"net power\", \"heat input\", \"thermal efficiency\",\n", " \"turbine inlet temperature\", \"turbine inlet pressure\",\n", " \"compressor inlet temperature\", \"compressor inlet pressure\",\n", "]\n", "pd.Series(model.get_results(indicators), name=\"design point\").round(4)" ] }, { "cell_type": "markdown", "id": "f1c0c414", "metadata": {}, "source": [ "## Validation \n", "\n", "The reference publication proves temperature, pressure as well as thermal and\n", "mechanical specific exergies for every connection. All deviations are close to\n", "zero on the state variables. The mechanical exergy has the largest relative\n", "deviation, which comes from a different reference state implementation rather\n", "than from the cycle model." ] }, { "cell_type": "code", "execution_count": null, "id": "873fd67a", "metadata": {}, "outputs": [], "source": [ "published = pd.DataFrame(\n", " # label: T in °C, p in bar, e_T in kJ/kg, e_M in kJ/kg\n", " [[ 1, 35.00, 75.00, 7.59, 198.46],\n", " [ 2, 123.26, 258.40, 34.86, 218.09],\n", " [ 3, 433.63, 257.00, 231.28, 217.95],\n", " [ 4, 600.00, 250.00, 362.76, 217.23],\n", " [ 5, 456.95, 77.95, 213.80, 198.80],\n", " [ 6, 128.26, 75.15, 33.56, 198.48],\n", " [ 10, 128.26, 75.15, 33.56, 198.48],\n", " [ 11, 264.11, 257.51, 116.43, 218.00],\n", " [ 12, 264.11, 257.51, 116.43, 218.00],\n", " [ 13, 264.11, 257.51, 116.43, 218.00],\n", " [ 14, 269.11, 76.94, 95.93, 198.68],\n", " [ 15, 128.26, 75.15, 33.56, 198.48]],\n", " columns=[\"label\", \"T\", \"p\", \"e_T\", \"e_M\"],\n", ").set_index(\"label\")\n", "\n", "tespy = pd.DataFrame({\n", " \"T\": {int(c.label): c.T.val for c in model.nw.conns[\"object\"]\n", " if c.label.isdigit()},\n", " \"p\": {int(c.label): c.p.val for c in model.nw.conns[\"object\"]\n", " if c.label.isdigit()},\n", "}).sort_index()\n", "\n", "comparison = published[[\"T\", \"p\"]].join(\n", " tespy, lsuffix=\" published\", rsuffix=\" tespy\"\n", ")\n", "comparison[\"ΔT in K\"] = comparison[\"T tespy\"] - comparison[\"T published\"]\n", "comparison[\"δp in %\"] = (\n", " (comparison[\"p tespy\"] - comparison[\"p published\"])\n", " / comparison[\"p published\"] * 100\n", ")\n", "comparison.round(3)" ] }, { "cell_type": "markdown", "id": "b00de688", "metadata": {}, "source": [ "## Ts diagram\n", "\n", "We can make the cycle plot in the Ts diagram:" ] }, { "cell_type": "code", "execution_count": null, "id": "090499fe", "metadata": {}, "outputs": [], "source": [ "fig, ax = model.plot_Ts_diagram_matplotlib(\"01\")" ] }, { "cell_type": "markdown", "id": "25e8a6f9", "metadata": {}, "source": [ "## Sensitivity analysis\n", "\n", "At the example of the paramters below a sensitivity analysis is carried out:\n", "\n", "| parameter | design value | range |\n", "|---|---|---|\n", "| turbine inlet temperature | 600 °C | 450 - 700 °C |\n", "| compressor inlet temperature | 35 °C | 32 - 45 °C |\n", "| compressor inlet pressure | 75 bar | 70 - 75 bar |" ] }, { "cell_type": "code", "execution_count": null, "id": "33459b53", "metadata": {}, "outputs": [], "source": [ "# keep build time for docs low\n", "n = 6 if os.getenv(\"TESPY_DOCS_BUILD\") else 13\n", "\n", "tit = model.sensitivity_analysis(\n", " param_dict={\"turbine inlet temperature\": np.linspace(450, 700, n)},\n", " result_param_list=[\"thermal efficiency\", \"heat input\"],\n", ")\n", "cit = model.sensitivity_analysis(\n", " param_dict={\"compressor inlet temperature\": np.linspace(32, 45, n)},\n", " result_param_list=[\"thermal efficiency\"],\n", ")\n", "cip = model.sensitivity_analysis(\n", " param_dict={\"compressor inlet pressure\": np.linspace(70, 75, n)},\n", " result_param_list=[\"thermal efficiency\"],\n", ")\n", "# solve_model() re-solves with whatever parameters are currently set,\n", "# so the design values are restated rather than assumed\n", "model.solve_model(**{'turbine inlet temperature': 600, 'compressor inlet temperature': 35, 'compressor inlet pressure': 75})" ] }, { "cell_type": "markdown", "id": "b1ec54c7", "metadata": {}, "source": [ "The turbine inlet temperature has the stronges effect on efficiency. The \n", "compressor inlet conditions also have a quite high effect, mostly due to the\n", "fact, that the fluid near to the critical point, where the density is high and\n", "the compression work small. Moving away from that point reduces efficiency." ] }, { "cell_type": "code", "execution_count": null, "id": "cd218eac", "metadata": {}, "outputs": [], "source": [ "fig, ax = plt.subplots(1, 3, figsize=(12, 3.6))\n", "ax[0].plot(tit[\"turbine inlet temperature\"], tit[\"thermal efficiency\"], \"o-\", color=\"tab:red\")\n", "ax[0].set_xlabel(\"turbine inlet temperature in °C\")\n", "ax[1].plot(cit[\"compressor inlet temperature\"], cit[\"thermal efficiency\"], \"o-\", color=\"tab:blue\")\n", "ax[1].set_xlabel(\"compressor inlet temperature in °C\")\n", "ax[1].axvline(31.0, ls=\"--\", color=\"gray\", lw=1)\n", "ax[1].annotate(\n", " \"critical\\ntemperature\", (31.0, cit[\"thermal efficiency\"].min()),\n", " textcoords=\"offset points\", xytext=(6, 4), fontsize=8, color=\"gray\"\n", ")\n", "ax[2].plot(cip[\"compressor inlet pressure\"], cip[\"thermal efficiency\"], \"o-\", color=\"tab:green\")\n", "ax[2].set_xlabel(\"compressor inlet pressure in bar\")\n", "ax[2].axvline(73.8, ls=\"--\", color=\"gray\", lw=1)\n", "ax[2].annotate(\n", " \"critical\\npressure\", (73.8, cip[\"thermal efficiency\"].min()),\n", " textcoords=\"offset points\", xytext=(-38, 4), fontsize=8, color=\"gray\"\n", ")\n", "for a in ax:\n", " a.set_ylabel(\"thermal efficiency\")\n", " a.grid(alpha=.3)\n", "plt.tight_layout()\n", "plt.close(fig)\n", "fig" ] }, { "cell_type": "markdown", "id": "630d749b", "metadata": {}, "source": [ "## Changing the split ratio\n", "\n", "The original data from the reference publication assume a specific split ratio\n", "of the mass flow between the compressor and the recomplressor. This is done\n", "implicitly through the temperature reference of connection 11. We can run a\n", "sensitivity analysis over this ratio:" ] }, { "cell_type": "code", "execution_count": null, "id": "c87d318a", "metadata": {}, "outputs": [], "source": [ "c10, c11, c15 = (model.nw.get_conn(x) for x in (\"10\", \"11\", \"15\"))\n", "c11.set_attr(T=None) # release the matched merge temperature\n", "\n", "result = model.sensitivity_analysis(\n", " param_dict={\"split ratio\": np.linspace(0.15, 0.30, 6)},\n", " result_param_list=[\"thermal efficiency\", \"T recompressed in\", \"T recuperated in\", \"status\"],\n", ")\n", "\n", "result.set_index(\"split ratio\").round(\n", " {\"thermal efficiency\": 3, \"status\": 0,\n", " \"T recompressed in\": 1, \"T recuperated in\": 1}\n", ")\n" ] }, { "cell_type": "markdown", "id": "bec29763", "metadata": {}, "source": [ "We monitor the highest efficiency at the highest split ratio, but we also see\n", "that the solution for that state is impossible. `status==1` indicates a \n", "vioalation of what is physically possible. In this case, the recuperator 1 has\n", "a crossing of the two temperature profiles. We can prevent this, by imposing\n", "the minimum pinch instead of the terminal temperature differences." ] }, { "cell_type": "code", "execution_count": null, "id": "8ed8a4e8", "metadata": {}, "outputs": [], "source": [ "model.solve_model(**{\"split ratio\": 0.3})\n", "pd.DataFrame({\n", " label: {\n", " \"ttd upper in K\": model.nw.get_comp(label).ttd_u.val,\n", " \"ttd lower in K\": model.nw.get_comp(label).ttd_l.val,\n", " \"pinch in K\": model.nw.get_comp(label).td_pinch.val,\n", " \"UA in MW/K\": model.nw.get_comp(label).UA.val,\n", " }\n", " for label in (\"Recuperator 1\", \"Recuperator 2\")\n", "}).round(3)" ] }, { "cell_type": "markdown", "id": "aa3df748", "metadata": {}, "source": [ "We can also plot the temperature profiles:" ] }, { "cell_type": "code", "execution_count": null, "id": "d667e153", "metadata": {}, "outputs": [], "source": [ "fig, ax = plt.subplots(1, 2, figsize=(11, 4))\n", "\n", "model.plot_QT_diagram_matplotlib(\"Recuperator 1\", ax[0])\n", "model.plot_QT_diagram_matplotlib(\"Recuperator 2\", ax[1])\n", "for a in ax:\n", " a.grid(alpha=.3)\n", "plt.tight_layout()\n", "plt.close(fig)\n", "fig" ] }, { "cell_type": "markdown", "id": "e0e9a5b4", "metadata": {}, "source": [ "So instead we switch the specifications to make use of `td_pinch` instead." ] }, { "cell_type": "code", "execution_count": null, "id": "26641000", "metadata": {}, "outputs": [], "source": [ "model.set_parameters(**{\n", " \"recuperator 1 ttd_l\": None, \"recuperator 2 ttd_l\": None,\n", " \"recuperator 1 pinch\": 5, \"recuperator 2 pinch\": 5\n", "})\n", "\n", "result = model.sensitivity_analysis(\n", " param_dict={\"split ratio\": np.linspace(0.15, 0.30, 6)},\n", " result_param_list=[\"thermal efficiency\", \"T recompressed in\", \"T recuperated in\", \"status\"],\n", ")\n", "\n", "result.set_index(\"split ratio\").round(\n", " {\"thermal efficiency\": 3, \"status\": 0,\n", " \"T recompressed in\": 1, \"T recuperated in\": 1}\n", ")\n" ] }, { "cell_type": "code", "execution_count": null, "id": "5e2ae3e9", "metadata": {}, "outputs": [], "source": [ "fig, ax = plt.subplots(1, 2, figsize=(11, 4))\n", "\n", "model.plot_QT_diagram_matplotlib(\"Recuperator 1\", ax[0])\n", "model.plot_QT_diagram_matplotlib(\"Recuperator 2\", ax[1])\n", "ax[0].set_title(\"Recuperator 1\")\n", "ax[1].set_title(\"Recuperator 2\")\n", "for a in ax:\n", " a.grid(alpha=.3)\n", "plt.tight_layout()\n", "plt.close(fig)\n", "fig" ] }, { "cell_type": "markdown", "id": "e458da4e", "metadata": {}, "source": [ "### Recuperator size analysis\n", "\n", "Instead of varying the split ratio, we carry out a sensitivity analysis on the\n", "sizing of the recuperators by imposing a lower or higher pinch value. For this\n", "we bring back the original split ratio defined by temperature equality and \n", "impose the lower terminal temperature difference. We look at recuperator 1\n", "only at first:" ] }, { "cell_type": "code", "execution_count": null, "id": "c45771a4", "metadata": {}, "outputs": [], "source": [ "model.set_parameters(\n", " **{\"split ratio\": None, \"recuperator 1 pinch\": None, \"recuperator 1 ttd_l\": 5}\n", ")\n", "c11, c12 = model.nw.get_conn([\"11\", \"12\"])\n", "c11.set_attr(T=Ref(c12, 1, 0))\n", "model.solve_model()\n", "\n", "n = 6 if os.getenv(\"TESPY_DOCS_BUILD\") else 12\n", "\n", "sizing = model.sensitivity_analysis(\n", " param_dict={\"recuperator 1 ttd_l\": np.linspace(2, 18, n)},\n", " result_param_list=[\"thermal efficiency\", \"recuperator 1 UA\"],\n", ")\n", "\n", "fig, ax = plt.subplots(1, 2, figsize=(11, 3.8))\n", "ax[0].plot(sizing[\"recuperator 1 ttd_l\"], sizing[\"thermal efficiency\"], \"o-\", color=\"tab:blue\")\n", "ax[0].set_xlabel(\"recuperator 1 terminal temperature difference in K\")\n", "ax[0].set_ylabel(\"thermal efficiency\")\n", "ax[1].plot(sizing[\"recuperator 1 UA\"], sizing[\"thermal efficiency\"], \"o-\", color=\"tab:red\")\n", "ax[1].set_xlabel(\"recuperator 1 UA in MW/K\")\n", "ax[1].set_ylabel(\"thermal efficiency\")\n", "for a in ax:\n", " a.grid(alpha=.3)\n", "plt.tight_layout()\n", "plt.close(fig)\n", "fig" ] }, { "cell_type": "markdown", "id": "43adcc64", "metadata": {}, "source": [ "We can see, that the efficiency gain is reduced with an increae in the\n", "recuperator's heat transfer coefficient, meaning a low temeprature difference\n", "increases the necessary area of the heat exchanger by a lot." ] }, { "cell_type": "markdown", "id": "c40d4da4", "metadata": {}, "source": [ "### Optimizing the two recuperators \n", "\n", "After the sensitivity analysis we optimize both heat exchangers simultaneously.\n", "We want to achieve high efficiency and low overall UA at the same time. With\n", "the NSGA-II algorithm we can make a pareto optimization.\n", "\n", "```{note}\n", "The generation count is reduced during a documentation build. Run the notebook\n", "interactively for a complete output.\n", "```" ] }, { "cell_type": "code", "execution_count": null, "id": "fab16eea", "metadata": {}, "outputs": [], "source": [ "n_gen = 3 if os.getenv(\"TESPY_DOCS_BUILD\") else 25\n", "\n", "model.set_parameters(**{\"recuperator 2 pinch\": None})\n", "log, result = model.optimize(\n", " algorithm=NSGA2(pop_size=24),\n", " termination=(\"n_gen\", n_gen),\n", " variables={\n", " \"recuperator 1 ttd_l\": {\"min\": 2, \"max\": 18},\n", " \"recuperator 2 ttd_l\": {\"min\": 2, \"max\": 18},\n", " },\n", " objective=[\"thermal efficiency\", \"recuperator UA\"],\n", " minimize_flags=[False, True],\n", " kpi=[\"heat input\"],\n", ")\n", "feasible = log.dropna(subset=[\"thermal efficiency\", \"recuperator UA\"])\n", "feasible.sort_values(\"thermal efficiency\", ascending=False).head().round(3)" ] }, { "cell_type": "code", "execution_count": null, "id": "aa3fb1c8", "metadata": {}, "outputs": [], "source": [ "fig, ax = plt.subplots(figsize=(10.5, 5.8))\n", "sc = ax.scatter(\n", " feasible[\"recuperator UA\"], feasible[\"thermal efficiency\"],\n", " c=feasible[\"recuperator 1 ttd_l\"], cmap=\"viridis\", s=26,\n", ")\n", "fig.colorbar(sc, label=\"recuperator 1 ttd in K\")\n", "ax.set_xlabel(\"recuperator combined UA in MW/K\")\n", "ax.set_ylabel(\"thermal efficiency\")\n", "ax.grid(alpha=.3)\n", "plt.close(fig)\n", "fig" ] } ], "metadata": { "kernelspec": { "display_name": "tespy (3.14.6.final.0)", "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.14.6" } }, "nbformat": 4, "nbformat_minor": 5 }