Skip to content

Commit

Permalink
Merge pull request #1680 from NREL/data_update_0763744
Browse files Browse the repository at this point in the history
Building Energy Standards Water Heater Data Update
  • Loading branch information
mdahlhausen authored Apr 3, 2024
2 parents 3b75d73 + 485fb80 commit 67f99df
Show file tree
Hide file tree
Showing 83 changed files with 435,709 additions and 430,044 deletions.
2 changes: 1 addition & 1 deletion data/standards/manage_OpenStudio_Standards.rb
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,7 @@ def unique_properties(sheet_name)
when 'unitary_acs'
['template', 'cooling_type', 'heating_type', 'subcategory', 'minimum_capacity', 'maximum_capacity', 'start_date', 'end_date']
when 'water_heaters'
['template', 'fuel_type', 'minimum_capacity', 'maximum_capacity', 'start_date', 'end_date']
['template', 'equipment_type', 'fuel_type', 'minimum_capacity', 'maximum_capacity', 'minimum_storage', 'maximum_storage', 'minimum_capacity_per_storage', 'maximum_capacity_per_storage', 'draw_profile', 'start_date', 'end_date']
when 'elevators'
['template', 'building_type']
when 'refrigeration_system_lineup', 'refrigeration_system'
Expand Down
1,346 changes: 673 additions & 673 deletions data/standards/test_performance_expected_dd_results.csv

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions lib/openstudio-standards.rb
Original file line number Diff line number Diff line change
Expand Up @@ -347,6 +347,7 @@ module OpenstudioStandards
require_relative "#{stds}/ashrae_90_1/ashrae_90_1_2019/ashrae_90_1_2019.FanVariableVolume"
require_relative "#{stds}/ashrae_90_1/ashrae_90_1_2019/ashrae_90_1_2019.Space"
require_relative "#{stds}/ashrae_90_1/ashrae_90_1_2019/ashrae_90_1_2019.ThermalZone"
require_relative "#{stds}/ashrae_90_1/ashrae_90_1_2019/ashrae_90_1_2019.WaterHeaterMixed"
# 90.1-PRM Common
require_relative "#{stds}/ashrae_90_1_prm/ashrae_90_1_prm"
require_relative "#{stds}/ashrae_90_1_prm/ashrae_90_1_prm.Model"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -612,7 +612,6 @@ def model_add_cw_loop(model,
next unless dd.dayType == 'SummerDesignDay'
next unless dd.name.get.to_s.include?('WB=>MDB')


if condenser_water_loop.model.version < OpenStudio::VersionString.new('3.3.0')
if dd.humidityIndicatingType == 'Wetbulb'
summer_oat_wb_c = dd.humidityIndicatingConditionsAtMaximumDryBulb
Expand Down
51 changes: 48 additions & 3 deletions lib/openstudio-standards/standards/Standards.Model.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2362,14 +2362,17 @@ def model_apply_infiltration_standard(model)
# the objects will only be returned if the specified area is between the minimum_area and maximum_area values.
# @param num_floors [Double] capacity of the object in question. If num_floors is supplied,
# the objects will only be returned if the specified num_floors is between the minimum_floors and maximum_floors values.
# @param fan_motor_hp [Double] fan motor brake horsepower.
# @param volume [Double] Equipment storage capacity in gallons.
# @param capacity_per_volume [Double] Equipment capacity per storage capacity in Btu/h/gal.
# @return [Array] returns an array of hashes, one hash per object. Array is empty if no results.
# @example Find all the schedule rules that match the name
# rules = model_find_objects(standards_data['schedules'], 'name' => schedule_name)
# if rules.size.zero?
# OpenStudio.logFree(OpenStudio::Error, 'openstudio.standards.Model', "Cannot find data for schedule: #{schedule_name}, will not be created.")
# return false
# end
def model_find_objects(hash_of_objects, search_criteria, capacity = nil, date = nil, area = nil, num_floors = nil, fan_motor_bhp = nil)
def model_find_objects(hash_of_objects, search_criteria, capacity = nil, date = nil, area = nil, num_floors = nil, fan_motor_bhp = nil, volume = nil, capacity_per_volume = nil)
matching_objects = []
if hash_of_objects.is_a?(Hash) && hash_of_objects.key?('table')
hash_of_objects = hash_of_objects['table']
Expand Down Expand Up @@ -2423,6 +2426,48 @@ def model_find_objects(hash_of_objects, search_criteria, capacity = nil, date =
end
end

# If volume was specified, narrow down the matching objects
unless volume.nil?
# Skip objects that don't have fields for minimum_storage and maximum_storage
matching_objects = matching_objects.reject { |object| !object.key?('minimum_storage') || !object.key?('maximum_storage') }

# Skip objects that don't have values specified for minimum_storage and maximum_storage
matching_objects = matching_objects.reject { |object| object['minimum_storage'].nil? || object['maximum_storage'].nil? }

# Skip objects whose the minimum volume is below or maximum volume above the specified volume
matching_volume_objects = matching_objects.reject { |object| volume.to_f < object['minimum_storage'].to_f || volume.to_f > object['maximum_storage'].to_f }

# If no object was found, round the volume down in case the number fell between the limits in the json file.
if matching_volume_objects.size.zero?
volume *= 0.99
# Skip objects whose minimum volume is below or maximum volume above the specified volume
matching_objects = matching_objects.reject { |object| volume.to_f <= object['minimum_storage'].to_f || volume.to_f >= object['maximum_storage'].to_f }
else
matching_objects = matching_volume_objects
end
end

# If capacity_per_volume was specified, narrow down the matching objects
unless capacity_per_volume.nil?
# Skip objects that don't have fields for minimum_capacity_per_storage and maximum_capacity_per_storage
matching_objects = matching_objects.reject { |object| !object.key?('minimum_capacity_per_storage') || !object.key?('maximum_capacity_per_storage') }

# Skip objects that don't have values specified for minimum_capacity_per_storage and maximum_capacity_per_storage
matching_objects = matching_objects.reject { |object| object['minimum_capacity_per_storage'].nil? || object['maximum_capacity_per_storage'].nil? }

# Skip objects whose the minimum capacity_per_volume is below or maximum capacity_per_volume above the specified capacity_per_volume
matching_capacity_per_volume_objects = matching_objects.reject { |object| capacity_per_volume.to_f <= object['minimum_capacity_per_storage'].to_f || capacity_per_volume.to_f >= object['maximum_capacity_per_storage'].to_f }

# If no object was found, round the volume down in case the number fell between the limits in the json file.
if matching_capacity_per_volume_objects.size.zero?
capacity_per_volume *= 0.99
# Skip objects whose minimum capacity_per_volume is below or maximum capacity_per_volume above the specified capacity_per_volume
matching_objects = matching_objects.reject { |object| capacity_per_volume.to_f <= object['minimum_capacity_per_storage'].to_f || capacity_per_volume.to_f >= object['maximum_capacity_per_storage'].to_f }
else
matching_objects = matching_capacity_per_volume_objects
end
end

# If fan_motor_bhp was specified, narrow down the matching objects
unless fan_motor_bhp.nil?
# Skip objects that don't have fields for minimum_capacity and maximum_capacity
Expand Down Expand Up @@ -2512,8 +2557,8 @@ def model_find_objects(hash_of_objects, search_criteria, capacity = nil, date =
# 'type' => 'Enclosed',
# }
# motor_properties = self.model.find_object(motors, search_criteria, capacity: 2.5)
def model_find_object(hash_of_objects, search_criteria, capacity = nil, date = nil, area = nil, num_floors = nil, fan_motor_bhp = nil)
matching_objects = model_find_objects(hash_of_objects, search_criteria, capacity, date, area, num_floors, fan_motor_bhp)
def model_find_object(hash_of_objects, search_criteria, capacity = nil, date = nil, area = nil, num_floors = nil, fan_motor_bhp = nil, volume = nil, capacity_per_volume = nil)
matching_objects = model_find_objects(hash_of_objects, search_criteria, capacity, date, area, num_floors, fan_motor_bhp, volume, capacity_per_volume)

# Check the number of matching objects found
if matching_objects.size.zero?
Expand Down
102 changes: 83 additions & 19 deletions lib/openstudio-standards/standards/Standards.WaterHeaterMixed.rb
Original file line number Diff line number Diff line change
Expand Up @@ -54,23 +54,15 @@ def water_heater_mixed_apply_efficiency(water_heater_mixed)
OpenStudio.logFree(OpenStudio::Warn, 'openstudio.standards.WaterHeaterMixed', "For #{water_heater_mixed.name}, fuel type of #{fuel_type} is not yet supported, standard will not be applied.")
end

# Get the water heater properties
search_criteria = {}
search_criteria['template'] = template
search_criteria['fuel_type'] = fuel_type
wh_props = model_find_object(standards_data['water_heaters'], search_criteria, capacity_btu_per_hr)
unless wh_props
OpenStudio.logFree(OpenStudio::Warn, 'openstudio.standards.WaterHeaterMixed', "For #{water_heater_mixed.name}, cannot find water heater properties, cannot apply efficiency standard.")
return false
end
wh_props = water_heater_mixed_get_efficiency_requirement(water_heater_mixed, fuel_type, capacity_btu_per_hr, volume_gal)
return false if wh_props == {}

# Calculate the water heater efficiency and
# skin loss coefficient (UA) using different methods,
# depending on the metrics specified by the standard
water_heater_efficiency = nil
ua_btu_per_hr_per_f = nil

# Rarely specified by thermal efficiency alone
if wh_props['thermal_efficiency'] && !wh_props['standby_loss_capacity_allowance']
thermal_efficiency = wh_props['thermal_efficiency']
water_heater_efficiency = thermal_efficiency
Expand Down Expand Up @@ -98,20 +90,25 @@ def water_heater_mixed_apply_efficiency(water_heater_mixed)
vol_drt = wh_props['uniform_energy_factor_volume_allowance']
uniform_energy_factor = base_uniform_energy_factor - (vol_drt * volume_gal)
end
energy_factor = water_heater_convert_uniform_energy_factor_to_energy_factor(fuel_type, uniform_energy_factor, capacity_btu_per_hr, volume_gal)
energy_factor = water_heater_convert_uniform_energy_factor_to_energy_factor(water_heater_mixed, fuel_type, uniform_energy_factor, capacity_btu_per_hr, volume_gal)
water_heater_efficiency, ua_btu_per_hr_per_f = water_heater_convert_energy_factor_to_thermal_efficiency_and_ua(fuel_type, energy_factor, capacity_btu_per_hr)
# Two booster water heaters
ua_btu_per_hr_per_f = water_heater_mixed.name.to_s.include?('Booster') ? ua_btu_per_hr_per_f * 2 : ua_btu_per_hr_per_f
end

# Typically specified this way for large electric water heaters
if wh_props['standby_loss_base'] && wh_props['standby_loss_volume_allowance']
if wh_props['standby_loss_base'] && (wh_props['standby_loss_volume_allowance'] || wh_props['standby_loss_square_root_volume_allowance'])
# Fixed water heater efficiency per PNNL
water_heater_efficiency = 1.0
# Calculate the max allowable standby loss (SL)
sl_base = wh_props['standby_loss_base']
sl_drt = wh_props['standby_loss_volume_allowance']
sl_btu_per_hr = sl_base + (sl_drt * Math.sqrt(volume_gal))
if wh_props['standby_loss_square_root_volume_allowance']
sl_drt = wh_props['standby_loss_square_root_volume_allowance']
sl_btu_per_hr = sl_base + (sl_drt * Math.sqrt(volume_gal))
else # standby_loss_volume_allowance
sl_drt = wh_props['standby_loss_volume_allowance']
sl_btu_per_hr = sl_base + (sl_drt * volume_gal)
end
# Calculate the skin loss coefficient (UA)
ua_btu_per_hr_per_f = @instvarbuilding_type == 'MidriseApartment' ? sl_btu_per_hr / 70 * 23 : sl_btu_per_hr / 70
ua_btu_per_hr_per_f = water_heater_mixed.name.to_s.include?('Booster') ? ua_btu_per_hr_per_f * 2 : ua_btu_per_hr_per_f
Expand All @@ -124,21 +121,28 @@ def water_heater_mixed_apply_efficiency(water_heater_mixed)
# Calculate the percent loss per hr
hr_loss_base = wh_props['hourly_loss_base']
hr_loss_allow = wh_props['hourly_loss_volume_allowance']
hrly_loss_pct = hr_loss_base + (hr_loss_allow / volume_gal) / 100.0
hrly_loss_pct = hr_loss_base + hr_loss_allow / volume_gal
# Convert to Btu/hr, assuming:
# Water at 120F, density = 8.25 lb/gal
# 1 Btu to raise 1 lb of water 1 F
# Therefore 8.25 Btu / gal of water * deg F
# 70F delta-T between water and zone
hrly_loss_btu_per_hr = hrly_loss_pct * volume_gal * 8.25 * 70
hrly_loss_btu_per_hr = (hrly_loss_pct / 100) * volume_gal * 8.25 * 70
# Calculate the skin loss coefficient (UA)
ua_btu_per_hr_per_f = hrly_loss_btu_per_hr / 70
end

# Typically specified this way for large natural gas water heaters
if wh_props['standby_loss_capacity_allowance'] && wh_props['standby_loss_volume_allowance'] && wh_props['thermal_efficiency']
if wh_props['standby_loss_capacity_allowance'] && (wh_props['standby_loss_volume_allowance'] || wh_props['standby_loss_square_root_volume_allowance']) && wh_props['thermal_efficiency']
sl_cap_adj = wh_props['standby_loss_capacity_allowance']
sl_vol_drt = wh_props['standby_loss_volume_allowance']
if !wh_props['standby_loss_volume_allowance'].nil?
sl_vol_drt = wh_props['standby_loss_volume_allowance']
elsif !wh_props['standby_loss_square_root_volume_allowance'].nil?
sl_vol_drt = wh_props['standby_loss_square_root_volume_allowance']
else
OpenStudio.logFree(OpenStudio::Warn, 'openstudio.standards.WaterHeaterMixed', "For #{water_heater_mixed.name}, could not retrieve the standby loss volume allowance.")
return false
end
et = wh_props['thermal_efficiency']
# Estimate storage tank volume
tank_volume = volume_gal > 100 ? (volume_gal - 100).round(0) : 0
Expand Down Expand Up @@ -194,6 +198,56 @@ def water_heater_mixed_apply_efficiency(water_heater_mixed)
return true
end

# @param water_heater_mixed [OpenStudio::Model::WaterHeaterMixed] water heater mixed object
# @param fuel_type [Float] water heater fuel type
# @param capacity_btu_per_hr [Float] water heater capacity in Btu/h
# @param volume_gal [Float] water heater gallons of storage
# @return [Hash] returns a hash wwith the applicable efficiency requirements
def water_heater_mixed_get_efficiency_requirement(water_heater_mixed, fuel_type, capacity_btu_per_hr, volume_gal)
# Get the water heater properties
search_criteria = {}
search_criteria['template'] = template
search_criteria['fuel_type'] = fuel_type
search_criteria['equipment_type'] = 'Storage Water Heaters'

# Search base on capacity first
wh_props_capacity = model_find_objects(standards_data['water_heaters'], search_criteria, capacity_btu_per_hr)
wh_props_capacity_and_volume = model_find_objects(standards_data['water_heaters'], search_criteria, capacity_btu_per_hr, nil, nil, nil, nil, volume_gal.round(0))
wh_props_capacity_and_capacity_btu_per_hr = model_find_objects(standards_data['water_heaters'], search_criteria, capacity_btu_per_hr, nil, nil, nil, nil, nil, capacity_btu_per_hr)
wh_props_capacity_and_volume_and_capacity_per_volume = model_find_objects(standards_data['water_heaters'], search_criteria, capacity_btu_per_hr, nil, nil, nil, nil, volume_gal, capacity_btu_per_hr / volume_gal)

# We consider that the lookup is successful if only one set of record is returned
if wh_props_capacity.size == 1
wh_props = wh_props_capacity[0]
elsif wh_props_capacity_and_volume.size == 1
wh_props = wh_props_capacity_and_volume[0]
elsif wh_props_capacity_and_capacity_btu_per_hr == 1
wh_props = wh_props_capacity_and_capacity_btu_per_hr[0]
elsif wh_props_capacity_and_volume_and_capacity_per_volume == 1
wh_props = wh_props_capacity_and_volume_and_capacity_per_volume[0]
else
# Search again with additional criteria
search_criteria = water_heater_mixed_additional_search_criteria(water_heater_mixed, search_criteria)
wh_props_capacity = model_find_objects(standards_data['water_heaters'], search_criteria, capacity_btu_per_hr)
wh_props_capacity_and_volume = model_find_objects(standards_data['water_heaters'], search_criteria, capacity_btu_per_hr, nil, nil, nil, nil, volume_gal.round(0))
wh_props_capacity_and_capacity_btu_per_hr = model_find_objects(standards_data['water_heaters'], search_criteria, capacity_btu_per_hr, nil, nil, nil, nil, nil, capacity_btu_per_hr)
wh_props_capacity_and_volume_and_capacity_per_volume = model_find_objects(standards_data['water_heaters'], search_criteria, capacity_btu_per_hr, nil, nil, nil, nil, volume_gal, capacity_btu_per_hr / volume_gal)
if wh_props_capacity.size == 1
wh_props = wh_props_capacity[0]
elsif wh_props_capacity_and_volume.size == 1
wh_props = wh_props_capacity_and_volume[0]
elsif wh_props_capacity_and_capacity_btu_per_hr == 1
wh_props = wh_props_capacity_and_capacity_btu_per_hr[0]
elsif wh_props_capacity_and_volume_and_capacity_per_volume == 1
wh_props = wh_props_capacity_and_volume_and_capacity_per_volume[0]
else
return {}
end
end

return wh_props
end

# Applies the correct fuel type for the water heaters
# in the baseline model. For most standards and for most building
# types, the baseline uses the same fuel type as the proposed.
Expand Down Expand Up @@ -257,12 +311,13 @@ def water_heater_determine_sub_type(fuel_type, capacity_btu_per_hr, volume_gal)

# Convert Uniform Energy Factor (UEF) to Energy Factor (EF)
#
# @param water_heater_mixed [OpenStudio::Model::WaterHeaterMixed] water heater mixed object
# @param fuel_type [String] water heater fuel type
# @param uniform_energy_factor [Float] water heater Uniform Energy Factor (UEF)
# @param capacity_btu_per_hr [Float] water heater capacity
# @param volume_gal [Float] water heater storage volume in gallons
# @return [Float] returns Energy Factor (EF)
def water_heater_convert_uniform_energy_factor_to_energy_factor(fuel_type, uniform_energy_factor, capacity_btu_per_hr, volume_gal)
def water_heater_convert_uniform_energy_factor_to_energy_factor(water_heater_mixed, fuel_type, uniform_energy_factor, capacity_btu_per_hr, volume_gal)
# Get water heater sub type
sub_type = water_heater_determine_sub_type(fuel_type, capacity_btu_per_hr, volume_gal)

Expand Down Expand Up @@ -319,4 +374,13 @@ def water_heater_convert_energy_factor_to_thermal_efficiency_and_ua(fuel_type, e

return water_heater_efficiency, ua_btu_per_hr_per_f
end

# Add additional search criteria for water heater lookup efficiency.
#
# @param water_heater_mixed [OpenStudio::Model::WaterHeaterMixed] water heater mixed object
# @param search_criteria [Hash] search criteria for looking up water heater data
# @return [Hash] updated search criteria
def water_heater_mixed_additional_search_criteria(water_heater_mixed, search_criteria)
return search_criteria
end
end
Loading

0 comments on commit 67f99df

Please sign in to comment.