use super::utils::ScalingMethods;
use super::*;
use crate::prelude::*;
use crate::utils::interp::InterpolatorMutMethods;
use std::f64::consts::PI;
#[serde_api]
#[derive(Deserialize, Serialize, Debug, Clone, PartialEq, StateMethods, SetCumulative)]
#[non_exhaustive]
#[serde(deny_unknown_fields)]
#[cfg_attr(feature = "pyo3", pyclass(module = "fastsim", subclass, eq))]
pub struct FuelConverter {
#[serde(default)]
#[has_state]
pub thrml: FuelConverterThermalOption,
#[serde(default)]
pub(in super::super) mass: Option<si::Mass>,
pub(in super::super) specific_pwr: Option<si::SpecificPower>,
pub pwr_out_max: si::Power,
#[serde(default)]
pub pwr_out_max_init: si::Power,
pub pwr_ramp_lag: si::Time,
pub eff_interp_from_pwr_out: InterpolatorEnumOwned<f64>,
#[serde(skip)]
pub(crate) pwr_for_peak_eff: si::Power,
pub pwr_idle_fuel: si::Power,
pub save_interval: Option<usize>,
#[serde(default)]
pub state: FuelConverterState,
#[serde(default)]
pub history: FuelConverterStateHistoryVec,
}
#[pyo3_api]
impl FuelConverter {
#[getter("eff_max")]
fn get_eff_max_py(&self) -> PyResult<f64> {
Ok(*self.get_eff_max()?)
}
#[setter("__eff_max")]
fn set_eff_max_py(&mut self, eff_max: f64) -> PyResult<()> {
Ok(self.set_eff_max(eff_max, None)?)
}
#[getter("eff_min")]
fn get_eff_min_py(&self) -> PyResult<f64> {
Ok(*self.get_eff_min()?)
}
#[setter("__eff_min")]
fn set_eff_min_py(&mut self, eff_min: f64) -> PyResult<()> {
Ok(self.set_eff_min(eff_min, None)?)
}
#[setter("__eff_range")]
fn set_eff_range_py(&mut self, eff_range: f64) -> PyResult<()> {
self.set_eff_range(eff_range)?;
Ok(())
}
#[getter("mass_kg")]
fn get_mass_py(&self) -> PyResult<Option<f64>> {
Ok(self.mass()?.map(|m| m.get::<si::kilogram>()))
}
#[getter]
fn get_specific_pwr_kw_per_kg(&self) -> Option<f64> {
self.specific_pwr
.map(|x| x.get::<si::kilowatt_per_kilogram>())
}
}
impl SerdeAPI for FuelConverter {}
impl Init for FuelConverter {
fn init(&mut self) -> Result<(), Error> {
let _ = self
.mass()
.map_err(|err| Error::InitError(format_dbg!(err)))?;
self.thrml.init()?;
self.state
.init()
.map_err(|err| Error::InitError(format_dbg!(err)))?;
let eff_max = self
.get_eff_max()
.map_err(|err| Error::InitError(format_dbg!(err)))?;
self.pwr_for_peak_eff = match &self.eff_interp_from_pwr_out {
InterpolatorEnum::Interp1D(interp) => *interp.data.grid[0]
.get(
interp
.data
.values
.iter()
.position(|eff| eff == eff_max)
.ok_or_else(|| Error::InitError(format_dbg!()))?,
)
.ok_or_else(|| Error::InitError(format_dbg!()))?,
_ => {
return Err(Error::InitError(format_dbg!(
"Only 1-D interpolators are supported"
)))
}
} * self.pwr_out_max;
Ok(())
}
}
impl HistoryMethods for FuelConverter {
fn save_interval(&self) -> anyhow::Result<Option<usize>> {
Ok(self.save_interval)
}
fn set_save_interval(&mut self, save_interval: Option<usize>) -> anyhow::Result<()> {
self.save_interval = save_interval;
self.thrml.set_save_interval(save_interval)?;
Ok(())
}
fn clear(&mut self) {
self.history.clear();
self.thrml.clear();
}
}
impl Mass for FuelConverter {
fn mass(&self) -> anyhow::Result<Option<si::Mass>> {
let derived_mass = self
.derived_mass()
.with_context(|| anyhow!(format_dbg!()))?;
if let (Some(derived_mass), Some(set_mass)) = (derived_mass, self.mass) {
ensure!(
utils::almost_eq_uom(&set_mass, &derived_mass, None),
format!(
"{}",
format_dbg!(utils::almost_eq_uom(&set_mass, &derived_mass, None)),
)
);
}
Ok(self.mass)
}
fn set_mass(
&mut self,
new_mass: Option<si::Mass>,
side_effect: MassSideEffect,
) -> anyhow::Result<()> {
let derived_mass = self
.derived_mass()
.with_context(|| anyhow!(format_dbg!()))?;
if let (Some(derived_mass), Some(new_mass)) = (derived_mass, new_mass) {
if derived_mass != new_mass {
match side_effect {
MassSideEffect::Extensive => {
self.pwr_out_max = self.specific_pwr.ok_or_else(|| {
anyhow!(
"{}\nExpected `self.specific_pwr` to be `Some`.",
format_dbg!()
)
})? * new_mass;
}
MassSideEffect::Intensive => {
self.specific_pwr = Some(self.pwr_out_max / new_mass);
}
MassSideEffect::None => {
self.specific_pwr = None;
}
}
}
} else if new_mass.is_none() {
self.specific_pwr = None;
}
self.mass = new_mass;
Ok(())
}
fn derived_mass(&self) -> anyhow::Result<Option<si::Mass>> {
Ok(self
.specific_pwr
.map(|specific_pwr| self.pwr_out_max / specific_pwr))
}
fn expunge_mass_fields(&mut self) {
self.mass = None;
self.specific_pwr = None;
}
}
impl FuelConverter {
pub fn set_curr_pwr_out_max(&mut self, dt: si::Time) -> anyhow::Result<()> {
if self.pwr_out_max_init == si::Power::ZERO {
self.pwr_out_max_init = self.pwr_out_max / 10.
};
let pwr_out_max = (*self.state.pwr_prop.get_stale(|| format_dbg!())?
+ *self.state.pwr_aux.get_stale(|| format_dbg!())?
+ self.pwr_out_max / self.pwr_ramp_lag * dt)
.min(self.pwr_out_max)
.max(self.pwr_out_max_init);
self.state
.pwr_out_max
.update(pwr_out_max, || format_dbg!())?;
Ok(())
}
pub fn set_curr_pwr_prop_max(&mut self, pwr_aux: si::Power) -> anyhow::Result<()> {
ensure!(
pwr_aux >= si::Power::ZERO,
format!(
"{}\n`pwr_aux` must be >= 0",
format_dbg!(pwr_aux >= si::Power::ZERO),
)
);
self.state.pwr_aux.update(pwr_aux, || format_dbg!())?;
self.state.pwr_prop_max.update(
*self.state.pwr_out_max.get_fresh(|| format_dbg!())? - pwr_aux,
|| format_dbg!(),
)?;
Ok(())
}
pub fn solve(
&mut self,
pwr_out_req: si::Power,
fc_on: bool,
dt: si::Time,
) -> anyhow::Result<()> {
self.state.fc_on.update(fc_on, || format_dbg!())?;
if fc_on {
self.state.time_on.increment(dt, || format_dbg!())?;
} else {
self.state
.time_on
.update(si::Time::ZERO, || format_dbg!())?;
}
ensure!(
pwr_out_req >= si::Power::ZERO,
format!(
"{}\n`pwr_out_req` must be >= 0",
format_dbg!(pwr_out_req >= si::Power::ZERO),
)
);
ensure!(
pwr_out_req <= *self.state.pwr_prop_max.get_fresh(|| format_dbg!())?,
format!(
"{}\n`pwr_out_req` ({} W) must be < `self.state.pwr_prop_max` ({} W)",
format_dbg!(),
pwr_out_req.get::<si::watt>().format_eng(Some(5)),
self.state
.pwr_prop_max
.get_fresh(|| format_dbg!())?
.get::<si::watt>()
.format_eng(Some(5))
)
);
ensure!(
fc_on || (pwr_out_req == si::Power::ZERO && *self.state.pwr_aux.get_fresh(|| format_dbg!())? == si::Power::ZERO),
format!(
"{}\nEngine is off but pwr_out_req + pwr_aux is non-zero\n`pwr_out_req`: {} kW\n`self.state.pwr_aux`: {} kW",
format_dbg!(
fc_on
|| (pwr_out_req == si::Power::ZERO
&& *self.state.pwr_aux.get_fresh(|| format_dbg!())? == si::Power::ZERO)
),
pwr_out_req.get::<si::kilowatt>(),
self.state.pwr_aux.get_fresh(|| format_dbg!())?.get::<si::kilowatt>()
)
);
self.state.pwr_prop.update(pwr_out_req, || format_dbg!())?;
self.state.eff.update(
if fc_on {
uc::R
* self
.eff_interp_from_pwr_out
.interpolate(&[((pwr_out_req
+ *self.state.pwr_aux.get_fresh(|| format_dbg!())?)
/ self.pwr_out_max)
.get::<si::ratio>()])
.with_context(|| {
anyhow!(
"{}\n failed to calculate {}",
format_dbg!(),
stringify!(self.state.eff)
)
})?
} else {
si::Ratio::ZERO
} * match self.thrml.temp_eff_coeff() {
Some(tec) => *tec.get_fresh(|| format_dbg!())?,
None => 1.0 * uc::R,
},
|| format_dbg!(),
)?;
ensure!(
(*self.state.eff.get_fresh(|| format_dbg!())? >= 0.0 * uc::R
&& *self.state.eff.get_fresh(|| format_dbg!())? <= 1.0 * uc::R),
format!(
"fc efficiency ({}) must be either between 0 and 1",
self.state
.eff
.get_fresh(|| format_dbg!())?
.get::<si::ratio>()
)
);
self.state.pwr_fuel.update(
if *self.state.fc_on.get_fresh(|| format_dbg!())? {
((pwr_out_req + *self.state.pwr_aux.get_fresh(|| format_dbg!())?)
/ *self.state.eff.get_fresh(|| format_dbg!())?)
.max(self.pwr_idle_fuel)
} else {
si::Power::ZERO
},
|| format_dbg!(),
)?;
self.state.pwr_loss.update(
*self.state.pwr_fuel.get_fresh(|| format_dbg!())?
- *self.state.pwr_prop.get_fresh(|| format_dbg!())?,
|| format_dbg!(),
)?;
Ok(())
}
pub fn solve_thermal(
&mut self,
te_amb: si::Temperature,
pwr_thrml_fc_to_cab: Option<si::Power>,
veh_state: &mut VehicleState,
dt: si::Time,
) -> anyhow::Result<()> {
let veh_speed = *veh_state.speed_ach.get_stale(|| format_dbg!())?;
self.thrml
.solve_thermal(&self.state, te_amb, pwr_thrml_fc_to_cab, veh_speed, dt)
.with_context(|| format_dbg!())
}
pub fn temperature(&self) -> Option<&TrackedState<si::Temperature>> {
match &self.thrml {
FuelConverterThermalOption::FuelConverterThermal(fct) => Some(&fct.state.temperature),
FuelConverterThermalOption::None => None,
}
}
pub fn get_eff_max(&self) -> anyhow::Result<&f64> {
self.eff_interp_from_pwr_out.max()
}
pub fn get_eff_min(&self) -> anyhow::Result<&f64> {
self.eff_interp_from_pwr_out.min()
}
pub fn set_eff_max(
&mut self,
eff_max: f64,
scaling: Option<ScalingMethods>,
) -> anyhow::Result<()> {
if (0.0..=1.0).contains(&eff_max) {
self.eff_interp_from_pwr_out.set_max(eff_max, scaling)?;
} else {
return Err(anyhow!(
"`eff_max` ({:.3}) must be between 0.0 and 1.0",
eff_max,
));
}
self.init().map_err(|err| anyhow!("{:?}", err))?;
Ok(())
}
pub fn set_eff_min(
&mut self,
eff_min: f64,
scaling: Option<ScalingMethods>,
) -> anyhow::Result<()> {
self.eff_interp_from_pwr_out.set_min(eff_min, scaling)
}
pub fn set_eff_range(&mut self, eff_range: f64) -> anyhow::Result<()> {
if (0. ..=1.0).contains(&eff_range) {
self.eff_interp_from_pwr_out.set_range(eff_range)
} else {
Err(anyhow!(format!(
"`eff_range` ({:.3}) must be between 0.0 and 1.0",
eff_range,
)))
}
}
pub fn fc_thrml_state_mut(&mut self) -> Option<&mut FuelConverterThermalState> {
match &mut self.thrml {
FuelConverterThermalOption::FuelConverterThermal(fct) => Some(&mut fct.state),
FuelConverterThermalOption::None => None,
}
}
}
impl TryFrom<fastsim_2::vehicle::RustVehicle> for FuelConverter {
type Error = anyhow::Error;
fn try_from(f2veh: fastsim_2::vehicle::RustVehicle) -> Result<FuelConverter, anyhow::Error> {
let mut fc: FuelConverter = FCBuilder {
pwr_out_max: f2veh.fc_max_kw * uc::KW,
pwr_ramp_lag: f2veh.fc_sec_to_peak_pwr * uc::S,
eff_interp_from_pwr_out: InterpolatorEnum::new_1d(
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,
]
.into(),
f2veh.fc_eff_map.clone().into(),
strategy::Linear,
Extrapolate::Error,
)
.with_context(|| format_dbg!())?,
pwr_for_peak_eff: uc::KW * f64::NAN, pwr_idle_fuel: si::Power::ZERO,
save_interval: Some(1),
}
.try_into()
.with_context(|| format_dbg!())?;
fc.init()?;
fc.set_mass(None, MassSideEffect::None)
.with_context(|| anyhow!(format_dbg!()))?;
Ok(fc)
}
}
impl TryFrom<FCBuilder> for FuelConverter {
type Error = anyhow::Error;
fn try_from(fcbuilder: FCBuilder) -> Result<FuelConverter, anyhow::Error> {
let mut fc = FuelConverter {
state: Default::default(),
thrml: Default::default(),
mass: None,
specific_pwr: None,
pwr_out_max: fcbuilder.pwr_out_max,
pwr_out_max_init: fcbuilder.pwr_out_max / fcbuilder.pwr_ramp_lag.get::<si::second>(),
pwr_ramp_lag: fcbuilder.pwr_ramp_lag,
eff_interp_from_pwr_out: fcbuilder.eff_interp_from_pwr_out,
pwr_for_peak_eff: uc::KW * f64::NAN, pwr_idle_fuel: si::Power::ZERO,
save_interval: Some(1),
history: Default::default(),
};
fc.init()?;
fc.set_mass(None, MassSideEffect::None)
.with_context(|| anyhow!(format_dbg!()))?;
Ok(fc)
}
}
#[serde_api]
#[derive(Deserialize, Serialize, Debug, Clone, PartialEq)]
#[serde(deny_unknown_fields)]
#[cfg_attr(feature = "pyo3", pyclass(module = "fastsim", subclass, eq))]
pub struct FCBuilder {
pub pwr_out_max: si::Power,
pub pwr_ramp_lag: si::Time,
pub eff_interp_from_pwr_out: InterpolatorEnumOwned<f64>,
#[serde(skip)]
pub(crate) pwr_for_peak_eff: si::Power,
pub pwr_idle_fuel: si::Power,
pub save_interval: Option<usize>,
}
#[serde_api]
#[derive(
Clone,
Debug,
Default,
Deserialize,
Serialize,
PartialEq,
HistoryVec,
StateMethods,
SetCumulative,
)]
#[non_exhaustive]
#[serde(default)]
#[serde(deny_unknown_fields)]
#[cfg_attr(feature = "pyo3", pyclass(module = "fastsim", subclass, eq))]
pub struct FuelConverterState {
pub i: TrackedState<usize>,
pub pwr_out_max: TrackedState<si::Power>,
pub pwr_prop_max: TrackedState<si::Power>,
pub eff: TrackedState<si::Ratio>,
pub pwr_prop: TrackedState<si::Power>,
pub energy_prop: TrackedState<si::Energy>,
pub pwr_aux: TrackedState<si::Power>,
pub energy_aux: TrackedState<si::Energy>,
pub pwr_fuel: TrackedState<si::Power>,
pub energy_fuel: TrackedState<si::Energy>,
pub pwr_loss: TrackedState<si::Power>,
pub energy_loss: TrackedState<si::Energy>,
pub fc_on: TrackedState<bool>,
pub time_on: TrackedState<si::Time>,
}
#[pyo3_api]
impl FuelConverterState {}
impl SerdeAPI for FuelConverterState {}
impl Init for FuelConverterState {}
#[derive(
Clone, Default, Debug, Serialize, Deserialize, PartialEq, IsVariant, derive_more::From, TryInto,
)]
pub enum FuelConverterThermalOption {
FuelConverterThermal(Box<FuelConverterThermal>),
#[default]
None,
}
impl StateMethods for FuelConverterThermalOption {}
impl SaveState for FuelConverterThermalOption {
fn save_state<F: Fn() -> String>(&mut self, loc: F) -> anyhow::Result<()> {
match self {
Self::FuelConverterThermal(fct) => fct.save_state(loc)?,
Self::None => {}
}
Ok(())
}
}
impl TrackedStateMethods for FuelConverterThermalOption {
fn check_and_reset<F: Fn() -> String>(&mut self, loc: F) -> anyhow::Result<()> {
match self {
Self::FuelConverterThermal(fct) => {
fct.check_and_reset(|| format!("{}\n{}", loc(), format_dbg!()))?
}
Self::None => {}
}
Ok(())
}
fn mark_fresh<F: Fn() -> String>(&mut self, loc: F) -> anyhow::Result<()> {
match self {
Self::FuelConverterThermal(fct) => {
fct.mark_fresh(|| format!("{}\n{}", loc(), format_dbg!()))?
}
Self::None => {}
}
Ok(())
}
}
impl Step for FuelConverterThermalOption {
fn step<F: Fn() -> String>(&mut self, loc: F) -> anyhow::Result<()> {
match self {
Self::FuelConverterThermal(fct) => fct.step(|| format!("{}\n{}", loc(), format_dbg!())),
Self::None => Ok(()),
}
}
fn reset_step<F: Fn() -> String>(&mut self, loc: F) -> anyhow::Result<()> {
match self {
Self::FuelConverterThermal(fct) => {
fct.reset_step(|| format!("{}\n{}", loc(), format_dbg!()))
}
Self::None => Ok(()),
}
}
}
impl Init for FuelConverterThermalOption {
fn init(&mut self) -> Result<(), Error> {
match self {
Self::FuelConverterThermal(fct) => fct.init()?,
Self::None => {}
}
Ok(())
}
}
impl SerdeAPI for FuelConverterThermalOption {}
impl SetCumulative for FuelConverterThermalOption {
fn set_cumulative<F: Fn() -> String>(&mut self, dt: si::Time, loc: F) -> anyhow::Result<()> {
match self {
Self::FuelConverterThermal(fct) => {
fct.set_cumulative(dt, || format!("{}\n{}", loc(), format_dbg!()))?
}
Self::None => {}
}
Ok(())
}
fn reset_cumulative<F: Fn() -> String>(&mut self, loc: F) -> anyhow::Result<()> {
match self {
Self::FuelConverterThermal(fct) => {
fct.reset_cumulative(|| format!("{}\n{}", loc(), format_dbg!()))?
}
Self::None => {}
}
Ok(())
}
}
impl HistoryMethods for FuelConverterThermalOption {
fn save_interval(&self) -> anyhow::Result<Option<usize>> {
match self {
FuelConverterThermalOption::FuelConverterThermal(fct) => fct.save_interval(),
FuelConverterThermalOption::None => Ok(None),
}
}
fn set_save_interval(&mut self, save_interval: Option<usize>) -> anyhow::Result<()> {
match self {
FuelConverterThermalOption::FuelConverterThermal(fct) => {
fct.set_save_interval(save_interval)
}
FuelConverterThermalOption::None => Ok(()),
}
}
fn clear(&mut self) {
match self {
FuelConverterThermalOption::FuelConverterThermal(fct) => {
fct.clear();
}
FuelConverterThermalOption::None => {}
}
}
}
impl FuelConverterThermalOption {
fn solve_thermal(
&mut self,
fc_state: &FuelConverterState,
te_amb: si::Temperature,
pwr_thrml_fc_to_cab: Option<si::Power>,
veh_speed: si::Velocity,
dt: si::Time,
) -> anyhow::Result<()> {
match self {
Self::FuelConverterThermal(fct) => fct
.solve(
fc_state,
te_amb,
pwr_thrml_fc_to_cab.unwrap_or_default(),
veh_speed,
dt,
)
.with_context(|| format_dbg!())?,
Self::None => {
ensure!(
pwr_thrml_fc_to_cab.is_none(),
format_dbg!(
"`FuelConverterThermal needs to be configured to provide heat demand`"
)
);
}
}
Ok(())
}
fn temp_eff_coeff(&self) -> Option<&TrackedState<si::Ratio>> {
match self {
Self::FuelConverterThermal(fct) => Some(&fct.state.eff_coeff),
Self::None => None,
}
}
}
#[serde_api]
#[derive(Deserialize, Serialize, Debug, Clone, PartialEq, StateMethods)]
#[cfg_attr(feature = "pyo3", pyclass(module = "fastsim", subclass, eq))]
#[non_exhaustive]
#[serde(deny_unknown_fields)]
pub struct FuelConverterThermal {
pub heat_capacitance: si::HeatCapacity,
pub length_for_convection: si::Length,
pub htc_to_amb_stop: si::HeatTransferCoeff,
pub conductance_from_comb: si::ThermalConductance,
pub max_frac_from_comb: si::Ratio,
pub tstat_te_sto: Option<si::Temperature>,
pub tstat_te_delta: Option<si::TemperatureInterval>,
#[serde(default = "tstat_interp_default")]
pub tstat_interp: Interp1DOwned<f64, strategy::Linear>,
pub radiator_effectiveness: si::Ratio,
pub fc_eff_model: FCTempEffModel,
#[serde(default)]
pub state: FuelConverterThermalState,
#[serde(default)]
pub history: FuelConverterThermalStateHistoryVec,
pub save_interval: Option<usize>,
}
#[pyo3_api]
impl FuelConverterThermal {
#[staticmethod]
#[pyo3(name = "default")]
fn default_py() -> Self {
Default::default()
}
}
impl HistoryMethods for FuelConverterThermal {
fn save_interval(&self) -> anyhow::Result<Option<usize>> {
Ok(self.save_interval)
}
fn set_save_interval(&mut self, save_interval: Option<usize>) -> anyhow::Result<()> {
self.save_interval = save_interval;
Ok(())
}
fn clear(&mut self) {
self.history.clear();
}
}
fn tstat_interp_default() -> Interp1DOwned<f64, strategy::Linear> {
Interp1D::new(
array![85.0, 90.0],
array![0.0, 1.0],
strategy::Linear,
Extrapolate::Clamp,
)
.unwrap()
}
lazy_static! {
pub static ref AFR_STOICH_GASOLINE: si::Ratio = uc::R * 14.7;
pub static ref GASOLINE_DENSITY: si::MassDensity = 0.75 * uc::KG / uc::L;
pub static ref GASOLINE_LHV: si::SpecificEnergy = 33.7 * uc::KWH / uc::GALLON / *GASOLINE_DENSITY;
pub static ref TE_ADIABATIC_STD: si::Temperature = Air::get_te_from_u(
Air::get_specific_energy(*TE_STD_AIR).with_context(|| format_dbg!()).unwrap()
+ (Octane::get_specific_energy(*TE_STD_AIR).with_context(|| format_dbg!()).unwrap()
+ *GASOLINE_LHV)
/ *AFR_STOICH_GASOLINE,
)
.with_context(|| format_dbg!()).unwrap_or_else(|_| panic!("{}\nFailed to calculate adiabatic flame temp for gasoline", format_dbg!()));
}
impl FuelConverterThermal {
fn solve(
&mut self,
fc_state: &FuelConverterState,
te_amb: si::Temperature,
pwr_thrml_fc_to_cab: si::Power,
veh_speed: si::Velocity,
dt: si::Time,
) -> anyhow::Result<()> {
self.state
.pwr_thrml_fc_to_cab
.update(pwr_thrml_fc_to_cab, || format_dbg!())?;
let te_air_film: si::Temperature = 0.5
* (self
.state
.temperature
.get_stale(|| format_dbg!())?
.get::<si::kelvin_abs>()
+ te_amb.get::<si::kelvin_abs>())
* uc::KELVIN;
let fc_air_film_re =
Air::get_density(Some(te_air_film), None) * veh_speed * self.length_for_convection
/ Air::get_dyn_visc(te_air_film).with_context(|| format_dbg!())?;
self.state.htc_to_amb.update(
if veh_speed < 1.0 * uc::MPS {
self.state.tstat_open_frac.update(
self.tstat_interp
.interpolate(&[self
.state
.temperature
.get_stale(|| format_dbg!())?
.get::<si::degree_celsius>()])
.with_context(|| format_dbg!())?,
|| format_dbg!(),
)?;
(uc::R
+ *self.state.tstat_open_frac.get_fresh(|| format_dbg!())?
* self.radiator_effectiveness)
* self.htc_to_amb_stop
} else {
let sphere_conv_params = get_sphere_conv_params(fc_air_film_re.get::<si::ratio>());
let htc_to_amb_sphere: si::HeatTransferCoeff = sphere_conv_params.0
* fc_air_film_re.get::<si::ratio>().powf(sphere_conv_params.1)
* Air::get_pr(te_air_film)
.with_context(|| format_dbg!())?
.get::<si::ratio>()
.powf(1.0 / 3.0)
* Air::get_therm_cond(te_air_film).with_context(|| format_dbg!())?
/ self.length_for_convection;
self.state.tstat_open_frac.update(
self.tstat_interp
.interpolate(&[self
.state
.temperature
.get_stale(|| format_dbg!())?
.get::<si::degree_celsius>()])
.with_context(|| format_dbg!())?,
|| format_dbg!(),
)?;
*self.state.tstat_open_frac.get_fresh(|| format_dbg!())? * htc_to_amb_sphere
},
|| format_dbg!(),
)?;
self.state.pwr_thrml_to_amb.update(
*self.state.htc_to_amb.get_fresh(|| format_dbg!())?
* PI
* self.length_for_convection.powi(P2::new())
/ 4.0
* (self
.state
.temperature
.get_stale(|| format_dbg!())?
.get::<si::degree_celsius>()
- te_amb.get::<si::degree_celsius>())
* uc::KELVIN_INT,
|| format_dbg!(),
)?;
self.state.te_adiabatic.update(
Air::get_te_from_u(
Air::get_specific_energy(*self.state.temperature.get_stale(|| format_dbg!())?)
.with_context(|| format_dbg!())?
+ (Octane::get_specific_energy(*self.state.temperature.get_stale(|| format_dbg!())?)
.with_context(|| format_dbg!())?
+ *GASOLINE_LHV)
/ *AFR_STOICH_GASOLINE,
)
.with_context(|| format_dbg!())?,
|| format_dbg!(),
)?;
self.state.pwr_fuel_as_heat.update(
*fc_state.pwr_fuel.get_stale(|| format_dbg!())?
- (*fc_state.pwr_prop.get_stale(|| format_dbg!())?
+ *fc_state.pwr_aux.get_stale(|| format_dbg!())?),
|| format_dbg!(),
)?;
self.state.pwr_thrml_to_tm.update(
(self.conductance_from_comb
* (self
.state
.te_adiabatic
.get_fresh(|| format_dbg!())?
.get::<si::degree_celsius>()
- self
.state
.temperature
.get_stale(|| format_dbg!())?
.get::<si::degree_celsius>())
* uc::KELVIN_INT)
.min(
self.max_frac_from_comb
* *self.state.pwr_fuel_as_heat.get_fresh(|| format_dbg!())?,
),
|| format_dbg!(),
)?;
let delta_temp: si::TemperatureInterval =
((*self.state.pwr_thrml_to_tm.get_fresh(|| format_dbg!())?
- *self.state.pwr_thrml_fc_to_cab.get_fresh(|| format_dbg!())?
- *self.state.pwr_thrml_to_amb.get_fresh(|| format_dbg!())?)
* dt)
/ self.heat_capacitance;
self.state.temperature.update(
*self.state.temperature.get_stale(|| format_dbg!())? + delta_temp,
|| format_dbg!(),
)?;
self.state.eff_coeff.update(
match self.fc_eff_model {
FCTempEffModel::Linear(FCTempEffModelLinear {
offset,
slope_per_kelvin: slope,
minimum,
}) => minimum.max(
{
let calc_unbound: si::Ratio = offset
+ slope * uc::R / uc::KELVIN
* *self.state.temperature.get_fresh(|| format_dbg!())?;
calc_unbound
}
.min(1.0 * uc::R),
),
FCTempEffModel::Exponential(FCTempEffModelExponential {
offset,
lag,
minimum,
}) => {
let dte: si::TemperatureInterval = (self
.state
.temperature
.get_fresh(|| format_dbg!())?
.get::<si::kelvin_abs>()
- offset.get::<si::kelvin_abs>())
* uc::KELVIN_INT;
((1.0 - f64::exp((-dte / lag).get::<si::ratio>())) * uc::R).max(minimum)
}
},
|| format_dbg!(),
)?;
Ok(())
}
}
impl SerdeAPI for FuelConverterThermal {}
impl SetCumulative for FuelConverterThermal {
fn set_cumulative<F: Fn() -> String>(&mut self, dt: si::Time, loc: F) -> anyhow::Result<()> {
self.state
.set_cumulative(dt, || format!("{}\n{}", loc(), format_dbg!()))
}
fn reset_cumulative<F: Fn() -> String>(&mut self, loc: F) -> anyhow::Result<()> {
self.state
.reset_cumulative(|| format!("{}\n{}", loc(), format_dbg!()))
}
}
impl Init for FuelConverterThermal {
fn init(&mut self) -> Result<(), Error> {
self.tstat_te_sto = self
.tstat_te_sto
.or(Some((85. + uc::CELSIUS_TO_KELVIN) * uc::KELVIN));
self.tstat_te_delta = self.tstat_te_delta.or(Some(5. * uc::KELVIN_INT));
self.tstat_interp = Interp1D::new(
array![
self.tstat_te_sto.unwrap().get::<si::degree_celsius>(),
self.tstat_te_sto.unwrap().get::<si::degree_celsius>()
+ self.tstat_te_delta.unwrap().get::<si::kelvin>(),
],
array![0.0, 1.0],
strategy::Linear,
Extrapolate::Clamp,
)
.map_err(|err| {
Error::InitError(format!(
"{}\n{}\n{}",
err,
format_dbg!(self.tstat_te_sto),
format_dbg!(self.tstat_te_delta)
))
})?;
Ok(())
}
}
impl Default for FuelConverterThermal {
fn default() -> Self {
let mut fct = Self {
heat_capacitance: Default::default(),
length_for_convection: Default::default(),
htc_to_amb_stop: Default::default(),
conductance_from_comb: Default::default(),
max_frac_from_comb: Default::default(),
tstat_te_sto: None,
tstat_te_delta: None,
tstat_interp: tstat_interp_default(),
radiator_effectiveness: Default::default(),
fc_eff_model: Default::default(),
state: Default::default(),
history: Default::default(),
save_interval: Some(1),
};
fct.init().unwrap();
fct
}
}
#[serde_api]
#[derive(
Clone, Debug, Deserialize, Serialize, PartialEq, HistoryVec, StateMethods, SetCumulative,
)]
#[serde(default)]
#[cfg_attr(feature = "pyo3", pyclass(module = "fastsim", subclass, eq))]
#[serde(deny_unknown_fields)]
pub struct FuelConverterThermalState {
pub i: TrackedState<usize>,
pub te_adiabatic: TrackedState<si::Temperature>,
pub temperature: TrackedState<si::Temperature>,
pub tstat_open_frac: TrackedState<f64>,
pub htc_to_amb: TrackedState<si::HeatTransferCoeff>,
pub pwr_thrml_to_amb: TrackedState<si::Power>,
pub energy_thrml_to_amb: TrackedState<si::Energy>,
pub eff_coeff: TrackedState<si::Ratio>,
pub pwr_thrml_fc_to_cab: TrackedState<si::Power>,
pub energy_thrml_fc_to_cab: TrackedState<si::Energy>,
pub pwr_fuel_as_heat: TrackedState<si::Power>,
pub energy_fuel_as_heat: TrackedState<si::Energy>,
pub pwr_thrml_to_tm: TrackedState<si::Power>,
pub energy_thrml_to_tm: TrackedState<si::Energy>,
}
#[pyo3_api]
impl FuelConverterThermalState {}
impl Init for FuelConverterThermalState {}
impl SerdeAPI for FuelConverterThermalState {}
impl Default for FuelConverterThermalState {
fn default() -> Self {
Self {
i: Default::default(),
te_adiabatic: TrackedState::new(*TE_ADIABATIC_STD),
temperature: TrackedState::new(*TE_STD_AIR),
tstat_open_frac: Default::default(),
htc_to_amb: Default::default(),
eff_coeff: TrackedState::new(uc::R),
pwr_thrml_fc_to_cab: Default::default(),
energy_thrml_fc_to_cab: Default::default(),
pwr_thrml_to_amb: Default::default(),
energy_thrml_to_amb: Default::default(),
pwr_fuel_as_heat: Default::default(),
energy_fuel_as_heat: Default::default(),
pwr_thrml_to_tm: Default::default(),
energy_thrml_to_tm: Default::default(),
}
}
}
#[derive(
Debug, Clone, Deserialize, Serialize, PartialEq, IsVariant, derive_more::From, TryInto,
)]
pub enum FCTempEffModel {
Linear(FCTempEffModelLinear),
Exponential(FCTempEffModelExponential),
}
impl Default for FCTempEffModel {
fn default() -> Self {
FCTempEffModel::Exponential(FCTempEffModelExponential::default())
}
}
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
#[serde(deny_unknown_fields)]
pub struct FCTempEffModelLinear {
pub offset: si::Ratio,
pub slope_per_kelvin: f64,
pub minimum: si::Ratio,
}
impl Default for FCTempEffModelLinear {
fn default() -> Self {
Self {
offset: 0.0 * uc::R,
slope_per_kelvin: 25.0,
minimum: 0.2 * uc::R,
}
}
}
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
#[serde(deny_unknown_fields)]
pub struct FCTempEffModelExponential {
pub offset: si::Temperature,
pub lag: si::TemperatureInterval,
pub minimum: si::Ratio,
}
impl Default for FCTempEffModelExponential {
fn default() -> Self {
Self {
offset: 0.0 * uc::KELVIN,
lag: 25.0 * uc::KELVIN_INT,
minimum: 0.2 * uc::R,
}
}
}