fastsim_core/vehicle/
conv.rs

1use super::*;
2
3#[serde_api]
4#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, StateMethods, SetCumulative)]
5#[non_exhaustive]
6#[serde(deny_unknown_fields)]
7#[cfg_attr(feature = "pyo3", pyclass(module = "fastsim", subclass, eq))]
8/// Conventional vehicle with only a FuelConverter as a power source
9pub struct ConventionalVehicle {
10    pub fs: FuelStorage,
11    #[has_state]
12    pub fc: FuelConverter,
13    #[has_state]
14    pub transmission: Transmission,
15    pub(crate) mass: Option<si::Mass>,
16    /// Alternator efficiency used to calculate aux mechanical power demand on engine
17    pub alt_eff: si::Ratio,
18}
19
20#[pyo3_api]
21impl ConventionalVehicle {}
22
23impl SerdeAPI for ConventionalVehicle {}
24impl Init for ConventionalVehicle {
25    fn init(&mut self) -> Result<(), Error> {
26        self.fc
27            .init()
28            .map_err(|err| Error::InitError(format_dbg!(err)))?;
29        self.fs
30            .init()
31            .map_err(|err| Error::InitError(format_dbg!(err)))?;
32        self.transmission
33            .init()
34            .map_err(|err| Error::InitError(format_dbg!(err)))?;
35        Ok(())
36    }
37}
38
39impl HistoryMethods for ConventionalVehicle {
40    fn save_interval(&self) -> anyhow::Result<Option<usize>> {
41        bail!("`save_interval` is not implemented in ConventionalVehicle")
42    }
43    fn set_save_interval(&mut self, save_interval: Option<usize>) -> anyhow::Result<()> {
44        // self.fs.set_save_interval(save_interval)?;
45        self.fc.set_save_interval(save_interval)?;
46        self.transmission.set_save_interval(save_interval)?;
47        Ok(())
48    }
49    fn clear(&mut self) {
50        self.fc.clear();
51        self.transmission.clear();
52    }
53}
54
55impl Powertrain for Box<ConventionalVehicle> {
56    fn set_curr_pwr_prop_out_max(
57        &mut self,
58        pwr_aux: si::Power,
59        dt: si::Time,
60        _veh_state: &VehicleState,
61    ) -> anyhow::Result<()> {
62        // TODO: account for transmission efficiency in here
63        self.fc
64            .set_curr_pwr_out_max(dt)
65            .with_context(|| anyhow!(format_dbg!()))?;
66        self.fc
67            .set_curr_pwr_prop_max(pwr_aux / self.alt_eff)
68            .with_context(|| anyhow!(format_dbg!()))?;
69        Ok(())
70    }
71
72    fn get_curr_pwr_prop_out_max(&self) -> anyhow::Result<(si::Power, si::Power)> {
73        Ok((
74            *self.fc.state.pwr_prop_max.get_fresh(|| format_dbg!())?,
75            si::Power::ZERO,
76        ))
77    }
78
79    fn solve(
80        &mut self,
81        pwr_out_req: si::Power,
82        _enabled: bool,
83        dt: si::Time,
84    ) -> anyhow::Result<()> {
85        // only positive power can come from powertrain.  Revisit this if engine braking model is needed.
86        let pwr_out_req = pwr_out_req.max(si::Power::ZERO);
87        let enabled = true; // TODO: replace with a stop/start model
88        let pwr_in_transmission = self
89            .transmission
90            .get_pwr_in_req(pwr_out_req)
91            .with_context(|| anyhow!(format_dbg!()))?;
92        self.fc
93            .solve(pwr_in_transmission, enabled, dt)
94            .with_context(|| anyhow!(format_dbg!()))?;
95        Ok(())
96    }
97
98    fn pwr_regen(&self) -> anyhow::Result<si::Power> {
99        Ok(si::Power::ZERO)
100    }
101}
102
103impl ConventionalVehicle {
104    pub fn solve_thermal(
105        &mut self,
106        te_amb: si::Temperature,
107        pwr_thrml_fc_to_cab: Option<si::Power>,
108        veh_state: &mut VehicleState,
109        dt: si::Time,
110    ) -> anyhow::Result<()> {
111        self.fc
112            .solve_thermal(te_amb, pwr_thrml_fc_to_cab, veh_state, dt)
113    }
114}
115
116impl Mass for ConventionalVehicle {
117    fn mass(&self) -> anyhow::Result<Option<si::Mass>> {
118        let derived_mass = self
119            .derived_mass()
120            .with_context(|| anyhow!(format_dbg!()))?;
121        match (derived_mass, self.mass) {
122            (Some(derived_mass), Some(set_mass)) => {
123                ensure!(
124                    utils::almost_eq_uom(&set_mass, &derived_mass, None),
125                    format!(
126                        "{}",
127                        format_dbg!(utils::almost_eq_uom(&set_mass, &derived_mass, None)),
128                    )
129                );
130                Ok(Some(set_mass))
131            }
132            _ => Ok(self.mass.or(derived_mass)),
133        }
134    }
135
136    fn set_mass(
137        &mut self,
138        new_mass: Option<si::Mass>,
139        side_effect: MassSideEffect,
140    ) -> anyhow::Result<()> {
141        ensure!(
142            side_effect == MassSideEffect::None,
143            "At the powertrain level, only `MassSideEffect::None` is allowed"
144        );
145        let derived_mass = self
146            .derived_mass()
147            .with_context(|| anyhow!(format_dbg!()))?;
148        self.mass = match new_mass {
149            // Set using provided `new_mass`, setting constituent mass fields to `None` to match if inconsistent
150            Some(new_mass) => {
151                if let Some(dm) = derived_mass {
152                    if dm != new_mass {
153                        self.expunge_mass_fields();
154                    }
155                }
156                Some(new_mass)
157            }
158            // Set using `derived_mass()`, failing if it returns `None`
159            None => Some(derived_mass.with_context(|| {
160                format!(
161                    "Not all mass fields in `{}` are set and no mass was provided.",
162                    stringify!(ConventionalVehicle)
163                )
164            })?),
165        };
166        Ok(())
167    }
168
169    fn derived_mass(&self) -> anyhow::Result<Option<si::Mass>> {
170        let fc_mass = self.fc.mass().with_context(|| anyhow!(format_dbg!()))?;
171        let fs_mass = self.fs.mass().with_context(|| anyhow!(format_dbg!()))?;
172        let transmission_mass = self
173            .transmission
174            .mass()
175            .with_context(|| anyhow!(format_dbg!()))?;
176        match (fc_mass, fs_mass, transmission_mass) {
177            (Some(fc_mass), Some(fs_mass), Some(transmission_mass)) => {
178                Ok(Some(fc_mass + fs_mass + transmission_mass))
179            }
180            (None, None, None) => Ok(None),
181            _ => bail!(
182                "`{}` field masses are not consistently set to `Some` or `None`",
183                stringify!(ConventionalVehicle)
184            ),
185        }
186    }
187
188    fn expunge_mass_fields(&mut self) {
189        self.fc.expunge_mass_fields();
190        self.fs.expunge_mass_fields();
191        self.transmission.expunge_mass_fields();
192        self.mass = None;
193    }
194}