class NECB2020

@ref [References::NECB2020]

Public Class Methods

new() click to toggle source
Calls superclass method NECB2017::new
# File lib/openstudio-standards/standards/necb/NECB2020/necb_2020.rb, line 17
def initialize
  super()
  @template = self.class.name
  @standards_data = self.load_standards_database_new()
  self.corrupt_standards_database()
end

Public Instance Methods

apply_standard_construction_properties(model:, necb_hdd: true, runner: nil, ext_wall_cond: nil, ext_floor_cond: nil, ext_roof_cond: nil, ground_wall_cond: nil, ground_floor_cond: nil, ground_roof_cond: nil, fixed_window_cond: nil, fixed_wind_solar_trans: nil, fixed_wind_vis_trans: nil, operable_wind_solar_trans: nil, operable_window_cond: nil, operable_wind_vis_trans: nil, glass_door_cond: nil, glass_door_solar_trans: nil, glass_door_vis_trans: nil, door_construction_cond: nil, overhead_door_cond: nil, skylight_cond: nil, skylight_solar_trans: nil, skylight_vis_trans: nil, tubular_daylight_dome_cond: nil, tubular_daylight_dome_solar_trans: nil, tubular_daylight_dome_vis_trans: nil, tubular_daylight_diffuser_cond: nil, tubular_daylight_diffuser_solar_trans: nil, tubular_daylight_diffuser_vis_trans: nil) click to toggle source

Go through the default construction sets and hard-assigned constructions. Clone the existing constructions and set their intended surface type and standards construction type per the PRM. For some standards, this will involve making modifications. For others, it will not.

90.1-2007, 90.1-2010, 90.1-2013 @return [Boolean] returns true if successful, false if not

