fastsim_core/vehicle/
vehicle_model.rs

1use super::{hev::HEVPowertrainControls, *};
2use crate::prelude::*;
3pub mod fastsim2_interface;
4
5/// Possible aux load power sources
6#[derive(
7    Clone, Debug, Serialize, Deserialize, PartialEq, IsVariant, derive_more::From, TryInto,
8)]
9pub enum AuxSource {
10    /// Aux load power provided by ReversibleEnergyStorage with help from FuelConverter, if present
11    /// and needed
12    ReversibleEnergyStorage,
13    /// Aux load power provided by FuelConverter with help from ReversibleEnergyStorage, if present
14    /// and needed
15    FuelConverter,
16}
17
18impl SerdeAPI for AuxSource {}
19impl Init for AuxSource {}
20
21#[serde_api]
22#[cfg_attr(feature = "pyo3", pyclass(module = "fastsim", subclass, eq))]
23#[derive(PartialEq, Clone, Debug, Serialize, Deserialize, StateMethods)]
24#[non_exhaustive]
25#[serde(deny_unknown_fields)]
26/// Struct for simulating vehicle
27pub struct Vehicle {
28    /// Vehicle name
29    pub name: String,
30    /// Documentation (e.g. how this file was generated, calibration details)]
31    pub doc: Option<String>,
32    /// Year manufactured
33    pub year: u32,
34    #[has_state]
35    /// type of vehicle powertrain including contained type-specific parameters and variables
36    pub pt_type: PowertrainType,
37
38    /// Chassis model with various chassis-related parameters
39    pub chassis: Chassis,
40
41    /// Cabin thermal model
42    #[has_state]
43    #[serde(default)]
44    pub cabin: CabinOption,
45
46    /// HVAC model
47    #[has_state]
48    #[serde(default)]
49    pub hvac: HVACOption,
50
51    /// Total vehicle mass
52    pub(crate) mass: Option<si::Mass>,
53
54    /// Baseline power required by auxilliary systems
55    pub pwr_aux_base: si::Power,
56
57    /// time step interval at which `state` is saved into `history`
58    save_interval: Option<usize>,
59    /// current state of vehicle
60    #[serde(default)]
61    pub state: VehicleState,
62    /// Vector-like history of [Self::state]
63    #[serde(default)]
64    pub history: VehicleStateHistoryVec,
65}
66
67#[pyo3_api]
68impl Vehicle {
69    #[staticmethod]
70    fn try_from_fastsim2(veh: fastsim_2::vehicle::RustVehicle) -> PyResult<Vehicle> {
71        Ok(Self::try_from(veh.clone())?)
72    }
73
74    #[pyo3(name = "set_save_interval")]
75    #[pyo3(signature = (save_interval=None))]
76    /// Set save interval and cascade to nested components.
77    fn set_save_interval_py(&mut self, save_interval: Option<usize>) -> PyResult<()> {
78        self.set_save_interval(save_interval)
79            .map_err(|e| PyAttributeError::new_err(e.to_string()))
80    }
81
82    // despite having `getter` here, this seems to work as a function
83    #[getter("save_interval")]
84    /// Set save interval and cascade to nested components.
85    fn get_save_interval_py(&self) -> anyhow::Result<Option<usize>> {
86        self.save_interval()
87    }
88
89    #[getter]
90    fn get_fc(&self) -> Option<FuelConverter> {
91        self.fc().cloned()
92    }
93
94    #[getter]
95    fn get_res(&self) -> Option<ReversibleEnergyStorage> {
96        self.res().cloned()
97    }
98
99    #[getter]
100    fn get_em(&self) -> Option<ElectricMachine> {
101        self.em().cloned()
102    }
103
104    fn veh_type(&self) -> String {
105        self.pt_type.to_string()
106    }
107
108    // #[getter]
109    // fn get_pwr_rated_kilowatts(&self) -> f64 {
110    //     self.get_pwr_rated().get::<si::kilowatt>()
111    // }
112
113    // #[getter]
114    // fn get_mass_kg(&self) -> PyResult<Option<f64>> {
115    //     Ok(self.mass()?.map(|m| m))
116    // }
117
118    /// Load vehicle from file saved in fastsim-2 format
119    #[pyo3(name = "from_f2_file")]
120    #[staticmethod]
121    fn from_f2_file_py(file: PathBuf) -> anyhow::Result<Self> {
122        Self::from_f2_file(file)
123    }
124
125    #[pyo3(name = "to_fastsim2")]
126    fn to_fastsim2_py(&self) -> anyhow::Result<fastsim_2::vehicle::RustVehicle> {
127        self.to_fastsim2()
128    }
129
130    #[pyo3(name = "reset_py")]
131    /// Compines [Self::reset_cumulative], [Self::reset_step], [Self::clear]
132    fn reset_py(&mut self) -> anyhow::Result<()> {
133        self.reset_cumulative(|| format_dbg!())?;
134        self.reset_step(|| format_dbg!())?;
135        self.clear();
136        Ok(())
137    }
138
139    #[pyo3(name = "clear")]
140    fn clear_py(&mut self) {
141        self.clear()
142    }
143
144    #[pyo3(name = "reset_step")]
145    fn reset_step_py(&mut self) -> anyhow::Result<()> {
146        self.reset_step(|| format_dbg!())
147    }
148
149    #[pyo3(name = "reset_cumulative")]
150    fn reset_cumulative_py(&mut self) -> anyhow::Result<()> {
151        self.reset_cumulative(|| format_dbg!())
152    }
153}
154
155impl Mass for Vehicle {
156    fn mass(&self) -> anyhow::Result<Option<si::Mass>> {
157        let derived_mass = self
158            .derived_mass()
159            .with_context(|| anyhow!(format_dbg!()))?;
160        match (derived_mass, self.mass) {
161            (Some(derived_mass), Some(set_mass)) => {
162                ensure!(
163                    utils::almost_eq_uom(&set_mass, &derived_mass, None),
164                    format!(
165                        "{}",
166                        format_dbg!(utils::almost_eq_uom(&set_mass, &derived_mass, None)),
167                    )
168                );
169                Ok(Some(set_mass))
170            }
171            (None, None) => bail!(
172                "Not all mass fields in `{}` are set and no mass was previously set.",
173                stringify!(Vehicle)
174            ),
175            _ => Ok(self.mass.or(derived_mass)),
176        }
177    }
178
179    fn set_mass(
180        &mut self,
181        new_mass: Option<si::Mass>,
182        side_effect: MassSideEffect,
183    ) -> anyhow::Result<()> {
184        ensure!(
185            side_effect == MassSideEffect::None,
186            "At the vehicle level, only `MassSideEffect::None` is allowed"
187        );
188
189        let derived_mass = self
190            .derived_mass()
191            .with_context(|| anyhow!(format_dbg!()))?;
192        self.mass = match new_mass {
193            // Set using provided `new_mass`, setting constituent mass fields to `None` to match if inconsistent
194            Some(new_mass) => {
195                if let Some(dm) = derived_mass {
196                    if dm != new_mass {
197                        self.expunge_mass_fields();
198                    }
199                }
200                Some(new_mass)
201            }
202            // Set using `derived_mass()`, failing if it returns `None`
203            None => Some(derived_mass.with_context(|| {
204                format!(
205                    "Not all mass fields in `{}` are set and no mass was provided.",
206                    stringify!(Vehicle)
207                )
208            })?),
209        };
210        Ok(())
211    }
212
213    fn derived_mass(&self) -> anyhow::Result<Option<si::Mass>> {
214        let chassis_mass = self
215            .chassis
216            .mass()
217            .with_context(|| anyhow!(format_dbg!()))?;
218        let pt_mass = match &self.pt_type {
219            PowertrainType::ConventionalVehicle(conv) => conv.mass()?,
220            PowertrainType::HybridElectricVehicle(hev) => hev.mass()?,
221            PowertrainType::PlugInHybridElectricVehicle(phev) => phev.mass()?,
222            PowertrainType::BatteryElectricVehicle(bev) => bev.mass()?,
223        };
224        if let (Some(pt_mass), Some(chassis_mass)) = (pt_mass, chassis_mass) {
225            Ok(Some(pt_mass + chassis_mass))
226        } else {
227            Ok(None)
228        }
229    }
230
231    fn expunge_mass_fields(&mut self) {
232        self.chassis.expunge_mass_fields();
233        match &mut self.pt_type {
234            PowertrainType::ConventionalVehicle(conv) => conv.expunge_mass_fields(),
235            PowertrainType::HybridElectricVehicle(hev) => hev.expunge_mass_fields(),
236            PowertrainType::PlugInHybridElectricVehicle(phev) => phev.expunge_mass_fields(),
237            PowertrainType::BatteryElectricVehicle(bev) => bev.expunge_mass_fields(),
238        };
239    }
240}
241
242impl SerdeAPI for Vehicle {
243    #[cfg(feature = "resources")]
244    const RESOURCES_SUBDIR: &'static str = "vehicles";
245}
246impl Init for Vehicle {
247    fn init(&mut self) -> Result<(), Error> {
248        let _mass = self
249            .mass()
250            .map_err(|err| Error::InitError(format_dbg!(err)))?;
251        self.calculate_wheel_radius()
252            .map_err(|err| Error::InitError(format_dbg!(err)))?;
253        self.pt_type
254            .init()
255            .map_err(|err| Error::InitError(format_dbg!(err)))?;
256        let mass = self
257            .mass()
258            .unwrap_or(Some(0.0 * uc::KG))
259            .unwrap_or(0.0 * uc::KG);
260        let _ = match &self.pt_type {
261            PowertrainType::HybridElectricVehicle(hev) => hev.check_buffers(mass),
262            PowertrainType::PlugInHybridElectricVehicle(hev) => hev.check_buffers(mass),
263            _ => Ok(()),
264        };
265        Ok(())
266    }
267}
268
269impl HistoryMethods for Vehicle {
270    fn save_interval(&self) -> anyhow::Result<Option<usize>> {
271        Ok(self.save_interval)
272    }
273    fn set_save_interval(&mut self, save_interval: Option<usize>) -> anyhow::Result<()> {
274        self.save_interval = save_interval;
275        self.pt_type.set_save_interval(save_interval)?;
276        self.cabin.set_save_interval(save_interval)?;
277        self.hvac.set_save_interval(save_interval)?;
278        Ok(())
279    }
280    fn clear(&mut self) {
281        self.history.clear();
282        self.pt_type.clear();
283        self.cabin.clear();
284        self.hvac.clear();
285    }
286}
287
288/// TODO: update this constant to match fastsim-2 for gasoline
289pub(super) const FUEL_LHV_MJ_PER_KG: f64 = 43.2;
290const CONV: &str = "Conv";
291const HEV: &str = "HEV";
292const PHEV: &str = "PHEV";
293const BEV: &str = "BEV";
294
295impl SetCumulative for Vehicle {
296    fn set_cumulative<F: Fn() -> String>(&mut self, dt: si::Time, loc: F) -> anyhow::Result<()> {
297        self.state
298            .set_cumulative(dt, || format!("{}\n{}", loc(), format_dbg!()))?;
299        self.pt_type
300            .set_cumulative(dt, || format!("{}\n{}", loc(), format_dbg!()))?;
301        self.cabin
302            .set_cumulative(dt, || format!("{}\n{}", loc(), format_dbg!()))?;
303        self.hvac
304            .set_cumulative(dt, || format!("{}\n{}", loc(), format_dbg!()))?;
305        // this does not get handled by the `SetCumulative` derive macro
306        self.state.dist.increment(
307            *self.state.speed_ach.get_fresh(|| format_dbg!())? * dt,
308            || format_dbg!(),
309        )?;
310        Ok(())
311    }
312
313    fn reset_cumulative<F: Fn() -> String>(&mut self, loc: F) -> anyhow::Result<()> {
314        self.state
315            .reset_cumulative(|| format!("{}\n{}", loc(), format_dbg!()))?;
316        self.pt_type
317            .reset_cumulative(|| format!("{}\n{}", loc(), format_dbg!()))?;
318        self.cabin
319            .reset_cumulative(|| format!("{}\n{}", loc(), format_dbg!()))?;
320        self.hvac
321            .reset_cumulative(|| format!("{}\n{}", loc(), format_dbg!()))?;
322        // this does not get handled by the `SetCumulative` derive macro
323        self.state.dist.mark_stale();
324        self.state.dist.update(si::Length::ZERO, || format_dbg!())?;
325        self.state.time.mark_stale();
326        self.state.time.update(si::Time::ZERO, || format_dbg!())?;
327        self.state.speed_ach.mark_stale();
328        self.state
329            .speed_ach
330            .update(si::Velocity::ZERO, || format_dbg!())?;
331        Ok(())
332    }
333}
334
335impl Vehicle {
336    /// # Assumptions
337    /// - peak power of all components can be produced concurrently.
338    pub fn get_pwr_rated(&self) -> si::Power {
339        match (self.fc(), self.res()) {
340            (Some(fc), Some(res)) => fc.pwr_out_max + res.pwr_out_max,
341            (Some(fc), None) => fc.pwr_out_max,
342            (None, Some(res)) => res.pwr_out_max,
343            (None, None) => unreachable!(),
344        }
345    }
346
347    pub fn conv(&self) -> Option<&ConventionalVehicle> {
348        self.pt_type.conv()
349    }
350
351    pub fn hev(&self) -> Option<&HybridElectricVehicle> {
352        self.pt_type.hev()
353    }
354
355    // pub fn phev(&self) -> Option<&HybridElectricVehicle> {
356    //     self.pt_type.phev()
357    // }
358
359    pub fn bev(&self) -> Option<&BatteryElectricVehicle> {
360        self.pt_type.bev()
361    }
362
363    pub fn conv_mut(&mut self) -> Option<&mut ConventionalVehicle> {
364        self.pt_type.conv_mut()
365    }
366
367    pub fn hev_mut(&mut self) -> Option<&mut HybridElectricVehicle> {
368        self.pt_type.hev_mut()
369    }
370
371    // pub fn phev_mut(&mut self) -> Option<&mut HybridElectricVehicle> {
372    //     self.pt_type.phev_mut()
373    // }
374
375    pub fn bev_mut(&mut self) -> Option<&mut BatteryElectricVehicle> {
376        self.pt_type.bev_mut()
377    }
378
379    pub fn fc(&self) -> Option<&FuelConverter> {
380        self.pt_type.fc()
381    }
382
383    pub fn fc_mut(&mut self) -> Option<&mut FuelConverter> {
384        self.pt_type.fc_mut()
385    }
386
387    pub fn set_fc(&mut self, fc: FuelConverter) -> anyhow::Result<()> {
388        self.pt_type.set_fc(fc)
389    }
390
391    pub fn fs(&self) -> Option<&FuelStorage> {
392        self.pt_type.fs()
393    }
394
395    pub fn fs_mut(&mut self) -> Option<&mut FuelStorage> {
396        self.pt_type.fs_mut()
397    }
398
399    pub fn set_fs(&mut self, fs: FuelStorage) -> anyhow::Result<()> {
400        self.pt_type.set_fs(fs)
401    }
402
403    pub fn res(&self) -> Option<&ReversibleEnergyStorage> {
404        self.pt_type.res()
405    }
406
407    pub fn res_mut(&mut self) -> Option<&mut ReversibleEnergyStorage> {
408        self.pt_type.res_mut()
409    }
410
411    pub fn set_res(&mut self, res: ReversibleEnergyStorage) -> anyhow::Result<()> {
412        self.pt_type.set_res(res)
413    }
414
415    pub fn em(&self) -> Option<&ElectricMachine> {
416        self.pt_type.em()
417    }
418
419    pub fn em_mut(&mut self) -> Option<&mut ElectricMachine> {
420        self.pt_type.em_mut()
421    }
422
423    pub fn set_em(&mut self, em: ElectricMachine) -> anyhow::Result<()> {
424        self.pt_type.set_em(em)
425    }
426
427    pub fn trans(&self) -> Option<&Transmission> {
428        self.pt_type.trans()
429    }
430
431    pub fn trans_mut(&mut self) -> Option<&mut Transmission> {
432        self.pt_type.trans_mut()
433    }
434
435    pub fn set_trans(&mut self, trans: Transmission) -> anyhow::Result<()> {
436        self.pt_type.set_trans(trans)
437    }
438
439    /// Calculate wheel radius from tire code, if applicable
440    fn calculate_wheel_radius(&mut self) -> anyhow::Result<()> {
441        ensure!(
442            self.chassis.wheel_radius.is_some() || self.chassis.tire_code.is_some(),
443            "Either `wheel_radius` or `tire_code` must be supplied"
444        );
445        if self.chassis.wheel_radius.is_none() {
446            self.chassis.wheel_radius =
447                Some(utils::tire_code_to_radius(self.chassis.tire_code.as_ref().unwrap())? * uc::M)
448        }
449        Ok(())
450    }
451
452    /// Solves for energy consumption
453    pub fn solve_powertrain(&mut self, dt: si::Time) -> anyhow::Result<()> {
454        self.pt_type
455            .solve(
456                *self.state.pwr_tractive.get_fresh(|| format_dbg!())?,
457                true, // `enabled` should always be true at the powertrain level
458                dt,
459            )
460            .with_context(|| anyhow!(format_dbg!()))?;
461        self.state.pwr_brake.update(
462            -self
463                .state
464                .pwr_tractive
465                .get_fresh(|| format_dbg!())?
466                .max(si::Power::ZERO)
467                - self.pt_type.pwr_regen().with_context(|| format_dbg!())?,
468            || format_dbg!(),
469        )?;
470        Ok(())
471    }
472
473    pub fn set_curr_pwr_out_max(&mut self, dt: si::Time) -> anyhow::Result<()> {
474        // Calculate traction limits
475        let mass = self
476            .mass
477            .with_context(|| format!("{}\nMass should have been set before now", format_dbg!()))?;
478        let max_trac_accel = self.chassis.wheel_fric_coef
479            * self.chassis.drive_axle_weight_frac
480            * uc::ACC_GRAV
481            / (1.0 * uc::R
482                + self.chassis.cg_height * self.chassis.wheel_fric_coef / self.chassis.wheel_base);
483        let prev_speed = *self.state.speed_ach.get_stale(|| format_dbg!())?;
484        let max_trac_speed = prev_speed + (max_trac_accel * dt);
485        let max_trac_power = self.chassis.wheel_fric_coef
486            * self.chassis.drive_axle_weight_frac
487            * mass
488            * uc::ACC_GRAV
489            / (1.0 * uc::R
490                + self.chassis.cg_height * self.chassis.wheel_fric_coef / self.chassis.wheel_base)
491            * max_trac_speed;
492        // Calculate powertrain limits
493        self.pt_type
494            .set_curr_pwr_prop_out_max(
495                (si::Power::ZERO, si::Power::ZERO),
496                *self.state.pwr_aux.get_fresh(|| format_dbg!())?,
497                dt,
498                &self.state,
499            )
500            .with_context(|| anyhow!(format_dbg!()))?;
501        let pwr_prop_maxes = self
502            .pt_type
503            .get_curr_pwr_prop_out_max()
504            .with_context(|| anyhow!(format_dbg!()))?;
505        self.state.pwr_prop_fwd_max.update(
506            if pwr_prop_maxes.0 > max_trac_power {
507                max_trac_power
508            } else {
509                pwr_prop_maxes.0
510            },
511            || format_dbg!(),
512        )?;
513        self.state
514            .pwr_prop_bwd_max
515            .update(pwr_prop_maxes.1, || format_dbg!())?;
516
517        Ok(())
518    }
519
520    pub fn solve_thermal(
521        &mut self,
522        te_amb_air: si::Temperature,
523        dt: si::Time,
524    ) -> anyhow::Result<()> {
525        let te_fc: Option<si::Temperature> = self
526            .fc()
527            .and_then(|fc| fc.temperature().map(|fct| fct.get_stale(|| format_dbg!())))
528            .transpose()
529            .with_context(|| {
530                format!(
531                    "{}\nfuel converter temperature has not been properly set",
532                    format_dbg!()
533                )
534            })?
535            .copied();
536        let pwr_thrml_cab_to_res: si::Power = match self.res() {
537            Some(res) => match &res.thrml {
538                RESThermalOption::RESLumpedThermal(rlt) => {
539                    *rlt.state.pwr_thrml_from_cabin.get_stale(|| format_dbg!())?
540                }
541                RESThermalOption::None => si::Power::ZERO,
542            },
543            None => si::Power::ZERO,
544        };
545
546        let (pwr_thrml_fc_to_cabin, pwr_thrml_hvac_to_res, te_cab) = self
547            .solve_hvac_cab_res(te_amb_air, dt, te_fc, pwr_thrml_cab_to_res)
548            .with_context(|| format_dbg!())?;
549
550        self.pt_type
551            .solve_thermal(
552                te_amb_air,
553                pwr_thrml_fc_to_cabin,
554                &mut self.state,
555                pwr_thrml_hvac_to_res,
556                te_cab,
557                dt,
558            )
559            .with_context(|| format_dbg!())?;
560        Ok(())
561    }
562
563    fn solve_hvac_cab_res(
564        &mut self,
565        te_amb_air: si::Temperature,
566        dt: si::Time,
567        te_fc: Option<si::Temperature>,
568        pwr_thrml_cab_to_res: si::Power,
569    ) -> anyhow::Result<(
570        Option<si::Power>,
571        Option<si::Power>,
572        Option<si::Temperature>,
573    )> {
574        let res_thrml_state = self.pt_type.res_mut().and_then(|rm| rm.res_thrml_state());
575        let (pwr_thrml_fc_to_cabin, pwr_thrml_hvac_to_res, te_cab): (
576            Option<si::Power>,
577            Option<si::Power>,
578            Option<si::Temperature>,
579        ) = match (&mut self.cabin, &mut self.hvac, res_thrml_state) {
580            (CabinOption::None, HVACOption::None, None) => {
581                self.state
582                    .pwr_aux
583                    .update(self.pwr_aux_base, || format_dbg!())?;
584                (None, None, None)
585            }
586            (CabinOption::LumpedCabin(cab), HVACOption::LumpedCabin(hvac), None) => {
587                let (pwr_thrml_hvac_to_cabin, pwr_thrml_fc_to_cab) = hvac
588                    .solve(te_amb_air, te_fc, &cab.state, cab.heat_capacitance, dt)
589                    .with_context(|| format_dbg!())?;
590                let te_cab = cab
591                    .solve(
592                        te_amb_air,
593                        &self.state,
594                        pwr_thrml_hvac_to_cabin,
595                        Default::default(),
596                        dt,
597                    )
598                    .with_context(|| format_dbg!())?;
599                self.state.pwr_aux.update(
600                    self.pwr_aux_base
601                        + *hvac
602                            .state
603                            .pwr_aux_for_hvac
604                            .get_fresh(|| format_dbg!("hvac.state.pwr_aux_for_hvac"))?,
605                    || format_dbg!(),
606                )?;
607                (Some(pwr_thrml_fc_to_cab), None, Some(te_cab))
608            }
609            (
610                CabinOption::LumpedCabin(cab),
611                HVACOption::LumpedCabinAndRES(hvac),
612                Some(res_thrml_state),
613            ) => {
614                let (pwr_thrml_hvac_to_cabin, pwr_thrml_fc_to_cab, pwr_thrml_hvac_to_res) = hvac
615                    .solve(
616                        te_amb_air,
617                        te_fc,
618                        &cab.state,
619                        cab.heat_capacitance,
620                        res_thrml_state,
621                        dt,
622                    )
623                    .with_context(|| format_dbg!())?;
624                let te_cab = cab
625                    .solve(
626                        te_amb_air,
627                        &self.state,
628                        pwr_thrml_hvac_to_cabin,
629                        pwr_thrml_cab_to_res,
630                        dt,
631                    )
632                    .with_context(|| format_dbg!())?;
633                self.state.pwr_aux.update(
634                    self.pwr_aux_base
635                        + *hvac
636                            .state
637                            .pwr_aux_for_cab_hvac
638                            .get_fresh(|| format_dbg!())?
639                        + *hvac
640                            .state
641                            .pwr_aux_for_res_hvac
642                            .get_fresh(|| format_dbg!())?,
643                    || format_dbg!(),
644                )?;
645                ensure!(
646                    *self.state.pwr_aux.get_fresh(|| format_dbg!())? > si::Power::ZERO,
647                    format!(
648                        "{}\n{}\n{}",
649                        format_dbg!(self.state.pwr_aux),
650                        format_dbg!(hvac.state.pwr_aux_for_res_hvac),
651                        format_dbg!(hvac.state.pwr_aux_for_cab_hvac)
652                    )
653                );
654                (
655                    Some(pwr_thrml_fc_to_cab),
656                    Some(pwr_thrml_hvac_to_res),
657                    Some(te_cab),
658                )
659            }
660            (CabinOption::LumpedCabin(cab), HVACOption::LumpedCabin(hvac), Some(_)) => {
661                let (pwr_thrml_hvac_to_cabin, pwr_thrml_fc_to_cab) = hvac
662                    .solve(te_amb_air, te_fc, &cab.state, cab.heat_capacitance, dt)
663                    .with_context(|| format_dbg!())?;
664                let te_cab = cab
665                    .solve(
666                        te_amb_air,
667                        &self.state,
668                        pwr_thrml_hvac_to_cabin,
669                        Default::default(),
670                        dt,
671                    )
672                    .with_context(|| format_dbg!())?;
673                self.state.pwr_aux.update(
674                    self.pwr_aux_base
675                        + *hvac
676                            .state
677                            .pwr_aux_for_hvac
678                            .get_fresh(|| format_dbg!("hvac.state.pwr_aux_for_hvac"))?,
679                    || format_dbg!(),
680                )?;
681                (Some(pwr_thrml_fc_to_cab), None, Some(te_cab))
682            }
683            (CabinOption::LumpedCabin(cab), HVACOption::None, Some(_)) => {
684                let te_cab = cab
685                    .solve(
686                        te_amb_air,
687                        &self.state,
688                        si::Power::ZERO,
689                        si::Power::ZERO,
690                        dt,
691                    )
692                    .with_context(|| format_dbg!())?;
693                self.state
694                    .pwr_aux
695                    .update(self.pwr_aux_base, || format_dbg!())?;
696                (None, None, Some(te_cab))
697            }
698            (_, _, _) => {
699                bail!(
700                    "{}\nCabin, HVAC, and RESThermal configuration is either invalid or not yet implemented.\n{} - {} - {}",
701                    format_dbg!(),
702                    format!("{}", self.hvac),
703                    format!("{}", self.cabin),
704                    format!(
705                        "`res.res_thrml_state().is_some()`: {}",
706                        self.pt_type.res().and_then(|res| res.res_thrml_state()).is_some()
707                    ),
708                );
709            }
710        };
711        Ok((pwr_thrml_fc_to_cabin, pwr_thrml_hvac_to_res, te_cab))
712    }
713
714    #[allow(dead_code)]
715    fn from_f2_file(file: PathBuf) -> anyhow::Result<Self> {
716        use fastsim_2::traits::SerdeAPI;
717        let f2veh = fastsim_2::vehicle::RustVehicle::from_file(file, false)
718            .with_context(|| format_dbg!())?;
719        Self::try_from(f2veh)
720    }
721
722    pub(crate) fn mark_non_thermal_fresh(&mut self) -> Result<(), anyhow::Error> {
723        self.state.i.mark_stale();
724        self.state.time.mark_stale();
725        self.state.pwr_aux.mark_stale();
726        self.state.mass.mark_stale();
727        self.state.mark_fresh(|| format_dbg!())?;
728        self.state.energy_tractive.mark_stale();
729        self.state.energy_aux.mark_stale();
730        self.state.energy_drag.mark_stale();
731        self.state.energy_accel.mark_stale();
732        self.state.energy_ascent.mark_stale();
733        self.state.energy_rr.mark_stale();
734        self.state.energy_whl_inertia.mark_stale();
735        self.state.energy_brake.mark_stale();
736        self.state.dist.mark_stale();
737        if let Some(fc) = self.fc_mut() {
738            fc.state.i.mark_stale();
739            fc.state.mark_fresh(|| format_dbg!())?;
740            fc.state.energy_prop.mark_stale();
741            fc.state.energy_aux.mark_stale();
742            fc.state.energy_fuel.mark_stale();
743            fc.state.energy_loss.mark_stale();
744        }
745        if let Some(res) = self.res_mut() {
746            res.state.i.mark_stale();
747            res.state.soh.mark_stale();
748            res.state.mark_fresh(|| format_dbg!())?;
749            res.state.energy_out_electrical.mark_stale();
750            res.state.energy_out_prop.mark_stale();
751            res.state.energy_aux.mark_stale();
752            res.state.energy_loss.mark_stale();
753            res.state.energy_out_chemical.mark_stale();
754        }
755
756        if let Some(em) = self.em_mut() {
757            em.state.i.mark_stale();
758            em.state.mark_fresh(|| format_dbg!())?;
759            em.state.energy_out_req.mark_stale();
760            em.state.energy_elec_prop_in.mark_stale();
761            em.state.energy_mech_prop_out.mark_stale();
762            em.state.energy_mech_dyn_brake.mark_stale();
763            em.state.energy_elec_dyn_brake.mark_stale();
764            em.state.energy_loss.mark_stale();
765        }
766        if let Some(trans) = self.trans_mut() {
767            trans.state.i.mark_stale();
768            trans.state.mark_fresh(|| format_dbg!())?;
769            trans.state.energy_out.mark_stale();
770            trans.state.energy_in.mark_stale();
771            trans.state.energy_loss.mark_stale();
772        }
773        if let PowertrainType::HybridElectricVehicle(hev) = &mut self.pt_type {
774            match &mut hev.pt_cntrl {
775                HEVPowertrainControls::RGWDB(rgwdb) => rgwdb.state.i.mark_stale(),
776            }
777            hev.pt_cntrl.mark_fresh(|| format_dbg!())?
778        }
779        Ok(())
780    }
781}
782
783/// Vehicle state for current time step
784#[serde_api]
785#[derive(
786    Clone, Debug, Deserialize, Serialize, PartialEq, HistoryVec, StateMethods, SetCumulative,
787)]
788#[non_exhaustive]
789#[serde(default)]
790#[serde(deny_unknown_fields)]
791pub struct VehicleState {
792    /// time step index
793    pub i: TrackedState<usize>,
794
795    /// elapsed simulation time since start
796    pub time: TrackedState<si::Time>,
797
798    // power and energy fields
799    /// maximum forward propulsive power vehicle can produce
800    pub pwr_prop_fwd_max: TrackedState<si::Power>,
801    /// pwr exerted on wheels by powertrain
802    /// maximum backward propulsive power (e.g. regenerative braking) vehicle can produce
803    pub pwr_prop_bwd_max: TrackedState<si::Power>,
804    /// Tractive power for achieved speed
805    pub pwr_tractive: TrackedState<si::Power>,
806    /// Tractive power required for prescribed speed
807    pub pwr_tractive_for_cyc: TrackedState<si::Power>,
808    /// integral of [Self::pwr_tractive]
809    pub energy_tractive: TrackedState<si::Energy>,
810    /// time varying aux load
811    pub pwr_aux: TrackedState<si::Power>,
812    /// integral of [Self::pwr_aux]
813    pub energy_aux: TrackedState<si::Energy>,
814    /// Power applied to aero drag
815    pub pwr_drag: TrackedState<si::Power>,
816    /// integral of [Self::pwr_drag]
817    pub energy_drag: TrackedState<si::Energy>,
818    /// Power applied to acceleration (includes deceleration)
819    pub pwr_accel: TrackedState<si::Power>,
820    /// integral of [Self::pwr_accel]
821    pub energy_accel: TrackedState<si::Energy>,
822    /// Power applied to grade ascent
823    pub pwr_ascent: TrackedState<si::Power>,
824    /// integral of [Self::pwr_ascent]
825    pub energy_ascent: TrackedState<si::Energy>,
826    /// Power applied to rolling resistance
827    pub pwr_rr: TrackedState<si::Power>,
828    /// integral of [Self::pwr_rr]
829    pub energy_rr: TrackedState<si::Energy>,
830    /// Power applied to wheel and tire inertia
831    pub pwr_whl_inertia: TrackedState<si::Power>,
832    /// integral of [Self::pwr_whl_inertia]
833    pub energy_whl_inertia: TrackedState<si::Energy>,
834    /// Total braking power including regen
835    pub pwr_brake: TrackedState<si::Power>,
836    /// integral of [Self::pwr_brake]
837    pub energy_brake: TrackedState<si::Energy>,
838    /// whether powertrain can achieve power demand to achieve prescribed speed
839    /// in current time step
840    // because it should be assumed true in the first time step
841    pub cyc_met: TrackedState<bool>,
842    /// whether powertrain can achieve power demand to achieve prescribed speed
843    /// in entire cycle
844    pub cyc_met_overall: TrackedState<bool>,
845    /// actual achieved speed
846    pub speed_ach: TrackedState<si::Velocity>,
847    /// cumulative distance traveled, integral of [Self::speed_ach]
848    pub dist: TrackedState<si::Length>,
849    /// current grade
850    pub grade_curr: TrackedState<si::Ratio>,
851    /// current grade
852    // will be overridden during simulation anyway
853    pub elev_curr: TrackedState<si::Length>,
854    /// current air density
855    pub air_density: TrackedState<si::MassDensity>,
856    /// current mass
857    // TODO: make sure this gets updated appropriately
858    pub mass: TrackedState<si::Mass>,
859}
860
861impl SerdeAPI for VehicleState {}
862impl Init for VehicleState {}
863impl Default for VehicleState {
864    fn default() -> Self {
865        Self {
866            i: TrackedState::new(Default::default()),
867            time: Default::default(),
868            pwr_prop_fwd_max: Default::default(),
869            pwr_prop_bwd_max: Default::default(),
870            pwr_tractive: Default::default(),
871            pwr_tractive_for_cyc: Default::default(),
872            energy_tractive: Default::default(),
873            pwr_aux: Default::default(),
874            energy_aux: Default::default(),
875            pwr_drag: Default::default(),
876            energy_drag: Default::default(),
877            pwr_accel: Default::default(),
878            energy_accel: Default::default(),
879            pwr_ascent: Default::default(),
880            energy_ascent: Default::default(),
881            pwr_rr: Default::default(),
882            energy_rr: Default::default(),
883            pwr_whl_inertia: Default::default(),
884            energy_whl_inertia: Default::default(),
885            pwr_brake: Default::default(),
886            energy_brake: Default::default(),
887            cyc_met: TrackedState::new(true),
888            cyc_met_overall: TrackedState::new(true),
889            speed_ach: Default::default(),
890            dist: Default::default(),
891            // note that this value will be overwritten
892            grade_curr: Default::default(),
893            // note that this value will be overwritten
894            elev_curr: Default::default(),
895            air_density: Default::default(),
896            mass: TrackedState::new(uc::KG * f64::NAN),
897        }
898    }
899}
900
901#[cfg(test)]
902pub(crate) mod tests {
903    use super::*;
904
905    #[allow(dead_code)]
906    fn vehicles_dir() -> PathBuf {
907        PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("resources/vehicles")
908    }
909
910    #[cfg(feature = "yaml")]
911    /// Load representative conv from fastsim-2, convert to fastsim-3 format, and
912    /// save to file in the resources folder
913    pub(crate) fn mock_conv_veh() -> Vehicle {
914        let file_contents = include_str!("fastsim-2_2012_Ford_Fusion.yaml");
915        use fastsim_2::traits::SerdeAPI;
916        let veh = {
917            let f2veh = fastsim_2::vehicle::RustVehicle::from_yaml(file_contents, false).unwrap();
918            let veh = Vehicle::try_from(f2veh);
919            veh.unwrap()
920        };
921
922        veh.to_file(vehicles_dir().join("2012_Ford_Fusion.yaml"))
923            .unwrap();
924        assert!(veh.pt_type.is_conventional_vehicle());
925        veh
926    }
927
928    #[cfg(feature = "yaml")]
929    /// Load representative HEV from fastsim-2, convert to fastsim-3 format, and
930    /// save to file in the resources folder
931    pub(crate) fn mock_hev() -> Vehicle {
932        let file_contents = include_str!("fastsim-2_2016_TOYOTA_Prius_Two.yaml");
933        use fastsim_2::traits::SerdeAPI;
934        let veh = {
935            let f2veh = fastsim_2::vehicle::RustVehicle::from_yaml(file_contents, false).unwrap();
936            let veh = Vehicle::try_from(f2veh);
937            veh.unwrap()
938        };
939
940        veh.to_file(vehicles_dir().join("2016_TOYOTA_Prius_Two.yaml"))
941            .unwrap();
942        assert!(veh.pt_type.is_hybrid_electric_vehicle());
943        veh
944    }
945
946    #[cfg(feature = "yaml")]
947    /// Load representative BEV from fastsim-2, convert to fastsim-3 format, and
948    /// save to file in the resources folder
949    pub(crate) fn mock_bev() -> Vehicle {
950        let file_contents = include_str!("fastsim-2_2022_Renault_Zoe_ZE50_R135.yaml");
951        use fastsim_2::traits::SerdeAPI;
952        let veh = {
953            let f2veh = fastsim_2::vehicle::RustVehicle::from_yaml(file_contents, false).unwrap();
954            let veh = Vehicle::try_from(f2veh);
955            veh.unwrap()
956        };
957
958        veh.to_file(vehicles_dir().join("2022_Renault_Zoe_ZE50_R135.yaml"))
959            .unwrap();
960        assert!(veh.pt_type.is_battery_electric_vehicle());
961        veh
962    }
963
964    #[test]
965    #[cfg(feature = "yaml")]
966    pub(crate) fn test_conv_veh_init() {
967        use pretty_assertions::assert_eq;
968        let veh = mock_conv_veh();
969        let mut veh1 = veh.clone();
970        // NOTE: eventually figure out why the following assertions fail if
971        // `.to_yaml().uwrap()` is removed.  It's probably related to f64::NAN
972        assert_eq!(veh.to_yaml().unwrap(), veh1.to_yaml().unwrap());
973        veh1.init().unwrap();
974        assert_eq!(veh.to_yaml().unwrap(), veh1.to_yaml().unwrap());
975    }
976
977    #[test]
978    #[cfg(all(feature = "csv", feature = "resources"))]
979    fn test_to_fastsim2_conv() {
980        let veh = mock_conv_veh();
981        let cyc = crate::drive_cycle::Cycle::from_resource("udds.csv", false).unwrap();
982        let sd = crate::simdrive::SimDrive::new(veh, cyc, Default::default());
983        let mut sd2 = sd.to_fastsim2().unwrap();
984        sd2.sim_drive(None, None).unwrap();
985    }
986
987    #[test]
988    #[cfg(all(feature = "csv", feature = "resources"))]
989    fn test_to_fastsim2_hev() {
990        let veh = mock_hev();
991        let cyc = crate::drive_cycle::Cycle::from_resource("udds.csv", false).unwrap();
992        let sd = crate::simdrive::SimDrive::new(veh, cyc, Default::default());
993        let mut sd2 = sd.to_fastsim2().unwrap();
994        sd2.sim_drive(None, None).unwrap();
995    }
996
997    #[test]
998    #[cfg(all(feature = "csv", feature = "resources"))]
999    fn test_to_fastsim2_bev() {
1000        let veh = mock_bev();
1001        let cyc = crate::drive_cycle::Cycle::from_resource("udds.csv", false).unwrap();
1002        let sd = crate::simdrive::SimDrive::new(veh, cyc, Default::default());
1003        let mut sd2 = sd.to_fastsim2().unwrap();
1004        sd2.sim_drive(None, None).unwrap();
1005    }
1006
1007    type StructWithResources = Vehicle;
1008
1009    #[test]
1010    fn test_resources() {
1011        let mut time_to_panic = false;
1012
1013        let resource_list = StructWithResources::list_resources().unwrap();
1014        assert!(!resource_list.is_empty());
1015
1016        // verify that resources can all load
1017        for resource in resource_list {
1018            if let Err(e) = StructWithResources::from_resource(resource.clone(), false) {
1019                time_to_panic = true;
1020                eprintln!("Error loading {resource:?}: {e}\n");
1021            }
1022        }
1023        if time_to_panic {
1024            panic!()
1025        }
1026    }
1027
1028    #[test]
1029    fn test_calibrated_vehicles() {
1030        let mut time_to_panic = false;
1031
1032        // check that calibrated vehicles can load
1033        let paths: Vec<_> = std::fs::read_dir("../cal_and_val/f3-vehicles")
1034            .unwrap()
1035            .collect();
1036        assert!(!paths.is_empty());
1037        for path in paths {
1038            let p = path.unwrap().path();
1039            if let Err(e) = StructWithResources::from_file(p.clone(), false) {
1040                time_to_panic = true;
1041                eprintln!("Error loading {p:?}: {e}\n");
1042            }
1043        }
1044
1045        // check that calibrated thermal-equipped vehicles can load
1046        let paths: Vec<_> = std::fs::read_dir("../cal_and_val/thermal/f3-vehicles")
1047            .unwrap()
1048            .collect();
1049        assert!(!paths.is_empty());
1050        for path in paths {
1051            let p = path.unwrap().path();
1052            if let Err(e) = StructWithResources::from_file(p.clone(), false) {
1053                time_to_panic = true;
1054                eprintln!("Error loading {p:?}: {e}\n");
1055            }
1056        }
1057
1058        if time_to_panic {
1059            panic!()
1060        }
1061    }
1062}