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