Skip to content

Commit 310a7bd

Browse files
Postprocess modiffications (#145)
* ignoring vscode settings * changing name of schema from hold to rest * Fixing nomenclature errors Changing the ChargingState schema from 'hold' to 'rest' caused a number of issues, which should now have been fixed. * Changing the way we assign basic control methods The way to assign the simpler control methods is more streamlined. Short rests are tied only to their duration, independently of how many data points are present. Other short steps are classified as either pulses if they have >=5 data points, or short non-rests, in case there are <5 data points. Non-rest steps that last longer than 30 seconds but have fewer than 5 datapoints are considered unknown control method. * style fix * Allowing user to specify max duration of short period When computing the control method, we now allow the user to specify what they wish to consider is a short enough period to be classified as short rest, pulse, or short non-rest. That value defaults to 30 seconds, as originally intended. * Additional explanation to ControlMethod schema Included additional explanation for some of the pre-specified control methods. * StateOfCharge Enhancer considers coulombic efficiency Now, the StateOfCharge RawDataEnhancer takes into account Coulombic Efficiency, and outputs a third column to the raw dataframe, that of the CE adjusted charge, meaning, the actual charge in the battery, rather than the one observed by the cycler. It defaults to 1.0 (100% efficiency), so, by default, this new column is equivalent to the `'cycled_charge'` column. * style fix * fixing demo notebook
1 parent 5daea0a commit 310a7bd

17 files changed

Lines changed: 153 additions & 83 deletions

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,9 @@ ENV/
110110
env.bak/
111111
venv.bak/
112112

113+
# VSCode settings
114+
.vscode/
115+
113116
# Spyder project settings
114117
.spyderproject
115118
.spyproject

battdat/io/arbin.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ def read_file(self, file: str, file_number: int = 0, start_cycle: int = 0,
4949
# TODO (wardlt): This function should move to post-processing
5050
def compute_state(x):
5151
if abs(x) < 1e-6:
52-
return ChargingState.hold
52+
return ChargingState.rest
5353
return ChargingState.charging if x > 0 else ChargingState.discharging
5454

5555
df_out['state'] = df_out['current'].apply(compute_state)

battdat/io/maccor.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,7 @@ def _parse_time(time: str) -> float:
9696
df_out['time'] = df['DPt Time'].apply(_parse_time)
9797

9898
# 0 is rest, 1 is charge, -1 is discharge
99-
df_out.loc[df_out['state'] == 'R', 'state'] = ChargingState.hold
99+
df_out.loc[df_out['state'] == 'R', 'state'] = ChargingState.rest
100100
df_out.loc[df_out['state'] == 'C', 'state'] = ChargingState.charging
101101
df_out.loc[df_out['state'] == 'D', 'state'] = ChargingState.discharging
102102
df_out.loc[df_out['state'].apply(lambda x: x not in {'R', 'C', 'D'}), 'state'] = ChargingState.unknown

battdat/postprocess/integral.py

Lines changed: 46 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ def __init__(self, reuse_integrals: bool = True):
5656
"""
5757
5858
Args:
59-
reuse_integrals: Whether to reuse the ``cycle_capacity`` and ``cycle_energy`` if they are available
59+
reuse_integrals: Whether to reuse the ``cycled_charge`` and ``cycled_energy`` if they are available
6060
"""
6161
self.reuse_integrals = reuse_integrals
6262

@@ -87,9 +87,9 @@ def _summarize(self, raw_data: pd.DataFrame, cycle_data: pd.DataFrame):
8787
continue
8888

8989
# Perform the integration
90-
if self.reuse_integrals and 'cycle_energy' in cycle_subset.columns and 'cycle_capacity' in cycle_subset.columns:
91-
capacity_change = cycle_subset['cycle_capacity'].values * 3600 # To A-s
92-
energy_change = cycle_subset['cycle_energy'].values * 3600 # To J
90+
if self.reuse_integrals and 'cycled_energy' in cycle_subset.columns and 'cycled_charge' in cycle_subset.columns:
91+
capacity_change = cycle_subset['cycled_charge'].values * 3600 # To A-s
92+
energy_change = cycle_subset['cycled_energy'].values * 3600 # To J
9393
else:
9494
capacity_change = cumulative_trapezoid(cycle_subset['current'], x=cycle_subset['test_time'])
9595
energy_change = cumulative_trapezoid(cycle_subset['current'] * cycle_subset['voltage'], x=cycle_subset['test_time'])
@@ -132,12 +132,45 @@ class StateOfCharge(RawDataEnhancer):
132132
The energy change is determined by integrating the product
133133
of current and voltage.
134134
135-
Output dataframe has 2 new columns:
136-
- ``cycle_capacity``: Amount of charge charged since the beginning of the cycle, in A-hr
137-
- ``cycle_energy``: Amount of energy charged since the beginning of the cycle, in J
135+
Output dataframe has 3 new columns:
136+
- ``cycled_charge``: Amount of observed charge cycled since the beginning of the cycle, in A-hr
137+
- ``cycled_energy``: Amount of observed energy cycled since the beginning of the cycle, in W-hr
138+
- ``CE_charge``: Amount of charge in the battery relative to the beginning of the cycle, accounting for Coulombic
139+
Efficiency, in A-hr
138140
"""
141+
def __init__(self, coulombic_efficiency: float = 1.0):
142+
"""
143+
Args:
144+
coulombic_efficiency: Coulombic efficiency to use when computing the state of charge
145+
"""
146+
self.coulombic_efficiency = coulombic_efficiency
139147

140-
column_names = ['cycle_capacity', 'cycle_energy']
148+
@property
149+
def coulombic_efficiency(self) -> float:
150+
return self._ce
151+
152+
@coulombic_efficiency.setter
153+
def coulombic_efficiency(self, value: float):
154+
if value < 0 or value > 1:
155+
raise ValueError('Coulombic efficiency must be between 0 and 1')
156+
self._ce = value
157+
158+
@property
159+
def column_names(self) -> List[str]:
160+
return ['cycled_charge', 'cycled_energy', 'CE_charge']
161+
162+
def _get_CE_adjusted_curr(self, current: np.ndarray) -> np.ndarray:
163+
"""Adjust the current based on the coulombic efficiency
164+
165+
Args:
166+
current: Current array in A
167+
168+
Returns:
169+
Adjusted current array in A
170+
"""
171+
adjusted_current = current.copy()
172+
adjusted_current[current > 0] *= self.coulombic_efficiency
173+
return adjusted_current
141174

142175
def enhance(self, data: pd.DataFrame):
143176
# Add columns for the capacity and energy
@@ -153,9 +186,12 @@ def enhance(self, data: pd.DataFrame):
153186
cycle_subset = ordered_copy.iloc[start_ind:stop_ind]
154187

155188
# Perform the integration
189+
ce_adj_curr = self._get_CE_adjusted_curr(cycle_subset['current'].to_numpy())
156190
capacity_change = cumulative_trapezoid(cycle_subset['current'], x=cycle_subset['test_time'], initial=0)
191+
ce_charge = cumulative_trapezoid(ce_adj_curr, x=cycle_subset['test_time'], initial=0)
157192
energy_change = cumulative_trapezoid(cycle_subset['current'] * cycle_subset['voltage'], x=cycle_subset['test_time'], initial=0)
158193

159194
# Store them in the raw data
160-
data.loc[cycle_subset['index'], 'cycle_capacity'] = capacity_change / 3600 # To A-hr
161-
data.loc[cycle_subset['index'], 'cycle_energy'] = energy_change / 3600 # To W-hr
195+
data.loc[cycle_subset['index'], 'cycled_charge'] = capacity_change / 3600 # To A-hr
196+
data.loc[cycle_subset['index'], 'CE_charge'] = ce_charge / 3600 # To A-hr
197+
data.loc[cycle_subset['index'], 'cycled_energy'] = energy_change / 3600 # To W-hr

battdat/postprocess/tagging.py

Lines changed: 30 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,16 @@ class AddMethod(RawDataEnhancer):
2424
of these points then assigning regions to constant voltage or current if one varied
2525
more than twice the other.
2626
"""
27+
def __init__(self, short_period_threshold: float = 30.0):
28+
"""
29+
Args:
30+
short_period_threshold: Maximum duration of a step to be considered a short step, in seconds
31+
"""
32+
self.short_period_threshold = short_period_threshold
2733

28-
column_names = ['method']
34+
@property
35+
def column_names(self) -> List[str]:
36+
return ['method']
2937

3038
def enhance(self, df: pd.DataFrame):
3139
# Insert a new column into the dataframe, starting with everything marked as other
@@ -43,23 +51,26 @@ def enhance(self, df: pd.DataFrame):
4351
ind = cycle.index.values
4452
state = cycle['state'].values
4553

46-
if len(ind) < 5 and state[0] == ChargingState.hold:
47-
# if there's a very short rest (less than 5 points)
48-
# we label as "anomalous rest"
49-
df.loc[ind, 'method'] = ControlMethod.short_rest
50-
elif state[0] == ChargingState.hold:
51-
# if there are 5 or more points it's a
52-
# standard "rest"
54+
if t[-1] - t[0] < self.short_period_threshold:
55+
# The step is shorter than 30 seconds
56+
if state[0] == ChargingState.rest:
57+
# If the step is a rest, we label it as a short rest
58+
df.loc[ind, 'method'] = ControlMethod.short_rest
59+
elif len(ind) < 5:
60+
# The step contains fewer than 5 data points, so it is innapropriate to label it as anything
61+
# definitive other than a short non-rest
62+
df.loc[ind, 'method'] = ControlMethod.short_nonrest
63+
else:
64+
# The step is a pulse
65+
df.loc[ind, 'method'] = ControlMethod.pulse
66+
elif state[0] == ChargingState.rest:
67+
# This is a standard rest, which lasts longer than 30 seconds
5368
df.loc[ind, 'method'] = ControlMethod.rest
5469
elif len(ind) < 5:
55-
# if it's a charge or discharge and there
56-
# are fewer than 5 points it is an
57-
# "anomalous charge or discharge"
58-
df.loc[ind, 'method'] = ControlMethod.short_nonrest
59-
elif t[-1] - t[0] < 30:
60-
# if the step is less than 30 seconds
61-
# index as "pulse"
62-
df.loc[ind, 'method'] = ControlMethod.pulse
70+
# The step spans over 30 seconds, but has fewer than 5 data points, rendering inadequate for control
71+
# method determination
72+
df.loc[ind, 'method'] = ControlMethod.unknown
73+
6374
else:
6475
# Normalize the voltage and current before determining which one moves "more"
6576
for x in [voltage, current]:
@@ -191,7 +202,7 @@ def _determine_steps(df: DataFrame, column: str, output_col: str):
191202
def _determine_state(
192203
row: pd.Series,
193204
zero_threshold: float = 1.0e-4
194-
) -> Literal[ChargingState.charging, ChargingState.discharging, ChargingState.hold]:
205+
) -> Literal[ChargingState.charging, ChargingState.discharging, ChargingState.rest]:
195206
"""
196207
Function to help determine the state of the cell based on the current
197208
@@ -200,11 +211,11 @@ def _determine_state(
200211
zero_threshold: Maximum absolute value a current can take to be assigned rest. Defaults to 0.1 mA
201212
202213
Returns
203-
State of the cell, which can be either 'charging', 'discharging', or 'hold'
214+
State of the cell, which can be either 'charging', 'discharging', or 'rest'
204215
"""
205216
current = row['current']
206217
if abs(current) <= zero_threshold:
207-
return ChargingState.hold
218+
return ChargingState.rest
208219
elif current > 0.:
209220
return ChargingState.charging
210221
return ChargingState.discharging

battdat/schemas/column.py

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ class ChargingState(str, Enum):
1111
"""Potential charging states of the battery"""
1212

1313
charging = "charging"
14-
hold = "hold"
14+
rest = "resting"
1515
discharging = "discharging"
1616
unknown = "unknown"
1717

@@ -20,19 +20,25 @@ class ControlMethod(str, Enum):
2020
"""Method used to control battery during a certain step"""
2121

2222
short_rest = "short_rest"
23-
"""A very short rest period. Defined as a step with 4 or fewer measurements with near-zero current"""
23+
"""A very short rest period.
24+
Defined as a step with with near-zero current lasting for a short period of time, which defaults to 30 seconds."""
2425
rest = "rest"
2526
"""An extended period of neither charging nor discharging"""
2627
short_nonrest = "short_nonrest"
27-
"""A very short period of charging or discharging. Defined as a step with 4 or fewer measurements with at least one non-zero current."""
28+
"""A very short period of charging or discharging.
29+
Defined as a step with a non-zero current lasting for a short period of time (defaults to 30 seconds), but with
30+
fewer than 5 data points."""
31+
pulse = "pulse"
32+
"""A short period of a large current lasting for a short period of time, which defaults to 30 seconds.
33+
Must contain at least 5 data points."""
2834
constant_current = "constant_current"
2935
"""A step where the current is held constant"""
3036
constant_voltage = "constant_voltage"
3137
"""A step where the voltage is held constant"""
3238
constant_power = "constant_power"
3339
"""A step where the power is held constant"""
34-
pulse = "pulse"
35-
"""A short period of a large current"""
40+
unknown = "unknown"
41+
"""A step where the control method is not known"""
3642
other = "other"
3743

3844

docs/user-guide/post-processing/cell-capacity.ipynb

Lines changed: 43 additions & 29 deletions
Large diffs are not rendered by default.
-8.6 KB
Loading

tests/files/example-data/resistor-only_complex-cycling.ipynb

Lines changed: 4 additions & 4 deletions
Large diffs are not rendered by default.

tests/files/example-data/resistor-only_simple-cycling.ipynb

Lines changed: 4 additions & 4 deletions
Large diffs are not rendered by default.

0 commit comments

Comments
 (0)