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,419 @@
1
+ import numpy as np
2
+ import pandas as pd
3
+ from numba import njit
4
+
5
+ from pydpeet.process.sequence.utils.console_prints.log_time import log_time
6
+
7
+
8
+ def _annotate_primitives(
9
+ df: pd.DataFrame,
10
+ data_columns: dict[str, str],
11
+ thresholds: dict[str, float],
12
+ show_runtime: bool = False,
13
+ ) -> pd.DataFrame:
14
+ """
15
+ Annotates the primitives in the dataframe with ID, variable, duration, length, min, max, avg, type, direction, slope.
16
+
17
+ Parameters:
18
+ df (pd.DataFrame):Dataframe containing the primitives
19
+ data_columns (dict[str, str]): Mapping of column names to their respective short names
20
+ thresholds (dict[str, float]) : Threshold values for each type of annotation
21
+ show_runtime (bool) : Whether to show the runtime of this function, by default False
22
+
23
+ Returns
24
+ -------
25
+ df (pd.DataFrame): Dataframe with added columns for annotated primitives
26
+ """
27
+ df = _annotate_id(df, data_columns, show_runtime)
28
+ df = _annotate_variable(df, data_columns, show_runtime)
29
+ with log_time("adding duration, length, min, max, avg, type, direction, slope annotation", show_runtime):
30
+ df = _merged_annotations(df, data_columns, thresholds)
31
+
32
+ return df
33
+
34
+
35
+ @njit(cache=True)
36
+ def _compute_segment_id(ID_arrays) -> np.array:
37
+ """
38
+ Compute segment ID array using the ID arrays.
39
+
40
+ Parameters:
41
+ ID_arrays (numpy.ndarray): input arrays to compute segment ID
42
+ Example: ID_arrays = np.array([[-1, -1, 1], [-1, -1, 1], [3, -1, -1], [-1, 10, -1]])
43
+
44
+ Returns:
45
+ segment_id (numpy.ndarray): segment ID array with the same shape as the input array
46
+ Example: segment_id = np.array([1, 1, 2, 3])
47
+
48
+ Notes:
49
+ - The segment ID is computed by checking if each row differs from the previous row.
50
+ - If all columns in a row are -1, the segment ID for that row is -1.
51
+ - The segment ID is incremented for each row that differs from the previous row.
52
+ """
53
+ n_rows, n_cols = ID_arrays.shape
54
+ segment_id = np.empty(n_rows, dtype=np.int64)
55
+ segment_id[:] = -1 # default -1
56
+
57
+ # Check mask_all_minus1 per row: all columns == -1
58
+ mask_all_minus1 = np.ones(n_rows, dtype=np.bool_)
59
+ for i in range(n_rows):
60
+ for j in range(n_cols):
61
+ if ID_arrays[i, j] != -1:
62
+ mask_all_minus1[i] = False
63
+ break
64
+
65
+ # Initialize segment counter and track previous row
66
+ segment_counter = 0
67
+ segment_id[0] = segment_counter if not mask_all_minus1[0] else -1
68
+
69
+ for i in range(1, n_rows):
70
+ if mask_all_minus1[i]:
71
+ segment_id[i] = -1
72
+ else:
73
+ # Check if row differs from previous row
74
+ changed = False
75
+ for j in range(n_cols):
76
+ if ID_arrays[i, j] != ID_arrays[i - 1, j]:
77
+ changed = True
78
+ break
79
+ if changed:
80
+ segment_counter += 1
81
+ segment_id[i] = segment_counter
82
+
83
+ return segment_id
84
+
85
+
86
+ def _annotate_id(
87
+ df: pd.DataFrame,
88
+ data_columns: dict[str, str],
89
+ show_runtime: bool,
90
+ ) -> pd.DataFrame:
91
+ """
92
+ Adds a column 'ID' to the dataframe df based on the values of the columns specified in data_columns.
93
+
94
+ The 'ID' column is computed by checking if each row differs from the previous row.
95
+ If all columns in a row are -1, the 'ID' for that row is -1.
96
+ The 'ID' is incremented for each row that differs from the previous row.
97
+
98
+ Parameters:
99
+ df (pd.DataFrame): Input dataframe.
100
+ data_columns (dict[str, str]): Mapping of column names to their respective short names.
101
+ Example: data_columns = {"I": "Current", "P": "Power", "V": "Voltage"}
102
+ show_runtime (bool): Whether to show the runtime of this function, by default False.
103
+
104
+ Returns:
105
+ pd.DataFrame: The original dataframe with an added column named 'ID'.
106
+ """
107
+ with log_time("adding ID annotation", show_runtime):
108
+ cols = [f"Segment_{v}" for v in data_columns.values()]
109
+ arr = df[cols].to_numpy()
110
+ segment_id = _compute_segment_id(arr)
111
+ df["ID"] = segment_id
112
+
113
+ return df
114
+
115
+
116
+ def _annotate_variable(
117
+ df: pd.DataFrame,
118
+ data_columns: dict[str, str],
119
+ show_runtime: bool,
120
+ ) -> pd.DataFrame:
121
+ """
122
+ Adds a column 'Variable' to the dataframe df based on the values of the columns specified in data_columns.
123
+
124
+ Parameters:
125
+ df (pd.DataFrame): Input dataframe.
126
+ data_columns (dict[str, str]): Mapping of column names to their respective short names.
127
+ Example: data_columns = {"I": "Current", "P": "Power", "V": "Voltage"}
128
+ show_runtime (bool): Whether to show the runtime of this function, by default False.
129
+
130
+ Returns:
131
+ pd.DataFrame: The original dataframe with an added column named 'Variable'.
132
+ """
133
+ with log_time("adding variable annotation", show_runtime):
134
+ cols = [f"Segment_{v}" for v in data_columns.values()]
135
+ short_names = list(data_columns.keys())
136
+ data = df[cols].to_numpy()
137
+ variable_array = np.full(len(df), None, dtype=object)
138
+ for i, name in enumerate(short_names):
139
+ mask = (data[:, i] != -1) & (variable_array == np.array(None))
140
+ variable_array[mask] = name
141
+ df["Variable"] = variable_array
142
+
143
+ return df
144
+
145
+
146
+ @njit(cache=True)
147
+ def _merged_annotations_njit(
148
+ group_of_row: np.array,
149
+ group_var_vals: np.array,
150
+ values_matrix: np.array,
151
+ testtime_s: np.array,
152
+ current_mean_per_group: np.array,
153
+ thresholds_array: np.array,
154
+ ) -> tuple[np.array, np.array, np.array, np.array, np.array, np.array, np.array]:
155
+ """
156
+ Compute the following annotations for each row in the input dataframe:
157
+ - min value
158
+ - max value
159
+ - average value
160
+ - slope
161
+ - length (time range)
162
+ - type (Rest, Constant, Ramp)
163
+ - direction (Neutral, Charge, Discharge, Up, Down)
164
+
165
+ Parameters:
166
+ group_of_row (numpy.array): An array of length n, where each element is the group ID of the corresponding row.
167
+ group_var_vals (numpy.array): An array of length G, where each element is the variable index of the corresponding group.
168
+ values_matrix (numpy.array): A matrix of shape (n, num_vars), where each row is a set of measurements for the corresponding row in the input dataframe.
169
+ testtime_s (numpy.array): An array of length n, where each element is the test time for the corresponding row.
170
+ current_mean_per_group (numpy.array): An array of length G, where each element is the current mean of the corresponding group.
171
+ thresholds_array (numpy.array): An array of length num_vars, where each element is the threshold value for the corresponding variable.
172
+
173
+ Returns:
174
+ min_vals (numpy.array): An array of length n, where each element is the minimum value of the corresponding row of the Variable.
175
+ max_vals (numpy.array): An array of length n, where each element is the maximum value of the corresponding row of the Variable.
176
+ avg_vals (numpy.array): An array of length n, where each element is the average value of the corresponding row of the Variable.
177
+ slope_vals (numpy.array): An array of length n, where each element is the slope of the corresponding row of the Variable.
178
+ length_vals (numpy.array): An array of length n, where each element is the length (time range) of the corresponding row of the Variable.
179
+ type_codes (numpy.array): An array of length n, where each element is the type code of the corresponding row of the Variable.
180
+ dir_codes (numpy.array): An array of length n, where each element is the direction code of the corresponding row of the Variable.
181
+ """
182
+ n = group_of_row.shape[0]
183
+ G = group_var_vals.shape[0]
184
+
185
+ # Group accumulators (typed, fixed-size)
186
+ group_count = np.zeros(G, dtype=np.int64)
187
+ group_sum = np.zeros(G, dtype=np.float64)
188
+ group_min = np.full(G, np.nan, dtype=np.float64)
189
+ group_max = np.full(G, np.nan, dtype=np.float64)
190
+ first_val = np.full(G, np.nan, dtype=np.float64)
191
+ last_val = np.full(G, np.nan, dtype=np.float64)
192
+ init_flag = np.zeros(G, dtype=np.int8) # initialization flag
193
+ time_min = np.full(G, np.nan, dtype=np.float64)
194
+ time_max = np.full(G, np.nan, dtype=np.float64)
195
+
196
+ for i in range(n):
197
+ gid = group_of_row[i]
198
+ if gid == -1:
199
+ continue
200
+ var_idx = group_var_vals[gid]
201
+ val = values_matrix[i, var_idx]
202
+ t = testtime_s[i]
203
+
204
+ if init_flag[gid] == 0:
205
+ # initialize group (first encountered row for this group)
206
+ first_val[gid] = val
207
+ last_val[gid] = val
208
+ group_min[gid] = val
209
+ group_max[gid] = val
210
+ if not np.isnan(val):
211
+ group_sum[gid] = val
212
+ group_count[gid] = 1
213
+ else:
214
+ group_sum[gid] = 0.0
215
+ group_count[gid] = 0
216
+ time_min[gid] = t
217
+ time_max[gid] = t
218
+ init_flag[gid] = 1
219
+ else:
220
+ # update last (df order)
221
+ last_val[gid] = val
222
+ # update min/max/sum/count only for non-nan values
223
+ if not np.isnan(val):
224
+ if np.isnan(group_min[gid]) or val < group_min[gid]:
225
+ group_min[gid] = val
226
+ if np.isnan(group_max[gid]) or val > group_max[gid]:
227
+ group_max[gid] = val
228
+ group_sum[gid] += val
229
+ group_count[gid] += 1
230
+ # update time range
231
+ if t < time_min[gid]:
232
+ time_min[gid] = t
233
+ if t > time_max[gid]:
234
+ time_max[gid] = t
235
+
236
+ min_vals = np.full(n, np.nan, dtype=np.float64)
237
+ max_vals = np.full(n, np.nan, dtype=np.float64)
238
+ avg_vals = np.full(n, np.nan, dtype=np.float64)
239
+ slope_vals = np.full(n, np.nan, dtype=np.float64)
240
+ length_vals = np.full(n, np.nan, dtype=np.float64)
241
+ type_codes = np.full(n, -1, dtype=np.int64) # -1=None, 0=Rest,1=Constant,2=Ramp
242
+ dir_codes = np.full(n, -1, dtype=np.int64) # -1=None, 0=Neutral,1=Charge,2=Discharge,3=Up,4=Down
243
+
244
+ for i in range(n):
245
+ gid = group_of_row[i]
246
+ if gid == -1:
247
+ continue
248
+
249
+ vmin = group_min[gid]
250
+ vmax = group_max[gid]
251
+ cnt = group_count[gid]
252
+ s = group_sum[gid]
253
+ first = first_val[gid]
254
+ last = last_val[gid]
255
+ tmn = time_min[gid]
256
+ tmx = time_max[gid]
257
+
258
+ # avg
259
+ if cnt > 0:
260
+ avg = s / cnt
261
+ else:
262
+ avg = np.nan
263
+
264
+ # length (time range) using np.isnan checks
265
+ if np.isnan(tmn) or np.isnan(tmx):
266
+ length_val = np.nan
267
+ else:
268
+ length_val = tmx - tmn
269
+
270
+ # slope — denom = max(cnt - 1, 1)
271
+ denom = cnt - 1
272
+ if denom < 1:
273
+ denom = 1
274
+ slope_val = (last - first) / denom
275
+
276
+ # Type & Direction
277
+ thr = thresholds_array[group_var_vals[gid]]
278
+ curr_mean = current_mean_per_group[gid] if gid < current_mean_per_group.shape[0] else 0.0
279
+
280
+ if cnt == 0 or np.isnan(vmin) or np.isnan(vmax):
281
+ type_code = -1
282
+ dir_code = -1
283
+ else:
284
+ diff = vmax - vmin
285
+ if diff <= thr and abs(curr_mean) <= thr:
286
+ type_code = 0 # Rest
287
+ dir_code = 0 # Neutral
288
+ elif diff <= thr:
289
+ type_code = 1 # Constant
290
+ dir_code = 1 if curr_mean >= 0 else 2 # Charge / Discharge
291
+ else:
292
+ type_code = 2 # Ramp
293
+ dir_code = 3 if last > first else 4 # Up / Down
294
+
295
+ min_vals[i] = vmin
296
+ max_vals[i] = vmax
297
+ avg_vals[i] = avg
298
+ slope_vals[i] = slope_val
299
+ length_vals[i] = length_val
300
+ type_codes[i] = type_code
301
+ dir_codes[i] = dir_code
302
+
303
+ return min_vals, max_vals, avg_vals, slope_vals, length_vals, type_codes, dir_codes
304
+
305
+
306
+ def _merged_annotations(
307
+ df: pd.DataFrame,
308
+ data_columns: dict[str, str],
309
+ thresholds: dict[str, float],
310
+ ) -> pd.DataFrame:
311
+ # map variable names to integer var indices
312
+ """
313
+ Compute duration, length, min, max, avg, type, direction, slope for each group of (ID, Variable).
314
+
315
+ Parameters:
316
+ df (pd.DataFrame): DataFrame containing the primitives
317
+ data_columns (dict[str, str]): Mapping of column names to their respective short names
318
+ thresholds (dict[str, float]): Threshold values for each type of annotation
319
+
320
+ Returns:
321
+ pd.DataFrame: Dataframe with added columns for annotated primitives
322
+ """
323
+ var_names = list(data_columns.keys())
324
+ num_vars = len(var_names)
325
+ var_map = {name: idx for idx, name in enumerate(var_names)}
326
+
327
+ n = len(df)
328
+ ids = df["ID"].to_numpy(np.int64)
329
+
330
+ # map variables column (strings) to integer indices (-1 if not in var_map)
331
+ # use pandas.map for speed & clarity
332
+ variables = df["Variable"].map(lambda x: var_map.get(x, -1)).to_numpy(np.int64)
333
+
334
+ # Build values_matrix (n x num_vars) where column j is the measurement for var j
335
+ values_matrix = np.full((n, num_vars), np.nan, dtype=np.float64)
336
+ for var_name, col_name in data_columns.items():
337
+ var_idx = var_map[var_name]
338
+ values_matrix[:, var_idx] = df[col_name].to_numpy(np.float64)
339
+
340
+ testtime_s = df["Test_Time[s]"].to_numpy(np.float64)
341
+ current_a = df["Current[A]"].to_numpy(np.float64)
342
+
343
+ # Compute duration per row: count of rows per ID (original annotate_duration_length)
344
+ duration = np.zeros(n, dtype=np.int64)
345
+ valid_mask = ids != -1
346
+ if np.any(valid_mask):
347
+ ids_valid = ids[valid_mask]
348
+ unique_ids, inv_ids = np.unique(ids_valid, return_inverse=True)
349
+ counts_per_id = np.bincount(inv_ids)
350
+ duration_valid = counts_per_id[inv_ids]
351
+ duration[valid_mask] = duration_valid
352
+
353
+ # current_mean per ID (global, used by Type/Direction logic)
354
+ sums_per_id = np.bincount(inv_ids, weights=current_a[valid_mask])
355
+ mean_current_per_id = sums_per_id / counts_per_id
356
+ else:
357
+ unique_ids = np.array([], dtype=np.int64)
358
+ mean_current_per_id = np.array([], dtype=np.float64)
359
+
360
+ # Build groups for (ID, Variable) only for valid rows
361
+ K = num_vars + 1
362
+ mask_valid_var_and_id = (ids != -1) & (variables != -1)
363
+ if np.any(mask_valid_var_and_id):
364
+ keys_valid = ids[mask_valid_var_and_id].astype(np.int64) * K + variables[mask_valid_var_and_id]
365
+ unique_keys, inv_keys = np.unique(keys_valid, return_inverse=True)
366
+ # group_id_vals and group_var_vals
367
+ group_id_vals = unique_keys // K
368
+ group_var_vals = unique_keys % K
369
+
370
+ # group_of_row: map each row to group index (or -1)
371
+ group_of_row = np.full(n, -1, dtype=np.int64)
372
+ group_of_row[mask_valid_var_and_id] = inv_keys
373
+
374
+ # map group_id_vals to positions in unique_ids (unique_ids sorted)
375
+ group_id_pos = np.searchsorted(unique_ids, group_id_vals)
376
+ current_mean_per_group = mean_current_per_id[group_id_pos]
377
+ else:
378
+ group_of_row = np.full(n, -1, dtype=np.int64)
379
+ group_var_vals = np.zeros(0, dtype=np.int64)
380
+ current_mean_per_group = np.zeros(0, dtype=np.float64)
381
+
382
+ # thresholds array by var index
383
+ thresholds_array = np.zeros(num_vars, dtype=np.float64)
384
+ for var_name, thr in thresholds.items():
385
+ if var_name in var_map:
386
+ thresholds_array[var_map[var_name]] = thr
387
+
388
+ # Call Numba worker
389
+ min_vals, max_vals, avg_vals, slope_vals, length_vals, type_codes, dir_codes = _merged_annotations_njit(
390
+ group_of_row,
391
+ group_var_vals,
392
+ values_matrix,
393
+ testtime_s,
394
+ current_mean_per_group,
395
+ thresholds_array,
396
+ )
397
+
398
+ # Map type & direction codes to strings (or None)
399
+ type_map = np.array(["Rest", "Constant", "Ramp"], dtype=object)
400
+ dir_map = np.array(["Neutral", "Charge", "Discharge", "Up", "Down"], dtype=object)
401
+
402
+ type_col = np.empty(n, dtype=object)
403
+ dir_col = np.empty(n, dtype=object)
404
+ for i in range(n):
405
+ tc = type_codes[i]
406
+ dc = dir_codes[i]
407
+ type_col[i] = None if tc == -1 else type_map[tc]
408
+ dir_col[i] = None if dc == -1 else dir_map[dc]
409
+
410
+ df["Duration"] = duration
411
+ df["Length"] = length_vals
412
+ df["Min"] = min_vals
413
+ df["Max"] = max_vals
414
+ df["Avg"] = avg_vals
415
+ df["Type"] = type_col
416
+ df["Direction"] = dir_col
417
+ df["Slope"] = slope_vals
418
+
419
+ return df
@@ -0,0 +1,231 @@
1
+ from pydpeet.process.sequence.configs import config
2
+ from pydpeet.process.sequence.utils.preprocessing.calculate_thresholds import calculate_minimum_definitive_differences
3
+
4
+ # using neware thresholds as default
5
+ THRESHOLD_DICT = config.NEWARE_CT_4008Q_5V12A_S1
6
+ MIN_DEFINITIVE_VOLTAGE_DIFFERENCE, MIN_DEFINITIVE_CURRENT_DIFFERENCE = calculate_minimum_definitive_differences(
7
+ *THRESHOLD_DICT
8
+ )
9
+
10
+ ####### depending on the Noise needs to be adjusted even for measurements of the same device #######
11
+ SEGMENTS_TO_DETECT_CONFIG = [
12
+ # devide threshold by 2 because it's looking above and below the target line
13
+ ("Voltage[V]", MIN_DEFINITIVE_VOLTAGE_DIFFERENCE / 2),
14
+ ("Current[A]", MIN_DEFINITIVE_CURRENT_DIFFERENCE / 2),
15
+ ("Power[W]", (MIN_DEFINITIVE_VOLTAGE_DIFFERENCE + MIN_DEFINITIVE_CURRENT_DIFFERENCE) / 2),
16
+ ]
17
+ ####### depending on the Noise needs to be adjusted even for measurements of the same device #######
18
+ # ORDER IS IMPORTANT!
19
+ ADJUST_SEGMENTS_CONFIG = [
20
+ ("Voltage[V]", MIN_DEFINITIVE_VOLTAGE_DIFFERENCE),
21
+ ("Current[A]", MIN_DEFINITIVE_CURRENT_DIFFERENCE),
22
+ ("Power[W]", (MIN_DEFINITIVE_VOLTAGE_DIFFERENCE + MIN_DEFINITIVE_CURRENT_DIFFERENCE)),
23
+ ]
24
+
25
+ THRESHOLD_CV_SEGMENTS_0A_END = MIN_DEFINITIVE_CURRENT_DIFFERENCE
26
+
27
+ ####### HAS TO USE SAME KEY AS DATA_COLUMNS! only change the values of thresholds! ########
28
+ THRESHOLDS_PRIMITIVE_ANNOTATION = {
29
+ "V": MIN_DEFINITIVE_VOLTAGE_DIFFERENCE,
30
+ "I": MIN_DEFINITIVE_CURRENT_DIFFERENCE,
31
+ "P": MIN_DEFINITIVE_VOLTAGE_DIFFERENCE + MIN_DEFINITIVE_CURRENT_DIFFERENCE,
32
+ }
33
+
34
+ ########################################################################################################################
35
+ SHOW_RUNTIME = True
36
+ THRESHOLD_CONSOLE_PRINTS_ZERO_LENGTH_CHECK = 2
37
+ THRESHOLD_CONSOLE_PRINTS_CV_CHECK = 2
38
+ THRESHOLD_CONSOLE_PRINTS_FINETUNING_WIDTH = 2
39
+ THRESHOLD_CONSOLE_PRINTS_POWER_ZERO_WATT_CHECK = 10
40
+ ########################################################################################################################
41
+ # Visualize Data
42
+ VISUALIZE_PHASES_CONFIG = [
43
+ ("V", "blue"),
44
+ ("I", "red"),
45
+ ("P", "green"),
46
+ ]
47
+ LINE_VISUALIZATION_CONFIG = [
48
+ ("Voltage[V]", "blue", (2.4, 4.3)),
49
+ ("Current[A]", "red", (-10, 10)),
50
+ ("Power[W]", "green", (-40, 20)),
51
+ ]
52
+ START = 0
53
+ END = 1e100
54
+ USE_LINES_FOR_SEGMENTS = True
55
+ SHOW_COLUMN_NAMES = True
56
+ SHOW_TIME = True
57
+ SHOW_ID = True
58
+ SEGMENT_ALPHA = 0.3
59
+ WIDTH_HEIGHT_RATIO = [1, 0.3]
60
+ ########################################################################################################################
61
+ # Generate Instructions
62
+ END_CONDITION_MAP_GENERATE_INSTRUCTIONS = {
63
+ "CC": "voltage",
64
+ "CV": "current",
65
+ "CP": "voltage",
66
+ "Pause": "time",
67
+ }
68
+ ########################################################################################################################
69
+ SEQUENCES_CONFIG: dict[str, dict] = {
70
+ # Complex Sequences
71
+ # Loop rules: "loop": True, "exact_loops": 2, "min_loops": 2, "max_loops": 2, "minimum_IDs": 6
72
+ "Discharge_iOCV": {"loop": True, "minimum_IDs": 4, "sequence": ["CC_Discharge", "Pause"]},
73
+ "Charge_iOCV": {"loop": True, "min_loops": 2, "sequence": ["Pause", "CC_Charge"]},
74
+ "CCCV_Charge": {"loop": False, "sequence": ["CC_Charge", "CV_Charge"]},
75
+ }
76
+ SEGMENTS_CONFIG_SIMPLE: dict[str, dict] = {
77
+ # Primitive segments
78
+ # I, V, P,
79
+ # Charging, Discharging
80
+ "I": {
81
+ "rules": {
82
+ "variable": "I",
83
+ }
84
+ },
85
+ "V": {
86
+ "rules": {
87
+ "variable": "V",
88
+ }
89
+ },
90
+ "P": {
91
+ "rules": {
92
+ "variable": "P",
93
+ }
94
+ },
95
+ "Charging": {
96
+ "rules": {
97
+ "direction": "Charge",
98
+ }
99
+ },
100
+ "Discharging": {
101
+ "rules": {
102
+ "direction": "Discharge",
103
+ }
104
+ },
105
+ }
106
+ SEGMENTS_CONFIG_STANDARD: dict[str, dict] = {
107
+ # Primitive segments
108
+ # Pause
109
+ # CC_Charge, CV_Charge, CP_Charge
110
+ # CC_Discharge, CV_Discharge, CP_Discharge
111
+ # Ramp_Current_Charge, Ramp_Voltage_Charge, Ramp_Power_Charge
112
+ # Ramp_Current_Discharge, Ramp_Voltage_Discharge, Ramp_Power_Discharge
113
+ "Pause": {
114
+ "rules": {
115
+ "type": "Rest",
116
+ }
117
+ },
118
+ "CC_Charge": {
119
+ "rules": {
120
+ "variable": "I",
121
+ "type": "Constant",
122
+ "direction": "Charge",
123
+ }
124
+ },
125
+ "CV_Charge": {
126
+ "rules": {
127
+ "variable": "V",
128
+ "type": "Constant",
129
+ "direction": "Charge",
130
+ }
131
+ },
132
+ "CP_Charge": {
133
+ "rules": {
134
+ "variable": "P",
135
+ "type": "Constant",
136
+ "direction": "Charge",
137
+ }
138
+ },
139
+ "CC_Discharge": {
140
+ "rules": {
141
+ "variable": "I",
142
+ "type": "Constant",
143
+ "direction": "Discharge",
144
+ }
145
+ },
146
+ "CV_Discharge": {
147
+ "rules": {
148
+ "variable": "V",
149
+ "type": "Constant",
150
+ "direction": "Discharge",
151
+ }
152
+ },
153
+ "CP_Discharge": {
154
+ "rules": {
155
+ "variable": "P",
156
+ "type": "Constant",
157
+ "direction": "Discharge",
158
+ }
159
+ },
160
+ "CRamp_Charge": {
161
+ "rules": {
162
+ "variable": "I",
163
+ "type": "Ramp",
164
+ "direction": "Up",
165
+ }
166
+ },
167
+ "VRamp_Charge": {
168
+ "rules": {
169
+ "variable": "V",
170
+ "type": "Ramp",
171
+ "direction": "Up",
172
+ }
173
+ },
174
+ "PRamp_Charge": {
175
+ "rules": {
176
+ "variable": "P",
177
+ "type": "Ramp",
178
+ "direction": "Up",
179
+ }
180
+ },
181
+ "CRamp_Discharge": {
182
+ "rules": {
183
+ "variable": "I",
184
+ "type": "Ramp",
185
+ "direction": "Down",
186
+ }
187
+ },
188
+ "VRamp_Discharge": {
189
+ "rules": {
190
+ "variable": "V",
191
+ "type": "Ramp",
192
+ "direction": "Down",
193
+ }
194
+ },
195
+ "PRamp_Discharge": {
196
+ "rules": {
197
+ "variable": "P",
198
+ "type": "Ramp",
199
+ "direction": "Down",
200
+ }
201
+ },
202
+ }
203
+
204
+
205
+ SEGMENT_SEQUENCE_CONFIG = {
206
+ **SEQUENCES_CONFIG,
207
+ **SEGMENTS_CONFIG_STANDARD,
208
+ **SEGMENTS_CONFIG_SIMPLE,
209
+ }
210
+ ########################################################################################################################
211
+ #### These shouldn't be changed when using ppb Dataframes ####
212
+ DATA_COLUMNS = {
213
+ "V": "Voltage[V]",
214
+ "I": "Current[A]",
215
+ "P": "Power[W]",
216
+ }
217
+ #### These shouldn't be changed when using ppb Dataframes ####
218
+
219
+ FALLBACK_CONFIG = {
220
+ "SEGMENTS_TO_DETECT_CONFIG": SEGMENTS_TO_DETECT_CONFIG,
221
+ "ADJUST_SEGMENTS_CONFIG": ADJUST_SEGMENTS_CONFIG,
222
+ "THRESHOLDS_PRIMITIVE_ANNOTATION": THRESHOLDS_PRIMITIVE_ANNOTATION,
223
+ "SHOW_RUNTIME": SHOW_RUNTIME,
224
+ "DATA_COLUMNS": DATA_COLUMNS,
225
+ "MIN_DEFINITIVE_CURRENT_DIFFERENCE": MIN_DEFINITIVE_CURRENT_DIFFERENCE,
226
+ "THRESHOLD_CONSOLE_PRINTS_ZERO_LENGTH_CHECK": THRESHOLD_CONSOLE_PRINTS_ZERO_LENGTH_CHECK,
227
+ "THRESHOLD_CONSOLE_PRINTS_CV_CHECK": THRESHOLD_CONSOLE_PRINTS_CV_CHECK,
228
+ "THRESHOLD_CONSOLE_PRINTS_FINETUNING_WIDTH": THRESHOLD_CONSOLE_PRINTS_FINETUNING_WIDTH,
229
+ "THRESHOLD_CONSOLE_PRINTS_POWER_ZERO_WATT_CHECK": THRESHOLD_CONSOLE_PRINTS_POWER_ZERO_WATT_CHECK,
230
+ "THRESHOLD_CV_SEGMENTS_0A_END": THRESHOLD_CV_SEGMENTS_0A_END,
231
+ }