example_files/resources/hpxml-measures/HPXMLtoOpenStudio/resources/airflow.rb in urbanopt-cli-0.9.3 vs example_files/resources/hpxml-measures/HPXMLtoOpenStudio/resources/airflow.rb in urbanopt-cli-0.10.0
- old
+ new
@@ -1,28 +1,31 @@
# frozen_string_literal: true
class Airflow
+ # Constants
+ InfilPressureExponent = 0.65
+
def self.apply(model, runner, weather, spaces, hpxml, cfa, nbeds,
ncfl_ag, duct_systems, airloop_map, clg_ssn_sensor, eri_version,
- frac_windows_operable, apply_ashrae140_assumptions, schedules_file)
+ frac_windows_operable, apply_ashrae140_assumptions, schedules_file,
+ unavailable_periods, hvac_availability_sensor)
# Global variables
@runner = runner
@spaces = spaces
@year = hpxml.header.sim_calendar_year
- @infil_volume = hpxml.air_infiltration_measurements.select { |i| !i.infiltration_volume.nil? }[0].infiltration_volume
- @infil_height = hpxml.air_infiltration_measurements.select { |i| !i.infiltration_height.nil? }[0].infiltration_height
@living_space = spaces[HPXML::LocationLivingSpace]
@living_zone = @living_space.thermalZone.get
@nbeds = nbeds
@ncfl_ag = ncfl_ag
@eri_version = eri_version
@apply_ashrae140_assumptions = apply_ashrae140_assumptions
@cfa = cfa
@cooking_range_in_cond_space = hpxml.cooking_ranges.empty? ? true : HPXML::conditioned_locations_this_unit.include?(hpxml.cooking_ranges[0].location)
@clothes_dryer_in_cond_space = hpxml.clothes_dryers.empty? ? true : HPXML::conditioned_locations_this_unit.include?(hpxml.clothes_dryers[0].location)
+ @hvac_availability_sensor = hvac_availability_sensor
# Global sensors
@pbar_sensor = OpenStudio::Model::EnergyManagementSystemSensor.new(model, 'Site Outdoor Air Barometric Pressure')
@pbar_sensor.setName('out pb s')
@@ -75,11 +78,11 @@
# Vented clothes dryers
vented_dryers = hpxml.clothes_dryers.select { |cd| cd.is_vented && cd.vented_flow_rate.to_f > 0 }
# Initialization
- initialize_cfis(model, vent_fans_mech, airloop_map)
+ initialize_cfis(model, vent_fans_mech, airloop_map, unavailable_periods)
model.getAirLoopHVACs.each do |air_loop|
initialize_fan_objects(model, air_loop)
end
model.getZoneHVACFourPipeFanCoils.each do |fan_coil|
initialize_fan_objects(model, fan_coil)
@@ -95,30 +98,27 @@
set_wind_speed_correction(model, hpxml.site)
window_area = hpxml.windows.map { |w| w.area }.sum(0.0)
open_window_area = window_area * frac_windows_operable * 0.5 * 0.2 # Assume A) 50% of the area of an operable window can be open, and B) 20% of openable window area is actually open
- vented_attic = nil
- hpxml.attics.each do |attic|
- next unless attic.attic_type == HPXML::AtticTypeVented
+ vented_attic = hpxml.attics.find { |attic| attic.attic_type == HPXML::AtticTypeVented }
+ vented_crawl = hpxml.foundations.find { |foundation| foundation.foundation_type == HPXML::FoundationTypeCrawlspaceVented }
- vented_attic = attic
- break
+ _sla, living_ach50, nach, infil_volume, infil_height, a_ext = get_values_from_air_infiltration_measurements(hpxml, cfa, weather)
+ if @apply_ashrae140_assumptions
+ living_const_ach = nach
+ living_ach50 = nil
end
- vented_crawl = nil
- hpxml.foundations.each do |foundation|
- next unless foundation.foundation_type == HPXML::FoundationTypeCrawlspaceVented
+ living_const_ach *= a_ext unless living_const_ach.nil?
+ living_ach50 *= a_ext unless living_ach50.nil?
+ has_flue_chimney_in_cond_space = hpxml.air_infiltration.has_flue_or_chimney_in_conditioned_space
- vented_crawl = foundation
- break
- end
-
- apply_natural_ventilation_and_whole_house_fan(model, hpxml.site, vent_fans_whf, open_window_area, clg_ssn_sensor,
- hpxml.header.natvent_days_per_week)
+ apply_natural_ventilation_and_whole_house_fan(model, hpxml.site, vent_fans_whf, open_window_area, clg_ssn_sensor, hpxml.header.natvent_days_per_week,
+ infil_volume, infil_height, unavailable_periods)
apply_infiltration_and_ventilation_fans(model, weather, hpxml.site, vent_fans_mech, vent_fans_kitchen, vent_fans_bath, vented_dryers,
- hpxml.building_construction.has_flue_or_chimney, hpxml.air_infiltration_measurements,
- vented_attic, vented_crawl, clg_ssn_sensor, schedules_file, vent_fans_cfis_suppl)
+ has_flue_chimney_in_cond_space, living_ach50, living_const_ach, infil_volume, infil_height,
+ vented_attic, vented_crawl, clg_ssn_sensor, schedules_file, vent_fans_cfis_suppl, unavailable_periods)
end
def self.get_default_fraction_of_windows_operable()
# Combining the value below with the assumption that 50% of
# the area of an operable window can be open produces the
@@ -155,47 +155,82 @@
else
fail "Unexpected fan_type: '#{fan_type}'."
end
end
- def self.get_default_mech_vent_flow_rate(hpxml, vent_fan, infil_measurements, weather, cfa, nbeds)
- # Calculates Qfan cfm requirement per ASHRAE 62.2-2019
- infil_volume = infil_measurements[0].infiltration_volume
- infil_height = infil_measurements[0].infiltration_height
+ def self.get_infiltration_measurement_of_interest(infil_measurements)
+ # Returns the infiltration measurement that has the minimum information needed for simulation
+ infil_measurements.each do |measurement|
+ if [HPXML::UnitsACH, HPXML::UnitsCFM].include?(measurement.unit_of_measure) && !measurement.house_pressure.nil?
+ return measurement
+ elsif [HPXML::UnitsACHNatural, HPXML::UnitsCFMNatural].include? measurement.unit_of_measure
+ return measurement
+ elsif !measurement.effective_leakage_area.nil?
+ return measurement
+ end
+ end
+ fail 'Unexpected error.'
+ end
- infil_a_ext = 1.0
- if [HPXML::ResidentialTypeSFA, HPXML::ResidentialTypeApartment].include? hpxml.building_construction.residential_facility_type
- tot_cb_area, ext_cb_area = hpxml.compartmentalization_boundary_areas()
- infil_a_ext = ext_cb_area / tot_cb_area
+ def self.get_values_from_air_infiltration_measurements(hpxml, cfa, weather)
+ measurement = get_infiltration_measurement_of_interest(hpxml.air_infiltration_measurements)
+
+ volume = measurement.infiltration_volume
+ height = measurement.infiltration_height
+ if height.nil?
+ height = hpxml.inferred_infiltration_height(volume)
end
- sla = nil
- infil_measurements.each do |measurement|
- if [HPXML::UnitsACH, HPXML::UnitsCFM].include?(measurement.unit_of_measure) && !measurement.house_pressure.nil?
- if measurement.unit_of_measure == HPXML::UnitsACH
- ach50 = Airflow.calc_air_leakage_at_diff_pressure(0.65, measurement.air_leakage, measurement.house_pressure, 50.0)
- elsif measurement.unit_of_measure == HPXML::UnitsCFM
- achXX = measurement.air_leakage * 60.0 / infil_volume # Convert CFM to ACH
- ach50 = Airflow.calc_air_leakage_at_diff_pressure(0.65, achXX, measurement.house_pressure, 50.0)
- end
- sla = Airflow.get_infiltration_SLA_from_ACH50(ach50, 0.65, cfa, infil_volume)
- elsif measurement.unit_of_measure == HPXML::UnitsACHNatural
- sla = Airflow.get_infiltration_SLA_from_ACH(measurement.air_leakage, infil_height, weather)
+ sla, ach50, nach = nil
+ if [HPXML::UnitsACH, HPXML::UnitsCFM].include?(measurement.unit_of_measure)
+ if measurement.unit_of_measure == HPXML::UnitsACH
+ ach50 = calc_air_leakage_at_diff_pressure(InfilPressureExponent, measurement.air_leakage, measurement.house_pressure, 50.0)
+ elsif measurement.unit_of_measure == HPXML::UnitsCFM
+ achXX = measurement.air_leakage * 60.0 / volume # Convert CFM to ACH
+ ach50 = calc_air_leakage_at_diff_pressure(InfilPressureExponent, achXX, measurement.house_pressure, 50.0)
end
+ sla = get_infiltration_SLA_from_ACH50(ach50, InfilPressureExponent, cfa, volume)
+ nach = get_infiltration_ACH_from_SLA(sla, height, weather)
+ elsif [HPXML::UnitsACHNatural, HPXML::UnitsCFMNatural].include? measurement.unit_of_measure
+ if measurement.unit_of_measure == HPXML::UnitsACHNatural
+ nach = measurement.air_leakage
+ elsif measurement.unit_of_measure == HPXML::UnitsCFMNatural
+ nach = measurement.air_leakage * 60.0 / volume # Convert CFM to ACH
+ end
+ sla = get_infiltration_SLA_from_ACH(nach, height, weather)
+ ach50 = get_infiltration_ACH50_from_SLA(sla, InfilPressureExponent, cfa, volume)
+ elsif !measurement.effective_leakage_area.nil?
+ sla = UnitConversions.convert(measurement.effective_leakage_area, 'in^2', 'ft^2') / cfa
+ ach50 = get_infiltration_ACH50_from_SLA(sla, InfilPressureExponent, cfa, volume)
+ nach = get_infiltration_ACH_from_SLA(sla, height, weather)
+ else
+ fail 'Unexpected error.'
end
- nl = get_infiltration_NL_from_SLA(sla, infil_height)
+ if measurement.infiltration_type == HPXML::InfiltrationTypeUnitTotal
+ a_ext = measurement.a_ext # Adjustment ratio for SFA/MF units; exterior envelope area divided by total envelope area
+ end
+ a_ext = 1.0 if a_ext.nil?
+
+ return sla, ach50, nach, volume, height, a_ext
+ end
+
+ def self.get_default_mech_vent_flow_rate(hpxml, vent_fan, weather, cfa, nbeds)
+ # Calculates Qfan cfm requirement per ASHRAE 62.2-2019
+ sla, _ach50, _nach, _volume, height, a_ext = get_values_from_air_infiltration_measurements(hpxml, cfa, weather)
+
+ nl = get_infiltration_NL_from_SLA(sla, height)
q_inf = nl * weather.data.WSF * cfa / 7.3 # Effective annual average infiltration rate, cfm, eq. 4.5a
q_tot = get_mech_vent_qtot_cfm(nbeds, cfa)
if vent_fan.is_balanced?
phi = 1.0
else
phi = q_inf / q_tot
end
- q_fan = q_tot - phi * (q_inf * infil_a_ext)
+ q_fan = q_tot - phi * (q_inf * a_ext)
q_fan = [q_fan, 0].max
if not vent_fan.hours_in_operation.nil?
# Convert from hourly average requirement to actual fan flow rate
q_fan *= 24.0 / vent_fan.hours_in_operation
@@ -278,50 +313,46 @@
leakage_area.setWindCoefficient(c_w_SG * 0.01)
leakage_area.setSpace(space)
end
end
- def self.apply_natural_ventilation_and_whole_house_fan(model, site, vent_fans_whf, open_window_area, nv_clg_ssn_sensor,
- natvent_days_per_week)
- if @living_zone.thermostatSetpointDualSetpoint.is_initialized
- thermostat = @living_zone.thermostatSetpointDualSetpoint.get
- htg_sch = thermostat.heatingSetpointTemperatureSchedule.get
- clg_sch = thermostat.coolingSetpointTemperatureSchedule.get
- end
+ def self.apply_natural_ventilation_and_whole_house_fan(model, site, vent_fans_whf, open_window_area, nv_clg_ssn_sensor, natvent_days_per_week,
+ infil_volume, infil_height, unavailable_periods)
# NV Availability Schedule
- nv_avail_sch = create_nv_and_whf_avail_sch(model, Constants.ObjectNameNaturalVentilation, natvent_days_per_week)
+ nv_avail_sch = create_nv_and_whf_avail_sch(model, Constants.ObjectNameNaturalVentilation, natvent_days_per_week, unavailable_periods)
nv_avail_sensor = OpenStudio::Model::EnergyManagementSystemSensor.new(model, 'Schedule Value')
- nv_avail_sensor.setName("#{Constants.ObjectNameNaturalVentilation} avail s")
+ nv_avail_sensor.setName("#{Constants.ObjectNameNaturalVentilation} s")
nv_avail_sensor.setKeyName(nv_avail_sch.name.to_s)
# Availability Schedules paired with vent fan class
# If whf_num_days_per_week is exposed, can handle multiple fans with different days of operation
whf_avail_sensors = {}
vent_fans_whf.each_with_index do |vent_whf, index|
whf_num_days_per_week = 7 # FUTURE: Expose via HPXML?
obj_name = "#{Constants.ObjectNameWholeHouseFan} #{index}"
- whf_avail_sch = create_nv_and_whf_avail_sch(model, obj_name, whf_num_days_per_week)
+ whf_unavailable_periods = Schedule.get_unavailable_periods(@runner, SchedulesFile::ColumnWholeHouseFan, unavailable_periods)
+ whf_avail_sch = create_nv_and_whf_avail_sch(model, obj_name, whf_num_days_per_week, whf_unavailable_periods)
whf_avail_sensor = OpenStudio::Model::EnergyManagementSystemSensor.new(model, 'Schedule Value')
- whf_avail_sensor.setName("#{obj_name} avail s")
+ whf_avail_sensor.setName("#{obj_name} s")
whf_avail_sensor.setKeyName(whf_avail_sch.name.to_s)
whf_avail_sensors[vent_whf.id] = whf_avail_sensor
end
# Sensors
- if not htg_sch.nil?
+ if @living_zone.thermostatSetpointDualSetpoint.is_initialized
+ thermostat = @living_zone.thermostatSetpointDualSetpoint.get
+
htg_sp_sensor = OpenStudio::Model::EnergyManagementSystemSensor.new(model, 'Schedule Value')
htg_sp_sensor.setName('htg sp s')
- htg_sp_sensor.setKeyName(htg_sch.name.to_s)
- end
+ htg_sp_sensor.setKeyName(thermostat.heatingSetpointTemperatureSchedule.get.name.to_s)
- if not clg_sch.nil?
clg_sp_sensor = OpenStudio::Model::EnergyManagementSystemSensor.new(model, 'Schedule Value')
clg_sp_sensor.setName('clg sp s')
- clg_sp_sensor.setKeyName(clg_sch.name.to_s)
+ clg_sp_sensor.setKeyName(thermostat.coolingSetpointTemperatureSchedule.get.name.to_s)
end
# Actuators
nv_flow = OpenStudio::Model::SpaceInfiltrationDesignFlowRate.new(model)
nv_flow.setName(Constants.ObjectNameNaturalVentilation + ' flow')
@@ -346,11 +377,11 @@
whf_equip_def.setFractionRadiant(0)
whf_equip_def.setFractionLatent(0)
whf_equip_def.setFractionLost(1)
whf_equip.setSchedule(model.alwaysOnDiscreteSchedule)
whf_equip.setEndUseSubcategory(Constants.ObjectNameWholeHouseFan)
- whf_elec_actuator = OpenStudio::Model::EnergyManagementSystemActuator.new(whf_equip, *EPlus::EMSActuatorElectricEquipmentPower)
+ whf_elec_actuator = OpenStudio::Model::EnergyManagementSystemActuator.new(whf_equip, *EPlus::EMSActuatorElectricEquipmentPower, whf_equip.space.get)
whf_elec_actuator.setName("#{whf_equip.name} act")
# Assume located in attic floor if attic zone exists; otherwise assume it's through roof/wall.
whf_zone = nil
if not @spaces[HPXML::LocationAtticVented].nil?
@@ -367,14 +398,14 @@
liv_to_zone_flow_rate_actuator.setName("#{zone_mixing.name} act")
end
area = 0.6 * open_window_area # ft^2, for Sherman-Grimsrud
max_rate = 20.0 # Air Changes per hour
- max_flow_rate = max_rate * @infil_volume / UnitConversions.convert(1.0, 'hr', 'min')
+ max_flow_rate = max_rate * infil_volume / UnitConversions.convert(1.0, 'hr', 'min')
neutral_level = 0.5
hor_lk_frac = 0.0
- c_w, c_s = calc_wind_stack_coeffs(site, hor_lk_frac, neutral_level, @living_space, @infil_height)
+ c_w, c_s = calc_wind_stack_coeffs(site, hor_lk_frac, neutral_level, @living_space, infil_height)
max_oa_hr = 0.0115 # From BA HSP
max_oa_rh = 0.7 # From BA HSP
# Program
vent_program = OpenStudio::Model::EnergyManagementSystemProgram.new(model)
@@ -384,22 +415,33 @@
vent_program.addLine("Set Wout = #{@wout_sensor.name}")
vent_program.addLine("Set Pbar = #{@pbar_sensor.name}")
vent_program.addLine('Set Phiout = (@RhFnTdbWPb Tout Wout Pbar)')
vent_program.addLine("Set MaxHR = #{max_oa_hr}")
vent_program.addLine("Set MaxRH = #{max_oa_rh}")
- if (not htg_sp_sensor.nil?) && (not clg_sp_sensor.nil?)
- vent_program.addLine("Set Tnvsp = (#{htg_sp_sensor.name} + #{clg_sp_sensor.name}) / 2") # Average of heating/cooling setpoints to minimize incurring additional heating energy
+ if not thermostat.nil?
+ # Home has HVAC system (though setpoints may be defaulted); use the average of heating/cooling setpoints to minimize incurring additional heating energy.
+ vent_program.addLine("Set Tnvsp = (#{htg_sp_sensor.name} + #{clg_sp_sensor.name}) / 2")
else
- vent_program.addLine("Set Tnvsp = #{UnitConversions.convert(73.0, 'F', 'C')}") # Assumption when no HVAC system
+ # No HVAC system; use the average of defaulted heating/cooling setpoints.
+ default_htg_sp = UnitConversions.convert(HVAC.get_default_heating_setpoint(HPXML::HVACControlTypeManual)[0], 'F', 'C')
+ default_clg_sp = UnitConversions.convert(HVAC.get_default_cooling_setpoint(HPXML::HVACControlTypeManual)[0], 'F', 'C')
+ vent_program.addLine("Set Tnvsp = (#{default_htg_sp} + #{default_clg_sp}) / 2")
end
vent_program.addLine("Set NVavail = #{nv_avail_sensor.name}")
vent_program.addLine("Set ClgSsnAvail = #{nv_clg_ssn_sensor.name}")
vent_program.addLine("Set #{nv_flow_actuator.name} = 0") # Init
vent_program.addLine("Set #{whf_flow_actuator.name} = 0") # Init
vent_program.addLine("Set #{liv_to_zone_flow_rate_actuator.name} = 0") unless whf_zone.nil? # Init
vent_program.addLine("Set #{whf_elec_actuator.name} = 0") # Init
- vent_program.addLine('If (Wout < MaxHR) && (Phiout < MaxRH) && (Tin > Tout) && (Tin > Tnvsp) && (ClgSsnAvail > 0)')
+ infil_constraints = 'If ((Wout < MaxHR) && (Phiout < MaxRH) && (Tin > Tout) && (Tin > Tnvsp) && (ClgSsnAvail > 0))'
+ if not @hvac_availability_sensor.nil?
+ # We are using the availability schedule, but we also constrain the window opening based on temperatures and humidity.
+ # We're assuming that if the HVAC is not available, you'd ignore the humidity constraints we normally put on window opening per the old HSP guidance (RH < 70% and w < 0.015).
+ # Without, the humidity constraints prevent the window from opening during the entire period even though the sensible cooling would have really helped.
+ infil_constraints += "|| ((Tin > Tout) && (Tin > Tnvsp) && (#{@hvac_availability_sensor.name} == 0))"
+ end
+ vent_program.addLine(infil_constraints)
vent_program.addLine(' Set WHF_Flow = 0')
vent_fans_whf.each do |vent_whf|
vent_program.addLine(" Set WHF_Flow = WHF_Flow + #{UnitConversions.convert(vent_whf.flow_rate, 'cfm', 'm^3/s')} * #{whf_avail_sensors[vent_whf.id].name}")
end
vent_program.addLine(' Set Adj = (Tin-Tnvsp)/(Tin-Tout)')
@@ -430,27 +472,32 @@
manager.setName("#{vent_program.name} calling manager")
manager.setCallingPoint('BeginZoneTimestepAfterInitHeatBalance')
manager.addProgram(vent_program)
end
- def self.create_nv_and_whf_avail_sch(model, obj_name, num_days_per_week)
+ def self.create_nv_and_whf_avail_sch(model, obj_name, num_days_per_week, unavailable_periods = [])
avail_sch = OpenStudio::Model::ScheduleRuleset.new(model)
- avail_sch.setName("#{obj_name} avail schedule")
+ sch_name = "#{obj_name} schedule"
+ avail_sch.setName(sch_name)
+ avail_sch.defaultDaySchedule.setName("#{sch_name} default day")
Schedule.set_schedule_type_limits(model, avail_sch, Constants.ScheduleTypeLimitsOnOff)
on_rule = OpenStudio::Model::ScheduleRule.new(avail_sch)
- on_rule.setName("#{obj_name} avail schedule rule")
+ on_rule.setName("#{sch_name} rule")
on_rule_day = on_rule.daySchedule
- on_rule_day.setName("#{obj_name} avail schedule day")
+ on_rule_day.setName("#{sch_name} avail day")
on_rule_day.addValue(OpenStudio::Time.new(0, 24, 0, 0), 1)
method_array = ['setApplyMonday', 'setApplyWednesday', 'setApplyFriday', 'setApplySaturday', 'setApplyTuesday', 'setApplyThursday', 'setApplySunday']
for i in 1..7 do
if num_days_per_week >= i
on_rule.public_send(method_array[i - 1], true)
end
end
on_rule.setStartDate(OpenStudio::Date::fromDayOfYear(1))
on_rule.setEndDate(OpenStudio::Date::fromDayOfYear(365))
+
+ year = model.getYearDescription.assumedYear
+ Schedule.set_unavailable_periods(avail_sch, sch_name, unavailable_periods, year)
return avail_sch
end
def self.create_return_air_duct_zone(model, loop_name)
# Create the return air plenum zone, space
@@ -508,19 +555,19 @@
other_equip.setSchedule(model.alwaysOnDiscreteSchedule)
other_equip.setSpace(space)
other_equip_def.setFractionLost(frac_lost)
other_equip_def.setFractionLatent(frac_lat)
other_equip_def.setFractionRadiant(0.0)
- actuator = OpenStudio::Model::EnergyManagementSystemActuator.new(other_equip, *EPlus::EMSActuatorOtherEquipmentPower)
+ actuator = OpenStudio::Model::EnergyManagementSystemActuator.new(other_equip, *EPlus::EMSActuatorOtherEquipmentPower, other_equip.space.get)
actuator.setName("#{other_equip.name} act")
if not is_duct_load_for_report.nil?
other_equip.additionalProperties.setFeature(Constants.IsDuctLoadForReport, is_duct_load_for_report)
end
return actuator
end
- def self.initialize_cfis(model, vent_fans_mech, airloop_map)
+ def self.initialize_cfis(model, vent_fans_mech, airloop_map, unavailable_periods)
# Get AirLoop associated with CFIS
@cfis_airloop = {}
@cfis_t_sum_open_var = {}
@cfis_f_damper_extra_open_var = {}
return if vent_fans_mech.empty?
@@ -528,10 +575,12 @@
index = 0
vent_fans_mech.each do |vent_mech|
next if vent_mech.fan_type != HPXML::MechVentTypeCFIS
+ fail 'Cannot apply unavailable period(s) to CFIS systems.' if !unavailable_periods.empty?
+
vent_mech.distribution_system.hvac_systems.map { |system| system.id }.each do |cfis_id|
next if airloop_map[cfis_id].nil?
@cfis_airloop[vent_mech.id] = airloop_map[cfis_id]
end
@@ -605,11 +654,10 @@
end
end
def self.apply_ducts(model, ducts, object, vent_fans_mech)
ducts.each do |duct|
- duct.rvalue = get_duct_insulation_rvalue(duct.rvalue, duct.side) # Convert from nominal to actual R-value
if not duct.loc_schedule.nil?
# Pass MF space temperature schedule name
duct.location = duct.loc_schedule.name.to_s
elsif not duct.loc_space.nil?
duct.location = duct.loc_space.name.to_s
@@ -620,24 +668,10 @@
end
end
return if ducts.size == 0 # No ducts
- # get duct located zone or ambient temperature schedule objects
- duct_locations = ducts.map { |duct| if duct.zone.nil? then duct.loc_schedule else duct.zone end }.uniq
-
- # All duct zones are in living space?
- all_ducts_conditioned = true
- duct_locations.each do |duct_zone|
- if duct_locations.is_a? OpenStudio::Model::ThermalZone
- next if Geometry.is_living(duct_zone)
- end
-
- all_ducts_conditioned = false
- end
- return if all_ducts_conditioned
-
if object.is_a? OpenStudio::Model::AirLoopHVAC
# Most system types
# Set the return plenum
ra_duct_zone = create_return_air_duct_zone(model, object.name.to_s)
@@ -707,10 +741,13 @@
ra_w_sensor = OpenStudio::Model::EnergyManagementSystemSensor.new(model, 'Zone Mean Air Humidity Ratio')
ra_w_sensor.setName("#{ra_w_var.name} s")
ra_w_sensor.setKeyName(@living_zone.name.to_s)
end
+ # Get duct located zone or ambient temperature schedule objects
+ duct_locations = ducts.map { |duct| if duct.zone.nil? then duct.loc_schedule else duct.zone end }.uniq
+
# Create one duct program for each duct location zone
duct_locations.each_with_index do |duct_location, i|
next if (not duct_location.nil?) && (duct_location.name.to_s == @living_zone.name.to_s)
object_name_idx = "#{object.name}_#{i}"
@@ -905,13 +942,13 @@
elsif not duct.leakage_cfm25.nil?
leakage_cfm25s[duct.side] = 0 if leakage_cfm25s[duct.side].nil?
leakage_cfm25s[duct.side] += duct.leakage_cfm25
elsif not duct.leakage_cfm50.nil?
leakage_cfm25s[duct.side] = 0 if leakage_cfm25s[duct.side].nil?
- leakage_cfm25s[duct.side] += calc_air_leakage_at_diff_pressure(0.65, duct.leakage_cfm50, 50.0, 25.0)
+ leakage_cfm25s[duct.side] += calc_air_leakage_at_diff_pressure(InfilPressureExponent, duct.leakage_cfm50, 50.0, 25.0)
end
- ua_values[duct.side] += duct.area / duct.rvalue
+ ua_values[duct.side] += duct.area / duct.effective_rvalue
end
# Calculate fraction of outside air specific to this duct location
f_oa = 1.0
if duct_location.is_a? OpenStudio::Model::ThermalZone # in a space
@@ -1069,11 +1106,11 @@
end
if @cfis_airloop.values.include? object
cfis_id = @cfis_airloop.key(object)
- vent_mech = vent_fans_mech.select { |vfm| vfm.id == cfis_id }[0]
+ vent_mech = vent_fans_mech.find { |vfm| vfm.id == cfis_id }
add_cfis_duct_losses = (vent_mech.cfis_addtl_runtime_operating_mode == HPXML::CFISModeAirHandler)
if add_cfis_duct_losses
# Calculate additional CFIS duct losses during fan-only mode
duct_program.addLine("If #{@cfis_f_damper_extra_open_var[cfis_id].name} > 0")
@@ -1140,11 +1177,11 @@
space = @spaces[HPXML::LocationGarage]
area = UnitConversions.convert(space.floorArea, 'm^2', 'ft^2')
volume = UnitConversions.convert(space.volume, 'm^3', 'ft^3')
hor_lk_frac = 0.4
neutral_level = 0.5
- sla = get_infiltration_SLA_from_ACH50(ach50, 0.65, area, volume)
+ sla = get_infiltration_SLA_from_ACH50(ach50, InfilPressureExponent, area, volume)
ela = sla * area
c_w_SG, c_s_SG = calc_wind_stack_coeffs(site, hor_lk_frac, neutral_level, space)
apply_infiltration_to_unconditioned_space(model, space, nil, ela, c_w_SG, c_s_SG)
end
@@ -1212,11 +1249,11 @@
space = @spaces[HPXML::LocationAtticUnvented]
ach = get_default_unvented_space_ach()
apply_infiltration_to_unconditioned_space(model, space, ach, nil, nil, nil)
end
- def self.apply_local_ventilation(model, vent_object, obj_type_name, index)
+ def self.apply_local_ventilation(model, vent_object, obj_type_name, index, unavailable_periods)
daily_sch = [0.0] * 24
obj_name = "#{obj_type_name} #{index}"
remaining_hrs = vent_object.hours_in_operation
for hr in 1..(vent_object.hours_in_operation.ceil)
if remaining_hrs >= 1
@@ -1224,31 +1261,31 @@
else
daily_sch[(vent_object.start_hour + hr - 1) % 24] = remaining_hrs
end
remaining_hrs -= 1
end
- obj_sch = HourlyByMonthSchedule.new(model, "#{obj_name} schedule", [daily_sch] * 12, [daily_sch] * 12, Constants.ScheduleTypeLimitsFraction, false)
+ obj_sch = HourlyByMonthSchedule.new(model, "#{obj_name} schedule", [daily_sch] * 12, [daily_sch] * 12, Constants.ScheduleTypeLimitsFraction, false, unavailable_periods: unavailable_periods)
obj_sch_sensor = OpenStudio::Model::EnergyManagementSystemSensor.new(model, 'Schedule Value')
obj_sch_sensor.setName("#{obj_name} sch s")
obj_sch_sensor.setKeyName(obj_sch.schedule.name.to_s)
equip_def = OpenStudio::Model::ElectricEquipmentDefinition.new(model)
equip_def.setName(obj_name)
equip = OpenStudio::Model::ElectricEquipment.new(equip_def)
equip.setName(obj_name)
equip.setSpace(@living_space) # no heat gain, so assign the equipment to an arbitrary space
- equip_def.setDesignLevel(vent_object.fan_power * vent_object.quantity)
+ equip_def.setDesignLevel(vent_object.fan_power * vent_object.count)
equip_def.setFractionRadiant(0)
equip_def.setFractionLatent(0)
equip_def.setFractionLost(1)
equip.setSchedule(obj_sch.schedule)
equip.setEndUseSubcategory(Constants.ObjectNameMechanicalVentilation)
return obj_sch_sensor
end
- def self.apply_dryer_exhaust(model, vented_dryer, schedules_file, index)
+ def self.apply_dryer_exhaust(model, vented_dryer, schedules_file, index, unavailable_periods)
obj_name = "#{Constants.ObjectNameClothesDryerExhaust} #{index}"
# Create schedule
obj_sch = nil
if not schedules_file.nil?
@@ -1258,22 +1295,25 @@
end
if obj_sch.nil?
cd_weekday_sch = vented_dryer.weekday_fractions
cd_weekend_sch = vented_dryer.weekend_fractions
cd_monthly_sch = vented_dryer.monthly_multipliers
- obj_sch = MonthWeekdayWeekendSchedule.new(model, Constants.ObjectNameClothesDryer, cd_weekday_sch, cd_weekend_sch, cd_monthly_sch, Constants.ScheduleTypeLimitsFraction)
+ obj_sch = MonthWeekdayWeekendSchedule.new(model, Constants.ObjectNameClothesDryer, cd_weekday_sch, cd_weekend_sch, cd_monthly_sch, Constants.ScheduleTypeLimitsFraction, unavailable_periods: unavailable_periods)
obj_sch = obj_sch.schedule
obj_sch_name = obj_sch.name.to_s
full_load_hrs = Schedule.annual_equivalent_full_load_hrs(@year, obj_sch)
end
- # Assume standard dryer exhaust runs 1 hr/day per BA HSP
- cfm_mult = Constants.NumDaysInYear(@year) * vented_dryer.usage_multiplier / full_load_hrs
obj_sch_sensor = OpenStudio::Model::EnergyManagementSystemSensor.new(model, 'Schedule Value')
obj_sch_sensor.setName("#{obj_name} sch s")
obj_sch_sensor.setKeyName(obj_sch_name)
+ return obj_sch_sensor, 0 if full_load_hrs == 0
+
+ # Assume standard dryer exhaust runs 1 hr/day per BA HSP
+ cfm_mult = Constants.NumDaysInYear(@year) * vented_dryer.usage_multiplier / full_load_hrs
+
return obj_sch_sensor, cfm_mult
end
def self.calc_hrv_erv_effectiveness(vent_mech_fans)
# Create the mapping between mech vent instance and the effectiveness results
@@ -1442,11 +1482,11 @@
infil_program.addLine('EndIf')
end
end
- def self.add_ee_for_vent_fan_power(model, obj_name, sup_fans = [], exh_fans = [], bal_fans = [], erv_hrv_fans = [])
+ def self.add_ee_for_vent_fan_power(model, obj_name, sup_fans = [], exh_fans = [], bal_fans = [], erv_hrv_fans = [], unavailable_periods = [])
# Calculate fan heat fraction
# 1.0: Fan heat does not enter space (e.g., exhaust)
# 0.0: Fan heat does enter space (e.g., supply)
if obj_name == Constants.ObjectNameMechanicalVentilationHouseFanCFIS
fan_heat_lost_fraction = 0.0
@@ -1469,24 +1509,28 @@
else
fan_heat_lost_fraction = 1.0
end
end
+ # Availability Schedule
+ avail_sch = ScheduleConstant.new(model, obj_name + ' schedule', 1.0, Constants.ScheduleTypeLimitsFraction, unavailable_periods: unavailable_periods)
+ avail_sch = avail_sch.schedule
+
equip_def = OpenStudio::Model::ElectricEquipmentDefinition.new(model)
equip_def.setName(obj_name)
equip = OpenStudio::Model::ElectricEquipment.new(equip_def)
equip.setName(obj_name)
equip.setSpace(@living_space)
equip_def.setFractionRadiant(0)
equip_def.setFractionLatent(0)
- equip.setSchedule(model.alwaysOnDiscreteSchedule)
+ equip.setSchedule(avail_sch)
equip.setEndUseSubcategory(Constants.ObjectNameMechanicalVentilation)
equip_def.setFractionLost(fan_heat_lost_fraction)
equip_actuator = nil
if [Constants.ObjectNameMechanicalVentilationHouseFanCFIS,
Constants.ObjectNameMechanicalVentilationHouseFanCFISSupplFan].include? obj_name # actuate its power level in EMS
- equip_actuator = OpenStudio::Model::EnergyManagementSystemActuator.new(equip, *EPlus::EMSActuatorElectricEquipmentPower)
+ equip_actuator = OpenStudio::Model::EnergyManagementSystemActuator.new(equip, *EPlus::EMSActuatorElectricEquipmentPower, equip.space.get)
equip_actuator.setName("#{equip.name} act")
end
if not tot_fans_w.nil?
equip_def.setDesignLevel(tot_fans_w)
end
@@ -1517,41 +1561,44 @@
return fan_sens_load_actuator, fan_lat_load_actuator
end
def self.apply_infiltration_adjustment_to_conditioned(model, infil_program, vent_fans_kitchen, vent_fans_bath, vented_dryers, vent_mech_sup_tot,
- vent_mech_exh_tot, vent_mech_bal_tot, vent_mech_erv_hrv_tot, infil_flow_actuator, schedules_file)
+ vent_mech_exh_tot, vent_mech_bal_tot, vent_mech_erv_hrv_tot, infil_flow_actuator, schedules_file, unavailable_periods)
# Average in-unit CFMs (include recirculation from in unit CFMs for shared systems)
sup_cfm_tot = vent_mech_sup_tot.map { |vent_mech| vent_mech.average_total_unit_flow_rate }.sum(0.0)
exh_cfm_tot = vent_mech_exh_tot.map { |vent_mech| vent_mech.average_total_unit_flow_rate }.sum(0.0)
bal_cfm_tot = vent_mech_bal_tot.map { |vent_mech| vent_mech.average_total_unit_flow_rate }.sum(0.0)
erv_hrv_cfm_tot = vent_mech_erv_hrv_tot.map { |vent_mech| vent_mech.average_total_unit_flow_rate }.sum(0.0)
infil_program.addLine('Set Qrange = 0')
vent_fans_kitchen.each_with_index do |vent_kitchen, index|
# Electricity impact
- obj_sch_sensor = apply_local_ventilation(model, vent_kitchen, Constants.ObjectNameMechanicalVentilationRangeFan, index)
+ vent_kitchen_unavailable_periods = Schedule.get_unavailable_periods(@runner, SchedulesFile::ColumnKitchenFan, unavailable_periods)
+ obj_sch_sensor = apply_local_ventilation(model, vent_kitchen, Constants.ObjectNameMechanicalVentilationRangeFan, index, vent_kitchen_unavailable_periods)
next unless @cooking_range_in_cond_space
# Infiltration impact
- infil_program.addLine("Set Qrange = Qrange + #{UnitConversions.convert(vent_kitchen.flow_rate * vent_kitchen.quantity, 'cfm', 'm^3/s').round(5)} * #{obj_sch_sensor.name}")
+ infil_program.addLine("Set Qrange = Qrange + #{UnitConversions.convert(vent_kitchen.flow_rate * vent_kitchen.count, 'cfm', 'm^3/s').round(5)} * #{obj_sch_sensor.name}")
end
infil_program.addLine('Set Qbath = 0')
vent_fans_bath.each_with_index do |vent_bath, index|
# Electricity impact
- obj_sch_sensor = apply_local_ventilation(model, vent_bath, Constants.ObjectNameMechanicalVentilationBathFan, index)
+ vent_bath_unavailable_periods = Schedule.get_unavailable_periods(@runner, SchedulesFile::ColumnBathFan, unavailable_periods)
+ obj_sch_sensor = apply_local_ventilation(model, vent_bath, Constants.ObjectNameMechanicalVentilationBathFan, index, vent_bath_unavailable_periods)
# Infiltration impact
- infil_program.addLine("Set Qbath = Qbath + #{UnitConversions.convert(vent_bath.flow_rate * vent_bath.quantity, 'cfm', 'm^3/s').round(5)} * #{obj_sch_sensor.name}")
+ infil_program.addLine("Set Qbath = Qbath + #{UnitConversions.convert(vent_bath.flow_rate * vent_bath.count, 'cfm', 'm^3/s').round(5)} * #{obj_sch_sensor.name}")
end
infil_program.addLine('Set Qdryer = 0')
vented_dryers.each_with_index do |vented_dryer, index|
next unless @clothes_dryer_in_cond_space
# Infiltration impact
- obj_sch_sensor, cfm_mult = apply_dryer_exhaust(model, vented_dryer, schedules_file, index)
+ vented_dryer_unavailable_periods = Schedule.get_unavailable_periods(@runner, SchedulesFile::ColumnClothesDryer, unavailable_periods)
+ obj_sch_sensor, cfm_mult = apply_dryer_exhaust(model, vented_dryer, schedules_file, index, vented_dryer_unavailable_periods)
infil_program.addLine("Set Qdryer = Qdryer + #{UnitConversions.convert(vented_dryer.vented_flow_rate * cfm_mult, 'cfm', 'm^3/s').round(5)} * #{obj_sch_sensor.name}")
end
infil_program.addLine("Set QWHV_sup = #{UnitConversions.convert(sup_cfm_tot + bal_cfm_tot + erv_hrv_cfm_tot, 'cfm', 'm^3/s').round(5)}")
infil_program.addLine("Set QWHV_exh = #{UnitConversions.convert(exh_cfm_tot + bal_cfm_tot + erv_hrv_cfm_tot, 'cfm', 'm^3/s').round(5)}")
@@ -1693,12 +1740,13 @@
infil_program.addLine('EndIf')
infil_program.addLine("Set #{clg_energy_actuator.name} = PreCoolingWatt / #{f_precool.precooling_efficiency_cop}")
end
end
- def self.apply_infiltration_ventilation_to_conditioned(model, site, vent_fans_mech, living_ach50, living_const_ach, weather, vent_fans_kitchen, vent_fans_bath, vented_dryers,
- has_flue_chimney, clg_ssn_sensor, schedules_file, vent_fans_cfis_suppl)
+ def self.apply_infiltration_ventilation_to_conditioned(model, site, vent_fans_mech, living_ach50, living_const_ach, infil_volume, infil_height, weather,
+ vent_fans_kitchen, vent_fans_bath, vented_dryers, has_flue_chimney_in_cond_space, clg_ssn_sensor, schedules_file,
+ vent_fans_cfis_suppl, unavailable_periods)
# Categorize fans into different types
vent_mech_preheat = vent_fans_mech.select { |vent_mech| (not vent_mech.preheating_efficiency_cop.nil?) }
vent_mech_precool = vent_fans_mech.select { |vent_mech| (not vent_mech.precooling_efficiency_cop.nil?) }
vent_mech_sup_tot = vent_fans_mech.select { |vent_mech| vent_mech.fan_type == HPXML::MechVentTypeSupply }
@@ -1706,12 +1754,13 @@
vent_mech_cfis_tot = vent_fans_mech.select { |vent_mech| vent_mech.fan_type == HPXML::MechVentTypeCFIS }
vent_mech_bal_tot = vent_fans_mech.select { |vent_mech| vent_mech.fan_type == HPXML::MechVentTypeBalanced }
vent_mech_erv_hrv_tot = vent_fans_mech.select { |vent_mech| [HPXML::MechVentTypeERV, HPXML::MechVentTypeHRV].include? vent_mech.fan_type }
# Non-CFIS fan power
+ house_fan_unavailable_periods = Schedule.get_unavailable_periods(@runner, SchedulesFile::ColumnHouseFan, unavailable_periods)
add_ee_for_vent_fan_power(model, Constants.ObjectNameMechanicalVentilationHouseFan,
- vent_mech_sup_tot, vent_mech_exh_tot, vent_mech_bal_tot, vent_mech_erv_hrv_tot)
+ vent_mech_sup_tot, vent_mech_exh_tot, vent_mech_bal_tot, vent_mech_erv_hrv_tot, house_fan_unavailable_periods)
# CFIS fan power
cfis_fan_actuator = add_ee_for_vent_fan_power(model, Constants.ObjectNameMechanicalVentilationHouseFanCFIS) # Fan heat enters space
# CFIS supplemental fan power
@@ -1737,11 +1786,11 @@
# Living Space Infiltration Calculation/Program
infil_program = OpenStudio::Model::EnergyManagementSystemProgram.new(model)
infil_program.setName(Constants.ObjectNameInfiltration + ' program')
# Calculate infiltration without adjustment by ventilation
- apply_infiltration_to_conditioned(site, living_ach50, living_const_ach, infil_program, weather, has_flue_chimney)
+ apply_infiltration_to_conditioned(site, living_ach50, living_const_ach, infil_program, weather, has_flue_chimney_in_cond_space, infil_volume, infil_height)
# Common variable and load actuators across multiple mech vent calculations, create only once
fan_sens_load_actuator, fan_lat_load_actuator = setup_mech_vent_vars_actuators(model: model, program: infil_program)
# Apply CFIS
@@ -1750,11 +1799,11 @@
apply_cfis(infil_program, vent_mech_cfis_tot, cfis_fan_actuator, cfis_suppl_fan_actuator)
# Calculate Qfan, Qinf_adj
# Calculate adjusted infiltration based on mechanical ventilation system
apply_infiltration_adjustment_to_conditioned(model, infil_program, vent_fans_kitchen, vent_fans_bath, vented_dryers, vent_mech_sup_tot,
- vent_mech_exh_tot, vent_mech_bal_tot, vent_mech_erv_hrv_tot, infil_flow_actuator, schedules_file)
+ vent_mech_exh_tot, vent_mech_bal_tot, vent_mech_erv_hrv_tot, infil_flow_actuator, schedules_file, unavailable_periods)
# Address load of Qfan (Qload)
# Qload as variable for tracking outdoor air flow rate, excluding recirculation
infil_program.addLine('Set Qload = Qfan')
vent_fans_mech.each do |f|
@@ -1774,64 +1823,46 @@
program_calling_manager.setCallingPoint('BeginZoneTimestepAfterInitHeatBalance')
program_calling_manager.addProgram(infil_program)
end
def self.apply_infiltration_and_ventilation_fans(model, weather, site, vent_fans_mech, vent_fans_kitchen, vent_fans_bath, vented_dryers,
- has_flue_chimney, air_infils, vented_attic, vented_crawl, clg_ssn_sensor, schedules_file,
- vent_fans_cfis_suppl)
- # Get living space infiltration
- living_ach50 = nil
- living_const_ach = nil
- air_infils.each do |air_infil|
- if (air_infil.unit_of_measure == HPXML::UnitsACH) && !air_infil.house_pressure.nil?
- living_achXX = air_infil.air_leakage
- living_ach50 = calc_air_leakage_at_diff_pressure(0.65, living_achXX, air_infil.house_pressure, 50.0)
- elsif (air_infil.unit_of_measure == HPXML::UnitsCFM) && !air_infil.house_pressure.nil?
- living_achXX = air_infil.air_leakage * 60.0 / @infil_volume # Convert CFM to ACH
- living_ach50 = calc_air_leakage_at_diff_pressure(0.65, living_achXX, air_infil.house_pressure, 50.0)
- elsif air_infil.unit_of_measure == HPXML::UnitsACHNatural
- if @apply_ashrae140_assumptions
- living_const_ach = air_infil.air_leakage
- else
- sla = get_infiltration_SLA_from_ACH(air_infil.air_leakage, @infil_height, weather)
- living_ach50 = get_infiltration_ACH50_from_SLA(sla, 0.65, @cfa, @infil_volume)
- end
- end
- end
+ has_flue_chimney_in_cond_space, living_ach50, living_const_ach, infil_volume, infil_height, vented_attic,
+ vented_crawl, clg_ssn_sensor, schedules_file, vent_fans_cfis_suppl, unavailable_periods)
# Infiltration for unconditioned spaces
apply_infiltration_to_garage(model, site, living_ach50)
apply_infiltration_to_unconditioned_basement(model)
apply_infiltration_to_vented_crawlspace(model, weather, vented_crawl)
apply_infiltration_to_unvented_crawlspace(model)
apply_infiltration_to_vented_attic(model, weather, site, vented_attic)
apply_infiltration_to_unvented_attic(model)
# Infiltration/ventilation for conditioned space
- apply_infiltration_ventilation_to_conditioned(model, site, vent_fans_mech, living_ach50, living_const_ach, weather, vent_fans_kitchen, vent_fans_bath, vented_dryers,
- has_flue_chimney, clg_ssn_sensor, schedules_file, vent_fans_cfis_suppl)
+ apply_infiltration_ventilation_to_conditioned(model, site, vent_fans_mech, living_ach50, living_const_ach, infil_volume, infil_height, weather,
+ vent_fans_kitchen, vent_fans_bath, vented_dryers, has_flue_chimney_in_cond_space, clg_ssn_sensor, schedules_file,
+ vent_fans_cfis_suppl, unavailable_periods)
end
- def self.apply_infiltration_to_conditioned(site, living_ach50, living_const_ach, infil_program, weather, has_flue_chimney)
+ def self.apply_infiltration_to_conditioned(site, living_ach50, living_const_ach, infil_program, weather, has_flue_chimney_in_cond_space, infil_volume, infil_height)
site_ap = site.additional_properties
if living_ach50.to_f > 0
# Based on "Field Validation of Algebraic Equations for Stack and
# Wind Driven Air Infiltration Calculations" by Walker and Wilson (1998)
outside_air_density = UnitConversions.convert(weather.header.LocalPressure, 'atm', 'Btu/ft^3') / (Gas.Air.r * (weather.data.AnnualAvgDrybulb + 460.0))
- n_i = 0.65 # Pressure Exponent
- living_sla = get_infiltration_SLA_from_ACH50(living_ach50, n_i, @cfa, @infil_volume) # Calculate SLA
+ n_i = InfilPressureExponent
+ living_sla = get_infiltration_SLA_from_ACH50(living_ach50, n_i, @cfa, infil_volume) # Calculate SLA
a_o = living_sla * @cfa # Effective Leakage Area (ft^2)
# Flow Coefficient (cfm/inH2O^n) (based on ASHRAE HoF)
inf_conv_factor = 776.25 # [ft/min]/[inH2O^(1/2)*ft^(3/2)/lbm^(1/2)]
delta_pref = 0.016 # inH2O
c_i = a_o * (2.0 / outside_air_density)**0.5 * delta_pref**(0.5 - n_i) * inf_conv_factor
- if has_flue_chimney
+ if has_flue_chimney_in_cond_space
y_i = 0.2 # Fraction of leakage through the flue; 0.2 is a "typical" value according to THE ALBERTA AIR INFIL1RATION MODEL, Walker and Wilson, 1990
s_wflue = 1.0 # Flue Shelter Coefficient
else
y_i = 0.0 # Fraction of leakage through the flu
s_wflue = 0.0 # Flue Shelter Coefficient
@@ -1858,11 +1889,11 @@
if m_o <= 1.0
m_i = m_o # eq. 10
else
m_i = 1.0 # eq. 11
end
- if has_flue_chimney
+ if has_flue_chimney_in_cond_space
if @ncfl_ag <= 0
z_f = 1.0
else
z_f = (@ncfl_ag + 0.5) / @ncfl_ag # Typical value is 1.5 according to THE ALBERTA AIR INFIL1RATION MODEL, Walker and Wilson, 1990, presumably for a single story home
end
@@ -1870,11 +1901,11 @@
f_i = n_i * y_i * (z_f - 1.0)**((3.0 * n_i - 1.0) / 3.0) * (1.0 - (3.0 * (x_c - x_i)**2.0 * r_i**(1 - n_i)) / (2.0 * (z_f + 1.0))) # Additive flue function, Eq. 12
else
f_i = 0.0 # Additive flue function (eq. 12)
end
f_s = ((1.0 + n_i * r_i) / (n_i + 1.0)) * (0.5 - 0.5 * m_i**1.2)**(n_i + 1.0) + f_i
- stack_coef = f_s * (UnitConversions.convert(outside_air_density * Constants.g * @infil_height, 'lbm/(ft*s^2)', 'inH2O') / (Constants.AssumedInsideTemp + 460.0))**n_i # inH2O^n/R^n
+ stack_coef = f_s * (UnitConversions.convert(outside_air_density * Constants.g * infil_height, 'lbm/(ft*s^2)', 'inH2O') / (Constants.AssumedInsideTemp + 460.0))**n_i # inH2O^n/R^n
# Calculate wind coefficient
if not @spaces[HPXML::LocationCrawlspaceVented].nil?
if x_i > 1.0 - 2.0 * y_i
# Critical floor to ceiling difference above which f_w does not change (eq. 25)
@@ -1894,11 +1925,11 @@
infil_program.addLine("Set p_m = #{site_ap.ashrae_terrain_exponent}")
infil_program.addLine("Set p_s = #{site_ap.ashrae_site_terrain_exponent}")
infil_program.addLine("Set s_m = #{site_ap.ashrae_terrain_thickness}")
infil_program.addLine("Set s_s = #{site_ap.ashrae_site_terrain_thickness}")
infil_program.addLine("Set z_m = #{UnitConversions.convert(site_ap.height, 'ft', 'm')}")
- infil_program.addLine("Set z_s = #{UnitConversions.convert(@infil_height, 'ft', 'm')}")
+ infil_program.addLine("Set z_s = #{UnitConversions.convert(infil_height, 'ft', 'm')}")
infil_program.addLine('Set f_t = (((s_m/z_m)^p_m)*((z_s/s_s)^p_s))')
infil_program.addLine("Set Tdiff = #{@tin_sensor.name}-#{@tout_sensor.name}")
infil_program.addLine('Set dT = @Abs Tdiff')
infil_program.addLine("Set c = #{((UnitConversions.convert(c_i, 'cfm', 'm^3/s') / (UnitConversions.convert(1.0, 'inH2O', 'Pa')**n_i))).round(4)}")
infil_program.addLine("Set Cs = #{(stack_coef * (UnitConversions.convert(1.0, 'inH2O/R', 'Pa/K')**n_i)).round(4)}")
@@ -1909,11 +1940,11 @@
infil_program.addLine('Set Qinf = (((c*Cs*(dT^n))^2)+temp1)^0.5')
infil_program.addLine('Set Qinf = (@Max Qinf 0)')
elsif living_const_ach.to_f > 0
living_ach = living_const_ach
- infil_program.addLine("Set Qinf = #{living_ach * UnitConversions.convert(@infil_volume, 'ft^3', 'm^3') / UnitConversions.convert(1.0, 'hr', 's')}")
+ infil_program.addLine("Set Qinf = #{living_ach * UnitConversions.convert(infil_volume, 'ft^3', 'm^3') / UnitConversions.convert(1.0, 'hr', 's')}")
else
infil_program.addLine('Set Qinf = 0')
end
end
@@ -1948,63 +1979,103 @@
def self.get_infiltration_SLA_from_ACH(ach, infil_height, weather)
# Returns the infiltration SLA given an annual average ACH.
return ach / (weather.data.WSF * 1000 * (infil_height / 8.202)**0.4)
end
- def self.get_infiltration_SLA_from_ACH50(ach50, n_i, conditionedFloorArea, conditionedVolume, pressure_difference_Pa = 50)
+ def self.get_infiltration_SLA_from_ACH50(ach50, n_i, floor_area, volume)
# Returns the infiltration SLA given a ACH50.
- return ((ach50 * 0.283316478 * 4.0**n_i * conditionedVolume) / (conditionedFloorArea * UnitConversions.convert(1.0, 'ft^2', 'in^2') * pressure_difference_Pa**n_i * 60.0))
+ return ((ach50 * 0.283316478 * 4.0**n_i * volume) / (floor_area * UnitConversions.convert(1.0, 'ft^2', 'in^2') * 50.0**n_i * 60.0))
end
- def self.get_infiltration_ACH50_from_SLA(sla, n_i, conditionedFloorArea, conditionedVolume, pressure_difference_Pa = 50)
+ def self.get_infiltration_ACH50_from_SLA(sla, n_i, floor_area, volume)
# Returns the infiltration ACH50 given a SLA.
- return ((sla * conditionedFloorArea * UnitConversions.convert(1.0, 'ft^2', 'in^2') * pressure_difference_Pa**n_i * 60.0) / (0.283316478 * 4.0**n_i * conditionedVolume))
+ return ((sla * floor_area * UnitConversions.convert(1.0, 'ft^2', 'in^2') * 50.0**n_i * 60.0) / (0.283316478 * 4.0**n_i * volume))
end
def self.calc_duct_leakage_at_diff_pressure(q_old, p_old, p_new)
return q_old * (p_new / p_old)**0.6 # Derived from Equation C-1 (Annex C), p34, ASHRAE Standard 152-2004.
end
def self.calc_air_leakage_at_diff_pressure(n_i, q_old, p_old, p_new)
return q_old * (p_new / p_old)**n_i
end
- def self.get_duct_insulation_rvalue(nominal_rvalue, side)
- # Insulated duct values based on "True R-Values of Round Residential Ductwork"
- # by Palmiter & Kruse 2006. Linear extrapolation from SEEM's "DuctTrueRValues"
- # worksheet in, e.g., ExistingResidentialSingleFamily_SEEMRuns_v05.xlsm.
- #
- # Nominal | 4.2 | 6.0 | 8.0 | 11.0
- # --------|-----|-----|-----|----
- # Supply | 4.5 | 5.7 | 6.8 | 8.4
- # Return | 4.9 | 6.3 | 7.8 | 9.7
- #
- # Uninsulated ducts are set to R-1.7 based on ASHRAE HOF and the above paper.
- if nominal_rvalue <= 0
- return 1.7
+ def self.get_duct_effective_r_value(nominal_rvalue, side, buried_level)
+ if buried_level == HPXML::DuctBuriedInsulationNone
+ # Insulated duct values based on "True R-Values of Round Residential Ductwork"
+ # by Palmiter & Kruse 2006. Linear extrapolation from SEEM's "DuctTrueRValues"
+ # worksheet in, e.g., ExistingResidentialSingleFamily_SEEMRuns_v05.xlsm.
+ #
+ # Nominal | 4.2 | 6.0 | 8.0 | 11.0
+ # --------|-----|-----|-----|----
+ # Supply | 4.5 | 5.7 | 6.8 | 8.4
+ # Return | 4.9 | 6.3 | 7.8 | 9.7
+ #
+ # Uninsulated ducts are set to R-1.7 based on ASHRAE HOF and the above paper.
+
+ if nominal_rvalue <= 0
+ return 1.7
+ end
+ if side == HPXML::DuctTypeSupply
+ return 2.2438 + 0.5619 * nominal_rvalue
+ elsif side == HPXML::DuctTypeReturn
+ return 2.0388 + 0.7053 * nominal_rvalue
+ end
+ else
+ if side == HPXML::DuctTypeSupply
+ # Equations derived from Table 13 in https://www.nrel.gov/docs/fy13osti/55876.pdf
+ # assuming 8-in supply diameter
+ #
+ # Duct configuration | 4.2 | 6.0 | 8.0
+ # -------------------|------|------|-----
+ # Partially-buried | 7.8 | 9.9 | 11.8
+ # Fully buried | 11.3 | 13.2 | 15.1
+ # Deeply buried | 18.1 | 19.6 | 21.0
+
+ if buried_level == HPXML::DuctBuriedInsulationPartial
+ return 5.83 + 2.0 * nominal_rvalue
+ elsif buried_level == HPXML::DuctBuriedInsulationFull
+ return 9.4 + 1.9 * nominal_rvalue
+ elsif buried_level == HPXML::DuctBuriedInsulationDeep
+ return 16.67 + 1.45 * nominal_rvalue
+ end
+ elsif side == HPXML::DuctTypeReturn
+ # Equations derived from Table 13 in https://www.nrel.gov/docs/fy13osti/55876.pdf
+ # assuming 14-in return diameter
+ #
+ # Duct configuration | 4.2 | 6.0 | 8.0
+ # -------------------|------|------|-----
+ # Partially-buried | 10.1 | 12.6 | 15.1
+ # Fully buried | 14.3 | 16.7 | 19.2
+ # Deeply buried | 22.8 | 24.7 | 26.6
+
+ if buried_level == HPXML::DuctBuriedInsulationPartial
+ return 7.6 + 2.5 * nominal_rvalue
+ elsif buried_level == HPXML::DuctBuriedInsulationFull
+ return 11.83 + 2.45 * nominal_rvalue
+ elsif buried_level == HPXML::DuctBuriedInsulationDeep
+ return 20.9 + 1.9 * nominal_rvalue
+ end
+ end
end
- if side == HPXML::DuctTypeSupply
- return 2.2438 + 0.5619 * nominal_rvalue
- elsif side == HPXML::DuctTypeReturn
- return 2.0388 + 0.7053 * nominal_rvalue
- end
end
def self.get_mech_vent_qtot_cfm(nbeds, cfa)
# Returns Qtot cfm per ASHRAE 62.2-2019
return (nbeds + 1.0) * 7.5 + 0.03 * cfa
end
end
class Duct
- def initialize(side, loc_space, loc_schedule, leakage_frac, leakage_cfm25, leakage_cfm50, area, rvalue)
+ def initialize(side, loc_space, loc_schedule, leakage_frac, leakage_cfm25, leakage_cfm50, area, effective_rvalue, buried_level)
@side = side
@loc_space = loc_space
@loc_schedule = loc_schedule
@leakage_frac = leakage_frac
@leakage_cfm25 = leakage_cfm25
@leakage_cfm50 = leakage_cfm50
@area = area
- @rvalue = rvalue
+ @effective_rvalue = effective_rvalue
+ @buried_level = buried_level
end
- attr_accessor(:side, :loc_space, :loc_schedule, :leakage_frac, :leakage_cfm25, :leakage_cfm50, :area, :rvalue, :zone, :location)
+ attr_accessor(:side, :loc_space, :loc_schedule, :leakage_frac, :leakage_cfm25, :leakage_cfm50, :area, :effective_rvalue, :zone, :location, :buried_level)
end