steer-core 0.1.8__tar.gz → 0.1.10__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 (43) hide show
  1. {steer_core-0.1.8 → steer_core-0.1.10}/PKG-INFO +1 -1
  2. {steer_core-0.1.8 → steer_core-0.1.10}/setup.py +1 -0
  3. steer_core-0.1.10/steer_core/Apps/Components/MaterialSelectors.py +694 -0
  4. {steer_core-0.1.8 → steer_core-0.1.10}/steer_core/Apps/Components/SliderComponents.py +17 -3
  5. {steer_core-0.1.8 → steer_core-0.1.10}/steer_core/Apps/Utils/SliderControls.py +38 -17
  6. steer_core-0.1.10/steer_core/Data/database.db +0 -0
  7. {steer_core-0.1.8 → steer_core-0.1.10}/steer_core/DataManager.py +5 -3
  8. steer_core-0.1.10/steer_core/__init__.py +1 -0
  9. {steer_core-0.1.8 → steer_core-0.1.10}/steer_core.egg-info/PKG-INFO +1 -1
  10. {steer_core-0.1.8 → steer_core-0.1.10}/steer_core.egg-info/SOURCES.txt +2 -1
  11. steer_core-0.1.8/steer_core/Apps/Components/CompoundComponents.py +0 -0
  12. steer_core-0.1.8/steer_core/__init__.py +0 -1
  13. {steer_core-0.1.8 → steer_core-0.1.10}/README.md +0 -0
  14. {steer_core-0.1.8 → steer_core-0.1.10}/setup.cfg +0 -0
  15. {steer_core-0.1.8 → steer_core-0.1.10}/steer_core/Apps/Components/RangeSliderComponents.py +0 -0
  16. {steer_core-0.1.8 → steer_core-0.1.10}/steer_core/Apps/Components/__init__.py +0 -0
  17. {steer_core-0.1.8 → steer_core-0.1.10}/steer_core/Apps/ContextManagers.py +0 -0
  18. {steer_core-0.1.8 → steer_core-0.1.10}/steer_core/Apps/Performance/CallbackTimer.py +0 -0
  19. {steer_core-0.1.8 → steer_core-0.1.10}/steer_core/Apps/Performance/__init__.py +0 -0
  20. {steer_core-0.1.8 → steer_core-0.1.10}/steer_core/Apps/Utils/__init__.py +0 -0
  21. {steer_core-0.1.8 → steer_core-0.1.10}/steer_core/Apps/__init__.py +0 -0
  22. {steer_core-0.1.8 → steer_core-0.1.10}/steer_core/Constants/Units.py +0 -0
  23. {steer_core-0.1.8 → steer_core-0.1.10}/steer_core/Constants/Universal.py +0 -0
  24. {steer_core-0.1.8 → steer_core-0.1.10}/steer_core/Constants/__init__.py +0 -0
  25. {steer_core-0.1.8 → steer_core-0.1.10}/steer_core/ContextManagers/__init__.py +0 -0
  26. {steer_core-0.1.8 → steer_core-0.1.10}/steer_core/Data/__init__.py +0 -0
  27. {steer_core-0.1.8 → steer_core-0.1.10}/steer_core/Decorators/Coordinates.py +0 -0
  28. {steer_core-0.1.8 → steer_core-0.1.10}/steer_core/Decorators/Electrochemical.py +0 -0
  29. {steer_core-0.1.8 → steer_core-0.1.10}/steer_core/Decorators/General.py +0 -0
  30. {steer_core-0.1.8 → steer_core-0.1.10}/steer_core/Decorators/Objects.py +0 -0
  31. {steer_core-0.1.8 → steer_core-0.1.10}/steer_core/Decorators/__init__.py +0 -0
  32. {steer_core-0.1.8 → steer_core-0.1.10}/steer_core/Mixins/Colors.py +0 -0
  33. {steer_core-0.1.8 → steer_core-0.1.10}/steer_core/Mixins/Coordinates.py +0 -0
  34. {steer_core-0.1.8 → steer_core-0.1.10}/steer_core/Mixins/Data.py +0 -0
  35. {steer_core-0.1.8 → steer_core-0.1.10}/steer_core/Mixins/Serializer.py +0 -0
  36. {steer_core-0.1.8 → steer_core-0.1.10}/steer_core/Mixins/Validators.py +0 -0
  37. {steer_core-0.1.8 → steer_core-0.1.10}/steer_core/Mixins/__init__.py +0 -0
  38. {steer_core-0.1.8 → steer_core-0.1.10}/steer_core.egg-info/dependency_links.txt +0 -0
  39. {steer_core-0.1.8 → steer_core-0.1.10}/steer_core.egg-info/requires.txt +0 -0
  40. {steer_core-0.1.8 → steer_core-0.1.10}/steer_core.egg-info/top_level.txt +0 -0
  41. {steer_core-0.1.8 → steer_core-0.1.10}/test/test_compound_components.py +0 -0
  42. {steer_core-0.1.8 → steer_core-0.1.10}/test/test_compound_components_clean.py +0 -0
  43. {steer_core-0.1.8 → steer_core-0.1.10}/test/test_slider_controls.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: steer-core
3
- Version: 0.1.8
3
+ Version: 0.1.10
4
4
  Summary: Modelling energy storage from cell to site - STEER OpenCell Design
5
5
  Home-page: https://github.com/nicholas9182/steer-core/
6
6
  Author: Nicholas Siemons
@@ -22,6 +22,7 @@ setup(
22
22
  "scipy==1.15.3",
23
23
  "plotly==6.2.0",
24
24
  ],
25
+ package_data={"steer_core.Data": ["database.db"]},
25
26
  scripts=[],
