<jupyter_start><jupyter_text>Dataset The dataset contains measurements from more than 1000 lithium-ion cells with nominal capacity of 1.1 Ah and a nominal voltage of 3.3 V under various charge and discharge profiles. For this example, the data is curated to only contain a subset of measurements relevant to the features being extracted.  Training data contains measurements from 500 cells, validation data contains measurements from 300 cells and the test data contains measurements from 300 cells. Data for each cell is stored in a structure, which includes the following information:Descriptive data: Cell barcode, charging policy, cycle lifePer-cycle summary data: Cycle number, discharge capacity, internal resistance, charge timeData collected within a cycle: Time, temperature, linearly interpolated discharge capacity, linearly interpolated voltage<jupyter_code>list(f.keys())
trainData=f['trainData']
testData=f['testData']
valData=f['valData']<jupyter_output><empty_output><jupyter_text>Extract features from Features Extraction Function file<jupyter_code>!pip install ipynb
from ipynb.fs.full.FeatureExtraction import Feature
train_dict,df_train=Feature(trainData,f)
test_dict,df_test=Feature(testData,f)
val_dict,df_val=Feature(valData,f)<jupyter_output><empty_output><jupyter_text>Cycle life vs Capacity Discharge capacity is a key feature that can reflect the health of a battery, and its value is indicative of driving range inan electric vehicle. Plot discharge capacity for the first 1,000 cycles of cells in training data to visualize its change across the life of a cell.<jupyter_code>for i in train_dict.keys():    
    if np.size(train_dict[i]['summary']['cycle'])==np.size(np.unique(train_dict[i]['summary']['cycle'])):
        plt.plot( train_dict[i]['summary']['cycle'],  train_dict[i]['summary']['QD'])

plt.xlim([0,1200])
plt.ylim([0.85,1.1])
plt.xlabel('Cycle Number')
plt.ylabel('Discharge Capacity (Ah)')<jupyter_output><empty_output><jupyter_text>As seen in the plot, capacity fade accelerates near the end of life. However, capacity fade is negligible in the first 100 cycles and by itself is not a good feature for battery cycle life prediction. Therefore, a data-driven approach that considers voltage curves of each cycle, along with additional measurements, including cell internal resistance and temperature, is considered for predicting remaining cycle life. The cycle-to-cycle evolution of Q(V), the discharge voltage curve as a function of voltage for a given cycle, is an important feature space [1]. In particular, the change in discharge voltage curves between cycles, ΔQ(V), is very effective in degradation diagnosis. Therefore, select the difference in discharge capacity as a function of voltage between 100th and 10th cycle, ΔQ100−10(V).<jupyter_code>for i in train_dict.keys():    
    plt.plot(train_dict[i]['cycles']['100']['Qdlin']-train_dict[i]['cycles']['10']['Qdlin'], train_dict[i]['Vdlin'])

plt.xlabel('(Q100-Q10)(Ah)')
plt.ylabel('Voltage(V)')<jupyter_output><empty_output><jupyter_text>Compute summary statistics using ΔQ 100−10(V) curves of each cell as the condition indicator. It is important to bear in mind that these summary statistics do not need to have a clear physical meaning, they just need to exhibit predictive capabilities. For example, the variance of ΔQ100−10(V) and cycle life are highly correlated as seen in this log–log plot, thereby confirming this approach.<jupyter_code>for i in train_dict.keys():    
    cycleLife = train_dict[i]['cycle_life'][0,0]
    DeltaQ_var = (train_dict[i]['cycles']['100']['Qdlin']-train_dict[i]['cycles']['10']['Qdlin']).var()
    plt.plot(DeltaQ_var, cycleLife, marker="o", markersize=10,markerfacecolor="red")
    plt.loglog(base=10)
