fastsim_core/vehicle/
vehicle_model.rs

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