Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
45 commits
Select commit Hold shift + click to select a range
5a63105
Add GroupedSPMe voltage components
NicolaCourtier Jan 6, 2026
97d82e8
Change parameter grouping
NicolaCourtier Jan 7, 2026
5e236e2
Add minimum electrolyte events
NicolaCourtier Jan 7, 2026
9a56fa7
Add GroupedDFN
NicolaCourtier Jan 7, 2026
7d6e401
Add voltage components
NicolaCourtier Jan 7, 2026
f92085d
Add set_initial_state
NicolaCourtier Jan 8, 2026
b19d6fd
Add SPM voltage components
NicolaCourtier Jan 8, 2026
2a7e631
Merge branch 'develop' into grouped-voltage-components
NicolaCourtier Feb 18, 2026
5df89f9
Finish merging
NicolaCourtier Feb 18, 2026
aec20e8
Merge branch 'develop' into grouped-voltage-components
NicolaCourtier Mar 2, 2026
8c7bfff
Merge branch 'develop' into grouped-voltage-components
NicolaCourtier Mar 5, 2026
f57d55b
Merge branch 'develop' into grouped-voltage-components
NicolaCourtier Mar 17, 2026
2030fa3
Enable sto-dependent diffusion timescales
NicolaCourtier Mar 17, 2026
a965c28
Merge branch 'develop' into grouped-voltage-components
NicolaCourtier Mar 19, 2026
ee636b5
Pass ambient temperature
NicolaCourtier Mar 30, 2026
e2cf809
Merge branch 'develop' into grouped-voltage-components
NicolaCourtier Apr 14, 2026
6af3375
Merge branch 'develop' into grouped-voltage-components
NicolaCourtier May 15, 2026
129b632
Merge branch 'develop' into grouped-voltage-components
NicolaCourtier Aug 6, 2026
7ae8baf
Update event tolerance
NicolaCourtier May 15, 2026
b8d1a7a
Simplify open-circuit voltage
NicolaCourtier May 15, 2026
faa5fa0
Enable alpha not 0.5
NicolaCourtier May 28, 2026
1447a01
Refactor Butler Volmer
NicolaCourtier May 28, 2026
21fb48f
Move exchange rate to parameter set
NicolaCourtier May 28, 2026
27920e3
Add Butler-Volmer variations
NicolaCourtier Jun 3, 2026
3abd714
Squeeze array to get singular cost
NicolaCourtier Jun 4, 2026
25a4eb4
Test simulation of experiment
NicolaCourtier Aug 6, 2026
2b7d0ef
Get reaction rates from exchange current functions
NicolaCourtier Jun 10, 2026
1bb821e
Update plot variables
NicolaCourtier Jun 12, 2026
c5786e7
Update test
NicolaCourtier Jul 14, 2026
2d7590b
Update parameter setting
NicolaCourtier Jul 14, 2026
0e31769
Use positive domain name
NicolaCourtier Jul 20, 2026
a7cd2cd
Fix OCP averaging
NicolaCourtier Aug 6, 2026
f93e554
Allow diffusivity function of temperature
NicolaCourtier Aug 6, 2026
1196176
Create alternative_functions.py
NicolaCourtier Aug 6, 2026
1a2678d
Delay DFN
NicolaCourtier Aug 6, 2026
5b8a72f
Update CHANGELOG.md
NicolaCourtier Aug 6, 2026
73233c3
Update description
NicolaCourtier Aug 6, 2026
90e58d0
Add unit test
NicolaCourtier Aug 7, 2026
227ec5c
Move Butler-Volmer functions to base model
NicolaCourtier Aug 7, 2026
ee1285a
Fix reaction rate calculation
NicolaCourtier Aug 7, 2026
eef65c0
Switch reaction rate to reference current
NicolaCourtier Aug 7, 2026
abc079c
Rename prefactor to r2_scale
NicolaCourtier Aug 17, 2026
d028501
Move alternative_functions.py
NicolaCourtier Aug 17, 2026
70f13cf
Separate half-cell models
NicolaCourtier Aug 17, 2026
9f84322
Reset set_initial_state and use A.h
NicolaCourtier Aug 17, 2026
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

