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.
- steer_core-0.1.1/PKG-INFO +29 -0
- steer_core-0.1.1/README.md +0 -0
- steer_core-0.1.1/setup.cfg +4 -0
- steer_core-0.1.1/setup.py +40 -0
- steer_core-0.1.1/steer_core/Constants/Units.py +36 -0
- steer_core-0.1.1/steer_core/Constants/Universal.py +2 -0
- steer_core-0.1.1/steer_core/Constants/__init__.py +0 -0
- steer_core-0.1.1/steer_core/ContextManagers/__init__.py +0 -0
- steer_core-0.1.1/steer_core/DataManager.py +316 -0
- steer_core-0.1.1/steer_core/Decorators/Coordinates.py +46 -0
- steer_core-0.1.1/steer_core/Decorators/Electrochemical.py +28 -0
- steer_core-0.1.1/steer_core/Decorators/General.py +30 -0
- steer_core-0.1.1/steer_core/Decorators/Objects.py +14 -0
- steer_core-0.1.1/steer_core/Decorators/__init__.py +0 -0
- steer_core-0.1.1/steer_core/Mixins/Colors.py +41 -0
- steer_core-0.1.1/steer_core/Mixins/Coordinates.py +338 -0
- steer_core-0.1.1/steer_core/Mixins/Data.py +40 -0
- steer_core-0.1.1/steer_core/Mixins/Serializer.py +45 -0
- steer_core-0.1.1/steer_core/Mixins/__init__.py +0 -0
- steer_core-0.1.1/steer_core/Mixins/validators.py +420 -0
- steer_core-0.1.1/steer_core/__init__.py +1 -0
- steer_core-0.1.1/steer_core.egg-info/PKG-INFO +29 -0
- steer_core-0.1.1/steer_core.egg-info/SOURCES.txt +28 -0
- steer_core-0.1.1/steer_core.egg-info/dependency_links.txt +1 -0
- steer_core-0.1.1/steer_core.egg-info/requires.txt +11 -0
- steer_core-0.1.1/steer_core.egg-info/top_level.txt +1 -0
- steer_core-0.1.1/test/test_compound_components.py +341 -0
- steer_core-0.1.1/test/test_compound_components_clean.py +0 -0
- 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,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
|
+
|
File without changes
|
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
|
+
|