226 lines
10 KiB
Python
226 lines
10 KiB
Python
|
# Paul Gasper, NREL
|
||
|
# This model is replicated as reported by Schmalsteig et al, J. Power Sources 257 (2014) 325-334
|
||
|
# http://dx.doi.org/10.1016/j.jpowsour.2014.02.012
|
||
|
|
||
|
import numpy as np
|
||
|
from functions.extract_stressors import extract_stressors
|
||
|
from functions.state_functions import update_power_state
|
||
|
|
||
|
# EXPERIMENTAL AGING DATA SUMMARY:
|
||
|
# Calendar aging varied SOC at 50 Celsius, and temperature at 50% state-of-charge.
|
||
|
# Cycle aging varied depth-of-discharge and average state-of-charge at 35 Celsius at
|
||
|
# charge and discharge rates of 1C.
|
||
|
# Relative discharge capacity is reported from measurements recorded at 35 Celsius and 1C rate.
|
||
|
# Relative DC resistance is reported after fitting of 10s 1C discharge pulses near 50% state-of-charge.
|
||
|
|
||
|
# MODEL SENSITIVITY
|
||
|
# The model predicts degradation rate versus time as a function of temperature and average
|
||
|
# state-of-charge and degradation rate versus equivalent full cycles (charge-throughput) as
|
||
|
# a function of average voltage and depth-of-discharge.
|
||
|
|
||
|
# MODEL LIMITATIONS
|
||
|
# Cycle degradation predictions are NOT SENSITIVE TO TEMPERATURE OR C-RATE. Cycling degradation predictions
|
||
|
# are ONLY ACCURATE NEAR 1C RATE AND 35 CELSIUS CELL TEMPERATURE.
|
||
|
|
||
|
class Nmc111_Gr_Sanyo2Ah_Battery:
|
||
|
# Model predicting the degradation of Sanyo UR18650E cells, published by Schmalsteig et al:
|
||
|
# http://dx.doi.org/10.1016/j.jpowsour.2014.02.012.
|
||
|
# More detailed analysis of cell performance and voltage vs. state-of-charge data was copied from
|
||
|
# Ecker et al: http://dx.doi.org/10.1016/j.jpowsour.2013.09.143 (KNEE POINTS OBSERVED IN ECKER ET AL
|
||
|
# AT HIGH DEPTH OF DISCHARGE WERE SIMPLY NOT ADDRESSED DURING MODEL FITTING BY SCHMALSTEIG ET AL).
|
||
|
# Voltage lookup table here use data from Ecker et al for 0/10% SOC, and other values were extracted
|
||
|
# from Figure 1 in Schmalsteig et al using WebPlotDigitizer.
|
||
|
|
||
|
def __init__(self):
|
||
|
# States: Internal states of the battery model
|
||
|
self.states = {
|
||
|
'qLoss_t': np.array([0]),
|
||
|
'qLoss_EFC': np.array([0]),
|
||
|
'rGain_t': np.array([0]),
|
||
|
'rGain_EFC': np.array([0]),
|
||
|
}
|
||
|
|
||
|
# Outputs: Battery properties derived from state values
|
||
|
self.outputs = {
|
||
|
'q': np.array([1]),
|
||
|
'q_t': np.array([1]),
|
||
|
'q_EFC': np.array([1]),
|
||
|
'r': np.array([1]),
|
||
|
'r_t': np.array([1]),
|
||
|
'r_EFC': np.array([1]),
|
||
|
}
|
||
|
|
||
|
# Stressors: History of stressors on the battery
|
||
|
self.stressors = {
|
||
|
'delta_t_days': np.array([np.nan]),
|
||
|
't_days': np.array([0]),
|
||
|
'delta_efc': np.array([np.nan]),
|
||
|
'efc': np.array([0]),
|
||
|
'TdegK': np.array([np.nan]),
|
||
|
'soc': np.array([np.nan]),
|
||
|
'Vrms': np.array([np.nan]),
|
||
|
'dod': np.array([np.nan]),
|
||
|
}
|
||
|
|
||
|
# Rates: History of stressor-dependent degradation rates
|
||
|
self.rates = {
|
||
|
'q_alpha': np.array([np.nan]),
|
||
|
'q_beta': np.array([np.nan]),
|
||
|
'r_alpha': np.array([np.nan]),
|
||
|
'r_beta': np.array([np.nan]),
|
||
|
}
|
||
|
|
||
|
# Nominal capacity
|
||
|
@property
|
||
|
def _cap(self):
|
||
|
return 2.15
|
||
|
|
||
|
# SOC index
|
||
|
@property
|
||
|
def _soc_index(self):
|
||
|
return np.array([0,0.008637153,0.026779514,0.044921875,0.063064236,0.081206597,0.099348958,0.117491319,0.135633681,0.153776042,0.171918403,0.190060764,0.208203125,0.226345486,0.244487847,0.262630208,0.280772569,0.298914931,0.317057292,0.335199653,0.353342014,0.371484375,0.389626736,0.407769097,0.425911458,0.444053819,0.462196181,0.480338542,0.498480903,0.516623264,0.534765625,0.552907986,0.571050347,0.589192708,0.607335069,0.625477431,0.643619792,0.661762153,0.679904514,0.698046875,0.716189236,0.734331597,0.752473958,0.770616319,0.788758681,0.806901042,0.825043403,0.843185764,0.861328125,0.879470486,0.897612847,0.915755208,0.933897569,0.952039931,0.970182292,0.988324653,0.998220486,1])
|
||
|
|
||
|
# OCV
|
||
|
@property
|
||
|
def _ocv(self):
|
||
|
return np.array([3.331,3.345014187,3.37917149,3.411603677,3.440585632,3.466289865,3.490268982,3.511315401,3.529946658,3.547197821,3.561688798,3.574972194,3.586357962,3.597053683,3.605506753,3.613442288,3.620342753,3.626380661,3.632073544,3.63690387,3.642079219,3.646909545,3.652084894,3.657605266,3.663470662,3.670198615,3.677444104,3.686759732,3.696420384,3.708323686,3.720572012,3.734372943,3.749553967,3.765252525,3.781123596,3.797857224,3.814590852,3.832532062,3.849783226,3.866861877,3.884458064,3.900846669,3.917752809,3.934831461,3.953462717,3.971921462,3.991415276,4.011771649,4.03195551,4.05196686,4.070770628,4.087849279,4.104237884,4.120108955,4.135980025,4.152541142,4.160649189,4.162])
|
||
|
|
||
|
# Voltage lookup table
|
||
|
def calc_voltage(self, soc):
|
||
|
# calculate cell voltage from soc vector
|
||
|
return np.interp(soc, self._soc_index, self._ocv, left=self._ocv[0], right=self._ocv[-1])
|
||
|
|
||
|
# Define life model parameters
|
||
|
@property
|
||
|
def _params_life(self):
|
||
|
return {
|
||
|
# Capacity fade parameters
|
||
|
'qcal_A_V': 7.543,
|
||
|
'qcal_B_V': -23.75,
|
||
|
'qcal_C_TdegK': -6976,
|
||
|
'qcal_p': 0.75,
|
||
|
'qcyc_A_V': 7.348e-3,
|
||
|
'qcyc_B_V': 3.667,
|
||
|
'qcyc_C': 7.6e-4,
|
||
|
'qcyc_D_DOD': 4.081e-3,
|
||
|
'qcyc_p': 0.5,
|
||
|
|
||
|
# Resistance growth parameters
|
||
|
'rcal_A_V': 5.270,
|
||
|
'rcal_B_V': -16.32,
|
||
|
'rcal_C_TdegK': -5986,
|
||
|
'rcal_p': 0.75,
|
||
|
'rcyc_A_V': 2.153e-4,
|
||
|
'rcyc_B_V': 3.725,
|
||
|
'rcyc_C': -1.521e-5,
|
||
|
'rcyc_D_DOD': 2.798e-4,
|
||
|
}
|
||
|
|
||
|
# Battery model
|
||
|
def update_battery_state(self, t_secs, soc, T_celsius):
|
||
|
# Update the battery states, based both on the degradation state as well as the battery performance
|
||
|
# at the ambient temperature, T_celsius
|
||
|
# Inputs:
|
||
|
# t_secs (ndarry): vector of the time in seconds since beginning of life for the soc_timeseries data points
|
||
|
# soc (ndarry): vector of the state-of-charge of the battery at each t_sec
|
||
|
# T_celsius (ndarray): the temperature of the battery during this time period, in Celsius units.
|
||
|
|
||
|
# Check some input types:
|
||
|
if not isinstance(t_secs, np.ndarray):
|
||
|
raise TypeError('Input "t_secs" must be a numpy.ndarray')
|
||
|
if not isinstance(soc, np.ndarray):
|
||
|
raise TypeError('Input "soc" must be a numpy.ndarray')
|
||
|
if not isinstance(T_celsius, np.ndarray):
|
||
|
raise TypeError('Input "T_celsius" must be a numpy.ndarray')
|
||
|
if not (len(t_secs) == len(soc) and len(t_secs) == len(T_celsius)):
|
||
|
raise ValueError('All input timeseries must be the same length')
|
||
|
|
||
|
self.__update_states(t_secs, soc, T_celsius)
|
||
|
self.__update_outputs()
|
||
|
|
||
|
def __update_states(self, t_secs, soc, T_celsius):
|
||
|
# Update the battery states, based both on the degradation state as well as the battery performance
|
||
|
# at the ambient temperature, T_celsius
|
||
|
# Inputs:
|
||
|
# t_secs (ndarry): vector of the time in seconds since beginning of life for the soc_timeseries data points
|
||
|
# soc (ndarry): vector of the state-of-charge of the battery at each t_sec
|
||
|
# T_celsius (ndarray): the temperature of the battery during this time period, in Celsius units.
|
||
|
|
||
|
# Extract stressors
|
||
|
delta_t_secs = t_secs[-1] - t_secs[0]
|
||
|
delta_t_days, delta_efc, TdegK, soc, Ua, dod, Crate, cycles = extract_stressors(t_secs, soc, T_celsius)
|
||
|
# Calculate RMS voltage, charge throughput
|
||
|
V_rms = np.sqrt(np.mean(self.calc_voltage(soc)**2))
|
||
|
Ah_throughput = delta_efc * 2 * self._cap
|
||
|
|
||
|
# Grab parameters
|
||
|
p = self._params_life
|
||
|
|
||
|
# Calculate the degradation coefficients
|
||
|
alpha_cap = (p['qcal_A_V'] * self.calc_voltage(soc) + p['qcal_B_V']) * 1e6 * np.exp(p['qcal_C_TdegK'] / TdegK)
|
||
|
alpha_res = (p['rcal_A_V'] * self.calc_voltage(soc) + p['rcal_B_V']) * 1e5 * np.exp(p['rcal_C_TdegK'] / TdegK)
|
||
|
beta_cap = (
|
||
|
p['qcyc_A_V'] * (V_rms - p['qcyc_B_V']) ** 2
|
||
|
+ p['qcyc_C']
|
||
|
+ p['qcyc_D_DOD'] * dod
|
||
|
)
|
||
|
beta_res = (
|
||
|
p['rcyc_A_V'] * (V_rms - p['rcyc_B_V']) ** 2
|
||
|
+ p['rcyc_C']
|
||
|
+ p['rcyc_D_DOD'] * dod
|
||
|
)
|
||
|
|
||
|
# Calculate time based average of each rate
|
||
|
alpha_cap = np.trapz(alpha_cap, x=t_secs) / delta_t_secs
|
||
|
alpha_res = np.trapz(alpha_res, x=t_secs) / delta_t_secs
|
||
|
|
||
|
# Calculate incremental state changes
|
||
|
states = self.states
|
||
|
# Capacity
|
||
|
dq_t = update_power_state(states['qLoss_t'][-1], delta_t_days, alpha_cap, p['qcal_p'])
|
||
|
dq_EFC = update_power_state(states['qLoss_EFC'][-1], Ah_throughput, beta_cap, p['qcyc_p'])
|
||
|
# Resistance
|
||
|
dr_t = update_power_state(states['rGain_t'][-1], delta_t_days, alpha_res, p['rcal_p'])
|
||
|
dr_EFC = beta_res * Ah_throughput
|
||
|
|
||
|
# Accumulate and store states
|
||
|
dx = np.array([dq_t, dq_EFC, dr_t, dr_EFC])
|
||
|
for k, v in zip(states.keys(), dx):
|
||
|
x = self.states[k][-1] + v
|
||
|
self.states[k] = np.append(self.states[k], x)
|
||
|
|
||
|
# Store stressors
|
||
|
t_days = self.stressors['t_days'][-1] + delta_t_days
|
||
|
efc = self.stressors['efc'][-1] + delta_efc
|
||
|
stressors = np.array([delta_t_days, t_days, delta_efc, efc, np.mean(TdegK), np.mean(soc), V_rms, dod])
|
||
|
for k, v in zip(self.stressors.keys(), stressors):
|
||
|
self.stressors[k] = np.append(self.stressors[k], v)
|
||
|
|
||
|
# Store rates
|
||
|
rates = np.array([alpha_cap, beta_cap, alpha_res, beta_res])
|
||
|
for k, v in zip(self.rates.keys(), rates):
|
||
|
self.rates[k] = np.append(self.rates[k], v)
|
||
|
|
||
|
def __update_outputs(self):
|
||
|
# Calculate outputs, based on current battery state
|
||
|
states = self.states
|
||
|
p = self._params_life
|
||
|
|
||
|
# Capacity
|
||
|
q_t = 1 - states['qLoss_t'][-1]
|
||
|
q_EFC = 1 - states['qLoss_EFC'][-1]
|
||
|
q = 1 - states['qLoss_t'][-1] - states['qLoss_EFC'][-1]
|
||
|
|
||
|
# Resistance
|
||
|
r_t = 1 + states['rGain_t'][-1]
|
||
|
r_EFC = 1 + states['rGain_EFC'][-1]
|
||
|
r = 1 + states['rGain_t'][-1] + states['rGain_EFC'][-1]
|
||
|
|
||
|
# Assemble output
|
||
|
out = np.array([q, q_t, q_EFC, r, r_t, r_EFC])
|
||
|
# Store results
|
||
|
for k, v in zip(list(self.outputs.keys()), out):
|
||
|
self.outputs[k] = np.append(self.outputs[k], v)
|
||
|
|
||
|
|