1
1
from importlib .metadata import version
2
2
__version__ = version ("altrios" )
3
3
4
- from pathlib import Path
5
- import re
6
4
import numpy as np
5
+ import re
7
6
import logging
8
- import inspect
9
- from typing import List , Union , Dict , Optional
7
+ from typing import List , Union , Dict , Optional , Any
10
8
from typing_extensions import Self
11
9
import pandas as pd
12
10
import polars as pl
13
11
14
12
from altrios .loaders .powertrain_components import _res_from_excel
15
13
from altrios .utilities import set_param_from_path # noqa: F401
16
14
from altrios .utilities import copy_demo_files # noqa: F401
17
- from altrios import utilities as utils
18
- from altrios .utilities import package_root , resources_root
15
+ from altrios import utilities as utils # noqa: F401
16
+ from altrios .utilities import package_root , resources_root # noqa: F401
19
17
# make everything in altrios_pyo3 available here
20
- from altrios .altrios_pyo3 import *
21
- from altrios import *
18
+ from altrios .altrios_pyo3 import * # noqa: F403
19
+ from altrios import * # noqa: F403
22
20
23
21
DEFAULT_LOGGING_CONFIG = dict (
24
22
format = "%(asctime)s.%(msecs)03d | %(filename)s:%(lineno)s | %(levelname)s: %(message)s" ,
29
27
logging .basicConfig (** DEFAULT_LOGGING_CONFIG )
30
28
logger = logging .getLogger (__name__ )
31
29
30
+
32
31
def __array__ (self ):
33
32
return np .array (self .tolist ())
34
33
35
- # creates a list of all python classes from rust structs that need variable_path_list() and
36
- # history_path_list() added as methods
34
+
35
+ # creates a list of all python classes from rust structs that need python-side serde helpers
37
36
ACCEPTED_RUST_STRUCTS = [
38
- attr for attr in altrios_pyo3 .__dir__ () if not attr .startswith ("__" ) and isinstance (getattr (altrios_pyo3 , attr ), type ) and
39
- attr [0 ].isupper ()
37
+ attr for attr in altrios_pyo3 .__dir__ () if not attr .startswith ("__" ) and isinstance (getattr (altrios_pyo3 , attr ), type ) and # noqa: F405
38
+ attr [0 ].isupper ()
40
39
]
41
40
42
- def variable_path_list (self , element_as_list :bool = False ) -> List [str ]:
43
- """
44
- Returns list of key paths to all variables and sub-variables within
45
- dict version of `self`. See example usage in `altrios/demos/
46
- demo_variable_paths.py`.
47
-
48
- # Arguments:
49
- - `element_as_list`: if True, each element is itself a list of the path elements
50
- """
51
- return variable_path_list_from_py_objs (self .to_pydict (), element_as_list = element_as_list )
52
-
53
- def variable_path_list_from_py_objs (
54
- obj : Union [Dict , List ],
55
- pre_path :Optional [str ]= None ,
56
- element_as_list :bool = False ,
57
- ) -> List [str ]:
58
- """
59
- Returns list of key paths to all variables and sub-variables within
60
- dict version of class. See example usage in `altrios/demos/
61
- demo_variable_paths.py`.
62
-
63
- # Arguments:
64
- - `obj`: altrios object in dictionary form from `to_pydict()`
65
- - `pre_path`: This is used to call the method recursively and should not be
66
- specified by user. Specifies a path to be added in front of all paths
67
- returned by the method.
68
- - `element_as_list`: if True, each element is itself a list of the path elements
69
- """
70
- key_paths = []
71
- if isinstance (obj , dict ):
72
- for key , val in obj .items ():
73
- # check for nested dicts and call recursively
74
- if isinstance (val , dict ):
75
- key_path = f"['{ key } ']" if pre_path is None else pre_path + f"['{ key } ']"
76
- key_paths .extend (variable_path_list_from_py_objs (val , key_path ))
77
- # check for lists or other iterables that do not contain numeric data
78
- elif "__iter__" in dir (val ) and (len (val ) > 0 ) and not (isinstance (val [0 ], float ) or isinstance (val [0 ], int )):
79
- key_path = f"['{ key } ']" if pre_path is None else pre_path + f"['{ key } ']"
80
- key_paths .extend (variable_path_list_from_py_objs (val , key_path ))
81
- else :
82
- key_path = f"['{ key } ']" if pre_path is None else pre_path + f"['{ key } ']"
83
- key_paths .append (key_path )
84
-
85
- elif isinstance (obj , list ):
86
- for key , val in enumerate (obj ):
87
- # check for nested dicts and call recursively
88
- if isinstance (val , dict ):
89
- key_path = f"[{ key } ]" if pre_path is None else pre_path + f"[{ key } ]"
90
- key_paths .extend (variable_path_list_from_py_objs (val , key_path ))
91
- # check for lists or other iterables that do not contain numeric data
92
- elif "__iter__" in dir (val ) and (len (val ) > 0 ) and not (isinstance (val [0 ], float ) or isinstance (val [0 ], int )):
93
- key_path = f"[{ key } ]" if pre_path is None else pre_path + f"[{ key } ]"
94
- key_paths .extend (variable_path_list_from_py_objs (val , key_path ))
95
- else :
96
- key_path = f"[{ key } ]" if pre_path is None else pre_path + f"[{ key } ]"
97
- key_paths .append (key_path )
98
- if element_as_list :
99
- re_for_elems = re .compile ("\\ [('(\\ w+)'|(\\ w+))\\ ]" )
100
- for i , kp in enumerate (key_paths ):
101
- kp : str
102
- groups = re_for_elems .findall (kp )
103
- selected = [g [1 ] if len (g [1 ]) > 0 else g [2 ] for g in groups ]
104
- key_paths [i ] = selected
105
-
106
- return key_paths
107
-
108
- def history_path_list (self , element_as_list :bool = False ) -> List [str ]:
109
- """
110
- Returns a list of relative paths to all history variables (all variables
111
- that contain history as a subpath).
112
- See example usage in `altrios/demo_data/demo_variable_paths.py`.
113
-
114
- # Arguments
115
- - `element_as_list`: if True, each element is itself a list of the path elements
116
- """
117
- item_str = lambda item : item if not element_as_list else "." .join (item )
118
- history_path_list = [
119
- item for item in self .variable_path_list (
120
- element_as_list = element_as_list ) if "history" in item_str (item )
121
- ]
122
- return history_path_list
123
-
124
41
# TODO connect to crate features
125
42
data_formats = [
126
43
'yaml' ,
@@ -129,6 +46,7 @@ def history_path_list(self, element_as_list:bool=False) -> List[str]:
129
46
'json' ,
130
47
]
131
48
49
+
132
50
def to_pydict (self , data_fmt : str = "msg_pack" , flatten : bool = False ) -> Dict :
133
51
"""
134
52
Returns self converted to pure python dictionary with no nested Rust objects
@@ -140,10 +58,10 @@ def to_pydict(self, data_fmt: str = "msg_pack", flatten: bool = False) -> Dict:
140
58
assert data_fmt in data_formats , f"`data_fmt` must be one of { data_formats } "
141
59
match data_fmt :
142
60
case "msg_pack" :
143
- import msgpack
61
+ import msgpack # type: ignore[import-untyped]
144
62
pydict = msgpack .loads (self .to_msg_pack ())
145
63
case "yaml" :
146
- from yaml import load
64
+ from yaml import load # type: ignore[import-untyped]
147
65
try :
148
66
from yaml import CLoader as Loader
149
67
except ImportError :
@@ -156,10 +74,14 @@ def to_pydict(self, data_fmt: str = "msg_pack", flatten: bool = False) -> Dict:
156
74
if not flatten :
157
75
return pydict
158
76
else :
159
- return next (iter (pd .json_normalize (pydict , sep = "." ).to_dict (orient = 'records' )))
77
+ hist_len = get_hist_len (pydict )
78
+ assert hist_len is not None , "Cannot be flattened"
79
+ flat_dict = get_flattened (pydict , hist_len )
80
+ return flat_dict
81
+
160
82
161
83
@classmethod
162
- def from_pydict (cls , pydict : Dict , data_fmt : str = "msg_pack" , skip_init : bool = True ) -> Self :
84
+ def from_pydict (cls , pydict : Dict , data_fmt : str = "msg_pack" , skip_init : bool = False ) -> Self :
163
85
"""
164
86
Instantiates Self from pure python dictionary
165
87
# Arguments
@@ -176,56 +98,123 @@ def from_pydict(cls, pydict: Dict, data_fmt: str = "msg_pack", skip_init: bool =
176
98
obj = cls .from_yaml (yaml .dump (pydict ), skip_init = skip_init )
177
99
case "msg_pack" :
178
100
import msgpack
179
- try :
180
- obj = cls .from_msg_pack (
181
- msgpack .packb (pydict ), skip_init = skip_init )
182
- except Exception as err :
183
- print (
184
- f"{ err } \n Falling back to YAML." )
185
- obj = cls .from_pydict (
186
- pydict , data_fmt = "yaml" , skip_init = skip_init )
101
+ obj = cls .from_msg_pack (
102
+ msgpack .packb (pydict ), skip_init = skip_init )
187
103
case "json" :
188
104
from json import dumps
189
105
obj = cls .from_json (dumps (pydict ), skip_init = skip_init )
190
106
191
107
return obj
192
108
193
- def to_dataframe (self , pandas :bool = False ) -> [pd .DataFrame , pl .DataFrame , pl .LazyFrame ]:
109
+
110
+ def get_flattened (obj : Dict | List , hist_len : int , prepend_str : str = "" ) -> Dict :
111
+ """
112
+ Flattens and returns dictionary, separating keys and indices with a `"."`
113
+ # Arguments
114
+ # - `obj`: object to flatten
115
+ # - hist_len: length of any lists storing history data
116
+ # - `prepend_str`: prepend this to all keys in the returned `flat` dict
194
117
"""
195
- Returns time series results from altrios object as a Polars or Pandas dataframe.
118
+ flat : Dict = {}
119
+ if isinstance (obj , dict ):
120
+ for (k , v ) in obj .items ():
121
+ new_key = k if (prepend_str == "" ) else prepend_str + "." + k
122
+ if isinstance (v , dict ) or (isinstance (v , list ) and len (v ) != hist_len ):
123
+ flat .update (get_flattened (v , hist_len , prepend_str = new_key ))
124
+ else :
125
+ flat [new_key ] = v
126
+ elif isinstance (obj , list ):
127
+ for (i , v ) in enumerate (obj ):
128
+ new_key = i if (prepend_str == "" ) else prepend_str + "." + f"[{ i } ]"
129
+ if isinstance (v , dict ) or (isinstance (v , list ) and len (v ) != hist_len ):
130
+ flat .update (get_flattened (v , hist_len , prepend_str = new_key ))
131
+ else :
132
+ flat [new_key ] = v
133
+ else :
134
+ raise TypeError ("`obj` should be `dict` or `list`" )
135
+
136
+ return flat
137
+
138
+
139
+ def get_hist_len (obj : Dict ) -> Optional [int ]:
140
+ """
141
+ Finds nested `history` and gets lenth of first element
142
+ """
143
+ if 'history' in obj .keys ():
144
+ return len (next (iter (obj ['history' ].values ())))
145
+
146
+ elif next (iter (k for k in obj .keys () if re .search ("(history\\ .\\ w+)$" , k ) is not None ), None ) is not None :
147
+ return len (next ((v for (k , v ) in obj .items () if re .search ("(history\\ .\\ w+)$" , k ) is not None )))
148
+
149
+ for (k , v ) in obj .items ():
150
+ if isinstance (v , dict ):
151
+ hist_len = get_hist_len (v )
152
+ if hist_len is not None :
153
+ return hist_len
154
+ return None
155
+
156
+
157
+ def to_dataframe (self , pandas : bool = False , allow_partial : bool = False ) -> Union [pd .DataFrame , pl .DataFrame ]:
158
+ """
159
+ Returns time series results from fastsim object as a Polars or Pandas dataframe.
196
160
197
161
# Arguments
198
162
- `pandas`: returns pandas dataframe if True; otherwise, returns polars dataframe by default
163
+ - `allow_partial`: tries to return dataframe of length equal to solved time steps if simulation fails early
199
164
"""
200
- obj_dict = self .to_pydict ()
201
- history_paths = self .history_path_list (element_as_list = True )
202
- cols = ["." .join (hp ) for hp in history_paths ]
203
- vals = []
204
- for hp in history_paths :
205
- obj :Union [dict | list ] = obj_dict
206
- for elem in hp :
207
- try :
208
- obj = obj [elem ]
209
- except :
210
- obj = obj [int (elem )]
211
- vals .append (obj )
212
- if not pandas :
213
- df = pl .DataFrame ({col : val for col , val in zip (cols , vals )})
165
+ obj_dict = self .to_pydict (flatten = True )
166
+ history_keys = ['history.' , 'speed_trace.' , 'power_trace.' ]
167
+ hist_len = get_hist_len (obj_dict )
168
+ assert hist_len is not None
169
+
170
+ history_dict : Dict [str , Any ] = {}
171
+ for k , v in obj_dict .items ():
172
+ hk_in_k = any (hk in k for hk in history_keys )
173
+ if hk_in_k and ("__len__" in dir (v )):
174
+ if (len (v ) == hist_len ) or allow_partial :
175
+ history_dict [k ] = v
176
+
177
+ if allow_partial :
178
+ cutoff = min ([len (val ) for val in history_dict .values ()])
179
+
180
+ if not pandas :
181
+ try :
182
+ df = pl .DataFrame ({col : val [:cutoff ]
183
+ for col , val in history_dict .items ()})
184
+ except Exception as err :
185
+ raise Exception (f"{ err } \n `save_interval` may not be uniform" )
186
+ else :
187
+ try :
188
+ df = pd .DataFrame ({col : val [:cutoff ]
189
+ for col , val in history_dict .items ()})
190
+ except Exception as err :
191
+ raise Exception (f"{ err } \n `save_interval` may not be uniform" )
192
+
214
193
else :
215
- df = pd .DataFrame ({col : val for col , val in zip (cols , vals )})
194
+ if not pandas :
195
+ try :
196
+ df = pl .DataFrame (history_dict )
197
+ except Exception as err :
198
+ raise Exception (
199
+ f"{ err } \n Try passing `allow_partial=True` to `to_dataframe` or checking for consistent save intervals" )
200
+ else :
201
+ try :
202
+ df = pd .DataFrame (history_dict )
203
+ except Exception as err :
204
+ raise Exception (
205
+ f"{ err } \n Try passing `allow_partial=True` to `to_dataframe` or checking for consistent save intervals" )
216
206
return df
217
207
208
+
218
209
# adds variable_path_list() and history_path_list() as methods to all classes in
219
210
# ACCEPTED_RUST_STRUCTS
220
211
for item in ACCEPTED_RUST_STRUCTS :
221
- setattr (getattr (altrios_pyo3 , item ), "variable_path_list" , variable_path_list )
222
- setattr (getattr (altrios_pyo3 , item ), "history_path_list" , history_path_list )
223
- setattr (getattr (altrios_pyo3 , item ), "to_pydict" , to_pydict )
224
- setattr (getattr (altrios_pyo3 , item ), "from_pydict" , from_pydict )
225
- setattr (getattr (altrios_pyo3 , item ), "to_dataframe" , to_dataframe )
212
+ setattr (getattr (altrios_pyo3 , item ), "to_pydict" , to_pydict ) # noqa: F405
213
+ setattr (getattr (altrios_pyo3 , item ), "from_pydict" , from_pydict ) # noqa: F405
214
+ setattr (getattr (altrios_pyo3 , item ), "to_dataframe" , to_dataframe ) # noqa: F405
226
215
227
216
setattr (ReversibleEnergyStorage , "from_excel" , classmethod (_res_from_excel )) # noqa: F405
228
217
setattr (Pyo3VecWrapper , "__array__" , __array__ ) # noqa: F405
229
- setattr (Pyo3Vec2Wrapper , "__array__" , __array__ )
230
- setattr (Pyo3Vec3Wrapper , "__array__" , __array__ )
231
- setattr (Pyo3VecBoolWrapper , "__array__" , __array__ )
218
+ setattr (Pyo3Vec2Wrapper , "__array__" , __array__ ) # noqa: F405
219
+ setattr (Pyo3Vec3Wrapper , "__array__" , __array__ ) # noqa: F405
220
+ setattr (Pyo3VecBoolWrapper , "__array__" , __array__ ) # noqa: F405
0 commit comments