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

Building Energy Standards Water Heater Data Update #1680

Merged
merged 18 commits into from
Apr 3, 2024
Merged
Show file tree
Hide file tree
Changes from all 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
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?
Copy link
Collaborator

Choose a reason for hiding this comment

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

Consider storage volume as a lookup key.

# 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?
Copy link
Collaborator

Choose a reason for hiding this comment

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

Consider the ratio of capacity per volume as a lookup key.

# 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
Comment on lines -127 to +130
Copy link
Collaborator

Choose a reason for hiding this comment

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

The / 100 should be applied to hrly_loss_pct, not just to hr_loss_allow, otherwise hr_loss_base gets ignored and hrly_loss_btu_per_hr are overestimated.

# 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
Comment on lines +220 to +246
Copy link
Collaborator

Choose a reason for hiding this comment

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

Use a "stepped" approach to identify what the applicable requirement should be.


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