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_upstream: (si::Power, si::Power),
59        pwr_aux: si::Power,
60        dt: si::Time,
61        _veh_state: &VehicleState,
62    ) -> anyhow::Result<()> {
63        // TODO: account for transmission efficiency in here
64        self.fc
65            .set_curr_pwr_out_max(dt)
66            .with_context(|| anyhow!(format_dbg!()))?;
67        self.fc
68            .set_curr_pwr_prop_max(pwr_aux / self.alt_eff)
69            .with_context(|| anyhow!(format_dbg!()))?;
70        self.transmission
71            .set_curr_pwr_prop_out_max(
72                (
73                    *self.fc.state.pwr_prop_max.get_fresh(|| format_dbg!())?,
74                    si::Power::ZERO,
75                ),
76                f64::NAN * uc::W,
77                dt,
78                _veh_state,
79            )
80            .with_context(|| format_dbg!())?;
81        Ok(())
82    }
83
84    fn get_curr_pwr_prop_out_max(&self) -> anyhow::Result<(si::Power, si::Power)> {
85        self.transmission
86            .get_curr_pwr_prop_out_max()
87            .with_context(|| format_dbg!())
88    }
89
90    fn solve(
91        &mut self,
92        pwr_out_req: si::Power,
93        _enabled: bool,
94        dt: si::Time,
95    ) -> anyhow::Result<Option<si::Power>> {
96        // NOTE: think about the possibility of engine braking, not urgent
97        ensure!(pwr_out_req >= si::Power::ZERO, format_dbg!());
98        ensure!(almost_le_uom(
99            &pwr_out_req,
100            self.transmission
101                .state
102                .pwr_out_fwd_max
103                .get_fresh(|| format_dbg!())?,
104            None
105        ));
106        ensure!(almost_le_uom(
107            &pwr_out_req,
108            self.transmission
109                .state
110                .pwr_out_fwd_max
111                .get_fresh(|| format_dbg!())?,
112            None
113        ));
114        let enabled = true; // TODO: replace with a stop/start model
115        let pwr_in_transmission = self
116            .transmission
117            .solve(pwr_out_req, true, dt)
118            .with_context(|| format_dbg!())?
119            .with_context(|| format!("{}\nExpected `Some`", format_dbg!()))?;
120        self.fc
121            .solve(pwr_in_transmission, enabled, dt)
122            .with_context(|| anyhow!(format_dbg!()))?;
123        Ok(None)
124    }
125
126    fn pwr_regen(&self) -> anyhow::Result<si::Power> {
127        Ok(si::Power::ZERO)
128    }
129}
130
131impl ConventionalVehicle {
132    pub fn solve_thermal(
133        &mut self,
134        te_amb: si::Temperature,
135        pwr_thrml_fc_to_cab: Option<si::Power>,
136        veh_state: &mut VehicleState,
137        dt: si::Time,
138    ) -> anyhow::Result<()> {
139        self.fc
140            .solve_thermal(te_amb, pwr_thrml_fc_to_cab, veh_state, dt)
141    }
142}
143
144impl TryFrom<&fastsim_2::vehicle::RustVehicle> for ConventionalVehicle {
145    type Error = anyhow::Error;
146    fn try_from(f2veh: &fastsim_2::vehicle::RustVehicle) -> anyhow::Result<ConventionalVehicle> {
147        let conv = ConventionalVehicle {
148            fs: {
149                let mut fs = FuelStorage {
150                    pwr_out_max: f2veh.fs_max_kw * uc::KW,
151                    pwr_ramp_lag: f2veh.fs_secs_to_peak_pwr * uc::S,
152                    energy_capacity: f2veh.fs_kwh * uc::KWH,
153                    specific_energy: Some(
154                        super::vehicle_model::FUEL_LHV_MJ_PER_KG * uc::MJ / uc::KG,
155                    ),
156                    mass: None,
157                };
158                fs.set_mass(None, MassSideEffect::None)
159                    .with_context(|| anyhow!(format_dbg!()))?;
160                fs
161            },
162            fc: FuelConverter::try_from(f2veh.clone())?,
163            transmission: Transmission::try_from(f2veh.clone())?,
164            mass: None,
165            alt_eff: f2veh.alt_eff * uc::R,
166        };
167        Ok(conv)
168    }
169}
170
171impl Mass for ConventionalVehicle {
172    fn mass(&self) -> anyhow::Result<Option<si::Mass>> {
173        let derived_mass = self
174            .derived_mass()
175            .with_context(|| anyhow!(format_dbg!()))?;
176        match (derived_mass, self.mass) {
177            (Some(derived_mass), Some(set_mass)) => {
178                ensure!(
179                    utils::almost_eq_uom(&set_mass, &derived_mass, None),
180                    format!(
181                        "{}",
182                        format_dbg!(utils::almost_eq_uom(&set_mass, &derived_mass, None)),
183                    )
184                );
185                Ok(Some(set_mass))
186            }
187            _ => Ok(self.mass.or(derived_mass)),
188        }
189    }
190
191    fn set_mass(
192        &mut self,
193        new_mass: Option<si::Mass>,
194        side_effect: MassSideEffect,
195    ) -> anyhow::Result<()> {
196        ensure!(
197            side_effect == MassSideEffect::None,
198            "At the powertrain level, only `MassSideEffect::None` is allowed"
199        );
200        let derived_mass = self
201            .derived_mass()
202            .with_context(|| anyhow!(format_dbg!()))?;
203        self.mass = match new_mass {
204            // Set using provided `new_mass`, setting constituent mass fields to `None` to match if inconsistent
205            Some(new_mass) => {
206                if let Some(dm) = derived_mass {
207                    if dm != new_mass {
208                        self.expunge_mass_fields();
209                    }
210                }
211                Some(new_mass)
212            }
213            // Set using `derived_mass()`, failing if it returns `None`
214            None => Some(derived_mass.with_context(|| {
215                format!(
216                    "Not all mass fields in `{}` are set and no mass was provided.",
217                    stringify!(ConventionalVehicle)
218                )
219            })?),
220        };
221        Ok(())
222    }
223
224    fn derived_mass(&self) -> anyhow::Result<Option<si::Mass>> {
225        let fc_mass = self.fc.mass().with_context(|| anyhow!(format_dbg!()))?;
226        let fs_mass = self.fs.mass().with_context(|| anyhow!(format_dbg!()))?;
227        let transmission_mass = self
228            .transmission
229            .mass()
230            .with_context(|| anyhow!(format_dbg!()))?;
231        match (fc_mass, fs_mass, transmission_mass) {
232            (Some(fc_mass), Some(fs_mass), Some(transmission_mass)) => {
233                Ok(Some(fc_mass + fs_mass + transmission_mass))
234            }
235            (None, None, None) => Ok(None),
236            _ => bail!(
237                "`{}` field masses are not consistently set to `Some` or `None`",
238                stringify!(ConventionalVehicle)
239            ),
240        }
241    }
242
243    fn expunge_mass_fields(&mut self) {
244        self.fc.expunge_mass_fields();
245        self.fs.expunge_mass_fields();
246        self.transmission.expunge_mass_fields();
247        self.mass = None;
248    }
249}