Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
497909a
Add error
pipliggins Oct 13, 2025
f46fb35
Add test
pipliggins Oct 13, 2025
494e2a0
Add to changelog, edit message
pipliggins Oct 13, 2025
4d54de9
Merge branch 'develop' into 5200-multi-inputs-experiments
pipliggins Oct 27, 2025
32e3138
Merge branch 'develop' into 5200-multi-inputs-experiments
pipliggins Nov 3, 2025
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
c9b7c67
Merge branch 'develop' into 5200-multi-inputs-experiments
pipliggins Nov 21, 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
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@

## 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 +21,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 All @@ -31,6 +34,7 @@

- Set `zip(..., strict=True)` in solver and expression tree files to ensure iterable length safety. ([#5241](https://github.com/pybamm-team/PyBaMM/pull/5241))
- Adds `options` property to IDAKLU, fixes pickling issue with `__getstate__` when keys are not available. ([#5234](https://github.com/pybamm-team/PyBaMM/pull/5234))
- Fixed a bug where no error was raised if a list of input sets were provided to the solver while Experiments were being used ([#5226](https://github.com/pybamm-team/PyBaMM/pull/5226))
- Fixed a bug where simulations using output variables in `IDAKLUSolver` couldn't be pickled ([#5225](https://github.com/pybamm-team/PyBaMM/pull/5225))
- Added explicit warning in installation docs about unmaintained Conda recipe due to pybammsolvers split (Fixes #5155). See pull request [#5206](https://github.com/pybamm-team/PyBaMM/pull/5206)
- Fixed a bug where time-based Heaviside or modulo discontinuities could trigger out-of-bounds errors in time arrays. ([#5205](https://github.com/pybamm-team/PyBaMM/pull/5205))
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
6 changes: 6 additions & 0 deletions src/pybamm/simulation.py
Original file line number Diff line number Diff line change
Expand Up @@ -547,6 +547,12 @@ def solve(

elif self.operating_mode == "with experiment":
callbacks.on_experiment_start(logs)

if isinstance(inputs, list):
raise pybamm.SolverError(
"Solving with a list of input sets is not supported with experiments."
)

self.build_for_experiment(
initial_soc=initial_soc,
direction=direction,
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
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
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
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
14 changes: 14 additions & 0 deletions tests/unit/test_simulation.py
Original file line number Diff line number Diff line change
Expand Up @@ -952,3 +952,17 @@ def test_simulation_cannot_force_calc_esoh(self):
UserWarning, match="Model is not suitable for calculating eSOH"
):
sim.solve([0, 1], calc_esoh=True)

def test_error_solve_with_multiple_inputs_and_experiment(self):
experiment = pybamm.Experiment([("Charge at 1C for 1 hour")])
model = pybamm.lithium_ion.SPM()
sim = pybamm.Simulation(model, experiment=experiment)
parameter_loop = [
{"Current function [A]": 2},
{"Current function [A]": 4},
]
with pytest.raises(
pybamm.SolverError,
match="list of input sets is not supported with experiments",
):
sim.solve(inputs=parameter_loop)
Loading