steer-core 0.1.23__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.
Files changed (48) hide show
  1. steer_core-0.1.25/PKG-INFO +38 -0
  2. steer_core-0.1.25/pyproject.toml +104 -0
  3. steer_core-0.1.25/steer_core/Apps/Callbacks/ConfigInteractions.py +377 -0
  4. steer_core-0.1.25/steer_core/Apps/Callbacks/StyleManagement.py +60 -0
  5. {steer_core-0.1.23 → steer_core-0.1.25}/steer_core/Data/database.db +0 -0
  6. {steer_core-0.1.23 → steer_core-0.1.25}/steer_core/DataManager.py +20 -0
  7. {steer_core-0.1.23 → steer_core-0.1.25}/steer_core/Mixins/Coordinates.py +250 -4
  8. {steer_core-0.1.23 → steer_core-0.1.25}/steer_core/Mixins/Plotter.py +81 -0
  9. steer_core-0.1.25/steer_core/__init__.py +1 -0
  10. steer_core-0.1.25/steer_core.egg-info/PKG-INFO +38 -0
  11. {steer_core-0.1.23 → steer_core-0.1.25}/steer_core.egg-info/SOURCES.txt +3 -1
  12. steer_core-0.1.25/steer_core.egg-info/requires.txt +17 -0
  13. steer_core-0.1.23/PKG-INFO +0 -24
  14. steer_core-0.1.23/setup.py +0 -34
  15. steer_core-0.1.23/steer_core/__init__.py +0 -1
  16. steer_core-0.1.23/steer_core.egg-info/PKG-INFO +0 -24
  17. steer_core-0.1.23/steer_core.egg-info/requires.txt +0 -6
  18. {steer_core-0.1.23 → steer_core-0.1.25}/README.md +0 -0
  19. {steer_core-0.1.23 → steer_core-0.1.25}/setup.cfg +0 -0
  20. {steer_core-0.1.23 → steer_core-0.1.25}/steer_core/Apps/Components/MaterialSelectors.py +0 -0
  21. {steer_core-0.1.23 → steer_core-0.1.25}/steer_core/Apps/Components/RangeSliderComponents.py +0 -0
  22. {steer_core-0.1.23 → steer_core-0.1.25}/steer_core/Apps/Components/SliderComponents.py +0 -0
  23. {steer_core-0.1.23 → steer_core-0.1.25}/steer_core/Apps/Components/__init__.py +0 -0
  24. {steer_core-0.1.23 → steer_core-0.1.25}/steer_core/Apps/ContextManagers.py +0 -0
  25. {steer_core-0.1.23 → steer_core-0.1.25}/steer_core/Apps/Performance/CallbackTimer.py +0 -0
  26. {steer_core-0.1.23 → steer_core-0.1.25}/steer_core/Apps/Performance/__init__.py +0 -0
  27. {steer_core-0.1.23 → steer_core-0.1.25}/steer_core/Apps/Utils/SliderControls.py +0 -0
  28. {steer_core-0.1.23 → steer_core-0.1.25}/steer_core/Apps/Utils/__init__.py +0 -0
  29. {steer_core-0.1.23 → steer_core-0.1.25}/steer_core/Apps/__init__.py +0 -0
  30. {steer_core-0.1.23 → steer_core-0.1.25}/steer_core/Constants/Units.py +0 -0
  31. {steer_core-0.1.23 → steer_core-0.1.25}/steer_core/Constants/Universal.py +0 -0
  32. {steer_core-0.1.23 → steer_core-0.1.25}/steer_core/Constants/__init__.py +0 -0
  33. {steer_core-0.1.23 → steer_core-0.1.25}/steer_core/ContextManagers/__init__.py +0 -0
  34. {steer_core-0.1.23 → steer_core-0.1.25}/steer_core/Data/__init__.py +0 -0
  35. {steer_core-0.1.23 → steer_core-0.1.25}/steer_core/Decorators/Coordinates.py +0 -0
  36. {steer_core-0.1.23 → steer_core-0.1.25}/steer_core/Decorators/Electrochemical.py +0 -0
  37. {steer_core-0.1.23 → steer_core-0.1.25}/steer_core/Decorators/General.py +0 -0
  38. {steer_core-0.1.23 → steer_core-0.1.25}/steer_core/Decorators/Objects.py +0 -0
  39. {steer_core-0.1.23 → steer_core-0.1.25}/steer_core/Decorators/__init__.py +0 -0
  40. {steer_core-0.1.23 → steer_core-0.1.25}/steer_core/Mixins/Colors.py +0 -0
  41. {steer_core-0.1.23 → steer_core-0.1.25}/steer_core/Mixins/Data.py +0 -0
  42. {steer_core-0.1.23 → steer_core-0.1.25}/steer_core/Mixins/Dunder.py +0 -0
  43. {steer_core-0.1.23 → steer_core-0.1.25}/steer_core/Mixins/Serializer.py +0 -0
  44. {steer_core-0.1.23 → steer_core-0.1.25}/steer_core/Mixins/TypeChecker.py +0 -0
  45. {steer_core-0.1.23 → steer_core-0.1.25}/steer_core/Mixins/__init__.py +0 -0
  46. {steer_core-0.1.23 → steer_core-0.1.25}/steer_core.egg-info/dependency_links.txt +0 -0
  47. {steer_core-0.1.23 → steer_core-0.1.25}/steer_core.egg-info/top_level.txt +0 -0
  48. {steer_core-0.1.23 → 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
+
@@ -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
- raise ValueError(
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
- setup.py
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
@@ -0,0 +1,17 @@
1
+ pandas==2.1.4
2
+ numpy==1.26.4
3
+ datetime==5.5
4
+ plotly==6.2.0
5
+ scipy==1.15.3
6
+
7
+ [dev]
8
+ pytest>=7.0
9
+ pytest-cov
10
+ black
11
+ flake8
12
+ mypy
13
+ isort
14
+
15
+ [test]
16
+ pytest>=7.0
17
+ pytest-cov
@@ -1,24 +0,0 @@
1
- Metadata-Version: 2.4
2
- Name: steer-core
3
- Version: 0.1.23
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: scipy==1.15.3
16
- Requires-Dist: plotly==6.2.0
17
- Requires-Dist: dash==2.18.1
18
- Dynamic: author
19
- Dynamic: author-email
20
- Dynamic: classifier
21
- Dynamic: home-page
22
- Dynamic: requires-dist
23
- Dynamic: requires-python
24
- Dynamic: summary
@@ -1,34 +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
- "scipy==1.15.3",
23
- "plotly==6.2.0",
24
- "dash==2.18.1",
25
- ],
26
- package_data={"steer_core.Data": ["database.db"]},
27
- scripts=[],
28
- classifiers=[
29
- "Programming Language :: Python :: 3",
30
- "License :: OSI Approved :: MIT License",
31
- "Operating System :: OS Independent",
32
- ],
33
- python_requires=">=3.10",
34
- )
@@ -1 +0,0 @@
1
- __version__ = "0.1.23"
@@ -1,24 +0,0 @@
1
- Metadata-Version: 2.4
2
- Name: steer-core
3
- Version: 0.1.23
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: scipy==1.15.3
16
- Requires-Dist: plotly==6.2.0
17
- Requires-Dist: dash==2.18.1
18
- Dynamic: author
19
- Dynamic: author-email
20
- Dynamic: classifier
21
- Dynamic: home-page
22
- Dynamic: requires-dist
23
- Dynamic: requires-python
24
- Dynamic: summary
@@ -1,6 +0,0 @@
1
- pandas==2.1.4
2
- numpy==1.26.4
3
- datetime==5.5
4
- scipy==1.15.3
5
- plotly==6.2.0
6
- dash==2.18.1
File without changes
File without changes