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,243 @@
1
+ from pydpeet.process.sequence.configs import config
2
+ from pydpeet.process.sequence.utils.preprocessing.calculate_thresholds import calculate_minimum_definitive_differences
3
+
4
+ THRESHOLD_DICT_Custom = [ # ARBIN_OLD
5
+ 0.0005, # ACCURACY_VOLTAGE_SIGNAL
6
+ 0.01, # ACCURACY_CURRENT_SIGNAL
7
+ 0.0005, # ACCURACY_VOLTAGE_MEASUREMENT
8
+ 0.01, # ACCURACY_CURRENT_MEASUREMENT
9
+ 5, # FS_VOLTAGE
10
+ 3, # FS_CURRENT
11
+ ]
12
+ # use THRESHOLD_DICT = THRESHOLD_DICT_Custom if you don't want to use a predefined dictionary
13
+ THRESHOLD_DICT = config.NEWARE_CT_4008Q_5V12A_S1
14
+ MIN_DEFINITIVE_VOLTAGE_DIFFERENCE, MIN_DEFINITIVE_CURRENT_DIFFERENCE = calculate_minimum_definitive_differences(
15
+ *THRESHOLD_DICT
16
+ )
17
+
18
+ ####### depending on the Noise needs to be adjusted even for measurements of the same device #######
19
+ SEGMENTS_TO_DETECT_CONFIG = [
20
+ # devide threshold by 2 because it's looking above and below the target line
21
+ ("Voltage[V]", MIN_DEFINITIVE_VOLTAGE_DIFFERENCE / 2),
22
+ ("Current[A]", MIN_DEFINITIVE_CURRENT_DIFFERENCE / 2),
23
+ ("Power[W]", (MIN_DEFINITIVE_VOLTAGE_DIFFERENCE + MIN_DEFINITIVE_CURRENT_DIFFERENCE) / 2),
24
+ ]
25
+ ####### depending on the Noise needs to be adjusted even for measurements of the same device #######
26
+ # ORDER IS IMPORTANT!
27
+ ADJUST_SEGMENTS_CONFIG = [
28
+ ("Voltage[V]", MIN_DEFINITIVE_VOLTAGE_DIFFERENCE),
29
+ ("Current[A]", MIN_DEFINITIVE_CURRENT_DIFFERENCE),
30
+ ("Power[W]", (MIN_DEFINITIVE_VOLTAGE_DIFFERENCE + MIN_DEFINITIVE_CURRENT_DIFFERENCE)),
31
+ ]
32
+
33
+ THRESHOLD_CV_SEGMENTS_0A_END = MIN_DEFINITIVE_CURRENT_DIFFERENCE
34
+
35
+ ####### HAS TO USE SAME KEY AS DATA_COLUMNS! only change the values of thresholds! ########
36
+ THRESHOLDS_PRIMITIVE_ANNOTATION = {
37
+ "V": MIN_DEFINITIVE_VOLTAGE_DIFFERENCE,
38
+ "I": MIN_DEFINITIVE_CURRENT_DIFFERENCE,
39
+ "P": MIN_DEFINITIVE_VOLTAGE_DIFFERENCE + MIN_DEFINITIVE_CURRENT_DIFFERENCE,
40
+ }
41
+
42
+ ########################################################################################################################
43
+ SHOW_RUNTIME = True
44
+ SHOW_RUNTIME_ANNOTATION = True
45
+ SHOW_RUNTIME_ANALYZE = True
46
+ SHOW_RUNTIME_VISUALIZATION = True
47
+ THRESHOLD_CONSOLE_PRINTS_ZERO_LENGTH_CHECK = 2
48
+ THRESHOLD_CONSOLE_PRINTS_CV_CHECK = 2
49
+ THRESHOLD_CONSOLE_PRINTS_FINETUNING_WIDTH = 2
50
+ THRESHOLD_CONSOLE_PRINTS_POWER_ZERO_WATT_CHECK = 10
51
+ ########################################################################################################################
52
+ # Visualize Data
53
+ VISUALIZE_PHASES_CONFIG = [
54
+ ("V", "blue"),
55
+ ("I", "red"),
56
+ ("P", "green"),
57
+ ]
58
+ LINE_VISUALIZATION_CONFIG = [
59
+ ("Voltage[V]", "blue", (2.4, 4.3)),
60
+ ("Current[A]", "red", (-10, 10)),
61
+ ("Power[W]", "green", (-40, 20)),
62
+ ]
63
+ START = 0
64
+ END = 1e100
65
+ USE_LINES_FOR_SEGMENTS = True
66
+ SHOW_COLUMN_NAMES = True
67
+ SHOW_TIME = True
68
+ SHOW_ID = True
69
+ SEGMENT_ALPHA = 0.3
70
+ WIDTH_HEIGHT_RATIO = [1, 0.3]
71
+ ########################################################################################################################
72
+ # Generate Instructions
73
+ END_CONDITION_MAP_GENERATE_INSTRUCTIONS = {
74
+ "CC": "voltage",
75
+ "CV": "current",
76
+ "CP": "voltage",
77
+ "Pause": "time",
78
+ }
79
+ ########################################################################################################################
80
+ SEQUENCES_CONFIG: dict[str, dict] = {
81
+ # Complex Sequences
82
+ # Loop rules: "loop": True, "exact_loops": 2, "min_loops": 2, "max_loops": 2, "minimum_IDs": 6
83
+ "Discharge_iOCV": {"loop": True, "minimum_IDs": 4, "sequence": ["CC_Discharge", "Pause"]},
84
+ "Charge_iOCV": {"loop": True, "min_loops": 2, "sequence": ["Pause", "CC_Charge"]},
85
+ "CCCV_Charge": {"loop": False, "sequence": ["CC_Charge", "CV_Charge"]},
86
+ }
87
+ SEGMENTS_CONFIG_SIMPLE: dict[str, dict] = {
88
+ # Primitive segments
89
+ # I, V, P,
90
+ # Charging, Discharging
91
+ "I": {
92
+ "rules": {
93
+ "variable": "I",
94
+ }
95
+ },
96
+ "V": {
97
+ "rules": {
98
+ "variable": "V",
99
+ }
100
+ },
101
+ "P": {
102
+ "rules": {
103
+ "variable": "P",
104
+ }
105
+ },
106
+ "Charging": {
107
+ "rules": {
108
+ "direction": "Charge",
109
+ }
110
+ },
111
+ "Discharging": {
112
+ "rules": {
113
+ "direction": "Discharge",
114
+ }
115
+ },
116
+ }
117
+ SEGMENTS_CONFIG_STANDARD: dict[str, dict] = {
118
+ # Primitive segments
119
+ # Pause
120
+ # CC_Charge, CV_Charge, CP_Charge
121
+ # CC_Discharge, CV_Discharge, CP_Discharge
122
+ # Ramp_Current_Charge, Ramp_Voltage_Charge, Ramp_Power_Charge
123
+ # Ramp_Current_Discharge, Ramp_Voltage_Discharge, Ramp_Power_Discharge
124
+ "Pause": {
125
+ "rules": {
126
+ "type": "Rest",
127
+ }
128
+ },
129
+ "CC_Charge": {
130
+ "rules": {
131
+ "variable": "I",
132
+ "type": "Constant",
133
+ "direction": "Charge",
134
+ }
135
+ },
136
+ "CV_Charge": {
137
+ "rules": {
138
+ "variable": "V",
139
+ "type": "Constant",
140
+ "direction": "Charge",
141
+ }
142
+ },
143
+ "CP_Charge": {
144
+ "rules": {
145
+ "variable": "P",
146
+ "type": "Constant",
147
+ "direction": "Charge",
148
+ }
149
+ },
150
+ "CC_Discharge": {
151
+ "rules": {
152
+ "variable": "I",
153
+ "type": "Constant",
154
+ "direction": "Discharge",
155
+ }
156
+ },
157
+ "CV_Discharge": {
158
+ "rules": {
159
+ "variable": "V",
160
+ "type": "Constant",
161
+ "direction": "Discharge",
162
+ }
163
+ },
164
+ "CP_Discharge": {
165
+ "rules": {
166
+ "variable": "P",
167
+ "type": "Constant",
168
+ "direction": "Discharge",
169
+ }
170
+ },
171
+ "CRamp_Charge": {
172
+ "rules": {
173
+ "variable": "I",
174
+ "type": "Ramp",
175
+ "direction": "Up",
176
+ }
177
+ },
178
+ "VRamp_Charge": {
179
+ "rules": {
180
+ "variable": "V",
181
+ "type": "Ramp",
182
+ "direction": "Up",
183
+ }
184
+ },
185
+ "PRamp_Charge": {
186
+ "rules": {
187
+ "variable": "P",
188
+ "type": "Ramp",
189
+ "direction": "Up",
190
+ }
191
+ },
192
+ "CRamp_Discharge": {
193
+ "rules": {
194
+ "variable": "I",
195
+ "type": "Ramp",
196
+ "direction": "Down",
197
+ }
198
+ },
199
+ "VRamp_Discharge": {
200
+ "rules": {
201
+ "variable": "V",
202
+ "type": "Ramp",
203
+ "direction": "Down",
204
+ }
205
+ },
206
+ "PRamp_Discharge": {
207
+ "rules": {
208
+ "variable": "P",
209
+ "type": "Ramp",
210
+ "direction": "Down",
211
+ }
212
+ },
213
+ }
214
+
215
+
216
+ SEGMENT_SEQUENCE_CONFIG = {
217
+ **SEQUENCES_CONFIG,
218
+ **SEGMENTS_CONFIG_STANDARD,
219
+ **SEGMENTS_CONFIG_SIMPLE,
220
+ }
221
+ ########################################################################################################################
222
+ #### These shouldn't be changed when using ppb Dataframes ####
223
+ DATA_COLUMNS = {
224
+ "V": "Voltage[V]",
225
+ "I": "Current[A]",
226
+ "P": "Power[W]",
227
+ }
228
+ #### These shouldn't be changed when using ppb Dataframes ####
229
+
230
+ STEP_ANALYZER_PRIMITIVES_CONFIG_PRECOMPILE = {
231
+ "SEGMENTS_TO_DETECT_CONFIG": SEGMENTS_TO_DETECT_CONFIG,
232
+ "ADJUST_SEGMENTS_CONFIG": ADJUST_SEGMENTS_CONFIG,
233
+ "THRESHOLDS_PRIMITIVE_ANNOTATION": THRESHOLDS_PRIMITIVE_ANNOTATION,
234
+ "SHOW_RUNTIME": SHOW_RUNTIME,
235
+ "SHOW_RUNTIME_ANNOTATION": SHOW_RUNTIME_ANNOTATION,
236
+ "DATA_COLUMNS": DATA_COLUMNS,
237
+ "MIN_DEFINITIVE_CURRENT_DIFFERENCE": MIN_DEFINITIVE_CURRENT_DIFFERENCE,
238
+ "THRESHOLD_CONSOLE_PRINTS_ZERO_LENGTH_CHECK": THRESHOLD_CONSOLE_PRINTS_ZERO_LENGTH_CHECK,
239
+ "THRESHOLD_CONSOLE_PRINTS_CV_CHECK": THRESHOLD_CONSOLE_PRINTS_CV_CHECK,
240
+ "THRESHOLD_CONSOLE_PRINTS_FINETUNING_WIDTH": THRESHOLD_CONSOLE_PRINTS_FINETUNING_WIDTH,
241
+ "THRESHOLD_CONSOLE_PRINTS_POWER_ZERO_WATT_CHECK": THRESHOLD_CONSOLE_PRINTS_POWER_ZERO_WATT_CHECK,
242
+ "THRESHOLD_CV_SEGMENTS_0A_END": THRESHOLD_CV_SEGMENTS_0A_END,
243
+ }
@@ -0,0 +1,7 @@
1
+ """
2
+ Auto-generated __init__ file.
3
+ Created: 2026-03-06 15:11:49
4
+ """
5
+
6
+ # Restrictive package init: start with no public API
7
+ __all__ = []
@@ -0,0 +1,7 @@
1
+ """
2
+ Auto-generated __init__ file.
3
+ Created: 2026-03-06 15:11:49
4
+ """
5
+
6
+ # Restrictive package init: start with no public API
7
+ __all__ = []
@@ -0,0 +1,30 @@
1
+ import logging
2
+ from contextlib import contextmanager
3
+ from time import perf_counter
4
+
5
+
6
+ @contextmanager
7
+ def log_time(
8
+ description: str = "",
9
+ SHOW_RUNTIME: bool = True,
10
+ ):
11
+ """
12
+ Context manager to log the time taken by a block of code.
13
+
14
+ The log message will show the time taken in seconds with 4 decimal places,
15
+ and the description passed as argument.
16
+
17
+ Parameters:
18
+ description: str
19
+ The description of the block of code.
20
+ SHOW_RUNTIME: bool
21
+ Whether to print the time taken.
22
+
23
+ Returns:
24
+ None
25
+ """
26
+ start = perf_counter()
27
+ yield
28
+ end = perf_counter()
29
+ if SHOW_RUNTIME:
30
+ logging.info(f" {end - start:.4f}s {description}")
@@ -0,0 +1,7 @@
1
+ """
2
+ Auto-generated __init__ file.
3
+ Created: 2026-03-06 15:11:49
4
+ """
5
+
6
+ # Restrictive package init: start with no public API
7
+ __all__ = []
@@ -0,0 +1,166 @@
1
+ import pandas as pd
2
+
3
+ from pydpeet.process.sequence.utils.annotate.annotate_primitives import _merged_annotations
4
+
5
+
6
+ def df_primitives_correction(
7
+ df_primitives: pd.DataFrame,
8
+ correction_config: dict,
9
+ data_columns: dict[str, str],
10
+ thresholds: dict[str, float],
11
+ reindex: bool = True,
12
+ reannotate: bool = True,
13
+ ) -> pd.DataFrame:
14
+ """
15
+ Corrects the primitives in the dataframe based on the given configuration.
16
+
17
+ Parameters:
18
+ df_primitives (pd.DataFrame): Input dataframe containing the primitives.
19
+ correction_config (dict): Configuration for the correction, containing the following keys:
20
+ - replace_ID (dict): Mapping of old IDs to new IDs.
21
+ - replace_time (dict): Mapping of time ranges and the new labels for variable.
22
+ - replace_time_and_merge (dict): Mapping of time ranges and the new labels for variable + merges all into one segment.
23
+ - merge_left (list): List of IDs to merge with the previous segment.
24
+ - merge_right (list): List of IDs to merge with the next segment.
25
+ - merge_range (list): List of tuples of start and end IDs to merge. Keeping left Variable
26
+ Example: {'replace_time': {(0.0, 10.0): 'I', (10.0, 20.0): 'P'}, ...}
27
+ data_columns (dict): Mapping of column names to their respective short names.
28
+ Example: data_columns = {"I": "Current[A]", "P": "Power[W]", "V": "Voltage[V]"}
29
+ thresholds (dict): Threshold values for each type of annotation.
30
+ Example: {"V": 0.1, "I": 0.1, "P": 0.1}
31
+ reindex (bool): Whether to reindex the IDs to be consecutive, by default True.
32
+ reannotate (bool): Whether to reannotate the primitives after the correction, by default True.
33
+
34
+ Returns:
35
+ pd.DataFrame: The corrected dataframe with the added columns for annotated primitives.
36
+ """
37
+
38
+ df = df_primitives.copy()
39
+ max_id = df["ID"].max() + 1 # for new IDs
40
+
41
+ # --- REPLACE by ID ---
42
+ if "replace_ID" in correction_config:
43
+ for id_, new_label in correction_config["replace_ID"].items():
44
+ df.loc[df["ID"] == id_, "Variable"] = new_label
45
+
46
+ # --- REPLACE by TIME with splitting ---
47
+ if "replace_time" in correction_config:
48
+ for (start, end), new_label in correction_config["replace_time"].items():
49
+ overlapping_idx = df[(df["Test_Time[s]"] >= start) & (df["Test_Time[s]"] <= end)].index
50
+ if overlapping_idx.empty:
51
+ continue
52
+
53
+ overlapping_ids = df.loc[overlapping_idx, "ID"].unique()
54
+ for seg_id in overlapping_ids:
55
+ segment_rows = df[df["ID"] == seg_id]
56
+ seg_start_time = segment_rows["Test_Time[s]"].min()
57
+ seg_end_time = segment_rows["Test_Time[s]"].max()
58
+
59
+ if start <= seg_start_time and end >= seg_end_time:
60
+ mask = df["ID"] == seg_id
61
+ df.loc[mask, "Variable"] = new_label
62
+ else:
63
+ if seg_start_time < start:
64
+ mask_before = (df["ID"] == seg_id) & (df["Test_Time[s]"] < start)
65
+ df.loc[mask_before, "ID"] = max_id
66
+ max_id += 1
67
+
68
+ mask_overlap = (df["ID"] == seg_id) & (df["Test_Time[s]"] >= start) & (df["Test_Time[s]"] <= end)
69
+ df.loc[mask_overlap, "Variable"] = new_label
70
+ df.loc[mask_overlap, "ID"] = max_id
71
+ max_id += 1
72
+
73
+ if seg_end_time > end:
74
+ mask_after = (df["ID"] == seg_id) & (df["Test_Time[s]"] > end)
75
+ df.loc[mask_after, "ID"] = max_id
76
+ max_id += 1
77
+
78
+ # --- REPLACE by TIME and MERGE ---
79
+ if "replace_time_and_merge" in correction_config:
80
+ for (start, end), new_label in correction_config["replace_time_and_merge"].items():
81
+ overlapping_idx = df[(df["Test_Time[s]"] >= start) & (df["Test_Time[s]"] <= end)].index
82
+ if overlapping_idx.empty:
83
+ continue
84
+
85
+ overlapping_ids = df.loc[overlapping_idx, "ID"].unique()
86
+ new_id_for_merge = max_id
87
+ max_id += 1
88
+
89
+ for seg_id in overlapping_ids:
90
+ segment_rows = df[df["ID"] == seg_id]
91
+ seg_start_time = segment_rows["Test_Time[s]"].min()
92
+ seg_end_time = segment_rows["Test_Time[s]"].max()
93
+
94
+ if start <= seg_start_time and end >= seg_end_time:
95
+ mask = df["ID"] == seg_id
96
+ df.loc[mask, "Variable"] = new_label
97
+ df.loc[mask, "ID"] = new_id_for_merge
98
+ else:
99
+ if seg_start_time < start:
100
+ mask_before = (df["ID"] == seg_id) & (df["Test_Time[s]"] < start)
101
+ df.loc[mask_before, "ID"] = max_id
102
+ max_id += 1
103
+
104
+ mask_overlap = (df["ID"] == seg_id) & (df["Test_Time[s]"] >= start) & (df["Test_Time[s]"] <= end)
105
+ df.loc[mask_overlap, "Variable"] = new_label
106
+ df.loc[mask_overlap, "ID"] = new_id_for_merge
107
+
108
+ if seg_end_time > end:
109
+ mask_after = (df["ID"] == seg_id) & (df["Test_Time[s]"] > end)
110
+ df.loc[mask_after, "ID"] = max_id
111
+ max_id += 1
112
+
113
+ # --- MERGE LEFT ---
114
+ if "merge_left" in correction_config:
115
+ for id_ in correction_config["merge_left"]:
116
+ segment_rows = df[df["ID"] == id_]
117
+ if segment_rows.empty:
118
+ continue
119
+ first_pos = df.index.get_loc(segment_rows.index[0])
120
+ if first_pos == 0:
121
+ continue
122
+ left_id = df.iloc[first_pos - 1]["ID"]
123
+ left_variable = df.iloc[first_pos - 1]["Variable"]
124
+ df.loc[segment_rows.index, "ID"] = left_id
125
+ df.loc[segment_rows.index, "Variable"] = left_variable
126
+
127
+ # --- MERGE RIGHT ---
128
+ if "merge_right" in correction_config:
129
+ for id_ in correction_config["merge_right"]:
130
+ segment_rows = df[df["ID"] == id_]
131
+ if segment_rows.empty:
132
+ continue
133
+
134
+ first_pos = df.index.get_loc(segment_rows.index[0])
135
+ last_pos = df.index.get_loc(segment_rows.index[-1])
136
+
137
+ right_pos = last_pos + 1
138
+ if right_pos >= len(df):
139
+ continue
140
+
141
+ right_id = df.iloc[right_pos]["ID"]
142
+ right_variable = df.iloc[right_pos]["Variable"]
143
+ df.loc[segment_rows.index, "ID"] = right_id
144
+ df.loc[segment_rows.index, "Variable"] = right_variable
145
+
146
+ # --- MERGE RANGES ---
147
+ if "merge_range" in correction_config:
148
+ for start_id, end_id in correction_config["merge_range"]:
149
+ mask = (df["ID"] >= start_id) & (df["ID"] <= end_id)
150
+ if not mask.any():
151
+ continue
152
+ target_id = start_id
153
+ target_variable = df.loc[df["ID"] == target_id, "Variable"].iloc[0]
154
+ df.loc[mask, "ID"] = target_id
155
+ df.loc[mask, "Variable"] = target_variable
156
+
157
+ # --- REINDEX IDs to be consecutive ---
158
+ if reindex:
159
+ unique_ids = df["ID"].drop_duplicates().reset_index(drop=True)
160
+ id_mapping = {old_id: new_id for new_id, old_id in enumerate(unique_ids, start=1)}
161
+ df["ID"] = df["ID"].map(id_mapping)
162
+
163
+ if reannotate:
164
+ df = _merged_annotations(df, data_columns, thresholds)
165
+
166
+ return df
@@ -0,0 +1,168 @@
1
+ import logging
2
+ import operator
3
+ from functools import reduce
4
+
5
+ import pandas as pd
6
+ from pandas import DataFrame
7
+
8
+
9
+ # TODO: Docstring
10
+ def filter_df(
11
+ df_segments_and_sequences: pd.DataFrame,
12
+ df_primitives: pd.DataFrame,
13
+ rules: list[str],
14
+ standard_columns: list[str],
15
+ combine_op: str = "xor",
16
+ ) -> tuple[pd.DataFrame, pd.DataFrame]:
17
+ # Map string to actual function
18
+ comb_funcs = {"and": operator.and_, "or": operator.or_, "xor": operator.xor, "not": operator.not_}
19
+ if combine_op not in comb_funcs:
20
+ raise ValueError(f"combine_op must be one of {list(comb_funcs)}")
21
+
22
+ if not rules:
23
+ # No filtering, use all IDs
24
+ df_filtered_IDs = df_segments_and_sequences["ID"].values
25
+ else:
26
+ # build list of boolean Series for each rule
27
+ masks = []
28
+ for col in rules:
29
+ if col not in df_segments_and_sequences:
30
+ raise KeyError(f"column {col!r} not in DataFrame")
31
+ masks.append(operator.ne(df_segments_and_sequences[col], 0))
32
+
33
+ # combine masks with selected operator
34
+ combined_mask = reduce(comb_funcs[combine_op], masks)
35
+ df_filtered_IDs = df_segments_and_sequences[combined_mask]["ID"].values
36
+
37
+ # Create standard and non-standard dataframes
38
+ df_standard = df_primitives[standard_columns + ["ID", "Power[W]"]]
39
+ df_non_standard = df_primitives[
40
+ [col for col in df_primitives.columns if col not in standard_columns + ["Power[W]"]]
41
+ ]
42
+
43
+ # Nullify non-standard rows not in filtered IDs
44
+ mask = ~df_non_standard["ID"].isin(df_filtered_IDs)
45
+ cols_to_null = [col for col in df_non_standard.columns if col != "ID"]
46
+ df_non_standard.loc[mask, cols_to_null] = None
47
+ df_non_standard.loc[mask, "Variable"] = "Filtered"
48
+
49
+ # Combine standard and non-standard
50
+ df_filtered = pd.concat([df_standard, df_non_standard.drop(columns="ID")], axis=1)
51
+
52
+ return df_filtered, df_filtered_IDs
53
+
54
+
55
+ # TODO: Docstring
56
+ def return_or_print_blocks(
57
+ df_filtered: pd.DataFrame,
58
+ filtered_IDs: pd.Series | list | set | tuple,
59
+ print_blocks: bool = True,
60
+ ) -> None | list[dict]:
61
+ import numpy as np
62
+
63
+ # Ensure IDs are in a fast lookup structure
64
+ filtered_IDs_set = set(filtered_IDs)
65
+
66
+ # Convert necessary columns to arrays
67
+ ids = df_filtered["ID"].values
68
+ testtimes = df_filtered["Test_Time[s]"].values
69
+ is_filtered = df_filtered["Variable"].values == "Filtered"
70
+
71
+ # Boolean mask for unfiltered rows
72
+ unfiltered_mask = np.isin(ids, list(filtered_IDs_set)) & ~is_filtered
73
+
74
+ # Find block boundaries (diff between 0/1 in mask)
75
+ block_edges = np.diff(unfiltered_mask.astype(int))
76
+ starts = np.where(block_edges == 1)[0] + 1
77
+ ends = np.where(block_edges == -1)[0]
78
+
79
+ # Edge case: starts/ends at beginning or end
80
+ if unfiltered_mask[0]:
81
+ starts = np.insert(starts, 0, 0)
82
+ if unfiltered_mask[-1]:
83
+ ends = np.append(ends, len(unfiltered_mask) - 1)
84
+
85
+ blocks = []
86
+ for i, (start_idx, end_idx) in enumerate(zip(starts, ends, strict=False), start=-1):
87
+ start_id = ids[start_idx]
88
+ end_id = ids[end_idx]
89
+ start_time = testtimes[start_idx]
90
+ end_time = testtimes[end_idx]
91
+ if print_blocks:
92
+ logging.info("-" * 40)
93
+ logging.info(f"Block {i + 1}:")
94
+ logging.info(f" Start ID: {start_id}, Test_Time[s]: {start_time}")
95
+ logging.info(f" End ID: {end_id}, Test_Time[s]: {end_time}")
96
+ logging.info("-" * 40)
97
+
98
+ blocks.append(
99
+ {
100
+ "start_id": start_id,
101
+ "end_id": end_id,
102
+ "start_time": start_time,
103
+ "end_time": end_time,
104
+ }
105
+ )
106
+
107
+ return blocks
108
+
109
+
110
+ def split_df_by_blocks(
111
+ df_filtered: pd.DataFrame,
112
+ blocks: list[dict],
113
+ ) -> list[pd.DataFrame]:
114
+ """
115
+ Split df_filtered into multiple DataFrames per block.
116
+ Each block includes all rows from start_id to end_id (inclusive), even if IDs repeat.
117
+ """
118
+ dfs_per_block = []
119
+ ids = df_filtered["ID"].values
120
+
121
+ for block in blocks:
122
+ start_id = block["start_id"]
123
+ end_id = block["end_id"]
124
+
125
+ # Find ALL positions of start_id and end_id
126
+ start_positions = (ids == start_id).nonzero()[0]
127
+ end_positions = (ids == end_id).nonzero()[0]
128
+
129
+ if len(start_positions) == 0 or len(end_positions) == 0:
130
+ continue # skip if not found
131
+
132
+ start_idx = start_positions[0]
133
+ end_idx = end_positions[-1]
134
+
135
+ df_block = df_filtered.iloc[start_idx : end_idx + 1].copy()
136
+ dfs_per_block.append(df_block)
137
+
138
+ return dfs_per_block
139
+
140
+
141
+ def filter_and_split_df_by_blocks(
142
+ df_segments_and_sequences: pd.DataFrame,
143
+ df_primitives: pd.DataFrame,
144
+ rules: list[str],
145
+ combine_op: str = "or",
146
+ print_blocks: bool = False,
147
+ also_return_filtered_df: bool = True,
148
+ ) -> tuple[list[DataFrame], DataFrame] | list[DataFrame]:
149
+ standard_columns = ["Test_Time[s]", "Voltage[V]", "Current[A]", "Power[W]"]
150
+ logging.warning("Using default standard columns:")
151
+ logging.warning(standard_columns)
152
+
153
+ df_filtered, df_filtered_IDs = filter_df(
154
+ df_segments_and_sequences=df_segments_and_sequences,
155
+ df_primitives=df_primitives,
156
+ rules=rules,
157
+ combine_op=combine_op,
158
+ standard_columns=standard_columns,
159
+ )
160
+
161
+ blocks = return_or_print_blocks(df_filtered=df_filtered, filtered_IDs=df_filtered_IDs, print_blocks=print_blocks)
162
+
163
+ dfs_per_block = split_df_by_blocks(df_filtered=df_filtered, blocks=blocks)
164
+
165
+ if also_return_filtered_df:
166
+ return dfs_per_block, df_filtered
167
+
168
+ return dfs_per_block