# File lib/openstudio-standards/standards/necb/NECB2020/building_envelope.rb, line 13
def apply_standard_construction_properties(model:,
                                           necb_hdd: true,
                                           runner: nil,
                                           # ext surfaces
                                           ext_wall_cond: nil,
                                           ext_floor_cond: nil,
                                           ext_roof_cond: nil,
                                           # ground surfaces
                                           ground_wall_cond: nil,
                                           ground_floor_cond: nil,
                                           ground_roof_cond: nil,
                                           # fixed Windows
                                           fixed_window_cond: nil,
                                           fixed_wind_solar_trans: nil,
                                           fixed_wind_vis_trans: nil,
                                           # operable windows
                                           operable_wind_solar_trans: nil,
                                           operable_window_cond: nil,
                                           operable_wind_vis_trans: nil,
                                           # glass doors
                                           glass_door_cond: nil,
                                           glass_door_solar_trans: nil,
                                           glass_door_vis_trans: nil,
                                           # opaque doors
                                           door_construction_cond: nil,
                                           overhead_door_cond: nil,
                                           # skylights
                                           skylight_cond: nil,
                                           skylight_solar_trans: nil,
                                           skylight_vis_trans: nil,
                                           # tubular daylight dome
                                           tubular_daylight_dome_cond: nil,
                                           tubular_daylight_dome_solar_trans: nil,
                                           tubular_daylight_dome_vis_trans: nil,
                                           # tubular daylight diffuser
                                           tubular_daylight_diffuser_cond: nil,
                                           tubular_daylight_diffuser_solar_trans: nil,
                                           tubular_daylight_diffuser_vis_trans: nil)

  model.getDefaultConstructionSets.sort.each do |default_surface_construction_set|
    BTAP.runner_register('Info', 'apply_standard_construction_properties', runner)
    if model.weatherFile.empty? || model.weatherFile.get.path.empty? || !File.exist?(model.weatherFile.get.path.get.to_s)

      BTAP.runner_register('Error', 'Weather file is not defined. Please ensure the weather file is defined and exists.', runner)
      return false
    end

    # hdd required to get correct conductance values from the json file.
    hdd = get_necb_hdd18(model: model, necb_hdd: necb_hdd)
        
    # Lambdas are preferred over methods in methods for small utility methods.
    correct_cond = lambda do |conductivity, surface_type|
      return conductivity.nil? || conductivity.to_f <= 0.0 || conductivity == "NECB_Default" ? eval(model_find_objects(@standards_data['surface_thermal_transmittance'], surface_type)[0]['formula']) : conductivity.to_f
    end

    # Converts trans and vis to nil if requesting default.. or casts the string to a float.
    correct_vis_trans = lambda do |value|
      return value.nil? || value.to_f <= 0.0 || value == "NECB_Default" ? nil : value.to_f
    end

    BTAP::Resources::Envelope::ConstructionSets.customize_default_surface_construction_set!(model: model,
                                                                                            name: "#{default_surface_construction_set.name.get} at hdd = #{hdd}",
                                                                                            default_surface_construction_set: default_surface_construction_set,
                                                                                            # ext surfaces
                                                                                            ext_wall_cond: correct_cond.call(ext_wall_cond, {'boundary_condition' => 'Outdoors', 'surface' => 'Wall'}),
                                                                                            ext_floor_cond: correct_cond.call(ext_floor_cond, {'boundary_condition' => 'Outdoors', 'surface' => 'Floor'}),
                                                                                            ext_roof_cond: correct_cond.call(ext_roof_cond, {'boundary_condition' => 'Outdoors', 'surface' => 'RoofCeiling'}),
                                                                                            # ground surfaces
                                                                                            ground_wall_cond: correct_cond.call(ground_wall_cond, {'boundary_condition' => 'Ground', 'surface' => 'Wall'}),
                                                                                            ground_floor_cond: correct_cond.call(ground_floor_cond, {'boundary_condition' => 'Ground', 'surface' => 'Floor'}),
                                                                                            ground_roof_cond: correct_cond.call(ground_roof_cond, {'boundary_condition' => 'Ground', 'surface' => 'RoofCeiling'}),
                                                                                            # fixed Windows
                                                                                            fixed_window_cond: correct_cond.call(fixed_window_cond, {'boundary_condition' => 'Outdoors', 'surface' => 'Window'}),
                                                                                            fixed_wind_solar_trans: correct_vis_trans.call(fixed_wind_solar_trans),
                                                                                            fixed_wind_vis_trans: correct_vis_trans.call(fixed_wind_vis_trans),
                                                                                            # operable windows
                                                                                            operable_wind_solar_trans: correct_vis_trans.call(operable_wind_solar_trans),
                                                                                            operable_window_cond: correct_cond.call(fixed_window_cond, {'boundary_condition' => 'Outdoors', 'surface' => 'Window'}),
                                                                                            operable_wind_vis_trans: correct_vis_trans.call(operable_wind_vis_trans),
                                                                                            # glass doors
                                                                                            glass_door_cond: correct_cond.call(glass_door_cond, {'boundary_condition' => 'Outdoors', 'surface' => 'Window'}),
                                                                                            glass_door_solar_trans: correct_vis_trans.call(glass_door_solar_trans),
                                                                                            glass_door_vis_trans: correct_vis_trans.call(glass_door_vis_trans),
                                                                                            # opaque doors
                                                                                            door_construction_cond: correct_cond.call(door_construction_cond, {'boundary_condition' => 'Outdoors', 'surface' => 'Door'}),
                                                                                            overhead_door_cond: correct_cond.call(overhead_door_cond, {'boundary_condition' => 'Outdoors', 'surface' => 'Door'}),
                                                                                            # skylights
                                                                                            skylight_cond: correct_cond.call(skylight_cond, {'boundary_condition' => 'Outdoors', 'surface' => 'Skylight'}),
                                                                                            skylight_solar_trans: correct_vis_trans.call(skylight_solar_trans),
                                                                                            skylight_vis_trans: correct_vis_trans.call(skylight_vis_trans),
                                                                                            # tubular daylight dome
                                                                                            tubular_daylight_dome_cond: correct_cond.call(skylight_cond, {'boundary_condition' => 'Outdoors', 'surface' => 'Skylight'}),
                                                                                            tubular_daylight_dome_solar_trans: correct_vis_trans.call(tubular_daylight_dome_solar_trans),
                                                                                            tubular_daylight_dome_vis_trans: correct_vis_trans.call(tubular_daylight_dome_vis_trans),
                                                                                            # tubular daylight diffuser
                                                                                            tubular_daylight_diffuser_cond: correct_cond.call(skylight_cond, {'boundary_condition' => 'Outdoors', 'surface' => 'Skylight'}),
                                                                                            tubular_daylight_diffuser_solar_trans: correct_vis_trans.call(tubular_daylight_diffuser_solar_trans),
                                                                                            tubular_daylight_diffuser_vis_trans: correct_vis_trans.call(tubular_daylight_diffuser_vis_trans)
    )
  end
  # sets all surfaces to use default constructions sets except adiabatic, where it does a hard assignment of the interior wall construction type.
  model.getPlanarSurfaces.sort.each(&:resetConstruction)
  # if the default construction set is defined..try to assign the interior wall to the adiabatic surfaces
  BTAP::Resources::Envelope.assign_interior_surface_construction_to_adiabatic_surfaces(model, nil)
  BTAP.runner_register('Info', ' apply_standard_construction_properties was sucessful.', runner)
