Source code for pybop.models.lithium_ion.utils
from collections.abc import Callable
from typing import TYPE_CHECKING
import numpy as np
from pybamm import Interpolant as PybammInterpolant
from scipy import interpolate
if TYPE_CHECKING:
from pybop.optimisers.base_optimiser import BaseOptimiser, OptimiserOptions
from pybop.costs.design_cost import DesignCost
from pybop.optimisers.scipy_optimisers import SciPyMinimize
from pybop.parameters.parameter import Parameter, Parameters
from pybop.problems.problem import Problem
from pybop.simulators.base_simulator import BaseSimulator
from pybop.simulators.solution import Solution
[docs]
class Interpolant:
"""
A class that returns a pybamm.Interpolant to pybamm models and otherwise
a numeric interpolant.
Parameters
----------
x : array_like
Input coordinates.
y : array_like
Output values corresponding to x.
name : str, optional
Name for the interpolant when used in PyBaMM.
bounds_error : bool, optional
If True, raise error when interpolating outside bounds.
fill_value : str or float, optional
Value to use for out-of-bounds interpolation.
axis : int, optional
Axis along which to interpolate.
"""
def __init__(
self,
x: np.ndarray,
y: np.ndarray,
name: str | None = None,
bounds_error: bool = False,
fill_value: str | float = "extrapolate",
axis: int = 0,
):
self.x = np.asarray(x)
self.y = np.asarray(y)
self.name = name
self._interp_func = self._create_interpolant(bounds_error, fill_value, axis)
[docs]
def _create_interpolant(
self, bounds_error: bool, fill_value: str | float, axis: int
):
"""Create the scipy interpolation function."""
return interpolate.interp1d(
self.x,
self.y,
bounds_error=bounds_error,
fill_value=fill_value,
axis=axis,
)
[docs]
def __call__(self, x: float | np.ndarray):
"""
Evaluate the interpolant at given points.
Parameters
----------
x : float or array_like
Points at which to evaluate the interpolant.
Returns
-------
float, array_like, or pybamm.Interpolant
Interpolated values or PyBaMM interpolant object.
"""
try:
# Try numeric evaluation first
return self._interp_func(x)
except Exception:
# Fall back to PyBaMM interpolant for symbolic evaluation
return PybammInterpolant(self.x, self.y, x, name=self.name)
[docs]
class InverseOCV:
"""
A class to find the stoichiometry corresponding to a given open-circuit
voltage.
Parameters
----------
ocv_function : Callable
The open-circuit voltage as a function of stoichiometry.
optimiser : pybop.BaseOptimiser, optional
The optimisation algorithm to use (default: pybop.SciPyMinimize).
optimiser_options : pybop.OptimiserOptions, optional
Options for the optimiser.
"""
def __init__(
self,
ocv_function: Callable,
optimiser: "BaseOptimiser | None" = None,
optimiser_options: "OptimiserOptions | None" = None,
):
self.optimiser = optimiser or SciPyMinimize
self.optimiser_options = optimiser_options or self.optimiser.default_options()
parameters = Parameters({"Root": Parameter(initial_value=0.5, bounds=[0, 1])})
# Set up a root-finding cost function
class OCVRoot(BaseSimulator):
def __init__(self, ocv_value: float):
super().__init__(parameters=parameters)
self.ocv_value = ocv_value
def solve_batch(self, inputs, calculate_sensitivities: bool = False):
solutions = []
for x in inputs:
diff = np.abs(ocv_function(x["Root"]) - self.ocv_value)
sol = Solution()
sol.set_solution_variable("Difference", data=np.asarray([diff]))
solutions.append(sol)
return solutions
self.ocv_root = OCVRoot
# Minimise to find the stoichiometry
self.cost = DesignCost(target="Difference")
self.cost.minimising = True
[docs]
def __call__(self, ocv_value: float) -> float:
"""
Estimate and return the stoichiometry.
Parameters
----------
ocv_value : float
The open-circuit voltage value [V] for which to estimate the stoichiometry.
Returns
-------
float
The stoichiometry corresponding to the open-circuit voltage value.
"""
problem = Problem(self.ocv_root(ocv_value), self.cost)
optim = self.optimiser(problem, options=self.optimiser_options)
result = optim.run()
return result.best_inputs["Root"]