BatterySimulatorBLAST/python/nmc111_gr_Sanyo2Ah_2014.py

226 lines
10 KiB
Python
Raw Permalink Normal View History

2023-04-08 06:05:55 +09:00
# 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)