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#[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.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 HEVPowertrainControls::Placeholder => {
136 todo!()
137 }
138 };
139 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 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 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 *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 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 let pwr_in_transmission = self
218 .transmission
219 .get_pwr_in_req(pwr_out_req)
220 .with_context(|| anyhow!(format_dbg!()))?;
221
222 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 self.res
242 .solve(res_pwr_out_req, dt)
243 .with_context(|| format_dbg!())?;
244 Ok(())
245 }
246
247 fn pwr_regen(&self) -> anyhow::Result<si::Power> {
249 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 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 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 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 pub i: TrackedState<usize>,
402 pub fc_temperature_too_low: TrackedState<bool>,
404 pub vehicle_speed_too_high: TrackedState<bool>,
407 pub on_time_too_short: TrackedState<bool>,
409 pub propulsion_power_demand: TrackedState<bool>,
411 pub propulsion_power_demand_soft: TrackedState<bool>,
413 pub aux_power_demand: TrackedState<bool>,
415 pub charging_for_low_soc: TrackedState<bool>,
417 pub soc_fc_on_buffer: TrackedState<si::Ratio>,
419}
420impl SerdeAPI for RGWDBState {}
421impl Init for RGWDBState {}
422
423impl RGWDBState {
424 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#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)]
440#[non_exhaustive]
441#[serde(deny_unknown_fields)]
442pub struct HEVSimulationParams {
443 pub res_per_fuel_lim: si::Ratio,
445 pub soc_balance_iter_err: u32,
447 pub balance_soc: bool,
449 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 #[default]
470 AuxOnResPriority,
471 AuxOnFcPriority,
473}
474
475#[derive(
476 Clone, Debug, PartialEq, Deserialize, Serialize, IsVariant, derive_more::From, TryInto,
477)]
478pub enum HEVPowertrainControls {
479 RGWDB(Box<RESGreedyWithDynamicBuffers>),
483 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 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_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 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#[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#[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 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}