diff --git a/fastsim-core/resources/vehicles/2016 Nissan Leaf 30 kWh thrml.yaml b/fastsim-core/resources/vehicles/2016 Nissan Leaf 30 kWh thrml.yaml index 1afe29c5..453769e2 100644 --- a/fastsim-core/resources/vehicles/2016 Nissan Leaf 30 kWh thrml.yaml +++ b/fastsim-core/resources/vehicles/2016 Nissan Leaf 30 kWh thrml.yaml @@ -88,7 +88,7 @@ pt_type: - 0.95 - 0.9400000000000001 - 0.93 - strategy: LeftNearest + strategy: Linear extrapolate: Error eff_interp_at_max_input: data: diff --git a/fastsim-core/resources/vehicles/2022 Tesla Model 3 RWD thrml.yaml b/fastsim-core/resources/vehicles/2022 Tesla Model 3 RWD thrml.yaml index c498280c..2de2b866 100644 --- a/fastsim-core/resources/vehicles/2022 Tesla Model 3 RWD thrml.yaml +++ b/fastsim-core/resources/vehicles/2022 Tesla Model 3 RWD thrml.yaml @@ -93,7 +93,7 @@ pt_type: - 0.95 - 0.9400000000000001 - 0.93 - strategy: LeftNearest + strategy: Linear extrapolate: Error eff_interp_at_max_input: data: diff --git a/fastsim-core/src/compat/fastsim_2/mod.rs b/fastsim-core/src/compat/fastsim_2/mod.rs index 1089842c..df59ed7e 100644 --- a/fastsim-core/src/compat/fastsim_2/mod.rs +++ b/fastsim-core/src/compat/fastsim_2/mod.rs @@ -235,10 +235,10 @@ impl Vehicle { mc_eff_array: Default::default(), // calculated in `set_derived` mc_eff_map: self .em() - .map(|em| match &em.eff_interp_achieved { - InterpolatorEnum::Interp1D(interp) => Ok(interp.data.values.clone()), - _ => bail!(format_dbg!( - "Only 1-D interpolators can be converted to FASTSim 2" + .map(|em| match &em.eff_interp { + EMEfficiency::PwrOutFrac(interp) => Ok(interp.data.values.clone()), + EMEfficiency::Constant(_) => bail!(format_dbg!( + "Only `PwrOutFrac` efficiency maps can be converted to FASTSim 2" )), }) .transpose()? @@ -268,10 +268,10 @@ impl Vehicle { // short array that can use xEV when implented. TODO: fix this when implementing xEV mc_pwr_out_perc: self .em() - .map(|em| match &em.eff_interp_achieved { - InterpolatorEnum::Interp1D(interp) => Ok(interp.data.grid[0].clone()), - _ => bail!(format_dbg!( - "Only 1-D interpolators can be converted to FASTSim 2" + .map(|em| match &em.eff_interp { + EMEfficiency::PwrOutFrac(interp) => Ok(interp.data.grid[0].clone()), + EMEfficiency::Constant(_) => bail!(format_dbg!( + "Only `PwrOutFrac` efficiency maps can be converted to FASTSim 2" )), }) .transpose()? @@ -553,25 +553,13 @@ impl TryFrom<&fastsim_core::vehicle::RustVehicle> for BatteryElectricVehicle { res: ReversibleEnergyStorage::try_from(f2veh.clone()).with_context(|| format_dbg!())?, em: ElectricMachine { state: Default::default(), - eff_interp_achieved: InterpolatorEnum::new_1d( + eff_interp: EMEfficiency::PwrOutFrac(Interp1D::new( f2veh.mc_pwr_out_perc.to_vec().into(), f2veh.mc_eff_array.to_vec().into(), strategy::Linear, Extrapolate::Error, - )?, - eff_interp_at_max_input: Some(InterpolatorEnum::new_1d( - // before adding the interpolator, pwr_in_frac_interp was set as Default::default(), can this - // be transferred over as done here, or does a new defualt need to be defined? - f2veh - .mc_pwr_out_perc - .iter() - .zip(f2veh.mc_eff_array.iter()) - .map(|(x, y)| x / y) - .collect(), - f2veh.mc_eff_array.to_vec().into(), - strategy::Linear, - Extrapolate::Error, )?), + _eff_interp_at_max_input_legacy: None, pwr_out_max: f2veh.mc_max_kw * uc::KW, specific_pwr: None, mass: None, @@ -673,12 +661,11 @@ impl TryFrom for ElectricMachine { f2veh: fastsim_core::vehicle::RustVehicle, ) -> Result { Ok(powertrain::electric_machine::EMBuilder { - eff_interp_achieved: { + eff_interp: { // fastsim-2's hard-coded short vector of percent of peak power let short_perc_out_vec = vec![0.0, 0.02, 0.04, 0.06, 0.08, 0.1, 0.2, 0.4, 0.6, 0.8, 1.0]; - // `InterpolatorEnum` for fastsim-3 - InterpolatorEnum::new_1d( + Interp1D::new( short_perc_out_vec.clone().into(), { // convert 101 element f2 array to shorter f2 array and use @@ -702,6 +689,7 @@ impl TryFrom for ElectricMachine { strategy::Linear, Extrapolate::Error, ) + .map(EMEfficiency::PwrOutFrac) } .with_context(|| { format!( diff --git a/fastsim-core/src/imports.rs b/fastsim-core/src/imports.rs index bab5e4fe..9802c363 100644 --- a/fastsim-core/src/imports.rs +++ b/fastsim-core/src/imports.rs @@ -11,6 +11,7 @@ pub(crate) use crate::simdrive::TraceMissOptions; pub(crate) use crate::traits::*; pub(crate) use crate::uc; pub(crate) use crate::utils; +pub(crate) use crate::utils::interp::*; pub(crate) use crate::utils::{ abs_checked_x_val, almost_eq, almost_eq_uom, almost_ge_uom, almost_le_uom, check_interp_frac_data, check_monotonicity, is_sorted, InterpRange, DIRECT_SET_ERR, diff --git a/fastsim-core/src/simdrivelabel/mod.rs b/fastsim-core/src/simdrivelabel/mod.rs index 46f46829..d42af1cd 100644 --- a/fastsim-core/src/simdrivelabel/mod.rs +++ b/fastsim-core/src/simdrivelabel/mod.rs @@ -1131,12 +1131,7 @@ pub fn run_label_simulations( min_soc = phev.res.min_soc; phev_max_regen = 0.98 * uc::R; veh_mass = *veh.state.mass.get_fresh(|| format_dbg!())?; - em_peak_eff = *phev - .em - .eff_interp_achieved - .max() - .with_context(|| format_dbg!())? - * uc::R; + em_peak_eff = *phev.em.eff_interp.max().with_context(|| format_dbg!())? * uc::R; energy_capacity = phev.res.energy_capacity; chg_eff = DEFAULT_CHG_EFF; fuel_storage_capacity = phev.fs.energy_capacity; @@ -1453,12 +1448,7 @@ pub fn get_label_fe_phev( min_soc = phev.res.min_soc; phev_max_regen = 0.98 * uc::R; veh_mass = *veh.state.mass.get_fresh(|| format_dbg!())?; - em_peak_eff = *phev - .em - .eff_interp_achieved - .max() - .with_context(|| format_dbg!())? - * uc::R; + em_peak_eff = *phev.em.eff_interp.max().with_context(|| format_dbg!())? * uc::R; energy_capacity = phev.res.energy_capacity; chg_eff = DEFAULT_CHG_EFF; // Use default charging efficiency } else { diff --git a/fastsim-core/src/utils/interp.rs b/fastsim-core/src/utils/interp.rs index d502c4c9..e5847ff1 100644 --- a/fastsim-core/src/utils/interp.rs +++ b/fastsim-core/src/utils/interp.rs @@ -1,4 +1,5 @@ use crate::imports::*; +use ninterp::error::InterpolateError; pub(crate) trait InterpolatorScanValues { fn try_for_each_value Result<(), E>>(&self, f: F) -> Result<(), E>; @@ -10,7 +11,10 @@ impl InterpolatorScanValues for Interp0D { } } -impl InterpolatorScanValues for Interp1D> { +impl InterpolatorScanValues for Interp1D +where + S: ninterp::strategy::traits::Strategy1D> + Clone, +{ fn try_for_each_value Result<(), E>>(&self, mut f: F) -> Result<(), E> { self.data.values.iter().copied().try_for_each(&mut f) } @@ -46,6 +50,91 @@ impl InterpolatorScanValues for InterpolatorEnum { } } +/// Borrowed view of a `Linear`-strategy 1-D interpolant that answers, on +/// `interpolate`, the inverse of the ratio `w(x) = x / f(x)` rather than `f(x)` +/// directly: given `w`, returns `f` at the `x` satisfying `x / f(x) == w`, with +/// no second interpolator built. Get one via [RatioInverse::ratio_inverse]. +/// +/// `f` is affine on each segment, so `w` is a Mobius transform of `x` there; Mobius +/// transforms invert to Mobius transforms, giving the closed form `f(w) = a / (1 - b*w)`. +pub(crate) struct RatioInverseView<'a> { + interp: &'a Interp1D, + extrapolate: Extrapolate, +} + +impl RatioInverseView<'_> { + pub fn interpolate(&self, point: &[f64]) -> Result { + let w = *point.first().ok_or_else(|| { + InterpolateError::Other("`RatioInverseView::interpolate` needs one point".into()) + })?; + + let x = &self.interp.data.grid[0]; + let y = &self.interp.data.values; + + let w_grid: Vec = x.iter().zip(y).map(|(xi, yi)| xi / yi).collect(); + let w_min = *w_grid + .first() + .ok_or_else(|| InterpolateError::Other("`RatioInverse` grid is empty".into()))?; + let w_max = *w_grid.last().unwrap(); + + if w < w_min || w > w_max { + match self.extrapolate { + // falls through to the segment solve below, which naturally + // extrapolates along the boundary segment's own slope + Extrapolate::Enable => {} + Extrapolate::Clamp => return Ok(if w < w_min { y[0] } else { y[y.len() - 1] }), + Extrapolate::Fill(value) => return Ok(value), + Extrapolate::Error => { + return Err(InterpolateError::Other( + format!("`RatioInverse` query w={w} outside domain [{w_min}, {w_max}]") + .into(), + )) + } + Extrapolate::Wrap => { + return Err(InterpolateError::Other( + "`RatioInverse` does not support `Extrapolate::Wrap`".into(), + )) + } + // `Extrapolate` is `#[non_exhaustive]` + _ => { + return Err(InterpolateError::Other( + "`RatioInverse` does not support this `Extrapolate` variant".into(), + )) + } + } + } + + let lower = + strategy::utils::locate_lower_index(ndarray::ArrayView1::from(w_grid.as_slice()), &w); + let (x0, y0, x1, y1) = (x[lower], y[lower], x[lower + 1], y[lower + 1]); + let b = (y1 - y0) / (x1 - x0); // f(x) = a + b*x on this segment + let a = y0 - b * x0; + + let denom = 1.0 - b * w; + if denom == 0.0 { + return Err(InterpolateError::Other( + format!("`RatioInverse` singular at w={w} (segment {lower})").into(), + )); + } + Ok(a / denom) + } +} + +pub(crate) trait RatioInverse { + /// Borrows `self` as a [RatioInverseView], so `.interpolate(&[w])` answers + /// the ratio-inverse query instead of the usual one. + fn ratio_inverse(&self, extrapolate: Extrapolate) -> RatioInverseView<'_>; +} + +impl RatioInverse for Interp1D { + fn ratio_inverse(&self, extrapolate: Extrapolate) -> RatioInverseView<'_> { + RatioInverseView { + interp: self, + extrapolate, + } + } +} + /// Methods for mutating interpolator data, e.g. proportionally scaling /// interpolator function data pub trait InterpolatorMutMethods { diff --git a/fastsim-core/src/vehicle/mod.rs b/fastsim-core/src/vehicle/mod.rs index 44621c0d..440b99e5 100755 --- a/fastsim-core/src/vehicle/mod.rs +++ b/fastsim-core/src/vehicle/mod.rs @@ -23,7 +23,7 @@ pub use chassis::Chassis; pub use conv::ConventionalVehicle; pub use database::*; pub use hev::HybridElectricVehicle; -pub use powertrain::electric_machine::ElectricMachine; +pub use powertrain::electric_machine::{EMEfficiency, ElectricMachine}; pub use powertrain::fuel_converter::FuelConverter; pub use powertrain::fuel_storage::FuelStorage; pub use powertrain::fuel_storage::FuelType; diff --git a/fastsim-core/src/vehicle/powertrain/electric_machine.rs b/fastsim-core/src/vehicle/powertrain/electric_machine.rs index 4bce7b8f..ae410200 100755 --- a/fastsim-core/src/vehicle/powertrain/electric_machine.rs +++ b/fastsim-core/src/vehicle/powertrain/electric_machine.rs @@ -6,6 +6,71 @@ use super::*; #[cfg(feature = "pyo3")] use crate::pyo3::*; +#[derive(Clone, Debug, Serialize, PartialEq, IsVariant, TryInto)] +/// Determines what [ElectricMachine] state variables to use in calculating efficiency +pub enum EMEfficiency { + /// Efficiency is constant + Constant(#[serde(serialize_with = "serialize_nested")] Interp0D), + /// Efficiency = f(output power / max output power) + PwrOutFrac(#[serde(serialize_with = "serialize_nested")] Interp1D), +} + +impl<'de> Deserialize<'de> for EMEfficiency { + /// Accepts the current, externally-tagged shape (`{Constant: ...}` / + /// `{PwrOutFrac: {...}}`) as well as the shape used by the old, now-removed + /// `eff_interp_achieved` field: a bare, untagged [`InterpolatorEnum`], with the + /// variant inferred from its own shape (see [`ElectricMachine::eff_interp`]). + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + #[derive(Deserialize)] + enum Tagged { + Constant(Interp0D), + PwrOutFrac(Interp1D), + } + + #[derive(Deserialize)] + #[serde(untagged)] + enum Repr { + Tagged(Tagged), + Legacy(InterpolatorEnum), + } + + Ok(match Repr::deserialize(deserializer)? { + Repr::Tagged(Tagged::Constant(interp)) => Self::Constant(interp), + Repr::Tagged(Tagged::PwrOutFrac(interp)) => Self::PwrOutFrac(interp), + Repr::Legacy(InterpolatorEnumBase::Interp0D(interp)) => Self::Constant(interp), + Repr::Legacy(InterpolatorEnumBase::Interp1D(interp)) => { + let strategy::enums::Strategy1DEnum::Linear(strategy) = interp.strategy else { + return Err(serde::de::Error::custom( + "legacy `EMEfficiency` data only supports the `Linear` strategy", + )); + }; + Self::PwrOutFrac( + Interp1D::new( + interp.data.grid[0].clone(), + interp.data.values.clone(), + strategy, + interp.extrapolate, + ) + .map_err(serde::de::Error::custom)?, + ) + } + Repr::Legacy(_) => { + return Err(serde::de::Error::custom( + "`EMEfficiency` only supports 0-D (`Constant`) or 1-D (`PwrOutFrac`) interpolator data", + )) + } + }) + } +} + +impl_efficiency_enum!(EMEfficiency { + Constant, + PwrOutFrac, +}); + #[serde_api] #[derive(Deserialize, Serialize, Debug, Clone, PartialEq, StateMethods, SetCumulative)] #[non_exhaustive] @@ -14,17 +79,18 @@ use crate::pyo3::*; /// Struct for modeling electric machines. This lumps performance and efficiency of motor and power /// electronics. pub struct ElectricMachine { - /// Efficiency interpolator corresponding to achieved output power - /// - /// Note that the Extrapolate field of this variable is changed in [Self::get_pwr_in_req] - #[serde(serialize_with = "serialize_nested")] - pub eff_interp_achieved: InterpolatorEnum, - /// Efficiency interpolator corresponding to max input power - /// If `None`, will be set during [Self::init]. - /// - /// Note that the Extrapolate field of this variable is changed in [Self::set_curr_pwr_prop_out_max] - #[serde(serialize_with = "serialize_nested")] - pub eff_interp_at_max_input: Option>, + /// Efficiency map + #[serde(alias = "eff_interp_achieved")] + pub eff_interp: EMEfficiency, + /// Legacy field from before `eff_interp_achieved`/`eff_interp_at_max_input` were + /// merged into `eff_interp`. `eff_interp_at_max_input` was always fully derived + /// from `eff_interp_achieved` and is now recomputed on the fly in + /// [`Self::set_curr_pwr_prop_out_max`], so it is safe to discard; this field + /// exists only so old files containing it still deserialize under + /// `deny_unknown_fields`. + #[serde(rename = "eff_interp_at_max_input", default, skip_serializing)] + #[allow(dead_code)] + pub(crate) _eff_interp_at_max_input_legacy: Option, /// Electrical input power fraction array at which efficiencies are evaluated. /// Calculated during runtime if not provided. // /// this will disappear and instead be in eff_interp_bwd @@ -101,16 +167,15 @@ impl ElectricMachine { impl ElectricMachine { pub fn new( - eff_interp_achieved: InterpolatorEnum, - eff_interp_at_max_input: Option>, + eff_interp: EMEfficiency, pwr_out_max: si::Power, specific_pwr: Option, mass: Option, save_interval: Option, ) -> anyhow::Result { let mut em = ElectricMachine { - eff_interp_achieved, - eff_interp_at_max_input, + eff_interp, + _eff_interp_at_max_input_legacy: None, pwr_out_max, specific_pwr, mass, @@ -161,75 +226,43 @@ impl Powertrain for ElectricMachine { stringify!(ElectricMachine::get_curr_pwr_prop_out_max) ); - // ensuring Extrapolate is Clamp in preparation for calculating eff_pos - - self.eff_interp_at_max_input - .as_mut() - .with_context(|| { - "eff_interp_bwd is None, which should never be the case at this point." - })? - .set_extrapolate(Extrapolate::Clamp)?; - let raw_tractive_lookup_ratio = (*pwr_in_fwd_lim / self.pwr_out_max).get::(); let raw_regen_lookup_ratio = (*pwr_in_bwd_lim / self.pwr_out_max).get::(); + self.state.eff_fwd_at_max_input.update( uc::R - * self - .eff_interp_at_max_input - .as_ref() - .map(|interpolator| { - interpolator - .interpolate(&[abs_checked_x_val( - raw_tractive_lookup_ratio, - match interpolator { - InterpolatorEnum::Interp1D(interp) => interp.data.grid[0] - .as_slice() - .ok_or_else(|| anyhow!(format_dbg!()))?, - _ => bail!("Only `InterpolatorEnum::Interp1D` is allowed."), - }, - )?]) - .map_err(|e| anyhow!(e)) - }) - .ok_or(anyhow!( - "eff_interp_bwd is None, which should never be the case at this point." - ))? - .with_context(|| { - anyhow!( - "{}\n failed to calculate {}", - format_dbg!(), - stringify!(eff_pos) - ) - })?, + * match &self.eff_interp { + EMEfficiency::Constant(interp) => interp.interpolate(&[])?, + EMEfficiency::PwrOutFrac(interp) => { + let x_in = abs_checked_x_val( + raw_tractive_lookup_ratio, + interp.data.grid[0] + .as_slice() + .ok_or_else(|| anyhow!(format_dbg!()))?, + )?; + interp + .ratio_inverse(Extrapolate::Clamp) + .interpolate(&[x_in])? + } + }, || format_dbg!(), )?; self.state.eff_at_max_regen.update( uc::R - * self - .eff_interp_at_max_input - .as_ref() - .map(|interpolator| { - interpolator - .interpolate(&[abs_checked_x_val( - raw_regen_lookup_ratio, - match interpolator { - InterpolatorEnum::Interp1D(interp) => interp.data.grid[0] - .as_slice() - .ok_or_else(|| anyhow!(format_dbg!()))?, - _ => bail!("Only `InterpolatorEnum::Interp1D` is allowed."), - }, - )?]) - .map_err(|e| anyhow!(e)) - }) - .ok_or(anyhow!( - "eff_interp_bwd is None, which should never be the case at this point." - ))? - .with_context(|| { - anyhow!( - "{}\n failed to calculate {}", - format_dbg!(), - stringify!(eff_neg) - ) - })?, + * match &self.eff_interp { + EMEfficiency::Constant(interp) => interp.interpolate(&[])?, + EMEfficiency::PwrOutFrac(interp) => { + let x_in = abs_checked_x_val( + raw_regen_lookup_ratio, + interp.data.grid[0] + .as_slice() + .ok_or_else(|| anyhow!(format_dbg!()))?, + )?; + interp + .ratio_inverse(Extrapolate::Clamp) + .interpolate(&[x_in])? + } + }, || format_dbg!(), )?; @@ -349,42 +382,32 @@ impl Powertrain for ElectricMachine { .pwr_mech_fwd_out_max .get_fresh(|| format_dbg!())?; - // ensuring eff_interp_fwd has Extrapolate set to Error before calculating self.state.eff - self.eff_interp_achieved - .set_extrapolate(Extrapolate::Error)?; + // ensuring eff_interp has Extrapolate set to Error before calculating self.state.eff + self.eff_interp.set_extrapolate(Extrapolate::Error)?; let raw_lookup_pwr_ratio = (pwr_out_req / self.pwr_out_max).get::(); let calculated_eff = uc::R - * match &self.eff_interp_achieved { - InterpolatorEnum::Interp1D(interp) => interp - .interpolate(&[{ - let pwr = |pwr_uncorrected: f64| -> anyhow::Result { - Ok({ - if interp.data.grid[0] - .first() - .with_context(|| anyhow!(format_dbg!()))? - >= &0. - { - pwr_uncorrected.max(0.) - } else { - pwr_uncorrected - } - }) - }; - pwr(raw_lookup_pwr_ratio)? - }]) - .map_err(|e| { + * match &self.eff_interp { + EMEfficiency::Constant(interp) => interp.interpolate(&[])?, + EMEfficiency::PwrOutFrac(interp) => { + // not needed during negative traction because friction braking is + // still included, so clamp to 0 rather than querying at negative x + let x_out = if interp.data.grid[0] + .first() + .with_context(|| anyhow!(format_dbg!()))? + >= &0. + { + raw_lookup_pwr_ratio.max(0.) + } else { + raw_lookup_pwr_ratio + }; + interp.interpolate(&[x_out]).map_err(|e| { anyhow!( "failed to calculate efficiency at line {} with originating error [{}]", format_dbg!(), e ) - })?, - _ => { - return Err(Error::InitError(format_dbg!( - "Only 1-D interpolators are supported" - )) - .into()) + })? } }; let eff_value = if is_max_output { @@ -457,43 +480,25 @@ impl Init for ElectricMachine { let _ = self .mass() .map_err(|err| Error::InitError(format_dbg!(err)))?; - let _ = check_interp_frac_data(match &mut self.eff_interp_achieved { - InterpolatorEnum::Interp1D(interp) => interp.data.grid[0].as_slice().ok_or(Error::Other("Cannot convert to slice".to_string()))?, _ => { - return Err(Error::InitError(format_dbg!( - "Only 1-D interpolators are supported" - ))) - }}, InterpRange::Either) - .map_err(|err| + if let EMEfficiency::PwrOutFrac(interp) = &self.eff_interp { + let _ = check_interp_frac_data( + interp + .data + .grid[0] + .as_slice() + .ok_or(Error::Other("Cannot convert to slice".to_string()))?, + InterpRange::Either, + ) + .map_err(|err| { Error::InitError(format!( "{}\nInvalid values for `ElectricMachine::pwr_out_frac_interp`; must range from [-1..1] or [0..1].", format_dbg!(err) - ) - ))?; + )) + })?; + } self.state .init() .map_err(|err| Error::InitError(format_dbg!(err)))?; - // sets eff_interp_bwd to eff_interp_fwd, but changes the x-value. - // TODO: what should the default strategy be for eff_interp_bwd? - let eff_interp_at_max_input = match &self.eff_interp_achieved { - InterpolatorEnum::Interp1D(interp) => { - InterpolatorEnum::new_1d( - interp.data.grid[0] - .iter() - .zip(&interp.data.values) - .map(|(x, y)| x / y) - .collect(), - interp.data.values.clone(), - // TODO: should these be set to be the same as eff_interp_fwd, - // as currently is done, or should they be set to be specific - // Extrapolate and Strategy types? - interp.strategy.clone(), - interp.extrapolate, - ) - } - _ => unimplemented!(), - } - .map_err(|e| Error::NinterpError(e.to_string()))?; - self.eff_interp_at_max_input = Some(eff_interp_at_max_input); Ok(()) } } @@ -591,8 +596,8 @@ impl TryFrom for ElectricMachine { type Error = anyhow::Error; fn try_from(em_builder: EMBuilder) -> anyhow::Result { let mut em = ElectricMachine { - eff_interp_achieved: em_builder.eff_interp_achieved.clone(), - eff_interp_at_max_input: None, + eff_interp: em_builder.eff_interp.clone(), + _eff_interp_at_max_input_legacy: None, pwr_out_max: em_builder.pwr_out_max, specific_pwr: None, mass: None, @@ -607,46 +612,26 @@ impl TryFrom for ElectricMachine { } impl ElectricMachine { - /// Returns max value of `eff_interp_fwd` + /// Returns max value of `eff_interp` pub fn get_eff_fwd_max(&self) -> anyhow::Result<&f64> { // since efficiency is all f64 between 0 and 1, NEG_INFINITY is safe - self.eff_interp_achieved.max() + self.eff_interp.max() } - /// Returns max value of `eff_interp_bwd` - pub fn get_eff_max_bwd(&self) -> anyhow::Result<&f64> { - self.eff_interp_at_max_input - .as_ref() - .with_context(|| "eff_interp_bwd should be Some by this point.")? - .max() - } - - /// Scales eff_interp_fwd and eff_interp_bwd by ratio of new `eff_max` per current calculated max + /// Scales eff_interp by ratio of new `eff_max` per current calculated max pub fn set_eff_fwd_max(&mut self, eff_max: f64) -> anyhow::Result<()> { if (0.0..=1.0).contains(&eff_max) { - let old_max_fwd = *self.get_eff_fwd_max()?; - let old_max_bwd = *self.get_eff_max_bwd()?; - match &mut self.eff_interp_achieved { - InterpolatorEnum::Interp1D(interp) => { + let old_max = *self.get_eff_fwd_max()?; + match &mut self.eff_interp { + EMEfficiency::Constant(interp) => interp.0 = eff_max, + EMEfficiency::PwrOutFrac(interp) => { interp.data.values = interp .data .values .iter() - .map(|x| x * eff_max / old_max_fwd) + .map(|x| x * eff_max / old_max) .collect::>(); } - _ => bail!("{}\n", "Only `InterpolatorEnum::Interp1D` is allowed."), - } - match &mut self.eff_interp_at_max_input { - Some(InterpolatorEnum::Interp1D(interp)) => { - interp.data.values = interp - .data - .values - .iter() - .map(|x| x * eff_max / old_max_bwd) - .collect::>(); - } - _ => bail!("{}\n", "Only `InterpolatorEnum::Interp1D` is allowed. eff_interp_bwd should be Some by this point."), } Ok(()) } else { @@ -657,171 +642,69 @@ impl ElectricMachine { } } - /// Returns min value of `eff_interp_fwd` + /// Returns min value of `eff_interp` pub fn get_eff_min_fwd(&self) -> anyhow::Result<&f64> { - self.eff_interp_achieved.min() - } - - /// Returns min value of `eff_interp_at_max_input` - pub fn get_eff_min_at_max_input(&self) -> anyhow::Result<&f64> { - self.eff_interp_at_max_input - .as_ref() - .context("eff_interp_bwd should be Some by this point")? - .min() + self.eff_interp.min() } - /// Max value of `eff_interp_fwd` minus min value of `eff_interp_fwd`. + /// Max value of `eff_interp` minus min value of `eff_interp`. pub fn get_eff_fwd_range(&self) -> anyhow::Result { Ok(self.get_eff_fwd_max()? - self.get_eff_min_fwd()?) } - /// Max value of `eff_interp_bwd` minus min value of `eff_interp_bwd`. - pub fn get_eff_range_bwd(&self) -> anyhow::Result { - Ok(self.get_eff_max_bwd()? - self.get_eff_min_at_max_input()?) - } - - /// Scales values of `eff_interp_fwd.f_x` and `eff_interp_bwd.f_x` without changing max such that max - min + /// Scales values of `eff_interp` without changing max such that max - min /// is equal to new range. Will change max if needed to ensure no values are /// less than zero. pub fn set_eff_fwd_range(&mut self, eff_range: f64) -> anyhow::Result<()> { - let eff_max_fwd = self.get_eff_fwd_max()?.to_owned(); - let eff_max_bwd = self.get_eff_max_bwd()?.to_owned(); + let eff_max = self.get_eff_fwd_max()?.to_owned(); if eff_range == 0.0 { - let f_x_fwd = vec![ - eff_max_fwd; - match &self.eff_interp_achieved { - InterpolatorEnum::Interp1D(interp) => interp.data.values.len(), - _ => { - return Err(Error::InitError(format_dbg!( - "Only 1-D interpolators are supported" - )) - .into()); - } - } - ]; - match &mut self.eff_interp_achieved { - InterpolatorEnum::Interp1D(interp) => interp.data.values = Array::from_vec(f_x_fwd), - _ => { - return Err(Error::InitError(format_dbg!( - "Only 1-D interpolators are supported" - )) - .into()); - } - }; - let f_x_bwd = vec![ - eff_max_bwd; - match &self.eff_interp_at_max_input { - Some(interp) => { - match interp { - InterpolatorEnum::Interp1D(interp) => interp.data.values.len(), - _ => { - return Err(Error::InitError(format_dbg!( - "Only 1-D interpolators are supported" - )) - .into()); - } - } - } - None => bail!("eff_interp_bwd should be Some by this point."), - } - ]; - self.eff_interp_at_max_input - .as_mut() - .map(|interpolator| match interpolator { - InterpolatorEnum::Interp1D(interp) => { - interp.data.values = Array::from_vec(f_x_bwd); - Ok(()) - } - _ => Err(Error::InitError(format_dbg!( - "Only 1-D interpolators are supported" - ))), - }) - .transpose()?; - Ok(()) - } else if (0.0..=1.0).contains(&eff_range) { - let old_min = self.get_eff_min_fwd()?; - let old_range = self.get_eff_fwd_max()? - old_min; - if old_range == 0.0 { - return Err(anyhow!( - "`eff_range` is already zero so it cannot be modified." - )); - } - match &mut self.eff_interp_achieved { - InterpolatorEnum::Interp1D(interp) => { - interp.data.values = interp - .data - .values - .iter() - .map(|x| eff_max_fwd + (x - eff_max_fwd) * eff_range / old_range) - .collect(); - interp.validate()?; - } - _ => bail!("{}\n", "Only `InterpolatorEnum::Interp1D` is allowed."), - } - if self.get_eff_min_fwd()? < &0. { - let x_neg = *self.get_eff_min_fwd()?; - match &mut self.eff_interp_achieved { - InterpolatorEnum::Interp1D(interp) => { - interp.data.values.map_inplace(|x| *x -= x_neg); - interp.validate()?; - } - _ => bail!("{}\n", "Only `InterpolatorEnum::Interp1D` is allowed."), - } - } - if self.get_eff_fwd_max()? > &1.0 { - return Err(anyhow!(format!( - "`eff_max` ({:.3}) must be no greater than 1.0", - self.get_eff_fwd_max()? - ))); - } - let old_min = self.get_eff_min_at_max_input()?; - let old_range = self.get_eff_max_bwd()? - old_min; - if old_range == 0.0 { - return Err(anyhow!( - "`eff_range` is already zero so it cannot be modified." - )); - } - - //TODO - match &mut self.eff_interp_at_max_input { - Some(InterpolatorEnum::Interp1D(interp)) => { - interp.data.values = interp - .data - .values - .iter() - .map(|x| eff_max_bwd + (x - eff_max_bwd) * eff_range / old_range) - .collect(); - } - _ => bail!("TODO"), + if let EMEfficiency::PwrOutFrac(interp) = &mut self.eff_interp { + let f_x = vec![eff_max; interp.data.values.len()]; + interp.data.values = Array::from_vec(f_x); } - - if self.get_eff_min_at_max_input()? < &0.0 { - let x_neg = *self.get_eff_min_at_max_input()?; - self.eff_interp_at_max_input - .as_mut() - .map(|interpolator| match interpolator { - InterpolatorEnum::Interp1D(interp) => { - interp.data.values.map_inplace(|x| *x -= x_neg); - interp.validate()?; - Ok(()) - } - _ => bail!("Only `InterpolatorEnum::Interp1D` is allowed."), - }) - .transpose()?; - } - if self.get_eff_max_bwd()? > &1.0 { - return Err(anyhow!(format!( - "`eff_max` ({:.3}) must be no greater than 1.0", - self.get_eff_max_bwd()? - ))); - } - Ok(()) - } else { - Err(anyhow!(format!( + return Ok(()); + } + if !(0.0..=1.0).contains(&eff_range) { + return Err(anyhow!( "`eff_range` ({:.3}) must be between 0.0 and 1.0", eff_range, - ))) + )); + } + let old_min = *self.get_eff_min_fwd()?; + let old_range = eff_max - old_min; + if old_range == 0.0 { + return Err(anyhow!( + "`eff_range` is already zero so it cannot be modified." + )); + } + match &mut self.eff_interp { + EMEfficiency::Constant(_) => bail!( + "`eff_range` cannot be set to a nonzero value for a `Constant` efficiency map." + ), + EMEfficiency::PwrOutFrac(interp) => { + interp.data.values = interp + .data + .values + .iter() + .map(|x| eff_max + (x - eff_max) * eff_range / old_range) + .collect(); + interp.validate()?; + } + } + if *self.get_eff_min_fwd()? < 0. { + let x_neg = *self.get_eff_min_fwd()?; + if let EMEfficiency::PwrOutFrac(interp) = &mut self.eff_interp { + interp.data.values.map_inplace(|x| *x -= x_neg); + interp.validate()?; + } } + if *self.get_eff_fwd_max()? > 1.0 { + return Err(anyhow!(format!( + "`eff_max` ({:.3}) must be no greater than 1.0", + self.get_eff_fwd_max()? + ))); + } + Ok(()) } } @@ -831,11 +714,8 @@ impl ElectricMachine { #[cfg_attr(feature = "pyo3", pyclass(module = "fastsim", subclass, eq))] /// Builder for [ElectricMachine]. Use this to instantiate EM with minimal parameterization pub struct EMBuilder { - /// Efficiency interpolator corresponding to achieved output power - /// - /// Note that the Extrapolate field of this variable is changed in [Self::get_pwr_in_req] - #[serde(serialize_with = "serialize_nested")] - pub eff_interp_achieved: InterpolatorEnum, + /// Efficiency map + pub eff_interp: EMEfficiency, /// Electrical input power fraction array at which efficiencies are evaluated. /// Calculated during runtime if not provided. // /// this will disappear and instead be in eff_interp_bwd diff --git a/fastsim-core/src/vehicle/vehicle_model.rs b/fastsim-core/src/vehicle/vehicle_model.rs index bcc32cc1..7794dd2f 100644 --- a/fastsim-core/src/vehicle/vehicle_model.rs +++ b/fastsim-core/src/vehicle/vehicle_model.rs @@ -1417,6 +1417,7 @@ impl Default for VehicleState { pub(crate) mod tests { use crate::vehicle::conv::{ConvPowertrainControls, ConvStartStopControl}; use crate::vehicle::hev::{HEVAuxControls, HEVSimulationParams, HEVStartStopControl}; + use crate::vehicle::powertrain::electric_machine::EMEfficiency; use crate::vehicle::powertrain::reversible_energy_storage::RESEfficiency; use super::*; @@ -1705,13 +1706,12 @@ pub(crate) mod tests { None, )?; let em = ElectricMachine::new( - InterpolatorEnum::new_1d( + EMEfficiency::PwrOutFrac(Interp1D::new( vec![0.0, 1.0].into(), vec![0.95, 0.95].into(), strategy::Linear, Extrapolate::Error, - )?, // eff_interp_achieved - None, // eff_interp_at_max_input + )?), // eff_interp 5.0 * uc::KW, // pwr_out_max None, // specific_pwr None, // mass