steer-core 0.1.1__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 (29) hide show
  1. steer_core-0.1.1/PKG-INFO +29 -0
  2. steer_core-0.1.1/README.md +0 -0
  3. steer_core-0.1.1/setup.cfg +4 -0
  4. steer_core-0.1.1/setup.py +40 -0
  5. steer_core-0.1.1/steer_core/Constants/Units.py +36 -0
  6. steer_core-0.1.1/steer_core/Constants/Universal.py +2 -0
  7. steer_core-0.1.1/steer_core/Constants/__init__.py +0 -0
  8. steer_core-0.1.1/steer_core/ContextManagers/__init__.py +0 -0
  9. steer_core-0.1.1/steer_core/DataManager.py +316 -0
  10. steer_core-0.1.1/steer_core/Decorators/Coordinates.py +46 -0
  11. steer_core-0.1.1/steer_core/Decorators/Electrochemical.py +28 -0
  12. steer_core-0.1.1/steer_core/Decorators/General.py +30 -0
  13. steer_core-0.1.1/steer_core/Decorators/Objects.py +14 -0
  14. steer_core-0.1.1/steer_core/Decorators/__init__.py +0 -0
  15. steer_core-0.1.1/steer_core/Mixins/Colors.py +41 -0
  16. steer_core-0.1.1/steer_core/Mixins/Coordinates.py +338 -0
  17. steer_core-0.1.1/steer_core/Mixins/Data.py +40 -0
  18. steer_core-0.1.1/steer_core/Mixins/Serializer.py +45 -0
  19. steer_core-0.1.1/steer_core/Mixins/__init__.py +0 -0
  20. steer_core-0.1.1/steer_core/Mixins/validators.py +420 -0
  21. steer_core-0.1.1/steer_core/__init__.py +1 -0
  22. steer_core-0.1.1/steer_core.egg-info/PKG-INFO +29 -0
  23. steer_core-0.1.1/steer_core.egg-info/SOURCES.txt +28 -0
  24. steer_core-0.1.1/steer_core.egg-info/dependency_links.txt +1 -0
  25. steer_core-0.1.1/steer_core.egg-info/requires.txt +11 -0
  26. steer_core-0.1.1/steer_core.egg-info/top_level.txt +1 -0
  27. steer_core-0.1.1/test/test_compound_components.py +341 -0
  28. steer_core-0.1.1/test/test_compound_components_clean.py +0 -0
  29. steer_core-0.1.1/test/test_slider_controls.py +266 -0
