diff --git a/src/core/controls/time_control.rs b/src/core/controls/time_control.rs index 99abf22..f5e99e9 100644 --- a/src/core/controls/time_control.rs +++ b/src/core/controls/time_control.rs @@ -1,7 +1,6 @@ // This module provides structs to model time controls use crate::simulation_time::{SimulationTimeIteration, HOURS_IN_DAY}; -use itertools::sorted; /// An object to model a time-only control with on/off (not modulating) operation pub struct OnOffTimeControl { diff --git a/src/core/space_heat_demand/building_element.rs b/src/core/space_heat_demand/building_element.rs index ae68ded..e32a9aa 100644 --- a/src/core/space_heat_demand/building_element.rs +++ b/src/core/space_heat_demand/building_element.rs @@ -1,3 +1,4 @@ +use crate::core::space_heat_demand::zone::NamedBuildingElement; use crate::core::units::average_monthly_to_annual; use crate::external_conditions::{CalculatedDirectDiffuseTotalIrradiance, ExternalConditions}; use crate::input::{BuildingElement, MassDistributionClass}; @@ -420,6 +421,24 @@ pub fn h_ri_for(_: &BuildingElement) -> f64 { H_RI } +pub fn mid_height_for(element: &BuildingElement) -> Option { + match element { + BuildingElement::Transparent { + base_height, + height, + .. + } => Some(base_height + height / 2.0), + _ => None, + } +} + +pub fn orientation_for(element: &BuildingElement) -> Option { + match element { + BuildingElement::Transparent { orientation, .. } => Some(*orientation), + _ => None, + } +} + #[derive(Debug, PartialEq)] enum HeatFlowDirection { Horizontal, @@ -427,6 +446,11 @@ enum HeatFlowDirection { Downwards, } +pub fn element_from_named(named_element: &NamedBuildingElement) -> &BuildingElement { + let NamedBuildingElement { element, .. } = named_element; + element +} + // Thermal properties of ground from BS EN ISO 13370:2017 Table 7 // Use values for clay or silt (same as BR 443 and SAP 10) const THERMAL_CONDUCTIVITY_OF_GROUND: f64 = 1.5; // in W/(m.K) diff --git a/src/core/space_heat_demand/ventilation_element.rs b/src/core/space_heat_demand/ventilation_element.rs index 611fceb..03de518 100644 --- a/src/core/space_heat_demand/ventilation_element.rs +++ b/src/core/space_heat_demand/ventilation_element.rs @@ -1,10 +1,17 @@ +use crate::core::controls::time_control::SetpointTimeControl; use crate::core::energy_supply::energy_supply::EnergySupply; +use crate::core::space_heat_demand::building_element::{ + area_for_building_element_input, element_from_named, mid_height_for, orientation_for, +}; +use crate::core::space_heat_demand::zone::NamedBuildingElement; use crate::core::units::{LITRES_PER_CUBIC_METRE, SECONDS_PER_HOUR, WATTS_PER_KILOWATT}; use crate::external_conditions::ExternalConditions; use crate::input::{ - EnergySupplyInput, InfiltrationBuildType, InfiltrationShelterType, InfiltrationTestType, + BuildingElement, EnergySupplyInput, InfiltrationBuildType, InfiltrationShelterType, + InfiltrationTestType, }; use crate::simulation_time::SimulationTimeIterator; +use std::collections::HashSet; fn air_change_rate_to_flow_rate(air_change_rate: f64, zone_volume: f64) -> f64 { air_change_rate * zone_volume / SECONDS_PER_HOUR as f64 @@ -503,9 +510,249 @@ impl VentilationElement for WholeHouseExtractVentilation { //tbc pub struct NaturalVentilation; -//tbc: -pub struct WindowOpeningForCooling { - external_conditions: ExternalConditions, // incomplete struct + +pub struct WindowOpeningForCooling<'a> { + window_area_equivalent: f64, + external_conditions: ExternalConditions, + openings: Option>, // actually only meaningfully contains BuildingElement::Transparent + control: &'a SetpointTimeControl, + natural_ventilation: Option, + a_b: Option, + a_w: Option, + opening_height_diff: f64, + opening_area_ratio: Option, + cross_vent: bool, + stack_vent: bool, +} + +impl<'a> WindowOpeningForCooling<'a> { + /// Arguments: + /// `window_area_equivalent` - maximum equivalent area of all openings in the relevant zone + /// `external_conditions` - reference to ExternalConditions object + /// `named_openings` - list of openings to be considered + /// `control` - reference to control object (must implement setpnt function) + /// `natural_ventilation` - reference to NaturalVentilation object, if building is naturally ventilated + pub fn new( + window_area_equivalent: f64, + external_conditions: ExternalConditions, + named_openings: &'a Vec, + control: &'a SetpointTimeControl, + natural_ventilation: Option, + ) -> Self { + let openings = named_openings + .iter() + .map(element_from_named) + .collect::>(); + // Assign equivalent areas to each window/group in proportion to actual area + let opening_area_total = openings + .iter() + .map(|element| area_for_building_element_input(element)) + .sum::(); + let opening_area_equiv_total_ratio = window_area_equivalent / opening_area_total; + + // Find orientation of largest window + // Find height of highest and lowest windows + let mut largest_op = &openings[0]; + let mut highest_op = &openings[0]; + let mut lowest_op = &openings[0]; + for op in openings[1..].iter() { + if area_for_building_element_input(op) > area_for_building_element_input(largest_op) { + largest_op = op; + } + if mid_height_for(op) > mid_height_for(highest_op) { + highest_op = op; + } + if mid_height_for(op) < mid_height_for(lowest_op) { + lowest_op = op; + } + } + let largest_op_orientation = orientation_for(largest_op).unwrap(); + let op_height_threshold = + (mid_height_for(highest_op).unwrap() - mid_height_for(lowest_op).unwrap()) / 2.0; + + let mut openings_same_side = vec![]; + let mut openings_opp_side = vec![]; + let mut openings_low = vec![]; + let mut openings_high = vec![]; + for op in named_openings.iter() { + // Determine orientation of other windows relative to largest + let mut op_rel_orientation = + (orientation_for(element_from_named(op)).unwrap() - largest_op_orientation).abs(); + if op_rel_orientation > 360.0 { + op_rel_orientation -= 360.0; + } + // Group windows into same, opposite and adjacent sides + if op_rel_orientation <= 45.0 { + openings_same_side.push(op); + } else if op_rel_orientation >= 135.0 { + openings_opp_side.push(op); + } + // Else opening is on adjacent side, so ignore + + // Assign windows to high and low groups based on which they are closest to + if mid_height_for(element_from_named(op)).unwrap() < op_height_threshold { + openings_low.push(op); + } else { + openings_high.push(op); + } + } + + let cross_vent = openings_opp_side.len() > 0; + + let mut a_b = None; + let mut a_w = None; + let mut stack_vent = false; + let mut opening_height_diff = 0.0; + let mut opening_area_ratio = None; + let mut openings_for_struct = None; + + if cross_vent { + let unique_openings_same_side = openings_same_side.iter().collect::>(); + let unique_openings_opp_side = openings_opp_side.iter().collect::>(); + let unique_openings_high = openings_high.iter().collect::>(); + let unique_openings_low = openings_low.iter().collect::>(); + let openings_same_side_high = unique_openings_same_side + .intersection(&unique_openings_high) + .collect::>(); + let openings_same_side_low = unique_openings_same_side + .intersection(&unique_openings_low) + .collect::>(); + let openings_opp_side_high = unique_openings_opp_side + .intersection(&unique_openings_high) + .collect::>(); + let openings_opp_side_low = unique_openings_same_side + .intersection(&unique_openings_low) + .collect::>(); + + // Calculate high and low opening areas on same and opposite sides of building + let a1 = opening_area_equiv_total_ratio + * openings_same_side_high + .iter() + .map(|nel| area_for_building_element_input(element_from_named(nel))) + .sum::(); + let a2 = opening_area_equiv_total_ratio + * openings_same_side_low + .iter() + .map(|nel| area_for_building_element_input(element_from_named(nel))) + .sum::(); + let a3 = opening_area_equiv_total_ratio + * openings_opp_side_high + .iter() + .map(|nel| area_for_building_element_input(element_from_named(nel))) + .sum::(); + let a4 = opening_area_equiv_total_ratio + * openings_opp_side_low + .iter() + .map(|nel| area_for_building_element_input(element_from_named(nel))) + .sum::(); + a_w = Some((1.0 / ((1.0 / ((a1 + a3).powi(2))) + 1.0 / ((a2 + a4).powi(2)))).sqrt()); + if a2 + a4 == 0.0 { + a_b = Some(0.0); + opening_height_diff = 0.0; + } else { + a_b = + Some((1.0 / ((1.0 / ((a1 + a3).powi(2))) + 1.0 / ((a2 + a4).powi(2)))).sqrt()); + + // Calculate area-weighted average height of windows in high and low groups + let opening_mid_height_ave_upper = (openings_same_side_high + .iter() + .map(|nel| { + let el = element_from_named(nel); + mid_height_for(el).unwrap() * area_for_building_element_input(el) + }) + .sum::() + + openings_opp_side_high + .iter() + .map(|nel| { + let el = element_from_named(nel); + mid_height_for(el).unwrap() * area_for_building_element_input(el) + }) + .sum::()) + / (openings_same_side_high + .iter() + .map(|nel| area_for_building_element_input(element_from_named(nel))) + .sum::() + + openings_opp_side_high + .iter() + .map(|nel| area_for_building_element_input(element_from_named(nel))) + .sum::()); + let opening_mid_height_ave_lower = (openings_same_side_low + .iter() + .map(|nel| { + let el = element_from_named(nel); + mid_height_for(el).unwrap() * area_for_building_element_input(el) + }) + .sum::() + + openings_opp_side_low + .iter() + .map(|nel| { + let el = element_from_named(nel); + mid_height_for(el).unwrap() * area_for_building_element_input(el) + }) + .sum::()) + / (openings_same_side_low + .iter() + .map(|nel| area_for_building_element_input(element_from_named(nel))) + .sum::() + + openings_opp_side_low + .iter() + .map(|nel| area_for_building_element_input(element_from_named(nel))) + .sum::()); + opening_height_diff = opening_mid_height_ave_upper - opening_mid_height_ave_lower; + } + } else { + if openings_high.len() > 1 && openings_low.len() > 1 { + stack_vent = true; + let opening_area_upper = openings_high + .iter() + .map(|nel| area_for_building_element_input(element_from_named(nel))) + .sum::(); + let opening_area_lower = openings_low + .iter() + .map(|nel| area_for_building_element_input(element_from_named(nel))) + .sum::(); + + // Calculate opening area ratio + opening_area_ratio = Some(opening_area_upper / opening_area_lower); + + let opening_mid_height_ave_upper = openings_high + .iter() + .map(|nel| { + let el = element_from_named(nel); + mid_height_for(el).unwrap() * area_for_building_element_input(el) + }) + .sum::() + / opening_area_upper; + let opening_mid_height_ave_lower = openings_low + .iter() + .map(|nel| { + let el = element_from_named(nel); + mid_height_for(el).unwrap() * area_for_building_element_input(el) + }) + .sum::() + / opening_area_lower; + // Calculate opening height difference + opening_height_diff = opening_mid_height_ave_upper - opening_mid_height_ave_lower; + } else { + stack_vent = false; + openings_for_struct = Some(openings); + } + } + + Self { + window_area_equivalent, + external_conditions, + openings: openings_for_struct, + control, + natural_ventilation, + a_b, + a_w, + opening_height_diff, + opening_area_ratio, + cross_vent, + stack_vent, + } + } } /// Calculate the supply temperature of the air flow element diff --git a/src/core/space_heat_demand/zone.rs b/src/core/space_heat_demand/zone.rs index 7b89afb..d4930b3 100644 --- a/src/core/space_heat_demand/zone.rs +++ b/src/core/space_heat_demand/zone.rs @@ -18,6 +18,7 @@ use crate::simulation_time::{SimulationTime, SimulationTimeIterator}; use nalgebra::{matrix, DMatrix}; use serde::Deserialize; use std::collections::HashMap; +use std::hash::{Hash, Hasher}; // Convective fractions // (default values from BS EN ISO 52016-1:2017, Table B.11) @@ -28,13 +29,13 @@ const F_SOL_C: f64 = 0.1; // (default value from BS EN ISO 52016-1:2017, Table B.17) const K_M_INT: f64 = 10000.0; // J / (m2.K) -pub struct Zone { +pub struct Zone<'a> { useful_area: f64, volume: f64, building_elements: Vec, thermal_bridging: ThermalBridging, vent_elements: Vec>, - vent_cool_extra: Option, + vent_cool_extra: Option>, tb_heat_trans_coeff: f64, /// total area of all building elements associated with this zone, in m2 area_el_total: f64, @@ -66,7 +67,7 @@ pub struct Zone { external_conditions: ExternalConditions, } -impl Zone { +impl<'a> Zone<'a> { /// Construct a Zone object /// /// ## Arguments @@ -379,7 +380,10 @@ pub fn space_heat_cool_demand( // and need to use interpolation to work out additional ventilation required // (just like calc for heat_cool_load_unrestricted below) let mut h_ve_cool_extra = 0.0; - // if vent_cool_extra.is_some() && temp_operative_free > temp_setpnt_cool_vent + if vent_cool_extra.is_some() && temp_operative_free > temp_setpnt_cool_vent.unwrap() { + // Calculate node and internal air temperatures with maximum additional ventilation + // finish + } (0.0, 0.0, 0.0) } @@ -894,8 +898,24 @@ fn temp_operative( #[derive(Clone)] pub struct NamedBuildingElement { - name: String, - element: BuildingElement, + pub name: String, + pub element: BuildingElement, +} + +// equality and hashing based on name for identity + +impl PartialEq for NamedBuildingElement { + fn eq(&self, other: &Self) -> bool { + self.name == other.name + } +} + +impl Eq for NamedBuildingElement {} + +impl Hash for NamedBuildingElement { + fn hash(&self, state: &mut H) { + self.name.hash(state); + } } struct HeatBalanceAirNode {