Skip to main content

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