From 78061bba6178eee8d72e136a83882f66a608aa7b Mon Sep 17 00:00:00 2001 From: Milan Rother Date: Sat, 14 Mar 2026 08:52:09 +0100 Subject: [PATCH 1/7] Refactor cell blocks: remove redundant protocol overrides and type annotations --- src/pathsim_batt/cells/pybamm_cell.py | 187 +++++++++++--------------- tests/cells/test_pybamm_cell.py | 11 +- 2 files changed, 81 insertions(+), 117 deletions(-) diff --git a/src/pathsim_batt/cells/pybamm_cell.py b/src/pathsim_batt/cells/pybamm_cell.py index 970a837..5adf4d8 100644 --- a/src/pathsim_batt/cells/pybamm_cell.py +++ b/src/pathsim_batt/cells/pybamm_cell.py @@ -1,12 +1,23 @@ -from __future__ import annotations - -from typing import Any +######################################################################################### +## +## PyBaMM CELL BLOCKS +## (cells/pybamm_cell.py) +## +## Battery cell blocks wrapping PyBaMM models +## for use in the PathSim simulation framework +## +######################################################################################### + +# IMPORTS =============================================================================== import numpy as np import pybamm + from pathsim.blocks._block import Block +# BASE ================================================================================== + class _CellBase(Block): """Shared base for PyBaMM cell blocks. @@ -17,26 +28,20 @@ class _CellBase(Block): initial_value = 0.0 # sentinel — makes PathSim call step() each cycle - def __init__( - self, - model: pybamm.BaseBatteryModel | None, - parameter_values: pybamm.ParameterValues | None, - initial_soc: float, - solver: pybamm.BaseSolver | None, - output_variables: list[str] | None, - thermal_option: str, - ) -> None: + def __init__(self, model, parameter_values, initial_soc, solver, + output_variables, thermal_option): super().__init__() self._initial_soc = float(initial_soc) self._extra_var_names = list(output_variables or []) - self.extra_outputs: dict[str, float] = {} - self._q_nominal: float | None = None + self.extra_outputs = {} + #model defaults if model is None: model = pybamm.lithium_ion.SPMe(options={"thermal": thermal_option}) self._model = model + #parameter setup — mark current and temperature as runtime inputs if parameter_values is None: parameter_values = pybamm.ParameterValues("Chen2020") parameter_values = parameter_values.copy() @@ -44,95 +49,81 @@ def __init__( parameter_values["Ambient temperature [K]"] = "[input]" self._parameter_values = parameter_values - if solver is None: - solver = pybamm.CasadiSolver(mode="safe") - self._pybamm_solver = solver + #solver + self._pybamm_solver = solver or pybamm.CasadiSolver(mode="safe") - self._sim: pybamm.Simulation | None = None - self._initialized = False + self._sim = None self._stepped = False - # --- PathSim protocol -------------------------------------------------- + # -- PathSim protocol --------------------------------------------------------------- - def __len__(self) -> int: + def __len__(self): return 0 - def set_solver(self, Solver: Any, parent: Any, **solver_args: Any) -> None: - # Create a dummy 1-state engine so PathSim adds this block to its - # dynamic set and calls step() each cycle. The engine state is unused. - if Solver is not None: - self.engine = Solver(np.array([0.0]), **solver_args) - - def reset(self) -> None: - self.inputs.reset() - self.outputs.reset() + def reset(self): + super().reset() self._sim = None - self._initialized = False self._stepped = False self.extra_outputs = {} - def buffer(self, dt: float) -> None: + def buffer(self, dt): super().buffer(dt) self._stepped = False - def step(self, t: float, dt: float) -> tuple[bool, float, float]: - if not self._initialized: - self._initialize() + def step(self, t, dt): + #lazy initialisation on first step + if self._sim is None: + self._sim = pybamm.Simulation( + self._model, + parameter_values=self._parameter_values, + solver=self._pybamm_solver, + ) + self._sim.build(initial_soc=self._initial_soc, inputs=self._pybamm_inputs()) + self._q_nominal = float(self._parameter_values["Nominal cell capacity [A.h]"]) + if not self._stepped: - self._sim.step(dt, inputs=self._build_inputs()) # type: ignore[union-attr] + self._sim.step(dt, inputs=self._pybamm_inputs()) self._stepped = True + return True, 0.0, 1.0 - # --- helpers ----------------------------------------------------------- + # -- helpers ------------------------------------------------------------------------ - def _build_inputs(self) -> dict[str, float]: + def _pybamm_inputs(self): + """Build PyBaMM input dict from current block inputs.""" T = float(self.inputs[1]) or 298.15 return { "Current function [A]": float(self.inputs[0]), "Ambient temperature [K]": T, } - def _compute_soc(self, sol: pybamm.Solution) -> float: + def _read_soc(self, sol): + """Compute state of charge from PyBaMM solution.""" q_dis = float(sol["Discharge capacity [A.h]"].entries[-1]) - return float(max(0.0, min(1.0, self._initial_soc - q_dis / self._q_nominal))) # type: ignore[operator] - - def _solution_ready(self) -> bool: - return ( - self._initialized - and self._sim is not None - and self._sim.solution is not None - ) - - def _initialize(self) -> None: - self._sim = pybamm.Simulation( - self._model, - parameter_values=self._parameter_values, - solver=self._pybamm_solver, - ) - self._sim.build(initial_soc=self._initial_soc, inputs=self._build_inputs()) - self._q_nominal = float(self._parameter_values["Nominal cell capacity [A.h]"]) - self._initialized = True + return max(0.0, min(1.0, self._initial_soc - q_dis / self._q_nominal)) + +# BLOCKS ================================================================================ class CellElectrical(_CellBase): """Cell block — electrical outputs only, external thermal coupling. PyBaMM runs with an isothermal assumption; PathSim is responsible for integrating the cell temperature ODE. Wire ``Q_heat`` to a - ``LumpedThermalModel`` (or similar) and feed its temperature output back + ``LumpedThermal`` (or similar) and feed its temperature output back to ``T_cell``. Parameters ---------- - model : + model : pybamm.BaseBatteryModel or None PyBaMM lithium-ion model. Defaults to ``SPMe(thermal="isothermal")``. - parameter_values : + parameter_values : pybamm.ParameterValues or None PyBaMM parameter set. Defaults to ``Chen2020``. - initial_soc : + initial_soc : float Initial state of charge (0–1). Default 1.0. - solver : + solver : pybamm.BaseSolver or None PyBaMM solver. Defaults to ``CasadiSolver(mode="safe")``. - output_variables : + output_variables : list[str] or None Extra PyBaMM variable names stored in ``block.extra_outputs`` after each step. @@ -151,30 +142,18 @@ class CellElectrical(_CellBase): input_port_labels = {"I": 0, "T_cell": 1} output_port_labels = {"V": 0, "Q_heat": 1, "SOC": 2} - def __init__( - self, - model: pybamm.BaseBatteryModel | None = None, - parameter_values: pybamm.ParameterValues | None = None, - initial_soc: float = 1.0, - solver: pybamm.BaseSolver | None = None, - output_variables: list[str] | None = None, - ) -> None: - super().__init__( - model, - parameter_values, - initial_soc, - solver, - output_variables, - thermal_option="isothermal", - ) - - def update(self, t: float) -> None: - if not self._solution_ready(): + def __init__(self, model=None, parameter_values=None, initial_soc=1.0, + solver=None, output_variables=None): + super().__init__(model, parameter_values, initial_soc, solver, + output_variables, thermal_option="isothermal") + + def update(self, t): + if self._sim is None or self._sim.solution is None: return - sol = self._sim.solution # type: ignore[union-attr] + sol = self._sim.solution self.outputs[0] = float(sol["Terminal voltage [V]"].entries[-1]) self.outputs[1] = float(sol["X-averaged total heating [W.m-3]"].entries[-1]) - self.outputs[2] = self._compute_soc(sol) + self.outputs[2] = self._read_soc(sol) for name in self._extra_var_names: self.extra_outputs[name] = float(sol[name].entries[-1]) @@ -188,15 +167,15 @@ class CellElectrothermal(_CellBase): Parameters ---------- - model : + model : pybamm.BaseBatteryModel or None PyBaMM lithium-ion model. Defaults to ``SPMe(thermal="lumped")``. - parameter_values : + parameter_values : pybamm.ParameterValues or None PyBaMM parameter set. Defaults to ``Chen2020``. - initial_soc : + initial_soc : float Initial state of charge (0–1). Default 1.0. - solver : + solver : pybamm.BaseSolver or None PyBaMM solver. Defaults to ``CasadiSolver(mode="safe")``. - output_variables : + output_variables : list[str] or None Extra PyBaMM variable names stored in ``block.extra_outputs`` after each step. @@ -216,31 +195,19 @@ class CellElectrothermal(_CellBase): input_port_labels = {"I": 0, "T_amb": 1} output_port_labels = {"V": 0, "T": 1, "Q_heat": 2, "SOC": 3} - def __init__( - self, - model: pybamm.BaseBatteryModel | None = None, - parameter_values: pybamm.ParameterValues | None = None, - initial_soc: float = 1.0, - solver: pybamm.BaseSolver | None = None, - output_variables: list[str] | None = None, - ) -> None: - super().__init__( - model, - parameter_values, - initial_soc, - solver, - output_variables, - thermal_option="lumped", - ) - - def update(self, t: float) -> None: - if not self._solution_ready(): + def __init__(self, model=None, parameter_values=None, initial_soc=1.0, + solver=None, output_variables=None): + super().__init__(model, parameter_values, initial_soc, solver, + output_variables, thermal_option="lumped") + + def update(self, t): + if self._sim is None or self._sim.solution is None: return - sol = self._sim.solution # type: ignore[union-attr] + sol = self._sim.solution self.outputs[0] = float(sol["Terminal voltage [V]"].entries[-1]) self.outputs[1] = float(sol["X-averaged cell temperature [K]"].entries[-1]) self.outputs[2] = float(sol["X-averaged total heating [W.m-3]"].entries[-1]) - self.outputs[3] = self._compute_soc(sol) + self.outputs[3] = self._read_soc(sol) for name in self._extra_var_names: self.extra_outputs[name] = float(sol[name].entries[-1]) diff --git a/tests/cells/test_pybamm_cell.py b/tests/cells/test_pybamm_cell.py index 95c849c..6150fd9 100644 --- a/tests/cells/test_pybamm_cell.py +++ b/tests/cells/test_pybamm_cell.py @@ -56,14 +56,12 @@ def test_custom_soc(self): def test_reset_clears_state(self): for cls in (CellElectrical, CellElectrothermal): cell = cls() - cell._initialized = True cell._sim = object() cell.reset() - self.assertFalse(cell._initialized) self.assertIsNone(cell._sim) -def _advance(cell, dt: float) -> None: +def _advance(cell, dt): """Simulate one PathSim timestep: buffer → step → update.""" cell.buffer(dt) cell.step(0.0, dt) @@ -79,9 +77,8 @@ def setUp(self): self.cell.inputs[1] = 298.15 # T_cell def test_lazy_init_on_first_step(self): - self.assertFalse(self.cell._initialized) + self.assertIsNone(self.cell._sim) _advance(self.cell, 1.0) - self.assertTrue(self.cell._initialized) self.assertIsNotNone(self.cell._sim) def test_outputs_in_range(self): @@ -115,9 +112,9 @@ def setUp(self): self.cell.inputs[1] = 298.15 # T_amb def test_lazy_init_on_first_step(self): - self.assertFalse(self.cell._initialized) + self.assertIsNone(self.cell._sim) _advance(self.cell, 1.0) - self.assertTrue(self.cell._initialized) + self.assertIsNotNone(self.cell._sim) def test_outputs_in_range(self): _advance(self.cell, 1.0) From 0fbaf780b2018e4a50a3162b8060b0079cf5a325 Mon Sep 17 00:00:00 2001 From: Milan Rother Date: Sat, 14 Mar 2026 08:52:13 +0100 Subject: [PATCH 2/7] Add section headers and match core code style in LumpedThermal --- src/pathsim_batt/thermal/lumped.py | 38 ++++++++++++++++++------------ 1 file changed, 23 insertions(+), 15 deletions(-) diff --git a/src/pathsim_batt/thermal/lumped.py b/src/pathsim_batt/thermal/lumped.py index 4f101f7..8a3164f 100644 --- a/src/pathsim_batt/thermal/lumped.py +++ b/src/pathsim_batt/thermal/lumped.py @@ -1,10 +1,21 @@ -from __future__ import annotations +######################################################################################### +## +## LUMPED THERMAL MODEL +## (thermal/lumped.py) +## +## Single-node thermal model for battery cell temperature +## +######################################################################################### + +# IMPORTS =============================================================================== import numpy as np -from numpy.typing import NDArray + from pathsim.blocks import DynamicalSystem +# BLOCKS ================================================================================ + class LumpedThermal(DynamicalSystem): """Single-node lumped thermal model. @@ -36,30 +47,27 @@ class LumpedThermal(DynamicalSystem): input_port_labels = {"Q_dot": 0, "T_amb": 1} output_port_labels = {"T": 0} - def __init__( - self, - mass: float = 0.065, - Cp: float = 750.0, - UA: float = 0.5, - T0: float = 298.15, - ) -> None: + def __init__(self, mass=0.065, Cp=750.0, UA=0.5, T0=298.15): + + #input validation if mass <= 0: - raise ValueError(f"mass must be positive, got {mass}") + raise ValueError(f"'mass' must be positive but is {mass}") if Cp <= 0: - raise ValueError(f"Cp must be positive, got {Cp}") + raise ValueError(f"'Cp' must be positive but is {Cp}") if UA < 0: - raise ValueError(f"UA must be non-negative, got {UA}") + raise ValueError(f"'UA' must be non-negative but is {UA}") + #store parameters self.mass = float(mass) self.Cp = float(Cp) self.UA = float(UA) - def _fn_d(x: NDArray, u: NDArray, _t: float) -> NDArray: - (T,) = x + def _fn_d(x, u, t): + T, = x Q_dot, T_amb = u return np.array([(Q_dot - self.UA * (T - T_amb)) / (self.mass * self.Cp)]) - def _fn_a(x: NDArray, _u: NDArray, _t: float) -> NDArray: + def _fn_a(x, u, t): return x.copy() super().__init__( From 2427bfbd698027cff6cb97e17724586c36cb74dd Mon Sep 17 00:00:00 2001 From: Milan Rother Date: Sat, 14 Mar 2026 08:52:18 +0100 Subject: [PATCH 3/7] Add LICENSE, trim .gitignore, remove pre-commit config --- .gitignore | 174 +++++----------------------------------- .pre-commit-config.yaml | 7 -- LICENSE | 21 +++++ 3 files changed, 39 insertions(+), 163 deletions(-) delete mode 100644 .pre-commit-config.yaml create mode 100644 LICENSE diff --git a/.gitignore b/.gitignore index 356c995..148ba07 100644 --- a/.gitignore +++ b/.gitignore @@ -1,9 +1,6 @@ -# setuptools-scm generated version file -_version.py - # Byte-compiled / optimized / DLL files __pycache__/ -*.py[codz] +*.py[cod] *$py.class # C extensions @@ -23,15 +20,12 @@ parts/ sdist/ var/ wheels/ -share/python-wheels/ *.egg-info/ .installed.cfg *.egg -MANIFEST +*.pypirc # PyInstaller -# Usually these files are written by a python script from a template -# before PyInstaller builds the exe, so as to inject date/other infos into it. *.manifest *.spec @@ -42,178 +36,46 @@ pip-delete-this-directory.txt # Unit test / coverage reports htmlcov/ .tox/ -.nox/ .coverage .coverage.* .cache nosetests.xml coverage.xml *.cover -*.py.cover .hypothesis/ +test_reports/ +*.pyc .pytest_cache/ -cover/ - -# Translations -*.mo -*.pot - -# Django stuff: -*.log -local_settings.py -db.sqlite3 -db.sqlite3-journal - -# Flask stuff: -instance/ -.webassets-cache - -# Scrapy stuff: -.scrapy - -# Sphinx documentation -docs/_build/ - -# PyBuilder -.pybuilder/ -target/ # Jupyter Notebook .ipynb_checkpoints -# IPython -profile_default/ -ipython_config.py - # pyenv -# For a library or package, you might want to ignore these files since the code is -# intended to run in multiple environments; otherwise, check them in: -# .python-version - -# pipenv -# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. -# However, in case of collaboration, if having platform-specific dependencies or dependencies -# having no cross-platform support, pipenv may install dependencies that don't work, or not -# install all needed dependencies. -# Pipfile.lock - -# UV -# Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control. -# This is especially recommended for binary packages to ensure reproducibility, and is more -# commonly ignored for libraries. -# uv.lock - -# poetry -# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. -# This is especially recommended for binary packages to ensure reproducibility, and is more -# commonly ignored for libraries. -# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control -# poetry.lock -# poetry.toml - -# pdm -# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. -# pdm recommends including project-wide configuration in pdm.toml, but excluding .pdm-python. -# https://pdm-project.org/en/latest/usage/project/#working-with-version-control -# pdm.lock -# pdm.toml -.pdm-python -.pdm-build/ - -# pixi -# Similar to Pipfile.lock, it is generally recommended to include pixi.lock in version control. -# pixi.lock -# Pixi creates a virtual environment in the .pixi directory, just like venv module creates one -# in the .venv directory. It is recommended not to include this directory in version control. -.pixi - -# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm -__pypackages__/ - -# Celery stuff -celerybeat-schedule -celerybeat.pid +.python-version -# Redis -*.rdb -*.aof -*.pid - -# RabbitMQ -mnesia/ -rabbitmq/ -rabbitmq-data/ - -# ActiveMQ -activemq-data/ - -# SageMath parsed files -*.sage.py - -# Environments +# Environment .env -.envrc .venv env/ venv/ ENV/ -env.bak/ -venv.bak/ -# Spyder project settings -.spyderproject -.spyproject +# IDEs +.vscode/ +.idea/ -# Rope project settings -.ropeproject +# OS generated files +.DS_Store +Thumbs.db -# mkdocs documentation -/site +# Claude +.claude + +# setuptools-scm +*_version.py # mypy .mypy_cache/ -.dmypy.json -dmypy.json - -# Pyre type checker -.pyre/ - -# pytype static type analyzer -.pytype/ -# Cython debug symbols -cython_debug/ - -# PyCharm -# JetBrains specific template is maintained in a separate JetBrains.gitignore that can -# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore -# and can be added to the global gitignore or merged into this file. For a more nuclear -# option (not recommended) you can uncomment the following to ignore the entire idea folder. -# .idea/ - -# Abstra -# Abstra is an AI-powered process automation framework. -# Ignore directories containing user credentials, local state, and settings. -# Learn more at https://abstra.io/docs -.abstra/ - -# Visual Studio Code -# Visual Studio Code specific template is maintained in a separate VisualStudioCode.gitignore -# that can be found at https://github.com/github/gitignore/blob/main/Global/VisualStudioCode.gitignore -# and can be added to the global gitignore or merged into this file. However, if you prefer, -# you could uncomment the following to ignore the entire vscode folder -# .vscode/ - -# Ruff stuff: +# ruff .ruff_cache/ - -# PyPI configuration file -.pypirc - -# Marimo -marimo/_static/ -marimo/_lsp/ -__marimo__/ - -# Streamlit -.streamlit/secrets.toml diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml deleted file mode 100644 index d4de6b0..0000000 --- a/.pre-commit-config.yaml +++ /dev/null @@ -1,7 +0,0 @@ -repos: - - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.9.10 - hooks: - - id: ruff - args: [--fix] - - id: ruff-format diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..bbb7ceb --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 PathSim + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. From 1e377ed248b83514087b23494f629bb05efecb31 Mon Sep 17 00:00:00 2001 From: Milan Rother Date: Sat, 14 Mar 2026 08:52:21 +0100 Subject: [PATCH 4/7] Add project URLs and docs dep, fix README dead link --- README.md | 2 -- pyproject.toml | 15 ++++++++++++--- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 97125b5..24b364e 100644 --- a/README.md +++ b/README.md @@ -110,8 +110,6 @@ Sim.run(3600) scope.plot() ``` -Runnable scripts are in [`examples/`](examples/). - ## Thermal coupling modes | Mode | Block | Owns cell temperature | Use when | diff --git a/pyproject.toml b/pyproject.toml index 3ecb781..2174310 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -22,7 +22,6 @@ classifiers = [ "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", - "Programming Language :: Python :: 3.14", "Topic :: Scientific/Engineering", ] dependencies = [ @@ -35,10 +34,20 @@ test = [ "pytest>=7.0", "pytest-cov>=4.0", ] -lint = [ - "ruff>=0.9", +docs = [ + "sphinx>=7.0", + "furo", + "myst-parser", + "sphinx-copybutton", + "sphinx-design", ] +[project.urls] +Homepage = "https://pathsim.org" +Documentation = "https://docs.pathsim.org/batt" +Repository = "https://github.com/pathsim/pathsim-batt" +Issues = "https://github.com/pathsim/pathsim-batt/issues" + [tool.setuptools] package-dir = {"" = "src"} From c02d2f40e8907c5acae553fcb6708385cea23916 Mon Sep 17 00:00:00 2001 From: Milan Rother Date: Sat, 14 Mar 2026 08:54:28 +0100 Subject: [PATCH 5/7] Streamline README to match other toolbox repos --- README.md | 143 ++++++++++-------------------------------------------- 1 file changed, 26 insertions(+), 117 deletions(-) diff --git a/README.md b/README.md index 24b364e..3a2715e 100644 --- a/README.md +++ b/README.md @@ -1,138 +1,47 @@ -# pathsim-batt -Battery simulation blocks for [PathSim](https://github.com/pathsim/pathsim), using [PyBaMM](https://pybamm.org) as the electrochemical backend. +

