fastsim_core/
simdrivelabel.rs

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