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