From 95a2d031dc28c59a894c001654c4435d6df263e1 Mon Sep 17 00:00:00 2001 From: Chad Baker Date: Wed, 18 Dec 2024 16:12:18 -0700 Subject: [PATCH] almost to the point of being ready to merge into fastsim-3 then make demos and re-merge after that but first, troubleshoot compiler errors --- .../src/history_vec_derive.rs | 6 + fastsim-core/src/prelude.rs | 3 +- fastsim-core/src/simdrive.rs | 5 +- fastsim-core/src/vehicle/bev.rs | 4 +- fastsim-core/src/vehicle/cabin.rs | 14 +- fastsim-core/src/vehicle/conv.rs | 2 +- fastsim-core/src/vehicle/hev.rs | 2 +- fastsim-core/src/vehicle/hvac.rs | 518 +----------------- .../vehicle/hvac/hvac_sys_for_lumped_cabin.rs | 253 +++++++++ .../hvac/hvac_sys_for_lumped_cabin_and_res.rs | 470 ++++++++++++++++ .../src/vehicle/powertrain/fuel_converter.rs | 4 +- .../powertrain/reversible_energy_storage.rs | 245 ++------- fastsim-core/src/vehicle/powertrain/traits.rs | 2 +- fastsim-core/src/vehicle/powertrain_type.rs | 2 +- fastsim-core/src/vehicle/vehicle_model.rs | 42 +- .../vehicle_model/fastsim2_interface.rs | 2 - 16 files changed, 841 insertions(+), 733 deletions(-) create mode 100644 fastsim-core/src/vehicle/hvac/hvac_sys_for_lumped_cabin.rs create mode 100644 fastsim-core/src/vehicle/hvac/hvac_sys_for_lumped_cabin_and_res.rs diff --git a/fastsim-core/fastsim-proc-macros/src/history_vec_derive.rs b/fastsim-core/fastsim-proc-macros/src/history_vec_derive.rs index 5fabe3b2..0a6fdb0b 100755 --- a/fastsim-core/fastsim-proc-macros/src/history_vec_derive.rs +++ b/fastsim-core/fastsim-proc-macros/src/history_vec_derive.rs @@ -124,6 +124,12 @@ pub(crate) fn history_vec_derive(input: TokenStream) -> TokenStream { } state_vec } + + // TODO: flesh this out + // /// Returns fieldnames of any fields that are constant throughout history + // pub fn names_of_static_fields(&self) -> Vec { + + // } } impl Default for #new_name { diff --git a/fastsim-core/src/prelude.rs b/fastsim-core/src/prelude.rs index ca5b846d..572eb27c 100755 --- a/fastsim-core/src/prelude.rs +++ b/fastsim-core/src/prelude.rs @@ -20,7 +20,8 @@ pub use crate::vehicle::powertrain::fuel_converter::{ FuelConverterThermalOption, }; pub use crate::vehicle::powertrain::reversible_energy_storage::{ - ReversibleEnergyStorage, ReversibleEnergyStorageState, ReversibleEnergyStorageStateHistoryVec, + RESLumpedThermal, RESLumpedThermalState, RESThermalOption, ReversibleEnergyStorage, + ReversibleEnergyStorageState, ReversibleEnergyStorageStateHistoryVec, }; pub use crate::vehicle::PowertrainType; pub use crate::vehicle::Vehicle; diff --git a/fastsim-core/src/simdrive.rs b/fastsim-core/src/simdrive.rs index 1663a271..b8abd11e 100644 --- a/fastsim-core/src/simdrive.rs +++ b/fastsim-core/src/simdrive.rs @@ -189,8 +189,7 @@ impl SimDrive { let i = self.veh.state.i; let dt = self.cyc.dt_at_i(i)?; let speed_prev = self.veh.state.speed_ach; - // TODO: make sure `pwr_aux` is updated in here and propagated to `set_curr_pwr_out_max` - // maybe make a static `pwr_aux_max` and controls like: + // maybe make controls like: // ``` // pub enum HVACAuxPriority { // /// Prioritize [ReversibleEnergyStorage] thermal management @@ -200,7 +199,7 @@ impl SimDrive { // } // ``` self.veh - .solve_thermal(self.cyc.temp_amb_air[i], self.veh.state, dt) + .solve_thermal(self.cyc.temp_amb_air[i], dt) .with_context(|| format_dbg!())?; self.veh .set_curr_pwr_out_max(dt) diff --git a/fastsim-core/src/vehicle/bev.rs b/fastsim-core/src/vehicle/bev.rs index d3eb975c..b0f2b4e8 100644 --- a/fastsim-core/src/vehicle/bev.rs +++ b/fastsim-core/src/vehicle/bev.rs @@ -142,8 +142,8 @@ impl Powertrain for BatteryElectricVehicle { fn solve_thermal( &mut self, te_amb: si::Temperature, - _pwr_thrl_fc_to_cab: si::Power, - _veh_state: VehicleState, + _pwr_thrml_fc_to_cab: si::Power, + _veh_state: &mut VehicleState, dt: si::Time, ) -> anyhow::Result<()> { self.res diff --git a/fastsim-core/src/vehicle/cabin.rs b/fastsim-core/src/vehicle/cabin.rs index 8d7a61f2..b9bdc1ce 100644 --- a/fastsim-core/src/vehicle/cabin.rs +++ b/fastsim-core/src/vehicle/cabin.rs @@ -69,14 +69,14 @@ impl LumpedCabin { pub fn solve( &mut self, te_amb_air: si::Temperature, - veh_state: VehicleState, + veh_state: &mut VehicleState, pwr_thermal_from_hvac: si::Power, dt: si::Time, ) -> anyhow::Result<()> { self.state.pwr_thermal_from_hvac = pwr_thermal_from_hvac; // flat plate model for isothermal, mixed-flow from Incropera and deWitt, Fundamentals of Heat and Mass // Transfer, 7th Edition - let cab_te_film_ext = 0.5 * (self.state.temp + te_amb_air); + let cab_te_film_ext = 0.5 * (self.state.temperature + te_amb_air); self.state.reynolds_for_plate = Air::get_density(Some(cab_te_film_ext), Some(veh_state.elev_curr)) * veh_state.speed_ach @@ -107,15 +107,15 @@ impl LumpedCabin { * Air::get_therm_cond(cab_te_film_ext).with_context(|| format_dbg!())? / self.length) + 1.0 / self.cab_htc_to_amb); - (self.length * self.width) * htc_overall_moving * (te_amb_air - self.state.temp) + (self.length * self.width) * htc_overall_moving * (te_amb_air - self.state.temperature) } else { (self.length * self.width) / (1.0 / self.cab_htc_to_amb_stop + 1.0 / self.cab_htc_to_amb) - * (te_amb_air - self.state.temp) + * (te_amb_air - self.state.temperature) }; - self.state.temp_prev = self.state.temp; - self.state.temp += (self.state.pwr_thermal_from_hvac + self.state.pwr_thermal_from_amb) + self.state.temp_prev = self.state.temperature; + self.state.temperature += (self.state.pwr_thermal_from_hvac + self.state.pwr_thermal_from_amb) / self.heat_capacitance * dt; Ok(()) @@ -130,7 +130,7 @@ pub struct LumpedCabinState { /// time step counter pub i: u32, /// lumped cabin temperature - pub temp: si::Temperature, + pub temperature: si::Temperature, /// lumped cabin temperature at previous simulation time step // TODO: make sure this gets updated pub temp_prev: si::Temperature, diff --git a/fastsim-core/src/vehicle/conv.rs b/fastsim-core/src/vehicle/conv.rs index c2522830..31f500a4 100644 --- a/fastsim-core/src/vehicle/conv.rs +++ b/fastsim-core/src/vehicle/conv.rs @@ -58,7 +58,7 @@ impl Powertrain for Box { &mut self, te_amb: si::Temperature, pwr_thrl_fc_to_cab: si::Power, - veh_state: VehicleState, + veh_state: &mut VehicleState, dt: si::Time, ) -> anyhow::Result<()> { self.fc diff --git a/fastsim-core/src/vehicle/hev.rs b/fastsim-core/src/vehicle/hev.rs index a1c03dcb..c2283dc4 100644 --- a/fastsim-core/src/vehicle/hev.rs +++ b/fastsim-core/src/vehicle/hev.rs @@ -208,7 +208,7 @@ impl Powertrain for Box { &mut self, te_amb: si::Temperature, pwr_thrl_fc_to_cab: si::Power, - veh_state: VehicleState, + veh_state: &mut VehicleState, dt: si::Time, ) -> anyhow::Result<()> { self.fc diff --git a/fastsim-core/src/vehicle/hvac.rs b/fastsim-core/src/vehicle/hvac.rs index 6d009140..e0a9cffa 100644 --- a/fastsim-core/src/vehicle/hvac.rs +++ b/fastsim-core/src/vehicle/hvac.rs @@ -1,5 +1,11 @@ use super::*; +pub mod hvac_sys_for_lumped_cabin; +pub use hvac_sys_for_lumped_cabin::*; + +pub mod hvac_sys_for_lumped_cabin_and_res; +pub use hvac_sys_for_lumped_cabin_and_res::*; + /// Options for handling HVAC system #[derive(Clone, Default, Debug, Serialize, Deserialize, PartialEq, IsVariant)] pub enum HVACOption { @@ -32,515 +38,3 @@ impl Init for HVACOption { } } impl SerdeAPI for HVACOption {} - -#[fastsim_api] -#[derive(Deserialize, Serialize, Debug, Clone, PartialEq, HistoryMethods)] -/// HVAC system for [LumpedCabin] -pub struct HVACSystemForLumpedCabin { - /// set point temperature - pub te_set: si::Temperature, - /// deadband range. any cabin temperature within this range of - /// `te_set` results in no HVAC power draw - pub te_deadband: si::Temperature, - /// HVAC proportional gain - pub p: si::ThermalConductance, - /// HVAC integral gain [W / K / s], resets at zero crossing events - /// NOTE: `uom` crate does not have this unit, but it may be possible to make a custom unit for this - pub i: f64, - /// value at which [Self::i] stops accumulating - pub pwr_i_max: si::Power, - /// HVAC derivative gain [W / K * s] - /// NOTE: `uom` crate does not have this unit, but it may be possible to make a custom unit for this - pub d: f64, - /// max HVAC thermal power - pub pwr_thermal_max: si::Power, - /// coefficient between 0 and 1 to calculate HVAC efficiency by multiplying by - /// coefficient of performance (COP) - pub frac_of_ideal_cop: f64, - /// heat source - #[api(skip_get, skip_set)] - pub heat_source: CabinHeatSource, - /// max allowed aux load - pub pwr_aux_max: si::Power, - /// coefficient of performance of vapor compression cycle - #[serde(default, skip_serializing_if = "EqDefault::eq_default")] - pub state: HVACSystemForLumpedCabinState, - #[serde( - default, - skip_serializing_if = "HVACSystemForLumpedCabinStateHistoryVec::is_empty" - )] - pub history: HVACSystemForLumpedCabinStateHistoryVec, -} -impl Init for HVACSystemForLumpedCabin {} -impl SerdeAPI for HVACSystemForLumpedCabin {} -impl HVACSystemForLumpedCabin { - pub fn solve( - &mut self, - te_amb_air: si::Temperature, - te_fc: Option, - cab_state: LumpedCabinState, - cab_heat_cap: si::HeatCapacity, - dt: si::Time, - ) -> anyhow::Result<(si::Power, si::Power)> { - let (pwr_thrml_hvac_to_cabin, pwr_thrml_fc_to_cabin) = if cab_state.temp - <= self.te_set + self.te_deadband - && cab_state.temp >= self.te_set - self.te_deadband - { - // inside deadband; no hvac power is needed - - self.state.pwr_i = si::Power::ZERO; // reset to 0.0 - self.state.pwr_p = si::Power::ZERO; - self.state.pwr_d = si::Power::ZERO; - (si::Power::ZERO, si::Power::ZERO) - } else { - // outside deadband - let te_delta_vs_set = cab_state.temp - self.te_set; - let te_delta_vs_amb: si::Temperature = cab_state.temp - te_amb_air; - - self.state.pwr_p = -self.p * te_delta_vs_set; - self.state.pwr_i -= self.i * uc::W / uc::KELVIN / uc::S * te_delta_vs_set * dt; - self.state.pwr_i = self.state.pwr_i.max(-self.pwr_i_max).min(self.pwr_i_max); - self.state.pwr_d = - -self.d * uc::J / uc::KELVIN * ((cab_state.temp - cab_state.temp_prev) / dt); - - // https://en.wikipedia.org/wiki/Coefficient_of_performance#Theoretical_performance_limits - // cop_ideal is t_h / (t_h - t_c) for heating - // cop_ideal is t_c / (t_h - t_c) for cooling - - // divide-by-zero protection and realistic limit on COP - let cop_ideal = if te_delta_vs_amb.abs() < 5.0 * uc::KELVIN { - // cabin is cooler than ambient + threshold - // TODO: make this `5.0` not hardcoded - cab_state.temp / (5.0 * uc::KELVIN) - } else { - cab_state.temp / te_delta_vs_amb.abs() - }; - self.state.cop = cop_ideal * self.frac_of_ideal_cop; - assert!(self.state.cop > 0.0 * uc::R); - - let (pwr_thrml_hvac_to_cabin, pwr_thrml_fc_to_cabin) = - if cab_state.temp > self.te_set + self.te_deadband { - // COOLING MODE; cabin is hotter than set point - - if self.state.pwr_i > si::Power::ZERO { - // If `pwr_i` is greater than zero, reset to switch from heating to cooling - self.state.pwr_i = si::Power::ZERO; - } - let mut pwr_thrml_hvac_to_cab = - (self.state.pwr_p + self.state.pwr_i + self.state.pwr_d) - .max(-self.pwr_thermal_max); - - if (-pwr_thrml_hvac_to_cab / self.state.cop) > self.pwr_aux_max { - // TODO: maybe change this to a static `pwr_aux_max` - self.state.pwr_aux = self.pwr_aux_max; - // correct if limit is exceeded - pwr_thrml_hvac_to_cab = -self.state.pwr_aux * self.state.cop; - } else { - // TODO: maybe change this to a static `pwr_aux_max` - self.state.pwr_aux = pwr_thrml_hvac_to_cab / self.state.cop; - } - let pwr_thrml_fc_to_cabin = si::Power::ZERO; - (pwr_thrml_hvac_to_cab, pwr_thrml_fc_to_cabin) - } else { - // HEATING MODE; cabin is colder than set point - - if self.state.pwr_i < si::Power::ZERO { - // If `pwr_i` is less than zero reset to switch from cooling to heating - self.state.pwr_i = si::Power::ZERO; - } - let mut pwr_thrml_hvac_to_cabin = - (-self.state.pwr_p - self.state.pwr_i - self.state.pwr_d) - .min(self.pwr_thermal_max); - - // Assumes blower has negligible impact on aux load, may want to revise later - let pwr_thrml_fc_to_cabin = self - .handle_heat_source( - te_fc, - te_delta_vs_amb, - &mut pwr_thrml_hvac_to_cabin, - cab_heat_cap, - cab_state, - dt, - ) - .with_context(|| format_dbg!())?; - (pwr_thrml_hvac_to_cabin, pwr_thrml_fc_to_cabin) - }; - (pwr_thrml_hvac_to_cabin, pwr_thrml_fc_to_cabin) - }; - Ok((pwr_thrml_hvac_to_cabin, pwr_thrml_fc_to_cabin)) - } - - fn handle_heat_source( - &mut self, - te_fc: Option, - te_delta_vs_amb: si::Temperature, - pwr_thrml_hvac_to_cabin: &mut si::Power, - cab_heat_cap: si::HeatCapacity, - cab_state: LumpedCabinState, - dt: si::Time, - ) -> anyhow::Result { - let pwr_thrml_fc_to_cabin = match self.heat_source { - CabinHeatSource::FuelConverter => { - ensure!( - te_fc.is_some(), - "{}\nExpected vehicle with [FuelConverter] with thermal plant model.", - format_dbg!() - ); - // limit heat transfer to be substantially less than what is physically possible - // i.e. the engine can't drop below cabin temperature to heat the cabin - *pwr_thrml_hvac_to_cabin = pwr_thrml_hvac_to_cabin - .min( - cab_heat_cap * - (te_fc.unwrap() - cab_state.temp) - * 0.1 // so that it's substantially less - / dt, - ) - .max(si::Power::ZERO); - self.state.cop = f64::NAN * uc::R; - let pwr_thrml_fc_to_cabin = *pwr_thrml_hvac_to_cabin; - // Assumes aux power needed for heating is incorporated into based aux load. - // TODO: refine this, perhaps by making aux power - // proportional to heating power, to account for blower power - self.state.pwr_aux = si::Power::ZERO; - // TODO: think about what to do for PHEV, which needs careful consideration here - // HEV probably also needs careful consideration - // There needs to be an engine temperature (e.g. 60°C) below which the engine is forced on - pwr_thrml_fc_to_cabin - } - CabinHeatSource::ResistanceHeater => { - self.state.cop = uc::R; - self.state.pwr_aux = *pwr_thrml_hvac_to_cabin; // COP is 1 so does not matter - #[allow(clippy::let_and_return)] // for readability - let pwr_thrml_fc_to_cabin = si::Power::ZERO; - pwr_thrml_fc_to_cabin - } - CabinHeatSource::HeatPump => { - // https://en.wikipedia.org/wiki/Coefficient_of_performance#Theoretical_performance_limits - // cop_ideal is t_h / (t_h - t_c) for heating - // cop_ideal is t_c / (t_h - t_c) for cooling - - // divide-by-zero protection and realistic limit on COP - // TODO: make sure this is right for heating! - let cop_ideal = if te_delta_vs_amb.abs() < 5.0 * uc::KELVIN { - // cabin is cooler than ambient + threshold - // TODO: make this `5.0` not hardcoded - cab_state.temp / (5.0 * uc::KELVIN) - } else { - cab_state.temp / te_delta_vs_amb.abs() - }; - self.state.cop = cop_ideal * self.frac_of_ideal_cop; - assert!(self.state.cop > 0.0 * uc::R); - if (*pwr_thrml_hvac_to_cabin / self.state.cop) > self.pwr_aux_max { - self.state.pwr_aux = self.pwr_aux_max; - // correct if limit is exceeded - *pwr_thrml_hvac_to_cabin = -self.state.pwr_aux * self.state.cop; - } else { - self.state.pwr_aux = *pwr_thrml_hvac_to_cabin / self.state.cop; - } - #[allow(clippy::let_and_return)] // for readability - let pwr_thrml_fc_to_cabin = si::Power::ZERO; - pwr_thrml_fc_to_cabin - } - }; - Ok(pwr_thrml_fc_to_cabin) - } -} - -#[derive(Clone, Copy, Debug, Deserialize, Serialize, PartialEq)] -pub enum CabinHeatSource { - /// [FuelConverter], if applicable, provides heat for HVAC system - FuelConverter, - /// Resistance heater provides heat for HVAC system - ResistanceHeater, - /// Heat pump provides heat for HVAC system - HeatPump, -} -impl Init for CabinHeatSource {} -impl SerdeAPI for CabinHeatSource {} - -#[fastsim_api] -#[derive( - Clone, Copy, Debug, Default, Deserialize, Serialize, PartialEq, HistoryVec, SetCumulative, -)] -pub struct HVACSystemForLumpedCabinState { - /// time step counter - pub i: u32, - /// portion of total HVAC cooling/heating (negative/positive) power due to proportional gain - pub pwr_p: si::Power, - /// portion of total HVAC cooling/heating (negative/positive) cumulative energy due to proportional gain - pub energy_p: si::Energy, - /// portion of total HVAC cooling/heating (negative/positive) power due to integral gain - pub pwr_i: si::Power, - /// portion of total HVAC cooling/heating (negative/positive) cumulative energy due to integral gain - pub energy_i: si::Energy, - /// portion of total HVAC cooling/heating (negative/positive) power due to derivative gain - pub pwr_d: si::Power, - /// portion of total HVAC cooling/heating (negative/positive) cumulative energy due to derivative gain - pub energy_d: si::Energy, - /// coefficient of performance (i.e. efficiency) of vapor compression cycle - pub cop: si::Ratio, - /// Au power demand from HVAC system - pub pwr_aux: si::Power, - /// Cumulative aux energy for HVAC system - pub energy_aux: si::Energy, - /// Cumulative energy demand by HVAC system from thermal component (e.g. [FuelConverter]) - pub energy_thermal_req: si::Energy, -} -impl Init for HVACSystemForLumpedCabinState {} -impl SerdeAPI for HVACSystemForLumpedCabinState {} - -#[fastsim_api] -#[derive(Deserialize, Serialize, Debug, Clone, PartialEq, HistoryMethods)] -/// HVAC system for [LumpedCabin] and [ReversibleEnergyStorage] -pub struct HVACSystemForLumpedCabinAndRES { - /// set point temperature - pub te_set: si::Temperature, - /// deadband range. any cabin temperature within this range of - /// `te_set` results in no HVAC power draw - pub te_deadband: si::Temperature, - /// HVAC proportional gain - pub p: si::ThermalConductance, - /// HVAC integral gain [W / K / s], resets at zero crossing events - /// NOTE: `uom` crate does not have this unit, but it may be possible to make a custom unit for this - pub i: f64, - /// value at which [Self::i] stops accumulating - pub pwr_i_max: si::Power, - /// HVAC derivative gain [W / K * s] - /// NOTE: `uom` crate does not have this unit, but it may be possible to make a custom unit for this - pub d: f64, - /// max HVAC thermal power - pub pwr_thermal_max: si::Power, - /// coefficient between 0 and 1 to calculate HVAC efficiency by multiplying by - /// coefficient of performance (COP) - pub frac_of_ideal_cop: f64, - /// heat source - #[api(skip_get, skip_set)] - pub heat_source: CabinHeatSource, - /// max allowed aux load - pub pwr_aux_max: si::Power, - /// coefficient of performance of vapor compression cycle - #[serde(default, skip_serializing_if = "EqDefault::eq_default")] - pub state: HVACSystemForLumpedCabinAndRESState, - #[serde( - default, - skip_serializing_if = "HVACSystemForLumpedCabinAndRESStateHistoryVec::is_empty" - )] - pub history: HVACSystemForLumpedCabinAndRESStateHistoryVec, -} -impl Init for HVACSystemForLumpedCabinAndRES {} -impl SerdeAPI for HVACSystemForLumpedCabinAndRES {} -impl HVACSystemForLumpedCabinAndRES { - pub fn solve( - &mut self, - te_amb_air: si::Temperature, - te_fc: Option, - cab_state: LumpedCabinState, - cab_heat_cap: si::HeatCapacity, - dt: si::Time, - ) -> anyhow::Result<(si::Power, si::Power)> { - let (pwr_thrml_hvac_to_cabin, pwr_thrml_fc_to_cabin) = if cab_state.temp - <= self.te_set + self.te_deadband - && cab_state.temp >= self.te_set - self.te_deadband - { - // inside deadband; no hvac power is needed - - self.state.pwr_i = si::Power::ZERO; // reset to 0.0 - self.state.pwr_p = si::Power::ZERO; - self.state.pwr_d = si::Power::ZERO; - (si::Power::ZERO, si::Power::ZERO) - } else { - // outside deadband - let te_delta_vs_set = cab_state.temp - self.te_set; - let te_delta_vs_amb: si::Temperature = cab_state.temp - te_amb_air; - - self.state.pwr_p = -self.p * te_delta_vs_set; - self.state.pwr_i -= self.i * uc::W / uc::KELVIN / uc::S * te_delta_vs_set * dt; - self.state.pwr_i = self.state.pwr_i.max(-self.pwr_i_max).min(self.pwr_i_max); - self.state.pwr_d = - -self.d * uc::J / uc::KELVIN * ((cab_state.temp - cab_state.temp_prev) / dt); - - // https://en.wikipedia.org/wiki/Coefficient_of_performance#Theoretical_performance_limits - // cop_ideal is t_h / (t_h - t_c) for heating - // cop_ideal is t_c / (t_h - t_c) for cooling - - // divide-by-zero protection and realistic limit on COP - let cop_ideal = if te_delta_vs_amb.abs() < 5.0 * uc::KELVIN { - // cabin is cooler than ambient + threshold - // TODO: make this `5.0` not hardcoded - cab_state.temp / (5.0 * uc::KELVIN) - } else { - cab_state.temp / te_delta_vs_amb.abs() - }; - self.state.cop = cop_ideal * self.frac_of_ideal_cop; - assert!(self.state.cop > 0.0 * uc::R); - - let (pwr_thrml_hvac_to_cabin, pwr_thrml_fc_to_cabin) = - if cab_state.temp > self.te_set + self.te_deadband { - // COOLING MODE; cabin is hotter than set point - - if self.state.pwr_i > si::Power::ZERO { - // If `pwr_i` is greater than zero, reset to switch from heating to cooling - self.state.pwr_i = si::Power::ZERO; - } - let mut pwr_thrml_hvac_to_cab = - (self.state.pwr_p + self.state.pwr_i + self.state.pwr_d) - .max(-self.pwr_thermal_max); - - if (-pwr_thrml_hvac_to_cab / self.state.cop) > self.pwr_aux_max { - // TODO: maybe change this to a static `pwr_aux_max` - self.state.pwr_aux = self.pwr_aux_max; - // correct if limit is exceeded - pwr_thrml_hvac_to_cab = -self.state.pwr_aux * self.state.cop; - } else { - // TODO: maybe change this to a static `pwr_aux_max` - self.state.pwr_aux = pwr_thrml_hvac_to_cab / self.state.cop; - } - let pwr_thrml_fc_to_cabin = si::Power::ZERO; - (pwr_thrml_hvac_to_cab, pwr_thrml_fc_to_cabin) - } else { - // HEATING MODE; cabin is colder than set point - - if self.state.pwr_i < si::Power::ZERO { - // If `pwr_i` is less than zero reset to switch from cooling to heating - self.state.pwr_i = si::Power::ZERO; - } - let mut pwr_thrml_hvac_to_cabin = - (-self.state.pwr_p - self.state.pwr_i - self.state.pwr_d) - .min(self.pwr_thermal_max); - - // Assumes blower has negligible impact on aux load, may want to revise later - let pwr_thrml_fc_to_cabin = self - .handle_heat_source( - te_fc, - te_delta_vs_amb, - &mut pwr_thrml_hvac_to_cabin, - cab_heat_cap, - cab_state, - dt, - ) - .with_context(|| format_dbg!())?; - (pwr_thrml_hvac_to_cabin, pwr_thrml_fc_to_cabin) - }; - (pwr_thrml_hvac_to_cabin, pwr_thrml_fc_to_cabin) - }; - Ok((pwr_thrml_hvac_to_cabin, pwr_thrml_fc_to_cabin)) - } - - fn handle_heat_source( - &mut self, - te_fc: Option, - te_delta_vs_amb: si::Temperature, - pwr_thrml_hvac_to_cabin: &mut si::Power, - cab_heat_cap: si::HeatCapacity, - cab_state: LumpedCabinState, - dt: si::Time, - ) -> anyhow::Result { - let pwr_thrml_fc_to_cabin = match self.heat_source { - CabinHeatSource::FuelConverter => { - ensure!( - te_fc.is_some(), - "{}\nExpected vehicle with [FuelConverter] with thermal plant model.", - format_dbg!() - ); - // limit heat transfer to be substantially less than what is physically possible - // i.e. the engine can't drop below cabin temperature to heat the cabin - *pwr_thrml_hvac_to_cabin = pwr_thrml_hvac_to_cabin - .min( - cab_heat_cap * - (te_fc.unwrap() - cab_state.temp) - * 0.1 // so that it's substantially less - / dt, - ) - .max(si::Power::ZERO); - self.state.cop = f64::NAN * uc::R; - let pwr_thrml_fc_to_cabin = *pwr_thrml_hvac_to_cabin; - // Assumes aux power needed for heating is incorporated into based aux load. - // TODO: refine this, perhaps by making aux power - // proportional to heating power, to account for blower power - self.state.pwr_aux = si::Power::ZERO; - // TODO: think about what to do for PHEV, which needs careful consideration here - // HEV probably also needs careful consideration - // There needs to be an engine temperature (e.g. 60°C) below which the engine is forced on - pwr_thrml_fc_to_cabin - } - CabinHeatSource::ResistanceHeater => { - self.state.cop = uc::R; - self.state.pwr_aux = *pwr_thrml_hvac_to_cabin; // COP is 1 so does not matter - #[allow(clippy::let_and_return)] // for readability - let pwr_thrml_fc_to_cabin = si::Power::ZERO; - pwr_thrml_fc_to_cabin - } - CabinHeatSource::HeatPump => { - // https://en.wikipedia.org/wiki/Coefficient_of_performance#Theoretical_performance_limits - // cop_ideal is t_h / (t_h - t_c) for heating - // cop_ideal is t_c / (t_h - t_c) for cooling - - // divide-by-zero protection and realistic limit on COP - // TODO: make sure this is right for heating! - let cop_ideal = if te_delta_vs_amb.abs() < 5.0 * uc::KELVIN { - // cabin is cooler than ambient + threshold - // TODO: make this `5.0` not hardcoded - cab_state.temp / (5.0 * uc::KELVIN) - } else { - cab_state.temp / te_delta_vs_amb.abs() - }; - self.state.cop = cop_ideal * self.frac_of_ideal_cop; - assert!(self.state.cop > 0.0 * uc::R); - if (*pwr_thrml_hvac_to_cabin / self.state.cop) > self.pwr_aux_max { - self.state.pwr_aux = self.pwr_aux_max; - // correct if limit is exceeded - *pwr_thrml_hvac_to_cabin = -self.state.pwr_aux * self.state.cop; - } else { - self.state.pwr_aux = *pwr_thrml_hvac_to_cabin / self.state.cop; - } - #[allow(clippy::let_and_return)] // for readability - let pwr_thrml_fc_to_cabin = si::Power::ZERO; - pwr_thrml_fc_to_cabin - } - }; - Ok(pwr_thrml_fc_to_cabin) - } -} - -#[fastsim_api] -#[derive( - Clone, Copy, Debug, Default, Deserialize, Serialize, PartialEq, HistoryVec, SetCumulative, -)] -pub struct HVACSystemForLumpedCabinAndRESState { - /// time step counter - pub i: u32, - /// portion of total HVAC cooling/heating (negative/positive) power due to proportional gain - pub pwr_p: si::Power, - /// portion of total HVAC cooling/heating (negative/positive) cumulative energy due to proportional gain - pub energy_p: si::Energy, - /// portion of total HVAC cooling/heating (negative/positive) power due to integral gain - pub pwr_i: si::Power, - /// portion of total HVAC cooling/heating (negative/positive) cumulative energy due to integral gain - pub energy_i: si::Energy, - /// portion of total HVAC cooling/heating (negative/positive) power due to derivative gain - pub pwr_d: si::Power, - /// portion of total HVAC cooling/heating (negative/positive) cumulative energy due to derivative gain - pub energy_d: si::Energy, - /// portion of total HVAC cooling/heating (negative/positive) power to [ReversibleEnergyStorage] due to proportional gain - pub pwr_p_res: si::Power, - /// portion of total HVAC cooling/heating (negative/positive) cumulative energy to [ReversibleEnergyStorage] due to proportional gain - pub energy_p_res: si::Energy, - /// portion of total HVAC cooling/heating (negative/positive) power to [ReversibleEnergyStorage] due to integral gain - pub pwr_i_res: si::Power, - /// portion of total HVAC cooling/heating (negative/positive) cumulative energy to [ReversibleEnergyStorage] due to integral gain - pub energy_i_res: si::Energy, - /// portion of total HVAC cooling/heating (negative/positive) power to [ReversibleEnergyStorage] due to derivative gain - pub pwr_d_res: si::Power, - /// portion of total HVAC cooling/heating (negative/positive) cumulative energy to [ReversibleEnergyStorage] due to derivative gain - pub energy_d_res: si::Energy, - /// coefficient of performance (i.e. efficiency) of vapor compression cycle - pub cop: si::Ratio, - /// Au power demand from HVAC system - pub pwr_aux: si::Power, - /// Cumulative aux energy for HVAC system - pub energy_aux: si::Energy, - /// Cumulative energy demand by HVAC system from thermal component (e.g. [FuelConverter]) - pub energy_thermal_req: si::Energy, -} -impl Init for HVACSystemForLumpedCabinAndRESState {} -impl SerdeAPI for HVACSystemForLumpedCabinAndRESState {} diff --git a/fastsim-core/src/vehicle/hvac/hvac_sys_for_lumped_cabin.rs b/fastsim-core/src/vehicle/hvac/hvac_sys_for_lumped_cabin.rs new file mode 100644 index 00000000..9d6b537c --- /dev/null +++ b/fastsim-core/src/vehicle/hvac/hvac_sys_for_lumped_cabin.rs @@ -0,0 +1,253 @@ +use super::*; + +#[fastsim_api] +#[derive(Deserialize, Serialize, Debug, Clone, PartialEq, HistoryMethods)] +/// HVAC system for [LumpedCabin] +pub struct HVACSystemForLumpedCabin { + /// set point temperature + pub te_set: si::Temperature, + /// deadband range. any cabin temperature within this range of + /// `te_set` results in no HVAC power draw + pub te_deadband: si::Temperature, + /// HVAC proportional gain + pub p: si::ThermalConductance, + /// HVAC integral gain [W / K / s], resets at zero crossing events + /// NOTE: `uom` crate does not have this unit, but it may be possible to make a custom unit for this + pub i: f64, + /// value at which state.i stops accumulating + pub pwr_i_max: si::Power, + /// HVAC derivative gain [W / K * s] + /// NOTE: `uom` crate does not have this unit, but it may be possible to make a custom unit for this + pub d: f64, + /// max HVAC thermal power + pub pwr_thermal_max: si::Power, + /// coefficient between 0 and 1 to calculate HVAC efficiency by multiplying by + /// coefficient of performance (COP) + pub frac_of_ideal_cop: f64, + /// heat source + #[api(skip_get, skip_set)] + pub heat_source: CabinHeatSource, + /// max allowed aux load for HVAC + pub pwr_aux_for_hvac_max: si::Power, + /// coefficient of performance of vapor compression cycle + #[serde(default, skip_serializing_if = "EqDefault::eq_default")] + pub state: HVACSystemForLumpedCabinState, + #[serde( + default, + skip_serializing_if = "HVACSystemForLumpedCabinStateHistoryVec::is_empty" + )] + pub history: HVACSystemForLumpedCabinStateHistoryVec, +} +impl Init for HVACSystemForLumpedCabin {} +impl SerdeAPI for HVACSystemForLumpedCabin {} +impl HVACSystemForLumpedCabin { + pub fn solve( + &mut self, + te_amb_air: si::Temperature, + te_fc: Option, + cab_state: LumpedCabinState, + cab_heat_cap: si::HeatCapacity, + dt: si::Time, + ) -> anyhow::Result<(si::Power, si::Power)> { + let (pwr_thrml_hvac_to_cabin, pwr_thrml_fc_to_cabin, cop) = if cab_state.temperature + <= self.te_set + self.te_deadband + && cab_state.temperature >= self.te_set - self.te_deadband + { + // inside deadband; no hvac power is needed + + self.state.pwr_i = si::Power::ZERO; // reset to 0.0 + self.state.pwr_p = si::Power::ZERO; + self.state.pwr_d = si::Power::ZERO; + (si::Power::ZERO, si::Power::ZERO, f64::NAN * uc::R) + } else { + // outside deadband + let te_delta_vs_set = cab_state.temperature - self.te_set; + let te_delta_vs_amb: si::Temperature = cab_state.temperature - te_amb_air; + + self.state.pwr_p = -self.p * te_delta_vs_set; + self.state.pwr_i -= self.i * uc::W / uc::KELVIN / uc::S * te_delta_vs_set * dt; + self.state.pwr_i = self.state.pwr_i.max(-self.pwr_i_max).min(self.pwr_i_max); + self.state.pwr_d = + -self.d * uc::J / uc::KELVIN * ((cab_state.temperature - cab_state.temp_prev) / dt); + + let (pwr_thrml_hvac_to_cabin, pwr_thrml_fc_to_cabin, cop) = + if cab_state.temperature > self.te_set + self.te_deadband { + // COOLING MODE; cabin is hotter than set point + + // https://en.wikipedia.org/wiki/Coefficient_of_performance#Theoretical_performance_limits + // cop_ideal is t_h / (t_h - t_c) for heating + // cop_ideal is t_c / (t_h - t_c) for cooling + + // divide-by-zero protection and realistic limit on COP + let cop_ideal = if te_delta_vs_amb.abs() < 5.0 * uc::KELVIN { + // cabin is cooler than ambient + threshold + // TODO: make this `5.0` not hardcoded + cab_state.temperature / (5.0 * uc::KELVIN) + } else { + cab_state.temperature / te_delta_vs_amb.abs() + }; + let cop = cop_ideal * self.frac_of_ideal_cop; + assert!(cop > 0.0 * uc::R); + + if self.state.pwr_i > si::Power::ZERO { + // If `pwr_i` is greater than zero, reset to switch from heating to cooling + self.state.pwr_i = si::Power::ZERO; + } + let mut pwr_thrml_hvac_to_cab = + (self.state.pwr_p + self.state.pwr_i + self.state.pwr_d) + .max(-self.pwr_thermal_max); + + if (-pwr_thrml_hvac_to_cab / self.state.cop) > self.pwr_aux_for_hvac_max { + self.state.pwr_aux_for_hvac = self.pwr_aux_for_hvac_max; + // correct if limit is exceeded + pwr_thrml_hvac_to_cab = -self.state.pwr_aux_for_hvac * self.state.cop; + } else { + self.state.pwr_aux_for_hvac = -pwr_thrml_hvac_to_cab / self.state.cop; + } + let pwr_thrml_fc_to_cabin = si::Power::ZERO; + (pwr_thrml_hvac_to_cab, pwr_thrml_fc_to_cabin, cop) + } else { + // HEATING MODE; cabin is colder than set point + + if self.state.pwr_i < si::Power::ZERO { + // If `pwr_i` is less than zero reset to switch from cooling to heating + self.state.pwr_i = si::Power::ZERO; + } + let mut pwr_thrml_hvac_to_cabin = + (-self.state.pwr_p - self.state.pwr_i - self.state.pwr_d) + .min(self.pwr_thermal_max); + + // Assumes blower has negligible impact on aux load, may want to revise later + let (pwr_thrml_fc_to_cabin, cop) = self + .handle_heat_source( + te_fc, + te_delta_vs_amb, + &mut pwr_thrml_hvac_to_cabin, + cab_heat_cap, + cab_state, + dt, + ) + .with_context(|| format_dbg!())?; + (pwr_thrml_hvac_to_cabin, pwr_thrml_fc_to_cabin, cop) + }; + (pwr_thrml_hvac_to_cabin, pwr_thrml_fc_to_cabin, cop) + }; + self.state.cop = cop; + Ok((pwr_thrml_hvac_to_cabin, pwr_thrml_fc_to_cabin)) + } + + fn handle_heat_source( + &mut self, + te_fc: Option, + te_delta_vs_amb: si::Temperature, + pwr_thrml_hvac_to_cabin: &mut si::Power, + cab_heat_cap: si::HeatCapacity, + cab_state: LumpedCabinState, + dt: si::Time, + ) -> anyhow::Result<(si::Power, si::Ratio)> { + let (pwr_thrml_fc_to_cabin, cop) = match self.heat_source { + CabinHeatSource::FuelConverter => { + ensure!( + te_fc.is_some(), + "{}\nExpected vehicle with [FuelConverter] with thermal plant model.", + format_dbg!() + ); + // limit heat transfer to be substantially less than what is physically possible + // i.e. the engine can't drop below cabin temperature to heat the cabin + *pwr_thrml_hvac_to_cabin = pwr_thrml_hvac_to_cabin + .min( + cab_heat_cap * + (te_fc.unwrap() - cab_state.temperature) + * 0.1 // so that it's substantially less + / dt, + ) + .max(si::Power::ZERO); + let cop = f64::NAN * uc::R; + let pwr_thrml_fc_to_cabin = *pwr_thrml_hvac_to_cabin; + // Assumes aux power needed for heating is incorporated into based aux load. + // TODO: refine this, perhaps by making aux power + // proportional to heating power, to account for blower power + self.state.pwr_aux_for_hvac = si::Power::ZERO; + (pwr_thrml_fc_to_cabin, cop) + } + CabinHeatSource::ResistanceHeater => { + let cop = uc::R; + self.state.pwr_aux_for_hvac = *pwr_thrml_hvac_to_cabin; // COP is 1 so does not matter + #[allow(clippy::let_and_return)] // for readability + let pwr_thrml_fc_to_cabin = si::Power::ZERO; + (pwr_thrml_fc_to_cabin, cop) + } + CabinHeatSource::HeatPump => { + // https://en.wikipedia.org/wiki/Coefficient_of_performance#Theoretical_performance_limits + // cop_ideal is t_h / (t_h - t_c) for heating + // cop_ideal is t_c / (t_h - t_c) for cooling + + // divide-by-zero protection and realistic limit on COP + // TODO: make sure this is consist with above commented equation for heating! + let cop_ideal = if te_delta_vs_amb.abs() < 5.0 * uc::KELVIN { + // cabin is cooler than ambient + threshold + // TODO: make this `5.0` not hardcoded + cab_state.temperature / (5.0 * uc::KELVIN) + } else { + cab_state.temperature / te_delta_vs_amb.abs() + }; + let cop = cop_ideal * self.frac_of_ideal_cop; + assert!(cop > 0.0 * uc::R); + if (*pwr_thrml_hvac_to_cabin / self.state.cop) > self.pwr_aux_for_hvac_max { + self.state.pwr_aux_for_hvac = self.pwr_aux_for_hvac_max; + // correct if limit is exceeded + *pwr_thrml_hvac_to_cabin = -self.state.pwr_aux_for_hvac * self.state.cop; + } else { + self.state.pwr_aux_for_hvac = *pwr_thrml_hvac_to_cabin / self.state.cop; + } + #[allow(clippy::let_and_return)] // for readability + let pwr_thrml_fc_to_cabin = si::Power::ZERO; + (pwr_thrml_fc_to_cabin, cop) + } + }; + Ok((pwr_thrml_fc_to_cabin, cop)) + } +} + +#[derive(Clone, Copy, Debug, Deserialize, Serialize, PartialEq, IsVariant)] +pub enum CabinHeatSource { + /// [FuelConverter], if applicable, provides heat for HVAC system + FuelConverter, + /// Resistance heater provides heat for HVAC system + ResistanceHeater, + /// Heat pump provides heat for HVAC system + HeatPump, +} +impl Init for CabinHeatSource {} +impl SerdeAPI for CabinHeatSource {} + +#[fastsim_api] +#[derive( + Clone, Copy, Debug, Default, Deserialize, Serialize, PartialEq, HistoryVec, SetCumulative, +)] +pub struct HVACSystemForLumpedCabinState { + /// time step counter + pub i: u32, + /// portion of total HVAC cooling/heating (negative/positive) power due to proportional gain + pub pwr_p: si::Power, + /// portion of total HVAC cooling/heating (negative/positive) cumulative energy due to proportional gain + pub energy_p: si::Energy, + /// portion of total HVAC cooling/heating (negative/positive) power due to integral gain + pub pwr_i: si::Power, + /// portion of total HVAC cooling/heating (negative/positive) cumulative energy due to integral gain + pub energy_i: si::Energy, + /// portion of total HVAC cooling/heating (negative/positive) power due to derivative gain + pub pwr_d: si::Power, + /// portion of total HVAC cooling/heating (negative/positive) cumulative energy due to derivative gain + pub energy_d: si::Energy, + /// coefficient of performance (i.e. efficiency) of vapor compression cycle + pub cop: si::Ratio, + /// Aux power demand from HVAC system + pub pwr_aux_for_hvac: si::Power, + /// Cumulative aux energy for HVAC system + pub energy_aux: si::Energy, + /// Cumulative energy demand by HVAC system from thermal component (e.g. [FuelConverter]) + pub energy_thermal_req: si::Energy, +} +impl Init for HVACSystemForLumpedCabinState {} +impl SerdeAPI for HVACSystemForLumpedCabinState {} diff --git a/fastsim-core/src/vehicle/hvac/hvac_sys_for_lumped_cabin_and_res.rs b/fastsim-core/src/vehicle/hvac/hvac_sys_for_lumped_cabin_and_res.rs new file mode 100644 index 00000000..016fed55 --- /dev/null +++ b/fastsim-core/src/vehicle/hvac/hvac_sys_for_lumped_cabin_and_res.rs @@ -0,0 +1,470 @@ +use super::*; + +#[fastsim_api] +#[derive(Deserialize, Serialize, Debug, Clone, PartialEq, HistoryMethods)] +/// HVAC system for [LumpedCabin] and [] +pub struct HVACSystemForLumpedCabinAndRES { + /// set point temperature + pub te_set: si::Temperature, + /// deadband range. any cabin temperature within this range of + /// `te_set` results in no HVAC power draw + pub te_deadband: si::Temperature, + /// HVAC proportional gain for cabin + pub p_cabin: si::ThermalConductance, + /// HVAC integral gain [W / K / s] for cabin, resets at zero crossing events + /// NOTE: `uom` crate does not have this unit, but it may be possible to make a custom unit for this + pub i_cabin: f64, + /// value at which state.i stops accumulating for cabin + pub pwr_i_max_cabin: si::Power, + /// HVAC derivative gain [W / K * s] for cabin + /// NOTE: `uom` crate does not have this unit, but it may be possible to make a custom unit for this + pub d_cabin: f64, + /// max HVAC thermal power + /// HVAC proportional gain for [ReversibleEnergyStorage] + pub p_res: si::ThermalConductance, + /// HVAC integral gain [W / K / s] for [ReversibleEnergyStorage], resets at zero crossing events + /// NOTE: `uom` crate does not have this unit, but it may be possible to make a custom unit for this + pub i_res: f64, + /// value at which state.i stops accumulating for [ReversibleEnergyStorage] + pub pwr_i_max_res: si::Power, + /// HVAC derivative gain [W / K * s] for [ReversibleEnergyStorage] + /// NOTE: `uom` crate does not have this unit, but it may be possible to make a custom unit for this + pub d_res: f64, + /// max HVAC thermal power + pub pwr_thermal_max: si::Power, + /// coefficient between 0 and 1 to calculate HVAC efficiency by multiplying by + /// coefficient of performance (COP) + pub frac_of_ideal_cop: f64, + /// cabin heat source + #[api(skip_get, skip_set)] + pub cabin_heat_source: CabinHeatSource, + /// res heat source + #[api(skip_get, skip_set)] + pub res_heat_source: RESHeatSource, + /// res cooling source + #[api(skip_get, skip_set)] + pub res_cooling_source: RESCoolingSource, + /// max allowed aux load + pub pwr_aux_for_hvac_max: si::Power, + /// coefficient of performance of vapor compression cycle + #[serde(default, skip_serializing_if = "EqDefault::eq_default")] + pub state: HVACSystemForLumpedCabinAndRESState, + #[serde( + default, + skip_serializing_if = "HVACSystemForLumpedCabinAndRESStateHistoryVec::is_empty" + )] + pub history: HVACSystemForLumpedCabinAndRESStateHistoryVec, +} +impl Init for HVACSystemForLumpedCabinAndRES {} +impl SerdeAPI for HVACSystemForLumpedCabinAndRES {} +impl HVACSystemForLumpedCabinAndRES { + /// # Arguments + /// - `te_amb_air`: ambient air temperature + /// - `te_fc`: [FuelConverter] temperature, if equipped + /// - `cab_state`: [Cabin] state + /// - `cab_heat_cap`: [Cabin] heat capacity + /// - `res_temp`: [ReversibleEnergyStorage] temperature + /// - `res_temp_prev`: [ReversibleEnergyStorage] temperature at previous time step + /// - `dt`: time step size + /// + /// # Returns + /// - `pwr_thrml_hvac_to_cabin`: thermal power flowing from HVAC system to cabin + /// - `pwr_thrml_fc_to_cabin`: thermal power flowing from [FuelConverter] to cabin + /// - `pwr_thrml_hvac_to_res`: thermal power flowing from HVAC system to + /// [ReversibleEnergyStorage] `thrml` system + pub fn solve( + &mut self, + te_amb_air: si::Temperature, + te_fc: Option, + cab_state: LumpedCabinState, + cab_heat_cap: si::HeatCapacity, + res_temp: si::Temperature, + res_temp_prev: si::Temperature, + dt: si::Time, + ) -> anyhow::Result<(si::Power, si::Power, si::Power)> { + let mut pwr_thrml_hvac_to_cabin = self + .solve_for_cabin(te_fc, cab_state, cab_heat_cap, dt) + .with_context(|| format_dbg!())?; + let mut pwr_thrml_hvac_to_res: si::Power = self + .solve_for_res(res_temp, res_temp_prev, dt) + .with_context(|| format_dbg!())?; + let cop_ideal: si::Ratio = + if pwr_thrml_hvac_to_res + pwr_thrml_hvac_to_cabin > si::Power::ZERO { + // heating mode + // TODO: account for cabin and battery heat sources in COP calculation!!!! + + let (te_ref, te_delta_vs_amb) = if pwr_thrml_hvac_to_res > si::Power::ZERO { + // both powers are positive -- i.e. both are in heating mode + + let te_ref: si::Temperature = if cab_state.temperature > res_temp { + // cabin is hotter + cab_state.temperature + } else { + // battery is hotter + res_temp + }; + (te_ref, te_ref - te_amb_air) + } else if pwr_thrml_hvac_to_res >= si::Power::ZERO { + // `pwr_thrml_hvac_to_res` dominates need for heating + (res_temp, res_temp - te_amb_air) + } else { + // `pwr_thrml_hvac_to_res` dominates need for heating + (cab_state.temperature, cab_state.temperature - te_amb_air) + }; + + // https://en.wikipedia.org/wiki/Coefficient_of_performance#Theoretical_performance_limits + // cop_ideal is t_h / (t_h - t_c) for heating + // cop_ideal is t_c / (t_h - t_c) for cooling + + // divide-by-zero protection and realistic limit on COP + // TODO: make sure this is consistent with above commented equation for heating! + if te_delta_vs_amb.abs() < 5.0 * uc::KELVIN { + // cabin is cooler than ambient + threshold + // TODO: make this `5.0` not hardcoded + te_ref / (5.0 * uc::KELVIN) + } else { + te_ref / te_delta_vs_amb.abs() + } + } else if pwr_thrml_hvac_to_res + pwr_thrml_hvac_to_cabin < si::Power::ZERO { + // cooling mode + // TODO: account for battery cooling source in COP calculation!!!! + + let (te_ref, te_delta_vs_amb) = if pwr_thrml_hvac_to_res < si::Power::ZERO { + // both powers are negative -- i.e. both are in cooling mode + + let te_ref: si::Temperature = if cab_state.temperature < res_temp { + // cabin is colder + cab_state.temperature + } else { + // battery is colder + res_temp + }; + (te_ref, te_ref - te_amb_air) + } else if pwr_thrml_hvac_to_res >= si::Power::ZERO { + // `pwr_thrml_hvac_to_cabin` dominates need for cooling + (cab_state.temperature, cab_state.temperature - te_amb_air) + } else { + // `pwr_thrml_hvac_to_res` dominates need for cooling + (res_temp, res_temp - te_amb_air) + }; + + // https://en.wikipedia.org/wiki/Coefficient_of_performance#Theoretical_performance_limits + // cop_ideal is t_h / (t_h - t_c) for heating + // cop_ideal is t_c / (t_h - t_c) for cooling + + // divide-by-zero protection and realistic limit on COP + if te_delta_vs_amb.abs() < 5.0 * uc::KELVIN { + // cooling-dominating component is cooler than ambient + threshold + // TODO: make this `5.0` not hardcoded + te_ref / (5.0 * uc::KELVIN) + } else { + te_ref / te_delta_vs_amb.abs() + } + } else { + si::Ratio::ZERO + }; + self.state.cop = cop_ideal * self.frac_of_ideal_cop; + assert!(self.state.cop > 0.0 * uc::R); + + let mut pwr_thrml_fc_to_cabin = si::Power::ZERO; + self.state.pwr_aux_for_hvac = if pwr_thrml_hvac_to_cabin > si::Power::ZERO { + match self.cabin_heat_source { + CabinHeatSource::FuelConverter => { + pwr_thrml_fc_to_cabin = pwr_thrml_hvac_to_cabin; + // NOTE: should make this scale with power demand + si::Power::ZERO + } + CabinHeatSource::ResistanceHeater => pwr_thrml_hvac_to_cabin, + CabinHeatSource::HeatPump => pwr_thrml_hvac_to_cabin * self.state.cop, + } + } else { + -pwr_thrml_hvac_to_cabin * self.state.cop + } + if pwr_thrml_hvac_to_res > si::Power::ZERO { + match self.res_heat_source { + RESHeatSource::ResistanceHeater => pwr_thrml_hvac_to_res, + RESHeatSource::HeatPump => pwr_thrml_hvac_to_res * self.state.cop, + RESHeatSource::None => { + pwr_thrml_hvac_to_res = si::Power::ZERO; + si::Power::ZERO + } + } + } else { + match self.res_cooling_source { + RESCoolingSource::HVAC => -pwr_thrml_hvac_to_res * self.state.cop, + RESCoolingSource::None => { + pwr_thrml_hvac_to_res = si::Power::ZERO; + si::Power::ZERO + } + } + }; + + self.state.pwr_aux_for_hvac = if self.state.pwr_aux_for_hvac > self.pwr_aux_for_hvac_max { + pwr_thrml_hvac_to_res = + self.pwr_aux_for_hvac_max * self.state.cop * pwr_thrml_hvac_to_res + / (pwr_thrml_hvac_to_res + pwr_thrml_hvac_to_cabin); + pwr_thrml_hvac_to_cabin = + self.pwr_aux_for_hvac_max * self.state.cop * pwr_thrml_hvac_to_cabin + / (pwr_thrml_hvac_to_res + pwr_thrml_hvac_to_cabin); + if pwr_thrml_hvac_to_cabin > si::Power::ZERO + && self.cabin_heat_source.is_fuel_converter() + { + pwr_thrml_fc_to_cabin = pwr_thrml_hvac_to_cabin; + } + self.pwr_aux_for_hvac_max + } else { + self.state.pwr_aux_for_hvac + }; + + Ok(( + pwr_thrml_hvac_to_cabin, + pwr_thrml_fc_to_cabin, + pwr_thrml_hvac_to_res, + )) + } + + fn solve_for_cabin( + &mut self, + te_fc: Option, + cab_state: LumpedCabinState, + cab_heat_cap: si::HeatCapacity, + dt: si::Time, + ) -> anyhow::Result { + let pwr_thrml_hvac_to_cabin = if cab_state.temperature <= self.te_set + self.te_deadband + && cab_state.temperature >= self.te_set - self.te_deadband + { + // inside deadband; no hvac power is needed + + self.state.pwr_i = si::Power::ZERO; // reset to 0.0 + self.state.pwr_p = si::Power::ZERO; + self.state.pwr_d = si::Power::ZERO; + si::Power::ZERO + } else { + // outside deadband + let te_delta_vs_set = cab_state.temperature - self.te_set; + + self.state.pwr_p = -self.p_cabin * te_delta_vs_set; + self.state.pwr_i -= self.i_cabin * uc::W / uc::KELVIN / uc::S * te_delta_vs_set * dt; + self.state.pwr_i = self + .state + .pwr_i + .max(-self.pwr_i_max_cabin) + .min(self.pwr_i_max_cabin); + self.state.pwr_d = -self.d_cabin * uc::J / uc::KELVIN + * ((cab_state.temperature - cab_state.temp_prev) / dt); + + let pwr_thrml_hvac_to_cabin: si::Power = + if cab_state.temperature > self.te_set + self.te_deadband { + // COOLING MODE; cabin is hotter than set point + + if self.state.pwr_i > si::Power::ZERO { + // If `pwr_i` is greater than zero, reset to switch from heating to cooling + self.state.pwr_i = si::Power::ZERO; + } + let mut pwr_thrml_hvac_to_cab = + (self.state.pwr_p + self.state.pwr_i + self.state.pwr_d) + .max(-self.pwr_thermal_max); + + if (-pwr_thrml_hvac_to_cab / self.state.cop) > self.pwr_aux_for_hvac_max { + self.state.pwr_aux_for_hvac = self.pwr_aux_for_hvac_max; + // correct if limit is exceeded + pwr_thrml_hvac_to_cab = -self.state.pwr_aux_for_hvac * self.state.cop; + } else { + self.state.pwr_aux_for_hvac = pwr_thrml_hvac_to_cab / self.state.cop; + } + pwr_thrml_hvac_to_cab + } else { + // HEATING MODE; cabin is colder than set point + + if self.state.pwr_i < si::Power::ZERO { + // If `pwr_i` is less than zero reset to switch from cooling to heating + self.state.pwr_i = si::Power::ZERO; + } + let mut pwr_thrml_hvac_to_cabin: si::Power = + (-self.state.pwr_p - self.state.pwr_i - self.state.pwr_d) + .min(self.pwr_thermal_max); + + // Assumes blower has negligible impact on aux load, may want to revise later + self.handle_cabin_heat_source( + te_fc, + &mut pwr_thrml_hvac_to_cabin, + cab_heat_cap, + cab_state, + dt, + ) + .with_context(|| format_dbg!())?; + pwr_thrml_hvac_to_cabin + }; + pwr_thrml_hvac_to_cabin + }; + Ok(pwr_thrml_hvac_to_cabin) + } + + fn solve_for_res( + &mut self, + // reversible energy storage temp + res_temp: si::Temperature, + // reversible energy storage temp at previous time step + res_temp_prev: si::Temperature, + dt: si::Time, + ) -> anyhow::Result { + let pwr_thrml_hvac_to_res = if res_temp <= self.te_set + self.te_deadband + && res_temp >= self.te_set - self.te_deadband + { + // inside deadband; no hvac power is needed + + self.state.pwr_i_res = si::Power::ZERO; // reset to 0.0 + self.state.pwr_p_res = si::Power::ZERO; + self.state.pwr_d_res = si::Power::ZERO; + si::Power::ZERO + } else { + // outside deadband + let te_delta_vs_set = res_temp - self.te_set; + self.state.pwr_p_res = -self.p_res * te_delta_vs_set; + self.state.pwr_i_res -= self.i_res * uc::W / uc::KELVIN / uc::S * te_delta_vs_set * dt; + self.state.pwr_i_res = self + .state + .pwr_i_res + .max(-self.pwr_i_max_res) + .min(self.pwr_i_max_res); + self.state.pwr_d_res = + -self.d_res * uc::J / uc::KELVIN * ((res_temp - res_temp_prev) / dt); + + let pwr_thrml_hvac_to_res: si::Power = if res_temp > self.te_set + self.te_deadband { + // COOLING MODE; Reversible Energy Storage is hotter than set point + + if self.state.pwr_i_res > si::Power::ZERO { + // If `pwr_i_res` is greater than zero, reset to switch from heating to cooling + self.state.pwr_i_res = si::Power::ZERO; + } + let mut pwr_thrml_hvac_to_res = + (self.state.pwr_p_res + self.state.pwr_i_res + self.state.pwr_d_res) + .max(-self.pwr_thermal_max); + + if (-pwr_thrml_hvac_to_res / self.state.cop) > self.pwr_aux_for_hvac_max { + self.state.pwr_aux_for_hvac = self.pwr_aux_for_hvac_max; + // correct if limit is exceeded + pwr_thrml_hvac_to_res = -self.state.pwr_aux_for_hvac * self.state.cop; + } else { + self.state.pwr_aux_for_hvac = pwr_thrml_hvac_to_res / self.state.cop; + } + pwr_thrml_hvac_to_res + } else { + // HEATING MODE; Reversible Energy Storage is colder than set point + + if self.state.pwr_i_res < si::Power::ZERO { + // If `pwr_i_res` is less than zero reset to switch from cooling to heating + self.state.pwr_i_res = si::Power::ZERO; + } + let pwr_thrml_hvac_to_res = + (-self.state.pwr_p_res - self.state.pwr_i_res - self.state.pwr_d_res) + .min(self.pwr_thermal_max); + pwr_thrml_hvac_to_res + }; + pwr_thrml_hvac_to_res + }; + Ok(pwr_thrml_hvac_to_res) + } + + fn handle_cabin_heat_source( + &mut self, + te_fc: Option, + pwr_thrml_hvac_to_cabin: &mut si::Power, + cab_heat_cap: si::HeatCapacity, + cab_state: LumpedCabinState, + dt: si::Time, + ) -> anyhow::Result<()> { + match self.cabin_heat_source { + CabinHeatSource::FuelConverter => { + ensure!( + te_fc.is_some(), + "{}\nExpected vehicle with [FuelConverter] with thermal plant model.", + format_dbg!() + ); + // limit heat transfer to be substantially less than what is physically possible + // i.e. the engine can't drop below cabin temperature to heat the cabin + *pwr_thrml_hvac_to_cabin = pwr_thrml_hvac_to_cabin + .min( + cab_heat_cap * + (te_fc.unwrap() - cab_state.temperature) + * 0.1 // so that it's substantially less + / dt, + ) + .max(si::Power::ZERO); + } + CabinHeatSource::ResistanceHeater => { + *pwr_thrml_hvac_to_cabin = si::Power::ZERO; + } + CabinHeatSource::HeatPump => { + *pwr_thrml_hvac_to_cabin = si::Power::ZERO; + } + }; + Ok(()) + } +} + +#[fastsim_api] +#[derive( + Clone, Copy, Debug, Default, Deserialize, Serialize, PartialEq, HistoryVec, SetCumulative, +)] +pub struct HVACSystemForLumpedCabinAndRESState { + /// time step counter + pub i: u32, + /// portion of total HVAC cooling/heating (negative/positive) power due to proportional gain + pub pwr_p: si::Power, + /// portion of total HVAC cooling/heating (negative/positive) cumulative energy due to proportional gain + pub energy_p: si::Energy, + /// portion of total HVAC cooling/heating (negative/positive) power due to integral gain + pub pwr_i: si::Power, + /// portion of total HVAC cooling/heating (negative/positive) cumulative energy due to integral gain + pub energy_i: si::Energy, + /// portion of total HVAC cooling/heating (negative/positive) power due to derivative gain + pub pwr_d: si::Power, + /// portion of total HVAC cooling/heating (negative/positive) cumulative energy due to derivative gain + pub energy_d: si::Energy, + /// portion of total HVAC cooling/heating (negative/positive) power to [ReversibleEnergyStorage] due to proportional gain + pub pwr_p_res: si::Power, + /// portion of total HVAC cooling/heating (negative/positive) cumulative energy to [ReversibleEnergyStorage] due to proportional gain + pub energy_p_res: si::Energy, + /// portion of total HVAC cooling/heating (negative/positive) power to [ReversibleEnergyStorage] due to integral gain + pub pwr_i_res: si::Power, + /// portion of total HVAC cooling/heating (negative/positive) cumulative energy to [ReversibleEnergyStorage] due to integral gain + pub energy_i_res: si::Energy, + /// portion of total HVAC cooling/heating (negative/positive) power to [ReversibleEnergyStorage] due to derivative gain + pub pwr_d_res: si::Power, + /// portion of total HVAC cooling/heating (negative/positive) cumulative energy to [ReversibleEnergyStorage] due to derivative gain + pub energy_d_res: si::Energy, + /// coefficient of performance (i.e. efficiency) of vapor compression cycle + pub cop: si::Ratio, + /// Au power demand from HVAC system + pub pwr_aux_for_hvac: si::Power, + /// Cumulative aux energy for HVAC system + pub energy_aux: si::Energy, + /// Cumulative energy demand by HVAC system from thermal component (e.g. [FuelConverter]) + pub energy_thermal_req: si::Energy, +} +impl Init for HVACSystemForLumpedCabinAndRESState {} +impl SerdeAPI for HVACSystemForLumpedCabinAndRESState {} + +#[derive(Clone, Copy, Debug, Deserialize, Serialize, PartialEq)] +/// Heat source for [RESLumpedThermal] +pub enum RESHeatSource { + /// Resistance heater provides heat for HVAC system + ResistanceHeater, + /// Heat pump provides heat for HVAC system + HeatPump, + /// The battery is not actively heated + None, +} +impl Init for RESHeatSource {} +impl SerdeAPI for RESHeatSource {} + +#[derive(Clone, Copy, Debug, Deserialize, Serialize, PartialEq)] +/// Cooling source for [RESLumpedThermal] +pub enum RESCoolingSource { + /// Vapor compression system used for cabin HVAC also cools [RESLumpedThermal] + HVAC, + /// [RESLumpedThermal] is not actively cooled + None, +} +impl Init for RESCoolingSource {} +impl SerdeAPI for RESCoolingSource {} diff --git a/fastsim-core/src/vehicle/powertrain/fuel_converter.rs b/fastsim-core/src/vehicle/powertrain/fuel_converter.rs index f386f9a1..0ae34112 100755 --- a/fastsim-core/src/vehicle/powertrain/fuel_converter.rs +++ b/fastsim-core/src/vehicle/powertrain/fuel_converter.rs @@ -300,8 +300,6 @@ impl FuelConverter { ) ); - // TODO: consider how idle is handled. The goal is to make it so that even if `self.state.pwr_aux` is - // zero, there will be fuel consumption to overcome internal dissipation. self.state.pwr_fuel = if self.state.fc_on { ((pwr_out_req + self.state.pwr_aux) / self.state.eff).max(self.pwr_idle_fuel) } else { @@ -324,7 +322,7 @@ impl FuelConverter { &mut self, te_amb: si::Temperature, pwr_thrl_fc_to_cab: si::Power, - veh_state: VehicleState, + veh_state: &mut VehicleState, dt: si::Time, ) -> anyhow::Result<()> { let veh_speed = veh_state.speed_ach; diff --git a/fastsim-core/src/vehicle/powertrain/reversible_energy_storage.rs b/fastsim-core/src/vehicle/powertrain/reversible_energy_storage.rs index afd3574e..613ac8b8 100644 --- a/fastsim-core/src/vehicle/powertrain/reversible_energy_storage.rs +++ b/fastsim-core/src/vehicle/powertrain/reversible_energy_storage.rs @@ -253,10 +253,18 @@ impl ReversibleEnergyStorage { /// # Arguments /// - `fc_state`: [ReversibleEnergyStorage] state /// - `te_amb`: ambient temperature + /// - `pwr_thrml_hvac_to_res`: thermal power flowing from HVAC system to [ReversibleEnergyStorage] + /// - `te_cab`: cabin temperature for heat transfer interaction with [ReversiblEnergyStorage] /// - `dt`: time step size - pub fn solve_thermal(&mut self, te_amb: si::Temperature, dt: si::Time) -> anyhow::Result<()> { + pub fn solve_thermal( + &mut self, + te_amb: si::Temperature, + pwr_thrml_hvac_to_res: si::Power, + te_cab: si::Temperature, + dt: si::Time, + ) -> anyhow::Result<()> { self.thrml - .solve(self.state, te_amb, dt) + .solve(self.state, te_amb, pwr_thrml_hvac_to_res, te_cab, dt) .with_context(|| format_dbg!()) } @@ -496,6 +504,15 @@ impl ReversibleEnergyStorage { RESThermalOption::None => None, } } + + /// If thermal model is appropriately configured, returns lumped [Self] + /// temperature at previous time step + pub fn temp_prev(&self) -> Option { + match &self.thrml { + RESThermalOption::RESLumpedThermal(rest) => Some(rest.state.temp_prev), + RESThermalOption::None => None, + } + } } impl SetCumulative for ReversibleEnergyStorage { @@ -706,16 +723,19 @@ impl RESThermalOption { /// # Arguments /// - `res_state`: [ReversibleEnergyStorage] state /// - `te_amb`: ambient temperature + /// - `pwr_thrml_hvac_to_res`: thermal power flowing from HVAC system to [ReversibleEnergyStorage] /// - `dt`: time step size fn solve( &mut self, res_state: ReversibleEnergyStorageState, te_amb: si::Temperature, + pwr_thrml_hvac_to_res: si::Power, + te_cab: si::Temperature, dt: si::Time, ) -> anyhow::Result<()> { match self { Self::RESLumpedThermal(rest) => rest - .solve(res_state, te_amb, dt) + .solve(res_state, te_amb, pwr_thrml_hvac_to_res, te_cab, dt) .with_context(|| format_dbg!())?, Self::None => { // TODO: make sure this triggers error if appropriate @@ -732,17 +752,18 @@ pub struct RESLumpedThermal { /// [ReversibleEnergyStorage] thermal capacitance pub heat_capacitance: si::HeatCapacity, /// parameter for heat transfer coeff from [ReversibleEnergyStorage::thrml] to ambient - pub htc_to_amb: si::HeatTransferCoeff, + pub htc_to_amb: si::ThermalConductance, /// parameter for heat transfer coeff from [ReversibleEnergyStorage::thrml] to cabin - pub htc_to_cab: si::HeatTransferCoeff, - /// Thermal management system - pub cntrl_sys: RESThermalControlSystem, + pub htc_to_cab: si::ThermalConductance, /// current state #[serde(default, skip_serializing_if = "EqDefault::eq_default")] - pub state: RESThermalState, + pub state: RESLumpedThermalState, /// history of state - #[serde(default, skip_serializing_if = "RESThermalStateHistoryVec::is_empty")] - pub history: RESThermalStateHistoryVec, + #[serde( + default, + skip_serializing_if = "RESLumpedThermalStateHistoryVec::is_empty" + )] + pub history: RESLumpedThermalStateHistoryVec, // TODO: add `save_interval` and associated methods } @@ -753,204 +774,54 @@ impl RESLumpedThermal { &mut self, res_state: ReversibleEnergyStorageState, te_amb: si::Temperature, + pwr_thrml_hvac_to_res: si::Power, + te_cab: si::Temperature, dt: si::Time, ) -> anyhow::Result<()> { - todo!(); + self.state.temp_prev = self.state.temperature; + self.state.pwr_thrml_from_cabin = self.htc_to_cab * (te_cab - self.state.temperature); + self.state.pwr_thrml_from_amb = self.htc_to_cab * (te_amb - self.state.temperature); + self.state.temperature += (pwr_thrml_hvac_to_res + + res_state.pwr_out_electrical * (1.0 * uc::R - res_state.eff) + + self.state.pwr_thrml_from_cabin + + self.state.pwr_thrml_from_amb) + / self.heat_capacitance + * dt; Ok(()) } } #[fastsim_api] #[derive(Clone, Copy, Debug, Deserialize, Serialize, PartialEq, HistoryVec, SetCumulative)] -pub struct RESThermalState { +pub struct RESLumpedThermalState { /// time step index pub i: usize, /// Current thermal mass temperature pub temperature: si::Temperature, /// Thermal mass temperature at previous time step pub temp_prev: si::Temperature, + /// Thermal power flow to [RESLumpedThermal] from cabin + pub pwr_thrml_from_cabin: si::Power, + /// Thermal power flow to [RESLumpedThermal] from ambient + pub pwr_thrml_from_amb: si::Power, + /// Cumulatev thermal energy flow to [RESLumpedThermal] from cabin + pub energy_thrml_from_cabin: si::Energy, + /// Cumulatev thermal energy flow to [RESLumpedThermal] from ambient + pub energy_thrml_from_amb: si::Energy, } -impl Init for RESThermalState {} -impl SerdeAPI for RESThermalState {} -impl Default for RESThermalState { +impl Init for RESLumpedThermalState {} +impl SerdeAPI for RESLumpedThermalState {} +impl Default for RESLumpedThermalState { fn default() -> Self { Self { i: Default::default(), temperature: *TE_STD_AIR, temp_prev: *TE_STD_AIR, + pwr_thrml_from_cabin: Default::default(), + energy_thrml_from_cabin: Default::default(), + pwr_thrml_from_amb: Default::default(), + energy_thrml_from_amb: Default::default(), } } } - -#[fastsim_api] -#[derive(Deserialize, Serialize, Debug, Clone, PartialEq, HistoryMethods)] -/// HVAC system for [LumpedCabin] -pub struct RESThermalControlSystem { - /// set point temperature - pub te_set: si::Temperature, - /// deadband range. any cabin temperature within this range of - /// `te_set` results in no HVAC power draw - pub te_deadband: si::Temperature, - /// HVAC proportional gain - pub p: si::ThermalConductance, - /// HVAC integral gain [W / K / s], resets at zero crossing events - /// NOTE: `uom` crate does not have this unit, but it may be possible to make a custom unit for this - pub i: f64, - /// value at which [Self::i] stops accumulating - pub pwr_i_max: si::Power, - /// HVAC derivative gain [W / K * s] - /// NOTE: `uom` crate does not have this unit, but it may be possible to make a custom unit for this - pub d: f64, - /// max HVAC thermal power - pub pwr_thermal_max: si::Power, - /// coefficient between 0 and 1 to calculate HVAC efficiency by multiplying by - /// coefficient of performance (COP) - pub frac_of_ideal_cop: f64, - /// heat source - #[api(skip_get, skip_set)] - pub heat_source: RESHeatSource, - /// max allowed aux load - pub pwr_aux_max: si::Power, - /// coefficient of performance of vapor compression cycle - #[serde(default, skip_serializing_if = "EqDefault::eq_default")] - pub state: RESThermalControlSystemState, - #[serde( - default, - skip_serializing_if = "RESThermalControlSystemStateHistoryVec::is_empty" - )] - pub history: RESThermalControlSystemStateHistoryVec, - // TODO: add `save_interval` and associated methods -} -impl Init for RESThermalControlSystem {} -impl SerdeAPI for RESThermalControlSystem {} -impl RESThermalControlSystem { - pub fn get_pwr_thermal( - &mut self, - te_amb_air: si::Temperature, - res_thrml_state: RESThermalState, - dt: si::Time, - ) -> anyhow::Result { - let pwr_from_hvac = if res_thrml_state.temperature <= self.te_set + self.te_deadband - && res_thrml_state.temperature >= self.te_set - self.te_deadband - { - // inside deadband; no hvac power is needed - - self.state.pwr_i = si::Power::ZERO; // reset to 0.0 - self.state.pwr_p = si::Power::ZERO; - self.state.pwr_d = si::Power::ZERO; - si::Power::ZERO - } else { - let te_delta_vs_set = res_thrml_state.temperature - self.te_set; - let te_delta_vs_amb: si::Temperature = res_thrml_state.temperature - te_amb_air; - - self.state.pwr_p = -self.p * te_delta_vs_set; - self.state.pwr_i -= self.i * uc::W / uc::KELVIN / uc::S * te_delta_vs_set * dt; - self.state.pwr_i = self.state.pwr_i.max(-self.pwr_i_max).min(self.pwr_i_max); - self.state.pwr_d = -self.d * uc::J / uc::KELVIN - * ((res_thrml_state.temperature - res_thrml_state.temp_prev) / dt); - - // https://en.wikipedia.org/wiki/Coefficient_of_performance#Theoretical_performance_limits - // cop_ideal is t_h / (t_h - t_c) for heating - // cop_ideal is t_c / (t_h - t_c) for cooling - - // divide-by-zero protection and realistic limit on COP - let cop_ideal = if te_delta_vs_amb.abs() < 5.0 * uc::KELVIN { - // cabin is cooler than ambient + threshold - // TODO: make this `5.0` not hardcoded - res_thrml_state.temperature / (5.0 * uc::KELVIN) - } else { - res_thrml_state.temperature / te_delta_vs_amb.abs() - }; - self.state.cop = cop_ideal * self.frac_of_ideal_cop; - assert!(self.state.cop > 0.0 * uc::R); - - if res_thrml_state.temperature > self.te_set + self.te_deadband { - // COOLING MODE; cabin is hotter than set point - - if self.state.pwr_i > si::Power::ZERO { - // If `pwr_i` is greater than zero, reset to switch from heating to cooling - self.state.pwr_i = si::Power::ZERO; - } - let mut pwr_thermal_from_hvac = - (self.state.pwr_p + self.state.pwr_i + self.state.pwr_d) - .max(-self.pwr_thermal_max); - - if (-pwr_thermal_from_hvac / self.state.cop) > self.pwr_aux_max { - self.state.pwr_aux = self.pwr_aux_max; - // correct if limit is exceeded - pwr_thermal_from_hvac = -self.state.pwr_aux * self.state.cop; - } else { - self.state.pwr_aux = pwr_thermal_from_hvac / self.state.cop; - } - pwr_thermal_from_hvac - } else { - // HEATING MODE; cabin is colder than set point - - if self.state.pwr_i < si::Power::ZERO { - // If `pwr_i` is less than zero reset to switch from cooling to heating - self.state.pwr_i = si::Power::ZERO; - } - let mut pwr_thermal_from_hvac = - (-self.state.pwr_p - self.state.pwr_i - self.state.pwr_d) - .min(self.pwr_thermal_max); - - // Assumes blower has negligible impact on aux load, may want to revise later - match &self.heat_source { - RESHeatSource::SelfHeating => { - todo!() - } - RESHeatSource::HeatPump => { - todo!() - } - RESHeatSource::None => {} - } - pwr_thermal_from_hvac - } - }; - Ok(pwr_from_hvac) - } -} - -#[derive(Clone, Copy, Debug, Deserialize, Serialize, PartialEq)] -pub enum RESHeatSource { - /// Self heating - SelfHeating, - /// Heat pump provides heat for HVAC system - HeatPump, - /// No active heat source for [RESThermal] - None, -} -impl Init for RESHeatSource {} -impl SerdeAPI for RESHeatSource {} - -#[fastsim_api] -#[derive( - Clone, Copy, Debug, Default, Deserialize, Serialize, PartialEq, HistoryVec, SetCumulative, -)] -pub struct RESThermalControlSystemState { - /// time step counter - pub i: u32, - /// portion of total HVAC cooling/heating (negative/positive) power due to proportional gain - pub pwr_p: si::Power, - /// portion of total HVAC cooling/heating (negative/positive) cumulative energy due to proportional gain - pub energy_p: si::Energy, - /// portion of total HVAC cooling/heating (negative/positive) power due to integral gain - pub pwr_i: si::Power, - /// portion of total HVAC cooling/heating (negative/positive) cumulative energy due to integral gain - pub energy_i: si::Energy, - /// portion of total HVAC cooling/heating (negative/positive) power due to derivative gain - pub pwr_d: si::Power, - /// portion of total HVAC cooling/heating (negative/positive) cumulative energy due to derivative gain - pub energy_d: si::Energy, - /// coefficient of performance (i.e. efficiency) of vapor compression cycle - pub cop: si::Ratio, - /// Aux power demand from HVAC system - pub pwr_aux: si::Power, - /// Cumulative aux energy for HVAC system - pub energy_aux: si::Energy, - /// Cumulative energy demand by HVAC system from thermal component (e.g. [FuelConverter]) - pub energy_thermal_req: si::Energy, -} -impl Init for RESThermalControlSystemState {} -impl SerdeAPI for RESThermalControlSystemState {} diff --git a/fastsim-core/src/vehicle/powertrain/traits.rs b/fastsim-core/src/vehicle/powertrain/traits.rs index 83322400..93a845a7 100644 --- a/fastsim-core/src/vehicle/powertrain/traits.rs +++ b/fastsim-core/src/vehicle/powertrain/traits.rs @@ -39,7 +39,7 @@ pub trait Powertrain { &mut self, te_amb: si::Temperature, pwr_thrl_fc_to_cab: si::Power, - veh_state: VehicleState, + veh_state: &mut VehicleState, dt: si::Time, ) -> anyhow::Result<()>; diff --git a/fastsim-core/src/vehicle/powertrain_type.rs b/fastsim-core/src/vehicle/powertrain_type.rs index bf16de9c..3e1c39e6 100644 --- a/fastsim-core/src/vehicle/powertrain_type.rs +++ b/fastsim-core/src/vehicle/powertrain_type.rs @@ -51,7 +51,7 @@ impl Powertrain for PowertrainType { &mut self, te_amb: si::Temperature, pwr_thrl_fc_to_cab: si::Power, - veh_state: VehicleState, + veh_state: &mut VehicleState, dt: si::Time, ) -> anyhow::Result<()> { match self { diff --git a/fastsim-core/src/vehicle/vehicle_model.rs b/fastsim-core/src/vehicle/vehicle_model.rs index bd7fbb50..5c5c96fd 100644 --- a/fastsim-core/src/vehicle/vehicle_model.rs +++ b/fastsim-core/src/vehicle/vehicle_model.rs @@ -141,8 +141,6 @@ pub struct Vehicle { /// Baseline power required by auxilliary systems pub pwr_aux_base: si::Power, - /// Max power available for auxilliary systems - pub pwr_aux_max: si::Power, /// transmission efficiency // TODO: check if `trans_eff` is redundant (most likely) and fix @@ -410,8 +408,6 @@ impl Vehicle { /// Solves for energy consumption pub fn solve_powertrain(&mut self, dt: si::Time) -> anyhow::Result<()> { - // TODO: do something more sophisticated with pwr_aux - self.state.pwr_aux = self.pwr_aux_base; self.pt_type .solve( self.state.pwr_tractive, @@ -426,12 +422,11 @@ impl Vehicle { } pub fn set_curr_pwr_out_max(&mut self, dt: si::Time) -> anyhow::Result<()> { - // TODO: when a fancier model for `pwr_aux` is implemented, put it here // TODO: make transmission field in vehicle and make it be able to produce an efficiency - // TODO: account for traction limits here + // TODO: account for traction limits here or somewhere? self.pt_type - .set_curr_pwr_prop_out_max(self.pwr_aux_base, dt, self.state) + .set_curr_pwr_prop_out_max(self.state.pwr_aux, dt, self.state) .with_context(|| anyhow!(format_dbg!()))?; (self.state.pwr_prop_fwd_max, self.state.pwr_prop_bwd_max) = self @@ -445,27 +440,50 @@ impl Vehicle { pub fn solve_thermal( &mut self, te_amb_air: si::Temperature, - veh_state: VehicleState, dt: si::Time, ) -> anyhow::Result<()> { let te_fc: Option = self.fc().and_then(|fc| fc.temperature()); + let veh_state = &mut self.state; let pwr_thrml_fc_to_cabin = match (&mut self.cabin, &mut self.hvac) { - (CabinOption::None, HVACOption::None) => si::Power::ZERO, + (CabinOption::None, HVACOption::None) => { + veh_state.pwr_aux = self.pwr_aux_base; + si::Power::ZERO + } (CabinOption::LumpedCabin(cab), HVACOption::LumpedCabin(hvac)) => { let (pwr_thrml_hvac_to_cabin, pwr_thrml_fc_to_cab) = hvac .solve(te_amb_air, te_fc, cab.state, cab.heat_capacitance, dt) .with_context(|| format_dbg!())?; cab.solve(te_amb_air, veh_state, pwr_thrml_hvac_to_cabin, dt) .with_context(|| format_dbg!())?; + veh_state.pwr_aux = self.pwr_aux_base + hvac.state.pwr_aux_for_hvac; pwr_thrml_fc_to_cab } (CabinOption::LumpedCabin(cab), HVACOption::LumpedCabinAndRES(hvac)) => { - todo!("Connect HVAC system to RES."); - let (pwr_thrml_hvac_to_cabin, pwr_thrml_fc_to_cab) = hvac - .solve(te_amb_air, te_fc, cab.state, cab.heat_capacitance, dt) + let res = self.res_mut().with_context(|| format_dbg!("`HVACOption::LumpedCabinAndRES(...)` requires powertrain with `ReversibleEnergyStorage`"))?; + let (pwr_thrml_hvac_to_cabin, pwr_thrml_fc_to_cab, pwr_thrml_hvac_to_res) = hvac + .solve( + te_amb_air, + te_fc, + cab.state, + cab.heat_capacitance, + res.temperature().with_context(|| { + format_dbg!( + "`ReversibleEnergyStorage` must be configured with thermal model." + ) + })?, + res.temp_prev().with_context(|| { + format_dbg!( + "`ReversibleEnergyStorage` must be configured with thermal model." + ) + })?, + dt, + ) .with_context(|| format_dbg!())?; cab.solve(te_amb_air, veh_state, pwr_thrml_hvac_to_cabin, dt) .with_context(|| format_dbg!())?; + res.solve_thermal(te_amb_air, pwr_thrml_hvac_to_res, cab.state.temperature, dt) + .with_context(|| format_dbg!())?; + veh_state.pwr_aux = self.pwr_aux_base + hvac.state.pwr_aux_for_hvac; pwr_thrml_fc_to_cab } (CabinOption::LumpedCabinWithShell, HVACOption::LumpedCabinWithShell) => { diff --git a/fastsim-core/src/vehicle/vehicle_model/fastsim2_interface.rs b/fastsim-core/src/vehicle/vehicle_model/fastsim2_interface.rs index 7d48558d..4c20ceb8 100644 --- a/fastsim-core/src/vehicle/vehicle_model/fastsim2_interface.rs +++ b/fastsim-core/src/vehicle/vehicle_model/fastsim2_interface.rs @@ -18,8 +18,6 @@ impl TryFrom for Vehicle { cabin: Default::default(), hvac: Default::default(), pwr_aux_base: f2veh.aux_kw * uc::KW, - // high value to make sure it has no effect - pwr_aux_max: f2veh.aux_kw * 2. * uc::KW, trans_eff: f2veh.trans_eff * uc::R, state: Default::default(), save_interval,