Skip to content

Commit

Permalink
Change how we compute cell capacity and energy (#50)
Browse files Browse the repository at this point in the history
* Add a very simple example file

* Add a cycle starting from charge and starting from discharged

* Re-implement integrals and tests

No more steps

* Add schema, fix sign convention

* Caught another sign error

* Add notebook which explains the feature

* Fix capacity and energy integrators  (#55)

* Switching energy units to W-hr in schema to be consistent with calculations performed by CapacityPerCycle summarizer (#52)

Co-authored-by: Victor Venturi <vventuri@Victors-MBP.hsd1.il.comcast.net>

* Changing to_batdata_hdf method

Previously, it seemed that the cycle_stats attributed received the raw_data when
being saved. This hopefully fixes that.

* Add a very simple example file

* Add a cycle starting from charge and starting from discharged

* Re-implement integrals and tests

No more steps

* Add schema, fix sign convention

* Add notebook which explains the feature

* Caught another sign error

* Removing trailing white spaces from commented out lines

* Removing previous way of saving data from comments

* Removing trailing white space from new way of saving data

* Fixes on how capacity integrator works

1. Switched from cumulative_simpson to cumtrapz, which is more stable for the
    "erratic" battery data we usually deal with
2. Implemented correct convention for figuring out if the cycle starts in a
    charged or discharged state

---------

Co-authored-by: Victor Venturi <vventuri@Victors-MBP.hsd1.il.comcast.net>
Co-authored-by: Logan Ward <ward.logan.t@gmail.com>
Co-authored-by: Logan Ward <WardLT@users.noreply.github.com>

* Flake8 fixes

* Flip sign convention in extractors, test data

* Fix the charge capacity unit test

* Add test case with complex cycling

* Save capacity in W-hr

* Add a complex cycle example

* Test against XCEL dataset

Works when I assume XCEL has mis-label time
as seconds instead of minutes

* Reflect that we added another cycle

* Correct time column in test data

* Swap order in names so sorting is better

* Reuse integrals if available

* Clarify a point reviewer thought was confusing

---------

Co-authored-by: Victor Venturi <50371281+victorventuri@users.noreply.github.com>
Co-authored-by: Victor Venturi <vventuri@Victors-MBP.hsd1.il.comcast.net>
  • Loading branch information
3 people authored Apr 19, 2024
1 parent 83aeb00 commit c7f9f99
Show file tree
Hide file tree
Showing 17 changed files with 3,498 additions and 2,230 deletions.
2 changes: 1 addition & 1 deletion batdata/extractors/arbin.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ def generate_dataframe(self, file: str, file_number: int = 0, start_cycle: int =
df_out['cycle_number'] = df_out['cycle_number'].astype('int64')
df_out['file_number'] = file_number # df_out['cycle_number']*0
df_out['test_time'] = np.array(df['test_time'] - df['test_time'][0] + start_time, dtype=float)
df_out['current'] = df['Current']
df_out['current'] = -df['Current']
df_out['temperature'] = df['Temperature']
df_out['internal_resistance'] = df['Internal_Resistance']
df_out['voltage'] = df['Voltage']
Expand Down
15 changes: 4 additions & 11 deletions batdata/extractors/batterydata.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,9 +52,6 @@ def convert_raw_signal_to_batdata(input_df: pd.DataFrame, store_all: bool) -> pd
begin_time = datetime(year=1, month=1, day=1)
output['time'] = output['time'].apply(lambda x: (timedelta(days=x - 366) + begin_time).timestamp())

# Reverse the sign of current
output['current'] *= -1

# Add all other columns as-is
if store_all:
for col in input_df.columns:
Expand All @@ -66,10 +63,10 @@ def convert_raw_signal_to_batdata(input_df: pd.DataFrame, store_all: bool) -> pd

_name_map_summary = {
'Cycle_Index': 'cycle_number',
'Q_chg': 'charge_capacity',
'E_chg': 'charge_energy',
'Q_dis': 'discharge_capacity',
'E_dis': 'discharge_energy',
'Q_chg': 'capacity_charge',
'E_chg': 'energy_charge',
'Q_dis': 'capacity_discharge',
'E_dis': 'energy_discharge',
'CE': 'coulomb_efficiency',
'EE': 'energy_efficiency',
'tsecs_start': 'cycle_start',
Expand All @@ -96,10 +93,6 @@ def convert_summary_to_batdata(input_df: pd.DataFrame, store_all: bool) -> pd.Da
for orig, new in _name_map_summary.items():
output[new] = input_df[orig]

# Convert charge and discharge energy from W-hr to J
for c in ['charge_energy', 'discharge_energy']:
output[c] /= 3600

# Add all other columns as-is
if store_all:
for col in input_df.columns:
Expand Down
2 changes: 1 addition & 1 deletion batdata/extractors/maccor.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ def generate_dataframe(self, file: str, file_number: int = 0, start_cycle: int =
df_out['file_number'] = file_number # df_out['cycle_number']*0
df_out['test_time'] = df['Test (Min)'] * 60 - df['Test (Min)'].iloc[0] * 60 + start_time
df_out['state'] = df['State']
df_out['current'] = df['Amps']
df_out['current'] = -df['Amps']
df_out['current'] = np.where(df['State'] == 'D', -1 * df_out['current'], df_out['current'])
# 0 is rest, 1 is charge, -1 is discharge
df_out.loc[df_out['state'] == 'R', 'state'] = ChargingState.hold
Expand Down
70 changes: 41 additions & 29 deletions batdata/postprocess/integral.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

import numpy as np
import pandas as pd
from scipy.integrate import cumulative_simpson
from scipy.integrate import cumtrapz

from batdata.postprocess.base import RawDataEnhancer, CycleSummarizer

Expand Down Expand Up @@ -40,18 +40,27 @@ class CapacityPerCycle(CycleSummarizer):
Measurements of capacity and energy assume a cycle returns
the battery to the same state as it started the cycle.
Output dataframe has 4 new columns:
- ``discharge_capacity``: Discharge energy per cycle in A-hr
- ``charge_capacity``: Charge energy per the cycle in A-hr
- ``discharge_energy``: Discharge energy per cycle in J
- ``charge_energy``: Charge energy per the cycle in J
Output dataframe has 4 new columns.
- ``capacity_discharge``: Discharge capacity per cycle in A-hr
- ``capacity_charge``: Charge capacity per the cycle in A-hr
- ``energy_charge``: Discharge energy per cycle in J
- ``energy_discharge``: Charge energy per the cycle in J
The full definitions are provided in the :class:`~batdata.schemas.cycling.CycleLevelData` schema
"""

def __init__(self, reuse_integrals: bool = True):
"""
Args:
reuse_integrals: Whether to reuse the ``cycle_capacity`` and ``cycle_energy`` if they are available
"""
self.reuse_integrals = reuse_integrals

@property
def column_names(self) -> List[str]:
output = []
for name in ['charge', 'discharge']:
output.extend([f'{name}_energy', f'{name}_capacity'])
output.extend([f'energy_{name}', f'capacity_{name}'])
return output

def _summarize(self, raw_data: pd.DataFrame, cycle_data: pd.DataFrame):
Expand All @@ -69,36 +78,39 @@ def _summarize(self, raw_data: pd.DataFrame, cycle_data: pd.DataFrame):
cycle_subset = raw_data.iloc[start_ind:stop_ind]

# Perform the integration
# TODO (wardlt): Re-use columns from raw data if available
capacity_change = cumulative_simpson(cycle_subset['current'], x=cycle_subset['test_time'])
energy_change = cumulative_simpson(cycle_subset['current'] * cycle_subset['voltage'], x=cycle_subset['test_time'])
if self.reuse_integrals and 'cycle_energy' in cycle_subset.columns and 'cycle_capacity' in cycle_subset.columns:
capacity_change = cycle_subset['cycle_capacity'].values * 3600 # To A-s
energy_change = cycle_subset['cycle_energy'].values * 3600 # To J
else:
capacity_change = cumtrapz(cycle_subset['current'], x=cycle_subset['test_time'])
energy_change = cumtrapz(cycle_subset['current'] * cycle_subset['voltage'], x=cycle_subset['test_time'])

# Estimate if the battery starts as charged or discharged
max_charge = -capacity_change.min()
max_discharge = capacity_change.max()
max_charge = capacity_change.max()
max_discharge = -capacity_change.min()

starts_charged = max_discharge > max_charge
if np.isclose(max_discharge, max_charge, rtol=0.01):
warnings.warn('Unable to clearly detect if battery started charged or discharged. '
f'Amount discharged is {max_discharge:.2f} A-s, charged is {max_charge:.2f} A-s')
warnings.warn(f'Unable to clearly detect if battery started charged or discharged in cycle {cyc}. '
f'Amount discharged is {max_discharge:.2e} A-s, charged is {max_charge:.2e} A-s')

# Assign the charge and discharge capacity
# One capacity is beginning to maximum change, the other is maximum change to end
# Whether the measured capacities are
if starts_charged:
discharge_cap = max_discharge
charge_cap = max_discharge - capacity_change[-1]
discharge_eng = energy_change.max()
charge_eng = discharge_eng - energy_change[-1]
charge_cap = capacity_change[-1] + max_discharge
discharge_eng = -energy_change.min()
charge_eng = energy_change[-1] + discharge_eng
else:
charge_cap = max_charge
discharge_cap = capacity_change[-1] + max_charge
charge_eng = -energy_change.min()
discharge_eng = energy_change[-1] + charge_eng
discharge_cap = max_charge - capacity_change[-1]
charge_eng = energy_change.max()
discharge_eng = charge_eng - energy_change[-1]

cycle_data.loc[cyc, 'discharge_energy'] = discharge_eng
cycle_data.loc[cyc, 'charge_energy'] = charge_eng
cycle_data.loc[cyc, 'discharge_capacity'] = discharge_cap / 3600. # To A-hr
cycle_data.loc[cyc, 'charge_capacity'] = charge_cap / 3600.
cycle_data.loc[cyc, 'energy_charge'] = charge_eng / 3600. # To W-hr
cycle_data.loc[cyc, 'energy_discharge'] = discharge_eng / 3600.
cycle_data.loc[cyc, 'capacity_charge'] = charge_cap / 3600. # To A-hr
cycle_data.loc[cyc, 'capacity_discharge'] = discharge_cap / 3600.


class StateOfCharge(RawDataEnhancer):
Expand Down Expand Up @@ -131,9 +143,9 @@ def enhance(self, data: pd.DataFrame):
cycle_subset = ordered_copy.iloc[start_ind:stop_ind]

# Perform the integration
capacity_change = cumulative_simpson(cycle_subset['current'], x=cycle_subset['test_time'], initial=0)
energy_change = cumulative_simpson(cycle_subset['current'] * cycle_subset['voltage'], x=cycle_subset['test_time'], initial=0)
capacity_change = cumtrapz(cycle_subset['current'], x=cycle_subset['test_time'], initial=0)
energy_change = cumtrapz(cycle_subset['current'] * cycle_subset['voltage'], x=cycle_subset['test_time'], initial=0)

# Store them in the raw data
data.loc[cycle_subset['index'], 'cycle_capacity'] = capacity_change / 3600
data.loc[cycle_subset['index'], 'cycle_energy'] = energy_change
data.loc[cycle_subset['index'], 'cycle_capacity'] = capacity_change / 3600 # To A-hr
data.loc[cycle_subset['index'], 'cycle_energy'] = energy_change / 3600 # To W-hr
10 changes: 5 additions & 5 deletions batdata/schemas/cycling.py
Original file line number Diff line number Diff line change
Expand Up @@ -174,11 +174,11 @@ class CycleLevelData(ColumnSchema):
cycle_duration: List[float] = Field(None, description='Duration of this cycle. Units: s')

# Related to the total amount of energy or electrons moved
discharge_capacity: List[float] = Field(None, description='Total amount of electrons moved during discharge. Units: A-hr')
discharge_energy: List[float] = Field(None, description='Total amount of energy released during discharge. Units: W-hr')
charge_capacity: List[float] = Field(None, description='Total amount of electrons moved during charge. Units: A-hr')
charge_energy: List[float] = Field(None, description='Total amount of energy stored during charge. Units: W-hr')
coulomb_efficiency: List[float] = Field(None, description='Fraction of electrons that are lost during charge and recharge. Units: %')
capacity_discharge: List[float] = Field(None, description='Total amount of electrons released during discharge. Units: A-hr')
energy_discharge: List[float] = Field(None, description='Total amount of energy released during discharge. Units: W-hr')
capacity_charge: List[float] = Field(None, description='Total amount of electrons stored during charge. Units: A-hr')
energy_charge: List[float] = Field(None, description='Total amount of energy stored during charge. Units: W-hr')
coulomb_efficiency: List[float] = Field(None, description='Fraction of electric charge that is lost during charge and recharge. Units: %')
energy_efficiency: List[float] = Field(None, description='Amount of energy lost during charge and discharge')

# Related to voltage
Expand Down
524 changes: 508 additions & 16 deletions docs/features/cell-capacity.ipynb

Large diffs are not rendered by default.

Binary file modified docs/features/figures/explain-capacities.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion tests/extractors/test_batterydata.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ def test_detect_then_convert(test_files):
assert data.metadata.name == 'p492-13'

# Test a few of columns which require conversion
assert data.raw_data['cycle_number'].max() == 7
assert data.raw_data['cycle_number'].max() == 8
first_measurement = datetime.fromtimestamp(data.raw_data['time'].iloc[0])
assert first_measurement.year == 2020
assert first_measurement.day == 3
Expand Down
Loading

0 comments on commit c7f9f99

Please sign in to comment.