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.
- pydpeet/__init__.py +56 -0
- pydpeet/citations/__init__.py +7 -0
- pydpeet/citations/citeme.py +397 -0
- pydpeet/citations/html_writer.py +137 -0
- pydpeet/dev_utils/__init__.py +7 -0
- pydpeet/dev_utils/generate_inits/__init__.py +7 -0
- pydpeet/dev_utils/generate_inits/generate_inits.py +327 -0
- pydpeet/io/__init__.py +7 -0
- pydpeet/io/configs/__init__.py +7 -0
- pydpeet/io/configs/config.py +227 -0
- pydpeet/io/configs/const.py +1 -0
- pydpeet/io/convert.py +594 -0
- pydpeet/io/device/__init__.py +7 -0
- pydpeet/io/device/arbin_4_23_PV090331/__init__.py +7 -0
- pydpeet/io/device/arbin_4_23_PV090331/formatter.py +22 -0
- pydpeet/io/device/arbin_4_23_PV090331/mapper.py +18 -0
- pydpeet/io/device/arbin_4_23_PV090331/reader.py +68 -0
- pydpeet/io/device/arbin_8_00_PV221201/__init__.py +7 -0
- pydpeet/io/device/arbin_8_00_PV221201/formatter.py +26 -0
- pydpeet/io/device/arbin_8_00_PV221201/mapper.py +18 -0
- pydpeet/io/device/arbin_8_00_PV221201/reader.py +66 -0
- pydpeet/io/device/basytec_6_3_1_0/__init__.py +7 -0
- pydpeet/io/device/basytec_6_3_1_0/formatter.py +18 -0
- pydpeet/io/device/basytec_6_3_1_0/mapper.py +18 -0
- pydpeet/io/device/basytec_6_3_1_0/reader.py +54 -0
- pydpeet/io/device/digatron_4_20_6_236/__init__.py +7 -0
- pydpeet/io/device/digatron_4_20_6_236/formatter.py +27 -0
- pydpeet/io/device/digatron_4_20_6_236/mapper.py +18 -0
- pydpeet/io/device/digatron_4_20_6_236/reader.py +45 -0
- pydpeet/io/device/digatron_eis_4_20_6_236/__init__.py +7 -0
- pydpeet/io/device/digatron_eis_4_20_6_236/formatter.py +39 -0
- pydpeet/io/device/digatron_eis_4_20_6_236/mapper.py +18 -0
- pydpeet/io/device/digatron_eis_4_20_6_236/reader.py +42 -0
- pydpeet/io/device/neware_8_0_0_516/__init__.py +7 -0
- pydpeet/io/device/neware_8_0_0_516/formatter.py +31 -0
- pydpeet/io/device/neware_8_0_0_516/mapper.py +18 -0
- pydpeet/io/device/neware_8_0_0_516/reader.py +397 -0
- pydpeet/io/device/parstat_2_63_3/__init__.py +7 -0
- pydpeet/io/device/parstat_2_63_3/formatter.py +34 -0
- pydpeet/io/device/parstat_2_63_3/mapper.py +18 -0
- pydpeet/io/device/parstat_2_63_3/reader.py +34 -0
- pydpeet/io/device/safion_1_9/__init__.py +7 -0
- pydpeet/io/device/safion_1_9/formatter.py +25 -0
- pydpeet/io/device/safion_1_9/mapper.py +18 -0
- pydpeet/io/device/safion_1_9/reader.py +89 -0
- pydpeet/io/device/zahner/__init__.py +7 -0
- pydpeet/io/device/zahner/formatter.py +73 -0
- pydpeet/io/device/zahner/mapper.py +34 -0
- pydpeet/io/device/zahner/reader.py +46 -0
- pydpeet/io/device/zahner_new/__init__.py +7 -0
- pydpeet/io/device/zahner_new/formatter.py +72 -0
- pydpeet/io/device/zahner_new/mapper.py +50 -0
- pydpeet/io/device/zahner_new/reader.py +46 -0
- pydpeet/io/map.py +69 -0
- pydpeet/io/read.py +50 -0
- pydpeet/io/utils/__init__.py +7 -0
- pydpeet/io/utils/ext_path.py +34 -0
- pydpeet/io/utils/formatter_utils.py +429 -0
- pydpeet/io/utils/load_custom_module.py +33 -0
- pydpeet/io/utils/timing.py +36 -0
- pydpeet/io/write.py +99 -0
- pydpeet/process/__init__.py +7 -0
- pydpeet/process/analyze/__init__.py +7 -0
- pydpeet/process/analyze/average.py +297 -0
- pydpeet/process/analyze/capacity.py +208 -0
- pydpeet/process/analyze/configs/__init__.py +7 -0
- pydpeet/process/analyze/configs/battery_config.py +38 -0
- pydpeet/process/analyze/configs/ocv_config.py +177 -0
- pydpeet/process/analyze/configs/step_analyzer_config.py +178 -0
- pydpeet/process/analyze/cycle.py +63 -0
- pydpeet/process/analyze/efficiency.py +182 -0
- pydpeet/process/analyze/energy.py +51 -0
- pydpeet/process/analyze/extract/__init__.py +7 -0
- pydpeet/process/analyze/extract/dva_ica.py +224 -0
- pydpeet/process/analyze/extract/ocv.py +196 -0
- pydpeet/process/analyze/power.py +32 -0
- pydpeet/process/analyze/resistance.py +87 -0
- pydpeet/process/analyze/soc.py +418 -0
- pydpeet/process/analyze/soh.py +72 -0
- pydpeet/process/analyze/utils.py +122 -0
- pydpeet/process/merge/__init__.py +7 -0
- pydpeet/process/merge/series.py +274 -0
- pydpeet/process/sequence/__init__.py +7 -0
- pydpeet/process/sequence/configs/__init__.py +7 -0
- pydpeet/process/sequence/configs/config.py +17 -0
- pydpeet/process/sequence/step_analyzer.py +440 -0
- pydpeet/process/sequence/utils/__init__.py +7 -0
- pydpeet/process/sequence/utils/annotate/__init__.py +7 -0
- pydpeet/process/sequence/utils/annotate/annotate_primitives.py +419 -0
- pydpeet/process/sequence/utils/configs/CONFIG_Fallback.py +231 -0
- pydpeet/process/sequence/utils/configs/CONFIG_preprocessing.py +243 -0
- pydpeet/process/sequence/utils/configs/__init__.py +7 -0
- pydpeet/process/sequence/utils/console_prints/__init__.py +7 -0
- pydpeet/process/sequence/utils/console_prints/log_time.py +30 -0
- pydpeet/process/sequence/utils/postprocessing/__init__.py +7 -0
- pydpeet/process/sequence/utils/postprocessing/df_primitives_correction.py +166 -0
- pydpeet/process/sequence/utils/postprocessing/filter_df.py +168 -0
- pydpeet/process/sequence/utils/postprocessing/generate_instructions.py +297 -0
- pydpeet/process/sequence/utils/preprocessing/__init__.py +7 -0
- pydpeet/process/sequence/utils/preprocessing/calculate_thresholds.py +27 -0
- pydpeet/process/sequence/utils/processing/__init__.py +7 -0
- pydpeet/process/sequence/utils/processing/analyze_segments.py +368 -0
- pydpeet/process/sequence/utils/processing/attempt_to_merge_neighboring_segments.py +122 -0
- pydpeet/process/sequence/utils/processing/check_CV_results.py +226 -0
- pydpeet/process/sequence/utils/processing/check_power_zero_watt_segments.py +85 -0
- pydpeet/process/sequence/utils/processing/check_zero_length.py +117 -0
- pydpeet/process/sequence/utils/processing/split_in_segments.py +97 -0
- pydpeet/process/sequence/utils/processing/supress_smaller_segments.py +99 -0
- pydpeet/process/sequence/utils/processing/widen_constant_segments.py +185 -0
- pydpeet/process/sequence/utils/visualize/__init__.py +7 -0
- pydpeet/process/sequence/utils/visualize/visualize_data.py +288 -0
- pydpeet/res/__init__.py +7 -0
- pydpeet/settings.py +0 -0
- pydpeet/utils/__init__.py +7 -0
- pydpeet/utils/logging_style.py +28 -0
- pydpeet/version.py +0 -0
- pydpeet-0.2.0.dist-info/METADATA +85 -0
- pydpeet-0.2.0.dist-info/RECORD +122 -0
- pydpeet-0.2.0.dist-info/WHEEL +5 -0
- pydpeet-0.2.0.dist-info/licenses/AUTHORS.md +0 -0
- pydpeet-0.2.0.dist-info/licenses/LICENCE.md +27 -0
- 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
|