fastsim_core/vehicle/powertrain/
fuel_storage.rs

1use super::*;
2
3#[serde_api]
4#[derive(Deserialize, Serialize, Debug, Clone, PartialEq, SetCumulative)]
5#[cfg_attr(feature = "pyo3", pyclass(module = "fastsim", subclass, eq))]
6#[non_exhaustive]
7#[serde(deny_unknown_fields)]
8pub struct FuelStorage {
9    /// max power output
10    pub pwr_out_max: si::Power,
11    /// time to peak power
12    pub pwr_ramp_lag: si::Time,
13    /// energy capacity
14    pub energy_capacity: si::Energy,
15    /// Fuel and tank specific energy
16    pub(in super::super) specific_energy: Option<si::SpecificEnergy>,
17    /// Mass of fuel storage
18    #[serde(default)]
19    pub(in super::super) mass: Option<si::Mass>,
20    // TODO: add state to track fuel level and make sure mass changes propagate up to vehicle level,
21    // which should then include vehicle mass in state
22}
23
24#[pyo3_api]
25impl FuelStorage {
26    // TODO: decide on way to deal with `side_effect` coming after optional arg and uncomment
27    // #[setter("__mass_kg")]
28    // fn set_mass_py(&mut self, mass_kg: Option<f64>) -> anyhow::Result<()> {
29    //     self.set_mass(mass_kg.map(|m| m * uc::KG))?;
30    //     Ok(())
31    // }
32
33    // #[getter("mass_kg")]
34    // fn get_mass_py(&self) -> PyResult<Option<f64>> {
35    //     Ok(self.mass()?.map(|m| m.get::<si::kilogram>()))
36    // }
37}
38
39impl SerdeAPI for FuelStorage {}
40impl Init for FuelStorage {}
41
42impl Mass for FuelStorage {
43    fn mass(&self) -> anyhow::Result<Option<si::Mass>> {
44        let derived_mass = self
45            .derived_mass()
46            .with_context(|| anyhow!(format_dbg!()))?;
47        if let (Some(derived_mass), Some(set_mass)) = (derived_mass, self.mass) {
48            ensure!(
49                utils::almost_eq_uom(&set_mass, &derived_mass, None),
50                format!(
51                    "{}",
52                    format_dbg!(utils::almost_eq_uom(&set_mass, &derived_mass, None)),
53                )
54            );
55        }
56        Ok(self.mass)
57    }
58
59    fn set_mass(
60        &mut self,
61        new_mass: Option<si::Mass>,
62        side_effect: MassSideEffect,
63    ) -> anyhow::Result<()> {
64        let derived_mass = self
65            .derived_mass()
66            .with_context(|| anyhow!(format_dbg!()))?;
67        if let (Some(derived_mass), Some(new_mass)) = (derived_mass, new_mass) {
68            if derived_mass != new_mass {
69                match side_effect {
70                    MassSideEffect::Extensive => {
71                        self.energy_capacity = self.specific_energy.with_context(|| {
72                            format!(
73                                "{}\nExpected `self.specific_energy` to be `Some`.",
74                                format_dbg!()
75                            )
76                        })? * new_mass;
77                    }
78                    MassSideEffect::Intensive => {
79                        self.specific_energy = Some(self.energy_capacity / new_mass);
80                    }
81                    MassSideEffect::None => {
82                        self.specific_energy = None;
83                    }
84                }
85            }
86        } else if new_mass.is_none() {
87            self.specific_energy = None;
88        }
89        self.mass = new_mass;
90
91        Ok(())
92    }
93
94    fn derived_mass(&self) -> anyhow::Result<Option<si::Mass>> {
95        Ok(self
96            .specific_energy
97            .map(|specific_energy| self.energy_capacity / specific_energy))
98    }
99
100    fn expunge_mass_fields(&mut self) {
101        self.mass = None;
102        self.specific_energy = None;
103    }
104}