Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ pt_type:
- 0.95
- 0.9400000000000001
- 0.93
strategy: LeftNearest
strategy: Linear
extrapolate: Error
eff_interp_at_max_input:
data:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ pt_type:
- 0.95
- 0.9400000000000001
- 0.93
strategy: LeftNearest
strategy: Linear
extrapolate: Error
eff_interp_at_max_input:
data:
Expand Down
38 changes: 13 additions & 25 deletions fastsim-core/src/compat/fastsim_2/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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()?
Expand Down Expand Up @@ -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()?
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -673,12 +661,11 @@ impl TryFrom<fastsim_core::vehicle::RustVehicle> for ElectricMachine {
f2veh: fastsim_core::vehicle::RustVehicle,
) -> Result<ElectricMachine, anyhow::Error> {
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
Expand All @@ -702,6 +689,7 @@ impl TryFrom<fastsim_core::vehicle::RustVehicle> for ElectricMachine {
strategy::Linear,
Extrapolate::Error,
)
.map(EMEfficiency::PwrOutFrac)
}
.with_context(|| {
format!(
Expand Down
1 change: 1 addition & 0 deletions fastsim-core/src/imports.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
14 changes: 2 additions & 12 deletions fastsim-core/src/simdrivelabel/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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 {
Expand Down
91 changes: 90 additions & 1 deletion fastsim-core/src/utils/interp.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
use crate::imports::*;
use ninterp::error::InterpolateError;

pub(crate) trait InterpolatorScanValues {
fn try_for_each_value<E, F: FnMut(f64) -> Result<(), E>>(&self, f: F) -> Result<(), E>;
Expand All @@ -10,7 +11,10 @@ impl InterpolatorScanValues for Interp0D<f64> {
}
}

impl InterpolatorScanValues for Interp1D<f64, strategy::enums::Strategy1DEnum<f64>> {
impl<S> InterpolatorScanValues for Interp1D<f64, S>
where
S: ninterp::strategy::traits::Strategy1D<ndarray::OwnedRepr<f64>> + Clone,
{
fn try_for_each_value<E, F: FnMut(f64) -> Result<(), E>>(&self, mut f: F) -> Result<(), E> {
self.data.values.iter().copied().try_for_each(&mut f)
}
Expand Down Expand Up @@ -46,6 +50,91 @@ impl InterpolatorScanValues for InterpolatorEnum<f64> {
}
}

/// 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<f64, strategy::Linear>,
extrapolate: Extrapolate<f64>,
}

impl RatioInverseView<'_> {
pub fn interpolate(&self, point: &[f64]) -> Result<f64, InterpolateError> {
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<f64> = 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<T> {
/// Borrows `self` as a [RatioInverseView], so `.interpolate(&[w])` answers
/// the ratio-inverse query instead of the usual one.
fn ratio_inverse(&self, extrapolate: Extrapolate<T>) -> RatioInverseView<'_>;
}

impl RatioInverse<f64> for Interp1D<f64, strategy::Linear> {
fn ratio_inverse(&self, extrapolate: Extrapolate<f64>) -> RatioInverseView<'_> {
RatioInverseView {
interp: self,
extrapolate,
}
}
}

/// Methods for mutating interpolator data, e.g. proportionally scaling
/// interpolator function data
pub trait InterpolatorMutMethods {
Expand Down
2 changes: 1 addition & 1 deletion fastsim-core/src/vehicle/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Loading
Loading