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,368 @@
1
+ import pandas as pd
2
+
3
+ from pydpeet.process.sequence.utils.console_prints.log_time import log_time
4
+
5
+
6
+ def _match_rules(
7
+ df: pd.DataFrame,
8
+ rules: dict[str, str],
9
+ ) -> pd.Series:
10
+ """
11
+ Match segments in a DataFrame according to rules to create a mask that matches these rules.
12
+
13
+ The rules dictionary can contain the following keys:
14
+ - "min_length_sec": minimum length of the segment in seconds
15
+ - "max_length_sec": maximum length of the segment in seconds
16
+ - "min_avg_abs": minimum average absolute value of the segment
17
+ - "max_slope": maximum slope of the segment
18
+ - "min_abs_slope": minimum absolute slope of the segment
19
+ - "direction": direction of the segment
20
+ - "variable": variable name of the segment
21
+ - "type": type of the segment
22
+
23
+ Parameters:
24
+ df (pd.DataFrame): DataFrame to be matched
25
+ rules (Dict): Dict containing matching rules
26
+ Example: {'direction': 'Charge', 'type': 'Constant', 'variable': 'I'}
27
+ Returns:
28
+ pd.Series: Mask of True and False values where the segment matches the rules
29
+
30
+ """
31
+ mask = pd.Series(True, index=df.index)
32
+
33
+ if "min_length_sec" in rules:
34
+ mask &= df["Length"] >= rules["min_length_sec"]
35
+ if "max_length_sec" in rules:
36
+ mask &= df["Length"] <= rules["max_length_sec"]
37
+ if "min_avg_abs" in rules:
38
+ mask &= df["Avg"].abs() >= rules["min_avg_abs"]
39
+ if "max_slope" in rules:
40
+ mask &= df["Slope"].abs() <= rules["max_slope"]
41
+ if "min_abs_slope" in rules:
42
+ slope_abs = df["Slope"].abs()
43
+ mask &= slope_abs >= rules["min_abs_slope"]
44
+ if "direction" in rules:
45
+ if rules["direction"] == "Up":
46
+ mask &= df["Slope"] > 0
47
+ elif rules["direction"] == "Down":
48
+ mask &= df["Slope"] < 0
49
+ if "variable" in rules:
50
+ mask &= df["Variable"] == rules["variable"]
51
+ if "type" in rules:
52
+ mask &= df["Type"] == rules["type"]
53
+ if "direction" in rules:
54
+ mask &= df["Direction"] == rules["direction"]
55
+
56
+ return mask
57
+
58
+
59
+ def _get_step_mask(
60
+ df: pd.DataFrame,
61
+ step_name: str,
62
+ SEGMENT_SEQUENCE_CONFIG: dict,
63
+ ) -> pd.Series:
64
+ """
65
+ Get a mask for a step in a DataFrame based on its found segments that fit the SEGMENT_SEQUENCE_CONFIG.
66
+
67
+ Parameters:
68
+ df (pd.DataFrame): DataFrame to be matched
69
+ step_name (str): Name of the step in SEGMENT_SEQUENCE_CONFIG
70
+ SEGMENT_SEQUENCE_CONFIG (dict): Dict containing sequence names and rule dicts
71
+
72
+ Returns:
73
+ pd.Series: Mask of True and False values where the segment matches the rules
74
+ """
75
+ step_cfg = SEGMENT_SEQUENCE_CONFIG[step_name]
76
+ if "rules" in step_cfg:
77
+ return _match_rules(df, step_cfg["rules"])
78
+ elif "sequence" in step_cfg:
79
+ if step_name not in df.columns:
80
+ _tag_sequences(df, SEGMENT_SEQUENCE_CONFIG)
81
+
82
+ return df[step_name] != 0
83
+ else:
84
+ # fallback empty mask
85
+ return pd.Series(False, index=df.index)
86
+
87
+
88
+ def _match_merged_sequence(
89
+ df: pd.DataFrame,
90
+ subsequences: list[str],
91
+ SEGMENT_SEQUENCE_CONFIG: dict,
92
+ ) -> pd.Series:
93
+ """
94
+ Match a merged sequence of subsequences or segments in a DataFrame with incremental IDs.
95
+
96
+ Parameters:
97
+ df (pd.DataFrame): DataFrame to be matched
98
+ subsequences (list): List of sequence names that are merged
99
+ Example: ["Discharge_iOCV", "CC_Discharge", "CV_Discharge", "CC_Charge", "Charge_iOCV"]
100
+ SEGMENT_SEQUENCE_CONFIG (dict): Dict containing sequence names and rule dicts
101
+ Example:{"Discharge_iOCV": {"loop": True, "minimum_IDs": 4, "sequence": ["CC_Discharge","Pause"]}, ...}
102
+
103
+ Returns:
104
+ pd.Series: Mask of True and False values where the segment matches the rules
105
+ """
106
+ masks_list = []
107
+ lengths = []
108
+ for sub_seq in subsequences:
109
+ mask = _get_step_mask(df, sub_seq, SEGMENT_SEQUENCE_CONFIG).values
110
+ masks_list.append(mask)
111
+ # Count length of the subsequence pattern:
112
+ # We get the max consecutive segment length in the mask for this sub_seq
113
+ max_len = 0
114
+ current_len = 0
115
+ for val in mask:
116
+ if val:
117
+ current_len += 1
118
+ max_len = max(max_len, current_len)
119
+ else:
120
+ current_len = 0
121
+ lengths.append(max_len if max_len > 0 else 1)
122
+ total_length = sum(lengths)
123
+
124
+ n = len(df)
125
+ seg_id = 1
126
+ col_name = "_".join(subsequences) # dummy name if needed
127
+ if col_name not in df.columns:
128
+ df[col_name] = 0
129
+
130
+ i = 0
131
+ while i <= n - total_length:
132
+ # Check if concatenation of subsequences matches at i
133
+ match = True
134
+ offset = 0
135
+ for idx, mask in enumerate(masks_list):
136
+ length = lengths[idx]
137
+ if not all(mask[i + offset : i + offset + length]):
138
+ match = False
139
+ break
140
+ offset += length
141
+ if match:
142
+ # Assign segment ID
143
+ for k in range(i, i + total_length):
144
+ df.iat[k, df.columns.get_loc(col_name)] = seg_id
145
+ seg_id += 1
146
+ i += total_length
147
+ else:
148
+ i += 1
149
+
150
+ return df[col_name] != 0
151
+
152
+
153
+ def _tag_simple(
154
+ df: pd.DataFrame,
155
+ SEGMENT_SEQUENCE_CONFIG: dict,
156
+ ) -> pd.DataFrame:
157
+ """
158
+ Tag simple segments in a DataFrame with incremental IDs
159
+
160
+ Parameters:
161
+ df (pd.DataFrame): DataFrame to be tagged
162
+ SEGMENT_SEQUENCE_CONFIG (dict): Dict containing sequence names and rule dicts
163
+ Example:{"Current": {"rules": {"variable": "I", ...}}, ...}
164
+
165
+ Returns:
166
+ df (pd.DataFrame): DataFrame with tagged segments
167
+ """
168
+ for name, cfg in SEGMENT_SEQUENCE_CONFIG.items():
169
+ if "rules" not in cfg:
170
+ continue
171
+ mask = _match_rules(df, cfg["rules"])
172
+ # Assign incremental segment IDs to True rows in mask, else 0
173
+ df[name] = 0
174
+ # Find indices where mask is True
175
+ true_indices = df.index[mask]
176
+ # Assign incremental IDs for those rows
177
+ df.loc[true_indices, name] = range(1, len(true_indices) + 1)
178
+
179
+ return df
180
+
181
+
182
+ def _tag_sequences(
183
+ df: pd.DataFrame,
184
+ SEGMENT_SEQUENCE_CONFIG: dict,
185
+ ) -> pd.DataFrame:
186
+ """
187
+ Tag sequences in a DataFrame with incremental IDs (sequences spanning multiple rows use the same value for each row)
188
+
189
+ "merge": True - If you want to use subsequences in the definition of you sequence (e.g. "merge": True, "sequence": ["sequence1","sequence2"])
190
+ "loop": True - If you want to detect a looping definition (e.g. "loop": True, "sequence": ["CC_Discharge","Pause"])
191
+
192
+ Parameters:
193
+ df (pd.DataFrame): DataFrame to be tagged
194
+ SEGMENT_SEQUENCE_CONFIG (dict): Dict containing sequence names and rule dicts
195
+ Example:{"Discharge_iOCV": {"loop": True, "minimum_IDs": 4, "sequence": ["CC_Discharge","Pause"]}, ...}
196
+
197
+ Returns:
198
+ df (pd.DataFrame): DataFrame with tagged sequences
199
+ """
200
+ for name, cfg in SEGMENT_SEQUENCE_CONFIG.items():
201
+ if "sequence" not in cfg:
202
+ continue
203
+
204
+ df[name] = 0
205
+
206
+ if cfg.get("merge", False):
207
+ subsequences = cfg["sequence"]
208
+ for subseq in subsequences:
209
+ if subseq not in df.columns:
210
+ _tag_sequences(df, SEGMENT_SEQUENCE_CONFIG)
211
+ _ = _match_merged_sequence(df, subsequences, SEGMENT_SEQUENCE_CONFIG)
212
+ temp_col = "_".join(subsequences)
213
+ df[name] = df[temp_col]
214
+ df.drop(columns=[temp_col], inplace=True)
215
+ else:
216
+ seq = cfg["sequence"]
217
+ loop = cfg.get("loop", False)
218
+
219
+ step_masks = []
220
+ for step in seq:
221
+ mask = _get_step_mask(df, step, SEGMENT_SEQUENCE_CONFIG).values
222
+ step_masks.append(mask)
223
+
224
+ len_df = len(df)
225
+ len_sequence = len(seq)
226
+ seg_id = 1
227
+ i = 0
228
+
229
+ while i <= len_df - len_sequence:
230
+ if all(step_masks[j][i + j] for j in range(len_sequence)):
231
+ if not loop:
232
+ for j in range(len_sequence):
233
+ df.iat[i + j, df.columns.get_loc(name)] = seg_id
234
+ seg_id += 1
235
+ i += len_sequence
236
+ else:
237
+ start = i
238
+ loop_count = 0
239
+ while i <= len_df - len_sequence and all(step_masks[j][i + j] for j in range(len_sequence)):
240
+ loop_count += 1
241
+ i += len_sequence
242
+
243
+ total_ids = loop_count * len_sequence
244
+ valid = True
245
+
246
+ # Check constraints
247
+ if "minimum_IDs" in cfg:
248
+ valid &= total_ids >= cfg["minimum_IDs"]
249
+ if "min_loops" in cfg:
250
+ valid &= loop_count >= cfg["min_loops"]
251
+ if "max_loops" in cfg:
252
+ valid &= loop_count <= cfg["max_loops"]
253
+ if "exact_loops" in cfg:
254
+ # Prevent conflict with other loop constraints
255
+ if any(k in cfg for k in ["min_loops", "max_loops", "minimum_IDs"]):
256
+ raise ValueError(
257
+ f"'exact_loops' cannot be combined with other loop constraints in {name}"
258
+ )
259
+ valid &= loop_count == cfg["exact_loops"]
260
+
261
+ if valid:
262
+ for k in range(start, i):
263
+ df.iat[k, df.columns.get_loc(name)] = seg_id
264
+ seg_id += 1
265
+ # If not valid, rewind index to skip just one step
266
+ else:
267
+ i = start + 1
268
+ else:
269
+ i += 1
270
+
271
+ return df
272
+
273
+
274
+ def _assign_longest_sequence(
275
+ df: pd.DataFrame,
276
+ SEGMENT_SEQUENCE_CONFIG: dict,
277
+ ) -> pd.DataFrame:
278
+ """
279
+ Assigns the longest sequence found in the DataFrame to each row in the column 'Sequence' using the format "<Occurrence>_<TestName>".
280
+
281
+ Parameters:
282
+ df (pd.DataFrame): The DataFrame to be analyzed.
283
+ SEGMENT_SEQUENCE_CONFIG (dict): A dictionary containing the configuration for the analysis.
284
+ Example:{"Discharge_iOCV": {"loop": True, "minimum_IDs": 4, "sequence": ["CC_Discharge","Pause"]}, ...}
285
+
286
+ Returns:
287
+ df (pd.DataFrame): A DataFrame containing the columns 'ID', 'Sequence', and all columns specified in SEGMENT_SEQUENCE_CONFIG.
288
+ """
289
+ test_names = list(SEGMENT_SEQUENCE_CONFIG.keys())
290
+ df = df.copy()
291
+ df["Sequence"] = None
292
+ counters = {name: 0 for name in test_names}
293
+ n = len(df)
294
+ i = 0
295
+
296
+ # Pre-extract the relevant columns for faster access
297
+ columns_data = {name: df[name].values if name in df.columns else None for name in test_names}
298
+ index = df.index
299
+
300
+ for _ in range(n):
301
+ if i >= n:
302
+ break # safeguard, since we'll manually advance i
303
+
304
+ best_len = 0
305
+ best_name = None
306
+
307
+ for name in test_names:
308
+ col = columns_data[name]
309
+ if col is None or not col[i]:
310
+ continue
311
+ seg_id = col[i]
312
+
313
+ length = 0
314
+ for j in range(i, n):
315
+ if col[j] == seg_id:
316
+ length += 1
317
+ else:
318
+ break
319
+
320
+ if length > best_len:
321
+ best_len = length
322
+ best_name = name
323
+
324
+ if best_name:
325
+ counters[best_name] += 1
326
+ label = f"{counters[best_name]}_{best_name}"
327
+ df.loc[index[i : i + best_len], "Sequence"] = label
328
+ i += best_len
329
+ else:
330
+ i += 1
331
+
332
+ return df
333
+
334
+
335
+ def _analyze_segments(
336
+ df: pd.DataFrame,
337
+ SHOW_RUNTIME: bool,
338
+ SEGMENT_SEQUENCE_CONFIG: dict,
339
+ ) -> pd.DataFrame:
340
+ """
341
+ Analyzes a DataFrame and returns a DataFrame with the columns 'ID', 'Sequence', and
342
+ all columns specified in SEGMENT_SEQUENCE_CONFIG.
343
+
344
+ The function first tags simple segments in the DataFrame, then tags sequences
345
+ (each segment/sequence got the same value during it's ID(s)),
346
+ assigns the longest sequence found to each row in the column 'Sequence' using the format "<Occurrence>_<TestName>",
347
+
348
+ Parameters:
349
+ df (pd.DataFrame): The DataFrame to be analyzed.
350
+ SHOW_RUNTIME (bool): If True, the function logs the time taken to perform each step.
351
+ SEGMENT_SEQUENCE_CONFIG (dict): A dictionary containing the configuration for the analysis.
352
+ Example:{"Current": {"rules": {"variable": "I", ...}}, ...}
353
+
354
+ Returns:
355
+ df_segments_and_sequences (pd.DataFrame): A DataFrame containing the columns 'ID', 'Sequence', and all columns
356
+ specified in SEGMENT_SEQUENCE_CONFIG.
357
+ """
358
+ with log_time("tag simple segments", SHOW_RUNTIME):
359
+ df = _tag_simple(df, SEGMENT_SEQUENCE_CONFIG)
360
+ with log_time("tag sequences", SHOW_RUNTIME):
361
+ df = _tag_sequences(df, SEGMENT_SEQUENCE_CONFIG)
362
+ with log_time("assign longest sequence", SHOW_RUNTIME):
363
+ df = _assign_longest_sequence(df, SEGMENT_SEQUENCE_CONFIG)
364
+ with log_time("separate sequences and df_with_segments", SHOW_RUNTIME):
365
+ segment_keys = [key for key in SEGMENT_SEQUENCE_CONFIG.keys() if key in df.columns]
366
+ df_segments_and_sequences = df[["ID", "Sequence"] + segment_keys]
367
+
368
+ return df_segments_and_sequences
@@ -0,0 +1,122 @@
1
+ import numpy as np
2
+ import pandas as pd
3
+ from numba import njit
4
+
5
+
6
+ @njit(cache=True)
7
+ def _merge_segments_1d_optimized(
8
+ ids: np.array,
9
+ data: np.array,
10
+ threshold: float,
11
+ ) -> np.array:
12
+ """
13
+ Merge adjacent segments in a 1D array if the mean of the adjacent segments is within a threshold.
14
+
15
+ Parameters:
16
+ ids : numpy array of int
17
+ The array of segment IDs
18
+ data : numpy array of float
19
+ The array of data values
20
+ threshold : float
21
+ The maximum difference between adjacent segment means
22
+
23
+ Returns:
24
+ merged_ids : numpy array of int
25
+ The array of merged segment IDs
26
+
27
+ Notes:
28
+ This function uses a prefix sum to quickly compute the mean of each segment. It then checks each pair of adjacent segments if their mean difference is within the threshold. If so, it merges the two segments by replacing the IDs of the right segment with those of the left segment.
29
+
30
+ """
31
+ n = len(ids)
32
+ merged_ids = ids.copy()
33
+
34
+ # Build prefix sum of data
35
+ cumsum = np.empty(n + 1, dtype=np.float64)
36
+ cumsum[0] = 0.0
37
+ for i in range(n):
38
+ cumsum[i + 1] = cumsum[i] + data[i]
39
+
40
+ segments = []
41
+ start = -1
42
+ prev_id = -2
43
+
44
+ # Compute segments
45
+ for i in range(n):
46
+ cid = ids[i]
47
+ if cid != -1:
48
+ if cid != prev_id:
49
+ if start != -1:
50
+ segments.append((start, i - 1, prev_id))
51
+ start = i
52
+ else:
53
+ if start != -1:
54
+ segments.append((start, i - 1, prev_id))
55
+ start = -1
56
+ prev_id = cid
57
+ if start != -1:
58
+ segments.append((start, n - 1, prev_id))
59
+
60
+ # Merge adjacent segments if mean check passes
61
+ for seg_idx in range(len(segments) - 1):
62
+ start_left, end_left, id_left = segments[seg_idx]
63
+ start_right, end_right, id_right = segments[seg_idx + 1]
64
+
65
+ if end_left + 1 != start_right:
66
+ continue
67
+
68
+ total = cumsum[end_right + 1] - cumsum[start_left]
69
+ count = end_right - start_left + 1
70
+ mean_val = total / count
71
+
72
+ ok = True
73
+ for j in range(start_left, end_right + 1):
74
+ if abs(data[j] - mean_val) > threshold:
75
+ ok = False
76
+ break
77
+
78
+ if ok:
79
+ for j in range(start_right, end_right + 1):
80
+ merged_ids[j] = id_left
81
+
82
+ return merged_ids
83
+
84
+
85
+ def _attempt_to_merge_neighboring_segments(
86
+ df: pd.DataFrame,
87
+ adjust_segments_config: list[tuple[str, float]],
88
+ ) -> pd.DataFrame:
89
+ """
90
+ Attempt to merge neighboring segments in the DataFrame if they're within the specified thresholds.
91
+
92
+ Parameters:
93
+ df (pd.DataFrame): Input DataFrame with Segment columns.
94
+ adjust_segments_config (list of tuples): List of tuples containing column name and threshold value.
95
+
96
+ Returns:
97
+ pd.DataFrame: The original DataFrame with merged segments.
98
+
99
+ Notes:
100
+ This function assumes that the Segment columns are named as Segment_<column_name>.
101
+ """
102
+ Data_columns_name_list = [name for name, _ in adjust_segments_config]
103
+ thresholds = np.array([thresh for _, thresh in adjust_segments_config])
104
+ ID_columns_name_list = ["Segment_" + name for name in Data_columns_name_list]
105
+
106
+ df = df.copy()
107
+ ID_arr = df[ID_columns_name_list].to_numpy(dtype=np.int64)
108
+ Data_arr = df[Data_columns_name_list].to_numpy(dtype=np.float64)
109
+
110
+ n_rows, n_cols = ID_arr.shape
111
+ new_ID_arr = ID_arr.copy()
112
+
113
+ for col_idx in range(n_cols):
114
+ ids = ID_arr[:, col_idx]
115
+ data = Data_arr[:, col_idx]
116
+ thresh = thresholds[col_idx]
117
+ new_ID_arr[:, col_idx] = _merge_segments_1d_optimized(ids, data, thresh)
118
+
119
+ for idx, col in enumerate(ID_columns_name_list):
120
+ df[col] = new_ID_arr[:, idx]
121
+
122
+ return df