Skip to main content

fastsim_core/simdrivelabel/
mod.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::{params::SimParams, 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
20fn parse_model_year_or_2017(veh_year: Option<&str>) -> u32 {
21    if let Some(parsed_year) = veh_year.and_then(|year| year.parse::<u32>().ok()) {
22        parsed_year
23    } else {
24        eprintln!("Model year could not be parsed; using 2017 adjustment coefficients.");
25        2017
26    }
27}
28
29/// Get the 0 to 60 mph accelaration time from the given times and speeds.
30pub fn get_0_to_60_time_from_accel_data(accel_data: &AccelData) -> anyhow::Result<f64> {
31    // Check if vehicle reaches 60 mph
32    let first_ind_after_60_mph =
33        first_grtr(&accel_data.speed_mph, 60.).with_context(|| format_dbg!())?;
34
35    if accel_data.speed_mph.iter().any(|&x| x >= 60.0) {
36        // Create interpolator from speed to time
37        let interp = Interp1DView::new(
38            ArrayView::from(&accel_data.speed_mph[..first_ind_after_60_mph + 1]),
39            ArrayView::from(&accel_data.time_s[..first_ind_after_60_mph + 1]),
40            strategy::Linear,
41            Extrapolate::Clamp,
42        )
43        .map_err(|e| {
44            anyhow!(
45                "Failed to create interpolator at line {} with originating error [{}]",
46                format_dbg!(),
47                e
48            )
49        })?;
50
51        // Interpolate time at 60 mph
52        let accel_time = interp.interpolate(&[60.0]).map_err(|e| {
53            anyhow!(
54                "Failed to interpolate acceleration time at line {} with originating error [{}]",
55                format_dbg!(),
56                e
57            )
58        })?;
59        Ok(accel_time)
60    } else {
61        bail!("Vehicle does not reach 60 mph")
62    }
63}
64
65/// Run the acceleration test and return the time/speed trace.
66pub fn run_accel(
67    veh: &Vehicle,
68    sim_params: &HashMap<&'static str, SimParams>,
69) -> anyhow::Result<AccelData> {
70    let mut sd_accel = SimDrive::new(
71        veh.clone(),
72        CYC_ACCEL.clone(),
73        sim_params.get("accel").cloned(),
74    );
75    sd_accel.sim_params.trace_miss_opts = TraceMissOptions::Allow;
76    sd_accel.run_once().map_err(|e| {
77        anyhow!(
78            "Acceleration simdrive run_once failed at line {} with originating error [{}]",
79            format_dbg!(),
80            e
81        )
82    })?;
83    // Extract speed values in mph
84    let mut speed_mph: Vec<f64> = vec![];
85    for s in sd_accel.veh.history.speed_ach.clone() {
86        speed_mph.push(s.get_fresh(|| format_dbg!())?.get::<si::mile_per_hour>())
87    }
88    // Extract time values in seconds
89    let time_s: Vec<f64> = sd_accel
90        .cyc
91        .time
92        .iter()
93        .map(|t| t.get::<si::second>())
94        .collect();
95    Ok(AccelData { time_s, speed_mph })
96}
97
98/// Returns time [s] for 0-60 mph acceleration at max power
99pub fn get_0_to_60_time(sd_accel: &mut SimDrive) -> anyhow::Result<f64> {
100    sd_accel.sim_params.trace_miss_opts = TraceMissOptions::Allow;
101    sd_accel.run_once().map_err(|e| {
102        anyhow!(
103            "Acceleration simdrive run_once failed at line {} with originating error [{}]",
104            format_dbg!(),
105            e
106        )
107    })?;
108
109    // Extract speed values in mph
110    let mut speed_mph: Vec<f64> = vec![];
111    for s in sd_accel.veh.history.speed_ach.clone() {
112        speed_mph.push(s.get_fresh(|| format_dbg!())?.get::<si::mile_per_hour>())
113    }
114
115    // Extract time values in seconds
116    let time_s: Vec<f64> = sd_accel
117        .cyc
118        .time
119        .iter()
120        .map(|t| t.get::<si::second>())
121        .collect();
122
123    let accel_data = AccelData { time_s, speed_mph };
124    let accel_time = get_0_to_60_time_from_accel_data(&accel_data).map_err(|err| {
125        anyhow!(
126            "Vehicle {} doesn't reach 60 mph in the acceleration test at line {} with originating error [{}]",
127            sd_accel.veh.name,
128            format_dbg!(),
129            err
130        )
131    })?;
132    Ok(accel_time)
133}
134
135// const MPH_PER_MPS: f64 = 2.2369362921;
136const DEFAULT_CHG_EFF: f64 = 0.86;
137
138#[serde_api]
139#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
140#[cfg_attr(feature = "pyo3", pyclass(module = "fastsim", subclass, eq))]
141pub struct FuelProperties {
142    // TODO: make a way to serialize/deserialize with "J/m^3"
143    // fuel energy density
144    /// fuel energy density (i.e. energy per unit mass, which has the same base
145    /// units as pressure)
146    pub energy_density: si::Pressure,
147    /// fuel density
148    pub density: si::MassDensity,
149}
150
151impl Init for FuelProperties {}
152impl SerdeAPI for FuelProperties {}
153
154#[pyo3_api]
155impl FuelProperties {}
156
157impl Default for FuelProperties {
158    /// Default values for gasoline
159    fn default() -> Self {
160        Self {
161            energy_density: 33.7 * uc::KWH / uc::GALLON,
162            density: 0.75 * uc::KG / uc::L,
163        }
164    }
165}
166
167const J_PER_KWH: f64 = 3_600_000.0;
168lazy_static! {
169    static ref CUBIC_METER_PER_GAL: f64 = 3.79e-3;
170}
171
172impl FuelProperties {
173    fn kwh_per_gge(&self) -> f64 {
174        self.energy_density.get::<si::joule_per_cubic_meter>() / J_PER_KWH * *CUBIC_METER_PER_GAL
175    }
176}
177
178trait VehicleEfficiency {
179    fn mpg(&self, energy_density: si::Pressure) -> anyhow::Result<f64>;
180
181    fn kwh_per_mi(&self) -> anyhow::Result<f64>;
182}
183
184impl VehicleEfficiency for Vehicle {
185    fn mpg(&self, energy_density: si::Pressure) -> anyhow::Result<f64> {
186        if let Some(fc) = self.fc() {
187            Ok(self
188                .state
189                .dist
190                .get_fresh(|| format_dbg!())?
191                .get::<si::mile>()
192                / (*fc.state.energy_fuel.get_fresh(|| format_dbg!())? / energy_density)
193                    .get::<si::gallon>())
194        } else {
195            Ok(f64::NAN)
196        }
197    }
198
199    fn kwh_per_mi(&self) -> anyhow::Result<f64> {
200        if let Some(res) = self.res() {
201            Ok(res
202                .state
203                .energy_out_chemical
204                .get_fresh(|| format_dbg!())?
205                .get::<si::kilowatt_hour>()
206                / self
207                    .state
208                    .dist
209                    .get_fresh(|| format_dbg!())?
210                    .get::<si::mile>())
211        } else {
212            Ok(f64::NAN)
213        }
214    }
215}
216
217#[serde_api]
218#[derive(Clone, Default, Debug, Deserialize, Serialize, PartialEq)]
219#[cfg_attr(feature = "pyo3", pyclass(module = "fastsim", subclass, eq))]
220pub struct LabelFe {
221    pub veh: Option<Vehicle>,
222    pub adj_params: AdjCoef,
223    pub lab_udds_mpgge: f64,
224    pub lab_hwy_mpgge: f64,
225    pub lab_comb_mpgge: f64,
226    pub lab_udds_kwh_per_mi: f64,
227    pub lab_hwy_kwh_per_mi: f64,
228    pub lab_comb_kwh_per_mi: f64,
229    pub adj_udds_mpgge: f64,
230    pub adj_hwy_mpgge: f64,
231    pub adj_comb_mpgge: f64,
232    pub adj_udds_kwh_per_mi: f64,
233    pub adj_hwy_kwh_per_mi: f64,
234    pub adj_comb_kwh_per_mi: f64,
235    pub adj_udds_ess_kwh_per_mi: f64,
236    pub adj_hwy_ess_kwh_per_mi: f64,
237    pub adj_comb_ess_kwh_per_mi: f64,
238    pub net_range_miles: f64,
239    pub uf: Option<f64>,
240    pub net_accel: f64,
241    pub res_found: String,
242    pub phev_calcs: Option<LabelFePHEV>,
243    pub adj_cs_comb_mpgge: Option<f64>,
244    pub adj_cd_comb_mpgge: Option<f64>,
245    pub net_phev_cd_miles: Option<f64>,
246}
247
248#[pyo3_api]
249impl LabelFe {}
250
251impl Init for LabelFe {}
252impl SerdeAPI for LabelFe {}
253
254#[serde_api]
255#[derive(Default, Clone, Debug, Deserialize, Serialize, PartialEq)]
256#[cfg_attr(feature = "pyo3", pyclass(module = "fastsim", subclass, eq))]
257/// Label fuel economy values for a PHEV vehicle
258pub struct LabelFePHEV {
259    pub regen_soc_buffer: si::Ratio,
260    pub udds: PHEVCycleCalc,
261    pub hwy: PHEVCycleCalc,
262}
263
264#[pyo3_api]
265impl LabelFePHEV {}
266
267impl Init for LabelFePHEV {}
268impl SerdeAPI for LabelFePHEV {}
269
270#[serde_api]
271#[derive(Default, Clone, Debug, Deserialize, Serialize, PartialEq)]
272#[cfg_attr(feature = "pyo3", pyclass(module = "fastsim", subclass, eq))]
273/// Label fuel economy calculations for a specific cycle of a PHEV vehicle
274pub struct PHEVCycleCalc {
275    /// Charge depletion battery kW-hr
276    pub cd_ess_kwh: f64,
277    pub cd_ess_kwh_per_mi: f64,
278    /// Charge depletion fuel gallons
279    pub cd_fs_gal: f64,
280    pub cd_fs_kwh: f64,
281    pub cd_mpg: f64,
282    /// Number of cycles in charge depletion mode, up to transition
283    pub cd_cycs: f64,
284    pub cd_miles: f64,
285    pub cd_lab_mpg: f64,
286    pub cd_adj_mpg: f64,
287    /// Fraction of transition cycles spent in charge depletion
288    pub cd_frac_in_trans: f64,
289    /// SOC change during 1 cycle
290    pub trans_init_soc: si::Ratio,
291    /// charge depletion battery kW-hr
292    pub trans_ess_kwh: f64,
293    pub trans_ess_kwh_per_mi: f64,
294    pub trans_fs_gal: f64,
295    pub trans_fs_kwh: f64,
296    /// charge sustaining battery kW-hr
297    pub cs_ess_kwh: f64,
298    pub cs_ess_kwh_per_mi: f64,
299    /// charge sustaining fuel gallons
300    pub cs_fs_gal: f64,
301    pub cs_fs_kwh: f64,
302    pub cs_mpg: f64,
303    pub lab_mpgge: f64,
304    pub lab_kwh_per_mi: f64,
305    pub lab_uf: f64,
306    pub lab_uf_gpm: Vec<f64>,
307    pub lab_iter_uf: Vec<f64>,
308    pub lab_iter_uf_kwh_per_mi: Vec<f64>,
309    pub lab_iter_kwh_per_mi: Vec<f64>,
310    pub adj_iter_mpgge: Vec<f64>,
311    pub adj_iter_kwh_per_mi: Vec<f64>,
312    pub adj_iter_cd_miles: Vec<f64>,
313    pub adj_iter_uf: Vec<f64>,
314    pub adj_iter_uf_gpm: Vec<f64>,
315    pub adj_iter_uf_kwh_per_mi: Vec<f64>,
316    pub adj_cd_miles: f64,
317    pub adj_cd_mpgge: f64,
318    pub adj_cs_mpgge: f64,
319    pub adj_uf: f64,
320    pub adj_mpgge: f64,
321    pub adj_kwh_per_mi: f64,
322    pub adj_ess_kwh_per_mi: f64,
323    pub delta_soc: si::Ratio,
324    /// Total number of miles in charge depletion mode, assuming constant kWh_per_mi
325    pub total_cd_miles: f64,
326}
327
328impl Init for PHEVCycleCalc {}
329impl SerdeAPI for PHEVCycleCalc {}
330
331#[pyo3_api]
332impl PHEVCycleCalc {}
333
334#[derive(Clone, Serialize, Deserialize, Debug, PartialEq)]
335#[cfg_attr(feature = "pyo3", pyclass(module = "fastsim", subclass, eq))]
336pub struct AdjCoef {
337    pub city_intercept: f64,
338    pub city_slope: f64,
339    pub hwy_intercept: f64,
340    pub hwy_slope: f64,
341}
342
343#[pyo3_api]
344impl AdjCoef {}
345
346impl Init for AdjCoef {}
347impl SerdeAPI for AdjCoef {}
348
349impl Default for AdjCoef {
350    fn default() -> Self {
351        Self {
352            city_intercept: 0.003259,
353            city_slope: 1.1805,
354            hwy_intercept: 0.001376,
355            hwy_slope: 1.3466,
356        }
357    }
358}
359
360#[serde_api]
361#[derive(Clone, Serialize, Deserialize, Debug, PartialEq)]
362#[cfg_attr(feature = "pyo3", pyclass(module = "fastsim", subclass, eq))]
363pub struct PhevUtilizationParams {
364    pub adj_coef_map: HashMap<String, AdjCoef>,
365    /// Frequency of recharge events
366    pub rechg_freq_miles: Vec<f64>,
367    /// Array of utility factor
368    pub uf_array: Vec<f64>,
369}
370
371impl Init for PhevUtilizationParams {}
372impl SerdeAPI for PhevUtilizationParams {}
373
374impl Default for PhevUtilizationParams {
375    fn default() -> Self {
376        Self::from_json(&*PHEV_UTIL_PARAMS, false).unwrap()
377    }
378}
379
380lazy_static! {
381    static ref PHEV_UTIL_PARAMS: String = include_str!("longparams.json").to_string();
382}
383
384pub struct PhevVehicleInfo {
385    pub max_soc: si::Ratio,
386    pub min_soc: si::Ratio,
387    pub phev_max_regen: si::Ratio,
388    pub veh_mass: si::Mass,
389    pub em_peak_eff: si::Ratio,
390    pub energy_capacity: si::Energy,
391    pub chg_eff: f64,
392    pub fuel_storage_capacity: si::Energy,
393}
394
395pub struct PhevSimulationDataForLabel {
396    pub cd_fuel_consumed_kwh: f64,
397    pub cd_soc_start: f64,
398    pub cd_soc_end: f64,
399    pub cyc_dist_mi: f64,
400    pub cd_kwh_per_mi: f64,
401    // sd.veh.mpg(fuel_props.energy_density)?
402    pub cd_mpg: f64,
403    pub cs_fuel_consumed_kwh: f64,
404    // res.state.energy_out_chemical
405    pub cs_ess_energy_kwh: f64,
406    // sd.veh.kwh_per_mi()
407    pub cs_kwh_per_mi: f64,
408    // sd.veh.mpg()
409    // sd.veh.mpg(fuel_props.energy_density)
410    pub cs_mpg: f64,
411    // min of sd.veh.res().history.soc
412    pub cs_min_soc: f64,
413    // phev.fs.energy_capacity.get::<si::kilowatt_hour>()
414    pub cs_fs_energy_capacity_kwh: f64,
415}
416
417pub enum SimulationDataForLabel {
418    ConvOrHev {
419        veh_year: u32,
420        udds_mpgge: f64,
421        hwy_mpgge: f64,
422        /// Fuel storage usable energy in kWh
423        fs_energy_capacity_kwh: f64,
424    },
425    Bev {
426        veh_year: u32,
427        udds_kwh_per_mi: f64,
428        hwy_kwh_per_mi: f64,
429        bev_energy_capacity_kwh: f64,
430    },
431    Phev {
432        veh_year: u32,
433        info: PhevVehicleInfo,
434        udds: PhevSimulationDataForLabel,
435        hwy: PhevSimulationDataForLabel,
436    },
437}
438
439pub struct AccelData {
440    pub time_s: Vec<f64>,
441    pub speed_mph: Vec<f64>,
442}
443
444/// Calculate the transient cycle's init SOC.
445/// This is used for PHEV label fuel economy calculation.
446/// Returns the calculated SOC.
447pub fn calculate_transient_soc_helper(
448    max_soc: f64,
449    min_soc: f64,
450    energy_capacity_kwh: f64,
451    cyc_kwh_per_mi: f64,
452    soc_start: f64,
453    soc_end: f64,
454    dist_mi: f64,
455) -> f64 {
456    let total_cd_miles = ((max_soc - min_soc) * energy_capacity_kwh) / cyc_kwh_per_mi;
457    let cd_cycs = total_cd_miles / dist_mi;
458    let delta_soc = soc_start - soc_end;
459    max_soc - cd_cycs.floor() * delta_soc
460}
461
462/// A helper function to calculate label fuel economy for PHEVs.
463pub fn calculate_phev_label_helper(
464    info: &PhevVehicleInfo,
465    data: &PhevSimulationDataForLabel,
466    fuel_props: &FuelProperties,
467    max_epa_adj: f64,
468    phev_utilization_params: &PhevUtilizationParams,
469    adj_params: &AdjCoef,
470    label_fe_phev: &LabelFePHEV,
471    is_city: bool,
472) -> anyhow::Result<PHEVCycleCalc> {
473    let mut phev_calc = PHEVCycleCalc::default();
474    // charge depletion cycle has already been simulated
475    // charge depletion battery kW-hr
476    phev_calc.cd_ess_kwh =
477        ((info.max_soc - info.min_soc) * info.energy_capacity).get::<si::kilowatt_hour>();
478    let soc_start = data.cd_soc_start;
479    let soc_end = data.cd_soc_end;
480    let dist_mi = data.cyc_dist_mi;
481
482    // SOC change during 1 cycle
483    phev_calc.delta_soc = (soc_start - soc_end) * uc::R;
484    // total number of miles in charge depletion mode, assuming constant kWh_per_mi
485    phev_calc.total_cd_miles = ((info.max_soc - info.min_soc) * info.energy_capacity)
486        .get::<si::kilowatt_hour>()
487        / data.cd_kwh_per_mi;
488    // number of cycles in charge depletion mode, up to transition
489    phev_calc.cd_cycs = phev_calc.total_cd_miles / dist_mi;
490    // fraction of transition cycle spent in charge depletion
491    phev_calc.cd_frac_in_trans = phev_calc.cd_cycs % phev_calc.cd_cycs.floor();
492
493    // charge depletion fuel gallons - get from fuel converter
494    let fuel_energy_kwh = data.cd_fuel_consumed_kwh;
495    phev_calc.cd_fs_gal = fuel_energy_kwh / fuel_props.kwh_per_gge();
496    phev_calc.cd_fs_kwh = fuel_energy_kwh;
497    phev_calc.cd_ess_kwh_per_mi = data.cd_kwh_per_mi;
498    phev_calc.cd_mpg = data.cd_mpg;
499
500    // utility factor calculation for last charge depletion iteration and transition iteration
501    // ported from excel
502    let interp_x_vals: Vec<f64> = (0..((phev_calc.cd_cycs.ceil() + 1.0) as usize))
503        .map(|i| i as f64 * dist_mi)
504        .collect();
505
506    phev_calc.lab_iter_uf = vec![];
507    for x in interp_x_vals {
508        phev_calc.lab_iter_uf.push(
509            phev_utilization_params.uf_array[first_grtr(
510                &phev_utilization_params.rechg_freq_miles,
511                x,
512            )
513            .with_context(|| format_dbg!())?
514                - 1],
515        );
516    }
517
518    // transition cycle
519    phev_calc.trans_init_soc = info.max_soc - phev_calc.cd_cycs.floor() * phev_calc.delta_soc;
520
521    // charge depletion battery kW-hr
522    phev_calc.trans_ess_kwh = phev_calc.cd_ess_kwh_per_mi * dist_mi * phev_calc.cd_frac_in_trans;
523    phev_calc.trans_ess_kwh_per_mi = phev_calc.cd_ess_kwh_per_mi * phev_calc.cd_frac_in_trans;
524
525    // charge sustaining fuel gallons
526    let cs_fuel_energy_kwh = data.cs_fuel_consumed_kwh;
527    phev_calc.cs_fs_gal = cs_fuel_energy_kwh / fuel_props.kwh_per_gge();
528    // charge depletion fuel gallons, dependent on phev_calc.trans_fs_gal
529    phev_calc.trans_fs_gal = phev_calc.cs_fs_gal * (1.0 - phev_calc.cd_frac_in_trans);
530    phev_calc.cs_fs_kwh = cs_fuel_energy_kwh;
531    phev_calc.trans_fs_kwh = phev_calc.cs_fs_kwh * (1.0 - phev_calc.cd_frac_in_trans);
532    // charge sustaining battery kW-hr
533    let cs_ess_energy_kwh = data.cs_ess_energy_kwh;
534    phev_calc.cs_ess_kwh = cs_ess_energy_kwh;
535    phev_calc.cs_ess_kwh_per_mi = data.cs_kwh_per_mi;
536
537    let lab_iter_uf_diff = phev_calc.lab_iter_uf.diff();
538    phev_calc.lab_uf_gpm = [
539        phev_calc.trans_fs_gal * lab_iter_uf_diff.last().with_context(|| format_dbg!())?,
540        phev_calc.cs_fs_gal
541            * (1.0
542                - phev_calc
543                    .lab_iter_uf
544                    .last()
545                    .with_context(|| format_dbg!())?),
546    ]
547    .iter()
548    .map(|x| *x / dist_mi)
549    .collect();
550
551    // TODO: investigate. This does not seem correct but also appears in FASTSim 2
552    // shouldn't this be setting cs_mpg?
553    // Disabling for now. cd_mpg was already set above and cs_mpg is set below.
554    // phev_calc.cd_mpg = data.cd_mpg;
555
556    // city and highway cycle ranges
557    let min_soc_in_cycle = phev_calc.delta_soc.abs(); // Use delta_soc as proxy for min SOC change
558    phev_calc.cd_miles =
559        if (info.max_soc - label_fe_phev.regen_soc_buffer - min_soc_in_cycle) < 0.01 * uc::R {
560            1000.0
561        } else {
562            phev_calc.cd_cycs.ceil() * dist_mi
563        };
564    phev_calc.cd_lab_mpg = phev_calc
565        .lab_iter_uf
566        .last()
567        .with_context(|| format_dbg!())?
568        / (phev_calc.trans_fs_gal / dist_mi);
569
570    // charge sustaining
571    phev_calc.cs_mpg = dist_mi / phev_calc.cs_fs_gal;
572
573    phev_calc.lab_uf = phev_utilization_params.uf_array[first_grtr(
574        &phev_utilization_params.rechg_freq_miles,
575        phev_calc.cd_miles,
576    )
577    .with_context(|| format_dbg!())?
578        - 1];
579
580    // labCombMpgge
581    phev_calc.cd_adj_mpg =
582        phev_calc.lab_iter_uf.max()? / phev_calc.lab_uf_gpm[phev_calc.lab_uf_gpm.len() - 2];
583
584    phev_calc.lab_mpgge = 1.0
585        / (phev_calc.lab_uf / phev_calc.cd_adj_mpg + (1.0 - phev_calc.lab_uf) / phev_calc.cs_mpg);
586
587    let mut lab_iter_kwh_per_mi_vals = Vec::new();
588    lab_iter_kwh_per_mi_vals.push(0.0);
589    lab_iter_kwh_per_mi_vals
590        .extend(vec![phev_calc.cd_ess_kwh_per_mi; phev_calc.cd_cycs.floor() as usize].iter());
591    lab_iter_kwh_per_mi_vals.push(phev_calc.trans_ess_kwh_per_mi);
592    lab_iter_kwh_per_mi_vals.push(0.0);
593    phev_calc.lab_iter_kwh_per_mi = lab_iter_kwh_per_mi_vals;
594
595    let uf_diff = phev_calc.lab_iter_uf.diff();
596    let mut vals = Vec::new();
597    vals.push(0.0);
598    for i in 1..phev_calc.lab_iter_kwh_per_mi.len() - 1 {
599        // if i - 1 < uf_diff.len() {
600        if i < uf_diff.len() {
601            // vals.push(phev_calc.lab_iter_kwh_per_mi[i] * uf_diff[i - 1]);
602            vals.push(phev_calc.lab_iter_kwh_per_mi[i] * uf_diff[i]);
603        }
604    }
605    vals.push(0.0);
606    phev_calc.lab_iter_uf_kwh_per_mi = vals;
607
608    phev_calc.lab_kwh_per_mi = phev_calc
609        .lab_iter_uf_kwh_per_mi
610        .iter()
611        .fold(0.0, |acc, x| acc + x)
612        / phev_calc
613            .lab_iter_uf
614            .iter()
615            .fold(0.0f64, |acc, x| acc.max(*x));
616
617    let mut adj_iter_mpgge_vals = vec![0.0; phev_calc.cd_cycs.floor() as usize];
618    let mut adj_iter_kwh_per_mi_vals = vec![0.0; phev_calc.lab_iter_kwh_per_mi.len()];
619    if is_city {
620        adj_iter_mpgge_vals.push(f64::max(
621            1.0 / (adj_params.city_intercept
622                + (adj_params.city_slope
623                    / (data.cyc_dist_mi / (phev_calc.trans_fs_kwh / fuel_props.kwh_per_gge())))),
624            data.cyc_dist_mi / (phev_calc.trans_fs_kwh / fuel_props.kwh_per_gge())
625                * (1.0 - max_epa_adj),
626        ));
627        adj_iter_mpgge_vals.push(f64::max(
628            1.0 / (adj_params.city_intercept
629                + (adj_params.city_slope
630                    / (data.cyc_dist_mi / (phev_calc.cs_fs_kwh / fuel_props.kwh_per_gge())))),
631            data.cyc_dist_mi / (phev_calc.cs_fs_kwh / fuel_props.kwh_per_gge())
632                * (1.0 - max_epa_adj),
633        ));
634
635        for (c, _) in phev_calc.lab_iter_kwh_per_mi.iter().enumerate() {
636            if phev_calc.lab_iter_kwh_per_mi[c] == 0.0 {
637                adj_iter_kwh_per_mi_vals[c] = 0.0;
638            } else {
639                adj_iter_kwh_per_mi_vals[c] =
640                    (1.0 / f64::max(
641                        1.0 / (adj_params.city_intercept
642                            + (adj_params.city_slope
643                                / ((1.0 / phev_calc.lab_iter_kwh_per_mi[c])
644                                    * fuel_props.kwh_per_gge()))),
645                        (1.0 - max_epa_adj)
646                            * ((1.0 / phev_calc.lab_iter_kwh_per_mi[c]) * fuel_props.kwh_per_gge()),
647                    )) * fuel_props.kwh_per_gge();
648            }
649        }
650    } else {
651        adj_iter_mpgge_vals.push(f64::max(
652            1.0 / (adj_params.hwy_intercept
653                + (adj_params.hwy_slope
654                    / (data.cyc_dist_mi / (phev_calc.trans_fs_kwh / fuel_props.kwh_per_gge())))),
655            data.cyc_dist_mi / (phev_calc.trans_fs_kwh / fuel_props.kwh_per_gge())
656                * (1.0 - max_epa_adj),
657        ));
658        adj_iter_mpgge_vals.push(f64::max(
659            1.0 / (adj_params.hwy_intercept
660                + (adj_params.hwy_slope
661                    / (data.cyc_dist_mi / (phev_calc.cs_fs_kwh / fuel_props.kwh_per_gge())))),
662            data.cyc_dist_mi / (phev_calc.cs_fs_kwh / fuel_props.kwh_per_gge())
663                * (1.0 - max_epa_adj),
664        ));
665
666        for (c, _) in phev_calc.lab_iter_kwh_per_mi.iter().enumerate() {
667            if phev_calc.lab_iter_kwh_per_mi[c] == 0.0 {
668                adj_iter_kwh_per_mi_vals[c] = 0.0;
669            } else {
670                adj_iter_kwh_per_mi_vals[c] =
671                    (1.0 / f64::max(
672                        1.0 / (adj_params.hwy_intercept
673                            + (adj_params.hwy_slope
674                                / ((1.0 / phev_calc.lab_iter_kwh_per_mi[c])
675                                    * fuel_props.kwh_per_gge()))),
676                        (1.0 - max_epa_adj)
677                            * ((1.0 / phev_calc.lab_iter_kwh_per_mi[c]) * fuel_props.kwh_per_gge()),
678                    )) * fuel_props.kwh_per_gge();
679            }
680        }
681    }
682    phev_calc.adj_iter_mpgge = adj_iter_mpgge_vals;
683    phev_calc.adj_iter_kwh_per_mi = adj_iter_kwh_per_mi_vals;
684
685    phev_calc.adj_iter_cd_miles = vec![0.0; phev_calc.cd_cycs.ceil() as usize + 2];
686    for c in 0..phev_calc.adj_iter_cd_miles.len() {
687        if c == 0 {
688            phev_calc.adj_iter_cd_miles[c] = 0.0;
689        } else if c <= phev_calc.cd_cycs.floor() as usize {
690            phev_calc.adj_iter_cd_miles[c] = phev_calc.adj_iter_cd_miles[c - 1]
691                + phev_calc.cd_ess_kwh_per_mi * data.cyc_dist_mi / phev_calc.adj_iter_kwh_per_mi[c];
692        } else if c == phev_calc.cd_cycs.floor() as usize + 1 {
693            phev_calc.adj_iter_cd_miles[c] = phev_calc.adj_iter_cd_miles[c - 1]
694                + phev_calc.trans_ess_kwh_per_mi * data.cyc_dist_mi
695                    / phev_calc.adj_iter_kwh_per_mi[c];
696        } else {
697            phev_calc.adj_iter_cd_miles[c] = 0.0;
698        }
699    }
700
701    phev_calc.adj_cd_miles =
702        if info.max_soc - label_fe_phev.regen_soc_buffer - (data.cs_min_soc * uc::R) < 0.01 * uc::R
703        {
704            1000.0
705        } else {
706            *phev_calc.adj_iter_cd_miles.max()?
707        };
708
709    // utility factor calculation for last charge depletion iteration and transition iteration
710    // ported from excel
711
712    phev_calc.adj_iter_uf = vec![];
713    for x in phev_calc.adj_iter_cd_miles.clone() {
714        phev_calc.adj_iter_uf.push(
715            phev_utilization_params.uf_array[first_grtr(
716                &phev_utilization_params.rechg_freq_miles,
717                x,
718            )
719            .with_context(|| format_dbg!())?
720                - 1],
721        )
722    }
723
724    let adj_iter_uf_diff = phev_calc.adj_iter_uf.diff();
725    phev_calc.adj_iter_uf_gpm = vec![0.0; phev_calc.cd_cycs.floor() as usize];
726    phev_calc.adj_iter_uf_gpm.push(
727        (1.0 / phev_calc.adj_iter_mpgge[phev_calc.adj_iter_mpgge.len() - 2])
728            * adj_iter_uf_diff[adj_iter_uf_diff.len() - 2],
729    );
730    phev_calc.adj_iter_uf_gpm.push(
731        (1.0 / phev_calc
732            .adj_iter_mpgge
733            .last()
734            .with_context(|| format_dbg!())?)
735            * (1.0 - phev_calc.adj_iter_uf[phev_calc.adj_iter_uf.len() - 2]),
736    );
737
738    let adj_uf_diff = phev_calc.adj_iter_uf.diff();
739    phev_calc.adj_iter_uf_kwh_per_mi = phev_calc
740        .adj_iter_kwh_per_mi
741        .iter()
742        .zip(adj_uf_diff.iter())
743        .map(|(kwh, uf)| kwh * uf)
744        .collect();
745
746    let max_uf: f64 = phev_calc
747        .adj_iter_uf
748        .iter()
749        .fold(0.0f64, |acc, x| acc.max(*x));
750    phev_calc.adj_cd_mpgge =
751        1.0 / phev_calc.adj_iter_uf_gpm[phev_calc.adj_iter_uf_gpm.len() - 2] * max_uf;
752    phev_calc.adj_cs_mpgge = 1.0
753        / phev_calc
754            .adj_iter_uf_gpm
755            .last()
756            .with_context(|| format_dbg!())?
757        * (1.0 - max_uf);
758
759    phev_calc.adj_uf = phev_utilization_params.uf_array[first_grtr(
760        &phev_utilization_params.rechg_freq_miles,
761        phev_calc.adj_cd_miles,
762    )
763    .with_context(|| format_dbg!())?
764        - 1];
765
766    phev_calc.adj_mpgge = 1.0
767        / (phev_calc.adj_uf / phev_calc.adj_cd_mpgge
768            + (1.0 - phev_calc.adj_uf) / phev_calc.adj_cs_mpgge);
769
770    let uf_kwh_sum: f64 = phev_calc
771        .adj_iter_uf_kwh_per_mi
772        .iter()
773        .fold(0.0, |acc, x| acc + x);
774    phev_calc.adj_kwh_per_mi = uf_kwh_sum / max_uf / info.chg_eff;
775
776    phev_calc.adj_ess_kwh_per_mi = uf_kwh_sum / max_uf;
777
778    Ok(phev_calc)
779}
780
781/// This is a pure function that calculates the label fuel economy given
782/// simulation results.
783pub fn calculate_label_fuel_economy(
784    fuel_props: &FuelProperties,
785    phev_utilization_params: &PhevUtilizationParams,
786    max_epa_adj: f64,
787    sim_data: &SimulationDataForLabel,
788    accel_data: &AccelData,
789) -> anyhow::Result<LabelFe> {
790    let mut label_fe = LabelFe::default();
791    let veh_year = match sim_data {
792        SimulationDataForLabel::ConvOrHev { veh_year, .. }
793        | SimulationDataForLabel::Phev { veh_year, .. }
794        | SimulationDataForLabel::Bev { veh_year, .. } => *veh_year,
795    };
796    // find year-based adjustment parameters
797    let adj_params = if veh_year < 2017 {
798        &phev_utilization_params.adj_coef_map["2008"]
799    } else {
800        // assume 2017 coefficients are valid
801        &phev_utilization_params.adj_coef_map["2017"]
802    };
803    label_fe.adj_params = adj_params.clone();
804    match sim_data {
805        SimulationDataForLabel::ConvOrHev {
806            udds_mpgge,
807            hwy_mpgge,
808            fs_energy_capacity_kwh: fuel_storage_capacity_kwh,
809            ..
810        } => {
811            // compare to Excel 'VehicleIO'!C203 or 'VehicleIO'!labUddsMpgge
812            label_fe.lab_udds_mpgge = *udds_mpgge;
813            label_fe.lab_hwy_mpgge = *hwy_mpgge;
814            label_fe.lab_comb_mpgge = 1.0 / (0.55 / *udds_mpgge + 0.45 / *hwy_mpgge);
815            label_fe.lab_udds_kwh_per_mi = 0.0;
816            label_fe.lab_hwy_kwh_per_mi = 0.0;
817            label_fe.lab_comb_kwh_per_mi = 0.0;
818            // non-EV case
819            // CV or HEV case (not PHEV)
820            // HEV SOC iteration is handled in simdrive.SimDriveClassic
821            label_fe.adj_udds_mpgge =
822                1. / (adj_params.city_intercept + adj_params.city_slope / udds_mpgge);
823            // compare to Excel 'VehicleIO'!C203 or 'VehicleIO'!adjHwyMpgge
824            label_fe.adj_hwy_mpgge =
825                1. / (adj_params.hwy_intercept + adj_params.hwy_slope / hwy_mpgge);
826            label_fe.adj_comb_mpgge =
827                1. / (0.55 / label_fe.adj_udds_mpgge + 0.45 / label_fe.adj_hwy_mpgge);
828            let fuel_energy_gge = fuel_storage_capacity_kwh / fuel_props.kwh_per_gge();
829            label_fe.net_range_miles = fuel_energy_gge * label_fe.adj_comb_mpgge;
830        }
831        SimulationDataForLabel::Phev {
832            info, udds, hwy, ..
833        } => {
834            let mut phev_calcs = LabelFePHEV {
835                regen_soc_buffer: ((0.5 * info.veh_mass * ((60. * uc::MPH).powi(P2::new())))
836                    * info.phev_max_regen
837                    * info.em_peak_eff
838                    / info.energy_capacity)
839                    .min((info.max_soc - info.min_soc) / 2.0),
840                ..Default::default()
841            };
842            // UDDS
843            phev_calcs.udds = calculate_phev_label_helper(
844                info,
845                udds,
846                &fuel_props,
847                max_epa_adj,
848                &phev_utilization_params,
849                &adj_params,
850                &phev_calcs,
851                true,
852            )?;
853            // HWY
854            phev_calcs.hwy = calculate_phev_label_helper(
855                info,
856                hwy,
857                &fuel_props,
858                max_epa_adj,
859                &phev_utilization_params,
860                &adj_params,
861                &phev_calcs,
862                false,
863            )?;
864            // efficiency-related calculations
865            // lab
866            label_fe.lab_udds_mpgge = phev_calcs.udds.lab_mpgge;
867            label_fe.lab_hwy_mpgge = phev_calcs.hwy.lab_mpgge;
868            label_fe.lab_comb_mpgge =
869                1.0 / (0.55 / phev_calcs.udds.lab_mpgge + 0.45 / phev_calcs.hwy.lab_mpgge);
870
871            label_fe.lab_udds_kwh_per_mi = phev_calcs.udds.lab_kwh_per_mi;
872            label_fe.lab_hwy_kwh_per_mi = phev_calcs.hwy.lab_kwh_per_mi;
873            label_fe.lab_comb_kwh_per_mi =
874                0.55 * phev_calcs.udds.lab_kwh_per_mi + 0.45 * phev_calcs.hwy.lab_kwh_per_mi;
875
876            // adjusted
877            label_fe.adj_udds_mpgge = phev_calcs.udds.adj_mpgge;
878            label_fe.adj_hwy_mpgge = phev_calcs.hwy.adj_mpgge;
879            label_fe.adj_comb_mpgge =
880                1.0 / (0.55 / phev_calcs.udds.adj_mpgge + 0.45 / phev_calcs.hwy.adj_mpgge);
881
882            label_fe.adj_cs_comb_mpgge = Some(
883                1.0 / (0.55 / phev_calcs.udds.adj_cs_mpgge + 0.45 / phev_calcs.hwy.adj_cs_mpgge),
884            );
885            label_fe.adj_cd_comb_mpgge = Some(
886                1.0 / (0.55 / phev_calcs.udds.adj_cd_mpgge + 0.45 / phev_calcs.hwy.adj_cd_mpgge),
887            );
888
889            label_fe.adj_udds_kwh_per_mi = phev_calcs.udds.adj_kwh_per_mi;
890            label_fe.adj_hwy_kwh_per_mi = phev_calcs.hwy.adj_kwh_per_mi;
891            label_fe.adj_comb_kwh_per_mi =
892                0.55 * phev_calcs.udds.adj_kwh_per_mi + 0.45 * phev_calcs.hwy.adj_kwh_per_mi;
893
894            label_fe.adj_udds_ess_kwh_per_mi = phev_calcs.udds.adj_ess_kwh_per_mi;
895            label_fe.adj_hwy_ess_kwh_per_mi = phev_calcs.hwy.adj_ess_kwh_per_mi;
896            label_fe.adj_comb_ess_kwh_per_mi = 0.55 * phev_calcs.udds.adj_ess_kwh_per_mi
897                + 0.45 * phev_calcs.hwy.adj_ess_kwh_per_mi;
898
899            // range for combined city/highway
900            // utility factor (percent driving in charge depletion mode)
901            label_fe.uf = Some(
902                phev_utilization_params.uf_array[first_grtr(
903                    &phev_utilization_params.rechg_freq_miles,
904                    0.55 * phev_calcs.udds.adj_cd_miles + 0.45 * phev_calcs.hwy.adj_cd_miles,
905                )
906                .with_context(|| format_dbg!())?
907                    - 1],
908            );
909
910            label_fe.net_phev_cd_miles =
911                Some(0.55 * phev_calcs.udds.adj_cd_miles + 0.45 * phev_calcs.hwy.adj_cd_miles);
912
913            // For PHEVs, calculate net range as the sum of CD range and CS range
914            // Get CS range by determining how much fuel energy remains after depleting the battery
915            let fuel_energy_kwh = info.fuel_storage_capacity.get::<si::kilowatt_hour>();
916            let fuel_energy_gge = fuel_energy_kwh / fuel_props.kwh_per_gge();
917
918            label_fe.net_range_miles = (fuel_energy_gge
919                - label_fe.net_phev_cd_miles.with_context(|| format_dbg!())?
920                    / label_fe.adj_cd_comb_mpgge.with_context(|| format_dbg!())?)
921                * label_fe.adj_cs_comb_mpgge.with_context(|| format_dbg!())?
922                + label_fe.net_phev_cd_miles.with_context(|| format_dbg!())?;
923
924            label_fe.phev_calcs = Some(phev_calcs);
925        }
926        SimulationDataForLabel::Bev {
927            udds_kwh_per_mi,
928            hwy_kwh_per_mi,
929            bev_energy_capacity_kwh,
930            ..
931        } => {
932            label_fe.lab_udds_mpgge = 0.0;
933            label_fe.lab_hwy_mpgge = 0.0;
934            label_fe.lab_comb_mpgge = 0.0;
935            label_fe.lab_udds_kwh_per_mi = *udds_kwh_per_mi;
936            label_fe.lab_hwy_kwh_per_mi = *hwy_kwh_per_mi;
937            label_fe.lab_comb_kwh_per_mi = 0.55 * *udds_kwh_per_mi + 0.45 * *hwy_kwh_per_mi;
938            // EV case
939            // Mpgge is all zero for EV
940            label_fe.adj_udds_mpgge = 0.;
941            label_fe.adj_hwy_mpgge = 0.;
942            label_fe.adj_comb_mpgge = 0.;
943            // EV Case
944            label_fe.adj_udds_kwh_per_mi =
945                (1. / f64::max(
946                    1. / (adj_params.city_intercept
947                        + (adj_params.city_slope
948                            / ((1. / label_fe.lab_udds_kwh_per_mi) * fuel_props.kwh_per_gge()))),
949                    (1. / label_fe.lab_udds_kwh_per_mi)
950                        * fuel_props.kwh_per_gge()
951                        * (1. - max_epa_adj),
952                )) * fuel_props.kwh_per_gge()
953                    / DEFAULT_CHG_EFF;
954            label_fe.adj_hwy_kwh_per_mi =
955                (1. / f64::max(
956                    1. / (adj_params.hwy_intercept
957                        + (adj_params.hwy_slope
958                            / ((1. / label_fe.lab_hwy_kwh_per_mi) * fuel_props.kwh_per_gge()))),
959                    (1. / label_fe.lab_hwy_kwh_per_mi)
960                        * fuel_props.kwh_per_gge()
961                        * (1. - max_epa_adj),
962                )) * fuel_props.kwh_per_gge()
963                    / DEFAULT_CHG_EFF;
964            label_fe.adj_comb_kwh_per_mi =
965                0.55 * label_fe.adj_udds_kwh_per_mi + 0.45 * label_fe.adj_hwy_kwh_per_mi;
966
967            label_fe.adj_udds_ess_kwh_per_mi = label_fe.adj_udds_kwh_per_mi * DEFAULT_CHG_EFF;
968            label_fe.adj_hwy_ess_kwh_per_mi = label_fe.adj_hwy_kwh_per_mi * DEFAULT_CHG_EFF;
969            label_fe.adj_comb_ess_kwh_per_mi = label_fe.adj_comb_kwh_per_mi * DEFAULT_CHG_EFF;
970
971            // range for combined city/highway
972            // Get energy capacity from the proper powertrain
973            label_fe.net_range_miles = bev_energy_capacity_kwh / label_fe.adj_comb_ess_kwh_per_mi;
974        }
975    }
976
977    // process acceleration test data
978    label_fe.net_accel = get_0_to_60_time_from_accel_data(accel_data).map_err(|e| {
979        anyhow!(
980            "get_0_to_60_time_from_accel_data failed at line {} with originating error [{}]",
981            format_dbg!(),
982            e
983        )
984    })?;
985
986    // success Boolean -- did all of the tests work(e.g. met trace within ~2 mph)?
987    label_fe.res_found = String::from("model needs to be implemented for this");
988
989    Ok(label_fe)
990}
991
992fn run_simdrive_with_init_soc(
993    veh: &Vehicle,
994    cycle: &str,
995    init_soc: si::Ratio,
996) -> anyhow::Result<SimDrive> {
997    let mut sd = SimDrive::new(veh.clone(), Cycle::from_resource(cycle, false)?, None);
998    let res_mut = sd.veh.res_mut().with_context(|| format_dbg!())?;
999    res_mut.state.soc.mark_stale();
1000    res_mut.state.soc.update(init_soc, || format_dbg!())?;
1001    sd.reset_cumulative(|| format_dbg!())?;
1002    sd.reset_step(|| format_dbg!())?;
1003    sd.clear();
1004    sd.run_once().map_err(|e| {
1005        anyhow!(
1006            "run_simdrive_with_init_soc failed at line {} with originating error [{}]",
1007            format_dbg!(),
1008            e
1009        )
1010    })?;
1011    Ok(sd)
1012}
1013
1014/// Runs the appropriate simulations required for calculating
1015/// the label fuel economy for the given vehicle.
1016/// NOTE: does not run the acceleration test.
1017pub fn run_label_simulations(
1018    veh: &mut Vehicle,
1019    // max_epa_adj: Option<f64>,
1020    fuel_props: Option<FuelProperties>,
1021    phev_utilization_params: Option<PhevUtilizationParams>,
1022    sim_params: &HashMap<&'static str, SimParams>,
1023) -> anyhow::Result<(SimulationDataForLabel, HashMap<&'static str, SimDrive>)> {
1024    // let max_epa_adj = max_epa_adj.unwrap_or(0.3);
1025    let phev_utilization_params = &phev_utilization_params.unwrap_or_default();
1026    let fuel_props = fuel_props.unwrap_or_default();
1027
1028    let mut cyc: HashMap<&str, Cycle> = HashMap::new();
1029    let mut sd = HashMap::new();
1030    let mut label_fe = LabelFe::default();
1031
1032    label_fe.veh = Some(veh.clone());
1033
1034    // load the cycles and instantiate simdrive objects
1035    cyc.insert("accel", CYC_ACCEL.clone());
1036    cyc.insert("udds", Cycle::from_resource("udds.csv", false)?);
1037    cyc.insert("hwy", Cycle::from_resource("hwfet.csv", false)?);
1038
1039    if veh.pt_type.is_plug_in_hybrid_electric_vehicle() {
1040        let rm = veh.res_mut().unwrap();
1041        rm.state.soc.check_and_reset(|| format_dbg!()).unwrap();
1042        rm.state.soc.update(rm.max_soc, || format_dbg!()).unwrap();
1043    }
1044
1045    // run simdrive for non-phev powertrains
1046    sd.insert(
1047        "udds",
1048        SimDrive::new(
1049            veh.clone(),
1050            cyc["udds"].clone(),
1051            sim_params.get("udds").cloned(),
1052        ),
1053    );
1054    sd.insert(
1055        "hwy",
1056        SimDrive::new(
1057            veh.clone(),
1058            cyc["hwy"].clone(),
1059            sim_params.get("hwy").cloned(),
1060        ),
1061    );
1062
1063    for (k, val) in sd.iter_mut() {
1064        val.run().map_err(|e| {
1065            anyhow!(
1066                "run_label_simulations failed for key {} at line {} with originating error [{}]",
1067                k,
1068                format_dbg!(),
1069                e
1070            )
1071        })?;
1072    }
1073
1074    let veh_year = parse_model_year_or_2017(veh.year.as_deref());
1075
1076    // find year-based adjustment parameters
1077    let adj_params = if veh_year < 2017 {
1078        &phev_utilization_params.adj_coef_map["2008"]
1079    } else {
1080        // assume 2017 coefficients are valid
1081        &phev_utilization_params.adj_coef_map["2017"]
1082    };
1083    label_fe.adj_params = adj_params.clone();
1084
1085    // Check powertrain type
1086    let is_conv = matches!(veh.pt_type, PowertrainType::ConventionalVehicle(_));
1087    let is_hev = matches!(veh.pt_type, PowertrainType::HybridElectricVehicle(_));
1088    let is_phev = matches!(veh.pt_type, PowertrainType::PlugInHybridElectricVehicle(_));
1089    let is_bev = matches!(veh.pt_type, PowertrainType::BatteryElectricVehicle(_));
1090
1091    if is_hev || is_conv {
1092        Ok((
1093            SimulationDataForLabel::ConvOrHev {
1094                veh_year,
1095                udds_mpgge: sd["udds"].veh.mpg(fuel_props.energy_density)?,
1096                hwy_mpgge: sd["hwy"].veh.mpg(fuel_props.energy_density)?,
1097                fs_energy_capacity_kwh: veh
1098                    .pt_type
1099                    .fs()
1100                    .map(|fs| fs.energy_capacity.get::<si::kilowatt_hour>())
1101                    .unwrap_or(0.0),
1102            },
1103            sd,
1104        ))
1105    } else if is_bev {
1106        if let PowertrainType::BatteryElectricVehicle(bev) = &veh.pt_type {
1107            let res_energy_capacity_kwh =
1108                bev.res.energy_capacity_usable().get::<si::kilowatt_hour>();
1109            Ok((
1110                SimulationDataForLabel::Bev {
1111                    veh_year,
1112                    udds_kwh_per_mi: sd["udds"].veh.kwh_per_mi()?,
1113                    hwy_kwh_per_mi: sd["hwy"].veh.kwh_per_mi()?,
1114                    bev_energy_capacity_kwh: res_energy_capacity_kwh,
1115                },
1116                sd,
1117            ))
1118        } else {
1119            bail!("is_bev but powertrain not BEV")
1120        }
1121    } else if is_phev {
1122        // Get access to the PHEV powertrain
1123        let max_soc: si::Ratio;
1124        let min_soc: si::Ratio;
1125        let phev_max_regen: si::Ratio;
1126        let veh_mass: si::Mass;
1127        // equivalent to fastsim-2 `mc_peak_eff`
1128        let em_peak_eff: si::Ratio;
1129        // battery total energy capacity from soc of 1.0 to 0.0
1130        let energy_capacity: si::Energy;
1131        let chg_eff: f64;
1132        let fuel_storage_capacity: si::Energy;
1133        if let PowertrainType::PlugInHybridElectricVehicle(phev) = &veh.pt_type {
1134            max_soc = phev.res.max_soc;
1135            min_soc = phev.res.min_soc;
1136            phev_max_regen = 0.98 * uc::R;
1137            veh_mass = *veh.state.mass.get_fresh(|| format_dbg!())?;
1138            em_peak_eff = *phev
1139                .em
1140                .eff_interp_achieved
1141                .max()
1142                .with_context(|| format_dbg!())?
1143                * uc::R;
1144            energy_capacity = phev.res.energy_capacity;
1145            chg_eff = DEFAULT_CHG_EFF;
1146            fuel_storage_capacity = phev.fs.energy_capacity;
1147        } else {
1148            bail!("Vehicle is not a PHEV");
1149        }
1150
1151        // Create SimDrive objects for Charge Sustaining PHEV calculations
1152        let init_soc = min_soc + 0.01 * uc::R;
1153        let cs_udds_sd = run_simdrive_with_init_soc(veh, "udds.csv", init_soc)?;
1154        let cs_hwy_sd = run_simdrive_with_init_soc(veh, "hwfet.csv", init_soc)?;
1155        sd.insert("udds-cs", cs_udds_sd.clone());
1156        sd.insert("hwy-cs", cs_hwy_sd.clone());
1157        Ok((
1158            SimulationDataForLabel::Phev {
1159                veh_year,
1160                info: PhevVehicleInfo {
1161                    max_soc,
1162                    min_soc,
1163                    phev_max_regen,
1164                    veh_mass,
1165                    em_peak_eff,
1166                    energy_capacity,
1167                    chg_eff,
1168                    fuel_storage_capacity,
1169                },
1170                udds: PhevSimulationDataForLabel {
1171                    cd_fuel_consumed_kwh: {
1172                        if let Some(fc) = sd["udds"].veh.fc() {
1173                            fc.state
1174                                .energy_fuel
1175                                .get_fresh(|| format_dbg!())?
1176                                .get::<si::kilowatt_hour>()
1177                        } else {
1178                            0.0
1179                        }
1180                    },
1181                    cd_soc_start: {
1182                        if let Some(res) = sd["udds"].veh.res() {
1183                            res.history
1184                                .soc
1185                                .first()
1186                                .unwrap()
1187                                .get_fresh(|| format_dbg!())?
1188                                .get::<si::ratio>()
1189                        } else {
1190                            1.0
1191                        }
1192                    },
1193                    cd_soc_end: {
1194                        if let Some(res) = sd["udds"].veh.res() {
1195                            res.history
1196                                .soc
1197                                .last()
1198                                .unwrap()
1199                                .get_fresh(|| format_dbg!())?
1200                                .get::<si::ratio>()
1201                        } else {
1202                            0.0
1203                        }
1204                    },
1205                    cyc_dist_mi: {
1206                        sd["udds"]
1207                            .veh
1208                            .state
1209                            .dist
1210                            .get_fresh(|| format_dbg!())?
1211                            .get::<si::mile>()
1212                    },
1213                    cd_kwh_per_mi: sd["udds"].veh.kwh_per_mi()?,
1214                    cd_mpg: sd["udds"].veh.mpg(fuel_props.energy_density)?,
1215                    cs_fuel_consumed_kwh: {
1216                        if let Some(fc) = cs_udds_sd.veh.fc() {
1217                            fc.state
1218                                .energy_fuel
1219                                .get_fresh(|| format_dbg!())?
1220                                .get::<si::kilowatt_hour>()
1221                        } else {
1222                            0.0
1223                        }
1224                    },
1225                    cs_ess_energy_kwh: {
1226                        if let Some(res) = cs_udds_sd.veh.res() {
1227                            res.state
1228                                .energy_out_chemical
1229                                .get_fresh(|| format_dbg!())?
1230                                .get::<si::kilowatt_hour>()
1231                        } else {
1232                            0.0
1233                        }
1234                    },
1235                    cs_kwh_per_mi: cs_udds_sd.veh.kwh_per_mi()?,
1236                    cs_mpg: cs_udds_sd.veh.mpg(fuel_props.energy_density)?,
1237                    cs_min_soc: min_soc.get::<si::ratio>(),
1238                    cs_fs_energy_capacity_kwh: {
1239                        if let Some(fs) = veh.pt_type.fs() {
1240                            fs.energy_capacity.get::<si::kilowatt_hour>()
1241                        } else {
1242                            0.0
1243                        }
1244                    },
1245                },
1246                hwy: PhevSimulationDataForLabel {
1247                    cd_fuel_consumed_kwh: {
1248                        if let Some(fc) = sd["hwy"].veh.fc() {
1249                            fc.state
1250                                .energy_fuel
1251                                .get_fresh(|| format_dbg!())?
1252                                .get::<si::kilowatt_hour>()
1253                        } else {
1254                            0.0
1255                        }
1256                    },
1257                    cd_soc_start: {
1258                        if let Some(res) = sd["hwy"].veh.res() {
1259                            res.history
1260                                .soc
1261                                .first()
1262                                .unwrap()
1263                                .get_fresh(|| format_dbg!())?
1264                                .get::<si::ratio>()
1265                        } else {
1266                            1.0
1267                        }
1268                    },
1269                    cd_soc_end: {
1270                        if let Some(res) = sd["hwy"].veh.res() {
1271                            res.history
1272                                .soc
1273                                .last()
1274                                .unwrap()
1275                                .get_fresh(|| format_dbg!())?
1276                                .get::<si::ratio>()
1277                        } else {
1278                            0.0
1279                        }
1280                    },
1281                    cyc_dist_mi: {
1282                        sd["hwy"]
1283                            .veh
1284                            .state
1285                            .dist
1286                            .get_fresh(|| format_dbg!())?
1287                            .get::<si::mile>()
1288                    },
1289                    cd_kwh_per_mi: sd["hwy"].veh.kwh_per_mi()?,
1290                    cd_mpg: sd["hwy"].veh.mpg(fuel_props.energy_density)?,
1291                    cs_fuel_consumed_kwh: {
1292                        if let Some(fc) = cs_hwy_sd.veh.fc() {
1293                            fc.state
1294                                .energy_fuel
1295                                .get_fresh(|| format_dbg!())?
1296                                .get::<si::kilowatt_hour>()
1297                        } else {
1298                            0.0
1299                        }
1300                    },
1301                    cs_ess_energy_kwh: {
1302                        if let Some(res) = cs_hwy_sd.veh.res() {
1303                            res.state
1304                                .energy_out_chemical
1305                                .get_fresh(|| format_dbg!())?
1306                                .get::<si::kilowatt_hour>()
1307                        } else {
1308                            0.0
1309                        }
1310                    },
1311                    cs_kwh_per_mi: cs_hwy_sd.veh.kwh_per_mi()?,
1312                    cs_mpg: cs_hwy_sd.veh.mpg(fuel_props.energy_density)?,
1313                    cs_min_soc: min_soc.get::<si::ratio>(),
1314                    cs_fs_energy_capacity_kwh: {
1315                        if let Some(fs) = veh.pt_type.fs() {
1316                            fs.energy_capacity.get::<si::kilowatt_hour>()
1317                        } else {
1318                            0.0
1319                        }
1320                    },
1321                },
1322            },
1323            sd,
1324        ))
1325    } else {
1326        bail!("Unhandled powertrain type")
1327    }
1328}
1329
1330/// Generates label fuel economy (FE) values for a provided vehicle.
1331///
1332/// # Arguments
1333///
1334/// - `veh`: vehicle::Vehicle
1335/// - `full_detail`: boolean, default False
1336///   If True, sim_drive objects for each cycle are also returned.
1337/// - `verbose`: boolean, default false
1338///   If true, print out key results
1339///
1340/// Returns label fuel economy values as a struct and (optionally)
1341/// simdrive::SimDrive objects.
1342pub fn get_label_fe(
1343    veh: &mut Vehicle,
1344    max_epa_adj: Option<f64>,
1345    full_detail: bool,
1346    fuel_props: Option<FuelProperties>,
1347    phev_utilization_params: Option<PhevUtilizationParams>,
1348    sim_params: Option<HashMap<&'static str, SimParams>>,
1349    verbose: bool,
1350) -> anyhow::Result<(LabelFe, Option<HashMap<&'static str, SimDrive>>)> {
1351    let max_epa_adj = max_epa_adj.unwrap_or(0.3);
1352    let phev_utilization_params = &phev_utilization_params.unwrap_or_default();
1353    let fuel_props = fuel_props.unwrap_or_default();
1354    let veh_copy = veh.clone();
1355
1356    let sim_params = sim_params.unwrap_or_default();
1357
1358    let (sim_data, sd) = run_label_simulations(
1359        veh,
1360        Some(fuel_props.clone()),
1361        Some(phev_utilization_params.clone()),
1362        &sim_params,
1363    )?;
1364    let accel_data = run_accel(&veh_copy, &sim_params)?;
1365    let mut label_fe = calculate_label_fuel_economy(
1366        &fuel_props,
1367        phev_utilization_params,
1368        max_epa_adj,
1369        &sim_data,
1370        &accel_data,
1371    )?;
1372    label_fe.veh = Some(veh_copy);
1373
1374    if full_detail && verbose {
1375        println!("{label_fe:#?}");
1376        Ok((label_fe, Some(sd)))
1377    } else if full_detail {
1378        Ok((label_fe, Some(sd)))
1379    } else if verbose {
1380        println!("{label_fe:#?}");
1381        Ok((label_fe, None))
1382    } else {
1383        Ok((label_fe, None))
1384    }
1385}
1386
1387#[cfg(feature = "pyo3")]
1388#[pyfunction(name = "get_label_fe")]
1389#[cfg_attr(
1390    feature = "pyo3",
1391    pyo3(signature = (
1392        veh, max_epa_adj=None, full_detail=None, fuel_props=None, phev_utilization_params=None, sim_params=None, verbose=None))
1393)]
1394/// pyo3 version of [get_label_fe]
1395pub fn get_label_fe_py(
1396    veh: &mut Vehicle,
1397    max_epa_adj: Option<f64>,
1398    full_detail: Option<bool>,
1399    fuel_props: Option<FuelProperties>,
1400    phev_utilization_params: Option<PhevUtilizationParams>,
1401    sim_params: Option<HashMap<String, SimParams>>,
1402    verbose: Option<bool>,
1403) -> anyhow::Result<LabelFe> {
1404    let (label_fe, _) = get_label_fe(
1405        veh,
1406        max_epa_adj,
1407        full_detail.unwrap_or_default(),
1408        fuel_props,
1409        phev_utilization_params,
1410        sim_params
1411            .map(|m| {
1412                m.into_iter()
1413                    .map(|(k, v)| -> anyhow::Result<(&'static str, SimParams)> {
1414                        let key = match k.as_str() {
1415                            "udds" => "udds",
1416                            "hwy" => "hwy",
1417                            "accel" => "accel",
1418                            other => bail!("Unknown sim_params key: {other:?}"),
1419                        };
1420                        Ok((key, v))
1421                    })
1422                    .collect()
1423            })
1424            .transpose()?,
1425        verbose.unwrap_or_default(),
1426    )?;
1427    Ok(label_fe)
1428}
1429
1430/// PHEV-specific function for label fe.
1431///
1432/// # Arguments
1433/// - max_epa_adj: maximum EPA adjustment factor
1434///
1435/// # Returns
1436/// label fuel economy values for PHEV as a struct.
1437pub fn get_label_fe_phev(
1438    veh: &Vehicle,
1439    phev_utilization_params: &PhevUtilizationParams,
1440    adj_params: &AdjCoef,
1441    max_epa_adj: f64,
1442    fuel_props: &FuelProperties,
1443) -> anyhow::Result<LabelFePHEV> {
1444    // Get access to the PHEV powertrain
1445    let max_soc: si::Ratio;
1446    let min_soc: si::Ratio;
1447    let phev_max_regen: si::Ratio;
1448    let veh_mass: si::Mass;
1449    // equivalent to fastsim-2 `mc_peak_eff`
1450    let em_peak_eff: si::Ratio;
1451    // battery total energy capacity from soc of 1.0 to 0.0
1452    let energy_capacity: si::Energy;
1453    let chg_eff: f64;
1454
1455    if let PowertrainType::PlugInHybridElectricVehicle(phev) = &veh.pt_type {
1456        max_soc = phev.res.max_soc;
1457        min_soc = phev.res.min_soc;
1458        phev_max_regen = 0.98 * uc::R;
1459        veh_mass = *veh.state.mass.get_fresh(|| format_dbg!())?;
1460        em_peak_eff = *phev
1461            .em
1462            .eff_interp_achieved
1463            .max()
1464            .with_context(|| format_dbg!())?
1465            * uc::R;
1466        energy_capacity = phev.res.energy_capacity;
1467        chg_eff = DEFAULT_CHG_EFF; // Use default charging efficiency
1468    } else {
1469        bail!("Vehicle is not a PHEV");
1470    }
1471
1472    let mut label_fe_phev = LabelFePHEV {
1473        regen_soc_buffer: ((0.5 * veh_mass * ((60. * uc::MPH).powi(P2::new())))
1474            * phev_max_regen
1475            * em_peak_eff
1476            / energy_capacity)
1477            .min((max_soc - min_soc) / 2.0),
1478        ..Default::default()
1479    };
1480
1481    // Create SimDrive objects for PHEV calculations
1482    let mut sd: HashMap<&str, SimDrive> = HashMap::new();
1483    sd.insert(
1484        "udds",
1485        SimDrive::new(veh.clone(), Cycle::from_resource("udds.csv", false)?, None),
1486    );
1487    sd.insert(
1488        "hwy",
1489        SimDrive::new(veh.clone(), Cycle::from_resource("hwfet.csv", false)?, None),
1490    );
1491
1492    // charge sustaining behavior
1493    for (key, sd) in sd.iter_mut() {
1494        // do PHEV soc iteration
1495        // This runs 1 cycle starting at max SOC then runs 1 cycle starting at min SOC.
1496        // By assuming that the battery SOC depletion per mile is constant across cycles,
1497        // the first cycle can be extrapolated until charge sustaining kicks in.
1498        sd.run()?;
1499        let mut phev_calc = PHEVCycleCalc::default();
1500
1501        // charge depletion cycle has already been simulated
1502        // charge depletion battery kW-hr
1503        phev_calc.cd_ess_kwh = ((max_soc - min_soc) * energy_capacity).get::<si::kilowatt_hour>();
1504
1505        // Get SOC and distance values
1506        let res = sd.veh.res().with_context(|| format_dbg!())?;
1507        let soc_start = *res
1508            .history
1509            .soc
1510            .first()
1511            .with_context(|| format_dbg!())?
1512            .get_fresh(|| format_dbg!())?;
1513        let soc_end = *res
1514            .history
1515            .soc
1516            .last()
1517            .with_context(|| format_dbg!())?
1518            .get_fresh(|| format_dbg!())?;
1519        let dist_mi = sd
1520            .veh
1521            .state
1522            .dist
1523            .get_fresh(|| format_dbg!())?
1524            .get::<si::mile>();
1525
1526        // SOC change during 1 cycle
1527        phev_calc.delta_soc = soc_start - soc_end;
1528        // total number of miles in charge depletion mode, assuming constant kWh_per_mi
1529        phev_calc.total_cd_miles = ((max_soc - min_soc) * energy_capacity)
1530            .get::<si::kilowatt_hour>()
1531            / sd.veh.kwh_per_mi()?;
1532        // number of cycles in charge depletion mode, up to transition
1533        phev_calc.cd_cycs = phev_calc.total_cd_miles / dist_mi;
1534        // fraction of transition cycle spent in charge depletion
1535        phev_calc.cd_frac_in_trans = phev_calc.cd_cycs % phev_calc.cd_cycs.floor();
1536
1537        // charge depletion fuel gallons - get from fuel converter
1538        let fuel_energy_kwh = if let Some(fc) = sd.veh.fc() {
1539            fc.state
1540                .energy_fuel
1541                .get_fresh(|| format_dbg!())?
1542                .get::<si::kilowatt_hour>()
1543        } else {
1544            0.0
1545        };
1546        phev_calc.cd_fs_gal = fuel_energy_kwh / fuel_props.kwh_per_gge();
1547        phev_calc.cd_fs_kwh = fuel_energy_kwh;
1548        phev_calc.cd_ess_kwh_per_mi = sd.veh.kwh_per_mi()?;
1549        phev_calc.cd_mpg = sd.veh.mpg(fuel_props.energy_density)?;
1550
1551        // utility factor calculation for last charge depletion iteration and transition iteration
1552        // ported from excel
1553        let interp_x_vals: Vec<f64> = (0..((phev_calc.cd_cycs.ceil() + 1.0) as usize))
1554            .map(|i| i as f64 * dist_mi)
1555            .collect();
1556
1557        phev_calc.lab_iter_uf = vec![];
1558        for x in interp_x_vals {
1559            phev_calc.lab_iter_uf.push(
1560                phev_utilization_params.uf_array[first_grtr(
1561                    &phev_utilization_params.rechg_freq_miles,
1562                    x,
1563                )
1564                .with_context(|| format_dbg!())?
1565                    - 1],
1566            );
1567        }
1568
1569        // transition cycle
1570        phev_calc.trans_init_soc = max_soc - phev_calc.cd_cycs.floor() * phev_calc.delta_soc;
1571
1572        // run the transition cycle by setting initial SOC
1573        let res_mut = sd.veh.res_mut().with_context(|| format_dbg!())?;
1574        res_mut.state.soc.mark_stale();
1575        res_mut
1576            .state
1577            .soc
1578            .update(phev_calc.trans_init_soc, || format_dbg!())?;
1579        sd.reset_cumulative(|| format_dbg!())?;
1580        sd.reset_step(|| format_dbg!())?;
1581        sd.clear();
1582        sd.run_once().map_err(|err| {
1583            anyhow!(
1584                "run_once failed at line {} with originating error {}",
1585                format_dbg!(),
1586                err
1587            )
1588        })?;
1589
1590        // charge depletion battery kW-hr
1591        phev_calc.trans_ess_kwh =
1592            phev_calc.cd_ess_kwh_per_mi * dist_mi * phev_calc.cd_frac_in_trans;
1593        phev_calc.trans_ess_kwh_per_mi = phev_calc.cd_ess_kwh_per_mi * phev_calc.cd_frac_in_trans;
1594
1595        // charge sustaining
1596        // the 0.01 is here to be consistent with Excel
1597        let init_soc = min_soc + 0.01 * uc::R;
1598        let res_mut = sd.veh.res_mut().with_context(|| format_dbg!())?;
1599        res_mut.state.soc.mark_stale();
1600        res_mut.state.soc.update(init_soc, || format_dbg!())?;
1601        sd.reset_cumulative(|| format_dbg!())?;
1602        sd.reset_step(|| format_dbg!())?;
1603        sd.clear();
1604        sd.run_once().map_err(|err| {
1605            anyhow!(
1606                "run_once failed at line {} with originating error {}",
1607                format_dbg!(),
1608                err
1609            )
1610        })?;
1611
1612        // charge sustaining fuel gallons
1613        let cs_fuel_energy_kwh = if let Some(fc) = sd.veh.fc() {
1614            fc.state
1615                .energy_fuel
1616                .get_fresh(|| format_dbg!())?
1617                .get::<si::kilowatt_hour>()
1618        } else {
1619            0.0
1620        };
1621        phev_calc.cs_fs_gal = cs_fuel_energy_kwh / fuel_props.kwh_per_gge();
1622        // charge depletion fuel gallons, dependent on phev_calc.trans_fs_gal
1623        phev_calc.trans_fs_gal = phev_calc.cs_fs_gal * (1.0 - phev_calc.cd_frac_in_trans);
1624        phev_calc.cs_fs_kwh = cs_fuel_energy_kwh;
1625        phev_calc.trans_fs_kwh = phev_calc.cs_fs_kwh * (1.0 - phev_calc.cd_frac_in_trans);
1626        // charge sustaining battery kW-hr
1627        let cs_ess_energy_kwh = if let Some(res) = sd.veh.res() {
1628            res.state
1629                .energy_out_chemical
1630                .get_fresh(|| format_dbg!())?
1631                .get::<si::kilowatt_hour>()
1632        } else {
1633            0.0
1634        };
1635        phev_calc.cs_ess_kwh = cs_ess_energy_kwh;
1636        phev_calc.cs_ess_kwh_per_mi = sd.veh.kwh_per_mi()?;
1637
1638        let lab_iter_uf_diff = phev_calc.lab_iter_uf.diff();
1639        phev_calc.lab_uf_gpm = [
1640            phev_calc.trans_fs_gal * lab_iter_uf_diff.last().with_context(|| format_dbg!())?,
1641            phev_calc.cs_fs_gal
1642                * (1.0
1643                    - phev_calc
1644                        .lab_iter_uf
1645                        .last()
1646                        .with_context(|| format_dbg!())?),
1647        ]
1648        .iter()
1649        .map(|x| *x / dist_mi)
1650        .collect();
1651
1652        // TODO: check that below is correct. cd_mpg was already set above and cs_mpg is set below...
1653        // phev_calc.cd_mpg = sd.veh.mpg(fuel_props.energy_density)?;
1654
1655        // city and highway cycle ranges
1656        let min_soc_in_cycle = phev_calc.delta_soc.abs(); // Use delta_soc as proxy for min SOC change
1657        phev_calc.cd_miles =
1658            if (max_soc - label_fe_phev.regen_soc_buffer - min_soc_in_cycle) < 0.01 * uc::R {
1659                1000.0
1660            } else {
1661                phev_calc.cd_cycs.ceil() * dist_mi
1662            };
1663        phev_calc.cd_lab_mpg = phev_calc
1664            .lab_iter_uf
1665            .last()
1666            .with_context(|| format_dbg!())?
1667            / (phev_calc.trans_fs_gal / dist_mi);
1668
1669        // charge sustaining
1670        phev_calc.cs_mpg = dist_mi / phev_calc.cs_fs_gal;
1671
1672        phev_calc.lab_uf = phev_utilization_params.uf_array[first_grtr(
1673            &phev_utilization_params.rechg_freq_miles,
1674            phev_calc.cd_miles,
1675        )
1676        .with_context(|| format_dbg!())?
1677            - 1];
1678
1679        // labCombMpgge
1680        phev_calc.cd_adj_mpg =
1681            phev_calc.lab_iter_uf.max()? / phev_calc.lab_uf_gpm[phev_calc.lab_uf_gpm.len() - 2];
1682
1683        phev_calc.lab_mpgge = 1.0
1684            / (phev_calc.lab_uf / phev_calc.cd_adj_mpg
1685                + (1.0 - phev_calc.lab_uf) / phev_calc.cs_mpg);
1686
1687        let mut lab_iter_kwh_per_mi_vals = Vec::new();
1688        lab_iter_kwh_per_mi_vals.push(0.0);
1689        lab_iter_kwh_per_mi_vals
1690            .extend(vec![phev_calc.cd_ess_kwh_per_mi; phev_calc.cd_cycs.floor() as usize].iter());
1691        lab_iter_kwh_per_mi_vals.push(phev_calc.trans_ess_kwh_per_mi);
1692        lab_iter_kwh_per_mi_vals.push(0.0);
1693        phev_calc.lab_iter_kwh_per_mi = lab_iter_kwh_per_mi_vals;
1694
1695        let uf_diff = phev_calc.lab_iter_uf.diff();
1696        let mut vals = Vec::new();
1697        vals.push(0.0);
1698        for i in 1..phev_calc.lab_iter_kwh_per_mi.len() - 1 {
1699            if i - 1 < uf_diff.len() {
1700                vals.push(phev_calc.lab_iter_kwh_per_mi[i] * uf_diff[i - 1]);
1701            }
1702        }
1703        vals.push(0.0);
1704        phev_calc.lab_iter_uf_kwh_per_mi = vals;
1705
1706        phev_calc.lab_kwh_per_mi = phev_calc
1707            .lab_iter_uf_kwh_per_mi
1708            .iter()
1709            .fold(0.0, |acc, x| acc + x)
1710            / phev_calc
1711                .lab_iter_uf
1712                .iter()
1713                .fold(0.0f64, |acc, x| acc.max(*x));
1714
1715        let mut adj_iter_mpgge_vals = vec![0.0; phev_calc.cd_cycs.floor() as usize];
1716        let mut adj_iter_kwh_per_mi_vals = vec![0.0; phev_calc.lab_iter_kwh_per_mi.len()];
1717        if *key == "udds" {
1718            adj_iter_mpgge_vals.push(f64::max(
1719                1.0 / (adj_params.city_intercept
1720                    + (adj_params.city_slope
1721                        / (sd
1722                            .veh
1723                            .state
1724                            .dist
1725                            .get_fresh(|| format_dbg!())?
1726                            .get::<si::mile>()
1727                            / (phev_calc.trans_fs_kwh / fuel_props.kwh_per_gge())))),
1728                sd.veh
1729                    .state
1730                    .dist
1731                    .get_fresh(|| format_dbg!())?
1732                    .get::<si::mile>()
1733                    / (phev_calc.trans_fs_kwh / fuel_props.kwh_per_gge())
1734                    * (1.0 - max_epa_adj),
1735            ));
1736            adj_iter_mpgge_vals.push(f64::max(
1737                1.0 / (adj_params.city_intercept
1738                    + (adj_params.city_slope
1739                        / (sd
1740                            .veh
1741                            .state
1742                            .dist
1743                            .get_fresh(|| format_dbg!())?
1744                            .get::<si::mile>()
1745                            / (phev_calc.cs_fs_kwh / fuel_props.kwh_per_gge())))),
1746                sd.veh
1747                    .state
1748                    .dist
1749                    .get_fresh(|| format_dbg!())?
1750                    .get::<si::mile>()
1751                    / (phev_calc.cs_fs_kwh / fuel_props.kwh_per_gge())
1752                    * (1.0 - max_epa_adj),
1753            ));
1754
1755            for (c, _) in phev_calc.lab_iter_kwh_per_mi.iter().enumerate() {
1756                if phev_calc.lab_iter_kwh_per_mi[c] == 0.0 {
1757                    adj_iter_kwh_per_mi_vals[c] = 0.0;
1758                } else {
1759                    adj_iter_kwh_per_mi_vals[c] =
1760                        (1.0 / f64::max(
1761                            1.0 / (adj_params.city_intercept
1762                                + (adj_params.city_slope
1763                                    / ((1.0 / phev_calc.lab_iter_kwh_per_mi[c])
1764                                        * fuel_props.kwh_per_gge()))),
1765                            (1.0 - max_epa_adj)
1766                                * ((1.0 / phev_calc.lab_iter_kwh_per_mi[c])
1767                                    * fuel_props.kwh_per_gge()),
1768                        )) * fuel_props.kwh_per_gge();
1769                }
1770            }
1771        } else {
1772            adj_iter_mpgge_vals.push(f64::max(
1773                1.0 / (adj_params.hwy_intercept
1774                    + (adj_params.hwy_slope
1775                        / (sd
1776                            .veh
1777                            .state
1778                            .dist
1779                            .get_fresh(|| format_dbg!())?
1780                            .get::<si::mile>()
1781                            / (phev_calc.trans_fs_kwh / fuel_props.kwh_per_gge())))),
1782                sd.veh
1783                    .state
1784                    .dist
1785                    .get_fresh(|| format_dbg!())?
1786                    .get::<si::mile>()
1787                    / (phev_calc.trans_fs_kwh / fuel_props.kwh_per_gge())
1788                    * (1.0 - max_epa_adj),
1789            ));
1790            adj_iter_mpgge_vals.push(f64::max(
1791                1.0 / (adj_params.hwy_intercept
1792                    + (adj_params.hwy_slope
1793                        / (sd
1794                            .veh
1795                            .state
1796                            .dist
1797                            .get_fresh(|| format_dbg!())?
1798                            .get::<si::mile>()
1799                            / (phev_calc.cs_fs_kwh / fuel_props.kwh_per_gge())))),
1800                sd.veh
1801                    .state
1802                    .dist
1803                    .get_fresh(|| format_dbg!())?
1804                    .get::<si::mile>()
1805                    / (phev_calc.cs_fs_kwh / fuel_props.kwh_per_gge())
1806                    * (1.0 - max_epa_adj),
1807            ));
1808
1809            for (c, _) in phev_calc.lab_iter_kwh_per_mi.iter().enumerate() {
1810                if phev_calc.lab_iter_kwh_per_mi[c] == 0.0 {
1811                    adj_iter_kwh_per_mi_vals[c] = 0.0;
1812                } else {
1813                    adj_iter_kwh_per_mi_vals[c] =
1814                        (1.0 / f64::max(
1815                            1.0 / (adj_params.hwy_intercept
1816                                + (adj_params.hwy_slope
1817                                    / ((1.0 / phev_calc.lab_iter_kwh_per_mi[c])
1818                                        * fuel_props.kwh_per_gge()))),
1819                            (1.0 - max_epa_adj)
1820                                * ((1.0 / phev_calc.lab_iter_kwh_per_mi[c])
1821                                    * fuel_props.kwh_per_gge()),
1822                        )) * fuel_props.kwh_per_gge();
1823                }
1824            }
1825        }
1826        phev_calc.adj_iter_mpgge = adj_iter_mpgge_vals;
1827        phev_calc.adj_iter_kwh_per_mi = adj_iter_kwh_per_mi_vals;
1828
1829        phev_calc.adj_iter_cd_miles = vec![0.0; phev_calc.cd_cycs.ceil() as usize + 2];
1830        for c in 0..phev_calc.adj_iter_cd_miles.len() {
1831            if c == 0 {
1832                phev_calc.adj_iter_cd_miles[c] = 0.0;
1833            } else if c <= phev_calc.cd_cycs.floor() as usize {
1834                phev_calc.adj_iter_cd_miles[c] = phev_calc.adj_iter_cd_miles[c - 1]
1835                    + phev_calc.cd_ess_kwh_per_mi
1836                        * sd.veh
1837                            .state
1838                            .dist
1839                            .get_fresh(|| format_dbg!())?
1840                            .get::<si::mile>()
1841                        / phev_calc.adj_iter_kwh_per_mi[c];
1842            } else if c == phev_calc.cd_cycs.floor() as usize + 1 {
1843                phev_calc.adj_iter_cd_miles[c] = phev_calc.adj_iter_cd_miles[c - 1]
1844                    + phev_calc.trans_ess_kwh_per_mi
1845                        * sd.veh
1846                            .state
1847                            .dist
1848                            .get_fresh(|| format_dbg!())?
1849                            .get::<si::mile>()
1850                        / phev_calc.adj_iter_kwh_per_mi[c];
1851            } else {
1852                phev_calc.adj_iter_cd_miles[c] = 0.0;
1853            }
1854        }
1855
1856        let mut soc_hist: Vec<f64> = vec![];
1857        for soc in sd
1858            .veh
1859            .res()
1860            .with_context(|| format_dbg!())?
1861            .history
1862            .soc
1863            .clone()
1864        {
1865            soc_hist.push(soc.get_fresh(|| format_dbg!())?.get::<si::ratio>());
1866        }
1867
1868        phev_calc.adj_cd_miles =
1869            if max_soc - label_fe_phev.regen_soc_buffer - (*soc_hist.min()? * uc::R) < 0.01 * uc::R
1870            {
1871                1000.0
1872            } else {
1873                *phev_calc.adj_iter_cd_miles.max()?
1874            };
1875
1876        // utility factor calculation for last charge depletion iteration and transition iteration
1877        // ported from excel
1878
1879        phev_calc.adj_iter_uf = vec![];
1880        for x in phev_calc.adj_iter_cd_miles.clone() {
1881            phev_calc.adj_iter_uf.push(
1882                phev_utilization_params.uf_array[first_grtr(
1883                    &phev_utilization_params.rechg_freq_miles,
1884                    x,
1885                )
1886                .with_context(|| format_dbg!())?
1887                    - 1],
1888            )
1889        }
1890
1891        let adj_iter_uf_diff = phev_calc.adj_iter_uf.diff();
1892        phev_calc.adj_iter_uf_gpm = vec![0.0; phev_calc.cd_cycs.floor() as usize];
1893        phev_calc.adj_iter_uf_gpm.push(
1894            (1.0 / phev_calc.adj_iter_mpgge[phev_calc.adj_iter_mpgge.len() - 2])
1895                * adj_iter_uf_diff[adj_iter_uf_diff.len() - 2],
1896        );
1897        phev_calc.adj_iter_uf_gpm.push(
1898            (1.0 / phev_calc
1899                .adj_iter_mpgge
1900                .last()
1901                .with_context(|| format_dbg!())?)
1902                * (1.0 - phev_calc.adj_iter_uf[phev_calc.adj_iter_uf.len() - 2]),
1903        );
1904
1905        let adj_uf_diff = phev_calc.adj_iter_uf.diff();
1906        phev_calc.adj_iter_uf_kwh_per_mi = phev_calc
1907            .adj_iter_kwh_per_mi
1908            .iter()
1909            .zip(adj_uf_diff.iter())
1910            .map(|(kwh, uf)| kwh * uf)
1911            .collect();
1912
1913        let max_uf: f64 = phev_calc
1914            .adj_iter_uf
1915            .iter()
1916            .fold(0.0f64, |acc, x| acc.max(*x));
1917        phev_calc.adj_cd_mpgge =
1918            1.0 / phev_calc.adj_iter_uf_gpm[phev_calc.adj_iter_uf_gpm.len() - 2] * max_uf;
1919        phev_calc.adj_cs_mpgge = 1.0
1920            / phev_calc
1921                .adj_iter_uf_gpm
1922                .last()
1923                .with_context(|| format_dbg!())?
1924            * (1.0 - max_uf);
1925
1926        phev_calc.adj_uf = phev_utilization_params.uf_array[first_grtr(
1927            &phev_utilization_params.rechg_freq_miles,
1928            phev_calc.adj_cd_miles,
1929        )
1930        .with_context(|| format_dbg!())?
1931            - 1];
1932
1933        phev_calc.adj_mpgge = 1.0
1934            / (phev_calc.adj_uf / phev_calc.adj_cd_mpgge
1935                + (1.0 - phev_calc.adj_uf) / phev_calc.adj_cs_mpgge);
1936
1937        let uf_kwh_sum: f64 = phev_calc
1938            .adj_iter_uf_kwh_per_mi
1939            .iter()
1940            .fold(0.0, |acc, x| acc + x);
1941        phev_calc.adj_kwh_per_mi = uf_kwh_sum / max_uf / chg_eff;
1942
1943        phev_calc.adj_ess_kwh_per_mi = uf_kwh_sum / max_uf;
1944
1945        match *key {
1946            "udds" => label_fe_phev.udds = phev_calc.clone(),
1947            "hwy" => label_fe_phev.hwy = phev_calc.clone(),
1948            &_ => bail!("No field for cycle {}", key),
1949        };
1950    }
1951
1952    Ok(label_fe_phev)
1953}
1954
1955#[cfg(test)]
1956mod tests {
1957    use super::*;
1958
1959    #[cfg(feature = "compat")]
1960    use crate::compat::fastsim_2::fastsim_core::traits::SerdeAPI;
1961
1962    pub struct Tolerances {
1963        pub udds_tolerance: f64,
1964        pub comb_tolerance: f64,
1965        pub hwy_tolerance: f64,
1966        pub accel_tolerance: f64,
1967    }
1968
1969    #[cfg(feature = "compat")]
1970    fn assert_labels_match_within_tolerance(
1971        label_fe_f3: &LabelFe,
1972        label_fe_f2: &crate::compat::fastsim_2::fastsim_core::simdrivelabel::LabelFe,
1973        tol: &Tolerances,
1974        all_electric: bool,
1975    ) {
1976        let mut all_passed: bool = true;
1977        let mut message: String = String::new();
1978        // Check MPGe values for HEV
1979        if all_electric {
1980            let udds_err = (label_fe_f3.lab_udds_kwh_per_mi - label_fe_f2.lab_udds_kwh_per_mi)
1981                .abs()
1982                / label_fe_f2.lab_udds_kwh_per_mi;
1983            let test = udds_err < tol.udds_tolerance;
1984            all_passed = all_passed && test;
1985            message = format!(
1986                "{}\n{}UDDS kWh/mi mismatch: F3={:.3}, F2={:.3}; err = {:.3} (> tol {:.3})",
1987                message,
1988                if test { "  " } else { "* " },
1989                label_fe_f3.lab_udds_kwh_per_mi,
1990                label_fe_f2.lab_udds_kwh_per_mi,
1991                udds_err,
1992                tol.udds_tolerance
1993            );
1994            let comb_err = (label_fe_f3.lab_comb_kwh_per_mi - label_fe_f2.lab_comb_kwh_per_mi)
1995                .abs()
1996                / label_fe_f2.lab_comb_kwh_per_mi;
1997            let test = comb_err < tol.comb_tolerance;
1998            all_passed = all_passed && test;
1999            message = format!(
2000                "{}\n{}Combined kWh/mi mismatch: F3={:.3}, F2={:.3}; err = {:.3} (> tol {:.3})",
2001                message,
2002                if test { "  " } else { "* " },
2003                label_fe_f3.lab_comb_kwh_per_mi,
2004                label_fe_f2.lab_comb_kwh_per_mi,
2005                comb_err,
2006                tol.comb_tolerance
2007            );
2008            let hwy_err = (label_fe_f3.lab_hwy_kwh_per_mi - label_fe_f2.lab_hwy_kwh_per_mi).abs()
2009                / label_fe_f2.lab_hwy_kwh_per_mi;
2010            let test = hwy_err < tol.hwy_tolerance;
2011            all_passed = all_passed && test;
2012            message = format!(
2013                "{}\n{}HWY kWh/mi mismatch: F3={:.3}, F2={:.3}; err = {:.3} (> tol {:.3})",
2014                message,
2015                if test { "  " } else { "* " },
2016                label_fe_f3.lab_hwy_kwh_per_mi,
2017                label_fe_f2.lab_hwy_kwh_per_mi,
2018                hwy_err,
2019                tol.hwy_tolerance
2020            );
2021        } else {
2022            let udds_err = (label_fe_f3.lab_udds_mpgge - label_fe_f2.lab_udds_mpgge).abs()
2023                / label_fe_f2.lab_udds_mpgge;
2024            let test = udds_err < tol.udds_tolerance;
2025            all_passed = all_passed && test;
2026            message = format!(
2027                "{}\n{}UDDS MPGe mismatch: F3={:.3}, F2={:.3}; err = {:.3} (> tol {:.3})",
2028                message,
2029                if test { "  " } else { "* " },
2030                label_fe_f3.lab_udds_mpgge,
2031                label_fe_f2.lab_udds_mpgge,
2032                udds_err,
2033                tol.udds_tolerance
2034            );
2035            let comb_err = (label_fe_f3.lab_comb_mpgge - label_fe_f2.lab_comb_mpgge).abs()
2036                / label_fe_f2.lab_comb_mpgge;
2037            let test = comb_err < tol.comb_tolerance;
2038            all_passed = all_passed && test;
2039            message = format!(
2040                "{}\n{}Combined MPGe mismatch: F3={:.3}, F2={:.3}; err = {:.3} (> tol {:.3})",
2041                message,
2042                if test { "  " } else { "* " },
2043                label_fe_f3.lab_comb_mpgge,
2044                label_fe_f2.lab_comb_mpgge,
2045                comb_err,
2046                tol.comb_tolerance
2047            );
2048            let hwy_err = (label_fe_f3.lab_hwy_mpgge - label_fe_f2.lab_hwy_mpgge).abs()
2049                / label_fe_f2.lab_hwy_mpgge;
2050            let test = hwy_err < tol.hwy_tolerance;
2051            all_passed = all_passed && test;
2052            message = format!(
2053                "{}\n{}Hwy MPGe mismatch: F3={:.3}, F2={:.3}; err = {:.3} (> tol {:.3})",
2054                message,
2055                if test { "  " } else { "* " },
2056                label_fe_f3.lab_hwy_mpgge,
2057                label_fe_f2.lab_hwy_mpgge,
2058                hwy_err,
2059                tol.hwy_tolerance
2060            );
2061        }
2062        let accel_err =
2063            (label_fe_f3.net_accel - label_fe_f2.net_accel).abs() / label_fe_f2.net_accel;
2064        let test = accel_err < tol.accel_tolerance;
2065        all_passed = all_passed && test;
2066        message = format!(
2067            "{}\n{}Acceleration time mismatch: F3={:.3}, F2={:.3}; err = {:.3} (> tol {:.3})",
2068            message,
2069            if test { "  " } else { "* " },
2070            label_fe_f3.net_accel,
2071            label_fe_f2.net_accel,
2072            accel_err,
2073            tol.accel_tolerance
2074        );
2075        assert!(all_passed, "Individual Test Results:\n{}", message);
2076    }
2077
2078    /// Test that label FE calculations for conventional vehicles match FASTSim-2 results
2079    #[test]
2080    #[cfg(all(feature = "compat", feature = "resources", feature = "yaml"))]
2081    fn test_label_fe_conv_vs_fastsim2() {
2082        let file_contents = crate::compat::fastsim_2::ASSETS_DIR
2083            .get_file("vehicles/2012_Ford_Fusion.yaml")
2084            .unwrap()
2085            .contents();
2086        let f2veh = crate::compat::fastsim_2::fastsim_core::vehicle::RustVehicle::from_reader(
2087            file_contents,
2088            "yaml",
2089            false,
2090        )
2091        .unwrap();
2092        let mut veh = Vehicle::try_from(f2veh.clone()).unwrap();
2093
2094        // Get FASTSim-3 label FE results
2095        let (label_fe_f3, _) = get_label_fe(&mut veh, None, false, None, None, None, false)
2096            .with_context(|| format_dbg!())
2097            .unwrap();
2098
2099        // Get FASTSim-2 label FE results
2100        let (label_fe_f2, _) = crate::compat::fastsim_2::fastsim_core::simdrivelabel::get_label_fe(
2101            &f2veh.clone(),
2102            None,
2103            None,
2104        )
2105        .with_context(|| format_dbg!())
2106        .unwrap();
2107
2108        let tol = Tolerances {
2109            udds_tolerance: 0.03, // 3% tolerance
2110            comb_tolerance: 0.03,
2111            hwy_tolerance: 0.03,
2112            accel_tolerance: 0.06, // bumped from 0.05: F3 accel is now faster due to speed flooring fix
2113        };
2114
2115        assert_labels_match_within_tolerance(&label_fe_f3, &label_fe_f2, &tol, false);
2116
2117        println!("Conventional vehicle label FE test passed!");
2118        println!(
2119            "F3 Combined MPGe: {:.3}, F2: {:.3}",
2120            label_fe_f3.lab_comb_mpgge, label_fe_f2.lab_comb_mpgge
2121        );
2122    }
2123
2124    /// Test that label FE calculations for BEV vehicles match FASTSim-2 results
2125    #[test]
2126    #[cfg(all(feature = "compat", feature = "resources", feature = "yaml"))]
2127    fn test_label_fe_bev_vs_fastsim2() {
2128        let file_contents = crate::compat::fastsim_2::ASSETS_DIR
2129            .get_file("vehicles/2022_Renault_Zoe_ZE50_R135.yaml")
2130            .unwrap()
2131            .contents();
2132        let f2veh = crate::compat::fastsim_2::fastsim_core::vehicle::RustVehicle::from_reader(
2133            file_contents,
2134            "yaml",
2135            false,
2136        )
2137        .unwrap();
2138        let mut veh = Vehicle::try_from(f2veh.clone()).unwrap();
2139
2140        // Get FASTSim-3 label FE results
2141        let (label_fe_f3, _) = get_label_fe(&mut veh, None, false, None, None, None, false)
2142            .with_context(|| format_dbg!())
2143            .unwrap();
2144
2145        let (label_fe_f2, _) = crate::compat::fastsim_2::fastsim_core::simdrivelabel::get_label_fe(
2146            &f2veh.clone(),
2147            None,
2148            None,
2149        )
2150        .with_context(|| format_dbg!())
2151        .unwrap();
2152
2153        let tol = Tolerances {
2154            udds_tolerance: 0.011, // 1.1% tolerance
2155            comb_tolerance: 0.011,
2156            hwy_tolerance: 0.011,
2157            accel_tolerance: 0.15,
2158        };
2159
2160        assert_labels_match_within_tolerance(&label_fe_f3, &label_fe_f2, &tol, true);
2161
2162        println!("BEV label FE test passed!");
2163        println!(
2164            "F3 Combined kWh/mi: {:.3}, F2: {:.3}",
2165            label_fe_f3.lab_comb_kwh_per_mi, label_fe_f2.lab_comb_kwh_per_mi
2166        );
2167    }
2168
2169    /// Test that label FE calculations for HEV vehicles match FASTSim-2 results
2170    #[test]
2171    #[cfg(all(feature = "compat", feature = "resources", feature = "yaml"))]
2172    fn test_label_fe_hev_vs_fastsim2() {
2173        let file_contents = crate::compat::fastsim_2::ASSETS_DIR
2174            .get_file("vehicles/2016_Toyota_Prius_Two.yaml")
2175            .unwrap()
2176            .contents();
2177        let f2veh = crate::compat::fastsim_2::fastsim_core::vehicle::RustVehicle::from_reader(
2178            file_contents,
2179            "yaml",
2180            false,
2181        )
2182        .unwrap();
2183        let mut veh = Vehicle::try_from(f2veh.clone()).unwrap();
2184
2185        // Get FASTSim-3 label FE results
2186        let (label_fe_f3, _) = get_label_fe(&mut veh, None, false, None, None, None, false)
2187            .with_context(|| format_dbg!())
2188            .unwrap();
2189
2190        let (label_fe_f2, _) =
2191            crate::compat::fastsim_2::fastsim_core::simdrivelabel::get_label_fe(&f2veh, None, None)
2192                .with_context(|| format_dbg!())
2193                .unwrap();
2194
2195        // NOTE: EPA data is closer to Fastsim 3 results for UDDS
2196        // https://www.fueleconomy.gov/feg/PowerSearch.do?action=noform&path=1&year1=2016&year2=2016&make=Toyota&baseModel=Prius&srchtyp=ymm&pageno=1&rowLimit=50
2197        let tol = Tolerances {
2198            udds_tolerance: 0.15, // 15% tolerance
2199            comb_tolerance: 0.15,
2200            hwy_tolerance: 0.15,
2201            accel_tolerance: 0.06, // bumped from 0.05: F3 accel is now faster due to speed flooring fix
2202        };
2203
2204        assert_labels_match_within_tolerance(&label_fe_f3, &label_fe_f2, &tol, false);
2205    }
2206
2207    /// Test that creates a mock PHEV vehicle from FASTSim-2 data and compares label FE calculations
2208    #[test]
2209    #[cfg(all(feature = "compat", feature = "resources", feature = "yaml"))]
2210    fn test_label_fe_phev_vs_fastsim2() {
2211        let file_contents = crate::compat::fastsim_2::ASSETS_DIR
2212            .get_file("vehicles/2016_Chevrolet_Volt.yaml")
2213            .unwrap()
2214            .contents();
2215        // Load FASTSim-2 vehicle and convert to FASTSim-3
2216        let f2_veh = crate::compat::fastsim_2::fastsim_core::vehicle::RustVehicle::from_reader(
2217            file_contents,
2218            "yaml",
2219            false,
2220        )
2221        .with_context(|| format_dbg!())
2222        .unwrap();
2223        assert!(f2_veh.veh_pt_type == crate::compat::fastsim_2::fastsim_core::vehicle::PHEV);
2224        let mut veh = Vehicle::try_from(f2_veh.clone())
2225            .with_context(|| format_dbg!())
2226            .unwrap();
2227        assert!(
2228            veh.pt_type.is_plug_in_hybrid_electric_vehicle(),
2229            "`veh.pt_type.variant_as_str()`: {}\n`f2_veh.veh_pt_type`: {}",
2230            veh.pt_type.variant_as_str(),
2231            f2_veh.veh_pt_type
2232        );
2233
2234        // Get FASTSim-3 label FE results (if PHEV functionality is implemented)
2235        let label_fe_f3 = get_label_fe(&mut veh, None, false, None, None, None, false)
2236            .unwrap()
2237            .0;
2238
2239        // Get FASTSim-2 label FE results
2240        let label_fe_f2 = crate::compat::fastsim_2::fastsim_core::simdrivelabel::get_label_fe(
2241            &f2_veh, None, None,
2242        )
2243        .unwrap()
2244        .0;
2245
2246        let tol = Tolerances {
2247            udds_tolerance: 0.05, // 5% tolerance
2248            comb_tolerance: 0.05,
2249            hwy_tolerance: 0.05,
2250            accel_tolerance: 0.12, // bumped from 0.105: F3 accel is now faster due to speed flooring fix
2251        };
2252
2253        assert_labels_match_within_tolerance(&label_fe_f3, &label_fe_f2, &tol, false);
2254    }
2255
2256    fn frac_diff(base: f64, new_value: f64) -> f64 {
2257        let abs_diff = (new_value - base).abs();
2258        if base != 0.0 {
2259            abs_diff / base
2260        } else {
2261            abs_diff
2262        }
2263    }
2264
2265    #[cfg(feature = "compat")]
2266    fn assert_label_fe_same(
2267        label_fe_f2: &crate::compat::fastsim_2::fastsim_core::simdrivelabel::LabelFe,
2268        label_fe_f3: &LabelFe,
2269        tol: f64,
2270    ) {
2271        let mut all_pass = true;
2272        let mut message = String::new();
2273        let diff = frac_diff(label_fe_f2.lab_comb_mpgge, label_fe_f3.lab_comb_mpgge);
2274        all_pass = all_pass && diff < tol;
2275        message = format!(
2276            "{}\nlab_comb_mpgge: F3: {:.3}; F2: {:.3} ({:.3})",
2277            message, label_fe_f3.lab_comb_mpgge, label_fe_f2.lab_comb_mpgge, diff
2278        );
2279        let diff = frac_diff(
2280            label_fe_f2.lab_comb_kwh_per_mi,
2281            label_fe_f3.lab_comb_kwh_per_mi,
2282        );
2283        all_pass = all_pass && diff < tol;
2284        message = format!(
2285            "{}\nlab_comb_kwh_per_mi: F3: {:.3}; F2 {:.3} ({:.3})",
2286            message, label_fe_f3.lab_comb_kwh_per_mi, label_fe_f2.lab_comb_kwh_per_mi, diff
2287        );
2288        let diff = frac_diff(label_fe_f2.adj_udds_mpgge, label_fe_f3.adj_udds_mpgge);
2289        all_pass = all_pass && diff < tol;
2290        message = format!(
2291            "{}\nadj_udds_mpgge: F3: {:.3}; F2: {:.3} ({:.3})",
2292            message, label_fe_f3.adj_udds_mpgge, label_fe_f2.adj_udds_mpgge, diff
2293        );
2294        let diff = frac_diff(label_fe_f2.adj_hwy_mpgge, label_fe_f3.adj_hwy_mpgge);
2295        all_pass = all_pass && diff < tol;
2296        message = format!(
2297            "{}\nadj_hwy_mpgge: F3: {:.3}; F2: {:.3} ({:.3})",
2298            message, label_fe_f3.adj_hwy_mpgge, label_fe_f2.adj_hwy_mpgge, diff
2299        );
2300        let diff = frac_diff(label_fe_f2.adj_comb_mpgge, label_fe_f3.adj_comb_mpgge);
2301        all_pass = all_pass && diff < tol;
2302        message = format!(
2303            "{}\nadj_comb_mpgge: F3: {:.3}; F2: {:.3} ({:.3})",
2304            message, label_fe_f3.adj_comb_mpgge, label_fe_f2.adj_comb_mpgge, diff
2305        );
2306        let diff = frac_diff(
2307            label_fe_f2.adj_udds_kwh_per_mi,
2308            label_fe_f3.adj_udds_kwh_per_mi,
2309        );
2310        all_pass = all_pass && diff < tol;
2311        message = format!(
2312            "{}\nadj_udds_kwh_per_mi: F3 {:.3}; F2 {:.3} ({:.3})",
2313            message, label_fe_f3.adj_udds_kwh_per_mi, label_fe_f2.adj_udds_kwh_per_mi, diff
2314        );
2315        let diff = frac_diff(
2316            label_fe_f2.adj_hwy_kwh_per_mi,
2317            label_fe_f3.adj_hwy_kwh_per_mi,
2318        );
2319        all_pass = all_pass && diff < tol;
2320        message = format!(
2321            "{}\nadj_hwy_kwh_per_mi: F3 {:.3}; F2 {:.3} ({:.3})",
2322            message, label_fe_f3.adj_hwy_kwh_per_mi, label_fe_f2.adj_hwy_kwh_per_mi, diff
2323        );
2324        let diff = frac_diff(
2325            label_fe_f2.adj_comb_kwh_per_mi,
2326            label_fe_f3.adj_comb_kwh_per_mi,
2327        );
2328        all_pass = all_pass && diff < tol;
2329        message = format!(
2330            "{}\nadj_comb_kwh_per_mi: F3: {:.3}; F2: {:.3} ({:.3})",
2331            message, label_fe_f3.adj_comb_kwh_per_mi, label_fe_f2.adj_comb_kwh_per_mi, diff
2332        );
2333        let diff = frac_diff(label_fe_f2.net_accel, label_fe_f3.net_accel);
2334        all_pass = all_pass && diff < tol;
2335        message = format!(
2336            "{}\nnet_accel: F3: {:.3}; F2: {:.3} ({:.3})",
2337            message, label_fe_f3.net_accel, label_fe_f2.net_accel, diff
2338        );
2339        assert!(
2340            all_pass,
2341            "ERROR: At least some tests exceed tolerance of {:.3}:\n{}",
2342            tol, message
2343        );
2344    }
2345
2346    #[cfg(feature = "compat")]
2347    fn run_fe_label_comparison_for(
2348        f2veh: &crate::compat::fastsim_2::fastsim_core::vehicle::RustVehicle,
2349        tolerance: f64,
2350    ) {
2351        // Get FASTSim-2 label FE results
2352        let (label_fe_f2, result) =
2353            crate::compat::fastsim_2::fastsim_core::simdrivelabel::get_label_fe(
2354                &f2veh,
2355                Some(true),
2356                None,
2357            )
2358            .with_context(|| format_dbg!())
2359            .unwrap();
2360
2361        let sim_data = SimulationDataForLabel::ConvOrHev {
2362            veh_year: f2veh.veh_year,
2363            udds_mpgge: label_fe_f2.lab_udds_mpgge,
2364            hwy_mpgge: label_fe_f2.lab_hwy_mpgge,
2365            fs_energy_capacity_kwh: f2veh.fs_kwh,
2366        };
2367        let max_epa_adj = 0.3;
2368        assert!(result.is_some());
2369        let results_data = result.unwrap();
2370        assert!(results_data.contains_key("accel"));
2371        let accel_sd = &results_data["accel"];
2372        let accel_data = AccelData {
2373            time_s: accel_sd.cyc.time_s.to_vec(),
2374            speed_mph: accel_sd.mph_ach.to_vec(),
2375        };
2376        let label_fe_f3 = calculate_label_fuel_economy(
2377            &FuelProperties::default(),
2378            &PhevUtilizationParams::default(),
2379            max_epa_adj,
2380            &sim_data,
2381            &accel_data,
2382        )
2383        .expect("should return an OK result");
2384        assert_label_fe_same(&label_fe_f2, &label_fe_f3, tolerance);
2385    }
2386    #[test]
2387    #[cfg(all(feature = "compat", feature = "resources", feature = "yaml"))]
2388    pub fn test_label_fe_post_proc_calcs_for_conv() {
2389        let file_contents = crate::compat::fastsim_2::ASSETS_DIR
2390            .get_file("vehicles/2012_Ford_Fusion.yaml")
2391            .unwrap()
2392            .contents();
2393        let f2veh = crate::compat::fastsim_2::fastsim_core::vehicle::RustVehicle::from_reader(
2394            file_contents,
2395            "yaml",
2396            false,
2397        )
2398        .unwrap();
2399        let tolerance = 1e-6;
2400        run_fe_label_comparison_for(&f2veh, tolerance);
2401    }
2402    #[test]
2403    #[cfg(all(feature = "compat", feature = "resources", feature = "yaml"))]
2404    pub fn test_label_fe_post_proc_calcs_for_hev() {
2405        let file_contents = crate::compat::fastsim_2::ASSETS_DIR
2406            .get_file("vehicles/2016_Toyota_Prius_Two.yaml")
2407            .unwrap()
2408            .contents();
2409        let f2veh = crate::compat::fastsim_2::fastsim_core::vehicle::RustVehicle::from_reader(
2410            file_contents,
2411            "yaml",
2412            false,
2413        )
2414        .unwrap();
2415        let tolerance = 1e-6;
2416        run_fe_label_comparison_for(&f2veh, tolerance);
2417    }
2418    #[test]
2419    #[cfg(all(feature = "compat", feature = "resources", feature = "yaml"))]
2420    pub fn test_label_fe_post_proc_calcs_for_bev() {
2421        let file_contents = crate::compat::fastsim_2::ASSETS_DIR
2422            .get_file("vehicles/2022_Renault_Zoe_ZE50_R135.yaml")
2423            .unwrap()
2424            .contents();
2425        let f2veh = crate::compat::fastsim_2::fastsim_core::vehicle::RustVehicle::from_reader(
2426            file_contents,
2427            "yaml",
2428            false,
2429        )
2430        .unwrap();
2431
2432        // Get FASTSim-2 label FE results
2433        let (label_fe_f2, result) =
2434            crate::compat::fastsim_2::fastsim_core::simdrivelabel::get_label_fe(
2435                &f2veh,
2436                Some(true),
2437                None,
2438            )
2439            .with_context(|| format_dbg!())
2440            .unwrap();
2441        let sim_data = SimulationDataForLabel::Bev {
2442            veh_year: f2veh.veh_year,
2443            udds_kwh_per_mi: label_fe_f2.lab_udds_kwh_per_mi,
2444            hwy_kwh_per_mi: label_fe_f2.lab_hwy_kwh_per_mi,
2445            bev_energy_capacity_kwh: f2veh.ess_max_kwh,
2446        };
2447        let max_epa_adj = 0.3;
2448        assert!(result.is_some());
2449        let results_data = result.unwrap();
2450        assert!(results_data.contains_key("accel"));
2451        let accel_sd = &results_data["accel"];
2452        let accel_data = AccelData {
2453            time_s: accel_sd.cyc.time_s.to_vec(),
2454            speed_mph: accel_sd.mph_ach.to_vec(),
2455        };
2456        let label_fe_f3 = calculate_label_fuel_economy(
2457            &FuelProperties::default(),
2458            &PhevUtilizationParams::default(),
2459            max_epa_adj,
2460            &sim_data,
2461            &accel_data,
2462        )
2463        .expect("should have OK result");
2464        let tolerance = 1e-6;
2465        assert_label_fe_same(&label_fe_f2, &label_fe_f3, tolerance);
2466    }
2467    #[test]
2468    #[cfg(all(feature = "compat", feature = "resources", feature = "yaml"))]
2469    pub fn test_label_fe_post_proc_calcs_for_phev() {
2470        let file_contents = crate::compat::fastsim_2::ASSETS_DIR
2471            .get_file("vehicles/2016_Chevrolet_Volt.yaml")
2472            .unwrap()
2473            .contents();
2474
2475        // Load FASTSim-2 vehicle and convert to FASTSim-3
2476        let f2_veh = crate::compat::fastsim_2::fastsim_core::vehicle::RustVehicle::from_reader(
2477            file_contents,
2478            "yaml",
2479            false,
2480        )
2481        .with_context(|| format_dbg!())
2482        .unwrap();
2483        assert!(f2_veh.veh_pt_type == crate::compat::fastsim_2::fastsim_core::vehicle::PHEV);
2484
2485        let veh = Vehicle::try_from(f2_veh.clone())
2486            .with_context(|| format_dbg!())
2487            .unwrap();
2488        assert!(
2489            veh.pt_type.is_plug_in_hybrid_electric_vehicle(),
2490            "`veh.pt_type.variant_as_str()`: {}\n`f2_veh.veh_pt_type`: {}",
2491            veh.pt_type.variant_as_str(),
2492            f2_veh.veh_pt_type
2493        );
2494
2495        // Get FASTSim-2 label FE results
2496        let (label_fe_f2, result) =
2497            crate::compat::fastsim_2::fastsim_core::simdrivelabel::get_label_fe(
2498                &f2_veh,
2499                Some(true),
2500                None,
2501            )
2502            .with_context(|| format_dbg!())
2503            .unwrap();
2504        assert!(label_fe_f2.phev_calcs.is_some());
2505        let phev_calcs = label_fe_f2.phev_calcs.clone().unwrap();
2506        eprintln!("phev_calcs: {:?}", phev_calcs);
2507        assert!(result.is_some());
2508        let result = result.unwrap();
2509        eprintln!("result.keys: {:?}", result.keys());
2510        assert!(result.contains_key("udds"));
2511        let udds_result = &result["udds"];
2512        assert!(result.contains_key("hwy"));
2513        let hwy_result = &result["hwy"];
2514        eprintln!(
2515            "udds start soc: {:?}; min soc: {:?}",
2516            udds_result.soc[0],
2517            udds_result
2518                .soc
2519                .iter()
2520                .min_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal))
2521        );
2522        eprintln!(
2523            "hwy start soc: {:?}; min soc: {:?}",
2524            hwy_result.soc[0],
2525            hwy_result
2526                .soc
2527                .iter()
2528                .min_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal))
2529        );
2530        let fuel_props = FuelProperties::default();
2531        let sim_data = SimulationDataForLabel::Phev {
2532            veh_year: f2_veh.veh_year,
2533            info: PhevVehicleInfo {
2534                max_soc: f2_veh.max_soc * uc::R,
2535                min_soc: f2_veh.min_soc * uc::R,
2536                // NOTE: for F3, max regen is hard-coded to be 0.98; that just
2537                // happens to be what this vehicle model also uses.
2538                phev_max_regen: f2_veh.max_regen * uc::R,
2539                veh_mass: f2_veh.veh_kg * uc::KG,
2540                em_peak_eff: f2_veh.mc_peak_eff() * uc::R,
2541                energy_capacity: f2_veh.ess_max_kwh * uc::KWH,
2542                chg_eff: DEFAULT_CHG_EFF,
2543                fuel_storage_capacity: f2_veh.fs_kwh * uc::KWH,
2544            },
2545            udds: PhevSimulationDataForLabel {
2546                cd_fuel_consumed_kwh: phev_calcs.udds.cd_fs_kwh,
2547                cd_soc_start: f2_veh.max_soc,
2548                cd_soc_end: f2_veh.max_soc - phev_calcs.udds.delta_soc,
2549                cyc_dist_mi: udds_result.dist_mi.sum(),
2550                cd_kwh_per_mi: phev_calcs.udds.cd_ess_kwh_per_mi,
2551                // NOTE: calculating cd_mpg as F2's phev_calcs.udds.cd_mpg appears to have a mistake
2552                cd_mpg: udds_result.dist_mi.sum()
2553                    / (phev_calcs.udds.cd_fs_kwh / fuel_props.kwh_per_gge()),
2554                cs_fuel_consumed_kwh: phev_calcs.udds.cs_fs_kwh,
2555                cs_ess_energy_kwh: phev_calcs.udds.cs_ess_kwh,
2556                cs_kwh_per_mi: phev_calcs.udds.cs_ess_kwh_per_mi,
2557                cs_mpg: phev_calcs.udds.cs_mpg,
2558                cs_min_soc: f2_veh.min_soc,
2559                cs_fs_energy_capacity_kwh: f2_veh.fs_kwh,
2560            },
2561            hwy: PhevSimulationDataForLabel {
2562                cd_fuel_consumed_kwh: phev_calcs.hwy.cd_fs_kwh,
2563                cd_soc_start: f2_veh.max_soc,
2564                cd_soc_end: f2_veh.max_soc - phev_calcs.hwy.delta_soc,
2565                cyc_dist_mi: hwy_result.dist_mi.sum(),
2566                cd_kwh_per_mi: phev_calcs.hwy.cd_ess_kwh_per_mi,
2567                // NOTE: calculating cd_mpg as F2's phev_calcs.hwy.cd_mpg appears to have a mistake
2568                cd_mpg: hwy_result.dist_mi.sum()
2569                    / (phev_calcs.hwy.cd_fs_kwh / fuel_props.kwh_per_gge()),
2570                cs_fuel_consumed_kwh: phev_calcs.hwy.cs_fs_kwh,
2571                cs_ess_energy_kwh: phev_calcs.hwy.cs_ess_kwh,
2572                cs_kwh_per_mi: phev_calcs.hwy.cs_ess_kwh_per_mi,
2573                cs_mpg: phev_calcs.hwy.cs_mpg,
2574                cs_min_soc: f2_veh.min_soc,
2575                cs_fs_energy_capacity_kwh: f2_veh.fs_kwh,
2576            },
2577        };
2578        let max_epa_adj = 0.3;
2579        assert!(result.contains_key("accel"));
2580        let accel_sd = &result["accel"];
2581        let accel_data = AccelData {
2582            time_s: accel_sd.cyc.time_s.to_vec(),
2583            speed_mph: accel_sd.mph_ach.to_vec(),
2584        };
2585        let label_fe_f3 = calculate_label_fuel_economy(
2586            &FuelProperties::default(),
2587            &PhevUtilizationParams::default(),
2588            max_epa_adj,
2589            &sim_data,
2590            &accel_data,
2591        )
2592        .expect("expect OK result");
2593        let tolerance = 0.002;
2594        assert_label_fe_same(&label_fe_f2, &label_fe_f3, tolerance);
2595    }
2596}