example_files/resources/hpxml-measures/HPXMLtoOpenStudio/resources/airflow.rb in urbanopt-cli-0.8.3 vs example_files/resources/hpxml-measures/HPXMLtoOpenStudio/resources/airflow.rb in urbanopt-cli-0.9.0

- old
+ new

@@ -50,16 +50,20 @@ # Ventilation fans vent_fans_mech = [] vent_fans_kitchen = [] vent_fans_bath = [] vent_fans_whf = [] + vent_fans_cfis_suppl = [] 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 + if not vent_fan.is_cfis_supplemental_fan? + vent_fans_mech << vent_fan + else + vent_fans_cfis_suppl << vent_fan + end elsif vent_fan.used_for_seasonal_cooling_load_reduction vent_fans_whf << vent_fan elsif vent_fan.used_for_local_ventilation if vent_fan.fan_location == HPXML::LocationKitchen vent_fans_kitchen << vent_fan @@ -106,14 +110,15 @@ vented_crawl = foundation break end - 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, @runner, weather, hpxml.site, vent_fans_mech, vent_fans_kitchen, vent_fans_bath, vented_dryers, + 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_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) + vented_attic, vented_crawl, clg_ssn_sensor, schedules_file, vent_fans_cfis_suppl) 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 @@ -150,11 +155,11 @@ 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) + 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 infil_a_ext = 1.0 @@ -162,22 +167,22 @@ 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) + 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) 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 @@ -187,12 +192,18 @@ phi = 1.0 else phi = q_inf / q_tot end q_fan = q_tot - phi * (q_inf * infil_a_ext) + q_fan = [q_fan, 0].max - return [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 + end + + return q_fan end private def self.set_wind_speed_correction(model, site) @@ -267,20 +278,20 @@ leakage_area.setWindCoefficient(c_w_SG * 0.01) leakage_area.setSpace(space) end end - def self.apply_natural_ventilation_and_whole_house_fan(model, weather, site, vent_fans_whf, open_window_area, nv_clg_ssn_sensor) + 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 # NV Availability Schedule - nv_num_days_per_week = 7 # FUTURE: Expose via HPXML? - nv_avail_sch = create_nv_and_whf_avail_sch(model, Constants.ObjectNameNaturalVentilation, nv_num_days_per_week) + nv_avail_sch = create_nv_and_whf_avail_sch(model, Constants.ObjectNameNaturalVentilation, natvent_days_per_week) nv_avail_sensor = OpenStudio::Model::EnergyManagementSystemSensor.new(model, 'Schedule Value') nv_avail_sensor.setName("#{Constants.ObjectNameNaturalVentilation} avail s") nv_avail_sensor.setKeyName(nv_avail_sch.name.to_s) @@ -329,11 +340,11 @@ # Electric Equipment (for whole house fan electricity consumption) whf_equip_def = OpenStudio::Model::ElectricEquipmentDefinition.new(model) whf_equip_def.setName(Constants.ObjectNameWholeHouseFan) whf_equip = OpenStudio::Model::ElectricEquipment.new(whf_equip_def) whf_equip.setName(Constants.ObjectNameWholeHouseFan) - whf_equip.setSpace(@living_space) + whf_equip.setSpace(@living_space) # no heat gain, so assign the equipment to an arbitrary space 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) @@ -380,20 +391,23 @@ else vent_program.addLine("Set Tnvsp = #{UnitConversions.convert(73.0, 'F', 'C')}") # Assumption when no HVAC system 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)') 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)') 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 - vent_program.addLine(" Set #{nv_flow_actuator.name} = 0") vent_program.addLine(" Set #{whf_flow_actuator.name} = WHF_Flow*Adj") vent_program.addLine(" Set #{liv_to_zone_flow_rate_actuator.name} = WHF_Flow*Adj") unless whf_zone.nil? vent_program.addLine(' Set WHF_W = 0') vent_fans_whf.each do |vent_whf| vent_program.addLine(" Set WHF_W = WHF_W + #{vent_whf.fan_power} * #{whf_avail_sensors[vent_whf.id].name}") @@ -407,19 +421,11 @@ vent_program.addLine(' Set dT = (@Abs Tdiff)') vent_program.addLine(" Set Vwind = #{@vwind_sensor.name}") vent_program.addLine(' Set SGNV = NVArea*Adj*((((Cs*dT)+(Cw*(Vwind^2)))^0.5)/1000)') vent_program.addLine(" Set MaxNV = #{UnitConversions.convert(max_flow_rate, 'cfm', 'm^3/s')}") vent_program.addLine(" Set #{nv_flow_actuator.name} = (@Min SGNV MaxNV)") - vent_program.addLine(" Set #{whf_flow_actuator.name} = 0") - vent_program.addLine(" Set #{liv_to_zone_flow_rate_actuator.name} = 0") unless whf_zone.nil? - vent_program.addLine(" Set #{whf_elec_actuator.name} = 0") vent_program.addLine(' EndIf') - vent_program.addLine('Else') - vent_program.addLine(" Set #{nv_flow_actuator.name} = 0") - vent_program.addLine(" Set #{whf_flow_actuator.name} = 0") - vent_program.addLine(" Set #{liv_to_zone_flow_rate_actuator.name} = 0") unless whf_zone.nil? - vent_program.addLine(" Set #{whf_elec_actuator.name} = 0") vent_program.addLine('EndIf') manager = OpenStudio::Model::EnergyManagementSystemProgramCallingManager.new(model) manager.setName("#{vent_program.name} calling manager") manager.setCallingPoint('BeginZoneTimestepAfterInitHeatBalance') @@ -520,11 +526,11 @@ return if vent_fans_mech.empty? index = 0 vent_fans_mech.each do |vent_mech| - next unless (vent_mech.fan_type == HPXML::MechVentTypeCFIS) + next if vent_mech.fan_type != HPXML::MechVentTypeCFIS 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] @@ -1062,41 +1068,45 @@ duct_program.addLine("Set #{duct_actuators['liv_to_dz_flow_rate'].name} = #{duct_vars['liv_to_dz_flow_rate'].name}") end if @cfis_airloop.values.include? object - # Calculate additional CFIS duct losses during fan-only mode cfis_id = @cfis_airloop.key(object) vent_mech = vent_fans_mech.select { |vfm| vfm.id == cfis_id }[0] - duct_program.addLine("If #{@cfis_f_damper_extra_open_var[cfis_id].name} > 0") - duct_program.addLine(" Set cfis_m3s = (#{@fan_mfr_max_var[object].name} * #{vent_mech.cfis_vent_mode_airflow_fraction} / 1.16097654)") # Density of 1.16097654 was back calculated using E+ results - duct_program.addLine(" Set #{@fan_rtf_var[object].name} = #{@cfis_f_damper_extra_open_var[cfis_id].name}") # Need to use global vars to sync duct_program and infiltration program of different calling points - duct_program.addLine(" Set #{ah_vfr_var.name} = #{@fan_rtf_var[object].name}*cfis_m3s") - duct_program.addLine(" Set rho_in = (@RhoAirFnPbTdbW #{@pbar_sensor.name} #{@tin_sensor.name} #{@win_sensor.name})") - duct_program.addLine(" Set #{ah_mfr_var.name} = #{ah_vfr_var.name} * rho_in") - duct_program.addLine(" Set #{ah_tout_var.name} = #{ra_t_sensor.name}") - duct_program.addLine(" Set #{ah_wout_var.name} = #{ra_w_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}") - duct_program.addLine(" Run #{duct_subroutine.name}") - duct_program.addLine(" Set #{duct_actuators['cfis_supply_sens_lk_to_liv'].name} = #{duct_vars['supply_sens_lk_to_liv'].name}") - duct_program.addLine(" Set #{duct_actuators['cfis_supply_lat_lk_to_liv'].name} = #{duct_vars['supply_lat_lk_to_liv'].name}") - duct_program.addLine(" Set #{duct_actuators['cfis_supply_cond_to_liv'].name} = #{duct_vars['supply_cond_to_liv'].name}") - duct_program.addLine(" Set #{duct_actuators['cfis_return_sens_lk_to_rp'].name} = #{duct_vars['return_sens_lk_to_rp'].name}") - duct_program.addLine(" Set #{duct_actuators['cfis_return_lat_lk_to_rp'].name} = #{duct_vars['return_lat_lk_to_rp'].name}") - duct_program.addLine(" Set #{duct_actuators['cfis_return_cond_to_rp'].name} = #{duct_vars['return_cond_to_rp'].name}") - duct_program.addLine(" Set #{duct_actuators['cfis_return_cond_to_dz'].name} = #{duct_vars['return_cond_to_dz'].name}") - duct_program.addLine(" Set #{duct_actuators['cfis_supply_cond_to_dz'].name} = #{duct_vars['supply_cond_to_dz'].name}") - duct_program.addLine(" Set #{duct_actuators['cfis_supply_sens_lk_to_dz'].name} = #{duct_vars['supply_sens_lk_to_dz'].name}") - duct_program.addLine(" Set #{duct_actuators['cfis_supply_lat_lk_to_dz'].name} = #{duct_vars['supply_lat_lk_to_dz'].name}") - if not duct_actuators['dz_to_liv_flow_rate'].nil? - duct_program.addLine(" Set #{duct_actuators['cfis_dz_to_liv_flow_rate'].name} = #{duct_vars['dz_to_liv_flow_rate'].name}") + + 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") + duct_program.addLine(" Set cfis_m3s = (#{@fan_mfr_max_var[object].name} * #{vent_mech.cfis_vent_mode_airflow_fraction} / 1.16097654)") # Density of 1.16097654 was back calculated using E+ results + duct_program.addLine(" Set #{@fan_rtf_var[object].name} = #{@cfis_f_damper_extra_open_var[cfis_id].name}") # Need to use global vars to sync duct_program and infiltration program of different calling points + duct_program.addLine(" Set #{ah_vfr_var.name} = #{@fan_rtf_var[object].name}*cfis_m3s") + duct_program.addLine(" Set rho_in = (@RhoAirFnPbTdbW #{@pbar_sensor.name} #{@tin_sensor.name} #{@win_sensor.name})") + duct_program.addLine(" Set #{ah_mfr_var.name} = #{ah_vfr_var.name} * rho_in") + duct_program.addLine(" Set #{ah_tout_var.name} = #{ra_t_sensor.name}") + duct_program.addLine(" Set #{ah_wout_var.name} = #{ra_w_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}") + duct_program.addLine(" Run #{duct_subroutine.name}") + duct_program.addLine(" Set #{duct_actuators['cfis_supply_sens_lk_to_liv'].name} = #{duct_vars['supply_sens_lk_to_liv'].name}") + duct_program.addLine(" Set #{duct_actuators['cfis_supply_lat_lk_to_liv'].name} = #{duct_vars['supply_lat_lk_to_liv'].name}") + duct_program.addLine(" Set #{duct_actuators['cfis_supply_cond_to_liv'].name} = #{duct_vars['supply_cond_to_liv'].name}") + duct_program.addLine(" Set #{duct_actuators['cfis_return_sens_lk_to_rp'].name} = #{duct_vars['return_sens_lk_to_rp'].name}") + duct_program.addLine(" Set #{duct_actuators['cfis_return_lat_lk_to_rp'].name} = #{duct_vars['return_lat_lk_to_rp'].name}") + duct_program.addLine(" Set #{duct_actuators['cfis_return_cond_to_rp'].name} = #{duct_vars['return_cond_to_rp'].name}") + duct_program.addLine(" Set #{duct_actuators['cfis_return_cond_to_dz'].name} = #{duct_vars['return_cond_to_dz'].name}") + duct_program.addLine(" Set #{duct_actuators['cfis_supply_cond_to_dz'].name} = #{duct_vars['supply_cond_to_dz'].name}") + duct_program.addLine(" Set #{duct_actuators['cfis_supply_sens_lk_to_dz'].name} = #{duct_vars['supply_sens_lk_to_dz'].name}") + duct_program.addLine(" Set #{duct_actuators['cfis_supply_lat_lk_to_dz'].name} = #{duct_vars['supply_lat_lk_to_dz'].name}") + if not duct_actuators['dz_to_liv_flow_rate'].nil? + duct_program.addLine(" Set #{duct_actuators['cfis_dz_to_liv_flow_rate'].name} = #{duct_vars['dz_to_liv_flow_rate'].name}") + end + if not duct_actuators['liv_to_dz_flow_rate'].nil? + duct_program.addLine(" Set #{duct_actuators['cfis_liv_to_dz_flow_rate'].name} = #{duct_vars['liv_to_dz_flow_rate'].name}") + end + duct_program.addLine('Else') end - if not duct_actuators['liv_to_dz_flow_rate'].nil? - duct_program.addLine(" Set #{duct_actuators['cfis_liv_to_dz_flow_rate'].name} = #{duct_vars['liv_to_dz_flow_rate'].name}") - end - duct_program.addLine('Else') duct_program.addLine(" Set #{duct_actuators['cfis_supply_sens_lk_to_liv'].name} = 0") duct_program.addLine(" Set #{duct_actuators['cfis_supply_lat_lk_to_liv'].name} = 0") duct_program.addLine(" Set #{duct_actuators['cfis_supply_cond_to_liv'].name} = 0") duct_program.addLine(" Set #{duct_actuators['cfis_return_sens_lk_to_rp'].name} = 0") duct_program.addLine(" Set #{duct_actuators['cfis_return_lat_lk_to_rp'].name} = 0") @@ -1109,66 +1119,60 @@ duct_program.addLine(" Set #{duct_actuators['cfis_dz_to_liv_flow_rate'].name} = 0") end if not duct_actuators['liv_to_dz_flow_rate'].nil? duct_program.addLine(" Set #{duct_actuators['cfis_liv_to_dz_flow_rate'].name} = 0") end - duct_program.addLine('EndIf') + if add_cfis_duct_losses + duct_program.addLine('EndIf') + end end manager = OpenStudio::Model::EnergyManagementSystemProgramCallingManager.new(model) manager.setName("#{duct_program.name} calling manager") manager.setCallingPoint('EndOfSystemTimestepAfterHVACReporting') manager.addProgram(duct_program) end end - def self.apply_infiltration_to_garage(model, weather, site, ach50) + def self.apply_infiltration_to_garage(model, site, ach50) return if @spaces[HPXML::LocationGarage].nil? 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) ela = sla * area - ach = get_infiltration_ACH_from_SLA(sla, 8.202, weather) - 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) end - def self.apply_infiltration_to_unconditioned_basement(model, weather) + def self.apply_infiltration_to_unconditioned_basement(model) return if @spaces[HPXML::LocationBasementUnconditioned].nil? space = @spaces[HPXML::LocationBasementUnconditioned] - volume = UnitConversions.convert(space.volume, 'm^3', 'ft^3') 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, 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) + def self.apply_infiltration_to_unvented_crawlspace(model) return if @spaces[HPXML::LocationCrawlspaceUnvented].nil? space = @spaces[HPXML::LocationCrawlspaceUnvented] - volume = UnitConversions.convert(space.volume, 'm^3', 'ft^3') 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) return if @spaces[HPXML::LocationAtticVented].nil? @@ -1186,35 +1190,29 @@ vented_attic_sla = get_infiltration_SLA_from_ACH(vented_attic.vented_attic_ach, 8.202, weather) end end space = @spaces[HPXML::LocationAtticVented] - volume = UnitConversions.convert(space.volume, 'm^3', 'ft^3') if not vented_attic_sla.nil? vented_attic_area = UnitConversions.convert(space.floorArea, 'm^2', 'ft^2') hor_lk_frac = 0.75 neutral_level = 0.5 sla = vented_attic_sla - ach = get_infiltration_ACH_from_SLA(sla, 8.202, weather) ela = sla * vented_attic_area - 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) elsif not vented_attic_const_ach.nil? ach = vented_attic_const_ach - cfm = ach / UnitConversions.convert(1.0, 'hr', 'min') * volume apply_infiltration_to_unconditioned_space(model, space, ach, nil, nil, nil) end end - def self.apply_infiltration_to_unvented_attic(model, weather, site) + def self.apply_infiltration_to_unvented_attic(model) return if @spaces[HPXML::LocationAtticUnvented].nil? space = @spaces[HPXML::LocationAtticUnvented] - volume = UnitConversions.convert(space.volume, 'm^3', 'ft^3') 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_local_ventilation(model, vent_object, obj_type_name, index) daily_sch = [0.0] * 24 @@ -1235,22 +1233,22 @@ 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.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.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, runner, vented_dryer, schedules_file, index) + def self.apply_dryer_exhaust(model, vented_dryer, schedules_file, index) obj_name = "#{Constants.ObjectNameClothesDryerExhaust} #{index}" # Create schedule obj_sch = nil if not schedules_file.nil? @@ -1287,11 +1285,11 @@ if (vent_mech_cfm > 0) # Must assume an operating condition (HVI seems to use CSA 439) t_sup_in = 0.0 w_sup_in = 0.0028 t_exh_in = 22.0 - w_exh_in = 0.0065 + # w_exh_in = 0.0065 cp_a = 1006.0 p_fan = vent_mech.average_unit_fan_power # Watts m_fan = UnitConversions.convert(vent_mech_cfm, 'cfm', 'm^3/s') * 16.02 * Psychrometrics.rhoD_fT_w_P(UnitConversions.convert(t_sup_in, 'C', 'F'), w_sup_in, 14.7) # kg/s @@ -1367,84 +1365,135 @@ hrv_erv_effectiveness_map[vent_mech][:vent_mech_apparent_sens_eff] = vent_mech_apparent_sens_eff end return hrv_erv_effectiveness_map end - def self.apply_cfis(infil_program, vent_mech_fans, cfis_fan_actuator) - infil_program.addLine('Set QWHV_cfis_oa = 0.0') + def self.apply_cfis(infil_program, vent_mech_fans, cfis_fan_actuator, cfis_suppl_fan_actuator) + infil_program.addLine('Set QWHV_cfis_sup = 0.0') # CFIS supply outdoor airflow rate + infil_program.addLine('Set QWHV_cfis_suppl_sup = 0.0') # CFIS supplemental fan supply outdoor airflow rate + infil_program.addLine('Set QWHV_cfis_suppl_exh = 0.0') # CFIS supplemental fan exhaust outdoor airflow rate vent_mech_fans.each do |vent_mech| 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("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') cfis_open_time = [vent_mech.hours_in_operation / 24.0 * 60.0, 59.999].min # Minimum open time in minutes - infil_program.addLine("Set CFIS_t_min_hr_open = #{cfis_open_time}") # minutes per hour the CFIS damper is open - infil_program.addLine("Set CFIS_Q_duct_oa = #{UnitConversions.convert(vent_mech.oa_unit_flow_rate, 'cfm', 'm^3/s')}") + infil_program.addLine("Set cfis_t_min_hr_open = #{cfis_open_time}") # minutes per hour the CFIS damper is open + infil_program.addLine("Set cfis_Q_duct_oa = #{UnitConversions.convert(vent_mech.oa_unit_flow_rate, 'cfm', 'm^3/s')}") infil_program.addLine('Set cfis_f_damper_open = 0') # fraction of the timestep the CFIS damper is open infil_program.addLine("Set #{@cfis_f_damper_extra_open_var[vent_mech.id].name} = 0") # additional runtime fraction to meet min/hr - infil_program.addLine("If #{@cfis_t_sum_open_var[vent_mech.id].name} < CFIS_t_min_hr_open") - infil_program.addLine(" Set CFIS_t_fan_on = 60 - (CFIS_t_min_hr_open - #{@cfis_t_sum_open_var[vent_mech.id].name})") # minute at which the blower needs to turn on to meet the ventilation requirements + infil_program.addLine("If #{@cfis_t_sum_open_var[vent_mech.id].name} < cfis_t_min_hr_open") + infil_program.addLine(" Set cfis_t_fan_on = 60 - (cfis_t_min_hr_open - #{@cfis_t_sum_open_var[vent_mech.id].name})") # minute at which the blower needs to turn on to meet the ventilation requirements # Evaluate condition of whether supply fan has to run to achieve target minutes per hour of operation - infil_program.addLine(' If (Minute+0.00001) >= CFIS_t_fan_on') - # Consider fan rtf read in current calling point (results of previous time step) + CFIS_t_fan_on based on min/hr requirement and previous EMS results. - infil_program.addLine(' Set cfis_fan_runtime = @Max (@ABS(Minute - CFIS_t_fan_on)) (fan_rtf_hvac * ZoneTimeStep * 60)') + infil_program.addLine(' If (Minute+0.00001) >= cfis_t_fan_on') + # Consider fan rtf read in current calling point (results of previous time step) + cfis_t_fan_on based on min/hr requirement and previous EMS results. + infil_program.addLine(' Set cfis_fan_runtime = @Max (@ABS(Minute - cfis_t_fan_on)) (fan_rtf_hvac * ZoneTimeStep * 60)') # If fan_rtf_hvac, make sure it's not exceeding ventilation requirements - infil_program.addLine(" Set cfis_fan_runtime = @Min cfis_fan_runtime (CFIS_t_min_hr_open - #{@cfis_t_sum_open_var[vent_mech.id].name})") + infil_program.addLine(" Set cfis_fan_runtime = @Min cfis_fan_runtime (cfis_t_min_hr_open - #{@cfis_t_sum_open_var[vent_mech.id].name})") infil_program.addLine(' Set cfis_f_damper_open = cfis_fan_runtime/(60.0*ZoneTimeStep)') # calculates the portion of the current timestep the CFIS damper needs to be open infil_program.addLine(" Set #{@cfis_t_sum_open_var[vent_mech.id].name} = #{@cfis_t_sum_open_var[vent_mech.id].name}+cfis_fan_runtime") infil_program.addLine(" Set #{@cfis_f_damper_extra_open_var[vent_mech.id].name} = @Max (cfis_f_damper_open-fan_rtf_hvac) 0.0") - infil_program.addLine(" Set #{cfis_fan_actuator.name} = #{cfis_fan_actuator.name} + CFIS_fan_w*#{@cfis_f_damper_extra_open_var[vent_mech.id].name}") + if vent_mech.cfis_addtl_runtime_operating_mode == HPXML::CFISModeAirHandler + # Air handler meets additional runtime requirement + infil_program.addLine(" Set #{cfis_fan_actuator.name} = #{cfis_fan_actuator.name} + cfis_fan_w*#{@cfis_f_damper_extra_open_var[vent_mech.id].name}") + elsif vent_mech.cfis_addtl_runtime_operating_mode == HPXML::CFISModeSupplementalFan + if vent_mech.cfis_supplemental_fan.oa_unit_flow_rate < vent_mech.average_total_unit_flow_rate + @runner.registerWarning("CFIS supplemental fan '#{vent_mech.cfis_supplemental_fan.id}' is undersized (#{vent_mech.cfis_supplemental_fan.oa_unit_flow_rate} cfm) compared to the target hourly ventilation rate (#{vent_mech.average_total_unit_flow_rate} cfm).") + end + infil_program.addLine(" Set cfis_suppl_Q_oa = #{UnitConversions.convert(vent_mech.cfis_supplemental_fan.oa_unit_flow_rate, 'cfm', 'm^3/s')}") + infil_program.addLine(" Set cfis_suppl_f = #{@cfis_f_damper_extra_open_var[vent_mech.id].name} / (cfis_suppl_Q_oa / cfis_Q_duct_oa)") # Calculate desired runtime for supplemental fan to provide remaining ventilation requirement + infil_program.addLine(' Set cfis_suppl_f = @Min cfis_suppl_f 1.0') # Ensure desired runtime does not exceed 100% (if the supplemental fan is undersized) + infil_program.addLine(" Set cfis_suppl_fan_w = #{vent_mech.cfis_supplemental_fan.unit_fan_power}") # W + infil_program.addLine(" Set #{cfis_suppl_fan_actuator.name} = #{cfis_suppl_fan_actuator.name} + cfis_suppl_fan_w*cfis_suppl_f") + if vent_mech.cfis_supplemental_fan.fan_type == HPXML::MechVentTypeSupply + infil_program.addLine(' Set QWHV_cfis_suppl_sup = QWHV_cfis_suppl_sup + cfis_suppl_f * cfis_suppl_Q_oa') + elsif vent_mech.cfis_supplemental_fan.fan_type == HPXML::MechVentTypeExhaust + infil_program.addLine(' Set QWHV_cfis_suppl_exh = QWHV_cfis_suppl_exh + cfis_suppl_f * cfis_suppl_Q_oa') + end + end infil_program.addLine(' Else') # No need to turn on blower for extra ventilation infil_program.addLine(' Set cfis_fan_runtime = fan_rtf_hvac*ZoneTimeStep*60') - infil_program.addLine(" If (#{@cfis_t_sum_open_var[vent_mech.id].name}+cfis_fan_runtime) > CFIS_t_min_hr_open") + infil_program.addLine(" If (#{@cfis_t_sum_open_var[vent_mech.id].name}+cfis_fan_runtime) > cfis_t_min_hr_open") # Damper is only open for a portion of this time step to achieve target minutes per hour - infil_program.addLine(" Set cfis_fan_runtime = CFIS_t_min_hr_open-#{@cfis_t_sum_open_var[vent_mech.id].name}") + infil_program.addLine(" Set cfis_fan_runtime = cfis_t_min_hr_open-#{@cfis_t_sum_open_var[vent_mech.id].name}") infil_program.addLine(' Set cfis_f_damper_open = cfis_fan_runtime/(ZoneTimeStep*60)') - infil_program.addLine(" Set #{@cfis_t_sum_open_var[vent_mech.id].name} = CFIS_t_min_hr_open") + infil_program.addLine(" Set #{@cfis_t_sum_open_var[vent_mech.id].name} = cfis_t_min_hr_open") infil_program.addLine(' Else') # Damper is open and using call for heat/cool to supply fresh air infil_program.addLine(' Set cfis_fan_runtime = fan_rtf_hvac*ZoneTimeStep*60') infil_program.addLine(' Set cfis_f_damper_open = fan_rtf_hvac') infil_program.addLine(" Set #{@cfis_t_sum_open_var[vent_mech.id].name} = #{@cfis_t_sum_open_var[vent_mech.id].name}+cfis_fan_runtime") infil_program.addLine(' EndIf') - # Fan power is metered under fan cooling and heating meters infil_program.addLine(' EndIf') - infil_program.addLine(' Set QWHV_cfis_oa = QWHV_cfis_oa + cfis_f_damper_open * CFIS_Q_duct_oa') + + if vent_mech.cfis_addtl_runtime_operating_mode == HPXML::CFISModeSupplementalFan + infil_program.addLine(" Set cfis_f_damper_open = @Max (cfis_f_damper_open-#{@cfis_f_damper_extra_open_var[vent_mech.id].name}) 0.0") + else + end + infil_program.addLine(' Set QWHV_cfis_sup = QWHV_cfis_sup + cfis_f_damper_open * cfis_Q_duct_oa') + infil_program.addLine('EndIf') end end - def self.add_ee_for_vent_fan_power(model, obj_name, frac_lost, is_cfis, pow = 0.0) + def self.add_ee_for_vent_fan_power(model, obj_name, sup_fans = [], exh_fans = [], bal_fans = [], erv_hrv_fans = []) + # 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 + else + # Calculate total fan power + if obj_name == Constants.ObjectNameMechanicalVentilationHouseFanCFISSupplFan + sup_fans_w = sup_fans.map { |f| f.unit_fan_power }.sum(0.0) + exh_fans_w = exh_fans.map { |f| f.unit_fan_power }.sum(0.0) + bal_fans_w = (bal_fans + erv_hrv_fans).map { |f| f.unit_fan_power }.sum(0.0) + else + sup_fans_w = sup_fans.map { |f| f.average_unit_fan_power }.sum(0.0) + exh_fans_w = exh_fans.map { |f| f.average_unit_fan_power }.sum(0.0) + bal_fans_w = (bal_fans + erv_hrv_fans).map { |f| f.average_unit_fan_power }.sum(0.0) + end + tot_fans_w = sup_fans_w + exh_fans_w + bal_fans_w + + # Calculate weighted-average value + if tot_fans_w > 0.0 + fan_heat_lost_fraction = (1.0 * exh_fans_w + 0.0 * sup_fans_w + 0.5 * bal_fans_w) / tot_fans_w + else + fan_heat_lost_fraction = 1.0 + end + end + 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.setEndUseSubcategory(Constants.ObjectNameMechanicalVentilation) - equip_def.setFractionLost(frac_lost) - vent_mech_fan_actuator = nil - if is_cfis # actuate its power level in EMS - equip_def.setFractionLost(0.0) # Fan heat does enter space - vent_mech_fan_actuator = OpenStudio::Model::EnergyManagementSystemActuator.new(equip, *EPlus::EMSActuatorElectricEquipmentPower) - vent_mech_fan_actuator.setName("#{equip.name} act") - else - equip_def.setDesignLevel(pow) + 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.setName("#{equip.name} act") end + if not tot_fans_w.nil? + equip_def.setDesignLevel(tot_fans_w) + end - return vent_mech_fan_actuator + return equip_actuator end def self.setup_mech_vent_vars_actuators(model:, program:) # Actuators for mech vent fan sens_name = "#{Constants.ObjectNameMechanicalVentilationHouseFan} sensible load" @@ -1467,12 +1516,18 @@ program.addLine('Set ZoneAirEnth = (@HFnTdbW ZoneTemp ZoneW)') return fan_sens_load_actuator, fan_lat_load_actuator end - def self.apply_infiltration_adjustment_to_conditioned(model, runner, 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, schedules_file) + 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) + # 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) next unless @cooking_range_in_cond_space @@ -1492,20 +1547,19 @@ 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, runner, vented_dryer, schedules_file, index) + obj_sch_sensor, cfm_mult = apply_dryer_exhaust(model, vented_dryer, schedules_file, index) 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, 'cfm', 'm^3/s').round(5)}") - infil_program.addLine("Set QWHV_exh = #{UnitConversions.convert(exh_cfm_tot, 'cfm', 'm^3/s').round(5)}") - infil_program.addLine("Set QWHV_bal_erv_hrv = #{UnitConversions.convert(bal_cfm_tot + erv_hrv_cfm_tot, 'cfm', 'm^3/s').round(5)}") + 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)}") - infil_program.addLine('Set Qexhaust = Qrange + Qbath + Qdryer + QWHV_exh + QWHV_bal_erv_hrv') - infil_program.addLine('Set Qsupply = QWHV_sup + QWHV_bal_erv_hrv + QWHV_cfis_oa') + infil_program.addLine('Set Qexhaust = Qrange + Qbath + Qdryer + QWHV_exh + QWHV_cfis_suppl_exh') + infil_program.addLine('Set Qsupply = QWHV_sup + QWHV_cfis_sup + QWHV_cfis_suppl_sup') infil_program.addLine('Set Qfan = (@Max Qexhaust Qsupply)') if Constants.ERIVersions.index(@eri_version) >= Constants.ERIVersions.index('2019') # Follow ASHRAE 62.2-2016, Normative Appendix C equations for time-varying total airflow infil_program.addLine('If Qfan > 0') # Balanced system if the total supply airflow and total exhaust airflow are within 10% of their average. @@ -1526,12 +1580,12 @@ infil_program.addLine('Set Qinf_adj = Qtot - Qu - Qb') end infil_program.addLine("Set #{infil_flow_actuator.name} = Qinf_adj") end - def self.calculate_fan_loads(model, infil_program, vent_mech_erv_hrv_tot, hrv_erv_effectiveness_map, fan_sens_load_actuator, fan_lat_load_actuator, q_var, preconditioned = false) - # Variables for combined effectivenesses + def self.calculate_fan_loads(infil_program, vent_mech_erv_hrv_tot, hrv_erv_effectiveness_map, fan_sens_load_actuator, fan_lat_load_actuator, q_var, preconditioned = false) + # Variables for combined effectiveness infil_program.addLine('Set Effectiveness_Sens = 0.0') infil_program.addLine('Set Effectiveness_Lat = 0.0') # Calculate mass flow rate based on outdoor air density # Address load with flow-weighted combined effectiveness @@ -1542,12 +1596,15 @@ # 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 infil_program.addLine("If #{q_var} > 0") vent_mech_erv_hrv_tot.each do |vent_fan| - infil_program.addLine(" Set Effectiveness_Sens = Effectiveness_Sens + #{UnitConversions.convert(vent_fan.average_oa_unit_flow_rate, 'cfm', 'm^3/s').round(4)} / #{q_var} * #{hrv_erv_effectiveness_map[vent_fan][:vent_mech_sens_eff]}") - infil_program.addLine(" Set Effectiveness_Lat = Effectiveness_Lat + #{UnitConversions.convert(vent_fan.average_oa_unit_flow_rate, 'cfm', 'm^3/s').round(4)} / #{q_var} * #{hrv_erv_effectiveness_map[vent_fan][:vent_mech_lat_eff]}") + sens_eff = hrv_erv_effectiveness_map[vent_fan][:vent_mech_sens_eff] + lat_eff = hrv_erv_effectiveness_map[vent_fan][:vent_mech_lat_eff] + avg_oa_m3s = UnitConversions.convert(vent_fan.average_oa_unit_flow_rate, 'cfm', 'm^3/s').round(4) + infil_program.addLine(" Set Effectiveness_Sens = Effectiveness_Sens + #{avg_oa_m3s} / #{q_var} * #{sens_eff}") + infil_program.addLine(" Set Effectiveness_Lat = Effectiveness_Lat + #{avg_oa_m3s} / #{q_var} * #{lat_eff}") end infil_program.addLine('EndIf') infil_program.addLine('Set ERVCpMin = (@Min OASupCp ZoneCp)') infil_program.addLine('Set ERVSupOutTemp = OASupInTemp + ERVCpMin/OASupCp * Effectiveness_Sens * (ZoneTemp - OASupInTemp)') infil_program.addLine('Set ERVSupOutW = OASupInW + ERVCpMin/OASupCp * Effectiveness_Lat * (ZoneW - OASupInW)') @@ -1594,11 +1651,11 @@ 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) + calculate_fan_loads(infil_program, vent_mech_erv_hrv_tot, hrv_erv_effectiveness_map, fan_sens_load_actuator, fan_lat_load_actuator, 'Qpreheat', true) 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") @@ -1619,11 +1676,11 @@ 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) + calculate_fan_loads(infil_program, vent_mech_erv_hrv_tot, hrv_erv_effectiveness_map, fan_sens_load_actuator, fan_lat_load_actuator, 'Qprecool', true) 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") @@ -1636,48 +1693,40 @@ 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, runner, 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) + 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) # 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 } vent_mech_sup_tot = vent_fans_mech.select { |vent_mech| vent_mech.fan_type == HPXML::MechVentTypeSupply } vent_mech_exh_tot = vent_fans_mech.select { |vent_mech| vent_mech.fan_type == HPXML::MechVentTypeExhaust } 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 - sup_vent_mech_fan_w = vent_mech_sup_tot.map { |vent_mech| vent_mech.average_unit_fan_power }.sum(0.0) - exh_vent_mech_fan_w = vent_mech_exh_tot.map { |vent_mech| vent_mech.average_unit_fan_power }.sum(0.0) + 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) - # ERV/HRV and balanced system fan power combined altogether - bal_vent_mech_fan_w = (vent_mech_bal_tot + vent_mech_erv_hrv_tot).map { |vent_mech| vent_mech.average_unit_fan_power }.sum(0.0) - total_sup_exh_bal_w = sup_vent_mech_fan_w + exh_vent_mech_fan_w + bal_vent_mech_fan_w - # 1.0: Fan heat does not enter space, 0.0: Fan heat does enter space, 0.5: Supply fan heat enters space - if total_sup_exh_bal_w > 0.0 - fan_heat_lost_fraction = (1.0 * exh_vent_mech_fan_w + 0.0 * sup_vent_mech_fan_w + 0.5 * bal_vent_mech_fan_w) / total_sup_exh_bal_w + # CFIS fan power + cfis_fan_actuator = add_ee_for_vent_fan_power(model, Constants.ObjectNameMechanicalVentilationHouseFanCFIS) # Fan heat enters space + + # CFIS supplemental fan power + if not vent_fans_cfis_suppl.empty? + vent_mech_cfis_suppl_sup_tot = vent_fans_cfis_suppl.select { |vent_mech| vent_mech.fan_type == HPXML::MechVentTypeSupply } + vent_mech_cfis_suppl_exh_tot = vent_fans_cfis_suppl.select { |vent_mech| vent_mech.fan_type == HPXML::MechVentTypeExhaust } + cfis_suppl_fan_actuator = add_ee_for_vent_fan_power(model, Constants.ObjectNameMechanicalVentilationHouseFanCFISSupplFan, + vent_mech_cfis_suppl_sup_tot, vent_mech_cfis_suppl_exh_tot) else - fan_heat_lost_fraction = 1.0 + cfis_suppl_fan_actuator = nil end - add_ee_for_vent_fan_power(model, Constants.ObjectNameMechanicalVentilationHouseFan, fan_heat_lost_fraction, false, total_sup_exh_bal_w) - # CFIS fan power - cfis_fan_actuator = add_ee_for_vent_fan_power(model, Constants.ObjectNameMechanicalVentilationHouseFanCFIS, 0.0, true) - - # 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) - - # Calculate effectivenesses for all ERV/HRV and store results in a hash + # Calculate effectiveness for all ERV/HRV and store results in a hash hrv_erv_effectiveness_map = calc_hrv_erv_effectiveness(vent_mech_erv_hrv_tot) infil_flow = OpenStudio::Model::SpaceInfiltrationDesignFlowRate.new(model) infil_flow.setName(Constants.ObjectNameInfiltration + ' flow') infil_flow.setSchedule(model.alwaysOnDiscreteSchedule) @@ -1695,37 +1744,42 @@ # 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 infil_program.addLine("Set #{cfis_fan_actuator.name} = 0.0") - apply_cfis(infil_program, vent_mech_cfis_tot, cfis_fan_actuator) + infil_program.addLine("Set #{cfis_suppl_fan_actuator.name} = 0.0") unless cfis_suppl_fan_actuator.nil? + 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, runner, 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, schedules_file) + 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) # 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| + recirc_flow_rate = f.average_total_unit_flow_rate - f.average_oa_unit_flow_rate + next unless recirc_flow_rate > 0 + # Subtract recirculation air flow rate from Qfan, only come from supply side as exhaust is not allowed to have recirculation - 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)}") + infil_program.addLine("Set Qload = Qload - #{UnitConversions.convert(recirc_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') + calculate_fan_loads(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, clg_ssn_sensor) program_calling_manager = OpenStudio::Model::EnergyManagementSystemProgramCallingManager.new(model) program_calling_manager.setName("#{infil_program.name} calling manager") program_calling_manager.setCallingPoint('BeginZoneTimestepAfterInitHeatBalance') program_calling_manager.addProgram(infil_program) end - def self.apply_infiltration_and_ventilation_fans(model, runner, 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) + 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? @@ -1743,20 +1797,20 @@ end end end # Infiltration for unconditioned spaces - apply_infiltration_to_garage(model, weather, site, living_ach50) - apply_infiltration_to_unconditioned_basement(model, weather) + 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, weather) + apply_infiltration_to_unvented_crawlspace(model) apply_infiltration_to_vented_attic(model, weather, site, vented_attic) - apply_infiltration_to_unvented_attic(model, weather, site) + apply_infiltration_to_unvented_attic(model) # Infiltration/ventilation for conditioned space - apply_infiltration_ventilation_to_conditioned(model, runner, 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) + 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) end def self.apply_infiltration_to_conditioned(site, living_ach50, living_const_ach, infil_program, weather, has_flue_chimney) site_ap = site.additional_properties @@ -1785,16 +1839,14 @@ # Leakage distributions per Iain Walker (LBL) recommendations if not @spaces[HPXML::LocationCrawlspaceVented].nil? # 15% ceiling, 35% walls, 50% floor leakage distribution for vented crawl leakage_ceiling = 0.15 - leakage_walls = 0.35 leakage_floor = 0.50 else # 25% ceiling, 50% walls, 25% floor leakage distribution for slab/basement/unvented crawl leakage_ceiling = 0.25 - leakage_walls = 0.50 leakage_floor = 0.25 end r_i = (leakage_ceiling + leakage_floor) x_i = (leakage_ceiling - leakage_floor) @@ -1812,14 +1864,13 @@ 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 - x_c = r_i + (2.0 * (1.0 - r_i - y_i)) / (n_i + 1.0) - 2.0 * y_i * (z_f - 1.0)**n_i # Eq. 13 + x_c = r_i + (2.0 * (1.0 - r_i - y_i)) / (n_i + 1.0) - 2.0 * y_i * (z_f - 1.0)**n_i # Critical value of ceiling-floor leakage difference where the neutral level is located at the ceiling (eq. 13) 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 - x_c = r_i + (2.0 * (1.0 - r_i - y_i)) / (n_i + 1.0) # Critical value of ceiling-floor leakage difference where the neutral level is located at the ceiling (eq. 13) 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 @@ -1838,13 +1889,10 @@ j_i = (x_i + r_i + 2.0 * y_i) / 2.0 f_w = 0.19 * (2.0 - n_i) * (1.0 - ((x_i + r_i) / 2.0)**(1.5 - y_i)) - y_i / 4.0 * (j_i - 2.0 * y_i * j_i**4.0) end wind_coef = f_w * UnitConversions.convert(outside_air_density / 2.0, 'lbm/ft^3', 'inH2O/mph^2')**n_i # inH2O^n/mph^2n - living_ach = get_infiltration_ACH_from_SLA(living_sla, @infil_height, weather) - living_cfm = living_ach / UnitConversions.convert(1.0, 'hr', 'min') * @infil_volume - 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')}") @@ -1860,13 +1908,10 @@ 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 - living_ach = living_const_ach - living_cfm = living_ach / UnitConversions.convert(1.0, 'hr', 'min') * @infil_volume - 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