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.
Files changed (122) hide show
  1. pydpeet/__init__.py +56 -0
  2. pydpeet/citations/__init__.py +7 -0
  3. pydpeet/citations/citeme.py +397 -0
  4. pydpeet/citations/html_writer.py +137 -0
  5. pydpeet/dev_utils/__init__.py +7 -0
  6. pydpeet/dev_utils/generate_inits/__init__.py +7 -0
  7. pydpeet/dev_utils/generate_inits/generate_inits.py +327 -0
  8. pydpeet/io/__init__.py +7 -0
  9. pydpeet/io/configs/__init__.py +7 -0
  10. pydpeet/io/configs/config.py +227 -0
  11. pydpeet/io/configs/const.py +1 -0
  12. pydpeet/io/convert.py +594 -0
  13. pydpeet/io/device/__init__.py +7 -0
  14. pydpeet/io/device/arbin_4_23_PV090331/__init__.py +7 -0
  15. pydpeet/io/device/arbin_4_23_PV090331/formatter.py +22 -0
  16. pydpeet/io/device/arbin_4_23_PV090331/mapper.py +18 -0
  17. pydpeet/io/device/arbin_4_23_PV090331/reader.py +68 -0
  18. pydpeet/io/device/arbin_8_00_PV221201/__init__.py +7 -0
  19. pydpeet/io/device/arbin_8_00_PV221201/formatter.py +26 -0
  20. pydpeet/io/device/arbin_8_00_PV221201/mapper.py +18 -0
  21. pydpeet/io/device/arbin_8_00_PV221201/reader.py +66 -0
  22. pydpeet/io/device/basytec_6_3_1_0/__init__.py +7 -0
  23. pydpeet/io/device/basytec_6_3_1_0/formatter.py +18 -0
  24. pydpeet/io/device/basytec_6_3_1_0/mapper.py +18 -0
  25. pydpeet/io/device/basytec_6_3_1_0/reader.py +54 -0
  26. pydpeet/io/device/digatron_4_20_6_236/__init__.py +7 -0
  27. pydpeet/io/device/digatron_4_20_6_236/formatter.py +27 -0
  28. pydpeet/io/device/digatron_4_20_6_236/mapper.py +18 -0
  29. pydpeet/io/device/digatron_4_20_6_236/reader.py +45 -0
  30. pydpeet/io/device/digatron_eis_4_20_6_236/__init__.py +7 -0
  31. pydpeet/io/device/digatron_eis_4_20_6_236/formatter.py +39 -0
  32. pydpeet/io/device/digatron_eis_4_20_6_236/mapper.py +18 -0
  33. pydpeet/io/device/digatron_eis_4_20_6_236/reader.py +42 -0
  34. pydpeet/io/device/neware_8_0_0_516/__init__.py +7 -0
  35. pydpeet/io/device/neware_8_0_0_516/formatter.py +31 -0
  36. pydpeet/io/device/neware_8_0_0_516/mapper.py +18 -0
  37. pydpeet/io/device/neware_8_0_0_516/reader.py +397 -0
  38. pydpeet/io/device/parstat_2_63_3/__init__.py +7 -0
  39. pydpeet/io/device/parstat_2_63_3/formatter.py +34 -0
  40. pydpeet/io/device/parstat_2_63_3/mapper.py +18 -0
  41. pydpeet/io/device/parstat_2_63_3/reader.py +34 -0
  42. pydpeet/io/device/safion_1_9/__init__.py +7 -0
  43. pydpeet/io/device/safion_1_9/formatter.py +25 -0
  44. pydpeet/io/device/safion_1_9/mapper.py +18 -0
  45. pydpeet/io/device/safion_1_9/reader.py +89 -0
  46. pydpeet/io/device/zahner/__init__.py +7 -0
  47. pydpeet/io/device/zahner/formatter.py +73 -0
  48. pydpeet/io/device/zahner/mapper.py +34 -0
  49. pydpeet/io/device/zahner/reader.py +46 -0
  50. pydpeet/io/device/zahner_new/__init__.py +7 -0
  51. pydpeet/io/device/zahner_new/formatter.py +72 -0
  52. pydpeet/io/device/zahner_new/mapper.py +50 -0
  53. pydpeet/io/device/zahner_new/reader.py +46 -0
  54. pydpeet/io/map.py +69 -0
  55. pydpeet/io/read.py +50 -0
  56. pydpeet/io/utils/__init__.py +7 -0
  57. pydpeet/io/utils/ext_path.py +34 -0
  58. pydpeet/io/utils/formatter_utils.py +429 -0
  59. pydpeet/io/utils/load_custom_module.py +33 -0
  60. pydpeet/io/utils/timing.py +36 -0
  61. pydpeet/io/write.py +99 -0
  62. pydpeet/process/__init__.py +7 -0
  63. pydpeet/process/analyze/__init__.py +7 -0
  64. pydpeet/process/analyze/average.py +297 -0
  65. pydpeet/process/analyze/capacity.py +208 -0
  66. pydpeet/process/analyze/configs/__init__.py +7 -0
  67. pydpeet/process/analyze/configs/battery_config.py +38 -0
  68. pydpeet/process/analyze/configs/ocv_config.py +177 -0
  69. pydpeet/process/analyze/configs/step_analyzer_config.py +178 -0
  70. pydpeet/process/analyze/cycle.py +63 -0
  71. pydpeet/process/analyze/efficiency.py +182 -0
  72. pydpeet/process/analyze/energy.py +51 -0
  73. pydpeet/process/analyze/extract/__init__.py +7 -0
  74. pydpeet/process/analyze/extract/dva_ica.py +224 -0
  75. pydpeet/process/analyze/extract/ocv.py +196 -0
  76. pydpeet/process/analyze/power.py +32 -0
  77. pydpeet/process/analyze/resistance.py +87 -0
  78. pydpeet/process/analyze/soc.py +418 -0
  79. pydpeet/process/analyze/soh.py +72 -0
  80. pydpeet/process/analyze/utils.py +122 -0
  81. pydpeet/process/merge/__init__.py +7 -0
  82. pydpeet/process/merge/series.py +274 -0
  83. pydpeet/process/sequence/__init__.py +7 -0
  84. pydpeet/process/sequence/configs/__init__.py +7 -0
  85. pydpeet/process/sequence/configs/config.py +17 -0
  86. pydpeet/process/sequence/step_analyzer.py +440 -0
  87. pydpeet/process/sequence/utils/__init__.py +7 -0
  88. pydpeet/process/sequence/utils/annotate/__init__.py +7 -0
  89. pydpeet/process/sequence/utils/annotate/annotate_primitives.py +419 -0
  90. pydpeet/process/sequence/utils/configs/CONFIG_Fallback.py +231 -0
  91. pydpeet/process/sequence/utils/configs/CONFIG_preprocessing.py +243 -0
  92. pydpeet/process/sequence/utils/configs/__init__.py +7 -0
  93. pydpeet/process/sequence/utils/console_prints/__init__.py +7 -0
  94. pydpeet/process/sequence/utils/console_prints/log_time.py +30 -0
  95. pydpeet/process/sequence/utils/postprocessing/__init__.py +7 -0
  96. pydpeet/process/sequence/utils/postprocessing/df_primitives_correction.py +166 -0
  97. pydpeet/process/sequence/utils/postprocessing/filter_df.py +168 -0
  98. pydpeet/process/sequence/utils/postprocessing/generate_instructions.py +297 -0
  99. pydpeet/process/sequence/utils/preprocessing/__init__.py +7 -0
  100. pydpeet/process/sequence/utils/preprocessing/calculate_thresholds.py +27 -0
  101. pydpeet/process/sequence/utils/processing/__init__.py +7 -0
  102. pydpeet/process/sequence/utils/processing/analyze_segments.py +368 -0
  103. pydpeet/process/sequence/utils/processing/attempt_to_merge_neighboring_segments.py +122 -0
  104. pydpeet/process/sequence/utils/processing/check_CV_results.py +226 -0
  105. pydpeet/process/sequence/utils/processing/check_power_zero_watt_segments.py +85 -0
  106. pydpeet/process/sequence/utils/processing/check_zero_length.py +117 -0
  107. pydpeet/process/sequence/utils/processing/split_in_segments.py +97 -0
  108. pydpeet/process/sequence/utils/processing/supress_smaller_segments.py +99 -0
  109. pydpeet/process/sequence/utils/processing/widen_constant_segments.py +185 -0
  110. pydpeet/process/sequence/utils/visualize/__init__.py +7 -0
  111. pydpeet/process/sequence/utils/visualize/visualize_data.py +288 -0
  112. pydpeet/res/__init__.py +7 -0
  113. pydpeet/settings.py +0 -0
  114. pydpeet/utils/__init__.py +7 -0
  115. pydpeet/utils/logging_style.py +28 -0
  116. pydpeet/version.py +0 -0
  117. pydpeet-0.2.0.dist-info/METADATA +85 -0
  118. pydpeet-0.2.0.dist-info/RECORD +122 -0
  119. pydpeet-0.2.0.dist-info/WHEEL +5 -0
  120. pydpeet-0.2.0.dist-info/licenses/AUTHORS.md +0 -0
  121. pydpeet-0.2.0.dist-info/licenses/LICENCE.md +27 -0
  122. 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