example_files/resources/hpxml-measures/HPXMLtoOpenStudio/resources/airflow.rb in urbanopt-cli-0.6.4 vs example_files/resources/hpxml-measures/HPXMLtoOpenStudio/resources/airflow.rb in urbanopt-cli-0.7.0
- old
+ new
@@ -1,16 +1,17 @@
# frozen_string_literal: true
class Airflow
def self.apply(model, runner, weather, spaces, hpxml, cfa, nbeds,
- ncfl_ag, duct_systems, nv_clg_ssn_sensor, hvac_map, eri_version,
+ ncfl_ag, duct_systems, airloop_map, clg_ssn_sensor, eri_version,
frac_windows_operable, apply_ashrae140_assumptions, schedules_file)
# 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.inferred_infiltration_height(@infil_volume)
@living_space = spaces[HPXML::LocationLivingSpace]
@living_zone = @living_space.thermalZone.get
@nbeds = nbeds
@@ -40,24 +41,21 @@
@tout_sensor = OpenStudio::Model::EnergyManagementSystemSensor.new(model, 'Zone Outdoor Air Drybulb Temperature')
@tout_sensor.setName("#{Constants.ObjectNameAirflow} tt s")
@tout_sensor.setKeyName(@living_zone.name.to_s)
- # Adiabatic construction for duct plenum
+ @adiabatic_const = nil
- adiabatic_mat = OpenStudio::Model::MasslessOpaqueMaterial.new(model, 'Rough', 176.1)
- adiabatic_mat.setName('Adiabatic')
- @adiabatic_const = OpenStudio::Model::Construction.new(model)
- @adiabatic_const.setName('AdiabaticConst')
- @adiabatic_const.insertLayer(0, adiabatic_mat)
-
# Ventilation fans
vent_fans_mech = []
vent_fans_kitchen = []
vent_fans_bath = []
vent_fans_whf = []
hpxml.ventilation_fans.each do |vent_fan|
+ next unless vent_fan.flow_rate > 0
+ next unless vent_fan.hours_in_operation.nil? || vent_fan.hours_in_operation > 0
+
if vent_fan.used_for_whole_building_ventilation
vent_fans_mech << vent_fan
elsif vent_fan.used_for_seasonal_cooling_load_reduction
vent_fans_whf << vent_fan
elsif vent_fan.used_for_local_ventilation && vent_fan.fan_location == HPXML::LocationKitchen
@@ -68,19 +66,19 @@
@runner.registerWarning("Unexpected ventilation fan '#{vent_fan.id}'. The fan will not be modeled.")
end
end
# Vented clothes dryers in conditioned space
- vented_dryers = hpxml.clothes_dryers.select { |cd| cd.is_vented && cd.vented_flow_rate.to_f > 0 && [HPXML::LocationLivingSpace, HPXML::LocationBasementConditioned].include?(cd.location) }
+ vented_dryers = hpxml.clothes_dryers.select { |cd| cd.is_vented && cd.vented_flow_rate.to_f > 0 && HPXML::conditioned_locations_this_unit.include?(cd.location) }
# Initialization
- initialize_cfis(model, vent_fans_mech, hvac_map)
+ initialize_cfis(model, vent_fans_mech, airloop_map)
model.getAirLoopHVACs.each do |air_loop|
- initialize_air_loop_objects(model, air_loop)
+ initialize_fan_objects(model, air_loop)
end
model.getZoneHVACFourPipeFanCoils.each do |fan_coil|
- initialize_fan_coil_objects(model, fan_coil)
+ initialize_fan_objects(model, fan_coil)
end
# Apply ducts
duct_systems.each do |ducts, object|
@@ -104,14 +102,14 @@
next unless foundation.foundation_type == HPXML::FoundationTypeCrawlspaceVented
vented_crawl = foundation
end
- apply_natural_ventilation_and_whole_house_fan(model, weather, hpxml.site, vent_fans_whf, open_window_area, nv_clg_ssn_sensor)
+ apply_natural_ventilation_and_whole_house_fan(model, weather, hpxml.site, vent_fans_whf, open_window_area, clg_ssn_sensor)
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, hvac_map, schedules_file)
+ vented_attic, vented_crawl, clg_ssn_sensor, schedules_file)
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
@@ -126,10 +124,14 @@
def self.get_default_vented_crawl_sla()
return (1.0 / 150.0).round(6) # Table 4.2.2(1) - Crawlspaces
end
+ def self.get_default_unvented_space_ach()
+ return 0.1 # Assumption
+ end
+
def self.get_default_mech_vent_fan_power(vent_fan)
# 301-2019: Table 4.2.2(1b)
# Returns fan power in W/cfm
if vent_fan.is_shared_system
return 1.00 # Table 4.2.2(1) Note (n)
@@ -144,10 +146,51 @@
else
fail "Unexpected fan_type: '#{fan_type}'."
end
end
+ def self.get_default_mech_vent_flow_rate(hpxml, vent_fan, infil_measurements, weather, infil_a_ext, cfa, nbeds)
+ # Calculates Qfan cfm requirement per ASHRAE 62.2-2019
+ infil_volume = infil_measurements[0].infiltration_volume
+ infil_height = hpxml.inferred_infiltration_height(infil_volume)
+
+ 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
+ end
+
+ sla = nil
+ infil_measurements.each do |infil_measurement|
+ if (infil_measurement.unit_of_measure == HPXML::UnitsACHNatural) && infil_measurement.house_pressure.nil?
+ nach = infil_measurement.air_leakage
+ sla = get_infiltration_SLA_from_ACH(nach, infil_height, weather)
+ elsif (infil_measurement.unit_of_measure == HPXML::UnitsACH) && (infil_measurement.house_pressure == 50)
+ ach50 = infil_measurement.air_leakage
+ sla = get_infiltration_SLA_from_ACH50(ach50, 0.65, cfa, infil_volume)
+ elsif (infil_measurement.unit_of_measure == HPXML::UnitsCFM) && (infil_measurement.house_pressure == 50)
+ ach50 = infil_measurement.air_leakage * 60.0 / infil_volume
+ sla = get_infiltration_SLA_from_ACH50(ach50, 0.65, cfa, infil_volume)
+ end
+ break unless ach50.nil?
+ end
+
+ nl = get_infiltration_NL_from_SLA(sla, infil_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)
+
+ return [q_fan, 0].max
+ end
+
private
def self.set_wind_speed_correction(model, site)
site_ap = site.additional_properties
@@ -336,11 +379,11 @@
vent_program.addLine("Set NVavail = #{nv_avail_sensor.name}")
vent_program.addLine("Set ClgSsnAvail = #{nv_clg_ssn_sensor.name}")
vent_program.addLine('If (Wout < MaxHR) && (Phiout < MaxRH) && (Tin > Tout) && (Tin > Tnvsp) && (ClgSsnAvail > 0)')
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.rated_flow_rate, 'cfm', 'm^3/s')} * #{whf_avail_sensors[vent_whf.id].name}")
+ 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)')
vent_program.addLine(' Set Adj = (@Min Adj 1)')
vent_program.addLine(' Set Adj = (@Max Adj 0)')
vent_program.addLine(' If (WHF_Flow > 0)') # If available, prioritize whole house fan
@@ -415,10 +458,19 @@
ra_space = ra_space.get
ra_space.setName(loop_name + ' ret air space')
ra_space.setThermalZone(ra_duct_zone)
ra_space.surfaces.each do |surface|
+ if @adiabatic_const.nil?
+ adiabatic_mat = OpenStudio::Model::MasslessOpaqueMaterial.new(model, 'Rough', 176.1)
+ adiabatic_mat.setName('Adiabatic')
+
+ @adiabatic_const = OpenStudio::Model::Construction.new(model)
+ @adiabatic_const.setName('AdiabaticConst')
+ @adiabatic_const.insertLayer(0, adiabatic_mat)
+ end
+
surface.setConstruction(@adiabatic_const)
surface.setOutsideBoundaryCondition('Adiabatic')
surface.setSunExposure('NoSun')
surface.setWindExposure('NoWind')
surface_property_convection_coefficients = OpenStudio::Model::SurfacePropertyConvectionCoefficients.new(surface)
@@ -454,11 +506,11 @@
other_equip.additionalProperties.setFeature(Constants.IsDuctLoadForReport, is_duct_load_for_report)
end
return actuator
end
- def self.initialize_cfis(model, vent_fans_mech, hvac_map)
+ def self.initialize_cfis(model, vent_fans_mech, airloop_map)
# Get AirLoop associated with CFIS
@cfis_airloop = {}
@cfis_t_sum_open_var = {}
@cfis_f_damper_extra_open_var = {}
return if vent_fans_mech.empty?
@@ -466,23 +518,14 @@
index = 0
vent_fans_mech.each do |vent_mech|
next unless (vent_mech.fan_type == HPXML::MechVentTypeCFIS)
- cfis_sys_ids = vent_mech.distribution_system.hvac_systems.map { |system| system.id }
- # Get AirLoopHVACs associated with these HVAC systems
- hvac_map.each do |sys_id, hvacs|
- next unless cfis_sys_ids.include? sys_id
+ vent_mech.distribution_system.hvac_systems.map { |system| system.id }.each do |cfis_id|
+ next if airloop_map[cfis_id].nil?
- hvacs.each do |loop|
- next unless loop.is_a? OpenStudio::Model::AirLoopHVAC
- next if (not @cfis_airloop[vent_mech.id].nil?) && (@cfis_airloop[vent_mech.id] == loop) # already assigned
-
- fail 'Two airloops found for CFIS.' unless @cfis_airloop[vent_mech.id].nil?
-
- @cfis_airloop[vent_mech.id] = loop
- end
+ @cfis_airloop[vent_mech.id] = airloop_map[cfis_id]
end
@cfis_t_sum_open_var[vent_mech.id] = OpenStudio::Model::EnergyManagementSystemGlobalVariable.new(model, "#{Constants.ObjectNameMechanicalVentilation.gsub(' ', '_')}_cfis_t_sum_open_#{index}") # Sums the time during an hour the CFIS damper has been open
@cfis_f_damper_extra_open_var[vent_mech.id] = OpenStudio::Model::EnergyManagementSystemGlobalVariable.new(model, "#{Constants.ObjectNameMechanicalVentilation.gsub(' ', '_')}_cfis_f_extra_damper_open_#{index}") # Fraction of timestep the CFIS blower is running while hvac is not operating. Used by infiltration and duct leakage programs
@@ -504,78 +547,58 @@
index += 1
end
end
- def self.initialize_air_loop_objects(model, air_loop)
+ def self.initialize_fan_objects(model, osm_object)
@fan_rtf_var = {} if @fan_rtf_var.nil?
@fan_mfr_max_var = {} if @fan_mfr_max_var.nil?
@fan_rtf_sensor = {} if @fan_rtf_sensor.nil?
@fan_mfr_sensor = {} if @fan_mfr_sensor.nil?
# Get the supply fan
- system = HVAC.get_unitary_system_from_air_loop_hvac(air_loop)
- if system.nil? # Evaporative cooler supply fan directly on air loop
- supply_fan = air_loop.supplyFan.get
+ if osm_object.is_a? OpenStudio::Model::ZoneHVACFourPipeFanCoil
+ supply_fan = osm_object.supplyAirFan
+ elsif osm_object.is_a? OpenStudio::Model::AirLoopHVAC
+ system = HVAC.get_unitary_system_from_air_loop_hvac(osm_object)
+ if system.nil? # Evaporative cooler supply fan directly on air loop
+ supply_fan = osm_object.supplyFan.get
+ else
+ supply_fan = system.supplyFan.get
+ end
else
- supply_fan = system.supplyFan.get
+ fail 'Unexpected object type.'
end
- @fan_rtf_var[air_loop] = OpenStudio::Model::EnergyManagementSystemGlobalVariable.new(model, "#{air_loop.name} Fan RTF".gsub(' ', '_'))
+ @fan_rtf_var[osm_object] = OpenStudio::Model::EnergyManagementSystemGlobalVariable.new(model, "#{osm_object.name} Fan RTF".gsub(' ', '_'))
# Supply fan maximum mass flow rate
- @fan_mfr_max_var[air_loop] = OpenStudio::Model::EnergyManagementSystemInternalVariable.new(model, EPlus::EMSIntVarFanMFR)
- @fan_mfr_max_var[air_loop].setName("#{air_loop.name} max sup fan mfr")
- @fan_mfr_max_var[air_loop].setInternalDataIndexKeyName(supply_fan.name.to_s)
+ @fan_mfr_max_var[osm_object] = OpenStudio::Model::EnergyManagementSystemInternalVariable.new(model, EPlus::EMSIntVarFanMFR)
+ @fan_mfr_max_var[osm_object].setName("#{osm_object.name} max sup fan mfr")
+ @fan_mfr_max_var[osm_object].setInternalDataIndexKeyName(supply_fan.name.to_s)
- if supply_fan.to_FanOnOff.is_initialized
- @fan_rtf_sensor[air_loop] = OpenStudio::Model::EnergyManagementSystemSensor.new(model, 'Fan Runtime Fraction')
- @fan_rtf_sensor[air_loop].setName("#{@fan_rtf_var[air_loop].name} s")
- @fan_rtf_sensor[air_loop].setKeyName(supply_fan.name.to_s)
- elsif supply_fan.to_FanVariableVolume.is_initialized # Evaporative cooler
- @fan_mfr_sensor[air_loop] = OpenStudio::Model::EnergyManagementSystemSensor.new(model, 'Fan Air Mass Flow Rate')
- @fan_mfr_sensor[air_loop].setName("#{supply_fan.name} air MFR")
- @fan_mfr_sensor[air_loop].setKeyName("#{supply_fan.name}")
- @fan_rtf_sensor[air_loop] = OpenStudio::Model::EnergyManagementSystemGlobalVariable.new(model, "#{@fan_rtf_var[air_loop].name}_s")
+ if supply_fan.to_FanSystemModel.is_initialized
+ @fan_rtf_sensor[osm_object] = []
+ num_speeds = supply_fan.to_FanSystemModel.get.numberofSpeeds
+ for i in 1..num_speeds
+ if num_speeds == 1
+ var_name = 'Fan Runtime Fraction'
+ else
+ var_name = "Fan Runtime Fraction Speed #{i}"
+ end
+ rtf_sensor = OpenStudio::Model::EnergyManagementSystemSensor.new(model, var_name)
+ rtf_sensor.setName("#{@fan_rtf_var[osm_object].name} s")
+ rtf_sensor.setKeyName(supply_fan.name.to_s)
+ @fan_rtf_sensor[osm_object] << rtf_sensor
+ end
else
fail "Unexpected fan: #{supply_fan.name}"
end
end
- def self.initialize_fan_coil_objects(model, fan_coil)
- @fan_rtf_var = {} if @fan_rtf_var.nil?
- @fan_mfr_max_var = {} if @fan_mfr_max_var.nil?
- @fan_rtf_sensor = {} if @fan_rtf_sensor.nil?
- @fan_mfr_sensor = {} if @fan_mfr_sensor.nil?
-
- # Get the supply fan
- supply_fan = fan_coil.supplyAirFan
-
- @fan_rtf_var[fan_coil] = OpenStudio::Model::EnergyManagementSystemGlobalVariable.new(model, "#{fan_coil.name} Fan RTF".gsub(' ', '_'))
-
- # Supply fan maximum mass flow rate
- @fan_mfr_max_var[fan_coil] = OpenStudio::Model::EnergyManagementSystemInternalVariable.new(model, EPlus::EMSIntVarFanMFR)
- @fan_mfr_max_var[fan_coil].setName("#{fan_coil.name} max sup fan mfr")
- @fan_mfr_max_var[fan_coil].setInternalDataIndexKeyName(supply_fan.name.to_s)
-
- if supply_fan.to_FanOnOff.is_initialized
- @fan_rtf_sensor[fan_coil] = OpenStudio::Model::EnergyManagementSystemSensor.new(model, 'Fan Runtime Fraction')
- @fan_rtf_sensor[fan_coil].setName("#{@fan_rtf_var[fan_coil].name} s")
- @fan_rtf_sensor[fan_coil].setKeyName(supply_fan.name.to_s)
- else
- fail "Unexpected fan: #{supply_fan.name}"
- end
- end
-
def self.apply_ducts(model, ducts, object)
ducts.each do |duct|
- if duct.leakage_frac.nil? == duct.leakage_cfm25.nil?
- fail 'Ducts: Must provide either leakage fraction or cfm25, but not both.'
- end
- end
-
- 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?
@@ -870,10 +893,13 @@
leakage_fracs[duct.side] = 0 if leakage_fracs[duct.side].nil?
leakage_fracs[duct.side] += duct.leakage_frac
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)
end
ua_values[duct.side] += duct.area / duct.rvalue
end
# Calculate fraction of outside air specific to this duct location
@@ -1001,14 +1027,14 @@
# Duct Program
duct_program = OpenStudio::Model::EnergyManagementSystemProgram.new(model)
duct_program.setName(object_name_idx + ' duct program')
duct_program.addLine("Set #{ah_mfr_var.name} = #{ah_mfr_sensor.name}")
- if @fan_rtf_sensor[object].is_a? OpenStudio::Model::EnergyManagementSystemGlobalVariable
- duct_program.addLine("Set #{@fan_rtf_sensor[object].name} = #{@fan_mfr_sensor[object].name} / #{@fan_mfr_max_var[object].name}")
+ duct_program.addLine("Set #{@fan_rtf_var[object].name} = 0")
+ @fan_rtf_sensor[object].each do |rtf_sensor|
+ duct_program.addLine("Set #{@fan_rtf_var[object].name} = #{@fan_rtf_var[object].name} + #{rtf_sensor.name}")
end
- duct_program.addLine("Set #{@fan_rtf_var[object].name} = #{@fan_rtf_sensor[object].name}")
duct_program.addLine("Set #{ah_vfr_var.name} = #{ah_vfr_sensor.name}")
duct_program.addLine("Set #{ah_tout_var.name} = #{ah_tout_sensor.name}")
duct_program.addLine("Set #{ah_wout_var.name} = #{ah_wout_sensor.name}")
duct_program.addLine("Set #{ra_t_var.name} = #{ra_t_sensor.name}")
duct_program.addLine("Set #{ra_w_var.name} = #{ra_w_sensor.name}")
@@ -1110,33 +1136,33 @@
def self.apply_infiltration_to_unconditioned_basement(model, weather)
return if @spaces[HPXML::LocationBasementUnconditioned].nil?
space = @spaces[HPXML::LocationBasementUnconditioned]
volume = UnitConversions.convert(space.volume, 'm^3', 'ft^3')
- ach = 0.1 # Assumption
+ ach = get_default_unvented_space_ach()
cfm = ach / UnitConversions.convert(1.0, 'hr', 'min') * volume
apply_infiltration_to_unconditioned_space(model, space, ach, nil, nil, nil)
end
def self.apply_infiltration_to_vented_crawlspace(model, weather, vented_crawl)
return if @spaces[HPXML::LocationCrawlspaceVented].nil?
space = @spaces[HPXML::LocationCrawlspaceVented]
volume = UnitConversions.convert(space.volume, 'm^3', 'ft^3')
+ height = Geometry.get_height_of_spaces([space])
sla = vented_crawl.vented_crawlspace_sla
- ach = get_infiltration_ACH_from_SLA(sla, 8.202, weather)
+ ach = get_infiltration_ACH_from_SLA(sla, height, weather)
cfm = ach / UnitConversions.convert(1.0, 'hr', 'min') * volume
apply_infiltration_to_unconditioned_space(model, space, ach, nil, nil, nil)
end
def self.apply_infiltration_to_unvented_crawlspace(model, weather)
return if @spaces[HPXML::LocationCrawlspaceUnvented].nil?
space = @spaces[HPXML::LocationCrawlspaceUnvented]
volume = UnitConversions.convert(space.volume, 'm^3', 'ft^3')
- sla = 0 # Assumption
- ach = get_infiltration_ACH_from_SLA(sla, 8.202, weather)
+ ach = get_default_unvented_space_ach()
cfm = ach / UnitConversions.convert(1.0, 'hr', 'min') * volume
apply_infiltration_to_unconditioned_space(model, space, ach, nil, nil, nil)
end
def self.apply_infiltration_to_vented_attic(model, weather, site, vented_attic)
@@ -1177,20 +1203,14 @@
def self.apply_infiltration_to_unvented_attic(model, weather, site)
return if @spaces[HPXML::LocationAtticUnvented].nil?
space = @spaces[HPXML::LocationAtticUnvented]
- area = UnitConversions.convert(space.floorArea, 'm^2', 'ft^2')
volume = UnitConversions.convert(space.volume, 'm^3', 'ft^3')
- hor_lk_frac = 0.75
- neutral_level = 0.5
- sla = 0 # Assumption
- ach = get_infiltration_ACH_from_SLA(sla, 8.202, weather)
- ela = sla * area
+ ach = get_default_unvented_space_ach()
cfm = ach / UnitConversions.convert(1.0, 'hr', 'min') * volume
- 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)
+ apply_infiltration_to_unconditioned_space(model, space, ach, nil, nil, nil)
end
def self.apply_local_ventilation(model, vent_object_array, obj_type_name)
obj_sch_sensors = {}
vent_object_array.each_with_index do |vent_object, index|
@@ -1232,24 +1252,29 @@
obj_type_name = Constants.ObjectNameClothesDryerExhaust
vented_dryers.each_with_index do |vented_dryer, index|
obj_name = "#{obj_type_name} #{index}"
if not schedules_file.nil?
- obj_sch = schedules_file.create_schedule_file(col_name: 'clothes_dryer_exhaust')
- obj_sch_name = 'clothes_dryer_exhaust'
+ obj_sch = schedules_file.create_schedule_file(col_name: 'clothes_dryer')
+ obj_sch_name = 'clothes_dryer'
+ full_load_hrs = schedules_file.annual_equivalent_full_load_hrs(col_name: 'clothes_dryer')
else
- days_shift = -1.0 / 24.0 # Shift by 1 hour relative to clothes washer
- obj_sch = HotWaterSchedule.new(model, obj_type_name, @nbeds, days_shift, 24)
+ 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 = 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
- Schedule.set_schedule_type_limits(model, obj_sch, Constants.ScheduleTypeLimitsFraction)
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)
- obj_sch_sensors[vented_dryer.id] = obj_sch_sensor
+ obj_sch_sensors[vented_dryer.id] = [obj_sch_sensor, cfm_mult]
end
return obj_sch_sensors
end
@@ -1347,11 +1372,14 @@
def self.apply_cfis(infil_program, vent_mech_fans, cfis_fan_actuator)
infil_program.addLine('Set QWHV_cfis_oa = 0.0')
vent_mech_fans.each do |vent_mech|
- infil_program.addLine("Set fan_rtf_hvac = #{@fan_rtf_sensor[@cfis_airloop[vent_mech.id]].name}")
+ infil_program.addLine('Set fan_rtf_hvac = 0')
+ @fan_rtf_sensor[@cfis_airloop[vent_mech.id]].each do |rtf_sensor|
+ infil_program.addLine("Set fan_rtf_hvac = fan_rtf_hvac + #{rtf_sensor.name}")
+ end
infil_program.addLine("Set CFIS_fan_w = #{vent_mech.unit_fan_power}") # W
infil_program.addLine('If @ABS(Minute - ZoneTimeStep*60) < 0.1')
infil_program.addLine(" Set #{@cfis_t_sum_open_var[vent_mech.id].name} = 0") # New hour, time on summation re-initializes to 0
infil_program.addLine('EndIf')
@@ -1444,21 +1472,21 @@
def self.apply_infiltration_adjustment(infil_program, vent_fans_kitchen, vent_fans_bath, vented_dryers, sup_cfm_tot, exh_cfm_tot, bal_cfm_tot, erv_hrv_cfm_tot,
infil_flow_actuator, range_sch_sensors_map, bath_sch_sensors_map, dryer_exhaust_sch_sensors_map)
infil_program.addLine('Set Qrange = 0')
vent_fans_kitchen.each do |vent_kitchen|
- infil_program.addLine("Set Qrange = Qrange + #{UnitConversions.convert(vent_kitchen.rated_flow_rate * vent_kitchen.quantity, 'cfm', 'm^3/s').round(4)} * #{range_sch_sensors_map[vent_kitchen.id].name}")
+ infil_program.addLine("Set Qrange = Qrange + #{UnitConversions.convert(vent_kitchen.flow_rate * vent_kitchen.quantity, 'cfm', 'm^3/s').round(4)} * #{range_sch_sensors_map[vent_kitchen.id].name}")
end
infil_program.addLine('Set Qbath = 0')
vent_fans_bath.each do |vent_bath|
- infil_program.addLine("Set Qbath = Qbath + #{UnitConversions.convert(vent_bath.rated_flow_rate * vent_bath.quantity, 'cfm', 'm^3/s').round(4)} * #{bath_sch_sensors_map[vent_bath.id].name}")
+ infil_program.addLine("Set Qbath = Qbath + #{UnitConversions.convert(vent_bath.flow_rate * vent_bath.quantity, 'cfm', 'm^3/s').round(4)} * #{bath_sch_sensors_map[vent_bath.id].name}")
end
infil_program.addLine('Set Qdryer = 0')
vented_dryers.each do |vented_dryer|
- infil_program.addLine("Set Qdryer = Qdryer + #{UnitConversions.convert(vented_dryer.vented_flow_rate, 'cfm', 'm^3/s').round(4)} * #{dryer_exhaust_sch_sensors_map[vented_dryer.id].name}")
+ infil_program.addLine("Set Qdryer = Qdryer + #{UnitConversions.convert(vented_dryer.vented_flow_rate * dryer_exhaust_sch_sensors_map[vented_dryer.id][1], 'cfm', 'm^3/s').round(5)} * #{dryer_exhaust_sch_sensors_map[vented_dryer.id][0].name}")
end
infil_program.addLine("Set QWHV_sup = #{UnitConversions.convert(sup_cfm_tot, 'cfm', 'm^3/s').round(4)}")
infil_program.addLine("Set QWHV_exh = #{UnitConversions.convert(exh_cfm_tot, 'cfm', 'm^3/s').round(4)}")
infil_program.addLine("Set QWHV_bal_erv_hrv = #{UnitConversions.convert(bal_cfm_tot + erv_hrv_cfm_tot, 'cfm', 'm^3/s').round(4)}")
@@ -1495,10 +1523,12 @@
infil_program.addLine('Set Effectiveness_Lat = 0.0')
# Calculate mass flow rate based on outdoor air density
# Address load with flow-weighted combined effectiveness
infil_program.addLine("Set Fan_MFR = #{q_var} * OASupRho")
+ infil_program.addLine('Set ZoneInEnth = OASupInEnth')
+ infil_program.addLine('Set ZoneInTemp = OASupInTemp')
if not vent_mech_erv_hrv_tot.empty?
# ERV/HRV EMS load model
# E+ ERV model is using standard density for MFR calculation, caused discrepancy with other system types.
# Therefore ERV is modeled within EMS infiltration program
vent_mech_erv_hrv_tot.each do |vent_fan|
@@ -1511,74 +1541,94 @@
infil_program.addLine('Set ERVSupOutEnth = (@HFnTdbW ERVSupOutTemp ERVSupOutW)')
infil_program.addLine('Set ERVSensHeatTrans = Fan_MFR * OASupCp * (ERVSupOutTemp - OASupInTemp)')
infil_program.addLine('Set ERVTotalHeatTrans = Fan_MFR * (ERVSupOutEnth - OASupInEnth)')
infil_program.addLine('Set ERVLatHeatTrans = ERVTotalHeatTrans - ERVSensHeatTrans')
# ERV/HRV Load calculation
- infil_program.addLine('Set FanTotalToLv = Fan_MFR * (ERVSupOutEnth - ZoneAirEnth)')
- infil_program.addLine('Set FanSensToLv = Fan_MFR * ZoneCp * (ERVSupOutTemp - ZoneTemp)')
- infil_program.addLine('Set FanLatToLv = FanTotalToLv - FanSensToLv')
- else
- infil_program.addLine('Set FanTotalToLv = Fan_MFR * (OASupInEnth - ZoneAirEnth)')
- infil_program.addLine('Set FanSensToLv = Fan_MFR * ZoneCp * (OASupInTemp - ZoneTemp)')
- infil_program.addLine('Set FanLatToLv = FanTotalToLv - FanSensToLv')
+ infil_program.addLine('Set ZoneInEnth = ERVSupOutEnth')
+ infil_program.addLine('Set ZoneInTemp = ERVSupOutTemp')
end
+ infil_program.addLine('Set FanTotalToLv = Fan_MFR * (ZoneInEnth - ZoneAirEnth)')
+ infil_program.addLine('Set FanSensToLv = Fan_MFR * ZoneCp * (ZoneInTemp - ZoneTemp)')
+ infil_program.addLine('Set FanLatToLv = FanTotalToLv - FanSensToLv')
# Actuator,
# If preconditioned, handle actuators later in calculate_precond_loads
if not preconditioned
infil_program.addLine("Set #{fan_sens_load_actuator.name} = #{fan_sens_load_actuator.name} + FanSensToLv")
infil_program.addLine("Set #{fan_lat_load_actuator.name} = #{fan_lat_load_actuator.name} + FanLatToLv")
end
end
- def self.calculate_precond_loads(model, infil_program, vent_mech_preheat, vent_mech_precool, hrv_erv_effectiveness_map, fan_sens_load_actuator, fan_lat_load_actuator, hvac_map)
+ def self.calculate_precond_loads(model, infil_program, vent_mech_preheat, vent_mech_precool, hrv_erv_effectiveness_map, fan_sens_load_actuator, fan_lat_load_actuator, clg_ssn_sensor)
# Preconditioning
# Assume introducing no sensible loads to zone if preconditioned
+ if not vent_mech_preheat.empty?
+ htg_stp_sensor = OpenStudio::Model::EnergyManagementSystemSensor.new(model, 'Zone Thermostat Heating Setpoint Temperature')
+ htg_stp_sensor.setName("#{Constants.ObjectNameAirflow} htg stp s")
+ htg_stp_sensor.setKeyName(@living_zone.name.to_s)
+ infil_program.addLine("Set HtgStp = #{htg_stp_sensor.name}") # heating thermostat setpoint
+ end
+ if not vent_mech_precool.empty?
+ clg_stp_sensor = OpenStudio::Model::EnergyManagementSystemSensor.new(model, 'Zone Thermostat Cooling Setpoint Temperature')
+ clg_stp_sensor.setName("#{Constants.ObjectNameAirflow} clg stp s")
+ clg_stp_sensor.setKeyName(@living_zone.name.to_s)
+ infil_program.addLine("Set ClgStp = #{clg_stp_sensor.name}") # cooling thermostat setpoint
+ end
vent_mech_preheat.each_with_index do |f_preheat, i|
- infil_program.addLine('If OASupInTemp < ZoneTemp')
- htg_energy_actuator = create_other_equipment_object_and_actuator(model: model, name: "shared mech vent preheating energy #{i}", space: @living_space, frac_lat: 0.0, frac_lost: 1.0, hpxml_fuel_type: f_preheat.preheating_fuel, end_use: Constants.ObjectNameMechanicalVentilationPreconditioning)
- hvac_map["#{f_preheat.id}_preheat"] = [htg_energy_actuator.actuatedComponent.get]
+ infil_program.addLine("If (OASupInTemp < HtgStp) && (#{clg_ssn_sensor.name} < 1)")
+ htg_energy_actuator = create_other_equipment_object_and_actuator(model: model, name: "shared mech vent preheating energy #{i}", space: @living_space, frac_lat: 0.0, frac_lost: 1.0, hpxml_fuel_type: f_preheat.preheating_fuel, end_use: Constants.ObjectNameMechanicalVentilationPreheating)
+ htg_energy_actuator.actuatedComponent.get.additionalProperties.setFeature('HPXML_ID', f_preheat.id) # Used by reporting measure
infil_program.addLine(" Set Qpreheat = #{UnitConversions.convert(f_preheat.average_oa_unit_flow_rate, 'cfm', 'm^3/s').round(4)}")
if [HPXML::MechVentTypeERV, HPXML::MechVentTypeHRV].include? f_preheat.fan_type
vent_mech_erv_hrv_tot = [f_preheat]
else
vent_mech_erv_hrv_tot = []
end
calculate_fan_loads(model, infil_program, vent_mech_erv_hrv_tot, hrv_erv_effectiveness_map, fan_sens_load_actuator, fan_lat_load_actuator, 'Qpreheat', true)
- infil_program.addLine(" Set PreHeatingEnergy = (-FanSensToLv) * #{f_preheat.preheating_fraction_load_served}")
- infil_program.addLine(" Set #{fan_sens_load_actuator.name} = #{fan_sens_load_actuator.name} + PreHeatingEnergy")
- infil_program.addLine(" Set #{fan_lat_load_actuator.name} = #{fan_lat_load_actuator.name} - FanLatToLv")
- infil_program.addLine(" Set #{htg_energy_actuator.name} = PreHeatingEnergy / #{f_preheat.preheating_efficiency_cop}")
+ infil_program.addLine(' If ZoneInTemp < HtgStp')
+ infil_program.addLine(' Set FanSensToSpt = Fan_MFR * ZoneCp * (ZoneInTemp - HtgStp)')
+ infil_program.addLine(" Set PreHeatingWatt = (-FanSensToSpt) * #{f_preheat.preheating_fraction_load_served}")
+ infil_program.addLine(" Set #{fan_sens_load_actuator.name} = #{fan_sens_load_actuator.name} + PreHeatingWatt")
+ infil_program.addLine(" Set #{fan_lat_load_actuator.name} = #{fan_lat_load_actuator.name} - FanLatToLv") # Fixme:Does this assumption still apply?
+ infil_program.addLine(' Else')
+ infil_program.addLine(' Set PreHeatingWatt = 0.0')
+ infil_program.addLine(' EndIf')
infil_program.addLine('Else')
- infil_program.addLine(" Set #{htg_energy_actuator.name} = 0.0")
+ infil_program.addLine(' Set PreHeatingWatt = 0.0')
infil_program.addLine('EndIf')
+ infil_program.addLine("Set #{htg_energy_actuator.name} = PreHeatingWatt / #{f_preheat.preheating_efficiency_cop}")
end
vent_mech_precool.each_with_index do |f_precool, i|
- infil_program.addLine('If OASupInTemp > ZoneTemp')
- clg_energy_actuator = create_other_equipment_object_and_actuator(model: model, name: "shared mech vent precooling energy #{i}", space: @living_space, frac_lat: 0.0, frac_lost: 1.0, hpxml_fuel_type: f_precool.precooling_fuel, end_use: Constants.ObjectNameMechanicalVentilationPreconditioning)
- hvac_map["#{f_precool.id}_precool"] = [clg_energy_actuator.actuatedComponent.get]
+ infil_program.addLine("If (OASupInTemp > ClgStp) && (#{clg_ssn_sensor.name} > 0)")
+ clg_energy_actuator = create_other_equipment_object_and_actuator(model: model, name: "shared mech vent precooling energy #{i}", space: @living_space, frac_lat: 0.0, frac_lost: 1.0, hpxml_fuel_type: f_precool.precooling_fuel, end_use: Constants.ObjectNameMechanicalVentilationPrecooling)
+ clg_energy_actuator.actuatedComponent.get.additionalProperties.setFeature('HPXML_ID', f_precool.id) # Used by reporting measure
infil_program.addLine(" Set Qprecool = #{UnitConversions.convert(f_precool.average_oa_unit_flow_rate, 'cfm', 'm^3/s').round(4)}")
if [HPXML::MechVentTypeERV, HPXML::MechVentTypeHRV].include? f_precool.fan_type
vent_mech_erv_hrv_tot = [f_precool]
else
vent_mech_erv_hrv_tot = []
end
calculate_fan_loads(model, infil_program, vent_mech_erv_hrv_tot, hrv_erv_effectiveness_map, fan_sens_load_actuator, fan_lat_load_actuator, 'Qprecool', true)
- infil_program.addLine(" Set PreCoolingEnergy = FanSensToLv * #{f_precool.precooling_fraction_load_served}")
- infil_program.addLine(" Set #{fan_sens_load_actuator.name} = #{fan_sens_load_actuator.name} - PreCoolingEnergy")
- infil_program.addLine(" Set #{fan_lat_load_actuator.name} = #{fan_lat_load_actuator.name} - FanLatToLv")
- infil_program.addLine(" Set #{clg_energy_actuator.name} = PreCoolingEnergy / #{f_precool.precooling_efficiency_cop}")
+ infil_program.addLine(' If ZoneInTemp > ClgStp')
+ infil_program.addLine(' Set FanSensToSpt = Fan_MFR * ZoneCp * (ZoneInTemp - ClgStp)')
+ infil_program.addLine(" Set PreCoolingWatt = FanSensToSpt * #{f_precool.precooling_fraction_load_served}")
+ infil_program.addLine(" Set #{fan_sens_load_actuator.name} = #{fan_sens_load_actuator.name} - PreCoolingWatt")
+ infil_program.addLine(" Set #{fan_lat_load_actuator.name} = #{fan_lat_load_actuator.name} - FanLatToLv") # Fixme:Does this assumption still apply?
+ infil_program.addLine(' Else')
+ infil_program.addLine(' Set PreCoolingWatt = 0.0')
+ infil_program.addLine(' EndIf')
infil_program.addLine('Else')
- infil_program.addLine(" Set #{clg_energy_actuator.name} = 0.0")
+ infil_program.addLine(' Set PreCoolingWatt = 0.0')
infil_program.addLine('EndIf')
+ infil_program.addLine("Set #{clg_energy_actuator.name} = PreCoolingWatt / #{f_precool.precooling_efficiency_cop}")
end
end
def self.apply_infiltration_and_mechanical_ventilation(model, site, vent_fans_mech, living_ach50, living_const_ach, weather, vent_fans_kitchen, vent_fans_bath, vented_dryers,
- range_sch_sensors_map, bath_sch_sensors_map, dryer_exhaust_sch_sensors_map, has_flue_chimney, hvac_map)
+ range_sch_sensors_map, bath_sch_sensors_map, dryer_exhaust_sch_sensors_map, has_flue_chimney, clg_ssn_sensor)
# 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_shared = vent_fans_mech.select { |vent_mech| vent_mech.is_shared_system }
@@ -1649,20 +1699,20 @@
infil_program.addLine("Set Qload = Qload - #{UnitConversions.convert(f.average_total_unit_flow_rate - f.average_oa_unit_flow_rate, 'cfm', 'm^3/s').round(4)}")
end
calculate_fan_loads(model, infil_program, vent_mech_erv_hrv_tot, hrv_erv_effectiveness_map, fan_sens_load_actuator, fan_lat_load_actuator, 'Qload')
# Address preconditioning
- calculate_precond_loads(model, infil_program, vent_mech_preheat, vent_mech_precool, hrv_erv_effectiveness_map, fan_sens_load_actuator, fan_lat_load_actuator, hvac_map)
+ calculate_precond_loads(model, infil_program, vent_mech_preheat, vent_mech_precool, hrv_erv_effectiveness_map, fan_sens_load_actuator, fan_lat_load_actuator, clg_ssn_sensor)
program_calling_manager = OpenStudio::Model::EnergyManagementSystemProgramCallingManager.new(model)
program_calling_manager.setName("#{infil_program.name} calling manager")
program_calling_manager.setCallingPoint('BeginTimestepBeforePredictor')
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, hvac_map, schedules_file)
+ has_flue_chimney, air_infils, vented_attic, vented_crawl, clg_ssn_sensor, schedules_file)
# 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?
@@ -1696,11 +1746,11 @@
# Clothes dryer exhaust
dryer_exhaust_sch_sensors_map = apply_dryer_exhaust(model, vented_dryers, schedules_file)
# Get mechanical ventilation
apply_infiltration_and_mechanical_ventilation(model, site, vent_fans_mech, living_ach50, living_const_ach, weather, vent_fans_kitchen, vent_fans_bath, vented_dryers,
- range_sch_sensors_map, bath_sch_sensors_map, dryer_exhaust_sch_sensors_map, has_flue_chimney, hvac_map)
+ range_sch_sensors_map, bath_sch_sensors_map, dryer_exhaust_sch_sensors_map, has_flue_chimney, clg_ssn_sensor)
end
def self.apply_infiltration_to_living(site, living_ach50, living_const_ach, infil_program, weather, has_flue_chimney)
site_ap = site.additional_properties
@@ -1798,11 +1848,11 @@
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)}")
infil_program.addLine("Set Cw = #{(wind_coef * (UnitConversions.convert(1.0, 'inH2O/mph^2', 'Pa*s^2/m^2')**n_i)).round(4)}")
infil_program.addLine("Set n = #{n_i}")
- infil_program.addLine("Set sft = (f_t*#{(((site_ap.aim2_shelter_coeff * (1.0 - y_i)) + (s_wflue * (1.5 * y_i))))})")
+ infil_program.addLine("Set sft = (f_t*#{(site_ap.aim2_shelter_coeff * (1.0 - y_i)) + (s_wflue * (1.5 * y_i))})")
infil_program.addLine("Set temp1 = ((c*Cw)*((sft*#{@vwind_sensor.name})^(2*n)))^2")
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
@@ -1886,27 +1936,24 @@
elsif side == HPXML::DuctTypeReturn
return 2.0388 + 0.7053 * nominal_rvalue
end
end
- def self.get_mech_vent_whole_house_cfm(frac622, num_beds, cfa, std)
- # Returns the ASHRAE 62.2 whole house mechanical ventilation rate, excluding any infiltration credit.
- if std == '2013'
- return frac622 * ((num_beds + 1.0) * 7.5 + 0.03 * cfa)
- end
-
- return frac622 * ((num_beds + 1.0) * 7.5 + 0.01 * cfa)
+ 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, area, rvalue)
+ def initialize(side, loc_space, loc_schedule, leakage_frac, leakage_cfm25, leakage_cfm50, area, rvalue)
@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
end
- attr_accessor(:side, :loc_space, :loc_schedule, :leakage_frac, :leakage_cfm25, :area, :rvalue, :zone, :location)
+ attr_accessor(:side, :loc_space, :loc_schedule, :leakage_frac, :leakage_cfm25, :leakage_cfm50, :area, :rvalue, :zone, :location)
end