@@ -0,0 +1,29 @@
1
+ Metadata-Version: 2.4
2
+ Name: steer-core
3
+ Version: 0.1.1
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
13
+ Requires-Dist: numpy
14
+ Requires-Dist: datetime
15
+ Requires-Dist: scipy
16
+ Requires-Dist: shapely
17
+ Requires-Dist: plotly
18
+ Requires-Dist: dash
19
+ Requires-Dist: dash_bootstrap_components
20
+ Requires-Dist: flask_caching
21
+ Requires-Dist: nbformat
22
+ Requires-Dist: scipy
23
+ Dynamic: author
24
+ Dynamic: author-email
25
+ Dynamic: classifier
26
+ Dynamic: home-page
27
+ Dynamic: requires-dist
28
+ Dynamic: requires-python
29
+ Dynamic: summary
File without changes
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,40 @@
1
+ from distutils.core import setup
2
+ from setuptools import find_packages
3
+ import pathlib
4
+ import re
5
+
6
+ root = pathlib.Path(__file__).parent
7
+ init = root / "steer_core" / "__init__.py"
8
+ version = re.search(r'__version__\s*=\s*"([^"]+)"', init.read_text()).group(1)
9
+
10
+ setup(
11
+ name='steer-core',
12
+ version=version,
13
+ description='Modelling energy storage from cell to site - STEER OpenCell Design',
14
+ author='Nicholas Siemons',
15
+ author_email='nsiemons@stanford.edu',
16
+ url="https://github.com/nicholas9182/steer-core/",
17
+ packages=find_packages(),
18
+ install_requires=[
19
+ "pandas",
20
+ "numpy",
21
+ "datetime",
22
+ "scipy",
23
+ "shapely",
24
+ "plotly",
25
+ "dash",
26
+ "dash_bootstrap_components",
27
+ "flask_caching",
28
+ "nbformat",
29
+ "scipy"
30
+ ],
31
+ scripts=[],
32
+ classifiers=[
33
+ "Programming Language :: Python :: 3",
34
+ "License :: OSI Approved :: MIT License",
35
+ "Operating System :: OS Independent",
36
+ ],
37
+ python_requires=">=3.10",
38
+ )
39
+
40
+
@@ -0,0 +1,36 @@
1
+ ## Unit conversions
2
+ # Length units
3
+ KG_TO_G = 1e3
4
+ G_TO_KG = 1e-3
5
+ M_TO_CM = 1e2
6
+ CM_TO_M = 1e-2
7
+ M_TO_MM = 1e3
8
+ MM_TO_M = 1e-3
9
+ M_TO_DM = 1e1
10
+ DM_TO_M = 1e-1
11
+ MG_TO_KG = 1e-6
12
+ KG_TO_MG = 1e6
13
+ M_TO_UM = 1e6
14
+ UM_TO_M = 1e-6
15
+ MM_TO_CM = 1e-1
16
+ CM_TO_MM = 1e1
17
+ UM_TO_MM = 1e-3
18
+ mG_TO_G = 1e-3
19
+ G_TO_mG = 1e3
20
+ CM_TO_UM = 1e4
21
+ UM_TO_CM = 1e-4
22
+
23
+ # Current units
24
+ A_TO_mA = 1e3
25
+ mA_TO_A = 1e-3
26
+
27
+ # Time units
28
+ S_TO_H = 1/3600
29
+ H_TO_S = 3600
30
+
31
+ # Energy units
32
+ W_TO_KW = 1e-3
33
+
34
+ # Angle units
35
+ DEG_TO_RAD = 0.017453292519943295
36
+
@@ -0,0 +1,2 @@
1
+ ## Constants
2
+ PI = 3.14159265358979323846
File without changes
@@ -0,0 +1,316 @@
1
+ import sqlite3 as sql
2
+ from pathlib import Path
3
+ import pandas as pd
4
+
5
+ from steer_core.Constants.Units import *
6
+
7
+
8
+ class DataManager:
9
+
10
+ def __init__(self):
11
+
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()
15
+
16
+ def create_table(self, table_name: str, columns: dict):
17
+ """
18
+ Function to create a table in the database.
19
+
20
+ :param table_name: Name of the table.
21
+ :param columns: Dictionary of columns and their types.
22
+ """
23
+ columns_str = ', '.join([f'{k} {v}' for k, v in columns.items()])
24
+ self._cursor.execute(f'CREATE TABLE IF NOT EXISTS {table_name} ({columns_str})')
25
+ self._connection.commit()
26
+
27
+ def drop_table(self, table_name: str):
28
+ """
29
+ Function to drop a table from the database.
30
+
31
+ :param table_name: Name of the table.
32
+ """
33
+ self._cursor.execute(f'DROP TABLE IF EXISTS {table_name}')
34
+ self._connection.commit()
35
+
36
+ def get_table_names(self):
37
+ """
38
+ Function to get the names of all tables in the database.
39
+
40
+ :return: List of table names.
41
+ """
42
+ self._cursor.execute("SELECT name FROM sqlite_master WHERE type='table';")
43
+ return [row[0] for row in self._cursor.fetchall()]
44
+
45
+ def insert_data(self, table_name: str, data: pd.DataFrame):
46
+ """
47
+ Inserts data into the database only if it doesn’t already exist.
48
+
49
+ :param table_name: Name of the table.
50
+ :param data: DataFrame containing the data to insert.
51
+ """
52
+ for _, row in data.iterrows():
53
+ conditions = ' AND '.join([f"{col} = ?" for col in data.columns])
54
+ check_query = f"SELECT COUNT(*) FROM {table_name} WHERE {conditions}"
55
+
56
+ self._cursor.execute(check_query, tuple(row))
57
+ if self._cursor.fetchone()[0] == 0: # If the row does not exist, insert it
58
+ insert_query = f"INSERT INTO {table_name} ({', '.join(data.columns)}) VALUES ({', '.join(['?'] * len(row))})"
59
+ self._cursor.execute(insert_query, tuple(row))
60
+
61
+ self._connection.commit()
62
+
63
+ def get_data(self,
64
+ table_name: str,
65
+ columns: list = None,
66
+ condition: str | list[str] = None,
67
+ latest_column: str = None):
68
+ """
69
+ Retrieve data from the database.
70
+
71
+ :param table_name: Name of the table.
72
+ :param columns: List of columns to retrieve. If None, retrieves all columns.
73
+ :param condition: Optional condition (single string or list of conditions).
74
+ :param latest_column: Column name to find the most recent row.
75
+ """
76
+ # If columns is not provided, get all columns from the table
77
+ if columns is None:
78
+ self._cursor.execute(f"PRAGMA table_info({table_name})")
79
+ columns_info = self._cursor.fetchall()
80
+ columns = [col[1] for col in columns_info] # Extract column names
81
+ if not columns:
82
+ raise ValueError(f"Table '{table_name}' does not exist or has no columns.")
83
+
84
+ columns_str = ', '.join(columns)
85
+ query = f"SELECT {columns_str} FROM {table_name}"
86
+
87
+ # Add condition if specified
88
+ if condition:
89
+ if isinstance(condition, list):
90
+ condition_str = ' AND '.join(condition)
91
+ else:
92
+ condition_str = condition
93
+ query += f" WHERE {condition_str}"
94
+
95
+ # If latest_column is provided, get the most recent entry
96
+ if latest_column:
97
+ query += f" ORDER BY {latest_column} DESC LIMIT 1"
98
+
99
+ # Execute and return the result
100
+ self._cursor.execute(query)
101
+ data = self._cursor.fetchall()
102
+
103
+ return pd.DataFrame(data, columns=columns)
104
+
105
+ def get_unique_values(self, table_name: str, column_name: str):
106
+ """
107
+ Retrieves all unique values from a specified column.
108
+
109
+ :param table_name: The name of the table.
110
+ :param column_name: The column to retrieve unique values from.
111
+ :return: A list of unique values.
112
+ """
113
+ query = f"SELECT DISTINCT {column_name} FROM {table_name}"
114
+ self._cursor.execute(query)
115
+ return [row[0] for row in self._cursor.fetchall()]
116
+
117
+ def get_current_collector_materials(self, most_recent: bool = True) -> pd.DataFrame:
118
+ """
119
+ Retrieves current collector materials from the database.
120
+
121
+ :param most_recent: If True, returns only the most recent entry.
122
+ :return: DataFrame with current collector materials.
123
+ """
124
+ data = (self
125
+ .get_data(table_name='current_collector_materials')
126
+ .groupby('name', group_keys=False)
127
+ .apply(lambda x: x.sort_values('date', ascending=False).head(1) if most_recent else x)
128
+ .reset_index(drop=True)
129
+ )
130
+
131
+ return data
132
+
133
+ def get_insulation_materials(self, most_recent: bool = True) -> pd.DataFrame:
134
+ """
135
+ Retrieves insulation materials from the database.
136
+
137
+ :param most_recent: If True, returns only the most recent entry.
138
+ :return: DataFrame with insulation materials.
139
+ """
140
+ data = (
141
+ self
142
+ .get_data(
143
+ table_name='insulation_materials'
144
+ ).groupby(
145
+ 'name', group_keys=False
146
+ ).apply(
147
+ lambda x: x.sort_values('date', ascending=False).head(1) if most_recent else x
148
+ ).reset_index(
149
+ drop=True
150
+ )
151
+ )
152
+
153
+ return data
154
+
155
+ def get_cathode_materials(self, most_recent: bool = True) -> pd.DataFrame:
156
+ """
157
+ Retrieves cathode materials from the database.
158
+
159
+ :param most_recent: If True, returns only the most recent entry.
160
+ :return: DataFrame with cathode materials.
161
+ """
162
+ data = (
163
+ self
164
+ .get_data(
165
+ table_name='cathode_materials'
166
+ ).groupby(
167
+ 'name',
168
+ group_keys=False
169
+ ).apply(
170
+ lambda x: x.sort_values('date', ascending=False).head(1) if most_recent else x
171
+ ).reset_index(
172
+ drop=True
173
+ )
174
+ )
175
+
176
+ return data
177
+
178
+ def get_anode_materials(self, most_recent: bool = True) -> pd.DataFrame:
179
+ """
180
+ Retrieves anode materials from the database.
181
+
182
+ :param most_recent: If True, returns only the most recent entry.
183
+ :return: DataFrame with anode materials.
184
+ """
185
+ data = (
186
+ self
187
+ .get_data(
188
+ table_name='anode_materials'
189
+ ).groupby(
190
+ 'name',
191
+ group_keys=False
192
+ ).apply(
193
+ lambda x: x.sort_values('date', ascending=False).head(1) if most_recent else x
194
+ ).reset_index(
195
+ drop=True
196
+ )
197
+ )
198
+
199
+ return data
200
+
201
+ def get_binder_materials(self, most_recent: bool = True) -> pd.DataFrame:
202
+ """
203
+ Retrieves binder materials from the database.
204
+
205
+ :param most_recent: If True, returns only the most recent entry.
206
+ :return: DataFrame with binder materials.
207
+ """
208
+ data = (
209
+ self
210
+ .get_data(
211
+ table_name='binder_materials'
212
+ ).groupby(
213
+ 'name',
214
+ group_keys=False
215
+ ).apply(
216
+ lambda x: x.sort_values('date', ascending=False).head(1) if most_recent else x
217
+ ).reset_index(
218
+ drop=True
219
+ )
220
+ )
221
+
222
+ return data
223
+
224
+ def get_conductive_additive_materials(self, most_recent: bool = True) -> pd.DataFrame:
225
+ """
226
+ Retrieves conductive additives from the database.
227
+
228
+ :param most_recent: If True, returns only the most recent entry.
229
+ :return: DataFrame with conductive additives.
230
+ """
231
+ data = (
232
+ self
233
+ .get_data(
234
+ table_name='conductive_additive_materials'
235
+ ).groupby(
236
+ 'name',
237
+ group_keys=False
238
+ ).apply(
239
+ lambda x: x.sort_values('date', ascending=False).head(1) if most_recent else x
240
+ ).reset_index(
241
+ drop=True
242
+ )
243
+ )
244
+
245
+ return data
246
+
247
+ def get_separator_materials(self, most_recent: bool = True) -> pd.DataFrame:
248
+ """
249
+ Retrieves separator materials from the database.
250
+
251
+ :param most_recent: If True, returns only the most recent entry.
252
+ :return: DataFrame with separator materials.
253
+ """
254
+ data = (
255
+ self
256
+ .get_data(
257
+ table_name='separator_materials'
258
+ ).groupby(
259
+ 'name',
260
+ group_keys=False
261
+ ).apply(
262
+ lambda x: x.sort_values('date', ascending=False).head(1) if most_recent else x
263
+ ).reset_index(
264
+ drop=True
265
+ )
266
+ )
267
+
268
+ return data
269
+
270
+ @staticmethod
271
+ def read_half_cell_curve(half_cell_path) -> pd.DataFrame:
272
+ """
273
+ Function to read in a half cell curve for this active material
274
+
275
+ :param half_cell_path: Path to the half cell data file.
276
+ :return: DataFrame with the specific capacity and voltage.
277
+ """
278
+ try:
279
+ data = pd.read_csv(half_cell_path)
280
+ except:
281
+ raise FileNotFoundError(f"Could not find the file at {half_cell_path}")
282
+
283
+ if 'Specific Capacity (mAh/g)' not in data.columns:
284
+ raise ValueError("The file must have a column named 'Specific Capacity (mAh/g)'")
285
+
286
+ if 'Voltage (V)' not in data.columns:
287
+ raise ValueError("The file must have a column named 'Voltage (V)'")
288
+
289
+ if 'Step_ID' not in data.columns:
290
+ raise ValueError("The file must have a column named 'Step_ID'")
291
+
292
+ data = (data
293
+ .rename(columns={'Specific Capacity (mAh/g)': 'specific_capacity', 'Voltage (V)': 'voltage', 'Step_ID': 'step_id'})
294
+ .assign(specific_capacity=lambda x: x['specific_capacity'] * (H_TO_S * mA_TO_A / G_TO_KG))
295
+ .filter(['specific_capacity', 'voltage', 'step_id'])
296
+ .groupby(['specific_capacity', 'step_id'], group_keys=False)['voltage'].max()
297
+ .reset_index()
298
+ .sort_values(['step_id', 'specific_capacity'])
299
+ )
300
+
301
+ return data
302
+
303
+ def remove_data(self, table_name: str, condition: str):
304
+ """
305
+ Function to remove data from the database.
306
+
307
+ :param table_name: Name of the table.
308
+ :param condition: Condition to remove rows.
309
+ """
310
+ self._cursor.execute(f"DELETE FROM {table_name} WHERE {condition}")
311
+ self._connection.commit()
312
+
313
+ def __del__(self):
314
+ self._connection.close()
315
+
316
+
@@ -0,0 +1,46 @@
1
+ from functools import wraps
2
+
3
+
4
+ def calculate_coordinates(func):
5
+ """
6
+ Decorator to recalculate spatial properties after a method call.
7
+ This is useful for methods that modify the geometry of a component.
8
+ """
9
+ @wraps(func)
10
+ def wrapper(self, *args, **kwargs):
11
+ result = func(self, *args, **kwargs)
12
+ if hasattr(self, '_update_properties') and self._update_properties:
13
+ self._calculate_coordinates()
14
+ return result
15
+ return wrapper
16
+
17
+
18
+ def calculate_areas(func):
19
+ """
20
+ Decorator to recalculate areas after a method call.
21
+ This is useful for methods that modify the geometry of a component.
22
+ """
23
+ @wraps(func)
24
+ def wrapper(self, *args, **kwargs):
25
+ result = func(self, *args, **kwargs)
26
+ if hasattr(self, '_update_properties') and self._update_properties:
27
+ self._calculate_coordinates()
28
+ self._calculate_areas()
29
+ return result
30
+ return wrapper
31
+
32
+
33
+ def calculate_volumes(func):
34
+ """
35
+ Decorator to recalculate volumes after a method call.
36
+ This is useful for methods that modify the geometry of a component.
37
+ """
38
+ @wraps(func)
39
+ def wrapper(self, *args, **kwargs):
40
+ result = func(self, *args, **kwargs)
41
+ if hasattr(self, '_update_properties') and self._update_properties:
42
+ self._calculate_bulk_properties()
43
+ self._calculate_coordinates()
44
+ return result
45
+ return wrapper
46
+
@@ -0,0 +1,28 @@
1
+ from functools import wraps
2
+
3
+ def calculate_half_cell_curve(func):
4
+ """
5
+ Decorator to recalculate half-cell curve properties after a method call.
6
+ This is useful for methods that modify the half-cell curve data.
7
+ """
8
+ @wraps(func)
9
+ def wrapper(self, *args, **kwargs):
10
+ result = func(self, *args, **kwargs)
11
+ if hasattr(self, '_update_properties') and self._update_properties:
12
+ self._calculate_half_cell_curve()
13
+ return result
14
+ return wrapper
15
+
16
+
17
+ def calculate_half_cell_curves_properties(func):
18
+ """
19
+ Decorator to recalculate half-cell curves properties after a method call.
20
+ This is useful for methods that modify the half-cell curves data.
21
+ """
22
+ @wraps(func)
23
+ def wrapper(self, *args, **kwargs):
24
+ result = func(self, *args, **kwargs)
25
+ if hasattr(self, '_update_properties') and self._update_properties:
26
+ self._calculate_half_cell_curves_properties()
27
+ return result
28
+ return wrapper
@@ -0,0 +1,30 @@
1
+ from functools import wraps
2
+
3
+ def calculate_bulk_properties(func):
4
+ """
5
+ Decorator to recalculate bulk properties after a method call.
6
+ This is useful for methods that modify the material properties.
7
+ """
8
+ @wraps(func)
9
+ def wrapper(self, *args, **kwargs):
10
+ result = func(self, *args, **kwargs)
11
+ if hasattr(self, '_update_properties') and self._update_properties:
12
+ self._calculate_bulk_properties()
13
+ return result
14
+ return wrapper
15
+
16
+
17
+ def calculate_all_properties(func):
18
+ """
19
+ Decorator to recalculate both spatial and bulk properties after a method call.
20
+ This is useful for methods that modify both geometry and material properties.
21
+ """
22
+ @wraps(func)
23
+ def wrapper(self, *args, **kwargs):
24
+ result = func(self, *args, **kwargs)
25
+ if hasattr(self, '_update_properties') and self._update_properties:
26
+ self._calculate_all_properties()
27
+ return result
28
+ return wrapper
29
+
30
+
@@ -0,0 +1,14 @@
1
+ from functools import wraps
2
+
3
+ def calculate_weld_tab_properties(func):
4
+ """
5
+ Decorator to recalculate weld tab properties after a method call.
6
+ This is useful for methods that modify the weld tab geometry or material.
7
+ """
8
+ @wraps(func)
9
+ def wrapper(self, *args, **kwargs):
10
+ result = func(self, *args, **kwargs)
11
+ if hasattr(self, '_update_properties') and self._update_properties:
12
+ self._calculate_weld_tab_properties()
13
+ return result
14
+ return wrapper
File without changes
@@ -0,0 +1,41 @@
1
+ import numpy as np
2
+ import plotly.colors as pc
3
+
4
+ import pandas as pd
5
+ import numpy as np
6
+
7
+ class ColorMixin:
8
+ """
9
+ A class to manage colors, including conversion between hex and RGB formats,
10
+ and generating color gradients.
11
+ """
12
+ @staticmethod
13
+ def rgb_tuple_to_hex(rgb):
14
+ return '#{:02x}{:02x}{:02x}'.format(*rgb)
15
+
16
+ @staticmethod
17
+ def get_colorway(color1, color2, n):
18
+ """
19
+ Generate a list of n hex colors interpolated between two HTML hex colors.
20
+
21
+ Parameters
22
+ ----------
23
+ color1 : str
24
+ The first color in HTML hex format (e.g., '#ff0000').
25
+ color2 : str
26
+ The second color in HTML hex format (e.g., '#0000ff').
27
+ n : int
28
+ The number of colors to generate in the gradient.
29
+ """
30
+ # Convert hex to RGB (0–255)
31
+ rgb1 = np.array(pc.hex_to_rgb(color1))
32
+ rgb2 = np.array(pc.hex_to_rgb(color2))
33
+
34
+ # Interpolate and convert to hex
35
+ colors = [
36
+ ColorMixin.rgb_tuple_to_hex(tuple(((1 - t) * rgb1 + t * rgb2).astype(int)))
37
+ for t in np.linspace(0, 1, n)
38
+ ]
39
+
40
+ return colors
41
+