Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
7bb4d99
Don't be too strict with func_args longer than symbol.children
agriyakhetarpal Nov 13, 2025
56bd16f
Add a test
agriyakhetarpal Nov 13, 2025
e538149
Merge branch 'develop' into roundtrip-serialisation-fix
agriyakhetarpal Nov 14, 2025
425af11
Add support for uniform grid sizing across subdomains (#720) (#5253)
swastim01 Nov 15, 2025
d24a728
Merge branch 'develop' into roundtrip-serialisation-fix
agriyakhetarpal Nov 15, 2025
6ba2cde
Fix typo in Butler-Volmer equation docstring (#5279)
cnaples79 Nov 16, 2025
e893156
fix bug with bulk ocp lithiation (#5280)
rtimms Nov 18, 2025
6c8cbfd
doc: fix typo in concentration description in notebook (#5284)
gregordecristoforo Nov 19, 2025
af64ddd
Merge pull request #5276 from pybamm-team/main
valentinsulzer Nov 19, 2025
385ae7b
fix: instruct uv to install into system for CI (#5288)
pipliggins Nov 20, 2025
1319b4a
Fix `InputParameter` serialisation (#5289)
MarcBerliner Nov 20, 2025
eda4ebe
Merge branch 'develop' into roundtrip-serialisation-fix
valentinsulzer Nov 20, 2025
86fec31
Merge pull request #5274 from agriyakhetarpal/roundtrip-serialisation…
valentinsulzer Nov 20, 2025
80e1870
Bugfix: inputs for `initial_conditions_from` scale evaluation (#5285)
BradyPlanden Nov 21, 2025
73ab559
Add `silence_sundials_errors` solver option (#5290)
BradyPlanden Nov 24, 2025
28db6dd
add changelog entry
swastim01 Nov 4, 2025
978b152
refactor(test_parameters): removes duplication of ndarray checks via …
swastim01 Nov 26, 2025
56ef676
removed unintended changelog change
swastim01 Nov 26, 2025
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
2 changes: 1 addition & 1 deletion .github/workflows/benchmark_on_push.yml
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ jobs:
enable-cache: true

- name: Install python dependencies
run: uv pip install asv[virtualenv]
run: uv pip install --system asv

- name: Fetch base branch
run: |
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/periodic_benchmarks.yml
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ jobs:
enable-cache: true

- name: Install python dependencies
run: uv pip install asv[virtualenv]
run: uv pip install --system asv

- name: Run benchmarks
run: |
Expand Down
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,12 @@

## Features

- Adds `silence_sundials_errors` IDAKLU solver option with `default=False` to match historical output. ([#5290](https://github.com/pybamm-team/PyBaMM/pull/5290))

## Bug fixes

- Fix a bug with serialising `InputParameter`s. ([#5289](https://github.com/pybamm-team/PyBaMM/pull/5289))

# [v25.10.1](https://github.com/pybamm-team/PyBaMM/tree/v25.10.1) - 2025-11-14

## Features
Expand All @@ -19,6 +23,7 @@

## Features

- Added uniform grid sizing across subdomains in the x-dimension, ensuring consistent grid spacing when geometries have varying lengths. ([#5253](https://github.com/pybamm-team/PyBaMM/pull/5253))
- Added the `electrode_phases` kwarg to `plot_voltage_components()` which allows choosing between plotting primary or secondary phase overpotentials. ([#5229](https://github.com/pybamm-team/PyBaMM/pull/5229))
- Added the `num_steps_no_progress` and `t_no_progress` options in the `IDAKLUSolver` to early terminate the simulation if little progress is detected. ([#5201](https://github.com/pybamm-team/PyBaMM/pull/5201))
- EvaluateAt symbol: add support for children evaluated at edges ([#5190](https://github.com/pybamm-team/PyBaMM/pull/5190))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
"$$\n",
"\\left.c\\right\\vert_{t=0} = c_0,\n",
"$$\n",
"where $c$$ is the concentration, $r$ the radial coordinate, $t$ time, $R$ the particle radius, $D$ the diffusion coefficient, $j$ the interfacial current density, $F$ Faraday's constant, and $c_0$ the initial concentration. \n",
"where $c$ is the concentration, $r$ the radial coordinate, $t$ time, $R$ the particle radius, $D$ the diffusion coefficient, $j$ the interfacial current density, $F$ Faraday's constant, and $c_0$ the initial concentration. \n",
"\n",
"As in the previous example we use the following parameters:\n",
"\n",
Expand Down
2 changes: 2 additions & 0 deletions src/pybamm/expression_tree/operations/serialise.py
Original file line number Diff line number Diff line change
Expand Up @@ -1625,6 +1625,8 @@ def convert_symbol_from_json(json_data):
elif json_data["type"] == "Parameter":
# Convert stored parameters back to PyBaMM Parameter objects
return pybamm.Parameter(json_data["name"])
elif json_data["type"] == "InputParameter":
return pybamm.InputParameter(json_data["name"])
elif json_data["type"] == "Scalar":
# Convert stored numerical values back to PyBaMM Scalar objects
return pybamm.Scalar(json_data["value"])
Expand Down
33 changes: 33 additions & 0 deletions src/pybamm/meshes/meshes.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,39 @@
import pybamm


def compute_var_pts_from_thicknesses(electrode_thicknesses, grid_size):
"""
Compute a ``var_pts`` dictionary using electrode thicknesses and a target cell size (dx).

Added as per maintainer feedback in issue #<your-issue-number> to make mesh generation
explicit — ``grid_size`` now represents the mesh cell size in metres.

Parameters
----------
electrode_thicknesses : dict
Domain thicknesses in metres.
grid_size : float
Desired uniform mesh cell size (m).

Returns
-------
dict
Mapping of each domain to its computed grid points.
"""
if not isinstance(electrode_thicknesses, dict):
raise TypeError("electrode_thicknesses must be a dictionary")

if not isinstance(grid_size, (int | float)) or grid_size <= 0:
raise ValueError("grid_size must be a positive number")

var_pts = {}
for domain, thickness in electrode_thicknesses.items():
npts = max(round(thickness / grid_size), 2)
var_pts[domain] = {f"x_{domain[0]}": npts}

return var_pts


class Mesh(dict):
"""
Mesh contains a list of submeshes on each subdomain.
Expand Down
11 changes: 9 additions & 2 deletions src/pybamm/models/base_model.py
Original file line number Diff line number Diff line change
Expand Up @@ -906,7 +906,12 @@ def _build_model(self):
self.build_model_equations()

def set_initial_conditions_from(
self, solution, inplace=True, return_type="model", mesh=None
self,
solution,
inputs=None,
inplace=True,
return_type="model",
mesh=None,
):
"""
Update initial conditions with the final states from a Solution object or from
Expand All @@ -918,6 +923,8 @@ def set_initial_conditions_from(
----------
solution : :class:`pybamm.Solution`, or dict
The solution to use to initialize the model
inputs : dict
The dictionary of model input parameters.
inplace : bool, optional
Whether to modify the model inplace or create a new model (default True)
return_type : str, optional
Expand Down Expand Up @@ -1081,7 +1088,7 @@ def get_variable_state(var):
scale, reference = pybamm.Scalar(1), pybamm.Scalar(0)
initial_conditions[var] = (
pybamm.Vector(final_state_eval) - reference
) / scale.evaluate()
) / scale.evaluate(inputs=inputs)

# Also update the concatenated initial conditions if the model is already
# discretised
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -561,14 +561,14 @@ def _set_up_solve(self, inputs, direction):
def _solve_full(self, inputs, ics, direction):
sim = self._get_electrode_soh_sims_full(direction)
sim.build()
sim.built_model.set_initial_conditions_from(ics)
sim.built_model.set_initial_conditions_from(ics, inputs=inputs)
sol = sim.solve([0], inputs=inputs)
return sol

def _solve_split(self, inputs, ics, direction):
x100_sim, x0_sim = self._get_electrode_soh_sims_split(direction)
x100_sim.build()
x100_sim.built_model.set_initial_conditions_from(ics)
x100_sim.built_model.set_initial_conditions_from(ics, inputs=inputs)
x100_sol = x100_sim.solve([0], inputs=inputs)
if self.options["open-circuit potential"] == "MSMR":
inputs["Un(x_100)"] = x100_sol["Un(x_100)"].data[0]
Expand All @@ -577,7 +577,7 @@ def _solve_split(self, inputs, ics, direction):
inputs["x_100"] = x100_sol["x_100"].data[0]
inputs["y_100"] = x100_sol["y_100"].data[0]
x0_sim.build()
x0_sim.built_model.set_initial_conditions_from(ics)
x0_sim.built_model.set_initial_conditions_from(ics, inputs=inputs)
x0_sol = x0_sim.solve([0], inputs=inputs)

return x0_sol
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ class SymmetricButlerVolmer(BaseKinetics):
Submodel which implements the symmetric forward Butler-Volmer equation:

.. math::
j = 2 * j_0(c) * \\sinh(ne * F * \\eta_r(c) / RT)
j = 2 * j_0(c) * \\sinh(ne * F * \\eta_r(c) / 2RT)

Parameters
----------
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ def _get_coupled_variables(self, variables):
U_eq = self.phase_param.U(sto_surf, T)
U_eq_x_av = self.phase_param.U(sto_surf, T)
U_lith = self.phase_param.U(sto_surf, T, "lithiation")
U_lith_bulk = self.phase_param.U(sto_bulk, T_bulk)
U_lith_bulk = self.phase_param.U(sto_bulk, T_bulk, "lithiation")
U_delith = self.phase_param.U(sto_surf, T, "delithiation")
U_delith_bulk = self.phase_param.U(sto_bulk, T_bulk, "delithiation")

Expand Down
13 changes: 11 additions & 2 deletions src/pybamm/parameters/parameter_values.py
Original file line number Diff line number Diff line change
Expand Up @@ -866,12 +866,21 @@ def _process_function_parameter(self, symbol):
else:
new_children.append(self.process_symbol(child))

# Get the expression and inputs for the function
# Get the expression and inputs for the function.
# func_args may include arguments that were not explicitly wired up
# in this FunctionParameter (e.g., kwargs with default values). After
# serialisation/deserialisation, we only recover the children that were
# actually connected.
#
# Using strict=True here therefore raises a ValueError when there are
# more args than children. We allow func_args to be longer than
# symbol.children and only build the mapping for the args for which we
# actually have children.
expression = function_parameter.child
inputs = {
arg: child
for arg, child in zip(
function_parameter.func_args, symbol.children, strict=True
function_parameter.func_args, symbol.children, strict=False
)
}

Expand Down
4 changes: 3 additions & 1 deletion src/pybamm/solvers/base_solver.py
Original file line number Diff line number Diff line change
Expand Up @@ -1381,7 +1381,9 @@ def step(

else:
_, concatenated_initial_conditions = model.set_initial_conditions_from(
old_solution, return_type="ics"
old_solution,
inputs=model_inputs,
return_type="ics",
)
model.y0 = concatenated_initial_conditions.evaluate(0, inputs=model_inputs)
if using_sensitivities:
Expand Down
3 changes: 3 additions & 0 deletions src/pybamm/solvers/idaklu_solver.py
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,8 @@ class IDAKLUSolver(pybamm.BaseSolver):
"increment_factor": 1.0,
# Enable or disable linear solution scaling
"linear_solution_scaling": True,
# Silence Sundials errors during solve
"silence_sundials_errors": False,
## Main solver
# Maximum order of the linear multistep method
"max_order_bdf": 5,
Expand Down Expand Up @@ -176,6 +178,7 @@ def __init__(
"epsilon_linear_tolerance": 0.05,
"increment_factor": 1.0,
"linear_solution_scaling": True,
"silence_sundials_errors": False,
"max_order_bdf": 5,
"max_num_steps": 100000,
"dt_init": 0.0,
Expand Down
31 changes: 31 additions & 0 deletions tests/unit/test_meshes/test_meshes.py
Original file line number Diff line number Diff line change
Expand Up @@ -584,6 +584,37 @@ def test_to_json(self):

assert mesh_json == expected_json

def test_compute_var_pts_from_thicknesses_cell_size(self):
from pybamm.meshes.meshes import compute_var_pts_from_thicknesses

electrode_thicknesses = {
"negative electrode": 100e-6,
"separator": 25e-6,
"positive electrode": 100e-6,
}

cell_size = 5e-6 # 5 micrometres per cell
var_pts = compute_var_pts_from_thicknesses(electrode_thicknesses, cell_size)

assert isinstance(var_pts, dict)
assert all(isinstance(v, dict) for v in var_pts.values())
assert var_pts["negative electrode"]["x_n"] == 20
assert var_pts["separator"]["x_s"] == 5
assert var_pts["positive electrode"]["x_p"] == 20

def test_compute_var_pts_from_thicknesses_invalid_thickness_type(self):
from pybamm.meshes.meshes import compute_var_pts_from_thicknesses

with pytest.raises(TypeError):
compute_var_pts_from_thicknesses(["not", "a", "dict"], 1e-6)

def test_compute_var_pts_from_thicknesses_invalid_grid_size(self):
from pybamm.meshes.meshes import compute_var_pts_from_thicknesses

electrode_thicknesses = {"negative electrode": 100e-6}
with pytest.raises(ValueError):
compute_var_pts_from_thicknesses(electrode_thicknesses, -1e-6)


class TestMeshGenerator:
def test_init_name(self):
Expand Down
16 changes: 16 additions & 0 deletions tests/unit/test_parameters/conftest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import numpy as np
import pytest


@pytest.fixture
def assert_is_ndarray():
"""Recursively assert that all items in a structure are numpy arrays."""

def _assert(obj):
if isinstance(obj, list | tuple):
for item in obj:
_assert(item)
else:
assert isinstance(obj, np.ndarray)

return _assert
25 changes: 25 additions & 0 deletions tests/unit/test_parameters/test_parameter_values.py
Original file line number Diff line number Diff line change
Expand Up @@ -1298,6 +1298,31 @@ def test_to_json_with_filename(self):
finally:
os.remove(temp_path)

def test_roundtrip_with_keyword_args(self):
def func_no_kwargs(x):
return 2 * x

def func_with_kwargs(x, y=1):
return 2 * x

x = pybamm.Scalar(2)
func_param = pybamm.FunctionParameter("func", {"x": x})

parameter_values = pybamm.ParameterValues({"func": func_no_kwargs})
assert parameter_values.evaluate(func_param) == 4.0

serialized = parameter_values.to_json()
parameter_values_loaded = pybamm.ParameterValues.from_json(serialized)
assert parameter_values_loaded.evaluate(func_param) == 4.0

parameter_values = pybamm.ParameterValues({"func": func_with_kwargs})
assert parameter_values.evaluate(func_param) == 4.0

serialized = parameter_values.to_json()
parameter_values_loaded = pybamm.ParameterValues.from_json(serialized)

assert parameter_values_loaded.evaluate(func_param) == 4.0

def test_convert_symbols_in_dict_with_interpolator(self):
"""Test convert_symbols_in_dict with interpolator (covers lines 1154-1170)."""
import numpy as np
Expand Down
16 changes: 5 additions & 11 deletions tests/unit/test_parameters/test_process_parameter_data.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@

from pathlib import Path

import numpy as np
import pytest

import pybamm
Expand Down Expand Up @@ -34,18 +33,13 @@ def test_processed_name(self, parameter_data):
name, processed = parameter_data
assert processed[0] == name

def test_processed_structure(self, parameter_data):
name, processed = parameter_data
assert isinstance(processed[1], tuple)
assert isinstance(processed[1][0][0], np.ndarray)
assert isinstance(processed[1][1], np.ndarray)
def test_processed_structure(self, parameter_data, assert_is_ndarray):
_, processed = parameter_data

if len(processed[1][0]) > 1:
assert isinstance(processed[1][0][1], np.ndarray)
assert isinstance(processed[1], tuple)

elif len(processed[1]) == 3:
assert isinstance(processed[1][0][1], np.ndarray)
assert isinstance(processed[1][0][2], np.ndarray)
# Recursively check that all numpy arrays exist where expected
assert_is_ndarray(processed[1])

def test_error(self):
with pytest.raises(FileNotFoundError, match="Could not find file"):
Expand Down
8 changes: 8 additions & 0 deletions tests/unit/test_serialisation/test_serialisation.py
Original file line number Diff line number Diff line change
Expand Up @@ -617,6 +617,14 @@ def test_serialise_time(self):
t2 = convert_symbol_from_json(j)
assert isinstance(t2, pybamm.Time)

def test_serialise_input_parameter(self):
"""Test InputParameter serialization and deserialization."""
ip = pybamm.InputParameter("test_param")
j = convert_symbol_to_json(ip)
ip_restored = convert_symbol_from_json(j)
assert isinstance(ip_restored, pybamm.InputParameter)
assert ip_restored.name == "test_param"

def test_convert_symbol_to_json_with_number_and_list(self):
for val in (0, 3.14, -7, True):
out = convert_symbol_to_json(val)
Expand Down