Source code for ampworks.ocv._match_peaks
from __future__ import annotations
from typing import TYPE_CHECKING
import numpy as np
from scipy.optimize import minimize
from scipy.integrate import cumulative_trapezoid
if TYPE_CHECKING: # pragma: no cover
from ampworks.dqdv import DqdvSpline
[docs]
def match_peaks(
charge: DqdvSpline,
discharge: DqdvSpline,
x0: float | None = None,
display: bool = False,
) -> tuple[float, DqdvSpline]:
"""
Find symmetric `iR` shift to align dQdV peaks.
This function finds the optimal symmetric `iR` shift to apply to charge and
discharge dQdV curves to align their peaks. The shifted curves are then
averaged to produce a final dQdV curve. In addition to the `iR` value, a new
`DqdvSpline` object is returned containing the averaged dQdV curve. Use this
function to estimate an open-circuit voltage curve from low-rate charge and
discharge data.
Parameters
----------
charge : DqdvSpline
A fitted dQdV spline object for low-rate charge data.
discharge : DqdvSpline
A fitted dQdV spline object for low-rate discharge data.
x0 : float | None, optional
Initial guess for the symmetric iR shift, by default None, which uses a
heuristic based on peak locations.
display : bool, optional
Whether to display intermediate optimization results, by default False.
Returns
-------
iR, spline : tuple[float, DqdvSpline]
The optimal symmetric iR shift and the averaged dQdV spline.
See Also
--------
~ampworks.dqdv.DqdvSpline : Smoothing spline for dQdV curves.
"""
import ampworks as amp
# good starting guess
soc = np.linspace(0, 1, 201)
chg = {'Volts': charge.volts_(soc), 'dqdv': charge.dqdv_(soc)}
dis = {'Volts': discharge.volts_(soc), 'dqdv': discharge.dqdv_(soc)}
if x0 is None:
idx1 = np.argmax(np.abs(chg['dqdv']))
idx2 = np.argmax(np.abs(dis['dqdv']))
x0 = 0.5 * (chg['Volts'][idx1] - dis['Volts'][idx2])
# interpolate dqdv expressions at common voltages
Vmin = 0.5 * (np.min(chg['Volts']) + np.min(dis['Volts']))
Vmax = 0.5 * (np.max(chg['Volts']) + np.max(dis['Volts']))
n = np.ceil((Vmax - Vmin) / 1e-3)
volts = np.linspace(Vmin, Vmax, n.astype(int))
def shift_curves(iR: float) -> tuple[np.ndarray, np.ndarray]:
"""Symmetric shift of charge and discharge curves by iR."""
dqdv_chg = np.interp(volts, chg['Volts'] - iR, chg['dqdv'])
dqdv_dis = np.interp(volts, dis['Volts'] + iR, dis['dqdv'])
return dqdv_chg, dqdv_dis
# error function
def errfn(x: float) -> float:
"""Sum of squared differences between shifted curves."""
dqdv_chg, dqdv_dis = shift_curves(x)
return np.linalg.norm(dqdv_chg - dqdv_dis)
bounds = (0., 0.5*np.trapezoid(chg['Volts'] - dis['Volts'], x=soc))
def callback(intermediate_result) -> None:
"""Print intermediate results, both x and function value."""
return print(intermediate_result)
result = minimize(errfn, x0, method='L-BFGS-B', bounds=[bounds],
callback=callback if display else None)
# final approximation by averaging derivatives
dqdv_chg, dqdv_dis = shift_curves(result.x)
dqdv = 0.5 * (dqdv_chg + dqdv_dis)
soc = cumulative_trapezoid(dqdv, volts, initial=0.) # re-normalize
soc = soc / np.max(np.abs(soc))
# fake current and time to construct DqdvSpline output
amps = np.ones(volts.size)
seconds = soc / amps * 3600.
data = amp.Dataset({'Seconds': seconds, 'Amps': amps, 'Volts': volts})
spline = amp.dqdv.DqdvSpline().fit(data)
return result.x.item(), spline