fastsim_core/
vehicle_thermal.rs

1use crate::imports::*;
2use crate::proc_macros::{add_pyo3_api, HistoryVec};
3#[cfg(feature = "pyo3")]
4use crate::pyo3imports::*;
5#[cfg(feature = "pyo3")]
6use crate::utils;
7#[cfg(feature = "pyo3")]
8use crate::utils::Pyo3VecF64;
9use std::f64::consts::PI;
10
11/// Whether FC thermal modeling is handled by FASTSim
12#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
13pub enum FcModelTypes {
14    /// Thermal modeling of fuel converter is handled inside FASTSim
15    Internal(FcTempEffModel, FcTempEffComponent),
16    /// Thermal modeling of fuel converter will be overriden by wrapper code
17    External,
18}
19
20impl Default for FcModelTypes {
21    fn default() -> Self {
22        FcModelTypes::Internal(FcTempEffModel::default(), FcTempEffComponent::default())
23    }
24}
25
26/// Which commponent temperature affects FC efficency
27#[derive(Default, Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
28pub enum FcTempEffComponent {
29    /// FC efficiency is purely dependent on cat temp
30    Catalyst,
31    /// FC efficency is dependent on both cat and FC temp
32    CatAndFC,
33    /// FC efficiency is dependent on FC temp only
34    #[default]
35    FuelConverter,
36}
37
38/// Model variants for how FC efficiency depends on temperature
39#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
40pub enum FcTempEffModel {
41    /// Linear temperature dependence
42    Linear(FcTempEffModelLinear),
43    /// Exponential temperature dependence
44    Exponential(FcTempEffModelExponential),
45}
46
47impl Default for FcTempEffModel {
48    fn default() -> Self {
49        FcTempEffModel::Exponential(FcTempEffModelExponential::default())
50    }
51}
52
53#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
54pub struct FcTempEffModelLinear {
55    pub offset: f64,
56    pub slope: f64,
57    pub minimum: f64,
58}
59
60impl Default for FcTempEffModelLinear {
61    fn default() -> Self {
62        Self {
63            offset: 0.0,
64            slope: 25.0,
65            minimum: 0.2,
66        }
67    }
68}
69
70#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
71pub struct FcTempEffModelExponential {
72    /// temperature at which `fc_eta_temp_coeff` begins to grow
73    pub offset: f64,
74    /// exponential lag parameter
75    pub lag: f64,
76    /// minimum value that `fc_eta_temp_coeff` can take
77    pub minimum: f64,
78}
79
80impl Default for FcTempEffModelExponential {
81    fn default() -> Self {
82        Self {
83            offset: 0.0,
84            lag: 25.0,
85            minimum: 0.2,
86        }
87    }
88}
89
90/// Struct containing parameters and one time-varying variable for HVAC model
91#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, HistoryVec)]
92#[add_pyo3_api(
93    #[staticmethod]
94    #[pyo3(name = "default")]
95    pub fn default_py() -> Self {
96        Self::default()
97    }
98)]
99pub struct HVACModel {
100    /// set temperature for component (e.g. cabin, ESS)
101    pub te_set_deg_c: f64,
102    /// proportional control effort \[kW / °C\]
103    pub p_cntrl_kw_per_deg_c: f64,
104    /// integral control effort \[kW / (°C-seconds)\]
105    pub i_cntrl_kw_per_deg_c_scnds: f64,
106    /// derivative control effort \\[kW / (°C/second) = kJ / °C\\]
107    pub d_cntrl_kj_per_deg_c: f64,
108    /// Saturation value for integral control \[kW\].
109    /// Whenever `i_cntrl_kw` hit this value, it stops accumulating
110    pub cntrl_max_kw: f64,
111    /// deadband range.  any cabin temperature within this range of
112    /// `te_set_deg_c` results in no HVAC power draw
113    pub te_deadband_deg_c: f64,
114    /// current proportional control amount
115    pub p_cntrl_kw: f64,
116    /// current integral control amount
117    pub i_cntrl_kw: f64,
118    /// current derivative control amount
119    pub d_cntrl_kw: f64,
120    /// coefficient between 0 and 1 to calculate HVAC efficiency by multiplying by
121    /// coefficient of performance (COP)
122    pub frac_of_ideal_cop: f64,
123    /// whether heat comes from [FuelConverter]
124    pub use_fc_waste_heat: bool,
125    /// max cooling aux load
126    pub pwr_max_aux_load_for_cooling_kw: f64,
127    /// coefficient of performance of vapor compression cycle
128    pub cop: f64,
129    #[serde(skip)]
130    orphaned: bool,
131}
132
133impl SerdeAPI for HVACModel {}
134
135impl Default for HVACModel {
136    fn default() -> Self {
137        Self {
138            te_set_deg_c: 22.0,
139            p_cntrl_kw_per_deg_c: 0.1,
140            i_cntrl_kw_per_deg_c_scnds: 0.01,
141            d_cntrl_kj_per_deg_c: 0.1,
142            cntrl_max_kw: 5.0,
143            te_deadband_deg_c: 1.0,
144            p_cntrl_kw: 0.0,
145            i_cntrl_kw: 0.0,
146            d_cntrl_kw: 0.0,
147            frac_of_ideal_cop: 0.075, // this is based on Chad's engineering judgment
148            use_fc_waste_heat: true,
149            pwr_max_aux_load_for_cooling_kw: 5.0,
150            cop: 0.0,
151            orphaned: Default::default(),
152        }
153    }
154}
155
156/// Whether HVAC model is handled by FASTSim (internal) or not
157#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
158pub enum CabinHvacModelTypes {
159    /// HVAC is modeled natively
160    Internal(HVACModel),
161    External,
162}
163
164/// Whether compontent thermal model is handled by FASTSim
165#[derive(Debug, Default, Clone, Deserialize, Serialize, PartialEq, Eq)]
166pub enum ComponentModelTypes {
167    /// Component temperature is handled inside FASTSim
168    #[default]
169    Internal,
170    /// Component temperature will be overriden by wrapper code
171    External,
172}
173
174#[cfg_attr(feature = "pyo3", pyfunction)]
175/// Given Reynolds number `re`, return C and m to calculate Nusselt number for
176/// sphere, from Incropera's Intro to Heat Transfer, 5th Ed., eq. 7.44
177pub fn get_sphere_conv_params(re: f64) -> (f64, f64) {
178    let (c, m) = if re < 4.0 {
179        (0.989, 0.330)
180    } else if re < 40.0 {
181        (0.911, 0.385)
182    } else if re < 4e3 {
183        (0.683, 0.466)
184    } else if re < 40e3 {
185        (0.193, 0.618)
186    } else {
187        (0.027, 0.805)
188    };
189    (c, m)
190}
191
192/// Struct for containing vehicle thermal (and related) parameters.
193#[allow(non_snake_case)]
194#[derive(Deserialize, Serialize, Clone, Debug, PartialEq)]
195#[add_pyo3_api(
196    #[staticmethod]
197    #[pyo3(name = "default")]
198    pub fn default_py() -> Self {
199        Default::default()
200    }
201
202    pub fn set_cabin_hvac_model_internal(
203        &mut self,
204        hvac_model: HVACModel
205    ) -> anyhow::Result<()>{
206        check_orphaned_and_set!(self, cabin_hvac_model, CabinHvacModelTypes::Internal(hvac_model))
207    }
208
209    pub fn get_cabin_model_internal(&self, ) -> anyhow::Result<HVACModel> {
210        if let CabinHvacModelTypes::Internal(hvac_model) = &self.cabin_hvac_model {
211            Ok(hvac_model.clone())
212        } else {
213            bail!(PyAttributeError::new_err("HvacModelTypes::External variant currently used."))
214        }
215    }
216
217    pub fn set_cabin_hvac_model_external(&mut self) -> anyhow::Result<()> {
218        check_orphaned_and_set!(self, cabin_hvac_model, CabinHvacModelTypes::External)
219    }
220
221    pub fn set_fc_model_internal_exponential(
222        &mut self,
223        offset: f64,
224        lag: f64,
225        minimum: f64,
226        fc_temp_eff_component: String
227    ) -> anyhow::Result<()>{
228        let fc_temp_eff_comp = match fc_temp_eff_component.as_str() {
229            "FuelConverter" => FcTempEffComponent::FuelConverter,
230            "Catalyst" => FcTempEffComponent::Catalyst,
231            "CatAndFC" => FcTempEffComponent::CatAndFC,
232            _ => bail!("Invalid option for fc_temp_eff_component.")
233        };
234
235        check_orphaned_and_set!(
236            self,
237            fc_model,
238            FcModelTypes::Internal(
239                FcTempEffModel::Exponential(
240                    FcTempEffModelExponential{ offset, lag, minimum }),
241                    fc_temp_eff_comp
242            )
243        )
244    }
245
246    #[setter]
247    pub fn set_fc_exp_offset(&mut self, new_offset: f64) -> anyhow::Result<()> {
248        if !self.orphaned {
249            self.fc_model = if let FcModelTypes::Internal(fc_temp_eff_model, fc_temp_eff_comp) = &self.fc_model {
250                // If model is internal
251                if let FcTempEffModel::Exponential(FcTempEffModelExponential{ offset: _, lag, minimum }) = fc_temp_eff_model {
252                    // If model is exponential
253                    FcModelTypes::Internal(FcTempEffModel::Exponential
254                        (FcTempEffModelExponential{ offset: new_offset, lag: *lag, minimum: *minimum }),
255                        fc_temp_eff_comp.clone())
256                } else {
257                    // If model is not exponential
258                    FcModelTypes::Internal(FcTempEffModel::Exponential
259                        (FcTempEffModelExponential{ offset: new_offset, ..FcTempEffModelExponential::default() }),
260                        fc_temp_eff_comp.clone())
261                }
262            }  else {
263                // If model is not internal
264                FcModelTypes::Internal(FcTempEffModel::Exponential
265                    (FcTempEffModelExponential{ offset: new_offset, ..FcTempEffModelExponential::default() }),
266                    FcTempEffComponent::default())
267            };
268            Ok(())
269        } else {
270            bail!(PyAttributeError::new_err(utils::NESTED_STRUCT_ERR))
271        }
272    }
273
274    #[setter]
275    pub fn set_fc_exp_lag(&mut self, new_lag: f64) -> anyhow::Result<()>{
276        if !self.orphaned {
277            self.fc_model = if let FcModelTypes::Internal(fc_temp_eff_model, fc_temp_eff_comp) = &self.fc_model {
278                // If model is internal
279                if let FcTempEffModel::Exponential(FcTempEffModelExponential{ offset, lag: _, minimum }) = fc_temp_eff_model {
280                    // If model is exponential
281                    FcModelTypes::Internal(FcTempEffModel::Exponential
282                        (FcTempEffModelExponential{ offset: *offset, lag: new_lag, minimum: *minimum }),
283                        fc_temp_eff_comp.clone())
284                } else {
285                    // If model is not exponential
286                    FcModelTypes::Internal(FcTempEffModel::Exponential
287                        (FcTempEffModelExponential{ lag: new_lag, ..FcTempEffModelExponential::default() }),
288                        fc_temp_eff_comp.clone())
289                }
290            }  else {
291                // If model is not internal
292                FcModelTypes::Internal(FcTempEffModel::Exponential
293                    (FcTempEffModelExponential{ lag: new_lag, ..FcTempEffModelExponential::default() }),
294                    FcTempEffComponent::default())
295            };
296            Ok(())
297        } else {
298            bail!(PyAttributeError::new_err(utils::NESTED_STRUCT_ERR))
299        }
300    }
301
302    #[setter]
303    pub fn set_fc_exp_minimum(&mut self, new_minimum: f64) -> anyhow::Result<()> {
304        if !self.orphaned {
305            self.fc_model = if let FcModelTypes::Internal(fc_temp_eff_model, fc_temp_eff_comp) = &self.fc_model {
306                // If model is internal
307                if let FcTempEffModel::Exponential(FcTempEffModelExponential{ offset, lag, minimum: _ }) = fc_temp_eff_model {
308                    // If model is exponential
309                    FcModelTypes::Internal(FcTempEffModel::Exponential
310                        (FcTempEffModelExponential{ offset: *offset, lag: *lag, minimum: new_minimum }),
311                        fc_temp_eff_comp.clone())
312                } else {
313                    // If model is not exponential
314                    FcModelTypes::Internal(FcTempEffModel::Exponential
315                        (FcTempEffModelExponential{ minimum: new_minimum, ..FcTempEffModelExponential::default() }),
316                        fc_temp_eff_comp.clone())
317                }
318            }  else {
319                // If model is not internal
320                FcModelTypes::Internal(FcTempEffModel::Exponential
321                    (FcTempEffModelExponential{ minimum: new_minimum, ..FcTempEffModelExponential::default() }),
322                    FcTempEffComponent::default())
323            };
324            Ok(())
325        } else {
326            bail!(PyAttributeError::new_err(utils::NESTED_STRUCT_ERR))
327        }
328    }
329
330    #[getter]
331    pub fn get_fc_exp_offset(&mut self) -> anyhow::Result<f64> {
332        if let FcModelTypes::Internal(FcTempEffModel::Exponential(FcTempEffModelExponential{ offset, ..}), ..) = &self.fc_model {
333            Ok(*offset)
334        } else {
335            bail!(PyAttributeError::new_err("fc_model is not Exponential"))
336        }
337    }
338
339    #[getter]
340    pub fn get_fc_exp_lag(&mut self) -> anyhow::Result<f64> {
341        if let FcModelTypes::Internal(FcTempEffModel::Exponential(FcTempEffModelExponential{ lag, ..}), ..) = &self.fc_model {
342            Ok(*lag)
343        } else {
344            bail!(PyAttributeError::new_err("fc_model is not Exponential"))
345        }
346    }
347
348    #[getter]
349    pub fn get_fc_exp_minimum(&mut self) -> anyhow::Result<f64> {
350        if let FcModelTypes::Internal(FcTempEffModel::Exponential(FcTempEffModelExponential{ minimum, ..}), ..) = &self.fc_model {
351            Ok(*minimum)
352        } else {
353            bail!(PyAttributeError::new_err("fc_model is not Exponential"))
354        }
355    }
356
357    // TODO: make setters for all the other enum stuff
358)]
359pub struct VehicleThermal {
360    // fuel converter / engine
361    /// parameter fuel converter thermal mass \[kJ/K\]
362    pub fc_c_kj__k: f64,
363    /// parameter for engine characteristic length \[m\] for heat transfer calcs
364    pub fc_l: f64,
365    /// parameter for heat transfer coeff \[W / (m ** 2 * K)\] from eng to ambient during vehicle stop
366    pub fc_htc_to_amb_stop: f64,
367    /// coeff. for fraction of combustion heat that goes to fuel converter (engine)
368    /// thermal mass. Remainder goes to environment (e.g. via tailpipe)
369    pub fc_coeff_from_comb: f64,
370    /// parameter for temperature \[°C\] at which thermostat starts to open
371    pub tstat_te_sto_deg_c: f64,
372    /// temperature delta \[°C\] over which thermostat is partially open
373    pub tstat_te_delta_deg_c: f64,
374    /// radiator effectiveness -- ratio of active heat rejection from
375    /// radiator to passive heat rejection
376    pub rad_eps: f64,
377
378    /// temperature-dependent efficiency
379    /// fuel converter (engine or fuel cell) thermal model type
380    #[api(skip_get, skip_set)]
381    pub fc_model: FcModelTypes,
382
383    // battery
384    /// battery thermal mass \[kJ/K\]
385    pub ess_c_kj_k: f64,
386    /// effective (incl. any thermal management system) heat transfer coefficient from battery to ambient
387    pub ess_htc_to_amb: f64,
388    // battery controls
389    // TODO:
390    // need to flesh this out
391
392    // cabin
393    /// cabin model internal or external w.r.t. fastsim
394    #[api(skip_get, skip_set)]
395    pub cabin_hvac_model: CabinHvacModelTypes,
396    /// parameter for cabin thermal mass \[kJ/K\]
397    pub cab_c_kj__k: f64,
398    /// cabin length \[m\], modeled as a flat plate
399    pub cab_l_length: f64,
400    /// cabin width \[m\], modeled as a flat plate
401    pub cab_l_width: f64,
402    /// cabin shell thermal resistance \[m **2 * K / W\]
403    pub cab_r_to_amb: f64,
404    /// parameter for heat transfer coeff \[W / (m ** 2 * K)\] from cabin to ambient during
405    /// vehicle stop
406    pub cab_htc_to_amb_stop: f64,
407
408    // exhaust port
409    /// 'external' (effectively no model) is default
410    /// exhaust port model type
411    #[api(skip_get, skip_set)]
412    pub exhport_model: ComponentModelTypes,
413    /// thermal conductance \[W/K\] for heat transfer to ambient
414    pub exhport_ha_to_amb: f64,
415    /// thermal conductance \[W/K\] for heat transfer from exhaust
416    pub exhport_ha_int: f64,
417    /// exhaust port thermal capacitance \[kJ/K\]
418    pub exhport_c_kj__k: f64,
419
420    // catalytic converter (catalyst)
421    #[api(skip_get, skip_set)]
422    pub cat_model: ComponentModelTypes,
423    /// diameter \[m\] of catalyst as sphere for thermal model
424    pub cat_l: f64,
425    /// catalyst thermal capacitance \[kJ/K\]
426    pub cat_c_kj__K: f64,
427    /// parameter for heat transfer coeff \[W / (m ** 2 * K)\] from catalyst to ambient
428    /// during vehicle stop
429    pub cat_htc_to_amb_stop: f64,
430    /// lightoff temperature to be used when fc_temp_eff_component == 'hybrid'
431    pub cat_te_lightoff_deg_c: f64,
432    /// cat engine efficiency coeff. to be used when fc_temp_eff_component == 'hybrid'
433    pub cat_fc_eta_coeff: f64,
434
435    /// for pyo3 api
436    #[serde(skip)]
437    pub orphaned: bool,
438}
439
440impl SerdeAPI for VehicleThermal {}
441
442impl Default for VehicleThermal {
443    fn default() -> Self {
444        VehicleThermal {
445            fc_c_kj__k: 150.0,
446            fc_l: 1.0,
447            fc_htc_to_amb_stop: 50.0,
448            fc_coeff_from_comb: 1e-4,
449            tstat_te_sto_deg_c: 85.0,
450            tstat_te_delta_deg_c: 5.0,
451            rad_eps: 5.0,
452            fc_model: FcModelTypes::default(),
453            ess_c_kj_k: 200.0,   // similar size to engine
454            ess_htc_to_amb: 5.0, // typically well insulated from ambient inside cabin
455            cabin_hvac_model: CabinHvacModelTypes::External, // turned off by default
456            cab_c_kj__k: 125.0,
457            cab_l_length: 2.0,
458            cab_l_width: 2.0,
459            cab_r_to_amb: 0.02,
460            cab_htc_to_amb_stop: 10.0,
461            exhport_model: ComponentModelTypes::External, // turned off by default
462            exhport_ha_to_amb: 5.0,
463            exhport_ha_int: 100.0,
464            exhport_c_kj__k: 10.0,
465            cat_model: ComponentModelTypes::External, // turned off by default
466            cat_l: 0.50,
467            cat_c_kj__K: 15.0,
468            cat_htc_to_amb_stop: 10.0,
469            cat_te_lightoff_deg_c: 400.0,
470            cat_fc_eta_coeff: 0.3, // revisit this
471            orphaned: false,
472        }
473    }
474}
475
476impl VehicleThermal {
477    /// derived temperature \[ºC\] at which thermostat is fully open
478    pub fn tstat_te_fo_deg_c(&self) -> f64 {
479        self.tstat_te_sto_deg_c + self.tstat_te_delta_deg_c
480    }
481
482    /// parameter for engine surface area \[m**2\] for heat transfer calcs
483    pub fn fc_area_ext(&self) -> f64 {
484        PI * self.fc_l.powf(2.0) / 4.0
485    }
486
487    /// parameter for catalyst surface area \[m**2\] for heat transfer calcs
488    pub fn cat_area_ext(&self) -> f64 {
489        PI * self.cat_l.powf(2.0 / 4.0)
490    }
491}