steer-core 0.1.24__tar.gz → 0.1.25__tar.gz
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-0.1.25/PKG-INFO +38 -0
- steer_core-0.1.25/pyproject.toml +104 -0
- steer_core-0.1.25/steer_core/Apps/Callbacks/ConfigInteractions.py +377 -0
- steer_core-0.1.25/steer_core/Apps/Callbacks/StyleManagement.py +60 -0
- {steer_core-0.1.24 → steer_core-0.1.25}/steer_core/Data/database.db +0 -0
- {steer_core-0.1.24 → steer_core-0.1.25}/steer_core/DataManager.py +20 -0
- {steer_core-0.1.24 → steer_core-0.1.25}/steer_core/Mixins/Coordinates.py +250 -4
- {steer_core-0.1.24 → steer_core-0.1.25}/steer_core/Mixins/Plotter.py +81 -0
- steer_core-0.1.25/steer_core/__init__.py +1 -0
- steer_core-0.1.25/steer_core.egg-info/PKG-INFO +38 -0
- {steer_core-0.1.24 → steer_core-0.1.25}/steer_core.egg-info/SOURCES.txt +3 -1
- steer_core-0.1.25/steer_core.egg-info/requires.txt +17 -0
- steer_core-0.1.24/PKG-INFO +0 -23
- steer_core-0.1.24/setup.py +0 -33
- steer_core-0.1.24/steer_core/__init__.py +0 -1
- steer_core-0.1.24/steer_core.egg-info/PKG-INFO +0 -23
- steer_core-0.1.24/steer_core.egg-info/requires.txt +0 -5
- {steer_core-0.1.24 → steer_core-0.1.25}/README.md +0 -0
- {steer_core-0.1.24 → steer_core-0.1.25}/setup.cfg +0 -0
- {steer_core-0.1.24 → steer_core-0.1.25}/steer_core/Apps/Components/MaterialSelectors.py +0 -0
- {steer_core-0.1.24 → steer_core-0.1.25}/steer_core/Apps/Components/RangeSliderComponents.py +0 -0
- {steer_core-0.1.24 → steer_core-0.1.25}/steer_core/Apps/Components/SliderComponents.py +0 -0
- {steer_core-0.1.24 → steer_core-0.1.25}/steer_core/Apps/Components/__init__.py +0 -0
- {steer_core-0.1.24 → steer_core-0.1.25}/steer_core/Apps/ContextManagers.py +0 -0
- {steer_core-0.1.24 → steer_core-0.1.25}/steer_core/Apps/Performance/CallbackTimer.py +0 -0
- {steer_core-0.1.24 → steer_core-0.1.25}/steer_core/Apps/Performance/__init__.py +0 -0
- {steer_core-0.1.24 → steer_core-0.1.25}/steer_core/Apps/Utils/SliderControls.py +0 -0
- {steer_core-0.1.24 → steer_core-0.1.25}/steer_core/Apps/Utils/__init__.py +0 -0
- {steer_core-0.1.24 → steer_core-0.1.25}/steer_core/Apps/__init__.py +0 -0
- {steer_core-0.1.24 → steer_core-0.1.25}/steer_core/Constants/Units.py +0 -0
- {steer_core-0.1.24 → steer_core-0.1.25}/steer_core/Constants/Universal.py +0 -0
- {steer_core-0.1.24 → steer_core-0.1.25}/steer_core/Constants/__init__.py +0 -0
- {steer_core-0.1.24 → steer_core-0.1.25}/steer_core/ContextManagers/__init__.py +0 -0
- {steer_core-0.1.24 → steer_core-0.1.25}/steer_core/Data/__init__.py +0 -0
- {steer_core-0.1.24 → steer_core-0.1.25}/steer_core/Decorators/Coordinates.py +0 -0
- {steer_core-0.1.24 → steer_core-0.1.25}/steer_core/Decorators/Electrochemical.py +0 -0
- {steer_core-0.1.24 → steer_core-0.1.25}/steer_core/Decorators/General.py +0 -0
- {steer_core-0.1.24 → steer_core-0.1.25}/steer_core/Decorators/Objects.py +0 -0
- {steer_core-0.1.24 → steer_core-0.1.25}/steer_core/Decorators/__init__.py +0 -0
- {steer_core-0.1.24 → steer_core-0.1.25}/steer_core/Mixins/Colors.py +0 -0
- {steer_core-0.1.24 → steer_core-0.1.25}/steer_core/Mixins/Data.py +0 -0
- {steer_core-0.1.24 → steer_core-0.1.25}/steer_core/Mixins/Dunder.py +0 -0
- {steer_core-0.1.24 → steer_core-0.1.25}/steer_core/Mixins/Serializer.py +0 -0
- {steer_core-0.1.24 → steer_core-0.1.25}/steer_core/Mixins/TypeChecker.py +0 -0
- {steer_core-0.1.24 → steer_core-0.1.25}/steer_core/Mixins/__init__.py +0 -0
- {steer_core-0.1.24 → steer_core-0.1.25}/steer_core.egg-info/dependency_links.txt +0 -0
- {steer_core-0.1.24 → steer_core-0.1.25}/steer_core.egg-info/top_level.txt +0 -0
- {steer_core-0.1.24 → steer_core-0.1.25}/test/test_validation_mixin.py +0 -0
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: steer-core
|
|
3
|
+
Version: 0.1.25
|
|
4
|
+
Summary: Modelling energy storage from cell to site - STEER OpenCell Design
|
|
5
|
+
Author-email: Nicholas Siemons <nsiemons@stanford.edu>
|
|
6
|
+
Maintainer-email: Nicholas Siemons <nsiemons@stanford.edu>
|
|
7
|
+
License: MIT
|
|
8
|
+
Project-URL: Homepage, https://github.com/nicholas9182/steer-core/
|
|
9
|
+
Project-URL: Repository, https://github.com/nicholas9182/steer-core/
|
|
10
|
+
Project-URL: Issues, https://github.com/nicholas9182/steer-core/issues
|
|
11
|
+
Project-URL: Documentation, https://github.com/nicholas9182/steer-core/
|
|
12
|
+
Keywords: energy,storage,battery,modeling,simulation
|
|
13
|
+
Classifier: Development Status :: 4 - Beta
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
19
|
+
Classifier: Operating System :: OS Independent
|
|
20
|
+
Classifier: Topic :: Scientific/Engineering
|
|
21
|
+
Classifier: Topic :: Scientific/Engineering :: Physics
|
|
22
|
+
Requires-Python: >=3.10
|
|
23
|
+
Description-Content-Type: text/markdown
|
|
24
|
+
Requires-Dist: pandas==2.1.4
|
|
25
|
+
Requires-Dist: numpy==1.26.4
|
|
26
|
+
Requires-Dist: datetime==5.5
|
|
27
|
+
Requires-Dist: plotly==6.2.0
|
|
28
|
+
Requires-Dist: scipy==1.15.3
|
|
29
|
+
Provides-Extra: dev
|
|
30
|
+
Requires-Dist: pytest>=7.0; extra == "dev"
|
|
31
|
+
Requires-Dist: pytest-cov; extra == "dev"
|
|
32
|
+
Requires-Dist: black; extra == "dev"
|
|
33
|
+
Requires-Dist: flake8; extra == "dev"
|
|
34
|
+
Requires-Dist: mypy; extra == "dev"
|
|
35
|
+
Requires-Dist: isort; extra == "dev"
|
|
36
|
+
Provides-Extra: test
|
|
37
|
+
Requires-Dist: pytest>=7.0; extra == "test"
|
|
38
|
+
Requires-Dist: pytest-cov; extra == "test"
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61.0", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "steer-core"
|
|
7
|
+
dynamic = ["version"]
|
|
8
|
+
description = "Modelling energy storage from cell to site - STEER OpenCell Design"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.10"
|
|
11
|
+
license = {text = "MIT"}
|
|
12
|
+
authors = [
|
|
13
|
+
{name = "Nicholas Siemons", email = "nsiemons@stanford.edu"},
|
|
14
|
+
]
|
|
15
|
+
maintainers = [
|
|
16
|
+
{name = "Nicholas Siemons", email = "nsiemons@stanford.edu"},
|
|
17
|
+
]
|
|
18
|
+
keywords = ["energy", "storage", "battery", "modeling", "simulation"]
|
|
19
|
+
classifiers = [
|
|
20
|
+
"Development Status :: 4 - Beta",
|
|
21
|
+
"Programming Language :: Python :: 3",
|
|
22
|
+
"Programming Language :: Python :: 3.10",
|
|
23
|
+
"Programming Language :: Python :: 3.11",
|
|
24
|
+
"Programming Language :: Python :: 3.12",
|
|
25
|
+
"License :: OSI Approved :: MIT License",
|
|
26
|
+
"Operating System :: OS Independent",
|
|
27
|
+
"Topic :: Scientific/Engineering",
|
|
28
|
+
"Topic :: Scientific/Engineering :: Physics",
|
|
29
|
+
]
|
|
30
|
+
dependencies = [
|
|
31
|
+
"pandas==2.1.4",
|
|
32
|
+
"numpy==1.26.4",
|
|
33
|
+
"datetime==5.5",
|
|
34
|
+
"plotly==6.2.0",
|
|
35
|
+
"scipy==1.15.3",
|
|
36
|
+
]
|
|
37
|
+
|
|
38
|
+
[project.urls]
|
|
39
|
+
Homepage = "https://github.com/nicholas9182/steer-core/"
|
|
40
|
+
Repository = "https://github.com/nicholas9182/steer-core/"
|
|
41
|
+
Issues = "https://github.com/nicholas9182/steer-core/issues"
|
|
42
|
+
Documentation = "https://github.com/nicholas9182/steer-core/"
|
|
43
|
+
|
|
44
|
+
[project.optional-dependencies]
|
|
45
|
+
dev = [
|
|
46
|
+
"pytest>=7.0",
|
|
47
|
+
"pytest-cov",
|
|
48
|
+
"black",
|
|
49
|
+
"flake8",
|
|
50
|
+
"mypy",
|
|
51
|
+
"isort",
|
|
52
|
+
]
|
|
53
|
+
test = [
|
|
54
|
+
"pytest>=7.0",
|
|
55
|
+
"pytest-cov",
|
|
56
|
+
]
|
|
57
|
+
|
|
58
|
+
[tool.setuptools]
|
|
59
|
+
include-package-data = true
|
|
60
|
+
|
|
61
|
+
[tool.setuptools.packages.find]
|
|
62
|
+
where = ["."]
|
|
63
|
+
include = ["steer_core*"]
|
|
64
|
+
|
|
65
|
+
[tool.setuptools.package-data]
|
|
66
|
+
"steer_core.Data" = ["database.db"]
|
|
67
|
+
|
|
68
|
+
[tool.setuptools.dynamic]
|
|
69
|
+
version = {attr = "steer_core.__version__"}
|
|
70
|
+
|
|
71
|
+
[tool.black]
|
|
72
|
+
line-length = 88
|
|
73
|
+
target-version = ['py310']
|
|
74
|
+
include = '\.pyi?$'
|
|
75
|
+
extend-exclude = '''
|
|
76
|
+
/(
|
|
77
|
+
# directories
|
|
78
|
+
\.eggs
|
|
79
|
+
| \.git
|
|
80
|
+
| \.hg
|
|
81
|
+
| \.mypy_cache
|
|
82
|
+
| \.tox
|
|
83
|
+
| \.venv
|
|
84
|
+
| build
|
|
85
|
+
| dist
|
|
86
|
+
)/
|
|
87
|
+
'''
|
|
88
|
+
|
|
89
|
+
[tool.isort]
|
|
90
|
+
profile = "black"
|
|
91
|
+
multi_line_output = 3
|
|
92
|
+
line_length = 88
|
|
93
|
+
known_first_party = ["steer_core"]
|
|
94
|
+
|
|
95
|
+
[tool.pytest.ini_options]
|
|
96
|
+
minversion = "7.0"
|
|
97
|
+
addopts = "-ra -q --strict-markers"
|
|
98
|
+
testpaths = [
|
|
99
|
+
"tests",
|
|
100
|
+
]
|
|
101
|
+
python_files = [
|
|
102
|
+
"test_*.py",
|
|
103
|
+
"*_test.py",
|
|
104
|
+
]
|
|
@@ -0,0 +1,377 @@
|
|
|
1
|
+
from typing import List, Tuple
|
|
2
|
+
from dash import no_update
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def generate_parameters_and_ranges(object, config) -> Tuple[List[float], List[float], List[float]]:
|
|
6
|
+
"""
|
|
7
|
+
Generate parameter value lists and their corresponding min/max ranges for any object.
|
|
8
|
+
|
|
9
|
+
This function extracts parameter values from an object based on a configuration's
|
|
10
|
+
parameter list, and retrieves the minimum and maximum values for each parameter
|
|
11
|
+
if range attributes exist (e.g., '{param}_range'), otherwise uses the current
|
|
12
|
+
value as both min and max.
|
|
13
|
+
|
|
14
|
+
Parameters
|
|
15
|
+
----------
|
|
16
|
+
object : Any
|
|
17
|
+
The object from which to extract parameter values. Must have attributes
|
|
18
|
+
corresponding to the parameters listed in config.parameter_list.
|
|
19
|
+
config : Type
|
|
20
|
+
Configuration class or object that contains a 'parameter_list' attribute
|
|
21
|
+
specifying which parameters to extract from the object.
|
|
22
|
+
|
|
23
|
+
Returns
|
|
24
|
+
-------
|
|
25
|
+
Tuple[List[float], List[float], List[float]]
|
|
26
|
+
A tuple containing three lists:
|
|
27
|
+
- parameter_values: Current values of each parameter
|
|
28
|
+
- min_values: Minimum values for each parameter (from {param}_range[0]
|
|
29
|
+
or current value if no range exists)
|
|
30
|
+
- max_values: Maximum values for each parameter (from {param}_range[1]
|
|
31
|
+
or current value if no range exists)
|
|
32
|
+
|
|
33
|
+
Examples
|
|
34
|
+
--------
|
|
35
|
+
>>> class MyObject:
|
|
36
|
+
... def __init__(self):
|
|
37
|
+
... self.temperature = 25.0
|
|
38
|
+
... self.temperature_range = (0.0, 100.0)
|
|
39
|
+
... self.pressure = 1.0
|
|
40
|
+
>>>
|
|
41
|
+
>>> class Config:
|
|
42
|
+
... parameter_list = ['temperature', 'pressure']
|
|
43
|
+
>>>
|
|
44
|
+
>>> obj = MyObject()
|
|
45
|
+
>>> values, mins, maxs = generate_parameter_and_list_ranges(obj, Config)
|
|
46
|
+
>>> print(values) # [25.0, 1.0]
|
|
47
|
+
>>> print(mins) # [0.0, 1.0]
|
|
48
|
+
>>> print(maxs) # [100.0, 1.0]
|
|
49
|
+
|
|
50
|
+
Notes
|
|
51
|
+
-----
|
|
52
|
+
- If an object has a '{param}_range' attribute, it should be a tuple/list
|
|
53
|
+
with exactly two elements: (min_value, max_value)
|
|
54
|
+
- If no range attribute exists for a parameter, the current value is used
|
|
55
|
+
for both minimum and maximum values
|
|
56
|
+
- All extracted values are expected to be numeric (convertible to float)
|
|
57
|
+
"""
|
|
58
|
+
parameter_values = []
|
|
59
|
+
min_values = []
|
|
60
|
+
max_values = []
|
|
61
|
+
|
|
62
|
+
parameter_list = config.parameter_list
|
|
63
|
+
|
|
64
|
+
for param in parameter_list:
|
|
65
|
+
|
|
66
|
+
value = getattr(object, param)
|
|
67
|
+
parameter_values.append(value)
|
|
68
|
+
|
|
69
|
+
if hasattr(object, f"{param}_range"):
|
|
70
|
+
min_values.append(getattr(object, f"{param}_range")[0])
|
|
71
|
+
max_values.append(getattr(object, f"{param}_range")[1])
|
|
72
|
+
else:
|
|
73
|
+
min_values.append(value)
|
|
74
|
+
max_values.append(value)
|
|
75
|
+
|
|
76
|
+
return parameter_values, min_values, max_values
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def generate_rangeslider_parameters_and_ranges(object, config) -> Tuple[
|
|
81
|
+
List[float],
|
|
82
|
+
List[float],
|
|
83
|
+
List[float],
|
|
84
|
+
List[float],
|
|
85
|
+
]:
|
|
86
|
+
"""
|
|
87
|
+
Generate range slider parameter values and their corresponding min/max bounds for any object.
|
|
88
|
+
|
|
89
|
+
This function extracts range parameter values from an object based on a configuration's
|
|
90
|
+
range_slider_parameters list. Each parameter is expected to be a tuple/list representing
|
|
91
|
+
a range (start, end). It also retrieves the absolute minimum and maximum bounds for each
|
|
92
|
+
parameter from corresponding '{param}_range' attributes, or uses the current range values
|
|
93
|
+
as bounds if no range attribute exists.
|
|
94
|
+
|
|
95
|
+
Parameters
|
|
96
|
+
----------
|
|
97
|
+
object : Any
|
|
98
|
+
The object from which to extract range parameter values. Must have attributes
|
|
99
|
+
corresponding to the parameters listed in config.range_slider_parameters, where
|
|
100
|
+
each attribute contains a tuple/list of (start, end) values. Optionally has
|
|
101
|
+
'{param}_range' attributes defining the absolute bounds for each parameter.
|
|
102
|
+
config : Any
|
|
103
|
+
Configuration class or object that contains a 'range_slider_parameters' attribute
|
|
104
|
+
specifying which range parameters to extract from the object.
|
|
105
|
+
|
|
106
|
+
Returns
|
|
107
|
+
-------
|
|
108
|
+
Tuple[List[float], List[float], List[float], List[float]]
|
|
109
|
+
A tuple containing four lists:
|
|
110
|
+
- start_values: Start values for each range parameter (from param[0])
|
|
111
|
+
- end_values: End values for each range parameter (from param[1])
|
|
112
|
+
- min_values: Absolute minimum bounds for each parameter (from {param}_range[0]
|
|
113
|
+
or param[0] if no range exists)
|
|
114
|
+
- max_values: Absolute maximum bounds for each parameter (from {param}_range[1]
|
|
115
|
+
or param[1] if no range exists)
|
|
116
|
+
|
|
117
|
+
Examples
|
|
118
|
+
--------
|
|
119
|
+
>>> class MyCollector:
|
|
120
|
+
... def __init__(self):
|
|
121
|
+
... self.voltage_window = (2.5, 4.2) # Current range setting
|
|
122
|
+
... self.voltage_window_range = (0.0, 5.0) # Absolute bounds
|
|
123
|
+
... self.current_limit = (0.1, 2.0) # Current range setting
|
|
124
|
+
... # No current_limit_range, so uses current values as bounds
|
|
125
|
+
>>>
|
|
126
|
+
>>> class Config:
|
|
127
|
+
... range_slider_parameters = ['voltage_window', 'current_limit']
|
|
128
|
+
>>>
|
|
129
|
+
>>> collector = MyCollector()
|
|
130
|
+
>>> starts, ends, mins, maxs = generate_rangeslider_parameters_and_ranges_from_config(
|
|
131
|
+
... collector, Config)
|
|
132
|
+
>>> print(starts) # [2.5, 0.1]
|
|
133
|
+
>>> print(ends) # [4.2, 2.0]
|
|
134
|
+
>>> print(mins) # [0.0, 0.1] # Uses range for voltage, current value for current
|
|
135
|
+
>>> print(maxs) # [5.0, 2.0] # Uses range for voltage, current value for current
|
|
136
|
+
|
|
137
|
+
Notes
|
|
138
|
+
-----
|
|
139
|
+
- If config.range_slider_parameters is empty, returns four empty lists
|
|
140
|
+
- Each parameter in range_slider_parameters must exist as an attribute on the object
|
|
141
|
+
and contain exactly two elements: (start_value, end_value)
|
|
142
|
+
- If a parameter has a corresponding '{param}_range' attribute, those bounds are used
|
|
143
|
+
- If no range attribute exists for a parameter, the current start/end values are used
|
|
144
|
+
as the minimum/maximum bounds respectively
|
|
145
|
+
- All values are expected to be numeric (convertible to float)
|
|
146
|
+
- This function is typically used for range slider UI components where
|
|
147
|
+
users can select a range within predefined bounds
|
|
148
|
+
|
|
149
|
+
Raises
|
|
150
|
+
------
|
|
151
|
+
AttributeError
|
|
152
|
+
If the object doesn't have the required parameter attributes or if config
|
|
153
|
+
doesn't have range_slider_parameters attribute
|
|
154
|
+
IndexError
|
|
155
|
+
If parameter values don't contain exactly two elements
|
|
156
|
+
"""
|
|
157
|
+
start_values = []
|
|
158
|
+
end_values = []
|
|
159
|
+
min_values = []
|
|
160
|
+
max_values = []
|
|
161
|
+
|
|
162
|
+
parameter_list = config.range_slider_parameters
|
|
163
|
+
|
|
164
|
+
for param in parameter_list:
|
|
165
|
+
|
|
166
|
+
value = getattr(object, param)
|
|
167
|
+
start_values.append(value[0])
|
|
168
|
+
end_values.append(value[1])
|
|
169
|
+
|
|
170
|
+
if hasattr(object, f"{param}_range"):
|
|
171
|
+
min_values.append(getattr(object, f"{param}_range")[0])
|
|
172
|
+
max_values.append(getattr(object, f"{param}_range")[1])
|
|
173
|
+
else:
|
|
174
|
+
min_values.append(value[0])
|
|
175
|
+
max_values.append(value[1])
|
|
176
|
+
|
|
177
|
+
return start_values, end_values, min_values, max_values
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def validate_dependent_properties(object, config) -> None:
|
|
181
|
+
"""
|
|
182
|
+
Validate and clamp dependent properties to their valid hard ranges for any object.
|
|
183
|
+
|
|
184
|
+
This function validates parameter values from an object based on a configuration's
|
|
185
|
+
parameter list, checking each parameter against its corresponding '{param}_hard_range'
|
|
186
|
+
attribute. If a parameter value falls outside its hard range, it is automatically
|
|
187
|
+
clamped to the nearest valid boundary (minimum or maximum).
|
|
188
|
+
|
|
189
|
+
Parameters
|
|
190
|
+
----------
|
|
191
|
+
object : Any
|
|
192
|
+
The object whose parameter values will be validated and potentially modified.
|
|
193
|
+
Must have attributes corresponding to the parameters listed in config.parameter_list.
|
|
194
|
+
Should have '{param}_hard_range' attributes defining the valid bounds for validation.
|
|
195
|
+
config : Any
|
|
196
|
+
Configuration class or object that contains a 'parameter_list' attribute
|
|
197
|
+
specifying which parameters to validate on the object.
|
|
198
|
+
|
|
199
|
+
Returns
|
|
200
|
+
-------
|
|
201
|
+
None
|
|
202
|
+
This function modifies the object in-place and does not return any values.
|
|
203
|
+
|
|
204
|
+
Examples
|
|
205
|
+
--------
|
|
206
|
+
>>> class MyObject:
|
|
207
|
+
... def __init__(self):
|
|
208
|
+
... self.temperature = 150.0 # Outside valid range
|
|
209
|
+
... self.temperature_hard_range = (0.0, 100.0)
|
|
210
|
+
... self.pressure = 0.5 # Within valid range
|
|
211
|
+
... self.pressure_hard_range = (0.0, 10.0)
|
|
212
|
+
>>>
|
|
213
|
+
>>> class Config:
|
|
214
|
+
... parameter_list = ['temperature', 'pressure']
|
|
215
|
+
>>>
|
|
216
|
+
>>> obj = MyObject()
|
|
217
|
+
>>> print(obj.temperature) # 150.0 (before validation)
|
|
218
|
+
>>> validate_dependent_properties(obj, Config)
|
|
219
|
+
>>> print(obj.temperature) # 100.0 (clamped to maximum)
|
|
220
|
+
>>> print(obj.pressure) # 0.5 (unchanged, within range)
|
|
221
|
+
|
|
222
|
+
Notes
|
|
223
|
+
-----
|
|
224
|
+
- Only parameters with corresponding '{param}_hard_range' attributes are validated
|
|
225
|
+
- Parameters without hard range attributes are silently skipped (no validation performed)
|
|
226
|
+
- Hard range attributes should be tuples/lists with exactly two elements: (min_value, max_value)
|
|
227
|
+
- Values are clamped to the nearest boundary if they fall outside the valid range
|
|
228
|
+
- This function modifies the object in-place, changing parameter values as needed
|
|
229
|
+
- Typically used after parameter updates to ensure all values remain within valid bounds
|
|
230
|
+
|
|
231
|
+
Raises
|
|
232
|
+
------
|
|
233
|
+
AttributeError
|
|
234
|
+
If the object doesn't have a parameter attribute listed in config.parameter_list,
|
|
235
|
+
or if config doesn't have a parameter_list attribute
|
|
236
|
+
IndexError
|
|
237
|
+
If a hard_range attribute doesn't contain exactly two elements
|
|
238
|
+
"""
|
|
239
|
+
parameter_list = config.parameter_list
|
|
240
|
+
|
|
241
|
+
for param in parameter_list:
|
|
242
|
+
try:
|
|
243
|
+
# Get the hard range and current value for this parameter
|
|
244
|
+
param_range = getattr(object, f"{param}_hard_range")
|
|
245
|
+
param_value = getattr(object, param)
|
|
246
|
+
|
|
247
|
+
# Clamp value to valid range if necessary
|
|
248
|
+
if param_value < param_range[0]:
|
|
249
|
+
setattr(object, param, param_range[0])
|
|
250
|
+
elif param_value > param_range[1]:
|
|
251
|
+
setattr(object, param, param_range[1])
|
|
252
|
+
|
|
253
|
+
except AttributeError:
|
|
254
|
+
# Parameter or hard range doesn't exist, skip validation
|
|
255
|
+
continue
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
def create_no_update_response(
|
|
259
|
+
config=None,
|
|
260
|
+
existing_warnings: List[str] = [],
|
|
261
|
+
n: int = None,
|
|
262
|
+
n_rangeslider: int = None,
|
|
263
|
+
) -> Tuple:
|
|
264
|
+
"""
|
|
265
|
+
Create a no-update response tuple for Dash callbacks based on configuration parameters.
|
|
266
|
+
|
|
267
|
+
This function generates a standardized response tuple containing `no_update` values
|
|
268
|
+
for all UI components defined in a configuration object. This is typically used in
|
|
269
|
+
Dash callbacks when no updates should be made to the UI components, preserving
|
|
270
|
+
their current state while potentially updating warning messages.
|
|
271
|
+
|
|
272
|
+
Parameters
|
|
273
|
+
----------
|
|
274
|
+
config : Any, optional
|
|
275
|
+
Configuration object that defines the UI components and their parameters.
|
|
276
|
+
Should contain attributes like 'parameter_list', 'range_slider_parameters',
|
|
277
|
+
'dropdown_menu', 'radioitem_parameters', and 'text_parameters'.
|
|
278
|
+
If None, only basic response structure is created.
|
|
279
|
+
existing_warnings : List[str], default []
|
|
280
|
+
List of existing warning messages to include in the response.
|
|
281
|
+
These warnings will be preserved in the callback output.
|
|
282
|
+
n : int, optional
|
|
283
|
+
Number of regular slider/input parameters. If None, determined from
|
|
284
|
+
config.parameter_list length. Used to create the correct number of
|
|
285
|
+
no_update responses for sliders and inputs.
|
|
286
|
+
n_rangeslider : int, optional
|
|
287
|
+
Number of range slider parameters. If None, determined from
|
|
288
|
+
config.range_slider_parameters length. Used to create the correct
|
|
289
|
+
number of no_update responses for range sliders.
|
|
290
|
+
|
|
291
|
+
Returns
|
|
292
|
+
-------
|
|
293
|
+
Tuple
|
|
294
|
+
A tuple containing warning messages followed by no_update responses for all
|
|
295
|
+
UI components defined in the configuration:
|
|
296
|
+
- warnings: List[str] - Warning messages (first element)
|
|
297
|
+
- cache_key: no_update - Cache key for callback optimization
|
|
298
|
+
- slider_values: List[no_update] - Current slider values
|
|
299
|
+
- slider_mins: List[no_update] - Slider minimum values
|
|
300
|
+
- slider_maxs: List[no_update] - Slider maximum values
|
|
301
|
+
- slider_marks: List[no_update] - Slider tick marks
|
|
302
|
+
- slider_steps: List[no_update] - Slider step sizes
|
|
303
|
+
- input_steps: List[no_update] - Input field step sizes
|
|
304
|
+
|
|
305
|
+
Additional elements based on config attributes:
|
|
306
|
+
- dropdown_value: no_update (if config.dropdown_menu exists)
|
|
307
|
+
- range_slider_*: List[no_update] (if config.range_slider_parameters exists)
|
|
308
|
+
- radioitem_values: List[no_update] (if config.radioitem_parameters exists)
|
|
309
|
+
- text_values: List[no_update] (if config.text_parameters exists)
|
|
310
|
+
|
|
311
|
+
Examples
|
|
312
|
+
--------
|
|
313
|
+
>>> class Config:
|
|
314
|
+
... parameter_list = ['temperature', 'pressure']
|
|
315
|
+
... dropdown_menu = True
|
|
316
|
+
... range_slider_parameters = ['voltage_window']
|
|
317
|
+
... radioitem_parameters = ['mode']
|
|
318
|
+
... text_parameters = ['label']
|
|
319
|
+
>>>
|
|
320
|
+
>>> config = Config()
|
|
321
|
+
>>> warnings = ['Temperature out of range']
|
|
322
|
+
>>> response = create_no_update_response(config, warnings)
|
|
323
|
+
>>> print(len(response)) # Number of response elements
|
|
324
|
+
>>> print(response[0]) # ['Temperature out of range']
|
|
325
|
+
>>> print(response[1]) # no_update (cache_key)
|
|
326
|
+
|
|
327
|
+
Notes
|
|
328
|
+
-----
|
|
329
|
+
- This function is primarily used in Dash callback error handling or when
|
|
330
|
+
callback conditions are not met and no UI updates should occur
|
|
331
|
+
- The response tuple structure must match the callback's Output specification
|
|
332
|
+
- Optional UI components (dropdown, range sliders, radio items, text inputs)
|
|
333
|
+
are only included in the response if they exist in the configuration
|
|
334
|
+
- All UI component responses use Dash's `no_update` to preserve current state
|
|
335
|
+
- The function automatically determines response tuple length based on config
|
|
336
|
+
|
|
337
|
+
Raises
|
|
338
|
+
------
|
|
339
|
+
AttributeError
|
|
340
|
+
If config object doesn't have expected attributes when accessed
|
|
341
|
+
"""
|
|
342
|
+
n = len(config.parameter_list) if n is None else n
|
|
343
|
+
|
|
344
|
+
response = (
|
|
345
|
+
no_update, # cache_key
|
|
346
|
+
[no_update] * n, # slider values
|
|
347
|
+
[no_update] * n, # slider mins
|
|
348
|
+
[no_update] * n, # slider maxs
|
|
349
|
+
[no_update] * n, # slider marks
|
|
350
|
+
[no_update] * n, # slider steps
|
|
351
|
+
[no_update] * n, # input steps
|
|
352
|
+
)
|
|
353
|
+
|
|
354
|
+
if hasattr(config, "dropdown_menu") and config.dropdown_menu:
|
|
355
|
+
response += (no_update,)
|
|
356
|
+
|
|
357
|
+
if hasattr(config, "range_slider_parameters") and config.range_slider_parameters:
|
|
358
|
+
n_rangeslider = len(config.range_slider_parameters) if n_rangeslider is None else n_rangeslider
|
|
359
|
+
response += (
|
|
360
|
+
[no_update] * n_rangeslider, # range_slider_values
|
|
361
|
+
[no_update] * n_rangeslider, # range slider mins
|
|
362
|
+
[no_update] * n_rangeslider, # range slider maxs
|
|
363
|
+
[no_update] * n_rangeslider, # range slider marks
|
|
364
|
+
[no_update] * n_rangeslider, # range slider steps
|
|
365
|
+
[no_update] * n_rangeslider, # range slider start values
|
|
366
|
+
[no_update] * n_rangeslider, # range slider end values
|
|
367
|
+
)
|
|
368
|
+
|
|
369
|
+
if hasattr(config, "radioitem_parameters") and config.radioitem_parameters:
|
|
370
|
+
num_radioitem_params = len(config.radioitem_parameters)
|
|
371
|
+
response += ([no_update] * num_radioitem_params,) # radioitem values
|
|
372
|
+
|
|
373
|
+
if hasattr(config, "text_parameters") and config.text_parameters:
|
|
374
|
+
num_text_params = len(config.text_parameters)
|
|
375
|
+
response += ([no_update] * num_text_params,) # text item values
|
|
376
|
+
|
|
377
|
+
return (existing_warnings,) + tuple(response)
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
from dash import no_update
|
|
2
|
+
from typing import List
|
|
3
|
+
|
|
4
|
+
def update_style_display(current_style, target_display):
|
|
5
|
+
"""
|
|
6
|
+
Helper function to update style display property with safe None handling.
|
|
7
|
+
|
|
8
|
+
Args:
|
|
9
|
+
current_style: Current style dict or None
|
|
10
|
+
target_display: Target display value ("block" or "none")
|
|
11
|
+
|
|
12
|
+
Returns:
|
|
13
|
+
no_update if style already has target display, otherwise updated style dict
|
|
14
|
+
"""
|
|
15
|
+
current_display = (current_style or {}).get("display")
|
|
16
|
+
if current_display == target_display:
|
|
17
|
+
return no_update
|
|
18
|
+
return {**(current_style or {}), "display": target_display}
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def update_tab_styles(active_tab: str, tab_names: List[str], current_styles: List[dict]) -> List:
|
|
23
|
+
"""
|
|
24
|
+
Helper function to update tab styles based on the active tab.
|
|
25
|
+
Only updates styles when the display property needs to change.
|
|
26
|
+
|
|
27
|
+
Parameters
|
|
28
|
+
----------
|
|
29
|
+
active_tab : str
|
|
30
|
+
The currently active tab name
|
|
31
|
+
tab_names : List[str]
|
|
32
|
+
List of all tab names to check against
|
|
33
|
+
current_styles : List[dict]
|
|
34
|
+
List of current style dictionaries for each tab
|
|
35
|
+
|
|
36
|
+
Returns
|
|
37
|
+
-------
|
|
38
|
+
List
|
|
39
|
+
List of updated styles or no_update for each tab
|
|
40
|
+
"""
|
|
41
|
+
updated_styles = []
|
|
42
|
+
|
|
43
|
+
for tab_name, current_style in zip(tab_names, current_styles):
|
|
44
|
+
# Determine what the display should be
|
|
45
|
+
should_display = "block" if active_tab == tab_name else "none"
|
|
46
|
+
|
|
47
|
+
# Get current display value (handle None style case)
|
|
48
|
+
current_display = (current_style or {}).get("display")
|
|
49
|
+
|
|
50
|
+
# If display is already correct, return no_update
|
|
51
|
+
if current_display == should_display:
|
|
52
|
+
updated_styles.append(no_update)
|
|
53
|
+
else:
|
|
54
|
+
# Create new style preserving existing properties
|
|
55
|
+
new_style = {**(current_style or {}), "display": should_display}
|
|
56
|
+
updated_styles.append(new_style)
|
|
57
|
+
|
|
58
|
+
return updated_styles
|
|
59
|
+
|
|
60
|
+
|
|
Binary file
|
|
@@ -259,6 +259,26 @@ class DataManager:
|
|
|
259
259
|
)
|
|
260
260
|
|
|
261
261
|
return data
|
|
262
|
+
|
|
263
|
+
def get_tape_materials(self, most_recent: bool = True) -> pd.DataFrame:
|
|
264
|
+
"""
|
|
265
|
+
Retrieves tape materials from the database.
|
|
266
|
+
|
|
267
|
+
:param most_recent: If True, returns only the most recent entry.
|
|
268
|
+
:return: DataFrame with tape materials.
|
|
269
|
+
"""
|
|
270
|
+
data = (
|
|
271
|
+
self.get_data(table_name="tape_materials")
|
|
272
|
+
.groupby("name", group_keys=False)
|
|
273
|
+
.apply(
|
|
274
|
+
lambda x: x.sort_values("date", ascending=False).head(1)
|
|
275
|
+
if most_recent
|
|
276
|
+
else x
|
|
277
|
+
)
|
|
278
|
+
.reset_index(drop=True)
|
|
279
|
+
)
|
|
280
|
+
|
|
281
|
+
return data
|
|
262
282
|
|
|
263
283
|
@staticmethod
|
|
264
284
|
def read_half_cell_curve(half_cell_path) -> pd.DataFrame:
|
|
@@ -2,7 +2,7 @@ import numpy as np
|
|
|
2
2
|
import pandas as pd
|
|
3
3
|
from typing import Tuple
|
|
4
4
|
|
|
5
|
-
from shapely import Polygon
|
|
5
|
+
from shapely import Polygon, minimum_bounding_circle, Point
|
|
6
6
|
|
|
7
7
|
|
|
8
8
|
class CoordinateMixin:
|
|
@@ -10,6 +10,31 @@ class CoordinateMixin:
|
|
|
10
10
|
A class to manage and manipulate 3D coordinates.
|
|
11
11
|
Provides methods for rotation, area calculation, and coordinate ordering.
|
|
12
12
|
"""
|
|
13
|
+
@staticmethod
|
|
14
|
+
def get_radius_of_points(coords: np.ndarray) -> float:
|
|
15
|
+
"""Calculate the radius of a spiral given its coordinates.
|
|
16
|
+
|
|
17
|
+
Parameters
|
|
18
|
+
----------
|
|
19
|
+
coords : np.ndarray
|
|
20
|
+
Array of shape (N, 2) with columns [x, z]
|
|
21
|
+
|
|
22
|
+
Returns
|
|
23
|
+
-------
|
|
24
|
+
float
|
|
25
|
+
Radius of the spiral in meters
|
|
26
|
+
|
|
27
|
+
Raises
|
|
28
|
+
------
|
|
29
|
+
ValueError
|
|
30
|
+
If input coordinates are invalid
|
|
31
|
+
"""
|
|
32
|
+
polygon = Polygon(coords)
|
|
33
|
+
circle = minimum_bounding_circle(polygon)
|
|
34
|
+
center = circle.centroid
|
|
35
|
+
first_point = list(circle.exterior.coords)[0]
|
|
36
|
+
radius = Point(center).distance(Point(first_point))
|
|
37
|
+
return radius, (center.x, center.y)
|
|
13
38
|
|
|
14
39
|
@staticmethod
|
|
15
40
|
def _calculate_segment_center_line(x_coords: np.ndarray, z_coords: np.ndarray) -> np.ndarray:
|
|
@@ -52,7 +77,11 @@ class CoordinateMixin:
|
|
|
52
77
|
np.ndarray
|
|
53
78
|
For single polygon: Array with shape (2, 2) containing start and end points.
|
|
54
79
|
For multiple segments: Array with center lines for each segment separated by [NaN, NaN].
|
|
80
|
+
For empty coordinates: Empty array with shape (0, 2).
|
|
55
81
|
"""
|
|
82
|
+
if coordinates.size == 0:
|
|
83
|
+
return np.array([]).reshape(0, 2)
|
|
84
|
+
|
|
56
85
|
x_coords = coordinates[:, 0]
|
|
57
86
|
z_coords = coordinates[:, 2]
|
|
58
87
|
|
|
@@ -218,6 +247,7 @@ class CoordinateMixin:
|
|
|
218
247
|
|
|
219
248
|
@staticmethod
|
|
220
249
|
def order_coordinates_clockwise(df: pd.DataFrame, plane="xy") -> pd.DataFrame:
|
|
250
|
+
|
|
221
251
|
axis_1 = plane[0]
|
|
222
252
|
axis_2 = plane[1]
|
|
223
253
|
|
|
@@ -227,12 +257,230 @@ class CoordinateMixin:
|
|
|
227
257
|
angles = np.arctan2(df[axis_2] - cy, df[axis_1] - cx)
|
|
228
258
|
|
|
229
259
|
df["angle"] = angles
|
|
260
|
+
|
|
230
261
|
df_sorted = (
|
|
231
262
|
df.sort_values(by="angle").drop(columns="angle").reset_index(drop=True)
|
|
232
263
|
)
|
|
233
264
|
|
|
234
265
|
return df_sorted
|
|
235
266
|
|
|
267
|
+
@staticmethod
|
|
268
|
+
def order_coordinates_clockwise_numpy(
|
|
269
|
+
coords: np.ndarray,
|
|
270
|
+
plane: str = "xy"
|
|
271
|
+
) -> np.ndarray:
|
|
272
|
+
"""
|
|
273
|
+
Order 3D coordinates in clockwise direction based on a specified plane.
|
|
274
|
+
Handles multiple coordinate blocks separated by NaN rows.
|
|
275
|
+
|
|
276
|
+
Parameters
|
|
277
|
+
----------
|
|
278
|
+
coords : np.ndarray
|
|
279
|
+
Array of 3D coordinates with shape (N, 3) where columns are [x, y, z].
|
|
280
|
+
NaN rows indicate separations between coordinate blocks.
|
|
281
|
+
plane : str, optional
|
|
282
|
+
Plane to use for ordering ('xy', 'xz', 'yz'), by default 'xy'
|
|
283
|
+
|
|
284
|
+
Returns
|
|
285
|
+
-------
|
|
286
|
+
np.ndarray
|
|
287
|
+
Sorted coordinates array with same shape as input, with each block
|
|
288
|
+
sorted clockwise and separated by NaN rows
|
|
289
|
+
|
|
290
|
+
Raises
|
|
291
|
+
------
|
|
292
|
+
ValueError
|
|
293
|
+
If coords array doesn't have shape (N, 3) or plane is invalid
|
|
294
|
+
"""
|
|
295
|
+
if len(coords.shape) != 2 or coords.shape[1] != 3:
|
|
296
|
+
raise ValueError("coords must be a 2D array with 3 columns (x, y, z)")
|
|
297
|
+
|
|
298
|
+
if coords.shape[0] < 2:
|
|
299
|
+
return coords.copy()
|
|
300
|
+
|
|
301
|
+
# Map plane string to column indices
|
|
302
|
+
plane_mapping = {
|
|
303
|
+
'xy': (0, 1), # x, y columns
|
|
304
|
+
'xz': (0, 2), # x, z columns
|
|
305
|
+
'yz': (1, 2) # y, z columns
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
if plane not in plane_mapping:
|
|
309
|
+
raise ValueError(f"plane must be one of {list(plane_mapping.keys())}, got '{plane}'")
|
|
310
|
+
|
|
311
|
+
# Check if we have NaN rows (multiple coordinate blocks)
|
|
312
|
+
x_coords = coords[:, 0]
|
|
313
|
+
x_is_nan = np.isnan(x_coords)
|
|
314
|
+
|
|
315
|
+
if not np.any(x_is_nan):
|
|
316
|
+
# Single block - use original logic
|
|
317
|
+
return CoordinateMixin._sort_single_coordinate_block(coords, plane)
|
|
318
|
+
|
|
319
|
+
# Multiple blocks - extract and sort each block
|
|
320
|
+
segments = CoordinateMixin._extract_coordinate_blocks(coords)
|
|
321
|
+
sorted_blocks = []
|
|
322
|
+
|
|
323
|
+
for block in segments:
|
|
324
|
+
if len(block) > 1: # Only sort if block has more than 1 coordinate
|
|
325
|
+
sorted_block = CoordinateMixin._sort_single_coordinate_block(block, plane)
|
|
326
|
+
sorted_blocks.append(sorted_block)
|
|
327
|
+
elif len(block) == 1: # Single coordinate, keep as is
|
|
328
|
+
sorted_blocks.append(block)
|
|
329
|
+
|
|
330
|
+
return CoordinateMixin._concatenate_coordinate_blocks_with_nans(sorted_blocks)
|
|
331
|
+
|
|
332
|
+
@staticmethod
|
|
333
|
+
def concat_with_nan_separators(arrays: list) -> np.ndarray:
|
|
334
|
+
"""
|
|
335
|
+
Efficiently concatenate numpy arrays with NaN separators.
|
|
336
|
+
|
|
337
|
+
Parameters
|
|
338
|
+
----------
|
|
339
|
+
arrays : list
|
|
340
|
+
List of numpy arrays to concatenate
|
|
341
|
+
|
|
342
|
+
Returns
|
|
343
|
+
-------
|
|
344
|
+
np.ndarray
|
|
345
|
+
Concatenated array with NaN separators
|
|
346
|
+
"""
|
|
347
|
+
if not arrays:
|
|
348
|
+
return np.array([])
|
|
349
|
+
|
|
350
|
+
if len(arrays) == 1:
|
|
351
|
+
return arrays[0]
|
|
352
|
+
|
|
353
|
+
# Calculate total size needed
|
|
354
|
+
total_rows = sum(arr.shape[0] for arr in arrays) + len(arrays) - 1
|
|
355
|
+
n_cols = arrays[0].shape[1]
|
|
356
|
+
|
|
357
|
+
# Pre-allocate result array
|
|
358
|
+
result = np.empty((total_rows, n_cols))
|
|
359
|
+
|
|
360
|
+
current_row = 0
|
|
361
|
+
for i, arr in enumerate(arrays):
|
|
362
|
+
# Copy array data
|
|
363
|
+
result[current_row:current_row + arr.shape[0]] = arr
|
|
364
|
+
current_row += arr.shape[0]
|
|
365
|
+
|
|
366
|
+
# Add NaN separator (except after last array)
|
|
367
|
+
if i < len(arrays) - 1:
|
|
368
|
+
result[current_row] = np.nan
|
|
369
|
+
current_row += 1
|
|
370
|
+
|
|
371
|
+
return result
|
|
372
|
+
|
|
373
|
+
@staticmethod
|
|
374
|
+
def _sort_single_coordinate_block(
|
|
375
|
+
coords: np.ndarray,
|
|
376
|
+
plane: str
|
|
377
|
+
) -> np.ndarray:
|
|
378
|
+
"""
|
|
379
|
+
Sort a single coordinate block clockwise.
|
|
380
|
+
|
|
381
|
+
Parameters
|
|
382
|
+
----------
|
|
383
|
+
coords : np.ndarray
|
|
384
|
+
Array of 3D coordinates with shape (N, 3)
|
|
385
|
+
plane : str
|
|
386
|
+
Plane to use for ordering ('xy', 'xz', 'yz')
|
|
387
|
+
|
|
388
|
+
Returns
|
|
389
|
+
-------
|
|
390
|
+
np.ndarray
|
|
391
|
+
Sorted coordinates array
|
|
392
|
+
"""
|
|
393
|
+
plane_mapping = {
|
|
394
|
+
'xy': (0, 1), # x, y columns
|
|
395
|
+
'xz': (0, 2), # x, z columns
|
|
396
|
+
'yz': (1, 2) # y, z columns
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
axis_1_idx, axis_2_idx = plane_mapping[plane]
|
|
400
|
+
|
|
401
|
+
# Extract the relevant coordinates for the specified plane
|
|
402
|
+
axis_1_coords = coords[:, axis_1_idx]
|
|
403
|
+
axis_2_coords = coords[:, axis_2_idx]
|
|
404
|
+
|
|
405
|
+
# Calculate center point
|
|
406
|
+
cx = np.nanmean(axis_1_coords)
|
|
407
|
+
cy = np.nanmean(axis_2_coords)
|
|
408
|
+
|
|
409
|
+
# Calculate angles from center to each point
|
|
410
|
+
angles = np.arctan2(axis_2_coords - cy, axis_1_coords - cx)
|
|
411
|
+
|
|
412
|
+
# Sort by angle to get clockwise ordering
|
|
413
|
+
sorted_indices = np.argsort(angles)
|
|
414
|
+
|
|
415
|
+
return coords[sorted_indices]
|
|
416
|
+
|
|
417
|
+
@staticmethod
|
|
418
|
+
def _extract_coordinate_blocks(coords: np.ndarray) -> list:
|
|
419
|
+
"""
|
|
420
|
+
Extract coordinate blocks separated by NaN rows.
|
|
421
|
+
|
|
422
|
+
Parameters
|
|
423
|
+
----------
|
|
424
|
+
coords : np.ndarray
|
|
425
|
+
Array of 3D coordinates with NaN row separators
|
|
426
|
+
|
|
427
|
+
Returns
|
|
428
|
+
-------
|
|
429
|
+
list
|
|
430
|
+
List of coordinate block arrays
|
|
431
|
+
"""
|
|
432
|
+
blocks = []
|
|
433
|
+
x_coords = coords[:, 0]
|
|
434
|
+
x_is_nan = np.isnan(x_coords)
|
|
435
|
+
nan_indices = np.where(x_is_nan)[0]
|
|
436
|
+
start_idx = 0
|
|
437
|
+
|
|
438
|
+
# Process each block between NaN rows
|
|
439
|
+
for nan_idx in nan_indices:
|
|
440
|
+
if nan_idx > start_idx:
|
|
441
|
+
block = coords[start_idx:nan_idx]
|
|
442
|
+
if len(block) > 0:
|
|
443
|
+
blocks.append(block)
|
|
444
|
+
start_idx = nan_idx + 1
|
|
445
|
+
|
|
446
|
+
# Handle the last block if it exists
|
|
447
|
+
if start_idx < len(coords):
|
|
448
|
+
block = coords[start_idx:]
|
|
449
|
+
if len(block) > 0:
|
|
450
|
+
blocks.append(block)
|
|
451
|
+
|
|
452
|
+
return blocks
|
|
453
|
+
|
|
454
|
+
@staticmethod
|
|
455
|
+
def _concatenate_coordinate_blocks_with_nans(blocks: list) -> np.ndarray:
|
|
456
|
+
"""
|
|
457
|
+
Concatenate coordinate blocks with NaN row separators.
|
|
458
|
+
|
|
459
|
+
Parameters
|
|
460
|
+
----------
|
|
461
|
+
blocks : list
|
|
462
|
+
List of coordinate block arrays
|
|
463
|
+
|
|
464
|
+
Returns
|
|
465
|
+
-------
|
|
466
|
+
np.ndarray
|
|
467
|
+
Concatenated array with NaN separators
|
|
468
|
+
"""
|
|
469
|
+
if not blocks:
|
|
470
|
+
return np.array([]).reshape(0, 3)
|
|
471
|
+
|
|
472
|
+
result_parts = []
|
|
473
|
+
|
|
474
|
+
for i, block in enumerate(blocks):
|
|
475
|
+
result_parts.append(block)
|
|
476
|
+
|
|
477
|
+
# Add NaN separator between blocks (except for the last one)
|
|
478
|
+
if i < len(blocks) - 1:
|
|
479
|
+
nan_row = np.full((1, 3), np.nan)
|
|
480
|
+
result_parts.append(nan_row)
|
|
481
|
+
|
|
482
|
+
return np.vstack(result_parts)
|
|
483
|
+
|
|
236
484
|
@staticmethod
|
|
237
485
|
def _rotate_valid_coordinates(
|
|
238
486
|
coords: np.ndarray, axis: str, angle: float
|
|
@@ -261,9 +509,7 @@ class CoordinateMixin:
|
|
|
261
509
|
Calculate the area of a single closed shape using the shoelace formula.
|
|
262
510
|
"""
|
|
263
511
|
if len(x) < 3 or len(y) < 3:
|
|
264
|
-
|
|
265
|
-
"Trace must contain at least 3 points to form a closed shape."
|
|
266
|
-
)
|
|
512
|
+
return 0.0
|
|
267
513
|
|
|
268
514
|
# Convert to float arrays to avoid object dtype issues
|
|
269
515
|
x = np.asarray(x, dtype=float)
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import plotly.graph_objects as go
|
|
2
2
|
from typing import Dict, Any, Tuple, List, Union
|
|
3
|
+
from steer_core.Mixins.Coordinates import CoordinateMixin
|
|
3
4
|
|
|
4
5
|
|
|
5
6
|
class PlotterMixin:
|
|
@@ -46,6 +47,86 @@ class PlotterMixin:
|
|
|
46
47
|
x=0.5,
|
|
47
48
|
)
|
|
48
49
|
|
|
50
|
+
@staticmethod
|
|
51
|
+
def create_component_trace(
|
|
52
|
+
components,
|
|
53
|
+
coord_attr,
|
|
54
|
+
name,
|
|
55
|
+
line_width,
|
|
56
|
+
color_func,
|
|
57
|
+
unit_conversion_factor,
|
|
58
|
+
order_clockwise: str = None
|
|
59
|
+
):
|
|
60
|
+
"""
|
|
61
|
+
Create a single trace for a component or group of components with NaN separators.
|
|
62
|
+
|
|
63
|
+
Parameters
|
|
64
|
+
----------
|
|
65
|
+
components : list or object
|
|
66
|
+
Single component or list of components to process
|
|
67
|
+
coord_attr : str
|
|
68
|
+
Attribute path for coordinates (e.g., '_a_side_coating_coordinates')
|
|
69
|
+
name : str
|
|
70
|
+
Name for the trace
|
|
71
|
+
line_width : float
|
|
72
|
+
Width of the trace line
|
|
73
|
+
color_func : callable
|
|
74
|
+
Function to get color from component
|
|
75
|
+
unit_conversion_factor : float
|
|
76
|
+
Factor to convert coordinates to desired units
|
|
77
|
+
order_clockwise : str or None, optional
|
|
78
|
+
Plane for clockwise ordering ('xy', 'xz', 'yz') or None to disable, by default None
|
|
79
|
+
|
|
80
|
+
Returns
|
|
81
|
+
-------
|
|
82
|
+
go.Scatter or None
|
|
83
|
+
Plotly scatter trace or None if no valid coordinates
|
|
84
|
+
"""
|
|
85
|
+
# Convert single component to list for uniform processing
|
|
86
|
+
if not isinstance(components, list):
|
|
87
|
+
components = [components]
|
|
88
|
+
|
|
89
|
+
if not components:
|
|
90
|
+
return None
|
|
91
|
+
|
|
92
|
+
# Extract coordinates using nested getattr for dot notation
|
|
93
|
+
coord_arrays = []
|
|
94
|
+
for component in components:
|
|
95
|
+
coords = component
|
|
96
|
+
# Handle nested attributes like '_current_collector._body_coordinates'
|
|
97
|
+
for attr_part in coord_attr.split('.'):
|
|
98
|
+
coords = getattr(coords, attr_part)
|
|
99
|
+
|
|
100
|
+
if coords is not None and len(coords) > 0:
|
|
101
|
+
coord_arrays.append(coords)
|
|
102
|
+
|
|
103
|
+
if not coord_arrays:
|
|
104
|
+
return None
|
|
105
|
+
|
|
106
|
+
# Concatenate coordinates with NaN separators
|
|
107
|
+
combined_coords = CoordinateMixin.concat_with_nan_separators(coord_arrays)
|
|
108
|
+
|
|
109
|
+
# Order coordinates clockwise if requested
|
|
110
|
+
if order_clockwise is not None:
|
|
111
|
+
combined_coords = CoordinateMixin.order_coordinates_clockwise_numpy(combined_coords, plane=order_clockwise)
|
|
112
|
+
|
|
113
|
+
# Convert to mm and extract y,z coordinates directly (avoid DataFrame overhead)
|
|
114
|
+
y_coords = combined_coords[:, 1] * unit_conversion_factor
|
|
115
|
+
z_coords = combined_coords[:, 2] * unit_conversion_factor
|
|
116
|
+
|
|
117
|
+
# Create trace
|
|
118
|
+
return go.Scatter(
|
|
119
|
+
x=y_coords,
|
|
120
|
+
y=z_coords,
|
|
121
|
+
mode="lines",
|
|
122
|
+
name=name,
|
|
123
|
+
line={'width': line_width, 'color': "black"},
|
|
124
|
+
fill="toself",
|
|
125
|
+
fillcolor=color_func(components[0]),
|
|
126
|
+
legendgroup=name,
|
|
127
|
+
showlegend=True,
|
|
128
|
+
)
|
|
129
|
+
|
|
49
130
|
@staticmethod
|
|
50
131
|
def plot_breakdown_sunburst(
|
|
51
132
|
breakdown_dict: Dict[str, Any],
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.1.25"
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: steer-core
|
|
3
|
+
Version: 0.1.25
|
|
4
|
+
Summary: Modelling energy storage from cell to site - STEER OpenCell Design
|
|
5
|
+
Author-email: Nicholas Siemons <nsiemons@stanford.edu>
|
|
6
|
+
Maintainer-email: Nicholas Siemons <nsiemons@stanford.edu>
|
|
7
|
+
License: MIT
|
|
8
|
+
Project-URL: Homepage, https://github.com/nicholas9182/steer-core/
|
|
9
|
+
Project-URL: Repository, https://github.com/nicholas9182/steer-core/
|
|
10
|
+
Project-URL: Issues, https://github.com/nicholas9182/steer-core/issues
|
|
11
|
+
Project-URL: Documentation, https://github.com/nicholas9182/steer-core/
|
|
12
|
+
Keywords: energy,storage,battery,modeling,simulation
|
|
13
|
+
Classifier: Development Status :: 4 - Beta
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
19
|
+
Classifier: Operating System :: OS Independent
|
|
20
|
+
Classifier: Topic :: Scientific/Engineering
|
|
21
|
+
Classifier: Topic :: Scientific/Engineering :: Physics
|
|
22
|
+
Requires-Python: >=3.10
|
|
23
|
+
Description-Content-Type: text/markdown
|
|
24
|
+
Requires-Dist: pandas==2.1.4
|
|
25
|
+
Requires-Dist: numpy==1.26.4
|
|
26
|
+
Requires-Dist: datetime==5.5
|
|
27
|
+
Requires-Dist: plotly==6.2.0
|
|
28
|
+
Requires-Dist: scipy==1.15.3
|
|
29
|
+
Provides-Extra: dev
|
|
30
|
+
Requires-Dist: pytest>=7.0; extra == "dev"
|
|
31
|
+
Requires-Dist: pytest-cov; extra == "dev"
|
|
32
|
+
Requires-Dist: black; extra == "dev"
|
|
33
|
+
Requires-Dist: flake8; extra == "dev"
|
|
34
|
+
Requires-Dist: mypy; extra == "dev"
|
|
35
|
+
Requires-Dist: isort; extra == "dev"
|
|
36
|
+
Provides-Extra: test
|
|
37
|
+
Requires-Dist: pytest>=7.0; extra == "test"
|
|
38
|
+
Requires-Dist: pytest-cov; extra == "test"
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
README.md
|
|
2
|
-
|
|
2
|
+
pyproject.toml
|
|
3
3
|
steer_core/DataManager.py
|
|
4
4
|
steer_core/__init__.py
|
|
5
5
|
steer_core.egg-info/PKG-INFO
|
|
@@ -9,6 +9,8 @@ steer_core.egg-info/requires.txt
|
|
|
9
9
|
steer_core.egg-info/top_level.txt
|
|
10
10
|
steer_core/Apps/ContextManagers.py
|
|
11
11
|
steer_core/Apps/__init__.py
|
|
12
|
+
steer_core/Apps/Callbacks/ConfigInteractions.py
|
|
13
|
+
steer_core/Apps/Callbacks/StyleManagement.py
|
|
12
14
|
steer_core/Apps/Components/MaterialSelectors.py
|
|
13
15
|
steer_core/Apps/Components/RangeSliderComponents.py
|
|
14
16
|
steer_core/Apps/Components/SliderComponents.py
|
steer_core-0.1.24/PKG-INFO
DELETED
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
Metadata-Version: 2.4
|
|
2
|
-
Name: steer-core
|
|
3
|
-
Version: 0.1.24
|
|
4
|
-
Summary: Modelling energy storage from cell to site - STEER OpenCell Design
|
|
5
|
-
Home-page: https://github.com/nicholas9182/steer-core/
|
|
6
|
-
Author: Nicholas Siemons
|
|
7
|
-
Author-email: nsiemons@stanford.edu
|
|
8
|
-
Classifier: Programming Language :: Python :: 3
|
|
9
|
-
Classifier: License :: OSI Approved :: MIT License
|
|
10
|
-
Classifier: Operating System :: OS Independent
|
|
11
|
-
Requires-Python: >=3.10
|
|
12
|
-
Requires-Dist: pandas==2.1.4
|
|
13
|
-
Requires-Dist: numpy==1.26.4
|
|
14
|
-
Requires-Dist: datetime==5.5
|
|
15
|
-
Requires-Dist: plotly==6.2.0
|
|
16
|
-
Requires-Dist: scipy==1.15.3
|
|
17
|
-
Dynamic: author
|
|
18
|
-
Dynamic: author-email
|
|
19
|
-
Dynamic: classifier
|
|
20
|
-
Dynamic: home-page
|
|
21
|
-
Dynamic: requires-dist
|
|
22
|
-
Dynamic: requires-python
|
|
23
|
-
Dynamic: summary
|
steer_core-0.1.24/setup.py
DELETED
|
@@ -1,33 +0,0 @@
|
|
|
1
|
-
from setuptools import setup, find_packages
|
|
2
|
-
import pathlib
|
|
3
|
-
import re
|
|
4
|
-
|
|
5
|
-
root = pathlib.Path(__file__).parent
|
|
6
|
-
init = root / "steer_core" / "__init__.py"
|
|
7
|
-
version = re.search(r'__version__\s*=\s*"([^"]+)"', init.read_text()).group(1)
|
|
8
|
-
|
|
9
|
-
setup(
|
|
10
|
-
name="steer-core",
|
|
11
|
-
version=version,
|
|
12
|
-
description="Modelling energy storage from cell to site - STEER OpenCell Design",
|
|
13
|
-
author="Nicholas Siemons",
|
|
14
|
-
author_email="nsiemons@stanford.edu",
|
|
15
|
-
url="https://github.com/nicholas9182/steer-core/",
|
|
16
|
-
packages=find_packages(),
|
|
17
|
-
include_package_data=True,
|
|
18
|
-
install_requires=[
|
|
19
|
-
"pandas==2.1.4",
|
|
20
|
-
"numpy==1.26.4",
|
|
21
|
-
"datetime==5.5",
|
|
22
|
-
"plotly==6.2.0",
|
|
23
|
-
"scipy==1.15.3"
|
|
24
|
-
],
|
|
25
|
-
package_data={"steer_core.Data": ["database.db"]},
|
|
26
|
-
scripts=[],
|
|
27
|
-
classifiers=[
|
|
28
|
-
"Programming Language :: Python :: 3",
|
|
29
|
-
"License :: OSI Approved :: MIT License",
|
|
30
|
-
"Operating System :: OS Independent",
|
|
31
|
-
],
|
|
32
|
-
python_requires=">=3.10",
|
|
33
|
-
)
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
__version__ = "0.1.24"
|
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
Metadata-Version: 2.4
|
|
2
|
-
Name: steer-core
|
|
3
|
-
Version: 0.1.24
|
|
4
|
-
Summary: Modelling energy storage from cell to site - STEER OpenCell Design
|
|
5
|
-
Home-page: https://github.com/nicholas9182/steer-core/
|
|
6
|
-
Author: Nicholas Siemons
|
|
7
|
-
Author-email: nsiemons@stanford.edu
|
|
8
|
-
Classifier: Programming Language :: Python :: 3
|
|
9
|
-
Classifier: License :: OSI Approved :: MIT License
|
|
10
|
-
Classifier: Operating System :: OS Independent
|
|
11
|
-
Requires-Python: >=3.10
|
|
12
|
-
Requires-Dist: pandas==2.1.4
|
|
13
|
-
Requires-Dist: numpy==1.26.4
|
|
14
|
-
Requires-Dist: datetime==5.5
|
|
15
|
-
Requires-Dist: plotly==6.2.0
|
|
16
|
-
Requires-Dist: scipy==1.15.3
|
|
17
|
-
Dynamic: author
|
|
18
|
-
Dynamic: author-email
|
|
19
|
-
Dynamic: classifier
|
|
20
|
-
Dynamic: home-page
|
|
21
|
-
Dynamic: requires-dist
|
|
22
|
-
Dynamic: requires-python
|
|
23
|
-
Dynamic: summary
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|