From 9f6351cc5cdbe1587d4f61009bfc842193402c23 Mon Sep 17 00:00:00 2001 From: David Straub Date: Tue, 5 May 2026 17:33:04 +0200 Subject: [PATCH 1/2] Add example notebooks (fixes #6) --- docs/source/examples/01_spme_discharge.ipynb | 244 +++++++++++++ .../examples/02_electrothermal_coupling.ipynb | 266 ++++++++++++++ .../source/examples/03_dfn_cosimulation.ipynb | 324 ++++++++++++++++++ 3 files changed, 834 insertions(+) create mode 100644 docs/source/examples/01_spme_discharge.ipynb create mode 100644 docs/source/examples/02_electrothermal_coupling.ipynb create mode 100644 docs/source/examples/03_dfn_cosimulation.ipynb diff --git a/docs/source/examples/01_spme_discharge.ipynb b/docs/source/examples/01_spme_discharge.ipynb new file mode 100644 index 0000000..8056e4f --- /dev/null +++ b/docs/source/examples/01_spme_discharge.ipynb @@ -0,0 +1,244 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "29f723de", + "metadata": {}, + "source": [ + "# Constant-Current Discharge with the SPMe\n", + "\n", + "Constant-current (CC) discharge simulation using `CellElectrothermal`, which wraps\n", + "PyBaMM's SPMe with a lumped thermal sub-model. PathSim integrates the coupled ODE system.\n" + ] + }, + { + "cell_type": "markdown", + "id": "767e9ba8", + "metadata": {}, + "source": [ + "## Model\n", + "\n", + "The **SPMe** extends the Single Particle Model (SPM) with electrolyte concentration\n", + "dynamics, significantly improving accuracy at moderate-to-high C-rates. Solid-phase\n", + "diffusion in each electrode follows:\n", + "\n", + "$$\n", + "\\frac{\\partial c_{\\mathrm{s},k}}{\\partial t}\n", + "= \\frac{D_{\\mathrm{s},k}}{r^2}\n", + " \\frac{\\partial}{\\partial r}\\!\\left(r^2 \\frac{\\partial c_{\\mathrm{s},k}}{\\partial r}\\right)\n", + "$$\n", + "\n", + "The terminal voltage is determined by open-circuit potentials and Butler–Volmer overpotentials.\n", + "Cell temperature is tracked via PyBaMM's lumped thermal sub-model:\n", + "\n", + "$$\n", + "m C_p \\frac{dT}{dt} = \\dot{Q} - UA\\,(T - T_{\\mathrm{amb}})\n", + "$$\n", + "\n", + "> Brosa Planella et al., [arXiv:2203.16091](https://arxiv.org/abs/2203.16091) (2022).\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "5a49b97d", + "metadata": {}, + "outputs": [], + "source": [ + "import numpy as np\n", + "import matplotlib.pyplot as plt\n", + "\n", + "from pathsim import Simulation, Connection\n", + "from pathsim.blocks import Constant, Scope\n", + "from pathsim.solvers import ESDIRK43\n", + "\n", + "from pathsim_batt import CellElectrothermal" + ] + }, + { + "cell_type": "markdown", + "id": "39db4cc4", + "metadata": {}, + "source": [ + "## Single 1 C Discharge\n", + "\n", + "Chen2020 is a 21700-format NMC/graphite cell with 5 Ah nominal capacity, so 1 C = 5 A.\n", + "`ESDIRK43` is used because the discretised SPMe ODE is stiff.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "acf93927", + "metadata": {}, + "outputs": [], + "source": [ + "C_nom = 5.0 # Chen2020 nominal capacity [Ah]\n", + "T_amb0 = 298.15 # [K]\n", + "\n", + "cell = CellElectrothermal(initial_soc=1.0)\n", + "I_src = Constant(1.0 * C_nom)\n", + "T_src = Constant(T_amb0)\n", + "sco = Scope(labels=[\"V\", \"T\", \"Q_heat\", \"SOC\"])\n", + "\n", + "sim = Simulation(\n", + " blocks=[I_src, T_src, cell, sco],\n", + " connections=[\n", + " Connection(I_src, cell[\"I\"]),\n", + " Connection(T_src, cell[\"T_amb\"]),\n", + " Connection(cell[\"V\"], sco[0]),\n", + " Connection(cell[\"T\"], sco[1]),\n", + " Connection(cell[\"Q_heat\"], sco[2]),\n", + " Connection(cell[\"SOC\"], sco[3]),\n", + " ],\n", + " dt=10.0,\n", + " Solver=ESDIRK43,\n", + ")\n", + "\n", + "sim.run(3600.0)\n", + "t, [V, T, Q_heat, SOC] = sco.read()\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "a5452999", + "metadata": {}, + "outputs": [], + "source": [ + "fig, axes = plt.subplots(3, 1, figsize=(8, 7), sharex=True)\n", + "\n", + "axes[0].plot(t / 3600, V, color=\"steelblue\")\n", + "axes[0].set_ylabel(\"Terminal voltage / V\")\n", + "axes[0].set_ylim(2.4, 4.3)\n", + "axes[0].axhline(2.5, color=\"red\", linestyle=\"--\", linewidth=0.8, label=\"2.5 V cutoff\")\n", + "axes[0].legend()\n", + "\n", + "axes[1].plot(t / 3600, T - 273.15, color=\"orangered\")\n", + "axes[1].set_ylabel(\"Cell temperature / °C\")\n", + "\n", + "axes[2].plot(t / 3600, SOC * 100, color=\"forestgreen\")\n", + "axes[2].set_ylabel(\"SOC / %\")\n", + "axes[2].set_ylim(0, 105)\n", + "axes[2].set_xlabel(\"Time / h\")\n", + "\n", + "fig.suptitle(\"1 C Discharge — SPMe with Lumped Thermal (Chen2020)\", fontsize=12)\n", + "plt.tight_layout()\n", + "plt.show()\n" + ] + }, + { + "cell_type": "markdown", + "id": "e70116dd", + "metadata": {}, + "source": [ + "## C-Rate Sweep\n", + "\n", + "Higher C-rates cause larger concentration gradients and overpotentials, leading to\n", + "steeper voltage drop-off and more pronounced temperature rise.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "5da69059", + "metadata": {}, + "outputs": [], + "source": [ + "results = {}\n", + "\n", + "for c_rate in [0.5, 1.0, 2.0]:\n", + " I_src_r = Constant(c_rate * C_nom)\n", + " T_src_r = Constant(T_amb0)\n", + " cell_r = CellElectrothermal(initial_soc=1.0)\n", + " sco_r = Scope(labels=[\"V\", \"T\", \"SOC\"])\n", + "\n", + " sim_r = Simulation(\n", + " blocks=[I_src_r, T_src_r, cell_r, sco_r],\n", + " connections=[\n", + " Connection(I_src_r, cell_r[\"I\"]),\n", + " Connection(T_src_r, cell_r[\"T_amb\"]),\n", + " Connection(cell_r[\"V\"], sco_r[0]),\n", + " Connection(cell_r[\"T\"], sco_r[1]),\n", + " Connection(cell_r[\"SOC\"], sco_r[2]),\n", + " ],\n", + " dt=10.0,\n", + " Solver=ESDIRK43,\n", + " )\n", + "\n", + " sim_r.run(1800.0)\n", + " t_r, [V_r, T_r, SOC_r] = sco_r.read()\n", + " results[c_rate] = {\"t\": t_r, \"V\": V_r, \"T\": T_r, \"SOC\": SOC_r}\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "e613a2bf", + "metadata": {}, + "outputs": [], + "source": [ + "colors = {0.5: \"royalblue\", 1.0: \"forestgreen\", 2.0: \"orangered\"}\n", + "\n", + "fig, axes = plt.subplots(1, 2, figsize=(11, 4))\n", + "\n", + "for c_rate, res in results.items():\n", + " lbl = f\"{c_rate} C\"\n", + " clr = colors[c_rate]\n", + " axes[0].plot(res[\"SOC\"] * 100, res[\"V\"], label=lbl, color=clr)\n", + " axes[1].plot(res[\"t\"] / 3600, res[\"T\"] - 273.15, label=lbl, color=clr)\n", + "\n", + "axes[0].set_xlabel(\"SOC / %\")\n", + "axes[0].set_ylabel(\"Terminal voltage / V\")\n", + "axes[0].set_xlim(0, 100)\n", + "axes[0].set_ylim(2.4, 4.3)\n", + "axes[0].axhline(2.5, color=\"grey\", linestyle=\"--\", linewidth=0.8)\n", + "axes[0].invert_xaxis()\n", + "axes[0].legend()\n", + "axes[0].set_title(\"Voltage vs. SOC\")\n", + "\n", + "axes[1].set_xlabel(\"Time / h\")\n", + "axes[1].set_ylabel(\"Cell temperature / °C\")\n", + "axes[1].legend()\n", + "axes[1].set_title(\"Temperature Rise\")\n", + "\n", + "fig.suptitle(\"C-Rate Comparison — SPMe with Lumped Thermal (Chen2020)\", fontsize=12)\n", + "plt.tight_layout()\n", + "plt.show()\n" + ] + }, + { + "cell_type": "markdown", + "id": "b4a003bc", + "metadata": {}, + "source": [ + "## Summary\n", + "\n", + "- `CellElectrothermal` wraps the PyBaMM SPMe + lumped thermal ODE and integrates it as a standard `DynamicalSystem` in PathSim.\n", + "- Higher C-rates produce steeper V–SOC curves and greater temperature rise.\n", + "- For an external thermal model (e.g. a custom cooling loop), see notebook 02.\n" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "default (3.13.7)", + "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.13.7" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/docs/source/examples/02_electrothermal_coupling.ipynb b/docs/source/examples/02_electrothermal_coupling.ipynb new file mode 100644 index 0000000..29c164c --- /dev/null +++ b/docs/source/examples/02_electrothermal_coupling.ipynb @@ -0,0 +1,266 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "df5006aa", + "metadata": {}, + "source": [ + "# Electrothermal Coupling with an External Thermal Model\n", + "\n", + "`CellElectrical` uses PyBaMM's **isothermal** electrochemistry and exposes heat generation\n", + "as an output. Wiring it to `LumpedThermal` creates a closed electrothermal feedback loop\n", + "directly in PathSim — useful for pack-level thermal networks or custom cooling models.\n" + ] + }, + { + "cell_type": "markdown", + "id": "fc426df0", + "metadata": {}, + "source": [ + "## Model\n", + "\n", + "`LumpedThermal` implements a single-node energy balance:\n", + "\n", + "$$\n", + "m C_p \\frac{dT}{dt} = \\dot{Q} - UA\\,(T - T_{\\mathrm{amb}})\n", + "$$\n", + "\n", + "`CellElectrical` outputs heat generation as **volumetric** power [W m⁻³], while\n", + "`LumpedThermal` expects total power [W]. We absorb the electrode volume $V_\\mathrm{el}$\n", + "into the thermal parameters ($m_\\mathrm{eff} = m C_p / V_\\mathrm{el}$, $UA_\\mathrm{eff} = UA / V_\\mathrm{el}$)\n", + "so the block accepts [W m⁻³] directly.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "1d33ed8e", + "metadata": {}, + "outputs": [], + "source": [ + "import numpy as np\n", + "import matplotlib.pyplot as plt\n", + "\n", + "from pathsim import Simulation, Connection\n", + "from pathsim.blocks import Constant, Scope\n", + "from pathsim.solvers import ESDIRK43\n", + "\n", + "from pathsim_batt import CellElectrical, LumpedThermal" + ] + }, + { + "cell_type": "markdown", + "id": "7fb059c0", + "metadata": {}, + "source": [ + "## Simulation: 1 C Discharge with Electrothermal Feedback\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "e2853134", + "metadata": {}, + "outputs": [], + "source": [ + "C_nom = 5.0 # [Ah]\n", + "I_discharge = 1.0 * C_nom\n", + "T_amb0 = 298.15 # [K]\n", + "\n", + "mass = 0.065 # [kg]\n", + "Cp = 750.0 # [J kg⁻¹ K⁻¹]\n", + "UA = 0.5 # [W K⁻¹]\n", + "V_electrode = 2.5e-5 # [m³] — electrode stack volume for Chen2020 (21700)\n", + "\n", + "# Rescale so LumpedThermal accepts Q_dot in [W m⁻³] directly\n", + "mass_eff = mass * Cp / V_electrode\n", + "UA_eff = UA / V_electrode\n", + "\n", + "cell = CellElectrical(initial_soc=1.0)\n", + "thermal = LumpedThermal(mass=mass_eff, Cp=1.0, UA=UA_eff, T0=T_amb0)\n", + "I_src = Constant(I_discharge)\n", + "T_src = Constant(T_amb0)\n", + "sco = Scope(labels=[\"V\", \"SOC\", \"T_cell\"])\n", + "\n", + "sim = Simulation(\n", + " blocks=[I_src, T_src, cell, thermal, sco],\n", + " connections=[\n", + " Connection(I_src, cell[\"I\"]),\n", + " Connection(thermal[\"T\"], cell[\"T_cell\"]),\n", + " Connection(cell[\"Q_heat\"], thermal[\"Q_dot\"]),\n", + " Connection(T_src, thermal[\"T_amb\"]),\n", + " Connection(cell[\"V\"], sco[0]),\n", + " Connection(cell[\"SOC\"], sco[1]),\n", + " Connection(thermal[\"T\"], sco[2]),\n", + " ],\n", + " dt=10.0,\n", + " Solver=ESDIRK43,\n", + ")\n", + "\n", + "sim.run(1800.0)\n", + "t, [V, SOC, T_cell] = sco.read()\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "f850a4b1", + "metadata": {}, + "outputs": [], + "source": [ + "_cell_dbg = CellElectrical(initial_soc=1.0)\n", + "print(\"_thermal_extra_options:\", _cell_dbg._thermal_extra_options)\n", + "# Print private attrs that might hold initial state\n", + "print([a for a in dir(_cell_dbg) if \"init\" in a.lower() or \"state\" in a.lower() or \"x0\" in a.lower()])" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "f53b539c", + "metadata": {}, + "outputs": [], + "source": [ + "fig, axes = plt.subplots(3, 1, figsize=(8, 7), sharex=True)\n", + "\n", + "axes[0].plot(t / 3600, V, color=\"steelblue\")\n", + "axes[0].set_ylabel(\"Terminal voltage / V\")\n", + "axes[0].set_ylim(3.0, 4.3)\n", + "\n", + "axes[1].plot(t / 3600, T_cell - 273.15, color=\"orangered\")\n", + "axes[1].axhline(T_amb0 - 273.15, color=\"grey\", linestyle=\"--\",\n", + " linewidth=0.8, label=\"$T_{\\\\mathrm{amb}}$\")\n", + "axes[1].set_ylabel(\"Cell temperature / °C\")\n", + "axes[1].legend()\n", + "\n", + "axes[2].plot(t / 3600, SOC * 100, color=\"forestgreen\")\n", + "axes[2].set_ylabel(\"SOC / %\")\n", + "axes[2].set_ylim(0, 105)\n", + "axes[2].set_xlabel(\"Time / h\")\n", + "\n", + "fig.suptitle(\"1 C Discharge — External Electrothermal Coupling (Chen2020)\", fontsize=12)\n", + "plt.tight_layout()\n", + "plt.show()\n" + ] + }, + { + "cell_type": "markdown", + "id": "5307aa7b", + "metadata": {}, + "source": [ + "## Effect of Cooling Conditions\n", + "\n", + "Sweep of $UA$ from adiabatic to liquid-cooled to show the impact on voltage and temperature.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "74694603", + "metadata": {}, + "outputs": [], + "source": [ + "ua_scenarios = {\n", + " \"Adiabatic (UA = 0)\": 0.0,\n", + " \"Moderate (UA = 0.5 W K⁻¹)\": 0.5,\n", + " \"Aggressive (UA = 5 W K⁻¹)\": 5.0,\n", + "}\n", + "colors_ua = [\"firebrick\", \"steelblue\", \"teal\"]\n", + "ua_results = {}\n", + "\n", + "for (label, ua_val), clr in zip(ua_scenarios.items(), colors_ua):\n", + " I_src_i = Constant(I_discharge)\n", + " T_src_i = Constant(T_amb0)\n", + " cell_i = CellElectrical(initial_soc=1.0)\n", + " thermal_i = LumpedThermal(mass=mass_eff, Cp=1.0, UA=ua_val / V_electrode, T0=T_amb0)\n", + " sco_i = Scope(labels=[\"V\", \"T\", \"SOC\"])\n", + "\n", + " sim_i = Simulation(\n", + " blocks=[I_src_i, T_src_i, cell_i, thermal_i, sco_i],\n", + " connections=[\n", + " Connection(I_src_i, cell_i[\"I\"]),\n", + " Connection(thermal_i[\"T\"], cell_i[\"T_cell\"]),\n", + " Connection(cell_i[\"Q_heat\"], thermal_i[\"Q_dot\"]),\n", + " Connection(T_src_i, thermal_i[\"T_amb\"]),\n", + " Connection(cell_i[\"V\"], sco_i[0]),\n", + " Connection(thermal_i[\"T\"], sco_i[1]),\n", + " Connection(cell_i[\"SOC\"], sco_i[2]),\n", + " ],\n", + " dt=10.0,\n", + " Solver=ESDIRK43,\n", + " )\n", + "\n", + " sim_i.run(1800.0)\n", + " t_i, [V_i, T_i, SOC_i] = sco_i.read()\n", + " ua_results[label] = {\"t\": t_i, \"V\": V_i, \"T\": T_i, \"SOC\": SOC_i, \"color\": clr}\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "61ad60de", + "metadata": {}, + "outputs": [], + "source": [ + "fig, axes = plt.subplots(1, 2, figsize=(11, 4))\n", + "\n", + "for label, res in ua_results.items():\n", + " axes[0].plot(res[\"SOC\"] * 100, res[\"V\"], label=label, color=res[\"color\"])\n", + " axes[1].plot(res[\"t\"] / 3600, res[\"T\"] - 273.15, label=label, color=res[\"color\"])\n", + "\n", + "axes[0].set_xlabel(\"SOC / %\")\n", + "axes[0].set_ylabel(\"Terminal voltage / V\")\n", + "axes[0].set_xlim(0, 100)\n", + "axes[0].invert_xaxis()\n", + "axes[0].set_ylim(3.0, 4.3)\n", + "axes[0].legend(fontsize=8)\n", + "axes[0].set_title(\"Voltage vs. SOC\")\n", + "\n", + "axes[1].set_xlabel(\"Time / h\")\n", + "axes[1].set_ylabel(\"Cell temperature / °C\")\n", + "axes[1].axhline(T_amb0 - 273.15, color=\"grey\", linestyle=\":\",\n", + " linewidth=0.8, label=\"$T_{\\\\mathrm{amb}}$\")\n", + "axes[1].legend(fontsize=8)\n", + "axes[1].set_title(\"Temperature Rise vs. Cooling Intensity\")\n", + "\n", + "fig.suptitle(\"Effect of Cooling Condition — 1 C Discharge (Chen2020)\", fontsize=12)\n", + "plt.tight_layout()\n", + "plt.show()\n" + ] + }, + { + "cell_type": "markdown", + "id": "2c9f4dce", + "metadata": {}, + "source": [ + "## Summary\n", + "\n", + "- `CellElectrical` + `LumpedThermal` form a closed electrothermal feedback loop; PathSim resolves the coupling at each step.\n", + "- PyBaMM's volumetric heat [W m⁻³] is bridged to `LumpedThermal`'s [W] input by rescaling with the electrode volume.\n", + "- Stronger cooling keeps the cell cooler and slightly raises the discharge voltage.\n", + "- For DAE models (e.g. DFN), see notebook 03.\n" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "default (3.13.7)", + "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.13.7" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/docs/source/examples/03_dfn_cosimulation.ipynb b/docs/source/examples/03_dfn_cosimulation.ipynb new file mode 100644 index 0000000..3bbd6c3 --- /dev/null +++ b/docs/source/examples/03_dfn_cosimulation.ipynb @@ -0,0 +1,324 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "8f35936d", + "metadata": {}, + "source": [ + "# High-Fidelity DFN Simulation via Co-Simulation\n", + "\n", + "The **Doyle–Fuller–Newman (DFN)** model produces a DAE system after discretisation\n", + "and cannot be used with the monolithic ODE blocks. The co-simulation blocks\n", + "(`CellCoSimElectrothermal`, `CellCoSimElectrical`) let PyBaMM step the DAE internally\n", + "while PathSim receives zero-order-held outputs between macro-steps.\n" + ] + }, + { + "cell_type": "markdown", + "id": "29023c71", + "metadata": {}, + "source": [ + "## Why Co-Simulation?\n", + "\n", + "The DFN model resolves spatial gradients in both the solid and electrolyte phases.\n", + "The phase-potential equations are **algebraic**, so after spatial discretisation the\n", + "DFN becomes a DAE — incompatible with `CellElectrical` / `CellElectrothermal`.\n", + "\n", + "The co-simulation blocks call `pybamm.Simulation.step()` internally on a fixed macro-step\n", + "`dt` and expose zero-order-held outputs to PathSim. This supports any PyBaMM model.\n", + "\n", + "> Brosa Planella et al., [arXiv:2203.16091](https://arxiv.org/abs/2203.16091) (2022).\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "8d04e0ed", + "metadata": {}, + "outputs": [], + "source": [ + "import numpy as np\n", + "import matplotlib.pyplot as plt\n", + "import pybamm\n", + "\n", + "from pathsim import Simulation, Connection\n", + "from pathsim.blocks import Constant, Scope\n", + "from pathsim.solvers import ESDIRK43\n", + "\n", + "from pathsim_batt import (\n", + " CellElectrothermal,\n", + " CellCoSimElectrothermal,\n", + " CellCoSimElectrical,\n", + " LumpedThermal,\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "4955473d", + "metadata": {}, + "source": [ + "## Part 1: SPMe vs DFN with Built-in Thermal\n", + "\n", + "Both models use PyBaMM's lumped thermal sub-model. SPMe runs as a monolithic ODE in PathSim;\n", + "DFN runs via co-simulation with a 10 s macro-step.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "348cc178", + "metadata": {}, + "outputs": [], + "source": [ + "C_nom = 5.0 # Chen2020 nominal capacity [Ah]\n", + "I_1C = 1.0 * C_nom # 1 C current [A]\n", + "T_amb0 = 298.15 # ambient temperature [K]\n", + "t_end = 1800.0 # simulation duration [s] — 50 % SoC at 1 C\n", + "dt_sim = 10.0 # PathSim macro-step [s]\n", + "\n", + "\n", + "def run_electrothermal(cell_block, dt_ps=10.0):\n", + " \"\"\"Helper: discharge cell_block at 1 C and return (t, V, T, SOC).\"\"\"\n", + " I_src = Constant(I_1C)\n", + " T_src = Constant(T_amb0)\n", + " sco = Scope(labels=[\"V\", \"T\", \"SOC\"])\n", + "\n", + " sim = Simulation(\n", + " blocks=[I_src, T_src, cell_block, sco],\n", + " connections=[\n", + " Connection(I_src, cell_block[\"I\"]),\n", + " Connection(T_src, cell_block[\"T_amb\"]),\n", + " Connection(cell_block[\"V\"], sco[0]),\n", + " Connection(cell_block[\"T\"], sco[1]),\n", + " Connection(cell_block[\"SOC\"], sco[2]),\n", + " ],\n", + " dt=dt_ps,\n", + " Solver=ESDIRK43,\n", + " )\n", + " sim.run(t_end)\n", + " t, [V, T, SOC] = sco.read()\n", + " return t, V, T, SOC" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "94a297d5", + "metadata": {}, + "outputs": [], + "source": [ + "print(\"Running SPMe (monolithic)...\")\n", + "spme_cell = CellElectrothermal(initial_soc=1.0)\n", + "t_spme, V_spme, T_spme, SOC_spme = run_electrothermal(spme_cell, dt_ps=dt_sim)\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "3426a2e8", + "metadata": {}, + "outputs": [], + "source": [ + "print(\"Running DFN (co-simulation)...\")\n", + "dfn_cell = CellCoSimElectrothermal(\n", + " model=pybamm.lithium_ion.DFN(options={\"thermal\": \"lumped\"}),\n", + " initial_soc=1.0,\n", + " dt=dt_sim,\n", + ")\n", + "t_dfn, V_dfn, T_dfn, SOC_dfn = run_electrothermal(dfn_cell, dt_ps=dt_sim)\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "01c28659", + "metadata": {}, + "outputs": [], + "source": [ + "fig, axes = plt.subplots(1, 3, figsize=(13, 4))\n", + "\n", + "axes[0].plot(t_spme / 3600, V_spme, label=\"SPMe\", color=\"steelblue\")\n", + "axes[0].plot(t_dfn[1:] / 3600, V_dfn[1:], \"--\", label=\"DFN\", color=\"orangered\")\n", + "axes[0].set_xlabel(\"Time / h\")\n", + "axes[0].set_ylabel(\"Terminal voltage / V\")\n", + "axes[0].set_ylim(3.0, 4.3)\n", + "axes[0].legend()\n", + "axes[0].set_title(\"Voltage\")\n", + "\n", + "axes[1].plot(t_spme / 3600, T_spme - 273.15, label=\"SPMe\", color=\"steelblue\")\n", + "axes[1].plot(t_dfn[1:] / 3600, T_dfn[1:] - 273.15, \"--\", label=\"DFN\", color=\"orangered\")\n", + "axes[1].set_xlabel(\"Time / h\")\n", + "axes[1].set_ylabel(\"Cell temperature / °C\")\n", + "axes[1].legend()\n", + "axes[1].set_title(\"Temperature\")\n", + "\n", + "axes[2].plot(t_spme / 3600, SOC_spme * 100, label=\"SPMe\", color=\"steelblue\")\n", + "axes[2].plot(t_dfn[1:] / 3600, SOC_dfn[1:] * 100, \"--\", label=\"DFN\", color=\"orangered\")\n", + "axes[2].set_xlabel(\"Time / h\")\n", + "axes[2].set_ylabel(\"SOC / %\")\n", + "axes[2].set_ylim(0, 105)\n", + "axes[2].legend()\n", + "axes[2].set_title(\"State of Charge\")\n", + "\n", + "fig.suptitle(\"SPMe (monolithic) vs DFN (co-simulation) — 1 C Discharge\", fontsize=12)\n", + "plt.tight_layout()\n", + "plt.show()\n" + ] + }, + { + "cell_type": "markdown", + "id": "96e37c72", + "metadata": {}, + "source": [ + "## Part 2: DFN with External Thermal Model\n", + "\n", + "Same feedback topology as notebook 02, but with `CellCoSimElectrical` instead of\n", + "`CellElectrical`. We compare two macro-step sizes to illustrate ZOH approximation error.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "995b4c28", + "metadata": {}, + "outputs": [], + "source": [ + "mass = 0.065 # [kg]\n", + "Cp = 750.0 # [J kg⁻¹ K⁻¹]\n", + "UA = 0.5 # [W K⁻¹]\n", + "V_electrode = 2.5e-5 # [m³]\n", + "\n", + "mass_eff = mass * Cp / V_electrode\n", + "UA_eff = UA / V_electrode\n", + "\n", + "\n", + "def run_cosim_external(dt_macro):\n", + " I_src_cs = Constant(I_1C)\n", + " T_src_cs = Constant(T_amb0)\n", + " cell_cs = CellCoSimElectrical(\n", + " model=pybamm.lithium_ion.DFN(options={\n", + " \"thermal\": \"isothermal\",\n", + " \"calculate heat source for isothermal models\": \"true\",\n", + " }),\n", + " initial_soc=1.0,\n", + " dt=dt_macro,\n", + " )\n", + " thermal_cs = LumpedThermal(mass=mass_eff, Cp=1.0, UA=UA_eff, T0=T_amb0)\n", + " sco = Scope(labels=[\"V\", \"SOC\", \"T_cell\"])\n", + "\n", + " sim = Simulation(\n", + " blocks=[I_src_cs, T_src_cs, cell_cs, thermal_cs, sco],\n", + " connections=[\n", + " Connection(I_src_cs, cell_cs[\"I\"]),\n", + " Connection(thermal_cs[\"T\"], cell_cs[\"T_cell\"]),\n", + " Connection(cell_cs[\"Q_heat\"], thermal_cs[\"Q_dot\"]),\n", + " Connection(T_src_cs, thermal_cs[\"T_amb\"]),\n", + " Connection(cell_cs[\"V\"], sco[0]),\n", + " Connection(cell_cs[\"SOC\"], sco[1]),\n", + " Connection(thermal_cs[\"T\"], sco[2]),\n", + " ],\n", + " dt=dt_macro,\n", + " Solver=ESDIRK43,\n", + " )\n", + " sim.run(t_end)\n", + " t, [V, SOC, T_cell] = sco.read()\n", + " return t, V, T_cell, SOC\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "132c14aa", + "metadata": {}, + "outputs": [], + "source": [ + "print(\"Running DFN co-sim with external thermal (dt = 30 s)...\")\n", + "t_30, V_30, T_30, SOC_30 = run_cosim_external(dt_macro=30.0)\n", + "\n", + "print(\"Running DFN co-sim with external thermal (dt = 5 s)...\")\n", + "t_5, V_5, T_5, SOC_5 = run_cosim_external(dt_macro=5.0)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "863ae0bd", + "metadata": {}, + "outputs": [], + "source": [ + "fig, axes = plt.subplots(1, 3, figsize=(13, 4))\n", + "\n", + "axes[0].plot(t_30[1:] / 3600, V_30[1:], label=\"dt = 30 s\", color=\"darkorange\")\n", + "axes[0].plot(t_5[1:] / 3600, V_5[1:], \"--\", label=\"dt = 5 s\", color=\"steelblue\")\n", + "axes[0].set_xlabel(\"Time / h\")\n", + "axes[0].set_ylabel(\"Terminal voltage / V\")\n", + "axes[0].set_ylim(3.0, 4.3)\n", + "axes[0].legend()\n", + "axes[0].set_title(\"Voltage\")\n", + "\n", + "axes[1].plot(t_30[1:] / 3600, T_30[1:] - 273.15, label=\"dt = 30 s\", color=\"darkorange\")\n", + "axes[1].plot(t_5[1:] / 3600, T_5[1:] - 273.15, \"--\", label=\"dt = 5 s\", color=\"steelblue\")\n", + "axes[1].set_xlabel(\"Time / h\")\n", + "axes[1].set_ylabel(\"Cell temperature / °C\")\n", + "axes[1].legend()\n", + "axes[1].set_title(\"Temperature\")\n", + "\n", + "axes[2].plot(t_30[1:] / 3600, SOC_30[1:] * 100, label=\"dt = 30 s\", color=\"darkorange\")\n", + "axes[2].plot(t_5[1:] / 3600, SOC_5[1:] * 100, \"--\", label=\"dt = 5 s\", color=\"steelblue\")\n", + "axes[2].set_xlabel(\"Time / h\")\n", + "axes[2].set_ylabel(\"SOC / %\")\n", + "axes[2].set_ylim(0, 105)\n", + "axes[2].legend()\n", + "axes[2].set_title(\"State of Charge\")\n", + "\n", + "fig.suptitle(\n", + " \"DFN Co-Simulation + External Thermal — Effect of Macro-Step Size\",\n", + " fontsize=12,\n", + ")\n", + "plt.tight_layout()\n", + "plt.show()\n" + ] + }, + { + "cell_type": "markdown", + "id": "6101e515", + "metadata": {}, + "source": [ + "## Summary\n", + "\n", + "| Block | Model | Thermal |\n", + "|-------|-------|---------|\n", + "| `CellElectrothermal` | SPMe / SPM (ODE) | Built-in |\n", + "| `CellCoSimElectrothermal` | Any incl. DFN (DAE) | Built-in |\n", + "| `CellElectrical` | SPMe / SPM (ODE) | External (`LumpedThermal`) |\n", + "| `CellCoSimElectrical` | Any incl. DFN (DAE) | External (`LumpedThermal`) |\n", + "\n", + "- DFN resolves spatial gradients in both phases but produces a DAE — use the co-simulation blocks.\n", + "- A smaller macro-step `dt` reduces ZOH error at the cost of more PyBaMM `step()` calls.\n", + "- The external thermal loop extends naturally to multi-cell pack models.\n" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "default (3.13.7)", + "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.13.7" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} From a5d4e70cb46d71f82feefb7ed1ee06977becfb11 Mon Sep 17 00:00:00 2001 From: David Straub Date: Tue, 5 May 2026 22:45:59 +0200 Subject: [PATCH 2/2] Remove unused import --- docs/source/examples/01_spme_discharge.ipynb | 1 - docs/source/examples/02_electrothermal_coupling.ipynb | 1 - docs/source/examples/03_dfn_cosimulation.ipynb | 1 - 3 files changed, 3 deletions(-) diff --git a/docs/source/examples/01_spme_discharge.ipynb b/docs/source/examples/01_spme_discharge.ipynb index 8056e4f..d1545e2 100644 --- a/docs/source/examples/01_spme_discharge.ipynb +++ b/docs/source/examples/01_spme_discharge.ipynb @@ -45,7 +45,6 @@ "metadata": {}, "outputs": [], "source": [ - "import numpy as np\n", "import matplotlib.pyplot as plt\n", "\n", "from pathsim import Simulation, Connection\n", diff --git a/docs/source/examples/02_electrothermal_coupling.ipynb b/docs/source/examples/02_electrothermal_coupling.ipynb index 29c164c..94c2b0e 100644 --- a/docs/source/examples/02_electrothermal_coupling.ipynb +++ b/docs/source/examples/02_electrothermal_coupling.ipynb @@ -38,7 +38,6 @@ "metadata": {}, "outputs": [], "source": [ - "import numpy as np\n", "import matplotlib.pyplot as plt\n", "\n", "from pathsim import Simulation, Connection\n", diff --git a/docs/source/examples/03_dfn_cosimulation.ipynb b/docs/source/examples/03_dfn_cosimulation.ipynb index 3bbd6c3..904deb4 100644 --- a/docs/source/examples/03_dfn_cosimulation.ipynb +++ b/docs/source/examples/03_dfn_cosimulation.ipynb @@ -37,7 +37,6 @@ "metadata": {}, "outputs": [], "source": [ - "import numpy as np\n", "import matplotlib.pyplot as plt\n", "import pybamm\n", "\n",