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
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 };
90 if fc_on_soc > self.res.max_soc {
91 eprintln!("fc_on_soc > self.res.max_soc");
92 eprintln!("fc_on_soc: {:?}", fc_on_soc);
93 }
94 if fc_on_soc < self.res.min_soc {
95 eprintln!("fc_on_soc < self.res.min_soc");
96 eprintln!("fc_on_soc: {:?}", fc_on_soc);
97 }
98 if disch_buffer > self.res.energy_capacity_usable() {
99 eprintln!("disch_buffer < self.res.energy_capacity_usable()");
100 eprintln!(
101 "disch_buffer: {:?} kWh",
102 disch_buffer.get::<si::kilowatt_hour>()
103 );
104 eprintln!(
105 "RES usable energy capacity: {:?} kWh",
106 self.res.energy_capacity_usable().get::<si::kilowatt_hour>()
107 );
108 }
109 if chrg_buffer > self.res.energy_capacity_usable() {
110 eprintln!("disch_buffer < self.res.energy_capacity_usable()");
111 eprintln!(
112 "chrg_buffer: {:?} kWh",
113 chrg_buffer.get::<si::kilowatt_hour>()
114 );
115 eprintln!(
116 "RES usable energy capacity: {:?} kWh",
117 self.res.energy_capacity_usable().get::<si::kilowatt_hour>()
118 );
119 }
120 Ok(())
121 }
122}
123
124#[pyo3_api]
125impl HybridElectricVehicle {}
126
127impl HistoryMethods for HybridElectricVehicle {
128 fn save_interval(&self) -> anyhow::Result<Option<usize>> {
129 bail!("`save_interval` is not implemented in HybridElectricVehicle")
130 }
131 fn set_save_interval(&mut self, save_interval: Option<usize>) -> anyhow::Result<()> {
132 self.res.set_save_interval(save_interval)?;
133 self.fc.set_save_interval(save_interval)?;
135 self.em.set_save_interval(save_interval)?;
136 self.transmission.set_save_interval(save_interval)?;
137 self.pt_cntrl.set_save_interval(save_interval)?;
138 Ok(())
139 }
140 fn clear(&mut self) {
141 self.res.clear();
142 self.fc.clear();
144 self.em.clear();
145 self.transmission.clear();
146 self.pt_cntrl.clear();
147 }
148}
149
150impl Init for HybridElectricVehicle {
151 fn init(&mut self) -> Result<(), Error> {
152 self.fc
153 .init()
154 .map_err(|err| Error::InitError(format_dbg!(err)))?;
155 self.res
156 .init()
157 .map_err(|err| Error::InitError(format_dbg!(err)))?;
158 self.em
159 .init()
160 .map_err(|err| Error::InitError(format_dbg!(err)))?;
161 self.transmission
162 .init()
163 .map_err(|err| Error::InitError(format_dbg!(err)))?;
164 self.pt_cntrl
165 .init()
166 .map_err(|err| Error::InitError(format_dbg!(err)))?;
167 Ok(())
168 }
169}
170
171impl SerdeAPI for HybridElectricVehicle {}
172
173impl Powertrain for Box<HybridElectricVehicle> {
174 fn set_curr_pwr_prop_out_max(
175 &mut self,
176 _pwr_upstream: (si::Power, si::Power),
177 pwr_aux: si::Power,
178 dt: si::Time,
179 veh_state: &VehicleState,
180 ) -> anyhow::Result<()> {
181 let (disch_buffer, chrg_buffer) = match &mut self.pt_cntrl {
183 HEVPowertrainControls::RGWDB(rgwdb) => {
184 rgwdb.handle_fc_on_causes(&self.fc, veh_state, &self.res, &self.em.state)?;
185
186 let disch_buffer = (0.5
187 * *veh_state.mass.get_fresh(|| format_dbg!())?
188 * (rgwdb
189 .speed_soc_disch_buffer
190 .with_context(|| format_dbg!())?
191 .powi(P2::new())
192 - veh_state
193 .speed_ach
194 .get_stale(|| format_dbg!())?
195 .powi(P2::new())))
196 .max(si::Energy::ZERO)
197 * rgwdb
198 .speed_soc_disch_buffer_coeff
199 .with_context(|| format_dbg!())?;
200
201 let chrg_buffer = (0.5
202 * *veh_state.mass.get_fresh(|| format_dbg!())?
203 * (veh_state
204 .speed_ach
205 .get_stale(|| format_dbg!())?
206 .powi(P2::new())
207 - rgwdb
208 .speed_soc_regen_buffer
209 .with_context(|| format_dbg!())?
210 .powi(P2::new())))
211 .max(si::Energy::ZERO)
212 * rgwdb
213 .speed_soc_regen_buffer_coeff
214 .with_context(|| format_dbg!())?;
215
216 (disch_buffer, chrg_buffer)
217 }
218 };
219 self.fc
221 .set_curr_pwr_out_max(dt)
222 .with_context(|| anyhow!(format_dbg!()))?;
223 self.res
224 .set_curr_pwr_out_max(dt, disch_buffer, chrg_buffer)
225 .with_context(|| anyhow!(format_dbg!()))?;
226
227 let (pwr_aux_res, pwr_aux_fc) = {
229 match self.aux_cntrl {
230 HEVAuxControls::AuxOnResPriority => {
231 if pwr_aux <= *self.res.state.pwr_disch_max.get_fresh(|| format_dbg!())? {
232 (pwr_aux, si::Power::ZERO)
233 } else {
234 (si::Power::ZERO, pwr_aux)
235 }
236 }
237 HEVAuxControls::AuxOnFcPriority => (si::Power::ZERO, pwr_aux),
238 }
239 };
240
241 match &mut self.pt_cntrl {
242 HEVPowertrainControls::RGWDB(rgwdb) => {
243 rgwdb
244 .state
245 .aux_power_demand
246 .update(pwr_aux_fc > si::Power::ZERO, || format_dbg!())?;
247 }
248 }
249
250 self.fc
252 .set_curr_pwr_prop_max(pwr_aux_fc)
253 .with_context(|| anyhow!(format_dbg!()))?;
254 self.res
255 .set_curr_pwr_prop_max(pwr_aux_res)
256 .with_context(|| anyhow!(format_dbg!()))?;
257 self.em
258 .set_curr_pwr_prop_out_max(
259 self.res
262 .get_curr_pwr_prop_out_max()
263 .with_context(|| format_dbg!())?,
264 pwr_aux,
265 dt,
266 veh_state,
267 )
268 .with_context(|| anyhow!(format_dbg!()))?;
269 let em_pwr_prop_out_maxes = self
270 .em
271 .get_curr_pwr_prop_out_max()
272 .with_context(|| format_dbg!())?;
273 let fc_max = self.fc.state.pwr_prop_max.get_fresh(|| format_dbg!())?;
274 self.transmission
275 .set_curr_pwr_prop_out_max(
276 (em_pwr_prop_out_maxes.0 + *fc_max, em_pwr_prop_out_maxes.1),
277 f64::NAN * uc::W,
278 dt,
279 veh_state,
280 )
281 .with_context(|| format_dbg!())?;
282 Ok(())
283 }
284
285 fn get_curr_pwr_prop_out_max(&self) -> anyhow::Result<(si::Power, si::Power)> {
286 self.transmission
287 .get_curr_pwr_prop_out_max()
288 .with_context(|| format_dbg!())
289 }
290
291 fn solve(
292 &mut self,
293 pwr_out_req: si::Power,
294 _enabled: bool,
295 dt: si::Time,
296 ) -> anyhow::Result<Option<si::Power>> {
297 let pwr_in_transmission = self
302 .transmission
303 .solve(pwr_out_req, true, dt)
304 .with_context(|| format_dbg!())?
305 .with_context(|| format!("{}\nExpected `Some`", format_dbg!()))?;
306
307 let (fc_pwr_out_req, em_pwr_out_req) = self
313 .pt_cntrl
314 .get_pwr_fc_and_em(pwr_in_transmission, &self.fc, &self.em.state, &self.res)
315 .with_context(|| format_dbg!())?;
316 let fc_on: bool = self.pt_cntrl.engine_on()?;
317
318 self.fc
319 .solve(fc_pwr_out_req, fc_on, dt)
320 .with_context(|| format_dbg!())?;
321 let res_pwr_out_req = self
322 .em
323 .solve(em_pwr_out_req, true, dt)
324 .with_context(|| format_dbg!())?
325 .with_context(|| format!("{}\nExpected `Some`", format_dbg!()))?;
326 self.res
328 .solve(res_pwr_out_req, dt)
329 .with_context(|| format_dbg!())?;
330 Ok(None)
331 }
332
333 fn pwr_regen(&self) -> anyhow::Result<si::Power> {
335 self.transmission.pwr_regen().with_context(|| format_dbg!())
339 }
340}
341
342impl HybridElectricVehicle {
343 pub fn solve_thermal(
353 &mut self,
354 te_amb: si::Temperature,
355 pwr_thrml_fc_to_cab: Option<si::Power>,
356 veh_state: &mut VehicleState,
357 pwr_thrml_hvac_to_res: Option<si::Power>,
358 te_cab: Option<si::Temperature>,
359 dt: si::Time,
360 ) -> anyhow::Result<()> {
361 self.fc
362 .solve_thermal(te_amb, pwr_thrml_fc_to_cab, veh_state, dt)
363 .with_context(|| format_dbg!())?;
364 self.res
365 .solve_thermal(
366 te_amb,
367 pwr_thrml_hvac_to_res.unwrap_or_default(),
368 te_cab,
369 dt,
370 )
371 .with_context(|| format_dbg!())?;
372 Ok(())
373 }
374}
375
376impl TryFrom<&fastsim_2::vehicle::RustVehicle> for HybridElectricVehicle {
377 type Error = anyhow::Error;
378 fn try_from(f2veh: &fastsim_2::vehicle::RustVehicle) -> anyhow::Result<HybridElectricVehicle> {
379 let pt_cntrl = HEVPowertrainControls::RGWDB(Box::new(hev::RESGreedyWithDynamicBuffers {
380 speed_soc_fc_on_buffer: None,
381 speed_soc_fc_on_buffer_coeff: None,
382 speed_soc_disch_buffer: None,
383 speed_soc_disch_buffer_coeff: None,
384 speed_soc_regen_buffer: None,
385 speed_soc_regen_buffer_coeff: None,
386 fc_min_time_on: None,
388 speed_fc_forced_on: Some(f2veh.mph_fc_on * uc::MPH),
389 frac_pwr_demand_fc_forced_on: Some(
390 f2veh.kw_demand_fc_on / (f2veh.fc_max_kw + f2veh.ess_max_kw.min(f2veh.mc_max_kw))
391 * uc::R,
392 ),
393 frac_of_most_eff_pwr_to_run_fc: None,
394 temp_fc_forced_on: None,
395 temp_fc_allowed_off: None,
396 save_interval: Some(1),
397 state: Default::default(),
398 history: Default::default(),
399 }));
400 let mut hev = HybridElectricVehicle {
401 fs: {
402 let mut fs = FuelStorage {
403 pwr_out_max: f2veh.fs_max_kw * uc::KW,
404 pwr_ramp_lag: f2veh.fs_secs_to_peak_pwr * uc::S,
405 energy_capacity: f2veh.fs_kwh * 3.6 * uc::MJ,
406 specific_energy: None,
407 mass: None,
408 };
409 fs.set_mass(None, MassSideEffect::None)
410 .with_context(|| anyhow!(format_dbg!()))?;
411 fs
412 },
413 fc: FuelConverter::try_from(f2veh.clone())?,
414 res: ReversibleEnergyStorage::try_from(f2veh.clone()).with_context(|| format_dbg!())?,
415 em: ElectricMachine::try_from(f2veh.clone())?,
416 transmission: Transmission::try_from(f2veh.clone())?,
417 pt_cntrl,
418 mass: None,
419 sim_params: Default::default(),
420 aux_cntrl: Default::default(),
421 soc_bal_iter_history: Default::default(),
422 soc_bal_iters: Default::default(),
423 };
424 hev.init()?;
425 Ok(hev)
426 }
427}
428impl Mass for HybridElectricVehicle {
429 fn mass(&self) -> anyhow::Result<Option<si::Mass>> {
430 let derived_mass = self
431 .derived_mass()
432 .with_context(|| anyhow!(format_dbg!()))?;
433 match (derived_mass, self.mass) {
434 (Some(derived_mass), Some(set_mass)) => {
435 ensure!(
436 utils::almost_eq_uom(&set_mass, &derived_mass, None),
437 format!(
438 "{}",
439 format_dbg!(utils::almost_eq_uom(&set_mass, &derived_mass, None)),
440 )
441 );
442 Ok(Some(set_mass))
443 }
444 _ => Ok(self.mass.or(derived_mass)),
445 }
446 }
447
448 fn set_mass(
449 &mut self,
450 new_mass: Option<si::Mass>,
451 side_effect: MassSideEffect,
452 ) -> anyhow::Result<()> {
453 ensure!(
454 side_effect == MassSideEffect::None,
455 "At the powertrain level, only `MassSideEffect::None` is allowed"
456 );
457 let derived_mass = self
458 .derived_mass()
459 .with_context(|| anyhow!(format_dbg!()))?;
460 self.mass = match new_mass {
461 Some(new_mass) => {
463 if let Some(dm) = derived_mass {
464 if dm != new_mass {
465 self.expunge_mass_fields();
466 }
467 }
468 Some(new_mass)
469 }
470 None => Some(derived_mass.with_context(|| {
472 format!(
473 "Not all mass fields in `{}` are set and no mass was provided.",
474 stringify!(HybridElectricVehicle)
475 )
476 })?),
477 };
478 Ok(())
479 }
480
481 fn derived_mass(&self) -> anyhow::Result<Option<si::Mass>> {
482 let fc_mass = self.fc.mass().with_context(|| anyhow!(format_dbg!()))?;
483 let fs_mass = self.fs.mass().with_context(|| anyhow!(format_dbg!()))?;
484 let res_mass = self.res.mass().with_context(|| anyhow!(format_dbg!()))?;
485 let em_mass = self.em.mass().with_context(|| anyhow!(format_dbg!()))?;
486 let transmission_mass = self
487 .transmission
488 .mass()
489 .with_context(|| anyhow!(format_dbg!()))?;
490 match (fc_mass, fs_mass, res_mass, em_mass, transmission_mass) {
491 (
492 Some(fc_mass),
493 Some(fs_mass),
494 Some(res_mass),
495 Some(em_mass),
496 Some(transmission_mass),
497 ) => Ok(Some(
498 fc_mass + fs_mass + res_mass + em_mass + transmission_mass,
499 )),
500 (None, None, None, None, None) => Ok(None),
501 _ => bail!(
502 "`{}` field masses are not consistently set to `Some` or `None`",
503 stringify!(HybridElectricVehicle)
504 ),
505 }
506 }
507
508 fn expunge_mass_fields(&mut self) {
509 self.fc.expunge_mass_fields();
510 self.fs.expunge_mass_fields();
511 self.res.expunge_mass_fields();
512 self.em.expunge_mass_fields();
513 self.transmission.expunge_mass_fields();
514 self.mass = None;
515 }
516}
517
518#[serde_api]
519#[derive(
520 Clone,
521 Debug,
522 Default,
523 Deserialize,
524 Serialize,
525 PartialEq,
526 HistoryVec,
527 StateMethods,
528 SetCumulative,
529)]
530#[non_exhaustive]
531#[serde(deny_unknown_fields)]
532pub struct RGWDBState {
533 pub i: TrackedState<usize>,
535 pub fc_temperature_too_low: TrackedState<bool>,
537 pub vehicle_speed_too_high: TrackedState<bool>,
540 pub on_time_too_short: TrackedState<bool>,
542 pub propulsion_power_demand: TrackedState<bool>,
544 pub propulsion_power_demand_soft: TrackedState<bool>,
546 pub aux_power_demand: TrackedState<bool>,
548 pub charging_for_low_soc: TrackedState<bool>,
550 pub soc_fc_on_buffer: TrackedState<si::Ratio>,
552}
553impl SerdeAPI for RGWDBState {}
554impl Init for RGWDBState {}
555
556impl RGWDBState {
557 fn engine_on(&self) -> anyhow::Result<bool> {
559 Ok(*self.fc_temperature_too_low.get_fresh(|| format_dbg!())?
560 || *self.vehicle_speed_too_high.get_fresh(|| format_dbg!())?
561 || *self.on_time_too_short.get_fresh(|| format_dbg!())?
562 || *self.propulsion_power_demand.get_fresh(|| format_dbg!())?
563 || *self
564 .propulsion_power_demand_soft
565 .get_fresh(|| format_dbg!())?
566 || *self.aux_power_demand.get_fresh(|| format_dbg!())?
567 || *self.charging_for_low_soc.get_fresh(|| format_dbg!())?)
568 }
569}
570
571#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)]
573#[non_exhaustive]
574#[serde(deny_unknown_fields)]
575pub struct HEVSimulationParams {
576 pub res_per_fuel_lim: si::Ratio,
578 pub soc_balance_iter_err: u32,
580 pub balance_soc: bool,
582 pub save_soc_bal_iters: bool,
584}
585
586impl Default for HEVSimulationParams {
587 fn default() -> Self {
588 Self {
589 res_per_fuel_lim: uc::R * 0.005,
590 soc_balance_iter_err: 5,
591 balance_soc: true,
592 save_soc_bal_iters: false,
593 }
594 }
595}
596
597#[derive(
598 Clone, Debug, PartialEq, Deserialize, Serialize, Default, IsVariant, derive_more::From, TryInto,
599)]
600pub enum HEVAuxControls {
601 #[default]
603 AuxOnResPriority,
604 AuxOnFcPriority,
606}
607
608#[derive(
609 Clone, Debug, PartialEq, Deserialize, Serialize, IsVariant, derive_more::From, TryInto,
610)]
611pub enum HEVPowertrainControls {
612 RGWDB(Box<RESGreedyWithDynamicBuffers>),
616}
617
618impl Default for HEVPowertrainControls {
619 fn default() -> Self {
620 Self::RGWDB(Default::default())
621 }
622}
623
624impl SetCumulative for HEVPowertrainControls {
625 fn set_cumulative<F: Fn() -> String>(&mut self, dt: si::Time, loc: F) -> anyhow::Result<()> {
626 match self {
627 Self::RGWDB(rgwdb) => {
628 rgwdb.set_cumulative(dt, || format!("{}\n{}", loc(), format_dbg!()))?
629 }
630 }
631 Ok(())
632 }
633
634 fn reset_cumulative<F: Fn() -> String>(&mut self, loc: F) -> anyhow::Result<()> {
635 match self {
636 Self::RGWDB(rgwdb) => {
637 rgwdb.reset_cumulative(|| format!("{}\n{}", loc(), format_dbg!()))?
638 }
639 }
640 Ok(())
641 }
642}
643impl Step for HEVPowertrainControls {
644 fn step<F: Fn() -> String>(&mut self, loc: F) -> anyhow::Result<()> {
645 match self {
646 HEVPowertrainControls::RGWDB(rgwdb) => rgwdb.step(loc)?,
647 }
648 Ok(())
649 }
650
651 fn reset_step<F: Fn() -> String>(&mut self, loc: F) -> anyhow::Result<()> {
652 match self {
653 HEVPowertrainControls::RGWDB(rgwdb) => rgwdb.reset_step(loc)?,
654 }
655 Ok(())
656 }
657}
658
659impl StateMethods for HEVPowertrainControls {}
660
661impl SaveState for HEVPowertrainControls {
662 fn save_state<F: Fn() -> String>(&mut self, loc: F) -> anyhow::Result<()> {
663 match self {
664 HEVPowertrainControls::RGWDB(rgwdb) => rgwdb.save_state(loc)?,
665 }
666 Ok(())
667 }
668}
669impl TrackedStateMethods for HEVPowertrainControls {
670 fn check_and_reset<F: Fn() -> String>(&mut self, loc: F) -> anyhow::Result<()> {
671 match self {
672 HEVPowertrainControls::RGWDB(rgwdb) => rgwdb.check_and_reset(loc)?,
673 }
674 Ok(())
675 }
676
677 fn mark_fresh<F: Fn() -> String>(&mut self, loc: F) -> anyhow::Result<()> {
678 match self {
679 HEVPowertrainControls::RGWDB(rgwdb) => rgwdb.mark_fresh(loc)?,
680 }
681 Ok(())
682 }
683}
684impl HistoryMethods for HEVPowertrainControls {
685 fn set_save_interval(&mut self, save_interval: Option<usize>) -> anyhow::Result<()> {
686 match self {
687 HEVPowertrainControls::RGWDB(rgwdb) => Ok(rgwdb.set_save_interval(save_interval)?),
688 }
689 }
690
691 fn save_interval(&self) -> anyhow::Result<Option<usize>> {
692 match self {
693 HEVPowertrainControls::RGWDB(rgwdb) => rgwdb.save_interval(),
694 }
695 }
696 fn clear(&mut self) {
697 match self {
698 HEVPowertrainControls::RGWDB(rgwdb) => rgwdb.clear(),
699 }
700 }
701}
702
703impl Init for HEVPowertrainControls {
704 fn init(&mut self) -> Result<(), Error> {
705 match self {
706 Self::RGWDB(rgwb) => rgwb.init()?,
707 }
708 Ok(())
709 }
710}
711
712impl HEVPowertrainControls {
713 fn get_pwr_fc_and_em(
723 &mut self,
724 pwr_prop_req: si::Power,
725 fc: &FuelConverter,
726 em_state: &ElectricMachineState,
727 res: &ReversibleEnergyStorage,
728 ) -> anyhow::Result<(si::Power, si::Power)> {
729 let fc_state = &fc.state;
730 ensure!(
731 almost_le_uom(
733 &pwr_prop_req,
734 &(*em_state.pwr_mech_fwd_out_max.get_fresh(|| format_dbg!())?
735 + *fc_state.pwr_prop_max.get_fresh(|| format_dbg!())?),
736 None
737 ),
738 "{}
739`pwr_out_req`: {} kW
740`em_state.pwr_mech_fwd_out_max`: {} kW
741`fc_state.pwr_prop_max`: {} kW
742`res.state.soc`: {}",
743 format_dbg!(),
744 pwr_prop_req.get::<si::kilowatt>(),
745 em_state
746 .pwr_mech_fwd_out_max
747 .get_fresh(|| format_dbg!())?
748 .get::<si::kilowatt>(),
749 fc_state
750 .pwr_prop_max
751 .get_fresh(|| format_dbg!())?
752 .get::<si::kilowatt>(),
753 res.state
754 .soc
755 .get_fresh(|| format_dbg!())?
756 .get::<si::ratio>()
757 );
758
759 match self {
767 Self::RGWDB(rgwdb) => rgwdb.get_pwr_fc_and_em(fc, pwr_prop_req, em_state),
768 }
769 }
770
771 pub fn engine_on(&self) -> anyhow::Result<bool> {
772 match self {
773 Self::RGWDB(rgwdb) => rgwdb.state.engine_on(),
774 }
775 }
776}
777
778#[serde_api]
783#[derive(Clone, Debug, PartialEq, Deserialize, Serialize, Default, StateMethods, SetCumulative)]
784#[cfg_attr(feature = "pyo3", pyclass(module = "fastsim", subclass, eq))]
785#[non_exhaustive]
786#[serde(deny_unknown_fields)]
787pub struct RESGreedyWithDynamicBuffers {
788 pub speed_soc_disch_buffer: Option<si::Velocity>,
791 pub speed_soc_disch_buffer_coeff: Option<si::Ratio>,
793 pub speed_soc_fc_on_buffer: Option<si::Velocity>,
796 pub speed_soc_fc_on_buffer_coeff: Option<si::Ratio>,
798 pub speed_soc_regen_buffer: Option<si::Velocity>,
802 pub speed_soc_regen_buffer_coeff: Option<si::Ratio>,
804 pub fc_min_time_on: Option<si::Time>,
807 pub speed_fc_forced_on: Option<si::Velocity>,
809 pub frac_pwr_demand_fc_forced_on: Option<si::Ratio>,
812 pub frac_of_most_eff_pwr_to_run_fc: Option<si::Ratio>,
817 pub save_interval: Option<usize>,
821 #[serde(default)]
823 pub temp_fc_forced_on: Option<si::Temperature>,
824 #[serde(default)]
826 pub temp_fc_allowed_off: Option<si::Temperature>,
827 #[serde(default)]
829 pub state: RGWDBState,
830 #[serde(default)]
831 pub history: RGWDBStateHistoryVec,
833}
834
835#[pyo3_api]
836impl RESGreedyWithDynamicBuffers {}
837
838impl HistoryMethods for RESGreedyWithDynamicBuffers {
839 fn set_save_interval(&mut self, save_interval: Option<usize>) -> anyhow::Result<()> {
840 self.save_interval = save_interval;
841 Ok(())
842 }
843
844 fn save_interval(&self) -> anyhow::Result<Option<usize>> {
845 Ok(self.save_interval)
846 }
847
848 fn clear(&mut self) {
849 self.history.clear();
850 }
851}
852
853impl Init for RESGreedyWithDynamicBuffers {
854 fn init(&mut self) -> Result<(), Error> {
855 init_opt_default!(self, speed_soc_disch_buffer, 50.0 * uc::MPH);
857 init_opt_default!(self, speed_soc_disch_buffer_coeff, 1.0 * uc::R);
858 init_opt_default!(
859 self,
860 speed_soc_fc_on_buffer,
861 self.speed_soc_disch_buffer.unwrap() * 1.2
862 );
863 init_opt_default!(self, speed_soc_fc_on_buffer_coeff, 1.0 * uc::R);
864 init_opt_default!(self, speed_soc_regen_buffer, 30. * uc::MPH);
865 init_opt_default!(self, speed_soc_regen_buffer_coeff, 1.0 * uc::R);
866 init_opt_default!(self, fc_min_time_on, uc::S * 5.0);
867 init_opt_default!(self, speed_fc_forced_on, uc::MPH * 75.);
868 init_opt_default!(self, frac_pwr_demand_fc_forced_on, uc::R * 0.75);
869 init_opt_default!(self, frac_of_most_eff_pwr_to_run_fc, 1.0 * uc::R);
870 Ok(())
871 }
872}
873impl SerdeAPI for RESGreedyWithDynamicBuffers {}
874
875impl RESGreedyWithDynamicBuffers {
876 fn get_pwr_fc_and_em(
877 &mut self,
878 fc: &FuelConverter,
879 pwr_prop_req: si::Power,
880 em_state: &ElectricMachineState,
881 ) -> anyhow::Result<(si::Power, si::Power)> {
882 let em_pwr = pwr_prop_req
887 .min(*em_state.pwr_mech_fwd_out_max.get_fresh(|| format_dbg!())?)
888 .max(-*em_state.pwr_mech_regen_max.get_fresh(|| format_dbg!())?);
889 let (fc_pwr, em_pwr) = if !self.state.engine_on()? {
891 (si::Power::ZERO, em_pwr)
893 } else {
894 let frac_of_pwr_for_peak_eff: si::Ratio = self
896 .frac_of_most_eff_pwr_to_run_fc
897 .with_context(|| format_dbg!())?;
898 let fc_pwr = if pwr_prop_req < si::Power::ZERO {
899 (*em_state.pwr_mech_regen_max.get_fresh(|| format_dbg!())? + pwr_prop_req)
902 .min(fc.pwr_for_peak_eff * frac_of_pwr_for_peak_eff)
904 .max(si::Power::ZERO)
906 } else {
907 if pwr_prop_req - em_pwr > fc.pwr_for_peak_eff * frac_of_pwr_for_peak_eff {
909 pwr_prop_req - em_pwr
911 } else {
912 (pwr_prop_req - em_pwr)
917 .max(fc.pwr_for_peak_eff * frac_of_pwr_for_peak_eff)
921 .min(
924 pwr_prop_req
925 + *em_state.pwr_mech_regen_max.get_fresh(|| format_dbg!())?,
926 )
927 }
928 }
929 .min(*fc.state.pwr_prop_max.get_fresh(|| format_dbg!())?);
931
932 let em_pwr_corrected = (pwr_prop_req - fc_pwr)
934 .max(-*em_state.pwr_mech_regen_max.get_fresh(|| format_dbg!())?);
935 (fc_pwr, em_pwr_corrected)
936 };
937 Ok((fc_pwr, em_pwr))
938 }
939
940 fn handle_fc_on_causes(
941 &mut self,
942 fc: &FuelConverter,
943 veh_state: &VehicleState,
944 res: &ReversibleEnergyStorage,
945 em_state: &ElectricMachineState,
946 ) -> Result<(), anyhow::Error> {
947 self.handle_fc_on_causes_for_temp(fc)?;
948 self.handle_fc_on_causes_for_speed(veh_state)?;
949 self.handle_fc_on_causes_for_low_soc(res, veh_state)?;
950 self.handle_fc_on_causes_for_pwr_demand(
951 *veh_state
952 .pwr_tractive
953 .get_stale(|| format_dbg!(veh_state.pwr_tractive))?,
954 em_state,
955 &fc.state,
956 )
957 .with_context(|| format_dbg!())?;
958 self.handle_fc_on_causes_for_on_time(fc)?;
959 Ok(())
960 }
961
962 fn handle_fc_on_causes_for_on_time(&mut self, fc: &FuelConverter) -> Result<(), anyhow::Error> {
963 self.state.on_time_too_short.update(*fc.state.fc_on.get_stale(|| format_dbg!())? && *fc.state.time_on.get_stale(|| format_dbg!())?
964 < self.fc_min_time_on.with_context(|| {
965 anyhow!(
966 "{}\n Expected `ResGreedyWithBuffers::init` to have been called beforehand.",
967 format_dbg!()
968 )
969 })?, || format_dbg!())?;
970 Ok(())
971 }
972
973 fn handle_fc_on_causes_for_pwr_demand(
976 &mut self,
977 pwr_out_req_for_cyc: si::Power,
978 em_state: &ElectricMachineState,
979 fc_state: &FuelConverterState,
980 ) -> Result<(), anyhow::Error> {
981 let frac_pwr_demand_fc_forced_on: si::Ratio = self
982 .frac_pwr_demand_fc_forced_on
983 .with_context(|| format_dbg!())?;
984 self.state.propulsion_power_demand_soft.update(
985 pwr_out_req_for_cyc
986 > frac_pwr_demand_fc_forced_on
987 * (*em_state.pwr_mech_fwd_out_max.get_stale(|| format_dbg!())?
988 + *fc_state.pwr_out_max.get_stale(|| format_dbg!())?),
989 || format_dbg!(),
990 )?;
991 self.state.propulsion_power_demand.update(
992 pwr_out_req_for_cyc - *em_state.pwr_mech_fwd_out_max.get_stale(|| format_dbg!())?
993 >= si::Power::ZERO,
994 || format_dbg!(),
995 )?;
996 Ok(())
997 }
998
999 fn handle_fc_on_causes_for_low_soc(
1001 &mut self,
1002 res: &ReversibleEnergyStorage,
1003 veh_state: &VehicleState,
1004 ) -> anyhow::Result<()> {
1005 self.state.soc_fc_on_buffer.update(
1006 {
1007 let energy_delta_to_buffer_speed: si::Energy = 0.5
1008 * *veh_state.mass.get_fresh(|| format_dbg!())?
1009 * (self
1010 .speed_soc_fc_on_buffer
1011 .with_context(|| format_dbg!())?
1012 .powi(P2::new())
1013 - veh_state
1014 .speed_ach
1015 .get_stale(|| format_dbg!())?
1016 .powi(P2::new()));
1017 energy_delta_to_buffer_speed.max(si::Energy::ZERO)
1018 * self
1019 .speed_soc_fc_on_buffer_coeff
1020 .with_context(|| format_dbg!())?
1021 } / res.energy_capacity_usable()
1022 + res.min_soc,
1023 || format_dbg!(),
1024 )?;
1025 self.state.charging_for_low_soc.update(
1026 *res.state.soc.get_stale(|| format_dbg!())?
1027 < *self.state.soc_fc_on_buffer.get_fresh(|| format_dbg!())?,
1028 || format_dbg!(),
1029 )?;
1030 Ok(())
1031 }
1032
1033 fn handle_fc_on_causes_for_speed(&mut self, veh_state: &VehicleState) -> anyhow::Result<()> {
1035 self.state.vehicle_speed_too_high.update(
1036 *veh_state.speed_ach.get_stale(|| format_dbg!())?
1037 > self.speed_fc_forced_on.with_context(|| format_dbg!())?,
1038 || format_dbg!(),
1039 )?;
1040 Ok(())
1041 }
1042
1043 fn handle_fc_on_causes_for_temp(&mut self, fc: &FuelConverter) -> anyhow::Result<()> {
1046 match (
1047 match fc.temperature() {
1048 Some(fct) => Some(*fct.get_fresh(|| format_dbg!())?),
1049 None => None,
1050 },
1051 match fc.temperature() {
1052 Some(fct) => Some(*fct.get_fresh(|| format_dbg!())?),
1053 None => None,
1054 },
1055 self.temp_fc_forced_on,
1056 self.temp_fc_allowed_off,
1057 ) {
1058 (None, None, None, None) => {
1059 self.state
1060 .fc_temperature_too_low
1061 .update(false, || format_dbg!())?;
1062 }
1063 (
1064 Some(temperature),
1065 Some(temp_prev),
1066 Some(temp_fc_forced_on),
1067 Some(temp_fc_allowed_off),
1068 ) => {
1069 self.state.fc_temperature_too_low.update(
1070 temperature < temp_fc_forced_on ||
1072 (temp_prev < temp_fc_forced_on && temperature < temp_fc_allowed_off),
1074 || format_dbg!(),
1075 )?;
1076 }
1077 _ => {
1078 bail!(
1079 "{}\n`fc.temperature()`, `fc.temp_prev()`, `self.temp_fc_forced_on`, and
1080`self.temp_fc_allowed_off` must all be `None` or `Some` because these controls are necessary
1081for an HEV equipped with thermal models or superfluous otherwise",
1082 format_dbg!((
1083 fc.temperature(),
1084 self.temp_fc_forced_on,
1085 self.temp_fc_allowed_off
1086 ))
1087 );
1088 }
1089 }
1090 Ok(())
1091 }
1092}