{ "cells": [ { "cell_type": "markdown", "id": "8f112814", "metadata": {}, "source": [ "# Finned-tube models and their submodels\n", "\n", "PyTES models are fixed compositions of submodels: a fluid, a storage and a coupling, plus the\n", "device-level energy. This notebook shows how to inspect them, run a discharge, check the\n", "invariant every model meets (exact energy conservation), and set the initial state.\n", "\n", "For the geometry, the equations and the options of the finned-tube models, with a sketch of each, see the [Finned tube](https://pytes.readthedocs.io/en/latest/models/finned_tube.html) page of the documentation.\n", "\n", "[Tutorial 2](getting_started/tutorial_2_finned_tube_models.ipynb) introduces these models." ] }, { "cell_type": "code", "execution_count": 1, "id": "e4deee08", "metadata": { "execution": { "iopub.execute_input": "2026-10-23T22:01:03.160572Z", "iopub.status.busy": "2026-10-23T22:01:03.160260Z", "iopub.status.idle": "2026-10-23T22:01:03.984625Z", "shell.execute_reply": "2026-10-23T22:01:03.983516Z" } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "fluid: AdvectedFluid\n", " requires: ('Heat transfer fluid heat gain per unit length [W.m-1]', 'Initial uniform temperature [K]')\n", " provides: ('Heat transfer fluid specific enthalpy [J.kg-1]', 'Inlet specific enthalpy [J.kg-1]', 'Outlet specific enthalpy [J.kg-1]', 'Mass flow rate [kg.s-1]', 'Heat transfer fluid thermal energy [J]', 'Heat transfer fluid temperature [K]', 'Inlet temperature [K]', 'Outlet temperature [K]', 'Inlet-outlet temperature difference [K]', 'Heat transfer fluid velocity [m.s-1]', 'Volume flow rate [m3.s-1]', 'Pumped volume [m3]')\n", "storage: EnthalpyStorage\n", " requires: ('Phase-change material boundary heat flux [W.m-2]', 'Initial uniform temperature [K]')\n", " provides: ('Phase-change material enthalpy [J.m-3]', 'Phase-change material enthalpy rate of change [W.m-3]', 'Phase-change material conduction heating rate [W.m-3]', 'Phase-change material thermal energy [J]', 'Liquid fraction', 'Phase', 'Average liquid fraction', 'Phase-change material temperature [K]', 'Phase-change material average temperature [K]', 'Phase-change material cross-section average temperature [K]', 'Phase-change material surface temperature [K]', 'Phase-change material thermal conductivity [W.m-1.K-1]')\n", "coupling: WallCoupling\n", " requires: ('Phase-change material conduction heating rate [W.m-3]', 'Heat transfer fluid specific enthalpy [J.kg-1]', 'Heat transfer fluid temperature [K]', 'Phase-change material surface temperature [K]', 'Mass flow rate [kg.s-1]')\n", " provides: ('Heat transfer fluid heat gain per unit length [W.m-1]', 'Phase-change material boundary heat flux [W.m-2]', 'Heat transfer coefficient [W.m-2.K-1]', 'Surface Reynolds number', 'Surface Prandtl number')\n", "device energy: DeviceEnergy\n", " requires: ('Mass flow rate [kg.s-1]', 'Outlet specific enthalpy [J.kg-1]', 'Inlet specific enthalpy [J.kg-1]', 'Phase-change material thermal energy [J]', 'Heat transfer fluid thermal energy [J]', 'Phase-change material average temperature [K]', 'Phase-change material cross-section average temperature [K]')\n", " provides: ('Power [W]', 'Discharged energy [J]', 'Charged energy [J]', 'Discharged throughput [J]', 'Charged throughput [J]', 'Storage thermal energy [J]', 'Total thermal energy [J]', 'Heat loss power [W]', 'Storage average temperature [K]', 'Storage cross-section average temperature [K]', 'Initial uniform temperature [K]')\n" ] } ], "source": [ "import numpy as np\n", "import pybamm\n", "\n", "import pytes\n", "\n", "model = pytes.SharpFrontModel()\n", "for name, submodel in model.submodels.items():\n", " print(f\"{name}: {type(submodel).__name__}\")\n", " print(\" requires:\", submodel.requires)\n", " print(\" provides:\", submodel.provides)" ] }, { "cell_type": "markdown", "id": "a600ddb8", "metadata": {}, "source": [ "The model is an ordinary PyBaMM model, so `pybamm.Simulation` applies directly. By default\n", "the battery starts uniform at 70 °C (liquid) and is discharged with water at 10 °C. The model\n", "has no cut-off: it runs to the final time, and cut-offs are the terminations of an experiment (`pytes.experiments`; see [experiments](experiments.ipynb))." ] }, { "cell_type": "code", "execution_count": 2, "id": "35990921", "metadata": { "execution": { "iopub.execute_input": "2026-10-23T22:01:03.986545Z", "iopub.status.busy": "2026-10-23T22:01:03.986208Z", "iopub.status.idle": "2026-10-23T22:01:05.145253Z", "shell.execute_reply": "2026-10-23T22:01:05.143752Z" } }, "outputs": [ { "data": { "application/vnd.jupyter.widget-view+json": { "model_id": "94d777affad1493b94b6ab4892f84b39", "version_major": 2, "version_minor": 0 }, "text/plain": [ "interactive(children=(FloatSlider(value=0.0, description='t', max=1.0, step=0.01), Output()), _dom_classes=('w…" ] }, "metadata": {}, "output_type": "display_data" }, { "data": { "text/plain": [ "" ] }, "execution_count": 2, "metadata": {}, "output_type": "execute_result" } ], "source": [ "sim = pybamm.Simulation(model)\n", "solution = sim.solve([0, 3600], t_interp=np.linspace(0, 3600, 121))\n", "sim.plot()" ] }, { "cell_type": "markdown", "id": "adf370c1", "metadata": {}, "source": [ "Power is `mdot (h_out - h_in)`, positive on discharge. The change in total thermal energy\n", "(storage plus fluid) is minus the discharged energy, to round-off.\n", "\n", "There is no state of charge: the energy a device can hold depends on the temperatures it\n", "works between, which are a choice of the user. `pytes.energy_between` gives the energy between\n", "two uniform states, here the 10 °C to 70 °C range of this discharge." ] }, { "cell_type": "code", "execution_count": 3, "id": "001f2df5", "metadata": { "execution": { "iopub.execute_input": "2026-10-23T22:01:05.148462Z", "iopub.status.busy": "2026-10-23T22:01:05.148334Z", "iopub.status.idle": "2026-10-23T22:01:05.172190Z", "shell.execute_reply": "2026-10-23T22:01:05.171271Z" } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "max conservation error [J]: 1.4901161193847656e-08\n", "discharged energy [kWh]: 2.417954249270433\n", "energy between 10 and 70 degC [kWh]: 2.4523701748815085\n" ] } ], "source": [ "E = solution[\"Total thermal energy [J]\"].entries\n", "discharged = solution[\"Discharged energy [J]\"].entries\n", "print(\"max conservation error [J]:\", np.max(np.abs(E - E[0] + discharged)))\n", "print(\"discharged energy [kWh]:\", solution[\"Discharged energy [kWh]\"].entries[-1])\n", "energy_range = pytes.energy_between(model, 273.15 + 10, 273.15 + 70)\n", "print(\"energy between 10 and 70 degC [kWh]:\", energy_range / 3.6e6)" ] }, { "cell_type": "markdown", "id": "83eda932", "metadata": {}, "source": [ "The initial state is uniform at `\"Initial temperature [K]\"`, and solid at the melting\n", "temperature. For any other start, for example part way through a discharge, start from the\n", "end of an earlier solution with PyBaMM's `set_initial_conditions_from`." ] }, { "cell_type": "code", "execution_count": 4, "id": "8f18a7c9", "metadata": { "execution": { "iopub.execute_input": "2026-10-23T22:01:05.174491Z", "iopub.status.busy": "2026-10-23T22:01:05.174306Z", "iopub.status.idle": "2026-10-23T22:01:05.828492Z", "shell.execute_reply": "2026-10-23T22:01:05.827686Z" } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "initial temperature [degC]: 30.0\n" ] }, { "name": "stdout", "output_type": "stream", "text": [ "outlet temperature at the end of the first run and at the restart [degC]: 51.71779784644089 51.71779784644089\n" ] } ], "source": [ "parameter_values = model.default_parameter_values\n", "parameter_values[\"Initial temperature [K]\"] = 273.15 + 30\n", "warm = pybamm.Simulation(model, parameter_values=parameter_values).solve([0, 600])\n", "print(\n", " \"initial temperature [degC]:\", warm[\"Initial uniform temperature [degC]\"].entries[0]\n", ")\n", "\n", "partial = pybamm.Simulation(model).solve([0, 900])\n", "restarted = model.set_initial_conditions_from(partial, inplace=False)\n", "continued = pybamm.Simulation(restarted).solve([0, 900])\n", "print(\n", " \"outlet temperature at the end of the first run and at the restart [degC]:\",\n", " partial[\"Outlet temperature [degC]\"].entries[-1],\n", " continued[\"Outlet temperature [degC]\"].entries[0],\n", ")" ] }, { "cell_type": "markdown", "id": "aa71653e", "metadata": {}, "source": [ "The default PCM melts over a range of temperatures, `\"PCM melting range [K]\"` = 2 K from\n", "solidus to liquidus around its melting temperature, so the outlet temperature slopes through\n", "the melting plateau. With a range of 0, the PCM melts at a sharp melting point and the\n", "outlet stays flatter. `\"PCM smoothing width [K]\"` is separate: it only rounds the corners of the\n", "enthalpy-temperature relation, for speed, and may be set to 0." ] }, { "cell_type": "code", "execution_count": 5, "id": "87b57886", "metadata": { "execution": { "iopub.execute_input": "2026-10-23T22:01:05.830036Z", "iopub.status.busy": "2026-10-23T22:01:05.829899Z", "iopub.status.idle": "2026-10-23T22:01:06.758213Z", "shell.execute_reply": "2026-10-23T22:01:06.757243Z" } }, "outputs": [ { "data": { "application/vnd.jupyter.widget-view+json": { "model_id": "953407a8ba1e45068bc74f331cec9bcb", "version_major": 2, "version_minor": 0 }, "text/plain": [ "interactive(children=(FloatSlider(value=0.0, description='t', max=1.0, step=0.01), Output()), _dom_classes=('w…" ] }, "metadata": {}, "output_type": "display_data" }, { "data": { "text/plain": [ "" ] }, "execution_count": 5, "metadata": {}, "output_type": "execute_result" } ], "source": [ "sharp = model.default_parameter_values\n", "sharp[\"PCM melting range [K]\"] = 0\n", "sharp_solution = pybamm.Simulation(model, parameter_values=sharp).solve(\n", " [0, 3600], t_interp=np.linspace(0, 3600, 121)\n", ")\n", "pybamm.dynamic_plot(\n", " [solution, sharp_solution],\n", " [\"Outlet temperature [degC]\", \"Average liquid fraction\"],\n", " labels=[\"2 K melting range (default)\", \"sharp melting point\"],\n", ")" ] }, { "cell_type": "markdown", "id": "351d15f1", "metadata": {}, "source": [ "The mush model resolves the fins and the PCM between them, and has the same outputs, so\n", "the two compare one to one." ] }, { "cell_type": "code", "execution_count": 6, "id": "35da1f03", "metadata": { "execution": { "iopub.execute_input": "2026-10-23T22:01:06.760517Z", "iopub.status.busy": "2026-10-23T22:01:06.760401Z", "iopub.status.idle": "2026-10-23T22:01:24.488558Z", "shell.execute_reply": "2026-10-23T22:01:24.487344Z" } }, "outputs": [ { "data": { "application/vnd.jupyter.widget-view+json": { "model_id": "e64d34fccfbe4ee19847365bb9f69ed0", "version_major": 2, "version_minor": 0 }, "text/plain": [ "interactive(children=(FloatSlider(value=0.0, description='t', max=1.0, step=0.01), Output()), _dom_classes=('w…" ] }, "metadata": {}, "output_type": "display_data" }, { "data": { "text/plain": [ "" ] }, "execution_count": 6, "metadata": {}, "output_type": "execute_result" } ], "source": [ "mush = pytes.MushModel()\n", "mush_solution = pybamm.Simulation(\n", " mush, var_pts={mush.x: 10, mush.y: 20, mush.z: 20}\n", ").solve([0, 3600], t_interp=np.linspace(0, 3600, 121))\n", "pybamm.dynamic_plot(\n", " [solution, mush_solution],\n", " [\"Outlet temperature [degC]\", \"Power [kW]\", \"Discharged energy [kWh]\"],\n", ")" ] } ], "metadata": { "kernelspec": { "display_name": "pytes (3.14.4)", "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.4" } }, "nbformat": 4, "nbformat_minor": 5 }