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,185 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
|
|
3
|
+
import numpy as np
|
|
4
|
+
import pandas as pd
|
|
5
|
+
from numba import njit
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
# TODO: Typing correct?
|
|
9
|
+
@njit(cache=True)
|
|
10
|
+
def _widen_segments_numba(
|
|
11
|
+
Data_arr: np.ndarray,
|
|
12
|
+
ID_arr: np.ndarray,
|
|
13
|
+
thresholds: np.array,
|
|
14
|
+
) -> np.array:
|
|
15
|
+
"""
|
|
16
|
+
Widen constant segments in a DataFrame by identifying contiguous segments in each column,
|
|
17
|
+
and then widening each segment by extending it to the left and right until the
|
|
18
|
+
average of the segment exceeds the threshold.
|
|
19
|
+
|
|
20
|
+
Parameters:
|
|
21
|
+
Data_arr (numpy array): Input DataFrame with Segment columns.
|
|
22
|
+
ID_arr (numpy array): IDs of the segments in the DataFrame.
|
|
23
|
+
thresholds (numpy array): Threshold values per column.
|
|
24
|
+
|
|
25
|
+
Returns:
|
|
26
|
+
numpy array: The original DataFrame with widened segments.
|
|
27
|
+
"""
|
|
28
|
+
n_rows, n_cols = Data_arr.shape
|
|
29
|
+
new_ID_arr = ID_arr.copy()
|
|
30
|
+
|
|
31
|
+
for col_idx in range(n_cols):
|
|
32
|
+
thresh = thresholds[col_idx]
|
|
33
|
+
col_data = Data_arr[:, col_idx]
|
|
34
|
+
col_ids = new_ID_arr[:, col_idx]
|
|
35
|
+
|
|
36
|
+
i = 0
|
|
37
|
+
while i < n_rows:
|
|
38
|
+
col_id = col_ids[i]
|
|
39
|
+
if col_id == -1:
|
|
40
|
+
i += 1
|
|
41
|
+
continue
|
|
42
|
+
|
|
43
|
+
# Identify start and end of contiguous segment
|
|
44
|
+
start = i
|
|
45
|
+
while i + 1 < n_rows and col_ids[i + 1] == col_id:
|
|
46
|
+
i += 1
|
|
47
|
+
end = i
|
|
48
|
+
|
|
49
|
+
# Compute mean and check validity in one pass
|
|
50
|
+
seg_sum = 0.0
|
|
51
|
+
segment_length = end - start + 1
|
|
52
|
+
is_valid = True
|
|
53
|
+
for j in range(start, end + 1):
|
|
54
|
+
seg_sum += col_data[j]
|
|
55
|
+
avg = seg_sum / segment_length
|
|
56
|
+
|
|
57
|
+
for j in range(start, end + 1):
|
|
58
|
+
if abs(col_data[j] - avg) > thresh:
|
|
59
|
+
is_valid = False
|
|
60
|
+
break
|
|
61
|
+
if not is_valid:
|
|
62
|
+
i += 1
|
|
63
|
+
continue
|
|
64
|
+
|
|
65
|
+
# Extend to the right
|
|
66
|
+
extend_r = end
|
|
67
|
+
j = end + 1
|
|
68
|
+
while j < n_rows:
|
|
69
|
+
v = col_data[j]
|
|
70
|
+
cid_j = new_ID_arr[j, col_idx]
|
|
71
|
+
if abs(v - avg) > thresh or (cid_j != -1 and cid_j != col_id):
|
|
72
|
+
break
|
|
73
|
+
extend_r = j
|
|
74
|
+
j += 1
|
|
75
|
+
|
|
76
|
+
# Extend to the left
|
|
77
|
+
extend_l = start
|
|
78
|
+
j = start - 1
|
|
79
|
+
while j >= 0:
|
|
80
|
+
v = col_data[j]
|
|
81
|
+
cid_j = new_ID_arr[j, col_idx]
|
|
82
|
+
if abs(v - avg) > thresh or (cid_j != -1 and cid_j != col_id):
|
|
83
|
+
break
|
|
84
|
+
extend_l = j
|
|
85
|
+
j -= 1
|
|
86
|
+
|
|
87
|
+
# Assign widened ID to extended ranges
|
|
88
|
+
for j in range(extend_l, start):
|
|
89
|
+
new_ID_arr[j, col_idx] = col_id
|
|
90
|
+
for j in range(end + 1, extend_r + 1):
|
|
91
|
+
new_ID_arr[j, col_idx] = col_id
|
|
92
|
+
|
|
93
|
+
# Invalidate other columns only once for the widened areas
|
|
94
|
+
if extend_l < start or extend_r > end:
|
|
95
|
+
for other_col_idx in range(n_cols):
|
|
96
|
+
if other_col_idx == col_idx:
|
|
97
|
+
continue
|
|
98
|
+
for j in range(extend_l, start):
|
|
99
|
+
new_ID_arr[j, other_col_idx] = -1
|
|
100
|
+
for j in range(end + 1, extend_r + 1):
|
|
101
|
+
new_ID_arr[j, other_col_idx] = -1
|
|
102
|
+
|
|
103
|
+
i += 1
|
|
104
|
+
|
|
105
|
+
return new_ID_arr
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def _widen_constant_segments(
|
|
109
|
+
df: pd.DataFrame,
|
|
110
|
+
adjust_segments_config: list[tuple[str, float]],
|
|
111
|
+
Threshold_segments_to_print: int,
|
|
112
|
+
supress_IO_warnings: bool,
|
|
113
|
+
) -> pd.DataFrame:
|
|
114
|
+
"""
|
|
115
|
+
Widen constant segments in a DataFrame by identifying contiguous segments in each column,
|
|
116
|
+
and then widening each segment by extending it to the left and right until the
|
|
117
|
+
average of the segment exceeds the threshold.
|
|
118
|
+
|
|
119
|
+
Parameters:
|
|
120
|
+
df (pd.DataFrame): Input DataFrame with Segment columns.
|
|
121
|
+
adjust_segments_config (list of tuples): List of tuples containing column name and threshold value.
|
|
122
|
+
Example: [("Voltage[V]", 0.1), ("Current[A]", 0.1), ("Power[W]", 0.1)]
|
|
123
|
+
Threshold_segments_to_print (int): The maximum number of removed segments to print in the warning message.
|
|
124
|
+
supress_IO_warnings (bool): Whether to suppress warning messages.
|
|
125
|
+
|
|
126
|
+
Returns:
|
|
127
|
+
pd.DataFrame: The original DataFrame with widened segments.
|
|
128
|
+
"""
|
|
129
|
+
Data_columns_name_list = [name for name, _ in adjust_segments_config]
|
|
130
|
+
thresholds = np.array([thresh for _, thresh in adjust_segments_config], dtype=np.float64)
|
|
131
|
+
ID_columns_name_list = ["Segment_" + name for name, _ in adjust_segments_config]
|
|
132
|
+
|
|
133
|
+
Data_arr = df[Data_columns_name_list].to_numpy(dtype=np.float64)
|
|
134
|
+
ID_arr = df[ID_columns_name_list].to_numpy(dtype=np.int32)
|
|
135
|
+
|
|
136
|
+
n_rows = ID_arr.shape[0]
|
|
137
|
+
|
|
138
|
+
# Store start and end indices of original segments per column
|
|
139
|
+
original_segments = {col: [] for col in ID_columns_name_list}
|
|
140
|
+
for i, col in enumerate(ID_columns_name_list):
|
|
141
|
+
ids = ID_arr[:, i]
|
|
142
|
+
current_id = ids[0]
|
|
143
|
+
start_idx = 0 if current_id != -1 else None
|
|
144
|
+
for row in range(1, n_rows):
|
|
145
|
+
if ids[row] != current_id:
|
|
146
|
+
if current_id != -1:
|
|
147
|
+
original_segments[col].append((start_idx, row - 1))
|
|
148
|
+
current_id = ids[row]
|
|
149
|
+
start_idx = row if current_id != -1 else None
|
|
150
|
+
if current_id != -1:
|
|
151
|
+
original_segments[col].append((start_idx, n_rows - 1))
|
|
152
|
+
|
|
153
|
+
# Run widening
|
|
154
|
+
new_ID_arr = _widen_segments_numba(Data_arr, ID_arr, thresholds)
|
|
155
|
+
|
|
156
|
+
# Check removed segments and print one-line warning
|
|
157
|
+
for i, col in enumerate(ID_columns_name_list):
|
|
158
|
+
updated_ids = new_ID_arr[:, i]
|
|
159
|
+
removed_ranges = [
|
|
160
|
+
f"{start}:{end}" for start, end in original_segments[col] if np.all(updated_ids[start : end + 1] == -1)
|
|
161
|
+
]
|
|
162
|
+
if removed_ranges:
|
|
163
|
+
if not supress_IO_warnings:
|
|
164
|
+
if Threshold_segments_to_print:
|
|
165
|
+
if Threshold_segments_to_print < len(removed_ranges):
|
|
166
|
+
logging.warning(
|
|
167
|
+
f"Removed segments during finetuning of the width in '{col}': "
|
|
168
|
+
+ ", ".join(removed_ranges[:Threshold_segments_to_print])
|
|
169
|
+
+ " ..."
|
|
170
|
+
)
|
|
171
|
+
else:
|
|
172
|
+
logging.warning(
|
|
173
|
+
f"Removed segments during finetuning of the width in '{col}': "
|
|
174
|
+
+ ", ".join(removed_ranges[:Threshold_segments_to_print])
|
|
175
|
+
)
|
|
176
|
+
else:
|
|
177
|
+
logging.warning(
|
|
178
|
+
f"Removed segments during finetuning of the width in '{col}': " + ", ".join(removed_ranges)
|
|
179
|
+
)
|
|
180
|
+
|
|
181
|
+
# Update DataFrame
|
|
182
|
+
for i, col in enumerate(ID_columns_name_list):
|
|
183
|
+
df[col] = new_ID_arr[:, i]
|
|
184
|
+
|
|
185
|
+
return df
|
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import tkinter as tk
|
|
3
|
+
|
|
4
|
+
import matplotlib.pyplot as plt
|
|
5
|
+
import pandas as pd
|
|
6
|
+
|
|
7
|
+
from pydpeet.process.sequence.utils.console_prints.log_time import log_time
|
|
8
|
+
|
|
9
|
+
# -------------------------------------------------------------------
|
|
10
|
+
# Compute screen dimensions once
|
|
11
|
+
# -------------------------------------------------------------------
|
|
12
|
+
_root = tk.Tk()
|
|
13
|
+
_root.withdraw()
|
|
14
|
+
_root.update_idletasks()
|
|
15
|
+
_SCREEN_DIMS = (_root.winfo_screenwidth(), _root.winfo_screenheight())
|
|
16
|
+
try:
|
|
17
|
+
_root.destroy()
|
|
18
|
+
except tk.TclError:
|
|
19
|
+
pass
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _visualize_phases(
|
|
23
|
+
dataframe: pd.DataFrame,
|
|
24
|
+
start_time: float = 0.0,
|
|
25
|
+
end_time: float = 1e50,
|
|
26
|
+
segment_id_cols: list[str] = None,
|
|
27
|
+
segment_colors: (dict[str, str] | list[str]) = None,
|
|
28
|
+
segment_alpha: float = 0.3,
|
|
29
|
+
columns_to_visualize: list[str] = None,
|
|
30
|
+
line_colors: dict[str, str] = None,
|
|
31
|
+
y_axis_ranges: dict[str, tuple[float, float]] = None,
|
|
32
|
+
use_lines_for_segments: bool = True,
|
|
33
|
+
show_column_names: bool = True,
|
|
34
|
+
show_time: bool = True,
|
|
35
|
+
show_id: bool = True,
|
|
36
|
+
width_height_ratio: list[float, float] = None,
|
|
37
|
+
show_runtime: bool = True,
|
|
38
|
+
show_grid: bool = False,
|
|
39
|
+
) -> None:
|
|
40
|
+
"""
|
|
41
|
+
Visualizes the given dataframe by plotting all columns over time.
|
|
42
|
+
|
|
43
|
+
Parameters:
|
|
44
|
+
dataframe (pd.DataFrame): The dataframe to be visualized.
|
|
45
|
+
start_time (float, optional): The start time of the visualization. Defaults to 0.0.
|
|
46
|
+
end_time (float, optional): The end time of the visualization. Defaults to 1e50.
|
|
47
|
+
segment_id_cols (list[str], optional): The columns of the dataframe to be used as segment IDs. Defaults to None.
|
|
48
|
+
segment_colors (dict[str, str] | list[str], optional): The colors to be used for the segments. Defaults to None.
|
|
49
|
+
segment_alpha (float, optional): The alpha value of the segment backgrounds. Defaults to 0.3.
|
|
50
|
+
columns_to_visualize (list[str], optional): The columns of the dataframe to be visualized. Defaults to ['Voltage[V]', 'Current[A]', 'Power[W]'].
|
|
51
|
+
line_colors (dict[str, str], optional): The colors to be used for the lines. Defaults to None.
|
|
52
|
+
y_axis_ranges (dict[str, tuple[float, float]], optional): The ranges of the y-axis. Defaults to None.
|
|
53
|
+
use_lines_for_segments (bool, optional): Whether to use lines for the segments. Defaults to True.
|
|
54
|
+
show_column_names (bool, optional): Whether to show the column names. Defaults to True.
|
|
55
|
+
show_time (bool, optional): Whether to show the time. Defaults to True.
|
|
56
|
+
show_id (bool, optional): Whether to show the ID. Defaults to True.
|
|
57
|
+
width_height_ratio (list[float, float], optional): The ratio of the width to the height of the figure. Defaults to [1.0, 0.3].
|
|
58
|
+
show_runtime (bool, optional): Whether to show the runtime. Defaults to True.
|
|
59
|
+
show_grid (bool, optional): Whether to show the grid. Defaults to False.
|
|
60
|
+
|
|
61
|
+
Returns:
|
|
62
|
+
None
|
|
63
|
+
"""
|
|
64
|
+
# Set default values for mutable data structures
|
|
65
|
+
if columns_to_visualize is None:
|
|
66
|
+
columns_to_visualize = ["Voltage[V]", "Current[A]", "Power[W]"]
|
|
67
|
+
if width_height_ratio is None:
|
|
68
|
+
width_height_ratio = [1.0, 0.3]
|
|
69
|
+
|
|
70
|
+
# 1) Filter by time
|
|
71
|
+
with log_time("filtering by time", show_runtime):
|
|
72
|
+
mask = pd.Series(True, index=dataframe.index)
|
|
73
|
+
if start_time is not None:
|
|
74
|
+
mask &= dataframe["Test_Time[s]"] >= start_time
|
|
75
|
+
if end_time is not None:
|
|
76
|
+
mask &= dataframe["Test_Time[s]"] <= end_time
|
|
77
|
+
df = dataframe.loc[mask]
|
|
78
|
+
|
|
79
|
+
# 2) Normalize line colors and y-axis ranges
|
|
80
|
+
with log_time("normalizing colors and y-axis ranges", show_runtime):
|
|
81
|
+
line_colors = line_colors or {}
|
|
82
|
+
y_axis_ranges = y_axis_ranges or {}
|
|
83
|
+
|
|
84
|
+
# 3) Map segment colors to `Variable` values
|
|
85
|
+
with log_time("mapping segment colors", show_runtime):
|
|
86
|
+
segment_colors = {var: col for var, col in zip(segment_id_cols, segment_colors, strict=False)}
|
|
87
|
+
|
|
88
|
+
# 4) Set up figure
|
|
89
|
+
with log_time("setting up figure", show_runtime):
|
|
90
|
+
screen_w, screen_h = _SCREEN_DIMS
|
|
91
|
+
dpi = plt.rcParams["figure.dpi"]
|
|
92
|
+
fig_w, fig_h = (screen_w * width_height_ratio[0]) / dpi, (screen_h * width_height_ratio[1]) / dpi
|
|
93
|
+
|
|
94
|
+
plt.ioff()
|
|
95
|
+
fig, ax_base = plt.subplots(figsize=(fig_w, fig_h))
|
|
96
|
+
axes = {}
|
|
97
|
+
offset = 0.02
|
|
98
|
+
|
|
99
|
+
for idx, col in enumerate(columns_to_visualize):
|
|
100
|
+
if idx == 0:
|
|
101
|
+
ax = ax_base
|
|
102
|
+
elif idx == 1:
|
|
103
|
+
ax = ax_base.twinx()
|
|
104
|
+
else:
|
|
105
|
+
ax = ax_base.twinx()
|
|
106
|
+
ax.spines["right"].set_position(("axes", 1 + offset))
|
|
107
|
+
ax.spines["right"].set_visible(True)
|
|
108
|
+
ax.yaxis.set_label_position("right")
|
|
109
|
+
ax.yaxis.set_ticks_position("right")
|
|
110
|
+
offset += 0.05
|
|
111
|
+
|
|
112
|
+
ax.set_ylabel(col, color=line_colors.get(col))
|
|
113
|
+
ax.tick_params(axis="y", labelcolor=line_colors.get(col))
|
|
114
|
+
if col in y_axis_ranges:
|
|
115
|
+
ax.set_ylim(*y_axis_ranges[col])
|
|
116
|
+
axes[col] = ax
|
|
117
|
+
fig.subplots_adjust(right=1 + offset + 0.05)
|
|
118
|
+
|
|
119
|
+
# 5) Plot data
|
|
120
|
+
with log_time("plotting data", show_runtime):
|
|
121
|
+
t = df["Test_Time[s]"]
|
|
122
|
+
for col in columns_to_visualize:
|
|
123
|
+
if col in df.columns:
|
|
124
|
+
axes[col].plot(t, df[col], label=col, color=line_colors.get(col))
|
|
125
|
+
|
|
126
|
+
# 6) Group segments by ID + Variable
|
|
127
|
+
with log_time("grouping segments by ID + Variable", show_runtime):
|
|
128
|
+
stats = df.groupby(["ID", "Variable"])["Test_Time[s]"].agg(tmin="min", tmax="max").reset_index()
|
|
129
|
+
|
|
130
|
+
y0, y1 = ax_base.get_ylim()
|
|
131
|
+
height = y1 - y0
|
|
132
|
+
|
|
133
|
+
# 7) Draw segment backgrounds and labels
|
|
134
|
+
with log_time("drawing segment backgrounds and labels", show_runtime):
|
|
135
|
+
intervals = []
|
|
136
|
+
colors = []
|
|
137
|
+
vline_positions = []
|
|
138
|
+
mid_y = (y0 + y1) / 2
|
|
139
|
+
|
|
140
|
+
for _, row in stats.iterrows():
|
|
141
|
+
tmin, tmax = row["tmin"], row["tmax"]
|
|
142
|
+
duration = tmax - tmin
|
|
143
|
+
variable = row["Variable"]
|
|
144
|
+
color = segment_colors.get(variable, "gray")
|
|
145
|
+
|
|
146
|
+
intervals.append((tmin, duration))
|
|
147
|
+
colors.append(color)
|
|
148
|
+
|
|
149
|
+
if use_lines_for_segments:
|
|
150
|
+
vline_positions.append(tmin)
|
|
151
|
+
|
|
152
|
+
# Draw all backgrounds at once
|
|
153
|
+
ax_base.broken_barh(intervals, (y0, height), facecolors=colors, alpha=segment_alpha)
|
|
154
|
+
|
|
155
|
+
# Draw all vertical lines at once
|
|
156
|
+
if use_lines_for_segments and vline_positions:
|
|
157
|
+
ax_base.vlines(vline_positions, y0, y1, colors="k", alpha=segment_alpha)
|
|
158
|
+
|
|
159
|
+
# Draw text labels (still per segment, this is usually okay)
|
|
160
|
+
for _, row in stats.iterrows():
|
|
161
|
+
tmin, tmax = row["tmin"], row["tmax"]
|
|
162
|
+
duration = tmax - tmin
|
|
163
|
+
x_center = tmin + duration / 2
|
|
164
|
+
variable = row["Variable"]
|
|
165
|
+
id = row["ID"]
|
|
166
|
+
|
|
167
|
+
label_parts = []
|
|
168
|
+
if show_id:
|
|
169
|
+
label_parts.append(f"ID:{id}")
|
|
170
|
+
if show_column_names:
|
|
171
|
+
label_parts.append(f"{variable}")
|
|
172
|
+
if show_time:
|
|
173
|
+
label_parts.append(f"{duration:.1f}s")
|
|
174
|
+
|
|
175
|
+
label = " ".join(label_parts)
|
|
176
|
+
|
|
177
|
+
ax_base.text(x_center, mid_y, label, ha="center", va="center", rotation=90, size=10)
|
|
178
|
+
|
|
179
|
+
# 8) Adding grid and legend
|
|
180
|
+
with log_time("adding grid and legend", show_runtime):
|
|
181
|
+
# TODO power labels multiple times shown fix?
|
|
182
|
+
ax_base.set_xlabel("Testtime [s]")
|
|
183
|
+
legend_handles, legend_labels = [], []
|
|
184
|
+
for ax in axes.values():
|
|
185
|
+
handles, labels = ax.get_legend_handles_labels()
|
|
186
|
+
legend_handles += handles
|
|
187
|
+
legend_labels += labels
|
|
188
|
+
if handles:
|
|
189
|
+
ax_base.legend(legend_handles, legend_labels, loc="upper left")
|
|
190
|
+
if show_grid:
|
|
191
|
+
ax_base.grid()
|
|
192
|
+
plt.tight_layout()
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
# TODO: Docstring
|
|
196
|
+
def visualize_phases(
|
|
197
|
+
dataframe: pd.DataFrame,
|
|
198
|
+
start_time: float = None,
|
|
199
|
+
end_time: float = None,
|
|
200
|
+
visualize_phases_config: list[tuple[str, str]] = None,
|
|
201
|
+
segment_alpha: float = 0.3,
|
|
202
|
+
line_visualization_config: list[tuple[str, str, tuple[float, float]]] = None,
|
|
203
|
+
use_lines_for_segments: bool = True,
|
|
204
|
+
show_column_names: bool = True,
|
|
205
|
+
show_time: bool = True,
|
|
206
|
+
show_id: bool = True,
|
|
207
|
+
width_height_ratio: tuple[float, float] | list[float] = None,
|
|
208
|
+
show_runtime: bool = True,
|
|
209
|
+
) -> None:
|
|
210
|
+
# Set default values for mutable data structures
|
|
211
|
+
if visualize_phases_config is None:
|
|
212
|
+
visualize_phases_config = [
|
|
213
|
+
("V", "blue"),
|
|
214
|
+
("I", "red"),
|
|
215
|
+
("P", "green"),
|
|
216
|
+
]
|
|
217
|
+
if line_visualization_config is None:
|
|
218
|
+
line_visualization_config = [
|
|
219
|
+
("Voltage[V]", "blue", (2.3, 4.3)),
|
|
220
|
+
("Current[A]", "red", (-10, 10)),
|
|
221
|
+
# ("Power[W]", "green", (-40, 40)),
|
|
222
|
+
]
|
|
223
|
+
if width_height_ratio is None:
|
|
224
|
+
width_height_ratio: float = [1.0, 0.3]
|
|
225
|
+
|
|
226
|
+
if start_time is None:
|
|
227
|
+
logging.warning("start_time is None - setting it to 0.0")
|
|
228
|
+
start_time = 0.0
|
|
229
|
+
if end_time is None:
|
|
230
|
+
logging.warning("end_time is None - setting it to the biggest possible float")
|
|
231
|
+
end_time = float("inf")
|
|
232
|
+
if dataframe is None:
|
|
233
|
+
raise ValueError("dataframe is None")
|
|
234
|
+
if not isinstance(dataframe, pd.DataFrame):
|
|
235
|
+
raise TypeError("dataframe must be a pandas DataFrame")
|
|
236
|
+
if not (isinstance(width_height_ratio, list | tuple) and len(width_height_ratio) == 2):
|
|
237
|
+
raise ValueError("width_height_ratio must be a list or tuple of length 2")
|
|
238
|
+
if "Test_Time[s]" not in dataframe.columns:
|
|
239
|
+
raise ValueError("dataframe needs to have at least column 'Test_Time[s]'")
|
|
240
|
+
if "ID" not in dataframe.columns:
|
|
241
|
+
raise ValueError("dataframe needs to have at least column 'ID'")
|
|
242
|
+
if not isinstance(visualize_phases_config, list):
|
|
243
|
+
raise TypeError("visualize_phases_config must be a list")
|
|
244
|
+
if not isinstance(line_visualization_config, list):
|
|
245
|
+
raise TypeError("line_visualization_config must be a list")
|
|
246
|
+
if start_time >= end_time:
|
|
247
|
+
raise ValueError(f"start_time ({start_time}) must be less than end_time ({end_time})")
|
|
248
|
+
if not (0 <= segment_alpha <= 1):
|
|
249
|
+
logging.warning("segment_alpha must be between 0 and 1 - resetting it now to 0.3")
|
|
250
|
+
segment_alpha = 0.3
|
|
251
|
+
|
|
252
|
+
if start_time is None:
|
|
253
|
+
start_time = dataframe["Test_Time[s]"].min()
|
|
254
|
+
if end_time is None:
|
|
255
|
+
end_time = dataframe["Test_Time[s]"].max()
|
|
256
|
+
|
|
257
|
+
segment_id_cols = [col for col, _ in visualize_phases_config]
|
|
258
|
+
segment_colors = [color for _, color in visualize_phases_config]
|
|
259
|
+
columns_to_visualize = [col for col, _, _ in line_visualization_config]
|
|
260
|
+
line_colors = {col: color for col, color, _ in line_visualization_config}
|
|
261
|
+
y_axis_ranges = {col: y_range for col, _, y_range in line_visualization_config}
|
|
262
|
+
|
|
263
|
+
if not isinstance(segment_id_cols, list):
|
|
264
|
+
raise TypeError("segment_id_cols must be a list of strings")
|
|
265
|
+
if not isinstance(segment_colors, list | dict):
|
|
266
|
+
raise TypeError("segment_colors must be a list or dict")
|
|
267
|
+
if isinstance(segment_colors, list) and len(segment_id_cols) != len(segment_colors):
|
|
268
|
+
raise ValueError("segment_id_cols and segment_colors must have the same length")
|
|
269
|
+
if not isinstance(columns_to_visualize, list):
|
|
270
|
+
raise TypeError("columns_to_visualize must be a list")
|
|
271
|
+
|
|
272
|
+
_visualize_phases(
|
|
273
|
+
dataframe=dataframe,
|
|
274
|
+
start_time=start_time,
|
|
275
|
+
end_time=end_time,
|
|
276
|
+
segment_id_cols=segment_id_cols,
|
|
277
|
+
segment_colors=segment_colors,
|
|
278
|
+
segment_alpha=segment_alpha,
|
|
279
|
+
columns_to_visualize=columns_to_visualize,
|
|
280
|
+
line_colors=line_colors,
|
|
281
|
+
y_axis_ranges=y_axis_ranges,
|
|
282
|
+
use_lines_for_segments=use_lines_for_segments,
|
|
283
|
+
show_column_names=show_column_names,
|
|
284
|
+
show_time=show_time,
|
|
285
|
+
show_id=show_id,
|
|
286
|
+
width_height_ratio=width_height_ratio,
|
|
287
|
+
show_runtime=show_runtime,
|
|
288
|
+
)
|
pydpeet/res/__init__.py
ADDED
pydpeet/settings.py
ADDED
|
File without changes
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def set_logging_style(
|
|
5
|
+
level="WARNING", formatting_string="%(levelname)s | %(pathname)s:%(lineno)d | %(message)s"
|
|
6
|
+
) -> None:
|
|
7
|
+
"""
|
|
8
|
+
Sets up on import the logging configuration to use the specified level and a custom format.
|
|
9
|
+
|
|
10
|
+
The logging configuration is set to use the specified level. The format of the
|
|
11
|
+
messages is set to '<levelname> | <pathname>:<lineno> | <message>'.
|
|
12
|
+
|
|
13
|
+
Parameters
|
|
14
|
+
----------
|
|
15
|
+
level : int, optional
|
|
16
|
+
The logging level to use. Defaults to logging.WARNING.
|
|
17
|
+
formatting_string : str, optional
|
|
18
|
+
The formatting string to use for the log messages. Defaults to
|
|
19
|
+
'%(levelname)s | %(pathname)s:%(lineno)d | %(message)s'.
|
|
20
|
+
|
|
21
|
+
Returns
|
|
22
|
+
-------
|
|
23
|
+
None
|
|
24
|
+
"""
|
|
25
|
+
if isinstance(level, str):
|
|
26
|
+
level = getattr(logging, level)
|
|
27
|
+
|
|
28
|
+
logging.basicConfig(level=level, format=formatting_string, force=True)
|
pydpeet/version.py
ADDED
|
File without changes
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: PyDPEET
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: Python package to read, unify, and convert battery measurement data from arbitrary battery cyclers to Parquet files. This package also provides functions to process, evaluate, and visualise the standardised data.
|
|
5
|
+
Author: Alexander Günter Hinrichsen, Jan Kalisch, Daniel Schröder, Cataldo De Simone
|
|
6
|
+
Author-email: Anton Schlösser <a.schloesser@tu-berlin.de>, Martin Otto <m.otto.1@tu-berlin.de>
|
|
7
|
+
Requires-Python: >=3.12
|
|
8
|
+
Description-Content-Type: text/markdown
|
|
9
|
+
License-File: LICENCE.md
|
|
10
|
+
License-File: AUTHORS.md
|
|
11
|
+
Requires-Dist: pandas
|
|
12
|
+
Requires-Dist: numpy
|
|
13
|
+
Requires-Dist: pyarrow
|
|
14
|
+
Requires-Dist: python-calamine
|
|
15
|
+
Requires-Dist: numba
|
|
16
|
+
Requires-Dist: matplotlib
|
|
17
|
+
Requires-Dist: scipy
|
|
18
|
+
Requires-Dist: bibtexparser
|
|
19
|
+
Requires-Dist: scikit-learn
|
|
20
|
+
Provides-Extra: docs
|
|
21
|
+
Requires-Dist: sphinx; extra == "docs"
|
|
22
|
+
Requires-Dist: pydata-sphinx-theme; extra == "docs"
|
|
23
|
+
Requires-Dist: myst-parser; extra == "docs"
|
|
24
|
+
Requires-Dist: nbsphinx; extra == "docs"
|
|
25
|
+
Requires-Dist: sphinx-copybutton; extra == "docs"
|
|
26
|
+
Requires-Dist: bibtexparser; extra == "docs"
|
|
27
|
+
Dynamic: license-file
|
|
28
|
+
|
|
29
|
+
# PyDPEET - Fast and Easy Battery Data Unification, Processing, and Analysis
|
|
30
|
+
|
|
31
|
+
[[_TOC_]]
|
|
32
|
+
|
|
33
|
+
## Disclaimer
|
|
34
|
+
|
|
35
|
+
This README is still under (re-)construction due to changes in the codebase.
|
|
36
|
+
|
|
37
|
+
## Description
|
|
38
|
+
|
|
39
|
+
<!-- This project enables you to convert battery measurement data to a standardized format.
|
|
40
|
+
|
|
41
|
+
Cycler output their measurement data in different formats and different file types, like for example .csv and .xslx. Each has to be handled differently which makes it difficult to work with the data and that's the reason why we created a standardized format.
|
|
42
|
+
The standardized Data and Metadata can be used inside of the code and can be output as a .csv (Data), .xlsx (Data) or parquet(Data) to a output_path of your choosing.
|
|
43
|
+
|
|
44
|
+
Keeping additional data outside of our definition of the standardized columns and custom cycler handling is also possible. -->
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
## Standardised Format
|
|
48
|
+
|
|
49
|
+
The standard columns are defined as follows:
|
|
50
|
+
|
|
51
|
+
```python
|
|
52
|
+
STANDARD_COLUMNS = [
|
|
53
|
+
"Meta_Data",
|
|
54
|
+
"Step_Count",
|
|
55
|
+
"Voltage[V]",
|
|
56
|
+
"Current[A]",
|
|
57
|
+
"Temperature[°C]",
|
|
58
|
+
"Test_Time[s]",
|
|
59
|
+
"Date_Time",
|
|
60
|
+
"EIS_f[Hz]",
|
|
61
|
+
"EIS_Z_Real[Ohm]",
|
|
62
|
+
"EIS_Z_Imag[Ohm]",
|
|
63
|
+
"EIS_DC[A]"
|
|
64
|
+
]
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
## Meta data
|
|
68
|
+
|
|
69
|
+
## Supported Cyclers
|
|
70
|
+
|
|
71
|
+
## Installation
|
|
72
|
+
|
|
73
|
+
### For Developers
|
|
74
|
+
|
|
75
|
+
#### Rebuild package for "pip"
|
|
76
|
+
|
|
77
|
+
#### How to renew the documentation after implementing new functions
|
|
78
|
+
|
|
79
|
+
### For Users
|
|
80
|
+
|
|
81
|
+
## Usage
|
|
82
|
+
|
|
83
|
+
## Custom Handling of Cycler
|
|
84
|
+
|
|
85
|
+
## How to add a Custom Handling to the Project
|