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(sd_accel: &mut SimDrive) -> anyhow::Result<f64> {
22 sd_accel.sim_params.trace_miss_opts = TraceMissOptions::Allow;
23 sd_accel.walk().with_context(|| format_dbg!())?;
24
25 let mut speed_mph: Vec<f64> = vec![];
27 for s in sd_accel.veh.history.speed_ach.clone() {
28 speed_mph.push(s.get_fresh(|| format_dbg!())?.get::<si::mile_per_hour>())
29 }
30
31 let time_s: Vec<f64> = sd_accel
33 .cyc
34 .time
35 .iter()
36 .map(|t| t.get::<si::second>())
37 .collect();
38
39 if speed_mph.iter().any(|&x| x >= 60.0) {
41 let interp: InterpolatorEnumOwned<f64> = InterpolatorEnum::new_1d(
43 speed_mph.clone().into(),
44 time_s.clone().into(),
45 strategy::Linear,
46 Extrapolate::Clamp,
47 )
48 .with_context(|| format_dbg!())?;
49
50 let accel_time = interp.interpolate(&[60.0])?;
52 Ok(accel_time)
53 } else {
54 println!(
56 "Warning: Vehicle '{}' doesn't reach 60 mph in the acceleration test",
57 sd_accel.veh.name
58 );
59 Ok(f64::NAN)
60 }
61}
62
63const DEFAULT_CHG_EFF: f64 = 0.86;
65
66#[serde_api]
67#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
68#[cfg_attr(feature = "pyo3", pyclass(module = "fastsim", subclass, eq))]
69pub struct FuelProperties {
70 pub energy_density: si::Pressure,
75 pub density: si::MassDensity,
77}
78
79impl Init for FuelProperties {}
80impl SerdeAPI for FuelProperties {}
81
82#[pyo3_api]
83impl FuelProperties {}
84
85impl Default for FuelProperties {
86 fn default() -> Self {
88 Self {
89 energy_density: 33.7 * uc::KWH / uc::GALLON,
90 density: 0.75 * uc::KG / uc::L,
91 }
92 }
93}
94
95const J_PER_KWH: f64 = 3_600.0;
96lazy_static! {
97 static ref CUBIC_METER_PER_GAL: f64 = 3.79e-3;
98}
99
100impl FuelProperties {
101 fn kwh_per_gge(&self) -> f64 {
102 self.energy_density.get::<si::joule_per_cubic_meter>() / J_PER_KWH * *CUBIC_METER_PER_GAL
103 }
104}
105
106trait VehicleEfficiency {
107 fn mpg(&self, energy_density: si::Pressure) -> anyhow::Result<f64>;
108
109 fn kwh_per_mi(&self) -> anyhow::Result<f64>;
110}
111
112impl VehicleEfficiency for Vehicle {
113 fn mpg(&self, energy_density: si::Pressure) -> anyhow::Result<f64> {
114 if let Some(fc) = self.fc() {
115 Ok(self
116 .state
117 .dist
118 .get_fresh(|| format_dbg!())?
119 .get::<si::mile>()
120 / (*fc.state.energy_fuel.get_fresh(|| format_dbg!())? / energy_density)
121 .get::<si::gallon>())
122 } else {
123 Ok(f64::NAN)
124 }
125 }
126
127 fn kwh_per_mi(&self) -> anyhow::Result<f64> {
128 if let Some(res) = self.res() {
129 Ok(res
130 .state
131 .energy_out_chemical
132 .get_fresh(|| format_dbg!())?
133 .get::<si::kilowatt_hour>()
134 / self
135 .state
136 .dist
137 .get_fresh(|| format_dbg!())?
138 .get::<si::mile>())
139 } else {
140 Ok(f64::NAN)
141 }
142 }
143}
144
145#[serde_api]
146#[derive(Clone, Default, Debug, Deserialize, Serialize, PartialEq)]
147#[cfg_attr(feature = "pyo3", pyclass(module = "fastsim", subclass, eq))]
148pub struct LabelFe {
149 pub veh: Option<Vehicle>,
150 pub adj_params: AdjCoef,
151 pub lab_udds_mpgge: f64,
152 pub lab_hwy_mpgge: f64,
153 pub lab_comb_mpgge: f64,
154 pub lab_udds_kwh_per_mi: f64,
155 pub lab_hwy_kwh_per_mi: f64,
156 pub lab_comb_kwh_per_mi: f64,
157 pub adj_udds_mpgge: f64,
158 pub adj_hwy_mpgge: f64,
159 pub adj_comb_mpgge: f64,
160 pub adj_udds_kwh_per_mi: f64,
161 pub adj_hwy_kwh_per_mi: f64,
162 pub adj_comb_kwh_per_mi: f64,
163 pub adj_udds_ess_kwh_per_mi: f64,
164 pub adj_hwy_ess_kwh_per_mi: f64,
165 pub adj_comb_ess_kwh_per_mi: f64,
166 pub net_range_miles: f64,
167 pub uf: f64,
168 pub net_accel: f64,
169 pub res_found: String,
170 pub phev_calcs: Option<LabelFePHEV>,
171 pub adj_cs_comb_mpgge: Option<f64>,
172 pub adj_cd_comb_mpgge: Option<f64>,
173 pub net_phev_cd_miles: Option<f64>,
174}
175
176#[pyo3_api]
177impl LabelFe {}
178
179impl Init for LabelFe {}
180impl SerdeAPI for LabelFe {}
181
182#[serde_api]
183#[derive(Default, Clone, Debug, Deserialize, Serialize, PartialEq)]
184#[cfg_attr(feature = "pyo3", pyclass(module = "fastsim", subclass, eq))]
185pub struct LabelFePHEV {
187 pub regen_soc_buffer: si::Ratio,
188 pub udds: PHEVCycleCalc,
189 pub hwy: PHEVCycleCalc,
190}
191
192#[pyo3_api]
193impl LabelFePHEV {}
194
195impl Init for LabelFePHEV {}
196impl SerdeAPI for LabelFePHEV {}
197
198#[serde_api]
199#[derive(Default, Clone, Debug, Deserialize, Serialize, PartialEq)]
200#[cfg_attr(feature = "pyo3", pyclass(module = "fastsim", subclass, eq))]
201pub struct PHEVCycleCalc {
203 pub cd_ess_kwh: f64,
205 pub cd_ess_kwh_per_mi: f64,
206 pub cd_fs_gal: f64,
208 pub cd_fs_kwh: f64,
209 pub cd_mpg: f64,
210 pub cd_cycs: f64,
212 pub cd_miles: f64,
213 pub cd_lab_mpg: f64,
214 pub cd_adj_mpg: f64,
215 pub cd_frac_in_trans: f64,
217 pub trans_init_soc: si::Ratio,
219 pub trans_ess_kwh: f64,
221 pub trans_ess_kwh_per_mi: f64,
222 pub trans_fs_gal: f64,
223 pub trans_fs_kwh: f64,
224 pub cs_ess_kwh: f64,
226 pub cs_ess_kwh_per_mi: f64,
227 pub cs_fs_gal: f64,
229 pub cs_fs_kwh: f64,
230 pub cs_mpg: f64,
231 pub lab_mpgge: f64,
232 pub lab_kwh_per_mi: f64,
233 pub lab_uf: f64,
234 pub lab_uf_gpm: Vec<f64>,
235 pub lab_iter_uf: Vec<f64>,
236 pub lab_iter_uf_kwh_per_mi: Vec<f64>,
237 pub lab_iter_kwh_per_mi: Vec<f64>,
238 pub adj_iter_mpgge: Vec<f64>,
239 pub adj_iter_kwh_per_mi: Vec<f64>,
240 pub adj_iter_cd_miles: Vec<f64>,
241 pub adj_iter_uf: Vec<f64>,
242 pub adj_iter_uf_gpm: Vec<f64>,
243 pub adj_iter_uf_kwh_per_mi: Vec<f64>,
244 pub adj_cd_miles: f64,
245 pub adj_cd_mpgge: f64,
246 pub adj_cs_mpgge: f64,
247 pub adj_uf: f64,
248 pub adj_mpgge: f64,
249 pub adj_kwh_per_mi: f64,
250 pub adj_ess_kwh_per_mi: f64,
251 pub delta_soc: si::Ratio,
252 pub total_cd_miles: f64,
254}
255
256impl Init for PHEVCycleCalc {}
257impl SerdeAPI for PHEVCycleCalc {}
258
259#[pyo3_api]
260impl PHEVCycleCalc {}
261
262#[derive(Clone, Serialize, Deserialize, Debug, PartialEq)]
263#[cfg_attr(feature = "pyo3", pyclass(module = "fastsim", subclass, eq))]
264pub struct AdjCoef {
265 pub city_intercept: f64,
266 pub city_slope: f64,
267 pub hwy_intercept: f64,
268 pub hwy_slope: f64,
269}
270
271#[pyo3_api]
272impl AdjCoef {}
273
274impl Init for AdjCoef {}
275impl SerdeAPI for AdjCoef {}
276
277impl Default for AdjCoef {
278 fn default() -> Self {
279 Self {
280 city_intercept: 0.003259,
281 city_slope: 1.1805,
282 hwy_intercept: 0.001376,
283 hwy_slope: 1.3466,
284 }
285 }
286}
287
288#[serde_api]
289#[derive(Clone, Serialize, Deserialize, Debug, PartialEq)]
290#[cfg_attr(feature = "pyo3", pyclass(module = "fastsim", subclass, eq))]
291pub struct PhevUtilizationParams {
292 pub adj_coef_map: HashMap<String, AdjCoef>,
293 pub rechg_freq_miles: Vec<f64>,
295 pub uf_array: Vec<f64>,
297}
298
299impl Init for PhevUtilizationParams {}
300impl SerdeAPI for PhevUtilizationParams {}
301
302impl Default for PhevUtilizationParams {
303 fn default() -> Self {
304 Self::from_json(&*PHEV_UTIL_PARAMS, false).unwrap()
305 }
306}
307
308lazy_static! {
309 static ref PHEV_UTIL_PARAMS: String =
310 include_str!("./simdrivelabel/longparams.json").to_string();
311}
312
313pub fn get_label_fe(
326 veh: &mut Vehicle,
327 max_epa_adj: Option<f64>,
328 full_detail: bool,
329 fuel_props: Option<FuelProperties>,
330 phev_utilization_params: Option<PhevUtilizationParams>,
331 verbose: bool,
332) -> anyhow::Result<(LabelFe, Option<HashMap<&str, SimDrive>>)> {
333 let max_epa_adj = max_epa_adj.unwrap_or(0.3);
334 let phev_utilization_params = &phev_utilization_params.unwrap_or_default();
335 let fuel_props = fuel_props.unwrap_or_default();
336
337 let mut cyc: HashMap<&str, Cycle> = HashMap::new();
338 let mut sd = HashMap::new();
339 let mut label_fe = LabelFe::default();
340
341 label_fe.veh = Some(veh.clone());
342
343 cyc.insert("accel", CYC_ACCEL.clone());
345 cyc.insert("udds", Cycle::from_resource("udds.csv", false)?);
346 cyc.insert("hwy", Cycle::from_resource("hwfet.csv", false)?);
347
348 if veh.pt_type.is_plug_in_hybrid_electric_vehicle() {
349 let rm = veh.res_mut().unwrap();
350 rm.state.soc.check_and_reset(|| format_dbg!()).unwrap();
351 rm.state.soc.update(rm.max_soc, || format_dbg!()).unwrap();
352 }
353
354 sd.insert(
356 "udds",
357 SimDrive::new(veh.clone(), cyc["udds"].clone(), None),
358 );
359 sd.insert("hwy", SimDrive::new(veh.clone(), cyc["hwy"].clone(), None));
360
361 for (k, val) in sd.iter_mut() {
362 val.walk().with_context(|| format_dbg!(k))?;
363 }
364
365 let adj_params = if veh.year < 2017 {
367 &phev_utilization_params.adj_coef_map["2008"]
368 } else {
369 &phev_utilization_params.adj_coef_map["2017"]
371 };
372 label_fe.adj_params = adj_params.clone();
373
374 let is_phev = matches!(veh.pt_type, PowertrainType::PlugInHybridElectricVehicle(_));
376 let is_bev = matches!(veh.pt_type, PowertrainType::BatteryElectricVehicle(_));
377
378 if !is_phev {
380 if !is_bev {
381 label_fe.lab_udds_mpgge = sd["udds"].veh.mpg(fuel_props.energy_density)?;
383 label_fe.lab_hwy_mpgge = sd["hwy"].veh.mpg(fuel_props.energy_density)?;
385 label_fe.lab_comb_mpgge = 1.
386 / (0.55 / sd["udds"].veh.mpg(fuel_props.energy_density)?
387 + 0.45 / sd["hwy"].veh.mpg(fuel_props.energy_density)?);
388 } else {
389 label_fe.lab_udds_mpgge = 0.;
390 label_fe.lab_hwy_mpgge = 0.;
391 label_fe.lab_comb_mpgge = 0.;
392 }
393
394 if is_bev {
395 label_fe.lab_udds_kwh_per_mi = sd["udds"].veh.kwh_per_mi()?;
396 label_fe.lab_hwy_kwh_per_mi = sd["hwy"].veh.kwh_per_mi()?;
397 label_fe.lab_comb_kwh_per_mi =
398 0.55 * sd["udds"].veh.kwh_per_mi()? + 0.45 * sd["hwy"].veh.kwh_per_mi()?;
399 } else {
400 label_fe.lab_udds_kwh_per_mi = 0.;
401 label_fe.lab_hwy_kwh_per_mi = 0.;
402 label_fe.lab_comb_kwh_per_mi = 0.;
403 }
404
405 if !is_bev {
407 label_fe.adj_udds_mpgge = 1.
411 / (adj_params.city_intercept
412 + adj_params.city_slope / sd["udds"].veh.mpg(fuel_props.energy_density)?);
413 label_fe.adj_hwy_mpgge = 1.
415 / (adj_params.hwy_intercept
416 + adj_params.hwy_slope / sd["hwy"].veh.mpg(fuel_props.energy_density)?);
417 label_fe.adj_comb_mpgge =
418 1. / (0.55 / label_fe.adj_udds_mpgge + 0.45 / label_fe.adj_hwy_mpgge);
419 } else {
420 label_fe.adj_udds_mpgge = 0.;
423 label_fe.adj_hwy_mpgge = 0.;
424 label_fe.adj_comb_mpgge = 0.;
425 }
426
427 if is_bev {
429 label_fe.adj_udds_kwh_per_mi =
431 (1. / f64::max(
432 1. / (adj_params.city_intercept
433 + (adj_params.city_slope
434 / ((1. / label_fe.lab_udds_kwh_per_mi) * fuel_props.kwh_per_gge()))),
435 (1. / label_fe.lab_udds_kwh_per_mi)
436 * fuel_props.kwh_per_gge()
437 * (1. - max_epa_adj),
438 )) * fuel_props.kwh_per_gge()
439 / DEFAULT_CHG_EFF;
440 label_fe.adj_hwy_kwh_per_mi =
441 (1. / f64::max(
442 1. / (adj_params.hwy_intercept
443 + (adj_params.hwy_slope
444 / ((1. / label_fe.lab_hwy_kwh_per_mi) * fuel_props.kwh_per_gge()))),
445 (1. / label_fe.lab_hwy_kwh_per_mi)
446 * fuel_props.kwh_per_gge()
447 * (1. - max_epa_adj),
448 )) * fuel_props.kwh_per_gge()
449 / DEFAULT_CHG_EFF;
450 label_fe.adj_comb_kwh_per_mi =
451 0.55 * label_fe.adj_udds_kwh_per_mi + 0.45 * label_fe.adj_hwy_kwh_per_mi;
452
453 label_fe.adj_udds_ess_kwh_per_mi = label_fe.adj_udds_kwh_per_mi * DEFAULT_CHG_EFF;
454 label_fe.adj_hwy_ess_kwh_per_mi = label_fe.adj_hwy_kwh_per_mi * DEFAULT_CHG_EFF;
455 label_fe.adj_comb_ess_kwh_per_mi = label_fe.adj_comb_kwh_per_mi * DEFAULT_CHG_EFF;
456
457 if let PowertrainType::BatteryElectricVehicle(bev) = &veh.pt_type {
460 label_fe.net_range_miles = bev.res.energy_capacity.get::<si::kilowatt_hour>()
461 / label_fe.adj_comb_ess_kwh_per_mi;
462 }
463 }
464
465 label_fe.uf = 0.;
467 } else {
468 let phev_calcs = get_label_fe_phev(
470 veh,
471 phev_utilization_params,
472 adj_params,
473 max_epa_adj,
474 &fuel_props,
475 )?;
476 label_fe.phev_calcs = Some(phev_calcs.clone());
477
478 label_fe.lab_udds_mpgge = phev_calcs.udds.lab_mpgge;
481 label_fe.lab_hwy_mpgge = phev_calcs.hwy.lab_mpgge;
482 label_fe.lab_comb_mpgge =
483 1.0 / (0.55 / phev_calcs.udds.lab_mpgge + 0.45 / phev_calcs.hwy.lab_mpgge);
484
485 label_fe.lab_udds_kwh_per_mi = phev_calcs.udds.lab_kwh_per_mi;
486 label_fe.lab_hwy_kwh_per_mi = phev_calcs.hwy.lab_kwh_per_mi;
487 label_fe.lab_comb_kwh_per_mi =
488 0.55 * phev_calcs.udds.lab_kwh_per_mi + 0.45 * phev_calcs.hwy.lab_kwh_per_mi;
489
490 label_fe.adj_udds_mpgge = phev_calcs.udds.adj_mpgge;
492 label_fe.adj_hwy_mpgge = phev_calcs.hwy.adj_mpgge;
493 label_fe.adj_comb_mpgge =
494 1.0 / (0.55 / phev_calcs.udds.adj_mpgge + 0.45 / phev_calcs.hwy.adj_mpgge);
495
496 label_fe.adj_cs_comb_mpgge =
497 Some(1.0 / (0.55 / phev_calcs.udds.adj_cs_mpgge + 0.45 / phev_calcs.hwy.adj_cs_mpgge));
498 label_fe.adj_cd_comb_mpgge =
499 Some(1.0 / (0.55 / phev_calcs.udds.adj_cd_mpgge + 0.45 / phev_calcs.hwy.adj_cd_mpgge));
500
501 label_fe.adj_udds_kwh_per_mi = phev_calcs.udds.adj_kwh_per_mi;
502 label_fe.adj_hwy_kwh_per_mi = phev_calcs.hwy.adj_kwh_per_mi;
503 label_fe.adj_comb_kwh_per_mi =
504 0.55 * phev_calcs.udds.adj_kwh_per_mi + 0.45 * phev_calcs.hwy.adj_kwh_per_mi;
505
506 label_fe.adj_udds_ess_kwh_per_mi = phev_calcs.udds.adj_ess_kwh_per_mi;
507 label_fe.adj_hwy_ess_kwh_per_mi = phev_calcs.hwy.adj_ess_kwh_per_mi;
508 label_fe.adj_comb_ess_kwh_per_mi =
509 0.55 * phev_calcs.udds.adj_ess_kwh_per_mi + 0.45 * phev_calcs.hwy.adj_ess_kwh_per_mi;
510
511 label_fe.uf = phev_utilization_params.uf_array[first_grtr(
514 &phev_utilization_params.rechg_freq_miles,
515 0.55 * phev_calcs.udds.adj_cd_miles + 0.45 * phev_calcs.hwy.adj_cd_miles,
516 )
517 .with_context(|| format_dbg!())?
518 - 1];
519
520 label_fe.net_phev_cd_miles =
521 Some(0.55 * phev_calcs.udds.adj_cd_miles + 0.45 * phev_calcs.hwy.adj_cd_miles);
522
523 if let PowertrainType::PlugInHybridElectricVehicle(phev) = &veh.pt_type {
525 let fuel_energy_kwh = phev.fs.energy_capacity.get::<si::kilowatt_hour>();
527 let fuel_energy_gge = fuel_energy_kwh / fuel_props.kwh_per_gge();
528
529 label_fe.net_range_miles = (fuel_energy_gge
530 - label_fe.net_phev_cd_miles.with_context(|| format_dbg!())?
531 / label_fe.adj_cd_comb_mpgge.with_context(|| format_dbg!())?)
532 * label_fe.adj_cs_comb_mpgge.with_context(|| format_dbg!())?
533 + label_fe.net_phev_cd_miles.with_context(|| format_dbg!())?;
534 }
535 }
536
537 let mut sd_accel = SimDrive::new(veh.clone(), cyc["accel"].clone(), None);
539 label_fe.net_accel = get_0_to_60_time(&mut sd_accel)
540 .with_context(|| format!("`get_0_to_60_time`: {}", format_dbg!()))?;
541 sd.insert("accel", sd_accel);
542
543 label_fe.res_found = String::from("model needs to be implemented for this");
545
546 if full_detail && verbose {
547 println!("{label_fe:#?}");
548 Ok((label_fe, Some(sd)))
549 } else if full_detail {
550 Ok((label_fe, Some(sd)))
551 } else if verbose {
552 println!("{label_fe:#?}");
553 Ok((label_fe, None))
554 } else {
555 Ok((label_fe, None))
556 }
557}
558
559#[cfg(feature = "pyo3")]
560#[pyfunction(name = "get_label_fe")]
561#[cfg_attr(
562 feature = "pyo3",
563 pyo3(signature = (
564 veh, max_epa_adj=None, full_detail=None, fuel_props=None, phev_utilization_params=None, verbose=None))
565)]
566pub fn get_label_fe_py(
568 veh: &mut Vehicle,
569 max_epa_adj: Option<f64>,
570 full_detail: Option<bool>,
571 fuel_props: Option<FuelProperties>,
572 phev_utilization_params: Option<PhevUtilizationParams>,
573 verbose: Option<bool>,
574) -> anyhow::Result<LabelFe> {
575 let (label_fe, _) = get_label_fe(
576 veh,
577 max_epa_adj,
578 full_detail.unwrap_or_default(),
579 fuel_props,
580 phev_utilization_params,
581 verbose.unwrap_or_default(),
582 )?;
583 Ok(label_fe)
584}
585
586pub fn get_label_fe_phev(
594 veh: &Vehicle,
595 phev_utilization_params: &PhevUtilizationParams,
596 adj_params: &AdjCoef,
597 max_epa_adj: f64,
598 fuel_props: &FuelProperties,
599) -> anyhow::Result<LabelFePHEV> {
600 let max_soc: si::Ratio;
602 let min_soc: si::Ratio;
603 let phev_max_regen: si::Ratio;
604 let veh_mass: si::Mass;
605 let em_peak_eff: si::Ratio;
607 let energy_capacity: si::Energy;
609 let chg_eff: f64;
610
611 if let PowertrainType::PlugInHybridElectricVehicle(phev) = &veh.pt_type {
612 max_soc = phev.res.max_soc;
613 min_soc = phev.res.min_soc;
614 phev_max_regen = 0.98 * uc::R;
615 veh_mass = *veh.state.mass.get_fresh(|| format_dbg!())?;
616 em_peak_eff = *phev
617 .em
618 .eff_interp_achieved
619 .max()
620 .with_context(|| format_dbg!())?
621 * uc::R;
622 energy_capacity = phev.res.energy_capacity;
623 chg_eff = DEFAULT_CHG_EFF; } else {
625 bail!("Vehicle is not a PHEV");
626 }
627
628 let mut label_fe_phev = LabelFePHEV {
629 regen_soc_buffer: ((0.5 * veh_mass * ((60. * uc::MPH).powi(P2::new())))
630 * phev_max_regen
631 * em_peak_eff
632 / energy_capacity)
633 .min((max_soc - min_soc) / 2.0),
634 ..Default::default()
635 };
636
637 let mut sd: HashMap<&str, SimDrive> = HashMap::new();
639 sd.insert(
640 "udds",
641 SimDrive::new(veh.clone(), Cycle::from_resource("udds.csv", false)?, None),
642 );
643 sd.insert(
644 "hwy",
645 SimDrive::new(veh.clone(), Cycle::from_resource("hwfet.csv", false)?, None),
646 );
647
648 for (key, sd) in sd.iter_mut() {
650 sd.walk()?;
655 let mut phev_calc = PHEVCycleCalc::default();
656
657 phev_calc.cd_ess_kwh = ((max_soc - min_soc) * energy_capacity).get::<si::kilowatt_hour>();
660
661 let res = sd.veh.res().with_context(|| format_dbg!())?;
663 let soc_start = *res
664 .history
665 .soc
666 .first()
667 .with_context(|| format_dbg!())?
668 .get_fresh(|| format_dbg!())?;
669 let soc_end = *res
670 .history
671 .soc
672 .last()
673 .with_context(|| format_dbg!())?
674 .get_fresh(|| format_dbg!())?;
675 let dist_mi = sd
676 .veh
677 .state
678 .dist
679 .get_fresh(|| format_dbg!())?
680 .get::<si::mile>();
681
682 phev_calc.delta_soc = soc_start - soc_end;
684 phev_calc.total_cd_miles = ((max_soc - min_soc) * energy_capacity)
686 .get::<si::kilowatt_hour>()
687 / sd.veh.kwh_per_mi()?;
688 phev_calc.cd_cycs = phev_calc.total_cd_miles / dist_mi;
690 phev_calc.cd_frac_in_trans = phev_calc.cd_cycs % phev_calc.cd_cycs.floor();
692
693 let fuel_energy_kwh = if let Some(fc) = sd.veh.fc() {
695 fc.state
696 .energy_fuel
697 .get_fresh(|| format_dbg!())?
698 .get::<si::kilowatt_hour>()
699 } else {
700 0.0
701 };
702 phev_calc.cd_fs_gal = fuel_energy_kwh / fuel_props.kwh_per_gge();
703 phev_calc.cd_fs_kwh = fuel_energy_kwh;
704 phev_calc.cd_ess_kwh_per_mi = sd.veh.kwh_per_mi()?;
705 phev_calc.cd_mpg = sd.veh.mpg(fuel_props.energy_density)?;
706
707 let interp_x_vals: Vec<f64> = (0..((phev_calc.cd_cycs.ceil() + 1.0) as usize))
710 .map(|i| i as f64 * dist_mi)
711 .collect();
712
713 phev_calc.lab_iter_uf = vec![];
714 for x in interp_x_vals {
715 phev_calc.lab_iter_uf.push(
716 phev_utilization_params.uf_array[first_grtr(
717 &phev_utilization_params.rechg_freq_miles,
718 x,
719 )
720 .with_context(|| format_dbg!())?
721 - 1],
722 );
723 }
724
725 phev_calc.trans_init_soc = max_soc - phev_calc.cd_cycs.floor() * phev_calc.delta_soc;
727
728 let res_mut = sd.veh.res_mut().with_context(|| format_dbg!())?;
730 res_mut.state.soc.mark_stale();
731 res_mut
732 .state
733 .soc
734 .update(phev_calc.trans_init_soc, || format_dbg!())?;
735 sd.walk()?;
736
737 phev_calc.trans_ess_kwh =
739 phev_calc.cd_ess_kwh_per_mi * dist_mi * phev_calc.cd_frac_in_trans;
740 phev_calc.trans_ess_kwh_per_mi = phev_calc.cd_ess_kwh_per_mi * phev_calc.cd_frac_in_trans;
741
742 let init_soc = min_soc + 0.01 * uc::R;
745 let res_mut = sd.veh.res_mut().with_context(|| format_dbg!())?;
746 res_mut.state.soc.mark_stale();
747 res_mut.state.soc.update(init_soc, || format_dbg!())?;
748 sd.walk()?;
749
750 let cs_fuel_energy_kwh = if let Some(fc) = sd.veh.fc() {
752 fc.state
753 .energy_fuel
754 .get_fresh(|| format_dbg!())?
755 .get::<si::kilowatt_hour>()
756 } else {
757 0.0
758 };
759 phev_calc.cs_fs_gal = cs_fuel_energy_kwh / fuel_props.kwh_per_gge();
760 phev_calc.trans_fs_gal = phev_calc.cs_fs_gal * (1.0 - phev_calc.cd_frac_in_trans);
762 phev_calc.cs_fs_kwh = cs_fuel_energy_kwh;
763 phev_calc.trans_fs_kwh = phev_calc.cs_fs_kwh * (1.0 - phev_calc.cd_frac_in_trans);
764 let cs_ess_energy_kwh = if let Some(res) = sd.veh.res() {
766 res.state
767 .energy_out_chemical
768 .get_fresh(|| format_dbg!())?
769 .get::<si::kilowatt_hour>()
770 } else {
771 0.0
772 };
773 phev_calc.cs_ess_kwh = cs_ess_energy_kwh;
774 phev_calc.cs_ess_kwh_per_mi = sd.veh.kwh_per_mi()?;
775
776 let lab_iter_uf_diff = phev_calc.lab_iter_uf.diff();
777 phev_calc.lab_uf_gpm = [
778 phev_calc.trans_fs_gal * lab_iter_uf_diff.last().with_context(|| format_dbg!())?,
779 phev_calc.cs_fs_gal
780 * (1.0
781 - phev_calc
782 .lab_iter_uf
783 .last()
784 .with_context(|| format_dbg!())?),
785 ]
786 .iter()
787 .map(|x| *x / dist_mi)
788 .collect();
789
790 phev_calc.cd_mpg = sd.veh.mpg(fuel_props.energy_density)?;
791
792 let min_soc_in_cycle = phev_calc.delta_soc.abs(); phev_calc.cd_miles =
795 if (max_soc - label_fe_phev.regen_soc_buffer - min_soc_in_cycle) < 0.01 * uc::R {
796 1000.0
797 } else {
798 phev_calc.cd_cycs.ceil() * dist_mi
799 };
800 phev_calc.cd_lab_mpg = phev_calc
801 .lab_iter_uf
802 .last()
803 .with_context(|| format_dbg!())?
804 / (phev_calc.trans_fs_gal / dist_mi);
805
806 phev_calc.cs_mpg = dist_mi / phev_calc.cs_fs_gal;
808
809 phev_calc.lab_uf = phev_utilization_params.uf_array[first_grtr(
810 &phev_utilization_params.rechg_freq_miles,
811 phev_calc.cd_miles,
812 )
813 .with_context(|| format_dbg!())?
814 - 1];
815
816 phev_calc.cd_adj_mpg =
818 phev_calc.lab_iter_uf.max()? / phev_calc.lab_uf_gpm[phev_calc.lab_uf_gpm.len() - 2];
819
820 phev_calc.lab_mpgge = 1.0
821 / (phev_calc.lab_uf / phev_calc.cd_adj_mpg
822 + (1.0 - phev_calc.lab_uf) / phev_calc.cs_mpg);
823
824 let mut lab_iter_kwh_per_mi_vals = Vec::new();
825 lab_iter_kwh_per_mi_vals.push(0.0);
826 lab_iter_kwh_per_mi_vals
827 .extend(vec![phev_calc.cd_ess_kwh_per_mi; phev_calc.cd_cycs.floor() as usize].iter());
828 lab_iter_kwh_per_mi_vals.push(phev_calc.trans_ess_kwh_per_mi);
829 lab_iter_kwh_per_mi_vals.push(0.0);
830 phev_calc.lab_iter_kwh_per_mi = lab_iter_kwh_per_mi_vals;
831
832 let uf_diff = phev_calc.lab_iter_uf.diff();
833 let mut vals = Vec::new();
834 vals.push(0.0);
835 for i in 1..phev_calc.lab_iter_kwh_per_mi.len() - 1 {
836 if i - 1 < uf_diff.len() {
837 vals.push(phev_calc.lab_iter_kwh_per_mi[i] * uf_diff[i - 1]);
838 }
839 }
840 vals.push(0.0);
841 phev_calc.lab_iter_uf_kwh_per_mi = vals;
842
843 phev_calc.lab_kwh_per_mi = phev_calc
844 .lab_iter_uf_kwh_per_mi
845 .iter()
846 .fold(0.0, |acc, x| acc + x)
847 / phev_calc
848 .lab_iter_uf
849 .iter()
850 .fold(0.0f64, |acc, x| acc.max(*x));
851
852 let mut adj_iter_mpgge_vals = vec![0.0; phev_calc.cd_cycs.floor() as usize];
853 let mut adj_iter_kwh_per_mi_vals = vec![0.0; phev_calc.lab_iter_kwh_per_mi.len()];
854 if *key == "udds" {
855 adj_iter_mpgge_vals.push(f64::max(
856 1.0 / (adj_params.city_intercept
857 + (adj_params.city_slope
858 / (sd
859 .veh
860 .state
861 .dist
862 .get_fresh(|| format_dbg!())?
863 .get::<si::mile>()
864 / (phev_calc.trans_fs_kwh / fuel_props.kwh_per_gge())))),
865 sd.veh
866 .state
867 .dist
868 .get_fresh(|| format_dbg!())?
869 .get::<si::mile>()
870 / (phev_calc.trans_fs_kwh / fuel_props.kwh_per_gge())
871 * (1.0 - max_epa_adj),
872 ));
873 adj_iter_mpgge_vals.push(f64::max(
874 1.0 / (adj_params.city_intercept
875 + (adj_params.city_slope
876 / (sd
877 .veh
878 .state
879 .dist
880 .get_fresh(|| format_dbg!())?
881 .get::<si::mile>()
882 / (phev_calc.cs_fs_kwh / fuel_props.kwh_per_gge())))),
883 sd.veh
884 .state
885 .dist
886 .get_fresh(|| format_dbg!())?
887 .get::<si::mile>()
888 / (phev_calc.cs_fs_kwh / fuel_props.kwh_per_gge())
889 * (1.0 - max_epa_adj),
890 ));
891
892 for (c, _) in phev_calc.lab_iter_kwh_per_mi.iter().enumerate() {
893 if phev_calc.lab_iter_kwh_per_mi[c] == 0.0 {
894 adj_iter_kwh_per_mi_vals[c] = 0.0;
895 } else {
896 adj_iter_kwh_per_mi_vals[c] =
897 (1.0 / f64::max(
898 1.0 / (adj_params.city_intercept
899 + (adj_params.city_slope
900 / ((1.0 / phev_calc.lab_iter_kwh_per_mi[c])
901 * fuel_props.kwh_per_gge()))),
902 (1.0 - max_epa_adj)
903 * ((1.0 / phev_calc.lab_iter_kwh_per_mi[c])
904 * fuel_props.kwh_per_gge()),
905 )) * fuel_props.kwh_per_gge();
906 }
907 }
908 } else {
909 adj_iter_mpgge_vals.push(f64::max(
910 1.0 / (adj_params.hwy_intercept
911 + (adj_params.hwy_slope
912 / (sd
913 .veh
914 .state
915 .dist
916 .get_fresh(|| format_dbg!())?
917 .get::<si::mile>()
918 / (phev_calc.trans_fs_kwh / fuel_props.kwh_per_gge())))),
919 sd.veh
920 .state
921 .dist
922 .get_fresh(|| format_dbg!())?
923 .get::<si::mile>()
924 / (phev_calc.trans_fs_kwh / fuel_props.kwh_per_gge())
925 * (1.0 - max_epa_adj),
926 ));
927 adj_iter_mpgge_vals.push(f64::max(
928 1.0 / (adj_params.hwy_intercept
929 + (adj_params.hwy_slope
930 / (sd
931 .veh
932 .state
933 .dist
934 .get_fresh(|| format_dbg!())?
935 .get::<si::mile>()
936 / (phev_calc.cs_fs_kwh / fuel_props.kwh_per_gge())))),
937 sd.veh
938 .state
939 .dist
940 .get_fresh(|| format_dbg!())?
941 .get::<si::mile>()
942 / (phev_calc.cs_fs_kwh / fuel_props.kwh_per_gge())
943 * (1.0 - max_epa_adj),
944 ));
945
946 for (c, _) in phev_calc.lab_iter_kwh_per_mi.iter().enumerate() {
947 if phev_calc.lab_iter_kwh_per_mi[c] == 0.0 {
948 adj_iter_kwh_per_mi_vals[c] = 0.0;
949 } else {
950 adj_iter_kwh_per_mi_vals[c] =
951 (1.0 / f64::max(
952 1.0 / (adj_params.hwy_intercept
953 + (adj_params.hwy_slope
954 / ((1.0 / phev_calc.lab_iter_kwh_per_mi[c])
955 * fuel_props.kwh_per_gge()))),
956 (1.0 - max_epa_adj)
957 * ((1.0 / phev_calc.lab_iter_kwh_per_mi[c])
958 * fuel_props.kwh_per_gge()),
959 )) * fuel_props.kwh_per_gge();
960 }
961 }
962 }
963 phev_calc.adj_iter_mpgge = adj_iter_mpgge_vals;
964 phev_calc.adj_iter_kwh_per_mi = adj_iter_kwh_per_mi_vals;
965
966 phev_calc.adj_iter_cd_miles = vec![0.0; phev_calc.cd_cycs.ceil() as usize + 2];
967 for c in 0..phev_calc.adj_iter_cd_miles.len() {
968 if c == 0 {
969 phev_calc.adj_iter_cd_miles[c] = 0.0;
970 } else if c <= phev_calc.cd_cycs.floor() as usize {
971 phev_calc.adj_iter_cd_miles[c] = phev_calc.adj_iter_cd_miles[c - 1]
972 + phev_calc.cd_ess_kwh_per_mi
973 * sd.veh
974 .state
975 .dist
976 .get_fresh(|| format_dbg!())?
977 .get::<si::mile>()
978 / phev_calc.adj_iter_kwh_per_mi[c];
979 } else if c == phev_calc.cd_cycs.floor() as usize + 1 {
980 phev_calc.adj_iter_cd_miles[c] = phev_calc.adj_iter_cd_miles[c - 1]
981 + phev_calc.trans_ess_kwh_per_mi
982 * sd.veh
983 .state
984 .dist
985 .get_fresh(|| format_dbg!())?
986 .get::<si::mile>()
987 / phev_calc.adj_iter_kwh_per_mi[c];
988 } else {
989 phev_calc.adj_iter_cd_miles[c] = 0.0;
990 }
991 }
992
993 let mut soc_hist: Vec<f64> = vec![];
994 for soc in sd
995 .veh
996 .res()
997 .with_context(|| format_dbg!())?
998 .history
999 .soc
1000 .clone()
1001 {
1002 soc_hist.push(soc.get_fresh(|| format_dbg!())?.get::<si::ratio>());
1003 }
1004
1005 phev_calc.adj_cd_miles =
1006 if max_soc - label_fe_phev.regen_soc_buffer - (*soc_hist.min()? * uc::R) < 0.01 * uc::R
1007 {
1008 1000.0
1009 } else {
1010 *phev_calc.adj_iter_cd_miles.max()?
1011 };
1012
1013 phev_calc.adj_iter_uf = vec![];
1017 for x in phev_calc.adj_iter_cd_miles.clone() {
1018 phev_calc.adj_iter_uf.push(
1019 phev_utilization_params.uf_array[first_grtr(
1020 &phev_utilization_params.rechg_freq_miles,
1021 x,
1022 )
1023 .with_context(|| format_dbg!())?
1024 - 1],
1025 )
1026 }
1027
1028 let adj_iter_uf_diff = phev_calc.adj_iter_uf.diff();
1029 phev_calc.adj_iter_uf_gpm = vec![0.0; phev_calc.cd_cycs.floor() as usize];
1030 phev_calc.adj_iter_uf_gpm.push(
1031 (1.0 / phev_calc.adj_iter_mpgge[phev_calc.adj_iter_mpgge.len() - 2])
1032 * adj_iter_uf_diff[adj_iter_uf_diff.len() - 2],
1033 );
1034 phev_calc.adj_iter_uf_gpm.push(
1035 (1.0 / phev_calc
1036 .adj_iter_mpgge
1037 .last()
1038 .with_context(|| format_dbg!())?)
1039 * (1.0 - phev_calc.adj_iter_uf[phev_calc.adj_iter_uf.len() - 2]),
1040 );
1041
1042 let adj_uf_diff = phev_calc.adj_iter_uf.diff();
1043 phev_calc.adj_iter_uf_kwh_per_mi = phev_calc
1044 .adj_iter_kwh_per_mi
1045 .iter()
1046 .zip(adj_uf_diff.iter())
1047 .map(|(kwh, uf)| kwh * uf)
1048 .collect();
1049
1050 let max_uf: f64 = phev_calc
1051 .adj_iter_uf
1052 .iter()
1053 .fold(0.0f64, |acc, x| acc.max(*x));
1054 phev_calc.adj_cd_mpgge =
1055 1.0 / phev_calc.adj_iter_uf_gpm[phev_calc.adj_iter_uf_gpm.len() - 2] * max_uf;
1056 phev_calc.adj_cs_mpgge = 1.0
1057 / phev_calc
1058 .adj_iter_uf_gpm
1059 .last()
1060 .with_context(|| format_dbg!())?
1061 * (1.0 - max_uf);
1062
1063 phev_calc.adj_uf = phev_utilization_params.uf_array[first_grtr(
1064 &phev_utilization_params.rechg_freq_miles,
1065 phev_calc.adj_cd_miles,
1066 )
1067 .with_context(|| format_dbg!())?
1068 - 1];
1069
1070 phev_calc.adj_mpgge = 1.0
1071 / (phev_calc.adj_uf / phev_calc.adj_cd_mpgge
1072 + (1.0 - phev_calc.adj_uf) / phev_calc.adj_cs_mpgge);
1073
1074 let uf_kwh_sum: f64 = phev_calc
1075 .adj_iter_uf_kwh_per_mi
1076 .iter()
1077 .fold(0.0, |acc, x| acc + x);
1078 phev_calc.adj_kwh_per_mi = uf_kwh_sum / max_uf / chg_eff;
1079
1080 phev_calc.adj_ess_kwh_per_mi = uf_kwh_sum / max_uf;
1081
1082 match *key {
1083 "udds" => label_fe_phev.udds = phev_calc.clone(),
1084 "hwy" => label_fe_phev.hwy = phev_calc.clone(),
1085 &_ => bail!("No field for cycle {}", key),
1086 };
1087 }
1088
1089 Ok(label_fe_phev)
1090}
1091
1092#[cfg(test)]
1093mod tests {
1094 use super::*;
1095 use crate::vehicle::vehicle_model::f3veh_with_f2_eff;
1096
1097 #[test]
1099 #[cfg(all(feature = "resources", feature = "yaml"))]
1100 fn test_label_fe_conv_vs_fastsim2() {
1101 let file_contents = include_str!("vehicle/fastsim-2_2012_Ford_Fusion.yaml");
1102 use fastsim_2::traits::SerdeAPI;
1103 let f2veh = fastsim_2::vehicle::RustVehicle::from_yaml(file_contents, false).unwrap();
1104 let mut veh = Vehicle::try_from(f2veh.clone()).unwrap();
1105 f3veh_with_f2_eff(&f2veh, &mut veh);
1106
1107 let (label_fe_f3, _) = get_label_fe(&mut veh, None, false, None, None, false)
1109 .with_context(|| format_dbg!())
1110 .unwrap();
1111
1112 let (label_fe_f2, _) = fastsim_2::simdrivelabel::get_label_fe(&f2veh.clone(), None, None)
1114 .with_context(|| format_dbg!())
1115 .unwrap();
1116
1117 let tolerance = 0.01; assert!(
1122 (label_fe_f3.lab_udds_mpgge - label_fe_f2.lab_udds_mpgge).abs()
1123 / label_fe_f2.lab_udds_mpgge
1124 < tolerance,
1125 "UDDS MPGe mismatch: F3={:.3}, F2={:.3}",
1126 label_fe_f3.lab_udds_mpgge,
1127 label_fe_f2.lab_udds_mpgge
1128 );
1129
1130 assert!(
1131 (label_fe_f3.lab_hwy_mpgge - label_fe_f2.lab_hwy_mpgge).abs()
1132 / label_fe_f2.lab_hwy_mpgge
1133 < tolerance,
1134 "Highway MPGe mismatch: F3={:.3}, F2={:.3}",
1135 label_fe_f3.lab_hwy_mpgge,
1136 label_fe_f2.lab_hwy_mpgge
1137 );
1138
1139 assert!(
1140 (label_fe_f3.lab_comb_mpgge - label_fe_f2.lab_comb_mpgge).abs()
1141 / label_fe_f2.lab_comb_mpgge
1142 < tolerance,
1143 "Combined MPGe mismatch: F3={:.3}, F2={:.3}",
1144 label_fe_f3.lab_comb_mpgge,
1145 label_fe_f2.lab_comb_mpgge
1146 );
1147
1148 assert!(
1150 (label_fe_f3.adj_udds_mpgge - label_fe_f2.adj_udds_mpgge).abs()
1151 / label_fe_f2.adj_udds_mpgge
1152 < tolerance,
1153 "Adjusted UDDS MPGe mismatch: F3={:.3}, F2={:.3}",
1154 label_fe_f3.adj_udds_mpgge,
1155 label_fe_f2.adj_udds_mpgge
1156 );
1157
1158 assert!(
1159 (label_fe_f3.adj_comb_mpgge - label_fe_f2.adj_comb_mpgge).abs()
1160 / label_fe_f2.adj_comb_mpgge
1161 < tolerance,
1162 "Adjusted combined MPGe mismatch: F3={:.3}, F2={:.3}",
1163 label_fe_f3.adj_comb_mpgge,
1164 label_fe_f2.adj_comb_mpgge
1165 );
1166
1167 println!("Conventional vehicle label FE test passed!");
1168 println!(
1169 "F3 Combined MPGe: {:.3}, F2: {:.3}",
1170 label_fe_f3.lab_comb_mpgge, label_fe_f2.lab_comb_mpgge
1171 );
1172 }
1173
1174 }