end
load_standards_database_new() click to toggle source
Calls superclass method NECB2017#load_standards_database_new
# File lib/openstudio-standards/standards/necb/NECB2020/necb_2020.rb, line 24
def load_standards_database_new
  # load NECB2020 data.
  super()

  if __dir__[0] == ':' # Running from OpenStudio CLI
    embedded_files_relative('data/', /.*\.json/).each do |file|
      data = JSON.parse(EmbeddedScripting.getFileAsString(file))
      if !data['tables'].nil?
        @standards_data['tables'] = [*@standards_data['tables'], *data['tables']].to_h
      elsif !data['constants'].nil?
        @standards_data['constants'] = [*@standards_data['constants'], *data['constants']].to_h
      elsif !data['constants'].nil?
        @standards_data['formulas'] = [*@standards_data['formulas'], *data['formulas']].to_h
      end
    end
  else
    files = Dir.glob("#{File.dirname(__FILE__)}/data/*.json").select { |e| File.file? e }
    files.each do |file|
      data = JSON.parse(File.read(file))
      if !data['tables'].nil?
        @standards_data['tables'] = [*@standards_data['tables'], *data['tables']].to_h
      elsif !data['constants'].nil?
        @standards_data['constants'] = [*@standards_data['constants'], *data['constants']].to_h
      elsif !data['formulas'].nil?
        @standards_data['formulas'] = [*@standards_data['formulas'], *data['formulas']].to_h
      end
    end
  end
  # Write test report file.
  # Write database to file.
  # File.open(File.join(File.dirname(__FILE__), '..', 'NECB2017.json'), 'w') {|f| f.write(JSON.pretty_generate(@standards_data))}
  return @standards_data
end
set_necb_external_subsurface_conductance(subsurface, hdd) click to toggle source

Set all external subsurfaces (doors, windows, skylights) to NECB values. @author phylroy.lopez@nrcan.gc.ca @param subsurface [String] @param hdd [Float]

# File lib/openstudio-standards/standards/necb/NECB2020/building_envelope.rb, line 124
def set_necb_external_subsurface_conductance(subsurface, hdd)
  conductance_value = 0

  if subsurface.outsideBoundaryCondition.downcase.match('outdoors')
    case subsurface.subSurfaceType.downcase
    when /window/
      conductance_value = @standards_data['conductances']['Window'].find { |i| i['hdd'] > hdd }['thermal_transmittance'] * scaling_factor
    when /skylight/
      conductance_value = @standards_data['conductances']['Skylight'].find { |i| i['hdd'] > hdd }['thermal_transmittance'] * scaling_factor
    when /door/
      conductance_value = @standards_data['conductances']['Door'].find { |i| i['hdd'] > hdd }['thermal_transmittance'] * scaling_factor
    end
    subsurface.setRSI(1 / conductance_value)
  end
end
space_apply_infiltration_rate(space) click to toggle source

Set the infiltration rate for this space to include the impact of air leakage requirements in the standard.

Note that this is significantly different for NECB 2020 compared to previous codes.

The value is now specified at 75 Pa normalised by entire building surface area (previously 5 Pa
and for above grade surfaces only). Need to convert to 5 Pa and for the different surface area.

@return [Double] true if successful, false if not @todo handle doors and vestibules

