# Paul Gasper, NREL
# Model predicting the degradation of Sony-Murata 3 Ah LFP-Gr cylindrical cells.
# High power cells with low energy density and exceptional cycle life / cell quality.
# Data is from Technical University of Munich, reported in studies led by Maik Naumann.
# Capacity model identification was conducted at NREL. Resistance model is from Naumann et al.
# Naumann et al used an interative fitting procedure, but it was found that lower model error could be
# achieved by simply reoptimizing all resistance growth parameters to the entire data set.
# Calendar aging data source: https://doi.org/10.1016/j.est.2018.01.019
# Cycle aging data source: https://doi.org/10.1016/j.jpowsour.2019.227666
# Model identification source: https://doi.org/10.1149/1945-7111/ac86a8

import numpy as np
import scipy.stats as stats
from blast.models.degradation_model import BatteryDegradationModel


class Lfp_Gr_SonyMurata3Ah_Battery(BatteryDegradationModel):
    """
    Model predicting the degradation of Sony-Murata 3 Ah LFP-Gr cylindrical cells.
    Data is from Technical University of Munich, reported in studies led by Maik Naumann.
    Capacity model identification was conducted at NREL. Resistance model is from Naumann et al.
    Naumann et al used an interative fitting procedure, but it was found that lower model error could be
    achieved by simply reoptimizing all resistance growth parameters to the entire data set.
    Calendar aging data source: https://doi.org/10.1016/j.est.2018.01.019
    Cycle aging data source: https://doi.org/10.1016/j.jpowsour.2019.227666
    Model identification source: https://doi.org/10.1149/1945-7111/ac86a8

    Degradation rate is a function of the aging stressors, i.e., ambient temperature and use.
    The state of the battery is updated throughout the lifetime of the cell.
    Performance metrics are capacity and DC resistance. These metrics change as a function of the
    cell's current degradation state, as well as the ambient temperature. The model predicts time and 
    cycling dependent degradation. Cycling dependent degradation includes a break-in mechanism as well
    as long term cycling fade; the break-in mechanism strongly influenced results of the accelerated
    aging test, but is not expected to have much influence on real-world applications.
    Parameters to modify to change fade rates:
    - q1_b0: rate of capacity loss due to calendar degradation
    - q5_b0: rate of capacity loss due to cycling degradation
    - k_ref_r_cal: rate of resistance growth due to calendar degradation
    - A_r_cyc: rate of resistance growth due to cycling degradation

    .. note::
        EXPERIMENTAL AGING DATA SUMMARY:
            Aging test matrix varied temperature and state-of-charge for calendar aging, and
            varied depth-of-discharge, average state-of-charge, and C-rates for cycle aging.
            There is NO LOW TEMPERATURE cycling aging data, i.e., no lithium-plating induced by
            kinetic limitations on cell performance; CYCLING WAS ONLY DONE AT 25 CELSIUS AND 45 CELSIUS,
            so any model predictions at low temperature cannot incorporate low temperature degradation modes.
            Discharge capacity

        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 state-of-charge during a cycle, depth-of-discharge, and average of the
            charge and discharge C-rates.

        MODEL LIMITATIONS
            There is no influence of TEMPERATURE on CYCLING DEGRADATION RATE due to limited data. This is
            NOT PHYSICALLY REALISTIC AND IS BASED ON LIMITED DATA.
    """

    def __init__(self, degradation_scalar: float = 1, label: str = "LFP-Gr Sony-Murata"):
        # States: Internal states of the battery model
        self.states = {
            'qLoss_LLI_t': np.array([0]),
            'qLoss_LLI_EFC': np.array([0]),
            'qLoss_BreakIn_EFC': np.array([1e-10]),
            'rGain_LLI_t': np.array([0]),
            'rGain_LLI_EFC': np.array([0]),
        }

        # Outputs: Battery properties derived from state values
        self.outputs = {
            'q': np.array([1]),
            'q_LLI_t': np.array([1]),
            'q_LLI_EFC': np.array([1]),
            'q_BreakIn_EFC': np.array([1]),
            'r': np.array([1]),
            'r_LLI_t': np.array([1]),
            'r_LLI_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]), 
            'Ua': np.array([np.nan]), 
            'dod': np.array([np.nan]), 
            'Crate': np.array([np.nan]),
        }

        # Rates: History of stressor-dependent degradation rates
        self.rates = {
            'q1': np.array([np.nan]),
            'q3': np.array([np.nan]),
            'q5': np.array([np.nan]),
            'q7': np.array([np.nan]),
            'r_kcal': np.array([np.nan]),
            'r_kcyc': np.array([np.nan]),
        }

        # Expermental range: details on the range of experimental conditions, i.e.,
        # the range we expect the model to be valid in
        self.experimental_range = {
            'cycling_temperature': [20, 40],
            'dod': [0.8, 1],
            'soc': [0, 1],
            'max_rate_charge': 1,
            'max_rate_discharge': 2,
        }

        # Degradation scalar - scales all state changes by a coefficient
        self._degradation_scalar = degradation_scalar
        # Label for plotting
        self._label = label

    # Nominal capacity
    @property
    def cap(self):
        return 3

    # Define life model parameters
    @property
    def _params_life(self):
        return {
            # Capacity fade parameters
            'q2': 0.000130510034211874,
            'q1_b0': 0.989687151293590,             # CHANGE to modify calendar degradation rate
            'q1_b1': -2881067.56019324,
            'q1_b2': 8742.06309157261,
            'q3_b0': 0.000332850281062177,          
            'q3_b1': 734553185711.369,
            'q3_b2': -2.82161575620780e-06,
            'q3_b3': -3284991315.45121,
            'q3_b4': 0.00127227593657290,
            'q8': 0.00303553871631028,
            'q9': 1.43752162947637,
            'q7_b0': 0.582258029148225,
            'q7_soc_skew': 0.0583128906965484,
            'q7_soc_width': 0.208738181522897,
            'q7_dod_skew': -3.80744333129564,
            'q7_dod_width': 1.16126260428210,
            'q7_dod_growth': 25.4130804598602,
            'q6': 1.12847759334355,
            'q5_b0': -6.81260579372875e-06,         # CHANGE to modify cycling degradation rate
            'q5_b1': 2.59615973160844e-05,
            'q5_b2': 2.11559710307295e-06,
            # Resistance growth parameters
            'k_ref_r_cal': 3.4194e-10,    # CHANGE to modify calendar degradation rate
            'Ea_r_cal': 71827,
            'C_r_cal': -3.3903,
            'D_r_cal': 1.5604,
            'A_r_cyc': -0.002,        # CHANGE to modify cycling degradation rate
            'B_r_cyc': 0.0021,
            'C_r_cyc': 6.8477,
            'D_r_cyc': 0.91882
        }
    
    def update_rates(self, stressors: dict):
        # Calculate and update battery degradation rates based on stressor values
        # Inputs:
        #   stressors (dict): output from extract_stressors

        def _sigmoid(x, alpha, beta, gamma):
            return 2*alpha*(1/2 - 1/(1 + np.exp((beta*x)**gamma)))

        def _skewnormpdf(x, skew, width):
            x_prime = (x-0.5)/width
            return 2 * stats.norm.pdf(x_prime) * stats.norm.cdf(skew * (x_prime))

        # Unpack stressors
        t_secs = stressors["t_secs"]
        delta_t_secs = t_secs[-1] - t_secs[0]
        TdegK = stressors["TdegK"]
        soc = stressors["soc"]
        Ua = stressors["Ua"]
        dod = stressors["dod"]
        Crate = stressors["Crate"]
        
        # Grab parameters
        p = self._params_life

        # Calculate the degradation coefficients
        q1 = np.abs(
        p['q1_b0']
        * np.exp(p['q1_b1']*(1/(TdegK**2))*(Ua**0.5))
        * np.exp(p['q1_b2']*(1/TdegK)*(Ua**0.5))
        )
        q3 = np.abs(
            p['q3_b0']
            * np.exp(p['q3_b1']*(1/(TdegK**4))*(Ua**(1/3)))
            * np.exp(p['q3_b2']*(TdegK**3)*(Ua**(1/4)))
            * np.exp(p['q3_b3']*(1/(TdegK**3))*(Ua**(1/3)))
            * np.exp(p['q3_b4']*(TdegK**2)*(Ua**(1/4)))
            )
        q5 = np.abs(
            p['q5_b0']
            + p['q5_b1']*dod
            + p['q5_b2']*np.exp((dod**2)*(Crate**3))
            )
        q7 = np.abs(
            p['q7_b0']
            * _skewnormpdf(soc, p['q7_soc_skew'], p['q7_soc_width'])
            * _skewnormpdf(dod, p['q7_dod_skew'], p['q7_dod_width'])
            * _sigmoid(dod, 1, p['q7_dod_growth'], 1)
            )
        k_temp_r_cal = (
            p['k_ref_r_cal']
            * np.exp((-p['Ea_r_cal'] / 8.3144) * (1/TdegK - 1/298.15))
        )
        k_soc_r_cal = p['C_r_cal'] * (soc - 0.5)**3 + p['D_r_cal']
        k_Crate_r_cyc = p['A_r_cyc'] * Crate + p['B_r_cyc']
        k_dod_r_cyc = p['C_r_cyc']* (dod - 0.5)**3 + p['D_r_cyc']

        # Calculate time based average of each rate
        q1 = np.trapz(q1, x=t_secs) / delta_t_secs
        q3 = np.trapz(q3, x=t_secs) / delta_t_secs
        # q5 = np.trapz(q5, x=t_secs) / delta_t_secs # no time varying inputs
        q7 = np.trapz(q7, x=t_secs) / delta_t_secs
        k_temp_r_cal = np.trapz(k_temp_r_cal, x=t_secs) / delta_t_secs
        k_soc_r_cal = np.trapz(k_soc_r_cal, x=t_secs) / delta_t_secs
        # k_Crate_r_cyc = np.trapz(k_Crate_r_cyc, x=t_secs) / delta_t_secs # no time varying inputs
        # k_dod_r_cyc = np.trapz(k_dod_r_cyc, x=t_secs) / delta_t_secs # no time varying inputs

        # Store rates
        rates = np.array([q1, q3, q5, q7, k_temp_r_cal * k_soc_r_cal, k_Crate_r_cyc * k_dod_r_cyc])
        for k, v in zip(self.rates.keys(), rates):
            self.rates[k] = np.append(self.rates[k], v)
    
    def update_states(self, stressors: dict):
        # Update the battery states, based both on the degradation state as well as the battery performance
        # at the ambient temperature, T_celsius
        # Inputs:
            #   stressors (dict): output from extract_stressors
            
        # Unpack stressors
        delta_t_days = stressors["delta_t_days"]
        delta_efc = stressors["delta_efc"]
        
        # Grab parameters
        p = self._params_life

        # Grab rates, only keep most recent value
        r = self.rates.copy()
        for k, v in zip(r.keys(), r.values()):
            r[k] = v[-1]

        # Calculate incremental state changes
        states = self.states

        # Capacity
        dq_LLI_t = self._degradation_scalar * self._update_sigmoid_state(states['qLoss_LLI_t'][-1], delta_t_days, r['q1'], p['q2'], r['q3'])
        dq_LLI_EFC = self._degradation_scalar * self._update_power_B_state(states['qLoss_LLI_EFC'][-1], delta_efc, r['q5'], p['q6'])
        if delta_efc / delta_t_days > 3: # only evalaute if more than 3 full cycles per day
            dq_BreakIn_EFC = self._degradation_scalar * self._update_sigmoid_state(states['qLoss_BreakIn_EFC'][-1], delta_efc, r['q7'], p['q8'], p['q9'])
        else:
            dq_BreakIn_EFC = 0

        # Resistance
        dr_LLI_t = self._degradation_scalar * r['r_kcal'] * delta_t_days * 86400
        dr_LLI_EFC = self._degradation_scalar * r['r_kcyc'] * delta_efc / 100

        # Accumulate and store states
        dx = np.array([dq_LLI_t, dq_LLI_EFC, dq_BreakIn_EFC, dr_LLI_t, dr_LLI_EFC])
        for k, v in zip(states.keys(), dx):
            x = self.states[k][-1] + v
            self.states[k] = np.append(self.states[k], x)
    
    def update_outputs(self, stressors):
        # Calculate outputs, based on current battery state
        states = self.states

        # Capacity
        q_LLI_t = 1 - states['qLoss_LLI_t'][-1]
        q_LLI_EFC = 1 - states['qLoss_LLI_EFC'][-1]
        q_BreakIn_EFC = 1 - states['qLoss_BreakIn_EFC'][-1]
        q = 1 - states['qLoss_LLI_t'][-1] - states['qLoss_LLI_EFC'][-1] - states['qLoss_BreakIn_EFC'][-1]
        
        # Resistance
        r_LLI_t = 1 + states['rGain_LLI_t'][-1]
        r_LLI_EFC = 1 + states['rGain_LLI_EFC'][-1]
        r = 1 + states['rGain_LLI_t'][-1] + states['rGain_LLI_EFC'][-1]

        # Assemble output
        out = np.array([q, q_LLI_t, q_LLI_EFC, q_BreakIn_EFC, r, r_LLI_t, r_LLI_EFC])
        # Store results
        for k, v in zip(list(self.outputs.keys()), out):
            self.outputs[k] = np.append(self.outputs[k], v)
