PyDPEET 0.2.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pydpeet/__init__.py +56 -0
- pydpeet/citations/__init__.py +7 -0
- pydpeet/citations/citeme.py +397 -0
- pydpeet/citations/html_writer.py +137 -0
- pydpeet/dev_utils/__init__.py +7 -0
- pydpeet/dev_utils/generate_inits/__init__.py +7 -0
- pydpeet/dev_utils/generate_inits/generate_inits.py +327 -0
- pydpeet/io/__init__.py +7 -0
- pydpeet/io/configs/__init__.py +7 -0
- pydpeet/io/configs/config.py +227 -0
- pydpeet/io/configs/const.py +1 -0
- pydpeet/io/convert.py +594 -0
- pydpeet/io/device/__init__.py +7 -0
- pydpeet/io/device/arbin_4_23_PV090331/__init__.py +7 -0
- pydpeet/io/device/arbin_4_23_PV090331/formatter.py +22 -0
- pydpeet/io/device/arbin_4_23_PV090331/mapper.py +18 -0
- pydpeet/io/device/arbin_4_23_PV090331/reader.py +68 -0
- pydpeet/io/device/arbin_8_00_PV221201/__init__.py +7 -0
- pydpeet/io/device/arbin_8_00_PV221201/formatter.py +26 -0
- pydpeet/io/device/arbin_8_00_PV221201/mapper.py +18 -0
- pydpeet/io/device/arbin_8_00_PV221201/reader.py +66 -0
- pydpeet/io/device/basytec_6_3_1_0/__init__.py +7 -0
- pydpeet/io/device/basytec_6_3_1_0/formatter.py +18 -0
- pydpeet/io/device/basytec_6_3_1_0/mapper.py +18 -0
- pydpeet/io/device/basytec_6_3_1_0/reader.py +54 -0
- pydpeet/io/device/digatron_4_20_6_236/__init__.py +7 -0
- pydpeet/io/device/digatron_4_20_6_236/formatter.py +27 -0
- pydpeet/io/device/digatron_4_20_6_236/mapper.py +18 -0
- pydpeet/io/device/digatron_4_20_6_236/reader.py +45 -0
- pydpeet/io/device/digatron_eis_4_20_6_236/__init__.py +7 -0
- pydpeet/io/device/digatron_eis_4_20_6_236/formatter.py +39 -0
- pydpeet/io/device/digatron_eis_4_20_6_236/mapper.py +18 -0
- pydpeet/io/device/digatron_eis_4_20_6_236/reader.py +42 -0
- pydpeet/io/device/neware_8_0_0_516/__init__.py +7 -0
- pydpeet/io/device/neware_8_0_0_516/formatter.py +31 -0
- pydpeet/io/device/neware_8_0_0_516/mapper.py +18 -0
- pydpeet/io/device/neware_8_0_0_516/reader.py +397 -0
- pydpeet/io/device/parstat_2_63_3/__init__.py +7 -0
- pydpeet/io/device/parstat_2_63_3/formatter.py +34 -0
- pydpeet/io/device/parstat_2_63_3/mapper.py +18 -0
- pydpeet/io/device/parstat_2_63_3/reader.py +34 -0
- pydpeet/io/device/safion_1_9/__init__.py +7 -0
- pydpeet/io/device/safion_1_9/formatter.py +25 -0
- pydpeet/io/device/safion_1_9/mapper.py +18 -0
- pydpeet/io/device/safion_1_9/reader.py +89 -0
- pydpeet/io/device/zahner/__init__.py +7 -0
- pydpeet/io/device/zahner/formatter.py +73 -0
- pydpeet/io/device/zahner/mapper.py +34 -0
- pydpeet/io/device/zahner/reader.py +46 -0
- pydpeet/io/device/zahner_new/__init__.py +7 -0
- pydpeet/io/device/zahner_new/formatter.py +72 -0
- pydpeet/io/device/zahner_new/mapper.py +50 -0
- pydpeet/io/device/zahner_new/reader.py +46 -0
- pydpeet/io/map.py +69 -0
- pydpeet/io/read.py +50 -0
- pydpeet/io/utils/__init__.py +7 -0
- pydpeet/io/utils/ext_path.py +34 -0
- pydpeet/io/utils/formatter_utils.py +429 -0
- pydpeet/io/utils/load_custom_module.py +33 -0
- pydpeet/io/utils/timing.py +36 -0
- pydpeet/io/write.py +99 -0
- pydpeet/process/__init__.py +7 -0
- pydpeet/process/analyze/__init__.py +7 -0
- pydpeet/process/analyze/average.py +297 -0
- pydpeet/process/analyze/capacity.py +208 -0
- pydpeet/process/analyze/configs/__init__.py +7 -0
- pydpeet/process/analyze/configs/battery_config.py +38 -0
- pydpeet/process/analyze/configs/ocv_config.py +177 -0
- pydpeet/process/analyze/configs/step_analyzer_config.py +178 -0
- pydpeet/process/analyze/cycle.py +63 -0
- pydpeet/process/analyze/efficiency.py +182 -0
- pydpeet/process/analyze/energy.py +51 -0
- pydpeet/process/analyze/extract/__init__.py +7 -0
- pydpeet/process/analyze/extract/dva_ica.py +224 -0
- pydpeet/process/analyze/extract/ocv.py +196 -0
- pydpeet/process/analyze/power.py +32 -0
- pydpeet/process/analyze/resistance.py +87 -0
- pydpeet/process/analyze/soc.py +418 -0
- pydpeet/process/analyze/soh.py +72 -0
- pydpeet/process/analyze/utils.py +122 -0
- pydpeet/process/merge/__init__.py +7 -0
- pydpeet/process/merge/series.py +274 -0
- pydpeet/process/sequence/__init__.py +7 -0
- pydpeet/process/sequence/configs/__init__.py +7 -0
- pydpeet/process/sequence/configs/config.py +17 -0
- pydpeet/process/sequence/step_analyzer.py +440 -0
- pydpeet/process/sequence/utils/__init__.py +7 -0
- pydpeet/process/sequence/utils/annotate/__init__.py +7 -0
- pydpeet/process/sequence/utils/annotate/annotate_primitives.py +419 -0
- pydpeet/process/sequence/utils/configs/CONFIG_Fallback.py +231 -0
- pydpeet/process/sequence/utils/configs/CONFIG_preprocessing.py +243 -0
- pydpeet/process/sequence/utils/configs/__init__.py +7 -0
- pydpeet/process/sequence/utils/console_prints/__init__.py +7 -0
- pydpeet/process/sequence/utils/console_prints/log_time.py +30 -0
- pydpeet/process/sequence/utils/postprocessing/__init__.py +7 -0
- pydpeet/process/sequence/utils/postprocessing/df_primitives_correction.py +166 -0
- pydpeet/process/sequence/utils/postprocessing/filter_df.py +168 -0
- pydpeet/process/sequence/utils/postprocessing/generate_instructions.py +297 -0
- pydpeet/process/sequence/utils/preprocessing/__init__.py +7 -0
- pydpeet/process/sequence/utils/preprocessing/calculate_thresholds.py +27 -0
- pydpeet/process/sequence/utils/processing/__init__.py +7 -0
- pydpeet/process/sequence/utils/processing/analyze_segments.py +368 -0
- pydpeet/process/sequence/utils/processing/attempt_to_merge_neighboring_segments.py +122 -0
- pydpeet/process/sequence/utils/processing/check_CV_results.py +226 -0
- pydpeet/process/sequence/utils/processing/check_power_zero_watt_segments.py +85 -0
- pydpeet/process/sequence/utils/processing/check_zero_length.py +117 -0
- pydpeet/process/sequence/utils/processing/split_in_segments.py +97 -0
- pydpeet/process/sequence/utils/processing/supress_smaller_segments.py +99 -0
- pydpeet/process/sequence/utils/processing/widen_constant_segments.py +185 -0
- pydpeet/process/sequence/utils/visualize/__init__.py +7 -0
- pydpeet/process/sequence/utils/visualize/visualize_data.py +288 -0
- pydpeet/res/__init__.py +7 -0
- pydpeet/settings.py +0 -0
- pydpeet/utils/__init__.py +7 -0
- pydpeet/utils/logging_style.py +28 -0
- pydpeet/version.py +0 -0
- pydpeet-0.2.0.dist-info/METADATA +85 -0
- pydpeet-0.2.0.dist-info/RECORD +122 -0
- pydpeet-0.2.0.dist-info/WHEEL +5 -0
- pydpeet-0.2.0.dist-info/licenses/AUTHORS.md +0 -0
- pydpeet-0.2.0.dist-info/licenses/LICENCE.md +27 -0
- pydpeet-0.2.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
|
|
3
|
+
import matplotlib.pyplot as plt
|
|
4
|
+
import numpy as np
|
|
5
|
+
import pandas as pd
|
|
6
|
+
from scipy import integrate
|
|
7
|
+
from scipy.signal import savgol_filter
|
|
8
|
+
|
|
9
|
+
from pydpeet.process.analyze.configs.ocv_config import (
|
|
10
|
+
SEGMENT_SEQUENCE_CONFIG,
|
|
11
|
+
STEP_ANALYZER_PRIMITIVES_CONFIG,
|
|
12
|
+
)
|
|
13
|
+
from pydpeet.process.analyze.extract.ocv import extract_ocv_iocv
|
|
14
|
+
from pydpeet.process.analyze.soc import add_soc
|
|
15
|
+
from pydpeet.process.sequence.step_analyzer import (
|
|
16
|
+
add_primitive_segments,
|
|
17
|
+
extract_sequence_overview,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def compute_ocv_dva_ica(
|
|
22
|
+
df_primitives: pd.DataFrame = None,
|
|
23
|
+
df: pd.DataFrame = None,
|
|
24
|
+
min_pause_lenght: float = 120.0,
|
|
25
|
+
min_loops: float = 70,
|
|
26
|
+
soc_max_voltage: float = 4.21,
|
|
27
|
+
soc_min_voltage: float = 2.49,
|
|
28
|
+
soc_c_ref: float = 4.8,
|
|
29
|
+
savgol: bool = False,
|
|
30
|
+
savgol_window_lenght_percentage: float = 0.07,
|
|
31
|
+
visualize: bool = False,
|
|
32
|
+
) -> pd.DataFrame:
|
|
33
|
+
"""
|
|
34
|
+
Compute DVA and ICA curves from given data.
|
|
35
|
+
|
|
36
|
+
Parameters
|
|
37
|
+
----------
|
|
38
|
+
df_primitives : pandas.DataFrame or None
|
|
39
|
+
DataFrame containing the primitives of the data.
|
|
40
|
+
df : pandas.DataFrame or None
|
|
41
|
+
DataFrame containing the original data.
|
|
42
|
+
min_pause_lenght : float
|
|
43
|
+
Minimum length of a pause in seconds.
|
|
44
|
+
min_loops : float
|
|
45
|
+
Minimum number of unique IDs.
|
|
46
|
+
soc_max_voltage : float
|
|
47
|
+
Maximum voltage of the battery.
|
|
48
|
+
soc_min_voltage : float
|
|
49
|
+
Minimum voltage of the battery.
|
|
50
|
+
soc_c_ref : float
|
|
51
|
+
Reference capacity of the battery.
|
|
52
|
+
savgol : bool
|
|
53
|
+
Whether to apply Savitzky-Golay filter to smooth the data.
|
|
54
|
+
savgol_window_lenght_percentage : float
|
|
55
|
+
Percentage of the window length for the Savitzky-Golay filter.
|
|
56
|
+
visualize : bool
|
|
57
|
+
Whether to visualize the DVA and ICA curves over SOC.
|
|
58
|
+
|
|
59
|
+
Returns
|
|
60
|
+
-------
|
|
61
|
+
pd.DataFrame
|
|
62
|
+
DataFrame containing all DVA and ICA curves.
|
|
63
|
+
"""
|
|
64
|
+
logging.info("Executing DVA and ICA computation...")
|
|
65
|
+
# falls nur ein df übergeben wird:
|
|
66
|
+
if df is not None and df_primitives is not None:
|
|
67
|
+
raise ValueError("Please provide either df or df_primitives, not both!")
|
|
68
|
+
|
|
69
|
+
if df is not None:
|
|
70
|
+
df.drop_duplicates(subset=["Test_Time[s]"], inplace=True)
|
|
71
|
+
df.dropna(subset=["Test_Time[s]"], inplace=True)
|
|
72
|
+
df = df.sort_values("Test_Time[s]")
|
|
73
|
+
|
|
74
|
+
df_primitives = add_primitive_segments(
|
|
75
|
+
df=df,
|
|
76
|
+
STEP_ANALYZER_PRIMITIVES_CONFIG=STEP_ANALYZER_PRIMITIVES_CONFIG,
|
|
77
|
+
SHOW_RUNTIME=False,
|
|
78
|
+
check_CV_0Aend_segments_bool=False,
|
|
79
|
+
check_zero_length_segments_bool=False,
|
|
80
|
+
supress_IO_warnings=True,
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
if df_primitives is not None:
|
|
84
|
+
if df_primitives["Test_Time[s]"].duplicated().any():
|
|
85
|
+
raise ValueError("Duplicated 'Test_Time[s]' values found!")
|
|
86
|
+
|
|
87
|
+
if df_primitives["Test_Time[s]"].isna().any():
|
|
88
|
+
raise ValueError("NaN values found in 'Test_Time[s]'")
|
|
89
|
+
|
|
90
|
+
if not np.all(np.diff(df_primitives["Test_Time[s]"]) > 0):
|
|
91
|
+
raise ValueError("'Test_Time[s]' is not monotonically increasing!")
|
|
92
|
+
|
|
93
|
+
logging.info("Checking if SOC exists in dataframe...")
|
|
94
|
+
if "SOC" in df_primitives.columns:
|
|
95
|
+
logging.info("SOC already exists in df_primitives, skipping SOC calculation...")
|
|
96
|
+
else:
|
|
97
|
+
logging.info("SOC column does not exist in df_primitives, adding it...")
|
|
98
|
+
df_primitives = add_soc(
|
|
99
|
+
df_primitives,
|
|
100
|
+
method="withResetWhenFullAndEmpty",
|
|
101
|
+
max_Voltage=soc_max_voltage,
|
|
102
|
+
min_Voltage=soc_min_voltage,
|
|
103
|
+
C_ref=soc_c_ref,
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
df_segments_and_sequences = extract_sequence_overview(df_primitives, SEGMENT_SEQUENCE_CONFIG)
|
|
107
|
+
|
|
108
|
+
else:
|
|
109
|
+
raise ValueError("No df_primitives found!")
|
|
110
|
+
|
|
111
|
+
logging.info("Calling iOCV detection...")
|
|
112
|
+
# Get every iocv as df
|
|
113
|
+
dfs_per_block = extract_ocv_iocv(
|
|
114
|
+
min_pause_lenght=min_pause_lenght, min_loops=min_loops, visualize=False, df_primitives=df_primitives
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
# Compute SOC
|
|
118
|
+
logging.info("Checking if SOC exists in dataframe...")
|
|
119
|
+
if "SOC" in df_primitives.columns:
|
|
120
|
+
logging.info("SOC already exists in df_primitives, skipping SOC calculation...")
|
|
121
|
+
else:
|
|
122
|
+
logging.info("SOC column does not exist in df_primitives, adding it...")
|
|
123
|
+
df_primitives = add_soc(
|
|
124
|
+
df_primitives.copy(),
|
|
125
|
+
method="withResetWhenFullAndEmpty",
|
|
126
|
+
max_Voltage=soc_max_voltage,
|
|
127
|
+
min_Voltage=soc_min_voltage,
|
|
128
|
+
C_ref=soc_c_ref,
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
logging.info("Computing Capacity in Ah...")
|
|
132
|
+
capacity_Ah_points = (
|
|
133
|
+
integrate.cumulative_trapezoid((df_primitives["Current[A]"]), x=df_primitives["Test_Time[s]"], initial=0) / 3600
|
|
134
|
+
)
|
|
135
|
+
df_primitives["Capacity_Ah"] = capacity_Ah_points
|
|
136
|
+
|
|
137
|
+
logging.info("Labeling DVA/ICA blocks (Charge/Discharge)...")
|
|
138
|
+
dfs_with_type = []
|
|
139
|
+
for df_block in dfs_per_block:
|
|
140
|
+
if df_block.empty:
|
|
141
|
+
continue
|
|
142
|
+
|
|
143
|
+
first_id = df_block["ID"].iloc[0]
|
|
144
|
+
matching_row = df_segments_and_sequences[df_segments_and_sequences["ID"] == first_id]
|
|
145
|
+
|
|
146
|
+
if matching_row.empty:
|
|
147
|
+
block_type = "Unknown"
|
|
148
|
+
elif matching_row["Charge_iOCV"].iloc[0] != 0:
|
|
149
|
+
block_type = "Charge"
|
|
150
|
+
elif matching_row["Discharge_iOCV"].iloc[0] != 0:
|
|
151
|
+
block_type = "Discharge"
|
|
152
|
+
|
|
153
|
+
df_block["DVA_ICA_type"] = block_type
|
|
154
|
+
dfs_with_type.append(df_block)
|
|
155
|
+
|
|
156
|
+
dfs_per_block = dfs_with_type
|
|
157
|
+
|
|
158
|
+
logging.info("Computing DVA and ICA for every block...")
|
|
159
|
+
all_dva_ica_curves = []
|
|
160
|
+
for _, block in enumerate(dfs_per_block):
|
|
161
|
+
df_dva_ica = df_primitives.loc[df_primitives["Test_Time[s]"].isin(block["Test_Time[s]"])]
|
|
162
|
+
|
|
163
|
+
voltage = df_dva_ica["Voltage[V]"].to_numpy()
|
|
164
|
+
capacity = df_dva_ica["Capacity_Ah"].to_numpy()
|
|
165
|
+
|
|
166
|
+
dV = np.diff(voltage, prepend=np.nan)
|
|
167
|
+
dQ = np.diff(capacity, prepend=np.nan)
|
|
168
|
+
|
|
169
|
+
dv_dq = np.divide(dV, dQ)
|
|
170
|
+
df_dva_ica["dV_dQ"] = dv_dq
|
|
171
|
+
df_dva_ica["dQ_dV"] = 1 / df_dva_ica["dV_dQ"]
|
|
172
|
+
|
|
173
|
+
if savgol:
|
|
174
|
+
logging.info("Applying Savitzky-Golay filter...")
|
|
175
|
+
window_length = int(len(df_dva_ica["dQ_dV"]) * savgol_window_lenght_percentage)
|
|
176
|
+
if window_length % 2 == 0:
|
|
177
|
+
window_length += 1
|
|
178
|
+
window_length = max(window_length, 5)
|
|
179
|
+
df_dva_ica["dQ_dV"] = savgol_filter(df_dva_ica["dQ_dV"], window_length, 2)
|
|
180
|
+
|
|
181
|
+
df_dva_ica["DVA_ICA_type"] = block["DVA_ICA_type"].iloc[0]
|
|
182
|
+
all_dva_ica_curves.append(df_dva_ica)
|
|
183
|
+
|
|
184
|
+
if visualize:
|
|
185
|
+
logging.info("Plotting DVA and ICA curves...")
|
|
186
|
+
fig, (ax_dva, ax_ica) = plt.subplots(1, 2, figsize=(14, 6))
|
|
187
|
+
|
|
188
|
+
charge_blocks = [df for df in all_dva_ica_curves if df["DVA_ICA_type"].iloc[0] == "Charge"]
|
|
189
|
+
discharge_blocks = [df for df in all_dva_ica_curves if df["DVA_ICA_type"].iloc[0] == "Discharge"]
|
|
190
|
+
|
|
191
|
+
# Plot charge
|
|
192
|
+
for i, df_c in enumerate(charge_blocks):
|
|
193
|
+
ax_dva.plot(df_c["SOC"], df_c["dV_dQ"], color="blue", label=f"Charge DVA {i + 1}")
|
|
194
|
+
ax_ica.plot(df_c["SOC"], df_c["dQ_dV"], color="blue", label=f"Charge ICA {i + 1}")
|
|
195
|
+
|
|
196
|
+
# Plot discharge
|
|
197
|
+
for i, df_d in enumerate(discharge_blocks):
|
|
198
|
+
ax_dva.plot(df_d["SOC"], df_d["dV_dQ"], color="red", label=f"Discharge DVA {i + 1}")
|
|
199
|
+
ax_ica.plot(df_d["SOC"], df_d["dQ_dV"], color="red", label=f"Discharge ICA {i + 1}")
|
|
200
|
+
|
|
201
|
+
# Set DVA plot properties
|
|
202
|
+
ax_dva.set_xlabel("SOC", fontsize=15)
|
|
203
|
+
ax_dva.set_ylabel("dV/dQ", fontsize=15, color="blue")
|
|
204
|
+
ax_dva.set_title("Differential Voltage Analysis (DVA)", fontsize=15)
|
|
205
|
+
ax_dva.set_ylim(0, 4.00)
|
|
206
|
+
ax_dva.legend(fontsize=12, loc="upper left")
|
|
207
|
+
ax_dva.grid(True, linestyle="--", alpha=0.7)
|
|
208
|
+
ax_dva.tick_params(axis="both", which="major", labelsize=15)
|
|
209
|
+
ax_dva.tick_params(axis="y", labelcolor="b")
|
|
210
|
+
|
|
211
|
+
# Set ICA plot properties
|
|
212
|
+
ax_ica.set_xlabel("SOC", fontsize=15)
|
|
213
|
+
ax_ica.set_ylabel("dQ/dV", fontsize=15, color="blue")
|
|
214
|
+
ax_ica.set_title("Incremental Capacity Analysis (ICA)", fontsize=15)
|
|
215
|
+
ax_ica.legend(fontsize=12, loc="upper left")
|
|
216
|
+
ax_ica.grid(True, linestyle="--", alpha=0.7)
|
|
217
|
+
ax_ica.tick_params(axis="both", which="major", labelsize=15)
|
|
218
|
+
ax_ica.tick_params(axis="y", labelcolor="b")
|
|
219
|
+
|
|
220
|
+
plt.tight_layout()
|
|
221
|
+
plt.show()
|
|
222
|
+
logging.info("Returning DataFrame with all DVA and ICA Curves...")
|
|
223
|
+
|
|
224
|
+
return pd.concat(all_dva_ica_curves, ignore_index=True)
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
|
|
3
|
+
# plot import
|
|
4
|
+
import matplotlib.pyplot as plt
|
|
5
|
+
import numpy as np
|
|
6
|
+
import pandas as pd
|
|
7
|
+
from matplotlib.colors import LinearSegmentedColormap
|
|
8
|
+
|
|
9
|
+
from pydpeet.process.analyze.configs.battery_config import BatteryConfig
|
|
10
|
+
from pydpeet.process.analyze.configs.ocv_config import (
|
|
11
|
+
SEGMENT_SEQUENCE_CONFIG,
|
|
12
|
+
STEP_ANALYZER_PRIMITIVES_CONFIG,
|
|
13
|
+
)
|
|
14
|
+
from pydpeet.process.analyze.soc import (
|
|
15
|
+
SocMethod,
|
|
16
|
+
add_soc,
|
|
17
|
+
)
|
|
18
|
+
from pydpeet.process.sequence.step_analyzer import (
|
|
19
|
+
add_primitive_segments,
|
|
20
|
+
extract_sequence_overview,
|
|
21
|
+
)
|
|
22
|
+
from pydpeet.process.sequence.utils.postprocessing.filter_df import filter_and_split_df_by_blocks
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def extract_ocv_iocv(
|
|
26
|
+
min_pause_lenght: float = 120,
|
|
27
|
+
min_loops: float = 70,
|
|
28
|
+
visualize: bool = True,
|
|
29
|
+
df_primitives: pd.DataFrame = None,
|
|
30
|
+
df: pd.DataFrame = None,
|
|
31
|
+
config: BatteryConfig = None,
|
|
32
|
+
) -> pd.DataFrame:
|
|
33
|
+
"""
|
|
34
|
+
Compute iOCV blocks from given DataFrames.
|
|
35
|
+
|
|
36
|
+
Parameters
|
|
37
|
+
----------
|
|
38
|
+
min_pause_lenght : float
|
|
39
|
+
The minimum length of a pause in seconds.
|
|
40
|
+
min_loops : float
|
|
41
|
+
The minimum number of unique IDs.
|
|
42
|
+
visualize : bool
|
|
43
|
+
Whether to visualize the iOCV curves.
|
|
44
|
+
df_primitives : pandas.DataFrame
|
|
45
|
+
The DataFrame containing the primitives.
|
|
46
|
+
df : pandas.DataFrame
|
|
47
|
+
The original DataFrame.
|
|
48
|
+
soc_max_voltage : float
|
|
49
|
+
The maximum voltage of the battery.
|
|
50
|
+
soc_min_voltage : float
|
|
51
|
+
The minimum voltage of the battery.
|
|
52
|
+
soc_c_ref : float
|
|
53
|
+
The reference capacity of the battery.
|
|
54
|
+
Returns
|
|
55
|
+
-------
|
|
56
|
+
pd.DataFrame
|
|
57
|
+
A DataFrame containing all iOCV blocks.
|
|
58
|
+
|
|
59
|
+
"""
|
|
60
|
+
|
|
61
|
+
# Sanity Checks for Input
|
|
62
|
+
if df is not None and df_primitives is not None:
|
|
63
|
+
raise ValueError("Please provide either df or df_primitives, not both!")
|
|
64
|
+
raise ValueError("Please provide either df or df_primitives, not both!")
|
|
65
|
+
|
|
66
|
+
if df is not None:
|
|
67
|
+
df.drop_duplicates(subset=["Test_Time[s]"], inplace=True)
|
|
68
|
+
df.dropna(subset=["Test_Time[s]"], inplace=True)
|
|
69
|
+
df = df.sort_values("Test_Time[s]")
|
|
70
|
+
|
|
71
|
+
df_primitives = add_primitive_segments(
|
|
72
|
+
df=df,
|
|
73
|
+
STEP_ANALYZER_PRIMITIVES_CONFIG=STEP_ANALYZER_PRIMITIVES_CONFIG,
|
|
74
|
+
# SHOW_RUNTIME=False,
|
|
75
|
+
# check_CV_0Aend_segments_bool=False,
|
|
76
|
+
# check_zero_length_segments_bool=False,
|
|
77
|
+
# supress_IO_warnings=True
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
if df_primitives is not None:
|
|
81
|
+
if df_primitives["Test_Time[s]"].duplicated().any():
|
|
82
|
+
raise ValueError("Duplicated 'Test_Time[s]' values found!")
|
|
83
|
+
|
|
84
|
+
if df_primitives["Test_Time[s]"].isna().any():
|
|
85
|
+
raise ValueError("NaN values found in 'Test_Time[s]'")
|
|
86
|
+
|
|
87
|
+
if not np.all(np.diff(df_primitives["Test_Time[s]"]) > 0):
|
|
88
|
+
raise ValueError("'Test_Time[s]' is not monotonically increasing!")
|
|
89
|
+
|
|
90
|
+
logging.info("Checking if SOC exists in dataframe...")
|
|
91
|
+
|
|
92
|
+
if "SOC" in df_primitives.columns:
|
|
93
|
+
logging.info("SOC already exists in df_primitives, skipping SOC calculation...")
|
|
94
|
+
else:
|
|
95
|
+
logging.info("SOC column does not exist in df_primitives, adding it...")
|
|
96
|
+
df_primitives = add_soc(
|
|
97
|
+
df=df_primitives,
|
|
98
|
+
df_primitives=df_primitives,
|
|
99
|
+
standard_method=SocMethod.WITH_RESET_WHEN_FULL,
|
|
100
|
+
config=config,
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
df_segments_and_sequences = extract_sequence_overview(df_primitives, SEGMENT_SEQUENCE_CONFIG)
|
|
104
|
+
|
|
105
|
+
else:
|
|
106
|
+
raise ValueError("No df_primitives found!")
|
|
107
|
+
|
|
108
|
+
# Applying Rules for iOCV Sequences
|
|
109
|
+
_rules = ["Discharge_iOCV", "Charge_iOCV"]
|
|
110
|
+
_STANDARD_COLUMNS = [
|
|
111
|
+
"Test_Time[s]",
|
|
112
|
+
"Voltage[V]",
|
|
113
|
+
"Current[A]",
|
|
114
|
+
"Power[W]",
|
|
115
|
+
]
|
|
116
|
+
logging.info("Applying rules and standard columns to compute iOCV blocks...")
|
|
117
|
+
dfs_per_block = filter_and_split_df_by_blocks(
|
|
118
|
+
df_segments_and_sequences=df_segments_and_sequences,
|
|
119
|
+
df_primitives=df_primitives,
|
|
120
|
+
rules=_rules,
|
|
121
|
+
# standard_columns=_STANDARD_COLUMNS,
|
|
122
|
+
combine_op="or",
|
|
123
|
+
print_blocks=True,
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
logging.info("Filtering iOCV Points...")
|
|
127
|
+
|
|
128
|
+
# Filtering iOCV Points
|
|
129
|
+
dfs_per_block = [df[df["Type"] == "Rest"] for df in dfs_per_block[0]]
|
|
130
|
+
dfs_per_block = [df.loc[df.groupby("ID")["Test_Time[s]"].idxmax()] for df in dfs_per_block]
|
|
131
|
+
dfs_per_block = [df for df in dfs_per_block if df["ID"].nunique() >= min_loops]
|
|
132
|
+
dfs_per_block = [df for df in dfs_per_block if df["Duration"].min() >= min_pause_lenght]
|
|
133
|
+
|
|
134
|
+
logging.info("Filtering iOCV Charge and Discharge blocks...")
|
|
135
|
+
|
|
136
|
+
dfs_with_type = []
|
|
137
|
+
for df_block in dfs_per_block:
|
|
138
|
+
if df_block.empty:
|
|
139
|
+
continue
|
|
140
|
+
|
|
141
|
+
first_id = df_block["ID"].iloc[0]
|
|
142
|
+
matching_row = df_segments_and_sequences[df_segments_and_sequences["ID"] == first_id]
|
|
143
|
+
|
|
144
|
+
if matching_row.empty:
|
|
145
|
+
iocv_type = "Unknown"
|
|
146
|
+
elif matching_row["Charge_iOCV"].iloc[0] != 0:
|
|
147
|
+
iocv_type = "Charge"
|
|
148
|
+
elif matching_row["Discharge_iOCV"].iloc[0] != 0:
|
|
149
|
+
iocv_type = "Discharge"
|
|
150
|
+
else:
|
|
151
|
+
iocv_type = "Unknown"
|
|
152
|
+
|
|
153
|
+
df_block["iOCV_type"] = iocv_type
|
|
154
|
+
dfs_with_type.append(df_block)
|
|
155
|
+
|
|
156
|
+
dfs_per_block = dfs_with_type
|
|
157
|
+
|
|
158
|
+
if visualize:
|
|
159
|
+
logging.info("Plotting iOCV curves...")
|
|
160
|
+
|
|
161
|
+
fig, ax1 = plt.subplots(figsize=(12, 8))
|
|
162
|
+
ax1.set_xlabel("SOC", fontsize=15)
|
|
163
|
+
ax1.set_ylabel("Voltage[V]", fontsize=15, color="blue")
|
|
164
|
+
ax1.tick_params(axis="y", labelcolor="b", labelsize=15)
|
|
165
|
+
ax1.tick_params(axis="x", labelsize=15)
|
|
166
|
+
|
|
167
|
+
# Separate Charge and Discharge blocks
|
|
168
|
+
charge_blocks = [df for df in dfs_per_block if df["iOCV_type"].iloc[0] == "Charge"]
|
|
169
|
+
discharge_blocks = [df for df in dfs_per_block if df["iOCV_type"].iloc[0] == "Discharge"]
|
|
170
|
+
|
|
171
|
+
# Custom colormaps
|
|
172
|
+
charge_cmap = LinearSegmentedColormap.from_list("charge_cmap", ["green", "blue"])
|
|
173
|
+
discharge_cmap = LinearSegmentedColormap.from_list("discharge_cmap", ["red", "yellow"])
|
|
174
|
+
|
|
175
|
+
for i, df in enumerate(charge_blocks):
|
|
176
|
+
color = charge_cmap(i / max(len(charge_blocks) - 1, 1))
|
|
177
|
+
label = f"Charge iOCV {i + 1}"
|
|
178
|
+
ax1.plot(df["SOC"], df["Voltage[V]"], color=color, label=label, linewidth=2, linestyle="-", markersize=1)
|
|
179
|
+
|
|
180
|
+
for i, df in enumerate(discharge_blocks):
|
|
181
|
+
color = discharge_cmap(i / max(len(discharge_blocks) - 1, 1))
|
|
182
|
+
label = f"Discharge iOCV {i + 1}"
|
|
183
|
+
ax1.plot(df["SOC"], df["Voltage[V]"], color=color, label=label, linewidth=2, linestyle="-", markersize=1)
|
|
184
|
+
|
|
185
|
+
# Combine legends
|
|
186
|
+
handles1, labels1 = ax1.get_legend_handles_labels()
|
|
187
|
+
ax1.legend(handles1, labels1, loc="lower right", fontsize=15)
|
|
188
|
+
ax1.set_title("iOCV over SOC", fontsize=15)
|
|
189
|
+
ax1.grid(True, linestyle="--", alpha=0.7)
|
|
190
|
+
|
|
191
|
+
plt.tight_layout()
|
|
192
|
+
plt.show()
|
|
193
|
+
|
|
194
|
+
logging.info("Returning Dataframe with iOCV blocks...")
|
|
195
|
+
|
|
196
|
+
return dfs_per_block
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
|
|
3
|
+
import pandas as pd
|
|
4
|
+
|
|
5
|
+
from pydpeet.process.analyze.utils import (
|
|
6
|
+
StepTimer,
|
|
7
|
+
_check_columns,
|
|
8
|
+
)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def add_power(
|
|
12
|
+
df: pd.DataFrame,
|
|
13
|
+
verbose: bool = True,
|
|
14
|
+
) -> pd.DataFrame:
|
|
15
|
+
"""
|
|
16
|
+
Calculates power [W] from 'Current[A]' and 'Voltage[V]' columns and adds it as a new column.
|
|
17
|
+
|
|
18
|
+
Parameters:
|
|
19
|
+
- df (pandas.DataFrame): DataFrame containing 'Current[A]' and 'Voltage[V]' columns
|
|
20
|
+
|
|
21
|
+
Returns:
|
|
22
|
+
- pandas.DataFrame: DataFrame with added 'Power[W]' column
|
|
23
|
+
"""
|
|
24
|
+
logging.info("Calculating Power[W]...")
|
|
25
|
+
|
|
26
|
+
df_mod = df.copy()
|
|
27
|
+
with StepTimer(verbose) as st:
|
|
28
|
+
_check_columns(df_mod, ["Current[A]", "Voltage[V]"])
|
|
29
|
+
df_mod["Power[W]"] = df_mod["Current[A]"] * df_mod["Voltage[V]"]
|
|
30
|
+
st.log("calculated Power[W]")
|
|
31
|
+
|
|
32
|
+
return df_mod
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import inspect
|
|
2
|
+
import logging
|
|
3
|
+
|
|
4
|
+
import numpy as np
|
|
5
|
+
import pandas as pd
|
|
6
|
+
|
|
7
|
+
from pydpeet.process.analyze.configs.battery_config import BatteryConfig
|
|
8
|
+
from pydpeet.process.analyze.utils import (
|
|
9
|
+
StepTimer,
|
|
10
|
+
_check_columns,
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def add_resistance_internal(
|
|
15
|
+
df: pd.DataFrame,
|
|
16
|
+
config: BatteryConfig = None,
|
|
17
|
+
verbose: bool = True,
|
|
18
|
+
) -> pd.DataFrame:
|
|
19
|
+
"""
|
|
20
|
+
Calculate the internal resistance of a battery from given test data.
|
|
21
|
+
|
|
22
|
+
The internal resistance is calculated from the voltage and current differences
|
|
23
|
+
between consecutive points. The calculation is only performed when the following
|
|
24
|
+
conditions are met:
|
|
25
|
+
|
|
26
|
+
1. The time difference is positive (i.e. time is increasing)
|
|
27
|
+
2. The absolute time difference is less than or equal to `max_time_diff`
|
|
28
|
+
3. The absolute current difference is greater than or equal to `min_current_diff`
|
|
29
|
+
4. The absolute voltage difference is greater than or equal to `min_voltage_diff`
|
|
30
|
+
|
|
31
|
+
If `ignore_negative_resistance_values` is True, any calculated internal resistances
|
|
32
|
+
with a value less than or equal to zero are set to NaN. (mainly for a bug in Neware)
|
|
33
|
+
|
|
34
|
+
Parameters:
|
|
35
|
+
df (pandas.DataFrame): Input DataFrame containing battery test data
|
|
36
|
+
config (BatteryConfig): Configuration object containing parameters for internal resistance calculation
|
|
37
|
+
ignore_negative_resistance_values (bool, optional): Whether to set calculated internal resistances with a value less than or equal to zero to NaN (should only appear for Neware cells)
|
|
38
|
+
|
|
39
|
+
Returns:
|
|
40
|
+
pandas.DataFrame: DataFrame with added 'internal_resistance[ohm]', 'delta_t', 'delta_current', and 'delta_voltage' columns
|
|
41
|
+
"""
|
|
42
|
+
required_cols = ["Test_Time[s]", "Current[A]", "Voltage[V]"]
|
|
43
|
+
_check_columns(df, required_cols)
|
|
44
|
+
|
|
45
|
+
if config is None:
|
|
46
|
+
func_name = inspect.currentframe().f_code.co_name
|
|
47
|
+
raise ValueError(f"config is None, please provide a valid config for {func_name}")
|
|
48
|
+
|
|
49
|
+
min_current_diff = config.min_current_diff
|
|
50
|
+
max_time_diff = config.max_time_diff
|
|
51
|
+
min_voltage_diff = config.min_voltage_diff
|
|
52
|
+
ignore_negative_resistance_values = config.ignore_negative_resistance_values
|
|
53
|
+
|
|
54
|
+
df_mod = df.copy()
|
|
55
|
+
logging.info(f"Starting internal resistance computation on dataframe of size {len(df_mod)}...")
|
|
56
|
+
|
|
57
|
+
# Calculate differences
|
|
58
|
+
with StepTimer(verbose) as st:
|
|
59
|
+
delta_t = df_mod["Test_Time[s]"].diff()
|
|
60
|
+
delta_current = df_mod["Current[A]"].diff()
|
|
61
|
+
delta_voltage = df_mod["Voltage[V]"].diff()
|
|
62
|
+
st.log("calculated delta_t, delta_I, delta_V")
|
|
63
|
+
|
|
64
|
+
# Only calculate resistance when:
|
|
65
|
+
mask = (
|
|
66
|
+
(delta_t > 0) # Time is increasing
|
|
67
|
+
& ((delta_t <= max_time_diff) | (delta_t == 0)) # Within max time window
|
|
68
|
+
& (abs(delta_current) >= min_current_diff) # Significant current change
|
|
69
|
+
& (abs(delta_voltage) >= min_voltage_diff) # Significant voltage change
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
# Calculate resistance only for valid points
|
|
73
|
+
with StepTimer(verbose) as st:
|
|
74
|
+
with np.errstate(divide="ignore", invalid="ignore"):
|
|
75
|
+
resistance = delta_voltage / delta_current
|
|
76
|
+
resistance[~((delta_current != 0) & mask)] = np.nan # Set invalid calculations to NaN
|
|
77
|
+
st.log("computed internal resistance for valid points")
|
|
78
|
+
|
|
79
|
+
# Assign the calculated resistances
|
|
80
|
+
df_mod["InternalResistance[ohm]"] = resistance
|
|
81
|
+
|
|
82
|
+
if ignore_negative_resistance_values:
|
|
83
|
+
df_mod["InternalResistance[ohm]"] = df_mod["InternalResistance[ohm]"].mask(
|
|
84
|
+
df_mod["InternalResistance[ohm]"] <= 0
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
return df_mod
|