# File lib/openstudio-standards/standards/necb/NECB2020/necb_2020.rb, line 67
def space_apply_infiltration_rate(space)

  # Remove infiltration rates set at the space type.
  infiltration_data = @standards_data['infiltration']
  unless space.spaceType.empty?
    space.spaceType.get.spaceInfiltrationDesignFlowRates.each(&:remove)
  end
  # Remove infiltration rates set at the space object.
  space.spaceInfiltrationDesignFlowRates.each(&:remove)

  # Don't create an object if there is no exterior wall area.
  exterior_wall_and_roof_and_subsurface_area = OpenstudioStandards::Geometry.space_get_exterior_wall_and_subsurface_and_roof_area(space)
  if exterior_wall_and_roof_and_subsurface_area <= 0.0
    OpenStudio.logFree(OpenStudio::Info, 'openstudio.Standards.Model', "For #{template}, no exterior wall area was found in #{space.name}; no infiltration will be added.")
    return true
  end

  # Calculate total area of above and below grade envelope area in the entire model.
  totalAreaBuildingEnvelope = 0.0
  totalAboveGradeArea = 0.0

      space.model.getSpaces.each do |modelspace|
        multiplier = modelspace.multiplier
        modelspace.surfaces.each do |surface|
          if surface.outsideBoundaryCondition == "Outdoors" then
                area = surface.grossArea * multiplier
        totalAreaBuildingEnvelope += area
        totalAboveGradeArea += area
              elsif surface.outsideBoundaryCondition == "Ground" then
                area = surface.grossArea * multiplier
        totalAreaBuildingEnvelope += area
              end
        end
      end

      # Get infiltration rate from standards and convert to value at 5 Pa applied to all above grade surfaces.
  infil_75Pa_all_surf = self.get_standards_constant('infiltration_rate_m3_per_s_per_m2')
  infil_5Pa_above_grade = infil_75Pa_all_surf * ((5.0 / 75.0) ** (0.6)) * totalAreaBuildingEnvelope / totalAboveGradeArea
  OpenStudio.logFree(OpenStudio::Debug, 'openstudio.Standards.Space', "For #{space.name}, adj infil = #{infil_5Pa_above_grade.round(5)} m^3/s*m^2.")

  # Get any infiltration schedule already assigned to this space or its space type
  # If not, the always on schedule will be applied.
  infil_sch = nil
  unless space.spaceInfiltrationDesignFlowRates.empty?
    old_infil = space.spaceInfiltrationDesignFlowRates[0]
    if old_infil.schedule.is_initialized
      infil_sch = old_infil.schedule.get
    end
  end

  if infil_sch.nil? && space.spaceType.is_initialized
    space_type = space.spaceType.get
    unless space_type.spaceInfiltrationDesignFlowRates.empty?
      old_infil = space_type.spaceInfiltrationDesignFlowRates[0]
      if old_infil.schedule.is_initialized
        infil_sch = old_infil.schedule.get
      end
    end
  end

  if infil_sch.nil?
    infil_sch = space.model.alwaysOnDiscreteSchedule
  end

  # Create an infiltration rate object for this space.
  infiltration = OpenStudio::Model::SpaceInfiltrationDesignFlowRate.new(space.model)
  infiltration.setName("#{space.name} Infiltration")
  infiltration.setFlowperExteriorSurfaceArea(infil_5Pa_above_grade)
  infiltration.setSchedule(infil_sch)
  infiltration.setConstantTermCoefficient(self.get_standards_constant('infiltration_constant_term_coefficient'))
  infiltration.setTemperatureTermCoefficient(self.get_standards_constant('infiltration_constant_term_coefficient'))
  infiltration.setVelocityTermCoefficient(self.get_standards_constant('infiltration_velocity_term_coefficient'))
  infiltration.setVelocitySquaredTermCoefficient(self.get_standards_constant('infiltration_velocity_squared_term_coefficient'))
  infiltration.setSpace(space)
  return true
end
water_heater_mixed_apply_efficiency(water_heater_mixed) click to toggle source

Applies the standard efficiency ratings and typical losses and paraisitic loads to this object. Efficiency and skin loss coefficient (UA) Per PNNL www.energycodes.gov/sites/default/files/documents/PrototypeModelEnhancements_2014_0.pdf Appendix A: Service Water Heating

