use super::{utils::ScalingMethods, *};
use crate::utils::interp::InterpolatorMutMethods;
#[allow(unused_imports)]
#[cfg(feature = "pyo3")]
use crate::pyo3::*;
const TOL: f64 = 1e-3;
#[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 ReversibleEnergyStorage {
#[has_state]
#[serde(default)]
pub thrml: RESThermalOption,
#[serde(default)]
pub(in super::super) mass: Option<si::Mass>,
pub(in super::super) specific_energy: Option<si::SpecificEnergy>,
pub pwr_out_max: si::Power,
pub energy_capacity: si::Energy,
pub eff_interp: EffInterp,
pub min_soc: si::Ratio,
pub max_soc: si::Ratio,
pub save_interval: Option<usize>,
#[serde(default)]
pub state: ReversibleEnergyStorageState,
#[serde(default)]
pub history: ReversibleEnergyStorageStateHistoryVec,
}
#[pyo3_api]
impl ReversibleEnergyStorage {
#[pyo3(name = "set_mass")]
#[pyo3(signature = (mass_kg=None, side_effect=None))]
fn set_mass_py(
&mut self,
mass_kg: Option<f64>,
side_effect: Option<String>,
) -> anyhow::Result<()> {
let side_effect = side_effect.unwrap_or_else(|| "Intensive".into());
self.set_mass(
mass_kg.map(|m| m * uc::KG),
MassSideEffect::try_from(side_effect)?,
)?;
Ok(())
}
#[getter("mass_kg")]
fn get_mass_kg_py(&mut self) -> anyhow::Result<Option<f64>> {
Ok(self.mass()?.map(|m| m.get::<si::kilogram>()))
}
#[getter]
fn get_specific_energy_kjoules_per_kg(&self) -> Option<f64> {
self.specific_energy
.map(|se| se.get::<si::kilojoule_per_kilogram>())
}
#[getter]
fn get_energy_capacity_usable_joules(&self) -> f64 {
self.energy_capacity_usable().get::<si::joule>()
}
#[pyo3(name = "set_default_pwr_interp")]
fn set_default_pwr_interp_py(&mut self) -> anyhow::Result<()> {
self.set_default_pwr_interp()
}
#[pyo3(name = "set_default_pwr_and_soc_interp")]
fn set_default_pwr_and_soc_interp_py(&mut self) -> anyhow::Result<()> {
self.set_default_pwr_and_soc_interp()
}
#[pyo3(name = "set_default_pwr_and_temp_interp")]
fn set_default_pwr_and_temp_interp_py(&mut self) -> anyhow::Result<()> {
self.set_default_pwr_and_temp_interp()
}
#[pyo3(name = "set_default_pwr_soc_and_temp_interp")]
fn set_default_pwr_soc_and_temp_interp_py(&mut self) -> anyhow::Result<()> {
self.set_default_pwr_soc_and_temp_interp()
}
}
impl ReversibleEnergyStorage {
pub fn solve(&mut self, pwr_out_req: si::Power, dt: si::Time) -> anyhow::Result<()> {
let te_res: Option<si::Temperature> = self.temperature()?;
let state = &mut self.state;
ensure!(
*state.soc.get_stale(|| format_dbg!())? <= self.max_soc,
format_dbg!(state.soc.get_stale(|| format_dbg!())?.get::<si::ratio>())
);
ensure!(
almost_ge_uom(
state.soc.get_stale(|| format_dbg!())?,
&self.min_soc,
Some(1e-3)
),
"{}\n{}\n{}",
format_dbg!(state.soc.get_stale(|| format_dbg!())?.get::<si::ratio>()),
format_dbg!(state
.soc_disch_buffer
.get_fresh(|| format_dbg!())?
.get::<si::ratio>()),
format_dbg!(state.pwr_aux.get_fresh(|| format_dbg!())?.get::<si::watt>())
);
state.pwr_out_prop.update(pwr_out_req, || format_dbg!())?;
state.pwr_out_electrical.update(
*state.pwr_out_prop.get_fresh(|| format_dbg!())?
+ *state.pwr_aux.get_fresh(|| format_dbg!())?,
|| format_dbg!(),
)?;
if pwr_out_req + *state.pwr_aux.get_fresh(|| format_dbg!())? >= si::Power::ZERO {
ensure!(
utils::almost_le_uom(
&(pwr_out_req + *state.pwr_aux.get_fresh(|| format_dbg!())?),
&self.pwr_out_max,
Some(TOL)),
"{}\nres required power ({:.6} kW) exceeds static max discharge power ({:.6} kW)\nstate.soc = {}",
format_dbg!(utils::almost_le_uom(
&(pwr_out_req + *state.pwr_aux.get_fresh(|| format_dbg!())?),
&self.pwr_out_max,
Some(TOL)
)),
(pwr_out_req + *state.pwr_aux.get_fresh(|| format_dbg!())?).get::<si::kilowatt>(),
&self.pwr_out_max.get::<si::kilowatt>(),
state.soc.get_stale(|| format_dbg!())?.get::<si::ratio>()
);
ensure!(
utils::almost_le_uom(
&(pwr_out_req + *state.pwr_aux.get_fresh(|| format_dbg!())?),
state.pwr_disch_max.get_fresh(|| format_dbg!())?, Some(TOL)
),
"{}\nres required power ({:.6} kW) exceeds current max discharge power ({:.6} kW)\nstate.soc .get_fresh(|| format_dbg!())?= {}",
format_dbg!(utils::almost_le_uom(
&(pwr_out_req + *state.pwr_aux.get_fresh(|| format_dbg!())?),
state.pwr_disch_max.get_fresh(|| format_dbg!())?, Some(TOL)
)),
(pwr_out_req + *state.pwr_aux.get_fresh(|| format_dbg!())?).get::<si::kilowatt>(),
state.pwr_disch_max.get_fresh(|| format_dbg!())?.get::<si::kilowatt>(),
state.soc.get_stale(|| format_dbg!())?.get::<si::ratio>()
);
} else {
ensure!(
utils::almost_ge_uom(
&(pwr_out_req + *state.pwr_aux.get_fresh(|| format_dbg!())?),
&-self.pwr_out_max,
Some(TOL)
),
format!(
"{}\nres required power ({:.6} kW) exceeds static max power ({:.6} kW)",
format_dbg!(utils::almost_ge_uom(
&(pwr_out_req + *state.pwr_aux.get_fresh(|| format_dbg!())?),
&-self.pwr_out_max,
Some(TOL)
)),
(pwr_out_req + *state.pwr_aux.get_fresh(|| format_dbg!())?)
.get::<si::kilowatt>(),
state
.pwr_charge_max
.get_fresh(|| format_dbg!())?
.get::<si::kilowatt>()
)
);
ensure!(
utils::almost_ge_uom(
&(pwr_out_req + *state.pwr_aux.get_fresh(|| format_dbg!())?),
&-*state.pwr_charge_max.get_fresh(|| format_dbg!())?,
Some(TOL)
),
format!(
"{}\nres required power ({:.6} kW) exceeds current max power ({:.6} kW)",
format_dbg!(utils::almost_ge_uom(
&(pwr_out_req + *state.pwr_aux.get_fresh(|| format_dbg!())?),
&-*state.pwr_charge_max.get_fresh(|| format_dbg!())?,
Some(TOL)
)),
(pwr_out_req + *state.pwr_aux.get_fresh(|| format_dbg!())?)
.get::<si::kilowatt>(),
-state
.pwr_charge_max
.get_fresh(|| format_dbg!())?
.get::<si::kilowatt>()
)
);
}
let interp_pt: &[f64] = match &self.eff_interp {
EffInterp::Constant(_) => &[],
EffInterp::CRate(_) => &[state
.pwr_out_electrical
.get_fresh(|| format_dbg!())?
.get::<si::watt>()
/ self.energy_capacity.get::<si::watt_hour>()],
EffInterp::CRateSOC(_) => &[
state
.pwr_out_electrical
.get_fresh(|| format_dbg!())?
.get::<si::watt>()
/ self.energy_capacity.get::<si::watt_hour>(),
state.soc.get_stale(|| format_dbg!())?.get::<si::ratio>(),
],
EffInterp::CRateTemperature(_) => &[
state
.pwr_out_electrical
.get_fresh(|| format_dbg!())?
.get::<si::watt>()
/ self.energy_capacity.get::<si::watt_hour>(),
te_res
.with_context(|| format_dbg!("Expected thermal model to be configured"))?
.get::<si::degree_celsius>(),
],
EffInterp::CRateSOCTemperature(_) => &[
state
.pwr_out_electrical
.get_fresh(|| format_dbg!())?
.get::<si::watt>()
/ self.energy_capacity.get::<si::watt_hour>(),
state.soc.get_stale(|| format_dbg!())?.get::<si::ratio>(),
te_res
.with_context(|| format_dbg!("Expected thermal model to be configured"))?
.get::<si::degree_celsius>(),
],
};
state.eff.update(
self.eff_interp.interpolate(interp_pt)? * uc::R,
|| format_dbg!(),
)?;
ensure!(
*state.eff.get_fresh(|| format_dbg!())? >= 0.0 * uc::R
&& *state.eff.get_fresh(|| format_dbg!())? <= 1.0 * uc::R,
format!(
"{}\nres efficiency ({}) must be between 0 and 1",
format_dbg!(
*state.eff.get_fresh(|| format_dbg!())? >= 0.0 * uc::R
&& *state.eff.get_fresh(|| format_dbg!())? <= 1.0 * uc::R
),
state.eff.get_fresh(|| format_dbg!())?.get::<si::ratio>()
)
);
state.pwr_out_chemical.update(
if *state.pwr_out_electrical.get_fresh(|| format_dbg!())? > si::Power::ZERO {
*state.pwr_out_electrical.get_fresh(|| format_dbg!())?
/ *state.eff.get_fresh(|| format_dbg!())?
} else {
*state.pwr_out_electrical.get_fresh(|| format_dbg!())?
* *state.eff.get_fresh(|| format_dbg!())?
},
|| format_dbg!(),
)?;
state.pwr_loss.update(
(*state.pwr_out_chemical.get_fresh(|| format_dbg!())?
- *state.pwr_out_electrical.get_fresh(|| format_dbg!())?)
.abs(),
|| format_dbg!(),
)?;
state.soc.update(
*state.soc.get_stale(|| format_dbg!())?
- *state.pwr_out_chemical.get_fresh(|| format_dbg!())? * dt / self.energy_capacity,
|| format_dbg!(),
)?;
Ok(())
}
pub fn solve_thermal(
&mut self,
te_amb: si::Temperature,
pwr_thrml_hvac_to_res: si::Power,
te_cab: Option<si::Temperature>,
dt: si::Time,
) -> anyhow::Result<()> {
self.thrml
.solve(&mut self.state, te_amb, pwr_thrml_hvac_to_res, te_cab, dt)
.with_context(|| format_dbg!())
}
pub fn set_curr_pwr_out_max(
&mut self,
dt: si::Time,
disch_buffer: si::Energy,
chrg_buffer: si::Energy,
) -> anyhow::Result<()> {
self.set_pwr_disch_max(dt, disch_buffer)?;
self.set_pwr_charge_max(dt, chrg_buffer)?;
Ok(())
}
pub fn get_curr_pwr_prop_out_max(&self) -> anyhow::Result<(si::Power, si::Power)> {
Ok((
*self.state.pwr_prop_max.get_fresh(|| format_dbg!())?,
*self.state.pwr_regen_max.get_fresh(|| format_dbg!())?,
))
}
pub fn set_pwr_charge_max(
&mut self,
dt: si::Time,
chrg_buffer: si::Energy,
) -> anyhow::Result<()> {
let soc_buffer_delta = (chrg_buffer
/ (self.energy_capacity * (self.max_soc - self.min_soc)))
.max(si::Ratio::ZERO);
ensure!(soc_buffer_delta >= si::Ratio::ZERO, "{}", format_dbg!());
self.state
.soc_regen_buffer
.update(self.max_soc - soc_buffer_delta, || format_dbg!())?;
let pwr_max_for_dt = ((self.max_soc - *self.state.soc.get_stale(|| format_dbg!())?)
* self.energy_capacity
/ dt)
.max(si::Power::ZERO);
self.state.pwr_charge_max.update(
if *self.state.soc.get_stale(|| format_dbg!())?
<= *self.state.soc_regen_buffer.get_fresh(|| format_dbg!())?
{
self.pwr_out_max
} else if *self.state.soc.get_stale(|| format_dbg!())? < self.max_soc
&& soc_buffer_delta > si::Ratio::ZERO
{
self.pwr_out_max * (self.max_soc - *self.state.soc.get_stale(|| format_dbg!())?)
/ soc_buffer_delta
} else {
si::Power::ZERO
}
.min(pwr_max_for_dt),
|| format_dbg!(),
)?;
ensure!(
*self.state.pwr_charge_max.get_fresh(|| format_dbg!())? >= si::Power::ZERO,
"{}\n`{}` ({} W) must be greater than or equal to zero\n{}",
format_dbg!(),
stringify!(self.state.pwr_charge_max),
self.state
.pwr_charge_max
.get_fresh(|| format_dbg!())?
.get::<si::watt>()
.format_eng(None),
format_dbg!(soc_buffer_delta)
);
Ok(())
}
pub fn set_pwr_disch_max(
&mut self,
dt: si::Time,
disch_buffer: si::Energy,
) -> anyhow::Result<()> {
let soc_buffer_delta = (disch_buffer / self.energy_capacity_usable()).max(si::Ratio::ZERO);
ensure!(soc_buffer_delta >= si::Ratio::ZERO, "{}", format_dbg!());
self.state
.soc_disch_buffer
.update(self.min_soc + soc_buffer_delta, || format_dbg!())?;
let pwr_max_for_dt = ((*self.state.soc.get_stale(|| format_dbg!())? - self.min_soc)
* self.energy_capacity
/ dt)
.max(si::Power::ZERO);
self.state.pwr_disch_max.update(
if *self.state.soc.get_stale(|| format_dbg!())?
> *self.state.soc_disch_buffer.get_fresh(|| format_dbg!())?
{
self.pwr_out_max
} else if *self.state.soc.get_stale(|| format_dbg!())? > self.min_soc
&& soc_buffer_delta > si::Ratio::ZERO
{
self.pwr_out_max * (*self.state.soc.get_stale(|| format_dbg!())? - self.min_soc)
/ soc_buffer_delta
} else {
si::Power::ZERO
}
.min(pwr_max_for_dt),
|| format_dbg!(),
)?;
ensure!(
*self.state.pwr_disch_max.get_fresh(|| format_dbg!())? >= si::Power::ZERO,
"{}\n`{}` ({} W) must be greater than or equal to zero\n{}",
format_dbg!(),
stringify!(self.state.pwr_disch_max),
self.state
.pwr_disch_max
.get_fresh(|| format_dbg!())?
.get::<si::watt>()
.format_eng(None),
format_dbg!(soc_buffer_delta)
);
Ok(())
}
pub fn set_curr_pwr_prop_max(&mut self, pwr_aux: si::Power) -> anyhow::Result<()> {
let state = &mut self.state;
state.pwr_aux.update(pwr_aux, || format_dbg!())?;
state.pwr_prop_max.update(
*state.pwr_disch_max.get_fresh(|| format_dbg!())? - pwr_aux,
|| format_dbg!(),
)?;
state.pwr_regen_max.update(
*state.pwr_charge_max.get_fresh(|| format_dbg!())? + pwr_aux,
|| format_dbg!(),
)?;
ensure!(
pwr_aux <= *state.pwr_disch_max.get_fresh(|| format_dbg!())?,
"{}\n`{}` ({} W) must always be less than or equal to {} ({} W)\n`state.soc`:{}
`soc_disch_buffer`: {}",
format_dbg!(),
stringify!(pwr_aux),
pwr_aux.get::<si::watt>().format_eng(None),
stringify!(state.pwr_disch_max),
state
.pwr_disch_max
.get_fresh(|| format_dbg!())?
.get::<si::watt>()
.format_eng(None),
state
.soc
.get_stale(|| format_dbg!())?
.get::<si::ratio>()
.format_eng(None),
state
.soc_disch_buffer
.get_fresh(|| format_dbg!())?
.get::<si::ratio>()
.format_eng(None)
);
ensure!(
*state.pwr_prop_max.get_fresh(|| format_dbg!())? >= si::Power::ZERO,
"{}\n`{}` ({} W) must be greater than or equal to zero",
format_dbg!(),
stringify!(state.pwr_prop_max),
state
.pwr_prop_max
.get_fresh(|| format_dbg!())?
.get::<si::watt>()
.format_eng(None)
);
ensure!(
*state.pwr_regen_max.get_fresh(|| format_dbg!())? >= si::Power::ZERO,
"{}\n`{}` ({} W) must be greater than or equal to zero",
format_dbg!(),
stringify!(state.pwr_regen_max),
state
.pwr_regen_max
.get_fresh(|| format_dbg!())?
.get::<si::watt>()
.format_eng(None)
);
Ok(())
}
pub fn set_specific_energy(
mut self,
specific_energy: si::SpecificEnergy,
side_effect: SpecificEnergySideEffect,
) -> anyhow::Result<()> {
self.specific_energy = Some(specific_energy);
match side_effect {
SpecificEnergySideEffect::Mass => self.set_mass(
Some(self.energy_capacity / specific_energy),
MassSideEffect::Intensive,
)?,
SpecificEnergySideEffect::Energy => {
self.energy_capacity = specific_energy
* self.mass.with_context(|| {
format_dbg!("Expected `ReversibleEnergyStorage::mass` to have been set.")
})?;
}
}
Ok(())
}
pub fn get_eff_max(&self) -> anyhow::Result<f64> {
Ok(*self.eff_interp.max()?)
}
pub fn set_eff_max(
&mut self,
eff_max: f64,
scaling: Option<ScalingMethods>,
) -> anyhow::Result<()> {
self.eff_interp.set_max(eff_max, scaling)
}
pub fn get_eff_min(&self) -> anyhow::Result<&f64> {
self.eff_interp.min()
}
pub fn set_eff_min(
&mut self,
eff_min: f64,
scaling: Option<ScalingMethods>,
) -> anyhow::Result<()> {
self.eff_interp.set_min(eff_min, scaling)
}
pub fn get_eff_range(&self) -> anyhow::Result<f64> {
self.eff_interp.range()
}
pub fn set_eff_range(&mut self, eff_range: f64) -> anyhow::Result<()> {
self.eff_interp.set_range(eff_range)
}
pub fn energy_capacity_usable(&self) -> si::Energy {
self.energy_capacity * (self.max_soc - self.min_soc)
}
#[cfg(all(feature = "yaml", feature = "resources"))]
pub fn set_default_pwr_interp(&mut self) -> anyhow::Result<()> {
if let InterpolatorEnum::Interp1D(interp1d) =
InterpolatorEnum::from_resource("res/default_pwr.yaml", false)?
{
self.eff_interp = EffInterp::CRate(interp1d);
} else {
bail!("Invalid interpolator format. Expected `Interp1D`")
}
Ok(())
}
#[cfg(all(feature = "yaml", feature = "resources"))]
pub fn set_default_pwr_and_soc_interp(&mut self) -> anyhow::Result<()> {
if let InterpolatorEnum::Interp2D(interp2d) =
InterpolatorEnum::from_resource("res/default_pwr_and_soc.yaml", false)?
{
self.eff_interp = EffInterp::CRateSOC(interp2d);
} else {
bail!("Invalid interpolator format. Expected `Interp2D`")
}
Ok(())
}
#[cfg(all(feature = "yaml", feature = "resources"))]
pub fn set_default_pwr_and_temp_interp(&mut self) -> anyhow::Result<()> {
if let InterpolatorEnum::Interp2D(interp2d) =
InterpolatorEnum::from_resource("res/default_pwr_and_temp.yaml", false)?
{
self.eff_interp = EffInterp::CRateTemperature(interp2d);
} else {
bail!("Invalid interpolator format. Expected `Interp2D`")
}
Ok(())
}
#[cfg(all(feature = "yaml", feature = "resources"))]
pub fn set_default_pwr_soc_and_temp_interp(&mut self) -> anyhow::Result<()> {
if let InterpolatorEnum::Interp3D(interp3d) =
InterpolatorEnum::from_resource("res/default_pwr_soc_and_temp.yaml", false)?
{
self.eff_interp = EffInterp::CRateSOCTemperature(interp3d);
} else {
bail!("Invalid interpolator format. Expected `Interp2D`")
}
Ok(())
}
pub fn res_thrml_state(&self) -> Option<&RESLumpedThermalState> {
match &self.thrml {
RESThermalOption::RESLumpedThermal(rest) => Some(&rest.state),
RESThermalOption::None => None,
}
}
pub fn res_thrml_state_mut(&mut self) -> Option<&mut RESLumpedThermalState> {
match &mut self.thrml {
RESThermalOption::RESLumpedThermal(rest) => Some(&mut rest.state),
RESThermalOption::None => None,
}
}
pub fn temperature(&self) -> anyhow::Result<Option<si::Temperature>> {
match &self.thrml {
RESThermalOption::RESLumpedThermal(rest) => {
Some(rest.state.temperature.get_fresh(|| format_dbg!()).cloned())
}
RESThermalOption::None => None,
}
.transpose()
}
}
impl Mass for ReversibleEnergyStorage {
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.energy_capacity = self.specific_energy.ok_or_else(|| {
anyhow!(
"{}\nExpected `self.specific_energy` to be `Some`.",
format_dbg!()
)
})? * new_mass;
}
MassSideEffect::Intensive => {
self.specific_energy = Some(self.energy_capacity / new_mass);
}
MassSideEffect::None => {
self.specific_energy = None;
}
}
}
} else if new_mass.is_none() {
self.specific_energy = None;
}
self.mass = new_mass;
Ok(())
}
fn derived_mass(&self) -> anyhow::Result<Option<si::Mass>> {
Ok(self
.specific_energy
.map(|specific_energy| self.energy_capacity / specific_energy))
}
fn expunge_mass_fields(&mut self) {
self.mass = None;
self.specific_energy = None;
}
}
impl SerdeAPI for ReversibleEnergyStorage {}
impl Init for ReversibleEnergyStorage {
fn init(&mut self) -> Result<(), Error> {
let _ = self
.mass()
.map_err(|err| Error::InitError(format_dbg!(err)))?;
self.state
.init()
.map_err(|err| Error::InitError(format_dbg!(err)))?;
if self.max_soc <= self.min_soc {
return Err(Error::InitError(format!(
"{}\n`max_soc`: {} must be greater than `min_soc`: {}`",
format_dbg!(),
self.max_soc.get::<si::ratio>(),
self.min_soc.get::<si::ratio>(),
)));
};
Ok(())
}
}
impl HistoryMethods for ReversibleEnergyStorage {
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 TryFrom<fastsim_2::vehicle::RustVehicle> for ReversibleEnergyStorage {
type Error = anyhow::Error;
fn try_from(f2veh: fastsim_2::vehicle::RustVehicle) -> anyhow::Result<ReversibleEnergyStorage> {
let f3_res = ReversibleEnergyStorage {
thrml: Default::default(),
state: Default::default(),
mass: None,
specific_energy: None,
pwr_out_max: f2veh.ess_max_kw * uc::KW,
energy_capacity: f2veh.ess_max_kwh * uc::KWH,
eff_interp: EffInterp::Constant(Interp0D::new(f2veh.ess_round_trip_eff.sqrt())),
min_soc: f2veh.min_soc * uc::R,
max_soc: f2veh.max_soc * uc::R,
save_interval: Some(1),
history: Default::default(),
};
Ok(f3_res)
}
}
#[derive(
Clone, Debug, Serialize, Deserialize, PartialEq, IsVariant, derive_more::From, TryInto,
)]
pub enum SpecificEnergySideEffect {
Mass,
Energy,
}
#[serde_api]
#[derive(
Clone, Debug, Deserialize, Serialize, PartialEq, HistoryVec, StateMethods, SetCumulative,
)]
#[non_exhaustive]
#[serde(deny_unknown_fields)]
#[cfg_attr(feature = "pyo3", pyclass(module = "fastsim", subclass, eq))]
#[serde(default)]
pub struct ReversibleEnergyStorageState {
pub pwr_prop_max: TrackedState<si::Power>,
pub pwr_regen_max: TrackedState<si::Power>,
pub pwr_disch_max: TrackedState<si::Power>,
pub pwr_charge_max: TrackedState<si::Power>,
pub i: TrackedState<usize>,
pub soc: TrackedState<si::Ratio>,
pub soc_regen_buffer: TrackedState<si::Ratio>,
pub soc_disch_buffer: TrackedState<si::Ratio>,
pub eff: TrackedState<si::Ratio>,
pub soh: TrackedState<f64>,
pub pwr_out_electrical: TrackedState<si::Power>,
pub pwr_out_prop: TrackedState<si::Power>,
pub pwr_aux: TrackedState<si::Power>,
pub pwr_loss: TrackedState<si::Power>,
pub pwr_out_chemical: TrackedState<si::Power>,
pub energy_out_electrical: TrackedState<si::Energy>,
pub energy_out_prop: TrackedState<si::Energy>,
pub energy_aux: TrackedState<si::Energy>,
pub energy_loss: TrackedState<si::Energy>,
pub energy_out_chemical: TrackedState<si::Energy>,
}
#[pyo3_api]
impl ReversibleEnergyStorageState {}
impl Default for ReversibleEnergyStorageState {
fn default() -> Self {
Self {
pwr_prop_max: Default::default(),
pwr_regen_max: Default::default(),
pwr_disch_max: Default::default(),
pwr_charge_max: Default::default(),
i: Default::default(),
soc: TrackedState::new(uc::R * 0.5),
soc_regen_buffer: TrackedState::new(uc::R * 1.),
soc_disch_buffer: Default::default(),
eff: Default::default(),
soh: Default::default(),
pwr_out_electrical: Default::default(),
pwr_out_prop: Default::default(),
pwr_aux: Default::default(),
pwr_loss: Default::default(),
pwr_out_chemical: Default::default(),
energy_out_electrical: Default::default(),
energy_out_prop: Default::default(),
energy_aux: Default::default(),
energy_loss: Default::default(),
energy_out_chemical: Default::default(),
}
}
}
impl Init for ReversibleEnergyStorageState {}
impl SerdeAPI for ReversibleEnergyStorageState {}
#[derive(
Clone, Default, Debug, Serialize, Deserialize, PartialEq, IsVariant, derive_more::From, TryInto,
)]
pub enum RESThermalOption {
RESLumpedThermal(Box<RESLumpedThermal>),
#[default]
None,
}
impl SetCumulative for RESThermalOption {
fn set_cumulative<F: Fn() -> String>(&mut self, dt: si::Time, loc: F) -> anyhow::Result<()> {
match self {
Self::RESLumpedThermal(rlt) => {
rlt.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::RESLumpedThermal(rlt) => {
rlt.reset_cumulative(|| format!("{}\n{}", loc(), format_dbg!()))?
}
Self::None => {}
}
Ok(())
}
}
impl StateMethods for RESThermalOption {}
impl SaveState for RESThermalOption {
fn save_state<F: Fn() -> String>(&mut self, loc: F) -> anyhow::Result<()> {
match self {
Self::RESLumpedThermal(rlt) => rlt.save_state(loc)?,
Self::None => {}
}
Ok(())
}
}
impl TrackedStateMethods for RESThermalOption {
fn check_and_reset<F: Fn() -> String>(&mut self, loc: F) -> anyhow::Result<()> {
match self {
Self::RESLumpedThermal(rlt) => {
rlt.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::RESLumpedThermal(rlt) => {
rlt.mark_fresh(|| format!("{}\n{}", loc(), format_dbg!()))?
}
Self::None => {}
}
Ok(())
}
}
impl Step for RESThermalOption {
fn step<F: Fn() -> String>(&mut self, loc: F) -> anyhow::Result<()> {
match self {
Self::RESLumpedThermal(rlt) => rlt.step(|| format!("{}\n{}", loc(), format_dbg!())),
Self::None => Ok(()),
}
}
fn reset_step<F: Fn() -> String>(&mut self, loc: F) -> anyhow::Result<()> {
match self {
Self::RESLumpedThermal(rlt) => {
rlt.reset_step(|| format!("{}\n{}", loc(), format_dbg!()))
}
Self::None => Ok(()),
}
}
}
impl Init for RESThermalOption {
fn init(&mut self) -> Result<(), Error> {
match self {
Self::RESLumpedThermal(rest) => rest.init()?,
Self::None => {}
}
Ok(())
}
}
impl SerdeAPI for RESThermalOption {}
impl HistoryMethods for RESThermalOption {
fn save_interval(&self) -> anyhow::Result<Option<usize>> {
match self {
RESThermalOption::RESLumpedThermal(rlt) => rlt.save_interval(),
RESThermalOption::None => Ok(None),
}
}
fn set_save_interval(&mut self, save_interval: Option<usize>) -> anyhow::Result<()> {
match self {
RESThermalOption::RESLumpedThermal(rlt) => rlt.set_save_interval(save_interval),
RESThermalOption::None => Ok(()),
}
}
fn clear(&mut self) {
match self {
RESThermalOption::RESLumpedThermal(rlt) => rlt.clear(),
RESThermalOption::None => {}
}
}
}
impl RESThermalOption {
fn solve(
&mut self,
res_state: &mut ReversibleEnergyStorageState,
te_amb: si::Temperature,
pwr_thrml_hvac_to_res: si::Power,
te_cab: Option<si::Temperature>,
dt: si::Time,
) -> anyhow::Result<()> {
match self {
Self::RESLumpedThermal(rest) => rest
.solve(
res_state,
te_amb,
pwr_thrml_hvac_to_res,
te_cab.with_context(|| {
format_dbg!(
"`te_cab` must be `Some` for [RESThermalOption::RESLumpedThermal]"
)
})?,
dt,
)
.with_context(|| format_dbg!())?,
Self::None => {
}
}
Ok(())
}
}
#[serde_api]
#[derive(Default, Deserialize, Serialize, Debug, Clone, PartialEq, StateMethods, SetCumulative)]
#[cfg_attr(feature = "pyo3", pyclass(module = "fastsim", subclass, eq))]
#[serde(deny_unknown_fields)]
pub struct RESLumpedThermal {
pub heat_capacitance: si::HeatCapacity,
pub conductance_to_amb: si::ThermalConductance,
pub conductance_to_cab: si::ThermalConductance,
#[serde(default)]
pub state: RESLumpedThermalState,
#[serde(default)]
pub history: RESLumpedThermalStateHistoryVec,
pub save_interval: Option<usize>,
}
#[pyo3_api]
impl RESLumpedThermal {
#[staticmethod]
#[pyo3(name = "default")]
fn default_py() -> Self {
Default::default()
}
}
impl SerdeAPI for RESLumpedThermal {}
impl Init for RESLumpedThermal {}
impl HistoryMethods for RESLumpedThermal {
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()
}
}
impl RESLumpedThermal {
fn solve(
&mut self,
res_state: &mut ReversibleEnergyStorageState,
te_amb: si::Temperature,
pwr_thrml_hvac_to_res: si::Power,
te_cab: si::Temperature,
dt: si::Time,
) -> anyhow::Result<()> {
self.state.pwr_thrml_from_cabin.update(
self.conductance_to_cab
* (te_cab.get::<si::degree_celsius>()
- self
.state
.temperature
.get_stale(|| format_dbg!())?
.get::<si::degree_celsius>())
* uc::KELVIN_INT,
|| format_dbg!(),
)?;
self.state
.pwr_thrml_hvac_to_res
.update(pwr_thrml_hvac_to_res, || format_dbg!())?;
self.state.pwr_thrml_from_amb.update(
self.conductance_to_amb
* (te_amb.get::<si::degree_celsius>()
- self
.state
.temperature
.get_stale(|| format_dbg!())?
.get::<si::degree_celsius>())
* uc::KELVIN_INT,
|| format_dbg!(),
)?;
self.state.pwr_thrml_loss.update(
res_state
.pwr_out_electrical
.get_stale(|| format_dbg!())?
.abs()
* (1.0 * uc::R - *res_state.eff.get_stale(|| format_dbg!())?),
|| format_dbg!(),
)?;
self.state.temp_prev.update(
*self.state.temperature.get_stale(|| format_dbg!())?,
|| format_dbg!(),
)?;
self.state.temperature.update(
*self.state.temperature.get_stale(|| format_dbg!())?
+ (*self
.state
.pwr_thrml_hvac_to_res
.get_fresh(|| format_dbg!())?
+ *self.state.pwr_thrml_loss.get_fresh(|| format_dbg!())?
+ *self
.state
.pwr_thrml_from_cabin
.get_fresh(|| format_dbg!())?
+ *self.state.pwr_thrml_from_amb.get_fresh(|| format_dbg!())?)
/ self.heat_capacitance
* dt,
|| format_dbg!(),
)?;
Ok(())
}
}
#[serde_api]
#[derive(
Clone, Debug, Deserialize, Serialize, PartialEq, HistoryVec, StateMethods, SetCumulative,
)]
#[cfg_attr(feature = "pyo3", pyclass(module = "fastsim", subclass, eq))]
#[serde(deny_unknown_fields)]
pub struct RESLumpedThermalState {
pub i: TrackedState<usize>,
pub temperature: TrackedState<si::Temperature>,
pub temp_prev: TrackedState<si::Temperature>,
pub pwr_thrml_from_cabin: TrackedState<si::Power>,
pub energy_thrml_from_cabin: TrackedState<si::Energy>,
pub pwr_thrml_from_amb: TrackedState<si::Power>,
pub energy_thrml_from_amb: TrackedState<si::Energy>,
pub pwr_thrml_hvac_to_res: TrackedState<si::Power>,
pub energy_thrml_hvac_to_res: TrackedState<si::Energy>,
pub pwr_thrml_loss: TrackedState<si::Power>,
pub energy_thrml_loss: TrackedState<si::Energy>,
}
#[pyo3_api]
impl RESLumpedThermalState {
#[pyo3(name = "default")]
#[staticmethod]
fn default_py() -> Self {
Self::default()
}
}
impl Init for RESLumpedThermalState {}
impl SerdeAPI for RESLumpedThermalState {}
impl Default for RESLumpedThermalState {
fn default() -> Self {
Self {
i: Default::default(),
temperature: TrackedState::new(*TE_STD_AIR),
temp_prev: TrackedState::new(*TE_STD_AIR),
pwr_thrml_from_cabin: Default::default(),
energy_thrml_from_cabin: Default::default(),
pwr_thrml_from_amb: Default::default(),
energy_thrml_from_amb: Default::default(),
pwr_thrml_hvac_to_res: Default::default(),
energy_thrml_hvac_to_res: Default::default(),
pwr_thrml_loss: Default::default(),
energy_thrml_loss: Default::default(),
}
}
}
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, IsVariant, TryInto)]
pub enum EffInterp {
Constant(Interp0D<f64>),
CRate(Interp1DOwned<f64, strategy::enums::Strategy1DEnum>),
CRateSOCTemperature(Interp3DOwned<f64, strategy::enums::Strategy3DEnum>),
CRateTemperature(Interp2DOwned<f64, strategy::enums::Strategy2DEnum>),
CRateSOC(Interp2DOwned<f64, strategy::enums::Strategy2DEnum>),
}
impl Interpolator<f64> for EffInterp {
fn ndim(&self) -> usize {
match self {
EffInterp::Constant(interp) => interp.ndim(),
EffInterp::CRate(interp) => interp.ndim(),
EffInterp::CRateSOC(interp) => interp.ndim(),
EffInterp::CRateTemperature(interp) => interp.ndim(),
EffInterp::CRateSOCTemperature(interp) => interp.ndim(),
}
}
fn validate(&mut self) -> Result<(), ninterp::error::ValidateError> {
match self {
EffInterp::Constant(interp) => interp.validate(),
EffInterp::CRate(interp) => interp.validate(),
EffInterp::CRateSOC(interp) => interp.validate(),
EffInterp::CRateTemperature(interp) => interp.validate(),
EffInterp::CRateSOCTemperature(interp) => interp.validate(),
}
}
fn interpolate(&self, point: &[f64]) -> Result<f64, ninterp::error::InterpolateError> {
match self {
EffInterp::Constant(interp) => interp.interpolate(point),
EffInterp::CRate(interp) => interp.interpolate(point),
EffInterp::CRateSOC(interp) => interp.interpolate(point),
EffInterp::CRateTemperature(interp) => interp.interpolate(point),
EffInterp::CRateSOCTemperature(interp) => interp.interpolate(point),
}
}
fn set_extrapolate(
&mut self,
extrapolate: Extrapolate<f64>,
) -> Result<(), ninterp::error::ValidateError> {
match self {
EffInterp::Constant(interp) => interp.set_extrapolate(extrapolate),
EffInterp::CRate(interp) => interp.set_extrapolate(extrapolate),
EffInterp::CRateSOC(interp) => interp.set_extrapolate(extrapolate),
EffInterp::CRateTemperature(interp) => interp.set_extrapolate(extrapolate),
EffInterp::CRateSOCTemperature(interp) => interp.set_extrapolate(extrapolate),
}
}
}
impl Min<f64> for EffInterp {
fn min(&self) -> anyhow::Result<&f64> {
match self {
EffInterp::Constant(interp0d) => interp0d.min(),
EffInterp::CRate(interp1d) => interp1d.min(),
EffInterp::CRateSOC(interp2d) => interp2d.min(),
EffInterp::CRateTemperature(interp2d) => interp2d.min(),
EffInterp::CRateSOCTemperature(interp3d) => interp3d.min(),
}
}
}
impl Max<f64> for EffInterp {
fn max(&self) -> anyhow::Result<&f64> {
match self {
EffInterp::Constant(interp0d) => interp0d.max(),
EffInterp::CRate(interp1d) => interp1d.max(),
EffInterp::CRateSOC(interp2d) => interp2d.max(),
EffInterp::CRateTemperature(interp2d) => interp2d.max(),
EffInterp::CRateSOCTemperature(interp3d) => interp3d.max(),
}
}
}
impl Range<f64> for EffInterp {
fn range(&self) -> anyhow::Result<f64> {
match self {
EffInterp::Constant(interp0d) => interp0d.range(),
EffInterp::CRate(interp1d) => interp1d.range(),
EffInterp::CRateSOC(interp2d) => interp2d.range(),
EffInterp::CRateTemperature(interp2d) => interp2d.range(),
EffInterp::CRateSOCTemperature(interp3d) => interp3d.range(),
}
}
}
impl InterpolatorMutMethods for EffInterp {
fn set_min(&mut self, min: f64, scaling: Option<ScalingMethods>) -> anyhow::Result<()> {
match self {
EffInterp::Constant(interp0d) => interp0d.set_min(min, scaling),
EffInterp::CRate(interp1d) => interp1d.set_min(min, scaling),
EffInterp::CRateSOC(interp2d) => interp2d.set_min(min, scaling),
EffInterp::CRateTemperature(interp2d) => interp2d.set_min(min, scaling),
EffInterp::CRateSOCTemperature(interp3d) => interp3d.set_min(min, scaling),
}
}
fn set_max(&mut self, max: f64, scaling: Option<ScalingMethods>) -> anyhow::Result<()> {
match self {
EffInterp::Constant(interp0d) => interp0d.set_max(max, scaling),
EffInterp::CRate(interp1d) => interp1d.set_max(max, scaling),
EffInterp::CRateSOC(interp2d) => interp2d.set_max(max, scaling),
EffInterp::CRateTemperature(interp2d) => interp2d.set_max(max, scaling),
EffInterp::CRateSOCTemperature(interp3d) => interp3d.set_max(max, scaling),
}
}
fn set_range(&mut self, range: f64) -> anyhow::Result<()> {
match self {
EffInterp::Constant(interp0d) => interp0d.set_range(range),
EffInterp::CRate(interp1d) => interp1d.set_range(range),
EffInterp::CRateSOC(interp2d) => interp2d.set_range(range),
EffInterp::CRateTemperature(interp2d) => interp2d.set_range(range),
EffInterp::CRateSOCTemperature(interp3d) => interp3d.set_range(range),
}
}
}