fastsim_core/vehicle/
cabin.rs

1use super::*;
2// TODO: add parameters and/or cabin model variant for solar heat load
3
4/// Options for handling cabin thermal model
5#[derive(
6    Clone,
7    Default,
8    Debug,
9    Serialize,
10    Deserialize,
11    PartialEq,
12    IsVariant,
13    derive_more::From,
14    TryInto,
15    derive_more::Display,
16)]
17pub enum CabinOption {
18    /// Basic single thermal capacitance cabin thermal model, including HVAC
19    /// system and controls
20    #[display("LumpedCabin")]
21    LumpedCabin(Box<LumpedCabin>),
22    /// Cabin with interior and shell capacitances
23    #[display("LumpedCabinWithShell")]
24    LumpedCabinWithShell,
25    /// no cabin thermal model
26    #[default]
27    #[display("None")]
28    None,
29}
30
31impl StateMethods for CabinOption {}
32
33impl SaveState for CabinOption {
34    fn save_state<F: Fn() -> String>(&mut self, loc: F) -> anyhow::Result<()> {
35        match self {
36            Self::LumpedCabin(lc) => lc.save_state(loc)?,
37            Self::LumpedCabinWithShell => {
38                todo!()
39            }
40            Self::None => {}
41        }
42        Ok(())
43    }
44}
45impl TrackedStateMethods for CabinOption {
46    fn check_and_reset<F: Fn() -> String>(&mut self, loc: F) -> anyhow::Result<()> {
47        match self {
48            Self::LumpedCabin(lc) => {
49                lc.check_and_reset(|| format!("{}\n{}", loc(), format_dbg!()))?
50            }
51            Self::LumpedCabinWithShell => {
52                todo!()
53            }
54            Self::None => {}
55        }
56        Ok(())
57    }
58
59    fn mark_fresh<F: Fn() -> String>(&mut self, loc: F) -> anyhow::Result<()> {
60        match self {
61            Self::LumpedCabin(lc) => lc.mark_fresh(|| format!("{}\n{}", loc(), format_dbg!()))?,
62            Self::LumpedCabinWithShell => {
63                todo!()
64            }
65            Self::None => {}
66        }
67        Ok(())
68    }
69}
70impl Step for CabinOption {
71    fn step<F: Fn() -> String>(&mut self, loc: F) -> anyhow::Result<()> {
72        match self {
73            Self::LumpedCabin(lc) => lc.step(|| format!("{}\n{}", loc(), format_dbg!())),
74            Self::LumpedCabinWithShell => {
75                todo!()
76            }
77            Self::None => Ok(()),
78        }
79    }
80
81    fn reset_step<F: Fn() -> String>(&mut self, loc: F) -> anyhow::Result<()> {
82        match self {
83            Self::LumpedCabin(lc) => lc.reset_step(|| format!("{}\n{}", loc(), format_dbg!())),
84            Self::LumpedCabinWithShell => {
85                todo!()
86            }
87            Self::None => Ok(()),
88        }
89    }
90}
91impl Init for CabinOption {
92    fn init(&mut self) -> Result<(), Error> {
93        match self {
94            Self::LumpedCabin(scc) => scc.init()?,
95            Self::LumpedCabinWithShell => {
96                todo!()
97            }
98            Self::None => {}
99        }
100        Ok(())
101    }
102}
103impl SerdeAPI for CabinOption {}
104impl HistoryMethods for CabinOption {
105    fn save_interval(&self) -> anyhow::Result<Option<usize>> {
106        match self {
107            CabinOption::LumpedCabin(lc) => lc.save_interval(),
108            CabinOption::LumpedCabinWithShell => todo!(),
109            CabinOption::None => Ok(None),
110        }
111    }
112    fn set_save_interval(&mut self, save_interval: Option<usize>) -> anyhow::Result<()> {
113        match self {
114            CabinOption::LumpedCabin(lc) => lc.set_save_interval(save_interval),
115            CabinOption::LumpedCabinWithShell => todo!(),
116            CabinOption::None => Ok(()),
117        }
118    }
119    fn clear(&mut self) {
120        match self {
121            CabinOption::LumpedCabin(lc) => lc.clear(),
122            CabinOption::LumpedCabinWithShell => todo!(),
123            CabinOption::None => {}
124        }
125    }
126}
127impl SetCumulative for CabinOption {
128    fn set_cumulative<F: Fn() -> String>(&mut self, dt: si::Time, loc: F) -> anyhow::Result<()> {
129        match self {
130            Self::LumpedCabin(lc) => {
131                lc.set_cumulative(dt, || format!("{}\n{}", loc(), format_dbg!()))?
132            }
133            Self::LumpedCabinWithShell => todo!(),
134            Self::None => {}
135        }
136        Ok(())
137    }
138
139    fn reset_cumulative<F: Fn() -> String>(&mut self, loc: F) -> anyhow::Result<()> {
140        match self {
141            Self::LumpedCabin(lc) => {
142                lc.reset_cumulative(|| format!("{}\n{}", loc(), format_dbg!()))?
143            }
144            Self::LumpedCabinWithShell => todo!(),
145            Self::None => {}
146        }
147        Ok(())
148    }
149}
150
151#[serde_api]
152#[derive(Default, Deserialize, Serialize, Debug, Clone, PartialEq, StateMethods, SetCumulative)]
153#[non_exhaustive]
154#[serde(deny_unknown_fields)]
155#[cfg_attr(feature = "pyo3", pyclass(module = "fastsim", subclass, eq))]
156/// Basic single thermal capacitance cabin thermal model, including HVAC
157/// system and controls
158pub struct LumpedCabin {
159    /// Inverse of cabin shell thermal resistance
160    pub cab_shell_htc_to_amb: si::HeatTransferCoeff,
161    /// parameter for heat transfer coeff from cabin outer surface to ambient
162    /// during vehicle stop
163    pub cab_htc_to_amb_stop: si::HeatTransferCoeff,
164    /// cabin thermal capacitance
165    pub heat_capacitance: si::HeatCapacity,
166    /// cabin length, modeled as a flat plate
167    pub length: si::Length,
168    /// cabin width, modeled as a flat plate
169    pub width: si::Length,
170    #[serde(default)]
171    pub state: LumpedCabinState,
172    #[serde(default)]
173    pub history: LumpedCabinStateHistoryVec,
174    /// Time step interval at which history is saved
175    pub save_interval: Option<usize>,
176}
177
178#[pyo3_api]
179impl LumpedCabin {
180    #[staticmethod]
181    #[pyo3(name = "default")]
182    fn default_py() -> Self {
183        Default::default()
184    }
185}
186impl SerdeAPI for LumpedCabin {}
187impl Init for LumpedCabin {}
188impl HistoryMethods for LumpedCabin {
189    fn save_interval(&self) -> anyhow::Result<Option<usize>> {
190        Ok(self.save_interval)
191    }
192    fn set_save_interval(&mut self, save_interval: Option<usize>) -> anyhow::Result<()> {
193        self.save_interval = save_interval;
194        Ok(())
195    }
196    fn clear(&mut self) {
197        self.history.clear();
198    }
199}
200
201impl LumpedCabin {
202    /// Solve temperatures, HVAC powers, and cumulative energies of cabin and HVAC system
203    /// Arguments:
204    /// - `te_amb_air`: ambient air temperature
205    /// - `veh_state`: current [VehicleState]
206    /// - 'pwr_thrml_from_hvac`: power to cabin from [Vehicle::hvac] system
207    /// - `dt`: simulation time step size
208    /// # Returns
209    /// - `te_cab`: current cabin temperature, after solving cabin for current
210    ///   simulation time step
211    pub fn solve(
212        &mut self,
213        te_amb_air: si::Temperature,
214        veh_state: &VehicleState,
215        pwr_thrml_from_hvac: si::Power,
216        pwr_thrml_to_res: si::Power,
217        dt: si::Time,
218    ) -> anyhow::Result<si::Temperature> {
219        self.state
220            .pwr_thrml_from_hvac
221            .update(pwr_thrml_from_hvac, || format_dbg!())?;
222        self.state
223            .pwr_thrml_to_res
224            .update(pwr_thrml_to_res, || format_dbg!())?;
225        let cab_te_film_ext: si::Temperature = 0.5
226            * (self
227                .state
228                .temperature
229                .get_stale(|| format_dbg!())?
230                .get::<si::kelvin_abs>()
231                + te_amb_air.get::<si::kelvin_abs>())
232            * uc::KELVIN;
233        self.state.reynolds_for_plate.update(
234            Air::get_density(
235                Some(cab_te_film_ext),
236                Some(*veh_state.elev_curr.get_stale(|| format_dbg!())?),
237            ) * *veh_state.speed_ach.get_stale(|| format_dbg!())?
238                * self.length
239                / Air::get_dyn_visc(cab_te_film_ext).with_context(|| format_dbg!())?,
240            || format_dbg!(),
241        )?;
242        let re_l_crit = 5.0e5 * uc::R; // critical Re for transition to turbulence
243
244        let nu_l_bar: si::Ratio =
245            if *self.state.reynolds_for_plate.get_fresh(|| format_dbg!())? < re_l_crit {
246                // flat plate model for isothermal, mixed-flow from Incropera and deWitt, Fundamentals of Heat and Mass
247                // Transfer, 7th Edition
248                // equation 7.30
249                0.664
250                    * self
251                        .state
252                        .reynolds_for_plate
253                        .get_fresh(|| format_dbg!())?
254                        .get::<si::ratio>()
255                        .powf(0.5)
256                    * Air::get_pr(cab_te_film_ext)
257                        .with_context(|| format_dbg!())?
258                        .get::<si::ratio>()
259                        .powf(1.0 / 3.0)
260                    * uc::R
261            } else {
262                // equation 7.38
263                let a = 871.0; // equation 7.39
264                (0.037
265                    * self
266                        .state
267                        .reynolds_for_plate
268                        .get_fresh(|| format_dbg!())?
269                        .get::<si::ratio>()
270                        .powf(0.8)
271                    - a)
272                    * Air::get_pr(cab_te_film_ext).with_context(|| format_dbg!())?
273            };
274
275        self.state.pwr_thrml_from_amb.update(
276            if *veh_state.speed_ach.get_stale(|| format_dbg!())? > 2.0 * uc::MPH {
277                let htc_overall_moving: si::HeatTransferCoeff = 1.0
278                    / (1.0
279                        / (nu_l_bar
280                            * Air::get_therm_cond(cab_te_film_ext)
281                                .with_context(|| format_dbg!())?
282                            / self.length)
283                        + 1.0 / self.cab_shell_htc_to_amb);
284                (self.length * self.width)
285                    * htc_overall_moving
286                    * (te_amb_air.get::<si::degree_celsius>()
287                        - self
288                            .state
289                            .temperature
290                            .get_stale(|| format_dbg!())?
291                            .get::<si::degree_celsius>())
292                    * uc::KELVIN_INT
293            } else {
294                (self.length * self.width)
295                    / (1.0 / self.cab_htc_to_amb_stop + 1.0 / self.cab_shell_htc_to_amb)
296                    * (te_amb_air.get::<si::degree_celsius>()
297                        - self
298                            .state
299                            .temperature
300                            .get_stale(|| format_dbg!())?
301                            .get::<si::degree_celsius>())
302                    * uc::KELVIN_INT
303            },
304            || format_dbg!(),
305        )?;
306
307        self.state.temp_prev.update(
308            *self.state.temperature.get_stale(|| format_dbg!())?,
309            || format_dbg!(),
310        )?;
311        self.state.temperature.update(
312            *self.state.temperature.get_stale(|| format_dbg!())?
313                + (*self.state.pwr_thrml_from_hvac.get_fresh(|| format_dbg!())?
314                    + *self.state.pwr_thrml_from_amb.get_fresh(|| format_dbg!())?
315                    - *self.state.pwr_thrml_to_res.get_fresh(|| format_dbg!())?)
316                    / self.heat_capacitance
317                    * dt,
318            || format_dbg!(),
319        )?;
320        Ok(*self.state.temperature.get_fresh(|| format_dbg!())?)
321    }
322}
323
324#[serde_api]
325#[derive(
326    Clone, Debug, Deserialize, Serialize, PartialEq, HistoryVec, StateMethods, SetCumulative,
327)]
328#[cfg_attr(feature = "pyo3", pyclass(module = "fastsim", subclass, eq))]
329#[serde(deny_unknown_fields)]
330pub struct LumpedCabinState {
331    /// time step counter
332    pub i: TrackedState<usize>,
333    /// lumped cabin temperature
334    pub temperature: TrackedState<si::Temperature>,
335    /// lumped cabin temperature at start of previous time step
336    pub temp_prev: TrackedState<si::Temperature>,
337    /// Thermal power coming to cabin from [Vehicle::hvac] system.  Positive indicates
338    /// heating, and negative indicates cooling.
339    pub pwr_thrml_from_hvac: TrackedState<si::Power>,
340    /// Cumulative thermal energy coming to cabin from [Vehicle::hvac] system.
341    /// Positive indicates heating, and negative indicates cooling.
342    pub energy_thrml_from_hvac: TrackedState<si::Energy>,
343    /// Thermal power coming to cabin from ambient air.  Positive indicates
344    /// heating, and negative indicates cooling.
345    pub pwr_thrml_from_amb: TrackedState<si::Power>,
346    /// Cumulative thermal energy coming to cabin from ambient air.  Positive indicates
347    /// heating, and negative indicates cooling.
348    pub energy_thrml_from_amb: TrackedState<si::Energy>,
349    /// Thermal power flowing from [Cabin] to [ReversibleEnergyStorage] (zero if
350    /// not equipped) due to temperature delta
351    pub pwr_thrml_to_res: TrackedState<si::Power>,
352    /// Cumulative thermal energy flowing from [Cabin] to
353    /// [ReversibleEnergyStorage] due to temperature delta
354    pub energy_thrml_to_res: TrackedState<si::Energy>,
355    /// Reynolds number for flow over cabin, treating cabin as a flat plate
356    pub reynolds_for_plate: TrackedState<si::Ratio>,
357}
358
359#[pyo3_api]
360impl LumpedCabinState {
361    #[pyo3(name = "default")]
362    #[staticmethod]
363    fn default_py() -> Self {
364        Self::default()
365    }
366}
367
368impl Default for LumpedCabinState {
369    fn default() -> Self {
370        Self {
371            i: Default::default(),
372            temperature: TrackedState::new(*TE_STD_AIR),
373            temp_prev: TrackedState::new(*TE_STD_AIR),
374            pwr_thrml_from_hvac: Default::default(),
375            energy_thrml_from_hvac: Default::default(),
376            pwr_thrml_from_amb: Default::default(),
377            energy_thrml_from_amb: Default::default(),
378            pwr_thrml_to_res: Default::default(),
379            energy_thrml_to_res: Default::default(),
380            reynolds_for_plate: Default::default(),
381        }
382    }
383}
384impl Init for LumpedCabinState {}
385impl SerdeAPI for LumpedCabinState {}