26
27
  classifiers=[
27
28
  "Programming Language :: Python :: 3",
@@ -0,0 +1,694 @@
1
+ import dash as ds
2
+ from dash import Input, Output, dcc, html
3
+ from typing import Union, List, Dict
4
+ from .SliderComponents import SliderWithTextInput
5
+
6
+
7
+ class MaterialSelector:
8
+ """
9
+ A custom Dash component that combines material selection controls in a horizontal layout.
10
+
11
+ This component creates a horizontal row containing:
12
+ - A dropdown menu for selecting material names
13
+ - An input box for specifying weight fraction of the material
14
+ - Two SliderWithTextInput components for specific cost and density
15
+
16
+ The component is designed for material composition interfaces where users need to
17
+ select materials and specify their properties and proportions.
18
+
19
+ Attributes:
20
+ id_base (dict): Base identifier dictionary used to construct unique IDs for child components
21
+ material_options (List[Dict]): List of material options for the dropdown
22
+ default_material (str): Default selected material name
23
+ default_weight_fraction (float): Default weight fraction value
24
+ cost_config (dict): Configuration for the cost slider (min_val, max_val, step, etc.)
25
+ density_config (dict): Configuration for the density slider (min_val, max_val, step, etc.)
26
+ property_name (str): Property identifier used in component ID construction
27
+ title (str): Display title for the component
28
+ slider_disable (bool): Whether the sliders should be disabled
29
+ dropdown_id (dict): Computed ID for the dropdown component
30
+ weight_fraction_id (dict): Computed ID for the weight fraction input
31
+ cost_slider (SliderWithTextInput): Cost slider component
32
+ density_slider (SliderWithTextInput): Density slider component
33
+
34
+ Example:
35
+ >>> material_selector = MaterialSelector(
36
+ ... id_base={'type': 'material', 'index': 0},
37
+ ... material_options=[
38
+ ... {'label': 'Aluminum', 'value': 'aluminum'},
39
+ ... {'label': 'Steel', 'value': 'steel'},
40
+ ... {'label': 'Carbon Fiber', 'value': 'carbon_fiber'}
41
+ ... ],
42
+ ... default_material='aluminum',
43
+ ... default_weight_fraction=0.5,
44
+ ... cost_config={
45
+ ... 'min_val': 0.0,
46
+ ... 'max_val': 100.0,
47
+ ... 'step': 0.1,
48
+ ... 'mark_interval': 10.0,
49
+ ... 'default_val': 25.0,
50
+ ... 'title': 'Cost ($/kg)'
51
+ ... },
52
+ ... density_config={
53
+ ... 'min_val': 0.0,
54
+ ... 'max_val': 10.0,
55
+ ... 'step': 0.01,
56
+ ... 'mark_interval': 1.0,
57
+ ... 'default_val': 2.7,
58
+ ... 'title': 'Density (g/cm³)'
59
+ ... },
60
+ ... property_name='material_1',
61
+ ... title='Material 1'
62
+ ... )
63
+ >>> layout_element = material_selector() # Returns Dash HTML Div component
64
+ """
65
+
66
+ def __init__(
67
+ self,
68
+ id_base: dict,
69
+ material_options: List[Dict] = None,
70
+ slider_configs: dict = None,
71
+ cost_config: dict = None,
72
+ density_config: dict = None,
73
+ title: str = "Material",
74
+ default_material: str = None,
75
+ default_weight_percent: float = 0,
76
+ slider_disable: bool = False,
77
+ div_width: str = '100%',
78
+ hidden: bool = False,
79
+ ):
80
+ """
81
+ Initialize the MaterialSelector component.
82
+
83
+ Args:
84
+ id_base (dict): Base dictionary for generating component IDs.
85
+ material_options (List[Dict], optional): List of material options for dropdown.
86
+ Each dict should have 'label' and 'value' keys.
87
+ If None, defaults to empty list (no options).
88
+ slider_configs (dict, optional): Output from create_slider_config with arrays where:
89
+ - Index 0 = density configuration
90
+ - Index 1 = cost configuration
91
+ If provided, cost_config and density_config are ignored.
92
+ If empty dictionary {}, uses sensible defaults with preset values.
93
+ If None (default), uses sensible defaults with no initial values.
94
+ cost_config (dict, optional): Legacy cost slider configuration. Ignored if slider_config provided.
95
+ density_config (dict, optional): Legacy density slider configuration. Ignored if slider_config provided.
96
+ title (str, optional): Title displayed for the component. Defaults to "Material".
97
+ default_material (str, optional): Default selected material. If None, no material
98
+ will be initially selected in the dropdown.
99
+ default_weight_percent (float, optional): Default weight percentage (0.0-100.0). Defaults to 100.0.
100
+ slider_disable (bool, optional): Whether to disable sliders. Defaults to False.
101
+ div_width (str, optional): CSS width specification for the container div.
102
+ Defaults to '100%'.
103
+ hidden (bool, optional): Whether to hide the entire component. When True,
104
+ the component will be rendered with display: none.
105
+ Defaults to False.
106
+ """
107
+ self.id_base = id_base
108
+ self.material_options = material_options or [] # Default to empty list if None
109
+ self.default_material = default_material # Keep as None if not provided
110
+ self.default_weight_percent = default_weight_percent
111
+ self.cost_config = cost_config
112
+ self.density_config = density_config
113
+ self.title = title
114
+ self.slider_disable = slider_disable
115
+ self.div_width = div_width
116
+ self.hidden = hidden
117
+
118
+ # Generate component IDs
119
+ self.dropdown_id = self._make_id('dropdown')
120
+ self.weight_fraction_id = self._make_id('weight_fraction')
121
+
122
+ # Normalize configurations to handle both legacy and create_slider_config formats
123
+ if slider_configs is not None and slider_configs: # Check if not empty
124
+ # Use slider_configs arrays (index 0 = density, index 1 = cost)
125
+ density_normalized = self._normalize_config_from_arrays(slider_configs, 0, 'Density')
126
+ cost_normalized = self._normalize_config_from_arrays(slider_configs, 1, 'Cost')
127
+ elif slider_configs is not None and not slider_configs: # Empty dictionary
128
+ # Use sensible defaults for materials
129
+ density_normalized = {
130
+ 'min_val': 0.0,
131
+ 'max_val': 0.1,
132
+ 'step': 0.01,
133
+ 'mark_interval': 0.1,
134
+ 'title': 'Density',
135
+ 'default_val': 0.05
136
+ }
137
+ cost_normalized = {
138
+ 'min_val': 0.0,
139
+ 'max_val': 0.1,
140
+ 'step': 0.01,
141
+ 'mark_interval': 0.1,
142
+ 'title': 'Cost',
143
+ 'default_val': 0.05
144
+ }
145
+ else:
146
+ # slider_configs is None - use sensible defaults with None values
147
+ density_normalized = {
148
+ 'min_val': 0.0,
149
+ 'max_val': 0.1,
150
+ 'step': 0.01,
151
+ 'mark_interval': 0.1,
152
+ 'title': 'Density',
153
+ 'default_val': None # No initial value
154
+ }
155
+ cost_normalized = {
156
+ 'min_val': 0.0,
157
+ 'max_val': 0.1,
158
+ 'step': 0.01,
159
+ 'mark_interval': 0.1,
160
+ 'title': 'Cost',
161
+ 'default_val': None # No initial value
162
+ }
163
+
164
+ # Create slider components
165
+ cost_slider_kwargs = {
166
+ 'id_base': id_base,
167
+ 'min_val': cost_normalized['min_val'],
168
+ 'max_val': cost_normalized['max_val'],
169
+ 'step': cost_normalized['step'],
170
+ 'mark_interval': cost_normalized['mark_interval'],
171
+ 'property_name': "specific_cost",
172
+ 'title': cost_normalized['title'],
173
+ 'default_val': cost_normalized.get('default_val', cost_normalized['min_val']),
174
+ 'with_slider_titles': True,
175
+ 'slider_disable': slider_disable,
176
+ 'div_width': '100%'
177
+ }
178
+ # Add marks if available
179
+ if 'marks' in cost_normalized:
180
+ cost_slider_kwargs['marks'] = cost_normalized['marks']
181
+
182
+ self.cost_slider = SliderWithTextInput(**cost_slider_kwargs)
183
+
184
+ density_slider_kwargs = {
185
+ 'id_base': id_base,
186
+ 'min_val': density_normalized['min_val'],
187
+ 'max_val': density_normalized['max_val'],
188
+ 'step': density_normalized['step'],
189
+ 'mark_interval': density_normalized['mark_interval'],
190
+ 'property_name': "density",
191
+ 'title': density_normalized['title'],
192
+ 'default_val': density_normalized.get('default_val', density_normalized['min_val']),
193
+ 'with_slider_titles': True,
194
+ 'slider_disable': slider_disable,
195
+ 'div_width': '100%'
196
+ }
197
+ # Add marks if available
198
+ if 'marks' in density_normalized:
199
+ density_slider_kwargs['marks'] = density_normalized['marks']
200
+
201
+ self.density_slider = SliderWithTextInput(**density_slider_kwargs)
202
+
203
+ def _normalize_config_from_arrays(self, config: dict, index: int, default_title: str) -> dict:
204
+ """
205
+ Normalize slider configuration from create_slider_config output arrays.
206
+
207
+ Args:
208
+ config (dict): create_slider_config output with array values
209
+ index (int): Index to extract from each array (0 = density, 1 = cost)
210
+ default_title (str): Default title if not provided
211
+
212
+ Returns:
213
+ dict: Normalized configuration with min_val, max_val, step, mark_interval, title keys
214
+ """
215
+ normalized = {
216
+ 'min_val': config['min_vals'][index],
217
+ 'max_val': config['max_vals'][index],
218
+ 'step': config['step_vals'][index],
219
+ 'title': default_title # Use provided title
220
+ }
221
+
222
+ # Get pre-computed marks if available, otherwise calculate mark_interval
223
+ if 'mark_vals' in config and len(config['mark_vals']) > index:
224
+ marks = config['mark_vals'][index]
225
+ normalized['marks'] = marks # Pass the actual marks dictionary
226
+ if len(marks) >= 2:
227
+ # Calculate interval from first two marks
228
+ mark_positions = sorted(marks.keys())
229
+ normalized['mark_interval'] = mark_positions[1] - mark_positions[0]
230
+ else:
231
+ # Fallback: use step as mark interval
232
+ normalized['mark_interval'] = normalized['step']
233
+ else:
234
+ normalized['mark_interval'] = normalized['step']
235
+
236
+ # Add default value if available from grid values
237
+ if 'grid_slider_vals' in config and len(config['grid_slider_vals']) > index:
238
+ normalized['default_val'] = config['grid_slider_vals'][index]
239
+ else:
240
+ normalized['default_val'] = normalized['min_val']
241
+
242
+ return normalized
243
+
244
+ def _normalize_config(self, config: dict, config_type: str) -> dict:
245
+ """
246
+ Normalize slider configuration to handle both legacy and create_slider_config formats.
247
+
248
+ Args:
249
+ config (dict): Either legacy format or create_slider_config output
250
+ config_type (str): Type identifier ('cost' or 'density') for default title
251
+
252
+ Returns:
253
+ dict: Normalized configuration with min_val, max_val, step, mark_interval, title keys
254
+ """
255
+ # Check if this is a create_slider_config output (has min_vals, max_vals, etc.)
256
+ if 'min_vals' in config and 'max_vals' in config:
257
+ # This is create_slider_config output - use first element from arrays
258
+ normalized = {
259
+ 'min_val': config['min_vals'][0],
260
+ 'max_val': config['max_vals'][0],
261
+ 'step': config['step_vals'][0],
262
+ 'title': config_type.capitalize() # Default title
263
+ }
264
+
265
+ # Get pre-computed marks if available, otherwise calculate mark_interval
266
+ if 'mark_vals' in config and len(config['mark_vals']) > 0:
267
+ marks = config['mark_vals'][0]
268
+ normalized['marks'] = marks # Pass the actual marks dictionary
269
+ if len(marks) >= 2:
270
+ # Calculate interval from first two marks
271
+ mark_positions = sorted(marks.keys())
272
+ normalized['mark_interval'] = mark_positions[1] - mark_positions[0]
273
+ else:
274
+ # Fallback: use step as mark interval
275
+ normalized['mark_interval'] = normalized['step']
276
+ else:
277
+ normalized['mark_interval'] = normalized['step']
278
+
279
+ # Add default value if available from grid values
280
+ if 'grid_slider_vals' in config and len(config['grid_slider_vals']) > 0:
281
+ normalized['default_val'] = config['grid_slider_vals'][0]
282
+ else:
283
+ normalized['default_val'] = normalized['min_val']
284
+
285
+ else:
286
+ # This is legacy format - use as-is but ensure all required keys exist
287
+ normalized = config.copy()
288
+ if 'title' not in normalized:
289
+ normalized['title'] = config_type.capitalize()
290
+ if 'default_val' not in normalized:
291
+ normalized['default_val'] = normalized.get('min_val', 0)
292
+
293
+ return normalized
294
+
295
+ def _make_id(self, subtype: str):
296
+ """
297
+ Generate a unique ID dictionary for component sub-elements.
298
+
299
+ Args:
300
+ subtype (str): The specific component subtype.
301
+
302
+ Returns:
303
+ dict: Complete ID dictionary containing base ID information plus subtype.
304
+ """
305
+ return {**self.id_base, 'subtype': subtype}
306
+
307
+ def _make_dropdown(self):
308
+ """
309
+ Create and configure the material selection dropdown.
310
+
311
+ Returns:
312
+ dash.dcc.Dropdown: Configured dropdown component for material selection.
313
+ """
314
+ return dcc.Dropdown(
315
+ id=self.dropdown_id,
316
+ options=self.material_options,
317
+ value=self.default_material, # Can be None for no initial selection
318
+ disabled=self.slider_disable,
319
+ style={'width': '100%', 'margin-bottom': '10px'}
320
+ )
321
+
322
+ def _make_weight_fraction_input(self):
323
+ """
324
+ Create and configure the weight percentage input.
325
+
326
+ Returns:
327
+ dash.dcc.Input: Configured numeric input for weight percentage.
328
+ """
329
+ return dcc.Input(
330
+ id=self.weight_fraction_id,
331
+ type='number',
332
+ value=self.default_weight_percent, # Use percentage directly
333
+ min=0.0,
334
+ max=100.0,
335
+ step=0.1,
336
+ disabled=self.slider_disable,
337
+ style={'width': '100%', 'margin-bottom': '10px'}
338
+ )
339
+
340
+ def __call__(self):
341
+ """
342
+ Generate the complete component layout as a callable object.
343
+
344
+ Creates and returns a Dash HTML Div containing all components arranged
345
+ in a horizontal layout with proper spacing and styling.
346
+
347
+ Returns:
348
+ dash.html.Div: Complete component layout with horizontal arrangement.
349
+ """
350
+ return html.Div([
351
+ # Main horizontal layout
352
+ html.Div([
353
+ # Material selection column
354
+ html.Div([
355
+ html.P("Material:", style={'margin': '0px 0px 5px 0px', 'font-weight': 'bold'}),
356
+ self._make_dropdown()
357
+ ], style={'width': '20%', 'display': 'inline-block', 'vertical-align': 'top', 'padding-right': '25px'}),
358
+
359
+ # Weight percentage column
360
+ html.Div([
361
+ html.P("Weight Percentage:", style={'margin': '0px 0px 5px 0px', 'font-weight': 'bold'}),
362
+ self._make_weight_fraction_input()
363
+ ], style={'width': '15%', 'display': 'inline-block', 'vertical-align': 'top', 'padding-right': '25px'}),
364
+
365
+ # Density slider column
366
+ html.Div([
367
+ self.density_slider()
368
+ ], style={'width': '30%', 'display': 'inline-block', 'vertical-align': 'top', 'padding-right': '25px'}),
369
+
370
+ # Cost slider column
371
+ html.Div([
372
+ self.cost_slider()
373
+ ], style={'width': '30%', 'display': 'inline-block', 'vertical-align': 'top'})
374
+
375
+ ], style={'display': 'flex', 'align-items': 'flex-start', 'width': '100%', 'gap': '15px'})
376
+
377
+ ],
378
+ id=self.id_base,
379
+ style={
380
+ 'border': '1px solid #ddd',
381
+ 'border-radius': '5px',
382
+ 'padding': '15px',
383
+ 'margin': '10px 0px',
384
+ 'background-color': '#f9f9f9',
385
+ 'width': self.div_width,
386
+ 'display': 'none' if self.hidden else 'block'
387
+ })
388
+
389
+ @property
390
+ def components(self):
391
+ """
392
+ Get a dictionary mapping component types to their IDs.
393
+
394
+ Returns:
395
+ dict: Dictionary with component type keys mapping to their ID dictionaries.
396
+ """
397
+ return {
398
+ 'dropdown': self.dropdown_id,
399
+ 'weight_fraction': self.weight_fraction_id,
400
+ 'cost_slider': self.cost_slider.slider_id,
401
+ 'cost_input': self.cost_slider.input_id,
402
+ 'density_slider': self.density_slider.slider_id,
403
+ 'density_input': self.density_slider.input_id
404
+ }
405
+
406
+ def get_all_inputs(self):
407
+ """
408
+ Get Input objects for all component values.
409
+
410
+ Returns:
411
+ list: List containing Input objects for all components.
412
+ """
413
+ return [
414
+ Input(self.dropdown_id, 'value'),
415
+ Input(self.weight_fraction_id, 'value'),
416
+ Input(self.cost_slider.slider_id, 'value'),
417
+ Input(self.cost_slider.input_id, 'value'),
418
+ Input(self.density_slider.slider_id, 'value'),
419
+ Input(self.density_slider.input_id, 'value')
420
+ ]
421
+
422
+ def get_all_outputs(self):
423
+ """
424
+ Get Output objects for updating all component values.
425
+
426
+ Returns:
427
+ list: List containing Output objects for all components.
428
+ """
429
+ return [
430
+ Output(self.dropdown_id, 'value'),
431
+ Output(self.weight_fraction_id, 'value'),
432
+ Output(self.cost_slider.slider_id, 'value'),
433
+ Output(self.cost_slider.input_id, 'value'),
434
+ Output(self.density_slider.slider_id, 'value'),
435
+ Output(self.density_slider.input_id, 'value')
436
+ ]
437
+
438
+
439
+ class ActiveMaterialSelector(MaterialSelector):
440
+ """
441
+ A custom Dash component for active material selection with capacity controls.
442
+
443
+ This component extends MaterialSelector functionality by adding reversible and
444
+ irreversible capacity sliders alongside the standard density and cost controls.
445
+ It creates a horizontal row containing:
446
+ - A dropdown menu for selecting material names
447
+ - An input box for specifying weight percentage of the material
448
+ - Four SliderWithTextInput components for:
449
+ * Density
450
+ * Specific cost
451
+ * Reversible capacity scaling
452
+ * Irreversible capacity scaling
453
+
454
+ This component is designed for battery active material interfaces where users need to
455
+ specify both physical properties (cost, density) and electrochemical scaling factors
456
+ (reversible/irreversible capacity scaling).
457
+ """
458
+
459
+ def __init__(
460
+ self,
461
+ id_base: dict,
462
+ material_options: List[Dict] = None,
463
+ slider_configs: dict = None,
464
+ title: str = "Active Material",
465
+ default_material: str = None,
466
+ default_weight_percent: float = 0,
467
+ slider_disable: bool = False,
468
+ div_width: str = '100%',
469
+ hidden: bool = False,
470
+ ):
471
+ """
472
+ Initialize the ActiveMaterialSelector component.
473
+
474
+ Args:
475
+ id_base (dict): Base dictionary for generating component IDs.
476
+ material_options (List[Dict], optional): List of material options for dropdown.
477
+ Each dict should have 'label' and 'value' keys.
478
+ If None, defaults to empty list (no options).
479
+ slider_configs (dict): Output from create_slider_config with arrays where:
480
+ - Index 0 = density configuration
481
+ - Index 1 = cost configuration
482
+ - Index 2 = reversible capacity scaling configuration
483
+ - Index 3 = irreversible capacity scaling configuration
484
+ If empty dictionary {}, uses sensible defaults with preset values.
485
+ If None (default), uses sensible defaults with no initial values.
486
+ title (str, optional): Title displayed for the component. Defaults to "Active Material".
487
+ default_material (str, optional): Default selected material. If None, no material
488
+ will be initially selected in the dropdown.
489
+ default_weight_percent (float, optional): Default weight percentage (0.0-100.0). Defaults to 100.0.
490
+ slider_disable (bool, optional): Whether to disable sliders. Defaults to False.
491
+ div_width (str, optional): CSS width specification for the container div.
492
+ Defaults to '100%'.
493
+ hidden (bool, optional): Whether to hide the entire component. When True,
494
+ the component will be rendered with display: none.
495
+ Defaults to False.
496
+ """
497
+ # Handle different slider_configs scenarios
498
+ if slider_configs is not None and slider_configs:
499
+ # Use provided configuration
500
+ pass # slider_configs will be passed to parent
501
+ elif slider_configs is not None and not slider_configs:
502
+ # Empty dictionary - create sensible defaults with preset values
503
+ from ..Utils.SliderControls import create_slider_config
504
+ min_vals = [0.0, 0.0, 0.0, 0.0] # density, cost, rev_cap_scaling, irrev_cap_scaling
505
+ max_vals = [0.1, 0.1, 0.1, 0.1] # density, cost, rev_cap_scaling, irrev_cap_scaling
506
+ default_vals = [0.05, 0.05, 0.05, 0.05] # density, cost, rev_cap_scaling, irrev_cap_scaling
507
+ slider_configs = create_slider_config(min_vals, max_vals, default_vals)
508
+ else:
509
+ # slider_configs is None - create sensible defaults with None values
510
+ # Create manual config since create_slider_config doesn't handle None values
511
+ slider_configs = {
512
+ 'min_vals': [0.0, 0.0, 0.0, 0.0],
513
+ 'max_vals': [0.1, 0.1, 0.1, 0.1],
514
+ 'step_vals': [0.01, 0.01, 0.01, 0.01],
515
+ 'grid_slider_vals': [None, None, None, None],
516
+ 'mark_vals': [
517
+ {0.0: '', 0.1: ''}, # density
518
+ {0.0: '', 0.1: ''}, # cost
519
+ {0.0: '', 0.1: ''}, # rev_cap_scaling
520
+ {0.0: '', 0.1: ''} # irrev_cap_scaling
521
+ ]
522
+ }
523
+
524
+ # Initialize the parent MaterialSelector with first two sliders (density, cost)
525
+ super().__init__(
526
+ id_base=id_base,
527
+ material_options=material_options,
528
+ slider_configs=slider_configs, # Parent will use indices 0 and 1
529
+ title=title,
530
+ default_material=default_material,
531
+ default_weight_percent=default_weight_percent,
532
+ slider_disable=slider_disable,
533
+ div_width=div_width,
534
+ hidden=hidden
535
+ )
536
+
537
+ # Add the additional capacity sliders (indices 2 and 3)
538
+ reversible_capacity_normalized = self._normalize_config_from_arrays(slider_configs, 2, 'Reversible Capacity Scaling')
539
+ irreversible_capacity_normalized = self._normalize_config_from_arrays(slider_configs, 3, 'Irreversible Capacity Scaling')
540
+
541
+ # Create additional slider components
542
+ reversible_capacity_slider_kwargs = {
543
+ 'id_base': id_base,
544
+ 'min_val': reversible_capacity_normalized['min_val'],
545
+ 'max_val': reversible_capacity_normalized['max_val'],
546
+ 'step': reversible_capacity_normalized['step'],
547
+ 'mark_interval': reversible_capacity_normalized['mark_interval'],
548
+ 'property_name': "reversible_capacity_scaling",
549
+ 'title': reversible_capacity_normalized['title'],
550
+ 'default_val': reversible_capacity_normalized.get('default_val', reversible_capacity_normalized['min_val']),
551
+ 'with_slider_titles': True,
552
+ 'slider_disable': slider_disable,
553
+ 'div_width': '100%'
554
+ }
555
+ if 'marks' in reversible_capacity_normalized:
556
+ reversible_capacity_slider_kwargs['marks'] = reversible_capacity_normalized['marks']
557
+ self.reversible_capacity_slider = SliderWithTextInput(**reversible_capacity_slider_kwargs)
558
+
559
+ irreversible_capacity_slider_kwargs = {
560
+ 'id_base': id_base,
561
+ 'min_val': irreversible_capacity_normalized['min_val'],
562
+ 'max_val': irreversible_capacity_normalized['max_val'],
563
+ 'step': irreversible_capacity_normalized['step'],
564
+ 'mark_interval': irreversible_capacity_normalized['mark_interval'],
565
+ 'property_name': "irreversible_capacity_scaling",
566
+ 'title': irreversible_capacity_normalized['title'],
567
+ 'default_val': irreversible_capacity_normalized.get('default_val', irreversible_capacity_normalized['min_val']),
568
+ 'with_slider_titles': True,
569
+ 'slider_disable': slider_disable,
570
+ 'div_width': '100%'
571
+ }
572
+ if 'marks' in irreversible_capacity_normalized:
573
+ irreversible_capacity_slider_kwargs['marks'] = irreversible_capacity_normalized['marks']
574
+ self.irreversible_capacity_slider = SliderWithTextInput(**irreversible_capacity_slider_kwargs)
575
+
576
+ def __call__(self):
577
+ """
578
+ Generate the complete component layout as a callable object.
579
+
580
+ Creates and returns a Dash HTML Div containing all components arranged
581
+ in a horizontal layout with proper spacing and styling.
582
+
583
+ Returns:
584
+ dash.html.Div: Complete component layout with horizontal arrangement.
585
+ """
586
+ return html.Div([
587
+ # Main horizontal layout
588
+ html.Div([
589
+ # Material selection column (15%)
590
+ html.Div([
591
+ html.P("Material:", style={'margin': '0px 0px 5px 0px', 'font-weight': 'bold'}),
592
+ self._make_dropdown()
593
+ ], style={'width': '15%', 'display': 'inline-block', 'vertical-align': 'top', 'padding-right': '15px'}),
594
+
595
+ # Weight percentage column (15%)
596
+ html.Div([
597
+ html.P("Weight %:", style={'margin': '0px 0px 5px 0px', 'font-weight': 'bold'}),
598
+ self._make_weight_fraction_input()
599
+ ], style={'width': '15%', 'display': 'inline-block', 'vertical-align': 'top', 'padding-right': '15px'}),
600
+
601
+ # Density slider column (17.5%)
602
+ html.Div([
603
+ self.density_slider()
604
+ ], style={'width': '17.5%', 'display': 'inline-block', 'vertical-align': 'top', 'padding-right': '15px'}),
605
+
606
+ # Cost slider column (17.5%)
607
+ html.Div([
608
+ self.cost_slider()
609
+ ], style={'width': '17.5%', 'display': 'inline-block', 'vertical-align': 'top', 'padding-right': '15px'}),
610
+
611
+ # Reversible capacity slider column (17.5%)
612
+ html.Div([
613
+ self.reversible_capacity_slider()
614
+ ], style={'width': '17.5%', 'display': 'inline-block', 'vertical-align': 'top', 'padding-right': '15px'}),
615
+
616
+ # Irreversible capacity slider column (17.5%)
617
+ html.Div([
618
+ self.irreversible_capacity_slider()
619
+ ], style={'width': '17.5%', 'display': 'inline-block', 'vertical-align': 'top'})
620
+
621
+ ], style={'display': 'flex', 'align-items': 'flex-start', 'width': '100%', 'gap': '10px'})
622
+
623
+ ],
624
+ id=self.id_base,
625
+ style={
626
+ 'border': '1px solid #ddd',
627
+ 'border-radius': '5px',
628
+ 'padding': '15px',
629
+ 'margin': '10px 0px',
630
+ 'background-color': '#f9f9f9',
631
+ 'width': self.div_width,
632
+ 'display': 'none' if self.hidden else 'block'
633
+ })
634
+
635
+ @property
636
+ def components(self):
637
+ """
638
+ Get a dictionary mapping component types to their IDs.
639
+
640
+ Returns:
641
+ dict: Dictionary with component type keys mapping to their ID dictionaries.
642
+ """
643
+ # Get base components from parent
644
+ base_components = super().components
645
+
646
+ # Add the additional capacity slider components
647
+ base_components.update({
648
+ 'reversible_capacity_scaling_slider': self.reversible_capacity_slider.slider_id,
649
+ 'reversible_capacity_scaling_input': self.reversible_capacity_slider.input_id,
650
+ 'irreversible_capacity_scaling_slider': self.irreversible_capacity_slider.slider_id,
651
+ 'irreversible_capacity_scaling_input': self.irreversible_capacity_slider.input_id
652
+ })
653
+
654
+ return base_components
655
+
656
+ def get_all_inputs(self):
657
+ """
658
+ Get Input objects for all component values.
659
+
660
+ Returns:
661
+ list: List containing Input objects for all components.
662
+ """
663
+ # Get base inputs from parent
664
+ base_inputs = super().get_all_inputs()
665
+
666
+ # Add the additional capacity slider inputs
667
+ capacity_inputs = [
668
+ Input(self.reversible_capacity_slider.slider_id, 'value'),
669
+ Input(self.reversible_capacity_slider.input_id, 'value'),
670
+ Input(self.irreversible_capacity_slider.slider_id, 'value'),
671
+ Input(self.irreversible_capacity_slider.input_id, 'value')
672
+ ]
673
+
674
+ return base_inputs + capacity_inputs
675
+
676
+ def get_all_outputs(self):
677
+ """
678
+ Get Output objects for updating all component values.
679
+
680
+ Returns:
681
+ list: List containing Output objects for all components.
682
+ """
683
+ # Get base outputs from parent
684
+ base_outputs = super().get_all_outputs()
685
+
686
+ # Add the additional capacity slider outputs
687
+ capacity_outputs = [
688
+ Output(self.reversible_capacity_slider.slider_id, 'value'),
689
+ Output(self.reversible_capacity_slider.input_id, 'value'),
690
+ Output(self.irreversible_capacity_slider.slider_id, 'value'),
691
+ Output(self.irreversible_capacity_slider.input_id, 'value')
692
+ ]
693
+
694
+ return base_outputs + capacity_outputs
@@ -62,6 +62,7 @@ class SliderWithTextInput:
62
62
  slider_disable: bool = False,
63
63
  div_width: str = 'calc(90%)',
64
64
  message: str = None,
65
+ marks: dict = None,
65
66
  ):
