fastsim_core/vehicle/
vehicle_model.rs

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