plt.xlim(0.00001,0.002)
plt.ylim(300,2000)
plt.xlabel('DeltaQ_var')
plt.ylabel(' cycleLife')<jupyter_output><empty_output><jupyter_text>Polyfit function testing<jupyter_code>coeff2=np.polyfit(train_dict['c0']['summary']['cycle'][2:100], train_dict['c0']['summary']['QD'][2:100],1)
CapFadeCycle2Slope = coeff2[0];
CapFadeCycle2Intercept = coeff2[1];
print(CapFadeCycle2Slope)
df_train.head()
#Drop out outliers
df_train=df_train.loc[df_train['DeltaQ_var']>-5]
df_train.head()<jupyter_output><empty_output><jupyter_text>Correlation matrix (heatmap style)<jupyter_code>corrmat = df_train.corr()
f, ax = plt.subplots(figsize=(12, 9))
sns.heatmap(corrmat, vmax=.8, square=True);<jupyter_output><empty_output><jupyter_text>From the heat map there is strong relations between DeltaQ_var and DeltaQ_min. Qd2 to CapFadeCycle2Intercept also has strong relation  Scatter plots between 'CycleLife' and correlated variables<jupyter_code>#scatterplot
sns.set()
sns.pairplot(df_train)
plt.show();<jupyter_output><empty_output><jupyter_text>Although we already know some of the main figures, this mega plot gives us a reasonable idea about variables relationshipsClearl it shows there are strong relations between DeltaQ_var and DeltaQ_min with CycleLife  Feature Extraction for Model<jupyter_code>X_train = df_train.drop(['CycleLife'],axis=1, inplace=False)
y_train=df_train[['CycleLife']]
X_test = df_test.drop(['CycleLife'],axis=1, inplace=False)
y_test=df_test[['CycleLife']]
X_val = df_val.drop(['CycleLife'],axis=1, inplace=False)
y_val=df_val[['CycleLife']]<jupyter_output><empty_output><jupyter_text>Model Selection and Hyperparameter Tuning we will only use ElasticNet for our models to compare.<jupyter_code>from sklearn.model_selection import cross_val_score
from sklearn.linear_model import ElasticNet
from sklearn.metrics import classification_report
from sklearn.metrics import mean_squared_error
from sklearn.model_selection import GridSearchCV

# import warnings filter
from warnings import simplefilter
# ignore all future warnings
simplefilter(action='ignore', category=FutureWarning)
# define model
model = ElasticNet(max_iter=8000)
# define grid
grid = dict()
grid['l1_ratio']=np.arange(0, 1, 0.01)
alpha= [1e-6,1e-5, 1e-4, 1e-3, 0.0, 1.0, 10.0]
for i in range(len(alpha)):
    grid['alpha'] = [alpha[i]]
    # define search
    search = GridSearchCV(model, grid, scoring='neg_mean_squared_error', cv=4, n_jobs=-1)
    # perform the search
    results = search.fit(X_train, y_train)
    # summarize
    print('RMSE: %.3f' % results.best_score_)
    print('Config: %s' % results.best_params_)<jupyter_output>RMSE: -10507.736
Config: {'alpha': 1e-06, 'l1_ratio': 0.98}
RMSE: -11215.350
Config: {'alpha': 1e-05, 'l1_ratio': 0.99}
RMSE: -15916.106
Config: {'alpha': 0.0001, 'l1_ratio': 0.99}
RMSE: -18704.592
Config: {'alpha': 0.001, 'l1_ratio': 0.99}<jupyter_text>To make the fitted model robust, we will use validation data to select final values of alpha and l1_ratio hyperparameters. To this end, pick the coefficients corresponding to the four lowest RMSE values computed using the training data.<jupyter_code>grid = dict()
grid['l1_ratio']=[0,0.98,0.99]
grid['alpha']= [1e-6, 1e-5, 1e-3,0]
search = GridSearchCV(model, grid, scoring='neg_mean_squared_error', cv=4, n_jobs=-1)
results = search.fit(X_val, y_val)
print('RMSE: %.3f' % results.best_score_)
print('Config: %s' % results.best_params_)<jupyter_output>RMSE: -29227.943
Config: {'alpha': 0.001, 'l1_ratio': 0}<jupyter_text>Select the model with the least RMSE when using the validation data. The model related to the lowest RMSE value for training data is not the model corresponding to the lowest RMSE value for validation data. Using the validation data thereby increases model robustness.  Model Prediction<jupyter_code>model=ElasticNet(alpha=0.001, l1_ratio=0,max_iter=8000)
model.fit(X_train, y_train)
predCycleLife=model.predict(X_test)
print(predCycleLife)
fig,ax = plt.subplots(figsize=(8, 6))
plt.grid(True)
plt.gca().set_facecolor("white")
plt.scatter(y_test,predCycleLife, s=150, color='b')
plt.xlim(400,1400)
plt.ylim(400,1400)
plt.xlabel('Actual Cycle Life')
plt.ylabel('Predicted Cycle Life')

