Source code for ampworks.dqdv._lam_lli

from __future__ import annotations

from typing import TYPE_CHECKING

import numpy as np
import pandas as pd
import matplotlib.pyplot as plt

if TYPE_CHECKING:  # pragma: no cover
    from ._tables import DegModeTable, DqdvFitTable


[docs] def calc_lam_lli(fit_table: DqdvFitTable) -> pd.DataFrame: r""" Calculate degradation modes. Uses full cell capacity and fitted x0/x1 values from dqdv/dvdq fits to calculate theoretical electrode capacities, loss of active material (LAM), and loss of lithium inventory (LLI). The calculations are summarized below. For more detail and discussion, please refer to [1]_. Electrode capacities (Q) and loss of active material (LAM) are .. math:: Q_{ed} = \frac{\rm capacity}{x_{1,ed} - x_{0,ed}}, \quad \quad {\rm LAM}_{ed} = 1 - \frac{Q_{ed}}{Q_{ed}[0]}, where :math:`ed` is generic for 'electrode'. Outputs use 'n' and 'p' to differentiate between negative and positive electrodes, respectively. Loss of lithium inventory (LLI) is .. math:: {\rm Inv} = x_{0,n}Q_{n} + (1 - x_{0,p})Q_{p}, \quad \quad {\rm LLI} = 1 - \frac{\rm Inv}{\rm Inv[0]}, where :math:`{\rm Inv}` is the total lithium inventories using capacities :math:`Q` from above. If standard deviations of the x0/x1 stoichiometries are available in `results` (and are not NaN), then they are propagated to give uncertainty estimates for the LAM/LLI values. Reported uncertainties come from first-order Taylor series assumptions. If you trust your x0/x1 fits but see large or inconsistent uncertainties then it is also safe to trust the LAM/LLI values, but you may want to neglect LAM/LLI uncertainties. Note that `(1 - xp0)` is used instead of just `xp0` because x0 refers to the delithiated state of the positive electrode whereas `xn0` refers to the lithiated state of the negative electrode, and does not require the same inversion. Parameters ---------- fit_table : DqdvFitTable Table containing rows for fitted x0/x1 values from dqdv/dvdq fits. Returns ------- deg_table : DegModeTable Electrode capacities (Q) and loss of active material (LAM) for the negative (n) and positive (p) electrodes, and loss of lithium inventory (LLI). Capacities are in Ah. All other outputs are unitless. See Also -------- ~ampworks.dqdv.DqdvFitter : Access to fitting routines. ~ampworks.dqdv.DegModeTable : Table of calculated degradation modes. References ---------- .. [1] A. Weng, J. B. Siegel, and A. Stefanopoulou, "Differential voltage analysis for battery manufacturing process control," Frontiers in Energy Research, 2023, DOI: 10.3389/fenrg.2023.1087269 """ from ampworks.dqdv._tables import DegModeTable df = fit_table.df.copy() extra_cols = fit_table._extra_cols Ah = df.Ah.to_numpy() xn0, xn0_std = df.xn0.to_numpy(), df.xn0_std.to_numpy() xn1, xn1_std = df.xn1.to_numpy(), df.xn1_std.to_numpy() xp0, xp0_std = df.xp0.to_numpy(), df.xp0_std.to_numpy() xp1, xp1_std = df.xp1.to_numpy(), df.xp1_std.to_numpy() Qn = Ah / (xn1 - xn0) Qp = Ah / (xp1 - xp0) dQn = Ah / (xn1 - xn0)**2 # ignore lead -1 for xn1 b/c **2 below Qn_std = np.sqrt((dQn*xn1_std)**2 + (dQn*xn0_std)**2) dQp = Ah / (xp1 - xp0)**2 # ignore lead -1 for xp1 b/c **2 below Qp_std = np.sqrt((dQp*xp1_std)**2 + (dQp*xp0_std)**2) LAMn = 1. - Qn / Qn[0] LAMp = 1. - Qp / Qp[0] LAMn_std = Qn_std / Qn[0] LAMp_std = Qp_std / Qp[0] inv = xn0*Qn + (1. - xp0)*Qp LLI = 1. - inv / inv[0] inv_std = np.sqrt( ((Qn + xn0*dQn)*xn0_std)**2 # contribution from xn0 + ((xn0*dQn)*xn1_std)**2 # contribution from xn1 + ((-Qp + (1. - xp0)*dQp)*xp0_std)**2 # contribution from xp0 + (((1. - xp0)*dQp)*xn1_std)**2 # contribution from xp1 ) LLI_std = inv_std / inv[0] aging = pd.DataFrame({ 'Qn': Qn, 'Qn_std': Qn_std, 'Qp': Qp, 'Qp_std': Qp_std, 'Qc': Ah, 'LAMn': LAMn, 'LAMn_std': LAMn_std, 'LAMp': LAMp, 'LAMp_std': LAMp_std, 'LLI': LLI, 'LLI_std': LLI_std, }) for col in extra_cols: aging[col] = df[col] return DegModeTable(aging)
[docs] def plot_lam_lli( deg_table: DegModeTable, x_col: str | None = None, std: bool = False, return_axs: bool = False, ) -> np.ndarray[plt.Axes] | None: """ Plot degradation modes. Parameters ---------- deg_table : DegModeTable Container holding calculated degradation modes (LAM and LLI). x_col : str | None, optional A column name from 'fit_table` to use for the x-axis. If None (default) then the row indices are used. std : bool, optional Include shaded regions for estimated standard deviations of the LAM and LLI values when True. Default is False. return_axs : bool, optional If True (default), return the axes objects for the capacity, LAM, and LLI plots. Otherwise, returns None. Returns ------- axes : np.ndarray[plt.Axes] or None A 2x3 axes object containing the capacity, LAM, and LLI plots. This is only returned if `return_axs=True`, otherwise None. See Also -------- ~ampworks.dqdv.DqdvFitter : Access to the fitting routines. ~ampworks.dqdv.DegModeTable : Table of calculated degradation modes. ~ampworks.dqdv.calc_lam_lli : Calculate degradation modes before plottting. """ from ampworks.utils import _ExitHandler from ampworks.plotutils import format_ticks df = deg_table.df.copy() if x_col is None: xplt, xlabel = df.index, 'Index' else: xplt, xlabel = df[x_col], x_col.capitalize() shaded = {'alpha': 0.2, 'color': 'C0'} _, axs = plt.subplots( 2, 3, figsize=[9.0, 3.75], sharex=True, constrained_layout=True, ) df.plot( x_col, ['Qn', 'Qp', 'Qc', 'LAMn', 'LAMp', 'LLI'], subplots=True, color='C0', legend=False, xlabel=xlabel, ax=axs.flatten(), ) # first row: Qn, Qp, Qc if std: Qn, Qn_std = df[['Qn', 'Qn_std']].T.to_numpy() axs[0, 0].fill_between(xplt, Qn - Qn_std, Qn + Qn_std, **shaded) Qp, Qp_std = df[['Qp', 'Qp_std']].T.to_numpy() axs[0, 1].fill_between(xplt, Qp - Qp_std, Qp + Qp_std, **shaded) axs[0, 0].set_ylabel(r'$Q_{\rm NE}$ [Ah]') axs[0, 1].set_ylabel(r'$Q_{\rm PE}$ [Ah]') axs[0, 2].set_ylabel(r'$Q_{\rm cell}$ [Ah]') # second row: LAMn, LAMp, LLI if std: LAM, LAM_std = df[['LAMn', 'LAMn_std']].T.to_numpy() axs[1, 0].fill_between(xplt, LAM - LAM_std, LAM + LAM_std, **shaded) LAM, LAM_std = df[['LAMp', 'LAMp_std']].T.to_numpy() axs[1, 1].fill_between(xplt, LAM - LAM_std, LAM + LAM_std, **shaded) LLI, LLI_std = df[['LLI', 'LLI_std']].T.to_numpy() axs[1, 2].fill_between(xplt, LLI - LLI_std, LLI + LLI_std, **shaded) axs[1, 0].set_ylabel(r'LAM$_{\rm NE}$ [$-$]') axs[1, 1].set_ylabel(r'LAM$_{\rm PE}$ [$-$]') axs[1, 2].set_ylabel(r'LLI [$-$]') # formatting format_ticks(axs, xdiv=2, ydiv=2) _ExitHandler.register_atexit(plt.show) if return_axs: return axs