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,419 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
import pandas as pd
|
|
3
|
+
from numba import njit
|
|
4
|
+
|
|
5
|
+
from pydpeet.process.sequence.utils.console_prints.log_time import log_time
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def _annotate_primitives(
|
|
9
|
+
df: pd.DataFrame,
|
|
10
|
+
data_columns: dict[str, str],
|
|
11
|
+
thresholds: dict[str, float],
|
|
12
|
+
show_runtime: bool = False,
|
|
13
|
+
) -> pd.DataFrame:
|
|
14
|
+
"""
|
|
15
|
+
Annotates the primitives in the dataframe with ID, variable, duration, length, min, max, avg, type, direction, slope.
|
|
16
|
+
|
|
17
|
+
Parameters:
|
|
18
|
+
df (pd.DataFrame):Dataframe containing the primitives
|
|
19
|
+
data_columns (dict[str, str]): Mapping of column names to their respective short names
|
|
20
|
+
thresholds (dict[str, float]) : Threshold values for each type of annotation
|
|
21
|
+
show_runtime (bool) : Whether to show the runtime of this function, by default False
|
|
22
|
+
|
|
23
|
+
Returns
|
|
24
|
+
-------
|
|
25
|
+
df (pd.DataFrame): Dataframe with added columns for annotated primitives
|
|
26
|
+
"""
|
|
27
|
+
df = _annotate_id(df, data_columns, show_runtime)
|
|
28
|
+
df = _annotate_variable(df, data_columns, show_runtime)
|
|
29
|
+
with log_time("adding duration, length, min, max, avg, type, direction, slope annotation", show_runtime):
|
|
30
|
+
df = _merged_annotations(df, data_columns, thresholds)
|
|
31
|
+
|
|
32
|
+
return df
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@njit(cache=True)
|
|
36
|
+
def _compute_segment_id(ID_arrays) -> np.array:
|
|
37
|
+
"""
|
|
38
|
+
Compute segment ID array using the ID arrays.
|
|
39
|
+
|
|
40
|
+
Parameters:
|
|
41
|
+
ID_arrays (numpy.ndarray): input arrays to compute segment ID
|
|
42
|
+
Example: ID_arrays = np.array([[-1, -1, 1], [-1, -1, 1], [3, -1, -1], [-1, 10, -1]])
|
|
43
|
+
|
|
44
|
+
Returns:
|
|
45
|
+
segment_id (numpy.ndarray): segment ID array with the same shape as the input array
|
|
46
|
+
Example: segment_id = np.array([1, 1, 2, 3])
|
|
47
|
+
|
|
48
|
+
Notes:
|
|
49
|
+
- The segment ID is computed by checking if each row differs from the previous row.
|
|
50
|
+
- If all columns in a row are -1, the segment ID for that row is -1.
|
|
51
|
+
- The segment ID is incremented for each row that differs from the previous row.
|
|
52
|
+
"""
|
|
53
|
+
n_rows, n_cols = ID_arrays.shape
|
|
54
|
+
segment_id = np.empty(n_rows, dtype=np.int64)
|
|
55
|
+
segment_id[:] = -1 # default -1
|
|
56
|
+
|
|
57
|
+
# Check mask_all_minus1 per row: all columns == -1
|
|
58
|
+
mask_all_minus1 = np.ones(n_rows, dtype=np.bool_)
|
|
59
|
+
for i in range(n_rows):
|
|
60
|
+
for j in range(n_cols):
|
|
61
|
+
if ID_arrays[i, j] != -1:
|
|
62
|
+
mask_all_minus1[i] = False
|
|
63
|
+
break
|
|
64
|
+
|
|
65
|
+
# Initialize segment counter and track previous row
|
|
66
|
+
segment_counter = 0
|
|
67
|
+
segment_id[0] = segment_counter if not mask_all_minus1[0] else -1
|
|
68
|
+
|
|
69
|
+
for i in range(1, n_rows):
|
|
70
|
+
if mask_all_minus1[i]:
|
|
71
|
+
segment_id[i] = -1
|
|
72
|
+
else:
|
|
73
|
+
# Check if row differs from previous row
|
|
74
|
+
changed = False
|
|
75
|
+
for j in range(n_cols):
|
|
76
|
+
if ID_arrays[i, j] != ID_arrays[i - 1, j]:
|
|
77
|
+
changed = True
|
|
78
|
+
break
|
|
79
|
+
if changed:
|
|
80
|
+
segment_counter += 1
|
|
81
|
+
segment_id[i] = segment_counter
|
|
82
|
+
|
|
83
|
+
return segment_id
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def _annotate_id(
|
|
87
|
+
df: pd.DataFrame,
|
|
88
|
+
data_columns: dict[str, str],
|
|
89
|
+
show_runtime: bool,
|
|
90
|
+
) -> pd.DataFrame:
|
|
91
|
+
"""
|
|
92
|
+
Adds a column 'ID' to the dataframe df based on the values of the columns specified in data_columns.
|
|
93
|
+
|
|
94
|
+
The 'ID' column is computed by checking if each row differs from the previous row.
|
|
95
|
+
If all columns in a row are -1, the 'ID' for that row is -1.
|
|
96
|
+
The 'ID' is incremented for each row that differs from the previous row.
|
|
97
|
+
|
|
98
|
+
Parameters:
|
|
99
|
+
df (pd.DataFrame): Input dataframe.
|
|
100
|
+
data_columns (dict[str, str]): Mapping of column names to their respective short names.
|
|
101
|
+
Example: data_columns = {"I": "Current", "P": "Power", "V": "Voltage"}
|
|
102
|
+
show_runtime (bool): Whether to show the runtime of this function, by default False.
|
|
103
|
+
|
|
104
|
+
Returns:
|
|
105
|
+
pd.DataFrame: The original dataframe with an added column named 'ID'.
|
|
106
|
+
"""
|
|
107
|
+
with log_time("adding ID annotation", show_runtime):
|
|
108
|
+
cols = [f"Segment_{v}" for v in data_columns.values()]
|
|
109
|
+
arr = df[cols].to_numpy()
|
|
110
|
+
segment_id = _compute_segment_id(arr)
|
|
111
|
+
df["ID"] = segment_id
|
|
112
|
+
|
|
113
|
+
return df
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def _annotate_variable(
|
|
117
|
+
df: pd.DataFrame,
|
|
118
|
+
data_columns: dict[str, str],
|
|
119
|
+
show_runtime: bool,
|
|
120
|
+
) -> pd.DataFrame:
|
|
121
|
+
"""
|
|
122
|
+
Adds a column 'Variable' to the dataframe df based on the values of the columns specified in data_columns.
|
|
123
|
+
|
|
124
|
+
Parameters:
|
|
125
|
+
df (pd.DataFrame): Input dataframe.
|
|
126
|
+
data_columns (dict[str, str]): Mapping of column names to their respective short names.
|
|
127
|
+
Example: data_columns = {"I": "Current", "P": "Power", "V": "Voltage"}
|
|
128
|
+
show_runtime (bool): Whether to show the runtime of this function, by default False.
|
|
129
|
+
|
|
130
|
+
Returns:
|
|
131
|
+
pd.DataFrame: The original dataframe with an added column named 'Variable'.
|
|
132
|
+
"""
|
|
133
|
+
with log_time("adding variable annotation", show_runtime):
|
|
134
|
+
cols = [f"Segment_{v}" for v in data_columns.values()]
|
|
135
|
+
short_names = list(data_columns.keys())
|
|
136
|
+
data = df[cols].to_numpy()
|
|
137
|
+
variable_array = np.full(len(df), None, dtype=object)
|
|
138
|
+
for i, name in enumerate(short_names):
|
|
139
|
+
mask = (data[:, i] != -1) & (variable_array == np.array(None))
|
|
140
|
+
variable_array[mask] = name
|
|
141
|
+
df["Variable"] = variable_array
|
|
142
|
+
|
|
143
|
+
return df
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
@njit(cache=True)
|
|
147
|
+
def _merged_annotations_njit(
|
|
148
|
+
group_of_row: np.array,
|
|
149
|
+
group_var_vals: np.array,
|
|
150
|
+
values_matrix: np.array,
|
|
151
|
+
testtime_s: np.array,
|
|
152
|
+
current_mean_per_group: np.array,
|
|
153
|
+
thresholds_array: np.array,
|
|
154
|
+
) -> tuple[np.array, np.array, np.array, np.array, np.array, np.array, np.array]:
|
|
155
|
+
"""
|
|
156
|
+
Compute the following annotations for each row in the input dataframe:
|
|
157
|
+
- min value
|
|
158
|
+
- max value
|
|
159
|
+
- average value
|
|
160
|
+
- slope
|
|
161
|
+
- length (time range)
|
|
162
|
+
- type (Rest, Constant, Ramp)
|
|
163
|
+
- direction (Neutral, Charge, Discharge, Up, Down)
|
|
164
|
+
|
|
165
|
+
Parameters:
|
|
166
|
+
group_of_row (numpy.array): An array of length n, where each element is the group ID of the corresponding row.
|
|
167
|
+
group_var_vals (numpy.array): An array of length G, where each element is the variable index of the corresponding group.
|
|
168
|
+
values_matrix (numpy.array): A matrix of shape (n, num_vars), where each row is a set of measurements for the corresponding row in the input dataframe.
|
|
169
|
+
testtime_s (numpy.array): An array of length n, where each element is the test time for the corresponding row.
|
|
170
|
+
current_mean_per_group (numpy.array): An array of length G, where each element is the current mean of the corresponding group.
|
|
171
|
+
thresholds_array (numpy.array): An array of length num_vars, where each element is the threshold value for the corresponding variable.
|
|
172
|
+
|
|
173
|
+
Returns:
|
|
174
|
+
min_vals (numpy.array): An array of length n, where each element is the minimum value of the corresponding row of the Variable.
|
|
175
|
+
max_vals (numpy.array): An array of length n, where each element is the maximum value of the corresponding row of the Variable.
|
|
176
|
+
avg_vals (numpy.array): An array of length n, where each element is the average value of the corresponding row of the Variable.
|
|
177
|
+
slope_vals (numpy.array): An array of length n, where each element is the slope of the corresponding row of the Variable.
|
|
178
|
+
length_vals (numpy.array): An array of length n, where each element is the length (time range) of the corresponding row of the Variable.
|
|
179
|
+
type_codes (numpy.array): An array of length n, where each element is the type code of the corresponding row of the Variable.
|
|
180
|
+
dir_codes (numpy.array): An array of length n, where each element is the direction code of the corresponding row of the Variable.
|
|
181
|
+
"""
|
|
182
|
+
n = group_of_row.shape[0]
|
|
183
|
+
G = group_var_vals.shape[0]
|
|
184
|
+
|
|
185
|
+
# Group accumulators (typed, fixed-size)
|
|
186
|
+
group_count = np.zeros(G, dtype=np.int64)
|
|
187
|
+
group_sum = np.zeros(G, dtype=np.float64)
|
|
188
|
+
group_min = np.full(G, np.nan, dtype=np.float64)
|
|
189
|
+
group_max = np.full(G, np.nan, dtype=np.float64)
|
|
190
|
+
first_val = np.full(G, np.nan, dtype=np.float64)
|
|
191
|
+
last_val = np.full(G, np.nan, dtype=np.float64)
|
|
192
|
+
init_flag = np.zeros(G, dtype=np.int8) # initialization flag
|
|
193
|
+
time_min = np.full(G, np.nan, dtype=np.float64)
|
|
194
|
+
time_max = np.full(G, np.nan, dtype=np.float64)
|
|
195
|
+
|
|
196
|
+
for i in range(n):
|
|
197
|
+
gid = group_of_row[i]
|
|
198
|
+
if gid == -1:
|
|
199
|
+
continue
|
|
200
|
+
var_idx = group_var_vals[gid]
|
|
201
|
+
val = values_matrix[i, var_idx]
|
|
202
|
+
t = testtime_s[i]
|
|
203
|
+
|
|
204
|
+
if init_flag[gid] == 0:
|
|
205
|
+
# initialize group (first encountered row for this group)
|
|
206
|
+
first_val[gid] = val
|
|
207
|
+
last_val[gid] = val
|
|
208
|
+
group_min[gid] = val
|
|
209
|
+
group_max[gid] = val
|
|
210
|
+
if not np.isnan(val):
|
|
211
|
+
group_sum[gid] = val
|
|
212
|
+
group_count[gid] = 1
|
|
213
|
+
else:
|
|
214
|
+
group_sum[gid] = 0.0
|
|
215
|
+
group_count[gid] = 0
|
|
216
|
+
time_min[gid] = t
|
|
217
|
+
time_max[gid] = t
|
|
218
|
+
init_flag[gid] = 1
|
|
219
|
+
else:
|
|
220
|
+
# update last (df order)
|
|
221
|
+
last_val[gid] = val
|
|
222
|
+
# update min/max/sum/count only for non-nan values
|
|
223
|
+
if not np.isnan(val):
|
|
224
|
+
if np.isnan(group_min[gid]) or val < group_min[gid]:
|
|
225
|
+
group_min[gid] = val
|
|
226
|
+
if np.isnan(group_max[gid]) or val > group_max[gid]:
|
|
227
|
+
group_max[gid] = val
|
|
228
|
+
group_sum[gid] += val
|
|
229
|
+
group_count[gid] += 1
|
|
230
|
+
# update time range
|
|
231
|
+
if t < time_min[gid]:
|
|
232
|
+
time_min[gid] = t
|
|
233
|
+
if t > time_max[gid]:
|
|
234
|
+
time_max[gid] = t
|
|
235
|
+
|
|
236
|
+
min_vals = np.full(n, np.nan, dtype=np.float64)
|
|
237
|
+
max_vals = np.full(n, np.nan, dtype=np.float64)
|
|
238
|
+
avg_vals = np.full(n, np.nan, dtype=np.float64)
|
|
239
|
+
slope_vals = np.full(n, np.nan, dtype=np.float64)
|
|
240
|
+
length_vals = np.full(n, np.nan, dtype=np.float64)
|
|
241
|
+
type_codes = np.full(n, -1, dtype=np.int64) # -1=None, 0=Rest,1=Constant,2=Ramp
|
|
242
|
+
dir_codes = np.full(n, -1, dtype=np.int64) # -1=None, 0=Neutral,1=Charge,2=Discharge,3=Up,4=Down
|
|
243
|
+
|
|
244
|
+
for i in range(n):
|
|
245
|
+
gid = group_of_row[i]
|
|
246
|
+
if gid == -1:
|
|
247
|
+
continue
|
|
248
|
+
|
|
249
|
+
vmin = group_min[gid]
|
|
250
|
+
vmax = group_max[gid]
|
|
251
|
+
cnt = group_count[gid]
|
|
252
|
+
s = group_sum[gid]
|
|
253
|
+
first = first_val[gid]
|
|
254
|
+
last = last_val[gid]
|
|
255
|
+
tmn = time_min[gid]
|
|
256
|
+
tmx = time_max[gid]
|
|
257
|
+
|
|
258
|
+
# avg
|
|
259
|
+
if cnt > 0:
|
|
260
|
+
avg = s / cnt
|
|
261
|
+
else:
|
|
262
|
+
avg = np.nan
|
|
263
|
+
|
|
264
|
+
# length (time range) using np.isnan checks
|
|
265
|
+
if np.isnan(tmn) or np.isnan(tmx):
|
|
266
|
+
length_val = np.nan
|
|
267
|
+
else:
|
|
268
|
+
length_val = tmx - tmn
|
|
269
|
+
|
|
270
|
+
# slope — denom = max(cnt - 1, 1)
|
|
271
|
+
denom = cnt - 1
|
|
272
|
+
if denom < 1:
|
|
273
|
+
denom = 1
|
|
274
|
+
slope_val = (last - first) / denom
|
|
275
|
+
|
|
276
|
+
# Type & Direction
|
|
277
|
+
thr = thresholds_array[group_var_vals[gid]]
|
|
278
|
+
curr_mean = current_mean_per_group[gid] if gid < current_mean_per_group.shape[0] else 0.0
|
|
279
|
+
|
|
280
|
+
if cnt == 0 or np.isnan(vmin) or np.isnan(vmax):
|
|
281
|
+
type_code = -1
|
|
282
|
+
dir_code = -1
|
|
283
|
+
else:
|
|
284
|
+
diff = vmax - vmin
|
|
285
|
+
if diff <= thr and abs(curr_mean) <= thr:
|
|
286
|
+
type_code = 0 # Rest
|
|
287
|
+
dir_code = 0 # Neutral
|
|
288
|
+
elif diff <= thr:
|
|
289
|
+
type_code = 1 # Constant
|
|
290
|
+
dir_code = 1 if curr_mean >= 0 else 2 # Charge / Discharge
|
|
291
|
+
else:
|
|
292
|
+
type_code = 2 # Ramp
|
|
293
|
+
dir_code = 3 if last > first else 4 # Up / Down
|
|
294
|
+
|
|
295
|
+
min_vals[i] = vmin
|
|
296
|
+
max_vals[i] = vmax
|
|
297
|
+
avg_vals[i] = avg
|
|
298
|
+
slope_vals[i] = slope_val
|
|
299
|
+
length_vals[i] = length_val
|
|
300
|
+
type_codes[i] = type_code
|
|
301
|
+
dir_codes[i] = dir_code
|
|
302
|
+
|
|
303
|
+
return min_vals, max_vals, avg_vals, slope_vals, length_vals, type_codes, dir_codes
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
def _merged_annotations(
|
|
307
|
+
df: pd.DataFrame,
|
|
308
|
+
data_columns: dict[str, str],
|
|
309
|
+
thresholds: dict[str, float],
|
|
310
|
+
) -> pd.DataFrame:
|
|
311
|
+
# map variable names to integer var indices
|
|
312
|
+
"""
|
|
313
|
+
Compute duration, length, min, max, avg, type, direction, slope for each group of (ID, Variable).
|
|
314
|
+
|
|
315
|
+
Parameters:
|
|
316
|
+
df (pd.DataFrame): DataFrame containing the primitives
|
|
317
|
+
data_columns (dict[str, str]): Mapping of column names to their respective short names
|
|
318
|
+
thresholds (dict[str, float]): Threshold values for each type of annotation
|
|
319
|
+
|
|
320
|
+
Returns:
|
|
321
|
+
pd.DataFrame: Dataframe with added columns for annotated primitives
|
|
322
|
+
"""
|
|
323
|
+
var_names = list(data_columns.keys())
|
|
324
|
+
num_vars = len(var_names)
|
|
325
|
+
var_map = {name: idx for idx, name in enumerate(var_names)}
|
|
326
|
+
|
|
327
|
+
n = len(df)
|
|
328
|
+
ids = df["ID"].to_numpy(np.int64)
|
|
329
|
+
|
|
330
|
+
# map variables column (strings) to integer indices (-1 if not in var_map)
|
|
331
|
+
# use pandas.map for speed & clarity
|
|
332
|
+
variables = df["Variable"].map(lambda x: var_map.get(x, -1)).to_numpy(np.int64)
|
|
333
|
+
|
|
334
|
+
# Build values_matrix (n x num_vars) where column j is the measurement for var j
|
|
335
|
+
values_matrix = np.full((n, num_vars), np.nan, dtype=np.float64)
|
|
336
|
+
for var_name, col_name in data_columns.items():
|
|
337
|
+
var_idx = var_map[var_name]
|
|
338
|
+
values_matrix[:, var_idx] = df[col_name].to_numpy(np.float64)
|
|
339
|
+
|
|
340
|
+
testtime_s = df["Test_Time[s]"].to_numpy(np.float64)
|
|
341
|
+
current_a = df["Current[A]"].to_numpy(np.float64)
|
|
342
|
+
|
|
343
|
+
# Compute duration per row: count of rows per ID (original annotate_duration_length)
|
|
344
|
+
duration = np.zeros(n, dtype=np.int64)
|
|
345
|
+
valid_mask = ids != -1
|
|
346
|
+
if np.any(valid_mask):
|
|
347
|
+
ids_valid = ids[valid_mask]
|
|
348
|
+
unique_ids, inv_ids = np.unique(ids_valid, return_inverse=True)
|
|
349
|
+
counts_per_id = np.bincount(inv_ids)
|
|
350
|
+
duration_valid = counts_per_id[inv_ids]
|
|
351
|
+
duration[valid_mask] = duration_valid
|
|
352
|
+
|
|
353
|
+
# current_mean per ID (global, used by Type/Direction logic)
|
|
354
|
+
sums_per_id = np.bincount(inv_ids, weights=current_a[valid_mask])
|
|
355
|
+
mean_current_per_id = sums_per_id / counts_per_id
|
|
356
|
+
else:
|
|
357
|
+
unique_ids = np.array([], dtype=np.int64)
|
|
358
|
+
mean_current_per_id = np.array([], dtype=np.float64)
|
|
359
|
+
|
|
360
|
+
# Build groups for (ID, Variable) only for valid rows
|
|
361
|
+
K = num_vars + 1
|
|
362
|
+
mask_valid_var_and_id = (ids != -1) & (variables != -1)
|
|
363
|
+
if np.any(mask_valid_var_and_id):
|
|
364
|
+
keys_valid = ids[mask_valid_var_and_id].astype(np.int64) * K + variables[mask_valid_var_and_id]
|
|
365
|
+
unique_keys, inv_keys = np.unique(keys_valid, return_inverse=True)
|
|
366
|
+
# group_id_vals and group_var_vals
|
|
367
|
+
group_id_vals = unique_keys // K
|
|
368
|
+
group_var_vals = unique_keys % K
|
|
369
|
+
|
|
370
|
+
# group_of_row: map each row to group index (or -1)
|
|
371
|
+
group_of_row = np.full(n, -1, dtype=np.int64)
|
|
372
|
+
group_of_row[mask_valid_var_and_id] = inv_keys
|
|
373
|
+
|
|
374
|
+
# map group_id_vals to positions in unique_ids (unique_ids sorted)
|
|
375
|
+
group_id_pos = np.searchsorted(unique_ids, group_id_vals)
|
|
376
|
+
current_mean_per_group = mean_current_per_id[group_id_pos]
|
|
377
|
+
else:
|
|
378
|
+
group_of_row = np.full(n, -1, dtype=np.int64)
|
|
379
|
+
group_var_vals = np.zeros(0, dtype=np.int64)
|
|
380
|
+
current_mean_per_group = np.zeros(0, dtype=np.float64)
|
|
381
|
+
|
|
382
|
+
# thresholds array by var index
|
|
383
|
+
thresholds_array = np.zeros(num_vars, dtype=np.float64)
|
|
384
|
+
for var_name, thr in thresholds.items():
|
|
385
|
+
if var_name in var_map:
|
|
386
|
+
thresholds_array[var_map[var_name]] = thr
|
|
387
|
+
|
|
388
|
+
# Call Numba worker
|
|
389
|
+
min_vals, max_vals, avg_vals, slope_vals, length_vals, type_codes, dir_codes = _merged_annotations_njit(
|
|
390
|
+
group_of_row,
|
|
391
|
+
group_var_vals,
|
|
392
|
+
values_matrix,
|
|
393
|
+
testtime_s,
|
|
394
|
+
current_mean_per_group,
|
|
395
|
+
thresholds_array,
|
|
396
|
+
)
|
|
397
|
+
|
|
398
|
+
# Map type & direction codes to strings (or None)
|
|
399
|
+
type_map = np.array(["Rest", "Constant", "Ramp"], dtype=object)
|
|
400
|
+
dir_map = np.array(["Neutral", "Charge", "Discharge", "Up", "Down"], dtype=object)
|
|
401
|
+
|
|
402
|
+
type_col = np.empty(n, dtype=object)
|
|
403
|
+
dir_col = np.empty(n, dtype=object)
|
|
404
|
+
for i in range(n):
|
|
405
|
+
tc = type_codes[i]
|
|
406
|
+
dc = dir_codes[i]
|
|
407
|
+
type_col[i] = None if tc == -1 else type_map[tc]
|
|
408
|
+
dir_col[i] = None if dc == -1 else dir_map[dc]
|
|
409
|
+
|
|
410
|
+
df["Duration"] = duration
|
|
411
|
+
df["Length"] = length_vals
|
|
412
|
+
df["Min"] = min_vals
|
|
413
|
+
df["Max"] = max_vals
|
|
414
|
+
df["Avg"] = avg_vals
|
|
415
|
+
df["Type"] = type_col
|
|
416
|
+
df["Direction"] = dir_col
|
|
417
|
+
df["Slope"] = slope_vals
|
|
418
|
+
|
|
419
|
+
return df
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
from pydpeet.process.sequence.configs import config
|
|
2
|
+
from pydpeet.process.sequence.utils.preprocessing.calculate_thresholds import calculate_minimum_definitive_differences
|
|
3
|
+
|
|
4
|
+
# using neware thresholds as default
|
|
5
|
+
THRESHOLD_DICT = config.NEWARE_CT_4008Q_5V12A_S1
|
|
6
|
+
MIN_DEFINITIVE_VOLTAGE_DIFFERENCE, MIN_DEFINITIVE_CURRENT_DIFFERENCE = calculate_minimum_definitive_differences(
|
|
7
|
+
*THRESHOLD_DICT
|
|
8
|
+
)
|
|
9
|
+
|
|
10
|
+
####### depending on the Noise needs to be adjusted even for measurements of the same device #######
|
|
11
|
+
SEGMENTS_TO_DETECT_CONFIG = [
|
|
12
|
+
# devide threshold by 2 because it's looking above and below the target line
|
|
13
|
+
("Voltage[V]", MIN_DEFINITIVE_VOLTAGE_DIFFERENCE / 2),
|
|
14
|
+
("Current[A]", MIN_DEFINITIVE_CURRENT_DIFFERENCE / 2),
|
|
15
|
+
("Power[W]", (MIN_DEFINITIVE_VOLTAGE_DIFFERENCE + MIN_DEFINITIVE_CURRENT_DIFFERENCE) / 2),
|
|
16
|
+
]
|
|
17
|
+
####### depending on the Noise needs to be adjusted even for measurements of the same device #######
|
|
18
|
+
# ORDER IS IMPORTANT!
|
|
19
|
+
ADJUST_SEGMENTS_CONFIG = [
|
|
20
|
+
("Voltage[V]", MIN_DEFINITIVE_VOLTAGE_DIFFERENCE),
|
|
21
|
+
("Current[A]", MIN_DEFINITIVE_CURRENT_DIFFERENCE),
|
|
22
|
+
("Power[W]", (MIN_DEFINITIVE_VOLTAGE_DIFFERENCE + MIN_DEFINITIVE_CURRENT_DIFFERENCE)),
|
|
23
|
+
]
|
|
24
|
+
|
|
25
|
+
THRESHOLD_CV_SEGMENTS_0A_END = MIN_DEFINITIVE_CURRENT_DIFFERENCE
|
|
26
|
+
|
|
27
|
+
####### HAS TO USE SAME KEY AS DATA_COLUMNS! only change the values of thresholds! ########
|
|
28
|
+
THRESHOLDS_PRIMITIVE_ANNOTATION = {
|
|
29
|
+
"V": MIN_DEFINITIVE_VOLTAGE_DIFFERENCE,
|
|
30
|
+
"I": MIN_DEFINITIVE_CURRENT_DIFFERENCE,
|
|
31
|
+
"P": MIN_DEFINITIVE_VOLTAGE_DIFFERENCE + MIN_DEFINITIVE_CURRENT_DIFFERENCE,
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
########################################################################################################################
|
|
35
|
+
SHOW_RUNTIME = True
|
|
36
|
+
THRESHOLD_CONSOLE_PRINTS_ZERO_LENGTH_CHECK = 2
|
|
37
|
+
THRESHOLD_CONSOLE_PRINTS_CV_CHECK = 2
|
|
38
|
+
THRESHOLD_CONSOLE_PRINTS_FINETUNING_WIDTH = 2
|
|
39
|
+
THRESHOLD_CONSOLE_PRINTS_POWER_ZERO_WATT_CHECK = 10
|
|
40
|
+
########################################################################################################################
|
|
41
|
+
# Visualize Data
|
|
42
|
+
VISUALIZE_PHASES_CONFIG = [
|
|
43
|
+
("V", "blue"),
|
|
44
|
+
("I", "red"),
|
|
45
|
+
("P", "green"),
|
|
46
|
+
]
|
|
47
|
+
LINE_VISUALIZATION_CONFIG = [
|
|
48
|
+
("Voltage[V]", "blue", (2.4, 4.3)),
|
|
49
|
+
("Current[A]", "red", (-10, 10)),
|
|
50
|
+
("Power[W]", "green", (-40, 20)),
|
|
51
|
+
]
|
|
52
|
+
START = 0
|
|
53
|
+
END = 1e100
|
|
54
|
+
USE_LINES_FOR_SEGMENTS = True
|
|
55
|
+
SHOW_COLUMN_NAMES = True
|
|
56
|
+
SHOW_TIME = True
|
|
57
|
+
SHOW_ID = True
|
|
58
|
+
SEGMENT_ALPHA = 0.3
|
|
59
|
+
WIDTH_HEIGHT_RATIO = [1, 0.3]
|
|
60
|
+
########################################################################################################################
|
|
61
|
+
# Generate Instructions
|
|
62
|
+
END_CONDITION_MAP_GENERATE_INSTRUCTIONS = {
|
|
63
|
+
"CC": "voltage",
|
|
64
|
+
"CV": "current",
|
|
65
|
+
"CP": "voltage",
|
|
66
|
+
"Pause": "time",
|
|
67
|
+
}
|
|
68
|
+
########################################################################################################################
|
|
69
|
+
SEQUENCES_CONFIG: dict[str, dict] = {
|
|
70
|
+
# Complex Sequences
|
|
71
|
+
# Loop rules: "loop": True, "exact_loops": 2, "min_loops": 2, "max_loops": 2, "minimum_IDs": 6
|
|
72
|
+
"Discharge_iOCV": {"loop": True, "minimum_IDs": 4, "sequence": ["CC_Discharge", "Pause"]},
|
|
73
|
+
"Charge_iOCV": {"loop": True, "min_loops": 2, "sequence": ["Pause", "CC_Charge"]},
|
|
74
|
+
"CCCV_Charge": {"loop": False, "sequence": ["CC_Charge", "CV_Charge"]},
|
|
75
|
+
}
|
|
76
|
+
SEGMENTS_CONFIG_SIMPLE: dict[str, dict] = {
|
|
77
|
+
# Primitive segments
|
|
78
|
+
# I, V, P,
|
|
79
|
+
# Charging, Discharging
|
|
80
|
+
"I": {
|
|
81
|
+
"rules": {
|
|
82
|
+
"variable": "I",
|
|
83
|
+
}
|
|
84
|
+
},
|
|
85
|
+
"V": {
|
|
86
|
+
"rules": {
|
|
87
|
+
"variable": "V",
|
|
88
|
+
}
|
|
89
|
+
},
|
|
90
|
+
"P": {
|
|
91
|
+
"rules": {
|
|
92
|
+
"variable": "P",
|
|
93
|
+
}
|
|
94
|
+
},
|
|
95
|
+
"Charging": {
|
|
96
|
+
"rules": {
|
|
97
|
+
"direction": "Charge",
|
|
98
|
+
}
|
|
99
|
+
},
|
|
100
|
+
"Discharging": {
|
|
101
|
+
"rules": {
|
|
102
|
+
"direction": "Discharge",
|
|
103
|
+
}
|
|
104
|
+
},
|
|
105
|
+
}
|
|
106
|
+
SEGMENTS_CONFIG_STANDARD: dict[str, dict] = {
|
|
107
|
+
# Primitive segments
|
|
108
|
+
# Pause
|
|
109
|
+
# CC_Charge, CV_Charge, CP_Charge
|
|
110
|
+
# CC_Discharge, CV_Discharge, CP_Discharge
|
|
111
|
+
# Ramp_Current_Charge, Ramp_Voltage_Charge, Ramp_Power_Charge
|
|
112
|
+
# Ramp_Current_Discharge, Ramp_Voltage_Discharge, Ramp_Power_Discharge
|
|
113
|
+
"Pause": {
|
|
114
|
+
"rules": {
|
|
115
|
+
"type": "Rest",
|
|
116
|
+
}
|
|
117
|
+
},
|
|
118
|
+
"CC_Charge": {
|
|
119
|
+
"rules": {
|
|
120
|
+
"variable": "I",
|
|
121
|
+
"type": "Constant",
|
|
122
|
+
"direction": "Charge",
|
|
123
|
+
}
|
|
124
|
+
},
|
|
125
|
+
"CV_Charge": {
|
|
126
|
+
"rules": {
|
|
127
|
+
"variable": "V",
|
|
128
|
+
"type": "Constant",
|
|
129
|
+
"direction": "Charge",
|
|
130
|
+
}
|
|
131
|
+
},
|
|
132
|
+
"CP_Charge": {
|
|
133
|
+
"rules": {
|
|
134
|
+
"variable": "P",
|
|
135
|
+
"type": "Constant",
|
|
136
|
+
"direction": "Charge",
|
|
137
|
+
}
|
|
138
|
+
},
|
|
139
|
+
"CC_Discharge": {
|
|
140
|
+
"rules": {
|
|
141
|
+
"variable": "I",
|
|
142
|
+
"type": "Constant",
|
|
143
|
+
"direction": "Discharge",
|
|
144
|
+
}
|
|
145
|
+
},
|
|
146
|
+
"CV_Discharge": {
|
|
147
|
+
"rules": {
|
|
148
|
+
"variable": "V",
|
|
149
|
+
"type": "Constant",
|
|
150
|
+
"direction": "Discharge",
|
|
151
|
+
}
|
|
152
|
+
},
|
|
153
|
+
"CP_Discharge": {
|
|
154
|
+
"rules": {
|
|
155
|
+
"variable": "P",
|
|
156
|
+
"type": "Constant",
|
|
157
|
+
"direction": "Discharge",
|
|
158
|
+
}
|
|
159
|
+
},
|
|
160
|
+
"CRamp_Charge": {
|
|
161
|
+
"rules": {
|
|
162
|
+
"variable": "I",
|
|
163
|
+
"type": "Ramp",
|
|
164
|
+
"direction": "Up",
|
|
165
|
+
}
|
|
166
|
+
},
|
|
167
|
+
"VRamp_Charge": {
|
|
168
|
+
"rules": {
|
|
169
|
+
"variable": "V",
|
|
170
|
+
"type": "Ramp",
|
|
171
|
+
"direction": "Up",
|
|
172
|
+
}
|
|
173
|
+
},
|
|
174
|
+
"PRamp_Charge": {
|
|
175
|
+
"rules": {
|
|
176
|
+
"variable": "P",
|
|
177
|
+
"type": "Ramp",
|
|
178
|
+
"direction": "Up",
|
|
179
|
+
}
|
|
180
|
+
},
|
|
181
|
+
"CRamp_Discharge": {
|
|
182
|
+
"rules": {
|
|
183
|
+
"variable": "I",
|
|
184
|
+
"type": "Ramp",
|
|
185
|
+
"direction": "Down",
|
|
186
|
+
}
|
|
187
|
+
},
|
|
188
|
+
"VRamp_Discharge": {
|
|
189
|
+
"rules": {
|
|
190
|
+
"variable": "V",
|
|
191
|
+
"type": "Ramp",
|
|
192
|
+
"direction": "Down",
|
|
193
|
+
}
|
|
194
|
+
},
|
|
195
|
+
"PRamp_Discharge": {
|
|
196
|
+
"rules": {
|
|
197
|
+
"variable": "P",
|
|
198
|
+
"type": "Ramp",
|
|
199
|
+
"direction": "Down",
|
|
200
|
+
}
|
|
201
|
+
},
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
SEGMENT_SEQUENCE_CONFIG = {
|
|
206
|
+
**SEQUENCES_CONFIG,
|
|
207
|
+
**SEGMENTS_CONFIG_STANDARD,
|
|
208
|
+
**SEGMENTS_CONFIG_SIMPLE,
|
|
209
|
+
}
|
|
210
|
+
########################################################################################################################
|
|
211
|
+
#### These shouldn't be changed when using ppb Dataframes ####
|
|
212
|
+
DATA_COLUMNS = {
|
|
213
|
+
"V": "Voltage[V]",
|
|
214
|
+
"I": "Current[A]",
|
|
215
|
+
"P": "Power[W]",
|
|
216
|
+
}
|
|
217
|
+
#### These shouldn't be changed when using ppb Dataframes ####
|
|
218
|
+
|
|
219
|
+
FALLBACK_CONFIG = {
|
|
220
|
+
"SEGMENTS_TO_DETECT_CONFIG": SEGMENTS_TO_DETECT_CONFIG,
|
|
221
|
+
"ADJUST_SEGMENTS_CONFIG": ADJUST_SEGMENTS_CONFIG,
|
|
222
|
+
"THRESHOLDS_PRIMITIVE_ANNOTATION": THRESHOLDS_PRIMITIVE_ANNOTATION,
|
|
223
|
+
"SHOW_RUNTIME": SHOW_RUNTIME,
|
|
224
|
+
"DATA_COLUMNS": DATA_COLUMNS,
|
|
225
|
+
"MIN_DEFINITIVE_CURRENT_DIFFERENCE": MIN_DEFINITIVE_CURRENT_DIFFERENCE,
|
|
226
|
+
"THRESHOLD_CONSOLE_PRINTS_ZERO_LENGTH_CHECK": THRESHOLD_CONSOLE_PRINTS_ZERO_LENGTH_CHECK,
|
|
227
|
+
"THRESHOLD_CONSOLE_PRINTS_CV_CHECK": THRESHOLD_CONSOLE_PRINTS_CV_CHECK,
|
|
228
|
+
"THRESHOLD_CONSOLE_PRINTS_FINETUNING_WIDTH": THRESHOLD_CONSOLE_PRINTS_FINETUNING_WIDTH,
|
|
229
|
+
"THRESHOLD_CONSOLE_PRINTS_POWER_ZERO_WATT_CHECK": THRESHOLD_CONSOLE_PRINTS_POWER_ZERO_WATT_CHECK,
|
|
230
|
+
"THRESHOLD_CV_SEGMENTS_0A_END": THRESHOLD_CV_SEGMENTS_0A_END,
|
|
231
|
+
}
|