lib/openstudio-standards/standards/Standards.AirLoopHVAC.rb in openstudio-standards-0.2.10 vs lib/openstudio-standards/standards/Standards.AirLoopHVAC.rb in openstudio-standards-0.2.11.rc1

- old
+ new

@@ -1,6 +1,5 @@ - class Standard # @!group AirLoopHVAC # Apply multizone vav outdoor air method and # adjust multizone VAV damper positions @@ -32,11 +31,11 @@ # @todo nightcycle control # @todo night fan shutoff def air_loop_hvac_apply_standard_controls(air_loop_hvac, climate_zone) # Energy Recovery Ventilation if air_loop_hvac_energy_recovery_ventilator_required?(air_loop_hvac, climate_zone) - air_loop_hvac_apply_energy_recovery_ventilator(air_loop_hvac) + air_loop_hvac_apply_energy_recovery_ventilator(air_loop_hvac, climate_zone) end # Economizers air_loop_hvac_apply_economizer_limits(air_loop_hvac, climate_zone) air_loop_hvac_apply_economizer_integration(air_loop_hvac, climate_zone) @@ -98,20 +97,10 @@ ## OpenStudio.logFree(OpenStudio::Error, 'openstudio.standards.AirLoopHVAC', "For #{name}: there is a constant volume fan on a multizone vav system. Cannot apply static pressure reset controls.") ## end ## end end - # Single zone systems - if air_loop_hvac.thermalZones.size == 1 - air_loop_hvac_supply_return_exhaust_relief_fans(air_loop_hvac).each do |fan| - if fan.to_FanVariableVolume.is_initialized - fan_variable_volume_set_control_type(fan, 'Single Zone VAV Fan') - end - end - air_loop_hvac_apply_single_zone_controls(air_loop_hvac, climate_zone) - end - # DCV if air_loop_hvac_demand_control_ventilation_required?(air_loop_hvac, climate_zone) air_loop_hvac_enable_demand_control_ventilation(air_loop_hvac, climate_zone) # For systems that require DCV, # all individual zones that require DCV preserve @@ -157,23 +146,29 @@ air_loop_hvac_add_motorized_oa_damper(air_loop_hvac, 0.15, air_loop_hvac.availabilitySchedule) else air_loop_hvac_remove_motorized_oa_damper(air_loop_hvac) end - # Zones that require DCV preserve - # both per-area and per-person OA reqs. - # Other zones have OA reqs converted - # to per-area values only so that DCV + # Zones that require DCV preserve both per-area and per-person OA reqs. + # Other zones have OA reqs converted to per-area values only so that DCV air_loop_hvac.thermalZones.sort.each do |zone| if thermal_zone_demand_control_ventilation_required?(zone, climate_zone) thermal_zone_convert_oa_req_to_per_area(zone) end end # Optimum Start - if air_loop_hvac_optimum_start_required?(air_loop_hvac) - air_loop_hvac_enable_optimum_start(air_loop_hvac) + air_loop_hvac_enable_optimum_start(air_loop_hvac) if air_loop_hvac_optimum_start_required?(air_loop_hvac) + + # Single zone systems + if air_loop_hvac.thermalZones.size == 1 + air_loop_hvac_supply_return_exhaust_relief_fans(air_loop_hvac).each do |fan| + if fan.to_FanVariableVolume.is_initialized + fan_variable_volume_set_control_type(fan, 'Single Zone VAV Fan') + end + end + air_loop_hvac_apply_single_zone_controls(air_loop_hvac, climate_zone) end end # Apply all PRM baseline required controls to the airloop. # Only applies those controls that differ from the normal @@ -828,11 +823,17 @@ # 'ASHRAE 169-2013-7B', 'ASHRAE 169-2013-8A', 'ASHRAE 169-2013-8B' # @return [Bool] returns true if an economizer is required, false if not def air_loop_hvac_economizer_required?(air_loop_hvac, climate_zone) economizer_required = false - return economizer_required if air_loop_hvac.name.to_s.include? 'Outpatient F1' + # skip systems without outdoor air + return economizer_required unless air_loop_hvac.airLoopHVACOutdoorAirSystem.is_initialized + # Determine if the system serves residential spaces + is_res = false + if air_loop_hvac_residential_area_served(air_loop_hvac) > 0 + is_res = true + end # Determine if the airloop serves any computer rooms # / data centers, which changes the economizer. is_dc = false if air_loop_hvac_data_center_area_served(air_loop_hvac) > 0 @@ -856,25 +857,34 @@ # A big number of btu per hr as the minimum requirement if nil in spreadsheet infinity_btu_per_hr = 999_999_999_999 minimum_capacity_btu_per_hr = infinity_btu_per_hr if minimum_capacity_btu_per_hr.nil? + # Exception valid for 90.1-2004 (6.5.1.(e)) through 90.1-2013 (6.5.1.5) + if is_res + minimum_capacity_btu_per_hr *= 5 + end + # Check whether the system requires an economizer by comparing # the system capacity to the minimum capacity. total_cooling_capacity_w = air_loop_hvac_total_cooling_capacity(air_loop_hvac) total_cooling_capacity_btu_per_hr = OpenStudio.convert(total_cooling_capacity_w, 'W', 'Btu/hr').get if total_cooling_capacity_btu_per_hr >= minimum_capacity_btu_per_hr if is_dc OpenStudio.logFree(OpenStudio::Info, 'openstudio.standards.AirLoopHVAC', "#{air_loop_hvac.name} requires an economizer because the total cooling capacity of #{total_cooling_capacity_btu_per_hr.round} Btu/hr exceeds the minimum capacity of #{minimum_capacity_btu_per_hr.round} Btu/hr for data centers.") + elsif is_res + OpenStudio.logFree(OpenStudio::Info, 'openstudio.standards.AirLoopHVAC', "#{air_loop_hvac.name} requires an economizer because the total cooling capacity of #{total_cooling_capacity_btu_per_hr.round} Btu/hr exceeds the minimum capacity of #{minimum_capacity_btu_per_hr.round} Btu/hr for residential spaces.") else OpenStudio.logFree(OpenStudio::Info, 'openstudio.standards.AirLoopHVAC', "#{air_loop_hvac.name} requires an economizer because the total cooling capacity of #{total_cooling_capacity_btu_per_hr.round} Btu/hr exceeds the minimum capacity of #{minimum_capacity_btu_per_hr.round} Btu/hr.") end economizer_required = true else if is_dc OpenStudio.logFree(OpenStudio::Info, 'openstudio.standards.AirLoopHVAC', "#{air_loop_hvac.name} does not require an economizer because the total cooling capacity of #{total_cooling_capacity_btu_per_hr.round} Btu/hr is less than the minimum capacity of #{minimum_capacity_btu_per_hr.round} Btu/hr for data centers.") + elsif is_res + OpenStudio.logFree(OpenStudio::Info, 'openstudio.standards.AirLoopHVAC', "#{air_loop_hvac.name} requires an economizer because the total cooling capacity of #{total_cooling_capacity_btu_per_hr.round} Btu/hr exceeds the minimum capacity of #{minimum_capacity_btu_per_hr.round} Btu/hr for residential spaces.") else OpenStudio.logFree(OpenStudio::Info, 'openstudio.standards.AirLoopHVAC', "#{air_loop_hvac.name} does not require an economizer because the total cooling capacity of #{total_cooling_capacity_btu_per_hr.round} Btu/hr is less than the minimum capacity of #{minimum_capacity_btu_per_hr.round} Btu/hr.") end end @@ -974,11 +984,12 @@ case economizer_type when 'NoEconomizer' return [nil, nil, nil] when 'FixedDryBulb' case climate_zone - when 'ASHRAE 169-2006-1B', + when 'ASHRAE 169-2006-0B', + 'ASHRAE 169-2006-1B', 'ASHRAE 169-2006-2B', 'ASHRAE 169-2006-3B', 'ASHRAE 169-2006-3C', 'ASHRAE 169-2006-4B', 'ASHRAE 169-2006-4C', @@ -986,10 +997,11 @@ 'ASHRAE 169-2006-5C', 'ASHRAE 169-2006-6B', 'ASHRAE 169-2006-7B', 'ASHRAE 169-2006-8A', 'ASHRAE 169-2006-8B', + 'ASHRAE 169-2013-0B', 'ASHRAE 169-2013-1B', 'ASHRAE 169-2013-2B', 'ASHRAE 169-2013-3B', 'ASHRAE 169-2013-3C', 'ASHRAE 169-2013-4B', @@ -1006,14 +1018,16 @@ 'ASHRAE 169-2006-7A', 'ASHRAE 169-2013-5A', 'ASHRAE 169-2013-6A', 'ASHRAE 169-2013-7A' drybulb_limit_f = 70 - when 'ASHRAE 169-2006-1A', + when 'ASHRAE 169-2006-0A', + 'ASHRAE 169-2006-1A', 'ASHRAE 169-2006-2A', 'ASHRAE 169-2006-3A', 'ASHRAE 169-2006-4A', + 'ASHRAE 169-2013-0A', 'ASHRAE 169-2013-1A', 'ASHRAE 169-2013-2A', 'ASHRAE 169-2013-3A', 'ASHRAE 169-2013-4A' drybulb_limit_f = 65 @@ -1039,27 +1053,61 @@ # Determine if an integrated economizer is required integrated_economizer_required = air_loop_hvac_integrated_economizer_required?(air_loop_hvac, climate_zone) # Get the OA system and OA controller oa_sys = air_loop_hvac.airLoopHVACOutdoorAirSystem - if oa_sys.is_initialized - oa_sys = oa_sys.get - else - return false # No OA system - end - oa_control = oa_sys.getControllerOutdoorAir + return false unless oa_sys.is_initialized + + oa_sys = oa_sys.get + oa_control = oa_sys.getControllerOutdoorAir # Apply integrated or non-integrated economizer if integrated_economizer_required - oa_control.setLockoutType('NoLockout') + oa_control.setLockoutType('LockoutWithHeating') else - oa_control.setLockoutType('LockoutWithCompressor') + # If the airloop include hyrdronic cooling coils, + # prevent economizer from operating at and above SAT, + # similar to a non-integrated economizer. This is done + # because LockoutWithCompressor doesn't work with hydronic + # coils + if air_loop_hvac_include_hydronic_cooling_coil?(air_loop_hvac) + oa_control.setLockoutType('LockoutWithHeating') + oa_control.setEconomizerMaximumLimitDryBulbTemperature(standard_design_sizing_temperatures['clg_dsgn_sup_air_temp_c']) + else + oa_control.setLockoutType('LockoutWithCompressor') + end end return true end + # Determine if the airloop includes hydronic cooling coils + # + # @return [Bool] returns true if hydronic coolings coils are included on the airloop + def air_loop_hvac_include_hydronic_cooling_coil?(air_loop_hvac) + air_loop_hvac.supplyComponents.each do |comp| + return true if comp.to_CoilCoolingWater.is_initialized + end + return false + end + + # Determine if the airloop includes WSHP cooling coils + # + # @return [Bool] returns true if WSHP cooling coils are included on the airloop + def air_loop_hvac_include_wshp?(air_loop_hvac) + air_loop_hvac.supplyComponents.each do |comp| + return true if comp.to_CoilCoolingWaterToAirHeatPumpEquationFit.is_initialized + + if comp.to_AirLoopHVACUnitarySystem.is_initialized + clg_coil = comp.to_AirLoopHVACUnitarySystem.get.coolingCoil.get + return true if clg_coil.to_CoilCoolingWaterToAirHeatPumpEquationFit.is_initialized + + end + end + return false + end + # Determine if the system economizer must be integrated or not. # Default logic is from 90.1-2004. def air_loop_hvac_integrated_economizer_required?(air_loop_hvac, climate_zone) # Determine if it is a VAV system is_vav = air_loop_hvac_vav_system?(air_loop_hvac) @@ -1079,11 +1127,13 @@ integrated_economizer_required = false OpenStudio.logFree(OpenStudio::Info, 'openstudio.standards.AirLoopHVAC', "For #{air_loop_hvac.name}: non-integrated economizer per 6.5.1.3 exception b, DX system less than #{minimum_capacity_btu_per_hr}Btu/hr.") else # Exception c, Systems in climate zones 1,2,3a,4a,5a,5b,6,7,8 case climate_zone - when 'ASHRAE 169-2006-1A', + when 'ASHRAE 169-2006-0A', + 'ASHRAE 169-2006-0B', + 'ASHRAE 169-2006-1A', 'ASHRAE 169-2006-1B', 'ASHRAE 169-2006-2A', 'ASHRAE 169-2006-2B', 'ASHRAE 169-2006-3A', 'ASHRAE 169-2006-4A', @@ -1093,10 +1143,12 @@ 'ASHRAE 169-2006-6B', 'ASHRAE 169-2006-7A', 'ASHRAE 169-2006-7B', 'ASHRAE 169-2006-8A', 'ASHRAE 169-2006-8B', + 'ASHRAE 169-2013-0A', + 'ASHRAE 169-2013-0B', 'ASHRAE 169-2013-1A', 'ASHRAE 169-2013-1B', 'ASHRAE 169-2013-2A', 'ASHRAE 169-2013-2B', 'ASHRAE 169-2013-3A', @@ -1141,15 +1193,19 @@ min_int_area_served_ft2 = infinity_ft2 min_ext_area_served_ft2 = infinity_ft2 # Determine the minimum capacity that requires an economizer case climate_zone - when 'ASHRAE 169-2006-1A', + when 'ASHRAE 169-2006-0A', + 'ASHRAE 169-2006-0B', + 'ASHRAE 169-2006-1A', 'ASHRAE 169-2006-1B', 'ASHRAE 169-2006-2A', 'ASHRAE 169-2006-3A', 'ASHRAE 169-2006-4A', + 'ASHRAE 169-2013-0A', + 'ASHRAE 169-2013-0B', 'ASHRAE 169-2013-1A', 'ASHRAE 169-2013-1B', 'ASHRAE 169-2013-2A', 'ASHRAE 169-2013-3A', 'ASHRAE 169-2013-4A' @@ -1172,11 +1228,11 @@ # Check the floor area exception if int_area_served_m2 < min_int_area_served_m2 && ext_area_served_m2 < min_ext_area_served_m2 if min_int_area_served_ft2 == infinity_ft2 && min_ext_area_served_ft2 == infinity_ft2 OpenStudio.logFree(OpenStudio::Info, 'openstudio.standards.AirLoopHVAC', "For #{air_loop_hvac.name}: Economizer not required for climate zone #{climate_zone}.") else - OpenStudio.logFree(OpenStudio::Info, 'openstudio.standards.AirLoopHVAC', "For #{air_loop_hvac.name}: Economizer not required for because the interior area served of #{int_area_served_m2} ft2 < minimum of #{min_int_area_served_m2} and the perimeter area served of #{ext_area_served_m2} ft2 < minimum of #{min_ext_area_served_m2} for climate zone #{climate_zone}.") + OpenStudio.logFree(OpenStudio::Info, 'openstudio.standards.AirLoopHVAC', "For #{air_loop_hvac.name}: Economizer not required for because the interior area served of #{int_area_served_m2} ft2 is less than the minimum of #{min_int_area_served_m2} and the perimeter area served of #{ext_area_served_m2} ft2 is less than the minimum of #{min_ext_area_served_m2} for climate zone #{climate_zone}.") end return economizer_required end # If here, economizer required @@ -1257,11 +1313,12 @@ drybulb_limit_f = nil enthalpy_limit_btu_per_lb = nil dewpoint_limit_f = nil case climate_zone - when 'ASHRAE 169-2006-1B', + when 'ASHRAE 169-2006-0B', + 'ASHRAE 169-2006-1B', 'ASHRAE 169-2006-2B', 'ASHRAE 169-2006-3B', 'ASHRAE 169-2006-3C', 'ASHRAE 169-2006-4B', 'ASHRAE 169-2006-4C', @@ -1269,10 +1326,11 @@ 'ASHRAE 169-2006-5C', 'ASHRAE 169-2006-6B', 'ASHRAE 169-2006-7B', 'ASHRAE 169-2006-8A', 'ASHRAE 169-2006-8B', + 'ASHRAE 169-2013-0B', 'ASHRAE 169-2013-1B', 'ASHRAE 169-2013-2B', 'ASHRAE 169-2013-3B', 'ASHRAE 169-2013-3C', 'ASHRAE 169-2013-4B', @@ -1334,11 +1392,12 @@ end # Determine the prohibited types prohibited_types = [] case climate_zone - when 'ASHRAE 169-2006-1B', + when 'ASHRAE 169-2006-0B', + 'ASHRAE 169-2006-1B', 'ASHRAE 169-2006-2B', 'ASHRAE 169-2006-3B', 'ASHRAE 169-2006-3C', 'ASHRAE 169-2006-4B', 'ASHRAE 169-2006-4C', @@ -1346,10 +1405,11 @@ 'ASHRAE 169-2006-6B', 'ASHRAE 169-2006-7A', 'ASHRAE 169-2006-7B', 'ASHRAE 169-2006-8A', 'ASHRAE 169-2006-8B', + 'ASHRAE 169-2013-0B', 'ASHRAE 169-2013-1B', 'ASHRAE 169-2013-2B', 'ASHRAE 169-2013-3B', 'ASHRAE 169-2013-3C', 'ASHRAE 169-2013-4B', @@ -1359,24 +1419,26 @@ 'ASHRAE 169-2013-7A', 'ASHRAE 169-2013-7B', 'ASHRAE 169-2013-8A', 'ASHRAE 169-2013-8B' prohibited_types = ['FixedEnthalpy'] - when 'ASHRAE 169-2006-1A', + when 'ASHRAE 169-2006-0A', + 'ASHRAE 169-2006-1A', 'ASHRAE 169-2006-2A', 'ASHRAE 169-2006-3A', 'ASHRAE 169-2006-4A', + 'ASHRAE 169-2013-0A', 'ASHRAE 169-2013-1A', 'ASHRAE 169-2013-2A', 'ASHRAE 169-2013-3A', 'ASHRAE 169-2013-4A' prohibited_types = ['DifferentialDryBulb'] when 'ASHRAE 169-2006-5A', 'ASHRAE 169-2006-6A', 'ASHRAE 169-2013-5A', 'ASHRAE 169-2013-6A' - prohibited_types = [] + prohibited_types = [] end # Check if the specified type is allowed economizer_type_allowed = true if prohibited_types.include?(economizer_type) @@ -1427,15 +1489,11 @@ erv_required = false return erv_required end end - # ERV Not Applicable for AHUs that have DCV - # or that have no OA intake. - controller_oa = nil - controller_mv = nil - oa_system = nil + # ERV Not Applicable for AHUs that have DCV or that have no OA intake. if air_loop_hvac.airLoopHVACOutdoorAirSystem.is_initialized oa_system = air_loop_hvac.airLoopHVACOutdoorAirSystem.get controller_oa = oa_system.getControllerOutdoorAir controller_mv = controller_oa.controllerMechanicalVentilation if controller_mv.demandControlledVentilation == true @@ -1500,77 +1558,87 @@ def air_loop_hvac_energy_recovery_ventilator_flow_limit(air_loop_hvac, climate_zone, pct_oa) erv_cfm = nil # Not required return erv_cfm end - # Add an ERV to this airloop. - # Will be a rotary-type HX + # Determine whether to apply an Energy Recovery Ventilator 'ERV' or a Heat Recovery Ventilator 'HRV' depending on the climate zone + # Defaults to ERV. + # @return [String] the ERV type + def air_loop_hvac_energy_recovery_ventilator_type(air_loop_hvac, climate_zone) + erv_type = 'ERV' + return erv_type + end + + # Determine whether to use a Plate-Frame or Rotary Wheel style ERV depending on air loop outdoor air flow rate + # Defaults to Rotary. + # @return [String] the ERV type + def air_loop_hvac_energy_recovery_ventilator_heat_exchanger_type(air_loop_hvac) + heat_exchanger_type = 'Rotary' + return heat_exchanger_type + end + + # Add an ERV to this airloop # # @param (see #economizer_required?) # @return [Bool] Returns true if required, false if not. # @todo Add exception logic for systems serving parking garage, warehouse, or multifamily - def air_loop_hvac_apply_energy_recovery_ventilator(air_loop_hvac) - # Get the oa system + def air_loop_hvac_apply_energy_recovery_ventilator(air_loop_hvac, climate_zone) + # Get the OA system oa_system = nil if air_loop_hvac.airLoopHVACOutdoorAirSystem.is_initialized oa_system = air_loop_hvac.airLoopHVACOutdoorAirSystem.get else OpenStudio.logFree(OpenStudio::Info, 'openstudio.standards.AirLoopHVAC', "For #{air_loop_hvac.name}, ERV cannot be added because the system has no OA intake.") return false end - # Create an ERV + # Create an ERV and add it to the OA system erv = OpenStudio::Model::HeatExchangerAirToAirSensibleAndLatent.new(air_loop_hvac.model) - erv.setName("#{air_loop_hvac.name} ERV") - erv.setSensibleEffectivenessat100HeatingAirFlow(0.7) - erv.setLatentEffectivenessat100HeatingAirFlow(0.6) - erv.setSensibleEffectivenessat75HeatingAirFlow(0.7) - erv.setLatentEffectivenessat75HeatingAirFlow(0.6) - erv.setSensibleEffectivenessat100CoolingAirFlow(0.75) - erv.setLatentEffectivenessat100CoolingAirFlow(0.6) - erv.setSensibleEffectivenessat75CoolingAirFlow(0.75) - erv.setLatentEffectivenessat75CoolingAirFlow(0.6) + erv.addToNode(oa_system.outboardOANode.get) + + # Determine whether to use an ERV and HRV and heat exchanger style + erv_type = air_loop_hvac_energy_recovery_ventilator_type(air_loop_hvac, climate_zone) + heat_exchanger_type = air_loop_hvac_energy_recovery_ventilator_heat_exchanger_type(air_loop_hvac) + erv.setName("#{air_loop_hvac.name} #{erv_type}") + erv.setHeatExchangerType(heat_exchanger_type) + + # apply heat exchanger efficiencies + air_loop_hvac_apply_energy_recovery_ventilator_efficiency(erv, erv_type: erv_type, heat_exchanger_type: heat_exchanger_type) + + # Apply the prototype heat exchanger power assumptions for rotary style heat exchangers + heat_exchanger_air_to_air_sensible_and_latent_apply_prototype_nominal_electric_power(erv) + + # add economizer lockout erv.setSupplyAirOutletTemperatureControl(false) - erv.setHeatExchangerType('Rotary') - erv.setFrostControlType('ExhaustOnly') erv.setEconomizerLockout(true) + + # add defrost + erv.setFrostControlType('ExhaustOnly') erv.setThresholdTemperature(-23.3) # -10F erv.setInitialDefrostTimeFraction(0.167) erv.setRateofDefrostTimeFractionIncrease(1.44) - # Add the ERV to the OA system - erv.addToNode(oa_system.outboardOANode.get) - - # Add a setpoint manager OA pretreat - # to control the ERV + # Add a setpoint manager OA pretreat to control the ERV spm_oa_pretreat = OpenStudio::Model::SetpointManagerOutdoorAirPretreat.new(air_loop_hvac.model) spm_oa_pretreat.setMinimumSetpointTemperature(-99.0) spm_oa_pretreat.setMaximumSetpointTemperature(99.0) spm_oa_pretreat.setMinimumSetpointHumidityRatio(0.00001) spm_oa_pretreat.setMaximumSetpointHumidityRatio(1.0) - # Reference setpoint node and - # Mixed air stream node are outlet - # node of the OA system + # Reference setpoint node and mixed air stream node are outlet node of the OA system mixed_air_node = oa_system.mixedAirModelObject.get.to_Node.get spm_oa_pretreat.setReferenceSetpointNode(mixed_air_node) spm_oa_pretreat.setMixedAirStreamNode(mixed_air_node) - # Outdoor air node is - # the outboard OA node of teh OA system + # Outdoor air node is the outboard OA node of the OA system spm_oa_pretreat.setOutdoorAirStreamNode(oa_system.outboardOANode.get) - # Return air node is the inlet - # node of the OA system + # Return air node is the inlet node of the OA system return_air_node = oa_system.returnAirModelObject.get.to_Node.get spm_oa_pretreat.setReturnAirStreamNode(return_air_node) # Attach to the outlet of the ERV erv_outlet = erv.primaryAirOutletModelObject.get.to_Node.get spm_oa_pretreat.addToNode(erv_outlet) - # Apply the prototype Heat Exchanger power assumptions. - heat_exchanger_air_to_air_sensible_and_latent_apply_prototype_nominal_electric_power(erv) - - # Determine if the system is a DOAS based on - # whether there is 100% OA in heating and cooling sizing. + # Determine if the system is a DOAS based on whether there is 100% OA in heating and cooling sizing. is_doas = false sizing_system = air_loop_hvac.sizingSystem if sizing_system.allOutdoorAirinCooling && sizing_system.allOutdoorAirinHeating is_doas = true end @@ -1589,10 +1657,28 @@ oa_system.getControllerOutdoorAir.setHeatRecoveryBypassControlType(bypass_ctrl_type) return true end + # Apply efficiency values to the erv + # + # @param erv [OpenStudio::Model::HeatExchangerAirToAirSensibleAndLatent] erv to apply efficiency values + # @param erv_type [String] erv type ERV or HRV + # @param heat_exchanger_type [String] heat exchanger type Rotary or Plate + # @return erv [OpenStudio::Model::HeatExchangerAirToAirSensibleAndLatent] erv to apply efficiency values + def air_loop_hvac_apply_energy_recovery_ventilator_efficiency(erv, erv_type: 'ERV', heat_exchanger_type: 'Rotary') + erv.setSensibleEffectivenessat100HeatingAirFlow(0.7) + erv.setLatentEffectivenessat100HeatingAirFlow(0.6) + erv.setSensibleEffectivenessat75HeatingAirFlow(0.7) + erv.setLatentEffectivenessat75HeatingAirFlow(0.6) + erv.setSensibleEffectivenessat100CoolingAirFlow(0.75) + erv.setLatentEffectivenessat100CoolingAirFlow(0.6) + erv.setSensibleEffectivenessat75CoolingAirFlow(0.75) + erv.setLatentEffectivenessat75CoolingAirFlow(0.6) + return erv + end + # Determine if multizone vav optimization is required. # Defaults to 90.1-2007 logic, where it is not required. # # @param (see #economizer_required?) # @return [Bool] Returns true if required, false if not. @@ -1666,14 +1752,28 @@ # # @param (see #economizer_required?) # @return [Bool] Returns true if required, false if not. # @todo Add exception logic for systems serving parking garage, warehouse, or multifamily def air_loop_hvac_adjust_minimum_vav_damper_positions(air_loop_hvac) + # Do not apply the adjustment to some of the system in + # the hospital and outpatient which have their minimum + # damper position determined based on AIA 2001 ventilation + # requirements + if (@instvarbuilding_type == 'Hospital' && (air_loop_hvac.name.to_s.include?('VAV_ER') || air_loop_hvac.name.to_s.include?('VAV_ICU') || + air_loop_hvac.name.to_s.include?('VAV_OR') || air_loop_hvac.name.to_s.include?('VAV_LABS') || + air_loop_hvac.name.to_s.include?('VAV_PATRMS'))) || + (@instvarbuilding_type == 'Outpatient' && air_loop_hvac.name.to_s.include?('Outpatient F1')) + + return true + end + # Total uncorrected outdoor airflow rate v_ou = 0.0 air_loop_hvac.thermalZones.each do |zone| - v_ou += thermal_zone_outdoor_airflow_rate(zone) + # Vou is the system uncorrected outdoor airflow: + # Zone airflow is multiplied by the zone multiplier + v_ou += thermal_zone_outdoor_airflow_rate(zone) * zone.multiplier.to_f end v_ou_cfm = OpenStudio.convert(v_ou, 'm^3/s', 'cfm').get # System primary airflow rate (whether autosized or hard-sized) @@ -1696,10 +1796,14 @@ # When ventilation effectiveness is too low, # increase the minimum damper position. e_vzs = [] e_vzs_adj = [] num_zones_adj = 0 + + # Retrieve the sum of the zone minimum primary airflow + vpz_min_sum = air_loop_hvac.autosizeSumMinimumHeatingAirFlowRates + air_loop_hvac.thermalZones.sort.each do |zone| # Breathing zone airflow rate v_bz = thermal_zone_outdoor_airflow_rate(zone) # Zone air distribution, assumed 1 per PNNL @@ -1755,26 +1859,30 @@ min_zn_flow = term.fixedMinimumAirFlowRate.get end end end + # Zone ventilation efficiency calculation is computed + # on a per zone basis, the zone primary airflow is + # adjusted to removed the zone multiplier + v_pz /= zone.multiplier.to_f + # For VAV Reheat terminals, min flow is greater of mdp # and min flow rate / design flow rate. mdp = mdp_term - mdp_oa = min_zn_flow / v_ps + mdp_oa = min_zn_flow / v_pz if min_zn_flow > 0.0 mdp = [mdp_term, mdp_oa].max.round(2) end - # OpenStudio::logFree(OpenStudio::Info, 'openstudio.standards.AirLoopHVAC', "For #{self.name}: Zone #{zone.name} mdp_term = #{mdp_term.round(2)}, mdp_oa = #{mdp_oa.round(2)}; mdp_final = #{mdp}") # Zone minimum discharge airflow rate v_dz = v_pz * mdp # Zone discharge air fraction z_d = v_oz / v_dz - # Zone ventilation effectiveness !!! + # Zone ventilation effectiveness e_vz = 1.0 + x_s - z_d # Store the ventilation effectiveness e_vzs << e_vz @@ -1802,31 +1910,16 @@ # Zone ventilation effectiveness e_vz_adj = 1.0 + x_s - z_d_adj # Store the ventilation effectiveness e_vzs_adj << e_vz_adj - # Round the minimum damper position to avoid nondeterministic results # at the ~13th decimal place, which can cause regression errors mdp_adj = mdp_adj.round(11) # Set the adjusted minimum damper position - zone.equipment.each do |equip| - if equip.to_AirTerminalSingleDuctVAVHeatAndCoolNoReheat.is_initialized - term = equip.to_AirTerminalSingleDuctVAVHeatAndCoolNoReheat.get - term.setZoneMinimumAirFlowFraction(mdp_adj) - elsif equip.to_AirTerminalSingleDuctVAVHeatAndCoolReheat.is_initialized - term = equip.to_AirTerminalSingleDuctVAVHeatAndCoolReheat.get - term.setZoneMinimumAirFlowFraction(mdp_adj) - elsif equip.to_AirTerminalSingleDuctVAVNoReheat.is_initialized - term = equip.to_AirTerminalSingleDuctVAVNoReheat.get - term.setConstantMinimumAirFlowFraction(mdp_adj) - elsif equip.to_AirTerminalSingleDuctVAVReheat.is_initialized - term = equip.to_AirTerminalSingleDuctVAVReheat.get - term.setConstantMinimumAirFlowFraction(mdp_adj) - end - end + air_loop_hvac_set_minimum_damper_position(zone, mdp_adj) num_zones_adj += 1 OpenStudio.logFree(OpenStudio::Info, 'openstudio.standards.AirLoopHVAC', "For #{air_loop_hvac.name}: Zone #{zone.name} has a ventilation effectiveness of #{e_vz.round(2)}. Increasing to #{e_vz_adj.round(2)} by increasing minimum damper position from #{mdp.round(2)} to #{mdp_adj.round(2)}.") @@ -1848,10 +1941,20 @@ # Total system outdoor intake flow rate v_ot_adj = v_ou / e_v_adj v_ot_adj_cfm = OpenStudio.convert(v_ot_adj, 'm^3/s', 'cfm').get + # Adjust minimum damper position if the sum of maximum + # zone airflow are lower than the calculated system + # outdoor air intake + if v_ot_adj > vpz_min_sum && v_ot_adj > 0 + mdp_adj = [v_ot_adj / air_loop_hvac.autosizeSumAirTerminalMaxAirFlowRate, 1].min + air_loop_hvac.thermalZones.sort.each do |zone| + air_loop_hvac_set_minimum_damper_position(zone, mdp_adj) + end + end + # Report out the results of the multizone calculations if num_zones_adj > 0 OpenStudio.logFree(OpenStudio::Info, 'openstudio.standards.AirLoopHVAC', "For #{air_loop_hvac.name}: the multizone outdoor air calculation method was applied. A simple summation of the zone outdoor air requirements gives a value of #{v_ou_cfm.round} cfm. Applying the multizone method gives a value of #{v_ot_cfm.round} cfm, with an original system ventilation effectiveness of #{e_v.round(2)}. After increasing the minimum damper position in #{num_zones_adj} critical zones, the resulting requirement is #{v_ot_adj_cfm.round} cfm with a system ventilation effectiveness of #{e_v_adj.round(2)}.") else OpenStudio.logFree(OpenStudio::Info, 'openstudio.standards.AirLoopHVAC', "For #{air_loop_hvac.name}: the multizone outdoor air calculation method was applied. A simple summation of the zone requirements gives a value of #{v_ou_cfm.round} cfm. However, applying the multizone method requires #{v_ot_adj_cfm.round} cfm based on the ventilation effectiveness of the system.") @@ -1863,10 +1966,31 @@ sizing_system.setDesignOutdoorAirFlowRate(v_ot_adj) return true end + # Set an air terminal's minimum damper position + def air_loop_hvac_set_minimum_damper_position(zone, mdp) + zone.equipment.each do |equip| + if equip.to_AirTerminalSingleDuctVAVHeatAndCoolNoReheat.is_initialized + term = equip.to_AirTerminalSingleDuctVAVHeatAndCoolNoReheat.get + term.setZoneMinimumAirFlowFraction(mdp) + elsif equip.to_AirTerminalSingleDuctVAVHeatAndCoolReheat.is_initialized + term = equip.to_AirTerminalSingleDuctVAVHeatAndCoolReheat.get + term.setZoneMinimumAirFlowFraction(mdp) + elsif equip.to_AirTerminalSingleDuctVAVNoReheat.is_initialized + term = equip.to_AirTerminalSingleDuctVAVNoReheat.get + term.setConstantMinimumAirFlowFraction(mdp) + elsif equip.to_AirTerminalSingleDuctVAVReheat.is_initialized + term = equip.to_AirTerminalSingleDuctVAVReheat.get + term.setConstantMinimumAirFlowFraction(mdp) + end + end + + return true + end + # For critical zones of Outpatient, if the minimum airflow rate required by the accreditation standard (AIA 2001) is significantly # less than the autosized peak design airflow in any of the three climate zones (Houston, Baltimore and Burlington), the minimum # airflow fraction of the terminal units is reduced to the value: "required minimum airflow rate / autosized peak design flow" # Reference: <Achieving the 30% Goal: Energy and Cost Savings Analysis of ASHRAE Standard 90.1-2010> Page109-111 # For implementation purpose, since it is time-consuming to perform autosizing in three climate zones, just use @@ -1886,10 +2010,14 @@ if equip.to_AirTerminalSingleDuctVAVReheat.is_initialized vav_terminal = equip.to_AirTerminalSingleDuctVAVReheat.get rated_maximum_flow_rate = vav_terminal.autosizedMaximumAirFlowRate.get # compare the VAV autosized maximum airflow with the minimum airflow rate required by the accreditation standard ratio = minimum_airflow_per_zone / rated_maximum_flow_rate + + # round to avoid results variances in sizing runs + ratio = ratio.round(11) + if ratio >= 0.95 vav_terminal.setConstantMinimumAirFlowFraction(1) elsif ratio < 0.95 vav_terminal.setConstantMinimumAirFlowFraction(ratio) end @@ -2128,11 +2256,11 @@ # Attach the setpoint manager to the # supply outlet node of the system. sat_oa_reset.addToNode(supplyOutletNode) - OpenStudio.logFree(OpenStudio::Info, 'openstudio.standards.AirLoopHVAC', "For #{air_loop_hvac.name}: Supply air temperature reset was enabled. When OAT > #{hi_oat_f.round}F, SAT is #{sat_at_hi_oat_f.round}F. When OAT < #{lo_oat_f.round}F, SAT is #{sat_at_lo_oat_f.round}F. It varies linearly in between these points.") + OpenStudio.logFree(OpenStudio::Info, 'openstudio.standards.AirLoopHVAC', "For #{air_loop_hvac.name}: Supply air temperature reset was enabled. When OAT is greater than #{hi_oat_f.round}F, SAT is #{sat_at_hi_oat_f.round}F. When OAT is less than #{lo_oat_f.round}F, SAT is #{sat_at_lo_oat_f.round}F. It varies linearly in between these points.") return true end # Determine if the system has an economizer @@ -2246,10 +2374,25 @@ end return has_erv end + # Determine if the air loop is a unitary system + # + # @return [Bool] Returns true if a unitary system is present, false if not. + def air_loop_hvac_unitary_system?(air_loop_hvac) + is_unitary_system = false + air_loop_hvac.supplyComponents.each do |component| + obj_type = component.iddObjectType.valueName.to_s + case obj_type + when 'OS_AirLoopHVAC_UnitarySystem', 'OS_AirLoopHVAC_UnitaryHeatPump_AirToAir', 'OS_AirLoopHVAC_UnitaryHeatPump_AirToAir_MultiSpeed', 'OS_AirLoopHVAC_UnitaryHeatCool_VAVChangeoverBypass' + is_unitary_system = true + end + end + return is_unitary_system + end + # Set the VAV damper control to single maximum or dual maximum control depending on the standard. # # @return [Bool] Returns true if successful, false if not # @todo see if this impacts the sizing run. def air_loop_hvac_apply_vav_damper_action(air_loop_hvac) @@ -2368,11 +2511,11 @@ OpenStudio.logFree(OpenStudio::Info, 'openstudio.standards.AirLoopHVAC', "For #{air_loop_hvac.name}: Motorized OA damper not required because the system OA intake of #{oa_flow_cfm.round} cfm is less than the minimum threshold of #{minimum_oa_flow_cfm} cfm.") return motorized_oa_damper_required end # If here, motorized damper is required - OpenStudio.logFree(OpenStudio::Info, 'openstudio.standards.AirLoopHVAC', "For #{air_loop_hvac.name}: Motorized OA damper is required because the building has #{num_stories} stories, >= the minimum of #{maximum_stories} stories for climate zone #{climate_zone}, and the system OA intake of #{oa_flow_cfm.round} cfm is >= the minimum threshold of #{minimum_oa_flow_cfm} cfm. ") + OpenStudio.logFree(OpenStudio::Info, 'openstudio.standards.AirLoopHVAC', "For #{air_loop_hvac.name}: Motorized OA damper is required because the building has #{num_stories} stories which is greater than or equal to the minimum of #{maximum_stories} stories for climate zone #{climate_zone}, and the system OA intake of #{oa_flow_cfm.round} cfm is greater than or equal to the minimum threshold of #{minimum_oa_flow_cfm} cfm. ") motorized_oa_damper_required = true return motorized_oa_damper_required end @@ -2991,16 +3134,42 @@ if air_loop_hvac.designSupplyAirFlowRate.is_initialized design_supply_air_flow_rate = air_loop_hvac.designSupplyAirFlowRate.get elsif air_loop_hvac.autosizedDesignSupplyAirFlowRate.is_initialized design_supply_air_flow_rate = air_loop_hvac.autosizedDesignSupplyAirFlowRate.get else - OpenStudio.logFree(OpenStudio::Warn, 'openstudio.standards.AirLoopHVAC', "For #{air_loop_hvac.name} design sypply air flow rate is not available.") + OpenStudio.logFree(OpenStudio::Warn, 'openstudio.standards.AirLoopHVAC', "For #{air_loop_hvac.name} design supply air flow rate is not available.") end return design_supply_air_flow_rate end + # Determine how much residential area the airloop serves + # + # @returns [Double] res_area m^2 + def air_loop_hvac_residential_area_served(air_loop_hvac) + res_area = 0.0 + + air_loop_hvac.thermalZones.each do |zone| + zone.spaces.each do |space| + # Skip spaces with no space type + next if space.spaceType.empty? + + space_type = space.spaceType.get + + # Skip spaces with no standards space type + next if space_type.standardsSpaceType.empty? + + standards_space_type = space_type.standardsSpaceType.get + if standards_space_type.downcase.include?('apartment') || standards_space_type.downcase.include?('guestroom') || standards_space_type.downcase.include?('patroom') + res_area += space.floorArea + end + end + end + + return res_area + end + # Determine how much data center # area the airloop serves. # # @return [Double] the area of data center is served, # in m^2. @@ -3013,12 +3182,16 @@ air_loop_hvac.thermalZones.each do |zone| zone.spaces.each do |space| # Skip spaces with no space type next if space.spaceType.empty? + space_type = space.spaceType.get + + # Skip spaces with no standards space type next if space_type.standardsSpaceType.empty? + standards_space_type = space_type.standardsSpaceType.get # Counts as a data center if the name includes 'data' if standards_space_type.downcase.include?('data center') || standards_space_type.downcase.include?('datacenter') dc_area_m2 += space.floorArea end @@ -3028,9 +3201,22 @@ end end end return dc_area_m2 + end + + # Determine how many humidifies are on the airloop + # + # @return [Integer] the number of humidifiers + def air_loop_hvac_humidifier_count(air_loop_hvac) + humidifiers = 0 + air_loop_hvac.supplyComponents.each do |cmp| + if cmp.to_HumidifierSteamElectric.is_initialized + humidifiers += 1 + end + end + return humidifiers end # Sets the maximum reheat temperature to the specified # value for all reheat terminals (of any type) on the loop. #