@return [Boolean] true if successful, false if not

NECB2020 uses a different procedure calculate gas water heater efficiencies (compared to previous NECB)

# File lib/openstudio-standards/standards/necb/NECB2020/service_water_heating.rb, line 12
def water_heater_mixed_apply_efficiency(water_heater_mixed)
  # Get the capacity of the water heater
  # @todo add capability to pull autosized water heater capacity
  # if the Sizing:WaterHeater object is ever implemented in OpenStudio.
  capacity_w = water_heater_mixed.heaterMaximumCapacity
  if capacity_w.empty?
    OpenStudio.logFree(OpenStudio::Warn, 'openstudio.standards.WaterHeaterMixed', "For #{water_heater_mixed.name}, cannot find capacity, standard will not be applied.")
    return false
  else
    capacity_w = capacity_w.get
  end
  capacity_btu_per_hr = OpenStudio.convert(capacity_w, 'W', 'Btu/hr').get
  capacity_kbtu_per_hr = OpenStudio.convert(capacity_w, 'W', 'kBtu/hr').get

  # Get the volume of the water heater
  # @todo add capability to pull autosized water heater volume
  # if the Sizing:WaterHeater object is ever implemented in OpenStudio.
  volume_m3 = water_heater_mixed.tankVolume
  if volume_m3.empty?
    OpenStudio.logFree(OpenStudio::Warn, 'openstudio.standards.WaterHeaterMixed', "For #{water_heater_mixed.name}, cannot find volume, standard will not be applied.")
    return false
  else
    volume_m3 = volume_m3.get
  end
  volume_gal = OpenStudio.convert(volume_m3, 'm^3', 'gal').get
  volume_litre = OpenStudio.convert(volume_m3, 'm^3', 'L').get
  # Get the heater fuel type
  fuel_type = water_heater_mixed.heaterFuelType
  unless fuel_type == 'NaturalGas' || fuel_type == 'Electricity' || fuel_type == 'FuelOilNo2'
    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

  # Calculate the water heater efficiency and
  # skin loss coefficient (UA)
  # Calculate the energy factor (EF)
  # From PNNL http://www.energycodes.gov/sites/default/files/documents/PrototypeModelEnhancements_2014_0.pdf
  # Appendix A: Service Water Heating
        # and modified by PCF 1630 as noted below.

  water_heater_eff = nil
  ua_btu_per_hr_per_f = nil
  sl_btu_per_hr = nil
  q_load_btu_per_hr = nil
  uef = nil
  case fuel_type
  when 'Electricity'
    volume_litre_per_s = volume_m3 * 1000
    if capacity_btu_per_hr <= OpenStudio.convert(12, 'kW', 'Btu/hr').get
      # Fixed water heater efficiency per PNNL
      water_heater_eff = 1
      # Calculate the max allowable standby loss (SL)
      sl_w = if volume_litre_per_s < 270
               40 + 0.2 * volume_litre_per_s # assume bottom inlet
             else
               0.472 * volume_litre_per_s - 33.5
               # assume bottom inlet
             end
      sl_btu_per_hr = OpenStudio.convert(sl_w, 'W', 'Btu/hr').get
    else
      # Fixed water heater efficiency per PNNL
      water_heater_eff = 1
      # Calculate the max allowable standby loss (SL)   # use this - NECB does not give SL calculation for cap > 12 kW
      sl_w = 0.3 + 102.2/volume_litre_per_s
      sl_btu_per_hr = OpenStudio.convert(sl_w, 'W', 'Btu/hr').get
    end
    # Calculate the skin loss coefficient (UA)
    ua_btu_per_hr_per_f = sl_btu_per_hr / 70
  when 'NaturalGas', 'FuelOilNo2'
    # Performance requirements from NECB2020 Table 6.2.2.1 Gas-fired storage type
    
    # Performance requirement based on FHR and volume
    # Water heater parameters derived using the procedure described by:
    #   Maguire, J., & Roberts, D. (2020). DERIVING SIMULATION PARAMETERS FOR STORAGE-TYPE WATER HEATERS
    #   USING RATINGS DATA PRODUCED FROM THE UNIFORM ENERGY FACTOR TEST PROCEDURE. 2020 Building Performance
    #   Analysis Conference and SimBuild co-organized by ASHRAE and IBPSA-USA (pp. 325-331). Chicago: ASHRAE.
    #   https://www.ashrae.org/file%20library/conferences/specialty%20conferences/2020%20building%20performance/papers/d-bsc20-c039.pdf
    #
    #   AND
    #
    #   PNNL http://www.energycodes.gov/sites/default/files/documents/PrototypeModelEnhancements_2014_0.pdf

    # Assume fhr = peak demand flow
    tank_param = auto_size_shw_capacity(model:water_heater_mixed.model, shw_scale: 'NECB_Default')
    fhr_L_per_hr = tank_param['loop_peak_flow_rate_SI']
    fhr_L_per_hr = fhr_L_per_hr * 3600000
    if capacity_w <= 22000 and volume_litre >= 76 and volume_litre < 208
      if fhr_L_per_hr < 68
        uef = 0.3456 - 0.00053*volume_litre
        q_load_btu_per_hr = 5561
        volume_drawn_gal = 10
      elsif fhr_L_per_hr >= 68 and fhr_L_per_hr < 193
        uef = 0.5982 - 0.00050*volume_litre
        q_load_btu_per_hr = 21131
        volume_drawn_gal = 38
      elsif fhr_L_per_hr >= 193 and fhr_L_per_hr < 284
        uef = 0.6483 - 0.00045*volume_litre
        q_load_btu_per_hr = 30584
        volume_drawn_gal = 55
      elsif fhr_L_per_hr >= 284 
        uef = 0.6920 - 0.00034*volume_litre
        q_load_btu_per_hr = 46710
        volume_drawn_gal = 84
      end

      # Assume burner efficiency  (PNNL)
      water_heater_eff = 0.82

      # Estimate recovery efficiency (RE) and UA (Maguire and Robers, 2020)
      q_load_btu = volume_drawn_gal*8.30074*0.99826*(125-58) #water properties at 91.5F
      capacity_btu_per_hr = OpenStudio.convert(capacity_w, 'W', 'Btu/hr').get
      re = water_heater_eff + q_load_btu*(uef-water_heater_eff)/(24*capacity_btu_per_hr*uef)
      ua_btu_per_hr_per_f = (water_heater_eff-re)*capacity_btu_per_hr/(125-67.5)  

    elsif capacity_w <= 22000 and volume_litre >= 208 and volume_litre < 380
      if fhr_L_per_hr < 68
        uef = 0.6470 - 0.00016*volume_litre
        q_load_btu_per_hr = 5561
        volume_drawn_gal = 10
      elsif fhr_L_per_hr >= 68 and fhr_L_per_hr < 193
        uef = 0.7689 - 0.00013*volume_litre
        q_load_btu_per_hr = 21131
        volume_drawn_gal = 38
      elsif fhr_L_per_hr >= 193 and fhr_L_per_hr < 284
        uef = 0.7897 - 0.00011*volume_litre
        q_load_btu_per_hr = 30584
        volume_drawn_gal = 55
      elsif fhr_L_per_hr >= 284 
        uef = 0.8072 - 0.00008*volume_litre
        q_load_btu_per_hr = 46710
        volume_drawn_gal = 84
      end

      # Assume burner  efficiency  (PNNL)
      water_heater_eff = 0.82

      # Estimate recovery efficiency (RE) and UA (Maguire and Robers, 2020)
      q_load_btu = volume_drawn_gal*8.30074*0.99826*(125-58) #water properties at 91.5F
      capacity_btu_per_hr = OpenStudio.convert(capacity_w, 'W', 'Btu/hr').get
      # Estimate recovery efficiency (RE) and UA (Maguire and Robers, 2020)
      re = water_heater_eff + q_load_btu*(uef-water_heater_eff)/(24*capacity_btu_per_hr*uef)
      ua_btu_per_hr_per_f = (water_heater_eff-re)*capacity_btu_per_hr/(125-67.5)      

    elsif capacity_w > 22000 and capacity_w <= 30500 and volume_litre <= 454 
      # NOTE: volume_litre 454L in this case, refers to manufacturer stated volume.
      # Assume manufacturer rated volume = actual tank volume (value used in EnergyPlus)
      
      uef = 0.8107 - 0.00021*volume_litre

      # Assume burner efficiency  (PNNL)
      water_heater_eff = 0.82

      # Estimate recovery efficiency (RE) and UA (Maguire and Robers, 2020)
      capacity_btu_per_hr = OpenStudio.convert(capacity_w, 'W', 'Btu/hr').get
      if fhr_L_per_hr < 68
        q_load_btu_per_hr = 5561
        volume_drawn_gal = 10
      elsif fhr_L_per_hr >= 68 and fhr_L_per_hr < 193
        q_load_btu_per_hr = 21131
        volume_drawn_gal = 38
      elsif fhr_L_per_hr >= 193 and fhr_L_per_hr < 284
        q_load_btu_per_hr = 30584
        volume_drawn_gal = 55
      elsif fhr_L_per_hr >= 284 
        q_load_btu_per_hr = 46710
        volume_drawn_gal = 84
      end
      q_load_btu = volume_drawn_gal*8.30074*0.99826*(125-58) #water properties at 91.5F

      # Estimate recovery efficiency (RE) and UA (Maguire and Robers, 2020)
      re = water_heater_eff + q_load_btu*(uef-water_heater_eff)/(24*capacity_btu_per_hr*uef)
      ua_btu_per_hr_per_f = (water_heater_eff-re)*capacity_btu_per_hr/(125-67.5)      
      
    else # all other water heaters
      capacity_kw = capacity_w/1000
      capacity_btu_per_hr = OpenStudio.convert(capacity_w, 'W', 'Btu/hr').get
      # thermal efficiency (NECB2020)
      et = 0.9
      # maximum standby losses
      sl_w = 0.84*(1.25*capacity_kw + 16.57*(volume_litre**0.5))
      sl_btu_per_hr = OpenStudio.convert(sl_w, 'W', 'Btu/hr').get
      ua_btu_per_hr_per_f = sl_btu_per_hr*et / 70
      water_heater_eff = (ua_btu_per_hr_per_f*70 + capacity_btu_per_hr*et)/capacity_btu_per_hr
    end
  end

  # Convert to SI
  ua_w_per_k = OpenStudio.convert(ua_btu_per_hr_per_f, 'Btu/hr*R', 'W/K').get
  # Set the water heater properties
  # Efficiency
  water_heater_mixed.setHeaterThermalEfficiency(water_heater_eff)
  # Skin loss
  water_heater_mixed.setOffCycleLossCoefficienttoAmbientTemperature(ua_w_per_k)
  water_heater_mixed.setOnCycleLossCoefficienttoAmbientTemperature(ua_w_per_k)
  # @todo Parasitic loss (pilot light)
  # PNNL document says pilot lights were removed, but IDFs
  # still have the on/off cycle parasitic fuel consumptions filled in
  water_heater_mixed.setOnCycleParasiticFuelType(fuel_type)
  # self.setOffCycleParasiticFuelConsumptionRate(??)
  water_heater_mixed.setOnCycleParasiticHeatFractiontoTank(0)
  water_heater_mixed.setOffCycleParasiticFuelType(fuel_type)
  # self.setOffCycleParasiticFuelConsumptionRate(??)
  water_heater_mixed.setOffCycleParasiticHeatFractiontoTank(0.8)

  # set part-load performance curve
  if (fuel_type == 'NaturalGas') || (fuel_type == 'FuelOilNo2')
    plf_vs_plr_curve = model_add_curve(water_heater_mixed.model, 'SWH-EFFFPLR-NECB2011')
    water_heater_mixed.setPartLoadFactorCurve(plf_vs_plr_curve)
  end

  # Append the name with standards information
  water_heater_mixed.setName("#{water_heater_mixed.name} #{water_heater_eff.round(3)} Therm Eff")
  OpenStudio.logFree(OpenStudio::Info, 'openstudio.model.WaterHeaterMixed', "For #{template}: #{water_heater_mixed.name}; thermal efficiency = #{water_heater_eff.round(3)}, skin-loss UA = #{ua_btu_per_hr_per_f.round}Btu/hr-R")
  return true
end