66
67
  """
67
68
  Initialize the SliderWithTextInput component.
@@ -94,6 +95,11 @@ class SliderWithTextInput:
94
95
  message (str, optional): Optional message to display between the title
95
96
  and slider. If None, no message is displayed.
96
97
  Defaults to None.
98
+ marks (dict, optional): Pre-computed marks dictionary for the slider.
99
+ If provided, these marks will be used instead of
100
+ auto-generating marks from mark_interval.
101
+ Keys should be numeric positions, values should be labels.
102
+ Defaults to None.
97
103
 
98
104
  Raises:
99
105
  ValueError: If min_val >= max_val, or if step <= 0, or if mark_interval <= 0.
@@ -112,6 +118,7 @@ class SliderWithTextInput:
112
118
  self.div_width = div_width
113
119
  self.slider_disable = slider_disable
114
120
  self.message = message
121
+ self.marks = marks
115
122
 
116
123
  self.slider_id = self._make_id('slider')
117
124
  self.input_id = self._make_id('input')
@@ -149,10 +156,17 @@ class SliderWithTextInput:
149
156
  tick marks, and styling options.
150
157
 
151
158
  Note:
152
- - Tick marks are generated at intervals specified by mark_interval
159
+ - Uses pre-computed marks if provided, otherwise generates tick marks
160
+ at intervals specified by mark_interval
153
161
  - updatemode is set to 'mouseup' to reduce callback frequency
154
162
  - The slider can be disabled via the slider_disable attribute
155
163
  """
