Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Non-transient dwelling unit energy recovery ventilation #1165

Merged
merged 15 commits into from
Sep 14, 2021
Merged
Show file tree
Hide file tree
Changes from 12 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion data/standards/manage_OpenStudio_Standards.rb
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,8 @@ def unique_properties(sheet_name)
['template', 'fluid_type', 'fuel_type', 'condensing', 'condensing_control', 'minimum_capacity', 'maximum_capacity', 'start_date', 'end_date']
when 'chillers'
['template', 'cooling_type', 'condenser_type', 'compressor_type', 'absorption_type', 'variable_speed_drive', 'minimum_capacity', 'maximum_capacity', 'start_date', 'end_date']
when 'furnaces'
['template', 'minimum_capacity', 'maximum_capacity', 'start_date', 'end_date']
Copy link
Collaborator

@mdahlhausen mdahlhausen Sep 13, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

merge in the other commit for furnaces to this pull request

when 'heat_rejection'
['template', 'equipment_type', 'fan_type', 'start_date', 'end_date']
when 'water_source_heat_pumps'
Expand Down Expand Up @@ -143,7 +145,7 @@ def unique_properties(sheet_name)
when 'climate_zones'
['name', 'standard']
when 'energy_recovery'
['template', 'climate_zone', 'under_8000_hours']
['template', 'climate_zone', 'under_8000_hours', 'nontransient_dwelling', 'err_basis']
when 'space_types_lighting_control'
['template', 'building_type', 'space_type']
else
Expand Down Expand Up @@ -345,6 +347,7 @@ def export_spreadsheet_to_json(spreadsheet_titles, dataset_type: 'os_stds')
bool_cols << 'hx'
bool_cols << 'data_center'
bool_cols << 'under_8000_hours'
bool_cols << 'nontransient_dwelling'
bool_cols << 'u_value_includes_interior_film_coefficient'
bool_cols << 'u_value_includes_exterior_film_coefficient'

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -278,7 +278,7 @@ def model_add_lights_shutoff(model)
business_sensor.setName('Business_Sensor')
business_sensor_name = business_sensor.name.to_s

zones.each do |zone|
zones.sort.each do |zone|
spaces = zone.spaces
if spaces.length != 1
puts 'warning, there are more than one spaces in the zone, need to confirm the implementation'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -278,7 +278,7 @@ def model_add_lights_shutoff(model)
business_sensor.setName('Business_Sensor')
business_sensor_name = business_sensor.name.to_s

zones.each do |zone|
zones.sort.each do |zone|
spaces = zone.spaces
if spaces.length != 1
puts 'warning, there are more than one spaces in the zone, need to confirm the implementation'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,8 @@ def add_door_infiltration(climate_zone, model)

def model_update_fan_efficiency(model)
model.getFanOnOffs.sort.each do |fan_onoff|
next if fan_onoff.name.get.to_s.include?('ERV')