+ Battery simulation blocks for PathSim +

-## Installation +

+ License +

-```bash -pip install pathsim-batt -``` +

+ Documentation • + PathSim Homepage • + GitHub +

-## Blocks +--- -### `CellElectrothermal` +PathSim-Batt extends the [PathSim](https://github.com/pathsim/pathsim) simulation framework with battery cell blocks using [PyBaMM](https://pybamm.org) as the electrochemical backend. All blocks follow the standard PathSim block interface and can be connected into simulation diagrams. -Coupled electrical + thermal cell. PyBaMM's built-in lumped thermal model integrates the cell temperature ODE internally. Supply ambient temperature to couple to a pack-level thermal model. +## Blocks -``` -Inputs: I [A], T_amb [K] -Outputs: V [V], T [K], Q_heat [W/m³], SOC -``` +| Block | Description | Key Parameters | +|-------|-------------|----------------| +| `CellElectrothermal` | Coupled electrical + thermal cell (PyBaMM owns temperature ODE) | `model`, `parameter_values`, `initial_soc` | +| `CellElectrical` | Electrical only, isothermal (PathSim owns temperature ODE) | `model`, `parameter_values`, `initial_soc` | +| `LumpedThermal` | Single-node thermal model for external thermal coupling | `mass`, `Cp`, `UA`, `T0` | `Cell` is an alias for `CellElectrothermal`. -### `CellElectrical` - -Electrical outputs only. PyBaMM runs isothermal at the temperature you supply. Use this when PathSim owns the thermal dynamics (via `LumpedThermal` or a more complex thermal network). - -``` -Inputs: I [A], T_cell [K] -Outputs: V [V], Q_heat [W/m³], SOC -``` - -### `LumpedThermal` - -Single-node thermal model: `m·Cₚ·dT/dt = Q̇ − UA·(T − T_amb)` - -``` -Inputs: Q_dot [W], T_amb [K] -Outputs: T [K] -``` - -## Examples - -### 1 — Simple discharge (`CellElectrothermal`) - -```python -import pybamm -from pathsim import Simulation, Connection -from pathsim.blocks import Constant, Scope -from pathsim.solvers import SSPRK22 -from pathsim_batt import CellElectrothermal - -cell = CellElectrothermal(parameter_values=pybamm.ParameterValues("Chen2020")) -I_app = Constant(5.0) # 1 C -T_amb = Constant(298.15) -scope = Scope(labels=["V [V]", "SOC"]) - -Sim = Simulation( - blocks=[cell, I_app, T_amb, scope], - connections=[ - Connection(I_app, cell["I"]), - Connection(T_amb, cell["T_amb"]), - Connection(cell["V"], scope[0]), - Connection(cell["SOC"], scope[1]), - ], - dt=10.0, Solver=SSPRK22, -) - -Sim.run(3600) -scope.plot() -``` - -### 2 — External thermal coupling (`CellElectrical` + `LumpedThermal`) - -When you want PathSim to own the thermal ODE — for example to couple multiple cells to a shared cooling model: - -```python -import numpy as np, pybamm -from pathsim import Simulation, Connection -from pathsim.blocks import Constant, Scope, Amplifier -from pathsim.solvers import SSPRK22 -from pathsim_batt import CellElectrical -from pathsim_batt.thermal import LumpedThermal - -cell_volume = np.pi * 0.0105**2 * 0.070 # m³ (LG M50 21700) - -cell = CellElectrical(parameter_values=pybamm.ParameterValues("Chen2020")) -thermal = LumpedThermal(mass=0.065, Cp=750.0, UA=0.5, T0=298.15) -gain = Amplifier(cell_volume) # W/m³ → W -I_app = Constant(5.0) -T_amb = Constant(298.15) -scope = Scope(labels=["V [V]", "T [K]", "SOC"]) - -Sim = Simulation( - blocks=[cell, thermal, gain, I_app, T_amb, scope], - connections=[ - Connection(I_app, cell["I"]), - Connection(thermal["T"], cell["T_cell"]), # feedback - Connection(cell["Q_heat"], gain), - Connection(gain, thermal["Q_dot"]), - Connection(T_amb, thermal["T_amb"]), - Connection(cell["V"], scope[0]), - Connection(thermal["T"], scope[1]), - Connection(cell["SOC"], scope[2]), - ], - dt=10.0, Solver=SSPRK22, -) - -Sim.run(3600) -scope.plot() -``` +Any PyBaMM battery model and parameter set can be injected. Extra PyBaMM output variables are accessible via `block.extra_outputs`. ## Thermal coupling modes | Mode | Block | Owns cell temperature | Use when | |---|---|---|---| | Internal | `CellElectrothermal` | PyBaMM | Single-cell simulations, quick setup | -| External | `CellElectrical` + `LumpedThermal` | PathSim | Multi-cell packs, custom cooling models, different thermal timestep | +| External | `CellElectrical` + `LumpedThermal` | PathSim | Multi-cell packs, custom cooling models | -## Model and parameter set selection +## Install -Any PyBaMM battery model and parameter set can be injected: - -```python -model = pybamm.lithium_ion.DFN(options={"thermal": "lumped"}) -params = pybamm.ParameterValues("Mohtat2020") -cell = CellElectrothermal(model=model, parameter_values=params) +```bash +pip install pathsim-batt ``` -Extra PyBaMM output variables are accessible via `block.extra_outputs`: +## License -```python -cell = CellElectrothermal( - output_variables=["X-averaged negative particle surface concentration [mol.m-3]"] -) -# after stepping: -c_neg = cell.extra_outputs["X-averaged negative particle surface concentration [mol.m-3]"] -``` +MIT From 77f59039914c33e34b48c998301e6f0f49408ac2 Mon Sep 17 00:00:00 2001 From: Milan Rother Date: Sat, 14 Mar 2026 08:55:19 +0100 Subject: [PATCH 6/7] Add PyBaMM integration section to README --- README.md | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 3a2715e..5096822 100644 --- a/README.md +++ b/README.md @@ -27,7 +27,22 @@ PathSim-Batt extends the [PathSim](https://github.com/pathsim/pathsim) simulatio `Cell` is an alias for `CellElectrothermal`. -Any PyBaMM battery model and parameter set can be injected. Extra PyBaMM output variables are accessible via `block.extra_outputs`. +## PyBaMM integration + +The cell blocks wrap [PyBaMM](https://pybamm.org) models behind the PathSim block interface. PyBaMM handles the electrochemistry (SPMe, DFN, ...) while PathSim handles the system-level simulation loop, connections, and time stepping. + +- **Any PyBaMM model** can be injected via the `model` parameter +- **Any parameter set** can be used via `parameter_values` (defaults to `Chen2020`) +- **Extra output variables** from PyBaMM are accessible via `block.extra_outputs` +- **Lazy initialisation** — the PyBaMM simulation is only built on the first timestep + +```python +import pybamm + +model = pybamm.lithium_ion.DFN(options={"thermal": "lumped"}) +params = pybamm.ParameterValues("Mohtat2020") +cell = CellElectrothermal(model=model, parameter_values=params) +``` ## Thermal coupling modes From 492b189278eda4883f88334a12abc67f58b2077b Mon Sep 17 00:00:00 2001 From: Milan Rother <105657697+milanofthe@users.noreply.github.com> Date: Tue, 17 Mar 2026 10:21:51 +0100 Subject: [PATCH 7/7] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- src/pathsim_batt/cells/pybamm_cell.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/pathsim_batt/cells/pybamm_cell.py b/src/pathsim_batt/cells/pybamm_cell.py index 5adf4d8..07f62e6 100644 --- a/src/pathsim_batt/cells/pybamm_cell.py +++ b/src/pathsim_batt/cells/pybamm_cell.py @@ -36,12 +36,12 @@ def __init__(self, model, parameter_values, initial_soc, solver, self._extra_var_names = list(output_variables or []) self.extra_outputs = {} - #model defaults + # model defaults if model is None: model = pybamm.lithium_ion.SPMe(options={"thermal": thermal_option}) self._model = model - #parameter setup — mark current and temperature as runtime inputs + # parameter setup — mark current and temperature as runtime inputs if parameter_values is None: parameter_values = pybamm.ParameterValues("Chen2020") parameter_values = parameter_values.copy() @@ -49,7 +49,7 @@ def __init__(self, model, parameter_values, initial_soc, solver, parameter_values["Ambient temperature [K]"] = "[input]" self._parameter_values = parameter_values - #solver + # solver self._pybamm_solver = solver or pybamm.CasadiSolver(mode="safe") self._sim = None @@ -71,7 +71,7 @@ def buffer(self, dt): self._stepped = False def step(self, t, dt): - #lazy initialisation on first step + # lazy initialisation on first step if self._sim is None: self._sim = pybamm.Simulation( self._model,