class NECB2020
@ref [References::NECB2020]
Public Class Methods
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
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
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 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
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
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' # 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