fastsim_core/vehicle/
bev.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/// Battery electric vehicle
9pub struct BatteryElectricVehicle {
10    #[has_state]
11    pub res: ReversibleEnergyStorage,
12    #[has_state]
13    pub em: ElectricMachine,
14    #[has_state]
15    pub transmission: Transmission,
16    pub(crate) mass: Option<si::Mass>,
17}
18
19#[pyo3_api]
20impl BatteryElectricVehicle {}
21
22impl Init for BatteryElectricVehicle {
23    fn init(&mut self) -> Result<(), Error> {
24        self.res
25            .init()
26            .map_err(|err| Error::InitError(format_dbg!(err)))?;
27        self.em
28            .init()
29            .map_err(|err| Error::InitError(format_dbg!(err)))?;
30        self.transmission
31            .init()
32            .map_err(|err| Error::InitError(format_dbg!(err)))?;
33        Ok(())
34    }
35}
36
37impl SerdeAPI for BatteryElectricVehicle {}
38
39impl Mass for BatteryElectricVehicle {
40    fn mass(&self) -> anyhow::Result<Option<si::Mass>> {
41        let derived_mass = self
42            .derived_mass()
43            .with_context(|| anyhow!(format_dbg!()))?;
44        match (derived_mass, self.mass) {
45            (Some(derived_mass), Some(set_mass)) => {
46                ensure!(
47                    utils::almost_eq_uom(&set_mass, &derived_mass, None),
48                    format!(
49                        "{}",
50                        format_dbg!(utils::almost_eq_uom(&set_mass, &derived_mass, None)),
51                    )
52                );
53                Ok(Some(set_mass))
54            }
55            _ => Ok(self.mass.or(derived_mass)),
56        }
57    }
58
59    fn set_mass(
60        &mut self,
61        new_mass: Option<si::Mass>,
62        side_effect: MassSideEffect,
63    ) -> anyhow::Result<()> {
64        ensure!(
65            side_effect == MassSideEffect::None,
66            "At the powertrain level, only `MassSideEffect::None` is allowed"
67        );
68        let derived_mass = self
69            .derived_mass()
70            .with_context(|| anyhow!(format_dbg!()))?;
71        self.mass = match new_mass {
72            // Set using provided `new_mass`, setting constituent mass fields to `None` to match if inconsistent
73            Some(new_mass) => {
74                if let Some(dm) = derived_mass {
75                    if dm != new_mass {
76                        self.expunge_mass_fields();
77                    }
78                }
79                Some(new_mass)
80            }
81            // Set using `derived_mass()`, failing if it returns `None`
82            None => Some(derived_mass.with_context(|| {
83                format!(
84                    "Not all mass fields in `{}` are set and no mass was provided.",
85                    stringify!(BatteryElectricVehicle)
86                )
87            })?),
88        };
89        Ok(())
90    }
91
92    fn derived_mass(&self) -> anyhow::Result<Option<si::Mass>> {
93        let res_mass = self.res.mass().with_context(|| anyhow!(format_dbg!()))?;
94        let em_mass = self.em.mass().with_context(|| anyhow!(format_dbg!()))?;
95        let transmission_mass = self
96            .transmission
97            .mass()
98            .with_context(|| anyhow!(format_dbg!()))?;
99        match (res_mass, em_mass, transmission_mass) {
100            (Some(res_mass), Some(em_mass), Some(transmission_mass)) => {
101                Ok(Some(em_mass + res_mass + transmission_mass))
102            }
103            (None, None, None) => Ok(None),
104            _ => bail!(
105                "`{}` field masses are not consistently set to `Some` or `None`",
106                stringify!(BatteryElectricVehicle)
107            ),
108        }
109    }
110
111    fn expunge_mass_fields(&mut self) {
112        self.res.expunge_mass_fields();
113        self.em.expunge_mass_fields();
114        self.transmission.expunge_mass_fields();
115        self.mass = None;
116    }
117}
118
119impl HistoryMethods for BatteryElectricVehicle {
120    fn save_interval(&self) -> anyhow::Result<Option<usize>> {
121        bail!("`save_interval` is not implemented in BatteryElectricVehicle")
122    }
123    fn set_save_interval(&mut self, save_interval: Option<usize>) -> anyhow::Result<()> {
124        self.res.set_save_interval(save_interval)?;
125        self.em.set_save_interval(save_interval)?;
126        self.transmission.set_save_interval(save_interval)?;
127        Ok(())
128    }
129    fn clear(&mut self) {
130        self.res.clear();
131        self.em.clear();
132        self.transmission.clear();
133    }
134}
135
136impl Powertrain for BatteryElectricVehicle {
137    fn solve(
138        &mut self,
139        pwr_out_req: si::Power,
140        _enabled: bool,
141        dt: si::Time,
142    ) -> anyhow::Result<Option<si::Power>> {
143        let pwr_in_transmission = self
144            .transmission
145            .solve(pwr_out_req, true, dt)
146            .with_context(|| format_dbg!())?
147            .with_context(|| format!("{}\nExpected `Some`", format_dbg!()))?;
148        let pwr_in_em = self
149            .em
150            .solve(pwr_in_transmission, true, dt)
151            .with_context(|| {
152                format!(
153                    "{}\ntransmission `pwr_out_req`: {} kW\n`self.transmission.state.pwr_out_fwd_max`: {} kW",
154                    format_dbg!(),
155                    pwr_out_req.get::<si::kilowatt>().format_eng(None),
156                    self.transmission
157                        .state
158                        .pwr_out_fwd_max
159                        .get_fresh(|| format_dbg!())
160                        .unwrap()
161                        .get::<si::kilowatt>()
162                        .format_eng(None)
163                )
164            })?
165            .with_context(|| format!("{}\nExpected `Some`", format_dbg!()))?;
166        self.res
167            .solve(pwr_in_em, dt)
168            .with_context(|| format_dbg!())?;
169        Ok(None)
170    }
171
172    fn get_curr_pwr_prop_out_max(&self) -> anyhow::Result<(si::Power, si::Power)> {
173        self.transmission
174            .get_curr_pwr_prop_out_max()
175            .with_context(|| format_dbg!())
176    }
177
178    fn set_curr_pwr_prop_out_max(
179        &mut self,
180        _pwr_upstream: (si::Power, si::Power),
181        pwr_aux: si::Power,
182        dt: si::Time,
183        _veh_state: &VehicleState,
184    ) -> anyhow::Result<()> {
185        // TODO: account for transmission efficiency in here
186        let disch_buffer = si::Energy::ZERO;
187        let chrg_buffer = si::Energy::ZERO;
188        self.res
189            .set_curr_pwr_out_max(dt, disch_buffer, chrg_buffer)
190            .with_context(|| anyhow!(format_dbg!()))?;
191        self.res
192            .set_curr_pwr_prop_max(pwr_aux)
193            .with_context(|| anyhow!(format_dbg!()))?;
194        self.em
195            .set_curr_pwr_prop_out_max(
196                self.res
197                    .get_curr_pwr_prop_out_max()
198                    .with_context(|| format_dbg!())?,
199                f64::NAN * uc::W,
200                dt,
201                _veh_state,
202            )
203            .with_context(|| anyhow!(format_dbg!()))?;
204        self.transmission
205            .set_curr_pwr_prop_out_max(
206                self.em
207                    .get_curr_pwr_prop_out_max()
208                    .with_context(|| format_dbg!())?,
209                f64::NAN * uc::W,
210                dt,
211                _veh_state,
212            )
213            .with_context(|| anyhow!(format_dbg!()))?;
214
215        Ok(())
216    }
217
218    /// Regen braking power, positive means braking is happening
219    fn pwr_regen(&self) -> anyhow::Result<si::Power> {
220        // When `pwr_mech_prop_out` is negative, regen is happening.  First, clip it at 0, and then negate it.
221        // see https://play.rust-lang.org/?version=stable&mode=debug&edition=2021&gist=e8f7af5a6e436dd1163fa3c70931d18d
222        // for example
223        self.transmission.pwr_regen().with_context(|| format_dbg!())
224    }
225}
226
227impl BatteryElectricVehicle {
228    /// Solve change in temperature and other thermal effects
229    /// # Arguments
230    /// - `te_amb`: ambient temperature
231    /// - `pwr_thrml_hvac_to_res`: thermal power flowing from [Vehicle::hvac] system to [ReversibleEnergyStorage::thrml]
232    /// - `te_cab`: cabin temperature for heat transfer interaction with [ReversibleEnergyStorage]
233    /// - `dt`: simulation time step size
234    pub fn solve_thermal(
235        &mut self,
236        te_amb: si::Temperature,
237        pwr_thrml_hvac_to_res: si::Power,
238        te_cab: Option<si::Temperature>,
239        dt: si::Time,
240    ) -> anyhow::Result<()> {
241        self.res
242            .solve_thermal(te_amb, pwr_thrml_hvac_to_res, te_cab, dt)
243            .with_context(|| format_dbg!())?;
244        Ok(())
245    }
246}
247
248impl TryFrom<&fastsim_2::vehicle::RustVehicle> for BatteryElectricVehicle {
249    type Error = anyhow::Error;
250    fn try_from(f2veh: &fastsim_2::vehicle::RustVehicle) -> anyhow::Result<BatteryElectricVehicle> {
251        let bev = BatteryElectricVehicle {
252            res: ReversibleEnergyStorage::try_from(f2veh.clone()).with_context(|| format_dbg!())?,
253            em: ElectricMachine {
254                state: Default::default(),
255                eff_interp_achieved: InterpolatorEnum::new_1d(
256                    f2veh.mc_pwr_out_perc.clone(),
257                    f2veh.mc_eff_array.clone(),
258                    strategy::Linear,
259                    Extrapolate::Error,
260                )?,
261                eff_interp_at_max_input: Some(InterpolatorEnum::new_1d(
262                    // before adding the interpolator, pwr_in_frac_interp was set as Default::default(), can this
263                    // be transferred over as done here, or does a new defualt need to be defined?
264                    f2veh
265                        .mc_pwr_out_perc
266                        .iter()
267                        .zip(f2veh.mc_eff_array.iter())
268                        .map(|(x, y)| x / y)
269                        .collect(),
270                    f2veh.mc_eff_array.clone(),
271                    strategy::Linear,
272                    Extrapolate::Error,
273                )?),
274                pwr_out_max: f2veh.mc_max_kw * uc::KW,
275                specific_pwr: None,
276                mass: None,
277                save_interval: Some(1),
278                history: Default::default(),
279            },
280            transmission: Transmission::try_from(f2veh.clone())?,
281            mass: None,
282        };
283        Ok(bev)
284    }
285}