Demonstrate ECM Extractors#
The ECM extractors assemble the required components for Equivalent Circuit model using reference performance test data.
[1]:
%matplotlib inline
from matplotlib import pyplot as plt
from battdat.data import BatteryDataset
import numpy as np
Load an Example Dataset#
We use an example RPT cycle from the CAMP 2023 dataset to demonstrate parameter extraction
[2]:
data = BatteryDataset.from_hdf('files/example-camp-rpt.h5')
C:\Users\lward\AppData\Local\miniconda3\envs\moirae\lib\site-packages\battdat\data.py:57: UserWarning: Metadata was created in a different version of battdat. supplied=0.4.0, current=0.4.1.
warnings.warn(f'Metadata was created in a different version of battdat. supplied={supplied_version}, current={__version__}.')
[3]:
data.tables['raw_data']
[3]:
cycle_number | file_number | test_time | state | current | voltage | step_index | method | substep_index | cycle_capacity | cycle_energy | |
---|---|---|---|---|---|---|---|---|---|---|---|
0 | 1 | 0 | 120.042 | b'charging' | 0.260548 | 3.450065 | 0 | b'constant_current' | 0 | 0.000000 | 0.000000 |
1 | 1 | 0 | 139.182 | b'charging' | 0.260090 | 3.470207 | 0 | b'constant_current' | 0 | 0.001384 | 0.004789 |
2 | 1 | 0 | 439.182 | b'charging' | 0.260014 | 3.487297 | 0 | b'constant_current' | 0 | 0.023055 | 0.080177 |
3 | 1 | 0 | 739.182 | b'charging' | 0.259937 | 3.493553 | 0 | b'constant_current' | 0 | 0.044720 | 0.155796 |
4 | 1 | 0 | 1039.182 | b'charging' | 0.260166 | 3.499809 | 0 | b'constant_current' | 0 | 0.066391 | 0.231572 |
... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... |
180 | 1 | 0 | 41767.320 | b'hold' | 0.000000 | 3.272145 | 3 | b'rest' | 8 | -0.027665 | 0.011427 |
181 | 1 | 0 | 41777.322 | b'hold' | 0.000000 | 3.276875 | 3 | b'rest' | 8 | -0.027665 | 0.011427 |
182 | 1 | 0 | 41787.318 | b'hold' | 0.000000 | 3.280995 | 3 | b'rest' | 8 | -0.027665 | 0.011427 |
183 | 1 | 0 | 41797.320 | b'hold' | 0.000000 | 3.284504 | 3 | b'rest' | 8 | -0.027665 | 0.011427 |
184 | 1 | 0 | 41807.310 | b'hold' | 0.000000 | 3.287556 | 3 | b'rest' | 8 | -0.027665 | 0.011427 |
185 rows × 11 columns
The dataset contain a single slow cycle that samples the entire capacity of the cell.
[4]:
fig, axs = plt.subplots(2, 1, figsize=(3.5, 3.), sharex=True)
raw_data = data.tables['raw_data']
time = (raw_data['test_time'] - raw_data['test_time'].min()) / 3600
axs[0].plot(time, raw_data['voltage'])
axs[0].set_ylabel('Voltage (V)')
axs[1].plot(time, raw_data['current'])
axs[1].set_ylabel('Current (A)')
axs[1].set_xlabel('Time (hr)')
[4]:
Text(0.5, 0, 'Time (hr)')
![../_images/extractors_demonstrate-ecm-extractors_6_1.png](../_images/extractors_demonstrate-ecm-extractors_6_1.png)
Determining Capacity#
The MaxCapacityExtractor
determines capacity by integrating current over time then measuring difference between the maximum and minimum change in charge state.
Note: Integration is actually implemented in battdat.
[5]:
from moirae.extractors.ecm import MaxCapacityExtractor
[6]:
cap = MaxCapacityExtractor().extract(data)
cap
[6]:
MaxTheoreticalCapacity(updatable=set(), base_values=array([[1.48220808]]))
This should match up with the measured change in charge
[7]:
fig, ax = plt.subplots(figsize=(3.5, 2.))
ax.plot(time, raw_data['cycle_capacity'])
ax.set_xlim(ax.get_xlim())
ax.set_ylabel('Capacity Change (A-hr)')
ax.set_xlabel('Time (hr)')
min_cap, max_cap = raw_data['cycle_capacity'].min(), raw_data['cycle_capacity'].max()
ax.fill_between(ax.get_xlim(), min_cap, max_cap, alpha=0.5, color='red', edgecolor='none')
[7]:
<matplotlib.collections.PolyCollection at 0x2274cab1f90>
![../_images/extractors_demonstrate-ecm-extractors_11_1.png](../_images/extractors_demonstrate-ecm-extractors_11_1.png)
The width of the colored span is the estimated capacity and it spans from the lowest capacity (~11hr) and the highest (~6hr)
Open-Circuit Voltage Estimation#
The OCVExtractor
determines the Open Circuit Voltage by fitting a spline to voltage as a function of state of charge.
The first step is to compute the SOC from the capacity change during the cycle and the total capacity estimated by MaxCapacityExtractor
. As the actual state of charge cannot be directly measured, we assume the lowest capacity change is an SOC of 0.
[8]:
raw_data['soc'] = (raw_data['cycle_capacity'] - raw_data['cycle_capacity'].min()) / cap.base_values.item()
[9]:
fig, ax = plt.subplots(figsize=(3.5, 2.))
ax.plot(raw_data['soc'], raw_data['voltage'])
ax.set_ylabel('Voltage (V)')
ax.set_xlabel('SOC')
[9]:
Text(0.5, 0, 'SOC')
![../_images/extractors_demonstrate-ecm-extractors_15_1.png](../_images/extractors_demonstrate-ecm-extractors_15_1.png)
The voltage during charge and discharge are different. Moirae accounts for this by weighing points with lower current more strongly and fitting a smoothing spline.
Note: Make the spline fit data more closely by increating the number of SOC points
[10]:
from moirae.extractors.ecm import OCVExtractor
ocv = OCVExtractor(capacity=cap).extract(data)
[11]:
fig, ax = plt.subplots(figsize=(3.5, 2.))
ax.plot(raw_data['soc'], raw_data['voltage'], label='Data')
soc = np.linspace(0, 1, 64)
fit = ocv(soc)
ax.plot(soc, fit[0, 0, :], 'r--', label='OCV')
ax.set_ylabel('Voltage (V)')
ax.set_xlabel('SOC')
ax.legend()
[11]:
<matplotlib.legend.Legend at 0x2274db7dc00>
![../_images/extractors_demonstrate-ecm-extractors_18_1.png](../_images/extractors_demonstrate-ecm-extractors_18_1.png)
This yields a generally-good representation of the OCV.
[ ]: