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.
#