use super::{vehicle_model::VehicleState, *};
use crate::prelude::ElectricMachineState;
#[serde_api]
#[derive(Clone, Debug, PartialEq, Deserialize, Serialize, StateMethods, SetCumulative)]
#[non_exhaustive]
#[serde(deny_unknown_fields)]
#[cfg_attr(feature = "pyo3", pyclass(module = "fastsim", subclass, eq))]
pub struct HybridElectricVehicle {
#[has_state]
pub res: ReversibleEnergyStorage,
pub fs: FuelStorage,
#[has_state]
pub fc: FuelConverter,
#[has_state]
pub em: ElectricMachine,
#[has_state]
pub transmission: Transmission,
#[has_state]
#[serde(default)]
pub pt_cntrl: HEVPowertrainControls,
#[serde(default)]
pub aux_cntrl: HEVAuxControls,
pub(crate) mass: Option<si::Mass>,
#[serde(default)]
pub sim_params: HEVSimulationParams,
#[serde(default)]
pub soc_bal_iter_history: Vec<Self>,
#[serde(default)]
pub soc_bal_iters: TrackedState<u32>,
}
impl HybridElectricVehicle {
pub fn check_buffers(&self, veh_mass: si::Mass) -> anyhow::Result<()> {
let (disch_buffer, chrg_buffer, fc_on_soc) = match &self.pt_cntrl {
HEVPowertrainControls::RGWDB(rgwdb) => {
let disch_buffer = (0.5
* veh_mass
* rgwdb
.speed_soc_disch_buffer
.with_context(|| format_dbg!())?
.powi(P2::new()))
.max(si::Energy::ZERO)
* rgwdb
.speed_soc_disch_buffer_coeff
.with_context(|| format_dbg!())?;
let chrg_buffer = (0.5
* veh_mass
* ((70.0 * uc::MPH).powi(P2::new())
- rgwdb
.speed_soc_regen_buffer
.with_context(|| format_dbg!())?
.powi(P2::new())))
.max(si::Energy::ZERO)
* rgwdb
.speed_soc_regen_buffer_coeff
.with_context(|| format_dbg!())?;
let fc_on_soc = {
let energy_delta_to_buffer_speed: si::Energy = 0.5
* veh_mass
* rgwdb
.speed_soc_fc_on_buffer
.with_context(|| format_dbg!())?
.powi(P2::new());
energy_delta_to_buffer_speed.max(si::Energy::ZERO)
* rgwdb
.speed_soc_fc_on_buffer_coeff
.with_context(|| format_dbg!())?
} / self.res.energy_capacity_usable()
+ self.res.min_soc;
(disch_buffer, chrg_buffer, fc_on_soc)
}
};
if fc_on_soc > self.res.max_soc {
eprintln!("fc_on_soc > self.res.max_soc");
eprintln!("fc_on_soc: {:?}", fc_on_soc);
}
if fc_on_soc < self.res.min_soc {
eprintln!("fc_on_soc < self.res.min_soc");
eprintln!("fc_on_soc: {:?}", fc_on_soc);
}
if disch_buffer > self.res.energy_capacity_usable() {
eprintln!("disch_buffer < self.res.energy_capacity_usable()");
eprintln!(
"disch_buffer: {:?} kWh",
disch_buffer.get::<si::kilowatt_hour>()
);
eprintln!(
"RES usable energy capacity: {:?} kWh",
self.res.energy_capacity_usable().get::<si::kilowatt_hour>()
);
}
if chrg_buffer > self.res.energy_capacity_usable() {
eprintln!("disch_buffer < self.res.energy_capacity_usable()");
eprintln!(
"chrg_buffer: {:?} kWh",
chrg_buffer.get::<si::kilowatt_hour>()
);
eprintln!(
"RES usable energy capacity: {:?} kWh",
self.res.energy_capacity_usable().get::<si::kilowatt_hour>()
);
}
Ok(())
}
}
#[pyo3_api]
impl HybridElectricVehicle {}
impl HistoryMethods for HybridElectricVehicle {
fn save_interval(&self) -> anyhow::Result<Option<usize>> {
bail!("`save_interval` is not implemented in HybridElectricVehicle")
}
fn set_save_interval(&mut self, save_interval: Option<usize>) -> anyhow::Result<()> {
self.res.set_save_interval(save_interval)?;
self.fc.set_save_interval(save_interval)?;
self.em.set_save_interval(save_interval)?;
self.transmission.set_save_interval(save_interval)?;
self.pt_cntrl.set_save_interval(save_interval)?;
Ok(())
}
fn clear(&mut self) {
self.res.clear();
self.fc.clear();
self.em.clear();
self.transmission.clear();
self.pt_cntrl.clear();
}
}
impl Init for HybridElectricVehicle {
fn init(&mut self) -> Result<(), Error> {
self.fc
.init()
.map_err(|err| Error::InitError(format_dbg!(err)))?;
self.res
.init()
.map_err(|err| Error::InitError(format_dbg!(err)))?;
self.em
.init()
.map_err(|err| Error::InitError(format_dbg!(err)))?;
self.transmission
.init()
.map_err(|err| Error::InitError(format_dbg!(err)))?;
self.pt_cntrl
.init()
.map_err(|err| Error::InitError(format_dbg!(err)))?;
Ok(())
}
}
impl SerdeAPI for HybridElectricVehicle {}
impl Powertrain for Box<HybridElectricVehicle> {
fn set_curr_pwr_prop_out_max(
&mut self,
_pwr_upstream: (si::Power, si::Power),
pwr_aux: si::Power,
dt: si::Time,
veh_state: &VehicleState,
) -> anyhow::Result<()> {
let (disch_buffer, chrg_buffer) = match &mut self.pt_cntrl {
HEVPowertrainControls::RGWDB(rgwdb) => {
rgwdb.handle_fc_on_causes(&self.fc, veh_state, &self.res, &self.em.state)?;
let disch_buffer = (0.5
* *veh_state.mass.get_fresh(|| format_dbg!())?
* (rgwdb
.speed_soc_disch_buffer
.with_context(|| format_dbg!())?
.powi(P2::new())
- veh_state
.speed_ach
.get_stale(|| format_dbg!())?
.powi(P2::new())))
.max(si::Energy::ZERO)
* rgwdb
.speed_soc_disch_buffer_coeff
.with_context(|| format_dbg!())?;
let chrg_buffer = (0.5
* *veh_state.mass.get_fresh(|| format_dbg!())?
* (veh_state
.speed_ach
.get_stale(|| format_dbg!())?
.powi(P2::new())
- rgwdb
.speed_soc_regen_buffer
.with_context(|| format_dbg!())?
.powi(P2::new())))
.max(si::Energy::ZERO)
* rgwdb
.speed_soc_regen_buffer_coeff
.with_context(|| format_dbg!())?;
(disch_buffer, chrg_buffer)
}
};
self.fc
.set_curr_pwr_out_max(dt)
.with_context(|| anyhow!(format_dbg!()))?;
self.res
.set_curr_pwr_out_max(dt, disch_buffer, chrg_buffer)
.with_context(|| anyhow!(format_dbg!()))?;
let (pwr_aux_res, pwr_aux_fc) = {
match self.aux_cntrl {
HEVAuxControls::AuxOnResPriority => {
if pwr_aux <= *self.res.state.pwr_disch_max.get_fresh(|| format_dbg!())? {
(pwr_aux, si::Power::ZERO)
} else {
(si::Power::ZERO, pwr_aux)
}
}
HEVAuxControls::AuxOnFcPriority => (si::Power::ZERO, pwr_aux),
}
};
match &mut self.pt_cntrl {
HEVPowertrainControls::RGWDB(rgwdb) => {
rgwdb
.state
.aux_power_demand
.update(pwr_aux_fc > si::Power::ZERO, || format_dbg!())?;
}
}
self.fc
.set_curr_pwr_prop_max(pwr_aux_fc)
.with_context(|| anyhow!(format_dbg!()))?;
self.res
.set_curr_pwr_prop_max(pwr_aux_res)
.with_context(|| anyhow!(format_dbg!()))?;
self.em
.set_curr_pwr_prop_out_max(
self.res
.get_curr_pwr_prop_out_max()
.with_context(|| format_dbg!())?,
pwr_aux,
dt,
veh_state,
)
.with_context(|| anyhow!(format_dbg!()))?;
let em_pwr_prop_out_maxes = self
.em
.get_curr_pwr_prop_out_max()
.with_context(|| format_dbg!())?;
let fc_max = self.fc.state.pwr_prop_max.get_fresh(|| format_dbg!())?;
self.transmission
.set_curr_pwr_prop_out_max(
(em_pwr_prop_out_maxes.0 + *fc_max, em_pwr_prop_out_maxes.1),
f64::NAN * uc::W,
dt,
veh_state,
)
.with_context(|| format_dbg!())?;
Ok(())
}
fn get_curr_pwr_prop_out_max(&self) -> anyhow::Result<(si::Power, si::Power)> {
self.transmission
.get_curr_pwr_prop_out_max()
.with_context(|| format_dbg!())
}
fn solve(
&mut self,
pwr_out_req: si::Power,
_enabled: bool,
dt: si::Time,
) -> anyhow::Result<Option<si::Power>> {
let pwr_in_transmission = self
.transmission
.solve(pwr_out_req, true, dt)
.with_context(|| format_dbg!())?
.with_context(|| format!("{}\nExpected `Some`", format_dbg!()))?;
let (fc_pwr_out_req, em_pwr_out_req) = self
.pt_cntrl
.get_pwr_fc_and_em(pwr_in_transmission, &self.fc, &self.em.state, &self.res)
.with_context(|| format_dbg!())?;
let fc_on: bool = self.pt_cntrl.engine_on()?;
self.fc
.solve(fc_pwr_out_req, fc_on, dt)
.with_context(|| format_dbg!())?;
let res_pwr_out_req = self
.em
.solve(em_pwr_out_req, true, dt)
.with_context(|| format_dbg!())?
.with_context(|| format!("{}\nExpected `Some`", format_dbg!()))?;
self.res
.solve(res_pwr_out_req, dt)
.with_context(|| format_dbg!())?;
Ok(None)
}
fn pwr_regen(&self) -> anyhow::Result<si::Power> {
self.transmission.pwr_regen().with_context(|| format_dbg!())
}
}
impl HybridElectricVehicle {
pub fn solve_thermal(
&mut self,
te_amb: si::Temperature,
pwr_thrml_fc_to_cab: Option<si::Power>,
veh_state: &mut VehicleState,
pwr_thrml_hvac_to_res: Option<si::Power>,
te_cab: Option<si::Temperature>,
dt: si::Time,
) -> anyhow::Result<()> {
self.fc
.solve_thermal(te_amb, pwr_thrml_fc_to_cab, veh_state, dt)
.with_context(|| format_dbg!())?;
self.res
.solve_thermal(
te_amb,
pwr_thrml_hvac_to_res.unwrap_or_default(),
te_cab,
dt,
)
.with_context(|| format_dbg!())?;
Ok(())
}
}
impl TryFrom<&fastsim_2::vehicle::RustVehicle> for HybridElectricVehicle {
type Error = anyhow::Error;
fn try_from(f2veh: &fastsim_2::vehicle::RustVehicle) -> anyhow::Result<HybridElectricVehicle> {
let pt_cntrl = HEVPowertrainControls::RGWDB(Box::new(hev::RESGreedyWithDynamicBuffers {
speed_soc_fc_on_buffer: None,
speed_soc_fc_on_buffer_coeff: None,
speed_soc_disch_buffer: None,
speed_soc_disch_buffer_coeff: None,
speed_soc_regen_buffer: None,
speed_soc_regen_buffer_coeff: None,
fc_min_time_on: None,
speed_fc_forced_on: Some(f2veh.mph_fc_on * uc::MPH),
frac_pwr_demand_fc_forced_on: Some(
f2veh.kw_demand_fc_on / (f2veh.fc_max_kw + f2veh.ess_max_kw.min(f2veh.mc_max_kw))
* uc::R,
),
frac_of_most_eff_pwr_to_run_fc: None,
temp_fc_forced_on: None,
temp_fc_allowed_off: None,
save_interval: Some(1),
state: Default::default(),
history: Default::default(),
}));
let mut hev = HybridElectricVehicle {
fs: {
let mut fs = FuelStorage {
pwr_out_max: f2veh.fs_max_kw * uc::KW,
pwr_ramp_lag: f2veh.fs_secs_to_peak_pwr * uc::S,
energy_capacity: f2veh.fs_kwh * 3.6 * uc::MJ,
specific_energy: None,
mass: None,
};
fs.set_mass(None, MassSideEffect::None)
.with_context(|| anyhow!(format_dbg!()))?;
fs
},
fc: FuelConverter::try_from(f2veh.clone())?,
res: ReversibleEnergyStorage::try_from(f2veh.clone()).with_context(|| format_dbg!())?,
em: ElectricMachine::try_from(f2veh.clone())?,
transmission: Transmission::try_from(f2veh.clone())?,
pt_cntrl,
mass: None,
sim_params: Default::default(),
aux_cntrl: Default::default(),
soc_bal_iter_history: Default::default(),
soc_bal_iters: Default::default(),
};
hev.init()?;
Ok(hev)
}
}
impl Mass for HybridElectricVehicle {
fn mass(&self) -> anyhow::Result<Option<si::Mass>> {
let derived_mass = self
.derived_mass()
.with_context(|| anyhow!(format_dbg!()))?;
match (derived_mass, self.mass) {
(Some(derived_mass), Some(set_mass)) => {
ensure!(
utils::almost_eq_uom(&set_mass, &derived_mass, None),
format!(
"{}",
format_dbg!(utils::almost_eq_uom(&set_mass, &derived_mass, None)),
)
);
Ok(Some(set_mass))
}
_ => Ok(self.mass.or(derived_mass)),
}
}
fn set_mass(
&mut self,
new_mass: Option<si::Mass>,
side_effect: MassSideEffect,
) -> anyhow::Result<()> {
ensure!(
side_effect == MassSideEffect::None,
"At the powertrain level, only `MassSideEffect::None` is allowed"
);
let derived_mass = self
.derived_mass()
.with_context(|| anyhow!(format_dbg!()))?;
self.mass = match new_mass {
Some(new_mass) => {
if let Some(dm) = derived_mass {
if dm != new_mass {
self.expunge_mass_fields();
}
}
Some(new_mass)
}
None => Some(derived_mass.with_context(|| {
format!(
"Not all mass fields in `{}` are set and no mass was provided.",
stringify!(HybridElectricVehicle)
)
})?),
};
Ok(())
}
fn derived_mass(&self) -> anyhow::Result<Option<si::Mass>> {
let fc_mass = self.fc.mass().with_context(|| anyhow!(format_dbg!()))?;
let fs_mass = self.fs.mass().with_context(|| anyhow!(format_dbg!()))?;
let res_mass = self.res.mass().with_context(|| anyhow!(format_dbg!()))?;
let em_mass = self.em.mass().with_context(|| anyhow!(format_dbg!()))?;
let transmission_mass = self
.transmission
.mass()
.with_context(|| anyhow!(format_dbg!()))?;
match (fc_mass, fs_mass, res_mass, em_mass, transmission_mass) {
(
Some(fc_mass),
Some(fs_mass),
Some(res_mass),
Some(em_mass),
Some(transmission_mass),
) => Ok(Some(
fc_mass + fs_mass + res_mass + em_mass + transmission_mass,
)),
(None, None, None, None, None) => Ok(None),
_ => bail!(
"`{}` field masses are not consistently set to `Some` or `None`",
stringify!(HybridElectricVehicle)
),
}
}
fn expunge_mass_fields(&mut self) {
self.fc.expunge_mass_fields();
self.fs.expunge_mass_fields();
self.res.expunge_mass_fields();
self.em.expunge_mass_fields();
self.transmission.expunge_mass_fields();
self.mass = None;
}
}
#[serde_api]
#[derive(
Clone,
Debug,
Default,
Deserialize,
Serialize,
PartialEq,
HistoryVec,
StateMethods,
SetCumulative,
)]
#[non_exhaustive]
#[serde(deny_unknown_fields)]
pub struct RGWDBState {
pub i: TrackedState<usize>,
pub fc_temperature_too_low: TrackedState<bool>,
pub vehicle_speed_too_high: TrackedState<bool>,
pub on_time_too_short: TrackedState<bool>,
pub propulsion_power_demand: TrackedState<bool>,
pub propulsion_power_demand_soft: TrackedState<bool>,
pub aux_power_demand: TrackedState<bool>,
pub charging_for_low_soc: TrackedState<bool>,
pub soc_fc_on_buffer: TrackedState<si::Ratio>,
}
impl SerdeAPI for RGWDBState {}
impl Init for RGWDBState {}
impl RGWDBState {
fn engine_on(&self) -> anyhow::Result<bool> {
Ok(*self.fc_temperature_too_low.get_fresh(|| format_dbg!())?
|| *self.vehicle_speed_too_high.get_fresh(|| format_dbg!())?
|| *self.on_time_too_short.get_fresh(|| format_dbg!())?
|| *self.propulsion_power_demand.get_fresh(|| format_dbg!())?
|| *self
.propulsion_power_demand_soft
.get_fresh(|| format_dbg!())?
|| *self.aux_power_demand.get_fresh(|| format_dbg!())?
|| *self.charging_for_low_soc.get_fresh(|| format_dbg!())?)
}
}
#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)]
#[non_exhaustive]
#[serde(deny_unknown_fields)]
pub struct HEVSimulationParams {
pub res_per_fuel_lim: si::Ratio,
pub soc_balance_iter_err: u32,
pub balance_soc: bool,
pub save_soc_bal_iters: bool,
}
impl Default for HEVSimulationParams {
fn default() -> Self {
Self {
res_per_fuel_lim: uc::R * 0.005,
soc_balance_iter_err: 5,
balance_soc: true,
save_soc_bal_iters: false,
}
}
}
#[derive(
Clone, Debug, PartialEq, Deserialize, Serialize, Default, IsVariant, derive_more::From, TryInto,
)]
pub enum HEVAuxControls {
#[default]
AuxOnResPriority,
AuxOnFcPriority,
}
#[derive(
Clone, Debug, PartialEq, Deserialize, Serialize, IsVariant, derive_more::From, TryInto,
)]
pub enum HEVPowertrainControls {
RGWDB(Box<RESGreedyWithDynamicBuffers>),
}
impl Default for HEVPowertrainControls {
fn default() -> Self {
Self::RGWDB(Default::default())
}
}
impl SetCumulative for HEVPowertrainControls {
fn set_cumulative<F: Fn() -> String>(&mut self, dt: si::Time, loc: F) -> anyhow::Result<()> {
match self {
Self::RGWDB(rgwdb) => {
rgwdb.set_cumulative(dt, || format!("{}\n{}", loc(), format_dbg!()))?
}
}
Ok(())
}
fn reset_cumulative<F: Fn() -> String>(&mut self, loc: F) -> anyhow::Result<()> {
match self {
Self::RGWDB(rgwdb) => {
rgwdb.reset_cumulative(|| format!("{}\n{}", loc(), format_dbg!()))?
}
}
Ok(())
}
}
impl Step for HEVPowertrainControls {
fn step<F: Fn() -> String>(&mut self, loc: F) -> anyhow::Result<()> {
match self {
HEVPowertrainControls::RGWDB(rgwdb) => rgwdb.step(loc)?,
}
Ok(())
}
fn reset_step<F: Fn() -> String>(&mut self, loc: F) -> anyhow::Result<()> {
match self {
HEVPowertrainControls::RGWDB(rgwdb) => rgwdb.reset_step(loc)?,
}
Ok(())
}
}
impl StateMethods for HEVPowertrainControls {}
impl SaveState for HEVPowertrainControls {
fn save_state<F: Fn() -> String>(&mut self, loc: F) -> anyhow::Result<()> {
match self {
HEVPowertrainControls::RGWDB(rgwdb) => rgwdb.save_state(loc)?,
}
Ok(())
}
}
impl TrackedStateMethods for HEVPowertrainControls {
fn check_and_reset<F: Fn() -> String>(&mut self, loc: F) -> anyhow::Result<()> {
match self {
HEVPowertrainControls::RGWDB(rgwdb) => rgwdb.check_and_reset(loc)?,
}
Ok(())
}
fn mark_fresh<F: Fn() -> String>(&mut self, loc: F) -> anyhow::Result<()> {
match self {
HEVPowertrainControls::RGWDB(rgwdb) => rgwdb.mark_fresh(loc)?,
}
Ok(())
}
}
impl HistoryMethods for HEVPowertrainControls {
fn set_save_interval(&mut self, save_interval: Option<usize>) -> anyhow::Result<()> {
match self {
HEVPowertrainControls::RGWDB(rgwdb) => Ok(rgwdb.set_save_interval(save_interval)?),
}
}
fn save_interval(&self) -> anyhow::Result<Option<usize>> {
match self {
HEVPowertrainControls::RGWDB(rgwdb) => rgwdb.save_interval(),
}
}
fn clear(&mut self) {
match self {
HEVPowertrainControls::RGWDB(rgwdb) => rgwdb.clear(),
}
}
}
impl Init for HEVPowertrainControls {
fn init(&mut self) -> Result<(), Error> {
match self {
Self::RGWDB(rgwb) => rgwb.init()?,
}
Ok(())
}
}
impl HEVPowertrainControls {
fn get_pwr_fc_and_em(
&mut self,
pwr_prop_req: si::Power,
fc: &FuelConverter,
em_state: &ElectricMachineState,
res: &ReversibleEnergyStorage,
) -> anyhow::Result<(si::Power, si::Power)> {
let fc_state = &fc.state;
ensure!(
almost_le_uom(
&pwr_prop_req,
&(*em_state.pwr_mech_fwd_out_max.get_fresh(|| format_dbg!())?
+ *fc_state.pwr_prop_max.get_fresh(|| format_dbg!())?),
None
),
"{}
`pwr_out_req`: {} kW
`em_state.pwr_mech_fwd_out_max`: {} kW
`fc_state.pwr_prop_max`: {} kW
`res.state.soc`: {}",
format_dbg!(),
pwr_prop_req.get::<si::kilowatt>(),
em_state
.pwr_mech_fwd_out_max
.get_fresh(|| format_dbg!())?
.get::<si::kilowatt>(),
fc_state
.pwr_prop_max
.get_fresh(|| format_dbg!())?
.get::<si::kilowatt>(),
res.state
.soc
.get_fresh(|| format_dbg!())?
.get::<si::ratio>()
);
match self {
Self::RGWDB(rgwdb) => rgwdb.get_pwr_fc_and_em(fc, pwr_prop_req, em_state),
}
}
pub fn engine_on(&self) -> anyhow::Result<bool> {
match self {
Self::RGWDB(rgwdb) => rgwdb.state.engine_on(),
}
}
}
#[serde_api]
#[derive(Clone, Debug, PartialEq, Deserialize, Serialize, Default, StateMethods, SetCumulative)]
#[cfg_attr(feature = "pyo3", pyclass(module = "fastsim", subclass, eq))]
#[non_exhaustive]
#[serde(deny_unknown_fields)]
pub struct RESGreedyWithDynamicBuffers {
pub speed_soc_disch_buffer: Option<si::Velocity>,
pub speed_soc_disch_buffer_coeff: Option<si::Ratio>,
pub speed_soc_fc_on_buffer: Option<si::Velocity>,
pub speed_soc_fc_on_buffer_coeff: Option<si::Ratio>,
pub speed_soc_regen_buffer: Option<si::Velocity>,
pub speed_soc_regen_buffer_coeff: Option<si::Ratio>,
pub fc_min_time_on: Option<si::Time>,
pub speed_fc_forced_on: Option<si::Velocity>,
pub frac_pwr_demand_fc_forced_on: Option<si::Ratio>,
pub frac_of_most_eff_pwr_to_run_fc: Option<si::Ratio>,
pub save_interval: Option<usize>,
#[serde(default)]
pub temp_fc_forced_on: Option<si::Temperature>,
#[serde(default)]
pub temp_fc_allowed_off: Option<si::Temperature>,
#[serde(default)]
pub state: RGWDBState,
#[serde(default)]
pub history: RGWDBStateHistoryVec,
}
#[pyo3_api]
impl RESGreedyWithDynamicBuffers {}
impl HistoryMethods for RESGreedyWithDynamicBuffers {
fn set_save_interval(&mut self, save_interval: Option<usize>) -> anyhow::Result<()> {
self.save_interval = save_interval;
Ok(())
}
fn save_interval(&self) -> anyhow::Result<Option<usize>> {
Ok(self.save_interval)
}
fn clear(&mut self) {
self.history.clear();
}
}
impl Init for RESGreedyWithDynamicBuffers {
fn init(&mut self) -> Result<(), Error> {
init_opt_default!(self, speed_soc_disch_buffer, 50.0 * uc::MPH);
init_opt_default!(self, speed_soc_disch_buffer_coeff, 1.0 * uc::R);
init_opt_default!(
self,
speed_soc_fc_on_buffer,
self.speed_soc_disch_buffer.unwrap() * 1.2
);
init_opt_default!(self, speed_soc_fc_on_buffer_coeff, 1.0 * uc::R);
init_opt_default!(self, speed_soc_regen_buffer, 30. * uc::MPH);
init_opt_default!(self, speed_soc_regen_buffer_coeff, 1.0 * uc::R);
init_opt_default!(self, fc_min_time_on, uc::S * 5.0);
init_opt_default!(self, speed_fc_forced_on, uc::MPH * 75.);
init_opt_default!(self, frac_pwr_demand_fc_forced_on, uc::R * 0.75);
init_opt_default!(self, frac_of_most_eff_pwr_to_run_fc, 1.0 * uc::R);
Ok(())
}
}
impl SerdeAPI for RESGreedyWithDynamicBuffers {}
impl RESGreedyWithDynamicBuffers {
fn get_pwr_fc_and_em(
&mut self,
fc: &FuelConverter,
pwr_prop_req: si::Power,
em_state: &ElectricMachineState,
) -> anyhow::Result<(si::Power, si::Power)> {
let em_pwr = pwr_prop_req
.min(*em_state.pwr_mech_fwd_out_max.get_fresh(|| format_dbg!())?)
.max(-*em_state.pwr_mech_regen_max.get_fresh(|| format_dbg!())?);
let (fc_pwr, em_pwr) = if !self.state.engine_on()? {
(si::Power::ZERO, em_pwr)
} else {
let frac_of_pwr_for_peak_eff: si::Ratio = self
.frac_of_most_eff_pwr_to_run_fc
.with_context(|| format_dbg!())?;
let fc_pwr = if pwr_prop_req < si::Power::ZERO {
(*em_state.pwr_mech_regen_max.get_fresh(|| format_dbg!())? + pwr_prop_req)
.min(fc.pwr_for_peak_eff * frac_of_pwr_for_peak_eff)
.max(si::Power::ZERO)
} else {
if pwr_prop_req - em_pwr > fc.pwr_for_peak_eff * frac_of_pwr_for_peak_eff {
pwr_prop_req - em_pwr
} else {
(pwr_prop_req - em_pwr)
.max(fc.pwr_for_peak_eff * frac_of_pwr_for_peak_eff)
.min(
pwr_prop_req
+ *em_state.pwr_mech_regen_max.get_fresh(|| format_dbg!())?,
)
}
}
.min(*fc.state.pwr_prop_max.get_fresh(|| format_dbg!())?);
let em_pwr_corrected = (pwr_prop_req - fc_pwr)
.max(-*em_state.pwr_mech_regen_max.get_fresh(|| format_dbg!())?);
(fc_pwr, em_pwr_corrected)
};
Ok((fc_pwr, em_pwr))
}
fn handle_fc_on_causes(
&mut self,
fc: &FuelConverter,
veh_state: &VehicleState,
res: &ReversibleEnergyStorage,
em_state: &ElectricMachineState,
) -> Result<(), anyhow::Error> {
self.handle_fc_on_causes_for_temp(fc)?;
self.handle_fc_on_causes_for_speed(veh_state)?;
self.handle_fc_on_causes_for_low_soc(res, veh_state)?;
self.handle_fc_on_causes_for_pwr_demand(
*veh_state
.pwr_tractive
.get_stale(|| format_dbg!(veh_state.pwr_tractive))?,
em_state,
&fc.state,
)
.with_context(|| format_dbg!())?;
self.handle_fc_on_causes_for_on_time(fc)?;
Ok(())
}
fn handle_fc_on_causes_for_on_time(&mut self, fc: &FuelConverter) -> Result<(), anyhow::Error> {
self.state.on_time_too_short.update(*fc.state.fc_on.get_stale(|| format_dbg!())? && *fc.state.time_on.get_stale(|| format_dbg!())?
< self.fc_min_time_on.with_context(|| {
anyhow!(
"{}\n Expected `ResGreedyWithBuffers::init` to have been called beforehand.",
format_dbg!()
)
})?, || format_dbg!())?;
Ok(())
}
fn handle_fc_on_causes_for_pwr_demand(
&mut self,
pwr_out_req_for_cyc: si::Power,
em_state: &ElectricMachineState,
fc_state: &FuelConverterState,
) -> Result<(), anyhow::Error> {
let frac_pwr_demand_fc_forced_on: si::Ratio = self
.frac_pwr_demand_fc_forced_on
.with_context(|| format_dbg!())?;
self.state.propulsion_power_demand_soft.update(
pwr_out_req_for_cyc
> frac_pwr_demand_fc_forced_on
* (*em_state.pwr_mech_fwd_out_max.get_stale(|| format_dbg!())?
+ *fc_state.pwr_out_max.get_stale(|| format_dbg!())?),
|| format_dbg!(),
)?;
self.state.propulsion_power_demand.update(
pwr_out_req_for_cyc - *em_state.pwr_mech_fwd_out_max.get_stale(|| format_dbg!())?
>= si::Power::ZERO,
|| format_dbg!(),
)?;
Ok(())
}
fn handle_fc_on_causes_for_low_soc(
&mut self,
res: &ReversibleEnergyStorage,
veh_state: &VehicleState,
) -> anyhow::Result<()> {
self.state.soc_fc_on_buffer.update(
{
let energy_delta_to_buffer_speed: si::Energy = 0.5
* *veh_state.mass.get_fresh(|| format_dbg!())?
* (self
.speed_soc_fc_on_buffer
.with_context(|| format_dbg!())?
.powi(P2::new())
- veh_state
.speed_ach
.get_stale(|| format_dbg!())?
.powi(P2::new()));
energy_delta_to_buffer_speed.max(si::Energy::ZERO)
* self
.speed_soc_fc_on_buffer_coeff
.with_context(|| format_dbg!())?
} / res.energy_capacity_usable()
+ res.min_soc,
|| format_dbg!(),
)?;
self.state.charging_for_low_soc.update(
*res.state.soc.get_stale(|| format_dbg!())?
< *self.state.soc_fc_on_buffer.get_fresh(|| format_dbg!())?,
|| format_dbg!(),
)?;
Ok(())
}
fn handle_fc_on_causes_for_speed(&mut self, veh_state: &VehicleState) -> anyhow::Result<()> {
self.state.vehicle_speed_too_high.update(
*veh_state.speed_ach.get_stale(|| format_dbg!())?
> self.speed_fc_forced_on.with_context(|| format_dbg!())?,
|| format_dbg!(),
)?;
Ok(())
}
fn handle_fc_on_causes_for_temp(&mut self, fc: &FuelConverter) -> anyhow::Result<()> {
match (
match fc.temperature() {
Some(fct) => Some(*fct.get_fresh(|| format_dbg!())?),
None => None,
},
match fc.temperature() {
Some(fct) => Some(*fct.get_fresh(|| format_dbg!())?),
None => None,
},
self.temp_fc_forced_on,
self.temp_fc_allowed_off,
) {
(None, None, None, None) => {
self.state
.fc_temperature_too_low
.update(false, || format_dbg!())?;
}
(
Some(temperature),
Some(temp_prev),
Some(temp_fc_forced_on),
Some(temp_fc_allowed_off),
) => {
self.state.fc_temperature_too_low.update(
temperature < temp_fc_forced_on ||
(temp_prev < temp_fc_forced_on && temperature < temp_fc_allowed_off),
|| format_dbg!(),
)?;
}
_ => {
bail!(
"{}\n`fc.temperature()`, `fc.temp_prev()`, `self.temp_fc_forced_on`, and
`self.temp_fc_allowed_off` must all be `None` or `Some` because these controls are necessary
for an HEV equipped with thermal models or superfluous otherwise",
format_dbg!((
fc.temperature(),
self.temp_fc_forced_on,
self.temp_fc_allowed_off
))
);
}
}
Ok(())
}
}