ax.plot([0, 1], [0, 1], transform=ax.transAxes,ls="--", c=".3")
import matplotlib.lines as mlines
import matplotlib.transforms as mtransforms<jupyter_output><empty_output><jupyter_text>PyTorch Model<jupyter_code># Imports
import torch
import torch.nn as nn
import torch.nn.functional as F<jupyter_output><empty_output><jupyter_text>We will combined train and validation test and use it for PyTorch train set<jupyter_code>X_tr= pd.concat([X_train, X_val], axis=0)
y_tr=pd.concat([y_train, y_val], axis=0)
inputs=torch.from_numpy(X_tr.values).float()
targets=torch.from_numpy(y_tr.values).float()
print(inputs.shape)
print(targets.shape)<jupyter_output>torch.Size([83, 8])
torch.Size([83, 1])<jupyter_text>Dataset and DataLoader We'll create a TensorDataset, which allows access to rows from inputs and targets as tuples. We'll also create a DataLoader, to split the data into batches while training. It also provides other utilities like shuffling and sampling.<jupyter_code># Import tensor dataset & data loader
from torch.utils.data import TensorDataset, DataLoader
# Define dataset
train_ds = TensorDataset(inputs, targets)
train_ds[0:3]
# Define data loader
batch_size = 5
train_dl = DataLoader(train_ds, batch_size, shuffle=True)
next(iter(train_dl))<jupyter_output><empty_output><jupyter_text>Define Model<jupyter_code>class SimpleNet(nn.Module):
    # Initialize the layers
    def __init__(self):
        super().__init__()
        self.linear1 = nn.Linear(8, 8)
        self.act1 = nn.ReLU() # Activation function
        self.linear2 = nn.Linear(8, 16)
        self.act2 = nn.ReLU() # Activation function
        self.linear3 = nn.Linear(16, 1)
        
    # Perform the computation
    def forward(self, x):
        x = self.linear1(x)
        x = self.act1(x)
        x = self.linear2(x)
        x = self.act2(x)
        x = self.linear3(x)
        return x
model = SimpleNet()
opt = torch.optim.SGD(model.parameters(), 1e-5)
loss_fn = F.mse_loss<jupyter_output><empty_output><jupyter_text>Training with Multiple Epochs: The Complete Training Loop<jupyter_code>for epoch in range(1000):
    
    total_loss=0

    for batch in train_dl: #Get Batch
        inp,targ=batch

        preds=model(inp) #Pass Batch
        loss=loss_fn(preds,targ)

        loss.backward() #Calculate gradient
        opt.step() #Update weights
        opt.zero_grad()
        total_loss +=loss.item()


    print("epoch:",epoch,'Training loss: ', loss_fn(model(inputs), targets))
# Generate predictions with test data
inp=torch.from_numpy(X_test.values).float()
tar=torch.from_numpy(y_test.values).float()
preds1 = model(inp)
fig,ax = plt.subplots(figsize=(8, 6))
plt.grid(True)
plt.gca().set_facecolor("white")
plt.scatter(tar.detach().numpy(),preds1.detach().numpy(), s=150, color='b')
plt.xlim(300,1300)
plt.ylim(300,1300)
plt.xlabel('Actual Cycle Life')
plt.ylabel('Predicted Cycle Life')

ax.plot([0, 1], [0, 1], transform=ax.transAxes,ls="--", c=".3")<jupyter_output><empty_output>
{'fraction_non_alphanumeric': 0.11894273127753303, 'fraction_numerical': 0.03138766519823789, 'mean_word_length': 6.066796368352788, 'pattern_counts': {'":': 0, '<': 66, '<?xml version=': 0, '>': 67, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'chain_length': '17', 'dir': 'jupyter-structured-clean-dedup', 'id': '72670', 'license': 'MIT', 'n_tokens_mistral': 3854, 'n_tokens_neox': 3567, 'n_words': 989, 'path': 'Battery Cycle Life Capacity Prediction.ipynb', 'repo_name': 'mopu3263/Battery-Cycle-Life-Prediction', 'size': '10896'}