├── .gitignore ├── DT ├── create_data.py ├── dt_prediction.png ├── main.py ├── train.py └── utils.py ├── EKF ├── ekf_prediction.png ├── extended_kalman_filter.py ├── main.py └── utils.py ├── FNN ├── best_train.png ├── best_turnigy_graphene_model.h5 ├── create_data.py ├── fnn_prediction.png ├── main.py ├── model.py ├── train.py └── utils.py ├── Final Report.pdf ├── Final Slides.pdf ├── README.md ├── SOC-estimation-at-room-temperature-a-LA92-drive-cycle-at-25C-b-UDDS-drive-cycle-at.png ├── SOC_Estimation.pdf ├── data ├── BatteryModel.csv └── SOC-OCV.csv └── requirements.txt /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | *.py,cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | target/ 76 | 77 | # Jupyter Notebook 78 | .ipynb_checkpoints 79 | 80 | # IPython 81 | profile_default/ 82 | ipython_config.py 83 | 84 | # pyenv 85 | .python-version 86 | 87 | # pipenv 88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 91 | # install all needed dependencies. 92 | #Pipfile.lock 93 | 94 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 95 | __pypackages__/ 96 | 97 | # Celery stuff 98 | celerybeat-schedule 99 | celerybeat.pid 100 | 101 | # SageMath parsed files 102 | *.sage.py 103 | 104 | # Environments 105 | .env 106 | .venv 107 | env/ 108 | venv/ 109 | ENV/ 110 | env.bak/ 111 | venv.bak/ 112 | 113 | # Spyder project settings 114 | .spyderproject 115 | .spyproject 116 | 117 | # Rope project settings 118 | .ropeproject 119 | 120 | # mkdocs documentation 121 | /site 122 | 123 | # mypy 124 | .mypy_cache/ 125 | .dmypy.json 126 | dmypy.json 127 | 128 | # Pyre type checker 129 | .pyre/ 130 | data/* 131 | temp* 132 | *.csv 133 | !data/BatteryModel.csv 134 | !data/SOC-OCV.csv 135 | *.pkl 136 | *.mat -------------------------------------------------------------------------------- /DT/create_data.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | import numpy as np 3 | import pandas as pd 4 | from utils import load_data 5 | from tqdm import tqdm 6 | 7 | TEMPERATURES = [-10, -20, 0, 10, 25, 40] 8 | DRIVE_CYCLES = ['UDDS', 'HWFET', 'LA92', 'US06'] 9 | WINDOW_SIZE = 500 10 | 11 | def preprocess_data( 12 | df: pd.DataFrame 13 | ): 14 | # Sort data by time 15 | df = df.sort_values(by='RecordingTime').reset_index(drop=True) 16 | 17 | # Resample data at 1Hz 18 | df = df.loc[::10] 19 | 20 | # Apply rolling window to data on voltage, current and temperature 21 | df['Avg_Measured_Voltage'] = df['Measured_Voltage'].rolling(WINDOW_SIZE, min_periods=1).mean() 22 | df['Avg_Measured_Current'] = df['Measured_Current'].rolling(WINDOW_SIZE, min_periods=1).mean() 23 | df['Avg_Measured_Temperature'] = df['Measured_Temperature'].rolling(WINDOW_SIZE, min_periods=1).mean() 24 | 25 | return df 26 | 27 | def create_data( 28 | data_dir 29 | ) -> pd.DataFrame: 30 | 31 | data_dir = Path(data_dir) 32 | total_data = [] 33 | 34 | for t in tqdm(TEMPERATURES): 35 | t_dir = data_dir / f"{t} degC" 36 | assert t_dir.exists(), f"{t_dir} does not exist" 37 | 38 | for d in DRIVE_CYCLES: 39 | for f in t_dir.glob(f"*{d}*.mat"): 40 | 41 | cur_data = load_data(f) 42 | cur_data = preprocess_data(cur_data) 43 | total_data.append(cur_data) 44 | 45 | total_data = pd.concat(total_data) 46 | total_data.reset_index(drop=True, inplace=True) 47 | 48 | return total_data 49 | 50 | 51 | if __name__ == '__main__': 52 | data = create_data('../data/Turnigy Graphene') 53 | data.to_csv('turnigy_graphene_data.csv', index=False) -------------------------------------------------------------------------------- /DT/dt_prediction.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vstark21/SOC_estimation/3c0b93740924cbeeb16a4dd8f363129bee275228/DT/dt_prediction.png -------------------------------------------------------------------------------- /DT/main.py: -------------------------------------------------------------------------------- 1 | import pickle 2 | import numpy as np 3 | from utils import * 4 | import matplotlib.pyplot as plt 5 | from create_data import preprocess_data 6 | from train import FEATURES, SAVE_FILE 7 | 8 | DATA_FILE = '../04-20-19_05.34 780_LA92_25degC_Turnigy_Graphene.mat' 9 | 10 | if __name__ == '__main__': 11 | 12 | LiPoly = load_data(DATA_FILE) 13 | LiPoly = preprocess_data(LiPoly) 14 | 15 | # Battery capacity in Ah taken from data 16 | nominal_cap = 5. 17 | 18 | # Calculate the SOC using coloumb counting for comparision 19 | LiPoly['Measured_SOC'] = (nominal_cap + LiPoly['Ah']) * 100. / nominal_cap 20 | 21 | # Converting seconds to hours 22 | LiPoly['RecordingTime_Hours'] = LiPoly['RecordingTime'] / 3600 23 | 24 | model = pickle.load(open(SAVE_FILE, "rb")) 25 | SOC_Estimated = model.predict(LiPoly[FEATURES].values) * 100. 26 | SOC_Estimated = np.squeeze(SOC_Estimated) 27 | SOC_Estimated = simple_exponential_smoothing(SOC_Estimated, 0.25) 28 | 29 | percentage_error = np.abs(LiPoly['Measured_SOC'] - SOC_Estimated) / LiPoly['Measured_SOC'] * 100. 30 | print('Mean percentage error: {:.2f}%'.format(np.mean(percentage_error))) 31 | 32 | # Plot the results 33 | fig, axes = plt.subplots(1, 2) 34 | 35 | axes[0].plot(LiPoly['RecordingTime_Hours'], LiPoly['Measured_SOC'], label='Measured SOC', alpha=0.5) 36 | axes[0].plot(LiPoly['RecordingTime_Hours'], SOC_Estimated, label='Estimated SOC using DT', alpha=0.5) 37 | axes[0].set_ylabel('SOC [%]') 38 | axes[0].set_xlabel('Time [Hours]') 39 | axes[0].set_title('Measured SOC vs. Estimated SOC using DT') 40 | axes[0].legend() 41 | 42 | # Plot soc error 43 | axes[1].plot(LiPoly['RecordingTime_Hours'], (LiPoly['Measured_SOC'] - SOC_Estimated)) 44 | axes[1].set_ylabel('SOC Error [%]') 45 | axes[1].set_xlabel('Time [Hours]') 46 | axes[1].set_title('SOC Error') 47 | 48 | plt.show() 49 | 50 | -------------------------------------------------------------------------------- /DT/train.py: -------------------------------------------------------------------------------- 1 | import pickle 2 | import numpy as np 3 | import pandas as pd 4 | from pathlib import Path 5 | from xgboost import XGBRegressor 6 | from create_data import create_data 7 | from utils import compute_metrics 8 | from sklearn.model_selection import train_test_split 9 | 10 | NOMINAL_CAP = 5.0 11 | DATA_FILE = Path('turnigy_graphene_data.csv') 12 | FEATURES = ['Measured_Voltage', 'Measured_Current', 'Measured_Temperature', 'Avg_Measured_Voltage', 'Avg_Measured_Current', 'Avg_Measured_Temperature'] 13 | LABEL = 'Measured_SOC' 14 | SAVE_FILE = Path('best_model.pkl') 15 | 16 | if __name__ == '__main__': 17 | if not DATA_FILE.exists(): 18 | data = create_data('../data/Turnigy Graphene') 19 | data.to_csv(DATA_FILE, index=False) 20 | else: 21 | data = pd.read_csv(DATA_FILE) 22 | 23 | data[LABEL] = (NOMINAL_CAP + data['Ah']) * 100. / NOMINAL_CAP 24 | 25 | train, val = train_test_split(data, test_size=0.25, random_state=42) 26 | 27 | x_train = train[FEATURES].values 28 | y_train = train[LABEL].values / 100. 29 | x_val = val[FEATURES].values 30 | y_val = val[LABEL].values / 100. 31 | 32 | model = XGBRegressor(n_estimators=1000) 33 | model.fit(x_train, y_train) 34 | 35 | train_metrics = compute_metrics(model.predict(x_train), y_train) 36 | val_metrics = compute_metrics(model.predict(x_val), y_val) 37 | 38 | print(f"TRAIN METRICS: {train_metrics}") 39 | print(f"VAL METRICS: {val_metrics}") 40 | 41 | pickle.dump(model, open(SAVE_FILE, "wb")) 42 | -------------------------------------------------------------------------------- /DT/utils.py: -------------------------------------------------------------------------------- 1 | import scipy.io 2 | import numpy as np 3 | import pandas as pd 4 | from sklearn.metrics import mean_squared_error, mean_absolute_error 5 | 6 | def load_data( 7 | mat_file: str 8 | ) -> pd.DataFrame: 9 | data = scipy.io.loadmat(mat_file) 10 | meas = data['meas'] 11 | dtypes = meas.dtype 12 | meas = np.squeeze(meas).tolist() 13 | data_dict = {} 14 | for i, name in enumerate(dtypes.names): 15 | data_dict[name] = np.array(meas[i]).squeeze() 16 | 17 | data_dict = pd.DataFrame(data_dict) 18 | data_dict.rename(columns={ 19 | 'Time': 'RecordingTime', 20 | 'Voltage': 'Measured_Voltage', 21 | 'Current': 'Measured_Current', 22 | 'Battery_Temp_degC': 'Measured_Temperature', 23 | }, inplace=True) 24 | return data_dict 25 | 26 | def compute_metrics(preds, labels): 27 | return { 28 | 'mse': mean_squared_error(preds, labels), 29 | 'mae': mean_absolute_error(preds, labels) 30 | } 31 | 32 | def simple_exponential_smoothing(arr, alpha=0.9): 33 | """ 34 | Simple exponential smoothing 35 | """ 36 | smoothed = np.zeros(len(arr)) 37 | smoothed[0] = arr[0] 38 | for i in range(1, len(arr)): 39 | smoothed[i] = alpha * arr[i] + (1 - alpha) * smoothed[i - 1] 40 | return smoothed -------------------------------------------------------------------------------- /EKF/ekf_prediction.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vstark21/SOC_estimation/3c0b93740924cbeeb16a4dd8f363129bee275228/EKF/ekf_prediction.png -------------------------------------------------------------------------------- /EKF/extended_kalman_filter.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import pandas as pd 3 | from scipy.interpolate import interp2d 4 | from tqdm import tqdm 5 | 6 | def run_ekf( 7 | data: pd.DataFrame, 8 | bat_model: pd.DataFrame, 9 | soc_ocv: pd.DataFrame, 10 | ): 11 | Current = data['Measured_Current_R'].values 12 | Vt_Actual = data['Measured_Voltage'].values 13 | Temperature = data['Measured_Temperature'].values 14 | 15 | SOC_Init = 1 # Initial SOC 16 | X = np.array([[SOC_Init], [0], [0]]) # State space x parameter initialisation 17 | DeltaT = 1 # Sample time in seconds 18 | Qn_rated = 5. * 3600 # Ah to Amp-sec 19 | 20 | F_R0 = interp2d(bat_model['T'].values, bat_model.SOC.values, bat_model.R0.values) 21 | F_R1 = interp2d(bat_model['T'].values, bat_model.SOC.values, bat_model.R1.values) 22 | F_R2 = interp2d(bat_model['T'].values, bat_model.SOC.values, bat_model.R2.values) 23 | F_C1 = interp2d(bat_model['T'].values, bat_model.SOC.values, bat_model.C1.values) 24 | F_C2 = interp2d(bat_model['T'].values, bat_model.SOC.values, bat_model.C2.values) 25 | 26 | SOCOCV = np.polyfit(soc_ocv['SOC'].values, soc_ocv['OCV'].values, 11) 27 | dSOCOCV = np.polyder(SOCOCV) 28 | 29 | n_x = X.shape[0] 30 | R_x = 2.5e-5 31 | P_x = np.array([ 32 | [0.025, 0, 0], 33 | [0, 0.01, 0], 34 | [0, 0, 0.01] 35 | ]) 36 | Q_x = np.array([ 37 | [1.0e-6, 0, 0], 38 | [0, 1.0e-5, 0], 39 | [0, 0, 1.0e-5] 40 | ]) 41 | 42 | SOC_Estimated = [] 43 | Vt_Estimated = [] 44 | Vt_Error = [] 45 | ik = len(Current) 46 | 47 | for k in tqdm(range(ik)): 48 | T = Temperature[k] 49 | U = Current[k] 50 | SOC = X[0, 0] 51 | V1 = X[1, 0] 52 | V2 = X[2, 0] 53 | 54 | # Evaluate the battery parameter 55 | # Functions for the current temperature and SOC 56 | R0 = F_R0(T, SOC).squeeze() 57 | R1 = F_R1(T, SOC).squeeze() 58 | R2 = F_R2(T, SOC).squeeze() 59 | C1 = F_C1(T, SOC).squeeze() 60 | C2 = F_C2(T, SOC).squeeze() 61 | 62 | OCV = np.polyval(SOCOCV, SOC) 63 | 64 | Tau_1 = C1 * R1 65 | Tau_2 = C2 * R2 66 | 67 | a1 = np.exp(-DeltaT / Tau_1) 68 | a2 = np.exp(-DeltaT / Tau_2) 69 | 70 | b1 = R1 * (1 - np.exp(-DeltaT / Tau_1)) 71 | b2 = R2 * (1 - np.exp(-DeltaT / Tau_2)) 72 | 73 | Terminal_Voltage = OCV - U * R0 - V1 - V2 74 | eta = 1 75 | 76 | dOCV = np.polyval(dSOCOCV, SOC) 77 | C_x = np.array([[dOCV, -1, -1]]) 78 | 79 | Error_x = np.array([[Vt_Actual[k] - Terminal_Voltage]]) 80 | 81 | Vt_Estimated.append(Terminal_Voltage) 82 | SOC_Estimated.append(X[0, 0]) 83 | Vt_Error.append(Error_x.squeeze()) 84 | 85 | A = np.array([ 86 | [1, 0, 0], 87 | [0, a1, 0], 88 | [0, 0, a2] 89 | ]) 90 | B = np.array([[-(eta * DeltaT / Qn_rated)], [b1], [b2]]) 91 | X = (A @ X) + (B @ np.array([[U]])) 92 | P_x = (A @ P_x @ A.T) + Q_x 93 | 94 | a = (C_x @ P_x @ C_x.T) + R_x 95 | KalmanGain_x = P_x @ C_x.T @ np.linalg.inv(a) 96 | X = X + (KalmanGain_x @ Error_x) 97 | P_x = (np.eye(n_x, n_x) - (KalmanGain_x @ C_x)) @ P_x 98 | 99 | SOC_Estimated = np.array(SOC_Estimated) 100 | Vt_Estimated = np.array(Vt_Estimated) 101 | Vt_Error = np.array(Vt_Error) 102 | 103 | return SOC_Estimated, Vt_Estimated, Vt_Error 104 | 105 | 106 | -------------------------------------------------------------------------------- /EKF/main.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | from utils import * 3 | import matplotlib.pyplot as plt 4 | from extended_kalman_filter import run_ekf 5 | 6 | DATA_FILE = '../04-20-19_05.34 780_LA92_25degC_Turnigy_Graphene.mat' 7 | BATTERY_MODEL = '../data/BatteryModel.csv' 8 | SOC_OCV = '../data/SOC-OCV.csv' 9 | 10 | if __name__ == '__main__': 11 | 12 | LiPoly = load_data(DATA_FILE) 13 | 14 | # Battery capacity in Ah taken from data 15 | nominal_cap = 5. 16 | # Calculate the SOC using coloumb counting for comparision 17 | LiPoly['Measured_SOC'] = (nominal_cap + LiPoly['Ah']) * 100. / nominal_cap 18 | 19 | # Resample data 20 | LiPoly = LiPoly.loc[0::10] 21 | 22 | # Discharging: +ve current, charging: -ve current 23 | LiPoly['Measured_Current_R'] = LiPoly['Measured_Current'] * -1 24 | 25 | # Converting seconds to hours 26 | LiPoly['RecordingTime_Hours'] = LiPoly['RecordingTime'] / 3600 27 | 28 | # Load battery model and soc-ocv data 29 | battery_model = pd.read_csv(BATTERY_MODEL) 30 | soc_ocv = pd.read_csv(SOC_OCV) 31 | 32 | print(battery_model.head()) 33 | print(soc_ocv.head()) 34 | 35 | SOC_Estimated, Vt_Estimated, Vt_Error = run_ekf( 36 | LiPoly, battery_model, soc_ocv 37 | ) 38 | 39 | percentage_error = np.abs((LiPoly['Measured_SOC'] - SOC_Estimated * 100) / LiPoly['Measured_SOC']) * 100 40 | print('Mean percentage error: {:.2f}%'.format(np.mean(percentage_error))) 41 | 42 | # Plot the results 43 | fig, axes = plt.subplots(2, 2) 44 | 45 | axes[0, 0].plot(LiPoly['RecordingTime_Hours'], LiPoly['Measured_Voltage'], label='Measured Voltage', alpha=0.5) 46 | axes[0, 0].plot(LiPoly['RecordingTime_Hours'], Vt_Estimated, label='Estimated Voltage', alpha=0.5) 47 | axes[0, 0].set_ylabel('Terminal Voltage [V]') 48 | axes[0, 0].set_title('Measure vs. Estimated Terminal Voltage (V)') 49 | axes[0, 0].legend() 50 | 51 | axes[0, 1].plot(LiPoly['RecordingTime_Hours'], Vt_Error) 52 | axes[0, 1].set_ylabel('Terminal Voltage Error') 53 | axes[0, 1].set_title('Terminal Voltage Error') 54 | 55 | axes[1, 0].plot(LiPoly['RecordingTime_Hours'], LiPoly['Measured_SOC'], label='Measured SOC', alpha=0.5) 56 | axes[1, 0].plot(LiPoly['RecordingTime_Hours'], SOC_Estimated * 100., label='Estimated SOC using EKF', alpha=0.5) 57 | axes[1, 0].set_ylabel('SOC [%]') 58 | axes[1, 0].set_xlabel('Time [Hours]') 59 | axes[1, 0].set_title('Measured SOC vs. Estimated SOC using EKF') 60 | axes[1, 0].legend() 61 | 62 | # Plot soc error 63 | axes[1, 1].plot(LiPoly['RecordingTime_Hours'], (LiPoly['Measured_SOC'] - SOC_Estimated * 100.)) 64 | axes[1, 1].set_ylabel('SOC Error [%]') 65 | axes[1, 1].set_xlabel('Time [Hours]') 66 | axes[1, 1].set_title('SOC Error') 67 | 68 | plt.show() 69 | 70 | -------------------------------------------------------------------------------- /EKF/utils.py: -------------------------------------------------------------------------------- 1 | import scipy.io 2 | import numpy as np 3 | import pandas as pd 4 | 5 | def load_data( 6 | mat_file: str 7 | ) -> pd.DataFrame: 8 | data = scipy.io.loadmat(mat_file) 9 | meas = data['meas'] 10 | dtypes = meas.dtype 11 | meas = np.squeeze(meas).tolist() 12 | data_dict = {} 13 | for i, name in enumerate(dtypes.names): 14 | data_dict[name] = np.array(meas[i]).squeeze() 15 | 16 | data_dict = pd.DataFrame(data_dict) 17 | data_dict.rename(columns={ 18 | 'Time': 'RecordingTime', 19 | 'Voltage': 'Measured_Voltage', 20 | 'Current': 'Measured_Current', 21 | 'Battery_Temp_degC': 'Measured_Temperature', 22 | }, inplace=True) 23 | return data_dict 24 | -------------------------------------------------------------------------------- /FNN/best_train.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vstark21/SOC_estimation/3c0b93740924cbeeb16a4dd8f363129bee275228/FNN/best_train.png -------------------------------------------------------------------------------- /FNN/best_turnigy_graphene_model.h5: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vstark21/SOC_estimation/3c0b93740924cbeeb16a4dd8f363129bee275228/FNN/best_turnigy_graphene_model.h5 -------------------------------------------------------------------------------- /FNN/create_data.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | import numpy as np 3 | import pandas as pd 4 | from utils import load_data 5 | from tqdm import tqdm 6 | 7 | TEMPERATURES = [-10, -20, 0, 10, 25, 40] 8 | DRIVE_CYCLES = ['UDDS', 'HWFET', 'LA92', 'US06'] 9 | WINDOW_SIZE = 500 10 | 11 | def preprocess_data( 12 | df: pd.DataFrame 13 | ): 14 | # Sort data by time 15 | df = df.sort_values(by='RecordingTime').reset_index(drop=True) 16 | 17 | # Resample data at 1Hz 18 | df = df.loc[::10] 19 | 20 | # Apply rolling window to data on voltage, current and temperature 21 | df['Avg_Measured_Voltage'] = df['Measured_Voltage'].rolling(WINDOW_SIZE, min_periods=1).mean() 22 | df['Avg_Measured_Current'] = df['Measured_Current'].rolling(WINDOW_SIZE, min_periods=1).mean() 23 | df['Avg_Measured_Temperature'] = df['Measured_Temperature'].rolling(WINDOW_SIZE, min_periods=1).mean() 24 | 25 | return df 26 | 27 | def create_data( 28 | data_dir 29 | ) -> pd.DataFrame: 30 | 31 | data_dir = Path(data_dir) 32 | total_data = [] 33 | 34 | for t in tqdm(TEMPERATURES): 35 | t_dir = data_dir / f"{t} degC" 36 | assert t_dir.exists(), f"{t_dir} does not exist" 37 | 38 | for d in DRIVE_CYCLES: 39 | for f in t_dir.glob(f"*{d}*.mat"): 40 | 41 | cur_data = load_data(f) 42 | cur_data = preprocess_data(cur_data) 43 | total_data.append(cur_data) 44 | 45 | total_data = pd.concat(total_data) 46 | total_data.reset_index(drop=True, inplace=True) 47 | 48 | return total_data 49 | 50 | 51 | if __name__ == '__main__': 52 | data = create_data('../data/Turnigy Graphene') 53 | data.to_csv('turnigy_graphene_data.csv', index=False) -------------------------------------------------------------------------------- /FNN/fnn_prediction.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vstark21/SOC_estimation/3c0b93740924cbeeb16a4dd8f363129bee275228/FNN/fnn_prediction.png -------------------------------------------------------------------------------- /FNN/main.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | from utils import * 3 | import matplotlib.pyplot as plt 4 | from create_data import preprocess_data 5 | from train import FEATURES 6 | import tensorflow as tf 7 | from tensorflow import keras 8 | 9 | DATA_FILE = '../04-20-19_05.34 780_LA92_25degC_Turnigy_Graphene.mat' 10 | 11 | if __name__ == '__main__': 12 | 13 | LiPoly = load_data(DATA_FILE) 14 | LiPoly = preprocess_data(LiPoly) 15 | 16 | # Battery capacity in Ah taken from data 17 | nominal_cap = 5. 18 | 19 | # Calculate the SOC using coloumb counting for comparision 20 | LiPoly['Measured_SOC'] = (nominal_cap + LiPoly['Ah']) * 100. / nominal_cap 21 | 22 | # Converting seconds to hours 23 | LiPoly['RecordingTime_Hours'] = LiPoly['RecordingTime'] / 3600 24 | 25 | model = keras.models.load_model('best_turnigy_graphene_model.h5') 26 | SOC_Estimated = model.predict(LiPoly[FEATURES].values) * 100. 27 | SOC_Estimated = np.squeeze(SOC_Estimated) 28 | SOC_Estimated = simple_exponential_smoothing(SOC_Estimated, 0.25) 29 | 30 | percentage_error = np.abs(LiPoly['Measured_SOC'] - SOC_Estimated) / LiPoly['Measured_SOC'] * 100. 31 | print('Mean percentage error: {:.2f}%'.format(np.mean(percentage_error))) 32 | 33 | # Plot the results 34 | fig, axes = plt.subplots(1, 2) 35 | 36 | axes[0].plot(LiPoly['RecordingTime_Hours'], LiPoly['Measured_SOC'], label='Measured SOC', alpha=0.5) 37 | axes[0].plot(LiPoly['RecordingTime_Hours'], SOC_Estimated, label='Estimated SOC using FNN', alpha=0.5) 38 | axes[0].set_ylabel('SOC [%]') 39 | axes[0].set_xlabel('Time [Hours]') 40 | axes[0].set_title('Measured SOC vs. Estimated SOC using FNN') 41 | axes[0].legend() 42 | 43 | # Plot soc error 44 | axes[1].plot(LiPoly['RecordingTime_Hours'], (LiPoly['Measured_SOC'] - SOC_Estimated)) 45 | axes[1].set_ylabel('SOC Error [%]') 46 | axes[1].set_xlabel('Time [Hours]') 47 | axes[1].set_title('SOC Error') 48 | 49 | plt.show() 50 | 51 | -------------------------------------------------------------------------------- /FNN/model.py: -------------------------------------------------------------------------------- 1 | import tensorflow as tf 2 | from tensorflow import keras 3 | from keras import layers 4 | 5 | def create_model(input_shape): 6 | model = keras.Sequential([ 7 | layers.Input(shape=input_shape), 8 | layers.Dense(64, activation='tanh'), 9 | layers.Dense(128), 10 | layers.PReLU(), 11 | layers.Dense(64), 12 | layers.PReLU(), 13 | layers.Dense(1), 14 | layers.ReLU(max_value=1) 15 | ]) 16 | 17 | model.compile( 18 | optimizer=keras.optimizers.Adam(lr=0.001), 19 | loss='mse', 20 | metrics=['mae'] 21 | ) 22 | 23 | return model 24 | 25 | def scheduler(epoch, lr): 26 | gamma = 0.96 27 | lr *= gamma 28 | lr = max(lr, 1e-5) 29 | return lr 30 | 31 | def train_model(model, x_train, x_val, y_train, y_val, epochs, batch_size): 32 | history = model.fit( 33 | x=x_train, 34 | y=y_train, 35 | batch_size=batch_size, 36 | epochs=epochs, 37 | validation_data=(x_val, y_val), 38 | callbacks=[keras.callbacks.LearningRateScheduler(scheduler)] 39 | ) 40 | return history, model -------------------------------------------------------------------------------- /FNN/train.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import pandas as pd 3 | from pathlib import Path 4 | from create_data import create_data 5 | from model import create_model, train_model 6 | from sklearn.model_selection import train_test_split 7 | 8 | import tensorflow as tf 9 | import matplotlib.pyplot as plt 10 | 11 | NOMINAL_CAP = 5.0 12 | DATA_FILE = Path('turnigy_graphene_data.csv') 13 | FEATURES = ['Measured_Voltage', 'Measured_Current', 'Measured_Temperature', 'Avg_Measured_Voltage', 'Avg_Measured_Current', 'Avg_Measured_Temperature'] 14 | LABEL = 'Measured_SOC' 15 | EPOCHS = 100 16 | BATCH_SIZE = 1024 17 | 18 | if __name__ == '__main__': 19 | if not DATA_FILE.exists(): 20 | data = create_data('../data/Turnigy Graphene') 21 | else: 22 | data = pd.read_csv(DATA_FILE) 23 | 24 | data['Measured_SOC'] = (NOMINAL_CAP + data['Ah']) * 100. / NOMINAL_CAP 25 | data['RecordingTime_Hours'] = data['RecordingTime'] / 3600 26 | 27 | print(f"Total data shape: {data.shape}") 28 | 29 | train, val = train_test_split(data, test_size=0.25, random_state=42) 30 | 31 | print(f"Train data shape: {train.shape}") 32 | print(f"Test data shape: {val.shape}") 33 | 34 | x_train = train[FEATURES].values 35 | y_train = train[LABEL].values / 100. 36 | x_val = val[FEATURES].values 37 | y_val = val[LABEL].values / 100. 38 | 39 | model = create_model((len(FEATURES))) 40 | history, model = train_model( 41 | model, 42 | x_train, 43 | x_val, 44 | y_train, 45 | y_val, 46 | epochs=EPOCHS, 47 | batch_size=BATCH_SIZE 48 | ) 49 | model.save('turnigy_graphene_model.h5') 50 | 51 | fig, axes = plt.subplots(1, 3) 52 | axes[0].plot(history.history['loss'], label='Training Loss') 53 | axes[0].plot(history.history['val_loss'], label='Validation Loss') 54 | axes[0].set_title('MSE') 55 | axes[0].legend() 56 | 57 | axes[1].plot(history.history['mae'], label='Training MAE') 58 | axes[1].plot(history.history['val_mae'], label='Validation MAE') 59 | axes[1].set_title('MAE') 60 | axes[1].legend() 61 | 62 | axes[2].plot(history.history['lr'], label='Learning Rate') 63 | axes[2].set_title('Learning Rate') 64 | axes[2].legend() 65 | 66 | plt.show() -------------------------------------------------------------------------------- /FNN/utils.py: -------------------------------------------------------------------------------- 1 | import scipy.io 2 | import numpy as np 3 | import pandas as pd 4 | 5 | def load_data( 6 | mat_file: str 7 | ) -> pd.DataFrame: 8 | data = scipy.io.loadmat(mat_file) 9 | meas = data['meas'] 10 | dtypes = meas.dtype 11 | meas = np.squeeze(meas).tolist() 12 | data_dict = {} 13 | for i, name in enumerate(dtypes.names): 14 | data_dict[name] = np.array(meas[i]).squeeze() 15 | 16 | data_dict = pd.DataFrame(data_dict) 17 | data_dict.rename(columns={ 18 | 'Time': 'RecordingTime', 19 | 'Voltage': 'Measured_Voltage', 20 | 'Current': 'Measured_Current', 21 | 'Battery_Temp_degC': 'Measured_Temperature', 22 | }, inplace=True) 23 | return data_dict 24 | 25 | def simple_exponential_smoothing(arr, alpha=0.9): 26 | """ 27 | Simple exponential smoothing 28 | """ 29 | smoothed = np.zeros(len(arr)) 30 | smoothed[0] = arr[0] 31 | for i in range(1, len(arr)): 32 | smoothed[i] = alpha * arr[i] + (1 - alpha) * smoothed[i - 1] 33 | return smoothed -------------------------------------------------------------------------------- /Final Report.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vstark21/SOC_estimation/3c0b93740924cbeeb16a4dd8f363129bee275228/Final Report.pdf -------------------------------------------------------------------------------- /Final Slides.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vstark21/SOC_estimation/3c0b93740924cbeeb16a4dd8f363129bee275228/Final Slides.pdf -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # SOC_estimation 2 | 3 | State of charge estimation using Extended kalman filter and Feedforward neural network. You can find more details in the [presentation](SOC_Estimation.pdf). 4 | 5 | ## Usage 6 | 7 | Download data from [here](https://data.mendeley.com/datasets/4fx8cjprxm/1) and unzip it to `data/Turnigy Graphene` directory. Then install dependencies. 8 | 9 | ```bash 10 | pip install -r requirements.txt 11 | ``` 12 | 13 | **Running EKF** 14 | 15 | ```bash 16 | cd EKF 17 | python main.py 18 | ``` 19 | 20 | **Running FNN** 21 | 22 | ```bash 23 | cd FNN 24 | python main.py 25 | ``` 26 | 27 | ## References 28 | 29 | - [How to Estimate Battery State of Charge using Deep Learning](https://www.youtube.com/playlist?list=PLn8PRpmsu08qEaoBNHa16bPASDDKNBQI0) 30 | 31 | - [Overview of batteries State of Charge estimation methods](https://www.sciencedirect.com/science/article/pii/S2352146519301905/pdf?md5=e1eaca1e8655197f259bcf88b4e9e47f&pid=1-s2.0-S2352146519301905-main.pdf) 32 | 33 | - https://data.mendeley.com/datasets/4fx8cjprxm/ 34 | 35 | - [State of Charge Estimation Using Extended Kalman Filters for Battery Management System](https://energy.stanford.edu/sites/g/files/sbiybj9971/f/taborelli_onori_ievc.pdf) 36 | 37 | - [State of Charge Estimation based on Kalman Filter](https://in.mathworks.com/matlabcentral/fileexchange/90381-state-of-charge-estimation-function-based-on-kalman-filter) -------------------------------------------------------------------------------- /SOC-estimation-at-room-temperature-a-LA92-drive-cycle-at-25C-b-UDDS-drive-cycle-at.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vstark21/SOC_estimation/3c0b93740924cbeeb16a4dd8f363129bee275228/SOC-estimation-at-room-temperature-a-LA92-drive-cycle-at-25C-b-UDDS-drive-cycle-at.png -------------------------------------------------------------------------------- /SOC_Estimation.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vstark21/SOC_estimation/3c0b93740924cbeeb16a4dd8f363129bee275228/SOC_Estimation.pdf -------------------------------------------------------------------------------- /data/BatteryModel.csv: -------------------------------------------------------------------------------- 1 | SOC,R0,R1,R2,C1,C2,T 2 | 0.109684471,0.051018013,0.000490331,0.0005,20384.45985,200000,-10 3 | 0.159829145,0.033255232,0.000503898,0.0005,19885.42353,200000,-10 4 | 0.244257992,0.028805841,0.00050397,0.0005,19667.24701,200000,-10 5 | 0.286055681,0.025040092,0.000505462,0.0005,19681.54273,200000,-10 6 | 0.487131062,0.024254269,0.000505161,0.0005,20230.497,200000,-10 7 | 0.486642515,0.021873737,0.00050718,0.0005,19693.80303,200000,-10 8 | 0.696047799,0.021629019,0.00050004,0.0005,20364.90758,200000,-10 9 | 0.767149863,0.019957841,0.000504438,0.0005,20252.08473,200000,-10 10 | 0.890346522,0.020999023,0.000499932,0.0005,20201.20969,200000,-10 11 | 0.985512789,0.020897596,0.000504727,0.0005,19848.73883,200000,-10 12 | 0.084055466,0.001000137,0.00049907,0.000495631,20127.1588,201695.7649,0 13 | 0.121439882,0.012843066,0.000497947,0.000484431,20529.33159,207016.8256,0 14 | 0.275060229,0.015773489,0.000495452,0.000503443,20164.82372,199003.7664,0 15 | 0.299594591,0.010568913,0.000506356,0.000496275,19554.70217,202673.8409,0 16 | 0.451897067,0.012842549,0.000504828,0.00049695,19779.03762,200847.7107,0 17 | 0.565691713,0.010162405,0.000504599,0.000498807,19592.02478,202566.3514,0 18 | 0.677203234,0.010726533,0.000499319,0.000501132,20057.94523,198330.235,0 19 | 0.787878049,0.010155313,0.000505662,0.00049939,19775.01198,200313.3975,0 20 | 0.888095779,0.010507843,0.000498598,0.000500443,19945.22997,199863.4095,0 21 | 0.99542336,0.01041737,0.000497742,0.000501072,20218.9431,201614.0483,0 22 | 0.071110798,0.013846206,0.000517305,0.000499931,19295.09258,197943.7078,10 23 | 0.153371203,0.008950557,0.000504511,0.0004919,19973.9125,206282.4189,10 24 | 0.229926575,0.008780311,0.000504415,0.000522796,19997.24132,194454.2852,10 25 | 0.3329605,0.007834271,0.000511754,0.000497236,19539.13755,199967.0097,10 26 | 0.446926319,0.007371312,0.000496082,0.000498209,19976.48365,201208.1448,10 27 | 0.564954283,0.006847608,0.000496362,0.000496461,20060.411,201695.0925,10 28 | 0.679036072,0.006552556,0.000501225,0.000497273,20076.23433,205113.6228,10 29 | 0.78452919,0.006593628,0.000493715,0.000499376,20438.97058,199977.9364,10 30 | 0.892843585,0.006898022,0.00050351,0.000503661,19953.10493,196707.5541,10 31 | 0.994615265,0.00668035,0.000503348,0.000501774,19871.5676,200136.6125,10 32 | 0.065103309,0.017424954,0.000486022,0.000488772,20108.891,203502.8894,25 33 | 0.140862842,0.009366072,0.00050176,0.000497864,19824.19056,204200.8139,25 34 | 0.231201372,0.008508134,0.000499248,0.000499846,19993.18602,200037.9439,25 35 | 0.336262214,0.007834044,0.000496115,0.000495479,20178.12297,202403.3726,25 36 | 0.4367403,0.007232895,0.000499789,0.000500232,20137.88387,200030.2059,25 37 | 0.55121324,0.006716837,0.000495121,0.000502407,20477.31203,198381.048,25 38 | 0.662668404,0.006395642,0.000501191,0.000503902,20088.76679,198503.4131,25 39 | 0.772549675,0.006336677,0.000501637,0.000504341,19885.81375,198134.2765,25 40 | 0.890257972,0.00660493,0.000497013,0.000500666,20392.82212,199336.2547,25 41 | 0.997723571,0.006416031,0.00050321,0.000492471,20001.97694,199950.8149,25 42 | 0.00508583,0.00472931,0.00053604,0.000535684,16732.15337,171880.4513,40 43 | 0.077334304,0.001706837,0.000504197,0.00049106,19566.18357,206799.8055,40 44 | 0.212933763,0.002425138,0.000514078,0.000507067,20344.3909,198967.0011,40 45 | 0.262179829,0.001759095,0.000528667,0.000517108,19175.14084,193859.6045,40 46 | 0.413139091,0.001945511,0.000490667,0.000511585,20262.94837,203644.7837,40 47 | 0.520390223,0.00172803,0.000541283,0.000510458,18447.29543,195065.1482,40 48 | 0.636951682,0.001657929,0.000528384,0.0005091,19359.45269,196021.2757,40 49 | 0.762809801,0.001752199,0.000512774,0.000500386,19619.1726,192824.0397,40 50 | 0.887624617,0.001778161,0.00051786,0.000530881,19398.00219,189488.8157,40 51 | 0.994787581,0.001734029,0.000515144,0.000515451,19575.56183,199163.8823,40 52 | -------------------------------------------------------------------------------- /data/SOC-OCV.csv: -------------------------------------------------------------------------------- 1 | SOC,OCV,T 2 | 1,4.204,-10 3 | 0.95,4.145,-10 4 | 0.9,4.101,-10 5 | 0.8,3.964,-10 6 | 0.7,3.894,-10 7 | 0.6,3.839,-10 8 | 0.5,3.803,-10 9 | 0.4,3.783,-10 10 | 0.3,3.749,-10 11 | 0.2,3.706,-10 12 | 0.15,3.637,-10 13 | 1,4.204,0 14 | 0.95,4.149,0 15 | 0.9,4.103,0 16 | 0.8,3.979,0 17 | 0.7,3.906,0 18 | 0.6,3.849,0 19 | 0.5,3.811,0 20 | 0.4,3.784,0 21 | 0.3,3.75,0 22 | 0.2,3.693,0 23 | 0.15,3.687,0 24 | 0.1,3.488,0 25 | 1,4.201,10 26 | 0.95,4.15,10 27 | 0.9,4.103,10 28 | 0.8,3.989,10 29 | 0.7,3.911,10 30 | 0.6,3.852,10 31 | 0.5,3.813,10 32 | 0.4,3.782,10 33 | 0.3,3.746,10 34 | 0.2,3.701,10 35 | 0.15,3.686,10 36 | 0.1,3.513,10 37 | 1,4.199,25 38 | 0.95,4.151,25 39 | 0.9,4.104,25 40 | 0.8,4.001,25 41 | 0.7,3.916,25 42 | 0.6,3.856,25 43 | 0.5,3.816,25 44 | 0.4,3.777,25 45 | 0.3,3.738,25 46 | 0.2,3.693,25 47 | 0.15,3.681,25 48 | 0.1,3.532,25 49 | 1,4.196,40 50 | 0.95,4.147,40 51 | 0.9,4.1,40 52 | 0.8,4.005,40 53 | 0.7,3.916,40 54 | 0.6,3.857,40 55 | 0.5,3.816,40 56 | 0.4,3.772,40 57 | 0.3,3.73,40 58 | 0.2,3.685,40 59 | 0.15,3.674,40 60 | 0.1,3.536,40 61 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | pandas 2 | numpy 3 | scipy 4 | tqdm 5 | matplotlib 6 | tensorflow 7 | scikit-learn --------------------------------------------------------------------------------