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