fan_onoff.setFanEfficiency(0.53625)
fan_onoff.setMotorEfficiency(0.825)
end
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -86,4 +86,41 @@ def heat_exchanger_air_to_air_sensible_and_latent_apply_prototype_efficiency(hea

return true
end

# Set sensible and latent effectiveness at 100 and 75 heating and cooling airflow;
# The values are calculated by using ERR, which is introduced in 90.1-2016 Addendum CE
#
# This function is only used for nontransient dwelling units (Mid-rise and High-rise Apartment)
# @param [OpenStudio::Model::HeatExchangerAirToAirSensibleAndLatent] heat exchanger air to air sensible and latent
# @param [String] err
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

parameters not fully explained / specified

# @param [String] err basis (Cooling/Heating)
# @param [String] climate zone
def heat_exchanger_air_to_air_sensible_and_latent_apply_prototype_efficiency_err(heat_exchanger_air_to_air_sensible_and_latent, err, basis, climate_zone)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

even though the method name is long, avoid acronyms ('err') in method names

# Assumed to be sensible and latent at all flow
if err.nil?
full_htg_sens_eff = 0.0
full_htg_lat_eff = 0.0
part_htg_sens_eff = 0.0
part_htg_lat_eff = 0.0
full_cool_sens_eff = 0.0
full_cool_lat_eff = 0.0
part_cool_sens_eff = 0.0
part_cool_lat_eff = 0.0
else
err = enthalpy_recovery_ratio_design_to_typical_adjustment(err, climate_zone)
full_htg_sens_eff, full_htg_lat_eff, part_htg_sens_eff, part_htg_lat_eff, full_cool_sens_eff, full_cool_lat_eff, part_cool_sens_eff, part_cool_lat_eff = heat_exchanger_air_to_air_sensible_and_latent_enthalpy_recovery_ratio_to_effectiveness(err, basis)
end

heat_exchanger_air_to_air_sensible_and_latent.setSensibleEffectivenessat100HeatingAirFlow(full_htg_sens_eff)
heat_exchanger_air_to_air_sensible_and_latent.setLatentEffectivenessat100HeatingAirFlow(full_htg_lat_eff)
heat_exchanger_air_to_air_sensible_and_latent.setSensibleEffectivenessat75HeatingAirFlow(part_htg_sens_eff)
heat_exchanger_air_to_air_sensible_and_latent.setLatentEffectivenessat75HeatingAirFlow(part_htg_lat_eff)
heat_exchanger_air_to_air_sensible_and_latent.setSensibleEffectivenessat100CoolingAirFlow(full_cool_sens_eff)
heat_exchanger_air_to_air_sensible_and_latent.setLatentEffectivenessat100CoolingAirFlow(full_cool_lat_eff)
heat_exchanger_air_to_air_sensible_and_latent.setSensibleEffectivenessat75CoolingAirFlow(part_cool_sens_eff)
heat_exchanger_air_to_air_sensible_and_latent.setLatentEffectivenessat75CoolingAirFlow(part_cool_lat_eff)

OpenStudio.logFree(OpenStudio::Info, 'openstudio.standards.HeatExchangerSensLat', "For #{heat_exchanger_air_to_air_sensible_and_latent.name}: Set sensible and latent effectiveness calculated by using ERR.")
return true
end
end
Original file line number Diff line number Diff line change
Expand Up @@ -1583,10 +1583,61 @@ def model_apply_prototype_hvac_assumptions(model, building_type, climate_zone)
#
# @param model [OpenStudio::Model::Model] the model
def model_apply_prototype_hvac_efficiency_adjustments(model)
building_data = model_get_building_climate_zone_and_building_type(model)
building_type = building_data['building_type']
climate_zone = building_data['climate_zone']

# ERVs
# Applies the DOE Prototype Building assumption that ERVs use
# enthalpy wheels and therefore exceed the minimum effectiveness specified by 90.1
model.getHeatExchangerAirToAirSensibleAndLatents.each { |obj| heat_exchanger_air_to_air_sensible_and_latent_apply_prototype_efficiency(obj) }
if building_type == 'MidriseApartment' || building_type == 'HighriseApartment'
# Use standalone ERV in dwelling units to provide OA
# Loads are met by mechanical cooling and the heating system with a cycling fan
model.getAirLoopHVACs.each do |air_loop_hvac|
# Find out if air loop has an ERV (i.e. if heat recovery is required)
has_erv = false
has_erv = true if air_loop_hvac_energy_recovery?(air_loop_hvac)

serves_res_spc = false

air_loop_hvac.thermalZones.each do |zone|
next unless thermal_zone_residential?(zone)

model_add_residential_erv(model, zone, climate_zone, has_erv)

# Shut-off air loop level OA intake
oa_controller = air_loop_hvac.airLoopHVACOutdoorAirSystem.get.getControllerOutdoorAir
oa_controller.setMinimumOutdoorAirSchedule(model.alwaysOffDiscreteSchedule)

serves_res_spc = true
end

if has_erv & serves_res_spc
# Remove air loop ERV
air_loop_hvac_remove_erv(air_loop_hvac)
elsif has_erv
# Apply regular adjustment if the ERV doesn't serve a residential space
oa_sys = nil
if air_loop_hvac.airLoopHVACOutdoorAirSystem.is_initialized
oa_sys = air_loop_hvac.airLoopHVACOutdoorAirSystem.get
else
OpenStudio.logFree(OpenStudio::Info, 'openstudio.standards.AirLoopHVAC', "For #{air_loop_hvac.name}, ERV cannot be removed because the system has no OA intake.")
return false
end

# Get the existing ERV or create an ERV and add it to the OA system
oa_sys.oaComponents.each do |oa_comp|
if oa_comp.to_HeatExchangerAirToAirSensibleAndLatent.is_initialized
erv = oa_comp.to_HeatExchangerAirToAirSensibleAndLatent.get
heat_exchanger_air_to_air_sensible_and_latent_apply_prototype_efficiency(erv)
end
end
end
end
else
# Applies the DOE Prototype Building assumption that ERVs use
# enthalpy wheels and therefore exceed the minimum effectiveness specified by 90.1
model.getHeatExchangerAirToAirSensibleAndLatents.each { |obj| heat_exchanger_air_to_air_sensible_and_latent_apply_prototype_efficiency(obj) }
end

return true
end

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6435,4 +6435,181 @@ def model_add_swh_end_uses_by_spaceonly(model, space, swh_loop)
swh_loop.addDemandBranchForComponent(swh_connection)
return water_fixture
end

# Add a residential ERV: standalone ERV that operates to provide OA,
# use in conjuction witha system that having mechanical cooling and
# a heating coil
#
# @param model [OpenStudio::Model::Model] OpenStudio Model object
# @param thermal_zone [OpenStudio::Model::ThermalZone] OpenStudio ThermalZone object
# @param climate_zone [String] Climate zone
# @param energy_recovery [Boolean] Indicates if the ERV is to recover energy, if false, only provides OA
# @return [OpenStudio::Model::ZoneHVACEnergyRecoveryVentilator] Standalone ERV
def model_add_residential_erv(model,
thermal_zone,
climate_zone,
energy_recovery,
min_oa_flow_m3_per_s_per_m2: nil)

OpenStudio.logFree(OpenStudio::Info, 'openstudio.Model.Model', "Adding standalone ERV for #{thermal_zone.name}.")

# Exception 3 to 6.5.6.1.1
case template
when '90.1-2019'
case climate_zone
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-3B',
'ASHRAE 169-2006-3C',
'ASHRAE 169-2006-4A',
'ASHRAE 169-2006-4B',
'ASHRAE 169-2006-4C',
'ASHRAE 169-2006-5A',
'ASHRAE 169-2006-5B',
'ASHRAE 169-2006-5C',
'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',
'ASHRAE 169-2013-3B',
'ASHRAE 169-2013-3C',
'ASHRAE 169-2013-4A',
'ASHRAE 169-2013-4B',
'ASHRAE 169-2013-4C',
'ASHRAE 169-2013-5A',
'ASHRAE 169-2013-5B',
'ASHRAE 169-2013-5C'
if thermal_zone_floor_area(thermal_zone) <= OpenStudio.convert(500, 'ft^2', 'm^2').get
energy_recovery = false
OpenStudio.logFree(OpenStudio::Info, 'openstudio.Model.Model', "Energy recovery will not be modeled for the ERV serving #{thermal_zone.name}.")
end
end
end

# Determine ERR and design basis when energy recovery is required
#
# err = nil will trigger an ERV with no effectiveness that only provides OA
err = nil
if energy_recovery
case template
when '90.1-2019'
search_criteria = {
'template' => template,
'climate_zone' => climate_zone,
'under_8000_hours' => false,
'nontransient_dwelling' => true
}
else
search_criteria = {
'template' => template,
'climate_zone' => climate_zone,
'under_8000_hours' => false
}
end

erv_err = model_find_object(standards_data['energy_recovery'], search_criteria)

# Extract ERR from data lookup
if !erv_err.nil?
if erv_err['err'].nil? & erv_err['err_basis'].nil?
# If not included in the data, an enthalpy
# recovery ratio (ERR) of 50% is used
err = 0.5
case climate_zone
when 'ASHRAE 169-2006-6B',
'ASHRAE 169-2013-6B',
'ASHRAE 169-2006-7A',
'ASHRAE 169-2013-7A',
'ASHRAE 169-2006-7B',
'ASHRAE 169-2013-7B',
'ASHRAE 169-2006-8A',
'ASHRAE 169-2013-8A',
'ASHRAE 169-2006-8B',
'ASHRAE 169-2013-8B'
err_basis = 'heating'
else
err_basis = 'cooling'
end
else
err_basis = erv_err['err_basis'].downcase
err = erv_err['err']
end
end
end

# Create fans
#
# Fan power:
# No energy recovery = 0.806 W/cfm
# Energy recovery = 0.934 W/cfm
supply_fan = create_fan_by_name(model,
'ERV_Supply_Fan',
fan_name: "#{thermal_zone.name} ERV Supply Fan")
exhaust_fan = create_fan_by_name(model,
'ERV_Supply_Fan',
fan_name: "#{thermal_zone.name} ERV Exhaust Fan")
supply_fan.setMotorEfficiency(0.48)
exhaust_fan.setMotorEfficiency(0.48)
supply_fan.setFanTotalEfficiency(0.303158)
exhaust_fan.setFanTotalEfficiency(0.303158)
if energy_recovery
supply_fan.setPressureRise(270.64755)
exhaust_fan.setPressureRise(270.64755)
else
supply_fan.setPressureRise(233.6875)
exhaust_fan.setPressureRise(233.6875)
end

# Create ERV Controller
erv_controller = OpenStudio::Model::ZoneHVACEnergyRecoveryVentilatorController.new(model)
erv_controller.setName("#{thermal_zone.name} ERV Controller")
erv_controller.setControlHighIndoorHumidityBasedonOutdoorHumidityRatio(false)

# Create heat exchanger
heat_exchanger = OpenStudio::Model::HeatExchangerAirToAirSensibleAndLatent.new(model)
heat_exchanger.setName("#{thermal_zone.name} ERV HX")
heat_exchanger.setSupplyAirOutletTemperatureControl(false)
heat_exchanger.setHeatExchangerType('Rotary')
heat_exchanger.setEconomizerLockout(false)
heat_exchanger.setFrostControlType('ExhaustOnly')
heat_exchanger.setThresholdTemperature(-23.3)
heat_exchanger.setInitialDefrostTimeFraction(0.167)
heat_exchanger.setRateofDefrostTimeFractionIncrease(1.44)
heat_exchanger.setAvailabilitySchedule(model_add_schedule(model, 'Always On - No DD'))
heat_exchanger_air_to_air_sensible_and_latent_apply_prototype_efficiency_err(heat_exchanger, err, err_basis, climate_zone)

erv = OpenStudio::Model::ZoneHVACEnergyRecoveryVentilator.new(model, heat_exchanger, supply_fan, exhaust_fan)
erv.setName("#{thermal_zone.name} ERV")

erv.setController(erv_controller)
erv.addToThermalZone(thermal_zone)

# Set OA requirements; Assumes a default of 55 cfm
if min_oa_flow_m3_per_s_per_m2.nil?
erv.setSupplyAirFlowRate(OpenStudio.convert(55.0, 'cfm', 'm^3/s').get)
erv.setExhaustAirFlowRate(OpenStudio.convert(55.0, 'cfm', 'm^3/s').get)
else
erv.setVentilationRateperUnitFloorArea(min_oa_flow_m3_per_s_per_m2)
end

# Ensure the ERV takes priority, so ventilation load is included when treated by other zonal systems
# From EnergyPlus I/O reference:
# "For situations where one or more equipment types has limited capacity or limited control capability, order the
# sequence so that the most controllable piece of equipment runs last. For example, with a dedicated outdoor air
# system (DOAS), the air terminal for the DOAS should be assigned Heating Sequence = 1 and Cooling Sequence = 1.
# Any other equipment should be assigned sequence 2 or higher so that it will see the net load after the DOAS air
# is added to the zone."
thermal_zone.setCoolingPriority(erv.to_ModelObject.get, 1)
thermal_zone.setHeatingPriority(erv.to_ModelObject.get, 1)

return erv
end
end
21 changes: 21 additions & 0 deletions lib/openstudio-standards/standards/Standards.AirLoopHVAC.rb
Original file line number Diff line number Diff line change
Expand Up @@ -1567,6 +1567,27 @@ def air_loop_hvac_energy_recovery_ventilator_heat_exchanger_type(air_loop_hvac)
return heat_exchanger_type
end

def air_loop_hvac_remove_erv(air_loop_hvac)
# Get the OA system
oa_sys = nil
if air_loop_hvac.airLoopHVACOutdoorAirSystem.is_initialized
oa_sys = air_loop_hvac.airLoopHVACOutdoorAirSystem.get
else
OpenStudio.logFree(OpenStudio::Info, 'openstudio.standards.AirLoopHVAC', "For #{air_loop_hvac.name}, ERV cannot be removed because the system has no OA intake.")
return false
end

# Get the existing ERV or create an ERV and add it to the OA system
oa_sys.oaComponents.each do |oa_comp|
if oa_comp.to_HeatExchangerAirToAirSensibleAndLatent.is_initialized
erv = oa_comp.to_HeatExchangerAirToAirSensibleAndLatent.get
erv.remove
end
end

return true
end

# Add an ERV to this airloop
#
# @param (see #economizer_required?)
Expand Down
Loading