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