fastsim_core/
simdrivelabel.rs

1//! Module containing classes and methods for calculating label fuel economy.
2#![cfg(feature = "simdrivelabel")]
3
4use ndarray::Array;
5use serde::Serialize;
6use std::collections::HashMap;
7
8// crate local
9use crate::cycle::RustCycle;
10use crate::imports::*;
11use crate::params::*;
12use crate::proc_macros::add_pyo3_api;
13use crate::proc_macros::ApproxEq;
14
15#[cfg(feature = "pyo3")]
16use crate::pyo3imports::*;
17
18use crate::simdrive::{RustSimDrive, RustSimDriveParams};
19use crate::vehicle;
20
21#[add_pyo3_api]
22#[derive(Default, Serialize, Deserialize, Clone, Debug, PartialEq, ApproxEq)]
23pub struct LabelFe {
24    pub veh: vehicle::RustVehicle,
25    pub adj_params: AdjCoef,
26    pub lab_udds_mpgge: f64,
27    pub lab_hwy_mpgge: f64,
28    pub lab_comb_mpgge: f64,
29    pub lab_udds_kwh_per_mi: f64,
30    pub lab_hwy_kwh_per_mi: f64,
31    pub lab_comb_kwh_per_mi: f64,
32    pub adj_udds_mpgge: f64,
33    pub adj_hwy_mpgge: f64,
34    pub adj_comb_mpgge: f64,
35    pub adj_udds_kwh_per_mi: f64,
36    pub adj_hwy_kwh_per_mi: f64,
37    pub adj_comb_kwh_per_mi: f64,
38    pub adj_udds_ess_kwh_per_mi: f64,
39    pub adj_hwy_ess_kwh_per_mi: f64,
40    pub adj_comb_ess_kwh_per_mi: f64,
41    pub net_range_miles: f64,
42    pub uf: f64,
43    pub net_accel: f64,
44    pub res_found: String,
45    pub phev_calcs: Option<LabelFePHEV>,
46    pub adj_cs_comb_mpgge: Option<f64>,
47    pub adj_cd_comb_mpgge: Option<f64>,
48    pub net_phev_cd_miles: Option<f64>,
49    pub trace_miss_speed_mph: f64,
50}
51
52impl SerdeAPI for LabelFe {}
53
54#[add_pyo3_api]
55#[derive(Default, Serialize, Deserialize, Clone, Debug, PartialEq, ApproxEq)]
56/// Label fuel economy values for a PHEV vehicle
57pub struct LabelFePHEV {
58    pub regen_soc_buffer: f64,
59    pub udds: PHEVCycleCalc,
60    pub hwy: PHEVCycleCalc,
61}
62
63impl SerdeAPI for LabelFePHEV {}
64
65#[add_pyo3_api]
66#[derive(Default, Serialize, Deserialize, Clone, Debug, PartialEq, ApproxEq)]
67/// Label fuel economy calculations for a specific cycle of a PHEV vehicle
68pub struct PHEVCycleCalc {
69    /// Charge depletion battery kW-hr
70    pub cd_ess_kwh: f64,
71    pub cd_ess_kwh_per_mi: f64,
72    /// Charge depletion fuel gallons
73    pub cd_fs_gal: f64,
74    pub cd_fs_kwh: f64,
75    pub cd_mpg: f64,
76    /// Number of cycles in charge depletion mode, up to transition
77    pub cd_cycs: f64,
78    pub cd_miles: f64,
79    pub cd_lab_mpg: f64,
80    pub cd_adj_mpg: f64,
81    /// Fraction of transition cycles spent in charge depletion
82    pub cd_frac_in_trans: f64,
83    /// SOC change during 1 cycle
84    pub trans_init_soc: f64,
85    /// charge depletion battery kW-hr
86    pub trans_ess_kwh: f64,
87    pub trans_ess_kwh_per_mi: f64,
88    pub trans_fs_gal: f64,
89    pub trans_fs_kwh: f64,
90    /// charge sustaining battery kW-hr
91    pub cs_ess_kwh: f64,
92    pub cs_ess_kwh_per_mi: f64,
93    /// charge sustaining fuel gallons
94    pub cs_fs_gal: f64,
95    pub cs_fs_kwh: f64,
96    pub cs_mpg: f64,
97    pub lab_mpgge: f64,
98    pub lab_kwh_per_mi: f64,
99    pub lab_uf: f64,
100    pub lab_uf_gpm: Array1<f64>,
101    pub lab_iter_uf: Array1<f64>,
102    pub lab_iter_uf_kwh_per_mi: Array1<f64>,
103    pub lab_iter_kwh_per_mi: Array1<f64>,
104    pub adj_iter_mpgge: Array1<f64>,
105    pub adj_iter_kwh_per_mi: Array1<f64>,
106    pub adj_iter_cd_miles: Array1<f64>,
107    pub adj_iter_uf: Array1<f64>,
108    pub adj_iter_uf_gpm: Vec<f64>,
109    pub adj_iter_uf_kwh_per_mi: Array1<f64>,
110    pub adj_cd_miles: f64,
111    pub adj_cd_mpgge: f64,
112    pub adj_cs_mpgge: f64,
113    pub adj_uf: f64,
114    pub adj_mpgge: f64,
115    pub adj_kwh_per_mi: f64,
116    pub adj_ess_kwh_per_mi: f64,
117    pub delta_soc: f64,
118    /// Total number of miles in charge depletion mode, assuming constant kWh_per_mi
119    pub total_cd_miles: f64,
120}
121
122impl SerdeAPI for PHEVCycleCalc {}
123
124pub fn make_accel_trace() -> RustCycle {
125    let accel_cyc_secs = Array::range(0., 300., 0.1);
126    let cyc_len = accel_cyc_secs.len();
127    let mut accel_cyc_mps = Array::ones(cyc_len) * 90.0 / MPH_PER_MPS;
128    accel_cyc_mps[0] = 0.0;
129
130    RustCycle {
131        time_s: accel_cyc_secs,
132        mps: accel_cyc_mps,
133        grade: Array::zeros(cyc_len),
134        road_type: Array::zeros(cyc_len),
135        name: String::from("accel"),
136        orphaned: false,
137    }
138}
139
140#[cfg(feature = "pyo3")]
141#[pyfunction(name = "make_accel_trace")]
142/// pyo3 version of [make_accel_trace]
143pub fn make_accel_trace_py() -> RustCycle {
144    make_accel_trace()
145}
146
147pub fn get_net_accel(sd_accel: &mut RustSimDrive, scenario_name: &String) -> anyhow::Result<f64> {
148    #[cfg(feature = "logging")]
149    log::debug!("running `sim_drive_accel`");
150    sd_accel.sim_drive_accel(None, None)?;
151    if sd_accel.mph_ach.iter().any(|&x| x >= 60.) {
152        Ok(
153            interpolate(&60., &sd_accel.mph_ach, &sd_accel.cyc0.time_s, false)
154                .with_context(|| format_dbg!())?,
155        )
156    } else {
157        #[cfg(feature = "logging")]
158        log::warn!("vehicle '{}' never achieves 60 mph", scenario_name);
159        Ok(1e3)
160    }
161}
162
163#[cfg(feature = "pyo3")]
164#[pyfunction(name = "get_net_accel")]
165/// pyo3 version of [get_net_accel]
166pub fn get_net_accel_py(sd_accel: &mut RustSimDrive, scenario_name: &str) -> anyhow::Result<f64> {
167    let result = get_net_accel(sd_accel, &scenario_name.to_string())?;
168    Ok(result)
169}
170
171pub fn get_label_fe(
172    veh: &vehicle::RustVehicle,
173    full_detail: Option<bool>,
174    verbose: Option<bool>,
175) -> anyhow::Result<(LabelFe, Option<HashMap<&str, RustSimDrive>>)> {
176    // Generates label fuel economy (FE) values for a provided vehicle.
177    //
178    // Arguments:
179    // ----------
180    // veh : vehicle::RustVehicle
181    // full_detail : boolean, default False
182    //     If True, sim_drive objects for each cycle are also returned.
183    // verbose : boolean, default false
184    //     If true, print out key results
185    //
186    // Returns label fuel economy values as a struct and (optionally)
187    // simdrive::RustSimDrive objects.
188
189    let sim_params = RustSimDriveParams::default();
190    let props = RustPhysicalProperties::default();
191    let long_params = RustLongParams::default();
192
193    let mut cyc = HashMap::new();
194    let mut sd = HashMap::new();
195    let mut out = LabelFe::default();
196    let mut max_trace_miss_in_mph = 0.0;
197
198    out.veh = veh.clone();
199
200    // load the cycles and intstantiate simdrive objects
201    cyc.insert("accel", make_accel_trace());
202
203    cyc.insert("udds", RustCycle::from_resource("udds.csv", false)?);
204    cyc.insert("hwy", RustCycle::from_resource("hwfet.csv", false)?);
205
206    // run simdrive for non-phev powertrains
207    sd.insert("udds", RustSimDrive::new(cyc["udds"].clone(), veh.clone()));
208    sd.insert("hwy", RustSimDrive::new(cyc["hwy"].clone(), veh.clone()));
209
210    for (k, val) in sd.iter_mut() {
211        val.sim_drive(None, None)?;
212        let key = k;
213        let trace_miss_speed_mph = val.trace_miss_speed_mps * MPH_PER_MPS;
214        if (key == &"udds" || key == &"hwy") && trace_miss_speed_mph > max_trace_miss_in_mph {
215            max_trace_miss_in_mph = trace_miss_speed_mph;
216        }
217    }
218    out.trace_miss_speed_mph = max_trace_miss_in_mph;
219
220    // find year-based adjustment parameters
221    let adj_params = if veh.veh_year < 2017 {
222        &long_params.ld_fe_adj_coef.adj_coef_map["2008"]
223    } else {
224        // assume 2017 coefficients are valid
225        &long_params.ld_fe_adj_coef.adj_coef_map["2017"]
226    };
227    out.adj_params = adj_params.clone();
228
229    // run calculations for non-PHEV powertrains
230    if veh.veh_pt_type != vehicle::PHEV {
231        if veh.veh_pt_type != vehicle::BEV {
232            // compare to Excel 'VehicleIO'!C203 or 'VehicleIO'!labUddsMpgge
233            out.lab_udds_mpgge = sd["udds"].mpgge;
234            // compare to Excel 'VehicleIO'!C203 or 'VehicleIO'!labHwyMpgge
235            out.lab_hwy_mpgge = sd["hwy"].mpgge;
236            out.lab_comb_mpgge = 1. / (0.55 / sd["udds"].mpgge + 0.45 / sd["hwy"].mpgge);
237        } else {
238            out.lab_udds_mpgge = 0.;
239            out.lab_hwy_mpgge = 0.;
240            out.lab_comb_mpgge = 0.;
241        }
242
243        if veh.veh_pt_type == vehicle::BEV {
244            out.lab_udds_kwh_per_mi = sd["udds"].battery_kwh_per_mi;
245            out.lab_hwy_kwh_per_mi = sd["hwy"].battery_kwh_per_mi;
246            out.lab_comb_kwh_per_mi =
247                0.55 * sd["udds"].battery_kwh_per_mi + 0.45 * sd["hwy"].battery_kwh_per_mi;
248        } else {
249            out.lab_udds_kwh_per_mi = 0.;
250            out.lab_hwy_kwh_per_mi = 0.;
251            out.lab_comb_kwh_per_mi = 0.;
252        }
253
254        // adjusted values for mpg
255        if veh.veh_pt_type != vehicle::BEV {
256            // non-EV case
257            // CV or HEV case (not PHEV)
258            // HEV SOC iteration is handled in simdrive.SimDriveClassic
259            out.adj_udds_mpgge =
260                1. / (adj_params.city_intercept + adj_params.city_slope / sd["udds"].mpgge);
261            // compare to Excel 'VehicleIO'!C203 or 'VehicleIO'!adjHwyMpgge
262            out.adj_hwy_mpgge =
263                1. / (adj_params.hwy_intercept + adj_params.hwy_slope / sd["hwy"].mpgge);
264            out.adj_comb_mpgge = 1. / (0.55 / out.adj_udds_mpgge + 0.45 / out.adj_hwy_mpgge);
265        } else {
266            // EV case
267            // Mpgge is all zero for EV
268            out.adj_udds_mpgge = 0.;
269            out.adj_hwy_mpgge = 0.;
270            out.adj_comb_mpgge = 0.;
271        }
272
273        // adjusted kW-hr/mi
274        if veh.veh_pt_type == vehicle::BEV {
275            // EV Case
276            out.adj_udds_kwh_per_mi =
277                (1. / max(
278                    1. / (adj_params.city_intercept
279                        + (adj_params.city_slope
280                            / ((1. / out.lab_udds_kwh_per_mi) * props.kwh_per_gge))),
281                    (1. / out.lab_udds_kwh_per_mi)
282                        * props.kwh_per_gge
283                        * (1. - sim_params.max_epa_adj),
284                )) * props.kwh_per_gge
285                    / CHG_EFF;
286            out.adj_hwy_kwh_per_mi =
287                (1. / max(
288                    1. / (adj_params.hwy_intercept
289                        + (adj_params.hwy_slope
290                            / ((1. / out.lab_hwy_kwh_per_mi) * props.kwh_per_gge))),
291                    (1. / out.lab_hwy_kwh_per_mi)
292                        * props.kwh_per_gge
293                        * (1. - sim_params.max_epa_adj),
294                )) * props.kwh_per_gge
295                    / CHG_EFF;
296            out.adj_comb_kwh_per_mi =
297                0.55 * out.adj_udds_kwh_per_mi + 0.45 * out.adj_hwy_kwh_per_mi;
298
299            out.adj_udds_ess_kwh_per_mi = out.adj_udds_kwh_per_mi * CHG_EFF;
300            out.adj_hwy_ess_kwh_per_mi = out.adj_hwy_kwh_per_mi * CHG_EFF;
301            out.adj_comb_ess_kwh_per_mi = out.adj_comb_kwh_per_mi * CHG_EFF;
302
303            // range for combined city/highway
304            out.net_range_miles = veh.ess_max_kwh / out.adj_comb_ess_kwh_per_mi;
305        }
306
307        // utility factor (percent driving in PHEV charge depletion mode)
308        out.uf = 0.;
309    } else {
310        // PHEV
311        let phev_calcs =
312            get_label_fe_phev(veh, &mut sd, &long_params, adj_params, &sim_params, &props)?;
313        out.phev_calcs = Some(phev_calcs.clone());
314
315        // efficiency-related calculations
316        // lab
317        out.lab_udds_mpgge = phev_calcs.udds.lab_mpgge;
318        out.lab_hwy_mpgge = phev_calcs.hwy.lab_mpgge;
319        out.lab_comb_mpgge =
320            1.0 / (0.55 / phev_calcs.udds.lab_mpgge + 0.45 / phev_calcs.hwy.lab_mpgge);
321
322        out.lab_udds_kwh_per_mi = phev_calcs.udds.lab_kwh_per_mi;
323        out.lab_hwy_kwh_per_mi = phev_calcs.hwy.lab_kwh_per_mi;
324        out.lab_comb_kwh_per_mi =
325            0.55 * phev_calcs.udds.lab_kwh_per_mi + 0.45 * phev_calcs.hwy.lab_kwh_per_mi;
326
327        // adjusted
328        out.adj_udds_mpgge = phev_calcs.udds.adj_mpgge;
329        out.adj_hwy_mpgge = phev_calcs.hwy.adj_mpgge;
330        out.adj_comb_mpgge =
331            1.0 / (0.55 / phev_calcs.udds.adj_mpgge + 0.45 / phev_calcs.hwy.adj_mpgge);
332
333        out.adj_cs_comb_mpgge =
334            Some(1.0 / (0.55 / phev_calcs.udds.adj_cs_mpgge + 0.45 / phev_calcs.hwy.adj_cs_mpgge));
335        out.adj_cd_comb_mpgge =
336            Some(1.0 / (0.55 / phev_calcs.udds.adj_cd_mpgge + 0.45 / phev_calcs.hwy.adj_cd_mpgge));
337
338        out.adj_udds_kwh_per_mi = phev_calcs.udds.adj_kwh_per_mi;
339        out.adj_hwy_kwh_per_mi = phev_calcs.hwy.adj_kwh_per_mi;
340        out.adj_comb_kwh_per_mi =
341            0.55 * phev_calcs.udds.adj_kwh_per_mi + 0.45 * phev_calcs.hwy.adj_kwh_per_mi;
342
343        out.adj_udds_ess_kwh_per_mi = phev_calcs.udds.adj_ess_kwh_per_mi;
344        out.adj_hwy_ess_kwh_per_mi = phev_calcs.hwy.adj_ess_kwh_per_mi;
345        out.adj_comb_ess_kwh_per_mi =
346            0.55 * phev_calcs.udds.adj_ess_kwh_per_mi + 0.45 * phev_calcs.hwy.adj_ess_kwh_per_mi;
347
348        // range for combined city/highway
349        // utility factor (percent driving in charge depletion mode)
350        out.uf = long_params.uf_array[first_grtr(
351            &long_params.rechg_freq_miles,
352            0.55 * phev_calcs.udds.adj_cd_miles + 0.45 * phev_calcs.hwy.adj_cd_miles,
353        )
354        .unwrap()
355            - 1];
356
357        out.net_phev_cd_miles =
358            Some(0.55 * phev_calcs.udds.adj_cd_miles + 0.45 * phev_calcs.hwy.adj_cd_miles);
359
360        out.net_range_miles = (veh.fs_kwh / props.kwh_per_gge
361            - out.net_phev_cd_miles.unwrap() / out.adj_cd_comb_mpgge.unwrap())
362            * out.adj_cs_comb_mpgge.unwrap()
363            + out.net_phev_cd_miles.unwrap();
364    }
365
366    // run accelerating sim_drive
367    let mut sd_accel = RustSimDrive::new(cyc["accel"].clone(), veh.clone());
368    out.net_accel = get_net_accel(&mut sd_accel, &veh.scenario_name)?;
369    sd.insert("accel", sd_accel);
370
371    // success Boolean -- did all of the tests work(e.g. met trace within ~2 mph)?
372    out.res_found = String::from("model needs to be implemented for this"); // this may need fancier logic than just always being true
373
374    if full_detail.unwrap_or(false) && verbose.unwrap_or(false) {
375        println!("{:#?}", out);
376        Ok((out, Some(sd)))
377    } else if full_detail.unwrap_or(false) {
378        Ok((out, Some(sd)))
379    } else if verbose.unwrap_or(false) {
380        println!("{:#?}", out);
381        Ok((out, None))
382    } else {
383        Ok((out, None))
384    }
385}
386
387#[cfg(feature = "pyo3")]
388#[pyfunction(name = "get_label_fe")]
389#[cfg_attr(feature = "pyo3", pyo3(signature = (veh, full_detail=None, verbose=None)))]
390/// pyo3 version of [get_label_fe]
391pub fn get_label_fe_py(
392    veh: &vehicle::RustVehicle,
393    full_detail: Option<bool>,
394    verbose: Option<bool>,
395) -> anyhow::Result<(LabelFe, Option<HashMap<&str, RustSimDrive>>)> {
396    let result = get_label_fe(veh, full_detail, verbose)?;
397    Ok(result)
398}
399
400/// PHEV-specific function for label fe.
401///
402/// # Arguments
403/// - `veh` : vehicle::RustVehicle
404/// - `sd` : RustSimDrive objects to use for label fe calculations
405/// - `long_params` : Struct for longparams.json values
406/// - `adj_params`: Adjusted coefficients from longparams.json
407/// - `sim_params` : RustSimDriveParams
408/// - `props` : RustPhysicalProperties
409///
410/// # Returns
411/// label fuel economy values for PHEV as a struct.
412pub fn get_label_fe_phev(
413    veh: &vehicle::RustVehicle,
414    sd: &mut HashMap<&str, RustSimDrive>,
415    long_params: &RustLongParams,
416    adj_params: &AdjCoef,
417    sim_params: &RustSimDriveParams,
418    props: &RustPhysicalProperties,
419) -> anyhow::Result<LabelFePHEV> {
420    let mut phev_calcs = LabelFePHEV {
421        regen_soc_buffer: min(
422            ((0.5 * veh.veh_kg * ((60. * (1. / MPH_PER_MPS)).powi(2)))
423                * (1. / 3600.)
424                * (1. / 1000.)
425                * veh.max_regen
426                * veh.mc_peak_eff())
427                / veh.ess_max_kwh,
428            (veh.max_soc - veh.min_soc) / 2.0,
429        ),
430        ..Default::default()
431    };
432
433    // charge sustaining behavior
434    for (key, sd_val) in sd.iter_mut() {
435        // do PHEV soc iteration
436        // This runs 1 cycle starting at max SOC then runs 1 cycle starting at min SOC.
437        // By assuming that the battery SOC depletion per mile is constant across cycles,
438        // the first cycle can be extrapolated until charge sustaining kicks in.
439        sd_val.sim_drive(Some(veh.max_soc), None)?;
440        let mut phev_calc = PHEVCycleCalc::default();
441
442        // charge depletion cycle has already been simulated
443        // charge depletion battery kW-hr
444        phev_calc.cd_ess_kwh = (veh.max_soc - veh.min_soc) * veh.ess_max_kwh;
445
446        // SOC change during 1 cycle
447        phev_calc.delta_soc = sd_val.soc[0] - sd_val.soc.last().unwrap();
448        // total number of miles in charge depletion mode, assuming constant kWh_per_mi
449        phev_calc.total_cd_miles =
450            (veh.max_soc - veh.min_soc) * sd_val.veh.ess_max_kwh / sd_val.battery_kwh_per_mi;
451        // number of cycles in charge depletion mode, up to transition
452        phev_calc.cd_cycs = phev_calc.total_cd_miles / sd_val.dist_mi.sum();
453        // fraction of transition cycle spent in charge depletion
454        phev_calc.cd_frac_in_trans = phev_calc.cd_cycs % phev_calc.cd_cycs.floor();
455
456        // charge depletion fuel gallons
457        phev_calc.cd_fs_gal = sd_val.fs_kwh_out_ach.sum() / props.kwh_per_gge;
458        phev_calc.cd_fs_kwh = sd_val.fs_kwh_out_ach.sum();
459        phev_calc.cd_ess_kwh_per_mi = sd_val.battery_kwh_per_mi;
460        phev_calc.cd_mpg = sd_val.mpgge;
461
462        // utility factor calculation for last charge depletion iteration and transition iteration
463        // ported from excel
464        let interp_x_vals =
465            Array::range(0.0, phev_calc.cd_cycs.ceil() + 1.0, 1.0) * sd_val.dist_mi.sum();
466        phev_calc.lab_iter_uf = interp_x_vals
467            .iter()
468            .map(|x: &f64| -> f64 {
469                long_params.uf_array[first_grtr(&long_params.rechg_freq_miles, *x).unwrap() - 1]
470            })
471            .collect();
472
473        // transition cycle
474        phev_calc.trans_init_soc = veh.max_soc - phev_calc.cd_cycs.floor() * phev_calc.delta_soc;
475
476        // run the transition cycle
477        sd_val.sim_drive(Some(phev_calc.trans_init_soc), None)?;
478        // charge depletion battery kW-hr
479        phev_calc.trans_ess_kwh =
480            phev_calc.cd_ess_kwh_per_mi * sd_val.dist_mi.sum() * phev_calc.cd_frac_in_trans;
481        phev_calc.trans_ess_kwh_per_mi = phev_calc.cd_ess_kwh_per_mi * phev_calc.cd_frac_in_trans;
482
483        // charge sustaining
484        // the 0.01 is here to be consistent with Excel
485        let init_soc = sd_val.veh.min_soc + 0.01;
486        sd_val.sim_drive(Some(init_soc), None)?;
487        // charge sustaining fuel gallons
488        phev_calc.cs_fs_gal = sd_val.fs_kwh_out_ach.sum() / props.kwh_per_gge;
489        // charge depletion fuel gallons, dependent on phev_calc.trans_fs_gal
490        phev_calc.trans_fs_gal = phev_calc.cs_fs_gal * (1.0 - phev_calc.cd_frac_in_trans);
491        phev_calc.cs_fs_kwh = sd_val.fs_kwh_out_ach.sum();
492        phev_calc.trans_fs_kwh = phev_calc.cs_fs_kwh * (1.0 - phev_calc.cd_frac_in_trans);
493        // charge sustaining battery kW-hr
494        phev_calc.cs_ess_kwh = sd_val.ess_dischg_kj;
495        phev_calc.cs_ess_kwh_per_mi = sd_val.battery_kwh_per_mi;
496
497        let lab_iter_uf_diff = diff(&phev_calc.lab_iter_uf);
498        phev_calc.lab_uf_gpm = Array::from_vec(vec![
499            phev_calc.trans_fs_gal * lab_iter_uf_diff.last().unwrap(),
500            phev_calc.cs_fs_gal * (1.0 - phev_calc.lab_iter_uf.last().unwrap()),
501        ]) / sd_val.dist_mi.sum();
502
503        phev_calc.cd_mpg = sd_val.mpgge;
504
505        // city and highway cycle ranges
506        phev_calc.cd_miles =
507            if (veh.max_soc - phev_calcs.regen_soc_buffer - sd_val.soc.min()?) < 0.01 {
508                1000.0
509            } else {
510                phev_calc.cd_cycs.ceil() * sd_val.dist_mi.sum()
511            };
512        phev_calc.cd_lab_mpg =
513            phev_calc.lab_iter_uf.last().unwrap() / (phev_calc.trans_fs_gal / sd_val.dist_mi.sum());
514
515        // charge sustaining
516        phev_calc.cs_mpg = sd_val.dist_mi.sum() / phev_calc.cs_fs_gal;
517
518        phev_calc.lab_uf = long_params.uf_array
519            [first_grtr(&long_params.rechg_freq_miles, phev_calc.cd_miles).unwrap() - 1];
520
521        // labCombMpgge
522        phev_calc.cd_adj_mpg =
523            phev_calc.lab_iter_uf.max()? / phev_calc.lab_uf_gpm[phev_calc.lab_uf_gpm.len() - 2];
524
525        phev_calc.lab_mpgge = 1.0
526            / (phev_calc.lab_uf / phev_calc.cd_adj_mpg
527                + (1.0 - phev_calc.lab_uf) / phev_calc.cs_mpg);
528
529        let mut lab_iter_kwh_per_mi_vals = Vec::new();
530        lab_iter_kwh_per_mi_vals.push(0.0);
531        lab_iter_kwh_per_mi_vals
532            .extend(vec![phev_calc.cd_ess_kwh_per_mi; phev_calc.cd_cycs.floor() as usize].iter());
533        lab_iter_kwh_per_mi_vals.push(phev_calc.trans_ess_kwh_per_mi);
534        lab_iter_kwh_per_mi_vals.push(0.0);
535        phev_calc.lab_iter_kwh_per_mi = Array::from_vec(lab_iter_kwh_per_mi_vals);
536        let mut vals = Vec::new();
537        vals.push(0.0);
538        vals.extend(
539            (&phev_calc
540                .lab_iter_kwh_per_mi
541                .slice(s![1..phev_calc.lab_iter_kwh_per_mi.len() - 1])
542                * &diff(&phev_calc.lab_iter_uf).slice(s![1..]))
543                .iter(),
544        );
545        vals.push(0.0);
546        phev_calc.lab_iter_uf_kwh_per_mi = Array::from_vec(vals);
547
548        phev_calc.lab_kwh_per_mi =
549            phev_calc.lab_iter_uf_kwh_per_mi.sum() / phev_calc.lab_iter_uf.max()?;
550
551        let mut adj_iter_mpgge_vals = vec![0.0; phev_calc.cd_cycs.floor() as usize];
552        let mut adj_iter_kwh_per_mi_vals = vec![0.0; phev_calc.lab_iter_kwh_per_mi.len()];
553        if *key == "udds" {
554            adj_iter_mpgge_vals.push(max(
555                1.0 / (adj_params.city_intercept
556                    + (adj_params.city_slope
557                        / (sd_val.dist_mi.sum() / (phev_calc.trans_fs_kwh / props.kwh_per_gge)))),
558                sd_val.dist_mi.sum() / (phev_calc.trans_fs_kwh / props.kwh_per_gge)
559                    * (1.0 - sim_params.max_epa_adj),
560            ));
561            adj_iter_mpgge_vals.push(max(
562                1.0 / (adj_params.city_intercept
563                    + (adj_params.city_slope
564                        / (sd_val.dist_mi.sum() / (phev_calc.cs_fs_kwh / props.kwh_per_gge)))),
565                sd_val.dist_mi.sum() / (phev_calc.cs_fs_kwh / props.kwh_per_gge)
566                    * (1.0 - sim_params.max_epa_adj),
567            ));
568
569            for (c, _) in phev_calc.lab_iter_kwh_per_mi.iter().enumerate() {
570                if phev_calc.lab_iter_kwh_per_mi[c] == 0.0 {
571                    adj_iter_kwh_per_mi_vals[c] = 0.0;
572                } else {
573                    adj_iter_kwh_per_mi_vals[c] =
574                        (1.0 / max(
575                            1.0 / (adj_params.city_intercept
576                                + (adj_params.city_slope
577                                    / ((1.0 / phev_calc.lab_iter_kwh_per_mi[c])
578                                        * props.kwh_per_gge))),
579                            (1.0 - sim_params.max_epa_adj)
580                                * ((1.0 / phev_calc.lab_iter_kwh_per_mi[c]) * props.kwh_per_gge),
581                        )) * props.kwh_per_gge;
582                }
583            }
584        } else {
585            adj_iter_mpgge_vals.push(max(
586                1.0 / (adj_params.hwy_intercept
587                    + (adj_params.hwy_slope
588                        / (sd_val.dist_mi.sum() / (phev_calc.trans_fs_kwh / props.kwh_per_gge)))),
589                sd_val.dist_mi.sum() / (phev_calc.trans_fs_kwh / props.kwh_per_gge)
590                    * (1.0 - sim_params.max_epa_adj),
591            ));
592            adj_iter_mpgge_vals.push(max(
593                1.0 / (adj_params.hwy_intercept
594                    + (adj_params.hwy_slope
595                        / (sd_val.dist_mi.sum() / (phev_calc.cs_fs_kwh / props.kwh_per_gge)))),
596                sd_val.dist_mi.sum() / (phev_calc.cs_fs_kwh / props.kwh_per_gge)
597                    * (1.0 - sim_params.max_epa_adj),
598            ));
599
600            for (c, _) in phev_calc.lab_iter_kwh_per_mi.iter().enumerate() {
601                if phev_calc.lab_iter_kwh_per_mi[c] == 0.0 {
602                    adj_iter_kwh_per_mi_vals[c] = 0.0;
603                } else {
604                    adj_iter_kwh_per_mi_vals[c] =
605                        (1.0 / max(
606                            1.0 / (adj_params.hwy_intercept
607                                + (adj_params.hwy_slope
608                                    / ((1.0 / phev_calc.lab_iter_kwh_per_mi[c])
609                                        * props.kwh_per_gge))),
610                            (1.0 - sim_params.max_epa_adj)
611                                * ((1.0 / phev_calc.lab_iter_kwh_per_mi[c]) * props.kwh_per_gge),
612                        )) * props.kwh_per_gge;
613                }
614            }
615        }
616        phev_calc.adj_iter_mpgge = Array::from(adj_iter_mpgge_vals);
617        phev_calc.adj_iter_kwh_per_mi = Array::from(adj_iter_kwh_per_mi_vals);
618
619        phev_calc.adj_iter_cd_miles =
620            Array::from_vec(vec![0.0; phev_calc.cd_cycs.ceil() as usize + 2]);
621        for c in 0..phev_calc.adj_iter_cd_miles.len() {
622            if c == 0 {
623                phev_calc.adj_iter_cd_miles[c] = 0.0;
624            } else if c <= phev_calc.cd_cycs.floor() as usize {
625                phev_calc.adj_iter_cd_miles[c] = phev_calc.adj_iter_cd_miles[c - 1]
626                    + phev_calc.cd_ess_kwh_per_mi * sd_val.dist_mi.sum()
627                        / phev_calc.adj_iter_kwh_per_mi[c];
628            } else if c == phev_calc.cd_cycs.floor() as usize + 1 {
629                phev_calc.adj_iter_cd_miles[c] = phev_calc.adj_iter_cd_miles[c - 1]
630                    + phev_calc.trans_ess_kwh_per_mi * sd_val.dist_mi.sum()
631                        / phev_calc.adj_iter_kwh_per_mi[c];
632            } else {
633                phev_calc.adj_iter_cd_miles[c] = 0.0;
634            }
635        }
636
637        phev_calc.adj_cd_miles =
638            if veh.max_soc - phev_calcs.regen_soc_buffer - sd_val.soc.min()? < 0.01 {
639                1000.0
640            } else {
641                *phev_calc.adj_iter_cd_miles.max()?
642            };
643
644        // utility factor calculation for last charge depletion iteration and transition iteration
645        // ported from excel
646        phev_calc.adj_iter_uf = phev_calc
647            .adj_iter_cd_miles
648            .iter()
649            .map(|x: &f64| -> f64 {
650                long_params.uf_array[first_grtr(&long_params.rechg_freq_miles, *x).unwrap() - 1]
651            })
652            .collect();
653
654        let adj_iter_uf_diff = diff(&phev_calc.adj_iter_uf);
655        phev_calc.adj_iter_uf_gpm = vec![0.0; phev_calc.cd_cycs.floor() as usize];
656        phev_calc.adj_iter_uf_gpm.push(
657            (1.0 / phev_calc.adj_iter_mpgge[phev_calc.adj_iter_mpgge.len() - 2])
658                * adj_iter_uf_diff[adj_iter_uf_diff.len() - 2],
659        );
660        phev_calc.adj_iter_uf_gpm.push(
661            (1.0 / phev_calc.adj_iter_mpgge.last().unwrap())
662                * (1.0 - phev_calc.adj_iter_uf[phev_calc.adj_iter_uf.len() - 2]),
663        );
664
665        phev_calc.adj_iter_uf_kwh_per_mi =
666            &phev_calc.adj_iter_kwh_per_mi * &diff(&phev_calc.adj_iter_uf);
667
668        phev_calc.adj_cd_mpgge = 1.0
669            / phev_calc.adj_iter_uf_gpm[phev_calc.adj_iter_uf_gpm.len() - 2]
670            * phev_calc.adj_iter_uf.max()?;
671        phev_calc.adj_cs_mpgge = 1.0 / phev_calc.adj_iter_uf_gpm.last().unwrap()
672            * (1.0 - phev_calc.adj_iter_uf.max()?);
673
674        phev_calc.adj_uf = long_params.uf_array
675            [first_grtr(&long_params.rechg_freq_miles, phev_calc.adj_cd_miles).unwrap() - 1];
676
677        phev_calc.adj_mpgge = 1.0
678            / (phev_calc.adj_uf / phev_calc.adj_cd_mpgge
679                + (1.0 - phev_calc.adj_uf) / phev_calc.adj_cs_mpgge);
680
681        phev_calc.adj_kwh_per_mi =
682            phev_calc.adj_iter_uf_kwh_per_mi.sum() / phev_calc.adj_iter_uf.max()? / veh.chg_eff;
683
684        phev_calc.adj_ess_kwh_per_mi =
685            phev_calc.adj_iter_uf_kwh_per_mi.sum() / phev_calc.adj_iter_uf.max()?;
686
687        match *key {
688            "udds" => phev_calcs.udds = phev_calc.clone(),
689            "hwy" => phev_calcs.hwy = phev_calc.clone(),
690            &_ => bail!("No field for cycle {}", key),
691        };
692    }
693
694    Ok(phev_calcs)
695}
696
697#[cfg(feature = "pyo3")]
698#[pyfunction(name = "get_label_fe_phev")]
699/// pyo3 version of [get_label_fe_phev]
700pub fn get_label_fe_phev_py(
701    veh: &vehicle::RustVehicle,
702    sd_dict: Bound<PyDict>,
703    adj_params: AdjCoef,
704    long_params: RustLongParams,
705    sim_params: &RustSimDriveParams,
706    props: RustPhysicalProperties,
707) -> anyhow::Result<LabelFePHEV> {
708    let mut sd_mut: HashMap<String, RustSimDrive> = HashMap::new();
709    for (key, value) in sd_dict.keys().into_iter().zip(sd_dict.values().into_iter()) {
710        let key_extracted = key
711            .extract::<String>()
712            .with_context(|| format!("{}\nFailed to extract key", format_dbg!()))?;
713        let value_extracted = value
714            .extract()
715            .with_context(|| format!("{}\nFailed to extract value", format_dbg!()))?;
716        sd_mut.insert(key_extracted, value_extracted);
717    }
718
719    // type conversion on keys to satisfy function arg below
720    let mut sd_mut =
721        HashMap::from_iter(sd_mut.iter().map(|item| (item.0.as_str(), item.1.clone())));
722
723    get_label_fe_phev(
724        veh,
725        &mut sd_mut,
726        &long_params,
727        &adj_params,
728        sim_params,
729        &props,
730    )
731}
732
733#[cfg(test)]
734mod simdrivelabel_tests {
735    use super::*;
736
737    #[test]
738    fn test_get_label_fe_conv() {
739        let veh = vehicle::RustVehicle::mock_vehicle();
740        let (mut label_fe, _) = get_label_fe(&veh, None, None).unwrap();
741        // For some reason, RustVehicle::mock_vehicle() != RustVehicle::mock_vehicle()
742        // Therefore, veh field in both structs replaced with Default for comparison purposes
743        // The reason this fails is that NaN != NaN. mock_vehicle defaults some values to NaN.
744        let ref_veh = vehicle::RustVehicle::default();
745        label_fe.veh = ref_veh.clone();
746        // println!("Calculated net accel: {}", label_fe.net_accel);
747
748        let label_fe_truth = LabelFe {
749            veh: ref_veh,
750            adj_params: RustLongParams::default().ld_fe_adj_coef.adj_coef_map["2008"].clone(),
751            lab_udds_mpgge: 32.47503766676829,
752            lab_hwy_mpgge: 42.265348793379445,
753            lab_comb_mpgge: 36.25407690819302,
754            lab_udds_kwh_per_mi: 0.,
755            lab_hwy_kwh_per_mi: 0.,
756            lab_comb_kwh_per_mi: 0.,
757            adj_udds_mpgge: 25.246151811422468,
758            adj_hwy_mpgge: 30.08729992782952,
759            adj_comb_mpgge: 27.21682755127691,
760            adj_udds_kwh_per_mi: 0.,
761            adj_hwy_kwh_per_mi: 0.,
762            adj_comb_kwh_per_mi: 0.,
763            adj_udds_ess_kwh_per_mi: 0.,
764            adj_hwy_ess_kwh_per_mi: 0.,
765            adj_comb_ess_kwh_per_mi: 0.,
766            net_range_miles: 0.,
767            uf: 0.,
768            net_accel: 9.451683946821882,
769            res_found: String::from("model needs to be implemented for this"),
770            phev_calcs: None,
771            adj_cs_comb_mpgge: None,
772            adj_cd_comb_mpgge: None,
773            net_phev_cd_miles: None,
774            trace_miss_speed_mph: 0.0,
775        };
776
777        // println!(
778        //     "Percent diff to Python calc: {:.3}%",
779        //     100. * (label_fe_truth.net_accel - label_fe.net_accel) / label_fe_truth.net_accel
780        // );
781
782        assert!(
783            label_fe.approx_eq(&label_fe_truth, 1e-10),
784            "label_fe:\n{}\n\nlabel_fe_truth:\n{}",
785            label_fe.to_json().unwrap(),
786            label_fe_truth.to_json().unwrap(),
787        );
788    }
789
790    #[test]
791    fn test_get_label_fe_phev() {
792        let mut veh = vehicle::RustVehicle {
793            props: RustPhysicalProperties {
794                air_density_kg_per_m3: 1.2,
795                a_grav_mps2: 9.81,
796                kwh_per_gge: 33.7,
797                fuel_rho_kg__L: 0.75,
798                fuel_afr_stoich: 14.7,
799                orphaned: false,
800            },
801            veh_kg: Default::default(),
802            scenario_name: "2016 Chevrolet Volt".into(),
803            selection: 13,
804            veh_year: 2016,
805            veh_pt_type: "PHEV".into(),
806            drag_coef: 0.3,
807            frontal_area_m2: 2.565,
808            glider_kg: 950.564,
809            veh_cg_m: 0.53,
810            drive_axle_weight_frac: 0.59,
811            wheel_base_m: 2.6,
812            cargo_kg: 136.0,
813            veh_override_kg: None,
814            comp_mass_multiplier: 1.4,
815            fs_max_kw: 2000.0,
816            fs_secs_to_peak_pwr: 1.0,
817            fs_kwh: 297.0,
818            fs_kwh_per_kg: 9.89,
819            fc_max_kw: 75.0,
820            fc_pwr_out_perc: Array1::from(vec![
821                0.0, 0.005, 0.015, 0.04, 0.06, 0.1, 0.14, 0.2, 0.4, 0.6, 0.8, 1.0,
822            ]),
823            fc_eff_map: Array1::from(vec![
824                0.1, 0.12, 0.16, 0.22, 0.28, 0.33, 0.35, 0.36, 0.35, 0.34, 0.32, 0.3,
825            ]),
826            fc_eff_type: "SI".into(),
827            fc_sec_to_peak_pwr: 6.0,
828            fc_base_kg: 61.0,
829            fc_kw_per_kg: 2.13,
830            min_fc_time_on: 30.0,
831            idle_fc_kw: 1.5,
832            mc_max_kw: 111.0,
833            mc_pwr_out_perc: Array1::from(vec![
834                0.0, 0.02, 0.04, 0.06, 0.08, 0.1, 0.2, 0.4, 0.6, 0.8, 1.0,
835            ]),
836            mc_eff_map: Array1::from(vec![
837                0.84, 0.86, 0.88, 0.9, 0.91, 0.92, 0.94, 0.95, 0.95, 0.94, 0.93,
838            ]),
839            mc_sec_to_peak_pwr: 3.0,
840            mc_pe_kg_per_kw: 0.833,
841            mc_pe_base_kg: 21.6,
842            ess_max_kw: 115.0,
843            ess_max_kwh: 18.4,
844            ess_kg_per_kwh: 8.0,
845            ess_base_kg: 75.0,
846            ess_round_trip_eff: 0.97,
847            ess_life_coef_a: 110.0,
848            ess_life_coef_b: -0.6811,
849            min_soc: 0.15,
850            max_soc: 0.9,
851            ess_dischg_to_fc_max_eff_perc: 1.0,
852            ess_chg_to_fc_max_eff_perc: 0.0,
853            wheel_inertia_kg_m2: 0.815,
854            num_wheels: 4.0,
855            wheel_rr_coef: 0.007,
856            wheel_radius_m: 0.336,
857            wheel_coef_of_fric: 0.7,
858            max_accel_buffer_mph: 60.0,
859            max_accel_buffer_perc_of_useable_soc: 0.2,
860            perc_high_acc_buf: 0.0,
861            mph_fc_on: 85.0,
862            kw_demand_fc_on: 120.0,
863            max_regen: 0.98,
864            stop_start: false,
865            force_aux_on_fc: false,
866            alt_eff: 1.0,
867            chg_eff: 0.86,
868            aux_kw: 0.3,
869            trans_kg: 114.0,
870            trans_eff: 0.98,
871            ess_to_fuel_ok_error: 0.005,
872            small_motor_power_kw: 7.5,
873            large_motor_power_kw: 75.0,
874            fc_perc_out_array: FC_PERC_OUT_ARRAY.into(),
875            mc_kw_out_array: Default::default(),
876            mc_max_elec_in_kw: Default::default(),
877            mc_full_eff_array: Default::default(),
878            max_trac_mps2: Default::default(),
879            ess_mass_kg: Default::default(),
880            mc_mass_kg: Default::default(),
881            fc_mass_kg: Default::default(),
882            fs_mass_kg: Default::default(),
883            mc_perc_out_array: Default::default(),
884            regen_a: 500.0,
885            regen_b: 0.99,
886            charging_on: false,
887            no_elec_sys: false,
888            no_elec_aux: false,
889            max_roadway_chg_kw: Array1::from(vec![0.0, 0.0, 0.0, 0.0, 0.0, 0.0]),
890            input_kw_out_array: Array1::from(vec![
891                0.0,
892                0.375,
893                1.125,
894                3.0,
895                4.5,
896                7.5,
897                10.500000000000002,
898                15.0,
899                30.0,
900                45.0,
901                60.0,
902                75.0,
903            ]),
904            fc_kw_out_array: Default::default(),
905            fc_eff_array: Default::default(),
906            modern_max: 0.95,
907            mc_eff_array: Array1::from(vec![
908                0.84, 0.86, 0.88, 0.9, 0.91, 0.92, 0.94, 0.95, 0.95, 0.94, 0.93,
909            ]),
910            mc_kw_in_array: Default::default(),
911            val_udds_mpgge: f64::NAN,
912            val_hwy_mpgge: f64::NAN,
913            val_comb_mpgge: 42.0,
914            val_udds_kwh_per_mile: f64::NAN,
915            val_hwy_kwh_per_mile: f64::NAN,
916            val_comb_kwh_per_mile: 0.31,
917            val_cd_range_mi: 53.0,
918            val_const65_mph_kwh_per_mile: f64::NAN,
919            val_const60_mph_kwh_per_mile: f64::NAN,
920            val_const55_mph_kwh_per_mile: f64::NAN,
921            val_const45_mph_kwh_per_mile: f64::NAN,
922            val_unadj_udds_kwh_per_mile: f64::NAN,
923            val_unadj_hwy_kwh_per_mile: f64::NAN,
924            val0_to60_mph: 8.4,
925            val_ess_life_miles: 120000.0,
926            val_range_miles: f64::NAN,
927            val_veh_base_cost: 17000.0,
928            val_msrp: 33170.0,
929            fc_peak_eff_override: None,
930            mc_peak_eff_override: None,
931            orphaned: false,
932            ..Default::default()
933        };
934        veh.set_derived().unwrap();
935
936        let (mut label_fe, _) = get_label_fe(&veh, None, None).unwrap();
937        // For some reason, RustVehicle::mock_vehicle() != RustVehicle::mock_vehicle()
938        // Therefore, veh field in both structs replaced with Default for comparison purposes
939        label_fe.veh = vehicle::RustVehicle::default();
940        // TODO: Figure out why net_accel values are different
941        println!("Calculated net accel: {}", label_fe.net_accel);
942        println!(
943            "Percent diff to Python calc: {:.3}%",
944            100. * (9.451683946821882 - label_fe.net_accel) / 9.451683946821882
945        );
946        label_fe.net_accel = 1000.;
947
948        let udds = PHEVCycleCalc {
949            cd_ess_kwh: 13.799999999999999,
950            cd_ess_kwh_per_mi: 0.1670807863534209,
951            cd_fs_gal: 0.0,
952            cd_fs_kwh: 0.0,
953            cd_mpg: 65.0128437991813,
954            cd_cycs: 11.083418864860784,
955            cd_miles: 89.42523198551896,
956            cd_lab_mpg: 59.77814990568397,
957            cd_adj_mpg: 2968.1305812156647,
958            cd_frac_in_trans: 0.08341886486078387,
959            trans_init_soc: 0.15564484203010176,
960            trans_ess_kwh: 0.10386509335387073,
961            trans_ess_kwh_per_mi: 0.013937689537649522,
962            trans_fs_gal: 0.105063189381161,
963            trans_fs_kwh: 3.5406294821451265,
964            cs_ess_kwh: -27.842875966770062,
965            cs_ess_kwh_per_mi: -0.001037845633667792,
966            cs_fs_gal: 0.11462508375235472,
967            cs_fs_kwh: 3.8628653224543545,
968            cs_mpg: 65.01284379918131,
969            lab_mpgge: 370.06411942132064,
970            lab_kwh_per_mi: 0.16342111007981494,
971            lab_uf: 0.8427800000000001,
972            lab_uf_gpm: Array::from_vec(vec![0.00028394, 0.00241829]),
973            lab_iter_uf: Array::from_vec(vec![
974                0., 0.16268, 0.28152, 0.41188, 0.51506, 0.59611, 0.64532, 0.69897, 0.74176,
975                0.77648, 0.79825, 0.82264, 0.84278,
976            ]),
977            lab_iter_uf_kwh_per_mi: Array::from_vec(vec![
978                0., 0.0271807, 0.01985588, 0.02178065, 0.0172394, 0.0135419, 0.00822205,
979                0.00896388, 0.00714939, 0.00580104, 0.00363735, 0.0040751, 0.00028071, 0.,
980            ]),
981            lab_iter_kwh_per_mi: Array::from_vec(vec![
982                0., 0.16708079, 0.16708079, 0.16708079, 0.16708079, 0.16708079, 0.16708079,
983                0.16708079, 0.16708079, 0.16708079, 0.16708079, 0.16708079, 0.01393769, 0.,
984            ]),
985            adj_iter_mpgge: Array::from_vec(vec![
986                0.,
987                0.,
988                0.,
989                0.,
990                0.,
991                0.,
992                0.,
993                0.,
994                0.,
995                0.,
996                0.,
997                50.2456134,
998                46.69198818,
999            ]),
1000            adj_iter_kwh_per_mi: Array::from_vec(vec![
1001                0., 0.23868684, 0.23868684, 0.23868684, 0.23868684, 0.23868684, 0.23868684,
1002                0.23868684, 0.23868684, 0.23868684, 0.23868684, 0.23868684, 0.01991099, 0.,
1003            ]),
1004            adj_iter_cd_miles: Array::from_vec(vec![
1005                0.,
1006                5.21647187,
1007                10.43294373,
1008                15.6494156,
1009                20.86588746,
1010                26.08235933,
1011                31.29883119,
1012                36.51530306,
1013                41.73177493,
1014                46.94824679,
1015                52.16471866,
1016                57.38119052,
1017                62.59766239,
1018                0.,
1019            ]),
1020            adj_iter_uf: Array::from_vec(vec![
1021                0., 0.11878, 0.2044, 0.31698, 0.38194, 0.46652, 0.53737, 0.57771, 0.62998, 0.6599,
1022                0.69897, 0.73185, 0.75126, 0.,
1023            ]),
1024            adj_iter_uf_gpm: vec![
1025                0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.0003863, 0.00532725,
1026            ],
1027            adj_iter_uf_kwh_per_mi: Array::from_vec(vec![
1028                0., 0.02835122, 0.02043637, 0.02687136, 0.0155051, 0.02018813, 0.01691096,
1029                0.00962863, 0.01247616, 0.00714151, 0.00932549, 0.00784802, 0.00038647, 0.,
1030            ]),
1031            adj_cd_miles: 62.59766238986325,
1032            adj_cd_mpgge: 1944.7459827561047,
1033            adj_cs_mpgge: 46.69198818435928,
1034            adj_uf: 0.75126,
1035            adj_mpgge: 175.0223917643415,
1036            adj_kwh_per_mi: 0.27097024959679444,
1037            adj_ess_kwh_per_mi: 0.23303441465324323,
1038            delta_soc: 0.0676686507245362,
1039            total_cd_miles: 82.59477526523773,
1040        };
1041
1042        let hwy = PHEVCycleCalc {
1043            cd_ess_kwh: 13.799999999999999,
1044            cd_ess_kwh_per_mi: 0.19912462736394723,
1045            cd_fs_gal: 0.0,
1046            cd_fs_kwh: 0.0,
1047            cd_mpg: 61.75832757157714,
1048            cd_cycs: 6.75533367335913,
1049            cd_miles: 71.81337619240335,
1050            cd_lab_mpg: 199.76659107309018,
1051            cd_adj_mpg: 4975.506626976092,
1052            cd_frac_in_trans: 0.7553336733591296,
1053            trans_init_soc: 0.23385969996618272,
1054            trans_ess_kwh: 1.5430184793777608,
1055            trans_ess_kwh_per_mi: 0.15040553624307812,
1056            trans_fs_gal: 0.040643020828268026,
1057            trans_fs_kwh: 1.3696698019126325,
1058            cs_ess_kwh: -27.84287564320177,
1059            cs_ess_kwh_per_mi: -0.0007538835761840731,
1060            cs_fs_gal: 0.1661161198039534,
1061            cs_fs_kwh: 5.59811323739323,
1062            cs_mpg: 61.75832757157714,
1063            lab_mpgge: 282.75893721314793,
1064            lab_kwh_per_mi: 0.19665299886733625,
1065            lab_uf: 0.7914100000000001,
1066            lab_uf_gpm: Array::from_vec(vec![0.00015906, 0.00337752]),
1067            lab_iter_uf: Array::from_vec(vec![
1068                0., 0.2044, 0.38194, 0.51506, 0.62998, 0.69897, 0.75126, 0.79141,
1069            ]),
1070            lab_iter_uf_kwh_per_mi: Array::from_vec(vec![
1071                0., 0.04070107, 0.03535259, 0.02650747, 0.0228834, 0.01373761, 0.01041223,
1072                0.00603878, 0.,
1073            ]),
1074            lab_iter_kwh_per_mi: Array::from_vec(vec![
1075                0., 0.19912463, 0.19912463, 0.19912463, 0.19912463, 0.19912463, 0.19912463,
1076                0.15040554, 0.,
1077            ]),
1078            adj_iter_mpgge: Array::from_vec(vec![0., 0., 0., 0., 0., 0., 176.69300837, 43.2308293]),
1079            adj_iter_kwh_per_mi: Array::from_vec(vec![
1080                0., 0.28446375, 0.28446375, 0.28446375, 0.28446375, 0.28446375, 0.28446375,
1081                0.21486505, 0.,
1082            ]),
1083            adj_iter_cd_miles: Array::from_vec(vec![
1084                0.,
1085                7.18133762,
1086                14.36267524,
1087                21.54401286,
1088                28.72535048,
1089                35.9066881,
1090                43.08802572,
1091                50.26936333,
1092                0.,
1093            ]),
1094            adj_iter_uf: Array::from_vec(vec![
1095                0., 0.16268, 0.28152, 0.41188, 0.49148, 0.57771, 0.64532, 0.68662, 0.,
1096            ]),
1097            adj_iter_uf_gpm: vec![0., 0., 0., 0., 0., 0., 0.00023374, 0.00724899],
1098            adj_iter_uf_kwh_per_mi: Array::from_vec(vec![
1099                0., 0.04627656, 0.03380567, 0.03708269, 0.02264331, 0.02452931, 0.01923259,
1100                0.00887393, 0.,
1101            ]),
1102            adj_cd_miles: 50.26936333468235,
1103            adj_cd_mpgge: 2937.5533511975764,
1104            adj_cs_mpgge: 43.230829300104,
1105            adj_uf: 0.68662,
1106            adj_mpgge: 133.64102451254365,
1107            adj_kwh_per_mi: 0.3259039663244739,
1108            adj_ess_kwh_per_mi: 0.2802774110390475,
1109            delta_soc: 0.11102338333896955,
1110            total_cd_miles: 69.30333119859274,
1111        };
1112
1113        let phev_calcs = LabelFePHEV {
1114            regen_soc_buffer: 0.00957443430586049,
1115            udds,
1116            hwy,
1117        };
1118
1119        let label_fe_truth = LabelFe {
1120            veh: vehicle::RustVehicle::default(),
1121            adj_params: RustLongParams::default().ld_fe_adj_coef.adj_coef_map["2008"].clone(),
1122            lab_udds_mpgge: 370.06411942132064,
1123            lab_hwy_mpgge: 282.75893721314793,
1124            lab_comb_mpgge: 324.91895455274005,
1125            lab_udds_kwh_per_mi: 0.16342111007981494,
1126            lab_hwy_kwh_per_mi: 0.19665299886733625,
1127            lab_comb_kwh_per_mi: 0.17837546003419952,
1128            adj_udds_mpgge: 175.0223917643415,
1129            adj_hwy_mpgge: 133.64102451254365,
1130            adj_comb_mpgge: 153.61727461480555,
1131            adj_udds_kwh_per_mi: 0.27097024959679444,
1132            adj_hwy_kwh_per_mi: 0.3259039663244739,
1133            adj_comb_kwh_per_mi: 0.29569042212425023,
1134            adj_udds_ess_kwh_per_mi: 0.23303441465324323,
1135            adj_hwy_ess_kwh_per_mi: 0.2802774110390475,
1136            adj_comb_ess_kwh_per_mi: 0.25429376302685514,
1137            net_range_miles: 453.1180867180584,
1138            uf: 0.73185,
1139            // net_accel: 7.962519496024332, <- Correct accel value
1140            net_accel: 1000.,
1141            res_found: String::from("model needs to be implemented for this"),
1142            phev_calcs: Some(phev_calcs),
1143            adj_cs_comb_mpgge: Some(45.06826741586106),
1144            adj_cd_comb_mpgge: Some(2293.5675017498143),
1145            net_phev_cd_miles: Some(57.04992781503185),
1146            trace_miss_speed_mph: 0.0,
1147        };
1148
1149        let tol = 1e-8;
1150        assert!(label_fe.veh.approx_eq(&label_fe_truth.veh, tol));
1151        assert!(
1152            label_fe
1153                .phev_calcs
1154                .approx_eq(&label_fe_truth.phev_calcs, tol),
1155            "label_fe.phev_calcs: {:?}",
1156            &label_fe.phev_calcs
1157        );
1158        assert!(label_fe.approx_eq(&label_fe_truth, tol));
1159    }
1160}