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))]
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 #[serde(skip_serializing_if = "Vec::is_empty")]
35 pub soc_bal_iter_history: Vec<Self>,
36 #[serde(default)]
39 pub soc_bal_iters: TrackedState<u32>,
40}
41
42#[pyo3_api]
43impl HybridElectricVehicle {}
44
45impl HistoryMethods for HybridElectricVehicle {
46 fn save_interval(&self) -> anyhow::Result<Option<usize>> {
47 bail!("`save_interval` is not implemented in HybridElectricVehicle")
48 }
49 fn set_save_interval(&mut self, save_interval: Option<usize>) -> anyhow::Result<()> {
50 self.res.set_save_interval(save_interval)?;
51 self.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.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 let (disch_buffer, chrg_buffer) = match &mut self.pt_cntrl {
100 HEVPowertrainControls::RGWDB(rgwdb) => {
101 rgwdb.handle_fc_on_causes(&self.fc, veh_state, &self.res, &self.em.state)?;
102
103 let disch_buffer = (0.5
104 * *veh_state.mass.get_fresh(|| format_dbg!())?
105 * (rgwdb
106 .speed_soc_disch_buffer
107 .with_context(|| format_dbg!())?
108 .powi(typenum::P2::new())
109 - veh_state
110 .speed_ach
111 .get_stale(|| format_dbg!())?
112 .powi(typenum::P2::new())))
113 .max(si::Energy::ZERO)
114 * rgwdb
115 .speed_soc_disch_buffer_coeff
116 .with_context(|| format_dbg!())?;
117
118 let chrg_buffer = (0.5
119 * *veh_state.mass.get_fresh(|| format_dbg!())?
120 * (veh_state
121 .speed_ach
122 .get_stale(|| format_dbg!())?
123 .powi(typenum::P2::new())
124 - rgwdb
125 .speed_soc_regen_buffer
126 .with_context(|| format_dbg!())?
127 .powi(typenum::P2::new())))
128 .max(si::Energy::ZERO)
129 * rgwdb
130 .speed_soc_regen_buffer_coeff
131 .with_context(|| format_dbg!())?;
132
133 (disch_buffer, chrg_buffer)
134 }
135 };
136 self.fc
138 .set_curr_pwr_out_max(dt)
139 .with_context(|| anyhow!(format_dbg!()))?;
140 self.res
141 .set_curr_pwr_out_max(dt, disch_buffer, chrg_buffer)
142 .with_context(|| anyhow!(format_dbg!()))?;
143
144 let (pwr_aux_res, pwr_aux_fc) = {
146 match self.aux_cntrl {
147 HEVAuxControls::AuxOnResPriority => {
148 if pwr_aux <= *self.res.state.pwr_disch_max.get_fresh(|| format_dbg!())? {
149 (pwr_aux, si::Power::ZERO)
150 } else {
151 (si::Power::ZERO, pwr_aux)
152 }
153 }
154 HEVAuxControls::AuxOnFcPriority => (si::Power::ZERO, pwr_aux),
155 }
156 };
157
158 match &mut self.pt_cntrl {
159 HEVPowertrainControls::RGWDB(rgwdb) => {
160 rgwdb
161 .state
162 .aux_power_demand
163 .update(pwr_aux_fc > si::Power::ZERO, || format_dbg!())?;
164 }
165 }
166
167 self.fc
169 .set_curr_pwr_prop_max(pwr_aux_fc)
170 .with_context(|| anyhow!(format_dbg!()))?;
171 self.res
172 .set_curr_pwr_prop_max(pwr_aux_res)
173 .with_context(|| anyhow!(format_dbg!()))?;
174 self.em
175 .set_curr_pwr_prop_out_max(
176 *self.res.state.pwr_prop_max.get_fresh(|| format_dbg!())?,
179 *self.res.state.pwr_regen_max.get_fresh(|| format_dbg!())?,
180 dt,
181 )
182 .with_context(|| anyhow!(format_dbg!()))?;
183 Ok(())
185 }
186
187 fn get_curr_pwr_prop_out_max(&self) -> anyhow::Result<(si::Power, si::Power)> {
188 Ok((
189 *self
190 .em
191 .state
192 .pwr_mech_fwd_out_max
193 .get_fresh(|| format_dbg!())?
194 + *self.fc.state.pwr_prop_max.get_fresh(|| format_dbg!())?,
195 *self
196 .em
197 .state
198 .pwr_mech_regen_max
199 .get_fresh(|| format_dbg!())?,
200 ))
201 }
202
203 fn solve(
204 &mut self,
205 pwr_out_req: si::Power,
206 _enabled: bool,
207 dt: si::Time,
208 ) -> anyhow::Result<()> {
209 let pwr_in_transmission = self
214 .transmission
215 .get_pwr_in_req(pwr_out_req)
216 .with_context(|| anyhow!(format_dbg!()))?;
217
218 let (fc_pwr_out_req, em_pwr_out_req) = self
224 .pt_cntrl
225 .get_pwr_fc_and_em(pwr_in_transmission, &self.fc, &self.em.state, &self.res)
226 .with_context(|| format_dbg!())?;
227 let fc_on: bool = self.pt_cntrl.engine_on()?;
228
229 self.fc
230 .solve(fc_pwr_out_req, fc_on, dt)
231 .with_context(|| format_dbg!())?;
232 let res_pwr_out_req = self
233 .em
234 .get_pwr_in_req(em_pwr_out_req, dt)
235 .with_context(|| format_dbg!())?;
236 self.res
238 .solve(res_pwr_out_req, dt)
239 .with_context(|| format_dbg!())?;
240 Ok(())
241 }
242
243 fn pwr_regen(&self) -> anyhow::Result<si::Power> {
245 Ok(-self
249 .em
250 .state
251 .pwr_mech_prop_out
252 .get_fresh(|| format_dbg!())?
253 .max(si::Power::ZERO))
254 }
255}
256
257impl HybridElectricVehicle {
258 pub fn solve_thermal(
268 &mut self,
269 te_amb: si::Temperature,
270 pwr_thrml_fc_to_cab: Option<si::Power>,
271 veh_state: &mut VehicleState,
272 pwr_thrml_hvac_to_res: Option<si::Power>,
273 te_cab: Option<si::Temperature>,
274 dt: si::Time,
275 ) -> anyhow::Result<()> {
276 self.fc
277 .solve_thermal(te_amb, pwr_thrml_fc_to_cab, veh_state, dt)
278 .with_context(|| format_dbg!())?;
279 self.res
280 .solve_thermal(
281 te_amb,
282 pwr_thrml_hvac_to_res.unwrap_or_default(),
283 te_cab,
284 dt,
285 )
286 .with_context(|| format_dbg!())?;
287 Ok(())
288 }
289}
290
291impl Mass for HybridElectricVehicle {
292 fn mass(&self) -> anyhow::Result<Option<si::Mass>> {
293 let derived_mass = self
294 .derived_mass()
295 .with_context(|| anyhow!(format_dbg!()))?;
296 match (derived_mass, self.mass) {
297 (Some(derived_mass), Some(set_mass)) => {
298 ensure!(
299 utils::almost_eq_uom(&set_mass, &derived_mass, None),
300 format!(
301 "{}",
302 format_dbg!(utils::almost_eq_uom(&set_mass, &derived_mass, None)),
303 )
304 );
305 Ok(Some(set_mass))
306 }
307 _ => Ok(self.mass.or(derived_mass)),
308 }
309 }
310
311 fn set_mass(
312 &mut self,
313 new_mass: Option<si::Mass>,
314 side_effect: MassSideEffect,
315 ) -> anyhow::Result<()> {
316 ensure!(
317 side_effect == MassSideEffect::None,
318 "At the powertrain level, only `MassSideEffect::None` is allowed"
319 );
320 let derived_mass = self
321 .derived_mass()
322 .with_context(|| anyhow!(format_dbg!()))?;
323 self.mass = match new_mass {
324 Some(new_mass) => {
326 if let Some(dm) = derived_mass {
327 if dm != new_mass {
328 self.expunge_mass_fields();
329 }
330 }
331 Some(new_mass)
332 }
333 None => Some(derived_mass.with_context(|| {
335 format!(
336 "Not all mass fields in `{}` are set and no mass was provided.",
337 stringify!(HybridElectricVehicle)
338 )
339 })?),
340 };
341 Ok(())
342 }
343
344 fn derived_mass(&self) -> anyhow::Result<Option<si::Mass>> {
345 let fc_mass = self.fc.mass().with_context(|| anyhow!(format_dbg!()))?;
346 let fs_mass = self.fs.mass().with_context(|| anyhow!(format_dbg!()))?;
347 let res_mass = self.res.mass().with_context(|| anyhow!(format_dbg!()))?;
348 let em_mass = self.em.mass().with_context(|| anyhow!(format_dbg!()))?;
349 let transmission_mass = self
350 .transmission
351 .mass()
352 .with_context(|| anyhow!(format_dbg!()))?;
353 match (fc_mass, fs_mass, res_mass, em_mass, transmission_mass) {
354 (
355 Some(fc_mass),
356 Some(fs_mass),
357 Some(res_mass),
358 Some(em_mass),
359 Some(transmission_mass),
360 ) => Ok(Some(
361 fc_mass + fs_mass + res_mass + em_mass + transmission_mass,
362 )),
363 (None, None, None, None, None) => Ok(None),
364 _ => bail!(
365 "`{}` field masses are not consistently set to `Some` or `None`",
366 stringify!(HybridElectricVehicle)
367 ),
368 }
369 }
370
371 fn expunge_mass_fields(&mut self) {
372 self.fc.expunge_mass_fields();
373 self.fs.expunge_mass_fields();
374 self.res.expunge_mass_fields();
375 self.em.expunge_mass_fields();
376 self.transmission.expunge_mass_fields();
377 self.mass = None;
378 }
379}
380
381#[serde_api]
382#[derive(
383 Clone,
384 Debug,
385 Default,
386 Deserialize,
387 Serialize,
388 PartialEq,
389 HistoryVec,
390 StateMethods,
391 SetCumulative,
392)]
393#[non_exhaustive]
394#[serde(deny_unknown_fields)]
395pub struct RGWDBState {
396 pub i: TrackedState<usize>,
398 pub fc_temperature_too_low: TrackedState<bool>,
400 pub vehicle_speed_too_high: TrackedState<bool>,
403 pub on_time_too_short: TrackedState<bool>,
405 pub propulsion_power_demand: TrackedState<bool>,
407 pub propulsion_power_demand_soft: TrackedState<bool>,
409 pub aux_power_demand: TrackedState<bool>,
411 pub charging_for_low_soc: TrackedState<bool>,
413 pub soc_fc_on_buffer: TrackedState<si::Ratio>,
415}
416impl SerdeAPI for RGWDBState {}
417impl Init for RGWDBState {}
418
419impl RGWDBState {
420 fn engine_on(&self) -> anyhow::Result<bool> {
422 Ok(*self.fc_temperature_too_low.get_fresh(|| format_dbg!())?
423 || *self.vehicle_speed_too_high.get_fresh(|| format_dbg!())?
424 || *self.on_time_too_short.get_fresh(|| format_dbg!())?
425 || *self.propulsion_power_demand.get_fresh(|| format_dbg!())?
426 || *self
427 .propulsion_power_demand_soft
428 .get_fresh(|| format_dbg!())?
429 || *self.aux_power_demand.get_fresh(|| format_dbg!())?
430 || *self.charging_for_low_soc.get_fresh(|| format_dbg!())?)
431 }
432}
433
434#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)]
436#[non_exhaustive]
437#[serde(deny_unknown_fields)]
438pub struct HEVSimulationParams {
439 pub res_per_fuel_lim: si::Ratio,
441 pub soc_balance_iter_err: u32,
443 pub balance_soc: bool,
445 pub save_soc_bal_iters: bool,
447}
448
449impl Default for HEVSimulationParams {
450 fn default() -> Self {
451 Self {
452 res_per_fuel_lim: uc::R * 0.005,
453 soc_balance_iter_err: 5,
454 balance_soc: true,
455 save_soc_bal_iters: false,
456 }
457 }
458}
459
460#[derive(
461 Clone, Debug, PartialEq, Deserialize, Serialize, Default, IsVariant, derive_more::From, TryInto,
462)]
463pub enum HEVAuxControls {
464 #[default]
466 AuxOnResPriority,
467 AuxOnFcPriority,
469}
470
471#[derive(
472 Clone, Debug, PartialEq, Deserialize, Serialize, IsVariant, derive_more::From, TryInto,
473)]
474pub enum HEVPowertrainControls {
475 RGWDB(Box<RESGreedyWithDynamicBuffers>),
479}
480
481impl Default for HEVPowertrainControls {
482 fn default() -> Self {
483 Self::RGWDB(Default::default())
484 }
485}
486
487impl SetCumulative for HEVPowertrainControls {
488 fn set_cumulative<F: Fn() -> String>(&mut self, dt: si::Time, loc: F) -> anyhow::Result<()> {
489 match self {
490 Self::RGWDB(rgwdb) => {
491 rgwdb.set_cumulative(dt, || format!("{}\n{}", loc(), format_dbg!()))?
492 }
493 }
494 Ok(())
495 }
496}
497impl Step for HEVPowertrainControls {
498 fn step<F: Fn() -> String>(&mut self, loc: F) -> anyhow::Result<()> {
499 match self {
500 HEVPowertrainControls::RGWDB(rgwdb) => rgwdb.step(loc)?,
501 }
502 Ok(())
503 }
504
505 fn reset_step<F: Fn() -> String>(&mut self, loc: F) -> anyhow::Result<()> {
506 match self {
507 HEVPowertrainControls::RGWDB(rgwdb) => rgwdb.reset_step(loc)?,
508 }
509 Ok(())
510 }
511}
512
513impl StateMethods for HEVPowertrainControls {}
514
515impl SaveState for HEVPowertrainControls {
516 fn save_state<F: Fn() -> String>(&mut self, loc: F) -> anyhow::Result<()> {
517 match self {
518 HEVPowertrainControls::RGWDB(rgwdb) => rgwdb.save_state(loc)?,
519 }
520 Ok(())
521 }
522}
523impl TrackedStateMethods for HEVPowertrainControls {
524 fn check_and_reset<F: Fn() -> String>(&mut self, loc: F) -> anyhow::Result<()> {
525 match self {
526 HEVPowertrainControls::RGWDB(rgwdb) => rgwdb.check_and_reset(loc)?,
527 }
528 Ok(())
529 }
530
531 fn mark_fresh<F: Fn() -> String>(&mut self, loc: F) -> anyhow::Result<()> {
532 match self {
533 HEVPowertrainControls::RGWDB(rgwdb) => rgwdb.mark_fresh(loc)?,
534 }
535 Ok(())
536 }
537}
538impl HistoryMethods for HEVPowertrainControls {
539 fn set_save_interval(&mut self, save_interval: Option<usize>) -> anyhow::Result<()> {
540 match self {
541 HEVPowertrainControls::RGWDB(rgwdb) => Ok(rgwdb.set_save_interval(save_interval)?),
542 }
543 }
544
545 fn save_interval(&self) -> anyhow::Result<Option<usize>> {
546 match self {
547 HEVPowertrainControls::RGWDB(rgwdb) => rgwdb.save_interval(),
548 }
549 }
550 fn clear(&mut self) {
551 match self {
552 HEVPowertrainControls::RGWDB(rgwdb) => rgwdb.clear(),
553 }
554 }
555}
556
557impl Init for HEVPowertrainControls {
558 fn init(&mut self) -> Result<(), Error> {
559 match self {
560 Self::RGWDB(rgwb) => rgwb.init()?,
561 }
562 Ok(())
563 }
564}
565
566impl HEVPowertrainControls {
567 fn get_pwr_fc_and_em(
577 &mut self,
578 pwr_prop_req: si::Power,
579 fc: &FuelConverter,
580 em_state: &ElectricMachineState,
581 res: &ReversibleEnergyStorage,
582 ) -> anyhow::Result<(si::Power, si::Power)> {
583 let fc_state = &fc.state;
584 ensure!(
585 almost_le_uom(
587 &pwr_prop_req,
588 &(*em_state.pwr_mech_fwd_out_max.get_fresh(|| format_dbg!())?
589 + *fc_state.pwr_prop_max.get_fresh(|| format_dbg!())?),
590 None
591 ),
592 "{}
593`pwr_out_req`: {} kW
594`em_state.pwr_mech_fwd_out_max`: {} kW
595`fc_state.pwr_prop_max`: {} kW
596`res.state.soc`: {}",
597 format_dbg!(),
598 pwr_prop_req.get::<si::kilowatt>(),
599 em_state
600 .pwr_mech_fwd_out_max
601 .get_fresh(|| format_dbg!())?
602 .get::<si::kilowatt>(),
603 fc_state
604 .pwr_prop_max
605 .get_fresh(|| format_dbg!())?
606 .get::<si::kilowatt>(),
607 res.state
608 .soc
609 .get_fresh(|| format_dbg!())?
610 .get::<si::ratio>()
611 );
612
613 match self {
621 Self::RGWDB(rgwdb) => rgwdb.get_pwr_fc_and_em(fc, pwr_prop_req, em_state),
622 }
623 }
624
625 pub fn engine_on(&self) -> anyhow::Result<bool> {
626 match self {
627 Self::RGWDB(rgwdb) => rgwdb.state.engine_on(),
628 }
629 }
630}
631
632#[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 pub speed_soc_disch_buffer: Option<si::Velocity>,
645 pub speed_soc_disch_buffer_coeff: Option<si::Ratio>,
647 pub speed_soc_fc_on_buffer: Option<si::Velocity>,
650 pub speed_soc_fc_on_buffer_coeff: Option<si::Ratio>,
652 pub speed_soc_regen_buffer: Option<si::Velocity>,
656 pub speed_soc_regen_buffer_coeff: Option<si::Ratio>,
658 pub fc_min_time_on: Option<si::Time>,
661 pub speed_fc_forced_on: Option<si::Velocity>,
663 pub frac_pwr_demand_fc_forced_on: Option<si::Ratio>,
666 pub frac_of_most_eff_pwr_to_run_fc: Option<si::Ratio>,
671 pub save_interval: Option<usize>,
675 #[serde(default)]
677 pub temp_fc_forced_on: Option<si::Temperature>,
678 #[serde(default)]
680 pub temp_fc_allowed_off: Option<si::Temperature>,
681 #[serde(default)]
683 pub state: RGWDBState,
684 #[serde(default, skip_serializing_if = "RGWDBStateHistoryVec::is_empty")]
685 pub history: RGWDBStateHistoryVec,
687}
688
689#[pyo3_api]
690impl RESGreedyWithDynamicBuffers {}
691
692impl HistoryMethods for RESGreedyWithDynamicBuffers {
693 fn set_save_interval(&mut self, save_interval: Option<usize>) -> anyhow::Result<()> {
694 self.save_interval = save_interval;
695 Ok(())
696 }
697
698 fn save_interval(&self) -> anyhow::Result<Option<usize>> {
699 Ok(self.save_interval)
700 }
701
702 fn clear(&mut self) {
703 self.history.clear();
704 }
705}
706
707impl Init for RESGreedyWithDynamicBuffers {
708 fn init(&mut self) -> Result<(), Error> {
709 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 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 let (fc_pwr, em_pwr) = if !self.state.engine_on()? {
745 (si::Power::ZERO, em_pwr)
747 } else {
748 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 (*em_state.pwr_mech_regen_max.get_fresh(|| format_dbg!())? + pwr_prop_req)
756 .min(fc.pwr_for_peak_eff * frac_of_pwr_for_peak_eff)
758 .max(si::Power::ZERO)
760 } else {
761 if pwr_prop_req - em_pwr > fc.pwr_for_peak_eff * frac_of_pwr_for_peak_eff {
763 pwr_prop_req - em_pwr
765 } else {
766 (pwr_prop_req - em_pwr)
771 .max(fc.pwr_for_peak_eff * frac_of_pwr_for_peak_eff)
775 .min(
778 pwr_prop_req
779 + *em_state.pwr_mech_regen_max.get_fresh(|| format_dbg!())?,
780 )
781 }
782 }
783 .min(*fc.state.pwr_prop_max.get_fresh(|| format_dbg!())?);
785
786 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 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 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 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 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 < temp_fc_forced_on ||
926 (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}