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