{ "cells": [ { "cell_type": "markdown", "id": "5dc2f305", "metadata": {}, "source": [ "(tutorial_co2_refrigeration_label)=\n", "\n", "# CO2 refrigeration system\n", "\n", "This notebook models a CO2 transcritical booster refrigeration system of the\n", "kind used in supermarkets. The model is built with\n", "[TESPy](https://tespy.readthedocs.io/) and [CoolProp](http://www.coolprop.org/)\n", "for the R744 (CO2) property data.\n", "\n", "The booster-cycle topology, component set, and boundary conditions used here\n", "(evaporator pressures and cooling loads, compressor isentropic efficiencies,\n", "gas cooler pressure and outlet temperature, receiver pressure) are taken from:\n", "\n", "> Francesco D\"Ettorre, Christian Heerup, *\"Chapter 5 - Modelling of a commercial refrigeration system in Python\"*,\n", "> Case Studies in Energy Systems, Elsevier, 2026, Pages 163-189, ISBN 9780443238635, https://doi.org/10.1016/B978-0-443-23863-5.00010-5.\n", "\n", "The model is implemented as a subclass of tespy\"s \n", "[ModelTemplate](https://github.com/oemof/tespy/blob/dev/src/tespy/models/template.py)\n", "class, which provides the parameter handling, solving with automatic recovery,\n", "fluid property diagram plotting, sensitivity analysis and optimization\n", "plumbing. On top of the case study itself, the notebook shows how to\n", "\n", "- run the subcritical baseline case and inspect performance indicators,\n", "- plot cycle and heat exchanger TQ diagrams,\n", "- switch to transcritical operation at high ambient temperature,\n", "- run a sensitivity analysis on the gas cooler pressure to visualize the COP\n", " trade-off in transcritical operation, and\n", "- find the COP-optimal combination of gas cooler and receiver pressure with\n", " pymoo.\n", "\n", "\n", "The flowsheet of the CO2 system is shown below.\n", "\n", "```{image} /_static/images/tutorials/co2_booster/flowsheet.svg\n", ":align: center\n", ":class: only-light\n", ":width: 80%\n", "```\n", "\n", "```{image} /_static/images/tutorials/co2_booster/flowsheet_darkmode.svg\n", ":align: center\n", ":class: only-dark\n", ":width: 80%\n", "```" ] }, { "cell_type": "markdown", "id": "85f2a94e", "metadata": {}, "source": [ "## Imports\n", "\n", "Everything model related comes from tespy: the components and connections to\n", "build the network and the `ModelTemplate` base class from `tespy.models`.\n", "`FluidPropertyDiagram` from [fluprodia](https://fluprodia.readthedocs.io) draws\n", "the background isolines of the log(*p*)-*h* diagrams and `PSO` (particle swarm\n", "optimization) from [pymoo](https://pymoo.org) is the optimization algorithm\n", "used later." ] }, { "cell_type": "code", "execution_count": null, "id": "4b3eb72a", "metadata": {}, "outputs": [], "source": [ "import os\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.soo.nonconvex.pso import PSO\n", "\n", "from tespy.components import (\n", " Sink, Source, SimpleHeatExchanger, CycleCloser, Valve, Compressor,\n", " DropletSeparator, Merge, Splitter, SectionedHeatExchanger\n", ")\n", "from tespy.connections import Connection\n", "from tespy.models import ModelTemplate\n", "from tespy.networks import Network" ] }, { "cell_type": "markdown", "id": "6d118ad0", "metadata": {}, "source": [ "## The `CO2BoosterModel` class\n", "\n", "`ModelTemplate` requires the implementation of a small number of methods:\n", "\n", "- `_create_network` builds the topology, applies initial boundary conditions\n", " and solves the model once, so that a stable solution is available. The base\n", " class falls back to that solution automatically whenever a later solve\n", " corrupts the model.\n", "- `_parameter_lookup` maps flat, human readable parameter names to their\n", " location in the network, e.g. `[\"Connections\", \"c10\", \"p\"]`. These names are\n", " the single interface used by `set_parameters`, `get_parameter`, the\n", " sensitivity analysis and the optimizer.\n", "- `solve_model` is the entry point the optimizer calls. In this example it\n", " simply runs a design simulation.\n", "\n", "Besides the settable parameters, the lookup also contains read-only indicators\n", "using the `{\"get\": callable}` form. The coefficient of performance relates the\n", "two cooling loads to the total compression power\n", "\n", "```{math}\n", "\\mathrm{COP} = \\frac{\\dot{Q}_\\mathrm{evap,LT} + \\dot{Q}_\\mathrm{evap,MT}}{P_\\mathrm{comp,LT} + P_\\mathrm{comp,MT}}\n", "```\n", "\n", "and the flash gas fraction is the share of vapor bypassed from the receiver in\n", "the total high pressure valve mass flow\n", "\n", "```{math}\n", "x_\\mathrm{flash} = \\frac{\\dot{m}_\\mathrm{c16}}{\\dot{m}_\\mathrm{c3}}.\n", "```\n", "\n", "Both getters return `nan` if the last solve did not converge. The optimizer\n", "uses this information to penalize such objectives automatically, so failed\n", "evaluations are discarded.\n", "\n", "Finally, `_get_diagram` overrides the diagram setup of the base class. The\n", "default isolines only cover subcritical states, but the CO2 cycle crosses the\n", "critical point, so temperature isolines up to 150 °C and vapor quality isolines\n", "are calculated instead." ] }, { "cell_type": "code", "execution_count": null, "id": "8893e11d", "metadata": {}, "outputs": [], "source": [ "class CO2BoosterModel(ModelTemplate):\n", "\n", " def _parameter_lookup(self) -> dict:\n", " return {\n", " \"lt_evaporator_load_kW\": [\"Components\", \"lt-evaporator\", \"Q\"],\n", " \"mt_evaporator_load_kW\": [\"Components\", \"mt-evaporator\", \"Q\"],\n", " \"lt_compressor_isentropic_eff\": [\"Components\", \"lt-compressor\", \"eta_s\"],\n", " \"mt_compressor_isentropic_eff\": [\"Components\", \"mt-compressor\", \"eta_s\"],\n", " \"lt_evaporator_pressure_bar\": [\"Connections\", \"c6\", \"p\"],\n", " \"mt_evaporator_pressure_bar\": [\"Connections\", \"c13\", \"p\"],\n", " \"receiver_pressure_bar\": [\"Connections\", \"c3\", \"p\"],\n", " \"gas_cooler_pressure_bar\": [\"Connections\", \"c10\", \"p\"],\n", " \"gas_cooler_outlet_temperature_C\": [\"Connections\", \"c1\", \"T\"],\n", " \"gas_cooler_discharge_kW\": [\"Components\", \"gas-cooler\", \"Q\"],\n", " \"hrhx_water_inlet_temperature_C\": [\"Connections\", \"c11\", \"T\"],\n", " \"mt_compressor_power_kW\": [\"Components\", \"mt-compressor\", \"P\"],\n", " \"lt_compressor_power_kW\": [\"Components\", \"lt-compressor\", \"P\"],\n", " \"hrhx_heat_kW\": [\"Components\", \"heat-recovery-heat-exchanger\", \"Q\"],\n", " \"discharge_temperature_C\": [\"Connections\", \"c10\", \"T\"],\n", " \"COP\": {\"get\": self._calc_cop},\n", " \"total_compressor_power_kW\": {\"get\": self._calc_total_power},\n", " \"flash_gas_fraction\": {\"get\": self._calc_flash_gas_fraction},\n", " }\n", "\n", " def _calc_cop(self) -> float:\n", " if not self._solved:\n", " return np.nan\n", " Q_total = (\n", " self.get_parameter(\"lt_evaporator_load_kW\")\n", " + self.get_parameter(\"mt_evaporator_load_kW\")\n", " - self.get_parameter(\"hrhx_heat_kW\")\n", " )\n", " return Q_total / self._calc_total_power()\n", "\n", " def _calc_total_power(self) -> float:\n", " if not self._solved:\n", " return np.nan\n", " return (\n", " self.get_parameter(\"lt_compressor_power_kW\")\n", " + self.get_parameter(\"mt_compressor_power_kW\")\n", " )\n", "\n", " def _calc_flash_gas_fraction(self) -> float:\n", " if not self._solved:\n", " return np.nan\n", " return self.nw.get_conn(\"c16\").m.val / self.nw.get_conn(\"c3\").m.val\n", "\n", " def solve_model(self, **kWargs) -> None:\n", " self.solve_model_design(**kWargs)\n", "\n", " def _get_diagram(self, fluid_name):\n", " if fluid_name not in self._diagram_cache:\n", " diagram = FluidPropertyDiagram(fluid_name)\n", " diagram.set_unit_system(self.nw.units)\n", " diagram.set_isolines(\n", " T=np.arange(-50, 160, 10),\n", " p=np.array([]),\n", " h=np.array([]),\n", " s=np.array([]),\n", " vol=np.array([]),\n", " Q=np.linspace(0, 1, 11)\n", " )\n", " diagram.calc_isolines()\n", " self._diagram_cache[fluid_name] = diagram\n", " return self._diagram_cache[fluid_name]\n", "\n", " def _create_network(self) -> None:\n", " self.nw = Network(iterinfo=False)\n", " self.nw.units.set_defaults(\n", " temperature=\"°C\", pressure=\"bar\", pressure_difference=\"bar\",\n", " enthalpy=\"kJ/kg\", heat=\"kW\", power=\"kW\"\n", " )\n", "\n", " # === System components =========================================\n", " gas_cooler = SimpleHeatExchanger(\"gas-cooler\")\n", " cc = CycleCloser(\"cc\")\n", " hp_valve = Valve(\"high-pressure-valve\")\n", " bp_valve = Valve(\"by-pass-valve\")\n", " mt_exp_valve = Valve(\"mt-exp-valve\")\n", " lt_exp_valve = Valve(\"lt-exp-valve\")\n", " mt_evap = SimpleHeatExchanger(\"mt-evaporator\")\n", " lt_evap = SimpleHeatExchanger(\"lt-evaporator\")\n", " mt_comp = Compressor(\"mt-compressor\")\n", " lt_comp = Compressor(\"lt-compressor\")\n", " merge1 = Merge(\"mt-evaporator-outlet\")\n", " merge2 = Merge(\"mt-compressor-suction\")\n", " split = Splitter(\"receiver-liq-outlet\")\n", " receiver = DropletSeparator(\"receiver\")\n", " hrhx = SectionedHeatExchanger(\"heat-recovery-heat-exchanger\")\n", " water_inlet = Source(\"water-inlet\")\n", " water_outlet = Sink(\"water-outlet\")\n", "\n", " # === Connections ===============================================\n", " w1 = Connection(water_inlet, \"out1\", hrhx, \"in2\", \"w1\")\n", " w2 = Connection(hrhx, \"out2\", water_outlet, \"in1\", \"w2\")\n", "\n", " c1 = Connection(gas_cooler, \"out1\", cc, \"in1\", \"c1\")\n", " c2 = Connection(cc, \"out1\", hp_valve, \"in1\", \"c2\")\n", " c3 = Connection(hp_valve, \"out1\", receiver, \"in1\", \"c3\")\n", " c4 = Connection(receiver, \"out1\", split, \"in1\", \"c4\")\n", " c5 = Connection(split, \"out1\", lt_exp_valve, \"in1\", \"c5\")\n", " c6 = Connection(lt_exp_valve, \"out1\", lt_evap, \"in1\", \"c6\")\n", " c7 = Connection(lt_evap, \"out1\", lt_comp, \"in1\", \"c7\")\n", " c8 = Connection(lt_comp, \"out1\", merge2, \"in1\", \"c8\")\n", " c9 = Connection(merge2, \"out1\", mt_comp, \"in1\", \"c9\")\n", " c10 = Connection(mt_comp, \"out1\", hrhx, \"in1\", \"c10\")\n", " c11 = Connection(hrhx, \"out1\", gas_cooler, \"in1\", \"c11\")\n", " c12 = Connection(split, \"out2\", mt_exp_valve, \"in1\", \"c12\")\n", " c13 = Connection(mt_exp_valve, \"out1\", mt_evap, \"in1\", \"c13\")\n", " c14 = Connection(mt_evap, \"out1\", merge1, \"in1\", \"c14\")\n", " c15 = Connection(merge1, \"out1\", merge2, \"in2\", \"c15\")\n", " c16 = Connection(receiver, \"out2\", bp_valve, \"in1\", \"c16\")\n", " c17 = Connection(bp_valve, \"out1\", merge1, \"in2\", \"c17\")\n", "\n", " self.nw.add_conns(\n", " c1, c2, c3, c4, c5, c6, c7, c8, c9, c10, c11, c12, c13, c14,\n", " c15, c16, c17, w1, w2\n", " )\n", "\n", " # === Component parametrisation ==================================\n", " gas_cooler.set_attr(dp=0)\n", " hrhx.set_attr(dp1=0, dp2=0, td_pinch=5)\n", " mt_comp.set_attr(eta_s=.8)\n", " lt_comp.set_attr(eta_s=.85)\n", " mt_evap.set_attr(pr=1, Q=75)\n", " lt_evap.set_attr(pr=1, Q=25)\n", "\n", " c1.set_attr(T=15, fluid={\"R744\": 1})\n", " c3.set_attr(p=32)\n", " c6.set_attr(p=13)\n", " c13.set_attr(p=26)\n", " c10.set_attr(p=57.3)\n", " c7.set_attr(td_dew=10)\n", " c14.set_attr(td_dew=10)\n", "\n", " w1.set_attr(T=30, p=3, fluid={\"water\": 1})\n", " w2.set_attr(T=55)\n", "\n", " # === Solve ======================================================\n", " self.nw.solve(\"design\")\n", "\n", " self._solved = self.nw.status == 0\n", " self._stable_solution = self.nw.save(as_dict=True)" ] }, { "cell_type": "markdown", "id": "ce8074ce", "metadata": {}, "source": [ "## Case Study 1 - subcritical baseline\n", "\n", "The first run uses the operating conditions of **Case Study 1** from the book\n", "chapter:\n", "\n", "| Parameter | Value |\n", "|---|---|\n", "| LT evaporator pressure | 13 bar (≈ -33 °C) |\n", "| MT evaporator pressure | 26 bar (≈ -10 °C) |\n", "| Receiver pressure | 32 bar |\n", "| Gas cooler pressure | 57.3 bar |\n", "| Gas cooler outlet temperature | 15 °C (5 °C subcooling) |\n", "| LT cooling load | 25 kW |\n", "| MT cooling load | 75 kW |\n", "| LT compressor isentropic efficiency | 0.85 |\n", "| MT compressor isentropic efficiency | 0.80 |\n", "\n", "These match the defaults hardcoded in `_create_network`, so passing them\n", "through `solve_model` here is intended to show how values enter the model\n", "through the flat parameter interface.\n", "\n", "After the solve, `get_results` collects the performance indicators defined in\n", "the lookup. Note that the heat recovery duty is negative by TESPy sign\n", "convention." ] }, { "cell_type": "code", "execution_count": null, "id": "1e41bbf3", "metadata": {}, "outputs": [], "source": [ "case_study_1 = {\n", " \"lt_evaporator_pressure_bar\": 13,\n", " \"mt_evaporator_pressure_bar\": 26,\n", " \"receiver_pressure_bar\": 32,\n", " \"gas_cooler_pressure_bar\": 57.3,\n", " \"gas_cooler_outlet_temperature_C\": 15,\n", " \"lt_evaporator_load_kW\": 25,\n", " \"mt_evaporator_load_kW\": 75,\n", " \"lt_compressor_isentropic_eff\": 0.85,\n", " \"mt_compressor_isentropic_eff\": 0.80,\n", "}\n", "\n", "indicators = [\n", " \"COP\", \"total_compressor_power_kW\", \"hrhx_heat_kW\", \"flash_gas_fraction\",\n", " \"discharge_temperature_C\", \"gas_cooler_pressure_bar\", \"receiver_pressure_bar\",\n", " \"gas_cooler_discharge_kW\"\n", "]\n", "\n", "model = CO2BoosterModel()\n", "model.solve_model(**case_study_1)\n", "\n", "records = {}\n", "records[\"case study 1\"] = model.get_results(indicators)\n", "pd.Series(records[\"case study 1\"], name=\"case study 1\")" ] }, { "cell_type": "markdown", "id": "fc7e1ca6", "metadata": {}, "source": [ "The log(*p*)-*h* diagram can automatically be generated with\n", "`plot_logph_diagram_matplotlib`. It extracts all process lines of the cycle\n", "starting from connection `c1` and the diagram limits are derived from the\n", "actual state points. With the gas cooler outlet at 15 °C and 57.3 bar, the heat\n", "rejection ends below the critical point inside the subcooled liquid region." ] }, { "cell_type": "code", "execution_count": null, "id": "5241f139", "metadata": {}, "outputs": [], "source": [ "fig, ax = model.plot_logph_diagram_matplotlib(\"c1\")" ] }, { "cell_type": "markdown", "id": "e0cb8290", "metadata": {}, "source": [ "We can also generate TQ diagrams for heat exchangers that have both a hot and a\n", "cold side. This is the case for the heat recovery heat exchanger." ] }, { "cell_type": "code", "execution_count": null, "id": "f454005b", "metadata": {}, "outputs": [], "source": [ "fig, ax = model.plot_QT_diagram_matplotlib(\"heat-recovery-heat-exchanger\")" ] }, { "cell_type": "markdown", "id": "765cace8", "metadata": {}, "source": [ "## Transcritical operation at high ambient temperature\n", "\n", "CO2 has a critical temperature of only 31 °C. On a hot day the gas cooler can\n", "no longer condense the refrigerant at subcritical pressure. For example, we \n", "assume an outlet temperature of 35 °C. Then the heat rejection takes place at\n", "supercritical pressure. Pressure and temperature are decoupled at the gas\n", "cooler outlet, which turns the gas cooler pressure into a free control\n", "variable:\n", "\n", "- a **higher** pressure increases the compression work directly, but\n", "- it also **lowers** the gas cooler outlet enthalpy, which reduces the flash\n", " gas fraction after the high pressure valve, so less mass flow circulates\n", " through the MT compressor per unit of cooling capacity.\n", "\n", "As a first guess, we operate the system at 100 bar. The COP drops sharply\n", "compared to the subcritical winter case, because of the higher heat rejection\n", "temperature and because almost half of the expanded mass flow turns into flash\n", "gas." ] }, { "cell_type": "code", "execution_count": null, "id": "8328364e", "metadata": {}, "outputs": [], "source": [ "model.solve_model(\n", " gas_cooler_outlet_temperature_C=35, gas_cooler_pressure_bar=100\n", ")\n", "\n", "records[\"transcritical, 100 bar\"] = model.get_results(indicators)\n", "pd.Series(records[\"transcritical, 100 bar\"], name=\"transcritical, 100 bar\")" ] }, { "cell_type": "markdown", "id": "7cd02fee", "metadata": {}, "source": [ "In the log(*p*)-*h* diagram we can see the process pressure going beyond the\n", "two phase dome." ] }, { "cell_type": "code", "execution_count": null, "id": "21cf76aa", "metadata": {}, "outputs": [], "source": [ "fig, ax = model.plot_logph_diagram_matplotlib(\"c1\")" ] }, { "cell_type": "markdown", "id": "dbe3e619", "metadata": {}, "source": [ "The heat recovery heat exchanger now provides much more heat and at a higher\n", "temperature. The red line of the CO2 cooling down also shows the typical\n", "supercritical curvature of isobars in the TQ diagram." ] }, { "cell_type": "code", "execution_count": null, "id": "2b9e36e9", "metadata": {}, "outputs": [], "source": [ "fig, ax = model.plot_QT_diagram_matplotlib(\"heat-recovery-heat-exchanger\")" ] }, { "cell_type": "markdown", "id": "478f688b", "metadata": {}, "source": [ "## Sensitivity: COP over gas cooler pressure\n", "\n", "Before optimizing the cycle, a one dimensional parameter sweep visualizes the\n", "dependency between gas cooler pressure and COP. The `sensitivity_analysis`\n", "method of the base class runs one simulation per value, starting from the point\n", "closest to the current state and always stepping to the nearest unvisited\n", "point, which keeps the solver on a good initial guess throughout the sweep. The\n", "results are returned as a `DataFrame` in the requested order.\n", "\n", "The COP curve shows the two competing effects: below roughly 85 bar the rising\n", "flash gas fraction dominates and the COP collapses, above 90 bar the additional\n", "compression work slowly reduces the efficiency. In between lies a distinct\n", "optimum. In the second row we plot the heat recovered." ] }, { "cell_type": "code", "execution_count": null, "id": "b94db9df", "metadata": {}, "outputs": [], "source": [ "sweep = model.sensitivity_analysis(\n", " param_dict={\"gas_cooler_pressure_bar\": np.linspace(75, 110, 25)},\n", " result_param_list=[\"COP\", \"flash_gas_fraction\", \"hrhx_heat_kW\"],\n", ")" ] }, { "cell_type": "code", "execution_count": null, "id": "6f60417c", "metadata": {}, "outputs": [], "source": [ "fig, ax = plt.subplots(2, figsize=(10, 6), sharex=True)\n", "ax[0].plot(sweep[\"gas_cooler_pressure_bar\"], sweep[\"COP\"], \"o-\", color=\"tab:blue\")\n", "sweep_best = sweep.loc[sweep[\"COP\"].idxmax()]\n", "ax[0].plot(\n", " sweep_best[\"gas_cooler_pressure_bar\"], sweep_best[\"COP\"],\n", " \"*\", color=\"tab:red\", markersize=15\n", ")\n", "ax[0].set_ylabel(\"COP\")\n", "ax[1].plot(sweep[\"gas_cooler_pressure_bar\"], -sweep[\"hrhx_heat_kW\"], \"o-\", color=\"tab:red\")\n", "ax[1].set_ylabel(\"Recovered heat in kW\")\n", "ax[1].set_xlabel(\"gas cooler pressure in bar\")\n", "plt.close(fig)\n", "fig" ] }, { "cell_type": "markdown", "id": "269d90f5", "metadata": {}, "source": [ "## Optimization of the COP\n", "\n", "The sweep only varied the gas cooler pressure. The receiver pressure is a\n", "second degree of freedom as it shifts the split between flash gas (throttled\n", "and recompressed by the MT compressor) and liquid supplied to the evaporators.\n", "The `optimize` method of the base class wraps [pymoo](https://pymoo.org) and\n", "solves\n", "\n", "```{math}\n", "\\max_{p_\\mathrm{gc},\\; p_\\mathrm{rec}} \\quad \\mathrm{COP}\\left(p_\\mathrm{gc}, p_\\mathrm{rec}\\right)\n", "```\n", "\n", "subject to the box bounds {math}`75 \\leq p_\\mathrm{gc} \\leq 110` bar and\n", "{math}`28 \\leq p_\\mathrm{rec} \\leq 45` bar. The lower receiver bound keeps a\n", "control margin above the MT evaporation pressure of 26 bar.\n", "\n", "The decision variables and their bounds are regular parameter names from the\n", "lookup, so no extra glue code is needed. `minimize_flags=[False]` turns the\n", "minimization into a maximization, and the parameters passed as `kpi` are\n", "recorded alongside the objective for every evaluated individual. A particle\n", "swarm with 8 particles over 15 generations means 120 model evaluations.\n", "\n", "```{note}\n", "When this notebook is executed as part of a documentation build - locally or\n", "online - the number of generations is reduced to keep the build time short.\n", "Run the notebook interactively to execute the full 15 generations and\n", "reproduce the converged results discussed below.\n", "```" ] }, { "cell_type": "code", "execution_count": null, "id": "98792d85", "metadata": {}, "outputs": [], "source": [ "n_gen = 15\n", "if os.getenv(\"TESPY_DOCS_BUILD\") or os.getenv(\"GITHUB_ACTIONS\") == \"true\":\n", " n_gen = 2\n", "\n", "log, result = model.optimize(\n", " algorithm=PSO(pop_size=8),\n", " termination=(\"n_gen\", n_gen),\n", " variables={\n", " \"gas_cooler_pressure_bar\": {\"min\": 75, \"max\": 110},\n", " \"receiver_pressure_bar\": {\"min\": 28, \"max\": 45},\n", " },\n", " objective=[\"COP\"],\n", " minimize_flags=[False],\n", " kpi=[\"flash_gas_fraction\", \"discharge_temperature_C\", \"hrhx_heat_kW\"],\n", ")\n", "\n", "(\n", " log.sort_values(\"COP\", ascending=False).head()\n", " .rename(columns={\n", " \"gas_cooler_pressure_bar\": \"p gas cooler in bar\",\n", " \"receiver_pressure_bar\": \"p receiver in bar\",\n", " \"flash_gas_fraction\": \"flash gas fraction\",\n", " \"discharge_temperature_C\": \"T discharge in °C\",\n", " \"hrhx_heat_kW\": \"heat recovery in kW\",\n", " })\n", " .style.format(\"{:.2f}\")\n", " .format(\"{:.4f}\", subset=[\"COP\", \"flash gas fraction\"])\n", " .set_table_styles([{\"selector\": \"th\", \"props\": [(\"text-align\", \"left\")]}])\n", " .set_properties(**{\"text-align\": \"right\"})\n", " .hide(axis=\"index\")\n", ")" ] }, { "cell_type": "markdown", "id": "049eb89f", "metadata": {}, "source": [ "Plotting every evaluated individual on top of the sweep curve shows how the\n", "swarm converges towards the optimum. The color indicates the receiver pressure\n", "of each individual. Its influence on the COP is much weaker than that of the\n", "gas cooler pressure, so near the optimum the best individuals spread over a\n", "band of receiver pressures instead of collapsing onto a single value." ] }, { "cell_type": "code", "execution_count": null, "id": "f9b2403e", "metadata": {}, "outputs": [], "source": [ "fig, ax = plt.subplots(figsize=(10, 6))\n", "ax.plot(\n", " sweep[\"gas_cooler_pressure_bar\"], sweep[\"COP\"], \"-\", color=\"tab:gray\",\n", " label=\"sweep at 32 bar receiver pressure\"\n", ")\n", "sc = ax.scatter(\n", " log[\"gas_cooler_pressure_bar\"], log[\"COP\"],\n", " c=log[\"receiver_pressure_bar\"], cmap=\"viridis\", s=20\n", ")\n", "best = log.loc[log[\"COP\"].idxmax()]\n", "ax.plot(\n", " best[\"gas_cooler_pressure_bar\"], best[\"COP\"],\n", " \"*\", color=\"tab:red\", markersize=15, label=\"optimum\"\n", ")\n", "fig.colorbar(sc, label=\"receiver pressure in bar\")\n", "ax.set_xlabel(\"gas cooler pressure in bar\")\n", "ax.set_ylabel(\"COP\")\n", "ax.legend()\n", "plt.close(fig)\n", "fig" ] }, { "cell_type": "markdown", "id": "618762e1", "metadata": {}, "source": [ "## Best solution\n", "\n", "To restore the optimal operating point, the best individual from the log is\n", "passed back through `solve_model`. The log(*p*)-*h* diagram of the optimized\n", "cycle shows the reduced high side pressure compared to the 100 bar run showed\n", "initially." ] }, { "cell_type": "code", "execution_count": null, "id": "ad7b2399", "metadata": {}, "outputs": [], "source": [ "model.solve_model(\n", " gas_cooler_pressure_bar=best[\"gas_cooler_pressure_bar\"],\n", " receiver_pressure_bar=best[\"receiver_pressure_bar\"],\n", ")\n", "\n", "records[\"optimized\"] = model.get_results(indicators)\n", "fig, ax = model.plot_logph_diagram_matplotlib(\"c1\")" ] } ], "metadata": { "kernelspec": { "display_name": "tespy (3.14.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.0rc2" } }, "nbformat": 4, "nbformat_minor": 5 }