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 pub soc_bal_iter_history: Vec<Self>,
35 #[serde(default)]
38 pub soc_bal_iters: TrackedState<u32>,
39}
40
41#[pyo3_api]
42impl HybridElectricVehicle {}
43
44impl HistoryMethods for HybridElectricVehicle {
45 fn save_interval(&self) -> anyhow::Result<Option<usize>> {
46 bail!("`save_interval` is not implemented in HybridElectricVehicle")
47 }
48 fn set_save_interval(&mut self, save_interval: Option<usize>) -> anyhow::Result<()> {
49 self.res.set_save_interval(save_interval)?;
50 self.fc.set_save_interval(save_interval)?;
52 self.em.set_save_interval(save_interval)?;
53 self.transmission.set_save_interval(save_interval)?;
54 self.pt_cntrl.set_save_interval(save_interval)?;
55 Ok(())
56 }
57 fn clear(&mut self) {
58 self.res.clear();
59 self.fc.clear();
61 self.em.clear();
62 self.transmission.clear();
63 self.pt_cntrl.clear();
64 }
65}
66
67impl Init for HybridElectricVehicle {
68 fn init(&mut self) -> Result<(), Error> {
69 self.fc
70 .init()
71 .map_err(|err| Error::InitError(format_dbg!(err)))?;
72 self.res
73 .init()
74 .map_err(|err| Error::InitError(format_dbg!(err)))?;
75 self.em
76 .init()
77 .map_err(|err| Error::InitError(format_dbg!(err)))?;
78 self.transmission
79 .init()
80 .map_err(|err| Error::InitError(format_dbg!(err)))?;
81 self.pt_cntrl
82 .init()
83 .map_err(|err| Error::InitError(format_dbg!(err)))?;
84 Ok(())
85 }
86}
87
88impl SerdeAPI for HybridElectricVehicle {}
89
90impl Powertrain for Box<HybridElectricVehicle> {
91 fn set_curr_pwr_prop_out_max(
92 &mut self,
93 _pwr_upstream: (si::Power, si::Power),
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(P2::new())
109 - veh_state
110 .speed_ach
111 .get_stale(|| format_dbg!())?
112 .powi(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(P2::new())
124 - rgwdb
125 .speed_soc_regen_buffer
126 .with_context(|| format_dbg!())?
127 .powi(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
179 .get_curr_pwr_prop_out_max()
180 .with_context(|| format_dbg!())?,
181 pwr_aux,
182 dt,
183 veh_state,
184 )
185 .with_context(|| anyhow!(format_dbg!()))?;
186 let em_pwr_prop_out_maxes = self
187 .em
188 .get_curr_pwr_prop_out_max()
189 .with_context(|| format_dbg!())?;
190 let fc_max = self.fc.state.pwr_prop_max.get_fresh(|| format_dbg!())?;
191 self.transmission
192 .set_curr_pwr_prop_out_max(
193 (em_pwr_prop_out_maxes.0 + *fc_max, em_pwr_prop_out_maxes.1),
194 f64::NAN * uc::W,
195 dt,
196 veh_state,
197 )
198 .with_context(|| format_dbg!())?;
199 Ok(())
200 }
201
202 fn get_curr_pwr_prop_out_max(&self) -> anyhow::Result<(si::Power, si::Power)> {
203 self.transmission
204 .get_curr_pwr_prop_out_max()
205 .with_context(|| format_dbg!())
206 }
207
208 fn solve(
209 &mut self,
210 pwr_out_req: si::Power,
211 _enabled: bool,
212 dt: si::Time,
213 ) -> anyhow::Result<Option<si::Power>> {
214 let pwr_in_transmission = self
219 .transmission
220 .solve(pwr_out_req, true, dt)
221 .with_context(|| format_dbg!())?
222 .with_context(|| format!("{}\nExpected `Some`", format_dbg!()))?;
223
224 let (fc_pwr_out_req, em_pwr_out_req) = self
230 .pt_cntrl
231 .get_pwr_fc_and_em(pwr_in_transmission, &self.fc, &self.em.state, &self.res)
232 .with_context(|| format_dbg!())?;
233 let fc_on: bool = self.pt_cntrl.engine_on()?;
234
235 self.fc
236 .solve(fc_pwr_out_req, fc_on, dt)
237 .with_context(|| format_dbg!())?;
238 let res_pwr_out_req = self
239 .em
240 .solve(em_pwr_out_req, true, dt)
241 .with_context(|| format_dbg!())?
242 .with_context(|| format!("{}\nExpected `Some`", format_dbg!()))?;
243 self.res
245 .solve(res_pwr_out_req, dt)
246 .with_context(|| format_dbg!())?;
247 Ok(None)
248 }
249
250 fn pwr_regen(&self) -> anyhow::Result<si::Power> {
252 self.transmission.pwr_regen().with_context(|| format_dbg!())
256 }
257}
258
259impl HybridElectricVehicle {
260 pub fn solve_thermal(
270 &mut self,
271 te_amb: si::Temperature,
272 pwr_thrml_fc_to_cab: Option<si::Power>,
273 veh_state: &mut VehicleState,
274 pwr_thrml_hvac_to_res: Option<si::Power>,
275 te_cab: Option<si::Temperature>,
276 dt: si::Time,
277 ) -> anyhow::Result<()> {
278 self.fc
279 .solve_thermal(te_amb, pwr_thrml_fc_to_cab, veh_state, dt)
280 .with_context(|| format_dbg!())?;
281 self.res
282 .solve_thermal(
283 te_amb,
284 pwr_thrml_hvac_to_res.unwrap_or_default(),
285 te_cab,
286 dt,
287 )
288 .with_context(|| format_dbg!())?;
289 Ok(())
290 }
291}
292
293impl TryFrom<&fastsim_2::vehicle::RustVehicle> for HybridElectricVehicle {
294 type Error = anyhow::Error;
295 fn try_from(f2veh: &fastsim_2::vehicle::RustVehicle) -> anyhow::Result<HybridElectricVehicle> {
296 let pt_cntrl = HEVPowertrainControls::RGWDB(Box::new(hev::RESGreedyWithDynamicBuffers {
297 speed_soc_fc_on_buffer: None,
298 speed_soc_fc_on_buffer_coeff: None,
299 speed_soc_disch_buffer: None,
300 speed_soc_disch_buffer_coeff: None,
301 speed_soc_regen_buffer: None,
302 speed_soc_regen_buffer_coeff: None,
303 fc_min_time_on: None,
305 speed_fc_forced_on: Some(f2veh.mph_fc_on * uc::MPH),
306 frac_pwr_demand_fc_forced_on: Some(
307 f2veh.kw_demand_fc_on / (f2veh.fc_max_kw + f2veh.ess_max_kw.min(f2veh.mc_max_kw))
308 * uc::R,
309 ),
310 frac_of_most_eff_pwr_to_run_fc: None,
311 temp_fc_forced_on: None,
312 temp_fc_allowed_off: None,
313 save_interval: Some(1),
314 state: Default::default(),
315 history: Default::default(),
316 }));
317 let mut hev = HybridElectricVehicle {
318 fs: {
319 let mut fs = FuelStorage {
320 pwr_out_max: f2veh.fs_max_kw * uc::KW,
321 pwr_ramp_lag: f2veh.fs_secs_to_peak_pwr * uc::S,
322 energy_capacity: f2veh.fs_kwh * 3.6 * uc::MJ,
323 specific_energy: None,
324 mass: None,
325 };
326 fs.set_mass(None, MassSideEffect::None)
327 .with_context(|| anyhow!(format_dbg!()))?;
328 fs
329 },
330 fc: FuelConverter::try_from(f2veh.clone())?,
331 res: ReversibleEnergyStorage::try_from(f2veh.clone()).with_context(|| format_dbg!())?,
332 em: ElectricMachine::try_from(f2veh.clone())?,
333 transmission: Transmission::try_from(f2veh.clone())?,
334 pt_cntrl,
335 mass: None,
336 sim_params: Default::default(),
337 aux_cntrl: Default::default(),
338 soc_bal_iter_history: Default::default(),
339 soc_bal_iters: Default::default(),
340 };
341 hev.init()?;
342 Ok(hev)
343 }
344}
345impl Mass for HybridElectricVehicle {
346 fn mass(&self) -> anyhow::Result<Option<si::Mass>> {
347 let derived_mass = self
348 .derived_mass()
349 .with_context(|| anyhow!(format_dbg!()))?;
350 match (derived_mass, self.mass) {
351 (Some(derived_mass), Some(set_mass)) => {
352 ensure!(
353 utils::almost_eq_uom(&set_mass, &derived_mass, None),
354 format!(
355 "{}",
356 format_dbg!(utils::almost_eq_uom(&set_mass, &derived_mass, None)),
357 )
358 );
359 Ok(Some(set_mass))
360 }
361 _ => Ok(self.mass.or(derived_mass)),
362 }
363 }
364
365 fn set_mass(
366 &mut self,
367 new_mass: Option<si::Mass>,
368 side_effect: MassSideEffect,
369 ) -> anyhow::Result<()> {
370 ensure!(
371 side_effect == MassSideEffect::None,
372 "At the powertrain level, only `MassSideEffect::None` is allowed"
373 );
374 let derived_mass = self
375 .derived_mass()
376 .with_context(|| anyhow!(format_dbg!()))?;
377 self.mass = match new_mass {
378 Some(new_mass) => {
380 if let Some(dm) = derived_mass {
381 if dm != new_mass {
382 self.expunge_mass_fields();
383 }
384 }
385 Some(new_mass)
386 }
387 None => Some(derived_mass.with_context(|| {
389 format!(
390 "Not all mass fields in `{}` are set and no mass was provided.",
391 stringify!(HybridElectricVehicle)
392 )
393 })?),
394 };
395 Ok(())
396 }
397
398 fn derived_mass(&self) -> anyhow::Result<Option<si::Mass>> {
399 let fc_mass = self.fc.mass().with_context(|| anyhow!(format_dbg!()))?;
400 let fs_mass = self.fs.mass().with_context(|| anyhow!(format_dbg!()))?;
401 let res_mass = self.res.mass().with_context(|| anyhow!(format_dbg!()))?;
402 let em_mass = self.em.mass().with_context(|| anyhow!(format_dbg!()))?;
403 let transmission_mass = self
404 .transmission
405 .mass()
406 .with_context(|| anyhow!(format_dbg!()))?;
407 match (fc_mass, fs_mass, res_mass, em_mass, transmission_mass) {
408 (
409 Some(fc_mass),
410 Some(fs_mass),
411 Some(res_mass),
412 Some(em_mass),
413 Some(transmission_mass),
414 ) => Ok(Some(
415 fc_mass + fs_mass + res_mass + em_mass + transmission_mass,
416 )),
417 (None, None, None, None, None) => Ok(None),
418 _ => bail!(
419 "`{}` field masses are not consistently set to `Some` or `None`",
420 stringify!(HybridElectricVehicle)
421 ),
422 }
423 }
424
425 fn expunge_mass_fields(&mut self) {
426 self.fc.expunge_mass_fields();
427 self.fs.expunge_mass_fields();
428 self.res.expunge_mass_fields();
429 self.em.expunge_mass_fields();
430 self.transmission.expunge_mass_fields();
431 self.mass = None;
432 }
433}
434
435#[serde_api]
436#[derive(
437 Clone,
438 Debug,
439 Default,
440 Deserialize,
441 Serialize,
442 PartialEq,
443 HistoryVec,
444 StateMethods,
445 SetCumulative,
446)]
447#[non_exhaustive]
448#[serde(deny_unknown_fields)]
449pub struct RGWDBState {
450 pub i: TrackedState<usize>,
452 pub fc_temperature_too_low: TrackedState<bool>,
454 pub vehicle_speed_too_high: TrackedState<bool>,
457 pub on_time_too_short: TrackedState<bool>,
459 pub propulsion_power_demand: TrackedState<bool>,
461 pub propulsion_power_demand_soft: TrackedState<bool>,
463 pub aux_power_demand: TrackedState<bool>,
465 pub charging_for_low_soc: TrackedState<bool>,
467 pub soc_fc_on_buffer: TrackedState<si::Ratio>,
469}
470impl SerdeAPI for RGWDBState {}
471impl Init for RGWDBState {}
472
473impl RGWDBState {
474 fn engine_on(&self) -> anyhow::Result<bool> {
476 Ok(*self.fc_temperature_too_low.get_fresh(|| format_dbg!())?
477 || *self.vehicle_speed_too_high.get_fresh(|| format_dbg!())?
478 || *self.on_time_too_short.get_fresh(|| format_dbg!())?
479 || *self.propulsion_power_demand.get_fresh(|| format_dbg!())?
480 || *self
481 .propulsion_power_demand_soft
482 .get_fresh(|| format_dbg!())?
483 || *self.aux_power_demand.get_fresh(|| format_dbg!())?
484 || *self.charging_for_low_soc.get_fresh(|| format_dbg!())?)
485 }
486}
487
488#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)]
490#[non_exhaustive]
491#[serde(deny_unknown_fields)]
492pub struct HEVSimulationParams {
493 pub res_per_fuel_lim: si::Ratio,
495 pub soc_balance_iter_err: u32,
497 pub balance_soc: bool,
499 pub save_soc_bal_iters: bool,
501}
502
503impl Default for HEVSimulationParams {
504 fn default() -> Self {
505 Self {
506 res_per_fuel_lim: uc::R * 0.005,
507 soc_balance_iter_err: 5,
508 balance_soc: true,
509 save_soc_bal_iters: false,
510 }
511 }
512}
513
514#[derive(
515 Clone, Debug, PartialEq, Deserialize, Serialize, Default, IsVariant, derive_more::From, TryInto,
516)]
517pub enum HEVAuxControls {
518 #[default]
520 AuxOnResPriority,
521 AuxOnFcPriority,
523}
524
525#[derive(
526 Clone, Debug, PartialEq, Deserialize, Serialize, IsVariant, derive_more::From, TryInto,
527)]
528pub enum HEVPowertrainControls {
529 RGWDB(Box<RESGreedyWithDynamicBuffers>),
533}
534
535impl Default for HEVPowertrainControls {
536 fn default() -> Self {
537 Self::RGWDB(Default::default())
538 }
539}
540
541impl SetCumulative for HEVPowertrainControls {
542 fn set_cumulative<F: Fn() -> String>(&mut self, dt: si::Time, loc: F) -> anyhow::Result<()> {
543 match self {
544 Self::RGWDB(rgwdb) => {
545 rgwdb.set_cumulative(dt, || format!("{}\n{}", loc(), format_dbg!()))?
546 }
547 }
548 Ok(())
549 }
550
551 fn reset_cumulative<F: Fn() -> String>(&mut self, loc: F) -> anyhow::Result<()> {
552 match self {
553 Self::RGWDB(rgwdb) => {
554 rgwdb.reset_cumulative(|| format!("{}\n{}", loc(), format_dbg!()))?
555 }
556 }
557 Ok(())
558 }
559}
560impl Step for HEVPowertrainControls {
561 fn step<F: Fn() -> String>(&mut self, loc: F) -> anyhow::Result<()> {
562 match self {
563 HEVPowertrainControls::RGWDB(rgwdb) => rgwdb.step(loc)?,
564 }
565 Ok(())
566 }
567
568 fn reset_step<F: Fn() -> String>(&mut self, loc: F) -> anyhow::Result<()> {
569 match self {
570 HEVPowertrainControls::RGWDB(rgwdb) => rgwdb.reset_step(loc)?,
571 }
572 Ok(())
573 }
574}
575
576impl StateMethods for HEVPowertrainControls {}
577
578impl SaveState for HEVPowertrainControls {
579 fn save_state<F: Fn() -> String>(&mut self, loc: F) -> anyhow::Result<()> {
580 match self {
581 HEVPowertrainControls::RGWDB(rgwdb) => rgwdb.save_state(loc)?,
582 }
583 Ok(())
584 }
585}
586impl TrackedStateMethods for HEVPowertrainControls {
587 fn check_and_reset<F: Fn() -> String>(&mut self, loc: F) -> anyhow::Result<()> {
588 match self {
589 HEVPowertrainControls::RGWDB(rgwdb) => rgwdb.check_and_reset(loc)?,
590 }
591 Ok(())
592 }
593
594 fn mark_fresh<F: Fn() -> String>(&mut self, loc: F) -> anyhow::Result<()> {
595 match self {
596 HEVPowertrainControls::RGWDB(rgwdb) => rgwdb.mark_fresh(loc)?,
597 }
598 Ok(())
599 }
600}
601impl HistoryMethods for HEVPowertrainControls {
602 fn set_save_interval(&mut self, save_interval: Option<usize>) -> anyhow::Result<()> {
603 match self {
604 HEVPowertrainControls::RGWDB(rgwdb) => Ok(rgwdb.set_save_interval(save_interval)?),
605 }
606 }
607
608 fn save_interval(&self) -> anyhow::Result<Option<usize>> {
609 match self {
610 HEVPowertrainControls::RGWDB(rgwdb) => rgwdb.save_interval(),
611 }
612 }
613 fn clear(&mut self) {
614 match self {
615 HEVPowertrainControls::RGWDB(rgwdb) => rgwdb.clear(),
616 }
617 }
618}
619
620impl Init for HEVPowertrainControls {
621 fn init(&mut self) -> Result<(), Error> {
622 match self {
623 Self::RGWDB(rgwb) => rgwb.init()?,
624 }
625 Ok(())
626 }
627}
628
629impl HEVPowertrainControls {
630 fn get_pwr_fc_and_em(
640 &mut self,
641 pwr_prop_req: si::Power,
642 fc: &FuelConverter,
643 em_state: &ElectricMachineState,
644 res: &ReversibleEnergyStorage,
645 ) -> anyhow::Result<(si::Power, si::Power)> {
646 let fc_state = &fc.state;
647 ensure!(
648 almost_le_uom(
650 &pwr_prop_req,
651 &(*em_state.pwr_mech_fwd_out_max.get_fresh(|| format_dbg!())?
652 + *fc_state.pwr_prop_max.get_fresh(|| format_dbg!())?),
653 None
654 ),
655 "{}
656`pwr_out_req`: {} kW
657`em_state.pwr_mech_fwd_out_max`: {} kW
658`fc_state.pwr_prop_max`: {} kW
659`res.state.soc`: {}",
660 format_dbg!(),
661 pwr_prop_req.get::<si::kilowatt>(),
662 em_state
663 .pwr_mech_fwd_out_max
664 .get_fresh(|| format_dbg!())?
665 .get::<si::kilowatt>(),
666 fc_state
667 .pwr_prop_max
668 .get_fresh(|| format_dbg!())?
669 .get::<si::kilowatt>(),
670 res.state
671 .soc
672 .get_fresh(|| format_dbg!())?
673 .get::<si::ratio>()
674 );
675
676 match self {
684 Self::RGWDB(rgwdb) => rgwdb.get_pwr_fc_and_em(fc, pwr_prop_req, em_state),
685 }
686 }
687
688 pub fn engine_on(&self) -> anyhow::Result<bool> {
689 match self {
690 Self::RGWDB(rgwdb) => rgwdb.state.engine_on(),
691 }
692 }
693}
694
695#[serde_api]
700#[derive(Clone, Debug, PartialEq, Deserialize, Serialize, Default, StateMethods, SetCumulative)]
701#[cfg_attr(feature = "pyo3", pyclass(module = "fastsim", subclass, eq))]
702#[non_exhaustive]
703#[serde(deny_unknown_fields)]
704pub struct RESGreedyWithDynamicBuffers {
705 pub speed_soc_disch_buffer: Option<si::Velocity>,
708 pub speed_soc_disch_buffer_coeff: Option<si::Ratio>,
710 pub speed_soc_fc_on_buffer: Option<si::Velocity>,
713 pub speed_soc_fc_on_buffer_coeff: Option<si::Ratio>,
715 pub speed_soc_regen_buffer: Option<si::Velocity>,
719 pub speed_soc_regen_buffer_coeff: Option<si::Ratio>,
721 pub fc_min_time_on: Option<si::Time>,
724 pub speed_fc_forced_on: Option<si::Velocity>,
726 pub frac_pwr_demand_fc_forced_on: Option<si::Ratio>,
729 pub frac_of_most_eff_pwr_to_run_fc: Option<si::Ratio>,
734 pub save_interval: Option<usize>,
738 #[serde(default)]
740 pub temp_fc_forced_on: Option<si::Temperature>,
741 #[serde(default)]
743 pub temp_fc_allowed_off: Option<si::Temperature>,
744 #[serde(default)]
746 pub state: RGWDBState,
747 #[serde(default)]
748 pub history: RGWDBStateHistoryVec,
750}
751
752#[pyo3_api]
753impl RESGreedyWithDynamicBuffers {}
754
755impl HistoryMethods for RESGreedyWithDynamicBuffers {
756 fn set_save_interval(&mut self, save_interval: Option<usize>) -> anyhow::Result<()> {
757 self.save_interval = save_interval;
758 Ok(())
759 }
760
761 fn save_interval(&self) -> anyhow::Result<Option<usize>> {
762 Ok(self.save_interval)
763 }
764
765 fn clear(&mut self) {
766 self.history.clear();
767 }
768}
769
770impl Init for RESGreedyWithDynamicBuffers {
771 fn init(&mut self) -> Result<(), Error> {
772 init_opt_default!(self, speed_soc_disch_buffer, 70.0 * uc::MPH);
774 init_opt_default!(self, speed_soc_disch_buffer_coeff, 1.0 * uc::R);
775 init_opt_default!(
776 self,
777 speed_soc_fc_on_buffer,
778 self.speed_soc_disch_buffer.unwrap() * 1.5
779 );
780 init_opt_default!(self, speed_soc_fc_on_buffer_coeff, 1.0 * uc::R);
781 init_opt_default!(self, speed_soc_regen_buffer, 30. * uc::MPH);
782 init_opt_default!(self, speed_soc_regen_buffer_coeff, 1.0 * uc::R);
783 init_opt_default!(self, fc_min_time_on, uc::S * 5.0);
784 init_opt_default!(self, speed_fc_forced_on, uc::MPH * 75.);
785 init_opt_default!(self, frac_pwr_demand_fc_forced_on, uc::R * 0.75);
786 init_opt_default!(self, frac_of_most_eff_pwr_to_run_fc, 1.0 * uc::R);
787 Ok(())
788 }
789}
790impl SerdeAPI for RESGreedyWithDynamicBuffers {}
791
792impl RESGreedyWithDynamicBuffers {
793 fn get_pwr_fc_and_em(
794 &mut self,
795 fc: &FuelConverter,
796 pwr_prop_req: si::Power,
797 em_state: &ElectricMachineState,
798 ) -> anyhow::Result<(si::Power, si::Power)> {
799 let em_pwr = pwr_prop_req
804 .min(*em_state.pwr_mech_fwd_out_max.get_fresh(|| format_dbg!())?)
805 .max(-*em_state.pwr_mech_regen_max.get_fresh(|| format_dbg!())?);
806 let (fc_pwr, em_pwr) = if !self.state.engine_on()? {
808 (si::Power::ZERO, em_pwr)
810 } else {
811 let frac_of_pwr_for_peak_eff: si::Ratio = self
813 .frac_of_most_eff_pwr_to_run_fc
814 .with_context(|| format_dbg!())?;
815 let fc_pwr = if pwr_prop_req < si::Power::ZERO {
816 (*em_state.pwr_mech_regen_max.get_fresh(|| format_dbg!())? + pwr_prop_req)
819 .min(fc.pwr_for_peak_eff * frac_of_pwr_for_peak_eff)
821 .max(si::Power::ZERO)
823 } else {
824 if pwr_prop_req - em_pwr > fc.pwr_for_peak_eff * frac_of_pwr_for_peak_eff {
826 pwr_prop_req - em_pwr
828 } else {
829 (pwr_prop_req - em_pwr)
834 .max(fc.pwr_for_peak_eff * frac_of_pwr_for_peak_eff)
838 .min(
841 pwr_prop_req
842 + *em_state.pwr_mech_regen_max.get_fresh(|| format_dbg!())?,
843 )
844 }
845 }
846 .min(*fc.state.pwr_prop_max.get_fresh(|| format_dbg!())?);
848
849 let em_pwr_corrected = (pwr_prop_req - fc_pwr)
851 .max(-*em_state.pwr_mech_regen_max.get_fresh(|| format_dbg!())?);
852 (fc_pwr, em_pwr_corrected)
853 };
854 Ok((fc_pwr, em_pwr))
855 }
856
857 fn handle_fc_on_causes(
858 &mut self,
859 fc: &FuelConverter,
860 veh_state: &VehicleState,
861 res: &ReversibleEnergyStorage,
862 em_state: &ElectricMachineState,
863 ) -> Result<(), anyhow::Error> {
864 self.handle_fc_on_causes_for_temp(fc)?;
865 self.handle_fc_on_causes_for_speed(veh_state)?;
866 self.handle_fc_on_causes_for_low_soc(res, veh_state)?;
867 self.handle_fc_on_causes_for_pwr_demand(
868 *veh_state
869 .pwr_tractive
870 .get_stale(|| format_dbg!(veh_state.pwr_tractive))?,
871 em_state,
872 &fc.state,
873 )
874 .with_context(|| format_dbg!())?;
875 self.handle_fc_on_causes_for_on_time(fc)?;
876 Ok(())
877 }
878
879 fn handle_fc_on_causes_for_on_time(&mut self, fc: &FuelConverter) -> Result<(), anyhow::Error> {
880 self.state.on_time_too_short.update(*fc.state.fc_on.get_stale(|| format_dbg!())? && *fc.state.time_on.get_stale(|| format_dbg!())?
881 < self.fc_min_time_on.with_context(|| {
882 anyhow!(
883 "{}\n Expected `ResGreedyWithBuffers::init` to have been called beforehand.",
884 format_dbg!()
885 )
886 })?, || format_dbg!())?;
887 Ok(())
888 }
889
890 fn handle_fc_on_causes_for_pwr_demand(
893 &mut self,
894 pwr_out_req_for_cyc: si::Power,
895 em_state: &ElectricMachineState,
896 fc_state: &FuelConverterState,
897 ) -> Result<(), anyhow::Error> {
898 let frac_pwr_demand_fc_forced_on: si::Ratio = self
899 .frac_pwr_demand_fc_forced_on
900 .with_context(|| format_dbg!())?;
901 self.state.propulsion_power_demand_soft.update(
902 pwr_out_req_for_cyc
903 > frac_pwr_demand_fc_forced_on
904 * (*em_state.pwr_mech_fwd_out_max.get_stale(|| format_dbg!())?
905 + *fc_state.pwr_out_max.get_stale(|| format_dbg!())?),
906 || format_dbg!(),
907 )?;
908 self.state.propulsion_power_demand.update(
909 pwr_out_req_for_cyc - *em_state.pwr_mech_fwd_out_max.get_stale(|| format_dbg!())?
910 >= si::Power::ZERO,
911 || format_dbg!(),
912 )?;
913 Ok(())
914 }
915
916 fn handle_fc_on_causes_for_low_soc(
918 &mut self,
919 res: &ReversibleEnergyStorage,
920 veh_state: &VehicleState,
921 ) -> anyhow::Result<()> {
922 self.state.soc_fc_on_buffer.update(
923 {
924 let energy_delta_to_buffer_speed: si::Energy = 0.5
925 * *veh_state.mass.get_fresh(|| format_dbg!())?
926 * (self
927 .speed_soc_fc_on_buffer
928 .with_context(|| format_dbg!())?
929 .powi(P2::new())
930 - veh_state
931 .speed_ach
932 .get_stale(|| format_dbg!())?
933 .powi(P2::new()));
934 energy_delta_to_buffer_speed.max(si::Energy::ZERO)
935 * self
936 .speed_soc_fc_on_buffer_coeff
937 .with_context(|| format_dbg!())?
938 } / res.energy_capacity_usable()
939 + res.min_soc,
940 || format_dbg!(),
941 )?;
942 self.state.charging_for_low_soc.update(
943 *res.state.soc.get_stale(|| format_dbg!())?
944 < *self.state.soc_fc_on_buffer.get_fresh(|| format_dbg!())?,
945 || format_dbg!(),
946 )?;
947 Ok(())
948 }
949
950 fn handle_fc_on_causes_for_speed(&mut self, veh_state: &VehicleState) -> anyhow::Result<()> {
952 self.state.vehicle_speed_too_high.update(
953 *veh_state.speed_ach.get_stale(|| format_dbg!())?
954 > self.speed_fc_forced_on.with_context(|| format_dbg!())?,
955 || format_dbg!(),
956 )?;
957 Ok(())
958 }
959
960 fn handle_fc_on_causes_for_temp(&mut self, fc: &FuelConverter) -> anyhow::Result<()> {
963 match (
964 match fc.temperature() {
965 Some(fct) => Some(*fct.get_fresh(|| format_dbg!())?),
966 None => None,
967 },
968 match fc.temperature() {
969 Some(fct) => Some(*fct.get_fresh(|| format_dbg!())?),
970 None => None,
971 },
972 self.temp_fc_forced_on,
973 self.temp_fc_allowed_off,
974 ) {
975 (None, None, None, None) => {
976 self.state
977 .fc_temperature_too_low
978 .update(false, || format_dbg!())?;
979 }
980 (
981 Some(temperature),
982 Some(temp_prev),
983 Some(temp_fc_forced_on),
984 Some(temp_fc_allowed_off),
985 ) => {
986 self.state.fc_temperature_too_low.update(
987 temperature < temp_fc_forced_on ||
989 (temp_prev < temp_fc_forced_on && temperature < temp_fc_allowed_off),
991 || format_dbg!(),
992 )?;
993 }
994 _ => {
995 bail!(
996 "{}\n`fc.temperature()`, `fc.temp_prev()`, `self.temp_fc_forced_on`, and
997`self.temp_fc_allowed_off` must all be `None` or `Some` because these controls are necessary
998for an HEV equipped with thermal models or superfluous otherwise",
999 format_dbg!((
1000 fc.temperature(),
1001 self.temp_fc_forced_on,
1002 self.temp_fc_allowed_off
1003 ))
1004 );
1005 }
1006 }
1007 Ok(())
1008 }
1009}