diff --git a/rust/fastsim-core/src/calibration.rs b/rust/fastsim-core/src/calibration.rs new file mode 100644 index 00000000..b8663ef0 --- /dev/null +++ b/rust/fastsim-core/src/calibration.rs @@ -0,0 +1,118 @@ +use crate::imports::*; + +/// Skews the peak of a curve to a specified new x-value, redistributing other +/// x-values linearly, preserving relative distances between peak and endpoints. +/// Arguments: +/// ---------- +/// x: x-values of original curve (i.e. mc_pwr_out_perc when skewing motor +/// efficiency map for RustVehicle) +/// y: y-values of the original curve (i.e. mc_eff_map when skewing motor +/// efficiency map RustVehicle) +/// new_peak_x: new x-value at which to relocate peak +pub fn skewness_shift( + x: &Array1, + y: &Array1, + new_peak_x: f64, +) -> anyhow::Result<(Array1, Array1)> { + let y_max = y + .clone() + .into_iter() + .reduce(f64::max) + .with_context(|| "could not find maximum of y array")?; + + // Get index for maximum y-value. Use greatest index, if maximum occurs at + // multiple indexes. + let index_y_max = get_index_for_value(&y, y_max)?; + + // making vector versions of x and y arrays to manipulate + let x_vec = x.to_vec(); + let y_vec = y.to_vec(); + let mut x_new_left = vec![]; + let mut y_new_left = vec![]; + let mut x_new_right = vec![]; + let mut y_new_right = vec![]; + + // If points exist to the left of the peak + if (index_y_max != 0) && (new_peak_x != y[0]) { + for x_val in x_vec[0..index_y_max].iter() { + x_new_left.push( + x_vec[0] + (x_val - x_vec[0]) / (x_vec[index_y_max] - x[0]) * (new_peak_x - x[0]), + ) + } + y_new_left.append(y_vec[0..index_y_max].to_vec().as_mut()); + } + + // If points exist to the right of the peak + if (index_y_max != y.len() - 1) && (new_peak_x != x_vec[x.len() - 1]) { + for x_val in x_vec[index_y_max + 1..x.len()].iter() { + x_new_right.push( + new_peak_x + + (x_val - x[index_y_max]) / (x[x.len() - 1] - x[index_y_max]) + * (x[x.len() - 1] - new_peak_x), + ) + } + y_new_right.append(y_vec[index_y_max + 1..y.len()].to_vec().as_mut()); + } + + let mut x_new = vec![]; + x_new.append(x_new_left.as_mut()); + x_new.push(new_peak_x); + x_new.append(x_new_right.as_mut()); + + let mut y_new = vec![]; + y_new.append(y_new_left.as_mut()); + y_new.push(y_max); + y_new.append(y_new_right.as_mut()); + + // Quality checks + if x_new.len() != y_new.len() { + return Err(anyhow!( + "New x array and new y array do not have same length." + )); + } + if x_new[0] != x[0] { + return Err(anyhow!( + "The first value of the new x array does not match the first value of the old x array." + )); + } + let y_new_max = y_new + .clone() + .into_iter() + .reduce(f64::max) + .with_context(|| "could not find maximum of new y array")?; + let new_index_y_max = get_index_for_value(&y_new.clone().try_into()?, y_new_max)?; + if x_new[new_index_y_max] != new_peak_x { + return Err(anyhow!( + "The maximum in the new y array is not in the correct location." + )); + } + if x_new[x_new.len() - 1] != x[x.len() - 1] { + return Err(anyhow!( + "The last value of the new x array {} does not equal the last value of the old x array. {}", x_new[x_new.len() - 1], x[x.len() - 1] + )); + } + + Ok((x_new.try_into()?, y_new.try_into()?)) +} + +/// Gets the index for the a value in an array. If the value occurs more than +/// once in the array, chooses the largest index for which the value occurs. +/// Arguments: +/// ---------- +/// array: array to get index for +/// value: value to check array for and get index of +fn get_index_for_value(array: &Array1, value: f64) -> anyhow::Result { + let mut index: usize = 0; + let mut max_index_vec = vec![]; + for val in array.iter() { + if val == &value { + max_index_vec.push(index); + } + index = index + 1; + } + Ok(max_index_vec + .iter() + .max() + .with_context(|| "Value not found in array.")? + .to_owned()) +} diff --git a/rust/fastsim-core/src/lib.rs b/rust/fastsim-core/src/lib.rs index 36e87ae7..60d06fa0 100644 --- a/rust/fastsim-core/src/lib.rs +++ b/rust/fastsim-core/src/lib.rs @@ -43,6 +43,7 @@ pub mod imports; pub mod params; pub mod pyo3imports; pub mod simdrive; +mod calibration; pub use simdrive::simdrive_impl; pub mod simdrivelabel; pub mod thermal; diff --git a/rust/fastsim-core/src/vehicle.rs b/rust/fastsim-core/src/vehicle.rs index 088a576c..d09b1b18 100644 --- a/rust/fastsim-core/src/vehicle.rs +++ b/rust/fastsim-core/src/vehicle.rs @@ -1,5 +1,6 @@ //! Module containing vehicle struct and related functions. +use crate::calibration::skewness_shift; // local use crate::imports::*; use crate::params::*; @@ -56,6 +57,26 @@ lazy_static! { pub fn set_mc_peak_eff_py(&mut self, new_peak: f64) { self.set_mc_peak_eff(new_peak); } + + #[getter] + pub fn get_mc_eff_range_py(&self) -> anyhow::Result { + self.get_mc_eff_range() + } + + #[setter("mc_eff_range")] + pub fn set_mc_eff_range_py(&mut self, new_range: f64) -> anyhow::Result<()> { + self.set_mc_eff_range(new_range) + } + + #[getter] + pub fn get_fc_eff_range_py(&self) -> anyhow::Result { + self.get_fc_eff_range() + } + + #[setter("fc_eff_range")] + pub fn set_fc_eff_range_py(&mut self, new_range: f64) -> anyhow::Result<()> { + self.set_fc_eff_range(new_range) + } #[getter] pub fn get_max_fc_eff_kw(&self) -> f64 { @@ -94,6 +115,22 @@ lazy_static! { fn mock_vehicle_py() -> Self { Self::mock_vehicle() } + + #[setter("mc_eff_peak_pwr")] + pub fn set_mc_eff_peak_pwr_py<'py>( + &mut self, + new_peak_x: f64, + ) -> anyhow::Result<()> { + self.set_mc_eff_peak_pwr(new_peak_x) + } + + #[setter("fc_eff_peak_pwr")] + pub fn set_fc_eff_peak_pwr_py<'py>( + &mut self, + new_peak_x: f64, + ) -> anyhow::Result<()> { + self.set_fc_eff_peak_pwr(new_peak_x) + } )] #[cfg_attr(feature = "validation", derive(Validate))] #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, ApproxEq)] @@ -642,8 +679,9 @@ impl RustVehicle { } pub fn set_mc_peak_eff(&mut self, new_peak: f64) { - let mc_max_eff = self.mc_eff_array.max().unwrap(); + let mc_max_eff = self.mc_eff_array.max().unwrap().clone(); self.mc_eff_array *= new_peak / mc_max_eff; + self.mc_eff_map *= new_peak / mc_max_eff; let mc_max_full_eff = arrmax(&self.mc_full_eff_array); self.mc_full_eff_array = self .mc_full_eff_array @@ -652,6 +690,96 @@ impl RustVehicle { .collect(); } + /// Gets the minimum value of mc_eff_array + pub fn get_mc_eff_min(&self) -> anyhow::Result<&f64> { + self.mc_eff_array.min() + } + + /// Gets the max value of mc_eff_array + pub fn get_mc_eff_max(&self) -> anyhow::Result<&f64> { + self.mc_eff_array.max() + } + + /// Gets the range of mc_eff_array + pub fn get_mc_eff_range(&self) -> anyhow::Result { + Ok(self.get_mc_eff_max()? - self.get_mc_eff_min()?) + } + + /// Changes the range (max value - min value) of mc_eff_map and mc_eff_array + /// # Arguments + /// - new_range: new range for the mc_eff_map and mc_eff_array + pub fn set_mc_eff_range(&mut self, new_range: f64) -> anyhow::Result<()> { + let mc_eff_max = *self.get_mc_eff_max()?; + if new_range == 0.0 { + self.mc_eff_map = Array::zeros(self.mc_eff_map.len()) + mc_eff_max; + self.mc_eff_array = Array::zeros(self.mc_eff_array.len()) + mc_eff_max; + Ok(()) + } else if (0.0..=1.0).contains(&new_range) { + let old_range = self.get_mc_eff_range()?; + self.mc_eff_map = mc_eff_max + (&self.mc_eff_map - mc_eff_max) * new_range / old_range; + if self.get_mc_eff_min()? < &0.0 { + bail!("`mc_eff_min` ({:.3}) must not be negative", self.get_mc_eff_min()?) + } + ensure!( + self.get_mc_eff_max()? <= &1.0, + format!( + "{}\n`mc_eff_max` ({:.3}) must be no greater than 1.0", + format_dbg!(self.get_mc_eff_max()? <= &1.0), + self.get_mc_eff_max()? + ) + ); + self.mc_eff_array = self.mc_eff_map.clone(); + Ok(()) + } else { + bail!("`new_range` ({:.3}) must be between 0.0 and 1.0", new_range) + } + } + + /// Gets the minimum value of fc_eff_array + pub fn get_fc_eff_min(&self) -> anyhow::Result { + Ok(self.fc_eff_array.iter().copied().fold(f64::NAN, f64::min)) + } + + /// Gets the max value of fc_eff_array + pub fn get_fc_eff_max(&self) -> anyhow::Result { + Ok(self.fc_eff_array.iter().copied().fold(f64::NAN, f64::max)) + } + + /// Gets the range of fc_eff_array + pub fn get_fc_eff_range(&self) -> anyhow::Result { + Ok(self.get_fc_eff_max()? - self.get_fc_eff_min()?) + } + + /// Changes the range (max value - min value) of fc_eff_map and fc_eff_array + /// # Arguments + /// - new_range: new range for the fc_eff_map and fc_eff_array + pub fn set_fc_eff_range(&mut self, new_range: f64) -> anyhow::Result<()> { + let fc_eff_max = self.get_fc_eff_max()?; + if new_range == 0.0 { + self.fc_eff_map = Array::zeros(self.fc_eff_map.len()) + fc_eff_max; + self.fc_eff_array = (Array::zeros(self.fc_eff_array.len()) + fc_eff_max).to_vec(); + Ok(()) + } else if (0.0..=1.0).contains(&new_range) { + let old_range = self.get_fc_eff_range()?; + self.fc_eff_map = fc_eff_max + (&self.fc_eff_map - fc_eff_max) * new_range / old_range; + if self.get_fc_eff_min()? < 0.0 { + bail!("`fc_eff_min` ({:.3}) must not be negative", self.get_fc_eff_min()?) + } + ensure!( + self.get_fc_eff_max()? <= 1.0, + format!( + "{}\n`fc_eff_max` ({:.3}) must be no greater than 1.0", + format_dbg!(self.get_fc_eff_max()? <= 1.0), + self.get_fc_eff_max()? + ) + ); + self.fc_eff_array = self.fc_eff_map.to_vec(); + Ok(()) + } else { + bail!("`new_range` ({:.3}) must be between 0.0 and 1.0", new_range) + } + } + pub fn set_fc_peak_eff(&mut self, new_peak: f64) { let old_fc_peak_eff = self.fc_peak_eff(); let multiplier = new_peak / old_fc_peak_eff; @@ -1082,6 +1210,46 @@ impl RustVehicle { vehicle.doc = Some(vehicle_origin); Ok(vehicle) } + + /// Skews the peak of motor efficiency curve to new x-value, redistributing other + /// x-values linearly, preserving relative distances between peak and endpoints. + /// Arguments: + /// ---------- + /// new_peak_x: new x-value at which to relocate peak + + pub fn set_mc_eff_peak_pwr(&mut self, new_peak_x: f64) -> anyhow::Result<()> { + let short_arrays = skewness_shift(&self.mc_pwr_out_perc, &self.mc_eff_map, new_peak_x)?; + self.mc_pwr_out_perc = short_arrays.0; + self.mc_eff_map = short_arrays.1.clone(); + self.mc_eff_array = short_arrays.1; + self.mc_full_eff_array = self + .mc_perc_out_array + .iter() + .enumerate() + .map(|(idx, &x): (usize, &f64)| -> f64 { + if idx == 0 { + 0.0 + } else { + interpolate(&x, &self.mc_pwr_out_perc, &self.mc_eff_array, false) + } + }) + .collect(); + Ok(()) + } + + /// Skews the peak of fc efficiency curve to new x-value, redistributing other + /// x-values linearly, preserving relative distances between peak and endpoints. + /// Arguments: + /// ---------- + /// new_peak_x: new x-value at which to relocate peak + + pub fn set_fc_eff_peak_pwr(&mut self, new_peak_x: f64) -> anyhow::Result<()> { + let short_arrays = skewness_shift(&self.fc_pwr_out_perc, &self.fc_eff_map, new_peak_x)?; + self.fc_pwr_out_perc = short_arrays.0; + self.fc_eff_array = short_arrays.1.to_vec(); + self.fc_eff_map = short_arrays.1; + Ok(()) + } } impl Default for RustVehicle { @@ -1153,6 +1321,17 @@ mod tests { assert!(veh.veh_kg > 0.0); } + #[test] + fn test_set_mc_eff_range() { + let mut veh = RustVehicle::mock_vehicle(); + veh.set_mc_eff_range(0.7).unwrap(); + assert!(0.699 < veh.get_mc_eff_range().unwrap() && veh.get_mc_eff_range().unwrap() <= 0.701); + veh.set_mc_eff_range(0.5).unwrap(); + assert!(0.499 < veh.get_mc_eff_range().unwrap() && veh.get_mc_eff_range().unwrap() <= 0.501); + veh.set_mc_eff_range(0.).unwrap(); + assert!(veh.get_mc_eff_range().unwrap() == 0.); + } + #[test] fn test_veh_kg_override() { let veh_file = resources_path().join("vehdb/test_overrides.yaml"); diff --git a/rust/fastsim-core/src/vehicle_import.rs b/rust/fastsim-core/src/vehicle_import.rs index 711bc0fe..e4411914 100644 --- a/rust/fastsim-core/src/vehicle_import.rs +++ b/rust/fastsim-core/src/vehicle_import.rs @@ -230,6 +230,8 @@ impl SerdeAPI for EmissionsInfoFE {} #[add_pyo3_api] /// Struct containing vehicle data from EPA database pub struct VehicleDataEPA { + /// Index + pub index: u32, /// Model year #[serde(rename = "Model Year")] pub year: u32, @@ -372,7 +374,7 @@ pub fn get_vehicle_data_for_id( load_fegov_data_for_given_years(ddpath.as_path(), &emissions_data, &ys)?; let fegov_db = fegov_data_by_year .get(&y) - .context(format!("Could not get fueleconomy.gov data from year {y}"))?; + .with_context(|| format!("Could not get fueleconomy.gov data from year {y}"))?; for item in fegov_db.iter() { if item.id == id { return Ok(item.clone()); @@ -967,10 +969,10 @@ fn try_make_single_vehicle( 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; + mc_max_kw = other_inputs.mc_max_kw; min_soc = 0.0; max_soc = 1.0; - ess_max_kw = 1.05 * mc_max_kw; + ess_max_kw = other_inputs.ess_max_kw; ess_max_kwh = other_inputs.ess_max_kwh; mph_fc_on = 1.0; kw_demand_fc_on = 100.0; @@ -995,6 +997,7 @@ fn try_make_single_vehicle( // + (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 { + doc: Some(format!("EPA ({}) index {}", epa_data.year, epa_data.index)), 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, @@ -1629,6 +1632,7 @@ mod tests { emissions_list: emiss_list, }; let epatest_data = VehicleDataEPA { + index: 0, year: 2020, make: String::from("TOYOTA"), model: String::from("CAMRY"),