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,226 @@
1
+ import logging
2
+
3
+ import numpy as np
4
+ import pandas as pd
5
+ from numba import njit
6
+
7
+ from pydpeet.process.sequence.utils.console_prints.log_time import log_time
8
+ from pydpeet.process.sequence.utils.postprocessing.df_primitives_correction import df_primitives_correction
9
+
10
+
11
+ @njit(cache=True)
12
+ def _find_zero_segments(
13
+ times: np.ndarray,
14
+ currents: np.ndarray,
15
+ tolerance: float,
16
+ ) -> list[tuple[np.ndarray, np.ndarray]]:
17
+ """
18
+ Find segments where the absolute value of the current is less than or equal to the given tolerance.
19
+
20
+ Parameters:
21
+ times (np.ndarray): Array of timestamps.
22
+ currents (np.ndarray): Array of current values.
23
+ tolerance (float): Maximum absolute value of the current. [-tol,tol] is accepted
24
+
25
+ Returns:
26
+ list: List of tuples of start and end times for each zero segment.
27
+ """
28
+ n = len(currents)
29
+ mask = (~np.isnan(currents)) & (np.abs(currents) <= tolerance)
30
+
31
+ zero_segments = []
32
+ in_seg = False
33
+ start_idx = 0
34
+
35
+ for i in range(n):
36
+ if mask[i]:
37
+ if not in_seg:
38
+ start_idx = i
39
+ in_seg = True
40
+ else:
41
+ if in_seg:
42
+ zero_segments.append((times[start_idx], times[i - 1]))
43
+ in_seg = False
44
+
45
+ if in_seg: # ended inside a segment
46
+ zero_segments.append((times[start_idx], times[n - 1]))
47
+
48
+ return zero_segments
49
+
50
+
51
+ @njit(cache=True)
52
+ def _get_id_tol_range(
53
+ times: np.ndarray,
54
+ currents: np.ndarray,
55
+ tolerance: float,
56
+ ) -> tuple[float, float]:
57
+ """
58
+ Return (tmin, tmax) where tmin and tmax are the minimum and maximum times, respectively,
59
+ where the absolute value of the current is less than or equal to the given tolerance.
60
+
61
+ Parameters:
62
+ times (np.ndarray): Array of timestamps.
63
+ currents (np.ndarray): Array of current values.
64
+ tolerance (float): Maximum absolute value of the current. [-tol,tol] is accepted
65
+
66
+ Returns:
67
+ tuple: (tmin, tmax)
68
+ """
69
+ n = len(currents)
70
+ found = False
71
+ tmin = 0.0
72
+ tmax = 0.0
73
+
74
+ for i in range(n):
75
+ if np.abs(currents[i]) <= tolerance and not np.isnan(currents[i]):
76
+ if not found:
77
+ tmin = times[i]
78
+ tmax = times[i]
79
+ found = True
80
+ else:
81
+ if times[i] < tmin:
82
+ tmin = times[i]
83
+ if times[i] > tmax:
84
+ tmax = times[i]
85
+
86
+ if not found:
87
+ return np.nan, np.nan
88
+
89
+ return tmin, tmax
90
+
91
+
92
+ @njit(cache=True)
93
+ def _compute_new_ids(ids) -> np.ndarray:
94
+ """
95
+ Compute a new array of IDs based on the input array to have contiguous IDs
96
+ after inserting the falsely suppressed CC segment back in.
97
+
98
+ Parameters:
99
+ ids (np.ndarray): Input array of IDs.
100
+
101
+ Returns:
102
+ np.ndarray: New array of IDs.
103
+ """
104
+ n = len(ids)
105
+ new_ids = np.ones(n, dtype=np.int64)
106
+ for i in range(1, n):
107
+ if ids[i] > ids[i - 1]:
108
+ new_ids[i] = new_ids[i - 1] + 1
109
+ elif ids[i] == -2 and ids[i - 1] != -2:
110
+ new_ids[i] = new_ids[i - 1] + 1
111
+ else:
112
+ new_ids[i] = new_ids[i - 1]
113
+ return new_ids
114
+
115
+
116
+ def _check_CV_0Aend_segments(
117
+ df_primitives: pd.DataFrame,
118
+ tolerance: float,
119
+ SHOW_RUNTIME: bool,
120
+ DATA_COLUMNS: dict[str, str],
121
+ THRESHOLDS_PRIMITIVE_ANNOTATION: dict[str, float],
122
+ supress_IO_warnings: bool,
123
+ THRESHOLD_CONSOLE_PRINTS_CV_CHECK: int,
124
+ THRESHOLD_CONSOLE_PRINTS_ZERO_LENGTH_CHECK: int,
125
+ ) -> pd.DataFrame:
126
+ """
127
+ Check CV Segments that end with 0A to identify and correct falsely suppressed CC segment at the end,
128
+ that happen due to the slow sloping of the CV.
129
+
130
+ Parameters:
131
+ df_primitives (pd.DataFrame): DataFrame containing the primitives
132
+ tolerance (float): maximum absolute value of current that is allowed in the CV segment
133
+ SHOW_RUNTIME (bool): whether to print runtime information
134
+ DATA_COLUMNS (dict): dictionary of column names to be kept in the DataFrame
135
+ Example: {"I": "Current", "P": "Power", "V": "Voltage"}
136
+ THRESHOLDS_PRIMITIVE_ANNOTATION (dict): dictionary of threshold values for primitive annotation
137
+ Example: {"I": 0.1, "P": 0.1, "V": 0.1}
138
+ supress_IO_warnings (bool): whether to suppress runtime warnings
139
+ THRESHOLD_CONSOLE_PRINTS_CV_CHECK (int): threshold for the number of corrected segments to print
140
+ THRESHOLD_CONSOLE_PRINTS_ZERO_LENGTH_CHECK (int): threshold for the number of segments to print
141
+
142
+ Returns:
143
+ pd.DataFrame: Dataframe with added columns for annotated primitives
144
+ """
145
+
146
+ with log_time("checking CV Segments that end with 0A", SHOW_RUNTIME=SHOW_RUNTIME):
147
+ # convert once to numpy
148
+ times = df_primitives["Test_Time[s]"].to_numpy(np.float64)
149
+ currents = df_primitives["Current[A]"].to_numpy(np.float64)
150
+
151
+ # 1) compute global zero-current segments
152
+ zero_segments = _find_zero_segments(times, currents, tolerance)
153
+
154
+ # 2) get last entry per ID, restricted to Variable=='V' and ~0 current
155
+ last_entries = df_primitives.groupby("ID", sort=False).tail(1)
156
+ mask_V = last_entries["Variable"].to_numpy() == "V"
157
+ mask_tol = np.abs(last_entries["Current[A]"].to_numpy(np.float64)) <= tolerance
158
+ candidate_ids = last_entries.loc[mask_V & mask_tol, "ID"].to_numpy()
159
+
160
+ expanded_ranges = {}
161
+
162
+ # 3) process each candidate ID with numba-accelerated range finder
163
+ for id_val in candidate_ids:
164
+ sub = df_primitives[df_primitives["ID"] == id_val]
165
+ sub_times = sub["Test_Time[s]"].to_numpy(np.float64)
166
+ sub_currents = sub["Current[A]"].to_numpy(np.float64)
167
+
168
+ seg_min, seg_max = _get_id_tol_range(sub_times, sub_currents, tolerance)
169
+ if np.isnan(seg_min):
170
+ continue
171
+
172
+ # expand by global zero_segments
173
+ for zero_segments_start, zero_segments_end in zero_segments:
174
+ if zero_segments_end >= seg_min and zero_segments_start <= seg_max: # overlap
175
+ seg_min = min(seg_min, zero_segments_start)
176
+ seg_max = max(seg_max, zero_segments_end)
177
+
178
+ expanded_ranges[id_val] = (float(seg_min), float(seg_max))
179
+
180
+ with log_time("correcting CV Segments that end with 0A", SHOW_RUNTIME=SHOW_RUNTIME):
181
+ correction_config = {
182
+ "replace_time_and_merge": {(start, end): "I" for _, (start, end) in expanded_ranges.items()},
183
+ }
184
+ starts = np.array([s for s, e in expanded_ranges.values()])
185
+ ends = np.array([e for s, e in expanded_ranges.values()])
186
+ times = df_primitives["Test_Time[s]"].to_numpy(np.float64)
187
+
188
+ # single mask: True if time is inside any (start, end)
189
+ mask = np.zeros(len(times), dtype=bool)
190
+ for s, e in zip(starts, ends, strict=False):
191
+ mask |= (times >= s) & (times <= e)
192
+
193
+ df_CV_0Aend_segments = df_primitives.loc[mask]
194
+
195
+ df_CV_0Aend_segments = df_primitives_correction(
196
+ df_primitives=df_CV_0Aend_segments,
197
+ correction_config=correction_config,
198
+ data_columns=DATA_COLUMNS,
199
+ thresholds=THRESHOLDS_PRIMITIVE_ANNOTATION,
200
+ reindex=False,
201
+ reannotate=False,
202
+ )
203
+
204
+ # Update df_primitives with df_CV_0Aend_segments
205
+ df_CV_0Aend_segments["ID"] = -2
206
+ cols_to_update = ["ID", "Variable", "Duration", "Length", "Min", "Max", "Avg", "Type", "Direction", "Slope"]
207
+ df_primitives.loc[df_CV_0Aend_segments.index, cols_to_update] = df_CV_0Aend_segments[cols_to_update]
208
+
209
+ # Extract as numpy arrays
210
+ ids = df_primitives["ID"].to_numpy(dtype=np.int64)
211
+
212
+ # Run with numba
213
+ df_primitives["ID"] = _compute_new_ids(ids)
214
+
215
+ if not supress_IO_warnings:
216
+ if expanded_ranges:
217
+ logging.warning("Suspicious Voltage Segments that end with Current[A] = 0.0 found.")
218
+ if THRESHOLD_CONSOLE_PRINTS_CV_CHECK:
219
+ expanded_ranges_items = list(expanded_ranges.items())
220
+ if len(expanded_ranges_items) > THRESHOLD_CONSOLE_PRINTS_ZERO_LENGTH_CHECK:
221
+ preview = expanded_ranges_items[:THRESHOLD_CONSOLE_PRINTS_ZERO_LENGTH_CHECK]
222
+ logging.warning(f"Turned into CC segments: {preview}, ...")
223
+ else:
224
+ logging.warning(f"Turned into CC segments: {expanded_ranges_items}")
225
+
226
+ return df_primitives
@@ -0,0 +1,85 @@
1
+ import logging
2
+
3
+ import numpy as np
4
+ import pandas as pd
5
+
6
+ from pydpeet.process.sequence.utils.console_prints.log_time import log_time
7
+ from pydpeet.process.sequence.utils.postprocessing.df_primitives_correction import df_primitives_correction
8
+
9
+
10
+ def _check_power_zero_watt_segments(
11
+ df_primitives: pd.DataFrame,
12
+ SHOW_RUNTIME: bool,
13
+ THRESHOLDS_PRIMITIVE_ANNOTATION: dict[str, float],
14
+ supress_IO_warnings: bool,
15
+ THRESHOLD_CONSOLE_PRINTS_POWER_ZERO_WATT_CHECK: int,
16
+ DATA_COLUMNS: dict[str, str],
17
+ ) -> pd.DataFrame:
18
+ """
19
+ Check all segments in the dataframe for power segments with 0W and correct them by replacing them with a current or voltage segment.
20
+
21
+ Parameters:
22
+ df_primitives (pd.DataFrame): DataFrame containing the primitives
23
+ SHOW_RUNTIME (bool): whether to print runtime information
24
+ THRESHOLDS_PRIMITIVE_ANNOTATION (dict): dictionary of threshold values for primitive annotation
25
+ Example: {"I": 0.1, "P": 0.1, "V": 0.1}
26
+ supress_IO_warnings (bool): whether to suppress runtime warnings
27
+ THRESHOLD_CONSOLE_PRINTS_POWER_ZERO_WATT_CHECK (int): threshold for the number of corrected segments to print
28
+ DATA_COLUMNS (dict): dictionary of column names to be kept in the DataFrame
29
+ Example: {"I": "Current[A]", "P": "Power[W]", "V": "Voltage[V]"}
30
+
31
+ Returns:
32
+ pd.DataFrame: Dataframe with added columns for annotated primitives
33
+ """
34
+ with log_time("checking Power segments with 0W", SHOW_RUNTIME=SHOW_RUNTIME):
35
+ tol_current = THRESHOLDS_PRIMITIVE_ANNOTATION["I"]
36
+ tol_voltage = THRESHOLDS_PRIMITIVE_ANNOTATION["V"]
37
+
38
+ mask_P_current_zero = (df_primitives["Variable"] == "P") & (np.abs(df_primitives["Current[A]"]) < tol_current)
39
+ mask_P_current_nonzero = (df_primitives["Variable"] == "P") & (
40
+ np.abs(df_primitives["Voltage[V]"]) < tol_voltage
41
+ )
42
+
43
+ zero_Watt_ids_current = df_primitives.loc[mask_P_current_zero, "ID"].unique().tolist()
44
+ zero_Watt_ids_voltage = df_primitives.loc[mask_P_current_nonzero, "ID"].unique().tolist()
45
+
46
+ if not supress_IO_warnings:
47
+ if zero_Watt_ids_current or zero_Watt_ids_voltage:
48
+ logging.warning("Power Segments with 0W found.")
49
+ if THRESHOLD_CONSOLE_PRINTS_POWER_ZERO_WATT_CHECK:
50
+ if zero_Watt_ids_current:
51
+ if len(zero_Watt_ids_current) > THRESHOLD_CONSOLE_PRINTS_POWER_ZERO_WATT_CHECK:
52
+ logging.warning(
53
+ f"Replaced with current segment: {zero_Watt_ids_current[:THRESHOLD_CONSOLE_PRINTS_POWER_ZERO_WATT_CHECK]}, ..."
54
+ )
55
+ else:
56
+ logging.warning(
57
+ f"Replaced with current segment: {zero_Watt_ids_current[:THRESHOLD_CONSOLE_PRINTS_POWER_ZERO_WATT_CHECK]}"
58
+ )
59
+ if zero_Watt_ids_voltage:
60
+ if len(zero_Watt_ids_voltage) > THRESHOLD_CONSOLE_PRINTS_POWER_ZERO_WATT_CHECK:
61
+ logging.warning(
62
+ f"Replaced with voltage segment: {zero_Watt_ids_voltage[:THRESHOLD_CONSOLE_PRINTS_POWER_ZERO_WATT_CHECK]}, ..."
63
+ )
64
+ else:
65
+ logging.warning(
66
+ f"Replaced with voltage segment: {zero_Watt_ids_voltage[:THRESHOLD_CONSOLE_PRINTS_POWER_ZERO_WATT_CHECK]}"
67
+ )
68
+
69
+ with log_time("correcting Power segments with 0W", SHOW_RUNTIME=SHOW_RUNTIME):
70
+ correction_config = {
71
+ "replace_ID": {
72
+ **{id: "I" for id in zero_Watt_ids_current},
73
+ **{id: "V" for id in zero_Watt_ids_voltage},
74
+ }
75
+ }
76
+ df_primitives = df_primitives_correction(
77
+ df_primitives=df_primitives,
78
+ correction_config=correction_config,
79
+ data_columns=DATA_COLUMNS,
80
+ thresholds=THRESHOLDS_PRIMITIVE_ANNOTATION,
81
+ reindex=False,
82
+ reannotate=False,
83
+ )
84
+
85
+ return df_primitives
@@ -0,0 +1,117 @@
1
+ import logging
2
+
3
+ import numpy as np
4
+ import pandas as pd
5
+
6
+ from pydpeet.process.sequence.utils.console_prints.log_time import log_time
7
+ from pydpeet.process.sequence.utils.postprocessing.df_primitives_correction import df_primitives_correction
8
+
9
+
10
+ def _check_zero_length_segments(
11
+ df_primitives: pd.DataFrame,
12
+ SHOW_RUNTIME: bool,
13
+ DATA_COLUMNS: dict[str, str],
14
+ THRESHOLDS_PRIMITIVE_ANNOTATION: dict[str, float],
15
+ supress_IO_warnings: bool,
16
+ THRESHOLD_CONSOLE_PRINTS_ZERO_LENGTH_CHECK: int,
17
+ ) -> pd.DataFrame:
18
+ """
19
+ Checks all segments in the dataframe for zero length segments and correct them by merging with the closest neighbor segment.
20
+
21
+ Parameters:
22
+ df_primitives (pd.DataFrame): DataFrame containing the primitives
23
+ SHOW_RUNTIME (bool): whether to print runtime information
24
+ DATA_COLUMNS (dict): dictionary of column names to be kept in the DataFrame
25
+ Example: {"I": "Current[A]", "P": "Power[W]", "V": "Voltage[V]"}
26
+ THRESHOLDS_PRIMITIVE_ANNOTATION (dict): dictionary of threshold values for primitive annotation
27
+ Example: {"I": 0.1, "P": 0.1, "V": 0.1}
28
+ supress_IO_warnings (bool): whether to suppress runtime warnings
29
+ THRESHOLD_CONSOLE_PRINTS_ZERO_LENGTH_CHECK (int): threshold for the number of corrected segments to print
30
+
31
+ Returns:
32
+ pd.DataFrame: Dataframe with added columns for annotated primitives
33
+ """
34
+ with log_time("checking zero length segments", SHOW_RUNTIME=SHOW_RUNTIME):
35
+ zero_length_ids = df_primitives[df_primitives["Length"] == 0]["ID"].unique().tolist()
36
+
37
+ zero_length_ids_left_merge = []
38
+ zero_length_ids_right_merge = []
39
+
40
+ for zero_id in zero_length_ids:
41
+ # Get the first row where this ID appears
42
+ zero_rows = df_primitives[df_primitives["ID"] == zero_id]
43
+ if zero_rows.empty:
44
+ continue
45
+
46
+ zero_idx = zero_rows.index[0]
47
+ try:
48
+ idx_pos = df_primitives.index.get_loc(zero_idx)
49
+ except KeyError:
50
+ continue
51
+
52
+ # Get the row's Voltage, Current, Power
53
+ zero_vals = df_primitives.iloc[idx_pos][["Voltage[V]", "Current[A]", "Power[W]"]].values
54
+
55
+ # Initialize distances
56
+ dist_left = np.inf
57
+ dist_right = np.inf
58
+
59
+ # Compare with left neighbor if available and different ID
60
+ if idx_pos > 0:
61
+ left_row = df_primitives.iloc[idx_pos - 1]
62
+ if left_row["ID"] != zero_id:
63
+ left_vals = left_row[["Voltage[V]", "Current[A]", "Power[W]"]].values
64
+ dist_left = np.linalg.norm(zero_vals - left_vals)
65
+
66
+ # Compare with right neighbor if available and different ID
67
+ if idx_pos < len(df_primitives) - 1:
68
+ right_row = df_primitives.iloc[idx_pos + 1]
69
+ if right_row["ID"] != zero_id:
70
+ right_vals = right_row[["Voltage[V]", "Current[A]", "Power[W]"]].values
71
+ dist_right = np.linalg.norm(zero_vals - right_vals)
72
+
73
+ # Pick closer neighbor
74
+ if dist_left < dist_right:
75
+ zero_length_ids_left_merge.append(zero_id)
76
+ else:
77
+ zero_length_ids_right_merge.append(zero_id)
78
+
79
+ with log_time("correcting zero length segments", SHOW_RUNTIME=SHOW_RUNTIME):
80
+ correction_config = {
81
+ "merge_left": [*zero_length_ids_left_merge],
82
+ "merge_right": [*zero_length_ids_right_merge],
83
+ }
84
+
85
+ df_primitives = df_primitives_correction(
86
+ df_primitives=df_primitives,
87
+ correction_config=correction_config,
88
+ data_columns=DATA_COLUMNS,
89
+ thresholds=THRESHOLDS_PRIMITIVE_ANNOTATION,
90
+ reindex=False,
91
+ reannotate=False,
92
+ )
93
+
94
+ if not supress_IO_warnings:
95
+ if zero_length_ids_left_merge or zero_length_ids_right_merge:
96
+ logging.warning("Segments with duration of 1 sample found.")
97
+ if THRESHOLD_CONSOLE_PRINTS_ZERO_LENGTH_CHECK:
98
+ if zero_length_ids_left_merge:
99
+ if len(zero_length_ids_left_merge) > THRESHOLD_CONSOLE_PRINTS_ZERO_LENGTH_CHECK:
100
+ logging.warning(
101
+ f"Merged with left neighbor: {zero_length_ids_left_merge[:THRESHOLD_CONSOLE_PRINTS_ZERO_LENGTH_CHECK]}, ..."
102
+ )
103
+ else:
104
+ logging.warning(
105
+ f"Merged with left neighbor: {zero_length_ids_left_merge[:THRESHOLD_CONSOLE_PRINTS_ZERO_LENGTH_CHECK]}"
106
+ )
107
+ if zero_length_ids_right_merge:
108
+ if len(zero_length_ids_right_merge) > THRESHOLD_CONSOLE_PRINTS_ZERO_LENGTH_CHECK:
109
+ logging.warning(
110
+ f"Merged with right neighbor: {zero_length_ids_right_merge[:THRESHOLD_CONSOLE_PRINTS_ZERO_LENGTH_CHECK]}, ..."
111
+ )
112
+ else:
113
+ logging.warning(
114
+ f"Merged with right neighbor: {zero_length_ids_right_merge[:THRESHOLD_CONSOLE_PRINTS_ZERO_LENGTH_CHECK]}"
115
+ )
116
+
117
+ return df_primitives
@@ -0,0 +1,97 @@
1
+ import numpy as np
2
+ import pandas as pd
3
+ from numba import njit
4
+
5
+
6
+ @njit(cache=True)
7
+ def _assign_segments_with_fit(
8
+ times: np.ndarray,
9
+ values: np.ndarray,
10
+ threshold: float,
11
+ ) -> np.ndarray:
12
+ """
13
+ Assign segment IDs to each point in a time series by incrementally fitting linear
14
+ segments using least squares. A new segment is started when either the current
15
+ point OR the starting point of the segment deviates from the fitted line beyond
16
+ the threshold.
17
+
18
+ Parameters:
19
+ times (np.ndarray): Array of time values in seconds.
20
+ values (np.ndarray): Array of corresponding signal values.
21
+ threshold (float): Maximum allowed deviation from the fitted line to continue the segment.
22
+
23
+ Returns:
24
+ np.ndarray: Array of segment IDs assigned to each point.
25
+ """
26
+ n = times.shape[0]
27
+ seg_ids = np.empty(n, dtype=np.int32)
28
+ seg_ids[0] = 0
29
+
30
+ start = 0
31
+ sum_x = times[0]
32
+ sum_y = values[0]
33
+ sum_xx = times[0] ** 2
34
+ sum_xy = times[0] * values[0]
35
+ count = 1
36
+
37
+ for i in range(1, n):
38
+ sum_x += times[i]
39
+ sum_y += values[i]
40
+ sum_xx += times[i] ** 2
41
+ sum_xy += times[i] * values[i]
42
+ count += 1
43
+
44
+ denom = count * sum_xx - sum_x**2
45
+ if denom == 0:
46
+ slope = 0.0
47
+ intercept = values[start]
48
+ else:
49
+ slope = (count * sum_xy - sum_x * sum_y) / denom
50
+ intercept = (sum_y - slope * sum_x) / count
51
+
52
+ # Check deviation for current point and first point in segment
53
+ expected_current = slope * times[i] + intercept
54
+ deviation_current = abs(values[i] - expected_current)
55
+
56
+ expected_start = slope * times[start] + intercept
57
+ deviation_start = abs(values[start] - expected_start)
58
+
59
+ if deviation_current <= threshold and deviation_start <= threshold:
60
+ seg_ids[i] = seg_ids[i - 1]
61
+ else:
62
+ start = i
63
+ seg_ids[i] = seg_ids[i - 1] + 1
64
+ sum_x = times[i]
65
+ sum_y = values[i]
66
+ sum_xx = times[i] ** 2
67
+ sum_xy = times[i] * values[i]
68
+ count = 1
69
+
70
+ return seg_ids
71
+
72
+
73
+ def _split_in_segments_using_incremental_linear_fit(
74
+ df: pd.DataFrame,
75
+ column_name: str,
76
+ threshold: float,
77
+ ) -> pd.DataFrame:
78
+ """
79
+ Detect near-linear segments in columns of a DataFrame using incremental least squares fitting.
80
+
81
+ Parameters:
82
+ df (pd.DataFrame): Input DataFrame with a datetime index and a numeric column.
83
+ column_name (str): Name of the column containing the signal values.
84
+ threshold (float): Maximum deviation allowed from the fitted line to consider a point as part of the current segment.
85
+
86
+ Returns:
87
+ pd.DataFrame: The original DataFrame with an added column 'Segment_<column_name>' indicating segment IDs.
88
+
89
+ Assumes:
90
+ Column "Test_Time[s]" exists and is in seconds and a float
91
+ """
92
+ times = df["Test_Time[s]"].to_numpy()
93
+ values = df[column_name].to_numpy()
94
+ seg_ids = _assign_segments_with_fit(times, values, threshold)
95
+ df[f"Segment_{column_name}"] = seg_ids
96
+
97
+ return df
@@ -0,0 +1,99 @@
1
+ import numpy as np
2
+ import pandas as pd
3
+ from numba import njit
4
+
5
+
6
+ @njit(cache=True)
7
+ def _compute_segment_lengths_numba(
8
+ segment_ids: np.ndarray,
9
+ times: np.ndarray,
10
+ max_id: int,
11
+ ) -> np.ndarray:
12
+ """
13
+ Compute the length (time) of each segment in the given DataFrame.
14
+
15
+ Parameters:
16
+ segment_ids (np.ndarray): Array of segment IDs.
17
+ times (np.ndarray): Array of corresponding times.
18
+ max_id (int): Maximum segment ID.
19
+
20
+ Returns:
21
+ np.ndarray: Array of segment durations.
22
+
23
+ """
24
+ first_time = np.full(max_id + 1, -1.0)
25
+ last_time = np.full(max_id + 1, -1.0)
26
+
27
+ n = len(segment_ids)
28
+ for i in range(n):
29
+ seg_id = segment_ids[i]
30
+ t = times[i]
31
+ if seg_id == -1:
32
+ continue
33
+ if first_time[seg_id] == -1.0:
34
+ first_time[seg_id] = t
35
+ last_time[seg_id] = t
36
+
37
+ durations = last_time - first_time
38
+
39
+ result = np.full(n, 0.0)
40
+ valid = segment_ids != -1
41
+ result[valid] = durations[segment_ids[valid]]
42
+
43
+ return result
44
+
45
+
46
+ def _add_segment_lengths(
47
+ df: pd.DataFrame,
48
+ column_name: str,
49
+ ) -> pd.DataFrame:
50
+ """
51
+ Compute the length (time) of each segment in the given DataFrame.
52
+
53
+ Parameters:
54
+ df (pd.DataFrame): Input DataFrame containing 'Test_Time[s]' and 'Segment_<column_name>' columns.
55
+ column_name (str): Name of the column containing the signal values.
56
+
57
+ Returns:
58
+ pd.DataFrame: Modified DataFrame with an added column 'Length_Segment_<column_name>' containing segment durations.
59
+ """
60
+ segment_name = f"Segment_{column_name}"
61
+ segment_ids = df[segment_name].to_numpy(dtype=np.int32)
62
+ times = df["Test_Time[s]"].to_numpy(dtype=np.float64)
63
+
64
+ max_id = segment_ids.max()
65
+ durations = _compute_segment_lengths_numba(segment_ids, times, max_id)
66
+
67
+ df[f"Length_{segment_name}"] = durations
68
+
69
+ return df
70
+
71
+
72
+ def _keep_max_segment_id(
73
+ df: pd.DataFrame,
74
+ keep_max_segment_id_config: list[tuple[int, str]],
75
+ ) -> pd.DataFrame:
76
+ """
77
+ For each row in the DataFrame, retain the segment ID in the Segment column that has the
78
+ maximum associated length. Set all other segment IDs to -1.
79
+
80
+ Parameters:
81
+ df (pd.DataFrame): The input DataFrame.
82
+ keep_max_segment_id_config (list of tuples): List of (length_column, segment_column) pairs.
83
+
84
+ Returns:
85
+ pd.DataFrame: Modified DataFrame with updated Segment columns.
86
+ """
87
+ # Extract the length columns into a NumPy array for fast comparison
88
+ length_data = np.stack([df[length_col].values for length_col, _ in keep_max_segment_id_config], axis=1)
89
+
90
+ # Determine the index of the max length column per row
91
+ max_indices = np.argmax(length_data, axis=1)
92
+
93
+ # Set all segment columns to -1 where not the max, retain value otherwise
94
+ for i, (_, segment_col) in enumerate(keep_max_segment_id_config):
95
+ segment_data = df[segment_col].values.copy()
96
+ segment_data[max_indices != i] = -1
97
+ df[segment_col] = segment_data
98
+
99
+ return df