Skip to content
Open
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
16 changes: 9 additions & 7 deletions altrios-core/src/consist/locomotive/powertrain/fuel_converter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -80,8 +80,8 @@ pub struct FuelConverter {
pub(crate) pwr_for_peak_eff: si::Power,
/// idle fuel power to overcome internal friction (not including aux load)
pub pwr_idle_fuel: si::Power,
/// Interpolator for derating dynamic engine peak power based on altitude
/// and temperature. When interpolating, this returns fraction of normal
/// Interpolator for derating dynamic engine peak power based on altitude [m]
/// and temperature [*C]. When interpolating, this returns fraction of normal
/// peak power, e.g. a value of 1 means no derating and a value of 0 means
/// the engine is completely disabled.
#[api(skip_get, skip_set)]
Expand Down Expand Up @@ -190,10 +190,12 @@ impl FuelConverter {

let pwr_max_derated = match (&mut self.elev_and_temp_derate, elev_and_temp) {
(Some(elev_and_temp_derate), Some(elev_and_temp)) => {
elev_and_temp_derate.interpolate(&[
let derate_factor = elev_and_temp_derate.interpolate(&[
elev_and_temp.0.get::<si::meter>(),
elev_and_temp.1.get::<si::degree_celsius>(),
])? * self.pwr_out_max
])?;
ensure!(derate_factor <= 1.0 && derate_factor >= 0.0, format_dbg!());
derate_factor * self.pwr_out_max
}
(None, Some(_)) => bail!(
"{}\nExpected (self.elev_and_temp_derate, elev_and_temp) to both be Some or None",
Expand All @@ -209,7 +211,6 @@ impl FuelConverter {
self.pwr_out_max_init = self.pwr_out_max_init.max(self.pwr_out_max / 10.);
self.state.pwr_out_max = (self.state.pwr_mech_out
+ (self.pwr_out_max / self.pwr_ramp_lag) * dt)
.min(self.pwr_out_max)
.min(pwr_max_derated)
.max(self.pwr_out_max_init);
Ok(())
Expand Down Expand Up @@ -319,8 +320,9 @@ impl FuelConverter {
fn set_default_elev_and_temp_derate(&mut self) {
self.elev_and_temp_derate = Some(
Interp2D::new(
array![0.0, 3_000.0, 6_000.0],
array![0.0, 35.0, 45.0, 50.0],
array![0.0, 1_000.0, 2_000.0], // elevation in meters
array![0.0, 35.0, 45.0, 50.0], // temperature in degrees Celsius
// Fraction of static peak power
array![
[1.0, 1.0, 0.95, 0.8],
[0.95, 0.95, 0.9025, 0.76],
Expand Down
54 changes: 16 additions & 38 deletions python/altrios/demos/speed_limit_train_sim_demo_with_derating.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,24 +47,24 @@
fc_with_derate.set_default_elev_and_temp_derate()
fc_with_derate_dict = fc_with_derate.to_pydict()
Copy link
Collaborator Author

@calbaker calbaker May 22, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@FDsteven this fc_with_derate_dict is the conventional FC. We need to make an equivalent for the hybrid and set the derate on it instead!

  • get the HEL dict
  • pull the FC out
  • instantiate a new FC for the HEL
  • run the set_...derate method
  • turn it back into a dict
  • put in the HEL dict
  • turn the HEL dict back into a HEL


hel_new_dict = copy(hel_dict)
hel_new_dict['loco_type']['HybridLoco']['fc'] = fc_with_derate_dict
hel_with_derate = alt.Locomotive.from_pydict(hel_new_dict)
hel_with_derate_dict = copy(hel_dict)
hel_with_derate_dict['loco_type']['HybridLoco']['fc'] = fc_with_derate_dict
hel_with_derate = alt.Locomotive.from_pydict(hel_with_derate_dict)

conv_new_dict = alt.Locomotive.default().to_pydict()
conv_new_dict['loco_type']['ConventionalLoco']['fc'] = fc_with_derate_dict
conv_with_derate = alt.Locomotive.from_pydict(conv_new_dict)
conv_with_derate_dict = alt.Locomotive.default().to_pydict()
conv_with_derate_dict['loco_type']['ConventionalLoco']['fc'] = fc_with_derate_dict
conv_with_derate = alt.Locomotive.from_pydict(conv_with_derate_dict)

# construct a vector of one BEL, one HEL, and several conventional locomotives
loco_vec = (
[hel.clone()]
+ [alt.Locomotive.default()] * 1
[alt.Locomotive.default()] * 2
# + [hel.clone()]
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@FDsteven uncomment this

)

# construct a vector of one BEL, one HEL, and several conventional locomotives
loco_vec_with_derating = (
[hel_with_derate.clone()]
+ [conv_with_derate] * 1
[conv_with_derate] * 2
# + [hel_with_derate.clone()]
)

# instantiate consist
Expand Down Expand Up @@ -476,10 +476,10 @@ def plot_bel_pwr_and_soc(ts: alt.SpeedLimitTrainSim, mod_str: str) -> Tuple[plt.
ax[0].plot(
df_with_derate['history.time_seconds'],
df_with_derate['loco_con.history.energy_fuel_joules'] / 1e9,
label='with derate',
label='with derate',u
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • remove the ,u at the end of this line, d'oh!

)
ax[0].legend()
ax[0].set_ylabel("Cumulative Fuel [GJ]")
ax[0].set_ylabel("Cumu. Fuel [GJ]")

ax[1].plot(
df_no_derate['history.time_seconds'],
Expand All @@ -492,7 +492,7 @@ def plot_bel_pwr_and_soc(ts: alt.SpeedLimitTrainSim, mod_str: str) -> Tuple[plt.
label='with derate pwr max',
)
ax[1].legend()
ax[1].set_ylabel("Engine Power [MW]")
ax[1].set_ylabel("Eng. Pwr. [MW]")

ax[2].plot(
df_no_derate['history.time_seconds'],
Expand All @@ -512,41 +512,19 @@ def plot_bel_pwr_and_soc(ts: alt.SpeedLimitTrainSim, mod_str: str) -> Tuple[plt.
fig0, ax0 = plot_train_level_powers(train_sim, "No Derating")
fig1, ax1 = plot_train_network_info(train_sim, "No Derating")
fig2, ax2 = plot_consist_pwr(train_sim, "No Derating")
fig3, ax3 = plot_hel_pwr_and_soc(train_sim, "No Derating")
# fig3, ax3 = plot_hel_pwr_and_soc(train_sim, "No Derating")
# fig3.savefig("plots/hel with buffers.svg")
# fig4, ax4 = plot_bel_pwr_and_soc(train_sim, "No Derating")

fig0_sans_buffers, ax0_sans_buffers = plot_train_level_powers(train_sim_with_derating, "With Altitude and Temperature Derating")
fig1_sans_buffers, ax1_sans_buffers = plot_train_network_info(train_sim_with_derating, "With Altitude and Temperature Derating")
fig2_sans_buffers, ax2_sans_buffers = plot_consist_pwr(train_sim_with_derating, "With Altitude and Temperature Derating")
fig3_sans_buffers, ax3_sans_buffers = plot_hel_pwr_and_soc(train_sim_with_derating, "With Altitude and Temperature Derating")
# fig3_sans_buffers, ax3_sans_buffers = plot_hel_pwr_and_soc(train_sim_with_derating, "With Altitude and Temperature Derating")
# fig3_sans_buffers.savefig("plots/hel sans buffers.svg")
# fig4_sans_buffers, ax4_sans_buffers = plot_bel_pwr_and_soc(train_sim_sans_buffers, "With Altitude and Temperature Derating")

if SHOW_PLOTS:
plt.tight_layout()
plt.show()
# Impact of sweep of battery capacity TODO: make this happen

# whether to run assertions, enabled by default
ENABLE_ASSERTS = os.environ.get("ENABLE_ASSERTS", "true").lower() == "true"
# whether to override reference files used in assertions, disabled by default
ENABLE_REF_OVERRIDE = os.environ.get(
"ENABLE_REF_OVERRIDE", "false").lower() == "true"
# directory for reference files for checking sim results against expected results
ref_dir = alt.resources_root() / "demo_data/speed_limit_train_sim_demo/"

if ENABLE_REF_OVERRIDE:
ref_dir.mkdir(exist_ok=True, parents=True)
df: pl.DataFrame = train_sim.to_dataframe().lazy().collect()[-1]
df.write_csv(ref_dir / "to_dataframe_expected.csv")
if ENABLE_ASSERTS:
print("Checking output of `to_dataframe`")
to_dataframe_expected = pl.scan_csv(
ref_dir / "to_dataframe_expected.csv").collect()[-1]
assert to_dataframe_expected.equals(train_sim.to_dataframe()[-1]), \
f"to_dataframe_expected: \n{to_dataframe_expected}\ntrain_sim.to_dataframe()[-1]: \n{train_sim.to_dataframe()[-1]}" + \
"\ntry running with `ENABLE_REF_OVERRIDE=True`"
print("Success!")

# %%

Loading