fastsim_core/vehicle/
hev.rs

1use super::{vehicle_model::VehicleState, *};
2use crate::prelude::ElectricMachineState;
3
4#[serde_api]
5#[derive(Clone, Debug, PartialEq, Deserialize, Serialize, StateMethods, SetCumulative)]
6#[non_exhaustive]
7#[serde(deny_unknown_fields)]
8#[cfg_attr(feature = "pyo3", pyclass(module = "fastsim", subclass, eq))]
9/// Hybrid vehicle with both engine and reversible energy storage (aka battery)
10/// This type of vehicle is not likely to be widely prevalent due to modularity of consists.
11pub struct HybridElectricVehicle {
12    #[has_state]
13    pub res: ReversibleEnergyStorage,
14    pub fs: FuelStorage,
15    #[has_state]
16    pub fc: FuelConverter,
17    #[has_state]
18    pub em: ElectricMachine,
19    #[has_state]
20    pub transmission: Transmission,
21    /// control strategy for distributing power demand between `fc` and `res`
22    #[has_state]
23    #[serde(default)]
24    pub pt_cntrl: HEVPowertrainControls,
25    /// control strategy for distributing aux power demand between `fc` and `res`
26    #[serde(default)]
27    pub aux_cntrl: HEVAuxControls,
28    /// hybrid powertrain mass
29    pub(crate) mass: Option<si::Mass>,
30    #[serde(default)]
31    pub sim_params: HEVSimulationParams,
32    /// vector of SOC balance iterations
33    #[serde(default)]
34    pub soc_bal_iter_history: Vec<Self>,
35    /// Number of `walk` iterations required to achieve SOC balance (i.e. SOC
36    /// ends at same starting value, ensuring no net [ReversibleEnergyStorage] usage)
37    #[serde(default)]
38    pub soc_bal_iters: TrackedState<u32>,
39}
40
41#[pyo3_api]
42impl HybridElectricVehicle {}
43
44impl HistoryMethods for HybridElectricVehicle {
45    fn save_interval(&self) -> anyhow::Result<Option<usize>> {
46        bail!("`save_interval` is not implemented in HybridElectricVehicle")
47    }
48    fn set_save_interval(&mut self, save_interval: Option<usize>) -> anyhow::Result<()> {
49        self.res.set_save_interval(save_interval)?;
50        // self.fs.set_save_interval(save_interval)?;
51        self.fc.set_save_interval(save_interval)?;
52        self.em.set_save_interval(save_interval)?;
53        self.transmission.set_save_interval(save_interval)?;
54        self.pt_cntrl.set_save_interval(save_interval)?;
55        Ok(())
56    }
57    fn clear(&mut self) {
58        self.res.clear();
59        // self.fs.clear();
60        self.fc.clear();
61        self.em.clear();
62        self.transmission.clear();
63        self.pt_cntrl.clear();
64    }
65}
66
67impl Init for HybridElectricVehicle {
68    fn init(&mut self) -> Result<(), Error> {
69        self.fc
70            .init()
71            .map_err(|err| Error::InitError(format_dbg!(err)))?;
72        self.res
73            .init()
74            .map_err(|err| Error::InitError(format_dbg!(err)))?;
75        self.em
76            .init()
77            .map_err(|err| Error::InitError(format_dbg!(err)))?;
78        self.transmission
79            .init()
80            .map_err(|err| Error::InitError(format_dbg!(err)))?;
81        self.pt_cntrl
82            .init()
83            .map_err(|err| Error::InitError(format_dbg!(err)))?;
84        Ok(())
85    }
86}
87
88impl SerdeAPI for HybridElectricVehicle {}
89
90impl Powertrain for Box<HybridElectricVehicle> {
91    fn set_curr_pwr_prop_out_max(
92        &mut self,
93        _pwr_upstream: (si::Power, si::Power),
94        pwr_aux: si::Power,
95        dt: si::Time,
96        veh_state: &VehicleState,
97    ) -> anyhow::Result<()> {
98        // TODO: account for transmission efficiency in here
99        let (disch_buffer, chrg_buffer) = match &mut self.pt_cntrl {
100            HEVPowertrainControls::RGWDB(rgwdb) => {
101                rgwdb.handle_fc_on_causes(&self.fc, veh_state, &self.res, &self.em.state)?;
102
103                let disch_buffer = (0.5
104                    * *veh_state.mass.get_fresh(|| format_dbg!())?
105                    * (rgwdb
106                        .speed_soc_disch_buffer
107                        .with_context(|| format_dbg!())?
108                        .powi(P2::new())
109                        - veh_state
110                            .speed_ach
111                            .get_stale(|| format_dbg!())?
112                            .powi(P2::new())))
113                .max(si::Energy::ZERO)
114                    * rgwdb
115                        .speed_soc_disch_buffer_coeff
116                        .with_context(|| format_dbg!())?;
117
118                let chrg_buffer = (0.5
119                    * *veh_state.mass.get_fresh(|| format_dbg!())?
120                    * (veh_state
121                        .speed_ach
122                        .get_stale(|| format_dbg!())?
123                        .powi(P2::new())
124                        - rgwdb
125                            .speed_soc_regen_buffer
126                            .with_context(|| format_dbg!())?
127                            .powi(P2::new())))
128                .max(si::Energy::ZERO)
129                    * rgwdb
130                        .speed_soc_regen_buffer_coeff
131                        .with_context(|| format_dbg!())?;
132
133                (disch_buffer, chrg_buffer)
134            }
135        };
136        // set total max powers, including aux power
137        self.fc
138            .set_curr_pwr_out_max(dt)
139            .with_context(|| anyhow!(format_dbg!()))?;
140        self.res
141            .set_curr_pwr_out_max(dt, disch_buffer, chrg_buffer)
142            .with_context(|| anyhow!(format_dbg!()))?;
143
144        // determine distribution of aux power between engine and battery
145        let (pwr_aux_res, pwr_aux_fc) = {
146            match self.aux_cntrl {
147                HEVAuxControls::AuxOnResPriority => {
148                    if pwr_aux <= *self.res.state.pwr_disch_max.get_fresh(|| format_dbg!())? {
149                        (pwr_aux, si::Power::ZERO)
150                    } else {
151                        (si::Power::ZERO, pwr_aux)
152                    }
153                }
154                HEVAuxControls::AuxOnFcPriority => (si::Power::ZERO, pwr_aux),
155            }
156        };
157
158        match &mut self.pt_cntrl {
159            HEVPowertrainControls::RGWDB(rgwdb) => {
160                rgwdb
161                    .state
162                    .aux_power_demand
163                    .update(pwr_aux_fc > si::Power::ZERO, || format_dbg!())?;
164            }
165        }
166
167        // set max propulsion powers
168        self.fc
169            .set_curr_pwr_prop_max(pwr_aux_fc)
170            .with_context(|| anyhow!(format_dbg!()))?;
171        self.res
172            .set_curr_pwr_prop_max(pwr_aux_res)
173            .with_context(|| anyhow!(format_dbg!()))?;
174        self.em
175            .set_curr_pwr_prop_out_max(
176                // TODO: add means of controlling whether fc can provide power to em and also how much
177                // Try out a 'power out type' enum field on the fuel converter with variants for mechanical and electrical
178                self.res
179                    .get_curr_pwr_prop_out_max()
180                    .with_context(|| format_dbg!())?,
181                pwr_aux,
182                dt,
183                veh_state,
184            )
185            .with_context(|| anyhow!(format_dbg!()))?;
186        let em_pwr_prop_out_maxes = self
187            .em
188            .get_curr_pwr_prop_out_max()
189            .with_context(|| format_dbg!())?;
190        let fc_max = self.fc.state.pwr_prop_max.get_fresh(|| format_dbg!())?;
191        self.transmission
192            .set_curr_pwr_prop_out_max(
193                (em_pwr_prop_out_maxes.0 + *fc_max, em_pwr_prop_out_maxes.1),
194                f64::NAN * uc::W,
195                dt,
196                veh_state,
197            )
198            .with_context(|| format_dbg!())?;
199        Ok(())
200    }
201
202    fn get_curr_pwr_prop_out_max(&self) -> anyhow::Result<(si::Power, si::Power)> {
203        self.transmission
204            .get_curr_pwr_prop_out_max()
205            .with_context(|| format_dbg!())
206    }
207
208    fn solve(
209        &mut self,
210        pwr_out_req: si::Power,
211        _enabled: bool,
212        dt: si::Time,
213    ) -> anyhow::Result<Option<si::Power>> {
214        // TODO: address these concerns
215        // - what happens when the fc is on and producing more power than the
216        //   transmission requires? It seems like the excess goes straight to the battery,
217        //   but it should probably go thourgh the em somehow.
218        let pwr_in_transmission = self
219            .transmission
220            .solve(pwr_out_req, true, dt)
221            .with_context(|| format_dbg!())?
222            .with_context(|| format!("{}\nExpected `Some`", format_dbg!()))?;
223
224        // TODO: use an enum with a match here to determine whether power is shared by
225        // - fc and em (e.g. for ICE HEV)
226        //   or
227        // - fc and res (e.g. for H2FC HEV)
228
229        let (fc_pwr_out_req, em_pwr_out_req) = self
230            .pt_cntrl
231            .get_pwr_fc_and_em(pwr_in_transmission, &self.fc, &self.em.state, &self.res)
232            .with_context(|| format_dbg!())?;
233        let fc_on: bool = self.pt_cntrl.engine_on()?;
234
235        self.fc
236            .solve(fc_pwr_out_req, fc_on, dt)
237            .with_context(|| format_dbg!())?;
238        let res_pwr_out_req = self
239            .em
240            .solve(em_pwr_out_req, true, dt)
241            .with_context(|| format_dbg!())?
242            .with_context(|| format!("{}\nExpected `Some`", format_dbg!()))?;
243        // TODO: `res_pwr_out_req` probably does not include charging from the engine
244        self.res
245            .solve(res_pwr_out_req, dt)
246            .with_context(|| format_dbg!())?;
247        Ok(None)
248    }
249
250    /// Regen braking power, positive means braking is happening
251    fn pwr_regen(&self) -> anyhow::Result<si::Power> {
252        // When `pwr_mech_prop_out` is negative, regen is happening.  First, clip it at 0, and then negate it.
253        // see https://play.rust-lang.org/?version=stable&mode=debug&edition=2021&gist=e8f7af5a6e436dd1163fa3c70931d18d
254        // for example
255        self.transmission.pwr_regen().with_context(|| format_dbg!())
256    }
257}
258
259impl HybridElectricVehicle {
260    /// # Arguments
261    /// - `te_amb`: ambient temperature
262    /// - `pwr_thrml_fc_to_cab`: thermal power flow from [FuelConverter::thrml]
263    ///   to [Vehicle::cabin], if cabin is equipped
264    /// - `veh_state`: current [VehicleState]
265    /// - `pwr_thrml_hvac_to_res`: thermal power flow from [Vehicle::hvac] --
266    ///   zero if `None` is passed
267    /// - `te_cab`: cabin temperature, required if [ReversibleEnergyStorage::thrml] is `Some`
268    /// - `dt`: simulation time step size
269    pub fn solve_thermal(
270        &mut self,
271        te_amb: si::Temperature,
272        pwr_thrml_fc_to_cab: Option<si::Power>,
273        veh_state: &mut VehicleState,
274        pwr_thrml_hvac_to_res: Option<si::Power>,
275        te_cab: Option<si::Temperature>,
276        dt: si::Time,
277    ) -> anyhow::Result<()> {
278        self.fc
279            .solve_thermal(te_amb, pwr_thrml_fc_to_cab, veh_state, dt)
280            .with_context(|| format_dbg!())?;
281        self.res
282            .solve_thermal(
283                te_amb,
284                pwr_thrml_hvac_to_res.unwrap_or_default(),
285                te_cab,
286                dt,
287            )
288            .with_context(|| format_dbg!())?;
289        Ok(())
290    }
291}
292
293impl TryFrom<&fastsim_2::vehicle::RustVehicle> for HybridElectricVehicle {
294    type Error = anyhow::Error;
295    fn try_from(f2veh: &fastsim_2::vehicle::RustVehicle) -> anyhow::Result<HybridElectricVehicle> {
296        let pt_cntrl = HEVPowertrainControls::RGWDB(Box::new(hev::RESGreedyWithDynamicBuffers {
297            speed_soc_fc_on_buffer: None,
298            speed_soc_fc_on_buffer_coeff: None,
299            speed_soc_disch_buffer: None,
300            speed_soc_disch_buffer_coeff: None,
301            speed_soc_regen_buffer: None,
302            speed_soc_regen_buffer_coeff: None,
303            // note that this exists in `fastsim-2` but has no apparent effect!
304            fc_min_time_on: None,
305            speed_fc_forced_on: Some(f2veh.mph_fc_on * uc::MPH),
306            frac_pwr_demand_fc_forced_on: Some(
307                f2veh.kw_demand_fc_on / (f2veh.fc_max_kw + f2veh.ess_max_kw.min(f2veh.mc_max_kw))
308                    * uc::R,
309            ),
310            frac_of_most_eff_pwr_to_run_fc: None,
311            temp_fc_forced_on: None,
312            temp_fc_allowed_off: None,
313            save_interval: Some(1),
314            state: Default::default(),
315            history: Default::default(),
316        }));
317        let mut hev = HybridElectricVehicle {
318            fs: {
319                let mut fs = FuelStorage {
320                    pwr_out_max: f2veh.fs_max_kw * uc::KW,
321                    pwr_ramp_lag: f2veh.fs_secs_to_peak_pwr * uc::S,
322                    energy_capacity: f2veh.fs_kwh * 3.6 * uc::MJ,
323                    specific_energy: None,
324                    mass: None,
325                };
326                fs.set_mass(None, MassSideEffect::None)
327                    .with_context(|| anyhow!(format_dbg!()))?;
328                fs
329            },
330            fc: FuelConverter::try_from(f2veh.clone())?,
331            res: ReversibleEnergyStorage::try_from(f2veh.clone()).with_context(|| format_dbg!())?,
332            em: ElectricMachine::try_from(f2veh.clone())?,
333            transmission: Transmission::try_from(f2veh.clone())?,
334            pt_cntrl,
335            mass: None,
336            sim_params: Default::default(),
337            aux_cntrl: Default::default(),
338            soc_bal_iter_history: Default::default(),
339            soc_bal_iters: Default::default(),
340        };
341        hev.init()?;
342        Ok(hev)
343    }
344}
345impl Mass for HybridElectricVehicle {
346    fn mass(&self) -> anyhow::Result<Option<si::Mass>> {
347        let derived_mass = self
348            .derived_mass()
349            .with_context(|| anyhow!(format_dbg!()))?;
350        match (derived_mass, self.mass) {
351            (Some(derived_mass), Some(set_mass)) => {
352                ensure!(
353                    utils::almost_eq_uom(&set_mass, &derived_mass, None),
354                    format!(
355                        "{}",
356                        format_dbg!(utils::almost_eq_uom(&set_mass, &derived_mass, None)),
357                    )
358                );
359                Ok(Some(set_mass))
360            }
361            _ => Ok(self.mass.or(derived_mass)),
362        }
363    }
364
365    fn set_mass(
366        &mut self,
367        new_mass: Option<si::Mass>,
368        side_effect: MassSideEffect,
369    ) -> anyhow::Result<()> {
370        ensure!(
371            side_effect == MassSideEffect::None,
372            "At the powertrain level, only `MassSideEffect::None` is allowed"
373        );
374        let derived_mass = self
375            .derived_mass()
376            .with_context(|| anyhow!(format_dbg!()))?;
377        self.mass = match new_mass {
378            // Set using provided `new_mass`, setting constituent mass fields to `None` to match if inconsistent
379            Some(new_mass) => {
380                if let Some(dm) = derived_mass {
381                    if dm != new_mass {
382                        self.expunge_mass_fields();
383                    }
384                }
385                Some(new_mass)
386            }
387            // Set using `derived_mass()`, failing if it returns `None`
388            None => Some(derived_mass.with_context(|| {
389                format!(
390                    "Not all mass fields in `{}` are set and no mass was provided.",
391                    stringify!(HybridElectricVehicle)
392                )
393            })?),
394        };
395        Ok(())
396    }
397
398    fn derived_mass(&self) -> anyhow::Result<Option<si::Mass>> {
399        let fc_mass = self.fc.mass().with_context(|| anyhow!(format_dbg!()))?;
400        let fs_mass = self.fs.mass().with_context(|| anyhow!(format_dbg!()))?;
401        let res_mass = self.res.mass().with_context(|| anyhow!(format_dbg!()))?;
402        let em_mass = self.em.mass().with_context(|| anyhow!(format_dbg!()))?;
403        let transmission_mass = self
404            .transmission
405            .mass()
406            .with_context(|| anyhow!(format_dbg!()))?;
407        match (fc_mass, fs_mass, res_mass, em_mass, transmission_mass) {
408            (
409                Some(fc_mass),
410                Some(fs_mass),
411                Some(res_mass),
412                Some(em_mass),
413                Some(transmission_mass),
414            ) => Ok(Some(
415                fc_mass + fs_mass + res_mass + em_mass + transmission_mass,
416            )),
417            (None, None, None, None, None) => Ok(None),
418            _ => bail!(
419                "`{}` field masses are not consistently set to `Some` or `None`",
420                stringify!(HybridElectricVehicle)
421            ),
422        }
423    }
424
425    fn expunge_mass_fields(&mut self) {
426        self.fc.expunge_mass_fields();
427        self.fs.expunge_mass_fields();
428        self.res.expunge_mass_fields();
429        self.em.expunge_mass_fields();
430        self.transmission.expunge_mass_fields();
431        self.mass = None;
432    }
433}
434
435#[serde_api]
436#[derive(
437    Clone,
438    Debug,
439    Default,
440    Deserialize,
441    Serialize,
442    PartialEq,
443    HistoryVec,
444    StateMethods,
445    SetCumulative,
446)]
447#[non_exhaustive]
448#[serde(deny_unknown_fields)]
449pub struct RGWDBState {
450    /// time step index
451    pub i: TrackedState<usize>,
452    /// Engine must be on to self heat if thermal model is enabled
453    pub fc_temperature_too_low: TrackedState<bool>,
454    /// Engine must be on for high vehicle speed to ensure powertrain can meet
455    /// any spikes in power demand
456    pub vehicle_speed_too_high: TrackedState<bool>,
457    /// Engine has not been on long enough (usually 30 s)
458    pub on_time_too_short: TrackedState<bool>,
459    /// Powertrain power demand exceeds motor and/or battery capabilities
460    pub propulsion_power_demand: TrackedState<bool>,
461    /// Powertrain power demand exceeds optimal motor and/or battery output
462    pub propulsion_power_demand_soft: TrackedState<bool>,
463    /// Aux power demand exceeds battery capability
464    pub aux_power_demand: TrackedState<bool>,
465    /// SOC is below min buffer so FC is charging RES
466    pub charging_for_low_soc: TrackedState<bool>,
467    /// buffer at which FC is forced on
468    pub soc_fc_on_buffer: TrackedState<si::Ratio>,
469}
470impl SerdeAPI for RGWDBState {}
471impl Init for RGWDBState {}
472
473impl RGWDBState {
474    /// If any of the causes are true, engine must be on
475    fn engine_on(&self) -> anyhow::Result<bool> {
476        Ok(*self.fc_temperature_too_low.get_fresh(|| format_dbg!())?
477            || *self.vehicle_speed_too_high.get_fresh(|| format_dbg!())?
478            || *self.on_time_too_short.get_fresh(|| format_dbg!())?
479            || *self.propulsion_power_demand.get_fresh(|| format_dbg!())?
480            || *self
481                .propulsion_power_demand_soft
482                .get_fresh(|| format_dbg!())?
483            || *self.aux_power_demand.get_fresh(|| format_dbg!())?
484            || *self.charging_for_low_soc.get_fresh(|| format_dbg!())?)
485    }
486}
487
488/// Options for controlling simulation behavior
489#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)]
490#[non_exhaustive]
491#[serde(deny_unknown_fields)]
492pub struct HEVSimulationParams {
493    /// [ReversibleEnergyStorage] per [FuelConverter]
494    pub res_per_fuel_lim: si::Ratio,
495    /// Threshold of SOC balancing iteration for triggering error
496    pub soc_balance_iter_err: u32,
497    /// Whether to allow iteration to achieve SOC balance
498    pub balance_soc: bool,
499    /// Whether to save each SOC balance iteration    
500    pub save_soc_bal_iters: bool,
501}
502
503impl Default for HEVSimulationParams {
504    fn default() -> Self {
505        Self {
506            res_per_fuel_lim: uc::R * 0.005,
507            soc_balance_iter_err: 5,
508            balance_soc: true,
509            save_soc_bal_iters: false,
510        }
511    }
512}
513
514#[derive(
515    Clone, Debug, PartialEq, Deserialize, Serialize, Default, IsVariant, derive_more::From, TryInto,
516)]
517pub enum HEVAuxControls {
518    /// If feasible, use [ReversibleEnergyStorage] to handle aux power demand
519    #[default]
520    AuxOnResPriority,
521    /// If feasible, use [FuelConverter] to handle aux power demand
522    AuxOnFcPriority,
523}
524
525#[derive(
526    Clone, Debug, PartialEq, Deserialize, Serialize, IsVariant, derive_more::From, TryInto,
527)]
528pub enum HEVPowertrainControls {
529    /// Greedily uses [ReversibleEnergyStorage] with buffers that derate charge
530    /// and discharge power inside of static min and max SOC range.  Also, includes
531    /// buffer for forcing [FuelConverter] to be active/on.
532    RGWDB(Box<RESGreedyWithDynamicBuffers>),
533}
534
535impl Default for HEVPowertrainControls {
536    fn default() -> Self {
537        Self::RGWDB(Default::default())
538    }
539}
540
541impl SetCumulative for HEVPowertrainControls {
542    fn set_cumulative<F: Fn() -> String>(&mut self, dt: si::Time, loc: F) -> anyhow::Result<()> {
543        match self {
544            Self::RGWDB(rgwdb) => {
545                rgwdb.set_cumulative(dt, || format!("{}\n{}", loc(), format_dbg!()))?
546            }
547        }
548        Ok(())
549    }
550
551    fn reset_cumulative<F: Fn() -> String>(&mut self, loc: F) -> anyhow::Result<()> {
552        match self {
553            Self::RGWDB(rgwdb) => {
554                rgwdb.reset_cumulative(|| format!("{}\n{}", loc(), format_dbg!()))?
555            }
556        }
557        Ok(())
558    }
559}
560impl Step for HEVPowertrainControls {
561    fn step<F: Fn() -> String>(&mut self, loc: F) -> anyhow::Result<()> {
562        match self {
563            HEVPowertrainControls::RGWDB(rgwdb) => rgwdb.step(loc)?,
564        }
565        Ok(())
566    }
567
568    fn reset_step<F: Fn() -> String>(&mut self, loc: F) -> anyhow::Result<()> {
569        match self {
570            HEVPowertrainControls::RGWDB(rgwdb) => rgwdb.reset_step(loc)?,
571        }
572        Ok(())
573    }
574}
575
576impl StateMethods for HEVPowertrainControls {}
577
578impl SaveState for HEVPowertrainControls {
579    fn save_state<F: Fn() -> String>(&mut self, loc: F) -> anyhow::Result<()> {
580        match self {
581            HEVPowertrainControls::RGWDB(rgwdb) => rgwdb.save_state(loc)?,
582        }
583        Ok(())
584    }
585}
586impl TrackedStateMethods for HEVPowertrainControls {
587    fn check_and_reset<F: Fn() -> String>(&mut self, loc: F) -> anyhow::Result<()> {
588        match self {
589            HEVPowertrainControls::RGWDB(rgwdb) => rgwdb.check_and_reset(loc)?,
590        }
591        Ok(())
592    }
593
594    fn mark_fresh<F: Fn() -> String>(&mut self, loc: F) -> anyhow::Result<()> {
595        match self {
596            HEVPowertrainControls::RGWDB(rgwdb) => rgwdb.mark_fresh(loc)?,
597        }
598        Ok(())
599    }
600}
601impl HistoryMethods for HEVPowertrainControls {
602    fn set_save_interval(&mut self, save_interval: Option<usize>) -> anyhow::Result<()> {
603        match self {
604            HEVPowertrainControls::RGWDB(rgwdb) => Ok(rgwdb.set_save_interval(save_interval)?),
605        }
606    }
607
608    fn save_interval(&self) -> anyhow::Result<Option<usize>> {
609        match self {
610            HEVPowertrainControls::RGWDB(rgwdb) => rgwdb.save_interval(),
611        }
612    }
613    fn clear(&mut self) {
614        match self {
615            HEVPowertrainControls::RGWDB(rgwdb) => rgwdb.clear(),
616        }
617    }
618}
619
620impl Init for HEVPowertrainControls {
621    fn init(&mut self) -> Result<(), Error> {
622        match self {
623            Self::RGWDB(rgwb) => rgwb.init()?,
624        }
625        Ok(())
626    }
627}
628
629impl HEVPowertrainControls {
630    /// Determines power split between engine and electric machine
631    ///
632    /// # Arguments
633    /// - `pwr_prop_req`: tractive power required
634    /// - `veh_state`: vehicle state
635    /// - `hev_state`: HEV powertrain state
636    /// - `fc`: fuel converter
637    /// - `em_state`: electric machine state
638    /// - `res`: reversible energy storage (e.g. high voltage battery)
639    fn get_pwr_fc_and_em(
640        &mut self,
641        pwr_prop_req: si::Power,
642        fc: &FuelConverter,
643        em_state: &ElectricMachineState,
644        res: &ReversibleEnergyStorage,
645    ) -> anyhow::Result<(si::Power, si::Power)> {
646        let fc_state = &fc.state;
647        ensure!(
648            // `almost` is in case of negligible numerical precision discrepancies
649            almost_le_uom(
650                &pwr_prop_req,
651                &(*em_state.pwr_mech_fwd_out_max.get_fresh(|| format_dbg!())?
652                    + *fc_state.pwr_prop_max.get_fresh(|| format_dbg!())?),
653                None
654            ),
655            "{}
656`pwr_out_req`: {} kW
657`em_state.pwr_mech_fwd_out_max`: {} kW
658`fc_state.pwr_prop_max`: {} kW
659`res.state.soc`: {}",
660            format_dbg!(),
661            pwr_prop_req.get::<si::kilowatt>(),
662            em_state
663                .pwr_mech_fwd_out_max
664                .get_fresh(|| format_dbg!())?
665                .get::<si::kilowatt>(),
666            fc_state
667                .pwr_prop_max
668                .get_fresh(|| format_dbg!())?
669                .get::<si::kilowatt>(),
670            res.state
671                .soc
672                .get_fresh(|| format_dbg!())?
673                .get::<si::ratio>()
674        );
675
676        // # Brain dump for thermal stuff
677        // TODO: engine on/off w.r.t. thermal stuff should not come into play
678        // if there is no component (e.g. cabin) demanding heat from the engine.  My 2019
679        // Hyundai Ioniq will turn the engine off if there is no heat demand regardless of
680        // the coolant temperature
681        // TODO: make sure idle fuel gets converted to heat correctly
682
683        match self {
684            Self::RGWDB(rgwdb) => rgwdb.get_pwr_fc_and_em(fc, pwr_prop_req, em_state),
685        }
686    }
687
688    pub fn engine_on(&self) -> anyhow::Result<bool> {
689        match self {
690            Self::RGWDB(rgwdb) => rgwdb.state.engine_on(),
691        }
692    }
693}
694
695/// Greedily uses [ReversibleEnergyStorage] with buffers that derate charge
696/// and discharge power inside of static min and max SOC range.  Also, includes
697/// buffer for forcing [FuelConverter] to be active/on. See [Self::init] for
698/// default values.
699#[serde_api]
700#[derive(Clone, Debug, PartialEq, Deserialize, Serialize, Default, StateMethods, SetCumulative)]
701#[cfg_attr(feature = "pyo3", pyclass(module = "fastsim", subclass, eq))]
702#[non_exhaustive]
703#[serde(deny_unknown_fields)]
704pub struct RESGreedyWithDynamicBuffers {
705    /// RES energy delta from minimum SOC corresponding to kinetic energy of
706    /// vehicle at this speed that triggers ramp down in RES discharge.
707    pub speed_soc_disch_buffer: Option<si::Velocity>,
708    /// Coefficient for modifying amount of accel buffer
709    pub speed_soc_disch_buffer_coeff: Option<si::Ratio>,
710    /// RES energy delta from minimum SOC corresponding to kinetic energy of
711    /// vehicle at this speed that triggers FC to be forced on.
712    pub speed_soc_fc_on_buffer: Option<si::Velocity>,
713    /// Coefficient for modifying amount of [Self::speed_soc_fc_on_buffer]
714    pub speed_soc_fc_on_buffer_coeff: Option<si::Ratio>,
715    /// RES energy delta from maximum SOC corresponding to kinetic energy of
716    /// vehicle at current speed minus kinetic energy of vehicle at this speed
717    /// triggers ramp down in RES discharge
718    pub speed_soc_regen_buffer: Option<si::Velocity>,
719    /// Coefficient for modifying amount of regen buffer
720    pub speed_soc_regen_buffer_coeff: Option<si::Ratio>,
721    /// Minimum time engine must remain on if it was on during the previous
722    /// simulation time step.
723    pub fc_min_time_on: Option<si::Time>,
724    /// Speed at which [FuelConverter] is forced on.
725    pub speed_fc_forced_on: Option<si::Velocity>,
726    /// Fraction of total aux and powertrain rated power at which
727    /// [FuelConverter] is forced on.
728    pub frac_pwr_demand_fc_forced_on: Option<si::Ratio>,
729    /// Force engine, if on, to run at this fraction of power at which peak
730    /// efficiency occurs or the required power, whichever is greater. If SOC is
731    /// below min buffer or engine is otherwise forced on and battery has room
732    /// to receive charge, engine will run at this level and charge.
733    pub frac_of_most_eff_pwr_to_run_fc: Option<si::Ratio>,
734    /// Fraction of available charging capacity to use toward running the engine
735    /// efficiently.
736    /// Time step interval between saves. 1 is a good option. If None, no saving occurs.
737    pub save_interval: Option<usize>,
738    /// temperature at which engine is forced on to warm up
739    #[serde(default)]
740    pub temp_fc_forced_on: Option<si::Temperature>,
741    /// temperature at which engine is allowed to turn off due to being sufficiently warm
742    #[serde(default)]
743    pub temp_fc_allowed_off: Option<si::Temperature>,
744    /// current state of control variables
745    #[serde(default)]
746    pub state: RGWDBState,
747    #[serde(default)]
748    /// history of current state
749    pub history: RGWDBStateHistoryVec,
750}
751
752#[pyo3_api]
753impl RESGreedyWithDynamicBuffers {}
754
755impl HistoryMethods for RESGreedyWithDynamicBuffers {
756    fn set_save_interval(&mut self, save_interval: Option<usize>) -> anyhow::Result<()> {
757        self.save_interval = save_interval;
758        Ok(())
759    }
760
761    fn save_interval(&self) -> anyhow::Result<Option<usize>> {
762        Ok(self.save_interval)
763    }
764
765    fn clear(&mut self) {
766        self.history.clear();
767    }
768}
769
770impl Init for RESGreedyWithDynamicBuffers {
771    fn init(&mut self) -> Result<(), Error> {
772        // TODO: make sure these values propagate to the documented defaults above
773        init_opt_default!(self, speed_soc_disch_buffer, 70.0 * uc::MPH);
774        init_opt_default!(self, speed_soc_disch_buffer_coeff, 1.0 * uc::R);
775        init_opt_default!(
776            self,
777            speed_soc_fc_on_buffer,
778            self.speed_soc_disch_buffer.unwrap() * 1.5
779        );
780        init_opt_default!(self, speed_soc_fc_on_buffer_coeff, 1.0 * uc::R);
781        init_opt_default!(self, speed_soc_regen_buffer, 30. * uc::MPH);
782        init_opt_default!(self, speed_soc_regen_buffer_coeff, 1.0 * uc::R);
783        init_opt_default!(self, fc_min_time_on, uc::S * 5.0);
784        init_opt_default!(self, speed_fc_forced_on, uc::MPH * 75.);
785        init_opt_default!(self, frac_pwr_demand_fc_forced_on, uc::R * 0.75);
786        init_opt_default!(self, frac_of_most_eff_pwr_to_run_fc, 1.0 * uc::R);
787        Ok(())
788    }
789}
790impl SerdeAPI for RESGreedyWithDynamicBuffers {}
791
792impl RESGreedyWithDynamicBuffers {
793    fn get_pwr_fc_and_em(
794        &mut self,
795        fc: &FuelConverter,
796        pwr_prop_req: si::Power,
797        em_state: &ElectricMachineState,
798    ) -> anyhow::Result<(si::Power, si::Power)> {
799        // Tractive power `em` must provide before deciding power
800        // split, cannot exceed ElectricMachine max output power.
801        // Excess demand will be handled by `fc`.  Favors drawing power from
802        // `em` before engine
803        let em_pwr = pwr_prop_req
804            .min(*em_state.pwr_mech_fwd_out_max.get_fresh(|| format_dbg!())?)
805            .max(-*em_state.pwr_mech_regen_max.get_fresh(|| format_dbg!())?);
806        // tractive power handled by fc
807        let (fc_pwr, em_pwr) = if !self.state.engine_on()? {
808            // engine is off, and `em_pwr` has already been limited within bounds
809            (si::Power::ZERO, em_pwr)
810        } else {
811            // engine has been forced on
812            let frac_of_pwr_for_peak_eff: si::Ratio = self
813                .frac_of_most_eff_pwr_to_run_fc
814                .with_context(|| format_dbg!())?;
815            let fc_pwr = if pwr_prop_req < si::Power::ZERO {
816                // negative tractive power
817                // max power system can receive from engine during negative traction
818                (*em_state.pwr_mech_regen_max.get_fresh(|| format_dbg!())? + pwr_prop_req)
819                    // or peak efficiency power if it's lower than above
820                    .min(fc.pwr_for_peak_eff * frac_of_pwr_for_peak_eff)
821                    // but not negative
822                    .max(si::Power::ZERO)
823            } else {
824                // positive tractive power
825                if pwr_prop_req - em_pwr > fc.pwr_for_peak_eff * frac_of_pwr_for_peak_eff {
826                    // engine needs to run higher than peak efficiency point
827                    pwr_prop_req - em_pwr
828                } else {
829                    // engine does not need to run higher than peak
830                    // efficiency point to make tractive demand
831
832                    // fc handles all power not covered by em
833                    (pwr_prop_req - em_pwr)
834                        // and if that's less than the
835                        // efficiency-focused value, then operate at
836                        // that value
837                        .max(fc.pwr_for_peak_eff * frac_of_pwr_for_peak_eff)
838                        // but don't exceed what what the battery can
839                        // absorb + tractive demand
840                        .min(
841                            pwr_prop_req
842                                + *em_state.pwr_mech_regen_max.get_fresh(|| format_dbg!())?,
843                        )
844                }
845            }
846            // and don't exceed what the fc can do
847            .min(*fc.state.pwr_prop_max.get_fresh(|| format_dbg!())?);
848
849            // recalculate `em_pwr` based on `fc_pwr`
850            let em_pwr_corrected = (pwr_prop_req - fc_pwr)
851                .max(-*em_state.pwr_mech_regen_max.get_fresh(|| format_dbg!())?);
852            (fc_pwr, em_pwr_corrected)
853        };
854        Ok((fc_pwr, em_pwr))
855    }
856
857    fn handle_fc_on_causes(
858        &mut self,
859        fc: &FuelConverter,
860        veh_state: &VehicleState,
861        res: &ReversibleEnergyStorage,
862        em_state: &ElectricMachineState,
863    ) -> Result<(), anyhow::Error> {
864        self.handle_fc_on_causes_for_temp(fc)?;
865        self.handle_fc_on_causes_for_speed(veh_state)?;
866        self.handle_fc_on_causes_for_low_soc(res, veh_state)?;
867        self.handle_fc_on_causes_for_pwr_demand(
868            *veh_state
869                .pwr_tractive
870                .get_stale(|| format_dbg!(veh_state.pwr_tractive))?,
871            em_state,
872            &fc.state,
873        )
874        .with_context(|| format_dbg!())?;
875        self.handle_fc_on_causes_for_on_time(fc)?;
876        Ok(())
877    }
878
879    fn handle_fc_on_causes_for_on_time(&mut self, fc: &FuelConverter) -> Result<(), anyhow::Error> {
880        self.state.on_time_too_short.update(*fc.state.fc_on.get_stale(|| format_dbg!())? && *fc.state.time_on.get_stale(|| format_dbg!())?
881                    < self.fc_min_time_on.with_context(|| {
882                    anyhow!(
883                        "{}\n Expected `ResGreedyWithBuffers::init` to have been called beforehand.",
884                        format_dbg!()
885                    )
886                })?, || format_dbg!())?;
887        Ok(())
888    }
889
890    /// Determines whether power demand requires engine to be on.  Not needed during
891    /// negative traction.
892    fn handle_fc_on_causes_for_pwr_demand(
893        &mut self,
894        pwr_out_req_for_cyc: si::Power,
895        em_state: &ElectricMachineState,
896        fc_state: &FuelConverterState,
897    ) -> Result<(), anyhow::Error> {
898        let frac_pwr_demand_fc_forced_on: si::Ratio = self
899            .frac_pwr_demand_fc_forced_on
900            .with_context(|| format_dbg!())?;
901        self.state.propulsion_power_demand_soft.update(
902            pwr_out_req_for_cyc
903                > frac_pwr_demand_fc_forced_on
904                    * (*em_state.pwr_mech_fwd_out_max.get_stale(|| format_dbg!())?
905                        + *fc_state.pwr_out_max.get_stale(|| format_dbg!())?),
906            || format_dbg!(),
907        )?;
908        self.state.propulsion_power_demand.update(
909            pwr_out_req_for_cyc - *em_state.pwr_mech_fwd_out_max.get_stale(|| format_dbg!())?
910                >= si::Power::ZERO,
911            || format_dbg!(),
912        )?;
913        Ok(())
914    }
915
916    /// Detemrines whether engine must be on to charge battery
917    fn handle_fc_on_causes_for_low_soc(
918        &mut self,
919        res: &ReversibleEnergyStorage,
920        veh_state: &VehicleState,
921    ) -> anyhow::Result<()> {
922        self.state.soc_fc_on_buffer.update(
923            {
924                let energy_delta_to_buffer_speed: si::Energy = 0.5
925                    * *veh_state.mass.get_fresh(|| format_dbg!())?
926                    * (self
927                        .speed_soc_fc_on_buffer
928                        .with_context(|| format_dbg!())?
929                        .powi(P2::new())
930                        - veh_state
931                            .speed_ach
932                            .get_stale(|| format_dbg!())?
933                            .powi(P2::new()));
934                energy_delta_to_buffer_speed.max(si::Energy::ZERO)
935                    * self
936                        .speed_soc_fc_on_buffer_coeff
937                        .with_context(|| format_dbg!())?
938            } / res.energy_capacity_usable()
939                + res.min_soc,
940            || format_dbg!(),
941        )?;
942        self.state.charging_for_low_soc.update(
943            *res.state.soc.get_stale(|| format_dbg!())?
944                < *self.state.soc_fc_on_buffer.get_fresh(|| format_dbg!())?,
945            || format_dbg!(),
946        )?;
947        Ok(())
948    }
949
950    /// Determines whether enigne must be on for high speed
951    fn handle_fc_on_causes_for_speed(&mut self, veh_state: &VehicleState) -> anyhow::Result<()> {
952        self.state.vehicle_speed_too_high.update(
953            *veh_state.speed_ach.get_stale(|| format_dbg!())?
954                > self.speed_fc_forced_on.with_context(|| format_dbg!())?,
955            || format_dbg!(),
956        )?;
957        Ok(())
958    }
959
960    /// Determines whether engine needs to be on due to low temperature and pushes
961    /// appropriate variant to `fc_on_causes`
962    fn handle_fc_on_causes_for_temp(&mut self, fc: &FuelConverter) -> anyhow::Result<()> {
963        match (
964            match fc.temperature() {
965                Some(fct) => Some(*fct.get_fresh(|| format_dbg!())?),
966                None => None,
967            },
968            match fc.temperature() {
969                Some(fct) => Some(*fct.get_fresh(|| format_dbg!())?),
970                None => None,
971            },
972            self.temp_fc_forced_on,
973            self.temp_fc_allowed_off,
974        ) {
975            (None, None, None, None) => {
976                self.state
977                    .fc_temperature_too_low
978                    .update(false, || format_dbg!())?;
979            }
980            (
981                Some(temperature),
982                Some(temp_prev),
983                Some(temp_fc_forced_on),
984                Some(temp_fc_allowed_off),
985            ) => {
986                self.state.fc_temperature_too_low.update(
987                    // temperature is currently below forced on threshold
988                    temperature < temp_fc_forced_on ||
989            // temperature was below forced on threshold and still has not exceeded allowed off threshold
990            (temp_prev < temp_fc_forced_on && temperature < temp_fc_allowed_off),
991                    || format_dbg!(),
992                )?;
993            }
994            _ => {
995                bail!(
996                    "{}\n`fc.temperature()`, `fc.temp_prev()`, `self.temp_fc_forced_on`, and 
997`self.temp_fc_allowed_off` must all be `None` or `Some` because these controls are necessary
998for an HEV equipped with thermal models or superfluous otherwise",
999                    format_dbg!((
1000                        fc.temperature(),
1001                        self.temp_fc_forced_on,
1002                        self.temp_fc_allowed_off
1003                    ))
1004                );
1005            }
1006        }
1007        Ok(())
1008    }
1009}