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
140#[serde_api]
141#[derive(Default, Deserialize, Serialize, Debug, Clone, PartialEq, StateMethods, SetCumulative)]
142#[non_exhaustive]
143#[serde(deny_unknown_fields)]
144#[cfg_attr(feature = "pyo3", pyclass(module = "fastsim", subclass, eq))]
145/// Basic single thermal capacitance cabin thermal model, including HVAC
146/// system and controls
147pub struct LumpedCabin {
148    /// Inverse of cabin shell thermal resistance
149    pub cab_shell_htc_to_amb: si::HeatTransferCoeff,
150    /// parameter for heat transfer coeff from cabin outer surface to ambient
151    /// during vehicle stop
152    pub cab_htc_to_amb_stop: si::HeatTransferCoeff,
153    /// cabin thermal capacitance
154    pub heat_capacitance: si::HeatCapacity,
155    /// cabin length, modeled as a flat plate
156    pub length: si::Length,
157    /// cabin width, modeled as a flat plate
158    pub width: si::Length,
159    #[serde(default)]
160    pub state: LumpedCabinState,
161    #[serde(default, skip_serializing_if = "LumpedCabinStateHistoryVec::is_empty")]
162    pub history: LumpedCabinStateHistoryVec,
163    /// Time step interval at which history is saved
164    pub save_interval: Option<usize>,
165}
166
167#[pyo3_api]
168impl LumpedCabin {
169    #[staticmethod]
170    #[pyo3(name = "default")]
171    fn default_py() -> Self {
172        Default::default()
173    }
174}
175impl SerdeAPI for LumpedCabin {}
176impl Init for LumpedCabin {}
177impl HistoryMethods for LumpedCabin {
178    fn save_interval(&self) -> anyhow::Result<Option<usize>> {
179        Ok(self.save_interval)
180    }
181    fn set_save_interval(&mut self, save_interval: Option<usize>) -> anyhow::Result<()> {
182        self.save_interval = save_interval;
183        Ok(())
184    }
185    fn clear(&mut self) {
186        self.history.clear();
187    }
188}
189
190impl LumpedCabin {
191    /// Solve temperatures, HVAC powers, and cumulative energies of cabin and HVAC system
192    /// Arguments:
193    /// - `te_amb_air`: ambient air temperature
194    /// - `veh_state`: current [VehicleState]
195    /// - 'pwr_thrml_from_hvac`: power to cabin from [Vehicle::hvac] system
196    /// - `dt`: simulation time step size
197    /// # Returns
198    /// - `te_cab`: current cabin temperature, after solving cabin for current
199    ///     simulation time step
200    pub fn solve(
201        &mut self,
202        te_amb_air: si::Temperature,
203        veh_state: &VehicleState,
204        pwr_thrml_from_hvac: si::Power,
205        pwr_thrml_to_res: si::Power,
206        dt: si::Time,
207    ) -> anyhow::Result<si::Temperature> {
208        self.state
209            .pwr_thrml_from_hvac
210            .update(pwr_thrml_from_hvac, || format_dbg!())?;
211        self.state
212            .pwr_thrml_to_res
213            .update(pwr_thrml_to_res, || format_dbg!())?;
214        // flat plate model for isothermal, mixed-flow from Incropera and deWitt, Fundamentals of Heat and Mass
215        // Transfer, 7th Edition
216        let cab_te_film_ext: si::Temperature = 0.5
217            * (self
218                .state
219                .temperature
220                .get_stale(|| format_dbg!())?
221                .get::<si::kelvin_abs>()
222                + te_amb_air.get::<si::kelvin_abs>())
223            * uc::KELVIN;
224        self.state.reynolds_for_plate.update(
225            Air::get_density(
226                Some(cab_te_film_ext),
227                Some(*veh_state.elev_curr.get_stale(|| format_dbg!())?),
228            ) * *veh_state.speed_ach.get_stale(|| format_dbg!())?
229                * self.length
230                / Air::get_dyn_visc(cab_te_film_ext).with_context(|| format_dbg!())?,
231            || format_dbg!(),
232        )?;
233        let re_l_crit = 5.0e5 * uc::R; // critical Re for transition to turbulence
234
235        let nu_l_bar: si::Ratio =
236            if *self.state.reynolds_for_plate.get_fresh(|| format_dbg!())? < re_l_crit {
237                // equation 7.30
238                0.664
239                    * self
240                        .state
241                        .reynolds_for_plate
242                        .get_fresh(|| format_dbg!())?
243                        .get::<si::ratio>()
244                        .powf(0.5)
245                    * Air::get_pr(cab_te_film_ext)
246                        .with_context(|| format_dbg!())?
247                        .get::<si::ratio>()
248                        .powf(1.0 / 3.0)
249                    * uc::R
250            } else {
251                // equation 7.38
252                let a = 871.0; // equation 7.39
253                (0.037
254                    * self
255                        .state
256                        .reynolds_for_plate
257                        .get_fresh(|| format_dbg!())?
258                        .get::<si::ratio>()
259                        .powf(0.8)
260                    - a)
261                    * Air::get_pr(cab_te_film_ext).with_context(|| format_dbg!())?
262            };
263
264        self.state.pwr_thrml_from_amb.update(
265            if *veh_state.speed_ach.get_stale(|| format_dbg!())? > 2.0 * uc::MPH {
266                let htc_overall_moving: si::HeatTransferCoeff = 1.0
267                    / (1.0
268                        / (nu_l_bar
269                            * Air::get_therm_cond(cab_te_film_ext)
270                                .with_context(|| format_dbg!())?
271                            / self.length)
272                        + 1.0 / self.cab_shell_htc_to_amb);
273                (self.length * self.width)
274                    * htc_overall_moving
275                    * (te_amb_air.get::<si::degree_celsius>()
276                        - self
277                            .state
278                            .temperature
279                            .get_stale(|| format_dbg!())?
280                            .get::<si::degree_celsius>())
281                    * uc::KELVIN_INT
282            } else {
283                (self.length * self.width)
284                    / (1.0 / self.cab_htc_to_amb_stop + 1.0 / self.cab_shell_htc_to_amb)
285                    * (te_amb_air.get::<si::degree_celsius>()
286                        - self
287                            .state
288                            .temperature
289                            .get_stale(|| format_dbg!())?
290                            .get::<si::degree_celsius>())
291                    * uc::KELVIN_INT
292            },
293            || format_dbg!(),
294        )?;
295
296        self.state.temp_prev.update(
297            *self.state.temperature.get_stale(|| format_dbg!())?,
298            || format_dbg!(),
299        )?;
300        self.state.temperature.update(
301            *self.state.temperature.get_stale(|| format_dbg!())?
302                + (*self.state.pwr_thrml_from_hvac.get_fresh(|| format_dbg!())?
303                    + *self.state.pwr_thrml_from_amb.get_fresh(|| format_dbg!())?
304                    - *self.state.pwr_thrml_to_res.get_fresh(|| format_dbg!())?)
305                    / self.heat_capacitance
306                    * dt,
307            || format_dbg!(),
308        )?;
309        Ok(*self.state.temperature.get_fresh(|| format_dbg!())?)
310    }
311}
312
313#[serde_api]
314#[derive(
315    Clone, Debug, Deserialize, Serialize, PartialEq, HistoryVec, StateMethods, SetCumulative,
316)]
317#[cfg_attr(feature = "pyo3", pyclass(module = "fastsim", subclass, eq))]
318#[serde(deny_unknown_fields)]
319pub struct LumpedCabinState {
320    /// time step counter
321    pub i: TrackedState<usize>,
322    /// lumped cabin temperature
323    pub temperature: TrackedState<si::Temperature>,
324    /// lumped cabin temperature at start of previous time step
325    pub temp_prev: TrackedState<si::Temperature>,
326    /// Thermal power coming to cabin from [Vehicle::hvac] system.  Positive indicates
327    /// heating, and negative indicates cooling.
328    pub pwr_thrml_from_hvac: TrackedState<si::Power>,
329    /// Cumulative thermal energy coming to cabin from [Vehicle::hvac] system.
330    /// Positive indicates heating, and negative indicates cooling.
331    pub energy_thrml_from_hvac: TrackedState<si::Energy>,
332    /// Thermal power coming to cabin from ambient air.  Positive indicates
333    /// heating, and negative indicates cooling.
334    pub pwr_thrml_from_amb: TrackedState<si::Power>,
335    /// Cumulative thermal energy coming to cabin from ambient air.  Positive indicates
336    /// heating, and negative indicates cooling.
337    pub energy_thrml_from_amb: TrackedState<si::Energy>,
338    /// Thermal power flowing from [Cabin] to [ReversibleEnergyStorage] (zero if
339    /// not equipped) due to temperature delta
340    pub pwr_thrml_to_res: TrackedState<si::Power>,
341    /// Cumulative thermal energy flowing from [Cabin] to
342    /// [ReversibleEnergyStorage] due to temperature delta
343    pub energy_thrml_to_res: TrackedState<si::Energy>,
344    /// Reynolds number for flow over cabin, treating cabin as a flat plate
345    pub reynolds_for_plate: TrackedState<si::Ratio>,
346}
347
348#[pyo3_api]
349impl LumpedCabinState {
350    #[pyo3(name = "default")]
351    #[staticmethod]
352    fn default_py() -> Self {
353        Self::default()
354    }
355}
356
357impl Default for LumpedCabinState {
358    fn default() -> Self {
359        Self {
360            i: Default::default(),
361            temperature: TrackedState::new(*TE_STD_AIR),
362            temp_prev: TrackedState::new(*TE_STD_AIR),
363            pwr_thrml_from_hvac: Default::default(),
364            energy_thrml_from_hvac: Default::default(),
365            pwr_thrml_from_amb: Default::default(),
366            energy_thrml_from_amb: Default::default(),
367            pwr_thrml_to_res: Default::default(),
368            energy_thrml_to_res: Default::default(),
369            reynolds_for_plate: Default::default(),
370        }
371    }
372}
373impl Init for LumpedCabinState {}
374impl SerdeAPI for LumpedCabinState {}