164
+ # Use provided marks or generate them from mark_interval
165
+ if self.marks is not None:
166
+ slider_marks = self.marks
167
+ else:
168
+ slider_marks = {int(i): "" for i in np.arange(self.min_val, self.max_val + self.mark_interval, self.mark_interval)}
169
+
156
170
  return ds.dcc.Slider(
157
171
  id=self.slider_id,
158
172
  min=self.min_val,
@@ -160,7 +174,7 @@ class SliderWithTextInput:
160
174
  value=self.default_val,
161
175
  step=self.step,
162
176
  disabled=self.slider_disable,
163
- marks={int(i): "" for i in np.arange(self.min_val, self.max_val + self.mark_interval, self.mark_interval)},
177
+ marks=slider_marks,
164
178
  updatemode='mouseup'
165
179
  )
166
180
 
@@ -218,7 +232,7 @@ class SliderWithTextInput:
218
232
 
219
233
  # Build the component list
220
234
  components = [
221
- ds.html.P(slider_title, style={'margin-left': '20px', 'margin-bottom': '0px'})
235
+ ds.html.P(slider_title, style={'margin-left': '20px', 'margin-bottom': '0px', 'font-weight': 'bold'})
222
236
  ]
223
237
 
224
238
  # Add optional message if provided
@@ -7,7 +7,6 @@ slider configurations based on parameter ranges.
7
7
 
