1use super::{vehicle_model::VehicleState, *};
2use crate::{prelude::ElectricMachineState, vehicle::common::*};
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))]
9pub 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 #[has_state]
23 #[serde(default)]
24 pub pt_cntrl: HEVPowertrainControls,
25 #[serde(default)]
27 pub aux_cntrl: HEVAuxControls,
28 pub(crate) mass: Option<si::Mass>,
30 #[serde(default)]
31 pub sim_params: HEVSimulationParams,
32 #[serde(default)]
34 pub soc_bal_iter_history: Vec<Self>,
35 #[serde(default)]
38 pub soc_bal_iters: TrackedState<u32>,
39}
40
41impl HybridElectricVehicle {
42 pub fn check_buffers(&self, veh_mass: si::Mass) -> anyhow::Result<()> {
47 let (disch_buffer, chrg_buffer, fc_on_soc) = match &self.pt_cntrl {
49 HEVPowertrainControls::RGWDB(rgwdb) => {
50 let disch_buffer = (0.5
51 * veh_mass
52 * rgwdb
53 .speed_soc_disch_buffer
54 .with_context(|| format_dbg!())?
55 .powi(P2::new()))
56 .max(si::Energy::ZERO)
57 * rgwdb
58 .speed_soc_disch_buffer_coeff
59 .with_context(|| format_dbg!())?;
60
61 let chrg_buffer = (0.5
62 * veh_mass
63 * ((70.0 * uc::MPH).powi(P2::new())
64 - rgwdb
65 .speed_soc_regen_buffer
66 .with_context(|| format_dbg!())?
67 .powi(P2::new())))
68 .max(si::Energy::ZERO)
69 * rgwdb
70 .speed_soc_regen_buffer_coeff
71 .with_context(|| format_dbg!())?;
72
73 let fc_on_soc = {
74 let energy_delta_to_buffer_speed: si::Energy = 0.5
75 * veh_mass
76 * rgwdb
77 .speed_soc_fc_on_buffer
78 .with_context(|| format_dbg!())?
79 .powi(P2::new());
80 energy_delta_to_buffer_speed.max(si::Energy::ZERO)
81 * rgwdb
82 .speed_soc_fc_on_buffer_coeff
83 .with_context(|| format_dbg!())?
84 } / self.res.energy_capacity_usable()
85 + self.res.min_soc;
86
87 (disch_buffer, chrg_buffer, fc_on_soc)
88 }
89 HEVPowertrainControls::StartStop(_) => {
90 let fc_on_soc = 0.10 * (self.res.max_soc - self.res.min_soc) + self.res.min_soc;
91 let chrg_buffer = self.res.energy_capacity_usable();
92 let disch_buffer = self.res.energy_capacity_usable();
93 (disch_buffer, chrg_buffer, fc_on_soc)
94 }
95 };
96 if fc_on_soc > self.res.max_soc {
97 }
100 if fc_on_soc < self.res.min_soc {
101 }
104 if disch_buffer > self.res.energy_capacity_usable() {
105 }
115 if chrg_buffer > self.res.energy_capacity_usable() {
116 }
126 Ok(())
127 }
128}
129
130#[pyo3_api]
131impl HybridElectricVehicle {}
132
133impl HybridElectricVehicle {
134 pub fn new(
135 res: ReversibleEnergyStorage,
136 fs: FuelStorage,
137 fc: FuelConverter,
138 em: ElectricMachine,
139 transmission: Transmission,
140 pt_cntrl: HEVPowertrainControls,
141 aux_cntrl: HEVAuxControls,
142 mass: Option<si::Mass>,
143 sim_params: HEVSimulationParams,
144 ) -> anyhow::Result<Self> {
145 let mut hev = Self {
146 res,
147 fs,
148 fc,
149 em,
150 transmission,
151 pt_cntrl,
152 aux_cntrl,
153 mass,
154 sim_params,
155 soc_bal_iter_history: Default::default(),
156 soc_bal_iters: Default::default(),
157 };
158 hev.init()?;
159 Ok(hev)
160 }
161}
162
163impl HistoryMethods for HybridElectricVehicle {
164 fn save_interval(&self) -> anyhow::Result<Option<usize>> {
165 bail!("`save_interval` is not implemented in HybridElectricVehicle")
166 }
167 fn set_save_interval(&mut self, save_interval: Option<usize>) -> anyhow::Result<()> {
168 self.res.set_save_interval(save_interval)?;
169 self.fc.set_save_interval(save_interval)?;
171 self.em.set_save_interval(save_interval)?;
172 self.transmission.set_save_interval(save_interval)?;
173 self.pt_cntrl.set_save_interval(save_interval)?;
174 Ok(())
175 }
176 fn clear(&mut self) {
177 self.res.clear();
178 self.fc.clear();
180 self.em.clear();
181 self.transmission.clear();
182 self.pt_cntrl.clear();
183 }
184}
185
186impl Init for HybridElectricVehicle {
187 fn init(&mut self) -> Result<(), Error> {
188 self.fc
189 .init()
190 .map_err(|err| Error::InitError(format_dbg!(err)))?;
191 self.res
192 .init()
193 .map_err(|err| Error::InitError(format_dbg!(err)))?;
194 self.em
195 .init()
196 .map_err(|err| Error::InitError(format_dbg!(err)))?;
197 self.transmission
198 .init()
199 .map_err(|err| Error::InitError(format_dbg!(err)))?;
200 self.pt_cntrl
201 .init()
202 .map_err(|err| Error::InitError(format_dbg!(err)))?;
203 Ok(())
204 }
205}
206
207impl SerdeAPI for HybridElectricVehicle {}
208
209impl Powertrain for Box<HybridElectricVehicle> {
210 fn set_curr_pwr_prop_out_max(
211 &mut self,
212 _pwr_upstream: (si::Power, si::Power),
213 pwr_aux: si::Power,
214 dt: si::Time,
215 veh_state: &VehicleState,
216 ) -> anyhow::Result<()> {
217 let (disch_buffer, chrg_buffer) = match &mut self.pt_cntrl {
219 HEVPowertrainControls::RGWDB(rgwdb) => {
220 rgwdb.handle_fc_on_causes(&self.fc, veh_state, &self.res, &self.em.state)?;
221
222 let disch_buffer = (0.5
223 * *veh_state.mass.get_fresh(|| format_dbg!())?
224 * (rgwdb
225 .speed_soc_disch_buffer
226 .with_context(|| format_dbg!())?
227 .powi(P2::new())
228 - veh_state
229 .speed_ach
230 .get_stale(|| format_dbg!())?
231 .powi(P2::new())))
232 .max(si::Energy::ZERO)
233 * rgwdb
234 .speed_soc_disch_buffer_coeff
235 .with_context(|| format_dbg!())?;
236
237 let chrg_buffer = (0.5
238 * *veh_state.mass.get_fresh(|| format_dbg!())?
239 * (veh_state
240 .speed_ach
241 .get_stale(|| format_dbg!())?
242 .powi(P2::new())
243 - rgwdb
244 .speed_soc_regen_buffer
245 .with_context(|| format_dbg!())?
246 .powi(P2::new())))
247 .max(si::Energy::ZERO)
248 * rgwdb
249 .speed_soc_regen_buffer_coeff
250 .with_context(|| format_dbg!())?;
251
252 (disch_buffer, chrg_buffer)
253 }
254 HEVPowertrainControls::StartStop(cntrl) => {
255 cntrl.handle_fc_on_causes(&self.fc, veh_state, &self.res, dt)?;
256
257 let disch_buffer = 0.0 * uc::J;
258 let chrg_buffer = self.res.energy_capacity_usable();
259 (disch_buffer, chrg_buffer)
260 }
261 };
262 self.fc
264 .set_curr_pwr_out_max(dt)
265 .with_context(|| anyhow!(format_dbg!()))?;
266 self.res
267 .set_curr_pwr_out_max(dt, disch_buffer, chrg_buffer)
268 .with_context(|| anyhow!(format_dbg!()))?;
269
270 let (pwr_aux_res, pwr_aux_fc) = {
272 match self.aux_cntrl {
273 HEVAuxControls::AuxOnResPriority => {
274 if pwr_aux <= *self.res.state.pwr_disch_max.get_fresh(|| format_dbg!())? {
275 (pwr_aux, si::Power::ZERO)
276 } else {
277 (si::Power::ZERO, pwr_aux)
278 }
279 }
280 HEVAuxControls::AuxOnFcPriority => (si::Power::ZERO, pwr_aux),
281 }
282 };
283
284 match &mut self.pt_cntrl {
285 HEVPowertrainControls::RGWDB(rgwdb) => {
286 rgwdb
287 .state
288 .aux_power_demand
289 .update(pwr_aux_fc > si::Power::ZERO, || format_dbg!())?;
290 }
291 HEVPowertrainControls::StartStop(cntrl) => {
292 cntrl
293 .state
294 .aux_power_demand
295 .update(pwr_aux_fc > si::Power::ZERO, || format_dbg!())?;
296 }
297 }
298
299 self.fc
301 .set_curr_pwr_prop_max(pwr_aux_fc)
302 .with_context(|| anyhow!(format_dbg!()))?;
303 self.res
304 .set_curr_pwr_prop_max(pwr_aux_res)
305 .with_context(|| anyhow!(format_dbg!()))?;
306 self.em
307 .set_curr_pwr_prop_out_max(
308 self.res
311 .get_curr_pwr_prop_out_max()
312 .with_context(|| format_dbg!())?,
313 pwr_aux,
314 dt,
315 veh_state,
316 )
317 .with_context(|| anyhow!(format_dbg!()))?;
318 let em_pwr_prop_out_maxes = self
319 .em
320 .get_curr_pwr_prop_out_max()
321 .with_context(|| format_dbg!())?;
322 let fc_max = self.fc.state.pwr_prop_max.get_fresh(|| format_dbg!())?;
323 self.transmission
324 .set_curr_pwr_prop_out_max(
325 (em_pwr_prop_out_maxes.0 + *fc_max, em_pwr_prop_out_maxes.1),
326 f64::NAN * uc::W,
327 dt,
328 veh_state,
329 )
330 .with_context(|| format_dbg!())?;
331 Ok(())
332 }
333
334 fn get_curr_pwr_prop_out_max(&self) -> anyhow::Result<(si::Power, si::Power)> {
335 self.transmission
336 .get_curr_pwr_prop_out_max()
337 .with_context(|| format_dbg!())
338 }
339
340 fn solve(
341 &mut self,
342 pwr_out_req: si::Power,
343 _enabled: bool,
344 dt: si::Time,
345 ) -> anyhow::Result<Option<si::Power>> {
346 let pwr_in_transmission = self
351 .transmission
352 .solve(pwr_out_req, true, dt)
353 .with_context(|| format_dbg!())?
354 .with_context(|| format!("{}\nExpected `Some`", format_dbg!()))?;
355
356 let (fc_pwr_out_req, em_pwr_out_req) = self
362 .pt_cntrl
363 .get_pwr_fc_and_em(pwr_in_transmission, &self.fc, &self.em.state, &self.res)
364 .with_context(|| format_dbg!())?;
365 let fc_on: bool = self.pt_cntrl.fc_on().map_err(|err| {
366 anyhow::anyhow!(
367 "self.pt_cntrl.fc_on() failed at line {} with \
368 originating error [{}]",
369 format_dbg!(),
370 err
371 )
372 })?;
373
374 self.fc.solve(fc_pwr_out_req, fc_on, dt).map_err(|err| {
375 anyhow::anyhow!(
376 "self.fc.solve(fc_pwr_out_req, fc_on, dt) with values: \
377 fc_pwr_out_req={:?}, fc_on={}, dt={:?} \
378 failed at line {} \
379 with originating error [{}]",
380 fc_pwr_out_req,
381 fc_on,
382 dt,
383 format_dbg!(),
384 err
385 )
386 })?;
387
388 let res_pwr_out_req = self
389 .em
390 .solve(em_pwr_out_req, true, dt)
391 .map_err(|err| {
392 anyhow!(format!(
393 "em.solve failed at line {} with originating error [{}]",
394 format_dbg!(),
395 err
396 ))
397 })?
398 .with_context(|| format!("{}\nExpected `Some`", format_dbg!()))?;
399 self.res.solve(res_pwr_out_req, dt).map_err(|err| {
401 anyhow!(format!(
402 "res.solve failed at line {} with originating error [{}]",
403 format_dbg!(),
404 err
405 ))
406 })?;
407 Ok(None)
408 }
409
410 fn pwr_regen(&self) -> anyhow::Result<si::Power> {
412 self.transmission.pwr_regen().with_context(|| format_dbg!())
416 }
417}
418
419impl HybridElectricVehicle {
420 pub fn solve_thermal(
430 &mut self,
431 te_amb: si::Temperature,
432 pwr_thrml_fc_to_cab: Option<si::Power>,
433 veh_state: &mut VehicleState,
434 pwr_thrml_hvac_to_res: Option<si::Power>,
435 te_cab: Option<si::Temperature>,
436 dt: si::Time,
437 ) -> anyhow::Result<()> {
438 self.fc
439 .solve_thermal(te_amb, pwr_thrml_fc_to_cab, veh_state, dt)
440 .with_context(|| format_dbg!())?;
441 self.res
442 .solve_thermal(
443 te_amb,
444 pwr_thrml_hvac_to_res.unwrap_or_default(),
445 te_cab,
446 dt,
447 )
448 .with_context(|| format_dbg!())?;
449 Ok(())
450 }
451}
452
453impl Mass for HybridElectricVehicle {
454 fn mass(&self) -> anyhow::Result<Option<si::Mass>> {
455 let derived_mass = self
456 .derived_mass()
457 .with_context(|| anyhow!(format_dbg!()))?;
458 match (derived_mass, self.mass) {
459 (Some(derived_mass), Some(set_mass)) => {
460 ensure!(
461 utils::almost_eq_uom(&set_mass, &derived_mass, None),
462 format!(
463 "{}",
464 format_dbg!(utils::almost_eq_uom(&set_mass, &derived_mass, None)),
465 )
466 );
467 Ok(Some(set_mass))
468 }
469 _ => Ok(self.mass.or(derived_mass)),
470 }
471 }
472
473 fn set_mass(
474 &mut self,
475 new_mass: Option<si::Mass>,
476 side_effect: MassSideEffect,
477 ) -> anyhow::Result<()> {
478 ensure!(
479 side_effect == MassSideEffect::None,
480 "At the powertrain level, only `MassSideEffect::None` is allowed"
481 );
482 let derived_mass = self
483 .derived_mass()
484 .with_context(|| anyhow!(format_dbg!()))?;
485 self.mass = match (new_mass, derived_mass) {
486 (Some(new_mass), Some(dm)) => {
488 if dm != new_mass {
489 self.expunge_mass_fields();
490 }
491 Some(new_mass)
492 }
493 (Some(new_mass), None) => Some(new_mass),
494 (None, Some(dm)) => Some(dm),
495 (None, None) => bail!(
496 "Not all mass fields in `{}` are set and no mass was provided.",
497 stringify!(HybridElectricVehicle)
498 ),
499 };
500 ensure!(
501 self.mass > Some(0.0 * uc::KG),
502 "{} mass must be positive",
503 stringify!(HybridElectricVehicle)
504 );
505 Ok(())
506 }
507
508 fn derived_mass(&self) -> anyhow::Result<Option<si::Mass>> {
509 let fc_mass = self.fc.mass().with_context(|| anyhow!(format_dbg!()))?;
510 let fs_mass = self.fs.mass().with_context(|| anyhow!(format_dbg!()))?;
511 let res_mass = self.res.mass().with_context(|| anyhow!(format_dbg!()))?;
512 let em_mass = self.em.mass().with_context(|| anyhow!(format_dbg!()))?;
513 let transmission_mass = self
514 .transmission
515 .mass()
516 .with_context(|| anyhow!(format_dbg!()))?;
517 match (fc_mass, fs_mass, res_mass, em_mass, transmission_mass) {
518 (
519 Some(fc_mass),
520 Some(fs_mass),
521 Some(res_mass),
522 Some(em_mass),
523 Some(transmission_mass),
524 ) => Ok(Some(
525 fc_mass + fs_mass + res_mass + em_mass + transmission_mass,
526 )),
527 (None, None, None, None, None) => Ok(None),
528 _ => bail!(
529 "`{}` field masses are not consistently set to `Some` or `None`",
530 stringify!(HybridElectricVehicle)
531 ),
532 }
533 }
534
535 fn expunge_mass_fields(&mut self) {
536 self.fc.expunge_mass_fields();
537 self.fs.expunge_mass_fields();
538 self.res.expunge_mass_fields();
539 self.em.expunge_mass_fields();
540 self.transmission.expunge_mass_fields();
541 self.mass = None;
542 }
543}
544
545#[serde_api]
546#[derive(
547 Clone,
548 Debug,
549 Default,
550 Deserialize,
551 Serialize,
552 PartialEq,
553 HistoryVec,
554 StateMethods,
555 SetCumulative,
556)]
557#[non_exhaustive]
558#[serde(deny_unknown_fields)]
559pub struct RGWDBState {
560 pub i: TrackedState<usize>,
562 pub fc_temperature_too_low: TrackedState<bool>,
564 pub vehicle_speed_too_high: TrackedState<bool>,
567 pub on_time_too_short: TrackedState<bool>,
569 pub propulsion_power_demand: TrackedState<bool>,
571 pub propulsion_power_demand_soft: TrackedState<bool>,
573 pub aux_power_demand: TrackedState<bool>,
575 pub charging_for_low_soc: TrackedState<bool>,
577 pub soc_fc_on_buffer: TrackedState<si::Ratio>,
579}
580impl SerdeAPI for RGWDBState {}
581impl Init for RGWDBState {}
582
583impl RGWDBState {
584 fn fc_on(&self) -> anyhow::Result<bool> {
586 Ok(*self.fc_temperature_too_low.get_fresh(|| format_dbg!())?
587 || *self.vehicle_speed_too_high.get_fresh(|| format_dbg!())?
588 || *self.on_time_too_short.get_fresh(|| format_dbg!())?
589 || *self.propulsion_power_demand.get_fresh(|| format_dbg!())?
590 || *self
591 .propulsion_power_demand_soft
592 .get_fresh(|| format_dbg!())?
593 || *self.aux_power_demand.get_fresh(|| format_dbg!())?
594 || *self.charging_for_low_soc.get_fresh(|| format_dbg!())?)
595 }
596}
597
598#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)]
600#[non_exhaustive]
601#[serde(deny_unknown_fields)]
602pub struct HEVSimulationParams {
603 pub res_per_fuel_lim: si::Ratio,
605 pub soc_balance_iter_err: u32,
607 pub balance_soc: bool,
609 pub save_soc_bal_iters: bool,
611}
612
613impl HEVSimulationParams {
614 pub fn new(
615 res_per_fuel_lim: si::Ratio,
616 soc_balance_iter_err: u32,
617 balance_soc: bool,
618 save_soc_bal_iters: bool,
619 ) -> anyhow::Result<Self> {
620 Ok(Self {
621 res_per_fuel_lim,
622 soc_balance_iter_err,
623 balance_soc,
624 save_soc_bal_iters,
625 })
626 }
627}
628
629impl Default for HEVSimulationParams {
630 fn default() -> Self {
631 Self {
632 res_per_fuel_lim: uc::R * 0.005,
633 soc_balance_iter_err: 5,
634 balance_soc: true,
635 save_soc_bal_iters: false,
636 }
637 }
638}
639
640#[derive(
641 Clone, Debug, PartialEq, Deserialize, Serialize, Default, IsVariant, derive_more::From, TryInto,
642)]
643pub enum HEVAuxControls {
644 #[default]
646 AuxOnResPriority,
647 AuxOnFcPriority,
649}
650
651#[derive(
652 Clone, Debug, PartialEq, Deserialize, Serialize, IsVariant, derive_more::From, TryInto,
653)]
654pub enum HEVPowertrainControls {
655 RGWDB(Box<RESGreedyWithDynamicBuffers>),
659 #[serde(alias = "StopStart")]
662 StartStop(Box<HEVStartStopControl>),
663}
664
665impl Default for HEVPowertrainControls {
666 fn default() -> Self {
667 Self::RGWDB(Default::default())
668 }
669}
670
671impl SetCumulative for HEVPowertrainControls {
672 fn set_cumulative<F: Fn() -> String>(&mut self, dt: si::Time, loc: F) -> anyhow::Result<()> {
673 match self {
674 Self::RGWDB(rgwdb) => {
675 rgwdb.set_cumulative(dt, || format!("{}\n{}", loc(), format_dbg!()))?
676 }
677 Self::StartStop(cntrl) => {
678 cntrl.set_cumulative(dt, || format!("{}\n{}", loc(), format_dbg!()))?
679 }
680 }
681 Ok(())
682 }
683
684 fn reset_cumulative<F: Fn() -> String>(&mut self, loc: F) -> anyhow::Result<()> {
685 match self {
686 Self::RGWDB(rgwdb) => {
687 rgwdb.reset_cumulative(|| format!("{}\n{}", loc(), format_dbg!()))?
688 }
689 Self::StartStop(cntrl) => {
690 cntrl.reset_cumulative(|| format!("{}\n{}", loc(), format_dbg!()))?
691 }
692 }
693 Ok(())
694 }
695}
696impl Step for HEVPowertrainControls {
697 fn step<F: Fn() -> String>(&mut self, loc: F) -> anyhow::Result<()> {
698 match self {
699 HEVPowertrainControls::RGWDB(rgwdb) => rgwdb.step(loc)?,
700 HEVPowertrainControls::StartStop(cntrls) => cntrls.step(loc)?,
701 }
702 Ok(())
703 }
704
705 fn reset_step<F: Fn() -> String>(&mut self, loc: F) -> anyhow::Result<()> {
706 match self {
707 HEVPowertrainControls::RGWDB(rgwdb) => rgwdb.reset_step(loc)?,
708 HEVPowertrainControls::StartStop(cntrls) => cntrls.reset_step(loc)?,
709 }
710 Ok(())
711 }
712}
713
714impl StateMethods for HEVPowertrainControls {}
715
716impl SaveState for HEVPowertrainControls {
717 fn save_state<F: Fn() -> String>(&mut self, loc: F) -> anyhow::Result<()> {
718 match self {
719 HEVPowertrainControls::RGWDB(rgwdb) => rgwdb.save_state(loc)?,
720 HEVPowertrainControls::StartStop(cntrl) => cntrl.save_state(loc)?,
721 }
722 Ok(())
723 }
724}
725impl TrackedStateMethods for HEVPowertrainControls {
726 fn check_and_reset<F: Fn() -> String>(&mut self, loc: F) -> anyhow::Result<()> {
727 match self {
728 HEVPowertrainControls::RGWDB(rgwdb) => rgwdb.check_and_reset(loc)?,
729 HEVPowertrainControls::StartStop(cntrl) => cntrl.check_and_reset(loc)?,
730 }
731 Ok(())
732 }
733
734 fn mark_fresh<F: Fn() -> String>(&mut self, loc: F) -> anyhow::Result<()> {
735 match self {
736 HEVPowertrainControls::RGWDB(rgwdb) => rgwdb.mark_fresh(loc)?,
737 HEVPowertrainControls::StartStop(cntrl) => cntrl.mark_fresh(loc)?,
738 }
739 Ok(())
740 }
741}
742impl HistoryMethods for HEVPowertrainControls {
743 fn set_save_interval(&mut self, save_interval: Option<usize>) -> anyhow::Result<()> {
744 match self {
745 HEVPowertrainControls::RGWDB(rgwdb) => Ok(rgwdb.set_save_interval(save_interval)?),
746 HEVPowertrainControls::StartStop(cntrl) => Ok(cntrl.set_save_interval(save_interval)?),
747 }
748 }
749
750 fn save_interval(&self) -> anyhow::Result<Option<usize>> {
751 match self {
752 HEVPowertrainControls::RGWDB(rgwdb) => rgwdb.save_interval(),
753 HEVPowertrainControls::StartStop(cntrl) => cntrl.save_interval(),
754 }
755 }
756 fn clear(&mut self) {
757 match self {
758 HEVPowertrainControls::RGWDB(rgwdb) => rgwdb.clear(),
759 HEVPowertrainControls::StartStop(cntrl) => cntrl.clear(),
760 }
761 }
762}
763
764impl Init for HEVPowertrainControls {
765 fn init(&mut self) -> Result<(), Error> {
766 match self {
767 Self::RGWDB(rgwb) => rgwb.init()?,
768 Self::StartStop(cntrl) => cntrl.init()?,
769 }
770 Ok(())
771 }
772}
773
774impl HEVPowertrainControls {
775 fn get_pwr_fc_and_em(
785 &mut self,
786 pwr_prop_req: si::Power,
787 fc: &FuelConverter,
788 em_state: &ElectricMachineState,
789 res: &ReversibleEnergyStorage,
790 ) -> anyhow::Result<(si::Power, si::Power)> {
791 let fc_state = &fc.state;
792 ensure!(
793 almost_le_uom(
795 &pwr_prop_req,
796 &(*em_state.pwr_mech_fwd_out_max.get_fresh(|| format_dbg!())?
797 + *fc_state.pwr_prop_max.get_fresh(|| format_dbg!())?),
798 None
799 ),
800 "{}
801`pwr_out_req`: {} kW
802`em_state.pwr_mech_fwd_out_max`: {} kW
803`fc_state.pwr_prop_max`: {} kW
804`res.state.soc`: {}",
805 format_dbg!(),
806 pwr_prop_req.get::<si::kilowatt>(),
807 em_state
808 .pwr_mech_fwd_out_max
809 .get_fresh(|| format_dbg!())?
810 .get::<si::kilowatt>(),
811 fc_state
812 .pwr_prop_max
813 .get_fresh(|| format_dbg!())?
814 .get::<si::kilowatt>(),
815 res.state
816 .soc
817 .get_fresh(|| format_dbg!())?
818 .get::<si::ratio>()
819 );
820
821 match self {
829 Self::RGWDB(rgwdb) => rgwdb.get_pwr_fc_and_em(fc, pwr_prop_req, em_state),
830 Self::StartStop(cntrl) => cntrl.get_pwr_fc_and_em(fc, pwr_prop_req, em_state),
831 }
832 }
833
834 pub fn fc_on(&self) -> anyhow::Result<bool> {
835 match self {
836 Self::RGWDB(rgwdb) => rgwdb.state.fc_on(),
837 Self::StartStop(cntrl) => cntrl.state.fc_on(),
838 }
839 }
840
841 pub fn handle_fc_on_causes_for_speed(&mut self, speed: si::Velocity) -> anyhow::Result<()> {
842 match self {
843 Self::StartStop(cntrl) => HEVStartStopControl::handle_fc_on_causes_for_speed(
844 &mut cntrl.state.vehicle_not_stopped,
845 speed,
846 cntrl.stopped_speed_threshold,
847 )?,
848 _ => (),
849 }
850 Ok(())
851 }
852}
853
854#[serde_api]
859#[derive(Clone, Debug, PartialEq, Deserialize, Serialize, Default, StateMethods, SetCumulative)]
860#[cfg_attr(feature = "pyo3", pyclass(module = "fastsim", subclass, eq))]
861#[non_exhaustive]
862#[serde(deny_unknown_fields)]
863pub struct RESGreedyWithDynamicBuffers {
864 pub speed_soc_disch_buffer: Option<si::Velocity>,
867 pub speed_soc_disch_buffer_coeff: Option<si::Ratio>,
869 pub speed_soc_fc_on_buffer: Option<si::Velocity>,
872 pub speed_soc_fc_on_buffer_coeff: Option<si::Ratio>,
874 pub speed_soc_regen_buffer: Option<si::Velocity>,
878 pub speed_soc_regen_buffer_coeff: Option<si::Ratio>,
880 pub fc_min_time_on: Option<si::Time>,
883 pub speed_fc_forced_on: Option<si::Velocity>,
885 pub frac_pwr_demand_fc_forced_on: Option<si::Ratio>,
888 pub frac_of_most_eff_pwr_to_run_fc: Option<si::Ratio>,
893 pub save_interval: Option<usize>,
897 #[serde(default)]
899 pub temp_fc_forced_on: Option<si::Temperature>,
900 #[serde(default)]
902 pub temp_fc_allowed_off: Option<si::Temperature>,
903 #[serde(default)]
905 pub state: RGWDBState,
906 #[serde(default, skip_serializing_if = "RGWDBStateHistoryVec::is_empty")]
908 pub history: RGWDBStateHistoryVec,
909}
910
911#[pyo3_api]
912impl RESGreedyWithDynamicBuffers {}
913
914impl RESGreedyWithDynamicBuffers {
915 pub fn new(
916 speed_soc_disch_buffer: Option<si::Velocity>,
917 speed_soc_disch_buffer_coeff: Option<si::Ratio>,
918 speed_soc_fc_on_buffer: Option<si::Velocity>,
919 speed_soc_fc_on_buffer_coeff: Option<si::Ratio>,
920 speed_soc_regen_buffer: Option<si::Velocity>,
921 speed_soc_regen_buffer_coeff: Option<si::Ratio>,
922 fc_min_time_on: Option<si::Time>,
923 speed_fc_forced_on: Option<si::Velocity>,
924 frac_pwr_demand_fc_forced_on: Option<si::Ratio>,
925 frac_of_most_eff_pwr_to_run_fc: Option<si::Ratio>,
926 temp_fc_forced_on: Option<si::Temperature>,
927 temp_fc_allowed_off: Option<si::Temperature>,
928 save_interval: Option<usize>,
929 ) -> anyhow::Result<Self> {
930 let mut res_greedy_w_dynamic_buffers = Self {
931 speed_soc_disch_buffer,
932 speed_soc_disch_buffer_coeff,
933 speed_soc_fc_on_buffer,
934 speed_soc_fc_on_buffer_coeff,
935 speed_soc_regen_buffer,
936 speed_soc_regen_buffer_coeff,
937 fc_min_time_on,
938 speed_fc_forced_on,
939 frac_pwr_demand_fc_forced_on,
940 frac_of_most_eff_pwr_to_run_fc,
941 temp_fc_forced_on,
942 temp_fc_allowed_off,
943 state: RGWDBState::default(),
944 history: RGWDBStateHistoryVec::default(),
945 save_interval,
946 };
947 res_greedy_w_dynamic_buffers.init()?;
948 Ok(res_greedy_w_dynamic_buffers)
949 }
950}
951
952impl HistoryMethods for RESGreedyWithDynamicBuffers {
953 fn set_save_interval(&mut self, save_interval: Option<usize>) -> anyhow::Result<()> {
954 self.save_interval = save_interval;
955 Ok(())
956 }
957
958 fn save_interval(&self) -> anyhow::Result<Option<usize>> {
959 Ok(self.save_interval)
960 }
961
962 fn clear(&mut self) {
963 self.history.clear();
964 }
965}
966
967impl Init for RESGreedyWithDynamicBuffers {
968 fn init(&mut self) -> Result<(), Error> {
969 init_opt_default!(self, speed_soc_disch_buffer, 50.0 * uc::MPH);
971 init_opt_default!(self, speed_soc_disch_buffer_coeff, 1.0 * uc::R);
972 init_opt_default!(
973 self,
974 speed_soc_fc_on_buffer,
975 self.speed_soc_disch_buffer.unwrap() * 1.2
976 );
977 init_opt_default!(self, speed_soc_fc_on_buffer_coeff, 1.0 * uc::R);
978 init_opt_default!(self, speed_soc_regen_buffer, 30. * uc::MPH);
979 init_opt_default!(self, speed_soc_regen_buffer_coeff, 1.0 * uc::R);
980 init_opt_default!(self, fc_min_time_on, uc::S * 5.0);
981 init_opt_default!(self, speed_fc_forced_on, uc::MPH * 75.);
982 init_opt_default!(self, frac_pwr_demand_fc_forced_on, uc::R * 0.75);
983 init_opt_default!(self, frac_of_most_eff_pwr_to_run_fc, 1.0 * uc::R);
984 Ok(())
985 }
986}
987impl SerdeAPI for RESGreedyWithDynamicBuffers {}
988
989impl RESGreedyWithDynamicBuffers {
990 fn get_pwr_fc_and_em(
991 &mut self,
992 fc: &FuelConverter,
993 pwr_prop_req: si::Power,
994 em_state: &ElectricMachineState,
995 ) -> anyhow::Result<(si::Power, si::Power)> {
996 let em_pwr = pwr_prop_req
1001 .min(*em_state.pwr_mech_fwd_out_max.get_fresh(|| format_dbg!())?)
1002 .max(-*em_state.pwr_mech_regen_max.get_fresh(|| format_dbg!())?);
1003 let (fc_pwr, em_pwr) = if !self.state.fc_on()? {
1005 (si::Power::ZERO, em_pwr)
1007 } else {
1008 let frac_of_pwr_for_peak_eff: si::Ratio = self
1010 .frac_of_most_eff_pwr_to_run_fc
1011 .with_context(|| format_dbg!())?;
1012 let fc_pwr = if pwr_prop_req < si::Power::ZERO {
1013 (*em_state.pwr_mech_regen_max.get_fresh(|| format_dbg!())? + pwr_prop_req)
1016 .min(fc.pwr_for_peak_eff * frac_of_pwr_for_peak_eff)
1018 .max(si::Power::ZERO)
1020 } else {
1021 if pwr_prop_req - em_pwr > fc.pwr_for_peak_eff * frac_of_pwr_for_peak_eff {
1023 pwr_prop_req - em_pwr
1025 } else {
1026 (pwr_prop_req - em_pwr)
1031 .max(fc.pwr_for_peak_eff * frac_of_pwr_for_peak_eff)
1035 .min(
1038 pwr_prop_req
1039 + *em_state.pwr_mech_regen_max.get_fresh(|| format_dbg!())?,
1040 )
1041 }
1042 }
1043 .min(*fc.state.pwr_prop_max.get_fresh(|| format_dbg!())?);
1045
1046 let em_pwr_corrected = (pwr_prop_req - fc_pwr)
1048 .max(-*em_state.pwr_mech_regen_max.get_fresh(|| format_dbg!())?);
1049 (fc_pwr, em_pwr_corrected)
1050 };
1051 Ok((fc_pwr, em_pwr))
1052 }
1053
1054 fn handle_fc_on_causes(
1055 &mut self,
1056 fc: &FuelConverter,
1057 veh_state: &VehicleState,
1058 res: &ReversibleEnergyStorage,
1059 em_state: &ElectricMachineState,
1060 ) -> Result<(), anyhow::Error> {
1061 self.handle_fc_on_causes_for_temp(fc)?;
1062 self.handle_fc_on_causes_for_speed(veh_state)?;
1063 self.handle_fc_on_causes_for_low_soc(res, veh_state)?;
1064 self.handle_fc_on_causes_for_pwr_demand(
1065 *veh_state
1066 .pwr_tractive
1067 .get_stale(|| format_dbg!(veh_state.pwr_tractive))?,
1068 em_state,
1069 &fc.state,
1070 )
1071 .with_context(|| format_dbg!())?;
1072 self.handle_fc_on_causes_for_on_time(fc)?;
1073 Ok(())
1074 }
1075
1076 fn handle_fc_on_causes_for_on_time(&mut self, fc: &FuelConverter) -> Result<(), anyhow::Error> {
1077 self.state.on_time_too_short.update(*fc.state.fc_on.get_stale(|| format_dbg!())? && *fc.state.time_on.get_stale(|| format_dbg!())?
1078 < self.fc_min_time_on.with_context(|| {
1079 anyhow!(
1080 "{}\n Expected `ResGreedyWithBuffers::init` to have been called beforehand.",
1081 format_dbg!()
1082 )
1083 })?, || format_dbg!())?;
1084 Ok(())
1085 }
1086
1087 fn handle_fc_on_causes_for_pwr_demand(
1090 &mut self,
1091 pwr_out_req_for_cyc: si::Power,
1092 em_state: &ElectricMachineState,
1093 fc_state: &FuelConverterState,
1094 ) -> Result<(), anyhow::Error> {
1095 let frac_pwr_demand_fc_forced_on: si::Ratio = self
1096 .frac_pwr_demand_fc_forced_on
1097 .with_context(|| format_dbg!())?;
1098 self.state.propulsion_power_demand_soft.update(
1099 pwr_out_req_for_cyc
1100 > frac_pwr_demand_fc_forced_on
1101 * (*em_state.pwr_mech_fwd_out_max.get_stale(|| format_dbg!())?
1102 + *fc_state.pwr_out_max.get_stale(|| format_dbg!())?),
1103 || format_dbg!(),
1104 )?;
1105 self.state.propulsion_power_demand.update(
1106 pwr_out_req_for_cyc - *em_state.pwr_mech_fwd_out_max.get_stale(|| format_dbg!())?
1107 >= si::Power::ZERO,
1108 || format_dbg!(),
1109 )?;
1110 Ok(())
1111 }
1112
1113 fn handle_fc_on_causes_for_low_soc(
1115 &mut self,
1116 res: &ReversibleEnergyStorage,
1117 veh_state: &VehicleState,
1118 ) -> anyhow::Result<()> {
1119 self.state.soc_fc_on_buffer.update(
1120 {
1121 let energy_delta_to_buffer_speed: si::Energy = 0.5
1122 * *veh_state.mass.get_fresh(|| format_dbg!())?
1123 * (self
1124 .speed_soc_fc_on_buffer
1125 .with_context(|| format_dbg!())?
1126 .powi(P2::new())
1127 - veh_state
1128 .speed_ach
1129 .get_stale(|| format_dbg!())?
1130 .powi(P2::new()));
1131 energy_delta_to_buffer_speed.max(si::Energy::ZERO)
1132 * self
1133 .speed_soc_fc_on_buffer_coeff
1134 .with_context(|| format_dbg!())?
1135 } / res.energy_capacity_usable()
1136 + res.min_soc,
1137 || format_dbg!(),
1138 )?;
1139 self.state.charging_for_low_soc.update(
1140 *res.state.soc.get_stale(|| format_dbg!())?
1141 < *self.state.soc_fc_on_buffer.get_fresh(|| format_dbg!())?,
1142 || format_dbg!(),
1143 )?;
1144 Ok(())
1145 }
1146
1147 fn handle_fc_on_causes_for_speed(&mut self, veh_state: &VehicleState) -> anyhow::Result<()> {
1149 self.state.vehicle_speed_too_high.update(
1150 *veh_state.speed_ach.get_stale(|| format_dbg!())?
1151 > self.speed_fc_forced_on.with_context(|| format_dbg!())?,
1152 || format_dbg!(),
1153 )?;
1154 Ok(())
1155 }
1156
1157 fn handle_fc_on_causes_for_temp(&mut self, fc: &FuelConverter) -> anyhow::Result<()> {
1160 match (
1161 match fc.temperature() {
1162 Some(fct) => Some(*fct.get_fresh(|| format_dbg!())?),
1163 None => None,
1164 },
1165 match fc.temperature() {
1166 Some(fct) => Some(*fct.get_fresh(|| format_dbg!())?),
1167 None => None,
1168 },
1169 self.temp_fc_forced_on,
1170 self.temp_fc_allowed_off,
1171 ) {
1172 (None, None, None, None) => {
1173 self.state
1174 .fc_temperature_too_low
1175 .update(false, || format_dbg!())?;
1176 }
1177 (
1178 Some(temperature),
1179 Some(temp_prev),
1180 Some(temp_fc_forced_on),
1181 Some(temp_fc_allowed_off),
1182 ) => {
1183 self.state.fc_temperature_too_low.update(
1184 temperature < temp_fc_forced_on ||
1186 (temp_prev < temp_fc_forced_on && temperature < temp_fc_allowed_off),
1188 || format_dbg!(),
1189 )?;
1190 }
1191 _ => {
1192 bail!(
1193 "{}\n`fc.temperature()`, `fc.temp_prev()`, `self.temp_fc_forced_on`, and
1194`self.temp_fc_allowed_off` must all be `None` or `Some` because these controls are necessary
1195for an HEV equipped with thermal models or superfluous otherwise",
1196 format_dbg!((
1197 fc.temperature(),
1198 self.temp_fc_forced_on,
1199 self.temp_fc_allowed_off
1200 ))
1201 );
1202 }
1203 }
1204 Ok(())
1205 }
1206}
1207
1208#[serde_api]
1209#[derive(
1210 Clone,
1211 Debug,
1212 Default,
1213 Deserialize,
1214 Serialize,
1215 PartialEq,
1216 HistoryVec,
1217 StateMethods,
1218 SetCumulative,
1219)]
1220#[non_exhaustive]
1221#[serde(deny_unknown_fields)]
1222pub struct StartStopState {
1223 pub i: TrackedState<usize>,
1225 pub fc_temperature_too_low: TrackedState<bool>,
1227 pub vehicle_not_stopped: TrackedState<bool>,
1229 pub on_time_too_short: TrackedState<bool>,
1231 pub aux_power_demand: TrackedState<bool>,
1233 pub charging_for_low_soc: TrackedState<bool>,
1235 pub time_vehicle_stopped: TrackedState<si::Time>,
1237 pub vehicle_not_stopped_long_enough: TrackedState<bool>,
1239 pub has_traction_power_request: TrackedState<bool>,
1241}
1242
1243impl StartStopState {
1244 fn fc_on(&self) -> anyhow::Result<bool> {
1246 Ok(*self.fc_temperature_too_low.get_fresh(|| format_dbg!())?
1247 || *self.vehicle_not_stopped.get_fresh(|| format_dbg!())?
1248 || *self.on_time_too_short.get_fresh(|| format_dbg!())?
1249 || *self.aux_power_demand.get_fresh(|| format_dbg!())?
1250 || *self.charging_for_low_soc.get_fresh(|| format_dbg!())?
1251 || *self
1252 .vehicle_not_stopped_long_enough
1253 .get_fresh(|| format_dbg!())?
1254 || *self
1255 .has_traction_power_request
1256 .get_fresh(|| format_dbg!())?)
1257 }
1258}
1259
1260#[serde_api]
1261#[derive(Clone, Debug, PartialEq, Deserialize, Serialize, StateMethods, SetCumulative)]
1262#[cfg_attr(feature = "pyo3", pyclass(module = "fastsim", subclass, eq))]
1263#[non_exhaustive]
1264#[serde(deny_unknown_fields)]
1265pub struct HEVStartStopControl {
1266 pub fc_min_time_on: Option<si::Time>,
1269 pub soc_fc_forced_on: Option<si::Ratio>,
1272 pub frac_of_most_eff_pwr_to_run_fc: Option<si::Ratio>,
1277 #[serde(default)]
1279 pub temp_fc_forced_on: Option<si::Temperature>,
1280 #[serde(default)]
1282 pub temp_fc_allowed_off: Option<si::Temperature>,
1283 #[serde(default)]
1287 pub time_delay_after_stop_until_fc_can_turn_off: Option<si::Time>,
1288 #[serde(default = "HEVStartStopControl::def_stopped_speed_threshold")]
1290 pub stopped_speed_threshold: si::Velocity,
1291 pub em_can_regen: Option<bool>,
1293 #[serde(default)]
1294 pub save_interval: Option<usize>,
1296 #[serde(default)]
1298 pub state: StartStopState,
1299 #[serde(default, skip_serializing_if = "StartStopStateHistoryVec::is_empty")]
1301 pub history: StartStopStateHistoryVec,
1302}
1303
1304#[pyo3_api]
1305impl HEVStartStopControl {}
1306
1307impl HistoryMethods for HEVStartStopControl {
1308 fn set_save_interval(&mut self, save_interval: Option<usize>) -> anyhow::Result<()> {
1309 self.save_interval = save_interval;
1310 Ok(())
1311 }
1312
1313 fn save_interval(&self) -> anyhow::Result<Option<usize>> {
1314 Ok(self.save_interval)
1315 }
1316
1317 fn clear(&mut self) {
1318 self.history.clear();
1319 }
1320}
1321
1322impl Init for HEVStartStopControl {
1323 fn init(&mut self) -> Result<(), Error> {
1324 init_opt_default!(self, fc_min_time_on, 5.0 * uc::S);
1325 init_opt_default!(self, soc_fc_forced_on, 0.1 * uc::R);
1326 init_opt_default!(self, frac_of_most_eff_pwr_to_run_fc, 1.0 * uc::R);
1327 init_opt_default!(
1328 self,
1329 time_delay_after_stop_until_fc_can_turn_off,
1330 0.0 * uc::S
1331 );
1332 Ok(())
1333 }
1334}
1335
1336impl SerdeAPI for HEVStartStopControl {}
1337
1338impl Default for HEVStartStopControl {
1339 fn default() -> Self {
1340 Self {
1341 fc_min_time_on: Option::default(),
1342 soc_fc_forced_on: Option::default(),
1343 frac_of_most_eff_pwr_to_run_fc: Option::default(),
1344 temp_fc_forced_on: Option::default(),
1345 temp_fc_allowed_off: Option::default(),
1346 time_delay_after_stop_until_fc_can_turn_off: Option::default(),
1347 stopped_speed_threshold: Self::def_stopped_speed_threshold(),
1348 em_can_regen: Option::default(),
1349 save_interval: Option::default(),
1350 state: StartStopState::default(),
1351 history: StartStopStateHistoryVec::default(),
1352 }
1353 }
1354}
1355
1356impl StartStopControl for HEVStartStopControl {}
1357
1358impl HEVStartStopControl {
1359 fn def_stopped_speed_threshold() -> si::Velocity {
1360 0.05 * uc::MPS
1361 }
1362
1363 pub fn new(
1364 fc_min_time_on: Option<si::Time>,
1365 soc_fc_forced_on: Option<si::Ratio>,
1366 frac_of_most_eff_pwr_to_run_fc: Option<si::Ratio>,
1367 temp_fc_forced_on: Option<si::Temperature>,
1368 temp_fc_allowed_off: Option<si::Temperature>,
1369 time_delay_after_stop_until_fc_can_turn_off: Option<si::Time>,
1370 em_can_regen: Option<bool>,
1371 save_interval: Option<usize>,
1372 ) -> anyhow::Result<Self> {
1373 let mut result = Self {
1374 fc_min_time_on,
1375 soc_fc_forced_on,
1376 frac_of_most_eff_pwr_to_run_fc,
1377 temp_fc_forced_on,
1378 temp_fc_allowed_off,
1379 time_delay_after_stop_until_fc_can_turn_off,
1380 stopped_speed_threshold: Self::def_stopped_speed_threshold(),
1381 em_can_regen,
1382 save_interval,
1383 state: StartStopState::default(),
1384 history: StartStopStateHistoryVec::default(),
1385 };
1386 result.init()?;
1387 Ok(result)
1388 }
1389
1390 fn get_pwr_fc_and_em(
1391 &mut self,
1392 fc: &FuelConverter,
1393 pwr_prop_req: si::Power,
1394 em_state: &ElectricMachineState,
1395 ) -> anyhow::Result<(si::Power, si::Power)> {
1396 let no_prop_pwr_demand = pwr_prop_req == si::Power::ZERO;
1397 let em_can_regen = self.em_can_regen.unwrap_or(true);
1398 let em_pwr = pwr_prop_req.min(si::Power::ZERO).max(if em_can_regen {
1399 -*em_state.pwr_mech_regen_max.get_fresh(|| format_dbg!())?
1400 } else {
1401 si::Power::ZERO
1402 });
1403 let (fc_pwr, em_pwr) = {
1404 let frac_of_pwr_for_peak_eff: si::Ratio = self
1406 .frac_of_most_eff_pwr_to_run_fc
1407 .with_context(|| format_dbg!())?;
1408 let fc_pwr = if pwr_prop_req < si::Power::ZERO {
1409 (*em_state.pwr_mech_regen_max.get_fresh(|| format_dbg!())? + pwr_prop_req)
1412 .min(fc.pwr_for_peak_eff * frac_of_pwr_for_peak_eff)
1414 .max(si::Power::ZERO)
1416 } else if no_prop_pwr_demand {
1417 0.0 * uc::W
1421 } else {
1422 if pwr_prop_req - em_pwr > fc.pwr_for_peak_eff * frac_of_pwr_for_peak_eff {
1424 pwr_prop_req - em_pwr
1426 } else {
1427 (pwr_prop_req - em_pwr)
1432 .max(fc.pwr_for_peak_eff * frac_of_pwr_for_peak_eff)
1436 .min(
1439 pwr_prop_req
1440 + *em_state.pwr_mech_regen_max.get_fresh(|| format_dbg!())?,
1441 )
1442 }
1443 }
1444 .min(*fc.state.pwr_prop_max.get_fresh(|| format_dbg!())?);
1446
1447 let em_pwr_corrected = (pwr_prop_req - fc_pwr).max(if em_can_regen {
1449 -*em_state.pwr_mech_regen_max.get_fresh(|| format_dbg!())?
1450 } else {
1451 si::Power::ZERO
1452 });
1453 (fc_pwr, em_pwr_corrected)
1454 };
1455 Self::handle_fc_on_causes_for_propulsion_request(
1456 &mut self.state.has_traction_power_request,
1457 fc_pwr,
1458 )?;
1459 Ok((fc_pwr, em_pwr))
1460 }
1461
1462 pub fn handle_fc_on_causes(
1463 &mut self,
1464 fc: &FuelConverter,
1465 veh_state: &VehicleState,
1466 res: &ReversibleEnergyStorage,
1467 dt: si::Time,
1468 ) -> Result<(), anyhow::Error> {
1469 Self::handle_fc_on_causes_for_stopped_time(
1471 &mut self.state.time_vehicle_stopped,
1472 &mut self.state.vehicle_not_stopped_long_enough,
1473 veh_state,
1474 dt,
1475 self.time_delay_after_stop_until_fc_can_turn_off,
1476 self.stopped_speed_threshold,
1477 )?;
1478 Self::handle_fc_on_causes_for_temp(
1479 fc,
1480 self.temp_fc_forced_on,
1481 self.temp_fc_allowed_off,
1482 &mut self.state.fc_temperature_too_low,
1483 )?;
1484 self.handle_fc_on_causes_for_low_soc(res)?;
1486 Self::handle_fc_on_causes_for_on_time(
1487 fc,
1488 self.fc_min_time_on,
1489 &mut self.state.on_time_too_short,
1490 )?;
1491 Ok(())
1492 }
1493
1494 fn handle_fc_on_causes_for_low_soc(
1495 &mut self,
1496 res: &ReversibleEnergyStorage,
1497 ) -> anyhow::Result<()> {
1498 let soc_fc_forced_on = if let Some(soc_frac) = self.soc_fc_forced_on {
1499 soc_frac * (res.max_soc - res.min_soc) + res.min_soc
1500 } else {
1501 0.1 * (res.max_soc - res.min_soc) + res.min_soc
1502 };
1503 self.state.charging_for_low_soc.update(
1504 *res.state.soc.get_stale(|| format_dbg!())? < soc_fc_forced_on,
1505 || format_dbg!(),
1506 )?;
1507 Ok(())
1508 }
1509}