Skip to main content

fastsim_core/vehicle/
conv.rs

1use crate::vehicle::common::StartStopControl;
2
3use super::*;
4
5#[serde_api]
6#[derive(Clone, Debug, PartialEq, Deserialize, Serialize, StateMethods, SetCumulative)]
7#[cfg_attr(feature = "pyo3", pyclass(module = "fastsim", subclass, eq))]
8#[non_exhaustive]
9#[serde(deny_unknown_fields)]
10pub struct DfcoControls {
11    /// If true DFCO is enabled, else it will never run.
12    pub dfco_enabled: bool,
13    /// The minimum speed at or above which which DFCO can activate.
14    pub minimum_dfco_speed: si::Velocity,
15    /// The minimum vehicle acceleration required for
16    /// DFCO to be able to activate.
17    pub minimum_dfco_deceleration: si::Acceleration,
18    /// Speed threshold at or below which DFCO is considered unavailable due to near-stop operation.
19    #[serde(default = "DfcoControls::def_stopped_speed_threshold")]
20    pub stopped_speed_threshold: si::Velocity,
21    #[serde(default)]
22    /// Time step interval between saves. 1 is a good option. If None, no saving occurs.
23    pub save_interval: Option<usize>,
24    /// current state of control variables
25    #[serde(default)]
26    pub state: DfcoState,
27    /// history of current state
28    #[serde(default, skip_serializing_if = "DfcoStateHistoryVec::is_empty")]
29    pub history: DfcoStateHistoryVec,
30}
31
32#[pyo3_api]
33impl DfcoControls {}
34
35impl DfcoControls {
36    fn def_stopped_speed_threshold() -> si::Velocity {
37        0.05 * uc::MPS
38    }
39
40    /// Determine if decel fuel cut-off (DFCO) is disabled based on vehicle
41    /// dynamics considerations (i.e., speed, acceleration). Note: considerations
42    /// related to whether the engine is too cold and such would be handled
43    /// elsewhere.
44    pub fn is_dfco_disabled_due_to_veh_dynamics(
45        prev_speed: si::Velocity,
46        speed: si::Velocity,
47        dt: si::Time,
48        dfco_allowed: bool,
49        minimum_dfco_speed: si::Velocity,
50        minimum_dfco_deceleration: si::Acceleration,
51        stopped_speed_threshold: si::Velocity,
52    ) -> bool {
53        let decel = (speed - prev_speed) / dt;
54        let is_accel = decel > si::Acceleration::ZERO;
55        if !dfco_allowed {
56            true
57        } else if speed < minimum_dfco_speed {
58            true
59        } else if speed <= stopped_speed_threshold {
60            true
61        } else if is_accel || decel > minimum_dfco_deceleration {
62            true
63        } else {
64            // NOTE: we **can** apply DFCO
65            false
66        }
67    }
68}
69
70impl HistoryMethods for DfcoControls {
71    fn set_save_interval(&mut self, save_interval: Option<usize>) -> anyhow::Result<()> {
72        self.save_interval = save_interval;
73        Ok(())
74    }
75
76    fn save_interval(&self) -> anyhow::Result<Option<usize>> {
77        Ok(self.save_interval)
78    }
79
80    fn clear(&mut self) {
81        self.history.clear();
82    }
83}
84
85impl Init for DfcoControls {
86    fn init(&mut self) -> Result<(), Error> {
87        if self.minimum_dfco_deceleration > si::Acceleration::ZERO {
88            Err(Error::InitError(String::from(
89                "minimum_dfco_acceleration must be <= 0 m/s2",
90            )))
91        } else if self.minimum_dfco_speed < si::Velocity::ZERO {
92            Err(Error::InitError(String::from(
93                "minimum_dfco_speed must be >= 0 m/s",
94            )))
95        } else {
96            Ok(())
97        }
98    }
99}
100
101impl SerdeAPI for DfcoControls {}
102
103impl Default for DfcoControls {
104    fn default() -> Self {
105        Self {
106            dfco_enabled: bool::default(),
107            minimum_dfco_speed: si::Velocity::default(),
108            minimum_dfco_deceleration: si::Acceleration::default(),
109            stopped_speed_threshold: Self::def_stopped_speed_threshold(),
110            save_interval: Option::default(),
111            state: DfcoState::default(),
112            history: DfcoStateHistoryVec::default(),
113        }
114    }
115}
116
117impl DfcoControls {
118    pub fn new(
119        dfco_enabled: bool,
120        minimum_dfco_speed: si::Velocity,
121        minimum_dfco_deceleration: si::Acceleration,
122        save_interval: Option<usize>,
123    ) -> anyhow::Result<Self> {
124        let mut result = Self {
125            dfco_enabled,
126            minimum_dfco_speed,
127            minimum_dfco_deceleration,
128            stopped_speed_threshold: Self::def_stopped_speed_threshold(),
129            save_interval,
130            state: DfcoState::default(),
131            history: DfcoStateHistoryVec::default(),
132        };
133        result.init()?;
134        Ok(result)
135    }
136}
137
138#[serde_api]
139#[derive(
140    Clone,
141    Debug,
142    Default,
143    Deserialize,
144    Serialize,
145    PartialEq,
146    HistoryVec,
147    StateMethods,
148    SetCumulative,
149)]
150#[non_exhaustive]
151#[serde(deny_unknown_fields)]
152pub struct DfcoState {
153    /// time step index
154    pub i: TrackedState<usize>,
155    /// vehicle dynamics must support DFCO to be on
156    pub vehicle_dynamics_prevent_dfco: TrackedState<bool>,
157}
158
159#[serde_api]
160#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, StateMethods, SetCumulative)]
161#[non_exhaustive]
162#[serde(deny_unknown_fields)]
163#[cfg_attr(feature = "pyo3", pyclass(module = "fastsim", subclass, eq))]
164/// Conventional vehicle with only a FuelConverter as a power source
165pub struct ConventionalVehicle {
166    pub fs: FuelStorage,
167    #[has_state]
168    pub fc: FuelConverter,
169    #[has_state]
170    pub transmission: Transmission,
171    /// control strategy. Especially used for start-stop and DFCO.
172    #[has_state]
173    #[serde(default)]
174    pub pt_cntrl: ConvPowertrainControls,
175    #[has_state]
176    #[serde(default)]
177    pub dfco_cntrl: DfcoControls,
178    /// powertrain mass
179    pub(crate) mass: Option<si::Mass>,
180    /// Alternator efficiency used to calculate aux mechanical power demand on engine
181    pub alt_eff: si::Ratio,
182}
183
184#[pyo3_api]
185impl ConventionalVehicle {}
186
187impl ConventionalVehicle {
188    pub fn new(
189        fs: FuelStorage,
190        fc: FuelConverter,
191        transmission: Transmission,
192        mass: Option<si::Mass>,
193        pt_cntrl: ConvPowertrainControls,
194        dfco_cntrl: DfcoControls,
195        alt_eff: si::Ratio,
196    ) -> anyhow::Result<Self> {
197        let mut conv = Self {
198            fs,
199            fc,
200            transmission,
201            pt_cntrl,
202            dfco_cntrl,
203            mass,
204            alt_eff,
205        };
206        conv.init()?;
207        Ok(conv)
208    }
209}
210
211impl SerdeAPI for ConventionalVehicle {}
212impl Init for ConventionalVehicle {
213    fn init(&mut self) -> Result<(), Error> {
214        self.fc
215            .init()
216            .map_err(|err| Error::InitError(format_dbg!(err)))?;
217        self.fs
218            .init()
219            .map_err(|err| Error::InitError(format_dbg!(err)))?;
220        self.transmission
221            .init()
222            .map_err(|err| Error::InitError(format_dbg!(err)))?;
223        Ok(())
224    }
225}
226
227impl HistoryMethods for ConventionalVehicle {
228    fn save_interval(&self) -> anyhow::Result<Option<usize>> {
229        bail!("`save_interval` is not implemented in ConventionalVehicle")
230    }
231    fn set_save_interval(&mut self, save_interval: Option<usize>) -> anyhow::Result<()> {
232        // self.fs.set_save_interval(save_interval)?;
233        self.fc.set_save_interval(save_interval)?;
234        self.transmission.set_save_interval(save_interval)?;
235        Ok(())
236    }
237    fn clear(&mut self) {
238        self.fc.clear();
239        self.transmission.clear();
240    }
241}
242
243impl Powertrain for Box<ConventionalVehicle> {
244    fn set_curr_pwr_prop_out_max(
245        &mut self,
246        _pwr_upstream: (si::Power, si::Power),
247        pwr_aux: si::Power,
248        dt: si::Time,
249        veh_state: &VehicleState,
250    ) -> anyhow::Result<()> {
251        // TODO: account for transmission efficiency in here
252        self.fc
253            .set_curr_pwr_out_max(dt)
254            .with_context(|| anyhow!(format_dbg!()))?;
255        self.fc
256            .set_curr_pwr_prop_max(pwr_aux / self.alt_eff)
257            .with_context(|| anyhow!(format_dbg!()))?;
258        self.transmission
259            .set_curr_pwr_prop_out_max(
260                (
261                    *self.fc.state.pwr_prop_max.get_fresh(|| format_dbg!())?,
262                    si::Power::ZERO,
263                ),
264                f64::NAN * uc::W,
265                dt,
266                veh_state,
267            )
268            .with_context(|| format_dbg!())?;
269        match &mut self.pt_cntrl {
270            ConvPowertrainControls::Normal => (),
271            ConvPowertrainControls::StartStop(ss) => {
272                ss.handle_fc_on_causes(&self.fc, veh_state, dt)?;
273            }
274        }
275        Ok(())
276    }
277
278    fn get_curr_pwr_prop_out_max(&self) -> anyhow::Result<(si::Power, si::Power)> {
279        self.transmission
280            .get_curr_pwr_prop_out_max()
281            .with_context(|| format_dbg!())
282    }
283
284    fn solve(
285        &mut self,
286        pwr_out_req: si::Power,
287        _enabled: bool,
288        dt: si::Time,
289    ) -> anyhow::Result<Option<si::Power>> {
290        // NOTE: think about the possibility of engine braking, not urgent
291        ensure!(pwr_out_req >= si::Power::ZERO, format_dbg!());
292        ensure!(almost_le_uom(
293            &pwr_out_req,
294            self.transmission
295                .state
296                .pwr_out_fwd_max
297                .get_fresh(|| format_dbg!())?,
298            None
299        ));
300        ensure!(almost_le_uom(
301            &pwr_out_req,
302            self.transmission
303                .state
304                .pwr_out_fwd_max
305                .get_fresh(|| format_dbg!())?,
306            None
307        ));
308        let pwr_in_transmission = self
309            .transmission
310            .solve(pwr_out_req, true, dt)
311            .with_context(|| format_dbg!())?
312            .with_context(|| format!("{}\nExpected `Some`", format_dbg!()))?;
313        match &mut self.pt_cntrl {
314            ConvPowertrainControls::Normal => (),
315            ConvPowertrainControls::StartStop(ss) => {
316                ConvStartStopControl::handle_fc_on_causes_for_propulsion_request(
317                    &mut ss.state.has_traction_power_request,
318                    pwr_in_transmission,
319                )?;
320            }
321        }
322        let fc_on: bool = {
323            let fc_on = self.pt_cntrl.fc_on()?;
324            let fc_on_dfco = *self
325                .dfco_cntrl
326                .state
327                .vehicle_dynamics_prevent_dfco
328                .get_fresh(|| format_dbg!())?;
329            let no_tractive_effort_requested = pwr_out_req <= 1e-6 * uc::KW;
330            let fc_off = !fc_on || (!fc_on_dfco && no_tractive_effort_requested);
331            !fc_off
332        };
333        if !fc_on {
334            // NOTE: zero out aux loads if engine is off
335            // NOTE: we could possibly use Vehicle.pwr_aux_base
336            //       to tell if we have "regular" auxiliaries vs
337            //       "special" auxiliaries for which the engine
338            //       cannot be shut down.
339            self.fc.state.pwr_aux.mark_stale();
340            self.fc
341                .state
342                .pwr_aux
343                .update(si::Power::ZERO, || format_dbg!())?;
344        }
345        self.fc
346            .solve(pwr_in_transmission, fc_on, dt)
347            .with_context(|| anyhow!(format_dbg!()))?;
348        Ok(None)
349    }
350
351    fn pwr_regen(&self) -> anyhow::Result<si::Power> {
352        Ok(si::Power::ZERO)
353    }
354}
355
356impl ConventionalVehicle {
357    pub fn solve_thermal(
358        &mut self,
359        te_amb: si::Temperature,
360        pwr_thrml_fc_to_cab: Option<si::Power>,
361        veh_state: &mut VehicleState,
362        dt: si::Time,
363    ) -> anyhow::Result<()> {
364        self.fc
365            .solve_thermal(te_amb, pwr_thrml_fc_to_cab, veh_state, dt)
366    }
367}
368
369impl Mass for ConventionalVehicle {
370    fn mass(&self) -> anyhow::Result<Option<si::Mass>> {
371        let derived_mass = self
372            .derived_mass()
373            .with_context(|| anyhow!(format_dbg!()))?;
374        match (derived_mass, self.mass) {
375            (Some(derived_mass), Some(set_mass)) => {
376                ensure!(
377                    utils::almost_eq_uom(&set_mass, &derived_mass, None),
378                    format!(
379                        "{}",
380                        format_dbg!(utils::almost_eq_uom(&set_mass, &derived_mass, None)),
381                    )
382                );
383                Ok(Some(set_mass))
384            }
385            _ => Ok(self.mass.or(derived_mass)),
386        }
387    }
388
389    fn set_mass(
390        &mut self,
391        new_mass: Option<si::Mass>,
392        side_effect: MassSideEffect,
393    ) -> anyhow::Result<()> {
394        ensure!(
395            side_effect == MassSideEffect::None,
396            "At the powertrain level, only `MassSideEffect::None` is allowed"
397        );
398        let derived_mass = self
399            .derived_mass()
400            .with_context(|| anyhow!(format_dbg!()))?;
401        self.mass = match (new_mass, derived_mass) {
402            // Set using provided `new_mass`, setting constituent mass fields to `None` to match if inconsistent
403            (Some(new_mass), Some(dm)) => {
404                if dm != new_mass {
405                    self.expunge_mass_fields();
406                }
407                Some(new_mass)
408            }
409            (Some(new_mass), None) => Some(new_mass),
410            (None, Some(dm)) => Some(dm),
411            (None, None) => bail!(
412                "Not all mass fields in `{}` are set and no mass was provided.",
413                stringify!(ConventionalVehicle)
414            ),
415        };
416        ensure!(
417            self.mass > Some(0.0 * uc::KG),
418            "{} mass must be positive",
419            stringify!(ConventionalVehicle)
420        );
421        Ok(())
422    }
423
424    fn derived_mass(&self) -> anyhow::Result<Option<si::Mass>> {
425        let fc_mass = self.fc.mass().with_context(|| anyhow!(format_dbg!()))?;
426        let fs_mass = self.fs.mass().with_context(|| anyhow!(format_dbg!()))?;
427        let transmission_mass = self
428            .transmission
429            .mass()
430            .with_context(|| anyhow!(format_dbg!()))?;
431        match (fc_mass, fs_mass, transmission_mass) {
432            (Some(fc_mass), Some(fs_mass), Some(transmission_mass)) => {
433                Ok(Some(fc_mass + fs_mass + transmission_mass))
434            }
435            (None, None, None) => Ok(None),
436            _ => bail!(
437                "`{}` field masses are not consistently set to `Some` or `None`",
438                stringify!(ConventionalVehicle)
439            ),
440        }
441    }
442
443    fn expunge_mass_fields(&mut self) {
444        self.fc.expunge_mass_fields();
445        self.fs.expunge_mass_fields();
446        self.transmission.expunge_mass_fields();
447        self.mass = None;
448    }
449}
450
451#[derive(
452    Clone, Debug, PartialEq, Deserialize, Serialize, IsVariant, derive_more::From, TryInto,
453)]
454pub enum ConvPowertrainControls {
455    /// Normal controller that doesn't do anything special
456    Normal,
457    /// Start/Stop controller that allows the fuel converter to turn off at
458    /// stop under certain conditions
459    #[serde(alias = "StopStart")]
460    StartStop(Box<ConvStartStopControl>),
461}
462
463impl Default for ConvPowertrainControls {
464    fn default() -> Self {
465        Self::Normal
466    }
467}
468
469impl SetCumulative for ConvPowertrainControls {
470    fn set_cumulative<F: Fn() -> String>(&mut self, dt: si::Time, loc: F) -> anyhow::Result<()> {
471        match self {
472            Self::Normal => Ok(()),
473            Self::StartStop(cntrl) => {
474                cntrl.set_cumulative(dt, || format!("{}\n{}", loc(), format_dbg!()))?;
475                Ok(())
476            }
477        }
478    }
479
480    fn reset_cumulative<F: Fn() -> String>(&mut self, loc: F) -> anyhow::Result<()> {
481        match self {
482            Self::Normal => Ok(()),
483            Self::StartStop(cntrl) => {
484                cntrl.reset_cumulative(|| format!("{}\n{}", loc(), format_dbg!()))?;
485                Ok(())
486            }
487        }
488    }
489}
490
491impl Step for ConvPowertrainControls {
492    fn step<F: Fn() -> String>(&mut self, loc: F) -> anyhow::Result<()> {
493        match self {
494            Self::Normal => Ok(()),
495            Self::StartStop(cntrl) => cntrl.step(loc),
496        }
497    }
498
499    fn reset_step<F: Fn() -> String>(&mut self, loc: F) -> anyhow::Result<()> {
500        match self {
501            Self::Normal => Ok(()),
502            Self::StartStop(cntrls) => cntrls.reset_step(loc),
503        }
504    }
505}
506
507impl StateMethods for ConvPowertrainControls {}
508
509impl SaveState for ConvPowertrainControls {
510    fn save_state<F: Fn() -> String>(&mut self, loc: F) -> anyhow::Result<()> {
511        match self {
512            Self::Normal => Ok(()),
513            Self::StartStop(cntrl) => cntrl.save_state(loc),
514        }
515    }
516}
517
518impl TrackedStateMethods for ConvPowertrainControls {
519    fn check_and_reset<F: Fn() -> String>(&mut self, loc: F) -> anyhow::Result<()> {
520        match self {
521            Self::Normal => Ok(()),
522            Self::StartStop(cntrl) => cntrl.check_and_reset(loc),
523        }
524    }
525
526    fn mark_fresh<F: Fn() -> String>(&mut self, loc: F) -> anyhow::Result<()> {
527        match self {
528            Self::Normal => Ok(()),
529            Self::StartStop(cntrl) => cntrl.mark_fresh(loc),
530        }
531    }
532}
533
534impl HistoryMethods for ConvPowertrainControls {
535    fn set_save_interval(&mut self, save_interval: Option<usize>) -> anyhow::Result<()> {
536        match self {
537            Self::Normal => Ok(()),
538            Self::StartStop(cntrl) => Ok(cntrl.set_save_interval(save_interval)?),
539        }
540    }
541
542    fn save_interval(&self) -> anyhow::Result<Option<usize>> {
543        match self {
544            Self::Normal => Ok(Option::None),
545            Self::StartStop(cntrl) => cntrl.save_interval(),
546        }
547    }
548
549    fn clear(&mut self) {
550        match self {
551            Self::Normal => (),
552            Self::StartStop(cntrl) => cntrl.clear(),
553        }
554    }
555}
556
557impl Init for ConvPowertrainControls {
558    fn init(&mut self) -> Result<(), Error> {
559        match self {
560            Self::Normal => Ok(()),
561            Self::StartStop(cntrl) => cntrl.init(),
562        }
563    }
564}
565
566impl ConvPowertrainControls {
567    pub fn fc_on(&self) -> anyhow::Result<bool> {
568        match self {
569            Self::Normal => Ok(true),
570            Self::StartStop(cntrl) => cntrl.state.fc_on(),
571        }
572    }
573
574    pub fn handle_fc_on_causes_for_speed(&mut self, speed: si::Velocity) -> anyhow::Result<()> {
575        match self {
576            Self::Normal => Ok(()),
577            Self::StartStop(cntrl) => ConvStartStopControl::handle_fc_on_causes_for_speed(
578                &mut cntrl.state.vehicle_not_stopped,
579                speed,
580                cntrl.stopped_speed_threshold,
581            ),
582        }
583    }
584}
585
586#[serde_api]
587#[derive(Clone, Debug, PartialEq, Deserialize, Serialize, StateMethods, SetCumulative)]
588#[cfg_attr(feature = "pyo3", pyclass(module = "fastsim", subclass, eq))]
589#[non_exhaustive]
590#[serde(deny_unknown_fields)]
591pub struct ConvStartStopControl {
592    /// Minimum time engine must remain on if it was on during the previous
593    /// simulation time step.
594    #[serde(default)]
595    pub fc_min_time_on: Option<si::Time>,
596    /// temperature at which engine is forced on to warm up
597    #[serde(default)]
598    pub temp_fc_forced_on: Option<si::Temperature>,
599    /// temperature at which engine is allowed to turn off due to being sufficiently warm
600    #[serde(default)]
601    pub temp_fc_allowed_off: Option<si::Temperature>,
602    /// Time delay after the vehicle reaches a stop before the engine is allowed
603    /// to turn off. This is to try to prevent engine stopping when the vehicle
604    /// stop is only momentary.
605    #[serde(default)]
606    pub time_delay_after_stop_until_fc_can_turn_off: Option<si::Time>,
607    /// Speed threshold at or below which vehicle is considered stopped for start-stop logic.
608    #[serde(default = "ConvStartStopControl::def_stopped_speed_threshold")]
609    pub stopped_speed_threshold: si::Velocity,
610    #[serde(default)]
611    /// Time step interval between saves. 1 is a good option. If None, no saving occurs.
612    pub save_interval: Option<usize>,
613    /// current state of control variables
614    #[serde(default)]
615    pub state: ConvStartStopState,
616    /// history of current state
617    #[serde(
618        default,
619        skip_serializing_if = "ConvStartStopStateHistoryVec::is_empty"
620    )]
621    pub history: ConvStartStopStateHistoryVec,
622}
623
624#[pyo3_api]
625impl ConvStartStopControl {}
626
627impl StartStopControl for ConvStartStopControl {}
628
629impl HistoryMethods for ConvStartStopControl {
630    fn set_save_interval(&mut self, save_interval: Option<usize>) -> anyhow::Result<()> {
631        self.save_interval = save_interval;
632        Ok(())
633    }
634
635    fn save_interval(&self) -> anyhow::Result<Option<usize>> {
636        Ok(self.save_interval)
637    }
638
639    fn clear(&mut self) {
640        self.history.clear();
641    }
642}
643
644impl Init for ConvStartStopControl {
645    fn init(&mut self) -> Result<(), Error> {
646        init_opt_default!(self, fc_min_time_on, uc::S * 5.0);
647        init_opt_default!(
648            self,
649            time_delay_after_stop_until_fc_can_turn_off,
650            0.0 * uc::S
651        );
652        Ok(())
653    }
654}
655
656impl SerdeAPI for ConvStartStopControl {}
657
658impl Default for ConvStartStopControl {
659    fn default() -> Self {
660        Self {
661            fc_min_time_on: Option::default(),
662            temp_fc_forced_on: Option::default(),
663            temp_fc_allowed_off: Option::default(),
664            time_delay_after_stop_until_fc_can_turn_off: Option::default(),
665            stopped_speed_threshold: Self::def_stopped_speed_threshold(),
666            save_interval: Option::default(),
667            state: ConvStartStopState::default(),
668            history: ConvStartStopStateHistoryVec::default(),
669        }
670    }
671}
672
673impl ConvStartStopControl {
674    fn def_stopped_speed_threshold() -> si::Velocity {
675        0.05 * uc::MPS
676    }
677
678    pub fn new(
679        fc_min_time_on: Option<si::Time>,
680        temp_fc_forced_on: Option<si::Temperature>,
681        temp_fc_allowed_off: Option<si::Temperature>,
682        time_delay_after_stop_until_fc_can_turn_off: Option<si::Time>,
683        save_interval: Option<usize>,
684    ) -> anyhow::Result<Self> {
685        let mut result = Self {
686            fc_min_time_on,
687            temp_fc_forced_on,
688            temp_fc_allowed_off,
689            time_delay_after_stop_until_fc_can_turn_off,
690            stopped_speed_threshold: Self::def_stopped_speed_threshold(),
691            save_interval,
692            state: ConvStartStopState::default(),
693            history: ConvStartStopStateHistoryVec::default(),
694        };
695        result.init()?;
696        Ok(result)
697    }
698
699    pub fn handle_fc_on_causes(
700        &mut self,
701        fc: &FuelConverter,
702        veh_state: &VehicleState,
703        dt: si::Time,
704    ) -> anyhow::Result<()> {
705        // NOTE: handle_fc_on_causes_for_propulsion_request called elsewhere
706        Self::handle_fc_on_causes_for_stopped_time(
707            &mut self.state.time_vehicle_stopped,
708            &mut self.state.vehicle_not_stopped_long_enough,
709            veh_state,
710            dt,
711            self.time_delay_after_stop_until_fc_can_turn_off,
712            self.stopped_speed_threshold,
713        )?;
714        Self::handle_fc_on_causes_for_temp(
715            fc,
716            self.temp_fc_forced_on,
717            self.temp_fc_allowed_off,
718            &mut self.state.fc_temperature_too_low,
719        )?;
720        // NOTE: handle_fc_on_causes_for_speed(speed) called elsewhere
721        Self::handle_fc_on_causes_for_on_time(
722            fc,
723            self.fc_min_time_on,
724            &mut self.state.on_time_too_short,
725        )?;
726        Ok(())
727    }
728}
729
730#[serde_api]
731#[derive(
732    Clone,
733    Debug,
734    Default,
735    Deserialize,
736    Serialize,
737    PartialEq,
738    HistoryVec,
739    StateMethods,
740    SetCumulative,
741)]
742#[non_exhaustive]
743#[serde(deny_unknown_fields)]
744pub struct ConvStartStopState {
745    /// time step index
746    pub i: TrackedState<usize>,
747    /// Engine must be on to self heat if thermal model is enabled
748    pub fc_temperature_too_low: TrackedState<bool>,
749    /// Engine start-stop can only happen while vehicle is stopped
750    pub vehicle_not_stopped: TrackedState<bool>,
751    /// Engine has not been on long enough (usually 30 s)
752    pub on_time_too_short: TrackedState<bool>,
753    /// The total time vehicle has been stopped
754    pub time_vehicle_stopped: TrackedState<si::Time>,
755    /// Vehicle stopped time
756    pub vehicle_not_stopped_long_enough: TrackedState<bool>,
757    /// Vehicle has a request for traction power for the current timestep
758    pub has_traction_power_request: TrackedState<bool>,
759}
760
761impl ConvStartStopState {
762    /// If any of the causes are true, engine must be on
763    fn fc_on(&self) -> anyhow::Result<bool> {
764        let c1 = *self.fc_temperature_too_low.get_fresh(|| format_dbg!())?;
765        let c2 = *self.vehicle_not_stopped.get_fresh(|| format_dbg!())?;
766        let c3 = *self.on_time_too_short.get_fresh(|| format_dbg!())?;
767        let c4 = *self
768            .vehicle_not_stopped_long_enough
769            .get_fresh(|| format_dbg!())?;
770        let c5 = *self
771            .has_traction_power_request
772            .get_fresh(|| format_dbg!())?;
773        Ok(c1 || c2 || c3 || c4 || c5)
774    }
775}
776
777#[cfg(test)]
778pub(crate) mod tests {
779    use super::*;
780
781    fn make_favorable_dfco_conditions() -> (
782        si::Velocity,     // prev_speed
783        si::Velocity,     // speed
784        si::Time,         // dt
785        bool,             // dfco_allowed
786        si::Velocity,     // minimum_dfco_speed
787        si::Acceleration, // minimum_dfco_deceleration
788    ) {
789        (
790            40.0 * uc::MPH,
791            36.0 * uc::MPH,
792            1.0 * uc::S,
793            true,
794            20.0 * uc::MPH,
795            0.0 * uc::MPS2,
796        )
797    }
798
799    #[test]
800    fn dfco_activates_when_all_conditions_are_good() {
801        let (prev_speed, speed, dt, dfco_allowed, minimum_dfco_speed, minimum_dfco_deceleration) =
802            make_favorable_dfco_conditions();
803        let result = DfcoControls::is_dfco_disabled_due_to_veh_dynamics(
804            prev_speed,
805            speed,
806            dt,
807            dfco_allowed,
808            minimum_dfco_speed,
809            minimum_dfco_deceleration,
810            0.05 * uc::MPS,
811        );
812        assert_eq!(false, result);
813    }
814
815    #[test]
816    fn dfco_cannot_be_active_if_speed_too_low() {
817        let (_prev_speed, _speed, dt, dfco_allowed, minimum_dfco_speed, minimum_dfco_deceleration) =
818            make_favorable_dfco_conditions();
819        let prev_speed = 10.0 * uc::MPH;
820        let speed = 8.0 * uc::MPH;
821        let result = DfcoControls::is_dfco_disabled_due_to_veh_dynamics(
822            prev_speed,
823            speed,
824            dt,
825            dfco_allowed,
826            minimum_dfco_speed,
827            minimum_dfco_deceleration,
828            0.05 * uc::MPS,
829        );
830        assert_eq!(result, true);
831    }
832
833    #[test]
834    fn dfco_cannot_be_active_if_not_decelerating() {
835        let (prev_speed, _speed, dt, dfco_allowed, minimum_dfco_speed, minimum_dfco_deceleration) =
836            make_favorable_dfco_conditions();
837        let speed = prev_speed + 2.0 * uc::MPH;
838        let result = DfcoControls::is_dfco_disabled_due_to_veh_dynamics(
839            prev_speed,
840            speed,
841            dt,
842            dfco_allowed,
843            minimum_dfco_speed,
844            minimum_dfco_deceleration,
845            0.05 * uc::MPS,
846        );
847        assert_eq!(true, result);
848    }
849
850    #[test]
851    fn dfco_cannot_be_active_if_not_allowed() {
852        let (prev_speed, speed, dt, _dfco_allowed, minimum_dfco_speed, minimum_dfco_deceleration) =
853            make_favorable_dfco_conditions();
854        let dfco_allowed = false;
855        let result = DfcoControls::is_dfco_disabled_due_to_veh_dynamics(
856            prev_speed,
857            speed,
858            dt,
859            dfco_allowed,
860            minimum_dfco_speed,
861            minimum_dfco_deceleration,
862            0.05 * uc::MPS,
863        );
864        assert_eq!(true, result);
865    }
866}