8
8
  import math
9
9
  from typing import List, Union, Dict
10
- import numpy as np
11
10
 
12
11
 
13
12
 
@@ -179,11 +178,17 @@ def calculate_mark_intervals(min_values: List[float], max_values: List[float],
179
178
  """
180
179
  Calculate mark intervals for sliders based on range magnitude.
181
180
 
182
- Creates marks at regular intervals that align with intuitive values:
183
- - Range < 1: marks on every 0.1 (interval = 0.1)
184
- - Range < 10: marks on every integer (interval = 1.0)
185
- - Range < 100: marks on every multiple of 10 (interval = 10.0)
186
- - Range >= 100: marks on every multiple of 100 (interval = 100.0)
181
+ Creates marks at regular intervals that align with intuitive values while
182
+ avoiding overcrowding by ensuring no more than ~5-6 marks per slider:
183
+ - Range < 0.5: marks on every 0.1, but max 5 marks
184
+ - Range < 1: marks on every 0.2 (interval = 0.2)
185
+ - Range < 5: marks on every 1.0 (interval = 1.0)
186
+ - Range < 10: marks on every 2.0 (interval = 2.0)
187
+ - Range < 50: marks on every 10.0 (interval = 10.0)
188
+ - Range < 100: marks on every 20.0 (interval = 20.0)
189
+ - Range < 500: marks on every 100.0 (interval = 100.0)
190
+ - Range < 1000: marks on every 200.0 (interval = 200.0)
191
+ - Range >= 1000: marks on every 500.0 (interval = 500.0)
187
192
 
188
193
  Args:
189
194
  min_values (List[float]): List of minimum values for each parameter
@@ -195,10 +200,10 @@ def calculate_mark_intervals(min_values: List[float], max_values: List[float],
195
200
 
196
201
  Examples:
197
202
  >>> min_vals = [0, 0, 0, 0]
198
- >>> max_vals = [0.5, 5, 50, 500]
203
+ >>> max_vals = [0.3, 2, 250, 2000]
199
204
  >>> intervals = calculate_mark_intervals(min_vals, max_vals)
200
205
  >>> intervals
201
- [0.1, 1.0, 10.0, 100.0]
206
+ [0.1, 1.0, 100.0, 500.0]
202
207
  """
203
208
  if len(min_values) != len(max_values):
204
209
  raise ValueError("min_values and max_values must have the same length")
@@ -212,24 +217,40 @@ def calculate_mark_intervals(min_values: List[float], max_values: List[float],
212
217
  intervals.append(1.0)
213
218
  continue
214
219
 
215
- # Determine interval based on range magnitude
216
- # Also consider the number of marks to avoid overcrowding
217
- if range_val < 1:
218
- interval = 0.1 # Every 0.1 for very small ranges
220
+ # Determine interval based on range magnitude to avoid overcrowding
221
+ if range_val < 0.5:
222
+ # Very small ranges: use 0.1 but ensure max 5 marks
223
+ interval = 0.1
224
+ num_marks = range_val / interval
225
+ if num_marks > 5:
226
+ interval = range_val / 4 # Limit to ~4 marks
227
+ elif range_val < 1:
228
+ interval = 0.2 # Every 0.2 for ranges like 0.5-0.9
229
+ elif range_val < 5:
230
+ interval = 1.0 # Every integer for small ranges
219
231
  elif range_val < 10:
220
- interval = 1.0 # Every integer for ranges like 0-9.9
221
- elif range_val < 100:
232
+ interval = 2.0 # Every 2 units
233
+ elif range_val < 50:
222
234
  interval = 10.0 # Every multiple of 10
223
- else:
235
+ elif range_val < 100:
236
+ interval = 20.0 # Every multiple of 20
237
+ elif range_val < 500:
224
238
  interval = 100.0 # Every multiple of 100
239
+ elif range_val < 1000:
240
+ interval = 200.0 # Every multiple of 200
241
+ else:
242
+ interval = 500.0 # Every multiple of 500 for very large ranges
225
243
 
226
244
  intervals.append(interval)
227
245
 
228
246
  return intervals
229
247
 
230
248
 
231
- def create_slider_config(min_values: List[float], max_values: List[float],
232
- property_values: List[float] = None) -> dict:
249
+ def create_slider_config(
250
+ min_values: List[float],
251
+ max_values: List[float],
252
+ property_values: List[float] = None
253
+ ) -> dict:
233
254
  """
234
255
  Create complete slider configurations with automatically calculated steps and marks.
235
256
 
@@ -1,6 +1,7 @@
1
1
  import sqlite3 as sql
2
2
  from pathlib import Path
3
3
  import pandas as pd
4
+ import importlib.resources
4
5
 
5
6
  from steer_core.Constants.Units import *
6
7
 
@@ -9,9 +10,10 @@ class DataManager:
9
10
 
10
11
  def __init__(self):
11
12
 
12
- self._db_path = (Path(__file__).parent / '../steer_core/Data/database.db').resolve()
13
- self._connection = sql.connect(self._db_path)
14
- self._cursor = self._connection.cursor()
13
+ with importlib.resources.path("steer_core.Data", "database.db") as db_path:
14
+ self._db_path = db_path
15
+ self._connection = sql.connect(self._db_path)
16
+ self._cursor = self._connection.cursor()
15
17
 
16
18
  def create_table(self, table_name: str, columns: dict):
17
19
  """
@@ -0,0 +1 @@
1
+ __version__ = "0.1.10"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: steer-core
3
- Version: 0.1.8
3
+ Version: 0.1.10
4
4
  Summary: Modelling energy storage from cell to site - STEER OpenCell Design
5
5
  Home-page: https://github.com/nicholas9182/steer-core/
6
6
  Author: Nicholas Siemons
@@ -9,7 +9,7 @@ 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/Components/CompoundComponents.py
12
+ steer_core/Apps/Components/MaterialSelectors.py
13
13
  steer_core/Apps/Components/RangeSliderComponents.py
14
14
  steer_core/Apps/Components/SliderComponents.py
15
15
  steer_core/Apps/Components/__init__.py
@@ -22,6 +22,7 @@ steer_core/Constants/Universal.py
22
22
  steer_core/Constants/__init__.py
23
23
  steer_core/ContextManagers/__init__.py
24
24
  steer_core/Data/__init__.py
25
+ steer_core/Data/database.db
25
26
  steer_core/Decorators/Coordinates.py
26
27
  steer_core/Decorators/Electrochemical.py
27
28
  steer_core/Decorators/General.py
@@ -1 +0,0 @@
1
- __version__ = "0.1.8"
File without changes
File without changes