steer-core 0.1.16__py3-none-any.whl → 0.1.17__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.
- steer_core/Apps/Components/MaterialSelectors.py +444 -302
- steer_core/Apps/Components/RangeSliderComponents.py +176 -125
- steer_core/Apps/Components/SliderComponents.py +205 -184
- steer_core/Apps/ContextManagers.py +26 -20
- steer_core/Apps/Performance/CallbackTimer.py +3 -2
- steer_core/Apps/Utils/SliderControls.py +239 -209
- steer_core/Constants/Units.py +6 -1
- steer_core/Data/database.db +0 -0
- steer_core/DataManager.py +123 -122
- steer_core/Decorators/Coordinates.py +9 -4
- steer_core/Decorators/Electrochemical.py +7 -2
- steer_core/Decorators/General.py +7 -4
- steer_core/Decorators/Objects.py +4 -1
- steer_core/Mixins/Colors.py +185 -1
- steer_core/Mixins/Coordinates.py +112 -89
- steer_core/Mixins/Data.py +1 -1
- steer_core/Mixins/Plotter.py +149 -0
- steer_core/Mixins/Serializer.py +5 -7
- steer_core/Mixins/TypeChecker.py +42 -29
- steer_core/__init__.py +1 -1
- {steer_core-0.1.16.dist-info → steer_core-0.1.17.dist-info}/METADATA +1 -1
- steer_core-0.1.17.dist-info/RECORD +34 -0
- steer_core-0.1.16.dist-info/RECORD +0 -33
- {steer_core-0.1.16.dist-info → steer_core-0.1.17.dist-info}/WHEEL +0 -0
- {steer_core-0.1.16.dist-info → steer_core-0.1.17.dist-info}/top_level.txt +0 -0
steer_core/Mixins/Colors.py
CHANGED
|
@@ -4,14 +4,16 @@ import plotly.colors as pc
|
|
|
4
4
|
import pandas as pd
|
|
5
5
|
import numpy as np
|
|
6
6
|
|
|
7
|
+
|
|
7
8
|
class ColorMixin:
|
|
8
9
|
"""
|
|
9
10
|
A class to manage colors, including conversion between hex and RGB formats,
|
|
10
11
|
and generating color gradients.
|
|
11
12
|
"""
|
|
13
|
+
|
|
12
14
|
@staticmethod
|
|
13
15
|
def rgb_tuple_to_hex(rgb):
|
|
14
|
-
return
|
|
16
|
+
return "#{:02x}{:02x}{:02x}".format(*rgb)
|
|
15
17
|
|
|
16
18
|
@staticmethod
|
|
17
19
|
def get_colorway(color1, color2, n):
|
|
@@ -39,3 +41,185 @@ class ColorMixin:
|
|
|
39
41
|
|
|
40
42
|
return colors
|
|
41
43
|
|
|
44
|
+
@staticmethod
|
|
45
|
+
def adjust_fill_opacity(color_str: str, opacity: float) -> str:
|
|
46
|
+
"""
|
|
47
|
+
Adjust the fill opacity of any color format while preserving line opacity.
|
|
48
|
+
|
|
49
|
+
Parameters
|
|
50
|
+
----------
|
|
51
|
+
color_str : str
|
|
52
|
+
Color in any format (hex, rgb, rgba, named)
|
|
53
|
+
opacity : float
|
|
54
|
+
Target opacity (0.0 to 1.0)
|
|
55
|
+
|
|
56
|
+
Returns
|
|
57
|
+
-------
|
|
58
|
+
str
|
|
59
|
+
Color string with adjusted opacity in rgba format
|
|
60
|
+
"""
|
|
61
|
+
if not color_str:
|
|
62
|
+
return color_str
|
|
63
|
+
|
|
64
|
+
if "rgba" in color_str:
|
|
65
|
+
return ColorMixin._update_rgba_opacity(color_str, opacity)
|
|
66
|
+
elif "rgb" in color_str:
|
|
67
|
+
return color_str.replace("rgb(", "rgba(").replace(")", f", {opacity})")
|
|
68
|
+
elif color_str.startswith("#"):
|
|
69
|
+
return ColorMixin._hex_to_rgba(color_str, opacity)
|
|
70
|
+
|
|
71
|
+
# For named colors, try to convert or return as-is
|
|
72
|
+
return color_str
|
|
73
|
+
|
|
74
|
+
@staticmethod
|
|
75
|
+
def _hex_to_rgba(hex_color: str, opacity: float) -> str:
|
|
76
|
+
"""
|
|
77
|
+
Convert hex color to rgba with specified opacity.
|
|
78
|
+
|
|
79
|
+
Parameters
|
|
80
|
+
----------
|
|
81
|
+
hex_color : str
|
|
82
|
+
Hex color string (e.g., '#FF0000')
|
|
83
|
+
opacity : float
|
|
84
|
+
Target opacity (0.0 to 1.0)
|
|
85
|
+
|
|
86
|
+
Returns
|
|
87
|
+
-------
|
|
88
|
+
str
|
|
89
|
+
RGBA color string
|
|
90
|
+
"""
|
|
91
|
+
hex_color = hex_color.lstrip("#")
|
|
92
|
+
if len(hex_color) == 6:
|
|
93
|
+
try:
|
|
94
|
+
r = int(hex_color[0:2], 16)
|
|
95
|
+
g = int(hex_color[2:4], 16)
|
|
96
|
+
b = int(hex_color[4:6], 16)
|
|
97
|
+
return f"rgba({r}, {g}, {b}, {opacity})"
|
|
98
|
+
except ValueError:
|
|
99
|
+
return hex_color
|
|
100
|
+
elif len(hex_color) == 3:
|
|
101
|
+
# Handle short hex format (#RGB -> #RRGGBB)
|
|
102
|
+
try:
|
|
103
|
+
r = int(hex_color[0] * 2, 16)
|
|
104
|
+
g = int(hex_color[1] * 2, 16)
|
|
105
|
+
b = int(hex_color[2] * 2, 16)
|
|
106
|
+
return f"rgba({r}, {g}, {b}, {opacity})"
|
|
107
|
+
except ValueError:
|
|
108
|
+
return hex_color
|
|
109
|
+
|
|
110
|
+
return hex_color
|
|
111
|
+
|
|
112
|
+
@staticmethod
|
|
113
|
+
def _update_rgba_opacity(rgba_str: str, opacity: float) -> str:
|
|
114
|
+
"""
|
|
115
|
+
Update the opacity component of an existing rgba color string.
|
|
116
|
+
|
|
117
|
+
Parameters
|
|
118
|
+
----------
|
|
119
|
+
rgba_str : str
|
|
120
|
+
Existing rgba color string (e.g., 'rgba(255, 0, 0, 0.5)')
|
|
121
|
+
opacity : float
|
|
122
|
+
New opacity value (0.0 to 1.0)
|
|
123
|
+
|
|
124
|
+
Returns
|
|
125
|
+
-------
|
|
126
|
+
str
|
|
127
|
+
Updated rgba color string
|
|
128
|
+
"""
|
|
129
|
+
if "rgba(" not in rgba_str:
|
|
130
|
+
return rgba_str
|
|
131
|
+
|
|
132
|
+
try:
|
|
133
|
+
# Extract RGB components and replace alpha
|
|
134
|
+
rgb_part = rgba_str.split("rgba(")[1].rsplit(",", 1)[0]
|
|
135
|
+
return f"rgba({rgb_part}, {opacity})"
|
|
136
|
+
except (IndexError, ValueError):
|
|
137
|
+
return rgba_str
|
|
138
|
+
|
|
139
|
+
@staticmethod
|
|
140
|
+
def get_color_format(color_str: str) -> str:
|
|
141
|
+
"""
|
|
142
|
+
Detect the format of a color string.
|
|
143
|
+
|
|
144
|
+
Parameters
|
|
145
|
+
----------
|
|
146
|
+
color_str : str
|
|
147
|
+
Color string in any format
|
|
148
|
+
|
|
149
|
+
Returns
|
|
150
|
+
-------
|
|
151
|
+
str
|
|
152
|
+
Color format: 'hex', 'rgb', 'rgba', 'hsl', 'hsla', 'named', or 'unknown'
|
|
153
|
+
"""
|
|
154
|
+
if not color_str or not isinstance(color_str, str):
|
|
155
|
+
return "unknown"
|
|
156
|
+
|
|
157
|
+
color_str = color_str.strip().lower()
|
|
158
|
+
|
|
159
|
+
if color_str.startswith("#"):
|
|
160
|
+
return "hex"
|
|
161
|
+
elif color_str.startswith("rgba("):
|
|
162
|
+
return "rgba"
|
|
163
|
+
elif color_str.startswith("rgb("):
|
|
164
|
+
return "rgb"
|
|
165
|
+
elif color_str.startswith("hsla("):
|
|
166
|
+
return "hsla"
|
|
167
|
+
elif color_str.startswith("hsl("):
|
|
168
|
+
return "hsl"
|
|
169
|
+
else:
|
|
170
|
+
return "named" # Could be a named color like 'red', 'blue'
|
|
171
|
+
|
|
172
|
+
@staticmethod
|
|
173
|
+
def validate_opacity(opacity: float, param_name: str = "opacity") -> None:
|
|
174
|
+
"""
|
|
175
|
+
Validate that opacity is within valid range.
|
|
176
|
+
|
|
177
|
+
Parameters
|
|
178
|
+
----------
|
|
179
|
+
opacity : float
|
|
180
|
+
Opacity value to validate
|
|
181
|
+
param_name : str
|
|
182
|
+
Parameter name for error messages
|
|
183
|
+
|
|
184
|
+
Raises
|
|
185
|
+
------
|
|
186
|
+
ValueError
|
|
187
|
+
If opacity is not between 0.0 and 1.0
|
|
188
|
+
"""
|
|
189
|
+
if not isinstance(opacity, (int, float)):
|
|
190
|
+
raise ValueError(f"{param_name} must be a number, got {type(opacity)}")
|
|
191
|
+
|
|
192
|
+
if not (0.0 <= opacity <= 1.0):
|
|
193
|
+
raise ValueError(f"{param_name} must be between 0.0 and 1.0, got {opacity}")
|
|
194
|
+
|
|
195
|
+
@staticmethod
|
|
196
|
+
def adjust_trace_opacity(trace, opacity: float) -> None:
|
|
197
|
+
"""
|
|
198
|
+
Adjust opacity of a plotly trace in-place.
|
|
199
|
+
|
|
200
|
+
Parameters
|
|
201
|
+
----------
|
|
202
|
+
trace : plotly trace object
|
|
203
|
+
The trace to modify
|
|
204
|
+
opacity : float
|
|
205
|
+
Target opacity (0.0 to 1.0)
|
|
206
|
+
"""
|
|
207
|
+
ColorMixin.validate_opacity(opacity)
|
|
208
|
+
|
|
209
|
+
# Adjust fill color if present
|
|
210
|
+
if hasattr(trace, "fillcolor") and trace.fillcolor:
|
|
211
|
+
trace.fillcolor = ColorMixin.adjust_fill_opacity(trace.fillcolor, opacity)
|
|
212
|
+
|
|
213
|
+
# Adjust marker color if present
|
|
214
|
+
if (
|
|
215
|
+
hasattr(trace, "marker")
|
|
216
|
+
and hasattr(trace.marker, "color")
|
|
217
|
+
and trace.marker.color
|
|
218
|
+
):
|
|
219
|
+
trace.marker.color = ColorMixin.adjust_fill_opacity(
|
|
220
|
+
trace.marker.color, opacity
|
|
221
|
+
)
|
|
222
|
+
|
|
223
|
+
# Adjust line color if present (but maybe keep line opacity at 1.0?)
|
|
224
|
+
if hasattr(trace, "line") and hasattr(trace.line, "color") and trace.line.color:
|
|
225
|
+
trace.line.color = ColorMixin.adjust_fill_opacity(trace.line.color, opacity)
|
steer_core/Mixins/Coordinates.py
CHANGED
|
@@ -2,13 +2,17 @@ import numpy as np
|
|
|
2
2
|
import pandas as pd
|
|
3
3
|
from typing import Tuple
|
|
4
4
|
|
|
5
|
+
|
|
5
6
|
class CoordinateMixin:
|
|
6
7
|
"""
|
|
7
8
|
A class to manage and manipulate 3D coordinates.
|
|
8
9
|
Provides methods for rotation, area calculation, and coordinate ordering.
|
|
9
10
|
"""
|
|
11
|
+
|
|
10
12
|
@staticmethod
|
|
11
|
-
def rotate_coordinates(
|
|
13
|
+
def rotate_coordinates(
|
|
14
|
+
coords: np.ndarray, axis: str, angle: float, center: tuple = None
|
|
15
|
+
) -> np.ndarray:
|
|
12
16
|
"""
|
|
13
17
|
Rotate a (N, 3) NumPy array of 3D coordinates around the specified axis.
|
|
14
18
|
Can handle coordinates with None values (preserves None positions).
|
|
@@ -20,48 +24,64 @@ class CoordinateMixin:
|
|
|
20
24
|
:return: Rotated NumPy array of shape (N, 3)
|
|
21
25
|
"""
|
|
22
26
|
if coords.shape[1] != 3:
|
|
23
|
-
raise ValueError(
|
|
27
|
+
raise ValueError(
|
|
28
|
+
"Input array must have shape (N, 3) for x, y, z coordinates"
|
|
29
|
+
)
|
|
24
30
|
|
|
25
31
|
# Validate center parameter
|
|
26
32
|
if center is not None:
|
|
27
33
|
if not isinstance(center, (tuple, list)) or len(center) != 3:
|
|
28
|
-
raise ValueError(
|
|
34
|
+
raise ValueError(
|
|
35
|
+
"Center must be a tuple or list of 3 coordinates (x, y, z)"
|
|
36
|
+
)
|
|
29
37
|
if not all(isinstance(coord, (int, float)) for coord in center):
|
|
30
38
|
raise TypeError("All center coordinates must be numbers")
|
|
31
39
|
center = np.array(center, dtype=float)
|
|
32
40
|
|
|
33
41
|
# Check if we have None values
|
|
34
|
-
has_nones =
|
|
35
|
-
|
|
42
|
+
has_nones = (
|
|
43
|
+
np.any(pd.isna(coords[:, 0]))
|
|
44
|
+
if hasattr(pd, "isna")
|
|
45
|
+
else np.any(coords[:, 0] == None)
|
|
46
|
+
)
|
|
47
|
+
|
|
36
48
|
if has_nones:
|
|
37
49
|
# Create a copy to preserve original
|
|
38
50
|
result = coords.copy()
|
|
39
|
-
|
|
51
|
+
|
|
40
52
|
# Find non-None rows
|
|
41
|
-
x_is_none =
|
|
53
|
+
x_is_none = (
|
|
54
|
+
pd.isna(coords[:, 0]) if hasattr(pd, "isna") else (coords[:, 0] == None)
|
|
55
|
+
)
|
|
42
56
|
valid_mask = ~x_is_none
|
|
43
|
-
|
|
57
|
+
|
|
44
58
|
if np.any(valid_mask):
|
|
45
59
|
# Extract valid coordinates and convert to float
|
|
46
60
|
valid_coords = coords[valid_mask].astype(float)
|
|
47
|
-
|
|
61
|
+
|
|
48
62
|
# Apply rotation to valid coordinates
|
|
49
|
-
rotated_valid = CoordinateMixin._rotate_around_center(
|
|
50
|
-
|
|
63
|
+
rotated_valid = CoordinateMixin._rotate_around_center(
|
|
64
|
+
valid_coords, axis, angle, center
|
|
65
|
+
)
|
|
66
|
+
|
|
51
67
|
# Put rotated coordinates back in result
|
|
52
68
|
result[valid_mask] = rotated_valid
|
|
53
|
-
|
|
69
|
+
|
|
54
70
|
return result
|
|
55
|
-
|
|
71
|
+
|
|
56
72
|
else:
|
|
57
73
|
# No None values - use rotation with center
|
|
58
|
-
return CoordinateMixin._rotate_around_center(
|
|
74
|
+
return CoordinateMixin._rotate_around_center(
|
|
75
|
+
coords.astype(float), axis, angle, center
|
|
76
|
+
)
|
|
59
77
|
|
|
60
78
|
@staticmethod
|
|
61
|
-
def _rotate_around_center(
|
|
79
|
+
def _rotate_around_center(
|
|
80
|
+
coords: np.ndarray, axis: str, angle: float, center: np.ndarray = None
|
|
81
|
+
) -> np.ndarray:
|
|
62
82
|
"""
|
|
63
83
|
Rotate coordinates around a specified center point.
|
|
64
|
-
|
|
84
|
+
|
|
65
85
|
:param coords: NumPy array of shape (N, 3) with valid coordinates
|
|
66
86
|
:param axis: Axis to rotate around ('x', 'y', or 'z')
|
|
67
87
|
:param angle: Angle in degrees
|
|
@@ -71,18 +91,22 @@ class CoordinateMixin:
|
|
|
71
91
|
if center is None:
|
|
72
92
|
# Rotate around origin - use existing method
|
|
73
93
|
return CoordinateMixin._rotate_valid_coordinates(coords, axis, angle)
|
|
74
|
-
|
|
94
|
+
|
|
75
95
|
# Translate coordinates to center at origin
|
|
76
96
|
translated_coords = coords - center
|
|
77
|
-
|
|
97
|
+
|
|
78
98
|
# Rotate around origin
|
|
79
|
-
rotated_coords = CoordinateMixin._rotate_valid_coordinates(
|
|
80
|
-
|
|
99
|
+
rotated_coords = CoordinateMixin._rotate_valid_coordinates(
|
|
100
|
+
translated_coords, axis, angle
|
|
101
|
+
)
|
|
102
|
+
|
|
81
103
|
# Translate back to original position
|
|
82
104
|
return rotated_coords + center
|
|
83
105
|
|
|
84
106
|
@staticmethod
|
|
85
|
-
def build_square_array(
|
|
107
|
+
def build_square_array(
|
|
108
|
+
x: float, y: float, x_width: float, y_width: float
|
|
109
|
+
) -> Tuple[np.ndarray, np.ndarray]:
|
|
86
110
|
"""
|
|
87
111
|
Build a NumPy array representing a square or rectangle defined by its bottom-left corner (x, y)
|
|
88
112
|
and its width and height.
|
|
@@ -103,8 +127,7 @@ class CoordinateMixin:
|
|
|
103
127
|
return x_coords, y_coords
|
|
104
128
|
|
|
105
129
|
@staticmethod
|
|
106
|
-
def order_coordinates_clockwise(df: pd.DataFrame, plane=
|
|
107
|
-
|
|
130
|
+
def order_coordinates_clockwise(df: pd.DataFrame, plane="xy") -> pd.DataFrame:
|
|
108
131
|
axis_1 = plane[0]
|
|
109
132
|
axis_2 = plane[1]
|
|
110
133
|
|
|
@@ -113,13 +136,17 @@ class CoordinateMixin:
|
|
|
113
136
|
|
|
114
137
|
angles = np.arctan2(df[axis_2] - cy, df[axis_1] - cx)
|
|
115
138
|
|
|
116
|
-
df[
|
|
117
|
-
df_sorted =
|
|
139
|
+
df["angle"] = angles
|
|
140
|
+
df_sorted = (
|
|
141
|
+
df.sort_values(by="angle").drop(columns="angle").reset_index(drop=True)
|
|
142
|
+
)
|
|
118
143
|
|
|
119
144
|
return df_sorted
|
|
120
145
|
|
|
121
|
-
@staticmethod
|
|
122
|
-
def _rotate_valid_coordinates(
|
|
146
|
+
@staticmethod
|
|
147
|
+
def _rotate_valid_coordinates(
|
|
148
|
+
coords: np.ndarray, axis: str, angle: float
|
|
149
|
+
) -> np.ndarray:
|
|
123
150
|
"""
|
|
124
151
|
Rotate coordinates without None values using rotation matrices.
|
|
125
152
|
"""
|
|
@@ -127,35 +154,31 @@ class CoordinateMixin:
|
|
|
127
154
|
cos_a = np.cos(angle_rad)
|
|
128
155
|
sin_a = np.sin(angle_rad)
|
|
129
156
|
|
|
130
|
-
if axis ==
|
|
131
|
-
R = np.array([[1, 0, 0],
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
elif axis ==
|
|
135
|
-
R = np.array([[cos_a, 0, sin_a],
|
|
136
|
-
[0, 1, 0],
|
|
137
|
-
[-sin_a, 0, cos_a]])
|
|
138
|
-
elif axis == 'z':
|
|
139
|
-
R = np.array([[cos_a, -sin_a, 0],
|
|
140
|
-
[sin_a, cos_a, 0],
|
|
141
|
-
[0, 0, 1]])
|
|
157
|
+
if axis == "x":
|
|
158
|
+
R = np.array([[1, 0, 0], [0, cos_a, -sin_a], [0, sin_a, cos_a]])
|
|
159
|
+
elif axis == "y":
|
|
160
|
+
R = np.array([[cos_a, 0, sin_a], [0, 1, 0], [-sin_a, 0, cos_a]])
|
|
161
|
+
elif axis == "z":
|
|
162
|
+
R = np.array([[cos_a, -sin_a, 0], [sin_a, cos_a, 0], [0, 0, 1]])
|
|
142
163
|
else:
|
|
143
164
|
raise ValueError("Axis must be 'x', 'y', or 'z'.")
|
|
144
165
|
|
|
145
166
|
return coords @ R.T
|
|
146
|
-
|
|
167
|
+
|
|
147
168
|
@staticmethod
|
|
148
169
|
def _calculate_single_area(x: np.ndarray, y: np.ndarray) -> float:
|
|
149
170
|
"""
|
|
150
171
|
Calculate the area of a single closed shape using the shoelace formula.
|
|
151
172
|
"""
|
|
152
173
|
if len(x) < 3 or len(y) < 3:
|
|
153
|
-
raise ValueError(
|
|
154
|
-
|
|
174
|
+
raise ValueError(
|
|
175
|
+
"Trace must contain at least 3 points to form a closed shape."
|
|
176
|
+
)
|
|
177
|
+
|
|
155
178
|
# Convert to float arrays to avoid object dtype issues
|
|
156
179
|
x = np.asarray(x, dtype=float)
|
|
157
180
|
y = np.asarray(y, dtype=float)
|
|
158
|
-
|
|
181
|
+
|
|
159
182
|
# Ensure the shape is closed by appending the first point to the end
|
|
160
183
|
if (x[0], y[0]) != (x[-1], y[-1]):
|
|
161
184
|
x = np.append(x, x[0])
|
|
@@ -165,7 +188,7 @@ class CoordinateMixin:
|
|
|
165
188
|
area = 0.5 * np.abs(np.dot(x[:-1], y[1:]) - np.dot(y[:-1], x[1:]))
|
|
166
189
|
|
|
167
190
|
return float(area)
|
|
168
|
-
|
|
191
|
+
|
|
169
192
|
@staticmethod
|
|
170
193
|
def get_area_from_points(x: np.ndarray, y: np.ndarray) -> float:
|
|
171
194
|
"""
|
|
@@ -175,31 +198,33 @@ class CoordinateMixin:
|
|
|
175
198
|
# Convert to numpy arrays and handle object dtype
|
|
176
199
|
x = np.asarray(x)
|
|
177
200
|
y = np.asarray(y)
|
|
178
|
-
|
|
201
|
+
|
|
179
202
|
# Check if we have None values (multiple shapes)
|
|
180
|
-
x_is_none = pd.isna(x) if hasattr(pd,
|
|
181
|
-
|
|
203
|
+
x_is_none = pd.isna(x) if hasattr(pd, "isna") else (x == None)
|
|
204
|
+
|
|
182
205
|
if np.any(x_is_none):
|
|
183
206
|
total_area = 0.0
|
|
184
|
-
|
|
207
|
+
|
|
185
208
|
# Find None indices to split the shapes
|
|
186
209
|
none_indices = np.where(x_is_none)[0]
|
|
187
210
|
start_idx = 0
|
|
188
|
-
|
|
211
|
+
|
|
189
212
|
# Process each shape segment
|
|
190
213
|
for none_idx in none_indices:
|
|
191
214
|
if none_idx > start_idx:
|
|
192
215
|
# Extract segment coordinates
|
|
193
216
|
segment_x = x[start_idx:none_idx]
|
|
194
217
|
segment_y = y[start_idx:none_idx]
|
|
195
|
-
|
|
218
|
+
|
|
196
219
|
# Calculate area for this segment if it has enough points
|
|
197
220
|
if len(segment_x) >= 3:
|
|
198
|
-
area = CoordinateMixin._calculate_single_area(
|
|
221
|
+
area = CoordinateMixin._calculate_single_area(
|
|
222
|
+
segment_x, segment_y
|
|
223
|
+
)
|
|
199
224
|
total_area += area
|
|
200
|
-
|
|
225
|
+
|
|
201
226
|
start_idx = none_idx + 1
|
|
202
|
-
|
|
227
|
+
|
|
203
228
|
# Handle the last segment if it exists
|
|
204
229
|
if start_idx < len(x):
|
|
205
230
|
segment_x = x[start_idx:]
|
|
@@ -207,25 +232,17 @@ class CoordinateMixin:
|
|
|
207
232
|
if len(segment_x) >= 3:
|
|
208
233
|
area = CoordinateMixin._calculate_single_area(segment_x, segment_y)
|
|
209
234
|
total_area += area
|
|
210
|
-
|
|
235
|
+
|
|
211
236
|
return total_area
|
|
212
|
-
|
|
237
|
+
|
|
213
238
|
else:
|
|
214
239
|
# Single shape - use original logic
|
|
215
240
|
return CoordinateMixin._calculate_single_area(x, y)
|
|
216
241
|
|
|
217
242
|
@staticmethod
|
|
218
243
|
def extrude_footprint(
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
datum: np.ndarray,
|
|
222
|
-
thickness: float
|
|
223
|
-
) -> Tuple[
|
|
224
|
-
np.ndarray,
|
|
225
|
-
np.ndarray,
|
|
226
|
-
np.ndarray,
|
|
227
|
-
np.ndarray
|
|
228
|
-
]:
|
|
244
|
+
x: np.ndarray, y: np.ndarray, datum: np.ndarray, thickness: float
|
|
245
|
+
) -> Tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray]:
|
|
229
246
|
"""
|
|
230
247
|
Extrude the 2D footprint to 3D and label each point with its side ('a' or 'b'), with 'a' being the top side and 'b' the bottom side.
|
|
231
248
|
|
|
@@ -252,31 +269,31 @@ class CoordinateMixin:
|
|
|
252
269
|
x_full = np.concatenate([x, x])
|
|
253
270
|
y_full = np.concatenate([y, y])
|
|
254
271
|
z_full = np.concatenate([np.full_like(x, z_a), np.full_like(x, z_b)])
|
|
255
|
-
side_full = np.array([
|
|
272
|
+
side_full = np.array(["a"] * len(x) + ["b"] * len(x))
|
|
256
273
|
|
|
257
274
|
return x_full, y_full, z_full, side_full
|
|
258
|
-
|
|
275
|
+
|
|
259
276
|
@staticmethod
|
|
260
277
|
def remove_skip_coat_area(
|
|
261
|
-
x_coords: np.ndarray,
|
|
262
|
-
y_coords: np.ndarray,
|
|
263
|
-
weld_tab_positions: np.ndarray,
|
|
264
|
-
skip_coat_width: float
|
|
278
|
+
x_coords: np.ndarray,
|
|
279
|
+
y_coords: np.ndarray,
|
|
280
|
+
weld_tab_positions: np.ndarray,
|
|
281
|
+
skip_coat_width: float,
|
|
265
282
|
) -> Tuple[np.ndarray, np.ndarray]:
|
|
266
283
|
"""
|
|
267
284
|
Remove skip coat areas around weld tab positions from coordinates.
|
|
268
|
-
|
|
285
|
+
|
|
269
286
|
Parameters
|
|
270
287
|
----------
|
|
271
288
|
x_coords : np.ndarray
|
|
272
289
|
Array of x coordinates defining the boundary
|
|
273
|
-
y_coords : np.ndarray
|
|
290
|
+
y_coords : np.ndarray
|
|
274
291
|
Array of y coordinates defining the boundary
|
|
275
292
|
weld_tab_positions : np.ndarray
|
|
276
293
|
Array of x positions where weld tabs are located
|
|
277
294
|
skip_coat_width : float
|
|
278
295
|
Width of the skip coat area around each weld tab
|
|
279
|
-
|
|
296
|
+
|
|
280
297
|
Returns
|
|
281
298
|
-------
|
|
282
299
|
Tuple[np.ndarray, np.ndarray]
|
|
@@ -284,55 +301,61 @@ class CoordinateMixin:
|
|
|
284
301
|
"""
|
|
285
302
|
if len(x_coords) == 0 or len(y_coords) == 0:
|
|
286
303
|
return np.array([], dtype=float), np.array([], dtype=float)
|
|
287
|
-
|
|
304
|
+
|
|
288
305
|
x_min, x_max = np.min(x_coords), np.max(x_coords)
|
|
289
306
|
y_min, y_max = np.min(y_coords), np.max(y_coords)
|
|
290
|
-
|
|
307
|
+
|
|
291
308
|
# Filter weld tab positions to only include those within bounds
|
|
292
309
|
valid_positions = weld_tab_positions[
|
|
293
|
-
(weld_tab_positions + skip_coat_width >= x_min)
|
|
310
|
+
(weld_tab_positions + skip_coat_width >= x_min)
|
|
311
|
+
& (weld_tab_positions - skip_coat_width <= x_max)
|
|
294
312
|
]
|
|
295
|
-
|
|
313
|
+
|
|
296
314
|
# If no valid positions, return original rectangle
|
|
297
315
|
if len(valid_positions) == 0:
|
|
298
316
|
rect_x = [x_min, x_max, x_max, x_min, x_min]
|
|
299
317
|
rect_y = [y_min, y_min, y_max, y_max, y_min]
|
|
300
318
|
return np.array(rect_x, dtype=float), np.array(rect_y, dtype=float)
|
|
301
|
-
|
|
319
|
+
|
|
302
320
|
# Sort weld tab cut positions
|
|
303
321
|
cuts = np.sort(valid_positions)
|
|
304
322
|
half_width = skip_coat_width / 2
|
|
305
|
-
|
|
323
|
+
|
|
306
324
|
# Build kept horizontal segments by removing [cut - half, cut + half] around each cut
|
|
307
325
|
segments = []
|
|
308
326
|
start = x_min
|
|
309
|
-
|
|
327
|
+
|
|
310
328
|
for cut in cuts:
|
|
311
329
|
end = cut - half_width
|
|
312
330
|
if end > start:
|
|
313
331
|
segments.append((start, end))
|
|
314
332
|
start = cut + half_width
|
|
315
|
-
|
|
333
|
+
|
|
316
334
|
# Add final segment if there's remaining space
|
|
317
335
|
if start < x_max:
|
|
318
336
|
segments.append((start, x_max))
|
|
319
|
-
|
|
337
|
+
|
|
320
338
|
# Build rectangles for each kept segment with np.nan separators
|
|
321
339
|
x_result = []
|
|
322
340
|
y_result = []
|
|
323
|
-
|
|
341
|
+
|
|
324
342
|
for i, (segment_start, segment_end) in enumerate(segments):
|
|
325
343
|
# Create rectangle coordinates: bottom-left -> bottom-right -> top-right -> top-left -> close
|
|
326
|
-
rect_x = [
|
|
344
|
+
rect_x = [
|
|
345
|
+
segment_start,
|
|
346
|
+
segment_end,
|
|
347
|
+
segment_end,
|
|
348
|
+
segment_start,
|
|
349
|
+
segment_start,
|
|
350
|
+
]
|
|
327
351
|
rect_y = [y_min, y_min, y_max, y_max, y_min]
|
|
328
|
-
|
|
352
|
+
|
|
329
353
|
x_result.extend(rect_x)
|
|
330
354
|
y_result.extend(rect_y)
|
|
331
|
-
|
|
355
|
+
|
|
332
356
|
# Add np.nan separator (except for the last segment)
|
|
333
357
|
if i < len(segments) - 1: # Fixed: use index comparison
|
|
334
358
|
x_result.append(np.nan)
|
|
335
359
|
y_result.append(np.nan)
|
|
336
|
-
|
|
337
|
-
return np.array(x_result, dtype=float), np.array(y_result, dtype=float)
|
|
338
360
|
|
|
361
|
+
return np.array(x_result, dtype=float), np.array(y_result, dtype=float)
|
steer_core/Mixins/Data.py
CHANGED
|
@@ -7,6 +7,7 @@ class DataMixin:
|
|
|
7
7
|
A mixin class to handle data processing and validation for electrode materials.
|
|
8
8
|
Provides methods to calculate properties, check curve directions, and process half-cell curves.
|
|
9
9
|
"""
|
|
10
|
+
|
|
10
11
|
@staticmethod
|
|
11
12
|
def enforce_monotonicity(array: np.ndarray) -> np.ndarray:
|
|
12
13
|
"""
|
|
@@ -37,4 +38,3 @@ class DataMixin:
|
|
|
37
38
|
new_array = np.minimum.accumulate(new_array)
|
|
38
39
|
|
|
39
40
|
return new_array
|
|
40
|
-
|