fastsim_core/
simdrivelabel.rs

1//! Module containing classes and methods for calculating label fuel economy.
2
3use std::collections::HashMap;
4
5// crate local
6use crate::drive_cycle::{Cycle, CYC_ACCEL};
7use crate::imports::*;
8use crate::simdrive::SimDrive;
9use crate::vehicle::{PowertrainType, Vehicle};
10
11/// Return first index of `arr` greater than `cut`
12fn first_grtr(arr: &[f64], cut: f64) -> Option<usize> {
13    let len = arr.len();
14    if len == 0 {
15        return None;
16    }
17    Some(arr.iter().position(|&x| x > cut).unwrap_or(len - 1)) // unwrap_or allows for default if not found
18}
19
20/// Returns time [s] for 0-60 mph acceleration at max power
21pub fn get_0_to_60_time(sd_accel: &mut SimDrive) -> anyhow::Result<f64> {
22    sd_accel.sim_params.trace_miss_opts = TraceMissOptions::Allow;
23    sd_accel.walk().with_context(|| format_dbg!())?;
24
25    // Extract speed values in mph
26    let mut speed_mph: Vec<f64> = vec![];
27    for s in sd_accel.veh.history.speed_ach.clone() {
28        speed_mph.push(s.get_fresh(|| format_dbg!())?.get::<si::mile_per_hour>())
29    }
30
31    // Extract time values in seconds
32    let time_s: Vec<f64> = sd_accel
33        .cyc
34        .time
35        .iter()
36        .map(|t| t.get::<si::second>())
37        .collect();
38
39    // Check if vehicle reaches 60 mph
40    if speed_mph.iter().any(|&x| x >= 60.0) {
41        // Create interpolator from speed to time
42        let interp: InterpolatorEnumOwned<f64> = InterpolatorEnum::new_1d(
43            speed_mph.clone().into(),
44            time_s.clone().into(),
45            strategy::Linear,
46            Extrapolate::Clamp,
47        )
48        .with_context(|| format_dbg!())?;
49
50        // Interpolate time at 60 mph
51        let accel_time = interp.interpolate(&[60.0])?;
52        Ok(accel_time)
53    } else {
54        // Vehicle doesn't reach 60 mph
55        println!(
56            "Warning: Vehicle '{}' doesn't reach 60 mph in the acceleration test",
57            sd_accel.veh.name
58        );
59        Ok(f64::NAN)
60    }
61}
62
63// const MPH_PER_MPS: f64 = 2.2369362921;
64const DEFAULT_CHG_EFF: f64 = 0.86;
65
66#[serde_api]
67#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
68#[cfg_attr(feature = "pyo3", pyclass(module = "fastsim", subclass, eq))]
69pub struct FuelProperties {
70    // TODO: make a way to serialize/deserialize with "J/m^3"
71    // fuel energy density
72    /// fuel energy density (i.e. energy per unit mass, which has the same base
73    /// units as pressure)
74    pub energy_density: si::Pressure,
75    /// fuel density
76    pub density: si::MassDensity,
77}
78
79impl Init for FuelProperties {}
80impl SerdeAPI for FuelProperties {}
81
82#[pyo3_api]
83impl FuelProperties {}
84
85impl Default for FuelProperties {
86    /// Default values for gasoline
87    fn default() -> Self {
88        Self {
89            energy_density: 33.7 * uc::KWH / uc::GALLON,
90            density: 0.75 * uc::KG / uc::L,
91        }
92    }
93}
94
95const J_PER_KWH: f64 = 3_600.0;
96lazy_static! {
97    static ref CUBIC_METER_PER_GAL: f64 = 3.79e-3;
98}
99
100impl FuelProperties {
101    fn kwh_per_gge(&self) -> f64 {
102        self.energy_density.get::<si::joule_per_cubic_meter>() / J_PER_KWH * *CUBIC_METER_PER_GAL
103    }
104}
105
106trait VehicleEfficiency {
107    fn mpg(&self, energy_density: si::Pressure) -> anyhow::Result<f64>;
108
109    fn kwh_per_mi(&self) -> anyhow::Result<f64>;
110}
111
112impl VehicleEfficiency for Vehicle {
113    fn mpg(&self, energy_density: si::Pressure) -> anyhow::Result<f64> {
114        if let Some(fc) = self.fc() {
115            Ok(self
116                .state
117                .dist
118                .get_fresh(|| format_dbg!())?
119                .get::<si::mile>()
120                / (*fc.state.energy_fuel.get_fresh(|| format_dbg!())? / energy_density)
121                    .get::<si::gallon>())
122        } else {
123            Ok(f64::NAN)
124        }
125    }
126
127    fn kwh_per_mi(&self) -> anyhow::Result<f64> {
128        if let Some(res) = self.res() {
129            Ok(res
130                .state
131                .energy_out_chemical
132                .get_fresh(|| format_dbg!())?
133                .get::<si::kilowatt_hour>()
134                / self
135                    .state
136                    .dist
137                    .get_fresh(|| format_dbg!())?
138                    .get::<si::mile>())
139        } else {
140            Ok(f64::NAN)
141        }
142    }
143}
144
145#[serde_api]
146#[derive(Clone, Default, Debug, Deserialize, Serialize, PartialEq)]
147#[cfg_attr(feature = "pyo3", pyclass(module = "fastsim", subclass, eq))]
148pub struct LabelFe {
149    pub veh: Option<Vehicle>,
150    pub adj_params: AdjCoef,
151    pub lab_udds_mpgge: f64,
152    pub lab_hwy_mpgge: f64,
153    pub lab_comb_mpgge: f64,
154    pub lab_udds_kwh_per_mi: f64,
155    pub lab_hwy_kwh_per_mi: f64,
156    pub lab_comb_kwh_per_mi: f64,
157    pub adj_udds_mpgge: f64,
158    pub adj_hwy_mpgge: f64,
159    pub adj_comb_mpgge: f64,
160    pub adj_udds_kwh_per_mi: f64,
161    pub adj_hwy_kwh_per_mi: f64,
162    pub adj_comb_kwh_per_mi: f64,
163    pub adj_udds_ess_kwh_per_mi: f64,
164    pub adj_hwy_ess_kwh_per_mi: f64,
165    pub adj_comb_ess_kwh_per_mi: f64,
166    pub net_range_miles: f64,
167    pub uf: f64,
168    pub net_accel: f64,
169    pub res_found: String,
170    pub phev_calcs: Option<LabelFePHEV>,
171    pub adj_cs_comb_mpgge: Option<f64>,
172    pub adj_cd_comb_mpgge: Option<f64>,
173    pub net_phev_cd_miles: Option<f64>,
174}
175
176#[pyo3_api]
177impl LabelFe {}
178
179impl Init for LabelFe {}
180impl SerdeAPI for LabelFe {}
181
182#[serde_api]
183#[derive(Default, Clone, Debug, Deserialize, Serialize, PartialEq)]
184#[cfg_attr(feature = "pyo3", pyclass(module = "fastsim", subclass, eq))]
185/// Label fuel economy values for a PHEV vehicle
186pub struct LabelFePHEV {
187    pub regen_soc_buffer: si::Ratio,
188    pub udds: PHEVCycleCalc,
189    pub hwy: PHEVCycleCalc,
190}
191
192#[pyo3_api]
193impl LabelFePHEV {}
194
195impl Init for LabelFePHEV {}
196impl SerdeAPI for LabelFePHEV {}
197
198#[serde_api]
199#[derive(Default, Clone, Debug, Deserialize, Serialize, PartialEq)]
200#[cfg_attr(feature = "pyo3", pyclass(module = "fastsim", subclass, eq))]
201/// Label fuel economy calculations for a specific cycle of a PHEV vehicle
202pub struct PHEVCycleCalc {
203    /// Charge depletion battery kW-hr
204    pub cd_ess_kwh: f64,
205    pub cd_ess_kwh_per_mi: f64,
206    /// Charge depletion fuel gallons
207    pub cd_fs_gal: f64,
208    pub cd_fs_kwh: f64,
209    pub cd_mpg: f64,
210    /// Number of cycles in charge depletion mode, up to transition
211    pub cd_cycs: f64,
212    pub cd_miles: f64,
213    pub cd_lab_mpg: f64,
214    pub cd_adj_mpg: f64,
215    /// Fraction of transition cycles spent in charge depletion
216    pub cd_frac_in_trans: f64,
217    /// SOC change during 1 cycle
218    pub trans_init_soc: si::Ratio,
219    /// charge depletion battery kW-hr
220    pub trans_ess_kwh: f64,
221    pub trans_ess_kwh_per_mi: f64,
222    pub trans_fs_gal: f64,
223    pub trans_fs_kwh: f64,
224    /// charge sustaining battery kW-hr
225    pub cs_ess_kwh: f64,
226    pub cs_ess_kwh_per_mi: f64,
227    /// charge sustaining fuel gallons
228    pub cs_fs_gal: f64,
229    pub cs_fs_kwh: f64,
230    pub cs_mpg: f64,
231    pub lab_mpgge: f64,
232    pub lab_kwh_per_mi: f64,
233    pub lab_uf: f64,
234    pub lab_uf_gpm: Vec<f64>,
235    pub lab_iter_uf: Vec<f64>,
236    pub lab_iter_uf_kwh_per_mi: Vec<f64>,
237    pub lab_iter_kwh_per_mi: Vec<f64>,
238    pub adj_iter_mpgge: Vec<f64>,
239    pub adj_iter_kwh_per_mi: Vec<f64>,
240    pub adj_iter_cd_miles: Vec<f64>,
241    pub adj_iter_uf: Vec<f64>,
242    pub adj_iter_uf_gpm: Vec<f64>,
243    pub adj_iter_uf_kwh_per_mi: Vec<f64>,
244    pub adj_cd_miles: f64,
245    pub adj_cd_mpgge: f64,
246    pub adj_cs_mpgge: f64,
247    pub adj_uf: f64,
248    pub adj_mpgge: f64,
249    pub adj_kwh_per_mi: f64,
250    pub adj_ess_kwh_per_mi: f64,
251    pub delta_soc: si::Ratio,
252    /// Total number of miles in charge depletion mode, assuming constant kWh_per_mi
253    pub total_cd_miles: f64,
254}
255
256impl Init for PHEVCycleCalc {}
257impl SerdeAPI for PHEVCycleCalc {}
258
259#[pyo3_api]
260impl PHEVCycleCalc {}
261
262#[derive(Clone, Serialize, Deserialize, Debug, PartialEq)]
263#[cfg_attr(feature = "pyo3", pyclass(module = "fastsim", subclass, eq))]
264pub struct AdjCoef {
265    pub city_intercept: f64,
266    pub city_slope: f64,
267    pub hwy_intercept: f64,
268    pub hwy_slope: f64,
269}
270
271#[pyo3_api]
272impl AdjCoef {}
273
274impl Init for AdjCoef {}
275impl SerdeAPI for AdjCoef {}
276
277impl Default for AdjCoef {
278    fn default() -> Self {
279        Self {
280            city_intercept: 0.003259,
281            city_slope: 1.1805,
282            hwy_intercept: 0.001376,
283            hwy_slope: 1.3466,
284        }
285    }
286}
287
288#[serde_api]
289#[derive(Clone, Serialize, Deserialize, Debug, PartialEq)]
290#[cfg_attr(feature = "pyo3", pyclass(module = "fastsim", subclass, eq))]
291pub struct PhevUtilizationParams {
292    pub adj_coef_map: HashMap<String, AdjCoef>,
293    /// Frequency of recharge events
294    pub rechg_freq_miles: Vec<f64>,
295    /// Array of utility factor
296    pub uf_array: Vec<f64>,
297}
298
299impl Init for PhevUtilizationParams {}
300impl SerdeAPI for PhevUtilizationParams {}
301
302impl Default for PhevUtilizationParams {
303    fn default() -> Self {
304        Self::from_json(&*PHEV_UTIL_PARAMS, false).unwrap()
305    }
306}
307
308lazy_static! {
309    static ref PHEV_UTIL_PARAMS: String =
310        include_str!("./simdrivelabel/longparams.json").to_string();
311}
312
313/// Generates label fuel economy (FE) values for a provided vehicle.
314///
315/// # Arguments
316///
317/// - `veh`: vehicle::Vehicle
318/// - `full_detail`: boolean, default False
319///   If True, sim_drive objects for each cycle are also returned.
320/// - `verbose`: boolean, default false
321///   If true, print out key results
322///
323/// Returns label fuel economy values as a struct and (optionally)
324/// simdrive::SimDrive objects.
325pub fn get_label_fe(
326    veh: &mut Vehicle,
327    max_epa_adj: Option<f64>,
328    full_detail: bool,
329    fuel_props: Option<FuelProperties>,
330    phev_utilization_params: Option<PhevUtilizationParams>,
331    verbose: bool,
332) -> anyhow::Result<(LabelFe, Option<HashMap<&str, SimDrive>>)> {
333    let max_epa_adj = max_epa_adj.unwrap_or(0.3);
334    let phev_utilization_params = &phev_utilization_params.unwrap_or_default();
335    let fuel_props = fuel_props.unwrap_or_default();
336
337    let mut cyc: HashMap<&str, Cycle> = HashMap::new();
338    let mut sd = HashMap::new();
339    let mut label_fe = LabelFe::default();
340
341    label_fe.veh = Some(veh.clone());
342
343    // load the cycles and instantiate simdrive objects
344    cyc.insert("accel", CYC_ACCEL.clone());
345    cyc.insert("udds", Cycle::from_resource("udds.csv", false)?);
346    cyc.insert("hwy", Cycle::from_resource("hwfet.csv", false)?);
347
348    if veh.pt_type.is_plug_in_hybrid_electric_vehicle() {
349        let rm = veh.res_mut().unwrap();
350        rm.state.soc.check_and_reset(|| format_dbg!()).unwrap();
351        rm.state.soc.update(rm.max_soc, || format_dbg!()).unwrap();
352    }
353
354    // run simdrive for non-phev powertrains
355    sd.insert(
356        "udds",
357        SimDrive::new(veh.clone(), cyc["udds"].clone(), None),
358    );
359    sd.insert("hwy", SimDrive::new(veh.clone(), cyc["hwy"].clone(), None));
360
361    for (k, val) in sd.iter_mut() {
362        val.walk().with_context(|| format_dbg!(k))?;
363    }
364
365    // find year-based adjustment parameters
366    let adj_params = if veh.year < 2017 {
367        &phev_utilization_params.adj_coef_map["2008"]
368    } else {
369        // assume 2017 coefficients are valid
370        &phev_utilization_params.adj_coef_map["2017"]
371    };
372    label_fe.adj_params = adj_params.clone();
373
374    // Check powertrain type
375    let is_phev = matches!(veh.pt_type, PowertrainType::PlugInHybridElectricVehicle(_));
376    let is_bev = matches!(veh.pt_type, PowertrainType::BatteryElectricVehicle(_));
377
378    // run calculations for non-PHEV powertrains
379    if !is_phev {
380        if !is_bev {
381            // compare to Excel 'VehicleIO'!C203 or 'VehicleIO'!labUddsMpgge
382            label_fe.lab_udds_mpgge = sd["udds"].veh.mpg(fuel_props.energy_density)?;
383            // compare to Excel 'VehicleIO'!C203 or 'VehicleIO'!labHwyMpgge
384            label_fe.lab_hwy_mpgge = sd["hwy"].veh.mpg(fuel_props.energy_density)?;
385            label_fe.lab_comb_mpgge = 1.
386                / (0.55 / sd["udds"].veh.mpg(fuel_props.energy_density)?
387                    + 0.45 / sd["hwy"].veh.mpg(fuel_props.energy_density)?);
388        } else {
389            label_fe.lab_udds_mpgge = 0.;
390            label_fe.lab_hwy_mpgge = 0.;
391            label_fe.lab_comb_mpgge = 0.;
392        }
393
394        if is_bev {
395            label_fe.lab_udds_kwh_per_mi = sd["udds"].veh.kwh_per_mi()?;
396            label_fe.lab_hwy_kwh_per_mi = sd["hwy"].veh.kwh_per_mi()?;
397            label_fe.lab_comb_kwh_per_mi =
398                0.55 * sd["udds"].veh.kwh_per_mi()? + 0.45 * sd["hwy"].veh.kwh_per_mi()?;
399        } else {
400            label_fe.lab_udds_kwh_per_mi = 0.;
401            label_fe.lab_hwy_kwh_per_mi = 0.;
402            label_fe.lab_comb_kwh_per_mi = 0.;
403        }
404
405        // adjusted values for mpg
406        if !is_bev {
407            // non-EV case
408            // CV or HEV case (not PHEV)
409            // HEV SOC iteration is handled in simdrive.SimDriveClassic
410            label_fe.adj_udds_mpgge = 1.
411                / (adj_params.city_intercept
412                    + adj_params.city_slope / sd["udds"].veh.mpg(fuel_props.energy_density)?);
413            // compare to Excel 'VehicleIO'!C203 or 'VehicleIO'!adjHwyMpgge
414            label_fe.adj_hwy_mpgge = 1.
415                / (adj_params.hwy_intercept
416                    + adj_params.hwy_slope / sd["hwy"].veh.mpg(fuel_props.energy_density)?);
417            label_fe.adj_comb_mpgge =
418                1. / (0.55 / label_fe.adj_udds_mpgge + 0.45 / label_fe.adj_hwy_mpgge);
419        } else {
420            // EV case
421            // Mpgge is all zero for EV
422            label_fe.adj_udds_mpgge = 0.;
423            label_fe.adj_hwy_mpgge = 0.;
424            label_fe.adj_comb_mpgge = 0.;
425        }
426
427        // adjusted kW-hr/mi
428        if is_bev {
429            // EV Case
430            label_fe.adj_udds_kwh_per_mi =
431                (1. / f64::max(
432                    1. / (adj_params.city_intercept
433                        + (adj_params.city_slope
434                            / ((1. / label_fe.lab_udds_kwh_per_mi) * fuel_props.kwh_per_gge()))),
435                    (1. / label_fe.lab_udds_kwh_per_mi)
436                        * fuel_props.kwh_per_gge()
437                        * (1. - max_epa_adj),
438                )) * fuel_props.kwh_per_gge()
439                    / DEFAULT_CHG_EFF;
440            label_fe.adj_hwy_kwh_per_mi =
441                (1. / f64::max(
442                    1. / (adj_params.hwy_intercept
443                        + (adj_params.hwy_slope
444                            / ((1. / label_fe.lab_hwy_kwh_per_mi) * fuel_props.kwh_per_gge()))),
445                    (1. / label_fe.lab_hwy_kwh_per_mi)
446                        * fuel_props.kwh_per_gge()
447                        * (1. - max_epa_adj),
448                )) * fuel_props.kwh_per_gge()
449                    / DEFAULT_CHG_EFF;
450            label_fe.adj_comb_kwh_per_mi =
451                0.55 * label_fe.adj_udds_kwh_per_mi + 0.45 * label_fe.adj_hwy_kwh_per_mi;
452
453            label_fe.adj_udds_ess_kwh_per_mi = label_fe.adj_udds_kwh_per_mi * DEFAULT_CHG_EFF;
454            label_fe.adj_hwy_ess_kwh_per_mi = label_fe.adj_hwy_kwh_per_mi * DEFAULT_CHG_EFF;
455            label_fe.adj_comb_ess_kwh_per_mi = label_fe.adj_comb_kwh_per_mi * DEFAULT_CHG_EFF;
456
457            // range for combined city/highway
458            // Get energy capacity from the proper powertrain
459            if let PowertrainType::BatteryElectricVehicle(bev) = &veh.pt_type {
460                label_fe.net_range_miles = bev.res.energy_capacity.get::<si::kilowatt_hour>()
461                    / label_fe.adj_comb_ess_kwh_per_mi;
462            }
463        }
464
465        // utility factor (percent driving in PHEV charge depletion mode)
466        label_fe.uf = 0.;
467    } else {
468        // PHEV
469        let phev_calcs = get_label_fe_phev(
470            veh,
471            phev_utilization_params,
472            adj_params,
473            max_epa_adj,
474            &fuel_props,
475        )?;
476        label_fe.phev_calcs = Some(phev_calcs.clone());
477
478        // efficiency-related calculations
479        // lab
480        label_fe.lab_udds_mpgge = phev_calcs.udds.lab_mpgge;
481        label_fe.lab_hwy_mpgge = phev_calcs.hwy.lab_mpgge;
482        label_fe.lab_comb_mpgge =
483            1.0 / (0.55 / phev_calcs.udds.lab_mpgge + 0.45 / phev_calcs.hwy.lab_mpgge);
484
485        label_fe.lab_udds_kwh_per_mi = phev_calcs.udds.lab_kwh_per_mi;
486        label_fe.lab_hwy_kwh_per_mi = phev_calcs.hwy.lab_kwh_per_mi;
487        label_fe.lab_comb_kwh_per_mi =
488            0.55 * phev_calcs.udds.lab_kwh_per_mi + 0.45 * phev_calcs.hwy.lab_kwh_per_mi;
489
490        // adjusted
491        label_fe.adj_udds_mpgge = phev_calcs.udds.adj_mpgge;
492        label_fe.adj_hwy_mpgge = phev_calcs.hwy.adj_mpgge;
493        label_fe.adj_comb_mpgge =
494            1.0 / (0.55 / phev_calcs.udds.adj_mpgge + 0.45 / phev_calcs.hwy.adj_mpgge);
495
496        label_fe.adj_cs_comb_mpgge =
497            Some(1.0 / (0.55 / phev_calcs.udds.adj_cs_mpgge + 0.45 / phev_calcs.hwy.adj_cs_mpgge));
498        label_fe.adj_cd_comb_mpgge =
499            Some(1.0 / (0.55 / phev_calcs.udds.adj_cd_mpgge + 0.45 / phev_calcs.hwy.adj_cd_mpgge));
500
501        label_fe.adj_udds_kwh_per_mi = phev_calcs.udds.adj_kwh_per_mi;
502        label_fe.adj_hwy_kwh_per_mi = phev_calcs.hwy.adj_kwh_per_mi;
503        label_fe.adj_comb_kwh_per_mi =
504            0.55 * phev_calcs.udds.adj_kwh_per_mi + 0.45 * phev_calcs.hwy.adj_kwh_per_mi;
505
506        label_fe.adj_udds_ess_kwh_per_mi = phev_calcs.udds.adj_ess_kwh_per_mi;
507        label_fe.adj_hwy_ess_kwh_per_mi = phev_calcs.hwy.adj_ess_kwh_per_mi;
508        label_fe.adj_comb_ess_kwh_per_mi =
509            0.55 * phev_calcs.udds.adj_ess_kwh_per_mi + 0.45 * phev_calcs.hwy.adj_ess_kwh_per_mi;
510
511        // range for combined city/highway
512        // utility factor (percent driving in charge depletion mode)
513        label_fe.uf = phev_utilization_params.uf_array[first_grtr(
514            &phev_utilization_params.rechg_freq_miles,
515            0.55 * phev_calcs.udds.adj_cd_miles + 0.45 * phev_calcs.hwy.adj_cd_miles,
516        )
517        .with_context(|| format_dbg!())?
518            - 1];
519
520        label_fe.net_phev_cd_miles =
521            Some(0.55 * phev_calcs.udds.adj_cd_miles + 0.45 * phev_calcs.hwy.adj_cd_miles);
522
523        // For PHEVs, calculate net range as the sum of CD range and CS range
524        if let PowertrainType::PlugInHybridElectricVehicle(phev) = &veh.pt_type {
525            // Get CS range by determining how much fuel energy remains after depleting the battery
526            let fuel_energy_kwh = phev.fs.energy_capacity.get::<si::kilowatt_hour>();
527            let fuel_energy_gge = fuel_energy_kwh / fuel_props.kwh_per_gge();
528
529            label_fe.net_range_miles = (fuel_energy_gge
530                - label_fe.net_phev_cd_miles.with_context(|| format_dbg!())?
531                    / label_fe.adj_cd_comb_mpgge.with_context(|| format_dbg!())?)
532                * label_fe.adj_cs_comb_mpgge.with_context(|| format_dbg!())?
533                + label_fe.net_phev_cd_miles.with_context(|| format_dbg!())?;
534        }
535    }
536
537    // run accelerating sim_drive
538    let mut sd_accel = SimDrive::new(veh.clone(), cyc["accel"].clone(), None);
539    label_fe.net_accel = get_0_to_60_time(&mut sd_accel)
540        .with_context(|| format!("`get_0_to_60_time`: {}", format_dbg!()))?;
541    sd.insert("accel", sd_accel);
542
543    // success Boolean -- did all of the tests work(e.g. met trace within ~2 mph)?
544    label_fe.res_found = String::from("model needs to be implemented for this");
545
546    if full_detail && verbose {
547        println!("{label_fe:#?}");
548        Ok((label_fe, Some(sd)))
549    } else if full_detail {
550        Ok((label_fe, Some(sd)))
551    } else if verbose {
552        println!("{label_fe:#?}");
553        Ok((label_fe, None))
554    } else {
555        Ok((label_fe, None))
556    }
557}
558
559#[cfg(feature = "pyo3")]
560#[pyfunction(name = "get_label_fe")]
561#[cfg_attr(
562    feature = "pyo3",
563    pyo3(signature = (
564        veh, max_epa_adj=None, full_detail=None, fuel_props=None, phev_utilization_params=None, verbose=None))
565)]
566/// pyo3 version of [get_label_fe]
567pub fn get_label_fe_py(
568    veh: &mut Vehicle,
569    max_epa_adj: Option<f64>,
570    full_detail: Option<bool>,
571    fuel_props: Option<FuelProperties>,
572    phev_utilization_params: Option<PhevUtilizationParams>,
573    verbose: Option<bool>,
574) -> anyhow::Result<LabelFe> {
575    let (label_fe, _) = get_label_fe(
576        veh,
577        max_epa_adj,
578        full_detail.unwrap_or_default(),
579        fuel_props,
580        phev_utilization_params,
581        verbose.unwrap_or_default(),
582    )?;
583    Ok(label_fe)
584}
585
586/// PHEV-specific function for label fe.
587///
588/// # Arguments
589/// - max_epa_adj: maximum EPA adjustment factor
590///
591/// # Returns
592/// label fuel economy values for PHEV as a struct.
593pub fn get_label_fe_phev(
594    veh: &Vehicle,
595    phev_utilization_params: &PhevUtilizationParams,
596    adj_params: &AdjCoef,
597    max_epa_adj: f64,
598    fuel_props: &FuelProperties,
599) -> anyhow::Result<LabelFePHEV> {
600    // Get access to the PHEV powertrain
601    let max_soc: si::Ratio;
602    let min_soc: si::Ratio;
603    let phev_max_regen: si::Ratio;
604    let veh_mass: si::Mass;
605    // equivalent to fastsim-2 `mc_peak_eff`
606    let em_peak_eff: si::Ratio;
607    // battery total energy capacity from soc of 1.0 to 0.0
608    let energy_capacity: si::Energy;
609    let chg_eff: f64;
610
611    if let PowertrainType::PlugInHybridElectricVehicle(phev) = &veh.pt_type {
612        max_soc = phev.res.max_soc;
613        min_soc = phev.res.min_soc;
614        phev_max_regen = 0.98 * uc::R;
615        veh_mass = *veh.state.mass.get_fresh(|| format_dbg!())?;
616        em_peak_eff = *phev
617            .em
618            .eff_interp_achieved
619            .max()
620            .with_context(|| format_dbg!())?
621            * uc::R;
622        energy_capacity = phev.res.energy_capacity;
623        chg_eff = DEFAULT_CHG_EFF; // Use default charging efficiency
624    } else {
625        bail!("Vehicle is not a PHEV");
626    }
627
628    let mut label_fe_phev = LabelFePHEV {
629        regen_soc_buffer: ((0.5 * veh_mass * ((60. * uc::MPH).powi(P2::new())))
630            * phev_max_regen
631            * em_peak_eff
632            / energy_capacity)
633            .min((max_soc - min_soc) / 2.0),
634        ..Default::default()
635    };
636
637    // Create SimDrive objects for PHEV calculations
638    let mut sd: HashMap<&str, SimDrive> = HashMap::new();
639    sd.insert(
640        "udds",
641        SimDrive::new(veh.clone(), Cycle::from_resource("udds.csv", false)?, None),
642    );
643    sd.insert(
644        "hwy",
645        SimDrive::new(veh.clone(), Cycle::from_resource("hwfet.csv", false)?, None),
646    );
647
648    // charge sustaining behavior
649    for (key, sd) in sd.iter_mut() {
650        // do PHEV soc iteration
651        // This runs 1 cycle starting at max SOC then runs 1 cycle starting at min SOC.
652        // By assuming that the battery SOC depletion per mile is constant across cycles,
653        // the first cycle can be extrapolated until charge sustaining kicks in.
654        sd.walk()?;
655        let mut phev_calc = PHEVCycleCalc::default();
656
657        // charge depletion cycle has already been simulated
658        // charge depletion battery kW-hr
659        phev_calc.cd_ess_kwh = ((max_soc - min_soc) * energy_capacity).get::<si::kilowatt_hour>();
660
661        // Get SOC and distance values
662        let res = sd.veh.res().with_context(|| format_dbg!())?;
663        let soc_start = *res
664            .history
665            .soc
666            .first()
667            .with_context(|| format_dbg!())?
668            .get_fresh(|| format_dbg!())?;
669        let soc_end = *res
670            .history
671            .soc
672            .last()
673            .with_context(|| format_dbg!())?
674            .get_fresh(|| format_dbg!())?;
675        let dist_mi = sd
676            .veh
677            .state
678            .dist
679            .get_fresh(|| format_dbg!())?
680            .get::<si::mile>();
681
682        // SOC change during 1 cycle
683        phev_calc.delta_soc = soc_start - soc_end;
684        // total number of miles in charge depletion mode, assuming constant kWh_per_mi
685        phev_calc.total_cd_miles = ((max_soc - min_soc) * energy_capacity)
686            .get::<si::kilowatt_hour>()
687            / sd.veh.kwh_per_mi()?;
688        // number of cycles in charge depletion mode, up to transition
689        phev_calc.cd_cycs = phev_calc.total_cd_miles / dist_mi;
690        // fraction of transition cycle spent in charge depletion
691        phev_calc.cd_frac_in_trans = phev_calc.cd_cycs % phev_calc.cd_cycs.floor();
692
693        // charge depletion fuel gallons - get from fuel converter
694        let fuel_energy_kwh = if let Some(fc) = sd.veh.fc() {
695            fc.state
696                .energy_fuel
697                .get_fresh(|| format_dbg!())?
698                .get::<si::kilowatt_hour>()
699        } else {
700            0.0
701        };
702        phev_calc.cd_fs_gal = fuel_energy_kwh / fuel_props.kwh_per_gge();
703        phev_calc.cd_fs_kwh = fuel_energy_kwh;
704        phev_calc.cd_ess_kwh_per_mi = sd.veh.kwh_per_mi()?;
705        phev_calc.cd_mpg = sd.veh.mpg(fuel_props.energy_density)?;
706
707        // utility factor calculation for last charge depletion iteration and transition iteration
708        // ported from excel
709        let interp_x_vals: Vec<f64> = (0..((phev_calc.cd_cycs.ceil() + 1.0) as usize))
710            .map(|i| i as f64 * dist_mi)
711            .collect();
712
713        phev_calc.lab_iter_uf = vec![];
714        for x in interp_x_vals {
715            phev_calc.lab_iter_uf.push(
716                phev_utilization_params.uf_array[first_grtr(
717                    &phev_utilization_params.rechg_freq_miles,
718                    x,
719                )
720                .with_context(|| format_dbg!())?
721                    - 1],
722            );
723        }
724
725        // transition cycle
726        phev_calc.trans_init_soc = max_soc - phev_calc.cd_cycs.floor() * phev_calc.delta_soc;
727
728        // run the transition cycle by setting initial SOC
729        let res_mut = sd.veh.res_mut().with_context(|| format_dbg!())?;
730        res_mut.state.soc.mark_stale();
731        res_mut
732            .state
733            .soc
734            .update(phev_calc.trans_init_soc, || format_dbg!())?;
735        sd.walk()?;
736
737        // charge depletion battery kW-hr
738        phev_calc.trans_ess_kwh =
739            phev_calc.cd_ess_kwh_per_mi * dist_mi * phev_calc.cd_frac_in_trans;
740        phev_calc.trans_ess_kwh_per_mi = phev_calc.cd_ess_kwh_per_mi * phev_calc.cd_frac_in_trans;
741
742        // charge sustaining
743        // the 0.01 is here to be consistent with Excel
744        let init_soc = min_soc + 0.01 * uc::R;
745        let res_mut = sd.veh.res_mut().with_context(|| format_dbg!())?;
746        res_mut.state.soc.mark_stale();
747        res_mut.state.soc.update(init_soc, || format_dbg!())?;
748        sd.walk()?;
749
750        // charge sustaining fuel gallons
751        let cs_fuel_energy_kwh = if let Some(fc) = sd.veh.fc() {
752            fc.state
753                .energy_fuel
754                .get_fresh(|| format_dbg!())?
755                .get::<si::kilowatt_hour>()
756        } else {
757            0.0
758        };
759        phev_calc.cs_fs_gal = cs_fuel_energy_kwh / fuel_props.kwh_per_gge();
760        // charge depletion fuel gallons, dependent on phev_calc.trans_fs_gal
761        phev_calc.trans_fs_gal = phev_calc.cs_fs_gal * (1.0 - phev_calc.cd_frac_in_trans);
762        phev_calc.cs_fs_kwh = cs_fuel_energy_kwh;
763        phev_calc.trans_fs_kwh = phev_calc.cs_fs_kwh * (1.0 - phev_calc.cd_frac_in_trans);
764        // charge sustaining battery kW-hr
765        let cs_ess_energy_kwh = if let Some(res) = sd.veh.res() {
766            res.state
767                .energy_out_chemical
768                .get_fresh(|| format_dbg!())?
769                .get::<si::kilowatt_hour>()
770        } else {
771            0.0
772        };
773        phev_calc.cs_ess_kwh = cs_ess_energy_kwh;
774        phev_calc.cs_ess_kwh_per_mi = sd.veh.kwh_per_mi()?;
775
776        let lab_iter_uf_diff = phev_calc.lab_iter_uf.diff();
777        phev_calc.lab_uf_gpm = [
778            phev_calc.trans_fs_gal * lab_iter_uf_diff.last().with_context(|| format_dbg!())?,
779            phev_calc.cs_fs_gal
780                * (1.0
781                    - phev_calc
782                        .lab_iter_uf
783                        .last()
784                        .with_context(|| format_dbg!())?),
785        ]
786        .iter()
787        .map(|x| *x / dist_mi)
788        .collect();
789
790        phev_calc.cd_mpg = sd.veh.mpg(fuel_props.energy_density)?;
791
792        // city and highway cycle ranges
793        let min_soc_in_cycle = phev_calc.delta_soc.abs(); // Use delta_soc as proxy for min SOC change
794        phev_calc.cd_miles =
795            if (max_soc - label_fe_phev.regen_soc_buffer - min_soc_in_cycle) < 0.01 * uc::R {
796                1000.0
797            } else {
798                phev_calc.cd_cycs.ceil() * dist_mi
799            };
800        phev_calc.cd_lab_mpg = phev_calc
801            .lab_iter_uf
802            .last()
803            .with_context(|| format_dbg!())?
804            / (phev_calc.trans_fs_gal / dist_mi);
805
806        // charge sustaining
807        phev_calc.cs_mpg = dist_mi / phev_calc.cs_fs_gal;
808
809        phev_calc.lab_uf = phev_utilization_params.uf_array[first_grtr(
810            &phev_utilization_params.rechg_freq_miles,
811            phev_calc.cd_miles,
812        )
813        .with_context(|| format_dbg!())?
814            - 1];
815
816        // labCombMpgge
817        phev_calc.cd_adj_mpg =
818            phev_calc.lab_iter_uf.max()? / phev_calc.lab_uf_gpm[phev_calc.lab_uf_gpm.len() - 2];
819
820        phev_calc.lab_mpgge = 1.0
821            / (phev_calc.lab_uf / phev_calc.cd_adj_mpg
822                + (1.0 - phev_calc.lab_uf) / phev_calc.cs_mpg);
823
824        let mut lab_iter_kwh_per_mi_vals = Vec::new();
825        lab_iter_kwh_per_mi_vals.push(0.0);
826        lab_iter_kwh_per_mi_vals
827            .extend(vec![phev_calc.cd_ess_kwh_per_mi; phev_calc.cd_cycs.floor() as usize].iter());
828        lab_iter_kwh_per_mi_vals.push(phev_calc.trans_ess_kwh_per_mi);
829        lab_iter_kwh_per_mi_vals.push(0.0);
830        phev_calc.lab_iter_kwh_per_mi = lab_iter_kwh_per_mi_vals;
831
832        let uf_diff = phev_calc.lab_iter_uf.diff();
833        let mut vals = Vec::new();
834        vals.push(0.0);
835        for i in 1..phev_calc.lab_iter_kwh_per_mi.len() - 1 {
836            if i - 1 < uf_diff.len() {
837                vals.push(phev_calc.lab_iter_kwh_per_mi[i] * uf_diff[i - 1]);
838            }
839        }
840        vals.push(0.0);
841        phev_calc.lab_iter_uf_kwh_per_mi = vals;
842
843        phev_calc.lab_kwh_per_mi = phev_calc
844            .lab_iter_uf_kwh_per_mi
845            .iter()
846            .fold(0.0, |acc, x| acc + x)
847            / phev_calc
848                .lab_iter_uf
849                .iter()
850                .fold(0.0f64, |acc, x| acc.max(*x));
851
852        let mut adj_iter_mpgge_vals = vec![0.0; phev_calc.cd_cycs.floor() as usize];
853        let mut adj_iter_kwh_per_mi_vals = vec![0.0; phev_calc.lab_iter_kwh_per_mi.len()];
854        if *key == "udds" {
855            adj_iter_mpgge_vals.push(f64::max(
856                1.0 / (adj_params.city_intercept
857                    + (adj_params.city_slope
858                        / (sd
859                            .veh
860                            .state
861                            .dist
862                            .get_fresh(|| format_dbg!())?
863                            .get::<si::mile>()
864                            / (phev_calc.trans_fs_kwh / fuel_props.kwh_per_gge())))),
865                sd.veh
866                    .state
867                    .dist
868                    .get_fresh(|| format_dbg!())?
869                    .get::<si::mile>()
870                    / (phev_calc.trans_fs_kwh / fuel_props.kwh_per_gge())
871                    * (1.0 - max_epa_adj),
872            ));
873            adj_iter_mpgge_vals.push(f64::max(
874                1.0 / (adj_params.city_intercept
875                    + (adj_params.city_slope
876                        / (sd
877                            .veh
878                            .state
879                            .dist
880                            .get_fresh(|| format_dbg!())?
881                            .get::<si::mile>()
882                            / (phev_calc.cs_fs_kwh / fuel_props.kwh_per_gge())))),
883                sd.veh
884                    .state
885                    .dist
886                    .get_fresh(|| format_dbg!())?
887                    .get::<si::mile>()
888                    / (phev_calc.cs_fs_kwh / fuel_props.kwh_per_gge())
889                    * (1.0 - max_epa_adj),
890            ));
891
892            for (c, _) in phev_calc.lab_iter_kwh_per_mi.iter().enumerate() {
893                if phev_calc.lab_iter_kwh_per_mi[c] == 0.0 {
894                    adj_iter_kwh_per_mi_vals[c] = 0.0;
895                } else {
896                    adj_iter_kwh_per_mi_vals[c] =
897                        (1.0 / f64::max(
898                            1.0 / (adj_params.city_intercept
899                                + (adj_params.city_slope
900                                    / ((1.0 / phev_calc.lab_iter_kwh_per_mi[c])
901                                        * fuel_props.kwh_per_gge()))),
902                            (1.0 - max_epa_adj)
903                                * ((1.0 / phev_calc.lab_iter_kwh_per_mi[c])
904                                    * fuel_props.kwh_per_gge()),
905                        )) * fuel_props.kwh_per_gge();
906                }
907            }
908        } else {
909            adj_iter_mpgge_vals.push(f64::max(
910                1.0 / (adj_params.hwy_intercept
911                    + (adj_params.hwy_slope
912                        / (sd
913                            .veh
914                            .state
915                            .dist
916                            .get_fresh(|| format_dbg!())?
917                            .get::<si::mile>()
918                            / (phev_calc.trans_fs_kwh / fuel_props.kwh_per_gge())))),
919                sd.veh
920                    .state
921                    .dist
922                    .get_fresh(|| format_dbg!())?
923                    .get::<si::mile>()
924                    / (phev_calc.trans_fs_kwh / fuel_props.kwh_per_gge())
925                    * (1.0 - max_epa_adj),
926            ));
927            adj_iter_mpgge_vals.push(f64::max(
928                1.0 / (adj_params.hwy_intercept
929                    + (adj_params.hwy_slope
930                        / (sd
931                            .veh
932                            .state
933                            .dist
934                            .get_fresh(|| format_dbg!())?
935                            .get::<si::mile>()
936                            / (phev_calc.cs_fs_kwh / fuel_props.kwh_per_gge())))),
937                sd.veh
938                    .state
939                    .dist
940                    .get_fresh(|| format_dbg!())?
941                    .get::<si::mile>()
942                    / (phev_calc.cs_fs_kwh / fuel_props.kwh_per_gge())
943                    * (1.0 - max_epa_adj),
944            ));
945
946            for (c, _) in phev_calc.lab_iter_kwh_per_mi.iter().enumerate() {
947                if phev_calc.lab_iter_kwh_per_mi[c] == 0.0 {
948                    adj_iter_kwh_per_mi_vals[c] = 0.0;
949                } else {
950                    adj_iter_kwh_per_mi_vals[c] =
951                        (1.0 / f64::max(
952                            1.0 / (adj_params.hwy_intercept
953                                + (adj_params.hwy_slope
954                                    / ((1.0 / phev_calc.lab_iter_kwh_per_mi[c])
955                                        * fuel_props.kwh_per_gge()))),
956                            (1.0 - max_epa_adj)
957                                * ((1.0 / phev_calc.lab_iter_kwh_per_mi[c])
958                                    * fuel_props.kwh_per_gge()),
959                        )) * fuel_props.kwh_per_gge();
960                }
961            }
962        }
963        phev_calc.adj_iter_mpgge = adj_iter_mpgge_vals;
964        phev_calc.adj_iter_kwh_per_mi = adj_iter_kwh_per_mi_vals;
965
966        phev_calc.adj_iter_cd_miles = vec![0.0; phev_calc.cd_cycs.ceil() as usize + 2];
967        for c in 0..phev_calc.adj_iter_cd_miles.len() {
968            if c == 0 {
969                phev_calc.adj_iter_cd_miles[c] = 0.0;
970            } else if c <= phev_calc.cd_cycs.floor() as usize {
971                phev_calc.adj_iter_cd_miles[c] = phev_calc.adj_iter_cd_miles[c - 1]
972                    + phev_calc.cd_ess_kwh_per_mi
973                        * sd.veh
974                            .state
975                            .dist
976                            .get_fresh(|| format_dbg!())?
977                            .get::<si::mile>()
978                        / phev_calc.adj_iter_kwh_per_mi[c];
979            } else if c == phev_calc.cd_cycs.floor() as usize + 1 {
980                phev_calc.adj_iter_cd_miles[c] = phev_calc.adj_iter_cd_miles[c - 1]
981                    + phev_calc.trans_ess_kwh_per_mi
982                        * sd.veh
983                            .state
984                            .dist
985                            .get_fresh(|| format_dbg!())?
986                            .get::<si::mile>()
987                        / phev_calc.adj_iter_kwh_per_mi[c];
988            } else {
989                phev_calc.adj_iter_cd_miles[c] = 0.0;
990            }
991        }
992
993        let mut soc_hist: Vec<f64> = vec![];
994        for soc in sd
995            .veh
996            .res()
997            .with_context(|| format_dbg!())?
998            .history
999            .soc
1000            .clone()
1001        {
1002            soc_hist.push(soc.get_fresh(|| format_dbg!())?.get::<si::ratio>());
1003        }
1004
1005        phev_calc.adj_cd_miles =
1006            if max_soc - label_fe_phev.regen_soc_buffer - (*soc_hist.min()? * uc::R) < 0.01 * uc::R
1007            {
1008                1000.0
1009            } else {
1010                *phev_calc.adj_iter_cd_miles.max()?
1011            };
1012
1013        // utility factor calculation for last charge depletion iteration and transition iteration
1014        // ported from excel
1015
1016        phev_calc.adj_iter_uf = vec![];
1017        for x in phev_calc.adj_iter_cd_miles.clone() {
1018            phev_calc.adj_iter_uf.push(
1019                phev_utilization_params.uf_array[first_grtr(
1020                    &phev_utilization_params.rechg_freq_miles,
1021                    x,
1022                )
1023                .with_context(|| format_dbg!())?
1024                    - 1],
1025            )
1026        }
1027
1028        let adj_iter_uf_diff = phev_calc.adj_iter_uf.diff();
1029        phev_calc.adj_iter_uf_gpm = vec![0.0; phev_calc.cd_cycs.floor() as usize];
1030        phev_calc.adj_iter_uf_gpm.push(
1031            (1.0 / phev_calc.adj_iter_mpgge[phev_calc.adj_iter_mpgge.len() - 2])
1032                * adj_iter_uf_diff[adj_iter_uf_diff.len() - 2],
1033        );
1034        phev_calc.adj_iter_uf_gpm.push(
1035            (1.0 / phev_calc
1036                .adj_iter_mpgge
1037                .last()
1038                .with_context(|| format_dbg!())?)
1039                * (1.0 - phev_calc.adj_iter_uf[phev_calc.adj_iter_uf.len() - 2]),
1040        );
1041
1042        let adj_uf_diff = phev_calc.adj_iter_uf.diff();
1043        phev_calc.adj_iter_uf_kwh_per_mi = phev_calc
1044            .adj_iter_kwh_per_mi
1045            .iter()
1046            .zip(adj_uf_diff.iter())
1047            .map(|(kwh, uf)| kwh * uf)
1048            .collect();
1049
1050        let max_uf: f64 = phev_calc
1051            .adj_iter_uf
1052            .iter()
1053            .fold(0.0f64, |acc, x| acc.max(*x));
1054        phev_calc.adj_cd_mpgge =
1055            1.0 / phev_calc.adj_iter_uf_gpm[phev_calc.adj_iter_uf_gpm.len() - 2] * max_uf;
1056        phev_calc.adj_cs_mpgge = 1.0
1057            / phev_calc
1058                .adj_iter_uf_gpm
1059                .last()
1060                .with_context(|| format_dbg!())?
1061            * (1.0 - max_uf);
1062
1063        phev_calc.adj_uf = phev_utilization_params.uf_array[first_grtr(
1064            &phev_utilization_params.rechg_freq_miles,
1065            phev_calc.adj_cd_miles,
1066        )
1067        .with_context(|| format_dbg!())?
1068            - 1];
1069
1070        phev_calc.adj_mpgge = 1.0
1071            / (phev_calc.adj_uf / phev_calc.adj_cd_mpgge
1072                + (1.0 - phev_calc.adj_uf) / phev_calc.adj_cs_mpgge);
1073
1074        let uf_kwh_sum: f64 = phev_calc
1075            .adj_iter_uf_kwh_per_mi
1076            .iter()
1077            .fold(0.0, |acc, x| acc + x);
1078        phev_calc.adj_kwh_per_mi = uf_kwh_sum / max_uf / chg_eff;
1079
1080        phev_calc.adj_ess_kwh_per_mi = uf_kwh_sum / max_uf;
1081
1082        match *key {
1083            "udds" => label_fe_phev.udds = phev_calc.clone(),
1084            "hwy" => label_fe_phev.hwy = phev_calc.clone(),
1085            &_ => bail!("No field for cycle {}", key),
1086        };
1087    }
1088
1089    Ok(label_fe_phev)
1090}
1091
1092#[cfg(test)]
1093mod tests {
1094    use super::*;
1095    use crate::vehicle::vehicle_model::f3veh_with_f2_eff;
1096
1097    /// Test that label FE calculations for conventional vehicles match FASTSim-2 results
1098    #[test]
1099    #[cfg(all(feature = "resources", feature = "yaml"))]
1100    fn test_label_fe_conv_vs_fastsim2() {
1101        let file_contents = include_str!("vehicle/fastsim-2_2012_Ford_Fusion.yaml");
1102        use fastsim_2::traits::SerdeAPI;
1103        let f2veh = fastsim_2::vehicle::RustVehicle::from_yaml(file_contents, false).unwrap();
1104        let mut veh = Vehicle::try_from(f2veh.clone()).unwrap();
1105        f3veh_with_f2_eff(&f2veh, &mut veh);
1106
1107        // Get FASTSim-3 label FE results
1108        let (label_fe_f3, _) = get_label_fe(&mut veh, None, false, None, None, false)
1109            .with_context(|| format_dbg!())
1110            .unwrap();
1111
1112        // Get FASTSim-2 label FE results
1113        let (label_fe_f2, _) = fastsim_2::simdrivelabel::get_label_fe(&f2veh.clone(), None, None)
1114            .with_context(|| format_dbg!())
1115            .unwrap();
1116
1117        // Compare key results (allowing for small numerical differences)
1118        let tolerance = 0.01; // 1% tolerance
1119
1120        // Check MPGe values
1121        assert!(
1122            (label_fe_f3.lab_udds_mpgge - label_fe_f2.lab_udds_mpgge).abs()
1123                / label_fe_f2.lab_udds_mpgge
1124                < tolerance,
1125            "UDDS MPGe mismatch: F3={:.3}, F2={:.3}",
1126            label_fe_f3.lab_udds_mpgge,
1127            label_fe_f2.lab_udds_mpgge
1128        );
1129
1130        assert!(
1131            (label_fe_f3.lab_hwy_mpgge - label_fe_f2.lab_hwy_mpgge).abs()
1132                / label_fe_f2.lab_hwy_mpgge
1133                < tolerance,
1134            "Highway MPGe mismatch: F3={:.3}, F2={:.3}",
1135            label_fe_f3.lab_hwy_mpgge,
1136            label_fe_f2.lab_hwy_mpgge
1137        );
1138
1139        assert!(
1140            (label_fe_f3.lab_comb_mpgge - label_fe_f2.lab_comb_mpgge).abs()
1141                / label_fe_f2.lab_comb_mpgge
1142                < tolerance,
1143            "Combined MPGe mismatch: F3={:.3}, F2={:.3}",
1144            label_fe_f3.lab_comb_mpgge,
1145            label_fe_f2.lab_comb_mpgge
1146        );
1147
1148        // Check adjusted values
1149        assert!(
1150            (label_fe_f3.adj_udds_mpgge - label_fe_f2.adj_udds_mpgge).abs()
1151                / label_fe_f2.adj_udds_mpgge
1152                < tolerance,
1153            "Adjusted UDDS MPGe mismatch: F3={:.3}, F2={:.3}",
1154            label_fe_f3.adj_udds_mpgge,
1155            label_fe_f2.adj_udds_mpgge
1156        );
1157
1158        assert!(
1159            (label_fe_f3.adj_comb_mpgge - label_fe_f2.adj_comb_mpgge).abs()
1160                / label_fe_f2.adj_comb_mpgge
1161                < tolerance,
1162            "Adjusted combined MPGe mismatch: F3={:.3}, F2={:.3}",
1163            label_fe_f3.adj_comb_mpgge,
1164            label_fe_f2.adj_comb_mpgge
1165        );
1166
1167        println!("Conventional vehicle label FE test passed!");
1168        println!(
1169            "F3 Combined MPGe: {:.3}, F2: {:.3}",
1170            label_fe_f3.lab_comb_mpgge, label_fe_f2.lab_comb_mpgge
1171        );
1172    }
1173
1174    // /// Test that label FE calculations for BEV vehicles match FASTSim-2 results
1175    // #[test]
1176    // #[cfg(all(feature = "resources", feature = "yaml"))]
1177    // fn test_label_fe_bev_vs_fastsim2() {
1178    //     let file_contents = include_str!("vehicle/fastsim-2_2022_Renault_Zoe_ZE50_R135.yaml");
1179    //     use fastsim_2::traits::SerdeAPI;
1180    //     let f2veh = fastsim_2::vehicle::RustVehicle::from_yaml(file_contents, false).unwrap();
1181    //     let mut veh = Vehicle::try_from(f2veh.clone()).unwrap();
1182    //     f3veh_with_f2_eff(&f2veh, &mut veh);
1183
1184    //     // Get FASTSim-3 label FE results
1185    //     let (label_fe_f3, _) = get_label_fe(&mut veh, None, false, None, None, false)
1186    //         .with_context(|| format_dbg!())
1187    //         .unwrap();
1188
1189    //     let (label_fe_f2, _) = fastsim_2::simdrivelabel::get_label_fe(&f2veh.clone(), None, None)
1190    //         .with_context(|| format_dbg!())
1191    //         .unwrap();
1192
1193    //     let tolerance = 0.011; // 1.1% tolerance
1194
1195    //     // For BEV, check kWh/mi values instead of MPGe
1196    //     assert!(
1197    //         (label_fe_f3.lab_udds_kwh_per_mi - label_fe_f2.lab_udds_kwh_per_mi).abs()
1198    //             / label_fe_f2.lab_udds_kwh_per_mi
1199    //             < tolerance,
1200    //         "UDDS kWh/mi mismatch: F3={:.3}, F2={:.3}",
1201    //         label_fe_f3.lab_udds_kwh_per_mi,
1202    //         label_fe_f2.lab_udds_kwh_per_mi
1203    //     );
1204
1205    //     assert!(
1206    //         (label_fe_f3.lab_comb_kwh_per_mi - label_fe_f2.lab_comb_kwh_per_mi).abs()
1207    //             / label_fe_f2.lab_comb_kwh_per_mi
1208    //             < tolerance,
1209    //         "Combined kWh/mi mismatch: F3={:.3}, F2={:.3}",
1210    //         label_fe_f3.lab_comb_kwh_per_mi,
1211    //         label_fe_f2.lab_comb_kwh_per_mi
1212    //     );
1213
1214    //     println!("BEV label FE test passed!");
1215    //     println!(
1216    //         "F3 Combined kWh/mi: {:.3}, F2: {:.3}",
1217    //         label_fe_f3.lab_comb_kwh_per_mi, label_fe_f2.lab_comb_kwh_per_mi
1218    //     );
1219    // }
1220
1221    // /// Test that label FE calculations for HEV vehicles match FASTSim-2 results
1222    // #[test]
1223    // #[cfg(all(feature = "resources", feature = "yaml"))]
1224    // fn test_label_fe_hev_vs_fastsim2() {
1225    //     let file_contents = include_str!("vehicle/fastsim-2_2016_TOYOTA_Prius_Two.yaml");
1226    //     use fastsim_2::traits::SerdeAPI;
1227    //     let f2veh = fastsim_2::vehicle::RustVehicle::from_yaml(file_contents, false).unwrap();
1228    //     let mut veh = Vehicle::try_from(f2veh.clone()).unwrap();
1229    //     f3veh_with_f2_eff(&f2veh, &mut veh);
1230
1231    //     // Get FASTSim-3 label FE results
1232    //     let (label_fe_f3, _) = get_label_fe(&mut veh, None, false, None, None, false)
1233    //         .with_context(|| format_dbg!())
1234    //         .unwrap();
1235
1236    //     let (label_fe_f2, _) = fastsim_2::simdrivelabel::get_label_fe(&f2veh, None, None)
1237    //         .with_context(|| format_dbg!())
1238    //         .unwrap();
1239
1240    //     let tolerance = 0.1; // Temporarily increased tolerance to 10% for debugging
1241
1242    //     // Check MPGe values for HEV
1243    //     assert!(
1244    //         (label_fe_f3.lab_udds_mpgge - label_fe_f2.lab_udds_mpgge).abs()
1245    //             / label_fe_f2.lab_udds_mpgge
1246    //             < tolerance,
1247    //         "UDDS MPGe mismatch: F3={:.3}, F2={:.3}",
1248    //         label_fe_f3.lab_udds_mpgge,
1249    //         label_fe_f2.lab_udds_mpgge
1250    //     );
1251
1252    //     assert!(
1253    //         (label_fe_f3.lab_comb_mpgge - label_fe_f2.lab_comb_mpgge).abs()
1254    //             / label_fe_f2.lab_comb_mpgge
1255    //             < tolerance,
1256    //         "Combined MPGe mismatch: F3={:.3}, F2={:.3}",
1257    //         label_fe_f3.lab_comb_mpgge,
1258    //         label_fe_f2.lab_comb_mpgge
1259    //     );
1260
1261    //     assert!(
1262    //         (label_fe_f3.lab_hwy_mpgge - label_fe_f2.lab_hwy_mpgge).abs()
1263    //             / label_fe_f2.lab_hwy_mpgge
1264    //             < tolerance,
1265    //         "Hwy MPGe mismatch: F3={:.3}, F2={:.3}",
1266    //         label_fe_f3.lab_hwy_mpgge,
1267    //         label_fe_f2.lab_hwy_mpgge
1268    //     );
1269
1270    //     assert!(
1271    //         (label_fe_f3.net_accel - label_fe_f2.net_accel).abs() / label_fe_f2.net_accel
1272    //             < tolerance,
1273    //         "Hwy MPGe mismatch: F3={:.3}, F2={:.3}",
1274    //         label_fe_f3.net_accel,
1275    //         label_fe_f2.net_accel
1276    //     );
1277    // }
1278
1279    // /// Test that creates a mock PHEV vehicle from FASTSim-2 data and compares label FE calculations
1280    // #[test]
1281    // #[cfg(all(feature = "resources", feature = "yaml"))]
1282    // fn test_label_fe_phev_vs_fastsim2() {
1283    //     // Load a PHEV vehicle from the calibration directory (FASTSim-2 format)
1284    //     let f2_veh_path = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
1285    //         .parent()
1286    //         .with_context(|| format_dbg!())
1287    //         .unwrap()
1288    //         .join("cal_and_val/f2-vehicles/2016 CHEVROLET Volt.yaml");
1289
1290    //     if !f2_veh_path.exists() {
1291    //         println!("PHEV vehicle file not found, skipping test");
1292    //         return;
1293    //     }
1294
1295    //     let veh_contents = std::fs::read_to_string(&f2_veh_path)
1296    //         .with_context(|| format_dbg!())
1297    //         .unwrap();
1298
1299    //     // Load FASTSim-2 vehicle and convert to FASTSim-3
1300    //     let f2_veh: fastsim_2::vehicle::RustVehicle =
1301    //         fastsim_2::traits::SerdeAPI::from_yaml(&veh_contents, false)
1302    //             .with_context(|| format_dbg!())
1303    //             .unwrap();
1304    //     assert!(f2_veh.veh_pt_type == fastsim_2::vehicle::PHEV);
1305    //     let mut veh = Vehicle::try_from(f2_veh.clone())
1306    //         .with_context(|| format_dbg!())
1307    //         .unwrap();
1308    //     assert!(
1309    //         veh.pt_type.is_plug_in_hybrid_electric_vehicle(),
1310    //         "`veh.pt_type.variant_as_str()`: {}\n`f2_veh.veh_pt_type`: {}",
1311    //         veh.pt_type.variant_as_str(),
1312    //         f2_veh.veh_pt_type
1313    //     );
1314
1315    //     // Get FASTSim-3 label FE results (if PHEV functionality is implemented)
1316    //     let label_fe_f3 = get_label_fe(&mut veh, None, false, None, None, false)
1317    //         .unwrap()
1318    //         .0;
1319
1320    //     // Get FASTSim-2 label FE results
1321    //     let label_fe_f2 = fastsim_2::simdrivelabel::get_label_fe(&f2_veh, None, None)
1322    //         .unwrap()
1323    //         .0;
1324
1325    //     let tolerance = 0.05; // 5% tolerance for PHEV (more complex calculations)
1326
1327    //     // Check MPGe values for HEV
1328    //     assert!(
1329    //         (label_fe_f3.lab_udds_mpgge - label_fe_f2.lab_udds_mpgge).abs()
1330    //             / label_fe_f2.lab_udds_mpgge
1331    //             < tolerance,
1332    //         "UDDS MPGe mismatch: F3={:.3}, F2={:.3}",
1333    //         label_fe_f3.lab_udds_mpgge,
1334    //         label_fe_f2.lab_udds_mpgge
1335    //     );
1336
1337    //     assert!(
1338    //         (label_fe_f3.lab_comb_mpgge - label_fe_f2.lab_comb_mpgge).abs()
1339    //             / label_fe_f2.lab_comb_mpgge
1340    //             < tolerance,
1341    //         "Combined MPGe mismatch: F3={:.3}, F2={:.3}",
1342    //         label_fe_f3.lab_comb_mpgge,
1343    //         label_fe_f2.lab_comb_mpgge
1344    //     );
1345
1346    //     assert!(
1347    //         (label_fe_f3.lab_hwy_mpgge - label_fe_f2.lab_hwy_mpgge).abs()
1348    //             / label_fe_f2.lab_hwy_mpgge
1349    //             < tolerance,
1350    //         "Hwy MPGe mismatch: F3={:.3}, F2={:.3}",
1351    //         label_fe_f3.lab_hwy_mpgge,
1352    //         label_fe_f2.lab_hwy_mpgge
1353    //     );
1354
1355    //     assert!(
1356    //         (label_fe_f3.net_accel - label_fe_f2.net_accel).abs() / label_fe_f2.net_accel
1357    //             < tolerance,
1358    //         "Hwy MPGe mismatch: F3={:.3}, F2={:.3}",
1359    //         label_fe_f3.net_accel,
1360    //         label_fe_f2.net_accel
1361    //     );
1362    // }
1363}