Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

BEL can fail trace without triggering error #99

Draft
wants to merge 6 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 15 additions & 9 deletions python/altrios/__init__.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
from pkg_resources import get_distribution
__version__ = get_distribution("altrios").version

from importlib.metadata import version
__version__ = version("altrios")
from pathlib import Path
import re
import numpy as np
Expand Down Expand Up @@ -48,7 +47,7 @@ def variable_path_list(self, element_as_list:bool=False) -> List[str]:
# Arguments:
- `element_as_list`: if True, each element is itself a list of the path elements
"""
return variable_path_list_from_py_objs(self.to_pydict(), element_as_list=element_as_list)
return variable_path_list_from_py_objs(self.to_pydict(flatten=False), element_as_list=element_as_list)

def variable_path_list_from_py_objs(
obj: Union[Dict, List],
Expand Down Expand Up @@ -121,25 +120,32 @@ def history_path_list(self, element_as_list:bool=False) -> List[str]:
]
return history_path_list

def to_pydict(self) -> Dict:
def to_pydict(self, flatten: bool=True) -> Dict:
"""
Returns self converted to pure python dictionary with no nested Rust objects
# Arguments
- `flatten`: if True, returns dict without any hierarchy
"""
from yaml import load
try:
from yaml import CLoader as Loader
except ImportError:
from yaml import Loader
pydict = load(self.to_yaml(), Loader = Loader)
return pydict

pydict = load(self.to_yaml(), Loader=Loader)
if not flatten:
return pydict
else:
return next(iter(pd.json_normalize(pydict, sep=".").to_dict(orient='records')))

@classmethod
def from_pydict(cls, pydict: Dict) -> Self:
"""
Instantiates Self from pure python dictionary
"""
import yaml
return cls.from_yaml(yaml.dump(pydict),skip_init=False)
return cls.from_yaml(yaml.dump(pydict))


def to_dataframe(self, pandas:bool=False) -> [pd.DataFrame, pl.DataFrame, pl.LazyFrame]:
"""
Expand All @@ -148,7 +154,7 @@ def to_dataframe(self, pandas:bool=False) -> [pd.DataFrame, pl.DataFrame, pl.Laz
# Arguments
- `pandas`: returns pandas dataframe if True; otherwise, returns polars dataframe by default
"""
obj_dict = self.to_pydict()
obj_dict = self.to_pydict(flatten=False)
history_paths = self.history_path_list(element_as_list=True)
cols = [".".join(hp) for hp in history_paths]
vals = []
Expand Down
1 change: 1 addition & 0 deletions python/altrios/altrios_pyo3.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -404,6 +404,7 @@ class LocomotiveSimulation(SerdeAPI):
i: int
loco_unit: Locomotive
power_trace: PowerTrace
allow_trace_miss: Optional[bool]
@classmethod
def __init__(cls) -> None: ...
def clone(self) -> Self: ...
Expand Down
8 changes: 6 additions & 2 deletions python/altrios/demos/bel_demo.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
# %% Copied from ALTRIOS version 'v0.2.3'. Guaranteed compatibility with this version only.
# %%
# Script for running the Wabtech BEL consist for sample data from Barstow to Stockton
# Consist comprises [2X Tier 4](https://www.wabteccorp.com/media/3641/download?inline)
Expand All @@ -10,6 +11,7 @@
import time
import os
import seaborn as sns
import pandas as pd

sns.set_theme()

Expand Down Expand Up @@ -44,7 +46,7 @@

# instantiate battery model
t0 = time.perf_counter()
sim = alt.LocomotiveSimulation(bel, pt, SAVE_INTERVAL)
sim = alt.LocomotiveSimulation(bel, pt, False, SAVE_INTERVAL)
t1 = time.perf_counter()
print(f"Time to load: {t1-t0:.3g}")

Expand All @@ -54,7 +56,7 @@
t1 = time.perf_counter()
print(f"Time to simulate: {t1-t0:.5g}")


#%%
bel_rslt = sim.loco_unit
t_s = np.array(sim.power_trace.time_seconds)

Expand Down Expand Up @@ -102,3 +104,5 @@
if SHOW_PLOTS:
plt.tight_layout()
plt.show()

# %%
231 changes: 231 additions & 0 deletions python/altrios/demos/bel_demo_powertrace_comparison.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,231 @@
# %% Copied from ALTRIOS version 'v0.2.3'. Guaranteed compatibility with this version only.
# %%
# Script for running the Wabtech BEL consist for sample data from Barstow to Stockton
# Consist comprises [2X Tier 4](https://www.wabteccorp.com/media/3641/download?inline)
# + [1x BEL](https://www.wabteccorp.com/media/466/download?inline)


import altrios as alt
import numpy as np
import matplotlib.pyplot as plt
import time
import os
import seaborn as sns
import pandas as pd
import json
import plotly.graph_objects as go
from plotly.subplots import make_subplots
import plotly.express as px

sns.set_theme()

SHOW_PLOTS = alt.utils.show_plots()


SAVE_INTERVAL = 1

#power level in horsepower by notch. negative is dynamic braking
notch_schedule = {-8 : -4400,
-7 : -3740,
-6 : -3070,
-5 : -2350,
-4 : -1765,
-3 : -1265,
-2 : -615,
-1 : -310,
0 : 0,
1 : 310,
2 : 615,
3 : 1265,
4 : 1765,
5 : 2350,
6 : 3070,
7 : 3740,
8 :4400
}


Trace1 = [[1, 120],
[2, 120],
[3, 120],
[4, 3600],
[1, 600],
[0, 120],
[-1, 600],
[-4, 600],
[-1, 120],
]

Trace2 = [[1, 120],
[2, 120],
[3, 120],
[4, 3600],
[1, 600],
[0, 120],
[-4, 600],
[-6, 600],
[-4, 120],
]
traces = [Trace1, Trace2]


def PowerTraceFromNotch(NotchTrace, NotchSchedule, Repeats=1):
TotalSeconds = np.array(NotchTrace)[:,1].sum()
PowerTrace = np.zeros(TotalSeconds)
CurrentSeconds = 0
for NotchDuration in NotchTrace:
print("{}, {}".format(CurrentSeconds, NotchSchedule[NotchDuration[0]]))
PowerTrace[CurrentSeconds:CurrentSeconds+NotchDuration[1]] = NotchSchedule[NotchDuration[0]]
CurrentSeconds = CurrentSeconds + NotchDuration[1]

#TODO: put slew rate limiter on power to make ramped notch transitions

PowerTrace = np.tile(PowerTrace, Repeats)
PowerTrace = PowerTrace * 745.699872 #convert from hp to watts

pt_dict = {
'time_seconds' : np.arange(0,PowerTrace.shape[0]).tolist(),
'pwr_watts' : PowerTrace.tolist(),
'engine_on' : [True] * PowerTrace.shape[0]
}
pt = alt.PowerTrace.from_json(json.dumps(pt_dict))
return pt

PowerTraces = []
for trace in traces:
print('--------------------')
PowerTraces.append(PowerTraceFromNotch(trace, notch_schedule, Repeats = 3))

# pt = alt.PowerTrace.default()
# pt_dict = json.loads(pt.to_json())
# pt_dict['time_seconds'] = np.arange(0,30000,100).tolist()
# pt = pt.from_json(json.dumps(pt_dict))



res = alt.ReversibleEnergyStorage.from_file(
alt.resources_root() /
"powertrains/reversible_energy_storages/Kokam_NMC_75Ah_flx_drive.yaml"
)
# instantiate electric drivetrain (motors and any gearboxes)
edrv = alt.ElectricDrivetrain(
pwr_out_frac_interp=[0., 1.],
eta_interp=[0.98, 0.98],
pwr_out_max_watts=5e9,
save_interval=SAVE_INTERVAL,
)


bel = alt.Locomotive.build_battery_electric_loco(
reversible_energy_storage=res,
drivetrain=edrv,
loco_params=alt.LocoParams.from_dict(dict(
pwr_aux_offset_watts=8.55e3,
pwr_aux_traction_coeff_ratio=540.e-6,
force_max_newtons=667.2e3,
)))

LocoRegenEnergy = []
LocoPositiveEnergy = []
PowerTracePositiveEnergy = []
PowerTraceRegenEnergy = []
FractionOfDemand = []
RegenFraction = []

fig = make_subplots(rows=len(PowerTraces),
cols=1,
specs=[[{"secondary_y": True}]]*len(PowerTraces))

i = 0
for trace in PowerTraces:
print(i)
# instantiate battery model
t0 = time.perf_counter()
sim = alt.LocomotiveSimulation(bel, trace, True, SAVE_INTERVAL)
t1 = time.perf_counter()
print(f"Time to load: {t1-t0:.3g}")

# simulate
t0 = time.perf_counter()
sim.walk()
t1 = time.perf_counter()
print(f"Time to simulate: {t1-t0:.5g}")

bel_rslt = sim.loco_unit

LocoPositiveEnergy.append(np.sum(np.clip(np.array(bel_rslt.history.pwr_out_watts) * 1e-6, a_max=np.inf, a_min=0))/3600)
LocoRegenEnergy.append(np.sum(np.clip(np.array(bel_rslt.history.pwr_out_watts) * 1e-6, a_max=0, a_min=-np.inf))/3600)
PowerTracePositiveEnergy.append(np.sum(np.clip(np.array(sim.power_trace.pwr_watts) * 1e-6, a_max=np.inf,a_min=0))/3600)
PowerTraceRegenEnergy.append(np.sum(np.clip(np.array(sim.power_trace.pwr_watts) * 1e-6, a_max=0, a_min=-np.inf))/3600)
FractionOfDemand.append(LocoPositiveEnergy[-1]/PowerTracePositiveEnergy[-1])
RegenFraction.append(LocoRegenEnergy[-1]/PowerTraceRegenEnergy[-1])
t_s = np.array(sim.power_trace.time_seconds)

fig.add_trace(
go.Scatter(x=t_s,
y=np.array(bel_rslt.history.pwr_out_watts) * 1e-6,
name='Locomotive Power',
marker={'color': px.colors.qualitative.Plotly[0]}),

row=i+1, col=1)

fig.add_trace(
go.Scatter(x=t_s,
y=np.array(sim.power_trace.pwr_watts) * 1e-6,
name='Power Demand',
marker={'color': px.colors.qualitative.Plotly[1]}),

row=i+1, col=1)

fig.add_trace(
go.Scatter(x=t_s,
y=np.array(bel_rslt.res.history.soc) * 100,
name='SOC',
marker={'color': px.colors.qualitative.Plotly[2]}),
secondary_y = True,
row=i+1, col=1)

fig.update_xaxes(tickfont=dict(family='Arial', size=20), row=i+1, col=1)
fig.update_yaxes(tickfont=dict(family='Arial', size=20), range=[-3.1, 3.1], row=i+1, col=1)
fig.update_yaxes(title_text="Power [MW]", title_font=dict(size=24, family='Arial'))
fig.update_yaxes(title_text="SOC [%]", title_font=dict(size=24, family='Arial'), secondary_y=True, range=[0, 100], showgrid=False)
fig.update_xaxes(title_text="Time [s]", title_font=dict(size=20, family='Arial'), row=i+1, col=1)
fig.update_layout(
legend_title=" ",
font=dict(
size=20))

i=i+1

fig.write_html('test.html', auto_open=True)

#%%
Data = pd.DataFrame()
Data['Actual Regen Energy'] = LocoRegenEnergy
Data['Actual Positive Tractive Effort'] =LocoPositiveEnergy
Data['Potential Positive Tractive Effort'] =PowerTracePositiveEnergy
Data['Potential Regen Energy'] = PowerTraceRegenEnergy
Data['Demand Fraction Achieved'] =FractionOfDemand
Data['Regen Fraction'] =RegenFraction
Data['Power Trace Name'] = ['Trace 1','Trace 2']
fig = px.bar(Data, x='Power Trace Name',
y=['Actual Positive Tractive Effort',
'Potential Positive Tractive Effort',
'Actual Regen Energy',
'Potential Regen Energy'],
barmode='group')

fig.update_xaxes(tickfont=dict(family='Arial', size=20), row=i+1, col=1)
fig.update_yaxes(tickfont=dict(family='Arial', size=20), range=[-3.1, 3.1], row=i+1, col=1)
fig.update_yaxes(title_text="Energy [MWh]", title_font=dict(size=24, family='Arial'))
fig.update_xaxes(title_font=dict(size=20, family='Arial'), row=i+1, col=1)
fig.update_layout(
legend_title=" ",
font=dict(
size=20))
fig.write_html('bar.html', auto_open=True)




# %%
2 changes: 1 addition & 1 deletion python/altrios/demos/conv_demo.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@

pt = alt.PowerTrace.default()

sim = alt.LocomotiveSimulation(conv, pt, SAVE_INTERVAL)
sim = alt.LocomotiveSimulation(conv, pt, save_interval=SAVE_INTERVAL)
t1 = time.perf_counter()

print(f"Time to load: {t1-t0:.3g}")
Expand Down
4 changes: 2 additions & 2 deletions python/altrios/tests/mock_resources.py
Original file line number Diff line number Diff line change
Expand Up @@ -143,9 +143,9 @@ def mock_locomotive_simulation(
pt: alt.PowerTrace = mock_power_trace(),
save_interval: Optional[int] = 1,
) -> alt.LocomotiveSimulation:
if not loco:
if loco is None:
loco = mock_conventional_loco(save_interval=save_interval)
sim = alt.LocomotiveSimulation(loco, pt, save_interval)
sim = alt.LocomotiveSimulation(loco, pt, False, save_interval)
return sim


Expand Down
Loading
Loading