Skip to content

Commit 610c0e5

Browse files
authored
Merge pull request #82 from NREL/car_counting
Enable car-km counting
2 parents dccce05 + c3a8510 commit 610c0e5

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

52 files changed

+2889
-8186
lines changed

.gitignore

+7
Original file line numberDiff line numberDiff line change
@@ -1217,3 +1217,10 @@ code\ meetings
12171217
altrios/resources/networks/Scenic - ALTRIOS Confidential.yaml
12181218
altrios/dispatch/*.json
12191219
activate.sh
1220+
*.xlsx
1221+
altrios/*.txt
1222+
truck_work_log.txt
1223+
vehicle_average_times.txt
1224+
avg_time_per_train.txt
1225+
crane_work_log.txt
1226+
hostler_work_log.txt

pyproject.toml

+13-2
Original file line numberDiff line numberDiff line change
@@ -43,10 +43,11 @@ dependencies = [
4343
"plotly",
4444
"typing_extensions",
4545
"pyyaml",
46-
"polars==0.20.25",
46+
"polars >= 1.20.0",
4747
"pyarrow",
4848
"requests",
4949
"PyYAML==6.0.2",
50+
"simpy",
5051
"msgpack==1.1.0",
5152
]
5253

@@ -55,7 +56,17 @@ homepage = "https://www.nrel.gov/transportation/altrios.html"
5556
source = "https://github.com/NREL/altrios"
5657

5758
[project.optional-dependencies]
58-
dev = ["black", "pytest", "maturin", "ipykernel"]
59+
dev = [
60+
"pytest~=8.3",
61+
"maturin~=1.8",
62+
"ipykernel~=6.29",
63+
"python-lsp-server~=1.10",
64+
"ruff~=0.9",
65+
"pylsp-mypy~=0.7.0",
66+
"memory_profiler~=0.61.0",
67+
"pymoo~=0.6",
68+
"plotly~=6.0",
69+
]
5970

6071
[tool.maturin]
6172
profile = "release"

python/altrios/__init__.py

+120-131
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,22 @@
11
from importlib.metadata import version
22
__version__ = version("altrios")
33

4-
from pathlib import Path
5-
import re
64
import numpy as np
5+
import re
76
import logging
8-
import inspect
9-
from typing import List, Union, Dict, Optional
7+
from typing import List, Union, Dict, Optional, Any
108
from typing_extensions import Self
119
import pandas as pd
1210
import polars as pl
1311

1412
from altrios.loaders.powertrain_components import _res_from_excel
1513
from altrios.utilities import set_param_from_path # noqa: F401
1614
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
1917
# 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
2220

2321
DEFAULT_LOGGING_CONFIG = dict(
2422
format="%(asctime)s.%(msecs)03d | %(filename)s:%(lineno)s | %(levelname)s: %(message)s",
@@ -29,98 +27,17 @@
2927
logging.basicConfig(**DEFAULT_LOGGING_CONFIG)
3028
logger = logging.getLogger(__name__)
3129

30+
3231
def __array__(self):
3332
return np.array(self.tolist())
3433

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
3736
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()
4039
]
4140

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-
12441
# TODO connect to crate features
12542
data_formats = [
12643
'yaml',
@@ -129,6 +46,7 @@ def history_path_list(self, element_as_list:bool=False) -> List[str]:
12946
'json',
13047
]
13148

49+
13250
def to_pydict(self, data_fmt: str = "msg_pack", flatten: bool = False) -> Dict:
13351
"""
13452
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:
14058
assert data_fmt in data_formats, f"`data_fmt` must be one of {data_formats}"
14159
match data_fmt:
14260
case "msg_pack":
143-
import msgpack
61+
import msgpack # type: ignore[import-untyped]
14462
pydict = msgpack.loads(self.to_msg_pack())
14563
case "yaml":
146-
from yaml import load
64+
from yaml import load # type: ignore[import-untyped]
14765
try:
14866
from yaml import CLoader as Loader
14967
except ImportError:
@@ -156,10 +74,14 @@ def to_pydict(self, data_fmt: str = "msg_pack", flatten: bool = False) -> Dict:
15674
if not flatten:
15775
return pydict
15876
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+
16082

16183
@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:
16385
"""
16486
Instantiates Self from pure python dictionary
16587
# Arguments
@@ -176,56 +98,123 @@ def from_pydict(cls, pydict: Dict, data_fmt: str = "msg_pack", skip_init: bool =
17698
obj = cls.from_yaml(yaml.dump(pydict), skip_init=skip_init)
17799
case "msg_pack":
178100
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}\nFalling 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)
187103
case "json":
188104
from json import dumps
189105
obj = cls.from_json(dumps(pydict), skip_init=skip_init)
190106

191107
return obj
192108

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
194117
"""
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.
196160
197161
# Arguments
198162
- `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
199164
"""
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+
214193
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}\nTry 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}\nTry passing `allow_partial=True` to `to_dataframe` or checking for consistent save intervals")
216206
return df
217207

208+
218209
# adds variable_path_list() and history_path_list() as methods to all classes in
219210
# ACCEPTED_RUST_STRUCTS
220211
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
226215

227216
setattr(ReversibleEnergyStorage, "from_excel", classmethod(_res_from_excel)) # noqa: F405
228217
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

Comments
 (0)