use std::collections::HashMap;
use crate::drive_cycle::{Cycle, CYC_ACCEL};
use crate::imports::*;
use crate::simdrive::SimDrive;
use crate::vehicle::{PowertrainType, Vehicle};
fn first_grtr(arr: &[f64], cut: f64) -> Option<usize> {
let len = arr.len();
if len == 0 {
return None;
}
Some(arr.iter().position(|&x| x > cut).unwrap_or(len - 1)) }
pub fn get_0_to_60_time_from_accel_data(accel_data: &AccelData) -> Option<f64> {
if accel_data.speed_mph.iter().any(|&x| x >= 60.0) {
let interp = {
let wrapped_interp = Interp1D::new(
Array::from_vec(accel_data.speed_mph.clone()),
Array::from_vec(accel_data.time_s.clone()),
strategy::Linear,
Extrapolate::Clamp,
);
if let Ok(interp) = wrapped_interp {
interp
} else {
return None;
}
};
let accel_time = {
let result = interp.interpolate(&[60.0]);
if let Ok(accel_time_s) = result {
accel_time_s
} else {
return None;
}
};
Some(accel_time)
} else {
None
}
}
pub fn run_accel(veh: &Vehicle) -> anyhow::Result<AccelData> {
let mut sd_accel = SimDrive::new(veh.clone(), CYC_ACCEL.clone(), None);
sd_accel.sim_params.trace_miss_opts = TraceMissOptions::Allow;
sd_accel.walk_once().with_context(|| format_dbg!())?;
let mut speed_mph: Vec<f64> = vec![];
for s in sd_accel.veh.history.speed_ach.clone() {
speed_mph.push(s.get_fresh(|| format_dbg!())?.get::<si::mile_per_hour>())
}
let time_s: Vec<f64> = sd_accel
.cyc
.time
.iter()
.map(|t| t.get::<si::second>())
.collect();
Ok(AccelData { time_s, speed_mph })
}
pub fn get_0_to_60_time(sd_accel: &mut SimDrive) -> anyhow::Result<f64> {
sd_accel.sim_params.trace_miss_opts = TraceMissOptions::Allow;
sd_accel.walk_once().with_context(|| format_dbg!())?;
let mut speed_mph: Vec<f64> = vec![];
for s in sd_accel.veh.history.speed_ach.clone() {
speed_mph.push(s.get_fresh(|| format_dbg!())?.get::<si::mile_per_hour>())
}
let time_s: Vec<f64> = sd_accel
.cyc
.time
.iter()
.map(|t| t.get::<si::second>())
.collect();
let accel_data = AccelData { time_s, speed_mph };
let result = get_0_to_60_time_from_accel_data(&accel_data);
match result {
Some(accel_time_s) => Ok(accel_time_s),
None => {
println!(
"Warning: Vehicle '{}' doesn't reach 60 mph in the acceleration test",
sd_accel.veh.name
);
Ok(f64::NAN)
}
}
}
const DEFAULT_CHG_EFF: f64 = 0.86;
#[serde_api]
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
#[cfg_attr(feature = "pyo3", pyclass(module = "fastsim", subclass, eq))]
pub struct FuelProperties {
pub energy_density: si::Pressure,
pub density: si::MassDensity,
}
impl Init for FuelProperties {}
impl SerdeAPI for FuelProperties {}
#[pyo3_api]
impl FuelProperties {}
impl Default for FuelProperties {
fn default() -> Self {
Self {
energy_density: 33.7 * uc::KWH / uc::GALLON,
density: 0.75 * uc::KG / uc::L,
}
}
}
const J_PER_KWH: f64 = 3_600_000.0;
lazy_static! {
static ref CUBIC_METER_PER_GAL: f64 = 3.79e-3;
}
impl FuelProperties {
fn kwh_per_gge(&self) -> f64 {
self.energy_density.get::<si::joule_per_cubic_meter>() / J_PER_KWH * *CUBIC_METER_PER_GAL
}
}
trait VehicleEfficiency {
fn mpg(&self, energy_density: si::Pressure) -> anyhow::Result<f64>;
fn kwh_per_mi(&self) -> anyhow::Result<f64>;
}
impl VehicleEfficiency for Vehicle {
fn mpg(&self, energy_density: si::Pressure) -> anyhow::Result<f64> {
if let Some(fc) = self.fc() {
Ok(self
.state
.dist
.get_fresh(|| format_dbg!())?
.get::<si::mile>()
/ (*fc.state.energy_fuel.get_fresh(|| format_dbg!())? / energy_density)
.get::<si::gallon>())
} else {
Ok(f64::NAN)
}
}
fn kwh_per_mi(&self) -> anyhow::Result<f64> {
if let Some(res) = self.res() {
Ok(res
.state
.energy_out_chemical
.get_fresh(|| format_dbg!())?
.get::<si::kilowatt_hour>()
/ self
.state
.dist
.get_fresh(|| format_dbg!())?
.get::<si::mile>())
} else {
Ok(f64::NAN)
}
}
}
#[serde_api]
#[derive(Clone, Default, Debug, Deserialize, Serialize, PartialEq)]
#[cfg_attr(feature = "pyo3", pyclass(module = "fastsim", subclass, eq))]
pub struct LabelFe {
pub veh: Option<Vehicle>,
pub adj_params: AdjCoef,
pub lab_udds_mpgge: f64,
pub lab_hwy_mpgge: f64,
pub lab_comb_mpgge: f64,
pub lab_udds_kwh_per_mi: f64,
pub lab_hwy_kwh_per_mi: f64,
pub lab_comb_kwh_per_mi: f64,
pub adj_udds_mpgge: f64,
pub adj_hwy_mpgge: f64,
pub adj_comb_mpgge: f64,
pub adj_udds_kwh_per_mi: f64,
pub adj_hwy_kwh_per_mi: f64,
pub adj_comb_kwh_per_mi: f64,
pub adj_udds_ess_kwh_per_mi: f64,
pub adj_hwy_ess_kwh_per_mi: f64,
pub adj_comb_ess_kwh_per_mi: f64,
pub net_range_miles: f64,
pub uf: f64,
pub net_accel: f64,
pub res_found: String,
pub phev_calcs: Option<LabelFePHEV>,
pub adj_cs_comb_mpgge: Option<f64>,
pub adj_cd_comb_mpgge: Option<f64>,
pub net_phev_cd_miles: Option<f64>,
}
#[pyo3_api]
impl LabelFe {}
impl Init for LabelFe {}
impl SerdeAPI for LabelFe {}
#[serde_api]
#[derive(Default, Clone, Debug, Deserialize, Serialize, PartialEq)]
#[cfg_attr(feature = "pyo3", pyclass(module = "fastsim", subclass, eq))]
pub struct LabelFePHEV {
pub regen_soc_buffer: si::Ratio,
pub udds: PHEVCycleCalc,
pub hwy: PHEVCycleCalc,
}
#[pyo3_api]
impl LabelFePHEV {}
impl Init for LabelFePHEV {}
impl SerdeAPI for LabelFePHEV {}
#[serde_api]
#[derive(Default, Clone, Debug, Deserialize, Serialize, PartialEq)]
#[cfg_attr(feature = "pyo3", pyclass(module = "fastsim", subclass, eq))]
pub struct PHEVCycleCalc {
pub cd_ess_kwh: f64,
pub cd_ess_kwh_per_mi: f64,
pub cd_fs_gal: f64,
pub cd_fs_kwh: f64,
pub cd_mpg: f64,
pub cd_cycs: f64,
pub cd_miles: f64,
pub cd_lab_mpg: f64,
pub cd_adj_mpg: f64,
pub cd_frac_in_trans: f64,
pub trans_init_soc: si::Ratio,
pub trans_ess_kwh: f64,
pub trans_ess_kwh_per_mi: f64,
pub trans_fs_gal: f64,
pub trans_fs_kwh: f64,
pub cs_ess_kwh: f64,
pub cs_ess_kwh_per_mi: f64,
pub cs_fs_gal: f64,
pub cs_fs_kwh: f64,
pub cs_mpg: f64,
pub lab_mpgge: f64,
pub lab_kwh_per_mi: f64,
pub lab_uf: f64,
pub lab_uf_gpm: Vec<f64>,
pub lab_iter_uf: Vec<f64>,
pub lab_iter_uf_kwh_per_mi: Vec<f64>,
pub lab_iter_kwh_per_mi: Vec<f64>,
pub adj_iter_mpgge: Vec<f64>,
pub adj_iter_kwh_per_mi: Vec<f64>,
pub adj_iter_cd_miles: Vec<f64>,
pub adj_iter_uf: Vec<f64>,
pub adj_iter_uf_gpm: Vec<f64>,
pub adj_iter_uf_kwh_per_mi: Vec<f64>,
pub adj_cd_miles: f64,
pub adj_cd_mpgge: f64,
pub adj_cs_mpgge: f64,
pub adj_uf: f64,
pub adj_mpgge: f64,
pub adj_kwh_per_mi: f64,
pub adj_ess_kwh_per_mi: f64,
pub delta_soc: si::Ratio,
pub total_cd_miles: f64,
}
impl Init for PHEVCycleCalc {}
impl SerdeAPI for PHEVCycleCalc {}
#[pyo3_api]
impl PHEVCycleCalc {}
#[derive(Clone, Serialize, Deserialize, Debug, PartialEq)]
#[cfg_attr(feature = "pyo3", pyclass(module = "fastsim", subclass, eq))]
pub struct AdjCoef {
pub city_intercept: f64,
pub city_slope: f64,
pub hwy_intercept: f64,
pub hwy_slope: f64,
}
#[pyo3_api]
impl AdjCoef {}
impl Init for AdjCoef {}
impl SerdeAPI for AdjCoef {}
impl Default for AdjCoef {
fn default() -> Self {
Self {
city_intercept: 0.003259,
city_slope: 1.1805,
hwy_intercept: 0.001376,
hwy_slope: 1.3466,
}
}
}
#[serde_api]
#[derive(Clone, Serialize, Deserialize, Debug, PartialEq)]
#[cfg_attr(feature = "pyo3", pyclass(module = "fastsim", subclass, eq))]
pub struct PhevUtilizationParams {
pub adj_coef_map: HashMap<String, AdjCoef>,
pub rechg_freq_miles: Vec<f64>,
pub uf_array: Vec<f64>,
}
impl Init for PhevUtilizationParams {}
impl SerdeAPI for PhevUtilizationParams {}
impl Default for PhevUtilizationParams {
fn default() -> Self {
Self::from_json(&*PHEV_UTIL_PARAMS, false).unwrap()
}
}
lazy_static! {
static ref PHEV_UTIL_PARAMS: String =
include_str!("./simdrivelabel/longparams.json").to_string();
}
pub struct PhevVehicleInfo {
pub max_soc: si::Ratio,
pub min_soc: si::Ratio,
pub phev_max_regen: si::Ratio,
pub veh_mass: si::Mass,
pub em_peak_eff: si::Ratio,
pub energy_capacity: si::Energy,
pub chg_eff: f64,
pub fuel_storage_capacity: si::Energy,
}
pub struct PhevSimulationDataForLabel {
pub cd_fuel_consumed_kwh: f64,
pub cd_soc_start: f64,
pub cd_soc_end: f64,
pub cyc_dist_mi: f64,
pub cd_kwh_per_mi: f64,
pub cd_mpg: f64,
pub cs_fuel_consumed_kwh: f64,
pub cs_ess_energy_kwh: f64,
pub cs_kwh_per_mi: f64,
pub cs_mpg: f64,
pub cs_min_soc: f64,
pub cs_fs_energy_capacity_kwh: f64,
}
pub enum SimulationDataForLabel {
ConvOrHev {
veh_year: u32,
udds_mpgge: f64,
hwy_mpgge: f64,
},
Bev {
veh_year: u32,
udds_kwh_per_mi: f64,
hwy_kwh_per_mi: f64,
bev_energy_capacity_kwh: f64,
},
Phev {
veh_year: u32,
info: PhevVehicleInfo,
udds: PhevSimulationDataForLabel,
hwy: PhevSimulationDataForLabel,
},
}
pub struct AccelData {
pub time_s: Vec<f64>,
pub speed_mph: Vec<f64>,
}
pub fn calculate_transient_soc_helper(
max_soc: f64,
min_soc: f64,
energy_capacity_kwh: f64,
cyc_kwh_per_mi: f64,
soc_start: f64,
soc_end: f64,
dist_mi: f64,
) -> f64 {
let total_cd_miles = ((max_soc - min_soc) * energy_capacity_kwh) / cyc_kwh_per_mi;
let cd_cycs = total_cd_miles / dist_mi;
let delta_soc = soc_start - soc_end;
max_soc - cd_cycs.floor() * delta_soc
}
pub fn calculate_phev_label_helper(
info: &PhevVehicleInfo,
data: &PhevSimulationDataForLabel,
fuel_props: &FuelProperties,
max_epa_adj: f64,
phev_utilization_params: &PhevUtilizationParams,
adj_params: &AdjCoef,
label_fe_phev: &LabelFePHEV,
is_city: bool,
) -> anyhow::Result<PHEVCycleCalc> {
let mut phev_calc = PHEVCycleCalc::default();
phev_calc.cd_ess_kwh =
((info.max_soc - info.min_soc) * info.energy_capacity).get::<si::kilowatt_hour>();
let soc_start = data.cd_soc_start;
let soc_end = data.cd_soc_end;
let dist_mi = data.cyc_dist_mi;
phev_calc.delta_soc = (soc_start - soc_end) * uc::R;
phev_calc.total_cd_miles = ((info.max_soc - info.min_soc) * info.energy_capacity)
.get::<si::kilowatt_hour>()
/ data.cd_kwh_per_mi;
phev_calc.cd_cycs = phev_calc.total_cd_miles / dist_mi;
phev_calc.cd_frac_in_trans = phev_calc.cd_cycs % phev_calc.cd_cycs.floor();
let fuel_energy_kwh = data.cd_fuel_consumed_kwh;
phev_calc.cd_fs_gal = fuel_energy_kwh / fuel_props.kwh_per_gge();
phev_calc.cd_fs_kwh = fuel_energy_kwh;
phev_calc.cd_ess_kwh_per_mi = data.cd_kwh_per_mi;
phev_calc.cd_mpg = data.cd_mpg;
let interp_x_vals: Vec<f64> = (0..((phev_calc.cd_cycs.ceil() + 1.0) as usize))
.map(|i| i as f64 * dist_mi)
.collect();
phev_calc.lab_iter_uf = vec![];
for x in interp_x_vals {
phev_calc.lab_iter_uf.push(
phev_utilization_params.uf_array[first_grtr(
&phev_utilization_params.rechg_freq_miles,
x,
)
.with_context(|| format_dbg!())?
- 1],
);
}
phev_calc.trans_init_soc = info.max_soc - phev_calc.cd_cycs.floor() * phev_calc.delta_soc;
phev_calc.trans_ess_kwh = phev_calc.cd_ess_kwh_per_mi * dist_mi * phev_calc.cd_frac_in_trans;
phev_calc.trans_ess_kwh_per_mi = phev_calc.cd_ess_kwh_per_mi * phev_calc.cd_frac_in_trans;
let cs_fuel_energy_kwh = data.cs_fuel_consumed_kwh;
phev_calc.cs_fs_gal = cs_fuel_energy_kwh / fuel_props.kwh_per_gge();
phev_calc.trans_fs_gal = phev_calc.cs_fs_gal * (1.0 - phev_calc.cd_frac_in_trans);
phev_calc.cs_fs_kwh = cs_fuel_energy_kwh;
phev_calc.trans_fs_kwh = phev_calc.cs_fs_kwh * (1.0 - phev_calc.cd_frac_in_trans);
let cs_ess_energy_kwh = data.cs_ess_energy_kwh;
phev_calc.cs_ess_kwh = cs_ess_energy_kwh;
phev_calc.cs_ess_kwh_per_mi = data.cs_kwh_per_mi;
let lab_iter_uf_diff = phev_calc.lab_iter_uf.diff();
phev_calc.lab_uf_gpm = [
phev_calc.trans_fs_gal * lab_iter_uf_diff.last().with_context(|| format_dbg!())?,
phev_calc.cs_fs_gal
* (1.0
- phev_calc
.lab_iter_uf
.last()
.with_context(|| format_dbg!())?),
]
.iter()
.map(|x| *x / dist_mi)
.collect();
let min_soc_in_cycle = phev_calc.delta_soc.abs(); phev_calc.cd_miles =
if (info.max_soc - label_fe_phev.regen_soc_buffer - min_soc_in_cycle) < 0.01 * uc::R {
1000.0
} else {
phev_calc.cd_cycs.ceil() * dist_mi
};
phev_calc.cd_lab_mpg = phev_calc
.lab_iter_uf
.last()
.with_context(|| format_dbg!())?
/ (phev_calc.trans_fs_gal / dist_mi);
phev_calc.cs_mpg = dist_mi / phev_calc.cs_fs_gal;
phev_calc.lab_uf = phev_utilization_params.uf_array[first_grtr(
&phev_utilization_params.rechg_freq_miles,
phev_calc.cd_miles,
)
.with_context(|| format_dbg!())?
- 1];
phev_calc.cd_adj_mpg =
phev_calc.lab_iter_uf.max()? / phev_calc.lab_uf_gpm[phev_calc.lab_uf_gpm.len() - 2];
phev_calc.lab_mpgge = 1.0
/ (phev_calc.lab_uf / phev_calc.cd_adj_mpg + (1.0 - phev_calc.lab_uf) / phev_calc.cs_mpg);
let mut lab_iter_kwh_per_mi_vals = Vec::new();
lab_iter_kwh_per_mi_vals.push(0.0);
lab_iter_kwh_per_mi_vals
.extend(vec![phev_calc.cd_ess_kwh_per_mi; phev_calc.cd_cycs.floor() as usize].iter());
lab_iter_kwh_per_mi_vals.push(phev_calc.trans_ess_kwh_per_mi);
lab_iter_kwh_per_mi_vals.push(0.0);
phev_calc.lab_iter_kwh_per_mi = lab_iter_kwh_per_mi_vals;
let uf_diff = phev_calc.lab_iter_uf.diff();
let mut vals = Vec::new();
vals.push(0.0);
for i in 1..phev_calc.lab_iter_kwh_per_mi.len() - 1 {
if i < uf_diff.len() {
vals.push(phev_calc.lab_iter_kwh_per_mi[i] * uf_diff[i]);
}
}
vals.push(0.0);
phev_calc.lab_iter_uf_kwh_per_mi = vals;
phev_calc.lab_kwh_per_mi = phev_calc
.lab_iter_uf_kwh_per_mi
.iter()
.fold(0.0, |acc, x| acc + x)
/ phev_calc
.lab_iter_uf
.iter()
.fold(0.0f64, |acc, x| acc.max(*x));
let mut adj_iter_mpgge_vals = vec![0.0; phev_calc.cd_cycs.floor() as usize];
let mut adj_iter_kwh_per_mi_vals = vec![0.0; phev_calc.lab_iter_kwh_per_mi.len()];
if is_city {
adj_iter_mpgge_vals.push(f64::max(
1.0 / (adj_params.city_intercept
+ (adj_params.city_slope
/ (data.cyc_dist_mi / (phev_calc.trans_fs_kwh / fuel_props.kwh_per_gge())))),
data.cyc_dist_mi / (phev_calc.trans_fs_kwh / fuel_props.kwh_per_gge())
* (1.0 - max_epa_adj),
));
adj_iter_mpgge_vals.push(f64::max(
1.0 / (adj_params.city_intercept
+ (adj_params.city_slope
/ (data.cyc_dist_mi / (phev_calc.cs_fs_kwh / fuel_props.kwh_per_gge())))),
data.cyc_dist_mi / (phev_calc.cs_fs_kwh / fuel_props.kwh_per_gge())
* (1.0 - max_epa_adj),
));
for (c, _) in phev_calc.lab_iter_kwh_per_mi.iter().enumerate() {
if phev_calc.lab_iter_kwh_per_mi[c] == 0.0 {
adj_iter_kwh_per_mi_vals[c] = 0.0;
} else {
adj_iter_kwh_per_mi_vals[c] =
(1.0 / f64::max(
1.0 / (adj_params.city_intercept
+ (adj_params.city_slope
/ ((1.0 / phev_calc.lab_iter_kwh_per_mi[c])
* fuel_props.kwh_per_gge()))),
(1.0 - max_epa_adj)
* ((1.0 / phev_calc.lab_iter_kwh_per_mi[c]) * fuel_props.kwh_per_gge()),
)) * fuel_props.kwh_per_gge();
}
}
} else {
adj_iter_mpgge_vals.push(f64::max(
1.0 / (adj_params.hwy_intercept
+ (adj_params.hwy_slope
/ (data.cyc_dist_mi / (phev_calc.trans_fs_kwh / fuel_props.kwh_per_gge())))),
data.cyc_dist_mi / (phev_calc.trans_fs_kwh / fuel_props.kwh_per_gge())
* (1.0 - max_epa_adj),
));
adj_iter_mpgge_vals.push(f64::max(
1.0 / (adj_params.hwy_intercept
+ (adj_params.hwy_slope
/ (data.cyc_dist_mi / (phev_calc.cs_fs_kwh / fuel_props.kwh_per_gge())))),
data.cyc_dist_mi / (phev_calc.cs_fs_kwh / fuel_props.kwh_per_gge())
* (1.0 - max_epa_adj),
));
for (c, _) in phev_calc.lab_iter_kwh_per_mi.iter().enumerate() {
if phev_calc.lab_iter_kwh_per_mi[c] == 0.0 {
adj_iter_kwh_per_mi_vals[c] = 0.0;
} else {
adj_iter_kwh_per_mi_vals[c] =
(1.0 / f64::max(
1.0 / (adj_params.hwy_intercept
+ (adj_params.hwy_slope
/ ((1.0 / phev_calc.lab_iter_kwh_per_mi[c])
* fuel_props.kwh_per_gge()))),
(1.0 - max_epa_adj)
* ((1.0 / phev_calc.lab_iter_kwh_per_mi[c]) * fuel_props.kwh_per_gge()),
)) * fuel_props.kwh_per_gge();
}
}
}
phev_calc.adj_iter_mpgge = adj_iter_mpgge_vals;
phev_calc.adj_iter_kwh_per_mi = adj_iter_kwh_per_mi_vals;
phev_calc.adj_iter_cd_miles = vec![0.0; phev_calc.cd_cycs.ceil() as usize + 2];
for c in 0..phev_calc.adj_iter_cd_miles.len() {
if c == 0 {
phev_calc.adj_iter_cd_miles[c] = 0.0;
} else if c <= phev_calc.cd_cycs.floor() as usize {
phev_calc.adj_iter_cd_miles[c] = phev_calc.adj_iter_cd_miles[c - 1]
+ phev_calc.cd_ess_kwh_per_mi * data.cyc_dist_mi / phev_calc.adj_iter_kwh_per_mi[c];
} else if c == phev_calc.cd_cycs.floor() as usize + 1 {
phev_calc.adj_iter_cd_miles[c] = phev_calc.adj_iter_cd_miles[c - 1]
+ phev_calc.trans_ess_kwh_per_mi * data.cyc_dist_mi
/ phev_calc.adj_iter_kwh_per_mi[c];
} else {
phev_calc.adj_iter_cd_miles[c] = 0.0;
}
}
phev_calc.adj_cd_miles =
if info.max_soc - label_fe_phev.regen_soc_buffer - (data.cs_min_soc * uc::R) < 0.01 * uc::R
{
1000.0
} else {
*phev_calc.adj_iter_cd_miles.max()?
};
phev_calc.adj_iter_uf = vec![];
for x in phev_calc.adj_iter_cd_miles.clone() {
phev_calc.adj_iter_uf.push(
phev_utilization_params.uf_array[first_grtr(
&phev_utilization_params.rechg_freq_miles,
x,
)
.with_context(|| format_dbg!())?
- 1],
)
}
let adj_iter_uf_diff = phev_calc.adj_iter_uf.diff();
phev_calc.adj_iter_uf_gpm = vec![0.0; phev_calc.cd_cycs.floor() as usize];
phev_calc.adj_iter_uf_gpm.push(
(1.0 / phev_calc.adj_iter_mpgge[phev_calc.adj_iter_mpgge.len() - 2])
* adj_iter_uf_diff[adj_iter_uf_diff.len() - 2],
);
phev_calc.adj_iter_uf_gpm.push(
(1.0 / phev_calc
.adj_iter_mpgge
.last()
.with_context(|| format_dbg!())?)
* (1.0 - phev_calc.adj_iter_uf[phev_calc.adj_iter_uf.len() - 2]),
);
let adj_uf_diff = phev_calc.adj_iter_uf.diff();
phev_calc.adj_iter_uf_kwh_per_mi = phev_calc
.adj_iter_kwh_per_mi
.iter()
.zip(adj_uf_diff.iter())
.map(|(kwh, uf)| kwh * uf)
.collect();
let max_uf: f64 = phev_calc
.adj_iter_uf
.iter()
.fold(0.0f64, |acc, x| acc.max(*x));
phev_calc.adj_cd_mpgge =
1.0 / phev_calc.adj_iter_uf_gpm[phev_calc.adj_iter_uf_gpm.len() - 2] * max_uf;
phev_calc.adj_cs_mpgge = 1.0
/ phev_calc
.adj_iter_uf_gpm
.last()
.with_context(|| format_dbg!())?
* (1.0 - max_uf);
phev_calc.adj_uf = phev_utilization_params.uf_array[first_grtr(
&phev_utilization_params.rechg_freq_miles,
phev_calc.adj_cd_miles,
)
.with_context(|| format_dbg!())?
- 1];
phev_calc.adj_mpgge = 1.0
/ (phev_calc.adj_uf / phev_calc.adj_cd_mpgge
+ (1.0 - phev_calc.adj_uf) / phev_calc.adj_cs_mpgge);
let uf_kwh_sum: f64 = phev_calc
.adj_iter_uf_kwh_per_mi
.iter()
.fold(0.0, |acc, x| acc + x);
phev_calc.adj_kwh_per_mi = uf_kwh_sum / max_uf / info.chg_eff;
phev_calc.adj_ess_kwh_per_mi = uf_kwh_sum / max_uf;
Ok(phev_calc)
}
pub fn calculate_label_fuel_economy(
fuel_props: &FuelProperties,
phev_utilization_params: &PhevUtilizationParams,
max_epa_adj: f64,
sim_data: &SimulationDataForLabel,
accel_data: &AccelData,
) -> anyhow::Result<LabelFe> {
let mut label_fe = LabelFe::default();
let veh_year = match sim_data {
SimulationDataForLabel::ConvOrHev { veh_year, .. }
| SimulationDataForLabel::Phev { veh_year, .. }
| SimulationDataForLabel::Bev { veh_year, .. } => *veh_year,
};
let is_phev = match sim_data {
SimulationDataForLabel::ConvOrHev { .. } => false,
SimulationDataForLabel::Phev { .. } => true,
SimulationDataForLabel::Bev { .. } => false,
};
let adj_params = if veh_year < 2017 {
&phev_utilization_params.adj_coef_map["2008"]
} else {
&phev_utilization_params.adj_coef_map["2017"]
};
label_fe.adj_params = adj_params.clone();
match sim_data {
SimulationDataForLabel::ConvOrHev {
udds_mpgge,
hwy_mpgge,
..
} => {
label_fe.lab_udds_mpgge = *udds_mpgge;
label_fe.lab_hwy_mpgge = *hwy_mpgge;
label_fe.lab_comb_mpgge = 1.0 / (0.55 / *udds_mpgge + 0.45 / *hwy_mpgge);
label_fe.lab_udds_kwh_per_mi = 0.0;
label_fe.lab_hwy_kwh_per_mi = 0.0;
label_fe.lab_comb_kwh_per_mi = 0.0;
label_fe.adj_udds_mpgge =
1. / (adj_params.city_intercept + adj_params.city_slope / udds_mpgge);
label_fe.adj_hwy_mpgge =
1. / (adj_params.hwy_intercept + adj_params.hwy_slope / hwy_mpgge);
label_fe.adj_comb_mpgge =
1. / (0.55 / label_fe.adj_udds_mpgge + 0.45 / label_fe.adj_hwy_mpgge);
}
SimulationDataForLabel::Phev {
info, udds, hwy, ..
} => {
let mut phev_calcs = LabelFePHEV {
regen_soc_buffer: ((0.5 * info.veh_mass * ((60. * uc::MPH).powi(P2::new())))
* info.phev_max_regen
* info.em_peak_eff
/ info.energy_capacity)
.min((info.max_soc - info.min_soc) / 2.0),
..Default::default()
};
phev_calcs.udds = calculate_phev_label_helper(
info,
udds,
&fuel_props,
max_epa_adj,
&phev_utilization_params,
&adj_params,
&phev_calcs,
true,
)?;
phev_calcs.hwy = calculate_phev_label_helper(
info,
hwy,
&fuel_props,
max_epa_adj,
&phev_utilization_params,
&adj_params,
&phev_calcs,
false,
)?;
label_fe.lab_udds_mpgge = phev_calcs.udds.lab_mpgge;
label_fe.lab_hwy_mpgge = phev_calcs.hwy.lab_mpgge;
label_fe.lab_comb_mpgge =
1.0 / (0.55 / phev_calcs.udds.lab_mpgge + 0.45 / phev_calcs.hwy.lab_mpgge);
label_fe.lab_udds_kwh_per_mi = phev_calcs.udds.lab_kwh_per_mi;
label_fe.lab_hwy_kwh_per_mi = phev_calcs.hwy.lab_kwh_per_mi;
label_fe.lab_comb_kwh_per_mi =
0.55 * phev_calcs.udds.lab_kwh_per_mi + 0.45 * phev_calcs.hwy.lab_kwh_per_mi;
label_fe.adj_udds_mpgge = phev_calcs.udds.adj_mpgge;
label_fe.adj_hwy_mpgge = phev_calcs.hwy.adj_mpgge;
label_fe.adj_comb_mpgge =
1.0 / (0.55 / phev_calcs.udds.adj_mpgge + 0.45 / phev_calcs.hwy.adj_mpgge);
label_fe.adj_cs_comb_mpgge = Some(
1.0 / (0.55 / phev_calcs.udds.adj_cs_mpgge + 0.45 / phev_calcs.hwy.adj_cs_mpgge),
);
label_fe.adj_cd_comb_mpgge = Some(
1.0 / (0.55 / phev_calcs.udds.adj_cd_mpgge + 0.45 / phev_calcs.hwy.adj_cd_mpgge),
);
label_fe.adj_udds_kwh_per_mi = phev_calcs.udds.adj_kwh_per_mi;
label_fe.adj_hwy_kwh_per_mi = phev_calcs.hwy.adj_kwh_per_mi;
label_fe.adj_comb_kwh_per_mi =
0.55 * phev_calcs.udds.adj_kwh_per_mi + 0.45 * phev_calcs.hwy.adj_kwh_per_mi;
label_fe.adj_udds_ess_kwh_per_mi = phev_calcs.udds.adj_ess_kwh_per_mi;
label_fe.adj_hwy_ess_kwh_per_mi = phev_calcs.hwy.adj_ess_kwh_per_mi;
label_fe.adj_comb_ess_kwh_per_mi = 0.55 * phev_calcs.udds.adj_ess_kwh_per_mi
+ 0.45 * phev_calcs.hwy.adj_ess_kwh_per_mi;
label_fe.uf = phev_utilization_params.uf_array[first_grtr(
&phev_utilization_params.rechg_freq_miles,
0.55 * phev_calcs.udds.adj_cd_miles + 0.45 * phev_calcs.hwy.adj_cd_miles,
)
.with_context(|| format_dbg!())?
- 1];
label_fe.net_phev_cd_miles =
Some(0.55 * phev_calcs.udds.adj_cd_miles + 0.45 * phev_calcs.hwy.adj_cd_miles);
let fuel_energy_kwh = info.fuel_storage_capacity.get::<si::kilowatt_hour>();
let fuel_energy_gge = fuel_energy_kwh / fuel_props.kwh_per_gge();
label_fe.net_range_miles = (fuel_energy_gge
- label_fe.net_phev_cd_miles.with_context(|| format_dbg!())?
/ label_fe.adj_cd_comb_mpgge.with_context(|| format_dbg!())?)
* label_fe.adj_cs_comb_mpgge.with_context(|| format_dbg!())?
+ label_fe.net_phev_cd_miles.with_context(|| format_dbg!())?;
label_fe.phev_calcs = Some(phev_calcs);
}
SimulationDataForLabel::Bev {
udds_kwh_per_mi,
hwy_kwh_per_mi,
bev_energy_capacity_kwh,
..
} => {
label_fe.lab_udds_mpgge = 0.0;
label_fe.lab_hwy_mpgge = 0.0;
label_fe.lab_comb_mpgge = 0.0;
label_fe.lab_udds_kwh_per_mi = *udds_kwh_per_mi;
label_fe.lab_hwy_kwh_per_mi = *hwy_kwh_per_mi;
label_fe.lab_comb_kwh_per_mi = 0.55 * *udds_kwh_per_mi + 0.45 * *hwy_kwh_per_mi;
label_fe.adj_udds_mpgge = 0.;
label_fe.adj_hwy_mpgge = 0.;
label_fe.adj_comb_mpgge = 0.;
label_fe.adj_udds_kwh_per_mi =
(1. / f64::max(
1. / (adj_params.city_intercept
+ (adj_params.city_slope
/ ((1. / label_fe.lab_udds_kwh_per_mi) * fuel_props.kwh_per_gge()))),
(1. / label_fe.lab_udds_kwh_per_mi)
* fuel_props.kwh_per_gge()
* (1. - max_epa_adj),
)) * fuel_props.kwh_per_gge()
/ DEFAULT_CHG_EFF;
label_fe.adj_hwy_kwh_per_mi =
(1. / f64::max(
1. / (adj_params.hwy_intercept
+ (adj_params.hwy_slope
/ ((1. / label_fe.lab_hwy_kwh_per_mi) * fuel_props.kwh_per_gge()))),
(1. / label_fe.lab_hwy_kwh_per_mi)
* fuel_props.kwh_per_gge()
* (1. - max_epa_adj),
)) * fuel_props.kwh_per_gge()
/ DEFAULT_CHG_EFF;
label_fe.adj_comb_kwh_per_mi =
0.55 * label_fe.adj_udds_kwh_per_mi + 0.45 * label_fe.adj_hwy_kwh_per_mi;
label_fe.adj_udds_ess_kwh_per_mi = label_fe.adj_udds_kwh_per_mi * DEFAULT_CHG_EFF;
label_fe.adj_hwy_ess_kwh_per_mi = label_fe.adj_hwy_kwh_per_mi * DEFAULT_CHG_EFF;
label_fe.adj_comb_ess_kwh_per_mi = label_fe.adj_comb_kwh_per_mi * DEFAULT_CHG_EFF;
label_fe.net_range_miles = bev_energy_capacity_kwh / label_fe.adj_comb_ess_kwh_per_mi;
}
}
if !is_phev {
label_fe.uf = 0.0;
}
label_fe.net_accel = match get_0_to_60_time_from_accel_data(accel_data) {
Some(accel_s) => accel_s,
None => f64::NAN,
};
label_fe.res_found = String::from("model needs to be implemented for this");
Ok(label_fe)
}
fn run_simdrive_with_init_soc(
veh: &Vehicle,
cycle: &str,
init_soc: si::Ratio,
) -> anyhow::Result<SimDrive> {
let mut sd = SimDrive::new(veh.clone(), Cycle::from_resource(cycle, false)?, None);
let res_mut = sd.veh.res_mut().with_context(|| format_dbg!())?;
res_mut.state.soc.mark_stale();
res_mut.state.soc.update(init_soc, || format_dbg!())?;
sd.reset_cumulative(|| format_dbg!())?;
sd.reset_step(|| format_dbg!())?;
sd.clear();
sd.walk_once().with_context(|| format_dbg!())?;
Ok(sd)
}
pub fn run_label_simulations(
veh: &mut Vehicle,
fuel_props: Option<FuelProperties>,
phev_utilization_params: Option<PhevUtilizationParams>,
) -> anyhow::Result<(SimulationDataForLabel, HashMap<&str, SimDrive>)> {
let phev_utilization_params = &phev_utilization_params.unwrap_or_default();
let fuel_props = fuel_props.unwrap_or_default();
let mut cyc: HashMap<&str, Cycle> = HashMap::new();
let mut sd = HashMap::new();
let mut label_fe = LabelFe::default();
label_fe.veh = Some(veh.clone());
cyc.insert("accel", CYC_ACCEL.clone());
cyc.insert("udds", Cycle::from_resource("udds.csv", false)?);
cyc.insert("hwy", Cycle::from_resource("hwfet.csv", false)?);
if veh.pt_type.is_plug_in_hybrid_electric_vehicle() {
let rm = veh.res_mut().unwrap();
rm.state.soc.check_and_reset(|| format_dbg!()).unwrap();
rm.state.soc.update(rm.max_soc, || format_dbg!()).unwrap();
}
sd.insert(
"udds",
SimDrive::new(veh.clone(), cyc["udds"].clone(), None),
);
sd.insert("hwy", SimDrive::new(veh.clone(), cyc["hwy"].clone(), None));
for (k, val) in sd.iter_mut() {
val.walk().with_context(|| format_dbg!(k))?;
}
let adj_params = if veh.year < 2017 {
&phev_utilization_params.adj_coef_map["2008"]
} else {
&phev_utilization_params.adj_coef_map["2017"]
};
label_fe.adj_params = adj_params.clone();
let is_conv = matches!(veh.pt_type, PowertrainType::ConventionalVehicle(_));
let is_hev = matches!(veh.pt_type, PowertrainType::HybridElectricVehicle(_));
let is_phev = matches!(veh.pt_type, PowertrainType::PlugInHybridElectricVehicle(_));
let is_bev = matches!(veh.pt_type, PowertrainType::BatteryElectricVehicle(_));
if is_hev || is_conv {
Ok((
SimulationDataForLabel::ConvOrHev {
veh_year: veh.year,
udds_mpgge: sd["udds"].veh.mpg(fuel_props.energy_density)?,
hwy_mpgge: sd["hwy"].veh.mpg(fuel_props.energy_density)?,
},
sd,
))
} else if is_bev {
if let PowertrainType::BatteryElectricVehicle(bev) = &veh.pt_type {
let res_energy_capacity_kwh = bev.res.energy_capacity.get::<si::kilowatt_hour>();
Ok((
SimulationDataForLabel::Bev {
veh_year: veh.year,
udds_kwh_per_mi: sd["udds"].veh.kwh_per_mi()?,
hwy_kwh_per_mi: sd["hwy"].veh.kwh_per_mi()?,
bev_energy_capacity_kwh: res_energy_capacity_kwh,
},
sd,
))
} else {
Err(anyhow!("is_bev but powertrain not BEV"))
}
} else if is_phev {
let max_soc: si::Ratio;
let min_soc: si::Ratio;
let phev_max_regen: si::Ratio;
let veh_mass: si::Mass;
let em_peak_eff: si::Ratio;
let energy_capacity: si::Energy;
let chg_eff: f64;
let fuel_storage_capacity: si::Energy;
if let PowertrainType::PlugInHybridElectricVehicle(phev) = &veh.pt_type {
max_soc = phev.res.max_soc;
min_soc = phev.res.min_soc;
phev_max_regen = 0.98 * uc::R;
veh_mass = *veh.state.mass.get_fresh(|| format_dbg!())?;
em_peak_eff = *phev
.em
.eff_interp_achieved
.max()
.with_context(|| format_dbg!())?
* uc::R;
energy_capacity = phev.res.energy_capacity;
chg_eff = DEFAULT_CHG_EFF;
fuel_storage_capacity = phev.fs.energy_capacity;
} else {
bail!("Vehicle is not a PHEV");
}
let init_soc = min_soc + 0.01 * uc::R;
let cs_udds_sd = run_simdrive_with_init_soc(veh, "udds.csv", init_soc)?;
let cs_hwy_sd = run_simdrive_with_init_soc(veh, "hwfet.csv", init_soc)?;
sd.insert("udds-cs", cs_udds_sd.clone());
sd.insert("hwy-cs", cs_hwy_sd.clone());
Ok((
SimulationDataForLabel::Phev {
veh_year: veh.year,
info: PhevVehicleInfo {
max_soc,
min_soc,
phev_max_regen,
veh_mass,
em_peak_eff,
energy_capacity,
chg_eff,
fuel_storage_capacity,
},
udds: PhevSimulationDataForLabel {
cd_fuel_consumed_kwh: {
if let Some(fc) = sd["udds"].veh.fc() {
fc.state
.energy_fuel
.get_fresh(|| format_dbg!())?
.get::<si::kilowatt_hour>()
} else {
0.0
}
},
cd_soc_start: {
if let Some(res) = sd["udds"].veh.res() {
res.history
.soc
.first()
.unwrap()
.get_fresh(|| format_dbg!())?
.get::<si::ratio>()
} else {
1.0
}
},
cd_soc_end: {
if let Some(res) = sd["udds"].veh.res() {
res.history
.soc
.last()
.unwrap()
.get_fresh(|| format_dbg!())?
.get::<si::ratio>()
} else {
0.0
}
},
cyc_dist_mi: {
sd["udds"]
.veh
.state
.dist
.get_fresh(|| format_dbg!())?
.get::<si::mile>()
},
cd_kwh_per_mi: sd["udds"].veh.kwh_per_mi()?,
cd_mpg: sd["udds"].veh.mpg(fuel_props.energy_density)?,
cs_fuel_consumed_kwh: {
if let Some(fc) = cs_udds_sd.veh.fc() {
fc.state
.energy_fuel
.get_fresh(|| format_dbg!())?
.get::<si::kilowatt_hour>()
} else {
0.0
}
},
cs_ess_energy_kwh: {
if let Some(res) = cs_udds_sd.veh.res() {
res.state
.energy_out_chemical
.get_fresh(|| format_dbg!())?
.get::<si::kilowatt_hour>()
} else {
0.0
}
},
cs_kwh_per_mi: cs_udds_sd.veh.kwh_per_mi()?,
cs_mpg: cs_udds_sd.veh.mpg(fuel_props.energy_density)?,
cs_min_soc: min_soc.get::<si::ratio>(),
cs_fs_energy_capacity_kwh: {
if let Some(fs) = veh.pt_type.fs() {
fs.energy_capacity.get::<si::kilowatt_hour>()
} else {
0.0
}
},
},
hwy: PhevSimulationDataForLabel {
cd_fuel_consumed_kwh: {
if let Some(fc) = sd["hwy"].veh.fc() {
fc.state
.energy_fuel
.get_fresh(|| format_dbg!())?
.get::<si::kilowatt_hour>()
} else {
0.0
}
},
cd_soc_start: {
if let Some(res) = sd["hwy"].veh.res() {
res.history
.soc
.first()
.unwrap()
.get_fresh(|| format_dbg!())?
.get::<si::ratio>()
} else {
1.0
}
},
cd_soc_end: {
if let Some(res) = sd["hwy"].veh.res() {
res.history
.soc
.last()
.unwrap()
.get_fresh(|| format_dbg!())?
.get::<si::ratio>()
} else {
0.0
}
},
cyc_dist_mi: {
sd["hwy"]
.veh
.state
.dist
.get_fresh(|| format_dbg!())?
.get::<si::mile>()
},
cd_kwh_per_mi: sd["hwy"].veh.kwh_per_mi()?,
cd_mpg: sd["hwy"].veh.mpg(fuel_props.energy_density)?,
cs_fuel_consumed_kwh: {
if let Some(fc) = cs_hwy_sd.veh.fc() {
fc.state
.energy_fuel
.get_fresh(|| format_dbg!())?
.get::<si::kilowatt_hour>()
} else {
0.0
}
},
cs_ess_energy_kwh: {
if let Some(res) = cs_hwy_sd.veh.res() {
res.state
.energy_out_chemical
.get_fresh(|| format_dbg!())?
.get::<si::kilowatt_hour>()
} else {
0.0
}
},
cs_kwh_per_mi: cs_hwy_sd.veh.kwh_per_mi()?,
cs_mpg: cs_hwy_sd.veh.mpg(fuel_props.energy_density)?,
cs_min_soc: min_soc.get::<si::ratio>(),
cs_fs_energy_capacity_kwh: {
if let Some(fs) = veh.pt_type.fs() {
fs.energy_capacity.get::<si::kilowatt_hour>()
} else {
0.0
}
},
},
},
sd,
))
} else {
Err(anyhow!("Unhandled powertrain type"))
}
}
pub fn get_label_fe(
veh: &mut Vehicle,
max_epa_adj: Option<f64>,
full_detail: bool,
fuel_props: Option<FuelProperties>,
phev_utilization_params: Option<PhevUtilizationParams>,
verbose: bool,
) -> anyhow::Result<(LabelFe, Option<HashMap<&str, SimDrive>>)> {
let max_epa_adj = max_epa_adj.unwrap_or(0.3);
let phev_utilization_params = &phev_utilization_params.unwrap_or_default();
let fuel_props = fuel_props.unwrap_or_default();
let veh_copy = veh.clone();
let (sim_data, sd) = run_label_simulations(
veh,
Some(fuel_props.clone()),
Some(phev_utilization_params.clone()),
)?;
let accel_data = run_accel(&veh_copy)?;
let mut label_fe = calculate_label_fuel_economy(
&fuel_props,
phev_utilization_params,
max_epa_adj,
&sim_data,
&accel_data,
)?;
label_fe.veh = Some(veh_copy);
if full_detail && verbose {
println!("{label_fe:#?}");
Ok((label_fe, Some(sd)))
} else if full_detail {
Ok((label_fe, Some(sd)))
} else if verbose {
println!("{label_fe:#?}");
Ok((label_fe, None))
} else {
Ok((label_fe, None))
}
}
#[cfg(feature = "pyo3")]
#[pyfunction(name = "get_label_fe")]
#[cfg_attr(
feature = "pyo3",
pyo3(signature = (
veh, max_epa_adj=None, full_detail=None, fuel_props=None, phev_utilization_params=None, verbose=None))
)]
pub fn get_label_fe_py(
veh: &mut Vehicle,
max_epa_adj: Option<f64>,
full_detail: Option<bool>,
fuel_props: Option<FuelProperties>,
phev_utilization_params: Option<PhevUtilizationParams>,
verbose: Option<bool>,
) -> anyhow::Result<LabelFe> {
let (label_fe, _) = get_label_fe(
veh,
max_epa_adj,
full_detail.unwrap_or_default(),
fuel_props,
phev_utilization_params,
verbose.unwrap_or_default(),
)?;
Ok(label_fe)
}
pub fn get_label_fe_phev(
veh: &Vehicle,
phev_utilization_params: &PhevUtilizationParams,
adj_params: &AdjCoef,
max_epa_adj: f64,
fuel_props: &FuelProperties,
) -> anyhow::Result<LabelFePHEV> {
let max_soc: si::Ratio;
let min_soc: si::Ratio;
let phev_max_regen: si::Ratio;
let veh_mass: si::Mass;
let em_peak_eff: si::Ratio;
let energy_capacity: si::Energy;
let chg_eff: f64;
if let PowertrainType::PlugInHybridElectricVehicle(phev) = &veh.pt_type {
max_soc = phev.res.max_soc;
min_soc = phev.res.min_soc;
phev_max_regen = 0.98 * uc::R;
veh_mass = *veh.state.mass.get_fresh(|| format_dbg!())?;
em_peak_eff = *phev
.em
.eff_interp_achieved
.max()
.with_context(|| format_dbg!())?
* uc::R;
energy_capacity = phev.res.energy_capacity;
chg_eff = DEFAULT_CHG_EFF; } else {
bail!("Vehicle is not a PHEV");
}
let mut label_fe_phev = LabelFePHEV {
regen_soc_buffer: ((0.5 * veh_mass * ((60. * uc::MPH).powi(P2::new())))
* phev_max_regen
* em_peak_eff
/ energy_capacity)
.min((max_soc - min_soc) / 2.0),
..Default::default()
};
let mut sd: HashMap<&str, SimDrive> = HashMap::new();
sd.insert(
"udds",
SimDrive::new(veh.clone(), Cycle::from_resource("udds.csv", false)?, None),
);
sd.insert(
"hwy",
SimDrive::new(veh.clone(), Cycle::from_resource("hwfet.csv", false)?, None),
);
for (key, sd) in sd.iter_mut() {
sd.walk()?;
let mut phev_calc = PHEVCycleCalc::default();
phev_calc.cd_ess_kwh = ((max_soc - min_soc) * energy_capacity).get::<si::kilowatt_hour>();
let res = sd.veh.res().with_context(|| format_dbg!())?;
let soc_start = *res
.history
.soc
.first()
.with_context(|| format_dbg!())?
.get_fresh(|| format_dbg!())?;
let soc_end = *res
.history
.soc
.last()
.with_context(|| format_dbg!())?
.get_fresh(|| format_dbg!())?;
let dist_mi = sd
.veh
.state
.dist
.get_fresh(|| format_dbg!())?
.get::<si::mile>();
phev_calc.delta_soc = soc_start - soc_end;
phev_calc.total_cd_miles = ((max_soc - min_soc) * energy_capacity)
.get::<si::kilowatt_hour>()
/ sd.veh.kwh_per_mi()?;
phev_calc.cd_cycs = phev_calc.total_cd_miles / dist_mi;
phev_calc.cd_frac_in_trans = phev_calc.cd_cycs % phev_calc.cd_cycs.floor();
let fuel_energy_kwh = if let Some(fc) = sd.veh.fc() {
fc.state
.energy_fuel
.get_fresh(|| format_dbg!())?
.get::<si::kilowatt_hour>()
} else {
0.0
};
phev_calc.cd_fs_gal = fuel_energy_kwh / fuel_props.kwh_per_gge();
phev_calc.cd_fs_kwh = fuel_energy_kwh;
phev_calc.cd_ess_kwh_per_mi = sd.veh.kwh_per_mi()?;
phev_calc.cd_mpg = sd.veh.mpg(fuel_props.energy_density)?;
let interp_x_vals: Vec<f64> = (0..((phev_calc.cd_cycs.ceil() + 1.0) as usize))
.map(|i| i as f64 * dist_mi)
.collect();
phev_calc.lab_iter_uf = vec![];
for x in interp_x_vals {
phev_calc.lab_iter_uf.push(
phev_utilization_params.uf_array[first_grtr(
&phev_utilization_params.rechg_freq_miles,
x,
)
.with_context(|| format_dbg!())?
- 1],
);
}
phev_calc.trans_init_soc = max_soc - phev_calc.cd_cycs.floor() * phev_calc.delta_soc;
let res_mut = sd.veh.res_mut().with_context(|| format_dbg!())?;
res_mut.state.soc.mark_stale();
res_mut
.state
.soc
.update(phev_calc.trans_init_soc, || format_dbg!())?;
sd.reset_cumulative(|| format_dbg!())?;
sd.reset_step(|| format_dbg!())?;
sd.clear();
sd.walk_once().with_context(|| format_dbg!())?;
phev_calc.trans_ess_kwh =
phev_calc.cd_ess_kwh_per_mi * dist_mi * phev_calc.cd_frac_in_trans;
phev_calc.trans_ess_kwh_per_mi = phev_calc.cd_ess_kwh_per_mi * phev_calc.cd_frac_in_trans;
let init_soc = min_soc + 0.01 * uc::R;
let res_mut = sd.veh.res_mut().with_context(|| format_dbg!())?;
res_mut.state.soc.mark_stale();
res_mut.state.soc.update(init_soc, || format_dbg!())?;
sd.reset_cumulative(|| format_dbg!())?;
sd.reset_step(|| format_dbg!())?;
sd.clear();
sd.walk_once().with_context(|| format_dbg!())?;
let cs_fuel_energy_kwh = if let Some(fc) = sd.veh.fc() {
fc.state
.energy_fuel
.get_fresh(|| format_dbg!())?
.get::<si::kilowatt_hour>()
} else {
0.0
};
phev_calc.cs_fs_gal = cs_fuel_energy_kwh / fuel_props.kwh_per_gge();
phev_calc.trans_fs_gal = phev_calc.cs_fs_gal * (1.0 - phev_calc.cd_frac_in_trans);
phev_calc.cs_fs_kwh = cs_fuel_energy_kwh;
phev_calc.trans_fs_kwh = phev_calc.cs_fs_kwh * (1.0 - phev_calc.cd_frac_in_trans);
let cs_ess_energy_kwh = if let Some(res) = sd.veh.res() {
res.state
.energy_out_chemical
.get_fresh(|| format_dbg!())?
.get::<si::kilowatt_hour>()
} else {
0.0
};
phev_calc.cs_ess_kwh = cs_ess_energy_kwh;
phev_calc.cs_ess_kwh_per_mi = sd.veh.kwh_per_mi()?;
let lab_iter_uf_diff = phev_calc.lab_iter_uf.diff();
phev_calc.lab_uf_gpm = [
phev_calc.trans_fs_gal * lab_iter_uf_diff.last().with_context(|| format_dbg!())?,
phev_calc.cs_fs_gal
* (1.0
- phev_calc
.lab_iter_uf
.last()
.with_context(|| format_dbg!())?),
]
.iter()
.map(|x| *x / dist_mi)
.collect();
let min_soc_in_cycle = phev_calc.delta_soc.abs(); phev_calc.cd_miles =
if (max_soc - label_fe_phev.regen_soc_buffer - min_soc_in_cycle) < 0.01 * uc::R {
1000.0
} else {
phev_calc.cd_cycs.ceil() * dist_mi
};
phev_calc.cd_lab_mpg = phev_calc
.lab_iter_uf
.last()
.with_context(|| format_dbg!())?
/ (phev_calc.trans_fs_gal / dist_mi);
phev_calc.cs_mpg = dist_mi / phev_calc.cs_fs_gal;
phev_calc.lab_uf = phev_utilization_params.uf_array[first_grtr(
&phev_utilization_params.rechg_freq_miles,
phev_calc.cd_miles,
)
.with_context(|| format_dbg!())?
- 1];
phev_calc.cd_adj_mpg =
phev_calc.lab_iter_uf.max()? / phev_calc.lab_uf_gpm[phev_calc.lab_uf_gpm.len() - 2];
phev_calc.lab_mpgge = 1.0
/ (phev_calc.lab_uf / phev_calc.cd_adj_mpg
+ (1.0 - phev_calc.lab_uf) / phev_calc.cs_mpg);
let mut lab_iter_kwh_per_mi_vals = Vec::new();
lab_iter_kwh_per_mi_vals.push(0.0);
lab_iter_kwh_per_mi_vals
.extend(vec![phev_calc.cd_ess_kwh_per_mi; phev_calc.cd_cycs.floor() as usize].iter());
lab_iter_kwh_per_mi_vals.push(phev_calc.trans_ess_kwh_per_mi);
lab_iter_kwh_per_mi_vals.push(0.0);
phev_calc.lab_iter_kwh_per_mi = lab_iter_kwh_per_mi_vals;
let uf_diff = phev_calc.lab_iter_uf.diff();
let mut vals = Vec::new();
vals.push(0.0);
for i in 1..phev_calc.lab_iter_kwh_per_mi.len() - 1 {
if i - 1 < uf_diff.len() {
vals.push(phev_calc.lab_iter_kwh_per_mi[i] * uf_diff[i - 1]);
}
}
vals.push(0.0);
phev_calc.lab_iter_uf_kwh_per_mi = vals;
phev_calc.lab_kwh_per_mi = phev_calc
.lab_iter_uf_kwh_per_mi
.iter()
.fold(0.0, |acc, x| acc + x)
/ phev_calc
.lab_iter_uf
.iter()
.fold(0.0f64, |acc, x| acc.max(*x));
let mut adj_iter_mpgge_vals = vec![0.0; phev_calc.cd_cycs.floor() as usize];
let mut adj_iter_kwh_per_mi_vals = vec![0.0; phev_calc.lab_iter_kwh_per_mi.len()];
if *key == "udds" {
adj_iter_mpgge_vals.push(f64::max(
1.0 / (adj_params.city_intercept
+ (adj_params.city_slope
/ (sd
.veh
.state
.dist
.get_fresh(|| format_dbg!())?
.get::<si::mile>()
/ (phev_calc.trans_fs_kwh / fuel_props.kwh_per_gge())))),
sd.veh
.state
.dist
.get_fresh(|| format_dbg!())?
.get::<si::mile>()
/ (phev_calc.trans_fs_kwh / fuel_props.kwh_per_gge())
* (1.0 - max_epa_adj),
));
adj_iter_mpgge_vals.push(f64::max(
1.0 / (adj_params.city_intercept
+ (adj_params.city_slope
/ (sd
.veh
.state
.dist
.get_fresh(|| format_dbg!())?
.get::<si::mile>()
/ (phev_calc.cs_fs_kwh / fuel_props.kwh_per_gge())))),
sd.veh
.state
.dist
.get_fresh(|| format_dbg!())?
.get::<si::mile>()
/ (phev_calc.cs_fs_kwh / fuel_props.kwh_per_gge())
* (1.0 - max_epa_adj),
));
for (c, _) in phev_calc.lab_iter_kwh_per_mi.iter().enumerate() {
if phev_calc.lab_iter_kwh_per_mi[c] == 0.0 {
adj_iter_kwh_per_mi_vals[c] = 0.0;
} else {
adj_iter_kwh_per_mi_vals[c] =
(1.0 / f64::max(
1.0 / (adj_params.city_intercept
+ (adj_params.city_slope
/ ((1.0 / phev_calc.lab_iter_kwh_per_mi[c])
* fuel_props.kwh_per_gge()))),
(1.0 - max_epa_adj)
* ((1.0 / phev_calc.lab_iter_kwh_per_mi[c])
* fuel_props.kwh_per_gge()),
)) * fuel_props.kwh_per_gge();
}
}
} else {
adj_iter_mpgge_vals.push(f64::max(
1.0 / (adj_params.hwy_intercept
+ (adj_params.hwy_slope
/ (sd
.veh
.state
.dist
.get_fresh(|| format_dbg!())?
.get::<si::mile>()
/ (phev_calc.trans_fs_kwh / fuel_props.kwh_per_gge())))),
sd.veh
.state
.dist
.get_fresh(|| format_dbg!())?
.get::<si::mile>()
/ (phev_calc.trans_fs_kwh / fuel_props.kwh_per_gge())
* (1.0 - max_epa_adj),
));
adj_iter_mpgge_vals.push(f64::max(
1.0 / (adj_params.hwy_intercept
+ (adj_params.hwy_slope
/ (sd
.veh
.state
.dist
.get_fresh(|| format_dbg!())?
.get::<si::mile>()
/ (phev_calc.cs_fs_kwh / fuel_props.kwh_per_gge())))),
sd.veh
.state
.dist
.get_fresh(|| format_dbg!())?
.get::<si::mile>()
/ (phev_calc.cs_fs_kwh / fuel_props.kwh_per_gge())
* (1.0 - max_epa_adj),
));
for (c, _) in phev_calc.lab_iter_kwh_per_mi.iter().enumerate() {
if phev_calc.lab_iter_kwh_per_mi[c] == 0.0 {
adj_iter_kwh_per_mi_vals[c] = 0.0;
} else {
adj_iter_kwh_per_mi_vals[c] =
(1.0 / f64::max(
1.0 / (adj_params.hwy_intercept
+ (adj_params.hwy_slope
/ ((1.0 / phev_calc.lab_iter_kwh_per_mi[c])
* fuel_props.kwh_per_gge()))),
(1.0 - max_epa_adj)
* ((1.0 / phev_calc.lab_iter_kwh_per_mi[c])
* fuel_props.kwh_per_gge()),
)) * fuel_props.kwh_per_gge();
}
}
}
phev_calc.adj_iter_mpgge = adj_iter_mpgge_vals;
phev_calc.adj_iter_kwh_per_mi = adj_iter_kwh_per_mi_vals;
phev_calc.adj_iter_cd_miles = vec![0.0; phev_calc.cd_cycs.ceil() as usize + 2];
for c in 0..phev_calc.adj_iter_cd_miles.len() {
if c == 0 {
phev_calc.adj_iter_cd_miles[c] = 0.0;
} else if c <= phev_calc.cd_cycs.floor() as usize {
phev_calc.adj_iter_cd_miles[c] = phev_calc.adj_iter_cd_miles[c - 1]
+ phev_calc.cd_ess_kwh_per_mi
* sd.veh
.state
.dist
.get_fresh(|| format_dbg!())?
.get::<si::mile>()
/ phev_calc.adj_iter_kwh_per_mi[c];
} else if c == phev_calc.cd_cycs.floor() as usize + 1 {
phev_calc.adj_iter_cd_miles[c] = phev_calc.adj_iter_cd_miles[c - 1]
+ phev_calc.trans_ess_kwh_per_mi
* sd.veh
.state
.dist
.get_fresh(|| format_dbg!())?
.get::<si::mile>()
/ phev_calc.adj_iter_kwh_per_mi[c];
} else {
phev_calc.adj_iter_cd_miles[c] = 0.0;
}
}
let mut soc_hist: Vec<f64> = vec![];
for soc in sd
.veh
.res()
.with_context(|| format_dbg!())?
.history
.soc
.clone()
{
soc_hist.push(soc.get_fresh(|| format_dbg!())?.get::<si::ratio>());
}
phev_calc.adj_cd_miles =
if max_soc - label_fe_phev.regen_soc_buffer - (*soc_hist.min()? * uc::R) < 0.01 * uc::R
{
1000.0
} else {
*phev_calc.adj_iter_cd_miles.max()?
};
phev_calc.adj_iter_uf = vec![];
for x in phev_calc.adj_iter_cd_miles.clone() {
phev_calc.adj_iter_uf.push(
phev_utilization_params.uf_array[first_grtr(
&phev_utilization_params.rechg_freq_miles,
x,
)
.with_context(|| format_dbg!())?
- 1],
)
}
let adj_iter_uf_diff = phev_calc.adj_iter_uf.diff();
phev_calc.adj_iter_uf_gpm = vec![0.0; phev_calc.cd_cycs.floor() as usize];
phev_calc.adj_iter_uf_gpm.push(
(1.0 / phev_calc.adj_iter_mpgge[phev_calc.adj_iter_mpgge.len() - 2])
* adj_iter_uf_diff[adj_iter_uf_diff.len() - 2],
);
phev_calc.adj_iter_uf_gpm.push(
(1.0 / phev_calc
.adj_iter_mpgge
.last()
.with_context(|| format_dbg!())?)
* (1.0 - phev_calc.adj_iter_uf[phev_calc.adj_iter_uf.len() - 2]),
);
let adj_uf_diff = phev_calc.adj_iter_uf.diff();
phev_calc.adj_iter_uf_kwh_per_mi = phev_calc
.adj_iter_kwh_per_mi
.iter()
.zip(adj_uf_diff.iter())
.map(|(kwh, uf)| kwh * uf)
.collect();
let max_uf: f64 = phev_calc
.adj_iter_uf
.iter()
.fold(0.0f64, |acc, x| acc.max(*x));
phev_calc.adj_cd_mpgge =
1.0 / phev_calc.adj_iter_uf_gpm[phev_calc.adj_iter_uf_gpm.len() - 2] * max_uf;
phev_calc.adj_cs_mpgge = 1.0
/ phev_calc
.adj_iter_uf_gpm
.last()
.with_context(|| format_dbg!())?
* (1.0 - max_uf);
phev_calc.adj_uf = phev_utilization_params.uf_array[first_grtr(
&phev_utilization_params.rechg_freq_miles,
phev_calc.adj_cd_miles,
)
.with_context(|| format_dbg!())?
- 1];
phev_calc.adj_mpgge = 1.0
/ (phev_calc.adj_uf / phev_calc.adj_cd_mpgge
+ (1.0 - phev_calc.adj_uf) / phev_calc.adj_cs_mpgge);
let uf_kwh_sum: f64 = phev_calc
.adj_iter_uf_kwh_per_mi
.iter()
.fold(0.0, |acc, x| acc + x);
phev_calc.adj_kwh_per_mi = uf_kwh_sum / max_uf / chg_eff;
phev_calc.adj_ess_kwh_per_mi = uf_kwh_sum / max_uf;
match *key {
"udds" => label_fe_phev.udds = phev_calc.clone(),
"hwy" => label_fe_phev.hwy = phev_calc.clone(),
&_ => bail!("No field for cycle {}", key),
};
}
Ok(label_fe_phev)
}
#[cfg(test)]
mod tests {
use super::*;
pub struct Tolerances {
pub udds_tolerance: f64,
pub comb_tolerance: f64,
pub hwy_tolerance: f64,
pub accel_tolerance: f64,
}
fn assert_labels_match_within_tolerance(
label_fe_f3: &LabelFe,
label_fe_f2: &fastsim_2::simdrivelabel::LabelFe,
tol: &Tolerances,
all_electric: bool,
) {
let mut all_passed: bool = true;
let mut message: String = String::new();
if all_electric {
let udds_err = (label_fe_f3.lab_udds_kwh_per_mi - label_fe_f2.lab_udds_kwh_per_mi)
.abs()
/ label_fe_f2.lab_udds_kwh_per_mi;
let test = udds_err < tol.udds_tolerance;
all_passed = all_passed && test;
message = format!(
"{}\n{}UDDS kWh/mi mismatch: F3={:.3}, F2={:.3}; err = {:.3} (> tol {:.3})",
message,
if test { " " } else { "* " },
label_fe_f3.lab_udds_kwh_per_mi,
label_fe_f2.lab_udds_kwh_per_mi,
udds_err,
tol.udds_tolerance
);
let comb_err = (label_fe_f3.lab_comb_kwh_per_mi - label_fe_f2.lab_comb_kwh_per_mi)
.abs()
/ label_fe_f2.lab_comb_kwh_per_mi;
let test = comb_err < tol.comb_tolerance;
all_passed = all_passed && test;
message = format!(
"{}\n{}Combined kWh/mi mismatch: F3={:.3}, F2={:.3}; err = {:.3} (> tol {:.3})",
message,
if test { " " } else { "* " },
label_fe_f3.lab_comb_kwh_per_mi,
label_fe_f2.lab_comb_kwh_per_mi,
comb_err,
tol.comb_tolerance
);
let hwy_err = (label_fe_f3.lab_hwy_kwh_per_mi - label_fe_f2.lab_hwy_kwh_per_mi).abs()
/ label_fe_f2.lab_hwy_kwh_per_mi;
let test = hwy_err < tol.hwy_tolerance;
all_passed = all_passed && test;
message = format!(
"{}\n{}HWY kWh/mi mismatch: F3={:.3}, F2={:.3}; err = {:.3} (> tol {:.3})",
message,
if test { " " } else { "* " },
label_fe_f3.lab_hwy_kwh_per_mi,
label_fe_f2.lab_hwy_kwh_per_mi,
hwy_err,
tol.hwy_tolerance
);
} else {
let udds_err = (label_fe_f3.lab_udds_mpgge - label_fe_f2.lab_udds_mpgge).abs()
/ label_fe_f2.lab_udds_mpgge;
let test = udds_err < tol.udds_tolerance;
all_passed = all_passed && test;
message = format!(
"{}\n{}UDDS MPGe mismatch: F3={:.3}, F2={:.3}; err = {:.3} (> tol {:.3})",
message,
if test { " " } else { "* " },
label_fe_f3.lab_udds_mpgge,
label_fe_f2.lab_udds_mpgge,
udds_err,
tol.udds_tolerance
);
let comb_err = (label_fe_f3.lab_comb_mpgge - label_fe_f2.lab_comb_mpgge).abs()
/ label_fe_f2.lab_comb_mpgge;
let test = comb_err < tol.comb_tolerance;
all_passed = all_passed && test;
message = format!(
"{}\n{}Combined MPGe mismatch: F3={:.3}, F2={:.3}; err = {:.3} (> tol {:.3})",
message,
if test { " " } else { "* " },
label_fe_f3.lab_comb_mpgge,
label_fe_f2.lab_comb_mpgge,
comb_err,
tol.comb_tolerance
);
let hwy_err = (label_fe_f3.lab_hwy_mpgge - label_fe_f2.lab_hwy_mpgge).abs()
/ label_fe_f2.lab_hwy_mpgge;
let test = hwy_err < tol.hwy_tolerance;
all_passed = all_passed && test;
message = format!(
"{}\n{}Hwy MPGe mismatch: F3={:.3}, F2={:.3}; err = {:.3} (> tol {:.3})",
message,
if test { " " } else { "* " },
label_fe_f3.lab_hwy_mpgge,
label_fe_f2.lab_hwy_mpgge,
hwy_err,
tol.hwy_tolerance
);
}
let accel_err =
(label_fe_f3.net_accel - label_fe_f2.net_accel).abs() / label_fe_f2.net_accel;
let test = accel_err < tol.accel_tolerance;
all_passed = all_passed && test;
message = format!(
"{}\n{}Acceleration time mismatch: F3={:.3}, F2={:.3}; err = {:.3} (> tol {:.3})",
message,
if test { " " } else { "* " },
label_fe_f3.net_accel,
label_fe_f2.net_accel,
accel_err,
tol.accel_tolerance
);
assert!(all_passed, "Individual Test Results:\n{}", message);
}
#[test]
#[cfg(all(feature = "resources", feature = "yaml"))]
fn test_label_fe_conv_vs_fastsim2() {
let file_contents = include_str!("vehicle/fastsim-2_2012_Ford_Fusion.yaml");
use fastsim_2::traits::SerdeAPI;
let f2veh = fastsim_2::vehicle::RustVehicle::from_yaml(file_contents, false).unwrap();
let mut veh = Vehicle::try_from(f2veh.clone()).unwrap();
let (label_fe_f3, _) = get_label_fe(&mut veh, None, false, None, None, false)
.with_context(|| format_dbg!())
.unwrap();
let (label_fe_f2, _) = fastsim_2::simdrivelabel::get_label_fe(&f2veh.clone(), None, None)
.with_context(|| format_dbg!())
.unwrap();
let tol = Tolerances {
udds_tolerance: 0.03, comb_tolerance: 0.03,
hwy_tolerance: 0.03,
accel_tolerance: 0.05,
};
assert_labels_match_within_tolerance(&label_fe_f3, &label_fe_f2, &tol, false);
println!("Conventional vehicle label FE test passed!");
println!(
"F3 Combined MPGe: {:.3}, F2: {:.3}",
label_fe_f3.lab_comb_mpgge, label_fe_f2.lab_comb_mpgge
);
}
#[test]
#[cfg(all(feature = "resources", feature = "yaml"))]
fn test_label_fe_bev_vs_fastsim2() {
let file_contents = include_str!("vehicle/fastsim-2_2022_Renault_Zoe_ZE50_R135.yaml");
use fastsim_2::traits::SerdeAPI;
let f2veh = fastsim_2::vehicle::RustVehicle::from_yaml(file_contents, false).unwrap();
let mut veh = Vehicle::try_from(f2veh.clone()).unwrap();
let (label_fe_f3, _) = get_label_fe(&mut veh, None, false, None, None, false)
.with_context(|| format_dbg!())
.unwrap();
let (label_fe_f2, _) = fastsim_2::simdrivelabel::get_label_fe(&f2veh.clone(), None, None)
.with_context(|| format_dbg!())
.unwrap();
let tol = Tolerances {
udds_tolerance: 0.011, comb_tolerance: 0.011,
hwy_tolerance: 0.011,
accel_tolerance: 0.15,
};
assert_labels_match_within_tolerance(&label_fe_f3, &label_fe_f2, &tol, true);
println!("BEV label FE test passed!");
println!(
"F3 Combined kWh/mi: {:.3}, F2: {:.3}",
label_fe_f3.lab_comb_kwh_per_mi, label_fe_f2.lab_comb_kwh_per_mi
);
}
#[test]
#[cfg(all(feature = "resources", feature = "yaml"))]
fn test_label_fe_hev_vs_fastsim2() {
let file_contents = include_str!("vehicle/fastsim-2_2016_TOYOTA_Prius_Two.yaml");
use fastsim_2::traits::SerdeAPI;
let f2veh = fastsim_2::vehicle::RustVehicle::from_yaml(file_contents, false).unwrap();
let mut veh = Vehicle::try_from(f2veh.clone()).unwrap();
let (label_fe_f3, _) = get_label_fe(&mut veh, None, false, None, None, false)
.with_context(|| format_dbg!())
.unwrap();
let (label_fe_f2, _) = fastsim_2::simdrivelabel::get_label_fe(&f2veh, None, None)
.with_context(|| format_dbg!())
.unwrap();
let tol = Tolerances {
udds_tolerance: 0.15, comb_tolerance: 0.15,
hwy_tolerance: 0.15,
accel_tolerance: 0.05, };
assert_labels_match_within_tolerance(&label_fe_f3, &label_fe_f2, &tol, false);
}
#[test]
#[cfg(all(feature = "resources", feature = "yaml"))]
fn test_label_fe_phev_vs_fastsim2() {
let f2_veh_path = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
.parent()
.with_context(|| format_dbg!())
.unwrap()
.join("cal_and_val/f2-vehicles/2016 CHEVROLET Volt.yaml");
if !f2_veh_path.exists() {
println!("PHEV vehicle file not found, skipping test");
return;
}
let veh_contents = std::fs::read_to_string(&f2_veh_path)
.with_context(|| format_dbg!())
.unwrap();
let f2_veh: fastsim_2::vehicle::RustVehicle =
fastsim_2::traits::SerdeAPI::from_yaml(&veh_contents, false)
.with_context(|| format_dbg!())
.unwrap();
assert!(f2_veh.veh_pt_type == fastsim_2::vehicle::PHEV);
let mut veh = Vehicle::try_from(f2_veh.clone())
.with_context(|| format_dbg!())
.unwrap();
assert!(
veh.pt_type.is_plug_in_hybrid_electric_vehicle(),
"`veh.pt_type.variant_as_str()`: {}\n`f2_veh.veh_pt_type`: {}",
veh.pt_type.variant_as_str(),
f2_veh.veh_pt_type
);
let label_fe_f3 = get_label_fe(&mut veh, None, false, None, None, false)
.unwrap()
.0;
let label_fe_f2 = fastsim_2::simdrivelabel::get_label_fe(&f2_veh, None, None)
.unwrap()
.0;
let tol = Tolerances {
udds_tolerance: 0.05, comb_tolerance: 0.05,
hwy_tolerance: 0.05,
accel_tolerance: 0.105,
};
assert_labels_match_within_tolerance(&label_fe_f3, &label_fe_f2, &tol, false);
}
fn frac_diff(base: f64, new_value: f64) -> f64 {
let abs_diff = (new_value - base).abs();
if base != 0.0 {
abs_diff / base
} else {
abs_diff
}
}
fn assert_label_fe_same(
label_fe_f2: &fastsim_2::simdrivelabel::LabelFe,
label_fe_f3: &LabelFe,
tol: f64,
) {
let mut all_pass = true;
let mut message = String::new();
let diff = frac_diff(label_fe_f2.lab_comb_mpgge, label_fe_f3.lab_comb_mpgge);
all_pass = all_pass && diff < tol;
message = format!(
"{}\nlab_comb_mpgge: F3: {:.3}; F2: {:.3} ({:.3})",
message, label_fe_f3.lab_comb_mpgge, label_fe_f2.lab_comb_mpgge, diff
);
let diff = frac_diff(
label_fe_f2.lab_comb_kwh_per_mi,
label_fe_f3.lab_comb_kwh_per_mi,
);
all_pass = all_pass && diff < tol;
message = format!(
"{}\nlab_comb_kwh_per_mi: F3: {:.3}; F2 {:.3} ({:.3})",
message, label_fe_f3.lab_comb_kwh_per_mi, label_fe_f2.lab_comb_kwh_per_mi, diff
);
let diff = frac_diff(label_fe_f2.adj_udds_mpgge, label_fe_f3.adj_udds_mpgge);
all_pass = all_pass && diff < tol;
message = format!(
"{}\nadj_udds_mpgge: F3: {:.3}; F2: {:.3} ({:.3})",
message, label_fe_f3.adj_udds_mpgge, label_fe_f2.adj_udds_mpgge, diff
);
let diff = frac_diff(label_fe_f2.adj_hwy_mpgge, label_fe_f3.adj_hwy_mpgge);
all_pass = all_pass && diff < tol;
message = format!(
"{}\nadj_hwy_mpgge: F3: {:.3}; F2: {:.3} ({:.3})",
message, label_fe_f3.adj_hwy_mpgge, label_fe_f2.adj_hwy_mpgge, diff
);
let diff = frac_diff(label_fe_f2.adj_comb_mpgge, label_fe_f3.adj_comb_mpgge);
all_pass = all_pass && diff < tol;
message = format!(
"{}\nadj_comb_mpgge: F3: {:.3}; F2: {:.3} ({:.3})",
message, label_fe_f3.adj_comb_mpgge, label_fe_f2.adj_comb_mpgge, diff
);
let diff = frac_diff(
label_fe_f2.adj_udds_kwh_per_mi,
label_fe_f3.adj_udds_kwh_per_mi,
);
all_pass = all_pass && diff < tol;
message = format!(
"{}\nadj_udds_kwh_per_mi: F3 {:.3}; F2 {:.3} ({:.3})",
message, label_fe_f3.adj_udds_kwh_per_mi, label_fe_f2.adj_udds_kwh_per_mi, diff
);
let diff = frac_diff(
label_fe_f2.adj_hwy_kwh_per_mi,
label_fe_f3.adj_hwy_kwh_per_mi,
);
all_pass = all_pass && diff < tol;
message = format!(
"{}\nadj_hwy_kwh_per_mi: F3 {:.3}; F2 {:.3} ({:.3})",
message, label_fe_f3.adj_hwy_kwh_per_mi, label_fe_f2.adj_hwy_kwh_per_mi, diff
);
let diff = frac_diff(
label_fe_f2.adj_comb_kwh_per_mi,
label_fe_f3.adj_comb_kwh_per_mi,
);
all_pass = all_pass && diff < tol;
message = format!(
"{}\nadj_comb_kwh_per_mi: F3: {:.3}; F2: {:.3} ({:.3})",
message, label_fe_f3.adj_comb_kwh_per_mi, label_fe_f2.adj_comb_kwh_per_mi, diff
);
let diff = frac_diff(label_fe_f2.net_accel, label_fe_f3.net_accel);
all_pass = all_pass && diff < tol;
message = format!(
"{}\nnet_accel: F3: {:.3}; F2: {:.3} ({:.3})",
message, label_fe_f3.net_accel, label_fe_f2.net_accel, diff
);
assert!(
all_pass,
"ERROR: At least some tests exceed tolerance of {:.3}:\n{}",
tol, message
);
}
fn run_fe_label_comparison_for(file_contents: &str, tolerance: f64) {
use fastsim_2::traits::SerdeAPI;
let f2veh = fastsim_2::vehicle::RustVehicle::from_yaml(file_contents, false).unwrap();
let f2veh_copy = f2veh.clone();
let (label_fe_f2, result) =
fastsim_2::simdrivelabel::get_label_fe(&f2veh_copy, Some(true), None)
.with_context(|| format_dbg!())
.unwrap();
let sim_data = SimulationDataForLabel::ConvOrHev {
veh_year: f2veh.veh_year,
udds_mpgge: label_fe_f2.lab_udds_mpgge,
hwy_mpgge: label_fe_f2.lab_hwy_mpgge,
};
let max_epa_adj = 0.3;
assert!(result.is_some());
let results_data = result.unwrap();
assert!(results_data.contains_key("accel"));
let accel_sd = &results_data["accel"];
let accel_data = AccelData {
time_s: accel_sd.cyc.time_s.to_vec(),
speed_mph: accel_sd.mph_ach.to_vec(),
};
let label_fe_f3 = calculate_label_fuel_economy(
&FuelProperties::default(),
&PhevUtilizationParams::default(),
max_epa_adj,
&sim_data,
&accel_data,
)
.expect("should return an OK result");
assert_label_fe_same(&label_fe_f2, &label_fe_f3, tolerance);
}
#[test]
#[cfg(all(feature = "resources", feature = "yaml"))]
pub fn test_label_fe_post_proc_calcs_for_conv() {
let file_contents = include_str!("vehicle/fastsim-2_2012_Ford_Fusion.yaml");
let tolerance = 1e-6;
run_fe_label_comparison_for(file_contents, tolerance);
}
#[test]
#[cfg(all(feature = "resources", feature = "yaml"))]
pub fn test_label_fe_post_proc_calcs_for_hev() {
let file_contents = include_str!("vehicle/fastsim-2_2016_TOYOTA_Prius_Two.yaml");
let tolerance = 1e-6;
run_fe_label_comparison_for(file_contents, tolerance);
}
#[test]
#[cfg(all(feature = "resources", feature = "yaml"))]
pub fn test_label_fe_post_proc_calcs_for_bev() {
let file_contents = include_str!("vehicle/fastsim-2_2022_Renault_Zoe_ZE50_R135.yaml");
use fastsim_2::traits::SerdeAPI;
let f2veh = fastsim_2::vehicle::RustVehicle::from_yaml(file_contents, false).unwrap();
let f2veh_copy = f2veh.clone();
let (label_fe_f2, result) =
fastsim_2::simdrivelabel::get_label_fe(&f2veh_copy, Some(true), None)
.with_context(|| format_dbg!())
.unwrap();
let sim_data = SimulationDataForLabel::Bev {
veh_year: f2veh.veh_year,
udds_kwh_per_mi: label_fe_f2.lab_udds_kwh_per_mi,
hwy_kwh_per_mi: label_fe_f2.lab_hwy_kwh_per_mi,
bev_energy_capacity_kwh: f2veh.ess_max_kwh,
};
let max_epa_adj = 0.3;
assert!(result.is_some());
let results_data = result.unwrap();
assert!(results_data.contains_key("accel"));
let accel_sd = &results_data["accel"];
let accel_data = AccelData {
time_s: accel_sd.cyc.time_s.to_vec(),
speed_mph: accel_sd.mph_ach.to_vec(),
};
let label_fe_f3 = calculate_label_fuel_economy(
&FuelProperties::default(),
&PhevUtilizationParams::default(),
max_epa_adj,
&sim_data,
&accel_data,
)
.expect("should have OK result");
let tolerance = 1e-6;
assert_label_fe_same(&label_fe_f2, &label_fe_f3, tolerance);
}
#[test]
#[cfg(all(feature = "resources", feature = "yaml"))]
pub fn test_label_fe_post_proc_calcs_for_phev() {
let f2_veh_path = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
.parent()
.with_context(|| format_dbg!())
.unwrap()
.join("cal_and_val/f2-vehicles/2016 CHEVROLET Volt.yaml");
if !f2_veh_path.exists() {
println!("PHEV vehicle file not found, skipping test");
return;
}
let veh_contents = std::fs::read_to_string(&f2_veh_path)
.with_context(|| format_dbg!())
.unwrap();
let f2_veh: fastsim_2::vehicle::RustVehicle =
fastsim_2::traits::SerdeAPI::from_yaml(&veh_contents, false)
.with_context(|| format_dbg!())
.unwrap();
assert!(f2_veh.veh_pt_type == fastsim_2::vehicle::PHEV);
let veh = Vehicle::try_from(f2_veh.clone())
.with_context(|| format_dbg!())
.unwrap();
assert!(
veh.pt_type.is_plug_in_hybrid_electric_vehicle(),
"`veh.pt_type.variant_as_str()`: {}\n`f2_veh.veh_pt_type`: {}",
veh.pt_type.variant_as_str(),
f2_veh.veh_pt_type
);
let f2veh_copy = f2_veh.clone();
let (label_fe_f2, result) =
fastsim_2::simdrivelabel::get_label_fe(&f2veh_copy, Some(true), None)
.with_context(|| format_dbg!())
.unwrap();
assert!(label_fe_f2.phev_calcs.is_some());
let phev_calcs = label_fe_f2.phev_calcs.clone().unwrap();
eprintln!("phev_calcs: {:?}", phev_calcs);
assert!(result.is_some());
let result = result.unwrap();
eprintln!("result.keys: {:?}", result.keys());
assert!(result.contains_key("udds"));
let udds_result = &result["udds"];
assert!(result.contains_key("hwy"));
let hwy_result = &result["hwy"];
eprintln!(
"udds start soc: {:?}; min soc: {:?}",
udds_result.soc[0],
udds_result.soc.min()
);
eprintln!(
"hwy start soc: {:?}; min soc: {:?}",
hwy_result.soc[0],
hwy_result.soc.min()
);
let fuel_props = FuelProperties::default();
let sim_data = SimulationDataForLabel::Phev {
veh_year: f2_veh.veh_year,
info: PhevVehicleInfo {
max_soc: f2_veh.max_soc * uc::R,
min_soc: f2_veh.min_soc * uc::R,
phev_max_regen: f2_veh.max_regen * uc::R,
veh_mass: f2_veh.veh_kg * uc::KG,
em_peak_eff: f2_veh.mc_peak_eff() * uc::R,
energy_capacity: f2_veh.ess_max_kwh * uc::KWH,
chg_eff: DEFAULT_CHG_EFF,
fuel_storage_capacity: f2_veh.fs_kwh * uc::KWH,
},
udds: PhevSimulationDataForLabel {
cd_fuel_consumed_kwh: phev_calcs.udds.cd_fs_kwh,
cd_soc_start: f2_veh.max_soc,
cd_soc_end: f2_veh.max_soc - phev_calcs.udds.delta_soc,
cyc_dist_mi: udds_result.dist_mi.sum(),
cd_kwh_per_mi: phev_calcs.udds.cd_ess_kwh_per_mi,
cd_mpg: udds_result.dist_mi.sum()
/ (phev_calcs.udds.cd_fs_kwh / fuel_props.kwh_per_gge()),
cs_fuel_consumed_kwh: phev_calcs.udds.cs_fs_kwh,
cs_ess_energy_kwh: phev_calcs.udds.cs_ess_kwh,
cs_kwh_per_mi: phev_calcs.udds.cs_ess_kwh_per_mi,
cs_mpg: phev_calcs.udds.cs_mpg,
cs_min_soc: f2_veh.min_soc,
cs_fs_energy_capacity_kwh: f2_veh.fs_kwh,
},
hwy: PhevSimulationDataForLabel {
cd_fuel_consumed_kwh: phev_calcs.hwy.cd_fs_kwh,
cd_soc_start: f2_veh.max_soc,
cd_soc_end: f2_veh.max_soc - phev_calcs.hwy.delta_soc,
cyc_dist_mi: hwy_result.dist_mi.sum(),
cd_kwh_per_mi: phev_calcs.hwy.cd_ess_kwh_per_mi,
cd_mpg: hwy_result.dist_mi.sum()
/ (phev_calcs.hwy.cd_fs_kwh / fuel_props.kwh_per_gge()),
cs_fuel_consumed_kwh: phev_calcs.hwy.cs_fs_kwh,
cs_ess_energy_kwh: phev_calcs.hwy.cs_ess_kwh,
cs_kwh_per_mi: phev_calcs.hwy.cs_ess_kwh_per_mi,
cs_mpg: phev_calcs.hwy.cs_mpg,
cs_min_soc: f2_veh.min_soc,
cs_fs_energy_capacity_kwh: f2_veh.fs_kwh,
},
};
let max_epa_adj = 0.3;
assert!(result.contains_key("accel"));
let accel_sd = &result["accel"];
let accel_data = AccelData {
time_s: accel_sd.cyc.time_s.to_vec(),
speed_mph: accel_sd.mph_ach.to_vec(),
};
let label_fe_f3 = calculate_label_fuel_economy(
&FuelProperties::default(),
&PhevUtilizationParams::default(),
max_epa_adj,
&sim_data,
&accel_data,
)
.expect("expect OK result");
let tolerance = 0.002;
assert_label_fe_same(&label_fe_f2, &label_fe_f3, tolerance);
}
}