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