From adfed29d7f35a1de78cfe5cb3e1bf40e803ae8b4 Mon Sep 17 00:00:00 2001 From: Chad Baker Date: Wed, 17 Jan 2024 13:25:45 -0700 Subject: [PATCH 01/30] `cargo test --no-default-features` works in rust/fastsim-core --- rust/fastsim-core/Cargo.toml | 29 ++-- .../src/add_pyo3_api/mod.rs | 1 + rust/fastsim-core/src/imports.rs | 1 + rust/fastsim-core/src/resources.rs | 2 + rust/fastsim-core/src/simdrive/cyc_mods.rs | 2 +- .../src/simdrive/simdrive_impl.rs | 4 +- rust/fastsim-core/src/simdrivelabel.rs | 8 +- rust/fastsim-core/src/thermal.rs | 2 +- rust/fastsim-core/src/traits.rs | 18 ++- rust/fastsim-core/src/utils.rs | 3 + rust/fastsim-core/src/vehicle.rs | 129 ++++++++++-------- rust/fastsim-core/src/vehicle_utils.rs | 30 +++- 12 files changed, 151 insertions(+), 78 deletions(-) diff --git a/rust/fastsim-core/Cargo.toml b/rust/fastsim-core/Cargo.toml index 31ecb917..c7c48d46 100644 --- a/rust/fastsim-core/Cargo.toml +++ b/rust/fastsim-core/Cargo.toml @@ -23,21 +23,21 @@ csv = "1.1" serde_json = "1.0.81" bincode = "1.3.3" log = "0.4.17" -polynomial = "0.2.4" -argmin = "0.7.0" -argmin-math = { version = "0.2.1", features = [ +polynomial = { optional = true, version = "0.2.4" } +argmin = { optional = true, version = "0.7.0" } +argmin-math = { optional = true, version = "0.2.1", features = [ "ndarray_latest-nolinalg-serde", ] } -curl = "0.4.44" -validator = { version = "0.16", features = ["derive"] } +curl = { optional = true, version = "0.4.44" } +validator = { version = "0.16", features = ["derive"], optional = true } lazy_static = "1.4.0" regex = "1.7.1" rayon = "1.7.0" zip = "0.6.6" -directories = "5.0.1" -include_dir = "0.7.3" +directories = { optional = true, version = "5.0.1" } +include_dir = { optional = true, version = "0.7.3" } itertools = "0.12.0" -ndarray-stats = "0.5.1" +ndarray-stats = { optional = true, version = "0.5.1" } tempfile = "3.8.1" [package.metadata] @@ -50,4 +50,17 @@ include = [ ] [features] +# to disable the default features, see +# https://doc.rust-lang.org/cargo/reference/features.html?highlight=no-default-features#the-default-feature +# and use the `--no-default-features` flag when compiling +# default = ["full"] pyo3 = ["dep:pyo3"] +full = [ + "dep:argmin", + "dep:argmin-math", + "dep:curl", + "dep:directories", + "dep:include_dir", + "dep:polynomial", + "dep:validator", +] diff --git a/rust/fastsim-core/fastsim-proc-macros/src/add_pyo3_api/mod.rs b/rust/fastsim-core/fastsim-proc-macros/src/add_pyo3_api/mod.rs index 41a66c25..26472417 100644 --- a/rust/fastsim-core/fastsim-proc-macros/src/add_pyo3_api/mod.rs +++ b/rust/fastsim-core/fastsim-proc-macros/src/add_pyo3_api/mod.rs @@ -213,6 +213,7 @@ pub fn add_pyo3_api(attr: TokenStream, item: TokenStream) -> TokenStream { /// /// * `filepath`: `str | pathlib.Path` - Filepath, relative to the top of the `resources` folder, from which to read the object /// + #[cfg(feature = "full")] #[staticmethod] #[pyo3(name = "from_resource")] pub fn from_resource_py(filepath: &PyAny) -> anyhow::Result { diff --git a/rust/fastsim-core/src/imports.rs b/rust/fastsim-core/src/imports.rs index f938a8cb..d85c525d 100644 --- a/rust/fastsim-core/src/imports.rs +++ b/rust/fastsim-core/src/imports.rs @@ -2,6 +2,7 @@ pub(crate) use anyhow::{anyhow, bail, ensure, Context}; pub(crate) use bincode; pub(crate) use log; pub(crate) use ndarray::{array, s, Array, Array1, Axis}; +#[cfg(feature = "full")] pub(crate) use ndarray_stats::QuantileExt; pub(crate) use serde::{Deserialize, Serialize}; pub(crate) use std::cmp; diff --git a/rust/fastsim-core/src/resources.rs b/rust/fastsim-core/src/resources.rs index 0cf74313..9bdad25d 100644 --- a/rust/fastsim-core/src/resources.rs +++ b/rust/fastsim-core/src/resources.rs @@ -1,2 +1,4 @@ +#![cfg(feature = "full")] + use include_dir::{include_dir, Dir}; pub const RESOURCES_DIR: Dir = include_dir!("$CARGO_MANIFEST_DIR/resources"); diff --git a/rust/fastsim-core/src/simdrive/cyc_mods.rs b/rust/fastsim-core/src/simdrive/cyc_mods.rs index d181818d..85872f27 100644 --- a/rust/fastsim-core/src/simdrive/cyc_mods.rs +++ b/rust/fastsim-core/src/simdrive/cyc_mods.rs @@ -159,7 +159,7 @@ impl RustSimDrive { let v_desired_m_per_s = if self.idm_target_speed_m_per_s[i] > 0.0 { self.idm_target_speed_m_per_s[i] } else { - *self.cyc0.mps.max().unwrap() + self.cyc0.mps.max().unwrap() }; // DERIVED VALUES self.cyc.mps[i] = self.next_speed_by_idm( diff --git a/rust/fastsim-core/src/simdrive/simdrive_impl.rs b/rust/fastsim-core/src/simdrive/simdrive_impl.rs index 2203ec01..a89f587d 100644 --- a/rust/fastsim-core/src/simdrive/simdrive_impl.rs +++ b/rust/fastsim-core/src/simdrive/simdrive_impl.rs @@ -1070,7 +1070,7 @@ impl RustSimDrive { self.mps_ach[i] = max( speed_guesses[_ys .iter() - .position(|x| x == _ys.min().unwrap()) + .position(|x| *x == _ys.min().unwrap()) .ok_or_else(|| anyhow!(format_dbg!(_ys.min().unwrap())))?], 0.0, ); @@ -1892,7 +1892,7 @@ impl RustSimDrive { ); } - self.trace_miss_speed_mps = *(&self.mps_ach - &self.cyc.mps).map(|x| x.abs()).max()?; + self.trace_miss_speed_mps = (&self.mps_ach - &self.cyc.mps).map(|x| x.abs()).max()?; if self.trace_miss_speed_mps > self.sim_params.trace_miss_speed_mps_tol { self.trace_miss = true; log::warn!( diff --git a/rust/fastsim-core/src/simdrivelabel.rs b/rust/fastsim-core/src/simdrivelabel.rs index 34f6167f..ad7b4889 100644 --- a/rust/fastsim-core/src/simdrivelabel.rs +++ b/rust/fastsim-core/src/simdrivelabel.rs @@ -1,6 +1,7 @@ //! Module containing classes and methods for calculating label fuel economy. use ndarray::Array; +#[cfg(feature = "full")] use ndarray_stats::QuantileExt; use serde::Serialize; use std::collections::HashMap; @@ -168,6 +169,7 @@ pub fn get_net_accel_py(sd_accel: &mut RustSimDrive, scenario_name: &str) -> any Ok(result) } +#[cfg(feature = "full")] pub fn get_label_fe( veh: &vehicle::RustVehicle, full_detail: Option, @@ -384,6 +386,7 @@ pub fn get_label_fe( } } +#[cfg(feature = "full")] #[cfg(feature = "pyo3")] #[pyfunction(name = "get_label_fe")] /// pyo3 version of [get_label_fe] @@ -638,7 +641,7 @@ pub fn get_label_fe_phev( if veh.max_soc - phev_calcs.regen_soc_buffer - sd_val.soc.min()? < 0.01 { 1000.0 } else { - *phev_calc.adj_iter_cd_miles.max()? + phev_calc.adj_iter_cd_miles.max()? }; // utility factor calculation for last charge depletion iteration and transition iteration @@ -722,6 +725,7 @@ pub fn get_label_fe_phev_py( } #[cfg(test)] +#[cfg(feature = "full")] mod simdrivelabel_tests { use super::*; @@ -777,6 +781,7 @@ mod simdrivelabel_tests { label_fe_truth.to_json().unwrap(), ); } + #[test] fn test_get_label_fe_phev() { let mut veh = vehicle::RustVehicle { @@ -1149,6 +1154,7 @@ mod simdrivelabel_tests { } } +#[cfg(feature = "full")] #[cfg(feature = "pyo3")] #[pyfunction(name = "get_label_fe_conv")] /// pyo3 version of [get_label_fe_conv] diff --git a/rust/fastsim-core/src/thermal.rs b/rust/fastsim-core/src/thermal.rs index 8eca94cf..3894b340 100644 --- a/rust/fastsim-core/src/thermal.rs +++ b/rust/fastsim-core/src/thermal.rs @@ -952,7 +952,7 @@ impl SimDriveHot { } } - if &self.sd.fc_kw_out_ach[i] == self.sd.veh.input_kw_out_array.max()? { + if self.sd.fc_kw_out_ach[i] == self.sd.veh.input_kw_out_array.max()? { self.sd.fc_kw_in_ach[i] = self.sd.fc_kw_out_ach[i] / (self.sd.veh.fc_eff_array.last().unwrap() * self.state.fc_eta_temp_coeff) } else { diff --git a/rust/fastsim-core/src/traits.rs b/rust/fastsim-core/src/traits.rs index e39c1678..d02c7e61 100644 --- a/rust/fastsim-core/src/traits.rs +++ b/rust/fastsim-core/src/traits.rs @@ -15,7 +15,7 @@ pub trait SerdeAPI: Serialize + for<'a> Deserialize<'a> { /// # Arguments: /// /// * `filepath` - Filepath, relative to the top of the `resources` folder, from which to read the object - /// + #[cfg(feature = "full")] fn from_resource>(filepath: P) -> anyhow::Result { let filepath = filepath.as_ref(); let extension = filepath @@ -274,3 +274,19 @@ where .all(|(key, value)| other.get(key).map_or(false, |v| value.approx_eq(v, tol))); } } + +pub trait IterMaxMin { + fn max(&self) -> anyhow::Result; + fn min(&self) -> anyhow::Result; +} + +impl IterMaxMin for Array1 { + fn max(&self) -> anyhow::Result { + ensure!(!self.is_empty()); + Ok(self.iter().fold(f64::NEG_INFINITY, |acc, x| x.max(acc))) + } + fn min(&self) -> anyhow::Result { + ensure!(!self.is_empty()); + Ok(self.iter().fold(f64::INFINITY, |acc, x| x.min(acc))) + } +} diff --git a/rust/fastsim-core/src/utils.rs b/rust/fastsim-core/src/utils.rs index 80681efa..1e74ab04 100644 --- a/rust/fastsim-core/src/utils.rs +++ b/rust/fastsim-core/src/utils.rs @@ -1,9 +1,11 @@ //! Module containing miscellaneous utility functions. +#[cfg(feature = "full")] use directories::ProjectDirs; use itertools::Itertools; use lazy_static::lazy_static; use ndarray::*; +#[cfg(feature = "full")] use ndarray_stats::QuantileExt; use regex::Regex; use std::collections::HashSet; @@ -515,6 +517,7 @@ pub fn tire_code_to_radius>(tire_code: S) -> anyhow::Result { Ok(radius_mm / 1000.0) } +#[cfg(feature = "full")] /// Creates/gets an OS-specific data directory and returns the path. pub fn create_project_subdir>(subpath: P) -> anyhow::Result { let proj_dirs = ProjectDirs::from("gov", "NREL", "fastsim").ok_or_else(|| { diff --git a/rust/fastsim-core/src/vehicle.rs b/rust/fastsim-core/src/vehicle.rs index 61968488..2a8e525d 100644 --- a/rust/fastsim-core/src/vehicle.rs +++ b/rust/fastsim-core/src/vehicle.rs @@ -9,6 +9,7 @@ use crate::pyo3imports::*; use lazy_static::lazy_static; use regex::Regex; +#[cfg(feature = "full")] use validator::Validate; // veh_pt_type options @@ -84,7 +85,8 @@ lazy_static! { Self::mock_vehicle() } )] -#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, ApproxEq, Validate)] +#[cfg_attr(feature = "full", derive(Validate))] +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, ApproxEq)] /// Struct containing vehicle attributes /// # Python Examples /// ```python @@ -114,23 +116,26 @@ pub struct RustVehicle { pub veh_year: u32, /// Vehicle powertrain type, one of \[[CONV](CONV), [HEV](HEV), [PHEV](PHEV), [BEV](BEV)\] #[serde(alias = "vehPtType")] - #[validate(regex( - path = "VEH_PT_TYPE_OPTIONS_REGEX", - message = "must be one of [\"Conv\", \"HEV\", \"PHEV\", \"BEV\"]" - ))] + #[cfg_attr( + feature = "full", + validate(regex( + path = "VEH_PT_TYPE_OPTIONS_REGEX", + message = "must be one of [\"Conv\", \"HEV\", \"PHEV\", \"BEV\"]" + )) + )] #[doc_field(skip_doc)] pub veh_pt_type: String, /// Aerodynamic drag coefficient #[serde(alias = "dragCoef")] - #[validate(range(min = 0))] + #[cfg_attr(feature = "full", validate(range(min = 0)))] pub drag_coef: f64, /// Frontal area, $m^2$ #[serde(alias = "frontalAreaM2")] - #[validate(range(min = 0))] + #[cfg_attr(feature = "full", validate(range(min = 0)))] pub frontal_area_m2: f64, /// Vehicle mass excluding cargo, passengers, and powertrain components, $kg$ #[serde(alias = "gliderKg")] - #[validate(range(min = 0))] + #[cfg_attr(feature = "full", validate(range(min = 0)))] pub glider_kg: f64, /// Vehicle center of mass height, $m$ /// **NOTE:** positive for FWD, negative for RWD, AWD, 4WD @@ -138,43 +143,43 @@ pub struct RustVehicle { pub veh_cg_m: f64, /// Fraction of weight on the drive axle while stopped #[serde(alias = "driveAxleWeightFrac")] - #[validate(range(min = 0, max = 1))] + #[cfg_attr(feature = "full", validate(range(min = 0, max = 1)))] pub drive_axle_weight_frac: f64, /// Wheelbase, $m$ #[serde(alias = "wheelBaseM")] - #[validate(range(min = 0))] + #[cfg_attr(feature = "full", validate(range(min = 0)))] pub wheel_base_m: f64, /// Cargo mass including passengers, $kg$ #[serde(alias = "cargoKg")] - #[validate(range(min = 0))] + #[cfg_attr(feature = "full", validate(range(min = 0)))] pub cargo_kg: f64, /// Total vehicle mass, overrides mass calculation, $kg$ #[serde(alias = "vehOverrideKg")] - #[validate(range(min = 0))] + #[cfg_attr(feature = "full", validate(range(min = 0)))] pub veh_override_kg: Option, /// Component mass multiplier for vehicle mass calculation #[serde(alias = "compMassMultiplier")] - #[validate(range(min = 0))] + #[cfg_attr(feature = "full", validate(range(min = 0)))] pub comp_mass_multiplier: f64, /// Fuel storage max power output, $kW$ #[serde(alias = "maxFuelStorKw")] - #[validate(range(min = 0))] + #[cfg_attr(feature = "full", validate(range(min = 0)))] pub fs_max_kw: f64, /// Fuel storage time to peak power, $s$ #[serde(alias = "fuelStorSecsToPeakPwr")] - #[validate(range(min = 0))] + #[cfg_attr(feature = "full", validate(range(min = 0)))] pub fs_secs_to_peak_pwr: f64, /// Fuel storage energy capacity, $kWh$ #[serde(alias = "fuelStorKwh")] - #[validate(range(min = 0))] + #[cfg_attr(feature = "full", validate(range(min = 0)))] pub fs_kwh: f64, /// Fuel specific energy, $\frac{kWh}{kg}$ #[serde(alias = "fuelStorKwhPerKg")] - #[validate(range(min = 0))] + #[cfg_attr(feature = "full", validate(range(min = 0)))] pub fs_kwh_per_kg: f64, /// Fuel converter peak continuous power, $kW$ #[serde(alias = "maxFuelConvKw")] - #[validate(range(min = 0))] + #[cfg_attr(feature = "full", validate(range(min = 0)))] pub fc_max_kw: f64, /// Fuel converter output power percentage map, x values of [fc_eff_map](RustVehicle::fc_eff_map) #[serde(alias = "fcPwrOutPerc")] @@ -185,34 +190,37 @@ pub struct RustVehicle { /// Fuel converter efficiency type, one of \[[SI](SI), [ATKINSON](ATKINSON), [DIESEL](DIESEL), [H2FC](H2FC), [HD_DIESEL](HD_DIESEL)\] /// Used for calculating [fc_eff_map](RustVehicle::fc_eff_map), and other calculations if H2FC #[serde(alias = "fcEffType")] - #[validate(regex( - path = "FC_EFF_TYPE_OPTIONS_REGEX", - message = "must be one of [\"SI\", \"Atkinson\", \"Diesel\", \"H2FC\", \"HD_Diesel\"]" - ))] + #[cfg_attr( + feature = "full", + validate(regex( + path = "FC_EFF_TYPE_OPTIONS_REGEX", + message = "must be one of [\"SI\", \"Atkinson\", \"Diesel\", \"H2FC\", \"HD_Diesel\"]" + )) + )] pub fc_eff_type: String, /// Fuel converter time to peak power, $s$ #[serde(alias = "fuelConvSecsToPeakPwr")] - #[validate(range(min = 0))] + #[cfg_attr(feature = "full", validate(range(min = 0)))] pub fc_sec_to_peak_pwr: f64, /// Fuel converter base mass, $kg$ #[serde(alias = "fuelConvBaseKg")] - #[validate(range(min = 0))] + #[cfg_attr(feature = "full", validate(range(min = 0)))] pub fc_base_kg: f64, /// Fuel converter specific power (power-to-weight ratio), $\frac{kW}{kg}$ #[serde(alias = "fuelConvKwPerKg")] - #[validate(range(min = 0))] + #[cfg_attr(feature = "full", validate(range(min = 0)))] pub fc_kw_per_kg: f64, /// Minimum time fuel converter must be on before shutoff (for HEV, PHEV) #[serde(alias = "minFcTimeOn")] - #[validate(range(min = 0))] + #[cfg_attr(feature = "full", validate(range(min = 0)))] pub min_fc_time_on: f64, /// Fuel converter idle power, $kW$ #[serde(alias = "idleFcKw")] - #[validate(range(min = 0))] + #[cfg_attr(feature = "full", validate(range(min = 0)))] pub idle_fc_kw: f64, /// Peak continuous electric motor power, $kW$ #[serde(alias = "mcMaxElecInKw")] - #[validate(range(min = 0))] + #[cfg_attr(feature = "full", validate(range(min = 0)))] pub mc_max_kw: f64, /// Electric motor output power percentage map, x values of [mc_eff_map](RustVehicle::mc_eff_map) #[serde(alias = "mcPwrOutPerc")] @@ -222,35 +230,35 @@ pub struct RustVehicle { pub mc_eff_map: Array1, /// Electric motor time to peak power, $s$ #[serde(alias = "motorSecsToPeakPwr")] - #[validate(range(min = 0))] + #[cfg_attr(feature = "full", validate(range(min = 0)))] pub mc_sec_to_peak_pwr: f64, /// Motor power electronics mass per power output, $\frac{kg}{kW}$ #[serde(alias = "mcPeKgPerKw")] - #[validate(range(min = 0))] + #[cfg_attr(feature = "full", validate(range(min = 0)))] pub mc_pe_kg_per_kw: f64, /// Motor power electronics base mass, $kg$ #[serde(alias = "mcPeBaseKg")] - #[validate(range(min = 0))] + #[cfg_attr(feature = "full", validate(range(min = 0)))] pub mc_pe_base_kg: f64, /// Traction battery maximum power output, $kW$ #[serde(alias = "maxEssKw")] - #[validate(range(min = 0))] + #[cfg_attr(feature = "full", validate(range(min = 0)))] pub ess_max_kw: f64, /// Traction battery energy capacity, $kWh$ #[serde(alias = "maxEssKwh")] - #[validate(range(min = 0))] + #[cfg_attr(feature = "full", validate(range(min = 0)))] pub ess_max_kwh: f64, /// Traction battery mass per energy, $\frac{kg}{kWh}$ #[serde(alias = "essKgPerKwh")] - #[validate(range(min = 0))] + #[cfg_attr(feature = "full", validate(range(min = 0)))] pub ess_kg_per_kwh: f64, /// Traction battery base mass, $kg$ #[serde(alias = "essBaseKg")] - #[validate(range(min = 0))] + #[cfg_attr(feature = "full", validate(range(min = 0)))] pub ess_base_kg: f64, /// Traction battery round-trip efficiency #[serde(alias = "essRoundTripEff")] - #[validate(range(min = 0, max = 1))] + #[cfg_attr(feature = "full", validate(range(min = 0, max = 1)))] pub ess_round_trip_eff: f64, /// Traction battery cycle life coefficient A, see [reference](https://web.archive.org/web/20090529194442/http://www.ocean.udel.edu/cms/wkempton/Kempton-V2G-pdfFiles/PDF%20format/Duvall-V2G-batteries-June05.pdf) #[serde(alias = "essLifeCoefA")] @@ -260,63 +268,63 @@ pub struct RustVehicle { pub ess_life_coef_b: f64, /// Traction battery minimum state of charge #[serde(alias = "minSoc")] - #[validate(range(min = 0, max = 1))] + #[cfg_attr(feature = "full", validate(range(min = 0, max = 1)))] pub min_soc: f64, /// Traction battery maximum state of charge #[serde(alias = "maxSoc")] - #[validate(range(min = 0, max = 1))] + #[cfg_attr(feature = "full", validate(range(min = 0, max = 1)))] pub max_soc: f64, /// ESS discharge effort toward max FC efficiency #[serde(alias = "essDischgToFcMaxEffPerc")] - #[validate(range(min = 0, max = 1))] + #[cfg_attr(feature = "full", validate(range(min = 0, max = 1)))] pub ess_dischg_to_fc_max_eff_perc: f64, /// ESS charge effort toward max FC efficiency #[serde(alias = "essChgToFcMaxEffPerc")] - #[validate(range(min = 0, max = 1))] + #[cfg_attr(feature = "full", validate(range(min = 0, max = 1)))] pub ess_chg_to_fc_max_eff_perc: f64, /// Mass moment of inertia per wheel, $kg \cdot m^2$ #[serde(alias = "wheelInertiaKgM2")] - #[validate(range(min = 0))] + #[cfg_attr(feature = "full", validate(range(min = 0)))] pub wheel_inertia_kg_m2: f64, /// Number of wheels #[serde(alias = "numWheels")] - #[validate(range(min = 0))] + #[cfg_attr(feature = "full", validate(range(min = 0)))] pub num_wheels: f64, // TODO: Shouldn't this just be a unsigned integer? u8 would work fine. /// Rolling resistance coefficient #[serde(alias = "wheelRrCoef")] - #[validate(range(min = 0))] + #[cfg_attr(feature = "full", validate(range(min = 0)))] pub wheel_rr_coef: f64, /// Wheel radius, $m$ #[serde(alias = "wheelRadiusM")] - #[validate(range(min = 0))] + #[cfg_attr(feature = "full", validate(range(min = 0)))] pub wheel_radius_m: f64, /// Wheel coefficient of friction #[serde(alias = "wheelCoefOfFric")] - #[validate(range(min = 0))] + #[cfg_attr(feature = "full", validate(range(min = 0)))] pub wheel_coef_of_fric: f64, /// Speed where the battery reserved for accelerating is zero #[serde(alias = "maxAccelBufferMph")] - #[validate(range(min = 0))] + #[cfg_attr(feature = "full", validate(range(min = 0)))] pub max_accel_buffer_mph: f64, /// Percent of usable battery energy reserved to help accelerate #[serde(alias = "maxAccelBufferPercOfUseableSoc")] - #[validate(range(min = 0, max = 1))] + #[cfg_attr(feature = "full", validate(range(min = 0, max = 1)))] pub max_accel_buffer_perc_of_useable_soc: f64, /// Percent SOC buffer for high accessory loads during cycles with long idle time #[serde(alias = "percHighAccBuf")] - #[validate(range(min = 0))] + #[cfg_attr(feature = "full", validate(range(min = 0)))] pub perc_high_acc_buf: f64, /// Speed at which the fuel converter must turn on, $mph$ #[serde(alias = "mphFcOn")] - #[validate(range(min = 0))] + #[cfg_attr(feature = "full", validate(range(min = 0)))] pub mph_fc_on: f64, /// Power demand above which to require fuel converter on, $kW$ #[serde(alias = "kwDemandFcOn")] - #[validate(range(min = 0))] + #[cfg_attr(feature = "full", validate(range(min = 0)))] pub kw_demand_fc_on: f64, /// Maximum brake regeneration efficiency #[serde(alias = "maxRegen")] - #[validate(range(min = 0, max = 1))] + #[cfg_attr(feature = "full", validate(range(min = 0, max = 1)))] pub max_regen: f64, /// Stop/start micro-HEV flag pub stop_start: bool, @@ -325,27 +333,27 @@ pub struct RustVehicle { pub force_aux_on_fc: bool, /// Alternator efficiency #[serde(alias = "altEff")] - #[validate(range(min = 0, max = 1))] + #[cfg_attr(feature = "full", validate(range(min = 0, max = 1)))] pub alt_eff: f64, /// Charger efficiency #[serde(alias = "chgEff")] - #[validate(range(min = 0, max = 1))] + #[cfg_attr(feature = "full", validate(range(min = 0, max = 1)))] pub chg_eff: f64, /// Auxiliary load power, $kW$ #[serde(alias = "auxKw")] - #[validate(range(min = 0))] + #[cfg_attr(feature = "full", validate(range(min = 0)))] pub aux_kw: f64, /// Transmission mass, $kg$ #[serde(alias = "transKg")] - #[validate(range(min = 0))] + #[cfg_attr(feature = "full", validate(range(min = 0)))] pub trans_kg: f64, /// Transmission efficiency #[serde(alias = "transEff")] - #[validate(range(min = 0, max = 1))] + #[cfg_attr(feature = "full", validate(range(min = 0, max = 1)))] pub trans_eff: f64, /// Maximum acceptable ratio of change in ESS energy to expended fuel energy (used in hybrid SOC balancing), $\frac{\Delta E_{ESS}}{\Delta E_{fuel}}$ #[serde(alias = "essToFuelOkError")] - #[validate(range(min = 0))] + #[cfg_attr(feature = "full", validate(range(min = 0)))] pub ess_to_fuel_ok_error: f64, #[doc(hidden)] #[doc_field(skip_doc)] @@ -527,11 +535,11 @@ pub struct RustVehicle { pub val_msrp: f64, /// Fuel converter efficiency peak override, scales entire curve #[serde(skip)] - #[validate(range(min = 0, max = 1))] + #[cfg_attr(feature = "full", validate(range(min = 0, max = 1)))] pub fc_peak_eff_override: Option, /// Motor efficiency peak override, scales entire curve #[serde(skip)] - #[validate(range(min = 0, max = 1))] + #[cfg_attr(feature = "full", validate(range(min = 0, max = 1)))] pub mc_peak_eff_override: Option, #[serde(skip)] #[doc(hidden)] @@ -675,6 +683,7 @@ impl RustVehicle { /// - `max_trac_mps2` pub fn set_derived(&mut self) -> anyhow::Result<()> { // Vehicle input validation + #[cfg(feature = "full")] self.validate()?; if self.scenario_name != "Template Vehicle for setting up data types" { @@ -1059,6 +1068,7 @@ impl SerdeAPI for RustVehicle { #[cfg(test)] mod tests { use super::*; + #[cfg(feature = "full")] use validator::ValidationErrors; #[test] @@ -1077,6 +1087,7 @@ mod tests { // the produced error for the offending field names } + #[cfg(feature = "full")] #[test] fn test_input_validation() { // set up vehicle input parameters diff --git a/rust/fastsim-core/src/vehicle_utils.rs b/rust/fastsim-core/src/vehicle_utils.rs index a34b99a8..3896b7c5 100644 --- a/rust/fastsim-core/src/vehicle_utils.rs +++ b/rust/fastsim-core/src/vehicle_utils.rs @@ -1,9 +1,13 @@ //! Module for utility functions that support the vehicle struct. -use argmin::core::{CostFunction, Error, Executor, OptimizationResult, State}; +#[cfg(feature = "full")] +use argmin::core::{CostFunction, Executor, OptimizationResult, State}; +#[cfg(feature = "full")] use argmin::solver::neldermead::NelderMead; +#[cfg(feature = "full")] use curl::easy::Easy; use ndarray::{array, Array1}; +#[cfg(feature = "full")] use polynomial::Polynomial; use serde::de::DeserializeOwned; use std::collections::HashMap; @@ -297,6 +301,7 @@ pub struct VehicleDataEPA { impl SerdeAPI for VehicleDataEPA {} +#[cfg(feature = "full")] #[cfg_attr(feature = "pyo3", pyfunction)] /// Gets options from fueleconomy.gov for the given vehicle year, make, and model /// @@ -753,6 +758,7 @@ pub struct OtherVehicleInputs { impl SerdeAPI for OtherVehicleInputs {} +#[cfg(feature = "full")] #[cfg_attr(feature = "pyo3", pyfunction)] /// Creates RustVehicle for the given vehicle using data from fueleconomy.gov and EPA databases /// The created RustVehicle is also written as a yaml file @@ -772,7 +778,7 @@ pub fn vehicle_import_by_id_and_year( other_inputs: &OtherVehicleInputs, cache_url: Option, data_dir: Option, -) -> Result { +) -> anyhow::Result { let mut maybe_veh: Option = None; let data_dir_path = if let Some(data_dir) = data_dir { PathBuf::from(data_dir) @@ -843,6 +849,7 @@ fn get_fuel_economy_gov_data_for_input_record( output } +#[cfg(feature = "full")] /// Try to make a single vehicle using the provided data sets. fn try_make_single_vehicle( fe_gov_data: &VehicleDataFE, @@ -1036,6 +1043,7 @@ fn try_make_single_vehicle( Some(veh) } +#[cfg(feature = "full")] fn try_import_vehicles( vir: &VehicleInputRecord, fegov_data: &[VehicleDataFE], @@ -1099,9 +1107,9 @@ pub fn export_vehicle_to_file(veh: &RustVehicle, file_path: String) -> anyhow::R } #[allow(non_snake_case)] -#[allow(clippy::too_many_arguments)] #[cfg_attr(feature = "pyo3", pyfunction)] #[allow(clippy::too_many_arguments)] +#[cfg(feature = "full")] pub fn abc_to_drag_coeffs( veh: &mut RustVehicle, a_lbf: f64, @@ -1220,12 +1228,14 @@ pub fn get_error_val(model: Array1, test: Array1, time_steps: Array1 { cycle: &'a RustCycle, vehicle: &'a RustVehicle, dyno_func_lb: &'a Polynomial, } +#[cfg(feature = "full")] impl CostFunction for GetError<'_> { type Param = Array1; type Output = f64; @@ -1441,6 +1451,7 @@ pub fn extract_zip(filepath: &Path, dest_dir: &Path) -> anyhow::Result<()> { Ok(()) } +#[cfg(feature = "full")] /// Assumes the parent directory exists. Assumes file doesn't exist (i.e., newly created) or that it will be truncated if it does. pub fn download_file_from_url(url: &str, file_path: &Path) -> anyhow::Result<()> { let mut handle = Easy::new(); @@ -1567,6 +1578,7 @@ fn extract_file_from_zip( Ok(()) } +#[cfg(feature = "full")] /// Checks the cache directory to see if data files have been downloaded /// If so, moves on without any further action. /// If not, downloads data by year from remote site if it exists @@ -1629,6 +1641,7 @@ fn populate_cache_for_given_years_if_needed( } #[cfg_attr(feature = "pyo3", pyfunction)] +#[cfg(feature = "full")] /// Import All Vehicles for the given Year, Make, and Model and supplied other inputs pub fn import_all_vehicles( year: u32, @@ -1686,6 +1699,7 @@ pub fn import_all_vehicles( Ok(vehs) } +#[cfg(feature = "full")] /// Import and Save All Vehicles Specified via Input File pub fn import_and_save_all_vehicles_from_file( input_path: &Path, @@ -1716,6 +1730,7 @@ pub fn import_and_save_all_vehicles_from_file( import_and_save_all_vehicles(&inputs, &fegov_data_by_year, &epatest_db, output_dir_path) } +#[cfg(feature = "full")] pub fn import_all_vehicles_from_record( inputs: &[VehicleInputRecord], fegov_data_by_year: &HashMap>, @@ -1739,6 +1754,7 @@ pub fn import_all_vehicles_from_record( vehs } +#[cfg(feature = "full")] pub fn import_and_save_all_vehicles( inputs: &[VehicleInputRecord], fegov_data_by_year: &HashMap>, @@ -1787,6 +1803,7 @@ mod vehicle_utils_tests { assert!(error_val.approx_eq(&0.8124999999999998, 1e-10)); } + #[cfg(feature = "full")] #[test] fn test_abc_to_drag_coeffs() { let mut veh: RustVehicle = RustVehicle::mock_vehicle(); @@ -1814,6 +1831,7 @@ mod vehicle_utils_tests { assert_eq!(wheel_rr_coef, veh.wheel_rr_coef); } + #[cfg(feature = "full")] #[test] fn test_create_new_vehicle_from_input_data() { let veh_record = VehicleInputRecord { @@ -1928,6 +1946,7 @@ mod vehicle_utils_tests { } } + #[cfg(feature = "full")] #[test] fn test_get_options_for_year_make_model() { let year = String::from("2020"); @@ -1941,6 +1960,7 @@ mod vehicle_utils_tests { } } + #[cfg(feature = "full")] #[test] fn test_import_robustness() { // Ensure 2019 data is cached @@ -1958,8 +1978,7 @@ mod vehicle_utils_tests { let veh_records = { let file = File::open(vehicles_path); if let Ok(f) = file { - let data_result: Result>, Error> = - read_records_from_file(f); + let data_result = read_records_from_file(f); if let Ok(data) = data_result { data } else { @@ -2011,6 +2030,7 @@ mod vehicle_utils_tests { assert!(success_frac > 0.90, "success_frac = {}", success_frac); } + #[cfg(feature = "full")] #[test] fn test_get_options_for_year_make_model_for_specified_cacheurl_and_data_dir() { let year = String::from("2020"); From 41ec2a17de11813254cb9b3ff3751b04b5460e5d Mon Sep 17 00:00:00 2001 From: Chad Baker Date: Wed, 17 Jan 2024 13:40:04 -0700 Subject: [PATCH 02/30] several failing python tests due to not having feature enabled --- rust/fastsim-py/Cargo.toml | 8 ++++++-- rust/fastsim-py/src/lib.rs | 7 +++++++ 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/rust/fastsim-py/Cargo.toml b/rust/fastsim-py/Cargo.toml index 11a55d4b..4dc280f9 100644 --- a/rust/fastsim-py/Cargo.toml +++ b/rust/fastsim-py/Cargo.toml @@ -11,9 +11,9 @@ repository = "https://github.com/NREL/fastsim" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] +pyo3-log = { workspace = true, optional = true } fastsim-core = { path = "../fastsim-core", features = ["pyo3"], version = "~0" } pyo3 = { workspace = true, features = ["extension-module", "anyhow"] } -pyo3-log = { workspace = true } log = "0.4.17" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html @@ -22,4 +22,8 @@ name = "fastsimrust" crate-type = ["cdylib"] [package.metadata] -include = ["../../NOTICE"] \ No newline at end of file +include = ["../../NOTICE"] + +[features] +# default = ["full"] +full = ["fastsim-core/full", "dep:pyo3-log"] diff --git a/rust/fastsim-py/src/lib.rs b/rust/fastsim-py/src/lib.rs index 5bd4ed32..4fa07a8e 100644 --- a/rust/fastsim-py/src/lib.rs +++ b/rust/fastsim-py/src/lib.rs @@ -5,6 +5,7 @@ use pyo3imports::*; /// Function for adding Rust structs as Python Classes #[pymodule] fn fastsimrust(py: Python, m: &PyModule) -> PyResult<()> { + #[cfg(feature = "full")] pyo3_log::init(); m.add_class::()?; m.add_class::()?; @@ -28,20 +29,26 @@ fn fastsimrust(py: Python, m: &PyModule) -> PyResult<()> { m.add_class::()?; cycle::register(py, m)?; + #[cfg(feature = "full")] m.add_function(wrap_pyfunction!(vehicle_utils::abc_to_drag_coeffs, m)?)?; m.add_function(wrap_pyfunction!(make_accel_trace_py, m)?)?; m.add_function(wrap_pyfunction!(get_net_accel_py, m)?)?; + #[cfg(feature = "full")] m.add_function(wrap_pyfunction!(get_label_fe_py, m)?)?; m.add_function(wrap_pyfunction!(get_label_fe_phev_py, m)?)?; + #[cfg(feature = "full")] m.add_function(wrap_pyfunction!(get_label_fe_conv_py, m)?)?; + #[cfg(feature = "full")] m.add_function(wrap_pyfunction!( vehicle_utils::get_options_for_year_make_model, m )?)?; + #[cfg(feature = "full")] m.add_function(wrap_pyfunction!( vehicle_utils::vehicle_import_by_id_and_year, m )?)?; + #[cfg(feature = "full")] m.add_function(wrap_pyfunction!(vehicle_utils::import_all_vehicles, m)?)?; m.add_function(wrap_pyfunction!(vehicle_utils::export_vehicle_to_file, m)?)?; From ea7ea69604b9cc6edc6ef59d8f576bd68df371c2 Mon Sep 17 00:00:00 2001 From: Chad Baker Date: Wed, 17 Jan 2024 14:20:00 -0700 Subject: [PATCH 03/30] removed `ndarray-stats` dependency `full` is a default feature in `fastsim-core` and `fastsim-py` --- rust/fastsim-core/Cargo.toml | 3 +- rust/fastsim-core/src/imports.rs | 2 -- rust/fastsim-core/src/simdrive/cyc_mods.rs | 2 +- .../src/simdrive/simdrive_impl.rs | 4 +-- rust/fastsim-core/src/simdrivelabel.rs | 4 +-- rust/fastsim-core/src/thermal.rs | 2 +- rust/fastsim-core/src/traits.rs | 34 +++++++++++++------ rust/fastsim-core/src/utils.rs | 2 -- rust/fastsim-core/src/vehicle_utils.rs | 5 +-- rust/fastsim-py/Cargo.toml | 2 +- 10 files changed, 34 insertions(+), 26 deletions(-) diff --git a/rust/fastsim-core/Cargo.toml b/rust/fastsim-core/Cargo.toml index c7c48d46..6de4075e 100644 --- a/rust/fastsim-core/Cargo.toml +++ b/rust/fastsim-core/Cargo.toml @@ -37,7 +37,6 @@ zip = "0.6.6" directories = { optional = true, version = "5.0.1" } include_dir = { optional = true, version = "0.7.3" } itertools = "0.12.0" -ndarray-stats = { optional = true, version = "0.5.1" } tempfile = "3.8.1" [package.metadata] @@ -53,7 +52,7 @@ include = [ # to disable the default features, see # https://doc.rust-lang.org/cargo/reference/features.html?highlight=no-default-features#the-default-feature # and use the `--no-default-features` flag when compiling -# default = ["full"] +default = ["full"] pyo3 = ["dep:pyo3"] full = [ "dep:argmin", diff --git a/rust/fastsim-core/src/imports.rs b/rust/fastsim-core/src/imports.rs index d85c525d..08bd3833 100644 --- a/rust/fastsim-core/src/imports.rs +++ b/rust/fastsim-core/src/imports.rs @@ -2,8 +2,6 @@ pub(crate) use anyhow::{anyhow, bail, ensure, Context}; pub(crate) use bincode; pub(crate) use log; pub(crate) use ndarray::{array, s, Array, Array1, Axis}; -#[cfg(feature = "full")] -pub(crate) use ndarray_stats::QuantileExt; pub(crate) use serde::{Deserialize, Serialize}; pub(crate) use std::cmp; pub(crate) use std::ffi::OsStr; diff --git a/rust/fastsim-core/src/simdrive/cyc_mods.rs b/rust/fastsim-core/src/simdrive/cyc_mods.rs index 85872f27..d181818d 100644 --- a/rust/fastsim-core/src/simdrive/cyc_mods.rs +++ b/rust/fastsim-core/src/simdrive/cyc_mods.rs @@ -159,7 +159,7 @@ impl RustSimDrive { let v_desired_m_per_s = if self.idm_target_speed_m_per_s[i] > 0.0 { self.idm_target_speed_m_per_s[i] } else { - self.cyc0.mps.max().unwrap() + *self.cyc0.mps.max().unwrap() }; // DERIVED VALUES self.cyc.mps[i] = self.next_speed_by_idm( diff --git a/rust/fastsim-core/src/simdrive/simdrive_impl.rs b/rust/fastsim-core/src/simdrive/simdrive_impl.rs index a89f587d..2203ec01 100644 --- a/rust/fastsim-core/src/simdrive/simdrive_impl.rs +++ b/rust/fastsim-core/src/simdrive/simdrive_impl.rs @@ -1070,7 +1070,7 @@ impl RustSimDrive { self.mps_ach[i] = max( speed_guesses[_ys .iter() - .position(|x| *x == _ys.min().unwrap()) + .position(|x| x == _ys.min().unwrap()) .ok_or_else(|| anyhow!(format_dbg!(_ys.min().unwrap())))?], 0.0, ); @@ -1892,7 +1892,7 @@ impl RustSimDrive { ); } - self.trace_miss_speed_mps = (&self.mps_ach - &self.cyc.mps).map(|x| x.abs()).max()?; + self.trace_miss_speed_mps = *(&self.mps_ach - &self.cyc.mps).map(|x| x.abs()).max()?; if self.trace_miss_speed_mps > self.sim_params.trace_miss_speed_mps_tol { self.trace_miss = true; log::warn!( diff --git a/rust/fastsim-core/src/simdrivelabel.rs b/rust/fastsim-core/src/simdrivelabel.rs index ad7b4889..6bb0f9aa 100644 --- a/rust/fastsim-core/src/simdrivelabel.rs +++ b/rust/fastsim-core/src/simdrivelabel.rs @@ -1,8 +1,6 @@ //! Module containing classes and methods for calculating label fuel economy. use ndarray::Array; -#[cfg(feature = "full")] -use ndarray_stats::QuantileExt; use serde::Serialize; use std::collections::HashMap; @@ -641,7 +639,7 @@ pub fn get_label_fe_phev( if veh.max_soc - phev_calcs.regen_soc_buffer - sd_val.soc.min()? < 0.01 { 1000.0 } else { - phev_calc.adj_iter_cd_miles.max()? + *phev_calc.adj_iter_cd_miles.max()? }; // utility factor calculation for last charge depletion iteration and transition iteration diff --git a/rust/fastsim-core/src/thermal.rs b/rust/fastsim-core/src/thermal.rs index 3894b340..f1362800 100644 --- a/rust/fastsim-core/src/thermal.rs +++ b/rust/fastsim-core/src/thermal.rs @@ -952,7 +952,7 @@ impl SimDriveHot { } } - if self.sd.fc_kw_out_ach[i] == self.sd.veh.input_kw_out_array.max()? { + if self.sd.fc_kw_out_ach[i] == *self.sd.veh.input_kw_out_array.max()? { self.sd.fc_kw_in_ach[i] = self.sd.fc_kw_out_ach[i] / (self.sd.veh.fc_eff_array.last().unwrap() * self.state.fc_eta_temp_coeff) } else { diff --git a/rust/fastsim-core/src/traits.rs b/rust/fastsim-core/src/traits.rs index d02c7e61..c109415f 100644 --- a/rust/fastsim-core/src/traits.rs +++ b/rust/fastsim-core/src/traits.rs @@ -275,18 +275,32 @@ where } } -pub trait IterMaxMin { - fn max(&self) -> anyhow::Result; - fn min(&self) -> anyhow::Result; +/// This trait was heavily inspired by `ndarray-stats` crate +pub trait IterMaxMin { + fn max(&self) -> anyhow::Result<&A>; + fn min(&self) -> anyhow::Result<&A>; } -impl IterMaxMin for Array1 { - fn max(&self) -> anyhow::Result { - ensure!(!self.is_empty()); - Ok(self.iter().fold(f64::NEG_INFINITY, |acc, x| x.max(acc))) +#[allow(clippy::manual_try_fold)] // `try_fold` is apparently not implemented +impl IterMaxMin for Array1 { + fn max(&self) -> anyhow::Result<&f64> { + let first = self.first().ok_or(anyhow!("empty input"))?; + self.fold(Ok(first), |acc, elem| { + let acc = acc?; + match elem.partial_cmp(acc).ok_or(anyhow!("undefined order"))? { + cmp::Ordering::Greater => Ok(elem), + _ => Ok(acc), + } + }) } - fn min(&self) -> anyhow::Result { - ensure!(!self.is_empty()); - Ok(self.iter().fold(f64::INFINITY, |acc, x| x.min(acc))) + fn min(&self) -> anyhow::Result<&f64> { + let first = self.first().ok_or(anyhow!("empty input"))?; + self.fold(Ok(first), |acc, elem| { + let acc = acc?; + match elem.partial_cmp(acc).ok_or(anyhow!("undefined order"))? { + cmp::Ordering::Less => Ok(elem), + _ => Ok(acc), + } + }) } } diff --git a/rust/fastsim-core/src/utils.rs b/rust/fastsim-core/src/utils.rs index 1e74ab04..6b695461 100644 --- a/rust/fastsim-core/src/utils.rs +++ b/rust/fastsim-core/src/utils.rs @@ -5,8 +5,6 @@ use directories::ProjectDirs; use itertools::Itertools; use lazy_static::lazy_static; use ndarray::*; -#[cfg(feature = "full")] -use ndarray_stats::QuantileExt; use regex::Regex; use std::collections::HashSet; diff --git a/rust/fastsim-core/src/vehicle_utils.rs b/rust/fastsim-core/src/vehicle_utils.rs index 3896b7c5..4dfa1d28 100644 --- a/rust/fastsim-core/src/vehicle_utils.rs +++ b/rust/fastsim-core/src/vehicle_utils.rs @@ -1240,7 +1240,7 @@ impl CostFunction for GetError<'_> { type Param = Array1; type Output = f64; - fn cost(&self, x: &Self::Param) -> Result { + fn cost(&self, x: &Self::Param) -> anyhow::Result { let mut veh: RustVehicle = self.vehicle.clone(); let cyc: RustCycle = self.cycle.clone(); let dyno_func_lb: Polynomial = self.dyno_func_lb.clone(); @@ -1978,7 +1978,8 @@ mod vehicle_utils_tests { let veh_records = { let file = File::open(vehicles_path); if let Ok(f) = file { - let data_result = read_records_from_file(f); + let data_result: anyhow::Result>> = + read_records_from_file(f); if let Ok(data) = data_result { data } else { diff --git a/rust/fastsim-py/Cargo.toml b/rust/fastsim-py/Cargo.toml index 4dc280f9..d8336d0b 100644 --- a/rust/fastsim-py/Cargo.toml +++ b/rust/fastsim-py/Cargo.toml @@ -25,5 +25,5 @@ crate-type = ["cdylib"] include = ["../../NOTICE"] [features] -# default = ["full"] +default = ["full"] full = ["fastsim-core/full", "dep:pyo3-log"] From 3e86c640e20c005264fc512ed07534c2045bcd5d Mon Sep 17 00:00:00 2001 From: Kyle Carow Date: Thu, 18 Jan 2024 12:02:37 -0700 Subject: [PATCH 04/30] add enabled_features function and resources feature --- rust/fastsim-core/Cargo.toml | 3 ++- .../fastsim-proc-macros/src/add_pyo3_api/mod.rs | 2 +- rust/fastsim-core/src/lib.rs | 13 +++++++++++++ rust/fastsim-core/src/resources.rs | 2 +- rust/fastsim-core/src/traits.rs | 2 +- rust/fastsim-py/src/lib.rs | 2 ++ 6 files changed, 20 insertions(+), 4 deletions(-) diff --git a/rust/fastsim-core/Cargo.toml b/rust/fastsim-core/Cargo.toml index 6de4075e..51d34798 100644 --- a/rust/fastsim-core/Cargo.toml +++ b/rust/fastsim-core/Cargo.toml @@ -59,7 +59,8 @@ full = [ "dep:argmin-math", "dep:curl", "dep:directories", - "dep:include_dir", "dep:polynomial", "dep:validator", + "resources", ] +resources = ["dep:include_dir"] diff --git a/rust/fastsim-core/fastsim-proc-macros/src/add_pyo3_api/mod.rs b/rust/fastsim-core/fastsim-proc-macros/src/add_pyo3_api/mod.rs index 26472417..ee679a59 100644 --- a/rust/fastsim-core/fastsim-proc-macros/src/add_pyo3_api/mod.rs +++ b/rust/fastsim-core/fastsim-proc-macros/src/add_pyo3_api/mod.rs @@ -213,7 +213,7 @@ pub fn add_pyo3_api(attr: TokenStream, item: TokenStream) -> TokenStream { /// /// * `filepath`: `str | pathlib.Path` - Filepath, relative to the top of the `resources` folder, from which to read the object /// - #[cfg(feature = "full")] + #[cfg(feature = "resources")] #[staticmethod] #[pyo3(name = "from_resource")] pub fn from_resource_py(filepath: &PyAny) -> anyhow::Result { diff --git a/rust/fastsim-core/src/lib.rs b/rust/fastsim-core/src/lib.rs index e6e6bd8d..6e47a2a1 100644 --- a/rust/fastsim-core/src/lib.rs +++ b/rust/fastsim-core/src/lib.rs @@ -53,3 +53,16 @@ pub mod vehicle_utils; pub use dev_proc_macros as proc_macros; #[cfg(not(feature = "dev-proc-macros"))] pub use fastsim_proc_macros as proc_macros; + +#[cfg_attr(feature = "pyo3", pyo3imports::pyfunction)] +pub fn enabled_features() -> Vec { + let mut enabled = Vec::new(); + + #[cfg(feature = "full")] + enabled.push("full".into()); + + #[cfg(feature = "resources")] + enabled.push("resources".into()); + + enabled +} diff --git a/rust/fastsim-core/src/resources.rs b/rust/fastsim-core/src/resources.rs index 9bdad25d..00fd59a6 100644 --- a/rust/fastsim-core/src/resources.rs +++ b/rust/fastsim-core/src/resources.rs @@ -1,4 +1,4 @@ -#![cfg(feature = "full")] +#![cfg(feature = "resources")] use include_dir::{include_dir, Dir}; pub const RESOURCES_DIR: Dir = include_dir!("$CARGO_MANIFEST_DIR/resources"); diff --git a/rust/fastsim-core/src/traits.rs b/rust/fastsim-core/src/traits.rs index c109415f..f08d31a6 100644 --- a/rust/fastsim-core/src/traits.rs +++ b/rust/fastsim-core/src/traits.rs @@ -15,7 +15,7 @@ pub trait SerdeAPI: Serialize + for<'a> Deserialize<'a> { /// # Arguments: /// /// * `filepath` - Filepath, relative to the top of the `resources` folder, from which to read the object - #[cfg(feature = "full")] + #[cfg(feature = "resources")] fn from_resource>(filepath: P) -> anyhow::Result { let filepath = filepath.as_ref(); let extension = filepath diff --git a/rust/fastsim-py/src/lib.rs b/rust/fastsim-py/src/lib.rs index 4fa07a8e..0ba8ed5b 100644 --- a/rust/fastsim-py/src/lib.rs +++ b/rust/fastsim-py/src/lib.rs @@ -52,5 +52,7 @@ fn fastsimrust(py: Python, m: &PyModule) -> PyResult<()> { m.add_function(wrap_pyfunction!(vehicle_utils::import_all_vehicles, m)?)?; m.add_function(wrap_pyfunction!(vehicle_utils::export_vehicle_to_file, m)?)?; + m.add_function(wrap_pyfunction!(enabled_features, m)?)?; + Ok(()) } From 29d9352fc030c65759b21befc726f5649ee8b74a Mon Sep 17 00:00:00 2001 From: Chad Baker Date: Mon, 29 Jan 2024 15:09:38 -0700 Subject: [PATCH 05/30] added doc for features --- rust/fastsim-core/src/lib.rs | 15 ++++++++++----- rust/fastsim-py/Cargo.toml | 3 ++- rust/fastsim-py/src/lib.rs | 7 +++++++ 3 files changed, 19 insertions(+), 6 deletions(-) diff --git a/rust/fastsim-core/src/lib.rs b/rust/fastsim-core/src/lib.rs index 6e47a2a1..33b28e97 100644 --- a/rust/fastsim-core/src/lib.rs +++ b/rust/fastsim-core/src/lib.rs @@ -6,10 +6,14 @@ //! FASTSim provides a simple way to compare powertrains and estimate the impact of technology //! improvements on light-, medium-, and heavy-duty vehicle efficiency, performance, cost, and battery life. //! More information here: - -//! # Installation -//! Currently, the Rust backend is only available through a Python API. - +//! +//! # Crate features +//! * **full** - When enabled (which is default), include additional capabilities that +//! require additional dependencies +//! * **resources** - When enabled (which is triggered by enabling full (thus default) +//! or enabling this feature directly), compiles commonly used resources (e.g. +//! standard drive cycles) for faster access. +//! //! # Python Examples //! ```python //! import fastsim @@ -55,8 +59,9 @@ pub use dev_proc_macros as proc_macros; pub use fastsim_proc_macros as proc_macros; #[cfg_attr(feature = "pyo3", pyo3imports::pyfunction)] +#[allow(clippy::vec_init_then_push)] pub fn enabled_features() -> Vec { - let mut enabled = Vec::new(); + let mut enabled = vec![]; #[cfg(feature = "full")] enabled.push("full".into()); diff --git a/rust/fastsim-py/Cargo.toml b/rust/fastsim-py/Cargo.toml index d8336d0b..eca5a71c 100644 --- a/rust/fastsim-py/Cargo.toml +++ b/rust/fastsim-py/Cargo.toml @@ -26,4 +26,5 @@ include = ["../../NOTICE"] [features] default = ["full"] -full = ["fastsim-core/full", "dep:pyo3-log"] +full = ["fastsim-core/full", "dep:pyo3-log", "resources"] +resources = ["fastsim-core/resources"] diff --git a/rust/fastsim-py/src/lib.rs b/rust/fastsim-py/src/lib.rs index 0ba8ed5b..90e3bf52 100644 --- a/rust/fastsim-py/src/lib.rs +++ b/rust/fastsim-py/src/lib.rs @@ -1,3 +1,10 @@ +//! # Crate features +//! * **full** - When enabled (which is default), include additional capabilities that +//! require additional dependencies +//! * **resources** - When enabled (which is triggered by enabling full (thus default) +//! or enabling this feature directly), compiles commonly used resources (e.g. +//! standard drive cycles) for faster access. + use fastsim_core::simdrivelabel::*; use fastsim_core::*; use pyo3imports::*; From 8dcfb44eb052e76dcdcefe644c59e9256c1891e5 Mon Sep 17 00:00:00 2001 From: Chad Baker Date: Mon, 29 Jan 2024 15:35:59 -0700 Subject: [PATCH 06/30] added `enabled_features` to pyi file --- python/fastsim/fastsimrust.pyi | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/python/fastsim/fastsimrust.pyi b/python/fastsim/fastsimrust.pyi index f00f624f..296c19cc 100644 --- a/python/fastsim/fastsimrust.pyi +++ b/python/fastsim/fastsimrust.pyi @@ -2,9 +2,6 @@ from __future__ import annotations from typing_extensions import Self from typing import Dict, List, Tuple, Optional, ByteString from abc import ABC -from fastsim.vehicle import VEHICLE_DIR -import yaml -from pathlib import Path class RustVec(ABC): def __repr__(self) -> str: @@ -1074,4 +1071,5 @@ def get_label_fe_phev( def get_label_fe_conv(veh: RustVehicle) -> LabelFe: ... +def enabled_features() -> List[str]: ... From 49421d2b65da46feba36c1e75e0d016d884c91b0 Mon Sep 17 00:00:00 2001 From: Chad Baker Date: Mon, 29 Jan 2024 16:02:19 -0700 Subject: [PATCH 07/30] when I build with `maturin develop --no-default-features` and test with `pytest -v python` I get: ImportError Traceback (most recent call last) Cell In[6], line 7 5 from fastsim import fastsimrust 6 if "full" in fastsimrust.enabled_features(): ----> 7 from fastsim.fastsimrust import abc_to_drag_coeffs ImportError: cannot import name abc_to_drag_coeffs from fastsim.fastsimrust (/Users/cbaker2/Documents/GitHub/fastsim/python/fastsim/fastsimrust.cpython-310-darwin.so) --- python/fastsim/tests/test_auxiliaries.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/python/fastsim/tests/test_auxiliaries.py b/python/fastsim/tests/test_auxiliaries.py index 9891f3d2..f00e2c14 100644 --- a/python/fastsim/tests/test_auxiliaries.py +++ b/python/fastsim/tests/test_auxiliaries.py @@ -2,7 +2,9 @@ from fastsim import auxiliaries from fastsim.vehicle import Vehicle from fastsim import utils -from fastsim.fastsimrust import abc_to_drag_coeffs +from fastsim import fastsimrust +if "full" in fastsimrust.enabled_features(): + from fastsim.fastsimrust import abc_to_drag_coeffs import numpy as np class test_auxiliaries(unittest.TestCase): From c716f8cc311ad800055f5d588e2738066a877318 Mon Sep 17 00:00:00 2001 From: Chad Baker Date: Mon, 29 Jan 2024 16:28:46 -0700 Subject: [PATCH 08/30] this did not solve the problem --- rust/fastsim-core/src/lib.rs | 31 +++++++++++++++++++++++++++---- 1 file changed, 27 insertions(+), 4 deletions(-) diff --git a/rust/fastsim-core/src/lib.rs b/rust/fastsim-core/src/lib.rs index 33b28e97..f10d1b48 100644 --- a/rust/fastsim-core/src/lib.rs +++ b/rust/fastsim-core/src/lib.rs @@ -58,16 +58,39 @@ pub use dev_proc_macros as proc_macros; #[cfg(not(feature = "dev-proc-macros"))] pub use fastsim_proc_macros as proc_macros; +use features_enabled::*; + #[cfg_attr(feature = "pyo3", pyo3imports::pyfunction)] #[allow(clippy::vec_init_then_push)] pub fn enabled_features() -> Vec { let mut enabled = vec![]; - #[cfg(feature = "full")] - enabled.push("full".into()); + if full_enabled() { + enabled.push("full".into()); + } - #[cfg(feature = "resources")] - enabled.push("resources".into()); + if resources_enabled() { + enabled.push("resources".into()); + } enabled } + +mod features_enabled { + #[cfg(feature = "full")] + pub(super) fn full_enabled() -> bool { + true + } + #[cfg(not(feature = "full"))] + pub(super) fn full_enabled() -> bool { + false + } + #[cfg(feature = "full")] + pub(super) fn resources_enabled() -> bool { + true + } + #[cfg(not(feature = "resources"))] + pub(super) fn resources_enabled() -> bool { + false + } +} From c7404a4f711e1fdbe0f2a56da2385b572d7930e6 Mon Sep 17 00:00:00 2001 From: Chad Baker Date: Mon, 29 Jan 2024 16:28:50 -0700 Subject: [PATCH 09/30] Revert "this did not solve the problem" This reverts commit c716f8cc311ad800055f5d588e2738066a877318. --- rust/fastsim-core/src/lib.rs | 31 ++++--------------------------- 1 file changed, 4 insertions(+), 27 deletions(-) diff --git a/rust/fastsim-core/src/lib.rs b/rust/fastsim-core/src/lib.rs index f10d1b48..33b28e97 100644 --- a/rust/fastsim-core/src/lib.rs +++ b/rust/fastsim-core/src/lib.rs @@ -58,39 +58,16 @@ pub use dev_proc_macros as proc_macros; #[cfg(not(feature = "dev-proc-macros"))] pub use fastsim_proc_macros as proc_macros; -use features_enabled::*; - #[cfg_attr(feature = "pyo3", pyo3imports::pyfunction)] #[allow(clippy::vec_init_then_push)] pub fn enabled_features() -> Vec { let mut enabled = vec![]; - if full_enabled() { - enabled.push("full".into()); - } + #[cfg(feature = "full")] + enabled.push("full".into()); - if resources_enabled() { - enabled.push("resources".into()); - } + #[cfg(feature = "resources")] + enabled.push("resources".into()); enabled } - -mod features_enabled { - #[cfg(feature = "full")] - pub(super) fn full_enabled() -> bool { - true - } - #[cfg(not(feature = "full"))] - pub(super) fn full_enabled() -> bool { - false - } - #[cfg(feature = "full")] - pub(super) fn resources_enabled() -> bool { - true - } - #[cfg(not(feature = "resources"))] - pub(super) fn resources_enabled() -> bool { - false - } -} From 4657451eaaebf64f3d6a173f03e2450b4114ba23 Mon Sep 17 00:00:00 2001 From: Kyle Carow Date: Tue, 30 Jan 2024 13:20:52 -0700 Subject: [PATCH 10/30] fix feature propogation --- pyproject.toml | 2 +- rust/fastsim-py/Cargo.toml | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 05555e9e..856cb31c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -27,7 +27,7 @@ dependencies = [ "pyyaml", "pytest", "setuptools<=65.6.3", # suppresses pkg_resources deprecation warning - "openpyxl>=3.1.2" + "openpyxl>=3.1.2", ] [project.urls] diff --git a/rust/fastsim-py/Cargo.toml b/rust/fastsim-py/Cargo.toml index eca5a71c..4a3fe51d 100644 --- a/rust/fastsim-py/Cargo.toml +++ b/rust/fastsim-py/Cargo.toml @@ -12,7 +12,9 @@ repository = "https://github.com/NREL/fastsim" [dependencies] pyo3-log = { workspace = true, optional = true } -fastsim-core = { path = "../fastsim-core", features = ["pyo3"], version = "~0" } +fastsim-core = { path = "../fastsim-core", features = [ + "pyo3", +], version = "~0", default-features = false } pyo3 = { workspace = true, features = ["extension-module", "anyhow"] } log = "0.4.17" From 4898928d6adec03bda1319397bf5c2703a78e1d2 Mon Sep 17 00:00:00 2001 From: Kyle Carow Date: Tue, 30 Jan 2024 13:42:40 -0700 Subject: [PATCH 11/30] carve validation into its own feature --- rust/fastsim-core/Cargo.toml | 3 +- rust/fastsim-core/src/lib.rs | 3 + rust/fastsim-core/src/vehicle.rs | 118 ++++++++++++++++--------------- rust/fastsim-py/Cargo.toml | 3 +- 4 files changed, 68 insertions(+), 59 deletions(-) diff --git a/rust/fastsim-core/Cargo.toml b/rust/fastsim-core/Cargo.toml index 51d34798..44b74788 100644 --- a/rust/fastsim-core/Cargo.toml +++ b/rust/fastsim-core/Cargo.toml @@ -60,7 +60,8 @@ full = [ "dep:curl", "dep:directories", "dep:polynomial", - "dep:validator", "resources", + "validation", ] resources = ["dep:include_dir"] +validation = ["dep:validator"] diff --git a/rust/fastsim-core/src/lib.rs b/rust/fastsim-core/src/lib.rs index 33b28e97..892b73d6 100644 --- a/rust/fastsim-core/src/lib.rs +++ b/rust/fastsim-core/src/lib.rs @@ -69,5 +69,8 @@ pub fn enabled_features() -> Vec { #[cfg(feature = "resources")] enabled.push("resources".into()); + #[cfg(feature = "validation")] + enabled.push("validation".into()); + enabled } diff --git a/rust/fastsim-core/src/vehicle.rs b/rust/fastsim-core/src/vehicle.rs index 2a8e525d..9b578e2b 100644 --- a/rust/fastsim-core/src/vehicle.rs +++ b/rust/fastsim-core/src/vehicle.rs @@ -7,9 +7,11 @@ use crate::proc_macros::{add_pyo3_api, doc_field, ApproxEq}; #[cfg(feature = "pyo3")] use crate::pyo3imports::*; +#[cfg(feature = "validation")] use lazy_static::lazy_static; +#[cfg(feature = "validation")] use regex::Regex; -#[cfg(feature = "full")] +#[cfg(feature = "validation")] use validator::Validate; // veh_pt_type options @@ -18,6 +20,7 @@ pub const HEV: &str = "HEV"; pub const PHEV: &str = "PHEV"; pub const BEV: &str = "BEV"; pub const VEH_PT_TYPES: [&str; 4] = [CONV, HEV, PHEV, BEV]; +#[cfg(feature = "validation")] lazy_static! { static ref VEH_PT_TYPE_OPTIONS_REGEX: Regex = Regex::new("Conv|HEV|PHEV|BEV").unwrap(); } @@ -29,6 +32,7 @@ pub const DIESEL: &str = "Diesel"; pub const H2FC: &str = "H2FC"; pub const HD_DIESEL: &str = "HD_Diesel"; pub const FC_EFF_TYPES: [&str; 5] = [SI, ATKINSON, DIESEL, H2FC, HD_DIESEL]; +#[cfg(feature = "validation")] lazy_static! { static ref FC_EFF_TYPE_OPTIONS_REGEX: Regex = Regex::new("SI|Atkinson|Diesel|H2FC|HD_Diesel").unwrap(); @@ -85,7 +89,7 @@ lazy_static! { Self::mock_vehicle() } )] -#[cfg_attr(feature = "full", derive(Validate))] +#[cfg_attr(feature = "validation", derive(Validate))] #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, ApproxEq)] /// Struct containing vehicle attributes /// # Python Examples @@ -117,7 +121,7 @@ pub struct RustVehicle { /// Vehicle powertrain type, one of \[[CONV](CONV), [HEV](HEV), [PHEV](PHEV), [BEV](BEV)\] #[serde(alias = "vehPtType")] #[cfg_attr( - feature = "full", + feature = "validation", validate(regex( path = "VEH_PT_TYPE_OPTIONS_REGEX", message = "must be one of [\"Conv\", \"HEV\", \"PHEV\", \"BEV\"]" @@ -127,15 +131,15 @@ pub struct RustVehicle { pub veh_pt_type: String, /// Aerodynamic drag coefficient #[serde(alias = "dragCoef")] - #[cfg_attr(feature = "full", validate(range(min = 0)))] + #[cfg_attr(feature = "validation", validate(range(min = 0)))] pub drag_coef: f64, /// Frontal area, $m^2$ #[serde(alias = "frontalAreaM2")] - #[cfg_attr(feature = "full", validate(range(min = 0)))] + #[cfg_attr(feature = "validation", validate(range(min = 0)))] pub frontal_area_m2: f64, /// Vehicle mass excluding cargo, passengers, and powertrain components, $kg$ #[serde(alias = "gliderKg")] - #[cfg_attr(feature = "full", validate(range(min = 0)))] + #[cfg_attr(feature = "validation", validate(range(min = 0)))] pub glider_kg: f64, /// Vehicle center of mass height, $m$ /// **NOTE:** positive for FWD, negative for RWD, AWD, 4WD @@ -143,43 +147,43 @@ pub struct RustVehicle { pub veh_cg_m: f64, /// Fraction of weight on the drive axle while stopped #[serde(alias = "driveAxleWeightFrac")] - #[cfg_attr(feature = "full", validate(range(min = 0, max = 1)))] + #[cfg_attr(feature = "validation", validate(range(min = 0, max = 1)))] pub drive_axle_weight_frac: f64, /// Wheelbase, $m$ #[serde(alias = "wheelBaseM")] - #[cfg_attr(feature = "full", validate(range(min = 0)))] + #[cfg_attr(feature = "validation", validate(range(min = 0)))] pub wheel_base_m: f64, /// Cargo mass including passengers, $kg$ #[serde(alias = "cargoKg")] - #[cfg_attr(feature = "full", validate(range(min = 0)))] + #[cfg_attr(feature = "validation", validate(range(min = 0)))] pub cargo_kg: f64, /// Total vehicle mass, overrides mass calculation, $kg$ #[serde(alias = "vehOverrideKg")] - #[cfg_attr(feature = "full", validate(range(min = 0)))] + #[cfg_attr(feature = "validation", validate(range(min = 0)))] pub veh_override_kg: Option, /// Component mass multiplier for vehicle mass calculation #[serde(alias = "compMassMultiplier")] - #[cfg_attr(feature = "full", validate(range(min = 0)))] + #[cfg_attr(feature = "validation", validate(range(min = 0)))] pub comp_mass_multiplier: f64, /// Fuel storage max power output, $kW$ #[serde(alias = "maxFuelStorKw")] - #[cfg_attr(feature = "full", validate(range(min = 0)))] + #[cfg_attr(feature = "validation", validate(range(min = 0)))] pub fs_max_kw: f64, /// Fuel storage time to peak power, $s$ #[serde(alias = "fuelStorSecsToPeakPwr")] - #[cfg_attr(feature = "full", validate(range(min = 0)))] + #[cfg_attr(feature = "validation", validate(range(min = 0)))] pub fs_secs_to_peak_pwr: f64, /// Fuel storage energy capacity, $kWh$ #[serde(alias = "fuelStorKwh")] - #[cfg_attr(feature = "full", validate(range(min = 0)))] + #[cfg_attr(feature = "validation", validate(range(min = 0)))] pub fs_kwh: f64, /// Fuel specific energy, $\frac{kWh}{kg}$ #[serde(alias = "fuelStorKwhPerKg")] - #[cfg_attr(feature = "full", validate(range(min = 0)))] + #[cfg_attr(feature = "validation", validate(range(min = 0)))] pub fs_kwh_per_kg: f64, /// Fuel converter peak continuous power, $kW$ #[serde(alias = "maxFuelConvKw")] - #[cfg_attr(feature = "full", validate(range(min = 0)))] + #[cfg_attr(feature = "validation", validate(range(min = 0)))] pub fc_max_kw: f64, /// Fuel converter output power percentage map, x values of [fc_eff_map](RustVehicle::fc_eff_map) #[serde(alias = "fcPwrOutPerc")] @@ -191,7 +195,7 @@ pub struct RustVehicle { /// Used for calculating [fc_eff_map](RustVehicle::fc_eff_map), and other calculations if H2FC #[serde(alias = "fcEffType")] #[cfg_attr( - feature = "full", + feature = "validation", validate(regex( path = "FC_EFF_TYPE_OPTIONS_REGEX", message = "must be one of [\"SI\", \"Atkinson\", \"Diesel\", \"H2FC\", \"HD_Diesel\"]" @@ -200,27 +204,27 @@ pub struct RustVehicle { pub fc_eff_type: String, /// Fuel converter time to peak power, $s$ #[serde(alias = "fuelConvSecsToPeakPwr")] - #[cfg_attr(feature = "full", validate(range(min = 0)))] + #[cfg_attr(feature = "validation", validate(range(min = 0)))] pub fc_sec_to_peak_pwr: f64, /// Fuel converter base mass, $kg$ #[serde(alias = "fuelConvBaseKg")] - #[cfg_attr(feature = "full", validate(range(min = 0)))] + #[cfg_attr(feature = "validation", validate(range(min = 0)))] pub fc_base_kg: f64, /// Fuel converter specific power (power-to-weight ratio), $\frac{kW}{kg}$ #[serde(alias = "fuelConvKwPerKg")] - #[cfg_attr(feature = "full", validate(range(min = 0)))] + #[cfg_attr(feature = "validation", validate(range(min = 0)))] pub fc_kw_per_kg: f64, /// Minimum time fuel converter must be on before shutoff (for HEV, PHEV) #[serde(alias = "minFcTimeOn")] - #[cfg_attr(feature = "full", validate(range(min = 0)))] + #[cfg_attr(feature = "validation", validate(range(min = 0)))] pub min_fc_time_on: f64, /// Fuel converter idle power, $kW$ #[serde(alias = "idleFcKw")] - #[cfg_attr(feature = "full", validate(range(min = 0)))] + #[cfg_attr(feature = "validation", validate(range(min = 0)))] pub idle_fc_kw: f64, /// Peak continuous electric motor power, $kW$ #[serde(alias = "mcMaxElecInKw")] - #[cfg_attr(feature = "full", validate(range(min = 0)))] + #[cfg_attr(feature = "validation", validate(range(min = 0)))] pub mc_max_kw: f64, /// Electric motor output power percentage map, x values of [mc_eff_map](RustVehicle::mc_eff_map) #[serde(alias = "mcPwrOutPerc")] @@ -230,35 +234,35 @@ pub struct RustVehicle { pub mc_eff_map: Array1, /// Electric motor time to peak power, $s$ #[serde(alias = "motorSecsToPeakPwr")] - #[cfg_attr(feature = "full", validate(range(min = 0)))] + #[cfg_attr(feature = "validation", validate(range(min = 0)))] pub mc_sec_to_peak_pwr: f64, /// Motor power electronics mass per power output, $\frac{kg}{kW}$ #[serde(alias = "mcPeKgPerKw")] - #[cfg_attr(feature = "full", validate(range(min = 0)))] + #[cfg_attr(feature = "validation", validate(range(min = 0)))] pub mc_pe_kg_per_kw: f64, /// Motor power electronics base mass, $kg$ #[serde(alias = "mcPeBaseKg")] - #[cfg_attr(feature = "full", validate(range(min = 0)))] + #[cfg_attr(feature = "validation", validate(range(min = 0)))] pub mc_pe_base_kg: f64, /// Traction battery maximum power output, $kW$ #[serde(alias = "maxEssKw")] - #[cfg_attr(feature = "full", validate(range(min = 0)))] + #[cfg_attr(feature = "validation", validate(range(min = 0)))] pub ess_max_kw: f64, /// Traction battery energy capacity, $kWh$ #[serde(alias = "maxEssKwh")] - #[cfg_attr(feature = "full", validate(range(min = 0)))] + #[cfg_attr(feature = "validation", validate(range(min = 0)))] pub ess_max_kwh: f64, /// Traction battery mass per energy, $\frac{kg}{kWh}$ #[serde(alias = "essKgPerKwh")] - #[cfg_attr(feature = "full", validate(range(min = 0)))] + #[cfg_attr(feature = "validation", validate(range(min = 0)))] pub ess_kg_per_kwh: f64, /// Traction battery base mass, $kg$ #[serde(alias = "essBaseKg")] - #[cfg_attr(feature = "full", validate(range(min = 0)))] + #[cfg_attr(feature = "validation", validate(range(min = 0)))] pub ess_base_kg: f64, /// Traction battery round-trip efficiency #[serde(alias = "essRoundTripEff")] - #[cfg_attr(feature = "full", validate(range(min = 0, max = 1)))] + #[cfg_attr(feature = "validation", validate(range(min = 0, max = 1)))] pub ess_round_trip_eff: f64, /// Traction battery cycle life coefficient A, see [reference](https://web.archive.org/web/20090529194442/http://www.ocean.udel.edu/cms/wkempton/Kempton-V2G-pdfFiles/PDF%20format/Duvall-V2G-batteries-June05.pdf) #[serde(alias = "essLifeCoefA")] @@ -268,63 +272,63 @@ pub struct RustVehicle { pub ess_life_coef_b: f64, /// Traction battery minimum state of charge #[serde(alias = "minSoc")] - #[cfg_attr(feature = "full", validate(range(min = 0, max = 1)))] + #[cfg_attr(feature = "validation", validate(range(min = 0, max = 1)))] pub min_soc: f64, /// Traction battery maximum state of charge #[serde(alias = "maxSoc")] - #[cfg_attr(feature = "full", validate(range(min = 0, max = 1)))] + #[cfg_attr(feature = "validation", validate(range(min = 0, max = 1)))] pub max_soc: f64, /// ESS discharge effort toward max FC efficiency #[serde(alias = "essDischgToFcMaxEffPerc")] - #[cfg_attr(feature = "full", validate(range(min = 0, max = 1)))] + #[cfg_attr(feature = "validation", validate(range(min = 0, max = 1)))] pub ess_dischg_to_fc_max_eff_perc: f64, /// ESS charge effort toward max FC efficiency #[serde(alias = "essChgToFcMaxEffPerc")] - #[cfg_attr(feature = "full", validate(range(min = 0, max = 1)))] + #[cfg_attr(feature = "validation", validate(range(min = 0, max = 1)))] pub ess_chg_to_fc_max_eff_perc: f64, /// Mass moment of inertia per wheel, $kg \cdot m^2$ #[serde(alias = "wheelInertiaKgM2")] - #[cfg_attr(feature = "full", validate(range(min = 0)))] + #[cfg_attr(feature = "validation", validate(range(min = 0)))] pub wheel_inertia_kg_m2: f64, /// Number of wheels #[serde(alias = "numWheels")] - #[cfg_attr(feature = "full", validate(range(min = 0)))] + #[cfg_attr(feature = "validation", validate(range(min = 0)))] pub num_wheels: f64, // TODO: Shouldn't this just be a unsigned integer? u8 would work fine. /// Rolling resistance coefficient #[serde(alias = "wheelRrCoef")] - #[cfg_attr(feature = "full", validate(range(min = 0)))] + #[cfg_attr(feature = "validation", validate(range(min = 0)))] pub wheel_rr_coef: f64, /// Wheel radius, $m$ #[serde(alias = "wheelRadiusM")] - #[cfg_attr(feature = "full", validate(range(min = 0)))] + #[cfg_attr(feature = "validation", validate(range(min = 0)))] pub wheel_radius_m: f64, /// Wheel coefficient of friction #[serde(alias = "wheelCoefOfFric")] - #[cfg_attr(feature = "full", validate(range(min = 0)))] + #[cfg_attr(feature = "validation", validate(range(min = 0)))] pub wheel_coef_of_fric: f64, /// Speed where the battery reserved for accelerating is zero #[serde(alias = "maxAccelBufferMph")] - #[cfg_attr(feature = "full", validate(range(min = 0)))] + #[cfg_attr(feature = "validation", validate(range(min = 0)))] pub max_accel_buffer_mph: f64, /// Percent of usable battery energy reserved to help accelerate #[serde(alias = "maxAccelBufferPercOfUseableSoc")] - #[cfg_attr(feature = "full", validate(range(min = 0, max = 1)))] + #[cfg_attr(feature = "validation", validate(range(min = 0, max = 1)))] pub max_accel_buffer_perc_of_useable_soc: f64, /// Percent SOC buffer for high accessory loads during cycles with long idle time #[serde(alias = "percHighAccBuf")] - #[cfg_attr(feature = "full", validate(range(min = 0)))] + #[cfg_attr(feature = "validation", validate(range(min = 0)))] pub perc_high_acc_buf: f64, /// Speed at which the fuel converter must turn on, $mph$ #[serde(alias = "mphFcOn")] - #[cfg_attr(feature = "full", validate(range(min = 0)))] + #[cfg_attr(feature = "validation", validate(range(min = 0)))] pub mph_fc_on: f64, /// Power demand above which to require fuel converter on, $kW$ #[serde(alias = "kwDemandFcOn")] - #[cfg_attr(feature = "full", validate(range(min = 0)))] + #[cfg_attr(feature = "validation", validate(range(min = 0)))] pub kw_demand_fc_on: f64, /// Maximum brake regeneration efficiency #[serde(alias = "maxRegen")] - #[cfg_attr(feature = "full", validate(range(min = 0, max = 1)))] + #[cfg_attr(feature = "validation", validate(range(min = 0, max = 1)))] pub max_regen: f64, /// Stop/start micro-HEV flag pub stop_start: bool, @@ -333,27 +337,27 @@ pub struct RustVehicle { pub force_aux_on_fc: bool, /// Alternator efficiency #[serde(alias = "altEff")] - #[cfg_attr(feature = "full", validate(range(min = 0, max = 1)))] + #[cfg_attr(feature = "validation", validate(range(min = 0, max = 1)))] pub alt_eff: f64, /// Charger efficiency #[serde(alias = "chgEff")] - #[cfg_attr(feature = "full", validate(range(min = 0, max = 1)))] + #[cfg_attr(feature = "validation", validate(range(min = 0, max = 1)))] pub chg_eff: f64, /// Auxiliary load power, $kW$ #[serde(alias = "auxKw")] - #[cfg_attr(feature = "full", validate(range(min = 0)))] + #[cfg_attr(feature = "validation", validate(range(min = 0)))] pub aux_kw: f64, /// Transmission mass, $kg$ #[serde(alias = "transKg")] - #[cfg_attr(feature = "full", validate(range(min = 0)))] + #[cfg_attr(feature = "validation", validate(range(min = 0)))] pub trans_kg: f64, /// Transmission efficiency #[serde(alias = "transEff")] - #[cfg_attr(feature = "full", validate(range(min = 0, max = 1)))] + #[cfg_attr(feature = "validation", validate(range(min = 0, max = 1)))] pub trans_eff: f64, /// Maximum acceptable ratio of change in ESS energy to expended fuel energy (used in hybrid SOC balancing), $\frac{\Delta E_{ESS}}{\Delta E_{fuel}}$ #[serde(alias = "essToFuelOkError")] - #[cfg_attr(feature = "full", validate(range(min = 0)))] + #[cfg_attr(feature = "validation", validate(range(min = 0)))] pub ess_to_fuel_ok_error: f64, #[doc(hidden)] #[doc_field(skip_doc)] @@ -535,11 +539,11 @@ pub struct RustVehicle { pub val_msrp: f64, /// Fuel converter efficiency peak override, scales entire curve #[serde(skip)] - #[cfg_attr(feature = "full", validate(range(min = 0, max = 1)))] + #[cfg_attr(feature = "validation", validate(range(min = 0, max = 1)))] pub fc_peak_eff_override: Option, /// Motor efficiency peak override, scales entire curve #[serde(skip)] - #[cfg_attr(feature = "full", validate(range(min = 0, max = 1)))] + #[cfg_attr(feature = "validation", validate(range(min = 0, max = 1)))] pub mc_peak_eff_override: Option, #[serde(skip)] #[doc(hidden)] @@ -683,7 +687,7 @@ impl RustVehicle { /// - `max_trac_mps2` pub fn set_derived(&mut self) -> anyhow::Result<()> { // Vehicle input validation - #[cfg(feature = "full")] + #[cfg(feature = "validation")] self.validate()?; if self.scenario_name != "Template Vehicle for setting up data types" { @@ -1068,7 +1072,7 @@ impl SerdeAPI for RustVehicle { #[cfg(test)] mod tests { use super::*; - #[cfg(feature = "full")] + #[cfg(feature = "validation")] use validator::ValidationErrors; #[test] @@ -1087,7 +1091,7 @@ mod tests { // the produced error for the offending field names } - #[cfg(feature = "full")] + #[cfg(feature = "validation")] #[test] fn test_input_validation() { // set up vehicle input parameters diff --git a/rust/fastsim-py/Cargo.toml b/rust/fastsim-py/Cargo.toml index 4a3fe51d..620d4c7f 100644 --- a/rust/fastsim-py/Cargo.toml +++ b/rust/fastsim-py/Cargo.toml @@ -28,5 +28,6 @@ include = ["../../NOTICE"] [features] default = ["full"] -full = ["fastsim-core/full", "dep:pyo3-log", "resources"] +full = ["fastsim-core/full", "dep:pyo3-log", "resources", "validation"] resources = ["fastsim-core/resources"] +validation = ["fastsim-core/validation"] From fc234dd8d51d01b1771747da97335a25973bf988 Mon Sep 17 00:00:00 2001 From: Kyle Carow Date: Wed, 31 Jan 2024 11:13:21 -0700 Subject: [PATCH 12/30] skip tests when required features inactive --- python/fastsim/demos/demo.py | 13 +++++++------ python/fastsim/demos/test_demos.py | 8 +++++++- python/fastsim/demos/vehicle_import_demo.py | 9 ++++++++- python/fastsim/tests/test_auxiliaries.py | 5 +++-- python/fastsim/tests/test_simdrivelabel.py | 16 ++++++++-------- 5 files changed, 33 insertions(+), 18 deletions(-) diff --git a/python/fastsim/demos/demo.py b/python/fastsim/demos/demo.py index 4581db2e..db9959c3 100644 --- a/python/fastsim/demos/demo.py +++ b/python/fastsim/demos/demo.py @@ -31,7 +31,6 @@ import sys import os from pathlib import Path -from fastsim.fastsimrust import abc_to_drag_coeffs import numpy as np import time import pandas as pd @@ -891,10 +890,12 @@ def get_sim_drive_vec( # values. # %% -test_veh = fsim.vehicle.Vehicle.from_vehdb(5, to_rust=True).to_rust() -(drag_coef, wheel_rr_coef) = abc_to_drag_coeffs(test_veh, 25.91, 0.1943, 0.01796, simdrive_optimize=True) +if "full" in fsim.fastsimrust.enabled_features(): + from fastsim.fastsimrust import abc_to_drag_coeffs + test_veh = fsim.vehicle.Vehicle.from_vehdb(5, to_rust=True).to_rust() + (drag_coef, wheel_rr_coef) = abc_to_drag_coeffs(test_veh, 25.91, 0.1943, 0.01796, simdrive_optimize=True) + + print(f'Drag Coefficient: {drag_coef:.3g}') + print(f'Wheel Rolling Resistance Coefficient: {wheel_rr_coef:.3g}') -# %% -print(f'Drag Coefficient: {drag_coef:.3g}') -print(f'Wheel Rolling Resistance Coefficient: {wheel_rr_coef:.3g}') # %% diff --git a/python/fastsim/demos/test_demos.py b/python/fastsim/demos/test_demos.py index 0ecc1882..f14fde1d 100644 --- a/python/fastsim/demos/test_demos.py +++ b/python/fastsim/demos/test_demos.py @@ -1,6 +1,7 @@ import subprocess import os from pathlib import Path +from fastsim import fastsimrust import pytest @@ -9,8 +10,13 @@ def demo_paths(): demo_paths.remove(Path(__file__).resolve()) return demo_paths +REQUIRED_FEATURES = {"vehicle_import_demo": "full"} + @pytest.mark.parametrize("demo_path", demo_paths(), ids=[dp.name for dp in demo_paths()]) def test_demo(demo_path: Path): + if demo_path.stem in REQUIRED_FEATURES.keys() and REQUIRED_FEATURES[demo_path.stem] not in fastsimrust.enabled_features(): + pytest.skip(f'requires "{REQUIRED_FEATURES[demo_path.stem]}" feature') + os.environ['SHOW_PLOTS'] = "false" rslt = subprocess.run( ["python", demo_path], @@ -19,4 +25,4 @@ def test_demo(demo_path: Path): text=True ) - assert rslt.returncode == 0, rslt.stderr \ No newline at end of file + assert rslt.returncode == 0, rslt.stderr diff --git a/python/fastsim/demos/vehicle_import_demo.py b/python/fastsim/demos/vehicle_import_demo.py index 26789d88..673c7d60 100644 --- a/python/fastsim/demos/vehicle_import_demo.py +++ b/python/fastsim/demos/vehicle_import_demo.py @@ -2,6 +2,13 @@ Vehicle Import Demonstration This module demonstrates the vehicle import API """ +# %% +from fastsim import fastsimrust + +REQUIRED_FEATURE = "full" +if __name__ == "__main__" and REQUIRED_FEATURE not in fastsimrust.enabled_features(): + raise NotImplementedError(f'Feature "{REQUIRED_FEATURE}" is required to run this demo') + # %% # Preamble: Basic imports import os, pathlib @@ -83,4 +90,4 @@ vehs = fsr.import_all_vehicles(int(year), make, model, other_inputs) if SHOW_PLOTS: for v in vehs: - print(f"Imported {v.scenario_name}") \ No newline at end of file + print(f"Imported {v.scenario_name}") diff --git a/python/fastsim/tests/test_auxiliaries.py b/python/fastsim/tests/test_auxiliaries.py index f00e2c14..449afb89 100644 --- a/python/fastsim/tests/test_auxiliaries.py +++ b/python/fastsim/tests/test_auxiliaries.py @@ -3,9 +3,8 @@ from fastsim.vehicle import Vehicle from fastsim import utils from fastsim import fastsimrust -if "full" in fastsimrust.enabled_features(): - from fastsim.fastsimrust import abc_to_drag_coeffs import numpy as np +import pytest class test_auxiliaries(unittest.TestCase): def setUp(self): @@ -49,7 +48,9 @@ def test_drag_coeffs_to_abc(self): self.assertAlmostEqual(0, b_lbf__mph) self.assertAlmostEqual(0.020817239083920212, c_lbf__mph2) + @pytest.mark.skipif("full" not in fastsimrust.enabled_features(), reason='requires "full" feature') def test_abc_to_drag_coeffs_rust_port(self): + from fastsim.fastsimrust import abc_to_drag_coeffs with np.errstate(divide='ignore'): veh = Vehicle.from_vehdb(5).to_rust() a = 25.91 diff --git a/python/fastsim/tests/test_simdrivelabel.py b/python/fastsim/tests/test_simdrivelabel.py index 3132c7f1..763ccd2c 100644 --- a/python/fastsim/tests/test_simdrivelabel.py +++ b/python/fastsim/tests/test_simdrivelabel.py @@ -1,13 +1,13 @@ import unittest -import numpy as np - -from fastsim import fastsimrust as fsr +import pytest +from fastsim import fastsimrust +@pytest.mark.skipif("full" not in fastsimrust.enabled_features(), reason='requires "full" feature') class TestSimDriveLabel(unittest.TestCase): def test_get_label_fe_conv(self): - veh = fsr.RustVehicle.mock_vehicle() - label_fe, _ = fsr.get_label_fe(veh, False, False) # Unpack the tuple + veh = fastsimrust.RustVehicle.mock_vehicle() + label_fe, _ = fastsimrust.get_label_fe(veh, False, False) # Unpack the tuple # Because the full test is already implemented in Rust, we # don't need a comprehensive check here. self.assertEqual(label_fe.lab_udds_mpgge, 32.47503766676829) @@ -16,10 +16,10 @@ def test_get_label_fe_conv(self): def test_get_label_fe_phev(self): # Set up the required parameters and objects needed for testing get_label_fe_phev - veh = fsr.RustVehicle.mock_vehicle() - label_fe, _ = fsr.get_label_fe(veh, False, False) + veh = fastsimrust.RustVehicle.mock_vehicle() + label_fe, _ = fastsimrust.get_label_fe(veh, False, False) self.assertEqual(label_fe.adj_udds_mpgge, 25.246151811422468) if __name__ == '__main__': - unittest.main() \ No newline at end of file + unittest.main() From cc6a2c8a919993f7d337139d61f1a8d6e6fb7150 Mon Sep 17 00:00:00 2001 From: Kyle Carow Date: Thu, 1 Feb 2024 13:42:35 -0700 Subject: [PATCH 13/30] first pass at carving out vehicle-import feature --- .../fastsim-cli/src/bin/vehicle-import-cli.rs | 2 +- rust/fastsim-core/Cargo.toml | 3 +- rust/fastsim-core/src/lib.rs | 2 +- rust/fastsim-core/src/vehicle_import.rs | 1788 ++++++++++++++++ rust/fastsim-core/src/vehicle_utils.rs | 1804 +---------------- rust/fastsim-py/Cargo.toml | 9 +- rust/fastsim-py/src/lib.rs | 15 +- 7 files changed, 1809 insertions(+), 1814 deletions(-) create mode 100644 rust/fastsim-core/src/vehicle_import.rs diff --git a/rust/fastsim-cli/src/bin/vehicle-import-cli.rs b/rust/fastsim-cli/src/bin/vehicle-import-cli.rs index 9d168233..d06b5601 100644 --- a/rust/fastsim-cli/src/bin/vehicle-import-cli.rs +++ b/rust/fastsim-cli/src/bin/vehicle-import-cli.rs @@ -1,7 +1,7 @@ use anyhow::{self, Context}; use clap::Parser; use fastsim_core::utils::create_project_subdir; -use fastsim_core::vehicle_utils::{get_default_cache_url, import_and_save_all_vehicles_from_file}; +use fastsim_core::vehicle_import::{get_default_cache_url, import_and_save_all_vehicles_from_file}; use std::fs; use std::path::{Path, PathBuf}; diff --git a/rust/fastsim-core/Cargo.toml b/rust/fastsim-core/Cargo.toml index 44b74788..1a6424c3 100644 --- a/rust/fastsim-core/Cargo.toml +++ b/rust/fastsim-core/Cargo.toml @@ -57,11 +57,12 @@ pyo3 = ["dep:pyo3"] full = [ "dep:argmin", "dep:argmin-math", - "dep:curl", "dep:directories", "dep:polynomial", "resources", "validation", + "vehicle-import", ] resources = ["dep:include_dir"] validation = ["dep:validator"] +vehicle-import = ["dep:curl", "full"] diff --git a/rust/fastsim-core/src/lib.rs b/rust/fastsim-core/src/lib.rs index 892b73d6..c78a5012 100644 --- a/rust/fastsim-core/src/lib.rs +++ b/rust/fastsim-core/src/lib.rs @@ -40,7 +40,6 @@ pub mod air; pub mod cycle; pub mod imports; pub mod params; -#[cfg(feature = "pyo3")] pub mod pyo3imports; pub mod simdrive; pub use simdrive::simdrive_impl; @@ -50,6 +49,7 @@ pub mod thermal; pub mod traits; pub mod utils; pub mod vehicle; +pub mod vehicle_import; pub mod vehicle_thermal; pub mod vehicle_utils; diff --git a/rust/fastsim-core/src/vehicle_import.rs b/rust/fastsim-core/src/vehicle_import.rs new file mode 100644 index 00000000..13a73b0e --- /dev/null +++ b/rust/fastsim-core/src/vehicle_import.rs @@ -0,0 +1,1788 @@ +#![cfg(feature = "vehicle-import")] + +use crate::params::*; +use crate::proc_macros::add_pyo3_api; +use curl::easy::Easy; +use serde::de::DeserializeOwned; +use std::collections::HashMap; +use std::collections::HashSet; +use std::io::prelude::Write; +use std::io::Read; +use std::num::ParseIntError; +use std::path::PathBuf; +use zip::ZipArchive; + +use crate::imports::*; +#[cfg(feature = "pyo3")] +use crate::pyo3imports::*; +use crate::vehicle::RustVehicle; +use crate::vehicle_utils::abc_to_drag_coeffs; + +#[derive(Debug, Serialize, Deserialize, PartialEq)] +/// Struct containing list of makes for a year from fueleconomy.gov +struct VehicleMakesFE { + #[serde(rename = "menuItem")] + /// List of vehicle makes + makes: Vec, +} + +#[derive(Debug, Serialize, Deserialize, PartialEq)] +/// Struct containing make information for a year fueleconomy.gov +struct MakeFE { + #[serde(rename = "text")] + /// Transmission of vehicle + make_name: String, +} + +#[derive(Debug, Serialize, Deserialize, PartialEq)] +/// Struct containing list of models for a year and make from fueleconomy.gov +struct VehicleModelsFE { + #[serde(rename = "menuItem")] + /// List of vehicle models + models: Vec, +} + +#[derive(Debug, Serialize, Deserialize, PartialEq)] +/// Struct containing model information for a year and make from fueleconomy.gov +struct ModelFE { + #[serde(rename = "text")] + /// Transmission of vehicle + model_name: String, +} + +#[derive(Debug, Serialize, Deserialize, PartialEq)] +/// Struct containing list of transmission options for vehicle from fueleconomy.gov +struct VehicleOptionsFE { + #[serde(rename = "menuItem")] + /// List of vehicle options (transmission and id) + options: Vec, +} + +#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)] +#[add_pyo3_api] +/// Struct containing transmission and id of a vehicle option from fueleconomy.gov +pub struct OptionFE { + #[serde(rename = "text")] + /// Transmission of vehicle + pub transmission: String, + #[serde(rename = "value")] + /// ID of vehicle on fueleconomy.gov + pub id: String, +} + +impl SerdeAPI for OptionFE {} + +#[derive(Default, Debug, Serialize, Deserialize, PartialEq, Clone)] +#[add_pyo3_api] +/// Struct containing vehicle data from fueleconomy.gov +pub struct VehicleDataFE { + /// Vehicle ID + pub id: i32, + + /// Model year + pub year: u32, + /// Vehicle make + pub make: String, + /// Vehicle model + pub model: String, + + /// EPA vehicle size class + #[serde(rename = "VClass")] + pub veh_class: String, + + /// Drive axle type (FWD, RWD, AWD, 4WD) + pub drive: String, + /// Type of alternative fuel vehicle (Hybrid, Plug-in Hybrid, EV) + #[serde(default, rename = "atvType")] + pub alt_veh_type: String, + + /// Combined vehicle fuel type (fuel 1 and fuel 2) + #[serde(rename = "fuelType")] + pub fuel_type: String, + /// Fuel type 1 + #[serde(rename = "fuelType1")] + pub fuel1: String, + /// Fuel type 2 + #[serde(default, rename = "fuelType2")] + pub fuel2: String, + + /// Description of engine + #[serde(default)] + pub eng_dscr: String, + /// Number of engine cylinders + #[serde(default)] + pub cylinders: String, + /// Engine displacement in liters + #[serde(default)] + pub displ: String, + /// transmission + #[serde(rename = "trany")] + pub transmission: String, + + /// "S" if vehicle has supercharger + #[serde(default, rename = "sCharger")] + pub super_charger: String, + /// "T" if vehicle has turbocharger + #[serde(default, rename = "tCharger")] + pub turbo_charger: String, + + /// Stop-start technology + #[serde(rename = "startStop")] + pub start_stop: String, + + /// Vehicle operates on blend of gasoline and electricity + #[serde(rename = "phevBlended")] + pub phev_blended: bool, + /// EPA composite gasoline-electricity city MPGe + #[serde(rename = "phevCity")] + pub phev_city_mpge: i32, + /// EPA composite gasoline-electricity combined MPGe + #[serde(rename = "phevComb")] + pub phev_comb_mpge: i32, + /// EPA composite gasoline-electricity highway MPGe + #[serde(rename = "phevHwy")] + pub phev_hwy_mpge: i32, + + /// Electric motor power (kW), not very consistent as an input + #[serde(default, rename = "evMotor")] + pub ev_motor_kw: String, + /// EV range + #[serde(rename = "range")] + pub range_ev: i32, + + /// City MPG for fuel 1 + #[serde(rename = "city08U")] + pub city_mpg_fuel1: f64, + /// City MPG for fuel 2 + #[serde(rename = "cityA08U")] + pub city_mpg_fuel2: f64, + /// Unadjusted unroaded city MPG for fuel 1 + #[serde(rename = "UCity")] + pub unadj_city_mpg_fuel1: f64, + /// Unadjusted unroaded city MPG for fuel 2 + #[serde(rename = "UCityA")] + pub unadj_city_mpg_fuel2: f64, + /// City electricity consumption in kWh/100 mi + #[serde(rename = "cityE")] + pub city_kwh_per_100mi: f64, + + /// Adjusted unrounded highway MPG for fuel 1 + #[serde(rename = "highway08U")] + pub highway_mpg_fuel1: f64, + /// Adjusted unrounded highway MPG for fuel 2 + #[serde(rename = "highwayA08U")] + pub highway_mpg_fuel2: f64, + /// Unadjusted unrounded highway MPG for fuel 1 + #[serde(default, rename = "UHighway")] + pub unadj_highway_mpg_fuel1: f64, + /// Unadjusted unrounded highway MPG for fuel 2 + #[serde(default, rename = "UHighwayA")] + pub unadj_highway_mpg_fuel2: f64, + /// Highway electricity consumption in kWh/100 mi + #[serde(default, rename = "highwayE")] + pub highway_kwh_per_100mi: f64, + + /// Combined MPG for fuel 1 + #[serde(rename = "comb08U")] + pub comb_mpg_fuel1: f64, + /// Combined MPG for fuel 2 + #[serde(rename = "combA08U")] + pub comb_mpg_fuel2: f64, + /// Combined electricity consumption in kWh/100 mi + #[serde(default, rename = "combE")] + pub comb_kwh_per_100mi: f64, + + /// List of emissions tests + #[serde(rename = "emissionsList")] + pub emissions_list: EmissionsListFE, +} + +impl SerdeAPI for VehicleDataFE {} + +#[derive(Default, Debug, Serialize, Deserialize, PartialEq, Clone)] +#[serde(rename_all = "camelCase")] +#[add_pyo3_api] +/// Struct containing list of emissions tests from fueleconomy.gov +pub struct EmissionsListFE { + /// + pub emissions_info: Vec, +} + +impl SerdeAPI for EmissionsListFE {} + +#[derive(Default, Debug, Serialize, Deserialize, PartialEq, Clone)] +#[serde(rename_all = "camelCase")] +#[add_pyo3_api] +/// Struct containing emissions test results from fueleconomy.gov +pub struct EmissionsInfoFE { + /// Engine family id / EPA test group + pub efid: String, + /// EPA smog rating + pub score: f64, + /// SmartWay score + pub smartway_score: i32, + /// Vehicle emission standard code + pub standard: String, + /// Vehicle emission standard + pub std_text: String, +} + +impl SerdeAPI for EmissionsInfoFE {} + +#[derive(Default, PartialEq, Clone, Debug, Deserialize, Serialize)] +#[add_pyo3_api] +/// Struct containing vehicle data from EPA database +pub struct VehicleDataEPA { + /// Model year + #[serde(rename = "Model Year")] + pub year: u32, + /// Vehicle make + #[serde(rename = "Represented Test Veh Make")] + pub make: String, + /// Vehicle model + #[serde(rename = "Represented Test Veh Model")] + pub model: String, + /// Vehicle test group + #[serde(rename = "Actual Tested Testgroup")] + pub test_id: String, + /// Engine displacement + #[serde(rename = "Test Veh Displacement (L)")] + pub displ: f64, + /// Engine power in hp + #[serde(rename = "Rated Horsepower")] + pub eng_pwr_hp: u32, + /// Number of cylinders + #[serde(rename = "# of Cylinders and Rotors")] + pub cylinders: String, + /// Transmission type code + #[serde(rename = "Tested Transmission Type Code")] + pub transmission_code: String, + /// Transmission type + #[serde(rename = "Tested Transmission Type")] + pub transmission_type: String, + /// Number of gears + #[serde(rename = "# of Gears")] + pub gears: u32, + /// Drive system code + #[serde(rename = "Drive System Code")] + pub drive_code: String, + /// Drive system type + #[serde(rename = "Drive System Description")] + pub drive: String, + /// Test weight in lbs + #[serde(rename = "Equivalent Test Weight (lbs.)")] + pub test_weight_lbs: f64, + /// Fuel type used for EPA test + #[serde(rename = "Test Fuel Type Description")] + pub test_fuel_type: String, + /// Dyno coefficient a in lbf + #[serde(rename = "Target Coef A (lbf)")] + pub a_lbf: f64, + /// Dyno coefficient b in lbf/mph + #[serde(rename = "Target Coef B (lbf/mph)")] + pub b_lbf_per_mph: f64, + /// Dyno coefficient c in lbf/mph^2 + #[serde(rename = "Target Coef C (lbf/mph**2)")] + pub c_lbf_per_mph2: f64, +} + +impl SerdeAPI for VehicleDataEPA {} + +#[cfg_attr(feature = "pyo3", pyfunction)] +/// Gets options from fueleconomy.gov for the given vehicle year, make, and model +/// +/// Arguments: +/// ---------- +/// year: Vehicle year +/// make: Vehicle make +/// model: Vehicle model (must match model on fueleconomy.gov) +/// +/// Returns: +/// -------- +/// Vec: Data for the available options for that vehicle year/make/model from fueleconomy.gov +pub fn get_options_for_year_make_model( + year: &str, + make: &str, + model: &str, + cache_url: Option, + data_dir: Option, +) -> anyhow::Result> { + // prep the cache for year + let y: u32 = year.trim().parse()?; + let ys: HashSet = { + let mut h = HashSet::new(); + h.insert(y); + h + }; + let ddpath = if let Some(dd) = data_dir { + PathBuf::from(dd) + } else { + create_project_subdir("fe_label_data")? + }; + let cache_url = if let Some(url) = &cache_url { + url.clone() + } else { + get_default_cache_url() + }; + let has_data = populate_cache_for_given_years_if_needed(ddpath.as_path(), &ys, &cache_url)?; + if !has_data { + return Err(anyhow!( + "Unable to load or download cache data from {cache_url}" + )); + } + let emissions_data = load_emissions_data_for_given_years(ddpath.as_path(), &ys)?; + let fegov_data_by_year = + load_fegov_data_for_given_years(ddpath.as_path(), &emissions_data, &ys)?; + if let Some(fegov_db) = fegov_data_by_year.get(&y) { + let mut hits: Vec = Vec::new(); + for item in fegov_db.iter() { + if item.make == make && item.model == model { + hits.push(item.clone()); + } + } + Ok(hits) + } else { + Ok(vec![]) + } +} + +fn derive_transmission_specs(fegov: &VehicleDataFE) -> (u32, String) { + let num_gears_fe_gov: u32; + let transmission_fe_gov: String; + // Based on reference: https://www.fueleconomy.gov/feg/findacarhelp.shtml#engine + if fegov.transmission.contains("Manual") { + transmission_fe_gov = String::from('M'); + num_gears_fe_gov = fegov.transmission.as_str()[fegov.transmission.find("-spd").unwrap() - 1 + ..fegov.transmission.find("-spd").unwrap()] + .parse() + .unwrap(); + } else if fegov.transmission.contains("variable gear ratios") { + transmission_fe_gov = String::from("CVT"); + num_gears_fe_gov = 1; + } else if fegov.transmission.contains("AV-S") { + transmission_fe_gov = String::from("SCV"); + num_gears_fe_gov = fegov.transmission.as_str() + [fegov.transmission.find('S').unwrap() + 1..fegov.transmission.find(')').unwrap()] + .parse() + .unwrap(); + } else if fegov.transmission.contains("AM-S") { + transmission_fe_gov = String::from("AMS"); + num_gears_fe_gov = fegov.transmission.as_str() + [fegov.transmission.find('S').unwrap() + 1..fegov.transmission.find(')').unwrap()] + .parse() + .unwrap(); + } else if fegov.transmission.contains('S') { + transmission_fe_gov = String::from("SA"); + num_gears_fe_gov = fegov.transmission.as_str() + [fegov.transmission.find('S').unwrap() + 1..fegov.transmission.find(')').unwrap()] + .parse() + .unwrap(); + } else if fegov.transmission.contains("-spd") { + transmission_fe_gov = String::from('A'); + num_gears_fe_gov = fegov.transmission.as_str()[fegov.transmission.find("-spd").unwrap() - 1 + ..fegov.transmission.find("-spd").unwrap()] + .parse() + .unwrap(); + } else { + transmission_fe_gov = String::from('A'); + num_gears_fe_gov = { + let res: Result = fegov.transmission.as_str() + [fegov.transmission.find("(A").unwrap() + 2..fegov.transmission.find(')').unwrap()] + .parse(); + if let Ok(n) = res { + n + } else { + 1 + } + } + } + (num_gears_fe_gov, transmission_fe_gov) +} + +/// Match EPA Test Data with FuelEconomy.gov data and return best match +/// The matching algorithm tries to find the best match in the EPA Test data for the given FuelEconomy.gov data +/// The algorithm works as follows: +/// - only EPA Test Data matching the year and make of the FuelEconomy.gov data will be considered +/// - we try to match on both the efid/test id and also the model name +/// - next, for each match, we calculate a score based on matching various powertrain aspects based on: +/// - transmission type +/// - number of gears in the transmission +/// - drive type (all-wheel drive / 4-wheel drive, etc.) +/// - (for non-EVs) +/// - engine displacement +/// - number of cylinders +/// RETURNS: the EPA Test data with the best match on make and/or efid/test id. When multiple vehicles match +/// the same make name/ efid/test-id, we return the one with the highest score +fn match_epatest_with_fegov_v2( + fegov: &VehicleDataFE, + epatest_data: &[VehicleDataEPA], +) -> Option { + let fe_model_upper: String = fegov.model.to_uppercase().replace("4WD", "AWD"); + let fe_model_words: Vec<&str> = fe_model_upper.split_ascii_whitespace().collect(); + let num_fe_model_words = fe_model_words.len(); + let fegov_disp = fegov.displ.parse::().unwrap_or_default(); + let efid = if !fegov.emissions_list.emissions_info.is_empty() { + fegov.emissions_list.emissions_info[0].efid.clone() + } else { + String::new() + }; + let fegov_drive = { + let mut s = String::new(); + if !fegov.drive.is_empty() { + let maybe_char = fegov.drive.chars().next(); + if let Some(c) = maybe_char { + s.push(c); + } + s + } else { + s + } + }; + let (num_gears_fe_gov, transmission_fe_gov) = derive_transmission_specs(fegov); + let epa_candidates = { + let mut xs: Vec<(f64, f64, VehicleDataEPA)> = Vec::new(); + for x in epatest_data { + if x.year == fegov.year && x.make.eq_ignore_ascii_case(&fegov.make) { + let mut score = 0.0; + + // Things we Don't Want to Match + if x.test_fuel_type.contains("Cold CO") { + continue; + } + let matching_test_id = if !x.test_id.is_empty() && !efid.is_empty() { + x.test_id.ends_with(&efid[1..efid.len()]) + } else { + false + }; + // ID match + let name_match = if matching_test_id || x.model.eq_ignore_ascii_case(&fegov.model) { + 1.0 + } else { + let epa_model_upper = x.model.to_uppercase().replace("4WD", "AWD"); + let epa_model_words: Vec<&str> = + epa_model_upper.split_ascii_whitespace().collect(); + let num_epa_model_words = epa_model_words.len(); + let mut match_count = 0; + for word in &epa_model_words { + match_count += fe_model_words.contains(word) as i64; + } + let match_frac: f64 = (match_count as f64 * match_count as f64) + / (num_epa_model_words as f64 * num_fe_model_words as f64); + match_frac + }; + if name_match == 0.0 { + continue; + } + // By PT Type + if fegov.alt_veh_type == *"EV" { + if x.cylinders.is_empty() && x.displ.round() == 0.0 { + score += 1.0; + } + } else { + let epa_disp = (x.displ * 10.0).round() / 10.0; + if x.cylinders == fegov.cylinders && epa_disp == fegov_disp { + score += 1.0; + } + } + // Drive Code + let drive_code = if x.model.contains("4WD") + || x.model.contains("AWD") + || x.drive.contains("4-Wheel Drive") + { + String::from('A') + } else { + x.drive.clone() + }; + if drive_code == fegov_drive { + score += 1.0; + } + // Transmission Type and Num Gears + if x.transmission_code == transmission_fe_gov { + score += 0.5; + } else if transmission_fe_gov.starts_with(x.transmission_type.as_str()) { + score += 0.25; + } + if x.gears == num_gears_fe_gov { + score += 0.5; + } + xs.push((name_match, score, x.clone())); + } + } + xs + }; + if epa_candidates.is_empty() { + None + } else { + let mut largest_id_match_value = 0.0; + let mut largest_score_value = 0.0; + let mut best_idx: usize = 0; + for (idx, item) in epa_candidates.iter().enumerate() { + if item.0 > largest_id_match_value + || (item.0 == largest_id_match_value && item.1 > largest_score_value) + { + largest_id_match_value = item.0; + largest_score_value = item.1; + best_idx = idx; + } + } + if largest_id_match_value == 0.0 { + None + } else { + Some(epa_candidates[best_idx].2.clone()) + } + } +} + +/// Match EPA Test Data with FuelEconomy.gov data and return best match +#[allow(dead_code)] +fn match_epatest_with_fegov( + fegov: &VehicleDataFE, + epatest_data: &[VehicleDataEPA], +) -> Option { + if fegov.emissions_list.emissions_info.is_empty() { + return None; + } + // Keep track of best match to fueleconomy.gov model name for all vehicles and vehicles with matching efid/test id + let mut veh_list_overall: HashMap> = HashMap::new(); + let mut veh_list_efid: HashMap> = HashMap::new(); + let mut best_match_percent_efid: f64 = 0.0; + let mut best_match_model_efid: String = String::new(); + let mut best_match_percent_overall: f64 = 0.0; + let mut best_match_model_overall: String = String::new(); + + let fe_model_upper: String = fegov.model.to_uppercase().replace("4WD", "AWD"); + let fe_model_words: Vec<&str> = fe_model_upper.split(' ').collect(); + let num_fe_model_words = fe_model_words.len(); + let efid: &String = &fegov.emissions_list.emissions_info[0].efid; + + for veh_epa in epatest_data { + // Find matches between EPA vehicle model name and fe.gov vehicle model name + let mut match_count: i64 = 0; + let epa_model_upper = veh_epa.model.to_uppercase().replace("4WD", "AWD"); + let epa_model_words: Vec<&str> = epa_model_upper.split(' ').collect(); + let num_epa_model_words = epa_model_words.len(); + for word in &epa_model_words { + match_count += fe_model_words.contains(word) as i64; + } + // Calculate composite match percentage + let match_percent: f64 = (match_count as f64 * match_count as f64) + / (num_epa_model_words as f64 * num_fe_model_words as f64); + + // Update overall hashmap with new entry + if veh_list_overall.contains_key(&veh_epa.model) { + if let Some(x) = veh_list_overall.get_mut(&veh_epa.model) { + (*x).push(veh_epa.clone()); + } + } else { + veh_list_overall.insert(veh_epa.model.clone(), vec![veh_epa.clone()]); + + if match_percent > best_match_percent_overall { + best_match_percent_overall = match_percent; + best_match_model_overall = veh_epa.model.clone(); + } + } + + // Update efid hashmap if fe.gov efid matches EPA test id + // (for some reason first character in id is almost always different) + if veh_epa.test_id.ends_with(&efid[1..efid.len()]) { + if veh_list_efid.contains_key(&veh_epa.model) { + if let Some(x) = veh_list_efid.get_mut(&veh_epa.model) { + (*x).push(veh_epa.clone()); + } + } else { + veh_list_efid.insert(veh_epa.model.clone(), vec![veh_epa.clone()]); + if match_percent > best_match_percent_efid { + best_match_percent_efid = match_percent; + best_match_model_efid = veh_epa.model.clone(); + } + } + } + } + + // Get EPA vehicle model that is best match to fe.gov vehicle + let veh_list: Vec = if best_match_model_efid == best_match_model_overall { + let x = veh_list_efid.get(&best_match_model_efid); + x?; + x.unwrap().to_vec() + } else { + veh_list_overall + .get(&best_match_model_overall) + .unwrap() + .to_vec() + }; + + // Get number of gears and convert fe.gov transmission description to EPA transmission description + let num_gears_fe_gov: u32; + let transmission_fe_gov: String; + // Based on reference: https://www.fueleconomy.gov/feg/findacarhelp.shtml#engine + if fegov.transmission.contains("Manual") { + transmission_fe_gov = String::from('M'); + num_gears_fe_gov = fegov.transmission.as_str()[fegov.transmission.find("-spd").unwrap() - 1 + ..fegov.transmission.find("-spd").unwrap()] + .parse() + .unwrap(); + } else if fegov.transmission.contains("variable gear ratios") { + transmission_fe_gov = String::from("CVT"); + num_gears_fe_gov = 1; + } else if fegov.transmission.contains("AV-S") { + transmission_fe_gov = String::from("SCV"); + num_gears_fe_gov = fegov.transmission.as_str() + [fegov.transmission.find('S').unwrap() + 1..fegov.transmission.find(')').unwrap()] + .parse() + .unwrap(); + } else if fegov.transmission.contains("AM-S") { + transmission_fe_gov = String::from("AMS"); + num_gears_fe_gov = fegov.transmission.as_str() + [fegov.transmission.find('S').unwrap() + 1..fegov.transmission.find(')').unwrap()] + .parse() + .unwrap(); + } else if fegov.transmission.contains('S') { + transmission_fe_gov = String::from("SA"); + num_gears_fe_gov = fegov.transmission.as_str() + [fegov.transmission.find('S').unwrap() + 1..fegov.transmission.find(')').unwrap()] + .parse() + .unwrap(); + } else if fegov.transmission.contains("-spd") { + transmission_fe_gov = String::from('A'); + num_gears_fe_gov = fegov.transmission.as_str()[fegov.transmission.find("-spd").unwrap() - 1 + ..fegov.transmission.find("-spd").unwrap()] + .parse() + .unwrap(); + } else { + transmission_fe_gov = String::from('A'); + num_gears_fe_gov = { + let res: Result = fegov.transmission.as_str() + [fegov.transmission.find("(A").unwrap() + 2..fegov.transmission.find(')').unwrap()] + .parse(); + if let Ok(n) = res { + n + } else { + 1 + } + } + } + + // Find EPA vehicle entry that matches fe.gov vehicle data + // If same vehicle model has multiple configurations, get most common configuration + let mut most_common_veh: VehicleDataEPA = VehicleDataEPA::default(); + let mut most_common_count: i32 = 0; + let mut current_veh: VehicleDataEPA = VehicleDataEPA::default(); + let mut current_count: i32 = 0; + for mut veh_epa in veh_list { + if veh_epa.model.contains("4WD") + || veh_epa.model.contains("AWD") + || veh_epa.drive.contains("4-Wheel Drive") + { + veh_epa.drive_code = String::from('A'); + veh_epa.drive = String::from("All Wheel Drive"); + } + if !veh_epa.test_fuel_type.contains("Cold CO") + && (veh_epa.transmission_code == transmission_fe_gov + || fegov + .transmission + .starts_with(veh_epa.transmission_type.as_str())) + && veh_epa.gears == num_gears_fe_gov + && veh_epa.drive_code == fegov.drive[0..1] + && ((fegov.alt_veh_type == *"EV" + && veh_epa.displ.round() == 0.0 + && veh_epa.cylinders == String::new()) + || ((veh_epa.displ * 10.0).round() / 10.0 + == (fegov.displ.parse::().unwrap_or_default()) + && veh_epa.cylinders == fegov.cylinders)) + { + if veh_epa == current_veh { + current_count += 1; + } else { + if current_count > most_common_count { + most_common_veh = current_veh.clone(); + most_common_count = current_count; + } + current_veh = veh_epa.clone(); + current_count = 1; + } + } + } + if current_count > most_common_count { + Some(current_veh) + } else { + Some(most_common_veh) + } +} + +#[derive(Default, PartialEq, Clone, Debug, Deserialize, Serialize)] +#[add_pyo3_api( + #[new] + pub fn __new__( + vehicle_width_in: f64, + vehicle_height_in: f64, + fuel_tank_gal: f64, + ess_max_kwh: f64, + mc_max_kw: f64, + ess_max_kw: f64, + fc_max_kw: Option + ) -> Self { + OtherVehicleInputs { + vehicle_width_in, + vehicle_height_in, + fuel_tank_gal, + ess_max_kwh, + mc_max_kw, + ess_max_kw, + fc_max_kw + } + } +)] +pub struct OtherVehicleInputs { + pub vehicle_width_in: f64, + pub vehicle_height_in: f64, + pub fuel_tank_gal: f64, + pub ess_max_kwh: f64, + pub mc_max_kw: f64, + pub ess_max_kw: f64, + pub fc_max_kw: Option, +} + +impl SerdeAPI for OtherVehicleInputs {} + +#[cfg(feature = "full")] +#[cfg_attr(feature = "pyo3", pyfunction)] +/// Creates RustVehicle for the given vehicle using data from fueleconomy.gov and EPA databases +/// The created RustVehicle is also written as a yaml file +/// +/// Arguments: +/// ---------- +/// vehicle_id: i32, Identifier at fueleconomy.gov for the desired vehicle +/// year: u32, the year of the vehicle +/// other_inputs: Other vehicle inputs required to create the vehicle +/// +/// Returns: +/// -------- +/// veh: RustVehicle for specificed vehicle +pub fn vehicle_import_by_id_and_year( + vehicle_id: i32, + year: u32, + other_inputs: &OtherVehicleInputs, + cache_url: Option, + data_dir: Option, +) -> anyhow::Result { + let mut maybe_veh: Option = None; + let data_dir_path = if let Some(data_dir) = data_dir { + PathBuf::from(data_dir) + } else { + create_project_subdir("fe_label_data")? + }; + let data_dir_path = data_dir_path.as_path(); + let model_years = { + let mut h: HashSet = HashSet::new(); + h.insert(year); + h + }; + let cache_url = if let Some(cache_url) = &cache_url { + cache_url.clone() + } else { + get_default_cache_url() + }; + let has_data = + populate_cache_for_given_years_if_needed(data_dir_path, &model_years, &cache_url)?; + if !has_data { + return Err(anyhow!( + "Unable to load or download cache data from {cache_url}" + )); + } + let emissions_data = load_emissions_data_for_given_years(data_dir_path, &model_years)?; + let fegov_data_by_year = + load_fegov_data_for_given_years(data_dir_path, &emissions_data, &model_years)?; + let epatest_db = read_epa_test_data_for_given_years(data_dir_path, &model_years)?; + if let Some(fe_gov_data) = fegov_data_by_year.get(&year) { + if let Some(epa_data) = epatest_db.get(&year) { + let fe_gov_data = { + let mut maybe_data = None; + for item in fe_gov_data { + if item.id == vehicle_id { + maybe_data = Some(item.clone()); + break; + } + } + maybe_data + }; + if let Some(fe_gov_data) = fe_gov_data { + if let Some(epa_data) = match_epatest_with_fegov_v2(&fe_gov_data, epa_data) { + maybe_veh = try_make_single_vehicle(&fe_gov_data, &epa_data, other_inputs); + } + } + } + } + match maybe_veh { + Some(veh) => Ok(veh), + None => Err(anyhow!("Unable to find/match vehicle in DB")), + } +} + +pub fn get_default_cache_url() -> String { + String::from("https://github.com/NREL/vehicle-data/raw/main/") +} + +fn get_fuel_economy_gov_data_for_input_record( + vir: &VehicleInputRecord, + fegov_data: &[VehicleDataFE], +) -> Vec { + let mut output: Vec = Vec::new(); + let vir_make = String::from(vir.make.to_lowercase().trim()); + let vir_model = String::from(vir.model.to_lowercase().trim()); + for fedat in fegov_data { + let fe_make = String::from(fedat.make.to_lowercase().trim()); + let fe_model = String::from(fedat.model.to_lowercase().trim()); + if fedat.year == vir.year && fe_make.eq(&vir_make) && fe_model.eq(&vir_model) { + output.push(fedat.clone()); + } + } + output +} + +#[cfg(feature = "full")] +/// Try to make a single vehicle using the provided data sets. +fn try_make_single_vehicle( + fe_gov_data: &VehicleDataFE, + epa_data: &VehicleDataEPA, + other_inputs: &OtherVehicleInputs, +) -> Option { + if epa_data == &VehicleDataEPA::default() { + return None; + } + let veh_pt_type: &str = match fe_gov_data.alt_veh_type.as_str() { + "Hybrid" => crate::vehicle::HEV, + "Plug-in Hybrid" => crate::vehicle::PHEV, + "EV" => crate::vehicle::BEV, + _ => crate::vehicle::CONV, + }; + + let fs_max_kw: f64; + let fc_max_kw: f64; + let fc_eff_type: String; + let fc_eff_map: Array1; + let mc_max_kw: f64; + let min_soc: f64; + let max_soc: f64; + let ess_dischg_to_fc_max_eff_perc: f64; + let mph_fc_on: f64; + let kw_demand_fc_on: f64; + let aux_kw: f64; + let trans_eff: f64; + let val_range_miles: f64; + let ess_max_kw: f64; + let ess_max_kwh: f64; + let fs_kwh: f64; + + let ref_veh: RustVehicle = Default::default(); + + if veh_pt_type == crate::vehicle::CONV { + fs_max_kw = 2000.0; + fs_kwh = other_inputs.fuel_tank_gal * ref_veh.props.kwh_per_gge; + fc_max_kw = epa_data.eng_pwr_hp as f64 / HP_PER_KW; + fc_eff_type = String::from(crate::vehicle::SI); + fc_eff_map = Array::from_vec(vec![ + 0.1, 0.12, 0.16, 0.22, 0.28, 0.33, 0.35, 0.36, 0.35, 0.34, 0.32, 0.3, + ]); + mc_max_kw = 0.0; + min_soc = 0.0; + max_soc = 1.0; + ess_dischg_to_fc_max_eff_perc = 0.0; + mph_fc_on = 55.0; + kw_demand_fc_on = 100.0; + aux_kw = 0.7; + trans_eff = 0.92; + val_range_miles = 0.0; + ess_max_kw = 0.0; + ess_max_kwh = 0.0; + } else if veh_pt_type == crate::vehicle::HEV { + fs_max_kw = 2000.0; + fs_kwh = other_inputs.fuel_tank_gal * ref_veh.props.kwh_per_gge; + fc_max_kw = other_inputs + .fc_max_kw + .unwrap_or(epa_data.eng_pwr_hp as f64 / HP_PER_KW); + fc_eff_type = String::from(crate::vehicle::ATKINSON); + fc_eff_map = Array::from_vec(vec![ + 0.10, 0.12, 0.28, 0.35, 0.375, 0.39, 0.40, 0.40, 0.38, 0.37, 0.36, 0.35, + ]); + min_soc = 0.0; + max_soc = 1.0; + ess_dischg_to_fc_max_eff_perc = 0.0; + mph_fc_on = 1.0; + kw_demand_fc_on = 100.0; + aux_kw = 0.5; + trans_eff = 0.95; + val_range_miles = 0.0; + ess_max_kw = other_inputs.ess_max_kw; + ess_max_kwh = other_inputs.ess_max_kwh; + mc_max_kw = other_inputs.mc_max_kw; + } else if veh_pt_type == crate::vehicle::PHEV { + fs_max_kw = 2000.0; + fs_kwh = other_inputs.fuel_tank_gal * ref_veh.props.kwh_per_gge; + fc_max_kw = other_inputs + .fc_max_kw + .unwrap_or(epa_data.eng_pwr_hp as f64 / HP_PER_KW); + fc_eff_type = String::from(crate::vehicle::ATKINSON); + fc_eff_map = Array::from_vec(vec![ + 0.10, 0.12, 0.28, 0.35, 0.375, 0.39, 0.40, 0.40, 0.38, 0.37, 0.36, 0.35, + ]); + min_soc = 0.0; + max_soc = 1.0; + ess_dischg_to_fc_max_eff_perc = 1.0; + mph_fc_on = 85.0; + kw_demand_fc_on = 120.0; + aux_kw = 0.3; + trans_eff = 0.98; + val_range_miles = 0.0; + ess_max_kw = other_inputs.ess_max_kw; + ess_max_kwh = other_inputs.ess_max_kwh; + mc_max_kw = other_inputs.mc_max_kw; + } else if veh_pt_type == crate::vehicle::BEV { + fs_max_kw = 0.0; + fs_kwh = 0.0; + fc_max_kw = 0.0; + fc_eff_type = String::from(crate::vehicle::SI); + fc_eff_map = Array::from_vec(vec![ + 0.10, 0.12, 0.16, 0.22, 0.28, 0.33, 0.35, 0.36, 0.35, 0.34, 0.32, 0.30, + ]); + mc_max_kw = epa_data.eng_pwr_hp as f64 / HP_PER_KW; + min_soc = 0.0; + max_soc = 1.0; + ess_max_kw = 1.05 * mc_max_kw; + ess_max_kwh = other_inputs.ess_max_kwh; + mph_fc_on = 1.0; + kw_demand_fc_on = 100.0; + aux_kw = 0.25; + trans_eff = 0.98; + val_range_miles = fe_gov_data.range_ev as f64; + ess_dischg_to_fc_max_eff_perc = 0.0; + } else { + println!("Unhandled vehicle powertrain type: {veh_pt_type}"); + return None; + } + + // TODO: fix glider_kg calculation + // https://github.com/NREL/fastsim/pull/30#issuecomment-1841413126 + // + // let glider_kg = (epa_data.test_weight_lbs / LBS_PER_KG) + // - ref_veh.cargo_kg + // - ref_veh.trans_kg + // - ref_veh.comp_mass_multiplier + // * ((fs_max_kw / ref_veh.fs_kwh_per_kg) + // + (ref_veh.fc_base_kg + fc_max_kw / ref_veh.fc_kw_per_kg) + // + (ref_veh.mc_pe_base_kg + mc_max_kw * ref_veh.mc_pe_kg_per_kw) + // + (ref_veh.ess_base_kg + ess_max_kwh * ref_veh.ess_kg_per_kwh)); + let mut veh = RustVehicle { + veh_override_kg: Some(epa_data.test_weight_lbs / LBS_PER_KG), + veh_cg_m: match fe_gov_data.drive.as_str() { + "Front-Wheel Drive" => 0.53, + _ => -0.53, + }, + // glider_kg, + scenario_name: format!( + "{} {} {}", + fe_gov_data.year, fe_gov_data.make, fe_gov_data.model + ), + max_roadway_chg_kw: Default::default(), + selection: 0, + veh_year: fe_gov_data.year, + veh_pt_type: String::from(veh_pt_type), + drag_coef: 0.0, // overridden + frontal_area_m2: 0.85 * (other_inputs.vehicle_width_in * other_inputs.vehicle_height_in) + / (IN_PER_M * IN_PER_M), + fs_kwh, + idle_fc_kw: 0.0, + mc_eff_map: Array1::::zeros(LARGE_BASELINE_EFF.len()), + wheel_rr_coef: 0.0, // overridden + stop_start: fe_gov_data.start_stop == "Y", + force_aux_on_fc: false, + val_udds_mpgge: fe_gov_data.city_mpg_fuel1, + val_hwy_mpgge: fe_gov_data.highway_mpg_fuel1, + val_comb_mpgge: fe_gov_data.comb_mpg_fuel1, + fc_peak_eff_override: None, + mc_peak_eff_override: Some(0.95), + fs_max_kw, + fc_max_kw, + fc_eff_type, + fc_eff_map, + mc_max_kw, + min_soc, + max_soc, + ess_dischg_to_fc_max_eff_perc, + mph_fc_on, + kw_demand_fc_on, + aux_kw, + trans_eff, + val_range_miles, + ess_max_kwh, + ess_max_kw, + ..Default::default() + }; + veh.set_derived().unwrap(); + + abc_to_drag_coeffs( + &mut veh, + epa_data.a_lbf, + epa_data.b_lbf_per_mph, + epa_data.c_lbf_per_mph2, + Some(false), + None, + None, + Some(true), + Some(false), + ); + Some(veh) +} + +#[cfg(feature = "full")] +fn try_import_vehicles( + vir: &VehicleInputRecord, + fegov_data: &[VehicleDataFE], + epatest_data: &[VehicleDataEPA], +) -> Vec { + let other_inputs = vir_to_other_inputs(vir); + // TODO: Aaron wanted custom scenario name option + let mut outputs: Vec = Vec::new(); + let fegov_hits: Vec = + get_fuel_economy_gov_data_for_input_record(vir, fegov_data); + for hit in fegov_hits { + if let Some(epa_data) = match_epatest_with_fegov_v2(&hit, epatest_data) { + if let Some(v) = try_make_single_vehicle(&hit, &epa_data, &other_inputs) { + let mut v = v.clone(); + if hit.alt_veh_type == *"EV" { + v.scenario_name = format!("{} (EV)", v.scenario_name); + } else { + let alt_type = if hit.alt_veh_type.is_empty() { + String::from("") + } else { + format!("{}, ", hit.alt_veh_type) + }; + v.scenario_name = format!( + "{} ( {} {} cylinders, {} L, {} )", + v.scenario_name, alt_type, hit.cylinders, hit.displ, hit.transmission + ); + } + outputs.push(v); + } else { + println!( + "Unable to create vehicle for {}-{}-{}", + vir.year, vir.make, vir.model + ); + } + } else { + println!( + "Did not match any EPA data for {}-{}-{}...", + vir.year, vir.make, vir.model + ); + } + } + outputs +} +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct VehicleInputRecord { + pub make: String, + pub model: String, + pub year: u32, + pub output_file_name: String, + pub vehicle_width_in: f64, + pub vehicle_height_in: f64, + pub fuel_tank_gal: f64, + pub ess_max_kwh: f64, + pub mc_max_kw: f64, + pub ess_max_kw: f64, + pub fc_max_kw: Option, +} + +/// Transltate a VehicleInputRecord to OtherVehicleInputs +fn vir_to_other_inputs(vir: &VehicleInputRecord) -> OtherVehicleInputs { + OtherVehicleInputs { + vehicle_width_in: vir.vehicle_width_in, + vehicle_height_in: vir.vehicle_height_in, + fuel_tank_gal: vir.fuel_tank_gal, + ess_max_kwh: vir.ess_max_kwh, + mc_max_kw: vir.mc_max_kw, + ess_max_kw: vir.ess_max_kw, + fc_max_kw: vir.fc_max_kw, + } +} + +fn read_vehicle_input_records_from_file( + filepath: &Path, +) -> anyhow::Result> { + let f = File::open(filepath)?; + read_records_from_file(f) +} + +fn read_records_from_file( + rdr: impl std::io::Read + std::io::Seek, +) -> anyhow::Result> { + let mut output: Vec = Vec::new(); + let mut reader = csv::Reader::from_reader(rdr); + for result in reader.deserialize() { + let record: T = result?; + output.push(record); + } + Ok(output) +} + +fn read_fuelecon_gov_emissions_to_hashmap( + rdr: impl std::io::Read + std::io::Seek, +) -> HashMap> { + let mut output: HashMap> = HashMap::new(); + let mut reader = csv::Reader::from_reader(rdr); + for result in reader.deserialize() { + if result.is_ok() { + let ok_result: Option> = result.ok(); + if let Some(item) = ok_result { + if let Some(id_str) = item.get("id") { + if let Ok(id) = str::parse::(id_str) { + output.entry(id).or_default(); + if let Some(ers) = output.get_mut(&id) { + let emiss = EmissionsInfoFE { + efid: item.get("efid").unwrap().clone(), + score: item.get("score").unwrap().parse().unwrap(), + smartway_score: item.get("smartwayScore").unwrap().parse().unwrap(), + standard: item.get("standard").unwrap().clone(), + std_text: item.get("stdText").unwrap().clone(), + }; + ers.push(emiss); + } + } + } + } + } + } + output +} + +fn read_fuelecon_gov_data_from_file( + rdr: impl std::io::Read + std::io::Seek, + emissions: &HashMap>, +) -> anyhow::Result> { + let mut output: Vec = Vec::new(); + let mut reader = csv::Reader::from_reader(rdr); + for result in reader.deserialize() { + let item: HashMap = result?; + let id: u32 = item.get("id").unwrap().parse::().unwrap(); + let emissions_list: EmissionsListFE = if emissions.contains_key(&id) { + EmissionsListFE { + emissions_info: emissions.get(&id).unwrap().to_vec(), + } + } else { + EmissionsListFE::default() + }; + let vd = VehicleDataFE { + id: item.get("id").unwrap().trim().parse().unwrap(), + + year: item.get("year").unwrap().parse::().unwrap(), + make: item.get("make").unwrap().clone(), + model: item.get("model").unwrap().clone(), + + veh_class: item.get("VClass").unwrap().clone(), + + drive: item.get("drive").unwrap().clone(), + alt_veh_type: item.get("atvType").unwrap().clone(), + + fuel_type: item.get("fuelType").unwrap().clone(), + fuel1: item.get("fuelType1").unwrap().clone(), + fuel2: item.get("fuelType2").unwrap().clone(), + + eng_dscr: item.get("eng_dscr").unwrap().clone(), + cylinders: item.get("cylinders").unwrap().clone(), + displ: item.get("displ").unwrap().clone(), + transmission: item.get("trany").unwrap().clone(), + + super_charger: item.get("sCharger").unwrap().clone(), + turbo_charger: item.get("tCharger").unwrap().clone(), + + start_stop: item.get("startStop").unwrap().clone(), + + phev_blended: item + .get("phevBlended") + .unwrap() + .trim() + .to_lowercase() + .parse::() + .unwrap(), + phev_city_mpge: item.get("phevCity").unwrap().parse::().unwrap(), + phev_comb_mpge: item.get("phevComb").unwrap().parse::().unwrap(), + phev_hwy_mpge: item.get("phevHwy").unwrap().parse::().unwrap(), + + ev_motor_kw: item.get("evMotor").unwrap().clone(), + range_ev: item.get("range").unwrap().parse::().unwrap(), + + city_mpg_fuel1: item.get("city08U").unwrap().parse::().unwrap(), + city_mpg_fuel2: item.get("cityA08U").unwrap().parse::().unwrap(), + unadj_city_mpg_fuel1: item.get("UCity").unwrap().parse::().unwrap(), + unadj_city_mpg_fuel2: item.get("UCityA").unwrap().parse::().unwrap(), + city_kwh_per_100mi: item.get("cityE").unwrap().parse::().unwrap(), + + highway_mpg_fuel1: item.get("highway08U").unwrap().parse::().unwrap(), + highway_mpg_fuel2: item.get("highwayA08U").unwrap().parse::().unwrap(), + unadj_highway_mpg_fuel1: item.get("UHighway").unwrap().parse::().unwrap(), + unadj_highway_mpg_fuel2: item.get("UHighwayA").unwrap().parse::().unwrap(), + highway_kwh_per_100mi: item.get("highwayE").unwrap().parse::().unwrap(), + + comb_mpg_fuel1: item.get("comb08U").unwrap().parse::().unwrap(), + comb_mpg_fuel2: item.get("combA08U").unwrap().parse::().unwrap(), + comb_kwh_per_100mi: item.get("combE").unwrap().parse::().unwrap(), + + emissions_list, + }; + output.push(vd); + } + Ok(output) +} +fn read_epa_test_data_for_given_years( + data_dir_path: &Path, + years: &HashSet, +) -> anyhow::Result>> { + let mut epatest_db: HashMap> = HashMap::new(); + for year in years { + let file_name = format!("{year}-testcar.csv"); + let p = data_dir_path.join(Path::new(&file_name)); + let f = File::open(p)?; + let records = read_records_from_file(f)?; + epatest_db.insert(*year, records); + } + Ok(epatest_db) +} + +fn determine_model_years_of_interest(virs: &[VehicleInputRecord]) -> HashSet { + HashSet::from_iter(virs.iter().map(|vir| vir.year)) +} + +fn load_emissions_data_for_given_years( + data_dir_path: &Path, + years: &HashSet, +) -> anyhow::Result>>> { + let mut data = HashMap::>>::new(); + for year in years { + let file_name = format!("{year}-emissions.csv"); + let emissions_path = data_dir_path.join(Path::new(&file_name)); + if !emissions_path.exists() { + // download from URL and cache + println!( + "DATA DOES NOT EXIST AT {}", + emissions_path.to_string_lossy() + ); + } + let emissions_db: HashMap> = { + let emissions_file = File::open(emissions_path)?; + read_fuelecon_gov_emissions_to_hashmap(emissions_file) + }; + data.insert(*year, emissions_db); + } + Ok(data) +} + +fn load_fegov_data_for_given_years( + data_dir_path: &Path, + emissions_by_year_and_by_id: &HashMap>>, + years: &HashSet, +) -> anyhow::Result>> { + let mut data = HashMap::>::new(); + for year in years { + if let Some(emissions_by_id) = emissions_by_year_and_by_id.get(year) { + let file_name = format!("{year}-vehicles.csv"); + let fegov_path = data_dir_path.join(Path::new(&file_name)); + let fegov_db: Vec = { + let fegov_file = File::open(fegov_path.as_path())?; + read_fuelecon_gov_data_from_file(fegov_file, emissions_by_id)? + }; + data.insert(*year, fegov_db); + } else { + println!("No fe.gov emissions data available for {year}"); + } + } + Ok(data) +} +#[cfg_attr(feature = "pyo3", pyfunction)] +#[cfg(feature = "full")] +/// Import All Vehicles for the given Year, Make, and Model and supplied other inputs +pub fn import_all_vehicles( + year: u32, + make: &str, + model: &str, + other_inputs: &OtherVehicleInputs, + cache_url: Option, + data_dir: Option, +) -> anyhow::Result> { + let vir = VehicleInputRecord { + year, + make: make.to_string(), + model: model.to_string(), + output_file_name: String::from(""), + vehicle_width_in: other_inputs.vehicle_width_in, + vehicle_height_in: other_inputs.vehicle_height_in, + fuel_tank_gal: other_inputs.fuel_tank_gal, + ess_max_kwh: other_inputs.ess_max_kwh, + mc_max_kw: other_inputs.mc_max_kw, + ess_max_kw: other_inputs.ess_max_kw, + fc_max_kw: other_inputs.fc_max_kw, + }; + let inputs = vec![vir]; + let model_years = { + let mut h: HashSet = HashSet::new(); + h.insert(year); + h + }; + let data_dir_path = if let Some(dd_path) = data_dir { + PathBuf::from(dd_path.clone()) + } else { + create_project_subdir("fe_label_data")? + }; + let data_dir_path = data_dir_path.as_path(); + let cache_url = if let Some(cache_url) = &cache_url { + cache_url.clone() + } else { + get_default_cache_url() + }; + let has_data = + populate_cache_for_given_years_if_needed(data_dir_path, &model_years, &cache_url)?; + if !has_data { + return Err(anyhow!( + "Unable to load or download cache data from {cache_url}" + )); + } + let emissions_data = load_emissions_data_for_given_years(data_dir_path, &model_years)?; + let fegov_data_by_year = + load_fegov_data_for_given_years(data_dir_path, &emissions_data, &model_years)?; + let epatest_db = read_epa_test_data_for_given_years(data_dir_path, &model_years)?; + let vehs = import_all_vehicles_from_record(&inputs, &fegov_data_by_year, &epatest_db) + .into_iter() + .map(|x| -> RustVehicle { x.1 }) + .collect(); + Ok(vehs) +} + +#[cfg(feature = "full")] +/// Import and Save All Vehicles Specified via Input File +pub fn import_and_save_all_vehicles_from_file( + input_path: &Path, + data_dir_path: &Path, + output_dir_path: &Path, + cache_url: Option, +) -> anyhow::Result<()> { + let cache_url = if let Some(url) = &cache_url { + url.clone() + } else { + get_default_cache_url() + }; + let inputs: Vec = read_vehicle_input_records_from_file(input_path)?; + println!("Found {} vehicle input records", inputs.len()); + let model_years = determine_model_years_of_interest(&inputs); + let has_data = + populate_cache_for_given_years_if_needed(data_dir_path, &model_years, &cache_url)?; + if !has_data { + return Err(anyhow!( + "Unable to load or download cache data from {cache_url}" + )); + } + let emissions_data = load_emissions_data_for_given_years(data_dir_path, &model_years)?; + let fegov_data_by_year = + load_fegov_data_for_given_years(data_dir_path, &emissions_data, &model_years)?; + let epatest_db = read_epa_test_data_for_given_years(data_dir_path, &model_years)?; + println!("Read {} files of epa test vehicle data", epatest_db.len()); + import_and_save_all_vehicles(&inputs, &fegov_data_by_year, &epatest_db, output_dir_path) +} + +#[cfg(feature = "full")] +pub fn import_all_vehicles_from_record( + inputs: &[VehicleInputRecord], + fegov_data_by_year: &HashMap>, + epatest_data_by_year: &HashMap>, +) -> Vec<(VehicleInputRecord, RustVehicle)> { + let mut vehs: Vec<(VehicleInputRecord, RustVehicle)> = Vec::new(); + for vir in inputs { + if let Some(fegov_data) = fegov_data_by_year.get(&vir.year) { + if let Some(epatest_data) = epatest_data_by_year.get(&vir.year) { + let vs = try_import_vehicles(vir, fegov_data, epatest_data); + for v in vs.iter() { + vehs.push((vir.clone(), v.clone())); + } + } else { + println!("No EPA test data available for year {}", vir.year); + } + } else { + println!("No FE.gov data available for year {}", vir.year); + } + } + vehs +} + +#[cfg(feature = "full")] +pub fn import_and_save_all_vehicles( + inputs: &[VehicleInputRecord], + fegov_data_by_year: &HashMap>, + epatest_data_by_year: &HashMap>, + output_dir_path: &Path, +) -> anyhow::Result<()> { + for (idx, (vir, veh)) in + import_all_vehicles_from_record(inputs, fegov_data_by_year, epatest_data_by_year) + .iter() + .enumerate() + { + let mut outfile: PathBuf = PathBuf::new(); + outfile.push(output_dir_path); + if idx > 0 { + let path = Path::new(&vir.output_file_name); + let stem = path.file_stem().unwrap().to_str().unwrap(); + let ext = path.extension().unwrap().to_str().unwrap(); + let output_file_name = format!("{stem}-{idx}.{ext}"); + println!("Multiple configurations found: output_file_name = {output_file_name}"); + outfile.push(Path::new(&output_file_name)); + } else { + outfile.push(Path::new(&vir.output_file_name)); + } + if let Some(full_outfile) = outfile.to_str() { + veh.to_file(full_outfile)?; + } else { + println!("Could not determine output file path"); + } + } + Ok(()) +} + +fn get_cache_url_for_year(cache_url: &str, year: &u32) -> anyhow::Result> { + let maybe_slash = if cache_url.ends_with('/') { "" } else { "/" }; + let target_url = format!("{cache_url}{maybe_slash}{year}.zip"); + Ok(Some(target_url)) +} +#[cfg(feature = "full")] +/// Checks the cache directory to see if data files have been downloaded +/// If so, moves on without any further action. +/// If not, downloads data by year from remote site if it exists +fn populate_cache_for_given_years_if_needed( + data_dir_path: &Path, + years: &HashSet, + cache_url: &str, +) -> anyhow::Result { + let mut all_data_available = true; + for year in years { + let veh_file_exists = { + let name = format!("{year}-vehicles.csv"); + let path = data_dir_path.join(Path::new(&name)); + path.exists() + }; + let emissions_file_exists = { + let name = format!("{year}-emissions.csv"); + let path = data_dir_path.join(Path::new(&name)); + path.exists() + }; + let epa_file_exists = { + let name = format!("{year}-testcar.csv"); + let path = data_dir_path.join(Path::new(&name)); + path.exists() + }; + if !veh_file_exists || !emissions_file_exists || !epa_file_exists { + all_data_available = false; + let zip_file_name = format!("{year}.zip"); + let zip_file_path = data_dir_path.join(Path::new(&zip_file_name)); + if let Some(url) = get_cache_url_for_year(cache_url, year)? { + println!("Downloading data for {year}: {url}"); + download_file_from_url(&url, &zip_file_path)?; + println!("... downloading data for {year}"); + let emissions_name = format!("{year}-emissions.csv"); + extract_file_from_zip( + zip_file_path.as_path(), + &emissions_name, + data_dir_path.join(Path::new(&emissions_name)).as_path(), + )?; + println!("... extracted {}", emissions_name); + let vehicles_name = format!("{year}-vehicles.csv"); + extract_file_from_zip( + zip_file_path.as_path(), + &vehicles_name, + data_dir_path.join(Path::new(&vehicles_name)).as_path(), + )?; + println!("... extracted {}", vehicles_name); + let epatests_name = format!("{year}-testcar.csv"); + extract_file_from_zip( + zip_file_path.as_path(), + &epatests_name, + data_dir_path.join(Path::new(&epatests_name)).as_path(), + )?; + println!("... extracted {}", epatests_name); + all_data_available = true; + } + } + } + Ok(all_data_available) +} + +fn extract_file_from_zip( + zip_file_path: &Path, + name_of_file_to_extract: &str, + path_to_save_to: &Path, +) -> anyhow::Result<()> { + let zipfile = File::open(zip_file_path)?; + let mut archive = ZipArchive::new(zipfile)?; + let mut file = archive.by_name(name_of_file_to_extract)?; + let mut contents = String::new(); + file.read_to_string(&mut contents)?; + std::fs::write(path_to_save_to, contents)?; + Ok(()) +} + +#[cfg(feature = "full")] +/// Assumes the parent directory exists. Assumes file doesn't exist (i.e., newly created) or that it will be truncated if it does. +pub fn download_file_from_url(url: &str, file_path: &Path) -> anyhow::Result<()> { + let mut handle = Easy::new(); + handle.follow_location(true)?; + handle.url(url)?; + let mut buffer = Vec::new(); + { + let mut transfer = handle.transfer(); + transfer.write_function(|data| { + buffer.extend_from_slice(data); + Ok(data.len()) + })?; + let result = transfer.perform(); + if result.is_err() { + return Err(anyhow!("Could not download from {}", url)); + } + } + println!("Downloaded data from {}; bytes: {}", url, buffer.len()); + if buffer.is_empty() { + return Err(anyhow!("No data available from {url}")); + } + { + let mut file = match File::create(file_path) { + Err(why) => { + return Err(anyhow!( + "couldn't open {}: {}", + file_path.to_str().unwrap(), + why + )) + } + Ok(file) => file, + }; + file.write_all(buffer.as_slice())?; + } + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[cfg(feature = "full")] + #[test] + fn test_create_new_vehicle_from_input_data() { + let veh_record = VehicleInputRecord { + make: String::from("Toyota"), + model: String::from("Camry"), + year: 2020, + output_file_name: String::from("2020-toyota-camry.yaml"), + vehicle_width_in: 72.4, + vehicle_height_in: 56.9, + fuel_tank_gal: 15.8, + ess_max_kwh: 0.0, + mc_max_kw: 0.0, + ess_max_kw: 0.0, + fc_max_kw: None, + }; + let emiss_info = vec![ + EmissionsInfoFE { + efid: String::from("LTYXV03.5M5B"), + score: 5.0, + smartway_score: -1, + standard: String::from("L3ULEV70"), + std_text: String::from("California LEV-III ULEV70"), + }, + EmissionsInfoFE { + efid: String::from("LTYXV03.5M5B"), + score: 5.0, + smartway_score: -1, + standard: String::from("T3B70"), + std_text: String::from("Federal Tier 3 Bin 70"), + }, + ]; + let emiss_list = EmissionsListFE { + emissions_info: emiss_info, + }; + let fegov_data = VehicleDataFE { + id: 32204, + + year: 2020, + make: String::from("Toyota"), + model: String::from("Camry"), + + veh_class: String::from("Midsize Cars"), + + drive: String::from("Front-Wheel Drive"), + alt_veh_type: String::from(""), + + fuel_type: String::from("Regular"), + fuel1: String::from("Regular Gasoline"), + fuel2: String::from(""), + + eng_dscr: String::from("SIDI & PFI"), + cylinders: String::from("6"), + displ: String::from("3.5"), + transmission: String::from("Automatic (S8)"), + + super_charger: String::from(""), + turbo_charger: String::from(""), + + start_stop: String::from("N"), + + phev_blended: false, + phev_city_mpge: 0, + phev_comb_mpge: 0, + phev_hwy_mpge: 0, + + ev_motor_kw: String::from(""), + range_ev: 0, + + city_mpg_fuel1: 16.4596, + city_mpg_fuel2: 0.0, + unadj_city_mpg_fuel1: 20.2988, + unadj_city_mpg_fuel2: 0.0, + city_kwh_per_100mi: 0.0, + + highway_mpg_fuel1: 22.5568, + highway_mpg_fuel2: 0.0, + unadj_highway_mpg_fuel1: 30.1798, + unadj_highway_mpg_fuel2: 0.0, + highway_kwh_per_100mi: 0.0, + + comb_mpg_fuel1: 18.7389, + comb_mpg_fuel2: 0.0, + comb_kwh_per_100mi: 0.0, + + emissions_list: emiss_list, + }; + let epatest_data = VehicleDataEPA { + year: 2020, + make: String::from("TOYOTA"), + model: String::from("CAMRY"), + test_id: String::from("JTYXV03.5M5B"), + displ: 3.456, + eng_pwr_hp: 301, + cylinders: String::from("6"), + transmission_code: String::from("A"), + transmission_type: String::from("Automatic"), + gears: 8, + drive_code: String::from("F"), + drive: String::from("2-Wheel Drive, Front"), + test_weight_lbs: 3875.0, + test_fuel_type: String::from("61"), + a_lbf: 24.843, + b_lbf_per_mph: 0.40298, + c_lbf_per_mph2: 0.015068, + }; + let other_inputs = vir_to_other_inputs(&veh_record); + let v = try_make_single_vehicle(&fegov_data, &epatest_data, &other_inputs); + assert!(v.is_some()); + if let Some(vs) = v { + assert_eq!(vs.scenario_name, String::from("2020 Toyota Camry")); + assert_eq!(vs.val_comb_mpgge, 18.7389); + } + } + + #[cfg(feature = "full")] + #[test] + fn test_get_options_for_year_make_model() { + let year = String::from("2020"); + let make = String::from("Toyota"); + let model = String::from("Corolla"); + let res = get_options_for_year_make_model(&year, &make, &model, None, None); + if let Err(err) = &res { + panic!("{:?}", err); + } else if let Ok(vs) = &res { + assert!(!vs.is_empty()); + } + } + + #[cfg(feature = "full")] + #[test] + fn test_import_robustness() { + // Ensure 2019 data is cached + let ddpath = create_project_subdir("fe_label_data").unwrap(); + let model_year: u32 = 2019; + let years = { + let mut s = HashSet::new(); + s.insert(model_year); + s + }; + let cache_url = get_default_cache_url(); + populate_cache_for_given_years_if_needed(ddpath.as_path(), &years, &cache_url).unwrap(); + // Load all year/make/models for 2019 + let vehicles_path = ddpath.join(Path::new("2019-vehicles.csv")); + let veh_records = { + let file = File::open(vehicles_path); + if let Ok(f) = file { + let data_result: anyhow::Result>> = + read_records_from_file(f); + if let Ok(data) = data_result { + data + } else { + vec![] + } + } else { + vec![] + } + }; + let mut num_success: usize = 0; + let other_inputs = OtherVehicleInputs { + vehicle_height_in: 72.4, + vehicle_width_in: 56.9, + fuel_tank_gal: 15.8, + ess_max_kwh: 0.0, + mc_max_kw: 0.0, + ess_max_kw: 0.0, + fc_max_kw: None, + }; + let mut num_records = 0; + let max_iter = veh_records.len(); + // NOTE: below, we can use fewer records in the interest of time as this is a long test with all records + // We skip because the vehicles at the beginning of the file tend to be more exotic and to not have + // EPA test entries. Thus, they are a bad representation of the whole. + let skip_idx: usize = 200; + for (num_iter, vr) in veh_records.iter().enumerate() { + if num_iter % skip_idx != 0 { + continue; + } + if num_iter >= max_iter { + break; + } + let make = vr.get("make"); + let model = vr.get("model"); + if let (Some(make), Some(model)) = (make, model) { + let result = + import_all_vehicles(model_year, make, model, &other_inputs, None, None); + if let Ok(vehs) = &result { + if !vehs.is_empty() { + num_success += 1; + } + } + } else { + panic!("Unable to find make and model in vehicle record"); + } + num_records += 1; + } + let success_frac: f64 = (num_success as f64) / (num_records as f64); + assert!(success_frac > 0.90, "success_frac = {}", success_frac); + } + + #[cfg(feature = "full")] + #[test] + fn test_get_options_for_year_make_model_for_specified_cacheurl_and_data_dir() { + let year = String::from("2020"); + let make = String::from("Toyota"); + let model = String::from("Corolla"); + let temp_dir = tempfile::tempdir().unwrap(); + let data_dir = temp_dir.path(); + let cacheurl = get_default_cache_url(); + assert!(!get_options_for_year_make_model( + &year, + &make, + &model, + Some(cacheurl), + Some(data_dir.to_str().unwrap().to_string()), + ) + .unwrap() + .is_empty()); + } +} diff --git a/rust/fastsim-core/src/vehicle_utils.rs b/rust/fastsim-core/src/vehicle_utils.rs index 4dfa1d28..bc882ad0 100644 --- a/rust/fastsim-core/src/vehicle_utils.rs +++ b/rust/fastsim-core/src/vehicle_utils.rs @@ -4,1108 +4,20 @@ use argmin::core::{CostFunction, Executor, OptimizationResult, State}; #[cfg(feature = "full")] use argmin::solver::neldermead::NelderMead; -#[cfg(feature = "full")] -use curl::easy::Easy; use ndarray::{array, Array1}; #[cfg(feature = "full")] use polynomial::Polynomial; -use serde::de::DeserializeOwned; -use std::collections::HashMap; -use std::collections::HashSet; -use std::fs::File; -use std::io::prelude::Write; -use std::io::Read; -use std::iter::FromIterator; -use std::num::ParseIntError; use std::option::Option; -use std::path::PathBuf; -use zip::ZipArchive; use crate::air::*; use crate::cycle::RustCycle; use crate::imports::*; use crate::params::*; -use crate::proc_macros::add_pyo3_api; #[cfg(feature = "pyo3")] use crate::pyo3imports::*; use crate::simdrive::RustSimDrive; use crate::vehicle::RustVehicle; -#[derive(Debug, Serialize, Deserialize, PartialEq)] -/// Struct containing list of makes for a year from fueleconomy.gov -struct VehicleMakesFE { - #[serde(rename = "menuItem")] - /// List of vehicle makes - makes: Vec, -} - -#[derive(Debug, Serialize, Deserialize, PartialEq)] -/// Struct containing make information for a year fueleconomy.gov -struct MakeFE { - #[serde(rename = "text")] - /// Transmission of vehicle - make_name: String, -} - -#[derive(Debug, Serialize, Deserialize, PartialEq)] -/// Struct containing list of models for a year and make from fueleconomy.gov -struct VehicleModelsFE { - #[serde(rename = "menuItem")] - /// List of vehicle models - models: Vec, -} - -#[derive(Debug, Serialize, Deserialize, PartialEq)] -/// Struct containing model information for a year and make from fueleconomy.gov -struct ModelFE { - #[serde(rename = "text")] - /// Transmission of vehicle - model_name: String, -} - -#[derive(Debug, Serialize, Deserialize, PartialEq)] -/// Struct containing list of transmission options for vehicle from fueleconomy.gov -struct VehicleOptionsFE { - #[serde(rename = "menuItem")] - /// List of vehicle options (transmission and id) - options: Vec, -} - -#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)] -#[add_pyo3_api] -/// Struct containing transmission and id of a vehicle option from fueleconomy.gov -pub struct OptionFE { - #[serde(rename = "text")] - /// Transmission of vehicle - pub transmission: String, - #[serde(rename = "value")] - /// ID of vehicle on fueleconomy.gov - pub id: String, -} - -impl SerdeAPI for OptionFE {} - -#[derive(Default, Debug, Serialize, Deserialize, PartialEq, Clone)] -#[add_pyo3_api] -/// Struct containing vehicle data from fueleconomy.gov -pub struct VehicleDataFE { - /// Vehicle ID - pub id: i32, - - /// Model year - pub year: u32, - /// Vehicle make - pub make: String, - /// Vehicle model - pub model: String, - - /// EPA vehicle size class - #[serde(rename = "VClass")] - pub veh_class: String, - - /// Drive axle type (FWD, RWD, AWD, 4WD) - pub drive: String, - /// Type of alternative fuel vehicle (Hybrid, Plug-in Hybrid, EV) - #[serde(default, rename = "atvType")] - pub alt_veh_type: String, - - /// Combined vehicle fuel type (fuel 1 and fuel 2) - #[serde(rename = "fuelType")] - pub fuel_type: String, - /// Fuel type 1 - #[serde(rename = "fuelType1")] - pub fuel1: String, - /// Fuel type 2 - #[serde(default, rename = "fuelType2")] - pub fuel2: String, - - /// Description of engine - #[serde(default)] - pub eng_dscr: String, - /// Number of engine cylinders - #[serde(default)] - pub cylinders: String, - /// Engine displacement in liters - #[serde(default)] - pub displ: String, - /// transmission - #[serde(rename = "trany")] - pub transmission: String, - - /// "S" if vehicle has supercharger - #[serde(default, rename = "sCharger")] - pub super_charger: String, - /// "T" if vehicle has turbocharger - #[serde(default, rename = "tCharger")] - pub turbo_charger: String, - - /// Stop-start technology - #[serde(rename = "startStop")] - pub start_stop: String, - - /// Vehicle operates on blend of gasoline and electricity - #[serde(rename = "phevBlended")] - pub phev_blended: bool, - /// EPA composite gasoline-electricity city MPGe - #[serde(rename = "phevCity")] - pub phev_city_mpge: i32, - /// EPA composite gasoline-electricity combined MPGe - #[serde(rename = "phevComb")] - pub phev_comb_mpge: i32, - /// EPA composite gasoline-electricity highway MPGe - #[serde(rename = "phevHwy")] - pub phev_hwy_mpge: i32, - - /// Electric motor power (kW), not very consistent as an input - #[serde(default, rename = "evMotor")] - pub ev_motor_kw: String, - /// EV range - #[serde(rename = "range")] - pub range_ev: i32, - - /// City MPG for fuel 1 - #[serde(rename = "city08U")] - pub city_mpg_fuel1: f64, - /// City MPG for fuel 2 - #[serde(rename = "cityA08U")] - pub city_mpg_fuel2: f64, - /// Unadjusted unroaded city MPG for fuel 1 - #[serde(rename = "UCity")] - pub unadj_city_mpg_fuel1: f64, - /// Unadjusted unroaded city MPG for fuel 2 - #[serde(rename = "UCityA")] - pub unadj_city_mpg_fuel2: f64, - /// City electricity consumption in kWh/100 mi - #[serde(rename = "cityE")] - pub city_kwh_per_100mi: f64, - - /// Adjusted unrounded highway MPG for fuel 1 - #[serde(rename = "highway08U")] - pub highway_mpg_fuel1: f64, - /// Adjusted unrounded highway MPG for fuel 2 - #[serde(rename = "highwayA08U")] - pub highway_mpg_fuel2: f64, - /// Unadjusted unrounded highway MPG for fuel 1 - #[serde(default, rename = "UHighway")] - pub unadj_highway_mpg_fuel1: f64, - /// Unadjusted unrounded highway MPG for fuel 2 - #[serde(default, rename = "UHighwayA")] - pub unadj_highway_mpg_fuel2: f64, - /// Highway electricity consumption in kWh/100 mi - #[serde(default, rename = "highwayE")] - pub highway_kwh_per_100mi: f64, - - /// Combined MPG for fuel 1 - #[serde(rename = "comb08U")] - pub comb_mpg_fuel1: f64, - /// Combined MPG for fuel 2 - #[serde(rename = "combA08U")] - pub comb_mpg_fuel2: f64, - /// Combined electricity consumption in kWh/100 mi - #[serde(default, rename = "combE")] - pub comb_kwh_per_100mi: f64, - - /// List of emissions tests - #[serde(rename = "emissionsList")] - pub emissions_list: EmissionsListFE, -} - -impl SerdeAPI for VehicleDataFE {} - -#[derive(Default, Debug, Serialize, Deserialize, PartialEq, Clone)] -#[serde(rename_all = "camelCase")] -#[add_pyo3_api] -/// Struct containing list of emissions tests from fueleconomy.gov -pub struct EmissionsListFE { - /// - pub emissions_info: Vec, -} - -impl SerdeAPI for EmissionsListFE {} - -#[derive(Default, Debug, Serialize, Deserialize, PartialEq, Clone)] -#[serde(rename_all = "camelCase")] -#[add_pyo3_api] -/// Struct containing emissions test results from fueleconomy.gov -pub struct EmissionsInfoFE { - /// Engine family id / EPA test group - pub efid: String, - /// EPA smog rating - pub score: f64, - /// SmartWay score - pub smartway_score: i32, - /// Vehicle emission standard code - pub standard: String, - /// Vehicle emission standard - pub std_text: String, -} - -impl SerdeAPI for EmissionsInfoFE {} - -#[derive(Default, PartialEq, Clone, Debug, Deserialize, Serialize)] -#[add_pyo3_api] -/// Struct containing vehicle data from EPA database -pub struct VehicleDataEPA { - /// Model year - #[serde(rename = "Model Year")] - pub year: u32, - /// Vehicle make - #[serde(rename = "Represented Test Veh Make")] - pub make: String, - /// Vehicle model - #[serde(rename = "Represented Test Veh Model")] - pub model: String, - /// Vehicle test group - #[serde(rename = "Actual Tested Testgroup")] - pub test_id: String, - /// Engine displacement - #[serde(rename = "Test Veh Displacement (L)")] - pub displ: f64, - /// Engine power in hp - #[serde(rename = "Rated Horsepower")] - pub eng_pwr_hp: u32, - /// Number of cylinders - #[serde(rename = "# of Cylinders and Rotors")] - pub cylinders: String, - /// Transmission type code - #[serde(rename = "Tested Transmission Type Code")] - pub transmission_code: String, - /// Transmission type - #[serde(rename = "Tested Transmission Type")] - pub transmission_type: String, - /// Number of gears - #[serde(rename = "# of Gears")] - pub gears: u32, - /// Drive system code - #[serde(rename = "Drive System Code")] - pub drive_code: String, - /// Drive system type - #[serde(rename = "Drive System Description")] - pub drive: String, - /// Test weight in lbs - #[serde(rename = "Equivalent Test Weight (lbs.)")] - pub test_weight_lbs: f64, - /// Fuel type used for EPA test - #[serde(rename = "Test Fuel Type Description")] - pub test_fuel_type: String, - /// Dyno coefficient a in lbf - #[serde(rename = "Target Coef A (lbf)")] - pub a_lbf: f64, - /// Dyno coefficient b in lbf/mph - #[serde(rename = "Target Coef B (lbf/mph)")] - pub b_lbf_per_mph: f64, - /// Dyno coefficient c in lbf/mph^2 - #[serde(rename = "Target Coef C (lbf/mph**2)")] - pub c_lbf_per_mph2: f64, -} - -impl SerdeAPI for VehicleDataEPA {} - -#[cfg(feature = "full")] -#[cfg_attr(feature = "pyo3", pyfunction)] -/// Gets options from fueleconomy.gov for the given vehicle year, make, and model -/// -/// Arguments: -/// ---------- -/// year: Vehicle year -/// make: Vehicle make -/// model: Vehicle model (must match model on fueleconomy.gov) -/// -/// Returns: -/// -------- -/// Vec: Data for the available options for that vehicle year/make/model from fueleconomy.gov -pub fn get_options_for_year_make_model( - year: &str, - make: &str, - model: &str, - cache_url: Option, - data_dir: Option, -) -> anyhow::Result> { - // prep the cache for year - let y: u32 = year.trim().parse()?; - let ys: HashSet = { - let mut h = HashSet::new(); - h.insert(y); - h - }; - let ddpath = if let Some(dd) = data_dir { - PathBuf::from(dd) - } else { - create_project_subdir("fe_label_data")? - }; - let cache_url = if let Some(url) = &cache_url { - url.clone() - } else { - get_default_cache_url() - }; - let has_data = populate_cache_for_given_years_if_needed(ddpath.as_path(), &ys, &cache_url)?; - if !has_data { - return Err(anyhow!( - "Unable to load or download cache data from {cache_url}" - )); - } - let emissions_data = load_emissions_data_for_given_years(ddpath.as_path(), &ys)?; - let fegov_data_by_year = - load_fegov_data_for_given_years(ddpath.as_path(), &emissions_data, &ys)?; - if let Some(fegov_db) = fegov_data_by_year.get(&y) { - let mut hits: Vec = Vec::new(); - for item in fegov_db.iter() { - if item.make == make && item.model == model { - hits.push(item.clone()); - } - } - Ok(hits) - } else { - Ok(vec![]) - } -} - -fn derive_transmission_specs(fegov: &VehicleDataFE) -> (u32, String) { - let num_gears_fe_gov: u32; - let transmission_fe_gov: String; - // Based on reference: https://www.fueleconomy.gov/feg/findacarhelp.shtml#engine - if fegov.transmission.contains("Manual") { - transmission_fe_gov = String::from('M'); - num_gears_fe_gov = fegov.transmission.as_str()[fegov.transmission.find("-spd").unwrap() - 1 - ..fegov.transmission.find("-spd").unwrap()] - .parse() - .unwrap(); - } else if fegov.transmission.contains("variable gear ratios") { - transmission_fe_gov = String::from("CVT"); - num_gears_fe_gov = 1; - } else if fegov.transmission.contains("AV-S") { - transmission_fe_gov = String::from("SCV"); - num_gears_fe_gov = fegov.transmission.as_str() - [fegov.transmission.find('S').unwrap() + 1..fegov.transmission.find(')').unwrap()] - .parse() - .unwrap(); - } else if fegov.transmission.contains("AM-S") { - transmission_fe_gov = String::from("AMS"); - num_gears_fe_gov = fegov.transmission.as_str() - [fegov.transmission.find('S').unwrap() + 1..fegov.transmission.find(')').unwrap()] - .parse() - .unwrap(); - } else if fegov.transmission.contains('S') { - transmission_fe_gov = String::from("SA"); - num_gears_fe_gov = fegov.transmission.as_str() - [fegov.transmission.find('S').unwrap() + 1..fegov.transmission.find(')').unwrap()] - .parse() - .unwrap(); - } else if fegov.transmission.contains("-spd") { - transmission_fe_gov = String::from('A'); - num_gears_fe_gov = fegov.transmission.as_str()[fegov.transmission.find("-spd").unwrap() - 1 - ..fegov.transmission.find("-spd").unwrap()] - .parse() - .unwrap(); - } else { - transmission_fe_gov = String::from('A'); - num_gears_fe_gov = { - let res: Result = fegov.transmission.as_str() - [fegov.transmission.find("(A").unwrap() + 2..fegov.transmission.find(')').unwrap()] - .parse(); - if let Ok(n) = res { - n - } else { - 1 - } - } - } - (num_gears_fe_gov, transmission_fe_gov) -} - -/// Match EPA Test Data with FuelEconomy.gov data and return best match -/// The matching algorithm tries to find the best match in the EPA Test data for the given FuelEconomy.gov data -/// The algorithm works as follows: -/// - only EPA Test Data matching the year and make of the FuelEconomy.gov data will be considered -/// - we try to match on both the efid/test id and also the model name -/// - next, for each match, we calculate a score based on matching various powertrain aspects based on: -/// - transmission type -/// - number of gears in the transmission -/// - drive type (all-wheel drive / 4-wheel drive, etc.) -/// - (for non-EVs) -/// - engine displacement -/// - number of cylinders -/// RETURNS: the EPA Test data with the best match on make and/or efid/test id. When multiple vehicles match -/// the same make name/ efid/test-id, we return the one with the highest score -fn match_epatest_with_fegov_v2( - fegov: &VehicleDataFE, - epatest_data: &[VehicleDataEPA], -) -> Option { - let fe_model_upper: String = fegov.model.to_uppercase().replace("4WD", "AWD"); - let fe_model_words: Vec<&str> = fe_model_upper.split_ascii_whitespace().collect(); - let num_fe_model_words = fe_model_words.len(); - let fegov_disp = fegov.displ.parse::().unwrap_or_default(); - let efid = if !fegov.emissions_list.emissions_info.is_empty() { - fegov.emissions_list.emissions_info[0].efid.clone() - } else { - String::new() - }; - let fegov_drive = { - let mut s = String::new(); - if !fegov.drive.is_empty() { - let maybe_char = fegov.drive.chars().next(); - if let Some(c) = maybe_char { - s.push(c); - } - s - } else { - s - } - }; - let (num_gears_fe_gov, transmission_fe_gov) = derive_transmission_specs(fegov); - let epa_candidates = { - let mut xs: Vec<(f64, f64, VehicleDataEPA)> = Vec::new(); - for x in epatest_data { - if x.year == fegov.year && x.make.eq_ignore_ascii_case(&fegov.make) { - let mut score = 0.0; - - // Things we Don't Want to Match - if x.test_fuel_type.contains("Cold CO") { - continue; - } - let matching_test_id = if !x.test_id.is_empty() && !efid.is_empty() { - x.test_id.ends_with(&efid[1..efid.len()]) - } else { - false - }; - // ID match - let name_match = if matching_test_id || x.model.eq_ignore_ascii_case(&fegov.model) { - 1.0 - } else { - let epa_model_upper = x.model.to_uppercase().replace("4WD", "AWD"); - let epa_model_words: Vec<&str> = - epa_model_upper.split_ascii_whitespace().collect(); - let num_epa_model_words = epa_model_words.len(); - let mut match_count = 0; - for word in &epa_model_words { - match_count += fe_model_words.contains(word) as i64; - } - let match_frac: f64 = (match_count as f64 * match_count as f64) - / (num_epa_model_words as f64 * num_fe_model_words as f64); - match_frac - }; - if name_match == 0.0 { - continue; - } - // By PT Type - if fegov.alt_veh_type == *"EV" { - if x.cylinders.is_empty() && x.displ.round() == 0.0 { - score += 1.0; - } - } else { - let epa_disp = (x.displ * 10.0).round() / 10.0; - if x.cylinders == fegov.cylinders && epa_disp == fegov_disp { - score += 1.0; - } - } - // Drive Code - let drive_code = if x.model.contains("4WD") - || x.model.contains("AWD") - || x.drive.contains("4-Wheel Drive") - { - String::from('A') - } else { - x.drive.clone() - }; - if drive_code == fegov_drive { - score += 1.0; - } - // Transmission Type and Num Gears - if x.transmission_code == transmission_fe_gov { - score += 0.5; - } else if transmission_fe_gov.starts_with(x.transmission_type.as_str()) { - score += 0.25; - } - if x.gears == num_gears_fe_gov { - score += 0.5; - } - xs.push((name_match, score, x.clone())); - } - } - xs - }; - if epa_candidates.is_empty() { - None - } else { - let mut largest_id_match_value = 0.0; - let mut largest_score_value = 0.0; - let mut best_idx: usize = 0; - for (idx, item) in epa_candidates.iter().enumerate() { - if item.0 > largest_id_match_value - || (item.0 == largest_id_match_value && item.1 > largest_score_value) - { - largest_id_match_value = item.0; - largest_score_value = item.1; - best_idx = idx; - } - } - if largest_id_match_value == 0.0 { - None - } else { - Some(epa_candidates[best_idx].2.clone()) - } - } -} - -/// Match EPA Test Data with FuelEconomy.gov data and return best match -#[allow(dead_code)] -fn match_epatest_with_fegov( - fegov: &VehicleDataFE, - epatest_data: &[VehicleDataEPA], -) -> Option { - if fegov.emissions_list.emissions_info.is_empty() { - return None; - } - // Keep track of best match to fueleconomy.gov model name for all vehicles and vehicles with matching efid/test id - let mut veh_list_overall: HashMap> = HashMap::new(); - let mut veh_list_efid: HashMap> = HashMap::new(); - let mut best_match_percent_efid: f64 = 0.0; - let mut best_match_model_efid: String = String::new(); - let mut best_match_percent_overall: f64 = 0.0; - let mut best_match_model_overall: String = String::new(); - - let fe_model_upper: String = fegov.model.to_uppercase().replace("4WD", "AWD"); - let fe_model_words: Vec<&str> = fe_model_upper.split(' ').collect(); - let num_fe_model_words = fe_model_words.len(); - let efid: &String = &fegov.emissions_list.emissions_info[0].efid; - - for veh_epa in epatest_data { - // Find matches between EPA vehicle model name and fe.gov vehicle model name - let mut match_count: i64 = 0; - let epa_model_upper = veh_epa.model.to_uppercase().replace("4WD", "AWD"); - let epa_model_words: Vec<&str> = epa_model_upper.split(' ').collect(); - let num_epa_model_words = epa_model_words.len(); - for word in &epa_model_words { - match_count += fe_model_words.contains(word) as i64; - } - // Calculate composite match percentage - let match_percent: f64 = (match_count as f64 * match_count as f64) - / (num_epa_model_words as f64 * num_fe_model_words as f64); - - // Update overall hashmap with new entry - if veh_list_overall.contains_key(&veh_epa.model) { - if let Some(x) = veh_list_overall.get_mut(&veh_epa.model) { - (*x).push(veh_epa.clone()); - } - } else { - veh_list_overall.insert(veh_epa.model.clone(), vec![veh_epa.clone()]); - - if match_percent > best_match_percent_overall { - best_match_percent_overall = match_percent; - best_match_model_overall = veh_epa.model.clone(); - } - } - - // Update efid hashmap if fe.gov efid matches EPA test id - // (for some reason first character in id is almost always different) - if veh_epa.test_id.ends_with(&efid[1..efid.len()]) { - if veh_list_efid.contains_key(&veh_epa.model) { - if let Some(x) = veh_list_efid.get_mut(&veh_epa.model) { - (*x).push(veh_epa.clone()); - } - } else { - veh_list_efid.insert(veh_epa.model.clone(), vec![veh_epa.clone()]); - if match_percent > best_match_percent_efid { - best_match_percent_efid = match_percent; - best_match_model_efid = veh_epa.model.clone(); - } - } - } - } - - // Get EPA vehicle model that is best match to fe.gov vehicle - let veh_list: Vec = if best_match_model_efid == best_match_model_overall { - let x = veh_list_efid.get(&best_match_model_efid); - x?; - x.unwrap().to_vec() - } else { - veh_list_overall - .get(&best_match_model_overall) - .unwrap() - .to_vec() - }; - - // Get number of gears and convert fe.gov transmission description to EPA transmission description - let num_gears_fe_gov: u32; - let transmission_fe_gov: String; - // Based on reference: https://www.fueleconomy.gov/feg/findacarhelp.shtml#engine - if fegov.transmission.contains("Manual") { - transmission_fe_gov = String::from('M'); - num_gears_fe_gov = fegov.transmission.as_str()[fegov.transmission.find("-spd").unwrap() - 1 - ..fegov.transmission.find("-spd").unwrap()] - .parse() - .unwrap(); - } else if fegov.transmission.contains("variable gear ratios") { - transmission_fe_gov = String::from("CVT"); - num_gears_fe_gov = 1; - } else if fegov.transmission.contains("AV-S") { - transmission_fe_gov = String::from("SCV"); - num_gears_fe_gov = fegov.transmission.as_str() - [fegov.transmission.find('S').unwrap() + 1..fegov.transmission.find(')').unwrap()] - .parse() - .unwrap(); - } else if fegov.transmission.contains("AM-S") { - transmission_fe_gov = String::from("AMS"); - num_gears_fe_gov = fegov.transmission.as_str() - [fegov.transmission.find('S').unwrap() + 1..fegov.transmission.find(')').unwrap()] - .parse() - .unwrap(); - } else if fegov.transmission.contains('S') { - transmission_fe_gov = String::from("SA"); - num_gears_fe_gov = fegov.transmission.as_str() - [fegov.transmission.find('S').unwrap() + 1..fegov.transmission.find(')').unwrap()] - .parse() - .unwrap(); - } else if fegov.transmission.contains("-spd") { - transmission_fe_gov = String::from('A'); - num_gears_fe_gov = fegov.transmission.as_str()[fegov.transmission.find("-spd").unwrap() - 1 - ..fegov.transmission.find("-spd").unwrap()] - .parse() - .unwrap(); - } else { - transmission_fe_gov = String::from('A'); - num_gears_fe_gov = { - let res: Result = fegov.transmission.as_str() - [fegov.transmission.find("(A").unwrap() + 2..fegov.transmission.find(')').unwrap()] - .parse(); - if let Ok(n) = res { - n - } else { - 1 - } - } - } - - // Find EPA vehicle entry that matches fe.gov vehicle data - // If same vehicle model has multiple configurations, get most common configuration - let mut most_common_veh: VehicleDataEPA = VehicleDataEPA::default(); - let mut most_common_count: i32 = 0; - let mut current_veh: VehicleDataEPA = VehicleDataEPA::default(); - let mut current_count: i32 = 0; - for mut veh_epa in veh_list { - if veh_epa.model.contains("4WD") - || veh_epa.model.contains("AWD") - || veh_epa.drive.contains("4-Wheel Drive") - { - veh_epa.drive_code = String::from('A'); - veh_epa.drive = String::from("All Wheel Drive"); - } - if !veh_epa.test_fuel_type.contains("Cold CO") - && (veh_epa.transmission_code == transmission_fe_gov - || fegov - .transmission - .starts_with(veh_epa.transmission_type.as_str())) - && veh_epa.gears == num_gears_fe_gov - && veh_epa.drive_code == fegov.drive[0..1] - && ((fegov.alt_veh_type == *"EV" - && veh_epa.displ.round() == 0.0 - && veh_epa.cylinders == String::new()) - || ((veh_epa.displ * 10.0).round() / 10.0 - == (fegov.displ.parse::().unwrap_or_default()) - && veh_epa.cylinders == fegov.cylinders)) - { - if veh_epa == current_veh { - current_count += 1; - } else { - if current_count > most_common_count { - most_common_veh = current_veh.clone(); - most_common_count = current_count; - } - current_veh = veh_epa.clone(); - current_count = 1; - } - } - } - if current_count > most_common_count { - Some(current_veh) - } else { - Some(most_common_veh) - } -} - -#[derive(Default, PartialEq, Clone, Debug, Deserialize, Serialize)] -#[add_pyo3_api( - #[new] - pub fn __new__( - vehicle_width_in: f64, - vehicle_height_in: f64, - fuel_tank_gal: f64, - ess_max_kwh: f64, - mc_max_kw: f64, - ess_max_kw: f64, - fc_max_kw: Option - ) -> Self { - OtherVehicleInputs { - vehicle_width_in, - vehicle_height_in, - fuel_tank_gal, - ess_max_kwh, - mc_max_kw, - ess_max_kw, - fc_max_kw - } - } -)] -pub struct OtherVehicleInputs { - pub vehicle_width_in: f64, - pub vehicle_height_in: f64, - pub fuel_tank_gal: f64, - pub ess_max_kwh: f64, - pub mc_max_kw: f64, - pub ess_max_kw: f64, - pub fc_max_kw: Option, -} - -impl SerdeAPI for OtherVehicleInputs {} - -#[cfg(feature = "full")] -#[cfg_attr(feature = "pyo3", pyfunction)] -/// Creates RustVehicle for the given vehicle using data from fueleconomy.gov and EPA databases -/// The created RustVehicle is also written as a yaml file -/// -/// Arguments: -/// ---------- -/// vehicle_id: i32, Identifier at fueleconomy.gov for the desired vehicle -/// year: u32, the year of the vehicle -/// other_inputs: Other vehicle inputs required to create the vehicle -/// -/// Returns: -/// -------- -/// veh: RustVehicle for specificed vehicle -pub fn vehicle_import_by_id_and_year( - vehicle_id: i32, - year: u32, - other_inputs: &OtherVehicleInputs, - cache_url: Option, - data_dir: Option, -) -> anyhow::Result { - let mut maybe_veh: Option = None; - let data_dir_path = if let Some(data_dir) = data_dir { - PathBuf::from(data_dir) - } else { - create_project_subdir("fe_label_data")? - }; - let data_dir_path = data_dir_path.as_path(); - let model_years = { - let mut h: HashSet = HashSet::new(); - h.insert(year); - h - }; - let cache_url = if let Some(cache_url) = &cache_url { - cache_url.clone() - } else { - get_default_cache_url() - }; - let has_data = - populate_cache_for_given_years_if_needed(data_dir_path, &model_years, &cache_url)?; - if !has_data { - return Err(anyhow!( - "Unable to load or download cache data from {cache_url}" - )); - } - let emissions_data = load_emissions_data_for_given_years(data_dir_path, &model_years)?; - let fegov_data_by_year = - load_fegov_data_for_given_years(data_dir_path, &emissions_data, &model_years)?; - let epatest_db = read_epa_test_data_for_given_years(data_dir_path, &model_years)?; - if let Some(fe_gov_data) = fegov_data_by_year.get(&year) { - if let Some(epa_data) = epatest_db.get(&year) { - let fe_gov_data = { - let mut maybe_data = None; - for item in fe_gov_data { - if item.id == vehicle_id { - maybe_data = Some(item.clone()); - break; - } - } - maybe_data - }; - if let Some(fe_gov_data) = fe_gov_data { - if let Some(epa_data) = match_epatest_with_fegov_v2(&fe_gov_data, epa_data) { - maybe_veh = try_make_single_vehicle(&fe_gov_data, &epa_data, other_inputs); - } - } - } - } - match maybe_veh { - Some(veh) => Ok(veh), - None => Err(anyhow!("Unable to find/match vehicle in DB")), - } -} - -fn get_fuel_economy_gov_data_for_input_record( - vir: &VehicleInputRecord, - fegov_data: &[VehicleDataFE], -) -> Vec { - let mut output: Vec = Vec::new(); - let vir_make = String::from(vir.make.to_lowercase().trim()); - let vir_model = String::from(vir.model.to_lowercase().trim()); - for fedat in fegov_data { - let fe_make = String::from(fedat.make.to_lowercase().trim()); - let fe_model = String::from(fedat.model.to_lowercase().trim()); - if fedat.year == vir.year && fe_make.eq(&vir_make) && fe_model.eq(&vir_model) { - output.push(fedat.clone()); - } - } - output -} - -#[cfg(feature = "full")] -/// Try to make a single vehicle using the provided data sets. -fn try_make_single_vehicle( - fe_gov_data: &VehicleDataFE, - epa_data: &VehicleDataEPA, - other_inputs: &OtherVehicleInputs, -) -> Option { - if epa_data == &VehicleDataEPA::default() { - return None; - } - let veh_pt_type: &str = match fe_gov_data.alt_veh_type.as_str() { - "Hybrid" => crate::vehicle::HEV, - "Plug-in Hybrid" => crate::vehicle::PHEV, - "EV" => crate::vehicle::BEV, - _ => crate::vehicle::CONV, - }; - - let fs_max_kw: f64; - let fc_max_kw: f64; - let fc_eff_type: String; - let fc_eff_map: Array1; - let mc_max_kw: f64; - let min_soc: f64; - let max_soc: f64; - let ess_dischg_to_fc_max_eff_perc: f64; - let mph_fc_on: f64; - let kw_demand_fc_on: f64; - let aux_kw: f64; - let trans_eff: f64; - let val_range_miles: f64; - let ess_max_kw: f64; - let ess_max_kwh: f64; - let fs_kwh: f64; - - let ref_veh: RustVehicle = Default::default(); - - if veh_pt_type == crate::vehicle::CONV { - fs_max_kw = 2000.0; - fs_kwh = other_inputs.fuel_tank_gal * ref_veh.props.kwh_per_gge; - fc_max_kw = epa_data.eng_pwr_hp as f64 / HP_PER_KW; - fc_eff_type = String::from(crate::vehicle::SI); - fc_eff_map = Array::from_vec(vec![ - 0.1, 0.12, 0.16, 0.22, 0.28, 0.33, 0.35, 0.36, 0.35, 0.34, 0.32, 0.3, - ]); - mc_max_kw = 0.0; - min_soc = 0.0; - max_soc = 1.0; - ess_dischg_to_fc_max_eff_perc = 0.0; - mph_fc_on = 55.0; - kw_demand_fc_on = 100.0; - aux_kw = 0.7; - trans_eff = 0.92; - val_range_miles = 0.0; - ess_max_kw = 0.0; - ess_max_kwh = 0.0; - } else if veh_pt_type == crate::vehicle::HEV { - fs_max_kw = 2000.0; - fs_kwh = other_inputs.fuel_tank_gal * ref_veh.props.kwh_per_gge; - fc_max_kw = other_inputs - .fc_max_kw - .unwrap_or(epa_data.eng_pwr_hp as f64 / HP_PER_KW); - fc_eff_type = String::from(crate::vehicle::ATKINSON); - fc_eff_map = Array::from_vec(vec![ - 0.10, 0.12, 0.28, 0.35, 0.375, 0.39, 0.40, 0.40, 0.38, 0.37, 0.36, 0.35, - ]); - min_soc = 0.0; - max_soc = 1.0; - ess_dischg_to_fc_max_eff_perc = 0.0; - mph_fc_on = 1.0; - kw_demand_fc_on = 100.0; - aux_kw = 0.5; - trans_eff = 0.95; - val_range_miles = 0.0; - ess_max_kw = other_inputs.ess_max_kw; - ess_max_kwh = other_inputs.ess_max_kwh; - mc_max_kw = other_inputs.mc_max_kw; - } else if veh_pt_type == crate::vehicle::PHEV { - fs_max_kw = 2000.0; - fs_kwh = other_inputs.fuel_tank_gal * ref_veh.props.kwh_per_gge; - fc_max_kw = other_inputs - .fc_max_kw - .unwrap_or(epa_data.eng_pwr_hp as f64 / HP_PER_KW); - fc_eff_type = String::from(crate::vehicle::ATKINSON); - fc_eff_map = Array::from_vec(vec![ - 0.10, 0.12, 0.28, 0.35, 0.375, 0.39, 0.40, 0.40, 0.38, 0.37, 0.36, 0.35, - ]); - min_soc = 0.0; - max_soc = 1.0; - ess_dischg_to_fc_max_eff_perc = 1.0; - mph_fc_on = 85.0; - kw_demand_fc_on = 120.0; - aux_kw = 0.3; - trans_eff = 0.98; - val_range_miles = 0.0; - ess_max_kw = other_inputs.ess_max_kw; - ess_max_kwh = other_inputs.ess_max_kwh; - mc_max_kw = other_inputs.mc_max_kw; - } else if veh_pt_type == crate::vehicle::BEV { - fs_max_kw = 0.0; - fs_kwh = 0.0; - fc_max_kw = 0.0; - fc_eff_type = String::from(crate::vehicle::SI); - fc_eff_map = Array::from_vec(vec![ - 0.10, 0.12, 0.16, 0.22, 0.28, 0.33, 0.35, 0.36, 0.35, 0.34, 0.32, 0.30, - ]); - mc_max_kw = epa_data.eng_pwr_hp as f64 / HP_PER_KW; - min_soc = 0.0; - max_soc = 1.0; - ess_max_kw = 1.05 * mc_max_kw; - ess_max_kwh = other_inputs.ess_max_kwh; - mph_fc_on = 1.0; - kw_demand_fc_on = 100.0; - aux_kw = 0.25; - trans_eff = 0.98; - val_range_miles = fe_gov_data.range_ev as f64; - ess_dischg_to_fc_max_eff_perc = 0.0; - } else { - println!("Unhandled vehicle powertrain type: {veh_pt_type}"); - return None; - } - - // TODO: fix glider_kg calculation - // https://github.com/NREL/fastsim/pull/30#issuecomment-1841413126 - // - // let glider_kg = (epa_data.test_weight_lbs / LBS_PER_KG) - // - ref_veh.cargo_kg - // - ref_veh.trans_kg - // - ref_veh.comp_mass_multiplier - // * ((fs_max_kw / ref_veh.fs_kwh_per_kg) - // + (ref_veh.fc_base_kg + fc_max_kw / ref_veh.fc_kw_per_kg) - // + (ref_veh.mc_pe_base_kg + mc_max_kw * ref_veh.mc_pe_kg_per_kw) - // + (ref_veh.ess_base_kg + ess_max_kwh * ref_veh.ess_kg_per_kwh)); - let mut veh = RustVehicle { - veh_override_kg: Some(epa_data.test_weight_lbs / LBS_PER_KG), - veh_cg_m: match fe_gov_data.drive.as_str() { - "Front-Wheel Drive" => 0.53, - _ => -0.53, - }, - // glider_kg, - scenario_name: format!( - "{} {} {}", - fe_gov_data.year, fe_gov_data.make, fe_gov_data.model - ), - max_roadway_chg_kw: Default::default(), - selection: 0, - veh_year: fe_gov_data.year, - veh_pt_type: String::from(veh_pt_type), - drag_coef: 0.0, // overridden - frontal_area_m2: 0.85 * (other_inputs.vehicle_width_in * other_inputs.vehicle_height_in) - / (IN_PER_M * IN_PER_M), - fs_kwh, - idle_fc_kw: 0.0, - mc_eff_map: Array1::::zeros(LARGE_BASELINE_EFF.len()), - wheel_rr_coef: 0.0, // overridden - stop_start: fe_gov_data.start_stop == "Y", - force_aux_on_fc: false, - val_udds_mpgge: fe_gov_data.city_mpg_fuel1, - val_hwy_mpgge: fe_gov_data.highway_mpg_fuel1, - val_comb_mpgge: fe_gov_data.comb_mpg_fuel1, - fc_peak_eff_override: None, - mc_peak_eff_override: Some(0.95), - fs_max_kw, - fc_max_kw, - fc_eff_type, - fc_eff_map, - mc_max_kw, - min_soc, - max_soc, - ess_dischg_to_fc_max_eff_perc, - mph_fc_on, - kw_demand_fc_on, - aux_kw, - trans_eff, - val_range_miles, - ess_max_kwh, - ess_max_kw, - ..Default::default() - }; - veh.set_derived().unwrap(); - - abc_to_drag_coeffs( - &mut veh, - epa_data.a_lbf, - epa_data.b_lbf_per_mph, - epa_data.c_lbf_per_mph2, - Some(false), - None, - None, - Some(true), - Some(false), - ); - Some(veh) -} - -#[cfg(feature = "full")] -fn try_import_vehicles( - vir: &VehicleInputRecord, - fegov_data: &[VehicleDataFE], - epatest_data: &[VehicleDataEPA], -) -> Vec { - let other_inputs = vir_to_other_inputs(vir); - // TODO: Aaron wanted custom scenario name option - let mut outputs: Vec = Vec::new(); - let fegov_hits: Vec = - get_fuel_economy_gov_data_for_input_record(vir, fegov_data); - for hit in fegov_hits { - if let Some(epa_data) = match_epatest_with_fegov_v2(&hit, epatest_data) { - if let Some(v) = try_make_single_vehicle(&hit, &epa_data, &other_inputs) { - let mut v = v.clone(); - if hit.alt_veh_type == *"EV" { - v.scenario_name = format!("{} (EV)", v.scenario_name); - } else { - let alt_type = if hit.alt_veh_type.is_empty() { - String::from("") - } else { - format!("{}, ", hit.alt_veh_type) - }; - v.scenario_name = format!( - "{} ( {} {} cylinders, {} L, {} )", - v.scenario_name, alt_type, hit.cylinders, hit.displ, hit.transmission - ); - } - outputs.push(v); - } else { - println!( - "Unable to create vehicle for {}-{}-{}", - vir.year, vir.make, vir.model - ); - } - } else { - println!( - "Did not match any EPA data for {}-{}-{}...", - vir.year, vir.make, vir.model - ); - } - } - outputs -} - -#[cfg_attr(feature = "pyo3", pyfunction)] -/// Export the given RustVehicle to file -/// -/// veh: The RustVehicle to export -/// file_path: the path to export to -/// -/// NOTE: the file extension is used to determine the export format. -/// Supported file types include yaml and JSON -/// -/// RETURN: -/// () -pub fn export_vehicle_to_file(veh: &RustVehicle, file_path: String) -> anyhow::Result<()> { - let processed_path = PathBuf::from(file_path); - let path_str = processed_path.to_str().unwrap_or(""); - veh.to_file(path_str)?; - Ok(()) -} - #[allow(non_snake_case)] #[cfg_attr(feature = "pyo3", pyfunction)] #[allow(clippy::too_many_arguments)] @@ -1138,7 +50,7 @@ pub fn abc_to_drag_coeffs( // show_plots: if True, plots are shown let air_props: AirProperties = AirProperties::default(); - let props: RustPhysicalProperties = RustPhysicalProperties::default(); + let props = RustPhysicalProperties::default(); let cur_ambient_air_density_kg__m3: f64 = if custom_rho.unwrap_or(false) { air_props.get_rho(custom_rho_temp_degC.unwrap_or(20.0), custom_rho_elevation_m) } else { @@ -1276,162 +188,6 @@ impl CostFunction for GetError<'_> { } } -#[derive(Debug, Serialize, Deserialize, Clone)] -pub struct VehicleInputRecord { - pub make: String, - pub model: String, - pub year: u32, - pub output_file_name: String, - pub vehicle_width_in: f64, - pub vehicle_height_in: f64, - pub fuel_tank_gal: f64, - pub ess_max_kwh: f64, - pub mc_max_kw: f64, - pub ess_max_kw: f64, - pub fc_max_kw: Option, -} - -/// Transltate a VehicleInputRecord to OtherVehicleInputs -fn vir_to_other_inputs(vir: &VehicleInputRecord) -> OtherVehicleInputs { - OtherVehicleInputs { - vehicle_width_in: vir.vehicle_width_in, - vehicle_height_in: vir.vehicle_height_in, - fuel_tank_gal: vir.fuel_tank_gal, - ess_max_kwh: vir.ess_max_kwh, - mc_max_kw: vir.mc_max_kw, - ess_max_kw: vir.ess_max_kw, - fc_max_kw: vir.fc_max_kw, - } -} - -fn read_vehicle_input_records_from_file( - filepath: &Path, -) -> anyhow::Result> { - let f = File::open(filepath)?; - read_records_from_file(f) -} - -fn read_records_from_file( - rdr: impl std::io::Read + std::io::Seek, -) -> anyhow::Result> { - let mut output: Vec = Vec::new(); - let mut reader = csv::Reader::from_reader(rdr); - for result in reader.deserialize() { - let record: T = result?; - output.push(record); - } - Ok(output) -} - -fn read_fuelecon_gov_emissions_to_hashmap( - rdr: impl std::io::Read + std::io::Seek, -) -> HashMap> { - let mut output: HashMap> = HashMap::new(); - let mut reader = csv::Reader::from_reader(rdr); - for result in reader.deserialize() { - if result.is_ok() { - let ok_result: Option> = result.ok(); - if let Some(item) = ok_result { - if let Some(id_str) = item.get("id") { - if let Ok(id) = str::parse::(id_str) { - output.entry(id).or_default(); - if let Some(ers) = output.get_mut(&id) { - let emiss = EmissionsInfoFE { - efid: item.get("efid").unwrap().clone(), - score: item.get("score").unwrap().parse().unwrap(), - smartway_score: item.get("smartwayScore").unwrap().parse().unwrap(), - standard: item.get("standard").unwrap().clone(), - std_text: item.get("stdText").unwrap().clone(), - }; - ers.push(emiss); - } - } - } - } - } - } - output -} - -fn read_fuelecon_gov_data_from_file( - rdr: impl std::io::Read + std::io::Seek, - emissions: &HashMap>, -) -> anyhow::Result> { - let mut output: Vec = Vec::new(); - let mut reader = csv::Reader::from_reader(rdr); - for result in reader.deserialize() { - let item: HashMap = result?; - let id: u32 = item.get("id").unwrap().parse::().unwrap(); - let emissions_list: EmissionsListFE = if emissions.contains_key(&id) { - EmissionsListFE { - emissions_info: emissions.get(&id).unwrap().to_vec(), - } - } else { - EmissionsListFE::default() - }; - let vd = VehicleDataFE { - id: item.get("id").unwrap().trim().parse().unwrap(), - - year: item.get("year").unwrap().parse::().unwrap(), - make: item.get("make").unwrap().clone(), - model: item.get("model").unwrap().clone(), - - veh_class: item.get("VClass").unwrap().clone(), - - drive: item.get("drive").unwrap().clone(), - alt_veh_type: item.get("atvType").unwrap().clone(), - - fuel_type: item.get("fuelType").unwrap().clone(), - fuel1: item.get("fuelType1").unwrap().clone(), - fuel2: item.get("fuelType2").unwrap().clone(), - - eng_dscr: item.get("eng_dscr").unwrap().clone(), - cylinders: item.get("cylinders").unwrap().clone(), - displ: item.get("displ").unwrap().clone(), - transmission: item.get("trany").unwrap().clone(), - - super_charger: item.get("sCharger").unwrap().clone(), - turbo_charger: item.get("tCharger").unwrap().clone(), - - start_stop: item.get("startStop").unwrap().clone(), - - phev_blended: item - .get("phevBlended") - .unwrap() - .trim() - .to_lowercase() - .parse::() - .unwrap(), - phev_city_mpge: item.get("phevCity").unwrap().parse::().unwrap(), - phev_comb_mpge: item.get("phevComb").unwrap().parse::().unwrap(), - phev_hwy_mpge: item.get("phevHwy").unwrap().parse::().unwrap(), - - ev_motor_kw: item.get("evMotor").unwrap().clone(), - range_ev: item.get("range").unwrap().parse::().unwrap(), - - city_mpg_fuel1: item.get("city08U").unwrap().parse::().unwrap(), - city_mpg_fuel2: item.get("cityA08U").unwrap().parse::().unwrap(), - unadj_city_mpg_fuel1: item.get("UCity").unwrap().parse::().unwrap(), - unadj_city_mpg_fuel2: item.get("UCityA").unwrap().parse::().unwrap(), - city_kwh_per_100mi: item.get("cityE").unwrap().parse::().unwrap(), - - highway_mpg_fuel1: item.get("highway08U").unwrap().parse::().unwrap(), - highway_mpg_fuel2: item.get("highwayA08U").unwrap().parse::().unwrap(), - unadj_highway_mpg_fuel1: item.get("UHighway").unwrap().parse::().unwrap(), - unadj_highway_mpg_fuel2: item.get("UHighwayA").unwrap().parse::().unwrap(), - highway_kwh_per_100mi: item.get("highwayE").unwrap().parse::().unwrap(), - - comb_mpg_fuel1: item.get("comb08U").unwrap().parse::().unwrap(), - comb_mpg_fuel2: item.get("combA08U").unwrap().parse::().unwrap(), - comb_kwh_per_100mi: item.get("combE").unwrap().parse::().unwrap(), - - emissions_list, - }; - output.push(vd); - } - Ok(output) -} - /// Given the path to a zip archive, print out the names of the files within that archive pub fn list_zip_contents(filepath: &Path) -> anyhow::Result<()> { let f = File::open(filepath)?; @@ -1451,344 +207,8 @@ pub fn extract_zip(filepath: &Path, dest_dir: &Path) -> anyhow::Result<()> { Ok(()) } -#[cfg(feature = "full")] -/// Assumes the parent directory exists. Assumes file doesn't exist (i.e., newly created) or that it will be truncated if it does. -pub fn download_file_from_url(url: &str, file_path: &Path) -> anyhow::Result<()> { - let mut handle = Easy::new(); - handle.follow_location(true)?; - handle.url(url)?; - let mut buffer = Vec::new(); - { - let mut transfer = handle.transfer(); - transfer.write_function(|data| { - buffer.extend_from_slice(data); - Ok(data.len()) - })?; - let result = transfer.perform(); - if result.is_err() { - return Err(anyhow!("Could not download from {}", url)); - } - } - println!("Downloaded data from {}; bytes: {}", url, buffer.len()); - if buffer.is_empty() { - return Err(anyhow!("No data available from {url}")); - } - { - let mut file = match File::create(file_path) { - Err(why) => { - return Err(anyhow!( - "couldn't open {}: {}", - file_path.to_str().unwrap(), - why - )) - } - Ok(file) => file, - }; - file.write_all(buffer.as_slice())?; - } - Ok(()) -} - -fn read_epa_test_data_for_given_years( - data_dir_path: &Path, - years: &HashSet, -) -> anyhow::Result>> { - let mut epatest_db: HashMap> = HashMap::new(); - for year in years { - let file_name = format!("{year}-testcar.csv"); - let p = data_dir_path.join(Path::new(&file_name)); - let f = File::open(p)?; - let records = read_records_from_file(f)?; - epatest_db.insert(*year, records); - } - Ok(epatest_db) -} - -fn determine_model_years_of_interest(virs: &[VehicleInputRecord]) -> HashSet { - HashSet::from_iter(virs.iter().map(|vir| vir.year)) -} - -fn load_emissions_data_for_given_years( - data_dir_path: &Path, - years: &HashSet, -) -> anyhow::Result>>> { - let mut data = HashMap::>>::new(); - for year in years { - let file_name = format!("{year}-emissions.csv"); - let emissions_path = data_dir_path.join(Path::new(&file_name)); - if !emissions_path.exists() { - // download from URL and cache - println!( - "DATA DOES NOT EXIST AT {}", - emissions_path.to_string_lossy() - ); - } - let emissions_db: HashMap> = { - let emissions_file = File::open(emissions_path)?; - read_fuelecon_gov_emissions_to_hashmap(emissions_file) - }; - data.insert(*year, emissions_db); - } - Ok(data) -} - -fn load_fegov_data_for_given_years( - data_dir_path: &Path, - emissions_by_year_and_by_id: &HashMap>>, - years: &HashSet, -) -> anyhow::Result>> { - let mut data = HashMap::>::new(); - for year in years { - if let Some(emissions_by_id) = emissions_by_year_and_by_id.get(year) { - let file_name = format!("{year}-vehicles.csv"); - let fegov_path = data_dir_path.join(Path::new(&file_name)); - let fegov_db: Vec = { - let fegov_file = File::open(fegov_path.as_path())?; - read_fuelecon_gov_data_from_file(fegov_file, emissions_by_id)? - }; - data.insert(*year, fegov_db); - } else { - println!("No fe.gov emissions data available for {year}"); - } - } - Ok(data) -} - -pub fn get_default_cache_url() -> String { - String::from("https://github.com/NREL/vehicle-data/raw/main/") -} - -fn get_cache_url_for_year(cache_url: &str, year: &u32) -> anyhow::Result> { - let maybe_slash = if cache_url.ends_with('/') { "" } else { "/" }; - let target_url = format!("{cache_url}{maybe_slash}{year}.zip"); - Ok(Some(target_url)) -} - -fn extract_file_from_zip( - zip_file_path: &Path, - name_of_file_to_extract: &str, - path_to_save_to: &Path, -) -> anyhow::Result<()> { - let zipfile = File::open(zip_file_path)?; - let mut archive = ZipArchive::new(zipfile)?; - let mut file = archive.by_name(name_of_file_to_extract)?; - let mut contents = String::new(); - file.read_to_string(&mut contents)?; - std::fs::write(path_to_save_to, contents)?; - Ok(()) -} - -#[cfg(feature = "full")] -/// Checks the cache directory to see if data files have been downloaded -/// If so, moves on without any further action. -/// If not, downloads data by year from remote site if it exists -fn populate_cache_for_given_years_if_needed( - data_dir_path: &Path, - years: &HashSet, - cache_url: &str, -) -> anyhow::Result { - let mut all_data_available = true; - for year in years { - let veh_file_exists = { - let name = format!("{year}-vehicles.csv"); - let path = data_dir_path.join(Path::new(&name)); - path.exists() - }; - let emissions_file_exists = { - let name = format!("{year}-emissions.csv"); - let path = data_dir_path.join(Path::new(&name)); - path.exists() - }; - let epa_file_exists = { - let name = format!("{year}-testcar.csv"); - let path = data_dir_path.join(Path::new(&name)); - path.exists() - }; - if !veh_file_exists || !emissions_file_exists || !epa_file_exists { - all_data_available = false; - let zip_file_name = format!("{year}.zip"); - let zip_file_path = data_dir_path.join(Path::new(&zip_file_name)); - if let Some(url) = get_cache_url_for_year(cache_url, year)? { - println!("Downloading data for {year}: {url}"); - download_file_from_url(&url, &zip_file_path)?; - println!("... downloading data for {year}"); - let emissions_name = format!("{year}-emissions.csv"); - extract_file_from_zip( - zip_file_path.as_path(), - &emissions_name, - data_dir_path.join(Path::new(&emissions_name)).as_path(), - )?; - println!("... extracted {}", emissions_name); - let vehicles_name = format!("{year}-vehicles.csv"); - extract_file_from_zip( - zip_file_path.as_path(), - &vehicles_name, - data_dir_path.join(Path::new(&vehicles_name)).as_path(), - )?; - println!("... extracted {}", vehicles_name); - let epatests_name = format!("{year}-testcar.csv"); - extract_file_from_zip( - zip_file_path.as_path(), - &epatests_name, - data_dir_path.join(Path::new(&epatests_name)).as_path(), - )?; - println!("... extracted {}", epatests_name); - all_data_available = true; - } - } - } - Ok(all_data_available) -} - -#[cfg_attr(feature = "pyo3", pyfunction)] -#[cfg(feature = "full")] -/// Import All Vehicles for the given Year, Make, and Model and supplied other inputs -pub fn import_all_vehicles( - year: u32, - make: &str, - model: &str, - other_inputs: &OtherVehicleInputs, - cache_url: Option, - data_dir: Option, -) -> anyhow::Result> { - let vir = VehicleInputRecord { - year, - make: make.to_string(), - model: model.to_string(), - output_file_name: String::from(""), - vehicle_width_in: other_inputs.vehicle_width_in, - vehicle_height_in: other_inputs.vehicle_height_in, - fuel_tank_gal: other_inputs.fuel_tank_gal, - ess_max_kwh: other_inputs.ess_max_kwh, - mc_max_kw: other_inputs.mc_max_kw, - ess_max_kw: other_inputs.ess_max_kw, - fc_max_kw: other_inputs.fc_max_kw, - }; - let inputs = vec![vir]; - let model_years = { - let mut h: HashSet = HashSet::new(); - h.insert(year); - h - }; - let data_dir_path = if let Some(dd_path) = data_dir { - PathBuf::from(dd_path.clone()) - } else { - create_project_subdir("fe_label_data")? - }; - let data_dir_path = data_dir_path.as_path(); - let cache_url = if let Some(cache_url) = &cache_url { - cache_url.clone() - } else { - get_default_cache_url() - }; - let has_data = - populate_cache_for_given_years_if_needed(data_dir_path, &model_years, &cache_url)?; - if !has_data { - return Err(anyhow!( - "Unable to load or download cache data from {cache_url}" - )); - } - let emissions_data = load_emissions_data_for_given_years(data_dir_path, &model_years)?; - let fegov_data_by_year = - load_fegov_data_for_given_years(data_dir_path, &emissions_data, &model_years)?; - let epatest_db = read_epa_test_data_for_given_years(data_dir_path, &model_years)?; - let vehs = import_all_vehicles_from_record(&inputs, &fegov_data_by_year, &epatest_db) - .into_iter() - .map(|x| -> RustVehicle { x.1 }) - .collect(); - Ok(vehs) -} - -#[cfg(feature = "full")] -/// Import and Save All Vehicles Specified via Input File -pub fn import_and_save_all_vehicles_from_file( - input_path: &Path, - data_dir_path: &Path, - output_dir_path: &Path, - cache_url: Option, -) -> anyhow::Result<()> { - let cache_url = if let Some(url) = &cache_url { - url.clone() - } else { - get_default_cache_url() - }; - let inputs: Vec = read_vehicle_input_records_from_file(input_path)?; - println!("Found {} vehicle input records", inputs.len()); - let model_years = determine_model_years_of_interest(&inputs); - let has_data = - populate_cache_for_given_years_if_needed(data_dir_path, &model_years, &cache_url)?; - if !has_data { - return Err(anyhow!( - "Unable to load or download cache data from {cache_url}" - )); - } - let emissions_data = load_emissions_data_for_given_years(data_dir_path, &model_years)?; - let fegov_data_by_year = - load_fegov_data_for_given_years(data_dir_path, &emissions_data, &model_years)?; - let epatest_db = read_epa_test_data_for_given_years(data_dir_path, &model_years)?; - println!("Read {} files of epa test vehicle data", epatest_db.len()); - import_and_save_all_vehicles(&inputs, &fegov_data_by_year, &epatest_db, output_dir_path) -} - -#[cfg(feature = "full")] -pub fn import_all_vehicles_from_record( - inputs: &[VehicleInputRecord], - fegov_data_by_year: &HashMap>, - epatest_data_by_year: &HashMap>, -) -> Vec<(VehicleInputRecord, RustVehicle)> { - let mut vehs: Vec<(VehicleInputRecord, RustVehicle)> = Vec::new(); - for vir in inputs { - if let Some(fegov_data) = fegov_data_by_year.get(&vir.year) { - if let Some(epatest_data) = epatest_data_by_year.get(&vir.year) { - let vs = try_import_vehicles(vir, fegov_data, epatest_data); - for v in vs.iter() { - vehs.push((vir.clone(), v.clone())); - } - } else { - println!("No EPA test data available for year {}", vir.year); - } - } else { - println!("No FE.gov data available for year {}", vir.year); - } - } - vehs -} - -#[cfg(feature = "full")] -pub fn import_and_save_all_vehicles( - inputs: &[VehicleInputRecord], - fegov_data_by_year: &HashMap>, - epatest_data_by_year: &HashMap>, - output_dir_path: &Path, -) -> anyhow::Result<()> { - for (idx, (vir, veh)) in - import_all_vehicles_from_record(inputs, fegov_data_by_year, epatest_data_by_year) - .iter() - .enumerate() - { - let mut outfile: PathBuf = PathBuf::new(); - outfile.push(output_dir_path); - if idx > 0 { - let path = Path::new(&vir.output_file_name); - let stem = path.file_stem().unwrap().to_str().unwrap(); - let ext = path.extension().unwrap().to_str().unwrap(); - let output_file_name = format!("{stem}-{idx}.{ext}"); - println!("Multiple configurations found: output_file_name = {output_file_name}"); - outfile.push(Path::new(&output_file_name)); - } else { - outfile.push(Path::new(&vir.output_file_name)); - } - if let Some(full_outfile) = outfile.to_str() { - veh.to_file(full_outfile)?; - } else { - println!("Could not determine output file path"); - } - } - Ok(()) -} - #[cfg(test)] -mod vehicle_utils_tests { +mod tests { use super::*; #[test] @@ -1830,224 +250,4 @@ mod vehicle_utils_tests { assert_eq!(drag_coef, veh.drag_coef); assert_eq!(wheel_rr_coef, veh.wheel_rr_coef); } - - #[cfg(feature = "full")] - #[test] - fn test_create_new_vehicle_from_input_data() { - let veh_record = VehicleInputRecord { - make: String::from("Toyota"), - model: String::from("Camry"), - year: 2020, - output_file_name: String::from("2020-toyota-camry.yaml"), - vehicle_width_in: 72.4, - vehicle_height_in: 56.9, - fuel_tank_gal: 15.8, - ess_max_kwh: 0.0, - mc_max_kw: 0.0, - ess_max_kw: 0.0, - fc_max_kw: None, - }; - let emiss_info = vec![ - EmissionsInfoFE { - efid: String::from("LTYXV03.5M5B"), - score: 5.0, - smartway_score: -1, - standard: String::from("L3ULEV70"), - std_text: String::from("California LEV-III ULEV70"), - }, - EmissionsInfoFE { - efid: String::from("LTYXV03.5M5B"), - score: 5.0, - smartway_score: -1, - standard: String::from("T3B70"), - std_text: String::from("Federal Tier 3 Bin 70"), - }, - ]; - let emiss_list = EmissionsListFE { - emissions_info: emiss_info, - }; - let fegov_data = VehicleDataFE { - id: 32204, - - year: 2020, - make: String::from("Toyota"), - model: String::from("Camry"), - - veh_class: String::from("Midsize Cars"), - - drive: String::from("Front-Wheel Drive"), - alt_veh_type: String::from(""), - - fuel_type: String::from("Regular"), - fuel1: String::from("Regular Gasoline"), - fuel2: String::from(""), - - eng_dscr: String::from("SIDI & PFI"), - cylinders: String::from("6"), - displ: String::from("3.5"), - transmission: String::from("Automatic (S8)"), - - super_charger: String::from(""), - turbo_charger: String::from(""), - - start_stop: String::from("N"), - - phev_blended: false, - phev_city_mpge: 0, - phev_comb_mpge: 0, - phev_hwy_mpge: 0, - - ev_motor_kw: String::from(""), - range_ev: 0, - - city_mpg_fuel1: 16.4596, - city_mpg_fuel2: 0.0, - unadj_city_mpg_fuel1: 20.2988, - unadj_city_mpg_fuel2: 0.0, - city_kwh_per_100mi: 0.0, - - highway_mpg_fuel1: 22.5568, - highway_mpg_fuel2: 0.0, - unadj_highway_mpg_fuel1: 30.1798, - unadj_highway_mpg_fuel2: 0.0, - highway_kwh_per_100mi: 0.0, - - comb_mpg_fuel1: 18.7389, - comb_mpg_fuel2: 0.0, - comb_kwh_per_100mi: 0.0, - - emissions_list: emiss_list, - }; - let epatest_data = VehicleDataEPA { - year: 2020, - make: String::from("TOYOTA"), - model: String::from("CAMRY"), - test_id: String::from("JTYXV03.5M5B"), - displ: 3.456, - eng_pwr_hp: 301, - cylinders: String::from("6"), - transmission_code: String::from("A"), - transmission_type: String::from("Automatic"), - gears: 8, - drive_code: String::from("F"), - drive: String::from("2-Wheel Drive, Front"), - test_weight_lbs: 3875.0, - test_fuel_type: String::from("61"), - a_lbf: 24.843, - b_lbf_per_mph: 0.40298, - c_lbf_per_mph2: 0.015068, - }; - let other_inputs = vir_to_other_inputs(&veh_record); - let v = try_make_single_vehicle(&fegov_data, &epatest_data, &other_inputs); - assert!(v.is_some()); - if let Some(vs) = v { - assert_eq!(vs.scenario_name, String::from("2020 Toyota Camry")); - assert_eq!(vs.val_comb_mpgge, 18.7389); - } - } - - #[cfg(feature = "full")] - #[test] - fn test_get_options_for_year_make_model() { - let year = String::from("2020"); - let make = String::from("Toyota"); - let model = String::from("Corolla"); - let res = get_options_for_year_make_model(&year, &make, &model, None, None); - if let Err(err) = &res { - panic!("{:?}", err); - } else if let Ok(vs) = &res { - assert!(!vs.is_empty()); - } - } - - #[cfg(feature = "full")] - #[test] - fn test_import_robustness() { - // Ensure 2019 data is cached - let ddpath = create_project_subdir("fe_label_data").unwrap(); - let model_year: u32 = 2019; - let years = { - let mut s = HashSet::new(); - s.insert(model_year); - s - }; - let cache_url = get_default_cache_url(); - populate_cache_for_given_years_if_needed(ddpath.as_path(), &years, &cache_url).unwrap(); - // Load all year/make/models for 2019 - let vehicles_path = ddpath.join(Path::new("2019-vehicles.csv")); - let veh_records = { - let file = File::open(vehicles_path); - if let Ok(f) = file { - let data_result: anyhow::Result>> = - read_records_from_file(f); - if let Ok(data) = data_result { - data - } else { - vec![] - } - } else { - vec![] - } - }; - let mut num_success: usize = 0; - let other_inputs = OtherVehicleInputs { - vehicle_height_in: 72.4, - vehicle_width_in: 56.9, - fuel_tank_gal: 15.8, - ess_max_kwh: 0.0, - mc_max_kw: 0.0, - ess_max_kw: 0.0, - fc_max_kw: None, - }; - let mut num_records = 0; - let max_iter = veh_records.len(); - // NOTE: below, we can use fewer records in the interest of time as this is a long test with all records - // We skip because the vehicles at the beginning of the file tend to be more exotic and to not have - // EPA test entries. Thus, they are a bad representation of the whole. - let skip_idx: usize = 200; - for (num_iter, vr) in veh_records.iter().enumerate() { - if num_iter % skip_idx != 0 { - continue; - } - if num_iter >= max_iter { - break; - } - let make = vr.get("make"); - let model = vr.get("model"); - if let (Some(make), Some(model)) = (make, model) { - let result = - import_all_vehicles(model_year, make, model, &other_inputs, None, None); - if let Ok(vehs) = &result { - if !vehs.is_empty() { - num_success += 1; - } - } - } else { - panic!("Unable to find make and model in vehicle record"); - } - num_records += 1; - } - let success_frac: f64 = (num_success as f64) / (num_records as f64); - assert!(success_frac > 0.90, "success_frac = {}", success_frac); - } - - #[cfg(feature = "full")] - #[test] - fn test_get_options_for_year_make_model_for_specified_cacheurl_and_data_dir() { - let year = String::from("2020"); - let make = String::from("Toyota"); - let model = String::from("Corolla"); - let temp_dir = tempfile::tempdir().unwrap(); - let data_dir = temp_dir.path(); - let cacheurl = get_default_cache_url(); - assert!(!get_options_for_year_make_model( - &year, - &make, - &model, - Some(cacheurl), - Some(data_dir.to_str().unwrap().to_string()), - ) - .unwrap() - .is_empty()); - } } diff --git a/rust/fastsim-py/Cargo.toml b/rust/fastsim-py/Cargo.toml index 620d4c7f..5c17c725 100644 --- a/rust/fastsim-py/Cargo.toml +++ b/rust/fastsim-py/Cargo.toml @@ -28,6 +28,13 @@ include = ["../../NOTICE"] [features] default = ["full"] -full = ["fastsim-core/full", "dep:pyo3-log", "resources", "validation"] +full = [ + "dep:pyo3-log", + "fastsim-core/full", + "resources", + "validation", + "vehicle-import", +] resources = ["fastsim-core/resources"] validation = ["fastsim-core/validation"] +vehicle-import = ["fastsim-core/vehicle-import"] diff --git a/rust/fastsim-py/src/lib.rs b/rust/fastsim-py/src/lib.rs index 90e3bf52..afd38353 100644 --- a/rust/fastsim-py/src/lib.rs +++ b/rust/fastsim-py/src/lib.rs @@ -29,7 +29,7 @@ fn fastsimrust(py: Python, m: &PyModule) -> PyResult<()> { m.add_class::()?; m.add_class::()?; m.add_class::()?; - m.add_class::()?; + m.add_class::()?; m.add_class::()?; m.add_class::()?; m.add_class::()?; @@ -45,19 +45,18 @@ fn fastsimrust(py: Python, m: &PyModule) -> PyResult<()> { m.add_function(wrap_pyfunction!(get_label_fe_phev_py, m)?)?; #[cfg(feature = "full")] m.add_function(wrap_pyfunction!(get_label_fe_conv_py, m)?)?; - #[cfg(feature = "full")] + #[cfg(feature = "vehicle-import")] m.add_function(wrap_pyfunction!( - vehicle_utils::get_options_for_year_make_model, + vehicle_import::get_options_for_year_make_model, m )?)?; - #[cfg(feature = "full")] + #[cfg(feature = "vehicle-import")] m.add_function(wrap_pyfunction!( - vehicle_utils::vehicle_import_by_id_and_year, + vehicle_import::vehicle_import_by_id_and_year, m )?)?; - #[cfg(feature = "full")] - m.add_function(wrap_pyfunction!(vehicle_utils::import_all_vehicles, m)?)?; - m.add_function(wrap_pyfunction!(vehicle_utils::export_vehicle_to_file, m)?)?; + #[cfg(feature = "vehicle-import")] + m.add_function(wrap_pyfunction!(vehicle_import::import_all_vehicles, m)?)?; m.add_function(wrap_pyfunction!(enabled_features, m)?)?; From 01b8b9d8913cef950b61c387d5d4b5b98b29009b Mon Sep 17 00:00:00 2001 From: Kyle Carow Date: Thu, 1 Feb 2024 14:03:44 -0700 Subject: [PATCH 14/30] fix failing demo --- python/fastsim/demos/vehicle_import_demo.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/python/fastsim/demos/vehicle_import_demo.py b/python/fastsim/demos/vehicle_import_demo.py index 673c7d60..f2f6c785 100644 --- a/python/fastsim/demos/vehicle_import_demo.py +++ b/python/fastsim/demos/vehicle_import_demo.py @@ -7,7 +7,9 @@ REQUIRED_FEATURE = "full" if __name__ == "__main__" and REQUIRED_FEATURE not in fastsimrust.enabled_features(): - raise NotImplementedError(f'Feature "{REQUIRED_FEATURE}" is required to run this demo') + raise NotImplementedError( + f'Feature "{REQUIRED_FEATURE}" is required to run this demo' + ) # %% # Preamble: Basic imports @@ -16,7 +18,7 @@ import fastsim.fastsimrust as fsr import fastsim.utils as utils -#for testing demo files, false when running automatic tests +# for testing demo files, false when running automatic tests SHOW_PLOTS = utils.show_plots() # %% @@ -73,7 +75,7 @@ rv = fsr.vehicle_import_by_id_and_year(data.id, int(year), other_inputs) -fsr.export_vehicle_to_file(rv, str(OUTPUT_DIR / "demo-vehicle.yaml")) +rv.to_file(OUTPUT_DIR / "demo-vehicle.yaml") # %% # Alternative API for importing all vehicles at once From 36769a703008e77335bbedc2c6f8597018cdaeb8 Mon Sep 17 00:00:00 2001 From: Kyle Carow Date: Fri, 2 Feb 2024 11:31:24 -0700 Subject: [PATCH 15/30] cleaned up some vehicle import syntax --- rust/fastsim-core/src/vehicle_import.rs | 188 ++++++++---------------- 1 file changed, 61 insertions(+), 127 deletions(-) diff --git a/rust/fastsim-core/src/vehicle_import.rs b/rust/fastsim-core/src/vehicle_import.rs index 13a73b0e..fd0eeee2 100644 --- a/rust/fastsim-core/src/vehicle_import.rs +++ b/rust/fastsim-core/src/vehicle_import.rs @@ -8,7 +8,6 @@ use std::collections::HashMap; use std::collections::HashSet; use std::io::prelude::Write; use std::io::Read; -use std::num::ParseIntError; use std::path::PathBuf; use zip::ZipArchive; @@ -314,22 +313,12 @@ pub fn get_options_for_year_make_model( h.insert(y); h }; - let ddpath = if let Some(dd) = data_dir { - PathBuf::from(dd) - } else { - create_project_subdir("fe_label_data")? - }; - let cache_url = if let Some(url) = &cache_url { - url.clone() - } else { - get_default_cache_url() - }; - let has_data = populate_cache_for_given_years_if_needed(ddpath.as_path(), &ys, &cache_url)?; - if !has_data { - return Err(anyhow!( - "Unable to load or download cache data from {cache_url}" - )); - } + // TODO: replace with unwrap_or_else + let ddpath = data_dir + .and_then(|path| Some(PathBuf::from(path))) + .unwrap_or(create_project_subdir("fe_label_data")?); + let cache_url = cache_url.unwrap_or_else(get_default_cache_url); + populate_cache_for_given_years_if_needed(ddpath.as_path(), &ys, &cache_url)?; let emissions_data = load_emissions_data_for_given_years(ddpath.as_path(), &ys)?; let fegov_data_by_year = load_fegov_data_for_given_years(ddpath.as_path(), &emissions_data, &ys)?; @@ -385,16 +374,10 @@ fn derive_transmission_specs(fegov: &VehicleDataFE) -> (u32, String) { .unwrap(); } else { transmission_fe_gov = String::from('A'); - num_gears_fe_gov = { - let res: Result = fegov.transmission.as_str() - [fegov.transmission.find("(A").unwrap() + 2..fegov.transmission.find(')').unwrap()] - .parse(); - if let Ok(n) = res { - n - } else { - 1 - } - } + num_gears_fe_gov = fegov.transmission.as_str() + [fegov.transmission.find("(A").unwrap() + 2..fegov.transmission.find(')').unwrap()] + .parse() + .unwrap_or(1); } (num_gears_fe_gov, transmission_fe_gov) } @@ -650,16 +633,10 @@ fn match_epatest_with_fegov( .unwrap(); } else { transmission_fe_gov = String::from('A'); - num_gears_fe_gov = { - let res: Result = fegov.transmission.as_str() - [fegov.transmission.find("(A").unwrap() + 2..fegov.transmission.find(')').unwrap()] - .parse(); - if let Ok(n) = res { - n - } else { - 1 - } - } + num_gears_fe_gov = fegov.transmission.as_str() + [fegov.transmission.find("(A").unwrap() + 2..fegov.transmission.find(')').unwrap()] + .parse() + .unwrap_or(1) } // Find EPA vehicle entry that matches fe.gov vehicle data @@ -744,7 +721,6 @@ pub struct OtherVehicleInputs { impl SerdeAPI for OtherVehicleInputs {} -#[cfg(feature = "full")] #[cfg_attr(feature = "pyo3", pyfunction)] /// Creates RustVehicle for the given vehicle using data from fueleconomy.gov and EPA databases /// The created RustVehicle is also written as a yaml file @@ -766,33 +742,21 @@ pub fn vehicle_import_by_id_and_year( data_dir: Option, ) -> anyhow::Result { let mut maybe_veh: Option = None; - let data_dir_path = if let Some(data_dir) = data_dir { - PathBuf::from(data_dir) - } else { - create_project_subdir("fe_label_data")? - }; - let data_dir_path = data_dir_path.as_path(); + // TODO: replace with unwrap_or_else + let data_dir_path = data_dir + .and_then(|path| Some(PathBuf::from(path))) + .unwrap_or(create_project_subdir("fe_label_data")?); let model_years = { let mut h: HashSet = HashSet::new(); h.insert(year); h }; - let cache_url = if let Some(cache_url) = &cache_url { - cache_url.clone() - } else { - get_default_cache_url() - }; - let has_data = - populate_cache_for_given_years_if_needed(data_dir_path, &model_years, &cache_url)?; - if !has_data { - return Err(anyhow!( - "Unable to load or download cache data from {cache_url}" - )); - } - let emissions_data = load_emissions_data_for_given_years(data_dir_path, &model_years)?; + let cache_url = cache_url.unwrap_or(get_default_cache_url()); + populate_cache_for_given_years_if_needed(&data_dir_path, &model_years, &cache_url)?; + let emissions_data = load_emissions_data_for_given_years(&data_dir_path, &model_years)?; let fegov_data_by_year = - load_fegov_data_for_given_years(data_dir_path, &emissions_data, &model_years)?; - let epatest_db = read_epa_test_data_for_given_years(data_dir_path, &model_years)?; + load_fegov_data_for_given_years(&data_dir_path, &emissions_data, &model_years)?; + let epatest_db = read_epa_test_data_for_given_years(&data_dir_path, &model_years)?; if let Some(fe_gov_data) = fegov_data_by_year.get(&year) { if let Some(epa_data) = epatest_db.get(&year) { let fe_gov_data = { @@ -839,7 +803,6 @@ fn get_fuel_economy_gov_data_for_input_record( output } -#[cfg(feature = "full")] /// Try to make a single vehicle using the provided data sets. fn try_make_single_vehicle( fe_gov_data: &VehicleDataFE, @@ -1033,7 +996,6 @@ fn try_make_single_vehicle( Some(veh) } -#[cfg(feature = "full")] fn try_import_vehicles( vir: &VehicleInputRecord, fegov_data: &[VehicleDataFE], @@ -1115,7 +1077,7 @@ fn read_vehicle_input_records_from_file( fn read_records_from_file( rdr: impl std::io::Read + std::io::Seek, ) -> anyhow::Result> { - let mut output: Vec = Vec::new(); + let mut output = Vec::new(); let mut reader = csv::Reader::from_reader(rdr); for result in reader.deserialize() { let record: T = result?; @@ -1232,16 +1194,14 @@ fn read_fuelecon_gov_data_from_file( } Ok(output) } -fn read_epa_test_data_for_given_years( - data_dir_path: &Path, +fn read_epa_test_data_for_given_years>( + data_dir_path: P, years: &HashSet, ) -> anyhow::Result>> { let mut epatest_db: HashMap> = HashMap::new(); for year in years { - let file_name = format!("{year}-testcar.csv"); - let p = data_dir_path.join(Path::new(&file_name)); - let f = File::open(p)?; - let records = read_records_from_file(f)?; + let p = data_dir_path.as_ref().join(format!("{year}-testcar.csv")); + let records = read_records_from_file(File::open(p)?)?; epatest_db.insert(*year, records); } Ok(epatest_db) @@ -1251,14 +1211,14 @@ fn determine_model_years_of_interest(virs: &[VehicleInputRecord]) -> HashSet>( + data_dir_path: P, years: &HashSet, ) -> anyhow::Result>>> { let mut data = HashMap::>>::new(); for year in years { let file_name = format!("{year}-emissions.csv"); - let emissions_path = data_dir_path.join(Path::new(&file_name)); + let emissions_path = data_dir_path.as_ref().join(file_name); if !emissions_path.exists() { // download from URL and cache println!( @@ -1275,8 +1235,8 @@ fn load_emissions_data_for_given_years( Ok(data) } -fn load_fegov_data_for_given_years( - data_dir_path: &Path, +fn load_fegov_data_for_given_years>( + data_dir_path: P, emissions_by_year_and_by_id: &HashMap>>, years: &HashSet, ) -> anyhow::Result>> { @@ -1284,7 +1244,7 @@ fn load_fegov_data_for_given_years( for year in years { if let Some(emissions_by_id) = emissions_by_year_and_by_id.get(year) { let file_name = format!("{year}-vehicles.csv"); - let fegov_path = data_dir_path.join(Path::new(&file_name)); + let fegov_path = data_dir_path.as_ref().join(file_name); let fegov_db: Vec = { let fegov_file = File::open(fegov_path.as_path())?; read_fuelecon_gov_data_from_file(fegov_file, emissions_by_id)? @@ -1297,7 +1257,7 @@ fn load_fegov_data_for_given_years( Ok(data) } #[cfg_attr(feature = "pyo3", pyfunction)] -#[cfg(feature = "full")] + /// Import All Vehicles for the given Year, Make, and Model and supplied other inputs pub fn import_all_vehicles( year: u32, @@ -1337,13 +1297,7 @@ pub fn import_all_vehicles( } else { get_default_cache_url() }; - let has_data = - populate_cache_for_given_years_if_needed(data_dir_path, &model_years, &cache_url)?; - if !has_data { - return Err(anyhow!( - "Unable to load or download cache data from {cache_url}" - )); - } + populate_cache_for_given_years_if_needed(data_dir_path, &model_years, &cache_url)?; let emissions_data = load_emissions_data_for_given_years(data_dir_path, &model_years)?; let fegov_data_by_year = load_fegov_data_for_given_years(data_dir_path, &emissions_data, &model_years)?; @@ -1355,7 +1309,6 @@ pub fn import_all_vehicles( Ok(vehs) } -#[cfg(feature = "full")] /// Import and Save All Vehicles Specified via Input File pub fn import_and_save_all_vehicles_from_file( input_path: &Path, @@ -1363,21 +1316,11 @@ pub fn import_and_save_all_vehicles_from_file( output_dir_path: &Path, cache_url: Option, ) -> anyhow::Result<()> { - let cache_url = if let Some(url) = &cache_url { - url.clone() - } else { - get_default_cache_url() - }; + let cache_url = cache_url.unwrap_or_else(get_default_cache_url); let inputs: Vec = read_vehicle_input_records_from_file(input_path)?; println!("Found {} vehicle input records", inputs.len()); let model_years = determine_model_years_of_interest(&inputs); - let has_data = - populate_cache_for_given_years_if_needed(data_dir_path, &model_years, &cache_url)?; - if !has_data { - return Err(anyhow!( - "Unable to load or download cache data from {cache_url}" - )); - } + populate_cache_for_given_years_if_needed(data_dir_path, &model_years, &cache_url)?; let emissions_data = load_emissions_data_for_given_years(data_dir_path, &model_years)?; let fegov_data_by_year = load_fegov_data_for_given_years(data_dir_path, &emissions_data, &model_years)?; @@ -1386,7 +1329,6 @@ pub fn import_and_save_all_vehicles_from_file( import_and_save_all_vehicles(&inputs, &fegov_data_by_year, &epatest_db, output_dir_path) } -#[cfg(feature = "full")] pub fn import_all_vehicles_from_record( inputs: &[VehicleInputRecord], fegov_data_by_year: &HashMap>, @@ -1410,7 +1352,6 @@ pub fn import_all_vehicles_from_record( vehs } -#[cfg(feature = "full")] pub fn import_and_save_all_vehicles( inputs: &[VehicleInputRecord], fegov_data_by_year: &HashMap>, @@ -1448,36 +1389,37 @@ fn get_cache_url_for_year(cache_url: &str, year: &u32) -> anyhow::Result>( + data_dir_path: P, years: &HashSet, cache_url: &str, -) -> anyhow::Result { +) -> anyhow::Result<()> { + let data_dir_path = data_dir_path.as_ref(); let mut all_data_available = true; for year in years { let veh_file_exists = { let name = format!("{year}-vehicles.csv"); - let path = data_dir_path.join(Path::new(&name)); + let path = data_dir_path.join(name); path.exists() }; let emissions_file_exists = { let name = format!("{year}-emissions.csv"); - let path = data_dir_path.join(Path::new(&name)); + let path = data_dir_path.join(name); path.exists() }; let epa_file_exists = { let name = format!("{year}-testcar.csv"); - let path = data_dir_path.join(Path::new(&name)); + let path = data_dir_path.join(name); path.exists() }; if !veh_file_exists || !emissions_file_exists || !epa_file_exists { all_data_available = false; let zip_file_name = format!("{year}.zip"); - let zip_file_path = data_dir_path.join(Path::new(&zip_file_name)); + let zip_file_path = data_dir_path.join(zip_file_name); if let Some(url) = get_cache_url_for_year(cache_url, year)? { println!("Downloading data for {year}: {url}"); download_file_from_url(&url, &zip_file_path)?; @@ -1486,28 +1428,32 @@ fn populate_cache_for_given_years_if_needed( extract_file_from_zip( zip_file_path.as_path(), &emissions_name, - data_dir_path.join(Path::new(&emissions_name)).as_path(), + data_dir_path.join(&emissions_name).as_path(), )?; println!("... extracted {}", emissions_name); let vehicles_name = format!("{year}-vehicles.csv"); extract_file_from_zip( zip_file_path.as_path(), &vehicles_name, - data_dir_path.join(Path::new(&vehicles_name)).as_path(), + data_dir_path.join(&vehicles_name).as_path(), )?; println!("... extracted {}", vehicles_name); let epatests_name = format!("{year}-testcar.csv"); extract_file_from_zip( zip_file_path.as_path(), &epatests_name, - data_dir_path.join(Path::new(&epatests_name)).as_path(), + data_dir_path.join(&epatests_name).as_path(), )?; println!("... extracted {}", epatests_name); all_data_available = true; } } } - Ok(all_data_available) + ensure!( + all_data_available, + "Unable to load or download cache data from {cache_url}" + ); + Ok(()) } fn extract_file_from_zip( @@ -1524,7 +1470,6 @@ fn extract_file_from_zip( Ok(()) } -#[cfg(feature = "full")] /// Assumes the parent directory exists. Assumes file doesn't exist (i.e., newly created) or that it will be truncated if it does. pub fn download_file_from_url(url: &str, file_path: &Path) -> anyhow::Result<()> { let mut handle = Easy::new(); @@ -1566,7 +1511,6 @@ pub fn download_file_from_url(url: &str, file_path: &Path) -> anyhow::Result<()> mod tests { use super::*; - #[cfg(feature = "full")] #[test] fn test_create_new_vehicle_from_input_data() { let veh_record = VehicleInputRecord { @@ -1673,29 +1617,20 @@ mod tests { c_lbf_per_mph2: 0.015068, }; let other_inputs = vir_to_other_inputs(&veh_record); - let v = try_make_single_vehicle(&fegov_data, &epatest_data, &other_inputs); - assert!(v.is_some()); - if let Some(vs) = v { - assert_eq!(vs.scenario_name, String::from("2020 Toyota Camry")); - assert_eq!(vs.val_comb_mpgge, 18.7389); - } + let v = try_make_single_vehicle(&fegov_data, &epatest_data, &other_inputs).unwrap(); + assert_eq!(v.scenario_name, String::from("2020 Toyota Camry")); + assert_eq!(v.val_comb_mpgge, 18.7389); } - #[cfg(feature = "full")] #[test] fn test_get_options_for_year_make_model() { let year = String::from("2020"); let make = String::from("Toyota"); let model = String::from("Corolla"); - let res = get_options_for_year_make_model(&year, &make, &model, None, None); - if let Err(err) = &res { - panic!("{:?}", err); - } else if let Ok(vs) = &res { - assert!(!vs.is_empty()); - } + let options = get_options_for_year_make_model(&year, &make, &model, None, None).unwrap(); + assert!(!options.is_empty()); } - #[cfg(feature = "full")] #[test] fn test_import_robustness() { // Ensure 2019 data is cached @@ -1709,7 +1644,7 @@ mod tests { let cache_url = get_default_cache_url(); populate_cache_for_given_years_if_needed(ddpath.as_path(), &years, &cache_url).unwrap(); // Load all year/make/models for 2019 - let vehicles_path = ddpath.join(Path::new("2019-vehicles.csv")); + let vehicles_path = ddpath.join("2019-vehicles.csv"); let veh_records = { let file = File::open(vehicles_path); if let Ok(f) = file { @@ -1766,7 +1701,6 @@ mod tests { assert!(success_frac > 0.90, "success_frac = {}", success_frac); } - #[cfg(feature = "full")] #[test] fn test_get_options_for_year_make_model_for_specified_cacheurl_and_data_dir() { let year = String::from("2020"); From 4fdecabe017a3a2848090f2ac82ae652cd7beb5c Mon Sep 17 00:00:00 2001 From: Kyle Carow Date: Fri, 2 Feb 2024 13:41:43 -0700 Subject: [PATCH 16/30] remove unnecessary type annotations --- rust/fastsim-core/src/vehicle_import.rs | 131 ++++++++++++------------ 1 file changed, 64 insertions(+), 67 deletions(-) diff --git a/rust/fastsim-core/src/vehicle_import.rs b/rust/fastsim-core/src/vehicle_import.rs index fd0eeee2..3fd59ed6 100644 --- a/rust/fastsim-core/src/vehicle_import.rs +++ b/rust/fastsim-core/src/vehicle_import.rs @@ -307,8 +307,8 @@ pub fn get_options_for_year_make_model( data_dir: Option, ) -> anyhow::Result> { // prep the cache for year - let y: u32 = year.trim().parse()?; - let ys: HashSet = { + let y = year.trim().parse()?; + let ys = { let mut h = HashSet::new(); h.insert(y); h @@ -323,7 +323,7 @@ pub fn get_options_for_year_make_model( let fegov_data_by_year = load_fegov_data_for_given_years(ddpath.as_path(), &emissions_data, &ys)?; if let Some(fegov_db) = fegov_data_by_year.get(&y) { - let mut hits: Vec = Vec::new(); + let mut hits = Vec::new(); for item in fegov_db.iter() { if item.make == make && item.model == model { hits.push(item.clone()); @@ -400,7 +400,7 @@ fn match_epatest_with_fegov_v2( fegov: &VehicleDataFE, epatest_data: &[VehicleDataEPA], ) -> Option { - let fe_model_upper: String = fegov.model.to_uppercase().replace("4WD", "AWD"); + let fe_model_upper = fegov.model.to_uppercase().replace("4WD", "AWD"); let fe_model_words: Vec<&str> = fe_model_upper.split_ascii_whitespace().collect(); let num_fe_model_words = fe_model_words.len(); let fegov_disp = fegov.displ.parse::().unwrap_or_default(); @@ -416,14 +416,12 @@ fn match_epatest_with_fegov_v2( if let Some(c) = maybe_char { s.push(c); } - s - } else { - s } + s }; let (num_gears_fe_gov, transmission_fe_gov) = derive_transmission_specs(fegov); let epa_candidates = { - let mut xs: Vec<(f64, f64, VehicleDataEPA)> = Vec::new(); + let mut xs = Vec::new(); for x in epatest_data { if x.year == fegov.year && x.make.eq_ignore_ascii_case(&fegov.make) { let mut score = 0.0; @@ -449,7 +447,7 @@ fn match_epatest_with_fegov_v2( for word in &epa_model_words { match_count += fe_model_words.contains(word) as i64; } - let match_frac: f64 = (match_count as f64 * match_count as f64) + let match_frac = (match_count as f64 * match_count as f64) / (num_epa_model_words as f64 * num_fe_model_words as f64); match_frac }; @@ -498,7 +496,7 @@ fn match_epatest_with_fegov_v2( } else { let mut largest_id_match_value = 0.0; let mut largest_score_value = 0.0; - let mut best_idx: usize = 0; + let mut best_idx = 0; for (idx, item) in epa_candidates.iter().enumerate() { if item.0 > largest_id_match_value || (item.0 == largest_id_match_value && item.1 > largest_score_value) @@ -528,19 +526,19 @@ fn match_epatest_with_fegov( // Keep track of best match to fueleconomy.gov model name for all vehicles and vehicles with matching efid/test id let mut veh_list_overall: HashMap> = HashMap::new(); let mut veh_list_efid: HashMap> = HashMap::new(); - let mut best_match_percent_efid: f64 = 0.0; - let mut best_match_model_efid: String = String::new(); - let mut best_match_percent_overall: f64 = 0.0; - let mut best_match_model_overall: String = String::new(); + let mut best_match_percent_efid = 0.0; + let mut best_match_model_efid = String::new(); + let mut best_match_percent_overall = 0.0; + let mut best_match_model_overall = String::new(); - let fe_model_upper: String = fegov.model.to_uppercase().replace("4WD", "AWD"); + let fe_model_upper = fegov.model.to_uppercase().replace("4WD", "AWD"); let fe_model_words: Vec<&str> = fe_model_upper.split(' ').collect(); let num_fe_model_words = fe_model_words.len(); - let efid: &String = &fegov.emissions_list.emissions_info[0].efid; + let efid = &fegov.emissions_list.emissions_info[0].efid; for veh_epa in epatest_data { // Find matches between EPA vehicle model name and fe.gov vehicle model name - let mut match_count: i64 = 0; + let mut match_count = 0; let epa_model_upper = veh_epa.model.to_uppercase().replace("4WD", "AWD"); let epa_model_words: Vec<&str> = epa_model_upper.split(' ').collect(); let num_epa_model_words = epa_model_words.len(); @@ -548,7 +546,7 @@ fn match_epatest_with_fegov( match_count += fe_model_words.contains(word) as i64; } // Calculate composite match percentage - let match_percent: f64 = (match_count as f64 * match_count as f64) + let match_percent = (match_count as f64 * match_count as f64) / (num_epa_model_words as f64 * num_fe_model_words as f64); // Update overall hashmap with new entry @@ -583,7 +581,7 @@ fn match_epatest_with_fegov( } // Get EPA vehicle model that is best match to fe.gov vehicle - let veh_list: Vec = if best_match_model_efid == best_match_model_overall { + let veh_list = if best_match_model_efid == best_match_model_overall { let x = veh_list_efid.get(&best_match_model_efid); x?; x.unwrap().to_vec() @@ -641,10 +639,10 @@ fn match_epatest_with_fegov( // Find EPA vehicle entry that matches fe.gov vehicle data // If same vehicle model has multiple configurations, get most common configuration - let mut most_common_veh: VehicleDataEPA = VehicleDataEPA::default(); - let mut most_common_count: i32 = 0; - let mut current_veh: VehicleDataEPA = VehicleDataEPA::default(); - let mut current_count: i32 = 0; + let mut most_common_veh = VehicleDataEPA::default(); + let mut most_common_count = 0; + let mut current_veh = VehicleDataEPA::default(); + let mut current_count = 0; for mut veh_epa in veh_list { if veh_epa.model.contains("4WD") || veh_epa.model.contains("AWD") @@ -741,13 +739,13 @@ pub fn vehicle_import_by_id_and_year( cache_url: Option, data_dir: Option, ) -> anyhow::Result { - let mut maybe_veh: Option = None; + let mut maybe_veh = None; // TODO: replace with unwrap_or_else let data_dir_path = data_dir .and_then(|path| Some(PathBuf::from(path))) .unwrap_or(create_project_subdir("fe_label_data")?); let model_years = { - let mut h: HashSet = HashSet::new(); + let mut h = HashSet::new(); h.insert(year); h }; @@ -790,7 +788,7 @@ fn get_fuel_economy_gov_data_for_input_record( vir: &VehicleInputRecord, fegov_data: &[VehicleDataFE], ) -> Vec { - let mut output: Vec = Vec::new(); + let mut output = Vec::new(); let vir_make = String::from(vir.make.to_lowercase().trim()); let vir_model = String::from(vir.model.to_lowercase().trim()); for fedat in fegov_data { @@ -812,7 +810,7 @@ fn try_make_single_vehicle( if epa_data == &VehicleDataEPA::default() { return None; } - let veh_pt_type: &str = match fe_gov_data.alt_veh_type.as_str() { + let veh_pt_type = match fe_gov_data.alt_veh_type.as_str() { "Hybrid" => crate::vehicle::HEV, "Plug-in Hybrid" => crate::vehicle::PHEV, "EV" => crate::vehicle::BEV, @@ -836,7 +834,7 @@ fn try_make_single_vehicle( let ess_max_kwh: f64; let fs_kwh: f64; - let ref_veh: RustVehicle = Default::default(); + let ref_veh = RustVehicle::default(); if veh_pt_type == crate::vehicle::CONV { fs_max_kw = 2000.0; @@ -954,7 +952,7 @@ fn try_make_single_vehicle( / (IN_PER_M * IN_PER_M), fs_kwh, idle_fc_kw: 0.0, - mc_eff_map: Array1::::zeros(LARGE_BASELINE_EFF.len()), + mc_eff_map: Array1::zeros(LARGE_BASELINE_EFF.len()), wheel_rr_coef: 0.0, // overridden stop_start: fe_gov_data.start_stop == "Y", force_aux_on_fc: false, @@ -1003,9 +1001,8 @@ fn try_import_vehicles( ) -> Vec { let other_inputs = vir_to_other_inputs(vir); // TODO: Aaron wanted custom scenario name option - let mut outputs: Vec = Vec::new(); - let fegov_hits: Vec = - get_fuel_economy_gov_data_for_input_record(vir, fegov_data); + let mut outputs = Vec::new(); + let fegov_hits = get_fuel_economy_gov_data_for_input_record(vir, fegov_data); for hit in fegov_hits { if let Some(epa_data) = match_epatest_with_fegov_v2(&hit, epatest_data) { if let Some(v) = try_make_single_vehicle(&hit, &epa_data, &other_inputs) { @@ -1080,7 +1077,7 @@ fn read_records_from_file( let mut output = Vec::new(); let mut reader = csv::Reader::from_reader(rdr); for result in reader.deserialize() { - let record: T = result?; + let record = result?; output.push(record); } Ok(output) @@ -1096,7 +1093,7 @@ fn read_fuelecon_gov_emissions_to_hashmap( let ok_result: Option> = result.ok(); if let Some(item) = ok_result { if let Some(id_str) = item.get("id") { - if let Ok(id) = str::parse::(id_str) { + if let Ok(id) = id_str.parse() { output.entry(id).or_default(); if let Some(ers) = output.get_mut(&id) { let emiss = EmissionsInfoFE { @@ -1120,12 +1117,12 @@ fn read_fuelecon_gov_data_from_file( rdr: impl std::io::Read + std::io::Seek, emissions: &HashMap>, ) -> anyhow::Result> { - let mut output: Vec = Vec::new(); + let mut output = Vec::new(); let mut reader = csv::Reader::from_reader(rdr); for result in reader.deserialize() { let item: HashMap = result?; - let id: u32 = item.get("id").unwrap().parse::().unwrap(); - let emissions_list: EmissionsListFE = if emissions.contains_key(&id) { + let id = item.get("id").unwrap().parse().unwrap(); + let emissions_list = if emissions.contains_key(&id) { EmissionsListFE { emissions_info: emissions.get(&id).unwrap().to_vec(), } @@ -1135,7 +1132,7 @@ fn read_fuelecon_gov_data_from_file( let vd = VehicleDataFE { id: item.get("id").unwrap().trim().parse().unwrap(), - year: item.get("year").unwrap().parse::().unwrap(), + year: item.get("year").unwrap().parse().unwrap(), make: item.get("make").unwrap().clone(), model: item.get("model").unwrap().clone(), @@ -1163,30 +1160,30 @@ fn read_fuelecon_gov_data_from_file( .unwrap() .trim() .to_lowercase() - .parse::() + .parse() .unwrap(), - phev_city_mpge: item.get("phevCity").unwrap().parse::().unwrap(), - phev_comb_mpge: item.get("phevComb").unwrap().parse::().unwrap(), - phev_hwy_mpge: item.get("phevHwy").unwrap().parse::().unwrap(), + phev_city_mpge: item.get("phevCity").unwrap().parse().unwrap(), + phev_comb_mpge: item.get("phevComb").unwrap().parse().unwrap(), + phev_hwy_mpge: item.get("phevHwy").unwrap().parse().unwrap(), ev_motor_kw: item.get("evMotor").unwrap().clone(), - range_ev: item.get("range").unwrap().parse::().unwrap(), + range_ev: item.get("range").unwrap().parse().unwrap(), - city_mpg_fuel1: item.get("city08U").unwrap().parse::().unwrap(), - city_mpg_fuel2: item.get("cityA08U").unwrap().parse::().unwrap(), - unadj_city_mpg_fuel1: item.get("UCity").unwrap().parse::().unwrap(), - unadj_city_mpg_fuel2: item.get("UCityA").unwrap().parse::().unwrap(), - city_kwh_per_100mi: item.get("cityE").unwrap().parse::().unwrap(), + city_mpg_fuel1: item.get("city08U").unwrap().parse().unwrap(), + city_mpg_fuel2: item.get("cityA08U").unwrap().parse().unwrap(), + unadj_city_mpg_fuel1: item.get("UCity").unwrap().parse().unwrap(), + unadj_city_mpg_fuel2: item.get("UCityA").unwrap().parse().unwrap(), + city_kwh_per_100mi: item.get("cityE").unwrap().parse().unwrap(), - highway_mpg_fuel1: item.get("highway08U").unwrap().parse::().unwrap(), - highway_mpg_fuel2: item.get("highwayA08U").unwrap().parse::().unwrap(), - unadj_highway_mpg_fuel1: item.get("UHighway").unwrap().parse::().unwrap(), - unadj_highway_mpg_fuel2: item.get("UHighwayA").unwrap().parse::().unwrap(), - highway_kwh_per_100mi: item.get("highwayE").unwrap().parse::().unwrap(), + highway_mpg_fuel1: item.get("highway08U").unwrap().parse().unwrap(), + highway_mpg_fuel2: item.get("highwayA08U").unwrap().parse().unwrap(), + unadj_highway_mpg_fuel1: item.get("UHighway").unwrap().parse().unwrap(), + unadj_highway_mpg_fuel2: item.get("UHighwayA").unwrap().parse().unwrap(), + highway_kwh_per_100mi: item.get("highwayE").unwrap().parse().unwrap(), - comb_mpg_fuel1: item.get("comb08U").unwrap().parse::().unwrap(), - comb_mpg_fuel2: item.get("combA08U").unwrap().parse::().unwrap(), - comb_kwh_per_100mi: item.get("combE").unwrap().parse::().unwrap(), + comb_mpg_fuel1: item.get("comb08U").unwrap().parse().unwrap(), + comb_mpg_fuel2: item.get("combA08U").unwrap().parse().unwrap(), + comb_kwh_per_100mi: item.get("combE").unwrap().parse().unwrap(), emissions_list, }; @@ -1198,7 +1195,7 @@ fn read_epa_test_data_for_given_years>( data_dir_path: P, years: &HashSet, ) -> anyhow::Result>> { - let mut epatest_db: HashMap> = HashMap::new(); + let mut epatest_db = HashMap::new(); for year in years { let p = data_dir_path.as_ref().join(format!("{year}-testcar.csv")); let records = read_records_from_file(File::open(p)?)?; @@ -1226,7 +1223,7 @@ fn load_emissions_data_for_given_years>( emissions_path.to_string_lossy() ); } - let emissions_db: HashMap> = { + let emissions_db = { let emissions_file = File::open(emissions_path)?; read_fuelecon_gov_emissions_to_hashmap(emissions_file) }; @@ -1245,7 +1242,7 @@ fn load_fegov_data_for_given_years>( if let Some(emissions_by_id) = emissions_by_year_and_by_id.get(year) { let file_name = format!("{year}-vehicles.csv"); let fegov_path = data_dir_path.as_ref().join(file_name); - let fegov_db: Vec = { + let fegov_db = { let fegov_file = File::open(fegov_path.as_path())?; read_fuelecon_gov_data_from_file(fegov_file, emissions_by_id)? }; @@ -1282,7 +1279,7 @@ pub fn import_all_vehicles( }; let inputs = vec![vir]; let model_years = { - let mut h: HashSet = HashSet::new(); + let mut h = HashSet::new(); h.insert(year); h }; @@ -1317,7 +1314,7 @@ pub fn import_and_save_all_vehicles_from_file( cache_url: Option, ) -> anyhow::Result<()> { let cache_url = cache_url.unwrap_or_else(get_default_cache_url); - let inputs: Vec = read_vehicle_input_records_from_file(input_path)?; + let inputs = read_vehicle_input_records_from_file(input_path)?; println!("Found {} vehicle input records", inputs.len()); let model_years = determine_model_years_of_interest(&inputs); populate_cache_for_given_years_if_needed(data_dir_path, &model_years, &cache_url)?; @@ -1334,7 +1331,7 @@ pub fn import_all_vehicles_from_record( fegov_data_by_year: &HashMap>, epatest_data_by_year: &HashMap>, ) -> Vec<(VehicleInputRecord, RustVehicle)> { - let mut vehs: Vec<(VehicleInputRecord, RustVehicle)> = Vec::new(); + let mut vehs = Vec::new(); for vir in inputs { if let Some(fegov_data) = fegov_data_by_year.get(&vir.year) { if let Some(epatest_data) = epatest_data_by_year.get(&vir.year) { @@ -1363,7 +1360,7 @@ pub fn import_and_save_all_vehicles( .iter() .enumerate() { - let mut outfile: PathBuf = PathBuf::new(); + let mut outfile = PathBuf::new(); outfile.push(output_dir_path); if idx > 0 { let path = Path::new(&vir.output_file_name); @@ -1635,7 +1632,7 @@ mod tests { fn test_import_robustness() { // Ensure 2019 data is cached let ddpath = create_project_subdir("fe_label_data").unwrap(); - let model_year: u32 = 2019; + let model_year = 2019; let years = { let mut s = HashSet::new(); s.insert(model_year); @@ -1659,7 +1656,7 @@ mod tests { vec![] } }; - let mut num_success: usize = 0; + let mut num_success = 0; let other_inputs = OtherVehicleInputs { vehicle_height_in: 72.4, vehicle_width_in: 56.9, @@ -1674,7 +1671,7 @@ mod tests { // NOTE: below, we can use fewer records in the interest of time as this is a long test with all records // We skip because the vehicles at the beginning of the file tend to be more exotic and to not have // EPA test entries. Thus, they are a bad representation of the whole. - let skip_idx: usize = 200; + let skip_idx = 200; for (num_iter, vr) in veh_records.iter().enumerate() { if num_iter % skip_idx != 0 { continue; @@ -1697,7 +1694,7 @@ mod tests { } num_records += 1; } - let success_frac: f64 = (num_success as f64) / (num_records as f64); + let success_frac = (num_success as f64) / (num_records as f64); assert!(success_frac > 0.90, "success_frac = {}", success_frac); } From 17194c7faf4e6bb17937f1c7edcfac02bdfac992 Mon Sep 17 00:00:00 2001 From: Kyle Carow Date: Tue, 6 Feb 2024 09:22:38 -0700 Subject: [PATCH 17/30] delete get_label_fe_conv weird function --- python/fastsim/fastsimrust.pyi | 4 ---- rust/fastsim-core/src/simdrivelabel.rs | 11 ----------- rust/fastsim-py/src/lib.rs | 2 -- 3 files changed, 17 deletions(-) diff --git a/python/fastsim/fastsimrust.pyi b/python/fastsim/fastsimrust.pyi index 296c19cc..a7f5131e 100644 --- a/python/fastsim/fastsimrust.pyi +++ b/python/fastsim/fastsimrust.pyi @@ -1067,9 +1067,5 @@ def get_label_fe_phev( props: RustPhysicalProperties, ) -> LabelFePHEV: ... - -def get_label_fe_conv(veh: RustVehicle) -> LabelFe: - ... def enabled_features() -> List[str]: ... - diff --git a/rust/fastsim-core/src/simdrivelabel.rs b/rust/fastsim-core/src/simdrivelabel.rs index 6bb0f9aa..cedad0e3 100644 --- a/rust/fastsim-core/src/simdrivelabel.rs +++ b/rust/fastsim-core/src/simdrivelabel.rs @@ -1151,14 +1151,3 @@ mod simdrivelabel_tests { assert!(label_fe.approx_eq(&label_fe_truth, tol)); } } - -#[cfg(feature = "full")] -#[cfg(feature = "pyo3")] -#[pyfunction(name = "get_label_fe_conv")] -/// pyo3 version of [get_label_fe_conv] -pub fn get_label_fe_conv_py() -> LabelFe { - let veh: vehicle::RustVehicle = vehicle::RustVehicle::mock_vehicle(); - let (mut label_fe, _) = get_label_fe(&veh, None, None).unwrap(); - label_fe.veh = vehicle::RustVehicle::default(); - label_fe -} diff --git a/rust/fastsim-py/src/lib.rs b/rust/fastsim-py/src/lib.rs index afd38353..c6d6c293 100644 --- a/rust/fastsim-py/src/lib.rs +++ b/rust/fastsim-py/src/lib.rs @@ -43,8 +43,6 @@ fn fastsimrust(py: Python, m: &PyModule) -> PyResult<()> { #[cfg(feature = "full")] m.add_function(wrap_pyfunction!(get_label_fe_py, m)?)?; m.add_function(wrap_pyfunction!(get_label_fe_phev_py, m)?)?; - #[cfg(feature = "full")] - m.add_function(wrap_pyfunction!(get_label_fe_conv_py, m)?)?; #[cfg(feature = "vehicle-import")] m.add_function(wrap_pyfunction!( vehicle_import::get_options_for_year_make_model, From a61132a8931d48f47a3b400cbed95c7f8625c7e3 Mon Sep 17 00:00:00 2001 From: Kyle Carow Date: Wed, 7 Feb 2024 09:17:17 -0700 Subject: [PATCH 18/30] minor syntax change --- rust/fastsim-core/src/vehicle_import.rs | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/rust/fastsim-core/src/vehicle_import.rs b/rust/fastsim-core/src/vehicle_import.rs index 3fd59ed6..1c274e60 100644 --- a/rust/fastsim-core/src/vehicle_import.rs +++ b/rust/fastsim-core/src/vehicle_import.rs @@ -322,17 +322,18 @@ pub fn get_options_for_year_make_model( let emissions_data = load_emissions_data_for_given_years(ddpath.as_path(), &ys)?; let fegov_data_by_year = load_fegov_data_for_given_years(ddpath.as_path(), &emissions_data, &ys)?; - if let Some(fegov_db) = fegov_data_by_year.get(&y) { - let mut hits = Vec::new(); - for item in fegov_db.iter() { - if item.make == make && item.model == model { - hits.push(item.clone()); + Ok(fegov_data_by_year + .get(&y) + .and_then(|fegov_db| { + let mut hits = Vec::new(); + for item in fegov_db.iter() { + if item.make == make && item.model == model { + hits.push(item.clone()); + } } - } - Ok(hits) - } else { - Ok(vec![]) - } + Some(hits) + }) + .unwrap_or_else(|| vec![])) } fn derive_transmission_specs(fegov: &VehicleDataFE) -> (u32, String) { From f919e509b5ae35cee3616c743eb7c4412a9a6b64 Mon Sep 17 00:00:00 2001 From: Kyle Carow Date: Mon, 26 Feb 2024 13:52:13 -0700 Subject: [PATCH 19/30] bincode feature --- rust/fastsim-core/Cargo.toml | 4 +++- rust/fastsim-core/fastsim-proc-macros/src/add_pyo3_api/mod.rs | 2 ++ rust/fastsim-core/src/cycle.rs | 2 ++ rust/fastsim-core/src/imports.rs | 1 + rust/fastsim-core/src/lib.rs | 3 +++ rust/fastsim-core/src/traits.rs | 4 ++++ 6 files changed, 15 insertions(+), 1 deletion(-) diff --git a/rust/fastsim-core/Cargo.toml b/rust/fastsim-core/Cargo.toml index 1a6424c3..a22c468f 100644 --- a/rust/fastsim-core/Cargo.toml +++ b/rust/fastsim-core/Cargo.toml @@ -21,7 +21,7 @@ serde_yaml = { workspace = true } ndarray = { workspace = true } csv = "1.1" serde_json = "1.0.81" -bincode = "1.3.3" +bincode = { optional = true, version = "1.3.3" } log = "0.4.17" polynomial = { optional = true, version = "0.2.4" } argmin = { optional = true, version = "0.7.0" } @@ -59,10 +59,12 @@ full = [ "dep:argmin-math", "dep:directories", "dep:polynomial", + "bincode", "resources", "validation", "vehicle-import", ] +bincode = ["dep:bincode"] resources = ["dep:include_dir"] validation = ["dep:validator"] vehicle-import = ["dep:curl", "full"] diff --git a/rust/fastsim-core/fastsim-proc-macros/src/add_pyo3_api/mod.rs b/rust/fastsim-core/fastsim-proc-macros/src/add_pyo3_api/mod.rs index ee679a59..b0adf877 100644 --- a/rust/fastsim-core/fastsim-proc-macros/src/add_pyo3_api/mod.rs +++ b/rust/fastsim-core/fastsim-proc-macros/src/add_pyo3_api/mod.rs @@ -307,6 +307,7 @@ pub fn add_pyo3_api(attr: TokenStream, item: TokenStream) -> TokenStream { } /// Write (serialize) an object to bincode-encoded `bytes` + #[cfg(feature = "bincode")] #[pyo3(name = "to_bincode")] pub fn to_bincode_py<'py>(&self, py: Python<'py>) -> anyhow::Result<&'py PyBytes> { Ok(PyBytes::new(py, &self.to_bincode()?)) @@ -318,6 +319,7 @@ pub fn add_pyo3_api(attr: TokenStream, item: TokenStream) -> TokenStream { /// /// * `encoded`: `bytes` - Encoded bytes to deserialize from /// + #[cfg(feature = "bincode")] #[staticmethod] #[pyo3(name = "from_bincode")] pub fn from_bincode_py(encoded: &PyBytes) -> anyhow::Result { diff --git a/rust/fastsim-core/src/cycle.rs b/rust/fastsim-core/src/cycle.rs index eb9d1979..bdc9112e 100644 --- a/rust/fastsim-core/src/cycle.rs +++ b/rust/fastsim-core/src/cycle.rs @@ -659,6 +659,7 @@ impl SerdeAPI for RustCycle { match extension.trim_start_matches('.').to_lowercase().as_str() { "yaml" | "yml" => serde_yaml::to_writer(&File::create(filepath)?, self)?, "json" => serde_json::to_writer(&File::create(filepath)?, self)?, + #[cfg(feature = "bincode")] "bin" => bincode::serialize_into(&File::create(filepath)?, self)?, "csv" => self.write_csv(&mut csv::Writer::from_path(filepath)?)?, _ => bail!( @@ -709,6 +710,7 @@ impl SerdeAPI for RustCycle { let mut deserialized = match format.trim_start_matches('.').to_lowercase().as_str() { "yaml" | "yml" => serde_yaml::from_reader(rdr)?, "json" => serde_json::from_reader(rdr)?, + #[cfg(feature = "bincode")] "bin" => bincode::deserialize_from(rdr)?, "csv" => { // Create empty cycle to be populated diff --git a/rust/fastsim-core/src/imports.rs b/rust/fastsim-core/src/imports.rs index 08bd3833..6216dd4c 100644 --- a/rust/fastsim-core/src/imports.rs +++ b/rust/fastsim-core/src/imports.rs @@ -1,4 +1,5 @@ pub(crate) use anyhow::{anyhow, bail, ensure, Context}; +#[cfg(feature = "bincode")] pub(crate) use bincode; pub(crate) use log; pub(crate) use ndarray::{array, s, Array, Array1, Axis}; diff --git a/rust/fastsim-core/src/lib.rs b/rust/fastsim-core/src/lib.rs index c78a5012..d3f1b70d 100644 --- a/rust/fastsim-core/src/lib.rs +++ b/rust/fastsim-core/src/lib.rs @@ -66,6 +66,9 @@ pub fn enabled_features() -> Vec { #[cfg(feature = "full")] enabled.push("full".into()); + #[cfg(feature = "bincode")] + enabled.push("bincode".into()); + #[cfg(feature = "resources")] enabled.push("resources".into()); diff --git a/rust/fastsim-core/src/traits.rs b/rust/fastsim-core/src/traits.rs index f08d31a6..2d6acdd7 100644 --- a/rust/fastsim-core/src/traits.rs +++ b/rust/fastsim-core/src/traits.rs @@ -45,6 +45,7 @@ pub trait SerdeAPI: Serialize + for<'a> Deserialize<'a> { match extension.trim_start_matches('.').to_lowercase().as_str() { "yaml" | "yml" => serde_yaml::to_writer(&File::create(filepath)?, self)?, "json" => serde_json::to_writer(&File::create(filepath)?, self)?, + #[cfg(feature = "bincode")] "bin" => bincode::serialize_into(&File::create(filepath)?, self)?, _ => bail!( "Unsupported format {extension:?}, must be one of {:?}", @@ -125,6 +126,7 @@ pub trait SerdeAPI: Serialize + for<'a> Deserialize<'a> { let mut deserialized: Self = match format.trim_start_matches('.').to_lowercase().as_str() { "yaml" | "yml" => serde_yaml::from_reader(rdr)?, "json" => serde_json::from_reader(rdr)?, + #[cfg(feature = "bincode")] "bin" => bincode::deserialize_from(rdr)?, _ => bail!( "Unsupported format {format:?}, must be one of {:?}", @@ -170,6 +172,7 @@ pub trait SerdeAPI: Serialize + for<'a> Deserialize<'a> { } /// Write (serialize) an object to bincode-encoded bytes + #[cfg(feature = "bincode")] fn to_bincode(&self) -> anyhow::Result> { Ok(bincode::serialize(&self)?) } @@ -180,6 +183,7 @@ pub trait SerdeAPI: Serialize + for<'a> Deserialize<'a> { /// /// * `encoded` - Encoded bytes to deserialize from /// + #[cfg(feature = "bincode")] fn from_bincode(encoded: &[u8]) -> anyhow::Result { let mut bincode_de: Self = bincode::deserialize(encoded)?; bincode_de.init()?; From b5206d6d43573ec2c395c980b9b9a0da79888bfb Mon Sep 17 00:00:00 2001 From: Kyle Carow Date: Mon, 26 Feb 2024 13:56:14 -0700 Subject: [PATCH 20/30] vehicle-import feature fixes --- python/fastsim/demos/test_demos.py | 2 +- python/fastsim/demos/vehicle_import_demo.py | 2 +- rust/fastsim-core/src/lib.rs | 3 +++ 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/python/fastsim/demos/test_demos.py b/python/fastsim/demos/test_demos.py index f14fde1d..42cb0cd4 100644 --- a/python/fastsim/demos/test_demos.py +++ b/python/fastsim/demos/test_demos.py @@ -10,7 +10,7 @@ def demo_paths(): demo_paths.remove(Path(__file__).resolve()) return demo_paths -REQUIRED_FEATURES = {"vehicle_import_demo": "full"} +REQUIRED_FEATURES = {"vehicle_import_demo": "vehicle-import"} @pytest.mark.parametrize("demo_path", demo_paths(), ids=[dp.name for dp in demo_paths()]) def test_demo(demo_path: Path): diff --git a/python/fastsim/demos/vehicle_import_demo.py b/python/fastsim/demos/vehicle_import_demo.py index f2f6c785..6e0c5d3f 100644 --- a/python/fastsim/demos/vehicle_import_demo.py +++ b/python/fastsim/demos/vehicle_import_demo.py @@ -5,7 +5,7 @@ # %% from fastsim import fastsimrust -REQUIRED_FEATURE = "full" +REQUIRED_FEATURE = "vehicle-import" if __name__ == "__main__" and REQUIRED_FEATURE not in fastsimrust.enabled_features(): raise NotImplementedError( f'Feature "{REQUIRED_FEATURE}" is required to run this demo' diff --git a/rust/fastsim-core/src/lib.rs b/rust/fastsim-core/src/lib.rs index d3f1b70d..ba174073 100644 --- a/rust/fastsim-core/src/lib.rs +++ b/rust/fastsim-core/src/lib.rs @@ -75,5 +75,8 @@ pub fn enabled_features() -> Vec { #[cfg(feature = "validation")] enabled.push("validation".into()); + #[cfg(feature = "vehicle-import")] + enabled.push("vehicle-import".into()); + enabled } From ff08ab54c41bbd31be8aca83f6e811bdaaebe6eb Mon Sep 17 00:00:00 2001 From: Kyle Carow Date: Mon, 26 Feb 2024 14:01:34 -0700 Subject: [PATCH 21/30] add bincode to features in fastsim-py --- rust/fastsim-core/Cargo.toml | 2 +- rust/fastsim-py/Cargo.toml | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/rust/fastsim-core/Cargo.toml b/rust/fastsim-core/Cargo.toml index a22c468f..79650e0d 100644 --- a/rust/fastsim-core/Cargo.toml +++ b/rust/fastsim-core/Cargo.toml @@ -53,7 +53,6 @@ include = [ # https://doc.rust-lang.org/cargo/reference/features.html?highlight=no-default-features#the-default-feature # and use the `--no-default-features` flag when compiling default = ["full"] -pyo3 = ["dep:pyo3"] full = [ "dep:argmin", "dep:argmin-math", @@ -65,6 +64,7 @@ full = [ "vehicle-import", ] bincode = ["dep:bincode"] +pyo3 = ["dep:pyo3"] resources = ["dep:include_dir"] validation = ["dep:validator"] vehicle-import = ["dep:curl", "full"] diff --git a/rust/fastsim-py/Cargo.toml b/rust/fastsim-py/Cargo.toml index 5c17c725..2b735eb1 100644 --- a/rust/fastsim-py/Cargo.toml +++ b/rust/fastsim-py/Cargo.toml @@ -31,10 +31,12 @@ default = ["full"] full = [ "dep:pyo3-log", "fastsim-core/full", + "bincode", "resources", "validation", "vehicle-import", ] +bincode = ["fastsim-core/bincode"] resources = ["fastsim-core/resources"] validation = ["fastsim-core/validation"] vehicle-import = ["fastsim-core/vehicle-import"] From 0133e46673ec53c3162e875bc9907dec64d2aaa0 Mon Sep 17 00:00:00 2001 From: Kyle Carow Date: Mon, 26 Feb 2024 14:21:13 -0700 Subject: [PATCH 22/30] made bincode nondefault --- rust/fastsim-core/Cargo.toml | 1 - rust/fastsim-py/Cargo.toml | 1 - 2 files changed, 2 deletions(-) diff --git a/rust/fastsim-core/Cargo.toml b/rust/fastsim-core/Cargo.toml index 79650e0d..bdfa41a9 100644 --- a/rust/fastsim-core/Cargo.toml +++ b/rust/fastsim-core/Cargo.toml @@ -58,7 +58,6 @@ full = [ "dep:argmin-math", "dep:directories", "dep:polynomial", - "bincode", "resources", "validation", "vehicle-import", diff --git a/rust/fastsim-py/Cargo.toml b/rust/fastsim-py/Cargo.toml index 2b735eb1..cb3905e8 100644 --- a/rust/fastsim-py/Cargo.toml +++ b/rust/fastsim-py/Cargo.toml @@ -31,7 +31,6 @@ default = ["full"] full = [ "dep:pyo3-log", "fastsim-core/full", - "bincode", "resources", "validation", "vehicle-import", From 3467e659979a459924e72da23b2d6de0d47a435c Mon Sep 17 00:00:00 2001 From: Kyle Carow Date: Mon, 26 Feb 2024 15:02:20 -0700 Subject: [PATCH 23/30] migrate from full to default feature --- python/fastsim/demos/demo.py | 2 +- python/fastsim/tests/test_auxiliaries.py | 2 +- python/fastsim/tests/test_simdrivelabel.py | 2 +- rust/fastsim-core/Cargo.toml | 5 ++--- rust/fastsim-core/src/lib.rs | 4 ++-- rust/fastsim-core/src/simdrivelabel.rs | 6 +++--- rust/fastsim-core/src/utils.rs | 4 ++-- rust/fastsim-core/src/vehicle_utils.rs | 14 +++++++------- rust/fastsim-py/Cargo.toml | 5 ++--- rust/fastsim-py/src/lib.rs | 6 +++--- 10 files changed, 24 insertions(+), 26 deletions(-) diff --git a/python/fastsim/demos/demo.py b/python/fastsim/demos/demo.py index db9959c3..1819d320 100644 --- a/python/fastsim/demos/demo.py +++ b/python/fastsim/demos/demo.py @@ -890,7 +890,7 @@ def get_sim_drive_vec( # values. # %% -if "full" in fsim.fastsimrust.enabled_features(): +if "default" in fsim.fastsimrust.enabled_features(): from fastsim.fastsimrust import abc_to_drag_coeffs test_veh = fsim.vehicle.Vehicle.from_vehdb(5, to_rust=True).to_rust() (drag_coef, wheel_rr_coef) = abc_to_drag_coeffs(test_veh, 25.91, 0.1943, 0.01796, simdrive_optimize=True) diff --git a/python/fastsim/tests/test_auxiliaries.py b/python/fastsim/tests/test_auxiliaries.py index 449afb89..6a95db9e 100644 --- a/python/fastsim/tests/test_auxiliaries.py +++ b/python/fastsim/tests/test_auxiliaries.py @@ -48,7 +48,7 @@ def test_drag_coeffs_to_abc(self): self.assertAlmostEqual(0, b_lbf__mph) self.assertAlmostEqual(0.020817239083920212, c_lbf__mph2) - @pytest.mark.skipif("full" not in fastsimrust.enabled_features(), reason='requires "full" feature') + @pytest.mark.skipif("default" not in fastsimrust.enabled_features(), reason='requires "default" feature') def test_abc_to_drag_coeffs_rust_port(self): from fastsim.fastsimrust import abc_to_drag_coeffs with np.errstate(divide='ignore'): diff --git a/python/fastsim/tests/test_simdrivelabel.py b/python/fastsim/tests/test_simdrivelabel.py index 763ccd2c..d2a8f958 100644 --- a/python/fastsim/tests/test_simdrivelabel.py +++ b/python/fastsim/tests/test_simdrivelabel.py @@ -3,7 +3,7 @@ from fastsim import fastsimrust -@pytest.mark.skipif("full" not in fastsimrust.enabled_features(), reason='requires "full" feature') +@pytest.mark.skipif("default" not in fastsimrust.enabled_features(), reason='requires "default" feature') class TestSimDriveLabel(unittest.TestCase): def test_get_label_fe_conv(self): veh = fastsimrust.RustVehicle.mock_vehicle() diff --git a/rust/fastsim-core/Cargo.toml b/rust/fastsim-core/Cargo.toml index bdfa41a9..30e090f2 100644 --- a/rust/fastsim-core/Cargo.toml +++ b/rust/fastsim-core/Cargo.toml @@ -52,8 +52,7 @@ include = [ # to disable the default features, see # https://doc.rust-lang.org/cargo/reference/features.html?highlight=no-default-features#the-default-feature # and use the `--no-default-features` flag when compiling -default = ["full"] -full = [ +default = [ "dep:argmin", "dep:argmin-math", "dep:directories", @@ -66,4 +65,4 @@ bincode = ["dep:bincode"] pyo3 = ["dep:pyo3"] resources = ["dep:include_dir"] validation = ["dep:validator"] -vehicle-import = ["dep:curl", "full"] +vehicle-import = ["dep:curl"] diff --git a/rust/fastsim-core/src/lib.rs b/rust/fastsim-core/src/lib.rs index ba174073..a095d211 100644 --- a/rust/fastsim-core/src/lib.rs +++ b/rust/fastsim-core/src/lib.rs @@ -63,8 +63,8 @@ pub use fastsim_proc_macros as proc_macros; pub fn enabled_features() -> Vec { let mut enabled = vec![]; - #[cfg(feature = "full")] - enabled.push("full".into()); + #[cfg(feature = "default")] + enabled.push("default".into()); #[cfg(feature = "bincode")] enabled.push("bincode".into()); diff --git a/rust/fastsim-core/src/simdrivelabel.rs b/rust/fastsim-core/src/simdrivelabel.rs index cedad0e3..458ce45e 100644 --- a/rust/fastsim-core/src/simdrivelabel.rs +++ b/rust/fastsim-core/src/simdrivelabel.rs @@ -167,7 +167,7 @@ pub fn get_net_accel_py(sd_accel: &mut RustSimDrive, scenario_name: &str) -> any Ok(result) } -#[cfg(feature = "full")] +#[cfg(feature = "default")] pub fn get_label_fe( veh: &vehicle::RustVehicle, full_detail: Option, @@ -384,7 +384,7 @@ pub fn get_label_fe( } } -#[cfg(feature = "full")] +#[cfg(feature = "default")] #[cfg(feature = "pyo3")] #[pyfunction(name = "get_label_fe")] /// pyo3 version of [get_label_fe] @@ -723,7 +723,7 @@ pub fn get_label_fe_phev_py( } #[cfg(test)] -#[cfg(feature = "full")] +#[cfg(feature = "default")] mod simdrivelabel_tests { use super::*; diff --git a/rust/fastsim-core/src/utils.rs b/rust/fastsim-core/src/utils.rs index 6b695461..efcabd9e 100644 --- a/rust/fastsim-core/src/utils.rs +++ b/rust/fastsim-core/src/utils.rs @@ -1,6 +1,6 @@ //! Module containing miscellaneous utility functions. -#[cfg(feature = "full")] +#[cfg(feature = "default")] use directories::ProjectDirs; use itertools::Itertools; use lazy_static::lazy_static; @@ -515,7 +515,7 @@ pub fn tire_code_to_radius>(tire_code: S) -> anyhow::Result { Ok(radius_mm / 1000.0) } -#[cfg(feature = "full")] +#[cfg(feature = "default")] /// Creates/gets an OS-specific data directory and returns the path. pub fn create_project_subdir>(subpath: P) -> anyhow::Result { let proj_dirs = ProjectDirs::from("gov", "NREL", "fastsim").ok_or_else(|| { diff --git a/rust/fastsim-core/src/vehicle_utils.rs b/rust/fastsim-core/src/vehicle_utils.rs index bc882ad0..14f2692f 100644 --- a/rust/fastsim-core/src/vehicle_utils.rs +++ b/rust/fastsim-core/src/vehicle_utils.rs @@ -1,11 +1,11 @@ //! Module for utility functions that support the vehicle struct. -#[cfg(feature = "full")] +#[cfg(feature = "default")] use argmin::core::{CostFunction, Executor, OptimizationResult, State}; -#[cfg(feature = "full")] +#[cfg(feature = "default")] use argmin::solver::neldermead::NelderMead; use ndarray::{array, Array1}; -#[cfg(feature = "full")] +#[cfg(feature = "default")] use polynomial::Polynomial; use std::option::Option; @@ -21,7 +21,7 @@ use crate::vehicle::RustVehicle; #[allow(non_snake_case)] #[cfg_attr(feature = "pyo3", pyfunction)] #[allow(clippy::too_many_arguments)] -#[cfg(feature = "full")] +#[cfg(feature = "default")] pub fn abc_to_drag_coeffs( veh: &mut RustVehicle, a_lbf: f64, @@ -140,14 +140,14 @@ pub fn get_error_val(model: Array1, test: Array1, time_steps: Array1 { cycle: &'a RustCycle, vehicle: &'a RustVehicle, dyno_func_lb: &'a Polynomial, } -#[cfg(feature = "full")] +#[cfg(feature = "default")] impl CostFunction for GetError<'_> { type Param = Array1; type Output = f64; @@ -223,7 +223,7 @@ mod tests { assert!(error_val.approx_eq(&0.8124999999999998, 1e-10)); } - #[cfg(feature = "full")] + #[cfg(feature = "default")] #[test] fn test_abc_to_drag_coeffs() { let mut veh: RustVehicle = RustVehicle::mock_vehicle(); diff --git a/rust/fastsim-py/Cargo.toml b/rust/fastsim-py/Cargo.toml index cb3905e8..6b5181ad 100644 --- a/rust/fastsim-py/Cargo.toml +++ b/rust/fastsim-py/Cargo.toml @@ -27,10 +27,9 @@ crate-type = ["cdylib"] include = ["../../NOTICE"] [features] -default = ["full"] -full = [ +default = [ "dep:pyo3-log", - "fastsim-core/full", + "fastsim-core/default", "resources", "validation", "vehicle-import", diff --git a/rust/fastsim-py/src/lib.rs b/rust/fastsim-py/src/lib.rs index c6d6c293..0cca60cc 100644 --- a/rust/fastsim-py/src/lib.rs +++ b/rust/fastsim-py/src/lib.rs @@ -12,7 +12,7 @@ use pyo3imports::*; /// Function for adding Rust structs as Python Classes #[pymodule] fn fastsimrust(py: Python, m: &PyModule) -> PyResult<()> { - #[cfg(feature = "full")] + #[cfg(feature = "default")] pyo3_log::init(); m.add_class::()?; m.add_class::()?; @@ -36,11 +36,11 @@ fn fastsimrust(py: Python, m: &PyModule) -> PyResult<()> { m.add_class::()?; cycle::register(py, m)?; - #[cfg(feature = "full")] + #[cfg(feature = "default")] m.add_function(wrap_pyfunction!(vehicle_utils::abc_to_drag_coeffs, m)?)?; m.add_function(wrap_pyfunction!(make_accel_trace_py, m)?)?; m.add_function(wrap_pyfunction!(get_net_accel_py, m)?)?; - #[cfg(feature = "full")] + #[cfg(feature = "default")] m.add_function(wrap_pyfunction!(get_label_fe_py, m)?)?; m.add_function(wrap_pyfunction!(get_label_fe_phev_py, m)?)?; #[cfg(feature = "vehicle-import")] From 27d0f805518657163db2716ac550dc63f340b85a Mon Sep 17 00:00:00 2001 From: Kyle Carow Date: Tue, 27 Feb 2024 14:31:40 -0700 Subject: [PATCH 24/30] introduce 'logging' feature --- rust/fastsim-core/Cargo.toml | 10 ++++++---- rust/fastsim-core/src/lib.rs | 3 +++ rust/fastsim-core/src/simdrive/cyc_mods.rs | 1 + rust/fastsim-core/src/simdrive/simdrive_impl.rs | 6 ++++++ rust/fastsim-core/src/simdrivelabel.rs | 2 ++ rust/fastsim-py/Cargo.toml | 6 +++++- rust/fastsim-py/src/lib.rs | 2 +- 7 files changed, 24 insertions(+), 6 deletions(-) diff --git a/rust/fastsim-core/Cargo.toml b/rust/fastsim-core/Cargo.toml index fa00c905..1721f8c4 100644 --- a/rust/fastsim-core/Cargo.toml +++ b/rust/fastsim-core/Cargo.toml @@ -21,8 +21,8 @@ serde_yaml = { workspace = true } ndarray = { workspace = true } csv = "1.1" serde_json = "1.0.81" -bincode = { optional = true, version = "1.3.3" } -log = "0.4.17" +bincode = { optional = true, version = "1.3.3" } +log = { optional = true, version = "0.4.17" } polynomial = { optional = true, version = "0.2.4" } argmin = { optional = true, version = "0.7.0" } argmin-math = { optional = true, version = "0.2.1", features = [ @@ -60,12 +60,14 @@ default = [ "dep:argmin-math", "dep:directories", "dep:polynomial", + "logging", "resources", "validation", "vehicle-import", ] -bincode = ["dep:bincode"] -pyo3 = ["dep:pyo3"] +bincode = ["dep:bincode"] # non-default: bincode broken for RustVehicle struct +logging = ["dep:log"] +pyo3 = ["dep:pyo3"] # non-default: feature for use with fastsim-py crate resources = ["dep:include_dir"] validation = ["dep:validator"] vehicle-import = ["dep:curl"] diff --git a/rust/fastsim-core/src/lib.rs b/rust/fastsim-core/src/lib.rs index a095d211..0db574f1 100644 --- a/rust/fastsim-core/src/lib.rs +++ b/rust/fastsim-core/src/lib.rs @@ -69,6 +69,9 @@ pub fn enabled_features() -> Vec { #[cfg(feature = "bincode")] enabled.push("bincode".into()); + #[cfg(feature = "logging")] + enabled.push("logging".into()); + #[cfg(feature = "resources")] enabled.push("resources".into()); diff --git a/rust/fastsim-core/src/simdrive/cyc_mods.rs b/rust/fastsim-core/src/simdrive/cyc_mods.rs index d181818d..91d9d62f 100644 --- a/rust/fastsim-core/src/simdrive/cyc_mods.rs +++ b/rust/fastsim-core/src/simdrive/cyc_mods.rs @@ -1073,6 +1073,7 @@ impl RustSimDrive { } adjusted_current_speed = true; } else { + #[cfg(feature = "logging")] log::warn!( "final_speed_m_per_s={} not close to coast_brake_start_speed={} for i={}; i_for_brake={}, traj_n={}", final_speed_m_per_s, diff --git a/rust/fastsim-core/src/simdrive/simdrive_impl.rs b/rust/fastsim-core/src/simdrive/simdrive_impl.rs index 802d5ed0..d604966a 100644 --- a/rust/fastsim-core/src/simdrive/simdrive_impl.rs +++ b/rust/fastsim-core/src/simdrive/simdrive_impl.rs @@ -500,11 +500,13 @@ impl RustSimDrive { if self.cyc.dt_s().iter().any(|&dt| dt > 5.0) { if self.sim_params.missed_trace_correction { + #[cfg(feature = "logging")] log::info!( "Max time dilation factor = {:.3}", (self.cyc.dt_s() / self.cyc0.dt_s()).max()? ); } + #[cfg(feature = "logging")] log::warn!( "Large time steps affect accuracy significantly (max time step = {:.3})", self.cyc.dt_s().max()? @@ -1835,6 +1837,7 @@ impl RustSimDrive { / (self.roadway_chg_kj + self.ess_dischg_kj + self.fuel_kj + self.ke_kj); if self.energy_audit_error.abs() > self.sim_params.energy_audit_error_tol { + #[cfg(feature = "logging")] log::warn!( "problem detected with conservation of energy; \ energy audit error: {:.5}", @@ -1867,6 +1870,7 @@ impl RustSimDrive { if !self.sim_params.missed_trace_correction { if self.trace_miss_dist_frac > self.sim_params.trace_miss_dist_tol { self.trace_miss = true; + #[cfg(feature = "logging")] log::warn!( "trace miss distance fraction {:.5} exceeds tolerance of {:.5}", self.trace_miss_dist_frac, @@ -1875,6 +1879,7 @@ impl RustSimDrive { } } else if self.trace_miss_time_frac > self.sim_params.trace_miss_time_tol { self.trace_miss = true; + #[cfg(feature = "logging")] log::warn!( "trace miss time fraction {:.5} exceeds tolerance of {:.5}", self.trace_miss_time_frac, @@ -1885,6 +1890,7 @@ impl RustSimDrive { self.trace_miss_speed_mps = *(&self.mps_ach - &self.cyc.mps).map(|x| x.abs()).max()?; if self.trace_miss_speed_mps > self.sim_params.trace_miss_speed_mps_tol { self.trace_miss = true; + #[cfg(feature = "logging")] log::warn!( "trace miss speed {:.5} m/s exceeds tolerance of {:.5} m/s", self.trace_miss_speed_mps, diff --git a/rust/fastsim-core/src/simdrivelabel.rs b/rust/fastsim-core/src/simdrivelabel.rs index 458ce45e..1d40d13d 100644 --- a/rust/fastsim-core/src/simdrivelabel.rs +++ b/rust/fastsim-core/src/simdrivelabel.rs @@ -144,6 +144,7 @@ pub fn make_accel_trace_py() -> RustCycle { } pub fn get_net_accel(sd_accel: &mut RustSimDrive, scenario_name: &String) -> anyhow::Result { + #[cfg(feature = "logging")] log::debug!("running `sim_drive_accel`"); sd_accel.sim_drive_accel(None, None)?; if sd_accel.mph_ach.iter().any(|&x| x >= 60.) { @@ -154,6 +155,7 @@ pub fn get_net_accel(sd_accel: &mut RustSimDrive, scenario_name: &String) -> any false, )) } else { + #[cfg(feature = "logging")] log::warn!("vehicle '{}' never achieves 60 mph", scenario_name); Ok(1e3) } diff --git a/rust/fastsim-py/Cargo.toml b/rust/fastsim-py/Cargo.toml index 6b5181ad..fef23e80 100644 --- a/rust/fastsim-py/Cargo.toml +++ b/rust/fastsim-py/Cargo.toml @@ -28,13 +28,17 @@ include = ["../../NOTICE"] [features] default = [ - "dep:pyo3-log", "fastsim-core/default", + "logging", "resources", "validation", "vehicle-import", ] bincode = ["fastsim-core/bincode"] +logging = [ + "dep:pyo3-log", + "fastsim-core/logging", +] resources = ["fastsim-core/resources"] validation = ["fastsim-core/validation"] vehicle-import = ["fastsim-core/vehicle-import"] diff --git a/rust/fastsim-py/src/lib.rs b/rust/fastsim-py/src/lib.rs index 0cca60cc..8cbb057a 100644 --- a/rust/fastsim-py/src/lib.rs +++ b/rust/fastsim-py/src/lib.rs @@ -12,7 +12,7 @@ use pyo3imports::*; /// Function for adding Rust structs as Python Classes #[pymodule] fn fastsimrust(py: Python, m: &PyModule) -> PyResult<()> { - #[cfg(feature = "default")] + #[cfg(feature = "logging")] pyo3_log::init(); m.add_class::()?; m.add_class::()?; From 2e817e8f8900b04a72c913277a59e7b4b9a0c132 Mon Sep 17 00:00:00 2001 From: Kyle Carow Date: Tue, 27 Feb 2024 15:16:35 -0700 Subject: [PATCH 25/30] introduce simdrivelabel feature --- rust/fastsim-core/Cargo.toml | 2 + rust/fastsim-core/src/lib.rs | 3 ++ rust/fastsim-core/src/simdrivelabel.rs | 54 +++++++++++++------------- rust/fastsim-py/Cargo.toml | 1 + rust/fastsim-py/src/lib.rs | 9 ++++- 5 files changed, 40 insertions(+), 29 deletions(-) diff --git a/rust/fastsim-core/Cargo.toml b/rust/fastsim-core/Cargo.toml index 1721f8c4..adc3cb54 100644 --- a/rust/fastsim-core/Cargo.toml +++ b/rust/fastsim-core/Cargo.toml @@ -62,6 +62,7 @@ default = [ "dep:polynomial", "logging", "resources", + "simdrivelabel", "validation", "vehicle-import", ] @@ -69,5 +70,6 @@ bincode = ["dep:bincode"] # non-default: bincode broken for RustVehicle struct logging = ["dep:log"] pyo3 = ["dep:pyo3"] # non-default: feature for use with fastsim-py crate resources = ["dep:include_dir"] +simdrivelabel = ["resources"] validation = ["dep:validator"] vehicle-import = ["dep:curl"] diff --git a/rust/fastsim-core/src/lib.rs b/rust/fastsim-core/src/lib.rs index 0db574f1..6193c73e 100644 --- a/rust/fastsim-core/src/lib.rs +++ b/rust/fastsim-core/src/lib.rs @@ -75,6 +75,9 @@ pub fn enabled_features() -> Vec { #[cfg(feature = "resources")] enabled.push("resources".into()); + #[cfg(feature = "simdrivelabel")] + enabled.push("simdrivelabel".into()); + #[cfg(feature = "validation")] enabled.push("validation".into()); diff --git a/rust/fastsim-core/src/simdrivelabel.rs b/rust/fastsim-core/src/simdrivelabel.rs index 1d40d13d..6d4f056f 100644 --- a/rust/fastsim-core/src/simdrivelabel.rs +++ b/rust/fastsim-core/src/simdrivelabel.rs @@ -1,4 +1,5 @@ //! Module containing classes and methods for calculating label fuel economy. +#![cfg(feature = "simdrivelabel")] use ndarray::Array; use serde::Serialize; @@ -169,7 +170,6 @@ pub fn get_net_accel_py(sd_accel: &mut RustSimDrive, scenario_name: &str) -> any Ok(result) } -#[cfg(feature = "default")] pub fn get_label_fe( veh: &vehicle::RustVehicle, full_detail: Option, @@ -188,14 +188,14 @@ pub fn get_label_fe( // Returns label fuel economy values as a struct and (optionally) // simdrive::RustSimDrive objects. - let sim_params: RustSimDriveParams = RustSimDriveParams::default(); - let props: RustPhysicalProperties = RustPhysicalProperties::default(); - let long_params: RustLongParams = RustLongParams::default(); + let sim_params = RustSimDriveParams::default(); + let props = RustPhysicalProperties::default(); + let long_params = RustLongParams::default(); - let mut cyc: HashMap<&str, RustCycle> = HashMap::new(); - let mut sd: HashMap<&str, RustSimDrive> = HashMap::new(); - let mut out: LabelFe = LabelFe::default(); - let mut max_trace_miss_in_mph: f64 = 0.0; + let mut cyc = HashMap::new(); + let mut sd = HashMap::new(); + let mut out = LabelFe::default(); + let mut max_trace_miss_in_mph = 0.0; out.veh = veh.clone(); @@ -220,7 +220,7 @@ pub fn get_label_fe( out.trace_miss_speed_mph = max_trace_miss_in_mph; // find year-based adjustment parameters - let adj_params: &AdjCoef = if veh.veh_year < 2017 { + let adj_params = if veh.veh_year < 2017 { &long_params.ld_fe_adj_coef.adj_coef_map["2008"] } else { // assume 2017 coefficients are valid @@ -310,7 +310,7 @@ pub fn get_label_fe( out.uf = 0.; } else { // PHEV - let phev_calcs: LabelFePHEV = + let phev_calcs = get_label_fe_phev(veh, &mut sd, &long_params, adj_params, &sim_params, &props)?; out.phev_calcs = Some(phev_calcs.clone()); @@ -386,7 +386,6 @@ pub fn get_label_fe( } } -#[cfg(feature = "default")] #[cfg(feature = "pyo3")] #[pyfunction(name = "get_label_fe")] /// pyo3 version of [get_label_fe] @@ -395,7 +394,7 @@ pub fn get_label_fe_py( full_detail: Option, verbose: Option, ) -> anyhow::Result<(LabelFe, Option>)> { - let result: (LabelFe, Option>) = + let result = get_label_fe(veh, full_detail, verbose)?; Ok(result) } @@ -440,7 +439,7 @@ pub fn get_label_fe_phev( // By assuming that the battery SOC depletion per mile is constant across cycles, // the first cycle can be extrapolated until charge sustaining kicks in. sd_val.sim_drive(Some(veh.max_soc), None)?; - let mut phev_calc: PHEVCycleCalc = PHEVCycleCalc::default(); + let mut phev_calc = PHEVCycleCalc::default(); // charge depletion cycle has already been simulated // charge depletion battery kW-hr @@ -464,7 +463,7 @@ pub fn get_label_fe_phev( // utility factor calculation for last charge depletion iteration and transition iteration // ported from excel - let interp_x_vals: Array1 = + let interp_x_vals = Array::range(0.0, phev_calc.cd_cycs.ceil() + 1.0, 1.0) * sd_val.dist_mi.sum(); phev_calc.lab_iter_uf = interp_x_vals .iter() @@ -485,7 +484,7 @@ pub fn get_label_fe_phev( // charge sustaining // the 0.01 is here to be consistent with Excel - let init_soc: f64 = sd_val.veh.min_soc + 0.01; + let init_soc = sd_val.veh.min_soc + 0.01; sd_val.sim_drive(Some(init_soc), None)?; // charge sustaining fuel gallons phev_calc.cs_fs_gal = sd_val.fs_kwh_out_ach.sum() / props.kwh_per_gge; @@ -497,7 +496,7 @@ pub fn get_label_fe_phev( phev_calc.cs_ess_kwh = sd_val.ess_dischg_kj; phev_calc.cs_ess_kwh_per_mi = sd_val.battery_kwh_per_mi; - let lab_iter_uf_diff: Array1 = diff(&phev_calc.lab_iter_uf); + let lab_iter_uf_diff = diff(&phev_calc.lab_iter_uf); phev_calc.lab_uf_gpm = Array::from_vec(vec![ phev_calc.trans_fs_gal * lab_iter_uf_diff.last().unwrap(), phev_calc.cs_fs_gal * (1.0 - phev_calc.lab_iter_uf.last().unwrap()), @@ -529,14 +528,14 @@ pub fn get_label_fe_phev( / (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 = Vec::new(); + 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 = Array::from_vec(lab_iter_kwh_per_mi_vals); - let mut vals: Vec = Vec::new(); + let mut vals = Vec::new(); vals.push(0.0); vals.extend( (&phev_calc @@ -551,8 +550,8 @@ pub fn get_label_fe_phev( phev_calc.lab_kwh_per_mi = phev_calc.lab_iter_uf_kwh_per_mi.sum() / phev_calc.lab_iter_uf.max()?; - let mut adj_iter_mpgge_vals: Vec = vec![0.0; phev_calc.cd_cycs.floor() as usize]; - let mut adj_iter_kwh_per_mi_vals: Vec = vec![0.0; phev_calc.lab_iter_kwh_per_mi.len()]; + 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(max( 1.0 / (adj_params.city_intercept @@ -654,7 +653,7 @@ pub fn get_label_fe_phev( }) .collect(); - let adj_iter_uf_diff: Array1 = diff(&phev_calc.adj_iter_uf); + let adj_iter_uf_diff = diff(&phev_calc.adj_iter_uf); 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]) @@ -725,13 +724,12 @@ pub fn get_label_fe_phev_py( } #[cfg(test)] -#[cfg(feature = "default")] mod simdrivelabel_tests { use super::*; #[test] fn test_get_label_fe_conv() { - let veh: vehicle::RustVehicle = vehicle::RustVehicle::mock_vehicle(); + let veh = vehicle::RustVehicle::mock_vehicle(); let (mut label_fe, _) = get_label_fe(&veh, None, None).unwrap(); // For some reason, RustVehicle::mock_vehicle() != RustVehicle::mock_vehicle() // Therefore, veh field in both structs replaced with Default for comparison purposes @@ -740,7 +738,7 @@ mod simdrivelabel_tests { label_fe.veh = ref_veh.clone(); // println!("Calculated net accel: {}", label_fe.net_accel); - let label_fe_truth: LabelFe = LabelFe { + let label_fe_truth = LabelFe { veh: ref_veh, adj_params: RustLongParams::default().ld_fe_adj_coef.adj_coef_map["2008"].clone(), lab_udds_mpgge: 32.47503766676829, @@ -940,7 +938,7 @@ mod simdrivelabel_tests { ); label_fe.net_accel = 1000.; - let udds: PHEVCycleCalc = PHEVCycleCalc { + let udds = PHEVCycleCalc { cd_ess_kwh: 13.799999999999999, cd_ess_kwh_per_mi: 0.1670807863534209, cd_fs_gal: 0.0, @@ -1034,7 +1032,7 @@ mod simdrivelabel_tests { total_cd_miles: 82.59477526523773, }; - let hwy: PHEVCycleCalc = PHEVCycleCalc { + let hwy = PHEVCycleCalc { cd_ess_kwh: 13.799999999999999, cd_ess_kwh_per_mi: 0.19912462736394723, cd_fs_gal: 0.0, @@ -1105,13 +1103,13 @@ mod simdrivelabel_tests { total_cd_miles: 69.30333119859274, }; - let phev_calcs: LabelFePHEV = LabelFePHEV { + let phev_calcs = LabelFePHEV { regen_soc_buffer: 0.00957443430586049, udds, hwy, }; - let label_fe_truth: LabelFe = LabelFe { + let label_fe_truth = LabelFe { veh: vehicle::RustVehicle::default(), adj_params: RustLongParams::default().ld_fe_adj_coef.adj_coef_map["2008"].clone(), lab_udds_mpgge: 370.06411942132064, diff --git a/rust/fastsim-py/Cargo.toml b/rust/fastsim-py/Cargo.toml index fef23e80..24f976c1 100644 --- a/rust/fastsim-py/Cargo.toml +++ b/rust/fastsim-py/Cargo.toml @@ -40,5 +40,6 @@ logging = [ "fastsim-core/logging", ] resources = ["fastsim-core/resources"] +simdrivelabel = ["fastsim-core/simdrivelabel", "resources"] validation = ["fastsim-core/validation"] vehicle-import = ["fastsim-core/vehicle-import"] diff --git a/rust/fastsim-py/src/lib.rs b/rust/fastsim-py/src/lib.rs index 8cbb057a..4fbf95ae 100644 --- a/rust/fastsim-py/src/lib.rs +++ b/rust/fastsim-py/src/lib.rs @@ -5,6 +5,7 @@ //! or enabling this feature directly), compiles commonly used resources (e.g. //! standard drive cycles) for faster access. +#[cfg(feature = "simdrivelabel")] use fastsim_core::simdrivelabel::*; use fastsim_core::*; use pyo3imports::*; @@ -30,18 +31,24 @@ fn fastsimrust(py: Python, m: &PyModule) -> PyResult<()> { m.add_class::()?; m.add_class::()?; m.add_class::()?; + #[cfg(feature = "simdrivelabel")] m.add_class::()?; + #[cfg(feature = "simdrivelabel")] m.add_class::()?; + #[cfg(feature = "simdrivelabel")] m.add_class::()?; m.add_class::()?; cycle::register(py, m)?; #[cfg(feature = "default")] m.add_function(wrap_pyfunction!(vehicle_utils::abc_to_drag_coeffs, m)?)?; + #[cfg(feature = "simdrivelabel")] m.add_function(wrap_pyfunction!(make_accel_trace_py, m)?)?; + #[cfg(feature = "simdrivelabel")] m.add_function(wrap_pyfunction!(get_net_accel_py, m)?)?; - #[cfg(feature = "default")] + #[cfg(feature = "simdrivelabel")] m.add_function(wrap_pyfunction!(get_label_fe_py, m)?)?; + #[cfg(feature = "simdrivelabel")] m.add_function(wrap_pyfunction!(get_label_fe_phev_py, m)?)?; #[cfg(feature = "vehicle-import")] m.add_function(wrap_pyfunction!( From 2e66068b96a6f1d15200fd2339c01e6d92089dde Mon Sep 17 00:00:00 2001 From: Kyle Carow Date: Tue, 27 Feb 2024 15:31:45 -0700 Subject: [PATCH 26/30] fix simdrivelabel feature-gate --- python/fastsim/tests/test_simdrivelabel.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/python/fastsim/tests/test_simdrivelabel.py b/python/fastsim/tests/test_simdrivelabel.py index d2a8f958..4b1fc8b2 100644 --- a/python/fastsim/tests/test_simdrivelabel.py +++ b/python/fastsim/tests/test_simdrivelabel.py @@ -3,7 +3,7 @@ from fastsim import fastsimrust -@pytest.mark.skipif("default" not in fastsimrust.enabled_features(), reason='requires "default" feature') +@pytest.mark.skipif("simdrivelabel" not in fastsimrust.enabled_features(), reason='requires "simdrivelabel" feature') class TestSimDriveLabel(unittest.TestCase): def test_get_label_fe_conv(self): veh = fastsimrust.RustVehicle.mock_vehicle() From aedac3c9e652dadfd78bb940512d99e90d7ef99e Mon Sep 17 00:00:00 2001 From: Kyle Carow Date: Tue, 27 Feb 2024 15:46:13 -0700 Subject: [PATCH 27/30] make simdrivelabel feature default --- rust/fastsim-py/Cargo.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/rust/fastsim-py/Cargo.toml b/rust/fastsim-py/Cargo.toml index 24f976c1..c067a7da 100644 --- a/rust/fastsim-py/Cargo.toml +++ b/rust/fastsim-py/Cargo.toml @@ -31,6 +31,7 @@ default = [ "fastsim-core/default", "logging", "resources", + "simdrivelabel", "validation", "vehicle-import", ] From 9cca67f9f74859987d8cf3e96f53809854b6f891 Mon Sep 17 00:00:00 2001 From: Kyle Carow Date: Wed, 28 Feb 2024 10:05:17 -0700 Subject: [PATCH 28/30] replace polynomial dep with closure --- rust/fastsim-core/Cargo.toml | 2 -- rust/fastsim-core/src/vehicle_utils.rs | 21 ++++++++++++--------- 2 files changed, 12 insertions(+), 11 deletions(-) diff --git a/rust/fastsim-core/Cargo.toml b/rust/fastsim-core/Cargo.toml index adc3cb54..7e03b061 100644 --- a/rust/fastsim-core/Cargo.toml +++ b/rust/fastsim-core/Cargo.toml @@ -23,7 +23,6 @@ csv = "1.1" serde_json = "1.0.81" bincode = { optional = true, version = "1.3.3" } log = { optional = true, version = "0.4.17" } -polynomial = { optional = true, version = "0.2.4" } argmin = { optional = true, version = "0.7.0" } argmin-math = { optional = true, version = "0.2.1", features = [ "ndarray_latest-nolinalg-serde", @@ -59,7 +58,6 @@ default = [ "dep:argmin", "dep:argmin-math", "dep:directories", - "dep:polynomial", "logging", "resources", "simdrivelabel", diff --git a/rust/fastsim-core/src/vehicle_utils.rs b/rust/fastsim-core/src/vehicle_utils.rs index 99c4d62c..de53eeec 100644 --- a/rust/fastsim-core/src/vehicle_utils.rs +++ b/rust/fastsim-core/src/vehicle_utils.rs @@ -5,8 +5,6 @@ use argmin::core::{CostFunction, Executor, OptimizationResult, State}; #[cfg(feature = "default")] use argmin::solver::neldermead::NelderMead; use ndarray::{array, Array1}; -#[cfg(feature = "default")] -use polynomial::Polynomial; use std::{result::Result, thread, time::Duration}; use ureq::{Error as OtherError, Error::Status, Response}; @@ -78,13 +76,13 @@ pub fn abc_to_drag_coeffs( }; // polynomial function for pounds vs speed - let dyno_func_lb: Polynomial = Polynomial::new(vec![a_lbf, b_lbf__mph, c_lbf__mph2]); + let dyno_func_lb = |x: &f64| a_lbf + b_lbf__mph * x + c_lbf__mph2 * x.powi(2); let drag_coef: f64; let wheel_rr_coef: f64; if simdrive_optimize.unwrap_or(true) { - let cost: GetError = GetError { + let cost = GetError { cycle: &cyc, vehicle: veh, dyno_func_lb: &dyno_func_lb, @@ -142,21 +140,26 @@ pub fn get_error_val(model: Array1, test: Array1, time_steps: Array1 { +struct GetError<'a, F> +where + F: Fn(&f64) -> f64, +{ cycle: &'a RustCycle, vehicle: &'a RustVehicle, - dyno_func_lb: &'a Polynomial, + dyno_func_lb: &'a F, } #[cfg(feature = "default")] -impl CostFunction for GetError<'_> { +impl CostFunction for GetError<'_, F> +where + F: Fn(&f64) -> f64, +{ type Param = Array1; type Output = f64; fn cost(&self, x: &Self::Param) -> anyhow::Result { let mut veh: RustVehicle = self.vehicle.clone(); let cyc: RustCycle = self.cycle.clone(); - let dyno_func_lb: Polynomial = self.dyno_func_lb.clone(); veh.drag_coef = x[0]; veh.wheel_rr_coef = x[1]; @@ -181,7 +184,7 @@ impl CostFunction for GetError<'_> { * (sd_coast.drag_kw + sd_coast.rr_kw) / sd_coast.mps_ach) .slice_move(s![0..cutoff]), - (sd_coast.mph_ach.map(|x| dyno_func_lb.eval(*x)) + (sd_coast.mph_ach.map(self.dyno_func_lb) * Array::from_vec(vec![super::params::N_PER_LBF; sd_coast.mph_ach.len()])) .slice_move(s![0..cutoff]), cyc.time_s.slice_move(s![0..cutoff]), From 5bb62a81e56333499c5f20f675568c02925dde16 Mon Sep 17 00:00:00 2001 From: Kyle Carow Date: Wed, 28 Feb 2024 11:11:32 -0700 Subject: [PATCH 29/30] remove unnecessary type annotations --- rust/fastsim-cli/src/bin/fastsim-cli.rs | 12 +- rust/fastsim-core/build.rs | 2 +- .../src/add_pyo3_api/mod.rs | 4 +- .../src/approx_eq_derive.rs | 2 +- .../fastsim-proc-macros/src/doc_field.rs | 4 +- rust/fastsim-core/src/cycle.rs | 42 ++--- rust/fastsim-core/src/imports.rs | 1 - rust/fastsim-core/src/params.rs | 20 +-- rust/fastsim-core/src/simdrive.rs | 36 ++-- rust/fastsim-core/src/simdrive/cyc_mods.rs | 46 ++--- .../src/simdrive/simdrive_impl.rs | 4 +- rust/fastsim-core/src/thermal.rs | 12 +- rust/fastsim-core/src/utils.rs | 30 ++-- rust/fastsim-core/src/vehicle.rs | 158 +++++++++--------- rust/fastsim-core/src/vehicle_utils.rs | 58 +++---- 15 files changed, 215 insertions(+), 216 deletions(-) diff --git a/rust/fastsim-cli/src/bin/fastsim-cli.rs b/rust/fastsim-cli/src/bin/fastsim-cli.rs index 3d73845a..99b80705 100644 --- a/rust/fastsim-cli/src/bin/fastsim-cli.rs +++ b/rust/fastsim-cli/src/bin/fastsim-cli.rs @@ -240,9 +240,9 @@ pub fn main() -> anyhow::Result<()> { // TODO: put in logic here for loading vehicle for adopt-hd // with same file format as regular adopt and same outputs retured - let is_adopt: bool = fastsim_api.adopt.is_some() && fastsim_api.adopt.unwrap(); - let mut fc_pwr_out_perc: Option> = None; - let mut hd_h2_diesel_ice_h2share: Option> = None; + let is_adopt = fastsim_api.adopt.is_some() && fastsim_api.adopt.unwrap(); + let mut fc_pwr_out_perc = None; + let mut hd_h2_diesel_ice_h2share = None; let veh = if let Some(veh_string) = fastsim_api.veh { if is_adopt || is_adopt_hd { let (veh_string, pwr_out_perc, h2share) = json_rewrite(veh_string)?; @@ -401,8 +401,8 @@ impl SerdeAPI for ParsedValue {} fn json_rewrite(x: String) -> anyhow::Result<(String, Option>, Option>)> { let adoptstring = x; - let mut fc_pwr_out_perc: Option> = None; - let mut hd_h2_diesel_ice_h2share: Option> = None; + let mut fc_pwr_out_perc = None; + let mut hd_h2_diesel_ice_h2share = None; let mut parsed_data: Value = serde_json::from_str(&adoptstring)?; @@ -440,7 +440,7 @@ fn json_rewrite(x: String) -> anyhow::Result<(String, Option>, Option publish_path, false => build_path, }; diff --git a/rust/fastsim-core/fastsim-proc-macros/src/add_pyo3_api/mod.rs b/rust/fastsim-core/fastsim-proc-macros/src/add_pyo3_api/mod.rs index a7189e34..c899c52b 100644 --- a/rust/fastsim-core/fastsim-proc-macros/src/add_pyo3_api/mod.rs +++ b/rust/fastsim-core/fastsim-proc-macros/src/add_pyo3_api/mod.rs @@ -9,7 +9,7 @@ pub fn add_pyo3_api(attr: TokenStream, item: TokenStream) -> TokenStream { let mut ast = syn::parse_macro_input!(item as syn::ItemStruct); // println!("{}", ast.ident.to_string()); let ident = &ast.ident; - let _is_state_or_history: bool = + let _is_state_or_history = ident.to_string().contains("State") || ident.to_string().contains("HistoryVec"); let mut impl_block = TokenStream2::default(); @@ -344,7 +344,7 @@ pub fn add_pyo3_api(attr: TokenStream, item: TokenStream) -> TokenStream { final_output.extend::(quote! { #[cfg_attr(feature="pyo3", pyclass(module = "fastsimrust", subclass))] }); - let mut output: TokenStream2 = ast.to_token_stream(); + let mut output = ast.to_token_stream(); output.extend(impl_block); // if ast.ident.to_string() == "RustSimDrive" { // println!("{}", output.to_string()); diff --git a/rust/fastsim-core/fastsim-proc-macros/src/approx_eq_derive.rs b/rust/fastsim-core/fastsim-proc-macros/src/approx_eq_derive.rs index 5babdf0a..34c467e8 100644 --- a/rust/fastsim-core/fastsim-proc-macros/src/approx_eq_derive.rs +++ b/rust/fastsim-core/fastsim-proc-macros/src/approx_eq_derive.rs @@ -20,7 +20,7 @@ pub fn approx_eq_derive(input: TokenStream) -> TokenStream { generated.append_all(quote! { impl ApproxEq for #name { fn approx_eq(&self, other: &#name, tol: f64) -> bool { - let mut approx_eq_vals: Vec = Vec::new(); + let mut approx_eq_vals = Vec::new(); #(approx_eq_vals.push(self.#field_names.approx_eq(&other.#field_names, tol));)* approx_eq_vals.iter().all(|&x| x) } diff --git a/rust/fastsim-core/fastsim-proc-macros/src/doc_field.rs b/rust/fastsim-core/fastsim-proc-macros/src/doc_field.rs index 068f334c..ec3599f7 100644 --- a/rust/fastsim-core/fastsim-proc-macros/src/doc_field.rs +++ b/rust/fastsim-core/fastsim-proc-macros/src/doc_field.rs @@ -7,7 +7,7 @@ pub fn doc_field(_attr: TokenStream, item: TokenStream) -> TokenStream { let new_fields = if let syn::Fields::Named(FieldsNamed { named, .. }) = &mut item_struct.fields { - let mut new_doc_fields: Vec = Vec::new(); + let mut new_doc_fields = Vec::new(); for field in named.iter_mut() { let mut skip_doc = false; remove_handled_attrs(field, &mut skip_doc); @@ -57,7 +57,7 @@ pub fn doc_field(_attr: TokenStream, item: TokenStream) -> TokenStream { let struct_vis = item_struct.vis; let struct_attrs = item_struct.attrs; - let output: TokenStream2 = quote! { + let output = quote! { #(#struct_attrs)* #struct_vis struct #struct_ident { #new_fields diff --git a/rust/fastsim-core/src/cycle.rs b/rust/fastsim-core/src/cycle.rs index 101bf295..a4009fb5 100644 --- a/rust/fastsim-core/src/cycle.rs +++ b/rust/fastsim-core/src/cycle.rs @@ -111,7 +111,7 @@ pub fn accel_for_constant_jerk(n: usize, a0: f64, k: f64, dt: f64) -> f64 { /// Apply `accel_for_constant_jerk` to full pub fn accel_array_for_constant_jerk(nmax: usize, a0: f64, k: f64, dt: f64) -> Array1 { - let mut accels: Vec = Vec::new(); + let mut accels = Vec::new(); for n in 0..nmax { accels.push(accel_for_constant_jerk(n, a0, k, dt)); } @@ -120,7 +120,7 @@ pub fn accel_array_for_constant_jerk(nmax: usize, a0: f64, k: f64, dt: f64) -> A /// Calculate the average speed per each step in m/s pub fn average_step_speeds(cyc: &RustCycle) -> Array1 { - let mut result: Vec = Vec::with_capacity(cyc.len()); + let mut result = Vec::with_capacity(cyc.len()); result.push(0.0); for i in 1..cyc.len() { result.push(0.5 * (cyc.mps[i] + cyc.mps[i - 1])); @@ -141,7 +141,7 @@ pub fn trapz_step_distances(cyc: &RustCycle) -> Array1 { } pub fn trapz_step_distances_primitive(time_s: &Array1, mps: &Array1) -> Array1 { - let mut delta_dists_m: Vec = Vec::with_capacity(time_s.len()); + let mut delta_dists_m = Vec::with_capacity(time_s.len()); delta_dists_m.push(0.0); for i in 1..time_s.len() { delta_dists_m.push((time_s[i] - time_s[i - 1]) * 0.5 * (mps[i] + mps[i - 1])); @@ -153,7 +153,7 @@ pub fn trapz_step_distances_primitive(time_s: &Array1, mps: &Array1) - /// (i.e., distance traveled up to sample point i-1) /// Distance is in meters. pub fn trapz_step_start_distance(cyc: &RustCycle, i: usize) -> f64 { - let mut dist_m: f64 = 0.0; + let mut dist_m = 0.0; for i in 1..i { dist_m += (cyc.time_s[i] - cyc.time_s[i - 1]) * 0.5 * (cyc.mps[i] + cyc.mps[i - 1]); } @@ -199,7 +199,7 @@ pub fn time_spent_moving(cyc: &RustCycle, stopped_speed_m_per_s: Option) -> /// that name to all microtrips pub fn to_microtrips(cycle: &RustCycle, stop_speed_m_per_s: Option) -> Vec { let stop_speed_m_per_s = stop_speed_m_per_s.unwrap_or(1e-6); - let mut microtrips: Vec = Vec::new(); + let mut microtrips = Vec::new(); let ts = cycle.time_s.to_vec(); let vs = cycle.mps.to_vec(); let gs = cycle.grade.to_vec(); @@ -278,7 +278,7 @@ pub fn create_dist_and_target_speeds_by_microtrip( } else { blend_factor }; - let mut dist_and_tgt_speeds: Vec<(f64, f64)> = Vec::new(); + let mut dist_and_tgt_speeds = Vec::new(); // Split cycle into microtrips let microtrips = to_microtrips(cyc, None); let mut dist_at_start_of_microtrip_m = 0.0; @@ -421,9 +421,9 @@ impl RustCycleCache { ndarrcumsum(&xs) }; let stops = Array::from_iter(cyc.mps.iter().map(|v| v <= &tol)); - let mut interp_ds: Vec = Vec::with_capacity(num_items); - let mut interp_is: Vec = Vec::with_capacity(num_items); - let mut interp_hs: Vec = Vec::with_capacity(num_items); + let mut interp_ds = Vec::with_capacity(num_items); + let mut interp_is = Vec::with_capacity(num_items); + let mut interp_hs = Vec::with_capacity(num_items); for idx in 0..num_items { let d = trapz_distances_m[idx]; if interp_ds.is_empty() || d > *interp_ds.last().unwrap() { @@ -968,7 +968,7 @@ impl RustCycle { distance_m: f64, cache: Option<&RustCycleCache>, ) -> f64 { - let tol: f64 = 1e-6; + let tol = 1e-6; match cache { Some(rcc) => { for (&dist, &v) in rcc.trapz_distances_m.iter().zip(self.mps.iter()) { @@ -1071,8 +1071,8 @@ impl RustCycle { // time-to-stop (s) let tts_s = -v0 / brake_accel_m_per_s2; // number of steps to take - let n: usize = (tts_s / dt).round() as usize; - let n: usize = if n < 2 { 2 } else { n }; // need at least 2 steps + let n = (tts_s / dt).round() as usize; + let n = if n < 2 { 2 } else { n }; // need at least 2 steps let (jerk_m_per_s3, accel_m_per_s2) = calc_constant_jerk_trajectory(n, 0.0, v0, dts_m, 0.0, dt)?; Ok(( @@ -1154,16 +1154,16 @@ pub fn detect_passing( } let zero_speed_tol_m_per_s = 1e-6; let dist_tol_m = dist_tol_m.unwrap_or(0.1); - let mut v0: f64 = cyc.mps[i - 1]; - let d0: f64 = trapz_step_start_distance(cyc, i); - let mut v0_lv: f64 = cyc0.mps[i - 1]; - let d0_lv: f64 = trapz_step_start_distance(cyc0, i); + let mut v0 = cyc.mps[i - 1]; + let d0 = trapz_step_start_distance(cyc, i); + let mut v0_lv = cyc0.mps[i - 1]; + let d0_lv = trapz_step_start_distance(cyc0, i); let mut d = d0; let mut d_lv = d0_lv; - let mut rendezvous_idx: Option = None; - let mut rendezvous_num_steps: usize = 0; - let mut rendezvous_distance_m: f64 = 0.0; - let mut rendezvous_speed_m_per_s: f64 = 0.0; + let mut rendezvous_idx = None; + let mut rendezvous_num_steps = 0; + let mut rendezvous_distance_m = 0.0; + let mut rendezvous_speed_m_per_s = 0.0; for di in 0..(cyc.mps.len() - i) { let idx = i + di; let v = cyc.mps[idx]; @@ -1237,7 +1237,7 @@ mod tests { #[test] fn test_loading_a_cycle_from_the_filesystem() { let cyc_file_path = resources_path().join("cycles/udds.csv"); - let expected_udds_length: usize = 1370; + let expected_udds_length = 1370; let cyc = RustCycle::from_csv_file(cyc_file_path).unwrap(); let num_entries = cyc.len(); assert_eq!(cyc.name, String::from("udds")); diff --git a/rust/fastsim-core/src/imports.rs b/rust/fastsim-core/src/imports.rs index f054cb15..6216dd4c 100644 --- a/rust/fastsim-core/src/imports.rs +++ b/rust/fastsim-core/src/imports.rs @@ -13,4 +13,3 @@ pub(crate) use std::path::PathBuf; pub(crate) use crate::traits::*; pub(crate) use crate::utils::*; -pub(crate) use crate::vehicle_utils::*; diff --git a/rust/fastsim-core/src/params.rs b/rust/fastsim-core/src/params.rs index 20a3877c..db40d339 100644 --- a/rust/fastsim-core/src/params.rs +++ b/rust/fastsim-core/src/params.rs @@ -58,12 +58,12 @@ impl SerdeAPI for RustPhysicalProperties {} impl Default for RustPhysicalProperties { fn default() -> Self { - let air_density_kg_per_m3: f64 = 1.2; - let a_grav_mps2: f64 = 9.81; - let kwh_per_gge: f64 = 33.7; + let air_density_kg_per_m3 = 1.2; + let a_grav_mps2 = 9.81; + let kwh_per_gge = 33.7; #[allow(non_snake_case)] - let fuel_rho_kg__L: f64 = 0.75; - let fuel_afr_stoich: f64 = 14.7; + let fuel_rho_kg__L = 0.75; + let fuel_afr_stoich = 14.7; Self { air_density_kg_per_m3, a_grav_mps2, @@ -161,7 +161,7 @@ impl SerdeAPI for AdjCoef {} impl Default for RustLongParams { fn default() -> Self { - let long_params_str: &str = include_str!("../resources/longparams.json"); + let long_params_str = include_str!("../resources/longparams.json"); let long_params = Self::from_json(long_params_str).unwrap(); long_params } @@ -173,22 +173,22 @@ mod params_test { #[test] fn test_get_long_params() { - let long_params: RustLongParams = RustLongParams::default(); + let long_params = RustLongParams::default(); - let adj_coef_2008: AdjCoef = AdjCoef { + let adj_coef_2008 = AdjCoef { city_intercept: 0.003259, city_slope: 1.1805, hwy_intercept: 0.001376, hwy_slope: 1.3466, }; - let adj_coef_2017: AdjCoef = AdjCoef { + let adj_coef_2017 = AdjCoef { city_intercept: 0.004091, city_slope: 1.1601, hwy_intercept: 0.003191, hwy_slope: 1.2945, }; - let mut adj_coef_map: HashMap = HashMap::new(); + let mut adj_coef_map = HashMap::new(); adj_coef_map.insert(String::from("2008"), adj_coef_2008); adj_coef_map.insert(String::from("2017"), adj_coef_2017); diff --git a/rust/fastsim-core/src/simdrive.rs b/rust/fastsim-core/src/simdrive.rs index 3f79035a..ef7f11a7 100644 --- a/rust/fastsim-core/src/simdrive.rs +++ b/rust/fastsim-core/src/simdrive.rs @@ -67,19 +67,19 @@ impl Default for RustSimDriveParams { // if true, missed trace correction is active, default = false let missed_trace_correction = false; // maximum time dilation factor to "catch up" with trace -- e.g. 1.0 means 100% increase in step size - let max_time_dilation: f64 = 1.0; + let max_time_dilation = 1.0; // minimum time dilation margin to let trace "catch up" -- e.g. -0.5 means 50% reduction in step size - let min_time_dilation: f64 = -0.5; - let time_dilation_tol: f64 = 5e-4; // convergence criteria for time dilation - let max_trace_miss_iters: u32 = 5; // number of iterations to achieve time dilation correction - let trace_miss_speed_mps_tol: f64 = 1.0; // # threshold of error in speed [m/s] that triggers warning - let trace_miss_time_tol: f64 = 1e-3; // threshold for printing warning when time dilation is active - let trace_miss_dist_tol: f64 = 1e-3; // threshold of fractional eror in distance that triggers warning - let sim_count_max: usize = 30; // max allowable number of HEV SOC iterations - let newton_gain: f64 = 0.9; // newton solver gain - let newton_max_iter: u32 = 100; // newton solver max iterations - let newton_xtol: f64 = 1e-9; // newton solver tolerance - let energy_audit_error_tol: f64 = 0.002; // tolerance for energy audit error warning, i.e. 0.1% + let min_time_dilation = -0.5; + let time_dilation_tol = 5e-4; // convergence criteria for time dilation + let max_trace_miss_iters = 5; // number of iterations to achieve time dilation correction + let trace_miss_speed_mps_tol = 1.0; // # threshold of error in speed [m/s] that triggers warning + let trace_miss_time_tol = 1e-3; // threshold for printing warning when time dilation is active + let trace_miss_dist_tol = 1e-3; // threshold of fractional eror in distance that triggers warning + let sim_count_max = 30; // max allowable number of HEV SOC iterations + let newton_gain = 0.9; // newton solver gain + let newton_max_iter = 100; // newton solver max iterations + let newton_xtol = 1e-9; // newton solver tolerance + let energy_audit_error_tol = 0.002; // tolerance for energy audit error warning, i.e. 0.1% // Coasting let coast_allow = false; let coast_allow_passing = false; @@ -99,7 +99,7 @@ impl Default for RustSimDriveParams { let idm_decel_m_per_s2 = 1.5; let idm_v_desired_in_m_per_s_by_distance_m = None; // EPA fuel economy adjustment parameters - let max_epa_adj: f64 = 0.3; // maximum EPA adjustment factor + let max_epa_adj = 0.3; // maximum EPA adjustment factor Self { favor_grade_accuracy, missed_trace_correction, @@ -207,9 +207,9 @@ impl Default for RustSimDriveParams { blend_factor: Option, min_target_speed_m_per_s: Option, ) -> anyhow::Result<()> { - let by_microtrip: bool = by_microtrip.unwrap_or(false); - let extend_fraction: f64 = extend_fraction.unwrap_or(0.1); - let blend_factor: f64 = blend_factor.unwrap_or(0.0); + let by_microtrip = by_microtrip.unwrap_or(false); + let extend_fraction = extend_fraction.unwrap_or(0.1); + let blend_factor = blend_factor.unwrap_or(0.0); let min_target_speed_m_per_s = min_target_speed_m_per_s.unwrap_or(8.0); self.activate_eco_cruise_rust( by_microtrip, extend_fraction, blend_factor, min_target_speed_m_per_s) @@ -571,10 +571,10 @@ impl SerdeAPI for RustSimDrive { // // SIM DRIVE // let mut sd = RustSimDrive::__new__(cyc, veh); -// let init_soc: f64 = 0.5; +// let init_soc = 0.5; // sd.walk(init_soc); -// let expected_final_i: usize = cycle_length; +// let expected_final_i = cycle_length; // assert_eq!(sd.i, expected_final_i); // } // } diff --git a/rust/fastsim-core/src/simdrive/cyc_mods.rs b/rust/fastsim-core/src/simdrive/cyc_mods.rs index 91d9d62f..bd2a06b9 100644 --- a/rust/fastsim-core/src/simdrive/cyc_mods.rs +++ b/rust/fastsim-core/src/simdrive/cyc_mods.rs @@ -5,7 +5,7 @@ use super::*; use crate::cycle::{ accel_array_for_constant_jerk, accel_for_constant_jerk, calc_constant_jerk_trajectory, create_dist_and_target_speeds_by_microtrip, detect_passing, extend_cycle, - trapz_distance_for_step, trapz_step_distances, trapz_step_start_distance, PassingInfo, + trapz_distance_for_step, trapz_step_distances, trapz_step_start_distance, }; use crate::simdrive::RustSimDrive; use crate::utils::{add_from, max, min, ndarrcumsum, ndarrunique}; @@ -119,7 +119,7 @@ impl RustSimDrive { let v0_m_per_s = self.mps_ach[i - 1]; let v0_lead_m_per_s = self.cyc0.mps[i - 1]; let dv0_m_per_s = v0_m_per_s - v0_lead_m_per_s; - let d0_lead_m: f64 = self.cyc0_cache.trapz_distances_m[(i - 1).max(0)] + s0_m; + let d0_lead_m = self.cyc0_cache.trapz_distances_m[(i - 1).max(0)] + s0_m; let d0_m = trapz_step_start_distance(&self.cyc, i); let s_m = max(d0_lead_m - d0_m, 0.01); // IDM EQUATIONS @@ -237,8 +237,8 @@ impl RustSimDrive { < self.sim_params.time_dilation_tol || self.cyc.mps[i] == 0.0; - let mut d_short: Vec = vec![]; - let mut t_dilation: Vec = vec![0.0]; // no time dilation initially + let mut d_short = vec![]; + let mut t_dilation = vec![0.0]; // no time dilation initially if !trace_met { self.trace_miss_iters[i] += 1; @@ -375,8 +375,8 @@ impl RustSimDrive { assert![a_brake <= 0.0]; let ds = &self.cyc0_cache.trapz_distances_m; let d0 = trapz_step_start_distance(&self.cyc, i); - let mut distances_m: Vec = Vec::with_capacity(ds.len()); - let mut grade_by_distance: Vec = Vec::with_capacity(ds.len()); + let mut distances_m = Vec::with_capacity(ds.len()); + let mut grade_by_distance = Vec::with_capacity(ds.len()); for idx in 0..ds.len() { if ds[idx] >= d0 { distances_m.push(ds[idx] - d0); @@ -408,19 +408,19 @@ impl RustSimDrive { let mut d = 0.0; let d_max = distances_m.last().unwrap() - dtb; let unique_grades = ndarrunique(&grade_by_distance); - let unique_grade: Option = if unique_grades.len() == 1 { + let unique_grade = if unique_grades.len() == 1 { Some(unique_grades[0]) } else { None }; - let has_unique_grade: bool = unique_grade.is_some(); + let has_unique_grade = unique_grade.is_some(); let max_iter = 180; let iters_per_step = if self.sim_params.favor_grade_accuracy { 2 } else { 1 }; - let mut new_speeds_m_per_s: Vec = Vec::with_capacity(max_iter as usize); + let mut new_speeds_m_per_s = Vec::with_capacity(max_iter as usize); let mut v = v0; let mut iter = 0; let mut idx = i; @@ -521,7 +521,7 @@ impl RustSimDrive { gs.len() ); let d0 = trapz_step_start_distance(&self.cyc, i); - let mut grade_by_distance: Vec = Vec::with_capacity(ds.len()); + let mut grade_by_distance = Vec::with_capacity(ds.len()); for idx in 0..ds.len() { if ds[idx] >= d0 { grade_by_distance.push(gs[idx]); @@ -624,10 +624,10 @@ impl RustSimDrive { let brake_accel_m_per_s2 = self.sim_params.coast_brake_accel_m_per_s2; let time_horizon_s = max(self.sim_params.coast_time_horizon_for_adjustment_s, 1.0); // distance_horizon_m = 1000.0 - let not_found_n: usize = 0; - let not_found_jerk_m_per_s3: f64 = 0.0; - let not_found_accel_m_per_s2: f64 = 0.0; - let not_found: (bool, usize, f64, f64) = ( + let not_found_n = 0; + let not_found_jerk_m_per_s3 = 0.0; + let not_found_accel_m_per_s2 = 0.0; + let not_found = ( false, not_found_n, not_found_jerk_m_per_s3, @@ -707,9 +707,9 @@ impl RustSimDrive { r_bi_jerk_m_per_s3, dt, ); - let as_bi_min: f64 = + let as_bi_min = as_bi.to_vec().into_iter().reduce(f64::min).unwrap_or(0.0); - let as_bi_max: f64 = + let as_bi_max = as_bi.to_vec().into_iter().reduce(f64::max).unwrap_or(0.0); let accel_spread = (as_bi_max - as_bi_min).abs(); let flag = (as_bi_max < (max_accel_m_per_s2 + 1e-6) @@ -756,14 +756,14 @@ impl RustSimDrive { for idx in i..self.cyc.len() { self.coast_delay_index[idx] = 0; // clear all future coast-delays } - let mut coast_delay: Option = None; + let mut coast_delay = None; if !self.sim_params.idm_allow && self.cyc.mps[i] < speed_tol { let d0 = trapz_step_start_distance(&self.cyc, i); let d0_lv = self.cyc0_cache.trapz_distances_m[i - 1]; let dtlv0 = d0_lv - d0; if dtlv0.abs() > dist_tol { let mut d_lv = 0.0; - let mut min_dtlv: Option = None; + let mut min_dtlv = None; for (idx, (&dd, &v)) in trapz_step_distances(&self.cyc0) .iter() .zip(self.cyc0.mps.iter()) @@ -814,11 +814,11 @@ impl RustSimDrive { /// RETURN: Bool, True if cyc was modified fn prevent_collisions(&mut self, i: usize, passing_tol_m: Option) -> anyhow::Result { let passing_tol_m = passing_tol_m.unwrap_or(1.0); - let collision: PassingInfo = detect_passing(&self.cyc, &self.cyc0, i, Some(passing_tol_m)); + let collision = detect_passing(&self.cyc, &self.cyc0, i, Some(passing_tol_m)); if !collision.has_collision { return Ok(false); } - let mut best: RendezvousTrajectory = RendezvousTrajectory { + let mut best = RendezvousTrajectory { found_trajectory: false, idx: 0, n: 0, @@ -869,8 +869,8 @@ impl RustSimDrive { collision.speed_m_per_s, dt, )?; - let mut accels_m_per_s2: Vec = vec![]; - let mut trace_accels_m_per_s2: Vec = vec![]; + let mut accels_m_per_s2 = vec![]; + let mut trace_accels_m_per_s2 = vec![]; for ni in 0..n { if (ni + idx + full_brake_steps) >= self.cyc.len() { break; @@ -887,7 +887,7 @@ impl RustSimDrive { / self.cyc.dt_s()[ni + idx + full_brake_steps], ); } - let all_sub_coast: bool = trace_accels_m_per_s2 + let all_sub_coast = trace_accels_m_per_s2 .iter() .copied() .zip(accels_m_per_s2.iter().copied()) diff --git a/rust/fastsim-core/src/simdrive/simdrive_impl.rs b/rust/fastsim-core/src/simdrive/simdrive_impl.rs index d604966a..38186339 100644 --- a/rust/fastsim-core/src/simdrive/simdrive_impl.rs +++ b/rust/fastsim-core/src/simdrive/simdrive_impl.rs @@ -27,11 +27,11 @@ pub struct CoastTrajectory { impl RustSimDrive { pub fn new(cyc: RustCycle, veh: RustVehicle) -> Self { - let hev_sim_count: usize = 0; + let hev_sim_count = 0; let cyc0 = cyc.clone(); let sim_params = RustSimDriveParams::default(); let props = params::RustPhysicalProperties::default(); - let i: usize = 1; // 1 # initialize step counter for possible use outside sim_drive_walk() + let i = 1; // 1 # initialize step counter for possible use outside sim_drive_walk() let cyc_len = cyc.len(); let cur_max_fs_kw_out = Array::zeros(cyc_len); let fc_trans_lim_kw = Array::zeros(cyc_len); diff --git a/rust/fastsim-core/src/thermal.rs b/rust/fastsim-core/src/thermal.rs index 10d87749..59943523 100644 --- a/rust/fastsim-core/src/thermal.rs +++ b/rust/fastsim-core/src/thermal.rs @@ -481,13 +481,13 @@ impl SimDriveHot { if let CabinHvacModelTypes::Internal(hvac_model) = &mut self.vehthrm.cabin_hvac_model { // flat plate model for isothermal, mixed-flow from Incropera and deWitt, Fundamentals of Heat and Mass // Transfer, 7th Edition - let cab_te_film_ext_deg_c: f64 = + let cab_te_film_ext_deg_c = 0.5 * (self.state.cab_te_deg_c + self.state.amb_te_deg_c); - let re_l: f64 = self.air.get_rho(cab_te_film_ext_deg_c, None) + let re_l = self.air.get_rho(cab_te_film_ext_deg_c, None) * self.sd.mps_ach[i - 1] * self.vehthrm.cab_l_length / self.air.get_mu(cab_te_film_ext_deg_c); - let re_l_crit: f64 = 5.0e5; // critical Re for transition to turbulence + let re_l_crit = 5.0e5; // critical Re for transition to turbulence let nu_l_bar = if re_l < re_l_crit { // equation 7.30 @@ -720,7 +720,7 @@ impl SimDriveHot { // Constitutive equations for catalyst // catalyst film temperature for property calculation - let cat_te_ext_film_deg_c: f64 = 0.5 * (self.state.cat_te_deg_c + self.state.amb_te_deg_c); + let cat_te_ext_film_deg_c = 0.5 * (self.state.cat_te_deg_c + self.state.amb_te_deg_c); // density * speed * diameter / dynamic viscosity self.state.cat_re_ext = self.air.get_rho(cat_te_ext_film_deg_c, None) * self.sd.mps_ach[i - 1] @@ -1105,7 +1105,7 @@ impl ThermalState { cat_te_deg_c_init: Option, ) -> Self { // Note default temperature is defined twice, see default() - let default_te_deg_c: f64 = 22.0; + let default_te_deg_c = 22.0; let amb_te_deg_c = amb_te_deg_c.unwrap_or(default_te_deg_c); Self { amb_te_deg_c, @@ -1123,7 +1123,7 @@ impl ThermalState { impl Default for ThermalState { fn default() -> Self { // Note default temperature is defined twice, see new() - let default_te_deg_c: f64 = 22.0; + let default_te_deg_c = 22.0; Self { fc_te_deg_c: default_te_deg_c, // overridden by new() diff --git a/rust/fastsim-core/src/utils.rs b/rust/fastsim-core/src/utils.rs index c24710ac..7974bfef 100644 --- a/rust/fastsim-core/src/utils.rs +++ b/rust/fastsim-core/src/utils.rs @@ -106,8 +106,8 @@ pub fn ndarrcumsum(arr: &Array1) -> Array1 { /// return the unique values of the array pub fn ndarrunique(arr: &Array1) -> Array1 { - let mut set: HashSet = HashSet::new(); - let mut new_arr: Vec = Vec::new(); + let mut set = HashSet::new(); + let mut new_arr = Vec::new(); let x_min = arr.min().unwrap(); let x_max = arr.max().unwrap(); let dx = if x_max == x_min { 1.0 } else { x_max - x_min }; @@ -131,8 +131,8 @@ pub fn interpolate( extrapolate: bool, ) -> f64 { assert!(x_data_in.len() == y_data_in.len()); - let mut new_x_data: Vec = Vec::new(); - let mut new_y_data: Vec = Vec::new(); + let mut new_x_data = Vec::new(); + let mut new_y_data = Vec::new(); let mut last_x = x_data_in[0]; for idx in 0..x_data_in.len() { if idx == 0 || (idx > 0 && x_data_in[idx] > last_x) { @@ -179,8 +179,8 @@ pub fn interpolate_vectors( extrapolate: bool, ) -> f64 { assert!(x_data_in.len() == y_data_in.len()); - let mut new_x_data: Vec = Vec::new(); - let mut new_y_data: Vec = Vec::new(); + let mut new_x_data = Vec::new(); + let mut new_y_data = Vec::new(); let mut last_x = x_data_in[0]; for idx in 0..x_data_in.len() { if idx == 0 || (idx > 0 && x_data_in[idx] > last_x) { @@ -670,33 +670,33 @@ mod tests { #[test] fn test_that_first_eq_finds_the_right_index_when_one_exists() { - let xs: [f64; 5] = [0.0, 1.2, 3.3, 4.4, 6.6]; + let xs = [0.0, 1.2, 3.3, 4.4, 6.6]; let idx = first_eq(&xs, 3.3).unwrap(); - let expected_idx: usize = 2; + let expected_idx = 2; assert_eq!(idx, expected_idx) } #[test] fn test_that_first_eq_yields_last_index_when_nothing_found() { - let xs: [f64; 5] = [0.0, 1.2, 3.3, 4.4, 6.6]; + let xs = [0.0, 1.2, 3.3, 4.4, 6.6]; let idx = first_eq(&xs, 7.0).unwrap(); - let expected_idx: usize = xs.len() - 1; + let expected_idx = xs.len() - 1; assert_eq!(idx, expected_idx) } #[test] fn test_that_first_grtr_finds_the_right_index_when_one_exists() { - let xs: [f64; 5] = [0.0, 1.2, 3.3, 4.4, 6.6]; + let xs = [0.0, 1.2, 3.3, 4.4, 6.6]; let idx = first_grtr(&xs, 3.0).unwrap(); - let expected_idx: usize = 2; + let expected_idx = 2; assert_eq!(idx, expected_idx) } #[test] fn test_that_first_grtr_yields_last_index_when_nothing_found() { - let xs: [f64; 5] = [0.0, 1.2, 3.3, 4.4, 6.6]; + let xs = [0.0, 1.2, 3.3, 4.4, 6.6]; let idx = first_grtr(&xs, 7.0).unwrap(); - let expected_idx: usize = xs.len() - 1; + let expected_idx = xs.len() - 1; assert_eq!(idx, expected_idx) } @@ -735,7 +735,7 @@ mod tests { } // #[test] // fn test_that_argmax_does_the_right_thing_on_an_empty_array(){ - // let xs: Array1 = Array::from_vec(vec![]); + // let xs = Array::from_vec(vec![]); // let idx = first_grtr(&xs); // // unclear what should happen here; np.argmax throws a ValueError in the case of an empty vector // // ... possibly we should return an Option type? diff --git a/rust/fastsim-core/src/vehicle.rs b/rust/fastsim-core/src/vehicle.rs index f51e3546..780d6ab6 100644 --- a/rust/fastsim-core/src/vehicle.rs +++ b/rust/fastsim-core/src/vehicle.rs @@ -1160,89 +1160,89 @@ mod tests { fn test_input_validation() { // set up vehicle input parameters let scenario_name = String::from("2016 FORD Escape 4cyl 2WD"); - let selection: u32 = 5; - let veh_year: u32 = 2016; + let selection = 5; + let veh_year = 2016; let veh_pt_type = String::from("whoops"); // bad input - let drag_coef: f64 = 0.355; - let frontal_area_m2: f64 = 3.066; - let glider_kg: f64 = -50.0; // bad input - let veh_cg_m: f64 = 0.53; - let drive_axle_weight_frac: f64 = 0.59; - let wheel_base_m: f64 = 2.6; - let cargo_kg: f64 = 136.0; - let veh_override_kg: Option = None; - let comp_mass_multiplier: f64 = 1.4; - let fs_max_kw: f64 = 2000.0; - let fs_secs_to_peak_pwr: f64 = 1.0; - let fs_kwh: f64 = 504.0; - let fs_kwh_per_kg: f64 = 9.89; - let fc_max_kw: f64 = -60.0; // bad input - let fc_pwr_out_perc: Vec = vec![ + let drag_coef = 0.355; + let frontal_area_m2 = 3.066; + let glider_kg = -50.0; // bad input + let veh_cg_m = 0.53; + let drive_axle_weight_frac = 0.59; + let wheel_base_m = 2.6; + let cargo_kg = 136.0; + let veh_override_kg = None; + let comp_mass_multiplier = 1.4; + let fs_max_kw = 2000.0; + let fs_secs_to_peak_pwr = 1.0; + let fs_kwh = 504.0; + let fs_kwh_per_kg = 9.89; + let fc_max_kw = -60.0; // bad input + let fc_pwr_out_perc = vec![ 0.0, 0.005, 0.015, 0.04, 0.06, 0.1, 0.14, 0.2, 0.4, 0.6, 0.8, 1.0, ]; - let fc_eff_type: String = String::from("SI"); - let fc_sec_to_peak_pwr: f64 = 6.0; - let fc_base_kg: f64 = 61.0; - let fc_kw_per_kg: f64 = 2.13; - let min_fc_time_on: f64 = 30.0; - let idle_fc_kw: f64 = 2.5; - let mc_max_kw: f64 = 0.0; - let mc_sec_to_peak_pwr: f64 = 4.0; - let mc_pe_kg_per_kw: f64 = 0.833; - let mc_pe_base_kg: f64 = 21.6; - let ess_max_kw: f64 = 0.0; - let ess_max_kwh: f64 = 0.0; - let ess_kg_per_kwh: f64 = 8.0; - let ess_base_kg: f64 = 75.0; - let ess_round_trip_eff: f64 = 0.97; - let ess_life_coef_a: f64 = 110.0; - let ess_life_coef_b: f64 = -0.6811; - let min_soc: f64 = -0.5; // bad input - let max_soc: f64 = 1.5; // bad input - let ess_dischg_to_fc_max_eff_perc: f64 = 0.0; - let ess_chg_to_fc_max_eff_perc: f64 = 0.0; - let wheel_inertia_kg_m2: f64 = 0.815; - let num_wheels: f64 = 4.0; - let wheel_rr_coef: f64 = 0.006; - let wheel_radius_m: f64 = 0.336; - let wheel_coef_of_fric: f64 = 0.7; - let max_accel_buffer_mph: f64 = 60.0; - let max_accel_buffer_perc_of_useable_soc: f64 = 0.2; - let perc_high_acc_buf: f64 = 0.0; - let mph_fc_on: f64 = 30.0; - let kw_demand_fc_on: f64 = 100.0; - let max_regen: f64 = 0.98; - let stop_start: bool = false; - let force_aux_on_fc: bool = true; - let alt_eff: f64 = 1.0; - let chg_eff: f64 = 0.86; - let aux_kw: f64 = 0.7; - let trans_kg: f64 = 114.0; - let trans_eff: f64 = 0.92; - let ess_to_fuel_ok_error: f64 = 0.005; - let val_udds_mpgge: f64 = 23.0; - let val_hwy_mpgge: f64 = 32.0; - let val_comb_mpgge: f64 = 26.0; - let val_udds_kwh_per_mile: f64 = f64::NAN; - let val_hwy_kwh_per_mile: f64 = f64::NAN; - let val_comb_kwh_per_mile: f64 = f64::NAN; - let val_cd_range_mi: f64 = f64::NAN; - let val_const65_mph_kwh_per_mile: f64 = f64::NAN; - let val_const60_mph_kwh_per_mile: f64 = f64::NAN; - let val_const55_mph_kwh_per_mile: f64 = f64::NAN; - let val_const45_mph_kwh_per_mile: f64 = f64::NAN; - let val_unadj_udds_kwh_per_mile: f64 = f64::NAN; - let val_unadj_hwy_kwh_per_mile: f64 = f64::NAN; - let val0_to60_mph: f64 = 9.9; - let val_ess_life_miles: f64 = f64::NAN; - let val_range_miles: f64 = f64::NAN; - let val_veh_base_cost: f64 = f64::NAN; - let val_msrp: f64 = f64::NAN; + let fc_eff_type = String::from("SI"); + let fc_sec_to_peak_pwr = 6.0; + let fc_base_kg = 61.0; + let fc_kw_per_kg = 2.13; + let min_fc_time_on = 30.0; + let idle_fc_kw = 2.5; + let mc_max_kw = 0.0; + let mc_sec_to_peak_pwr = 4.0; + let mc_pe_kg_per_kw = 0.833; + let mc_pe_base_kg = 21.6; + let ess_max_kw = 0.0; + let ess_max_kwh = 0.0; + let ess_kg_per_kwh = 8.0; + let ess_base_kg = 75.0; + let ess_round_trip_eff = 0.97; + let ess_life_coef_a = 110.0; + let ess_life_coef_b = -0.6811; + let min_soc = -0.5; // bad input + let max_soc = 1.5; // bad input + let ess_dischg_to_fc_max_eff_perc = 0.0; + let ess_chg_to_fc_max_eff_perc = 0.0; + let wheel_inertia_kg_m2 = 0.815; + let num_wheels = 4.0; + let wheel_rr_coef = 0.006; + let wheel_radius_m = 0.336; + let wheel_coef_of_fric = 0.7; + let max_accel_buffer_mph = 60.0; + let max_accel_buffer_perc_of_useable_soc = 0.2; + let perc_high_acc_buf = 0.0; + let mph_fc_on = 30.0; + let kw_demand_fc_on = 100.0; + let max_regen = 0.98; + let stop_start = false; + let force_aux_on_fc = true; + let alt_eff = 1.0; + let chg_eff = 0.86; + let aux_kw = 0.7; + let trans_kg = 114.0; + let trans_eff = 0.92; + let ess_to_fuel_ok_error = 0.005; + let val_udds_mpgge = 23.0; + let val_hwy_mpgge = 32.0; + let val_comb_mpgge = 26.0; + let val_udds_kwh_per_mile = f64::NAN; + let val_hwy_kwh_per_mile = f64::NAN; + let val_comb_kwh_per_mile = f64::NAN; + let val_cd_range_mi = f64::NAN; + let val_const65_mph_kwh_per_mile = f64::NAN; + let val_const60_mph_kwh_per_mile = f64::NAN; + let val_const55_mph_kwh_per_mile = f64::NAN; + let val_const45_mph_kwh_per_mile = f64::NAN; + let val_unadj_udds_kwh_per_mile = f64::NAN; + let val_unadj_hwy_kwh_per_mile = f64::NAN; + let val0_to60_mph = 9.9; + let val_ess_life_miles = f64::NAN; + let val_range_miles = f64::NAN; + let val_veh_base_cost = f64::NAN; + let val_msrp = f64::NAN; let props = RustPhysicalProperties::default(); - let regen_a: f64 = 500.0; - let regen_b: f64 = 0.99; - let fc_peak_eff_override: Option = None; - let mc_peak_eff_override: Option = Some(-0.50); // bad input + let regen_a = 500.0; + let regen_b = 0.99; + let fc_peak_eff_override = None; + let mc_peak_eff_override = Some(-0.50); // bad input let small_motor_power_kw = 7.5; let large_motor_power_kw = 75.0; let fc_perc_out_array = FC_PERC_OUT_ARRAY.clone().to_vec(); diff --git a/rust/fastsim-core/src/vehicle_utils.rs b/rust/fastsim-core/src/vehicle_utils.rs index de53eeec..ea6692ac 100644 --- a/rust/fastsim-core/src/vehicle_utils.rs +++ b/rust/fastsim-core/src/vehicle_utils.rs @@ -1,7 +1,7 @@ //! Module for utility functions that support the vehicle struct. #[cfg(feature = "default")] -use argmin::core::{CostFunction, Executor, OptimizationResult, State}; +use argmin::core::{CostFunction, Executor, State}; #[cfg(feature = "default")] use argmin::solver::neldermead::NelderMead; use ndarray::{array, Array1}; @@ -48,25 +48,25 @@ pub fn abc_to_drag_coeffs( // otherwise, directly use target A, B, C to calculate the results // show_plots: if True, plots are shown - let air_props: AirProperties = AirProperties::default(); + let air_props = AirProperties::default(); let props = RustPhysicalProperties::default(); - let cur_ambient_air_density_kg__m3: f64 = if custom_rho.unwrap_or(false) { + let cur_ambient_air_density_kg__m3 = if custom_rho.unwrap_or(false) { air_props.get_rho(custom_rho_temp_degC.unwrap_or(20.0), custom_rho_elevation_m) } else { props.air_density_kg_per_m3 }; - let vmax_mph: f64 = 70.0; - let a_newton: f64 = a_lbf * super::params::N_PER_LBF; - let _b_newton__mps: f64 = b_lbf__mph * super::params::N_PER_LBF * super::params::MPH_PER_MPS; - let c_newton__mps2: f64 = c_lbf__mph2 + let vmax_mph = 70.0; + let a_newton = a_lbf * super::params::N_PER_LBF; + let _b_newton__mps = b_lbf__mph * super::params::N_PER_LBF * super::params::MPH_PER_MPS; + let c_newton__mps2 = c_lbf__mph2 * super::params::N_PER_LBF * super::params::MPH_PER_MPS * super::params::MPH_PER_MPS; - let cd_len: usize = 300; + let cd_len = 300; - let cyc: RustCycle = RustCycle { + let cyc = RustCycle { time_s: (0..cd_len as i32).map(f64::from).collect(), mps: Array::linspace(vmax_mph / super::params::MPH_PER_MPS, 0.0, cd_len), grade: Array::zeros(cd_len), @@ -87,13 +87,13 @@ pub fn abc_to_drag_coeffs( vehicle: veh, dyno_func_lb: &dyno_func_lb, }; - let solver: NelderMead, f64> = + let solver = NelderMead::new(vec![array![0.0, 0.0], array![0.5, 0.0], array![0.5, 0.1]]); - let res: OptimizationResult<_, _, _> = Executor::new(cost, solver) + let res = Executor::new(cost, solver) .configure(|state| state.max_iters(100)) .run() .unwrap(); - let best_param: &Array1 = res.state().get_best_param().unwrap(); + let best_param = res.state().get_best_param().unwrap(); drag_coef = best_param[0]; wheel_rr_coef = best_param[1]; } else { @@ -129,8 +129,8 @@ pub fn get_error_val(model: Array1, test: Array1, time_steps: Array1 = (model - test).mapv(f64::abs); + let mut err = 0.0; + let y = (model - test).mapv(f64::abs); for index in 0..time_steps.len() - 1 { err += 0.5 * (time_steps[index + 1] - time_steps[index]) * (y[index] + y[index + 1]); @@ -158,22 +158,22 @@ where type Output = f64; fn cost(&self, x: &Self::Param) -> anyhow::Result { - let mut veh: RustVehicle = self.vehicle.clone(); - let cyc: RustCycle = self.cycle.clone(); + let mut veh = self.vehicle.clone(); + let cyc = self.cycle.clone(); veh.drag_coef = x[0]; veh.wheel_rr_coef = x[1]; - let mut sd_coast: RustSimDrive = RustSimDrive::new(self.cycle.clone(), veh); + let mut sd_coast = RustSimDrive::new(self.cycle.clone(), veh); sd_coast.impose_coast = Array::from_vec(vec![true; sd_coast.impose_coast.len()]); - let _sim_drive_result: Result<_, _> = sd_coast.sim_drive(None, None); + let _sim_drive_result = sd_coast.sim_drive(None, None); let cutoff_vec: Vec = sd_coast .mps_ach .indexed_iter() .filter_map(|(index, &item)| (item < 0.1).then_some(index)) .collect(); - let cutoff: usize = if cutoff_vec.is_empty() { + let cutoff = if cutoff_vec.is_empty() { sd_coast.mps_ach.len() } else { cutoff_vec[0] @@ -275,7 +275,7 @@ pub fn fetch_github_list(repo_url: Option) -> anyhow::Result let response = get_response(repo_url)?.into_reader(); let github_list: Vec = serde_json::from_reader(response).with_context(|| "Cannot parse github vehicle list.")?; - let mut vehicle_name_list: Vec = Vec::new(); + let mut vehicle_name_list = Vec::new(); for object in github_list.iter() { if object.url_type == "dir" { let url = &object.url; @@ -310,11 +310,11 @@ mod tests { #[test] fn test_get_error_val() { - let time_steps: Array1 = array![0.0, 1.0, 2.0, 3.0, 4.0]; - let model: Array1 = array![1.1, 4.6, 2.5, 3.7, 5.0]; - let test: Array1 = array![2.1, 4.5, 3.4, 4.8, 6.3]; + let time_steps = array![0.0, 1.0, 2.0, 3.0, 4.0]; + let model = array![1.1, 4.6, 2.5, 3.7, 5.0]; + let test = array![2.1, 4.5, 3.4, 4.8, 6.3]; - let error_val: f64 = get_error_val(model, test, time_steps); + let error_val = get_error_val(model, test, time_steps); println!("Error Value: {}", error_val); assert!(error_val.approx_eq(&0.8124999999999998, 1e-10)); @@ -323,12 +323,12 @@ mod tests { #[cfg(feature = "default")] #[test] fn test_abc_to_drag_coeffs() { - let mut veh: RustVehicle = RustVehicle::mock_vehicle(); - let a: f64 = 25.91; - let b: f64 = 0.1943; - let c: f64 = 0.01796; + let mut veh = RustVehicle::mock_vehicle(); + let a = 25.91; + let b = 0.1943; + let c = 0.01796; - let (drag_coef, wheel_rr_coef): (f64, f64) = abc_to_drag_coeffs( + let (drag_coef, wheel_rr_coef) = abc_to_drag_coeffs( &mut veh, a, b, From 2fb5d17f1ae65ba50adb1e6c09e3b92a613de8d7 Mon Sep 17 00:00:00 2001 From: Kyle Carow Date: Fri, 8 Mar 2024 13:13:47 -0700 Subject: [PATCH 30/30] --no-default-features fixes --- rust/fastsim-core/src/imports.rs | 1 + rust/fastsim-core/src/lib.rs | 1 + rust/fastsim-core/src/traits.rs | 2 ++ rust/fastsim-core/src/utils.rs | 13 +++++++++---- rust/fastsim-core/src/vehicle_utils.rs | 8 ++++++-- rust/fastsim-py/src/lib.rs | 2 +- 6 files changed, 20 insertions(+), 7 deletions(-) diff --git a/rust/fastsim-core/src/imports.rs b/rust/fastsim-core/src/imports.rs index 6216dd4c..5927a874 100644 --- a/rust/fastsim-core/src/imports.rs +++ b/rust/fastsim-core/src/imports.rs @@ -1,6 +1,7 @@ pub(crate) use anyhow::{anyhow, bail, ensure, Context}; #[cfg(feature = "bincode")] pub(crate) use bincode; +#[cfg(feature = "logging")] pub(crate) use log; pub(crate) use ndarray::{array, s, Array, Array1, Axis}; pub(crate) use serde::{Deserialize, Serialize}; diff --git a/rust/fastsim-core/src/lib.rs b/rust/fastsim-core/src/lib.rs index 6193c73e..649187d5 100644 --- a/rust/fastsim-core/src/lib.rs +++ b/rust/fastsim-core/src/lib.rs @@ -61,6 +61,7 @@ pub use fastsim_proc_macros as proc_macros; #[cfg_attr(feature = "pyo3", pyo3imports::pyfunction)] #[allow(clippy::vec_init_then_push)] pub fn enabled_features() -> Vec { + #[allow(unused_mut)] let mut enabled = vec![]; #[cfg(feature = "default")] diff --git a/rust/fastsim-core/src/traits.rs b/rust/fastsim-core/src/traits.rs index 05777613..e95cdd9b 100644 --- a/rust/fastsim-core/src/traits.rs +++ b/rust/fastsim-core/src/traits.rs @@ -224,6 +224,7 @@ pub trait SerdeAPI: Serialize + for<'a> Deserialize<'a> { /// the FASTSim data directory. If a path is given, the file will live /// within the path specified, within the subdirectory CACHE_FOLDER of the /// FASTSim data directory. + #[cfg(feature = "default")] fn to_cache>(&self, file_path: P) -> anyhow::Result<()> { let file_name = file_path .as_ref() @@ -266,6 +267,7 @@ pub trait SerdeAPI: Serialize + for<'a> Deserialize<'a> { /// find and instantiate the object. Instead, use the from_file method, and /// use the utils::path_to_cache() to find the FASTSim data directory /// location if needed. + #[cfg(feature = "default")] fn from_cache>(file_path: P) -> anyhow::Result { let full_file_path = Path::new(Self::CACHE_FOLDER).join(file_path); let path_including_directory = path_to_cache()?.join(full_file_path); diff --git a/rust/fastsim-core/src/utils.rs b/rust/fastsim-core/src/utils.rs index 7974bfef..68dc079e 100644 --- a/rust/fastsim-core/src/utils.rs +++ b/rust/fastsim-core/src/utils.rs @@ -6,8 +6,9 @@ use itertools::Itertools; use lazy_static::lazy_static; use ndarray::*; use regex::Regex; -use std::{collections::HashSet, io::Write}; -use url::Url; +use std::collections::HashSet; +#[cfg(feature = "default")] +use std::io::Write; #[cfg(feature = "default")] use curl::easy::Easy; @@ -519,6 +520,7 @@ pub fn tire_code_to_radius>(tire_code: S) -> anyhow::Result { } /// Assumes the parent directory exists. Assumes file doesn't exist (i.e., newly created) or that it will be truncated if it does. +#[cfg(feature = "default")] pub fn download_file_from_url(url: &str, file_path: &Path) -> anyhow::Result<()> { let mut handle = Easy::new(); handle.follow_location(true)?; @@ -555,8 +557,8 @@ pub fn download_file_from_url(url: &str, file_path: &Path) -> anyhow::Result<()> Ok(()) } -#[cfg(feature = "default")] /// Creates/gets an OS-specific data directory and returns the path. +#[cfg(feature = "default")] pub fn create_project_subdir>(subpath: P) -> anyhow::Result { let proj_dirs = ProjectDirs::from("gov", "NREL", "fastsim").ok_or_else(|| { anyhow!("Could not build path to project directory: \"gov.NREL.fastsim\"") @@ -567,6 +569,7 @@ pub fn create_project_subdir>(subpath: P) -> anyhow::Result anyhow::Result { let proj_dirs = ProjectDirs::from("gov", "NREL", "fastsim").ok_or_else(|| { anyhow!("Could not build path to project directory: \"gov.NREL.fastsim\"") @@ -588,6 +591,7 @@ pub fn path_to_cache() -> anyhow::Result { /// directories. If a single file needs deleting, the path_to_cache() function /// can be used to find the FASTSim data directory location. The file can then /// be found and manually deleted. +#[cfg(feature = "default")] pub fn clear_cache>(subpath: P) -> anyhow::Result<()> { let path = path_to_cache()?.join(subpath); Ok(std::fs::remove_dir_all(path)?) @@ -606,8 +610,9 @@ pub fn clear_cache>(subpath: P) -> anyhow::Result<()> { /// "rust_objects" for other Rust objects. /// Note: In order for the file to be save in the proper format, the URL needs /// to be a URL pointing directly to a file, for example a raw github URL. +#[cfg(feature = "default")] pub fn url_to_cache, P: AsRef>(url: S, subpath: P) -> anyhow::Result<()> { - let url = Url::parse(url.as_ref())?; + let url = url::Url::parse(url.as_ref())?; let file_name = url .path_segments() .and_then(|segments| segments.last()) diff --git a/rust/fastsim-core/src/vehicle_utils.rs b/rust/fastsim-core/src/vehicle_utils.rs index ea6692ac..6f2c6a2f 100644 --- a/rust/fastsim-core/src/vehicle_utils.rs +++ b/rust/fastsim-core/src/vehicle_utils.rs @@ -4,17 +4,21 @@ use argmin::core::{CostFunction, Executor, State}; #[cfg(feature = "default")] use argmin::solver::neldermead::NelderMead; -use ndarray::{array, Array1}; use std::{result::Result, thread, time::Duration}; use ureq::{Error as OtherError, Error::Status, Response}; +#[cfg(feature = "default")] use crate::air::*; +#[cfg(feature = "default")] use crate::cycle::RustCycle; use crate::imports::*; +#[cfg(feature = "default")] use crate::params::*; -#[cfg(feature = "pyo3")] +#[cfg(all(feature = "pyo3", feature = "default"))] use crate::pyo3imports::*; +#[cfg(feature = "default")] use crate::simdrive::RustSimDrive; +#[cfg(feature = "default")] use crate::vehicle::RustVehicle; #[allow(non_snake_case)] diff --git a/rust/fastsim-py/src/lib.rs b/rust/fastsim-py/src/lib.rs index 58689c37..a440af59 100644 --- a/rust/fastsim-py/src/lib.rs +++ b/rust/fastsim-py/src/lib.rs @@ -30,7 +30,6 @@ fn fastsimrust(py: Python, m: &PyModule) -> PyResult<()> { m.add_class::()?; m.add_class::()?; m.add_class::()?; - m.add_class::()?; cycle::register(py, m)?; @@ -50,6 +49,7 @@ fn fastsimrust(py: Python, m: &PyModule) -> PyResult<()> { } #[cfg(feature = "vehicle-import")] { + m.add_class::()?; m.add_function(wrap_pyfunction!( vehicle_import::get_options_for_year_make_model, m