## Features

- [#974](https://github.com/pybop-team/PyBOP/pull/974) - Adds voltage components to each grouped model as well as asymmetric and multiphase Butler-Volmer kinetics.
- [#969](https://github.com/pybop-team/PyBOP/pull/969) - Updates synthetic data and adds example script for thermal parameterisation.
- [#965](https://github.com/pybop-team/PyBOP/pull/965) - Adds synthetic data and example scripts for OCV parameterisation.
- [#963](https://github.com/pybop-team/PyBOP/pull/963) - Adds an example for generating synthetic data from a specification and exporting it to a PyProBE-compatible parquet file.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -223,7 +223,7 @@
"id": "12",
"metadata": {},
"source": [
"We can construct the model and parameter values for a two-RC circuit model with initial values as listed below. Note, the \"Initial SOC\" is shifted slightly to better match the zero degree data."
"We can construct the model and parameter values for a two-RC circuit model with initial values as listed below. Note, the \"Initial SoC\" is shifted slightly to better match the zero degree data."
]
},
{
Expand Down
10 changes: 6 additions & 4 deletions examples/scripts/battery_parameterisation/gitt_fitting.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@
)

# Group the parameters
grouped_parameter_values = pybop.lithium_ion.SPDiffusion.create_grouped_parameters(
grouped_parameter_values = pybop.li_half_cell.SPDiffusion.create_grouped_parameters(
parameter_values
)

Expand All @@ -60,11 +60,13 @@
gitt_parameter_data = gitt_fit()

# Plot the functional parameters
pybop.plot.dataset(gitt_parameter_data, signal=["Particle diffusion time scale [s]"])
pybop.plot.dataset(
gitt_parameter_data, signal=["Positive particle diffusion time scale [s]"]
)
pybop.plot.dataset(gitt_parameter_data, signal=["Series resistance [Ohm]"])

# Run the identified model
identified_model = pybop.lithium_ion.SPDiffusion(build=True)
identified_model = pybop.li_half_cell.SPDiffusion(build=True)
grouped_parameter_values.update(gitt_fit.best_inputs)
grouped_parameter_values["Current function [A]"] = pybamm.Interpolant(
dataset["Time [s]"], dataset["Current [A]"], pybamm.t
Expand All @@ -76,7 +78,7 @@
# Return to the original model and update the diffusivity value
diffusivity = np.mean(
parameter_values["Positive particle radius [m]"] ** 2
/ gitt_parameter_data["Particle diffusion time scale [s]"]
/ gitt_parameter_data["Positive particle diffusion time scale [s]"]
)
parameter_values.update({"Positive particle diffusivity [m2.s-1]": diffusivity})

Expand Down
2 changes: 1 addition & 1 deletion examples/scripts/battery_parameterisation/gitt_pulse.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@
)

# Group the parameters
grouped_parameter_values = pybop.lithium_ion.SPDiffusion.create_grouped_parameters(
grouped_parameter_values = pybop.li_half_cell.SPDiffusion.create_grouped_parameters(
parameter_values
)

Expand Down
27 changes: 15 additions & 12 deletions examples/scripts/comparison_examples/gitt_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
import pybamm

import pybop
from pybop.models.li_half_cell import SPDiffusion, WeppnerHuggins

# Define model and parameter values
model_options = {"working electrode": "positive"}
Expand Down Expand Up @@ -30,13 +31,13 @@
}
)

for model in [pybop.lithium_ion.WeppnerHuggins(), pybop.lithium_ion.SPDiffusion()]:
for model in [WeppnerHuggins(), SPDiffusion()]:
# GITT target parameter
diffusion_parameter = pybop.Parameter(pybop.Gaussian(5000, 1000))
if isinstance(model, pybop.lithium_ion.WeppnerHuggins):
if isinstance(model, WeppnerHuggins):
# Group parameter values
grouped_parameter_values = (
pybop.lithium_ion.WeppnerHuggins.create_grouped_parameters(parameter_values)
grouped_parameter_values = WeppnerHuggins.create_grouped_parameters(
parameter_values
)

# We can fit only the duration of the pulse
Expand All @@ -49,7 +50,7 @@
dataset["Discharge capacity [A.h]"][-1]
- dataset["Discharge capacity [A.h]"][0]
)
* (grouped_parameter_values["Theoretical electrode capacity [A.s]"] / 3600)
* grouped_parameter_values["Theoretical electrode capacity [A.h]"]
)
grouped_parameter_values.update(
{
Expand All @@ -61,7 +62,7 @@
# Fitting parameters
grouped_parameter_values.update(
{
"Particle diffusion time scale [s]": diffusion_parameter,
"Positive particle diffusion time scale [s]": diffusion_parameter,
"Reference voltage [V]": pybop.Parameter(
initial_value=grouped_parameter_values["Reference voltage [V]"],
),
Expand All @@ -70,14 +71,14 @@

else:
# Group parameter values
grouped_parameter_values = (
pybop.lithium_ion.SPDiffusion.create_grouped_parameters(parameter_values)
grouped_parameter_values = SPDiffusion.create_grouped_parameters(
parameter_values
)

# Fitting parameters
grouped_parameter_values.update(
{
"Particle diffusion time scale [s]": diffusion_parameter,
"Positive particle diffusion time scale [s]": diffusion_parameter,
"Series resistance [Ohm]": pybop.Parameter(
initial_value=grouped_parameter_values["Series resistance [Ohm]"],
),
Expand All @@ -87,7 +88,7 @@
# Build the problem
gitt_dataset = (
dataset.get_subset(pulse_index)
if isinstance(model, pybop.lithium_ion.WeppnerHuggins)
if isinstance(model, WeppnerHuggins)
else dataset
)
simulator = pybop.pybamm.Simulator(
Expand All @@ -97,13 +98,15 @@
problem = pybop.Problem(simulator, cost)

# Build the optimisation problem
optim = pybop.SciPyMinimize(problem)
options = pybop.SciPyMinimizeOptions(method="Nelder-Mead")
optim = pybop.SciPyMinimize(problem, options)

# Run the optimisation problem
result = optim.run()
print(result)
print(
"Diffusion time [s]:", result.best_inputs["Particle diffusion time scale [s]"]
"Diffusion time [s]:",
result.best_inputs["Positive particle diffusion time scale [s]"],
)

# Plot the timeseries output
Expand Down
2 changes: 1 addition & 1 deletion examples/scripts/dfn_parameterisation/3_cell_balancing.py
Original file line number Diff line number Diff line change
Expand Up @@ -354,7 +354,7 @@ def solve_batch(self, inputs, calculate_sensitivities: bool = False):
"Maximum negative stoichiometry": x_100,
"Minimum positive stoichiometry": y_100,
"Maximum positive stoichiometry": y_0,
"Measured cell capacity [A.s]": Q_soc / CE * 3600,
"Measured cell capacity [A.h]": Q_soc / CE,
"Coulombic efficiency": CE,
"Negative electrode pOCP [V]": negative_ocp_function,
"Positive electrode pOCP [V]": positive_ocp_function,
Expand Down
1 change: 1 addition & 0 deletions pybop/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@
# Model classes
#
from .models import lithium_ion
from .models import li_half_cell
from .models._exponential_decay import ExponentialDecayModel
from .models.lithium_ion.utils import Interpolant, InverseOCV

Expand Down
24 changes: 16 additions & 8 deletions pybop/applications/gitt_methods.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,15 +36,17 @@ def __init__(
):
self.parameter_values = parameter_values.copy()
self.parameters = {
"Particle diffusion time scale [s]": pybop.Parameter(bounds=[0, np.inf]),
"Positive particle diffusion time scale [s]": pybop.Parameter(
bounds=[0, np.inf]
),
"Series resistance [Ohm]": pybop.Parameter(bounds=[0, np.inf]),
}
self.cost = cost or pybop.RootMeanSquaredError
self.optimiser = optimiser or pybop.SciPyMinimize
self.optimiser_options = optimiser_options or self.optimiser.default_options()

# Create model
self.model = pybop.lithium_ion.SPDiffusion()
self.model = pybop.li_half_cell.SPDiffusion()
self.problem = None

def __call__(
Expand Down Expand Up @@ -110,7 +112,9 @@ def __init__(
self.optimiser_options = optimiser_options or self.optimiser.default_options()

# Set up OCV root-finding function
self.inverse_ocp = pybop.InverseOCV(parameter_values["Electrode OCP [V]"])
self.inverse_ocp = pybop.InverseOCV(
parameter_values["Positive electrode OCP [V]"]
)

# Initialise single pulse fitter
self.pulse_fit = GITTPulseFit(
Expand Down Expand Up @@ -148,7 +152,9 @@ def __call__(self) -> pybop.Dataset:
# Log the result
self.pulses.append(pulse_result)
diffusion_time.append(
pulse_result.best_inputs["Particle diffusion time scale [s]"]
pulse_result.best_inputs[
"Positive particle diffusion time scale [s]"
]
)
series_resistance.append(
pulse_result.best_inputs["Series resistance [Ohm]"]
Expand All @@ -169,14 +175,16 @@ def __call__(self) -> pybop.Dataset:
self.parameter_data = pybop.Dataset(
{
"Stoichiometry": np.asarray(stoichiometry),
"Particle diffusion time scale [s]": np.asarray(diffusion_time),
"Positive particle diffusion time scale [s]": np.asarray(
diffusion_time
),
"Series resistance [Ohm]": np.asarray(series_resistance),
cost_name: np.asarray(best_cost),
}
if len(stoichiometry) > 1 and stoichiometry[-1] > stoichiometry[0]
else {
"Stoichiometry": np.flipud(np.asarray(stoichiometry)),
"Particle diffusion time scale [s]": np.flipud(
"Positive particle diffusion time scale [s]": np.flipud(
np.asarray(diffusion_time)
),
"Series resistance [Ohm]": np.flipud(np.asarray(series_resistance)),
Expand All @@ -187,8 +195,8 @@ def __call__(self) -> pybop.Dataset:

# Compute mean values
self.best_inputs = {
"Particle diffusion time scale [s]": np.mean(
self.parameter_data["Particle diffusion time scale [s]"]
"Positive particle diffusion time scale [s]": np.mean(
self.parameter_data["Positive particle diffusion time scale [s]"]
),
"Series resistance [Ohm]": np.mean(
self.parameter_data["Series resistance [Ohm]"]
Expand Down
39 changes: 39 additions & 0 deletions pybop/models/alternative_functions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import pybamm

""" Alternative functions written as classes to allow pickling. """


class FunctionalDiffusionTime:
def __init__(self, r2_scale, D, c_scale):
self.r2_scale = r2_scale
self.D = D
self.c_scale = c_scale

def __call__(self, sto, T):
r2_scale, D, c_scale = self.r2_scale, self.D, self.c_scale
return r2_scale / D(sto * c_scale, T)


class AsymmetricButlerVolmer:
def __init__(self, alpha):
self.alpha = alpha # cathodic transfer coefficient

def __call__(self, sto_surf, sto_e, eta_RT_F):
alpha = self.alpha
j0 = sto_surf**alpha * (sto_e * (1 - sto_surf)) ** (1 - alpha)
return j0 * (pybamm.exp((1 - alpha) * eta_RT_F) - pybamm.exp(-alpha * eta_RT_F))


class MultiphaseButlerVolmer:
def __init__(self, alpha, omega):
self.alpha = alpha
self.omega = omega

def __call__(self, sto_surf, sto_e, eta_RT_F):
alpha, omega = self.alpha, self.omega
j0 = (
sto_surf ** (alpha * omega)
* (1 - sto_surf) ** ((1 - alpha) * omega)
* sto_e ** (1 - alpha)
)
return j0 * (pybamm.exp((1 - alpha) * eta_RT_F) - pybamm.exp(-alpha * eta_RT_F))
5 changes: 5 additions & 0 deletions pybop/models/li_half_cell/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
#
# Import lithium half cell models
#
from .sp_diffusion import SPDiffusion
from .weppner_huggins import WeppnerHuggins
112 changes: 112 additions & 0 deletions pybop/models/li_half_cell/base_model.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
import pybamm
from pybamm import FunctionParameter, Parameter
from pybamm import lithium_ion as pybamm_lithium_ion

from pybop.models.lithium_ion.utils import InverseOCV


class BaseHalfCellModel(pybamm_lithium_ion.BaseModel):
"""
A base model for PyBOP's grouped-parameter lithium-ion half-cell models.

Parameters
----------
name : str, optional
The name of the model.
**model_kwargs : optional
Valid PyBaMM model option keys and their values, for example:
options : dict, optional
A dictionary of options to customise the behaviour of the PyBaMM model.
build : bool, optional
If True, the model is built upon creation (default: False).
"""

def __init__(self, name="Base Model", **model_kwargs):
super().__init__(name=name, **model_kwargs)

def build_model(self):
"""
Build model variables and equations
Credit: PyBaMM
"""
self._build_model()

self._built = True
pybamm.logger.info(f"Finish building {self.name}")

@staticmethod
def set_initial_state(
initial_value,
parameter_values,
direction=None,
param=None,
inplace=True,
options=None,
inputs=None,
tol=1e-6,
):
"""
Set the value of the initial state of charge.

Parameters
----------
initial_value : float
Target initial value.
If float, interpreted as stoichiometry, must be between 0 and 1.
If string e.g. "4 V", interpreted as voltage, must be between V_min and V_max.
parameter_values : :class:`pybamm.ParameterValues`
Parameters and their corresponding values.
param : :class:`pybamm.LithiumIonParameters`, optional
The symbolic parameter set to use for the simulation.
If not provided, the default parameter set will be used.
inplace: bool, optional
If True, replace the parameters values in place. Otherwise, return a new set of
parameter values. Default is True.
options : dict-like, optional
A dictionary of options to be passed to the model, see
:class:`pybamm.BatteryModelOptions`.
inputs : dict, optional
A dictionary of input parameters to pass to the model when solving.
tol : float, optional
The tolerance for the solver used to compute the initial stoichiometries.
A lower value results in higher precision but may increase computation time.
Default is 1e-6.
"""
parameter_values = parameter_values if inplace else parameter_values.copy()

if isinstance(initial_value, str) and initial_value.endswith("V"):
V_init = float(initial_value[:-1])
V_min = parameter_values.evaluate(
Parameter("Lower voltage cut-off [V]"), inputs=inputs
)
V_max = parameter_values.evaluate(
Parameter("Upper voltage cut-off [V]"), inputs=inputs
)

if not V_min - tol <= V_init <= V_max + tol:
raise ValueError(
f"Initial voltage {V_init}V is outside the voltage limits ({V_min}, {V_max})."
)

def ocv_function(sto_p):
U_p = FunctionParameter(
"Positive electrode OCP [V]",
{"Positive particle stoichiometry": sto_p},
)
return parameter_values.evaluate(U_p, inputs=inputs).squeeze()

inverse_ocv = InverseOCV(ocv_function)
sto_p = inverse_ocv(V_init)

elif isinstance(initial_value, int | float):
sto_p = initial_value

else:
raise ValueError("Initial value must be a float or a string ending in 'V'.")

if not 0 <= sto_p <= 1:
raise ValueError("Initial stoichiometry should be between 0 and 1.")

parameter_values["Initial stoichiometry"] = sto_p

return parameter_values
Loading
Loading