Skip to main content

fastsim_core/vehicle/powertrain/
fuel_storage.rs

1use super::*;
2
3#[derive(Deserialize, Serialize, Debug, Clone, PartialEq)]
4pub enum FuelType {
5    Gasoline,
6    Diesel,
7    Hydrogen,
8}
9
10#[serde_api]
11#[derive(Deserialize, Serialize, Debug, Clone, PartialEq, SetCumulative)]
12#[cfg_attr(feature = "pyo3", pyclass(module = "fastsim", subclass, eq))]
13#[non_exhaustive]
14#[serde(deny_unknown_fields)]
15pub struct FuelStorage {
16    /// max power output
17    pub pwr_out_max: si::Power,
18    /// time to peak power
19    pub pwr_ramp_lag: si::Time,
20    /// fuel type
21    #[serde(default)]
22    pub fuel_type: Option<FuelType>,
23    /// energy capacity
24    pub energy_capacity: si::Energy,
25    /// Fuel and tank specific energy
26    pub(crate) specific_energy: Option<si::SpecificEnergy>,
27    /// Mass of fuel storage
28    #[serde(default)]
29    pub(crate) mass: Option<si::Mass>,
30    // TODO: add state to track fuel level and make sure mass changes propagate up to vehicle level,
31    // which should then include vehicle mass in state
32}
33
34#[pyo3_api]
35impl FuelStorage {
36    // TODO: decide on way to deal with `side_effect` coming after optional arg and uncomment
37    // #[setter("__mass_kg")]
38    // fn set_mass_py(&mut self, mass_kg: Option<f64>) -> anyhow::Result<()> {
39    //     self.set_mass(mass_kg.map(|m| m * uc::KG))?;
40    //     Ok(())
41    // }
42
43    // #[getter("mass_kg")]
44    // fn get_mass_py(&self) -> PyResult<Option<f64>> {
45    //     Ok(self.mass()?.map(|m| m.get::<si::kilogram>()))
46    // }
47}
48
49impl FuelStorage {
50    pub fn new(
51        pwr_out_max: si::Power,
52        pwr_ramp_lag: si::Time,
53        fuel_type: Option<FuelType>,
54        energy_capacity: si::Energy,
55        specific_energy: Option<si::SpecificEnergy>,
56        mass: Option<si::Mass>,
57    ) -> anyhow::Result<Self> {
58        let mut fs = Self {
59            pwr_out_max,
60            pwr_ramp_lag,
61            fuel_type,
62            energy_capacity,
63            specific_energy,
64            mass,
65        };
66        fs.init()?;
67        Ok(fs)
68    }
69}
70
71impl SerdeAPI for FuelStorage {}
72impl Init for FuelStorage {}
73
74impl Mass for FuelStorage {
75    fn mass(&self) -> anyhow::Result<Option<si::Mass>> {
76        let derived_mass = self
77            .derived_mass()
78            .with_context(|| anyhow!(format_dbg!()))?;
79        if let (Some(derived_mass), Some(set_mass)) = (derived_mass, self.mass) {
80            ensure!(
81                utils::almost_eq_uom(&set_mass, &derived_mass, None),
82                format!(
83                    "{}",
84                    format_dbg!(utils::almost_eq_uom(&set_mass, &derived_mass, None)),
85                )
86            );
87        }
88        Ok(self.mass)
89    }
90
91    fn set_mass(
92        &mut self,
93        new_mass: Option<si::Mass>,
94        side_effect: MassSideEffect,
95    ) -> anyhow::Result<()> {
96        let derived_mass = self
97            .derived_mass()
98            .with_context(|| anyhow!(format_dbg!()))?;
99        self.mass = match (new_mass, derived_mass) {
100            // Set using provided `new_mass`, setting constituent mass fields to `None` to match if inconsistent
101            (Some(new_mass), Some(dm)) => {
102                if dm != new_mass {
103                    match side_effect {
104                        MassSideEffect::Extensive => {
105                            self.energy_capacity = self.specific_energy.with_context(|| {
106                                format!(
107                                    "{}\nExpected `self.specific_energy` to be `Some`.",
108                                    format_dbg!()
109                                )
110                            })? * new_mass;
111                        }
112                        MassSideEffect::Intensive => {
113                            self.specific_energy = Some(self.energy_capacity / new_mass);
114                        }
115                        MassSideEffect::None => {
116                            self.specific_energy = None;
117                        }
118                    }
119                }
120                Some(new_mass)
121            }
122            (Some(new_mass), None) => Some(new_mass),
123            (None, Some(dm)) => Some(dm),
124            (None, None) => bail!(
125                "Not all mass fields in `{}` are set and no mass was provided.",
126                stringify!(FuelStorage)
127            ),
128        };
129        ensure!(
130            self.mass > Some(0.0 * uc::KG),
131            "{} mass must be positive",
132            stringify!(FuelStorage)
133        );
134        Ok(())
135    }
136
137    fn derived_mass(&self) -> anyhow::Result<Option<si::Mass>> {
138        Ok(self
139            .specific_energy
140            .map(|specific_energy| self.energy_capacity / specific_energy))
141    }
142
143    fn expunge_mass_fields(&mut self) {
144        self.mass = None;
145        self.specific_energy = None;
146    }
147}