Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
218 changes: 218 additions & 0 deletions docs/source/examples/01_spme_discharge.ipynb
Original file line number Diff line number Diff line change
@@ -0,0 +1,218 @@
{
"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 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]\nT_amb0 = 298.15 # [K]\n\ncell = CellElectrothermal(initial_soc=1.0)\nI_src = Constant(1.0 * C_nom)\nT_src = Constant(T_amb0)\nsco = Scope(labels=[\"V\", \"T\", \"Q_dot\", \"SOC\"])\n\nsim = 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_dot\"], sco[2]),\n Connection(cell[\"SOC\"], sco[3]),\n ],\n dt=10.0,\n Solver=ESDIRK43,\n)\n\nsim.run(3600.0)\nt, [V, T, Q_dot, 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
}
161 changes: 161 additions & 0 deletions docs/source/examples/02_electrothermal_coupling.ipynb
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
{
"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$$\nm C_p \\frac{dT}{dt} = \\dot{Q} - UA\\,(T - T_{\\mathrm{amb}})\n$$\n\n`CellElectrical` outputs total heat generation [W], which `LumpedThermal` accepts\ndirectly — no unit bridging needed.\n"
},
{
"cell_type": "code",
"execution_count": null,
"id": "1d33ed8e",
"metadata": {},
"outputs": [],
"source": [
"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]\nI_discharge = 1.0 * C_nom\nT_amb0 = 298.15 # [K]\n\nmass = 0.065 # [kg]\nCp = 750.0 # [J kg⁻¹ K⁻¹]\nUA = 0.5 # [W K⁻¹]\n\ncell = CellElectrical(initial_soc=1.0)\nthermal = LumpedThermal(mass=mass, Cp=Cp, UA=UA, T0=T_amb0)\nI_src = Constant(I_discharge)\nT_src = Constant(T_amb0)\nsco = Scope(labels=[\"V\", \"SOC\", \"T_cell\"])\n\nsim = 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_dot\"], 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\nsim.run(1800.0)\nt, [V, SOC, T_cell] = sco.read()\n"
},
{
"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}\ncolors_ua = [\"firebrick\", \"steelblue\", \"teal\"]\nua_results = {}\n\nfor (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, Cp=Cp, UA=ua_val, 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_dot\"], 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- `CellElectrical` outputs total heat [W], which connects directly to `LumpedThermal`'s input — no unit conversion needed.\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
}
Loading
Loading