fastsim_core/vehicle/
vehicle_model.rs

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