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#[named_struct_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            HEVPowertrainControls::Placeholder => {
136                todo!()
137            }
138        };
139        // set total max powers, including aux power
140        self.fc
141            .set_curr_pwr_out_max(dt)
142            .with_context(|| anyhow!(format_dbg!()))?;
143        self.res
144            .set_curr_pwr_out_max(dt, disch_buffer, chrg_buffer)
145            .with_context(|| anyhow!(format_dbg!()))?;
146
147        // determine distribution of aux power between engine and battery
148        let (pwr_aux_res, pwr_aux_fc) = {
149            match self.aux_cntrl {
150                HEVAuxControls::AuxOnResPriority => {
151                    if pwr_aux <= *self.res.state.pwr_disch_max.get_fresh(|| format_dbg!())? {
152                        (pwr_aux, si::Power::ZERO)
153                    } else {
154                        (si::Power::ZERO, pwr_aux)
155                    }
156                }
157                HEVAuxControls::AuxOnFcPriority => (si::Power::ZERO, pwr_aux),
158            }
159        };
160
161        match &mut self.pt_cntrl {
162            HEVPowertrainControls::RGWDB(rgwdb) => {
163                rgwdb
164                    .state
165                    .aux_power_demand
166                    .update(pwr_aux_fc > si::Power::ZERO, || format_dbg!())?;
167            }
168            HEVPowertrainControls::Placeholder => todo!(),
169        }
170
171        // set max propulsion powers
172        self.fc
173            .set_curr_pwr_prop_max(pwr_aux_fc)
174            .with_context(|| anyhow!(format_dbg!()))?;
175        self.res
176            .set_curr_pwr_prop_max(pwr_aux_res)
177            .with_context(|| anyhow!(format_dbg!()))?;
178        self.em
179            .set_curr_pwr_prop_out_max(
180                // TODO: add means of controlling whether fc can provide power to em and also how much
181                // Try out a 'power out type' enum field on the fuel converter with variants for mechanical and electrical
182                *self.res.state.pwr_prop_max.get_fresh(|| format_dbg!())?,
183                *self.res.state.pwr_regen_max.get_fresh(|| format_dbg!())?,
184                dt,
185            )
186            .with_context(|| anyhow!(format_dbg!()))?;
187        // TODO: add transmission here maybe?
188        Ok(())
189    }
190
191    fn get_curr_pwr_prop_out_max(&self) -> anyhow::Result<(si::Power, si::Power)> {
192        Ok((
193            *self
194                .em
195                .state
196                .pwr_mech_fwd_out_max
197                .get_fresh(|| format_dbg!())?
198                + *self.fc.state.pwr_prop_max.get_fresh(|| format_dbg!())?,
199            *self
200                .em
201                .state
202                .pwr_mech_regen_max
203                .get_fresh(|| format_dbg!())?,
204        ))
205    }
206
207    fn solve(
208        &mut self,
209        pwr_out_req: si::Power,
210        _enabled: bool,
211        dt: si::Time,
212    ) -> anyhow::Result<()> {
213        // TODO: address these concerns
214        // - what happens when the fc is on and producing more power than the
215        //   transmission requires? It seems like the excess goes straight to the battery,
216        //   but it should probably go thourgh the em somehow.
217        let pwr_in_transmission = self
218            .transmission
219            .get_pwr_in_req(pwr_out_req)
220            .with_context(|| anyhow!(format_dbg!()))?;
221
222        // TODO: use an enum with a match here to determine whether power is shared by
223        // - fc and em (e.g. for ICE HEV)
224        //   or
225        // - fc and res (e.g. for H2FC HEV)
226
227        let (fc_pwr_out_req, em_pwr_out_req) = self
228            .pt_cntrl
229            .get_pwr_fc_and_em(pwr_in_transmission, &self.fc, &self.em.state, &self.res)
230            .with_context(|| format_dbg!())?;
231        let fc_on: bool = self.pt_cntrl.engine_on()?;
232
233        self.fc
234            .solve(fc_pwr_out_req, fc_on, dt)
235            .with_context(|| format_dbg!())?;
236        let res_pwr_out_req = self
237            .em
238            .get_pwr_in_req(em_pwr_out_req, dt)
239            .with_context(|| format_dbg!())?;
240        // TODO: `res_pwr_out_req` probably does not include charging from the engine
241        self.res
242            .solve(res_pwr_out_req, dt)
243            .with_context(|| format_dbg!())?;
244        Ok(())
245    }
246
247    /// Regen braking power, positive means braking is happening
248    fn pwr_regen(&self) -> anyhow::Result<si::Power> {
249        // When `pwr_mech_prop_out` is negative, regen is happening.  First, clip it at 0, and then negate it.
250        // see https://play.rust-lang.org/?version=stable&mode=debug&edition=2021&gist=e8f7af5a6e436dd1163fa3c70931d18d
251        // for example
252        Ok(-self
253            .em
254            .state
255            .pwr_mech_prop_out
256            .get_fresh(|| format_dbg!())?
257            .max(si::Power::ZERO))
258    }
259}
260
261impl HybridElectricVehicle {
262    /// # Arguments
263    /// - `te_amb`: ambient temperature
264    /// - `pwr_thrml_fc_to_cab`: thermal power flow from [FuelConverter::thrml]
265    ///     to [Vehicle::cabin], if cabin is equipped
266    /// - `veh_state`: current [VehicleState]
267    /// - `pwr_thrml_hvac_to_res`: thermal power flow from [Vehicle::hvac] --
268    ///     zero if `None` is passed
269    /// - `te_cab`: cabin temperature, required if [ReversibleEnergyStorage::thrml] is `Some`
270    /// - `dt`: simulation time step size
271    pub fn solve_thermal(
272        &mut self,
273        te_amb: si::Temperature,
274        pwr_thrml_fc_to_cab: Option<si::Power>,
275        veh_state: &mut VehicleState,
276        pwr_thrml_hvac_to_res: Option<si::Power>,
277        te_cab: Option<si::Temperature>,
278        dt: si::Time,
279    ) -> anyhow::Result<()> {
280        self.fc
281            .solve_thermal(te_amb, pwr_thrml_fc_to_cab, veh_state, dt)
282            .with_context(|| format_dbg!())?;
283        self.res
284            .solve_thermal(
285                te_amb,
286                pwr_thrml_hvac_to_res.unwrap_or_default(),
287                te_cab,
288                dt,
289            )
290            .with_context(|| format_dbg!())?;
291        Ok(())
292    }
293}
294
295impl Mass for HybridElectricVehicle {
296    fn mass(&self) -> anyhow::Result<Option<si::Mass>> {
297        let derived_mass = self
298            .derived_mass()
299            .with_context(|| anyhow!(format_dbg!()))?;
300        match (derived_mass, self.mass) {
301            (Some(derived_mass), Some(set_mass)) => {
302                ensure!(
303                    utils::almost_eq_uom(&set_mass, &derived_mass, None),
304                    format!(
305                        "{}",
306                        format_dbg!(utils::almost_eq_uom(&set_mass, &derived_mass, None)),
307                    )
308                );
309                Ok(Some(set_mass))
310            }
311            _ => Ok(self.mass.or(derived_mass)),
312        }
313    }
314
315    fn set_mass(
316        &mut self,
317        new_mass: Option<si::Mass>,
318        side_effect: MassSideEffect,
319    ) -> anyhow::Result<()> {
320        ensure!(
321            side_effect == MassSideEffect::None,
322            "At the powertrain level, only `MassSideEffect::None` is allowed"
323        );
324        let derived_mass = self
325            .derived_mass()
326            .with_context(|| anyhow!(format_dbg!()))?;
327        self.mass = match new_mass {
328            // Set using provided `new_mass`, setting constituent mass fields to `None` to match if inconsistent
329            Some(new_mass) => {
330                if let Some(dm) = derived_mass {
331                    if dm != new_mass {
332                        self.expunge_mass_fields();
333                    }
334                }
335                Some(new_mass)
336            }
337            // Set using `derived_mass()`, failing if it returns `None`
338            None => Some(derived_mass.with_context(|| {
339                format!(
340                    "Not all mass fields in `{}` are set and no mass was provided.",
341                    stringify!(HybridElectricVehicle)
342                )
343            })?),
344        };
345        Ok(())
346    }
347
348    fn derived_mass(&self) -> anyhow::Result<Option<si::Mass>> {
349        let fc_mass = self.fc.mass().with_context(|| anyhow!(format_dbg!()))?;
350        let fs_mass = self.fs.mass().with_context(|| anyhow!(format_dbg!()))?;
351        let res_mass = self.res.mass().with_context(|| anyhow!(format_dbg!()))?;
352        let em_mass = self.em.mass().with_context(|| anyhow!(format_dbg!()))?;
353        let transmission_mass = self
354            .transmission
355            .mass()
356            .with_context(|| anyhow!(format_dbg!()))?;
357        match (fc_mass, fs_mass, res_mass, em_mass, transmission_mass) {
358            (
359                Some(fc_mass),
360                Some(fs_mass),
361                Some(res_mass),
362                Some(em_mass),
363                Some(transmission_mass),
364            ) => Ok(Some(
365                fc_mass + fs_mass + res_mass + em_mass + transmission_mass,
366            )),
367            (None, None, None, None, None) => Ok(None),
368            _ => bail!(
369                "`{}` field masses are not consistently set to `Some` or `None`",
370                stringify!(HybridElectricVehicle)
371            ),
372        }
373    }
374
375    fn expunge_mass_fields(&mut self) {
376        self.fc.expunge_mass_fields();
377        self.fs.expunge_mass_fields();
378        self.res.expunge_mass_fields();
379        self.em.expunge_mass_fields();
380        self.transmission.expunge_mass_fields();
381        self.mass = None;
382    }
383}
384
385#[serde_api]
386#[derive(
387    Clone,
388    Debug,
389    Default,
390    Deserialize,
391    Serialize,
392    PartialEq,
393    HistoryVec,
394    StateMethods,
395    SetCumulative,
396)]
397#[non_exhaustive]
398#[serde(deny_unknown_fields)]
399pub struct RGWDBState {
400    /// time step index
401    pub i: TrackedState<usize>,
402    /// Engine must be on to self heat if thermal model is enabled
403    pub fc_temperature_too_low: TrackedState<bool>,
404    /// Engine must be on for high vehicle speed to ensure powertrain can meet
405    /// any spikes in power demand
406    pub vehicle_speed_too_high: TrackedState<bool>,
407    /// Engine has not been on long enough (usually 30 s)
408    pub on_time_too_short: TrackedState<bool>,
409    /// Powertrain power demand exceeds motor and/or battery capabilities
410    pub propulsion_power_demand: TrackedState<bool>,
411    /// Powertrain power demand exceeds optimal motor and/or battery output
412    pub propulsion_power_demand_soft: TrackedState<bool>,
413    /// Aux power demand exceeds battery capability
414    pub aux_power_demand: TrackedState<bool>,
415    /// SOC is below min buffer so FC is charging RES
416    pub charging_for_low_soc: TrackedState<bool>,
417    /// buffer at which FC is forced on
418    pub soc_fc_on_buffer: TrackedState<si::Ratio>,
419}
420impl SerdeAPI for RGWDBState {}
421impl Init for RGWDBState {}
422
423impl RGWDBState {
424    /// If any of the causes are true, engine must be on
425    fn engine_on(&self) -> anyhow::Result<bool> {
426        Ok(*self.fc_temperature_too_low.get_fresh(|| format_dbg!())?
427            || *self.vehicle_speed_too_high.get_fresh(|| format_dbg!())?
428            || *self.on_time_too_short.get_fresh(|| format_dbg!())?
429            || *self.propulsion_power_demand.get_fresh(|| format_dbg!())?
430            || *self
431                .propulsion_power_demand_soft
432                .get_fresh(|| format_dbg!())?
433            || *self.aux_power_demand.get_fresh(|| format_dbg!())?
434            || *self.charging_for_low_soc.get_fresh(|| format_dbg!())?)
435    }
436}
437
438/// Options for controlling simulation behavior
439#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)]
440#[non_exhaustive]
441#[serde(deny_unknown_fields)]
442pub struct HEVSimulationParams {
443    /// [ReversibleEnergyStorage] per [FuelConverter]
444    pub res_per_fuel_lim: si::Ratio,
445    /// Threshold of SOC balancing iteration for triggering error
446    pub soc_balance_iter_err: u32,
447    /// Whether to allow iteration to achieve SOC balance
448    pub balance_soc: bool,
449    /// Whether to save each SOC balance iteration    
450    pub save_soc_bal_iters: bool,
451}
452
453impl Default for HEVSimulationParams {
454    fn default() -> Self {
455        Self {
456            res_per_fuel_lim: uc::R * 0.005,
457            soc_balance_iter_err: 5,
458            balance_soc: true,
459            save_soc_bal_iters: false,
460        }
461    }
462}
463
464#[derive(
465    Clone, Debug, PartialEq, Deserialize, Serialize, Default, IsVariant, derive_more::From, TryInto,
466)]
467pub enum HEVAuxControls {
468    /// If feasible, use [ReversibleEnergyStorage] to handle aux power demand
469    #[default]
470    AuxOnResPriority,
471    /// If feasible, use [FuelConverter] to handle aux power demand
472    AuxOnFcPriority,
473}
474
475#[derive(
476    Clone, Debug, PartialEq, Deserialize, Serialize, IsVariant, derive_more::From, TryInto,
477)]
478pub enum HEVPowertrainControls {
479    /// Greedily uses [ReversibleEnergyStorage] with buffers that derate charge
480    /// and discharge power inside of static min and max SOC range.  Also, includes
481    /// buffer for forcing [FuelConverter] to be active/on.
482    RGWDB(Box<RESGreedyWithDynamicBuffers>),
483    /// place holder for future variants
484    Placeholder,
485}
486
487impl Default for HEVPowertrainControls {
488    fn default() -> Self {
489        Self::RGWDB(Default::default())
490    }
491}
492
493impl SetCumulative for HEVPowertrainControls {
494    fn set_cumulative(&mut self, dt: si::Time) -> anyhow::Result<()> {
495        match self {
496            Self::RGWDB(rgwdb) => rgwdb.set_cumulative(dt)?,
497            Self::Placeholder => {}
498        }
499        Ok(())
500    }
501}
502impl Step for HEVPowertrainControls {
503    fn step<F: Fn() -> String>(&mut self, loc: F) -> anyhow::Result<()> {
504        match self {
505            HEVPowertrainControls::RGWDB(rgwdb) => rgwdb.step(loc)?,
506            HEVPowertrainControls::Placeholder => todo!(),
507        }
508        Ok(())
509    }
510}
511
512impl SaveState for HEVPowertrainControls {
513    fn save_state<F: Fn() -> String>(&mut self, loc: F) -> anyhow::Result<()> {
514        match self {
515            HEVPowertrainControls::RGWDB(rgwdb) => rgwdb.save_state(loc)?,
516            HEVPowertrainControls::Placeholder => todo!(),
517        }
518        Ok(())
519    }
520}
521impl CheckAndResetState for HEVPowertrainControls {
522    fn check_and_reset<F: Fn() -> String>(&mut self, loc: F) -> anyhow::Result<()> {
523        match self {
524            HEVPowertrainControls::RGWDB(rgwdb) => rgwdb.check_and_reset(loc)?,
525            HEVPowertrainControls::Placeholder => todo!(),
526        }
527        Ok(())
528    }
529}
530impl HistoryMethods for HEVPowertrainControls {
531    fn set_save_interval(&mut self, save_interval: Option<usize>) -> anyhow::Result<()> {
532        match self {
533            HEVPowertrainControls::RGWDB(rgwdb) => Ok(rgwdb.set_save_interval(save_interval)?),
534            HEVPowertrainControls::Placeholder => todo!("Placeholder"),
535        }
536    }
537
538    fn save_interval(&self) -> anyhow::Result<Option<usize>> {
539        match self {
540            HEVPowertrainControls::RGWDB(rgwdb) => rgwdb.save_interval(),
541            HEVPowertrainControls::Placeholder => todo!("Placeholder"),
542        }
543    }
544    fn clear(&mut self) {
545        match self {
546            HEVPowertrainControls::RGWDB(rgwdb) => rgwdb.clear(),
547            HEVPowertrainControls::Placeholder => todo!("Placeholder"),
548        }
549    }
550}
551
552impl Init for HEVPowertrainControls {
553    fn init(&mut self) -> Result<(), Error> {
554        match self {
555            Self::RGWDB(rgwb) => rgwb.init()?,
556            Self::Placeholder => {
557                todo!()
558            }
559        }
560        Ok(())
561    }
562}
563
564impl HEVPowertrainControls {
565    /// Determines power split between engine and electric machine
566    ///
567    /// # Arguments
568    /// - `pwr_prop_req`: tractive power required
569    /// - `veh_state`: vehicle state
570    /// - `hev_state`: HEV powertrain state
571    /// - `fc`: fuel converter
572    /// - `em_state`: electric machine state
573    /// - `res`: reversible energy storage (e.g. high voltage battery)
574    fn get_pwr_fc_and_em(
575        &mut self,
576        pwr_prop_req: si::Power,
577        fc: &FuelConverter,
578        em_state: &ElectricMachineState,
579        res: &ReversibleEnergyStorage,
580    ) -> anyhow::Result<(si::Power, si::Power)> {
581        let fc_state = &fc.state;
582        ensure!(
583            // `almost` is in case of negligible numerical precision discrepancies
584            almost_le_uom(
585                &pwr_prop_req,
586                &(*em_state.pwr_mech_fwd_out_max.get_fresh(|| format_dbg!())?
587                    + *fc_state.pwr_prop_max.get_fresh(|| format_dbg!())?),
588                None
589            ),
590            "{}
591`pwr_out_req`: {} kW
592`em_state.pwr_mech_fwd_out_max`: {} kW
593`fc_state.pwr_prop_max`: {} kW
594`res.state.soc`: {}",
595            format_dbg!(),
596            pwr_prop_req.get::<si::kilowatt>(),
597            em_state
598                .pwr_mech_fwd_out_max
599                .get_fresh(|| format_dbg!())?
600                .get::<si::kilowatt>(),
601            fc_state
602                .pwr_prop_max
603                .get_fresh(|| format_dbg!())?
604                .get::<si::kilowatt>(),
605            res.state
606                .soc
607                .get_fresh(|| format_dbg!())?
608                .get::<si::ratio>()
609        );
610
611        // # Brain dump for thermal stuff
612        // TODO: engine on/off w.r.t. thermal stuff should not come into play
613        // if there is no component (e.g. cabin) demanding heat from the engine.  My 2019
614        // Hyundai Ioniq will turn the engine off if there is no heat demand regardless of
615        // the coolant temperature
616        // TODO: make sure idle fuel gets converted to heat correctly
617
618        match self {
619            Self::RGWDB(rgwdb) => rgwdb.get_pwr_fc_and_em(fc, pwr_prop_req, em_state),
620            Self::Placeholder => todo!(),
621        }
622    }
623
624    pub fn engine_on(&self) -> anyhow::Result<bool> {
625        match self {
626            Self::RGWDB(rgwdb) => rgwdb.state.engine_on(),
627            Self::Placeholder => todo!(),
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#[named_struct_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}