class ASHRAE901PRM < Standard # @!group Model # Determines which system number is used for the baseline system. # @param model [OpenStudio::Model::Model] OpenStudio model object # @param climate_zone [String] ASHRAE climate zone, e.g. 'ASHRAE 169-2013-4A' # @return [String] the system number: 1_or_2, 3_or_4, 5_or_6, 7_or_8, 9_or_10 def model_prm_baseline_system_number(model, climate_zone, area_type, fuel_type, area_ft2, num_stories, custom) sys_num = nil # Customization - Xcel EDA Program Manual 2014 # Table 3.2.2 Baseline HVAC System Types if custom == 'Xcel Energy CO EDA' OpenStudio.logFree(OpenStudio::Info, 'openstudio.standards.Model', 'Custom; per Xcel EDA Program Manual 2014 Table 3.2.2 Baseline HVAC System Types, the 90.1-2010 lookup for HVAC system types shall be used.') # Set the area limit limit_ft2 = 25_000 case area_type when 'residential' sys_num = '1_or_2' when 'nonresidential' # nonresidential and 3 floors or less and <25,000 ft2 if num_stories <= 3 && area_ft2 < limit_ft2 sys_num = '3_or_4' # nonresidential and 4 or 5 floors or 5 floors or less and 25,000 ft2 to 150,000 ft2 elsif ((num_stories == 4 || num_stories == 5) && area_ft2 < limit_ft2) || (num_stories <= 5 && (area_ft2 >= limit_ft2 && area_ft2 <= 150_000)) sys_num = '5_or_6' # nonresidential and more than 5 floors or >150,000 ft2 elsif num_stories >= 5 || area_ft2 > 150_000 sys_num = '7_or_8' end when 'heatedonly' sys_num = '9_or_10' when 'retail' # Should only be hit by Xcel EDA sys_num = '3_or_4' end else # Set the area limit limit_ft2 = 25_000 case area_type when 'residential' sys_num = '1_or_2' when 'nonresidential' # nonresidential and 3 floors or less and <25,000 ft2 if num_stories <= 3 && area_ft2 < limit_ft2 sys_num = '3_or_4' # nonresidential and 4 or 5 floors or 5 floors or less and 25,000 ft2 to 150,000 ft2 elsif ((num_stories == 4 || num_stories == 5) && area_ft2 < limit_ft2) || (num_stories <= 5 && (area_ft2 >= limit_ft2 && area_ft2 <= 150_000)) sys_num = '5_or_6' # nonresidential and more than 5 floors or >150,000 ft2 elsif num_stories >= 5 || area_ft2 > 150_000 sys_num = '7_or_8' end when 'heatedonly' sys_num = '9_or_10' when 'retail' sys_num = '3_or_4' end end return sys_num end # Change the fuel type based on climate zone, depending on the standard. # For 90.1-2013, fuel type is based on climate zone, not the proposed model. # @param model [OpenStudio::Model::Model] OpenStudio model object # @param fuel_type [String] Valid choices are electric, fossil, fossilandelectric, # purchasedheat, purchasedcooling, purchasedheatandcooling # @param climate_zone [String] ASHRAE climate zone, e.g. 'ASHRAE 169-2013-4A' # @return [String] the revised fuel type def model_prm_baseline_system_change_fuel_type(model, fuel_type, climate_zone) # For 90.1-2013 the fuel type is determined based on climate zone. # Don't change the fuel if it purchased heating or cooling. if fuel_type == 'electric' || fuel_type == 'fossil' case climate_zone when 'ASHRAE 169-2006-1A', 'ASHRAE 169-2006-2A', 'ASHRAE 169-2006-3A', 'ASHRAE 169-2013-1A', 'ASHRAE 169-2013-2A', 'ASHRAE 169-2013-3A' fuel_type = 'electric' else fuel_type = 'fossil' end OpenStudio.logFree(OpenStudio::Info, 'openstudio.standards.Model', "Heating fuel is #{fuel_type} for 90.1-2013, climate zone #{climate_zone}. This is independent of the heating fuel type in the proposed building, per G3.1.1-3. This is different than previous versions of 90.1.") end return fuel_type end # Determines the fan type used by VAV_Reheat and VAV_PFP_Boxes systems. # Variable speed fan for 90.1-2019 # @param model [OpenStudio::Model::Model] OpenStudio model object # @return [String] the fan type: TwoSpeed Fan, Variable Speed Fan def model_baseline_system_vav_fan_type(model) fan_type = 'Variable Speed Fan' return fan_type end # Calculate the building envelope area according to the 90.1 definition # # @param model [OpenStudio::Model::Model] OpenStudio model object # @return [Double] Building envelope area in m2 def model_building_envelope_area(model) # Get the space building envelope area # According to the 90.1 definition, building envelope include: # - "the elements of a building that separate conditioned spaces from the exterior" # - "the elements of a building that separate conditioned space from unconditioned # space or that enclose semiheated spaces through which thermal energy may be # transferred to or from the exterior, to or from unconditioned spaces or to or # from conditioned spaces." building_envelope_area_m2 = 0 model.getSpaces.each do |space| building_envelope_area_m2 += OpenstudioStandards::Geometry.space_get_envelope_area(space) end if building_envelope_area_m2 == 0.0 OpenStudio.logFree(OpenStudio::Warn, 'openstudio.standards.Model', 'Calculated building envelope area is 0 m2, no infiltration will be added.') return 0.0 end return building_envelope_area_m2 end # This method creates customized infiltration objects for each # space and removes the SpaceType-level infiltration objects. # @param model [OpenStudio::Model::Model] openstudio model # @param specific_space_infiltration_rate_75_pa [Double] space infiltration rate at a pressure differential of 75 Pa # @return [Boolean] true if successful, false if not def model_apply_standard_infiltration(model, specific_space_infiltration_rate_75_pa = nil) # Model shouldn't use SpaceInfiltrationEffectiveLeakageArea # Excerpt from the EnergyPlus Input/Output reference manual: # "This model is based on work by Sherman and Grimsrud (1980) # and is appropriate for smaller, residential-type buildings." # Raise exception if the model does use this object ela = 0 model.getSpaceInfiltrationEffectiveLeakageAreas.sort.each do |eff_la| ela += 1 end if ela > 0 OpenStudio.logFree(OpenStudio::Warn, 'prm.log', 'The current model cannot include SpaceInfiltrationEffectiveLeakageArea. These objects will be skipped in modeling infiltration according to the 90.1-PRM rules.') end # Get the space building envelope area building_envelope_area_m2 = model_building_envelope_area(model) prm_raise(building_envelope_area_m2 > 0.0, @sizing_run_dir, 'Calculated building envelope area is 0 m2, Please check model inputs.') # Calculate current model air leakage rate @ 75 Pa and report it curr_tot_infil_m3_per_s_per_envelope_area = model_current_building_envelope_infiltration_at_75pa(model, building_envelope_area_m2) OpenStudio.logFree(OpenStudio::Info, 'prm.log', "The model's I_75Pa is estimated to be #{curr_tot_infil_m3_per_s_per_envelope_area} m3/s per m2 of total building envelope.") # Calculate building adjusted building envelope # air infiltration following the 90.1 PRM rules tot_infil_m3_per_s = model_adjusted_building_envelope_infiltration(building_envelope_area_m2, specific_space_infiltration_rate_75_pa) # Find infiltration method used in the model, if any. # # If multiple methods are used, use per above grade wall # area (i.e. exterior wall area), if air/changes per hour # or exterior surface area is used, use Flow/ExteriorWallArea infil_method = model_get_infiltration_method(model) infil_method = 'Flow/ExteriorWallArea' if infil_method != 'Flow/Area' || infil_method != 'Flow/ExteriorWallArea' infil_coefficients = model_get_infiltration_coefficients(model) # Set the infiltration rate at each space model.getSpaces.each do |space| space_apply_infiltration_rate(space, tot_infil_m3_per_s, infil_method, infil_coefficients) end # Remove infiltration rates set at the space type model.getSpaceTypes.each do |space_type| space_type.spaceInfiltrationDesignFlowRates.each(&:remove) end return true end # This method retrieves the type of infiltration input # used in the model. If input is inconsistent, returns # Flow/Area # @param model [OpenStudio::Model::Model] OpenStudio model object # @return [String] infiltration input type def model_get_infiltration_method(model) infil_method = nil model.getSpaces.each do |space| # Infiltration at the space level unless space.spaceInfiltrationDesignFlowRates.empty? old_infil = space.spaceInfiltrationDesignFlowRates[0] old_infil_method = old_infil.designFlowRateCalculationMethod.to_s # Return flow per space floor area if method is inconsisten in proposed model return 'Flow/Area' if infil_method != old_infil_method && !infil_method.nil? infil_method = old_infil_method end # Infiltration at the space type level if infil_method.nil? && space.spaceType.is_initialized space_type = space.spaceType.get unless space_type.spaceInfiltrationDesignFlowRates.empty? old_infil = space_type.spaceInfiltrationDesignFlowRates[0] old_infil_method = old_infil.designFlowRateCalculationMethod.to_s # Return flow per space floor area if method is inconsisten in proposed model return 'Flow/Area' if infil_method != old_infil_method && !infil_method.nil? infil_method = old_infil_method end end end return infil_method end # This method retrieves the infiltration coefficients # used in the model. If input is inconsitent, returns # [0, 0, 0.224, 0] as per PRM user manual # @param model [OpenStudio::Model::Model] OpenStudio model object # @return [String] infiltration input type def model_get_infiltration_coefficients(model) cst = nil temp = nil vel = nil vel_2 = nil infil_coeffs = [cst, temp, vel, vel_2] model.getSpaces.each do |space| # Infiltration at the space level unless space.spaceInfiltrationDesignFlowRates.empty? old_infil = space.spaceInfiltrationDesignFlowRates[0] cst = old_infil.constantTermCoefficient temp = old_infil.temperatureTermCoefficient vel = old_infil.velocityTermCoefficient vel_2 = old_infil.velocitySquaredTermCoefficient old_infil_coeffs = [cst, temp, vel, vel_2] if !(cst.nil? && temp.nil? && vel.nil? && vel_2.nil?) # Return flow per space floor area if method is inconsisten in proposed model return [0.0, 0.0, 0.224, 0.0] if infil_coeffs != old_infil_coeffs && !(infil_coeffs[0].nil? && infil_coeffs[1].nil? && infil_coeffs[2].nil? && infil_coeffs[3].nil?) infil_coeffs = old_infil_coeffs end # Infiltration at the space type level if infil_coeffs == [nil, nil, nil, nil] && space.spaceType.is_initialized space_type = space.spaceType.get unless space_type.spaceInfiltrationDesignFlowRates.empty? old_infil = space_type.spaceInfiltrationDesignFlowRates[0] cst = old_infil.constantTermCoefficient temp = old_infil.temperatureTermCoefficient vel = old_infil.velocityTermCoefficient vel_2 = old_infil.velocitySquaredTermCoefficient old_infil_coeffs = [cst, temp, vel, vel_2] if !(cst.nil? && temp.nil? && vel.nil? && vel_2.nil?) # Return flow per space floor area if method is inconsisten in proposed model return [0.0, 0.0, 0.224, 0.0] unless infil_coeffs != old_infil_coeffs && !(infil_coeffs[0].nil? && infil_coeffs[1].nil? && infil_coeffs[2].nil? && infil_coeffs[3].nil?) infil_coeffs = old_infil_coeffs end end end return infil_coeffs end # This methods calculate the air leakage rate of a space # # @param space [OpenStudio::Model::Space] OpenStudio Space object # @return [Double] Space air leakage rate def model_get_space_air_leakage(space) space_air_leakage = 0 space_multipler = space.multiplier # Infiltration at the space level unless space.spaceInfiltrationDesignFlowRates.empty? space.spaceInfiltrationDesignFlowRates.each do |infil_obj| unless infil_obj.designFlowRate.is_initialized if infil_obj.flowperSpaceFloorArea.is_initialized space_air_leakage += infil_obj.flowperSpaceFloorArea.get * space.floorArea * space_multipler elsif infil_obj.flowperExteriorSurfaceArea.is_initialized space_air_leakage += infil_obj.flowperExteriorSurfaceArea.get * space.exteriorArea * space_multipler elsif infil_obj.flowperExteriorWallArea.is_initialized space_air_leakage += infil_obj.flowperExteriorWallArea.get * space.exteriorWallArea * space_multipler elsif infil_obj.airChangesperHour.is_initialized space_air_leakage += infil_obj.airChangesperHour.get * space.volume * space_multipler / 3600 end end end end return space_air_leakage end # This methods calculate the current model air leakage rate @ 75 Pa. # It assumes that the model follows the PRM methods, see G3.1.1.4 # in 90.1-2019 for reference. # # @param model [OpenStudio::Model::Model] OpenStudio Model object # @param building_envelope_area_m2 [Double] Building envelope area as per 90.1 in m^2 # @return [Double] building model air leakage rate def model_current_building_envelope_infiltration_at_75pa(model, building_envelope_area_m2) bldg_air_leakage_rate = 0 model.getSpaces.each do |space| bldg_air_leakage_rate += model_get_space_air_leakage(space) end # adjust_infiltration_to_prototype_building_conditions(1) corresponds # to the 0.112 shown in G3.1.1.4 curr_tot_infil_m3_per_s_per_envelope_area = bldg_air_leakage_rate / OpenstudioStandards::Infiltration.adjust_infiltration_to_prototype_building_conditions(1) / building_envelope_area_m2 return curr_tot_infil_m3_per_s_per_envelope_area end # This method calculates the building envelope infiltration, # this approach uses the 90.1 PRM rules # # @param building_envelope_area_m2 [Double] Building envelope area as per 90.1 in m^2 # @param specific_space_infiltration_rate_75_pa [Double] Specific space infiltration rate at 75 pa # @return [Double] building envelope infiltration def model_adjusted_building_envelope_infiltration(building_envelope_area_m2, specific_space_infiltration_rate_75_pa = nil) # Determine the total building baseline infiltration rate in cfm per ft2 of the building envelope at 75 Pa if specific_space_infiltration_rate_75_pa.nil? basic_infil_rate_cfm_per_ft2 = space_infiltration_rate_75_pa else basic_infil_rate_cfm_per_ft2 = specific_space_infiltration_rate_75_pa end # Conversion factor conv_fact = OpenStudio.convert(1, 'm^3/s', 'ft^3/min').to_f / OpenStudio.convert(1, 'm^2', 'ft^2').to_f # Adjust the infiltration rate to the average pressure for the prototype buildings. # adj_infil_rate_cfm_per_ft2 = 0.112 * basic_infil_rate_cfm_per_ft2 adj_infil_rate_cfm_per_ft2 = OpenstudioStandards::Infiltration.adjust_infiltration_to_prototype_building_conditions(basic_infil_rate_cfm_per_ft2) adj_infil_rate_m3_per_s_per_m2 = adj_infil_rate_cfm_per_ft2 / conv_fact # Calculate the total infiltration tot_infil_m3_per_s = adj_infil_rate_m3_per_s_per_m2 * building_envelope_area_m2 return tot_infil_m3_per_s end # Apply the standard construction to each surface in the model, based on the construction type currently assigned. # # @param model [OpenStudio::Model::Model] OpenStudio model object # @param climate_zone [String] ASHRAE climate zone, e.g. 'ASHRAE 169-2013-4A' # @param wwr_building_type [String] building type used for defining window to wall ratio, e.g. 'Office > 50,000 sq ft' # @param wwr_info [Hash] A map that maps each building area type to its correspondent wwr. # @return [Boolean] returns true if successful, false if not def model_apply_standard_constructions(model, climate_zone, wwr_building_type: nil, wwr_info: {}) types_to_modify = [] # Possible boundary conditions are # Adiabatic # Surface # Outdoors # Ground # Foundation # GroundFCfactorMethod # OtherSideCoefficients # OtherSideConditionsModel # GroundSlabPreprocessorAverage # GroundSlabPreprocessorCore # GroundSlabPreprocessorPerimeter # GroundBasementPreprocessorAverageWall # GroundBasementPreprocessorAverageFloor # GroundBasementPreprocessorUpperWall # GroundBasementPreprocessorLowerWall # Possible surface types are # Floor # Wall # RoofCeiling # FixedWindow # OperableWindow # Door # GlassDoor # OverheadDoor # Skylight # TubularDaylightDome # TubularDaylightDiffuser # Create an array of surface types types_to_modify << ['Outdoors', 'Floor'] types_to_modify << ['Outdoors', 'Wall'] types_to_modify << ['Outdoors', 'RoofCeiling'] types_to_modify << ['Outdoors', 'FixedWindow'] types_to_modify << ['Outdoors', 'OperableWindow'] types_to_modify << ['Outdoors', 'Door'] types_to_modify << ['Outdoors', 'GlassDoor'] types_to_modify << ['Outdoors', 'OverheadDoor'] types_to_modify << ['Outdoors', 'Skylight'] types_to_modify << ['Surface', 'Floor'] types_to_modify << ['Surface', 'Wall'] types_to_modify << ['Surface', 'RoofCeiling'] types_to_modify << ['Surface', 'FixedWindow'] types_to_modify << ['Surface', 'OperableWindow'] types_to_modify << ['Surface', 'Door'] types_to_modify << ['Surface', 'GlassDoor'] types_to_modify << ['Surface', 'OverheadDoor'] types_to_modify << ['Ground', 'Floor'] types_to_modify << ['Ground', 'Wall'] types_to_modify << ['Foundation', 'Wall'] types_to_modify << ['GroundFCfactorMethod', 'Wall'] types_to_modify << ['OtherSideCoefficients', 'Wall'] types_to_modify << ['OtherSideConditionsModel', 'Wall'] types_to_modify << ['GroundBasementPreprocessorAverageWall', 'Wall'] types_to_modify << ['GroundBasementPreprocessorUpperWall', 'Wall'] types_to_modify << ['GroundBasementPreprocessorLowerWall', 'Wall'] types_to_modify << ['Foundation', 'Floor'] types_to_modify << ['GroundFCfactorMethod', 'Floor'] types_to_modify << ['OtherSideCoefficients', 'Floor'] types_to_modify << ['OtherSideConditionsModel', 'Floor'] types_to_modify << ['GroundSlabPreprocessorAverage', 'Floor'] types_to_modify << ['GroundSlabPreprocessorCore', 'Floor'] types_to_modify << ['GroundSlabPreprocessorPerimeter', 'Floor'] # Find just those surfaces surfaces_to_modify = [] surface_category = {} org_surface_boundary_conditions = {} types_to_modify.each do |boundary_condition, surface_type| # Surfaces model.getSurfaces.sort.each do |surf| next unless surf.outsideBoundaryCondition == boundary_condition next unless surf.surfaceType == surface_type # Check if surface is adjacent to an unenclosed or unconditioned space (e.g. attic or parking garage) if surf.outsideBoundaryCondition == 'Surface' adj_space = surf.adjacentSurface.get.space.get adj_space_cond_type = space_conditioning_category(adj_space) if adj_space_cond_type == 'Unconditioned' # Get adjacent surface adjacent_surf = surf.adjacentSurface.get # Store original boundary condition type org_surface_boundary_conditions[surf.name.to_s] = adjacent_surf # Identify this surface as exterior surface_category[surf] = 'ExteriorSurface' # Temporary change the surface's boundary condition to 'Outdoors' so it can be assigned a baseline construction surf.setOutsideBoundaryCondition('Outdoors') adjacent_surf.setOutsideBoundaryCondition('Outdoors') end end if boundary_condition == 'Outdoors' surface_category[surf] = 'ExteriorSurface' elsif ['Ground', 'Foundation', 'GroundFCfactorMethod', 'OtherSideCoefficients', 'OtherSideConditionsModel', 'GroundSlabPreprocessorAverage', 'GroundSlabPreprocessorCore', 'GroundSlabPreprocessorPerimeter', 'GroundBasementPreprocessorAverageWall', 'GroundBasementPreprocessorAverageFloor', 'GroundBasementPreprocessorUpperWall', 'GroundBasementPreprocessorLowerWall'].include?(boundary_condition) surface_category[surf] = 'GroundSurface' else surface_category[surf] = 'NA' end surfaces_to_modify << surf end # SubSurfaces model.getSubSurfaces.sort.each do |surf| next unless surf.outsideBoundaryCondition == boundary_condition next unless surf.subSurfaceType == surface_type surface_category[surf] = 'ExteriorSubSurface' surfaces_to_modify << surf end end # Modify these surfaces prev_created_consts = {} surfaces_to_modify.sort.each do |surf| # Get space conditioning space = surf.space.get space_cond_type = space_conditioning_category(space) # Do not modify constructions for unconditioned spaces prev_created_consts = planar_surface_apply_standard_construction(surf, climate_zone, prev_created_consts, wwr_building_type, wwr_info, surface_category[surf]) unless space_cond_type == 'Unconditioned' # Reset boundary conditions to original if they were temporary modified if org_surface_boundary_conditions.include?(surf.name.to_s) surf.setAdjacentSurface(org_surface_boundary_conditions[surf.name.to_s]) end end # List the unique array of constructions if prev_created_consts.size.zero? OpenStudio.logFree(OpenStudio::Warn, 'openstudio.standards.Model', 'None of the constructions in your proposed model have both Intended Surface Type and Standards Construction Type') else prev_created_consts.each do |surf_type, construction| OpenStudio.logFree(OpenStudio::Info, 'openstudio.standards.Model', "For #{surf_type.join(' ')}, applied #{construction.name}.") end end return true end # 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-2019 # @param model [OpenStudio::Model::Model] OpenStudio model object # @return [Boolean] returns true if successful, false if not def model_apply_prm_construction_types(model) types_to_modify = [] # Possible boundary conditions are # Adiabatic # Surface # Outdoors # Ground # Foundation # GroundFCfactorMethod # OtherSideCoefficients # OtherSideConditionsModel # GroundSlabPreprocessorAverage # GroundSlabPreprocessorCore # GroundSlabPreprocessorPerimeter # GroundBasementPreprocessorAverageWall # GroundBasementPreprocessorAverageFloor # GroundBasementPreprocessorUpperWall # GroundBasementPreprocessorLowerWall # Possible surface types are # AtticFloor # AtticWall # AtticRoof # DemisingFloor # DemisingWall # DemisingRoof # ExteriorFloor # ExteriorWall # ExteriorRoof # ExteriorWindow # ExteriorDoor # GlassDoor # GroundContactFloor # GroundContactWall # GroundContactRoof # InteriorFloor # InteriorWall # InteriorCeiling # InteriorPartition # InteriorWindow # InteriorDoor # OverheadDoor # Skylight # TubularDaylightDome # TubularDaylightDiffuser # Possible standards construction types # Mass # SteelFramed # WoodFramed # IEAD # View # Daylight # Swinging # NonSwinging # Heated # Unheated # RollUp # Sliding # Metal # Nonmetal framing (all) # Metal framing (curtainwall/storefront) # Metal framing (entrance door) # Metal framing (all other) # Metal Building # Attic and Other # Glass with Curb # Plastic with Curb # Without Curb # Create an array of types types_to_modify << ['Outdoors', 'ExteriorWall', 'SteelFramed'] types_to_modify << ['Outdoors', 'ExteriorRoof', 'IEAD'] types_to_modify << ['Outdoors', 'ExteriorFloor', 'SteelFramed'] types_to_modify << ['Outdoors', 'ExteriorWindow', 'Any Vertical Glazing'] types_to_modify << ['Outdoors', 'GlassDoor', 'Any Vertical Glazing'] types_to_modify << ['Outdoors', 'ExteriorDoor', 'NonSwinging'] types_to_modify << ['Outdoors', 'ExteriorDoor', 'Swinging'] types_to_modify << ['Ground', 'GroundContactFloor', 'Unheated'] types_to_modify << ['Ground', 'GroundContactWall', 'Mass'] # Foundation types_to_modify << ['Foundation', 'GroundContactFloor', 'Unheated'] types_to_modify << ['Foundation', 'GroundContactWall', 'Mass'] # F/C-Factor methods types_to_modify << ['GroundFCfactorMethod', 'GroundContactFloor', 'Unheated'] types_to_modify << ['GroundFCfactorMethod', 'GroundContactWall', 'Mass'] # Other side coefficients types_to_modify << ['OtherSideCoefficients', 'GroundContactFloor', 'Unheated'] types_to_modify << ['OtherSideConditionsModel', 'GroundContactFloor', 'Unheated'] types_to_modify << ['OtherSideCoefficients', 'GroundContactWall', 'Mass'] types_to_modify << ['OtherSideConditionsModel', 'GroundContactWall', 'Mass'] # Slab preprocessor types_to_modify << ['GroundSlabPreprocessorAverage', 'GroundContactFloor', 'Unheated'] types_to_modify << ['GroundSlabPreprocessorCore', 'GroundContactFloor', 'Unheated'] types_to_modify << ['GroundSlabPreprocessorPerimeter', 'GroundContactFloor', 'Unheated'] # Basement preprocessor types_to_modify << ['GroundBasementPreprocessorAverageWall', 'GroundContactWall', 'Mass'] types_to_modify << ['GroundBasementPreprocessorAverageFloor', 'GroundContactFloor', 'Unheated'] types_to_modify << ['GroundBasementPreprocessorUpperWall', 'GroundContactWall', 'Mass'] types_to_modify << ['GroundBasementPreprocessorLowerWall', 'GroundContactWall', 'Mass'] # Modify all constructions of each type types_to_modify.each do |boundary_cond, surf_type, const_type| constructions = OpenstudioStandards::Constructions.model_get_constructions(model, boundary_cond, surf_type) constructions.sort.each do |const| standards_info = const.standardsInformation standards_info.setIntendedSurfaceType(surf_type) standards_info.setStandardsConstructionType(const_type) end end return true end # Reduces the SRR to the values specified by the PRM. SRR reduction will be done by shrinking vertices toward the centroid. # # @param model [OpenStudio::Model::Model] OpenStudio model object # @return [Boolean] returns true if successful, false if not def model_apply_prm_baseline_skylight_to_roof_ratio(model) # Loop through all spaces in the model, and # per the 90.1-2019 PRM User Manual, only # account for exterior roofs for enclosed # spaces. Include space multipliers. roof_m2 = 0.001 # Avoids divide by zero errors later sky_m2 = 0 total_roof_m2 = 0.001 total_subsurface_m2 = 0 model.getSpaces.sort.each do |space| next if space_conditioning_category(space) == 'Unconditioned' # Loop through all surfaces in this space roof_area_m2 = 0 sky_area_m2 = 0 space.surfaces.sort.each do |surface| # Skip non-outdoor surfaces next unless surface.outsideBoundaryCondition == 'Outdoors' # Skip non-walls next unless surface.surfaceType == 'RoofCeiling' # This roof's gross area (including skylight area) roof_area_m2 += surface.grossArea * space.multiplier # Subsurfaces in this surface surface.subSurfaces.sort.each do |ss| next unless ss.subSurfaceType == 'Skylight' sky_area_m2 += ss.netArea * space.multiplier end end total_roof_m2 += roof_area_m2 total_subsurface_m2 += sky_area_m2 end # Calculate the SRR of each category srr = ((total_subsurface_m2 / total_roof_m2) * 100.0).round(1) OpenStudio.logFree(OpenStudio::Info, 'openstudio.standards.Model', "The skylight to roof ratios (SRRs) is: : #{srr.round}%.") # SRR limit srr_lim = model_prm_skylight_to_roof_ratio_limit(model) # Check against SRR limit red = srr > srr_lim # Stop here unless skylights need reducing return true unless red OpenStudio.logFree(OpenStudio::Info, 'openstudio.standards.Model', "Reducing the size of all skylights equally down to the limit of #{srr_lim.round}%.") # Determine the factors by which to reduce the skylight area mult = srr_lim / srr # Reduce the skylight area if any of the categories necessary model.getSpaces.sort.each do |space| next if space_conditioning_category(space) == 'Unconditioned' # Loop through all surfaces in this space space.surfaces.sort.each do |surface| # Skip non-outdoor surfaces next unless surface.outsideBoundaryCondition == 'Outdoors' # Skip non-walls next unless surface.surfaceType == 'RoofCeiling' # Subsurfaces in this surface surface.subSurfaces.sort.each do |ss| next unless ss.subSurfaceType == 'Skylight' # Reduce the size of the skylight red = 1.0 - mult OpenstudioStandards::Geometry.sub_surface_reduce_area_by_percent_by_shrinking_toward_centroid(ss, red) end end end return true end # Apply baseline values to exterior lights objects # Characterization of objects must be done via user data # # @param model [OpenStudio::Model::Model] OpenStudio model object def model_apply_baseline_exterior_lighting(model) model.getExteriorLightss.each do |ext_lights_obj| # Update existing exterior lights object: control, schedule, power ext_lights_obj.setControlOption('AstronomicalClock') ext_lights_obj.setSchedule(model.alwaysOnDiscreteSchedule) ext_lights_obj.setMultiplier(1) ext_lights_def = ext_lights_obj.exteriorLightsDefinition ext_ltg_pwr = get_additional_property_as_double(ext_lights_obj, 'design_level', 0.0) if ext_ltg_pwr > 0.0 ext_lights_def.setDesignLevel(ext_ltg_pwr) end end end # Function to add baseline elevators based on user data # @param model [OpenStudio::Model::Model] OpenStudio model object def model_add_prm_elevators(model) # Load elevator data from userdata csv files equipment_array = model.getElectricEquipments + model.getExteriorFuelEquipments equipment_array.each do |equipment| elevator_number_of_lifts = get_additional_property_as_integer(equipment, 'elevator_number_of_lifts', 0) next unless elevator_number_of_lifts > 0.0 elevator_name = equipment.name.get elevator_number_of_stories = get_additional_property_as_integer(equipment, 'elevator_number_of_stories', 0) elevator_weight_of_car = get_additional_property_as_double(equipment, 'elevator_weight_of_car', 0.0) elevator_rated_load = get_additional_property_as_double(equipment, 'elevator_rated_load', 0.0) elevator_speed_of_car = get_additional_property_as_double(equipment, 'elevator_speed_of_car', 0.0) elevator_counter_weight_of_car = get_additional_property_as_double(equipment, 'elevator_counter_weight_of_car', 0.0) if elevator_number_of_stories < 5 # From Table G3.9.2 performance rating method baseline elevator motor elevator_mech_eff = 0.58 elevator_counter_weight_of_car = 0.0 search_criteria = { 'template' => template, 'type' => 'Hydraulic' } else # From Table G3.9.2 performance rating method baseline elevator motor elevator_mech_eff = 0.64 # Determine the elevator counterweight if elevator_counter_weight_of_car == 0.0 # When the proposed design counterweight is not specified # it is determined as per Table G3.9.2 elevator_counter_weight_of_car = elevator_weight_of_car + 0.4 * elevator_rated_load end search_criteria = { 'template' => template, 'type' => 'Any' } end elevator_motor_bhp = (elevator_weight_of_car + elevator_rated_load - elevator_counter_weight_of_car) * elevator_speed_of_car / (33000 * elevator_mech_eff) # Lookup the minimum motor efficiency elevator_motor_eff = standards_data['motors'] motor_properties = model_find_object(elevator_motor_eff, search_criteria, nil, nil, nil, nil, elevator_motor_bhp) if motor_properties.nil? OpenStudio.logFree(OpenStudio::Error, 'prm.log', "For #{elevator_name}, could not find motor properties using search criteria: #{search_criteria}, motor_bhp = #{elevator_motor_bhp} hp.") return false end nominal_hp = motor_properties['maximum_capacity'].to_f.round(1) # Round to nearest whole HP for niceness if nominal_hp >= 2 nominal_hp = nominal_hp.round end # Get the efficiency based on the nominal horsepower # Add 0.01 hp to avoid search errors. motor_properties = model_find_object(elevator_motor_eff, search_criteria, nil, nil, nil, nil, nominal_hp + 0.01) if motor_properties.nil? OpenStudio.logFree(OpenStudio::Error, 'prm.log', "For #{elevator_name}, could not find nominal motor properties using search criteria: #{search_criteria}, motor_hp = #{nominal_hp} hp.") return false end motor_eff = motor_properties['nominal_full_load_efficiency'].to_f elevator_power = elevator_number_of_lifts * elevator_motor_bhp * 746 / motor_eff if equipment.is_a?(OpenStudio::Model::ElectricEquipment) equipment.electricEquipmentDefinition.setDesignLevel(elevator_power) else equipment.exteriorFuelEquipmentDefinition.setDesignLevel(elevator_power) end elevator_space = prm_get_optional_handler(equipment, @sizing_run_dir, 'space') # Add ventilation and lighting process loads if modeled in the proposed model misc_elevator_process_loads = 0.0 misc_elevator_process_loads += get_additional_property_as_double(equipment, 'elevator_ventilation_cfm', 0.0) * 0.33 misc_elevator_process_loads += get_additional_property_as_double(equipment, 'elevator_area_ft2', 0.0) * 3.14 if misc_elevator_process_loads > 0 misc_elevator_process_loads_def = OpenStudio::Model::ElectricEquipmentDefinition.new(model) misc_elevator_process_loads_def.setName("#{elevator_name} - Misc Process Loads - Def") misc_elevator_process_loads_def.setDesignLevel(misc_elevator_process_loads) misc_elevator_process_loads = OpenStudio::Model::ElectricEquipment.new(misc_elevator_process_loads_def) misc_elevator_process_loads.setName("#{elevator_name} - Misc Process Loads") misc_elevator_process_loads.setEndUseSubcategory('Elevators') misc_elevator_process_loads.setSchedule(model.alwaysOnDiscreteSchedule) misc_elevator_process_loads.setSpace(elevator_space) end end end # Add design day schedule objects for space loads, for PRM 2019 baseline models # @author Xuechen (Jerry) Lei, PNNL # @param model [OpenStudio::Model::Model] OpenStudio model object def model_apply_prm_baseline_sizing_schedule(model) space_loads = model.getSpaceLoads loads = [] space_loads.sort.each do |space_load| casted_load = model_cast_model_object(space_load) loads << casted_load unless casted_load.nil? end load_schedule_name_hash = { 'People' => 'numberofPeopleSchedule', 'Lights' => 'schedule', 'ElectricEquipment' => 'schedule', 'GasEquipment' => 'schedule', 'SpaceInfiltration_DesignFlowRate' => 'schedule' } loads.each do |load| load_type = load.iddObjectType.valueName.sub('OS_', '').strip load_schedule_name = load_schedule_name_hash[load_type] next unless !load_schedule_name.nil? # check if the load is in a dwelling space if load.spaceType.is_initialized space_type = load.spaceType.get elsif load.space.is_initialized && load.space.get.spaceType.is_initialized space_type = load.space.get.spaceType.get else space_type = nil puts "No hosting space/spacetype found for load: #{load.name}" end if !space_type.nil? && /apartment/i =~ space_type.standardsSpaceType.to_s load_in_dwelling = true else load_in_dwelling = false end load_schedule = load.public_send(load_schedule_name).get schedule_type = load_schedule.iddObjectType.valueName.sub('OS_', '').strip.sub('_', '') load_schedule = load_schedule.public_send("to_#{schedule_type}").get case schedule_type when 'ScheduleRuleset' load_schmax = OpenstudioStandards::Schedules.schedule_get_min_max(load_schedule)['max'] load_schmin = OpenstudioStandards::Schedules.schedule_get_min_max(load_schedule)['min'] load_schmode = get_weekday_values_from_8760(model, Array(OpenstudioStandards::Schedules.schedule_get_hourly_values(load_schedule)), value_includes_holiday = true).mode[0] # AppendixG-2019 G3.1.2.2.1 if load_type == 'SpaceInfiltration_DesignFlowRate' summer_value = load_schmax winter_value = load_schmax else summer_value = load_schmax winter_value = load_schmin end # AppendixG-2019 Exception to G3.1.2.2.1 if load_in_dwelling summer_value = load_schmode end # set cooling design day schedule summer_dd_schedule = OpenStudio::Model::ScheduleDay.new(model) summer_dd_schedule.setName("#{load.name} Summer Design Day") summer_dd_schedule.addValue(OpenStudio::Time.new(1.0), summer_value) load_schedule.setSummerDesignDaySchedule(summer_dd_schedule) # set heating design day schedule winter_dd_schedule = OpenStudio::Model::ScheduleDay.new(model) winter_dd_schedule.setName("#{load.name} Winter Design Day") winter_dd_schedule.addValue(OpenStudio::Time.new(1.0), winter_value) load_schedule.setWinterDesignDaySchedule(winter_dd_schedule) when 'ScheduleConstant' OpenStudio.logFree(OpenStudio::Warn, 'openstudio.standards.Model', "Space load #{load.name} has schedule type of ScheduleConstant. Nothing to be done for ScheduleConstant") next end end end # Applies the multi-zone VAV outdoor air sizing requirements to all applicable air loops in the model. # @note This is not applicable to the stable baseline; hence no action in this method # # @param model [OpenStudio::Model::Model] OpenStudio model object # @return [Boolean] returns true if successful, false if not def model_apply_multizone_vav_outdoor_air_sizing(model) return true end # Identifies non mechanically cooled ("nmc") systems, if applicable # and add a flag to the zone's and air loop's additional properties. # @todo Zone-level evaporative cooler is not currently supported # by OpenStudio, will need to be added to the method when supported. # # @param model [OpenStudio::Model::Model] OpenStudio model object # @return [Hash] Zone to nmc system type mapping def model_identify_non_mechanically_cooled_systems(model) # Iterate through zones to find out if they are served by nmc systems model.getThermalZones.each do |zone| # Check if airloop has economizer and either: # - No cooling coil and/or, # - An evaporative cooling coil zone.airLoopHVACs.each do |air_loop| if (!air_loop_hvac_include_cooling_coil?(air_loop) && air_loop_hvac_include_evaporative_cooler?(air_loop)) || (!air_loop_hvac_include_cooling_coil?(air_loop) && air_loop_hvac_include_economizer?(air_loop)) air_loop.additionalProperties.setFeature('non_mechanically_cooled', true) zone.additionalProperties.setFeature('non_mechanically_cooled', true) end end end end # Specify supply air temperature setpoint for unit heaters based on 90.1 Appendix G G3.1.2.8.2 # # @param thermal_zone [OpenStudio::Model::ThermalZone] OpenStudio ThermalZone Object # @return [Double] for zone with unit heaters, return design supply temperature; otherwise, return nil def thermal_zone_prm_unitheater_design_supply_temperature(thermal_zone) thermal_zone.equipment.each do |eqt| if eqt.to_ZoneHVACUnitHeater.is_initialized return OpenStudio.convert(105, 'F', 'C').get end end return nil end # Specify supply to room delta for laboratory spaces based on 90.1 Appendix G Exception to G3.1.2.8.1 # # @param thermal_zone [OpenStudio::Model::ThermalZone] OpenStudio ThermalZone Object # @return [Double] for zone with laboratory space, return 17; otherwise, return nil def thermal_zone_prm_lab_delta_t(thermal_zone) # For labs, add 17 delta-T; otherwise, add 20 delta-T thermal_zone.spaces.each do |space| space_std_type = space.spaceType.get.standardsSpaceType.get if space_std_type == 'laboratory' return 17 end end return nil end # Indicate if fan power breakdown (supply, return, and relief) # are needed # # @return [Boolean] true if necessary, false otherwise def model_get_fan_power_breakdown return true end # Applies the HVAC parts of the template to all objects in the model using the the template specified in the model. # # @param model [OpenStudio::Model::Model] OpenStudio model object # @param climate_zone [String] ASHRAE climate zone, e.g. 'ASHRAE 169-2013-4A' # @param apply_controls [Boolean] toggle whether to apply air loop and plant loop controls # @param sql_db_vars_map [Hash] hash map # @return [Boolean] returns true if successful, false if not def model_apply_hvac_efficiency_standard(model, climate_zone, apply_controls: true, sql_db_vars_map: nil) sql_db_vars_map = {} if sql_db_vars_map.nil? OpenStudio.logFree(OpenStudio::Info, 'openstudio.standards.Model', "Started applying HVAC efficiency standards for #{template} template.") # Air Loop Controls if apply_controls.nil? || apply_controls == true model.getAirLoopHVACs.sort.each { |obj| air_loop_hvac_apply_standard_controls(obj, climate_zone) } end # Plant Loop Controls if apply_controls.nil? || apply_controls == true model.getPlantLoops.sort.each { |obj| plant_loop_apply_standard_controls(obj, climate_zone) } end # Zone HVAC Controls model.getZoneHVACComponents.sort.each { |obj| zone_hvac_component_apply_standard_controls(obj) } # @todo The fan and pump efficiency will be done by another task. # Fans # model.getFanVariableVolumes.sort.each { |obj| fan_apply_standard_minimum_motor_efficiency(obj, fan_brake_horsepower(obj)) } # model.getFanConstantVolumes.sort.each { |obj| fan_apply_standard_minimum_motor_efficiency(obj, fan_brake_horsepower(obj)) } # model.getFanOnOffs.sort.each { |obj| fan_apply_standard_minimum_motor_efficiency(obj, fan_brake_horsepower(obj)) } # model.getFanZoneExhausts.sort.each { |obj| fan_apply_standard_minimum_motor_efficiency(obj, fan_brake_horsepower(obj)) } # Pumps # model.getPumpConstantSpeeds.sort.each { |obj| pump_apply_standard_minimum_motor_efficiency(obj) } # model.getPumpVariableSpeeds.sort.each { |obj| pump_apply_standard_minimum_motor_efficiency(obj) } # model.getHeaderedPumpsConstantSpeeds.sort.each { |obj| pump_apply_standard_minimum_motor_efficiency(obj) } # model.getHeaderedPumpsVariableSpeeds.sort.each { |obj| pump_apply_standard_minimum_motor_efficiency(obj) } # Zone level systems/components model.getThermalZones.each do |zone| if zone.additionalProperties.getFeatureAsString('baseline_system_type').is_initialized sys_type = zone.additionalProperties.getFeatureAsString('baseline_system_type').get end zone.equipment.each do |zone_equipment| if zone_equipment.to_ZoneHVACPackagedTerminalAirConditioner.is_initialized ptac = zone_equipment.to_ZoneHVACPackagedTerminalAirConditioner.get cooling_coil = ptac.coolingCoil sql_db_vars_map = set_coil_cooling_efficiency_and_curves(cooling_coil, sql_db_vars_map, sys_type) elsif zone_equipment.to_ZoneHVACPackagedTerminalHeatPump.is_initialized pthp = zone_equipment.to_ZoneHVACPackagedTerminalHeatPump.get cooling_coil = pthp.coolingCoil heating_coil = pthp.heatingCoil sql_db_vars_map = set_coil_cooling_efficiency_and_curves(cooling_coil, sql_db_vars_map, sys_type) sql_db_vars_map = set_coil_heating_efficiency_and_curves(heating_coil, sql_db_vars_map, sys_type) elsif zone_equipment.to_ZoneHVACUnitHeater.is_initialized unit_heater = zone_equipment.to_ZoneHVACUnitHeater.get heating_coil = unit_heater.heatingCoil sql_db_vars_map = set_coil_heating_efficiency_and_curves(heating_coil, sql_db_vars_map, sys_type) end end end # Airloop HVAC level components model.getAirLoopHVACs.sort.each do |air_loop| sys_type = air_loop.additionalProperties.getFeatureAsString('baseline_system_type').get air_loop.components.each do |icomponent| if icomponent.to_AirLoopHVACUnitarySystem.is_initialized unitary_system = icomponent.to_AirLoopHVACUnitarySystem.get if unitary_system.coolingCoil.is_initialized cooling_coil = unitary_system.coolingCoil.get sql_db_vars_map = set_coil_cooling_efficiency_and_curves(cooling_coil, sql_db_vars_map, sys_type) end if unitary_system.heatingCoil.is_initialized heating_coil = unitary_system.heatingCoil.get sql_db_vars_map = set_coil_heating_efficiency_and_curves(heating_coil, sql_db_vars_map, sys_type) end elsif icomponent.to_CoilCoolingDXSingleSpeed.is_initialized cooling_coil = icomponent.to_CoilCoolingDXSingleSpeed.get sql_db_vars_map = coil_cooling_dx_single_speed_apply_efficiency_and_curves(cooling_coil, sql_db_vars_map, sys_type) elsif icomponent.to_CoilCoolingDXTwoSpeed.is_initialized cooling_coil = icomponent.to_CoilCoolingDXTwoSpeed.get sql_db_vars_map = coil_cooling_dx_two_speed_apply_efficiency_and_curves(cooling_coil, sql_db_vars_map, sys_type) elsif icomponent.to_CoilHeatingDXSingleSpeed.is_initialized heating_coil = icomponent.to_CoilHeatingDXSingleSpeed.get sql_db_vars_map = coil_heating_dx_single_speed_apply_efficiency_and_curves(heating_coil, sql_db_vars_map, sys_type) elsif icomponent.to_CoilHeatingGas.is_initialized heating_coil = icomponent.to_CoilHeatingGas.get sql_db_vars_map = coil_heating_gas_apply_efficiency_and_curves(heating_coil, sql_db_vars_map, sys_type) end end end # Chillers model.getChillerElectricEIRs.sort.each { |obj| chiller_electric_eir_apply_efficiency_and_curves(obj) } # Boilers model.getBoilerHotWaters.sort.each { |obj| boiler_hot_water_apply_efficiency_and_curves(obj) } # Cooling Towers model.getCoolingTowerVariableSpeeds.sort.each { |obj| cooling_tower_variable_speed_apply_efficiency_and_curves(obj) } OpenStudio.logFree(OpenStudio::Info, 'openstudio.standards.Model', "Finished applying HVAC efficiency standards for #{template} template.") return true end # This function returns the cooling dx coil efficiency and curve coefficient in a Hashmap. # # @param cooling_coil [OpenStudio::Model::ModeObject] # @param sql_db_vars_map [Hash] hash map # @param sys_type [String] baseline system type string # @return [Hash] sql_db_vars_map def set_coil_cooling_efficiency_and_curves(cooling_coil, sql_db_vars_map, sys_type) if cooling_coil.to_CoilCoolingDXSingleSpeed.is_initialized # single speed coil sql_db_vars_map = coil_cooling_dx_single_speed_apply_efficiency_and_curves(cooling_coil.to_CoilCoolingDXSingleSpeed.get, sql_db_vars_map, sys_type) elsif cooling_coil.to_CoilCoolingDXTwoSpeed.is_initialized # two speed coil sql_db_vars_map = coil_cooling_dx_two_speed_apply_efficiency_and_curves(cooling_coil.to_CoilCoolingDXTwoSpeed.get, sql_db_vars_map, sys_type) else OpenStudio.logFree(OpenStudio::Warn, 'openstudio.standards.Model', "#{cooling_coil.name} is not single speed or two speed DX cooling coil. Nothing to be done for efficiency") end return sql_db_vars_map end # This function returns the heating dx coil efficiency and curve coefficient in a Hashmap. # # @param heating_coil [OpenStudio::Model::ModeObject] # @param sql_db_vars_map [Hash] hash map # @param sys_type [String] baseline system type string # @return [Hash] the hashmap contains the heating efficiency and curve coefficient for the heating_coil def set_coil_heating_efficiency_and_curves(heating_coil, sql_db_vars_map, sys_type) if heating_coil.to_CoilHeatingDXSingleSpeed.is_initialized # single speed coil sql_db_vars_map = coil_heating_dx_single_speed_apply_efficiency_and_curves(heating_coil.to_CoilHeatingDXSingleSpeed.get, sql_db_vars_map, sys_type) elsif heating_coil.to_CoilHeatingGas.is_initialized # single speed coil sql_db_vars_map = coil_heating_gas_apply_efficiency_and_curves(heating_coil.to_CoilHeatingGas.get, sql_db_vars_map, sys_type) else OpenStudio.logFree(OpenStudio::Warn, 'openstudio.standards.Model', "#{heating_coil.name} is not single speed DX heating coil. Nothing to be done for efficiency") end return sql_db_vars_map end # Template method for evaluate DCV requirements in the user model # # @param model [OpenStudio::Model::Model] OpenStudio model # @return [Boolean] returns true if successful, false if not def model_evaluate_dcv_requirements(model) model_mark_zone_dcv_existence(model) model_add_dcv_user_exception_properties(model) model_add_dcv_requirement_properties(model) model_add_apxg_dcv_properties(model) model_raise_user_model_dcv_errors(model) return true end # Template method for adding a setpoint manager for a coil control logic to a heating coil. # ASHRAE 90.1-2019 Appendix G. # # @param model [OpenStudio::Model::Model] OpenStudio model # @param thermal_zones Array([OpenStudio::Model::ThermalZone]) thermal zone array # @param coil [OpenStudio::Model::StraightComponent] heating coil # @return [Boolean] returns true if successful, false if not def model_set_central_preheat_coil_spm(model, thermal_zones, coil) # search for the highest zone setpoint temperature max_heat_setpoint = 0.0 coil_name = coil.name.get.to_s thermal_zones.each do |zone| tstat = zone.thermostatSetpointDualSetpoint if tstat.is_initialized tstat = tstat.get setpoint_sch = tstat.heatingSetpointTemperatureSchedule setpoint_c = OpenstudioStandards::Schedules.schedule_get_design_day_min_max(setpoint_sch.get, 'winter')['max'] next if setpoint_c.nil? if setpoint_c > max_heat_setpoint max_heat_setpoint = setpoint_c end end end # in this situation, we hard set the temperature to be 22 F # (ASHRAE 90.1 Room heating stepoint temperature is 72 F) max_heat_setpoint = 22.2 if max_heat_setpoint == 0.0 max_heat_setpoint_f = OpenStudio.convert(max_heat_setpoint, 'C', 'F').get preheat_setpoint_f = max_heat_setpoint_f - 20 preheat_setpoint_c = OpenStudio.convert(preheat_setpoint_f, 'F', 'C').get # create a new constant schedule and this method will add schedule limit type preheat_coil_sch = OpenstudioStandards::Schedules.create_constant_schedule_ruleset(model, preheat_setpoint_c, name: "#{coil_name} Setpoint Temp - #{preheat_setpoint_f.round}F", schedule_type_limit: 'Temperature') preheat_coil_manager = OpenStudio::Model::SetpointManagerScheduled.new(model, preheat_coil_sch) preheat_coil_manager.setName("#{coil_name} Preheat Coil Setpoint Manager") if coil.to_CoilHeatingWater.is_initialized preheat_coil_manager.addToNode(coil.airOutletModelObject.get.to_Node.get) elsif coil.to_CoilHeatingElectric.is_initialized preheat_coil_manager.addToNode(coil.outletModelObject.get.to_Node.get) elsif coil.to_CoilHeatingGas.is_initialized OpenStudio.logFree(OpenStudio::Warn, 'openstudio.models.CoilHeatingGas', 'Preheat coils in baseline system shall only be electric or hydronic. Current coil type: Natural Gas') preheat_coil_manager.addToNode(coil.airOutletModelObject.get.to_Node.get) end return true end # Add zone additional property "zone DCV implemented in user model": # - 'true' if zone OA flow requirement is specified as per person & airloop supporting this zone has DCV enabled # - 'false' otherwise # # @author Xuechen (Jerry) Lei, PNNL # @param model [OpenStudio::Model::Model] OpenStudio model # @return [Boolean] returns true if successful, false if not def model_mark_zone_dcv_existence(model) model.getAirLoopHVACs.each do |air_loop_hvac| next unless air_loop_hvac.airLoopHVACOutdoorAirSystem.is_initialized oa_system = air_loop_hvac.airLoopHVACOutdoorAirSystem.get controller_oa = oa_system.getControllerOutdoorAir controller_mv = controller_oa.controllerMechanicalVentilation next unless controller_mv.demandControlledVentilation == true air_loop_hvac.thermalZones.each do |thermal_zone| zone_dcv = false thermal_zone.spaces.each do |space| dsn_oa = space.designSpecificationOutdoorAir next if dsn_oa.empty? dsn_oa = dsn_oa.get next if dsn_oa.outdoorAirMethod == 'Maximum' if dsn_oa.outdoorAirFlowperPerson > 0 # only in this case the thermal zone is considered to be implemented with DCV zone_dcv = true end end if zone_dcv thermal_zone.additionalProperties.setFeature('zone DCV implemented in user model', true) end end end # mark unmarked zones model.getThermalZones.each do |zone| next if zone.additionalProperties.hasFeature('zone DCV implemented in user model') zone.additionalProperties.setFeature('zone DCV implemented in user model', false) end return true end # read user data and add to zone additional properties # "airloop user specified DCV exception" # "one user specified DCV exception" # # @author Xuechen (Jerry) Lei, PNNL # @param model [OpenStudio::Model::Model] OpenStudio model def model_add_dcv_user_exception_properties(model) model.getAirLoopHVACs.each do |air_loop_hvac| dcv_airloop_user_exception = false if standards_data.key?('userdata_airloop_hvac') standards_data['userdata_airloop_hvac'].each do |row| next unless row['name'].to_s.downcase.strip == air_loop_hvac.name.to_s.downcase.strip if row['dcv_exception_airloop'].to_s.upcase.strip == 'TRUE' dcv_airloop_user_exception = true break end end end air_loop_hvac.thermalZones.each do |thermal_zone| if dcv_airloop_user_exception thermal_zone.additionalProperties.setFeature('airloop user specified DCV exception', true) end end end # zone level exception tagging is put outside of airloop because it directly reads from user data and # a zone not under an airloop in user model may be in an airloop in baseline model.getThermalZones.each do |thermal_zone| dcv_zone_user_exception = false if standards_data.key?('userdata_thermal_zone') standards_data['userdata_thermal_zone'].each do |row| next unless row['name'].to_s.downcase.strip == thermal_zone.name.to_s.downcase.strip if row['dcv_exception_thermal_zone'].to_s.upcase.strip == 'TRUE' dcv_zone_user_exception = true break end end end if dcv_zone_user_exception thermal_zone.additionalProperties.setFeature('zone user specified DCV exception', true) end end # mark unmarked zones model.getThermalZones.each do |zone| next if zone.additionalProperties.hasFeature('airloop user specified DCV exception') zone.additionalProperties.setFeature('airloop user specified DCV exception', false) end model.getThermalZones.each do |zone| next if zone.additionalProperties.hasFeature('zone user specified DCV exception') zone.additionalProperties.setFeature('zone user specified DCV exception', false) end end # add zone additional property "airloop dcv required by 901" # - "true" if the airloop supporting this zone is required by 90.1 (non-exception requirement + user provided exception flag) to have DCV regarding user model # - "false" otherwise # add zone additional property "zone dcv required by 901" # - "true" if the zone is required by 90.1(non-exception requirement + user provided exception flag) to have DCV regarding user model # - 'flase' otherwise # # @author Xuechen (Jerry) Lei, PNNL # @param model [OpenStudio::Model::Model] OpenStudio model def model_add_dcv_requirement_properties(model) model.getAirLoopHVACs.each do |air_loop_hvac| if user_model_air_loop_hvac_demand_control_ventilation_required?(air_loop_hvac) air_loop_hvac.thermalZones.each do |thermal_zone| thermal_zone.additionalProperties.setFeature('airloop dcv required by 901', true) # the zone level dcv requirement can only be true if it is in an airloop that is required to have DCV if user_model_zone_demand_control_ventilation_required?(thermal_zone) thermal_zone.additionalProperties.setFeature('zone dcv required by 901', true) end end end end # mark unmarked zones model.getThermalZones.each do |zone| next if zone.additionalProperties.hasFeature('airloop dcv required by 901') zone.additionalProperties.setFeature('airloop dcv required by 901', false) end model.getThermalZones.each do |zone| next if zone.additionalProperties.hasFeature('zone dcv required by 901') zone.additionalProperties.setFeature('zone dcv required by 901', false) end end # based on previously added flag, raise error if DCV is required but not implemented in zones, in which case # baseline generation will be terminated; raise warning if DCV is not required but implemented, and continue baseline # generation # # @author Xuechen (Jerry) Lei, PNNL # @param model [OpenStudio::Model::Model] OpenStudio model # @todo JXL add log msgs to PRM logger def model_raise_user_model_dcv_errors(model) model.getThermalZones.each do |thermal_zone| if thermal_zone.additionalProperties.getFeatureAsBoolean('zone DCV implemented in user model').get && (!thermal_zone.additionalProperties.getFeatureAsBoolean('zone dcv required by 901').get || !thermal_zone.additionalProperties.getFeatureAsBoolean('airloop dcv required by 901').get) OpenStudio.logFree(OpenStudio::Warn, 'openstudio.standards.Model', "For thermal zone #{thermal_zone.name}, ASHRAE 90.1 2019 6.4.3.8 does NOT require this zone to have demand control ventilation, but it was implemented in the user model, Appendix G baseline generation will continue!") if thermal_zone.additionalProperties.hasFeature('apxg no need to have DCV') if !thermal_zone.additionalProperties.getFeatureAsBoolean('apxg no need to have DCV').get OpenStudio.logFree(OpenStudio::Warn, 'openstudio.standards.Model', "Moreover, for thermal zone #{thermal_zone.name}, Appendix G baseline model will have DCV based on ASHRAE 90.1 2019 G3.1.2.5") end end end if thermal_zone.additionalProperties.getFeatureAsBoolean('zone dcv required by 901').get && thermal_zone.additionalProperties.getFeatureAsBoolean('airloop dcv required by 901').get && !thermal_zone.additionalProperties.getFeatureAsBoolean('zone DCV implemented in user model').get OpenStudio.logFree(OpenStudio::Error, 'openstudio.standards.Model', "For thermal zone #{thermal_zone.name}, ASHRAE 90.1 2019 6.4.3.8 requires this zone to have demand control ventilation, but it was not implemented in the user model, Appendix G baseline generation should be terminated!") end end end # Check if zones in the baseline model (to be created) should have DCV based on 90.1 2019 G3.1.2.5. Zone additional # property 'apxg no need to have DCV' added # # @author Xuechen (Jerry) Lei, PNNL # @param model [OpenStudio::Model::Model] OpenStudio model def model_add_apxg_dcv_properties(model) model.getAirLoopHVACs.each do |air_loop_hvac| if air_loop_hvac.airLoopHVACOutdoorAirSystem.is_initialized oa_flow_m3_per_s = get_airloop_hvac_design_oa_from_sql(air_loop_hvac) else OpenStudio.logFree(OpenStudio::Info, 'openstudio.standards.AirLoopHVAC', "For #{air_loop_hvac.name}, DCV not applicable because it has no OA intake.") return false end oa_flow_cfm = OpenStudio.convert(oa_flow_m3_per_s, 'm^3/s', 'cfm').get if oa_flow_cfm <= 3000 air_loop_hvac.thermalZones.each do |thermal_zone| thermal_zone.additionalProperties.setFeature('apxg no need to have DCV', true) end else # oa_flow_cfg > 3000, check zone people density air_loop_hvac.thermalZones.each do |thermal_zone| area_served_m2 = 0 num_people = 0 thermal_zone.spaces.each do |space| area_served_m2 += space.floorArea num_people += space.numberOfPeople end area_served_ft2 = OpenStudio.convert(area_served_m2, 'm^2', 'ft^2').get occ_per_1000_ft2 = num_people / area_served_ft2 * 1000 if occ_per_1000_ft2 <= 100 thermal_zone.additionalProperties.setFeature('apxg no need to have DCV', true) else thermal_zone.additionalProperties.setFeature('apxg no need to have DCV', false) end end end end # if a zone does not have this additional property, it means it was not served by airloop. end # Set DCV in baseline HVAC system if required # # @author Xuechen (Jerry) Lei, PNNL # @param model [OpenStudio::Model::Model] OpenStudio model def model_set_baseline_demand_control_ventilation(model, climate_zone) model.getAirLoopHVACs.each do |air_loop_hvac| if baseline_air_loop_hvac_demand_control_ventilation_required?(air_loop_hvac) air_loop_hvac_enable_demand_control_ventilation(air_loop_hvac, climate_zone) air_loop_hvac.thermalZones.sort.each do |zone| unless baseline_thermal_zone_demand_control_ventilation_required?(zone) OpenstudioStandards::ThermalZone.thermal_zone_convert_outdoor_air_to_per_area(zone) end end end end end # A template method that handles the loading of user input data from multiple sources # include data source from: # 1. user data csv files # 2. data from measure and OpenStudio interface # @param [OpenStudio:model:Model] model # @param [String] climate_zone # @param [String] default_hvac_building_type # @param [String] default_wwr_building_type # @param [String] default_swh_building_type # @param [Hash] bldg_type_hvac_zone_hash A hash maps building type for hvac to a list of thermal zones # @return [Boolean] returns true if successful, false if not def handle_user_input_data(model, climate_zone, sizing_run_dir, default_hvac_building_type, default_wwr_building_type, default_swh_building_type, bldg_type_hvac_zone_hash) # Set sizing run directory @sizing_run_dir = sizing_run_dir # load the multiple building area types from user data handle_multi_building_area_types(model, climate_zone, default_hvac_building_type, default_wwr_building_type, default_swh_building_type, bldg_type_hvac_zone_hash) # load user data from proposed model handle_airloop_user_input_data(model) # exterior lighting handler handle_exterior_lighting_user_input_data(model) # load lights data from user data handle_lights_user_input_data(model) # load OA data from user data handle_outdoor_air_user_input_data(model) # load air loop DOAS user data from the proposed model handle_airloop_doas_user_input_data(model) # load zone HVAC user data from proposed model handle_zone_hvac_user_input_data(model) # load thermal zone user data from proposed model handle_thermal_zone_user_input_data(model) # load electric equipment user data handle_electric_equipment_user_input_data(model) # load gas equipment user data handle_gas_equipment_user_input_data(model) # load water use connection user data handle_wateruse_connections_user_input_data(model) # load water use equipment user data handle_wateruse_equipment_user_input_data(model, default_swh_building_type) # load water use equipment definition user data handle_wateruse_equipment_definition_user_input_data(model) return true end # A function to load lights from user data csv files # The file name is userdata_lights.csv # @param [OpenStudio::Model::Model] model def handle_lights_user_input_data(model) user_lights = get_userdata(UserDataFiles::LIGHTS) model.getLightss.each do |light| if user_lights user_data_updated = false user_lights.each do |user_light| next unless UserData.compare(light.name.get, user_light['name']) has_retail_display_exception = prm_read_user_data(user_light, 'has_retail_display_exception', false) if has_retail_display_exception light.additionalProperties.setFeature('has_retail_display_exception', true) else light.additionalProperties.setFeature('has_retail_display_exception', false) end has_unregulated_exception = prm_read_user_data(user_light, 'has_unregulated_exception', false) if has_unregulated_exception light.additionalProperties.setFeature('has_unregulated_exception', true) else light.additionalProperties.setFeature('has_unregulated_exception', false) end unregulated_category = prm_read_user_data(user_light, 'unregulated_category') if unregulated_category light.additionalProperties.setFeature('unregulated_category', unregulated_category) end user_data_updated = true end unless user_data_updated OpenStudio.logFree(OpenStudio::Info, 'prm.log', "WaterUseConnections name #{light.name.get} was not found in user data file: #{UserDataFiles::LIGHTS}; No user data applied.") end end end end # A function to load water use equipment definition from user data csv files # The file name is userdata_wateruse_equipment_definition.csv # @param [OpenStudio::Model::Model] model def handle_wateruse_equipment_definition_user_input_data(model) user_data_wateruse_equipment_definition = get_userdata(UserDataFiles::WATERUSE_EQUIPMENT_DEFINITION) model.getWaterUseEquipmentDefinitions.each do |wateruse_equipment| if user_data_wateruse_equipment_definition user_data_updated = false user_data_wateruse_equipment_definition.each do |user_wateruse| next unless UserData.compare(wateruse_equipment.name.get, user_wateruse['name']) peak_flow_rate = prm_read_user_data(user_wateruse, 'peak_flow_rate', nil) if peak_flow_rate wateruse_equipment.additionalProperties.setFeature('peak_flow_rate', peak_flow_rate) end flow_rate_fraction_schedule_name = prm_read_user_data(user_wateruse, 'flow_rate_fraction_schedule', '') # verify the schedule exist in the model prm_raise(model.getScheduleRulesetByName(flow_rate_fraction_schedule_name) || model.getScheduleCompactByName(flow_rate_fraction_schedule_name) || model.getScheduleConstantByName(flow_rate_fraction_schedule_name), @sizing_run_dir, "Cannot find #{flow_rate_fraction_schedule_name} in the model. Note, such schedule shall be one of the following type: RuleSet, Compact and Constant") wateruse_equipment.additionalProperties.setFeature('flow_rate_fraction_schedule', flow_rate_fraction_schedule_name) target_temperature_schedule_name = prm_read_user_data(user_wateruse, 'target_temperature_schedule', '') # verify the schedule exist in the model prm_raise(model.getScheduleRulesetByName(target_temperature_schedule_name) || model.getScheduleCompactByName(target_temperature_schedule_name) || model.getScheduleConstantByName(target_temperature_schedule_name), @sizing_run_dir, "Cannot find #{target_temperature_schedule_name} in the model. Note, such schedule shall be one of the following type: RuleSet, Compact and Constant") wateruse_equipment.additionalProperties.setFeature('target_temperature_schedule', target_temperature_schedule_name) user_data_updated = true end unless user_data_updated OpenStudio.logFree(OpenStudio::Info, 'prm.log', "WaterUseConnections name #{wateruse_equipment.name.get} was not found in user data file: #{UserDataFiles::WATERUSE_EQUIPMENT_DEFINITION}; No user data applied.") end end end end # A function to load water use equipment from user data csv files # The file name is userdata_wateruse_equipment.csv # @param model [OpenStudio::Model::Model] OpenStudio model # @param default_swh_building_type [String] SWH building type def handle_wateruse_equipment_user_input_data(model, default_swh_building_type) user_data_wateruse_equipment = get_userdata(UserDataFiles::WATERUSE_EQUIPMENT) user_data_building = get_userdata(UserDataFiles::BUILDING) # get swh building type from user data building default_type = default_swh_building_type if user_data_building building_name = prm_get_optional_handler(model, @sizing_run_dir, 'building', 'name') user_building_index = user_data_building.index { |user_building| UserData.compare(user_building['name'], building_name) } unless user_building_index.nil? || prm_read_user_data(user_data_building[user_building_index], 'building_type_swh', nil) # Only thermal zone in the buildings user data and have building_type_for_hvac data will be assigned. default_type = prm_read_user_data(user_data_building[user_building_index], 'building_type_swh', default_type) OpenStudio.logFree(OpenStudio::Info, 'prm.log', "Building type swh found in #{UserDataFiles::WATERUSE_EQUIPMENT} for building #{building_name}, set default building type swh to #{default_type}") end end model.getWaterUseEquipments.each do |wateruse_equipment| user_data_updated = false if user_data_wateruse_equipment user_data_wateruse_equipment.each do |user_wateruse| if UserData.compare(wateruse_equipment.name.get, user_wateruse['name']) building_type_swh = prm_read_user_data(user_wateruse, 'building_type_swh', nil) if building_type_swh wateruse_equipment.additionalProperties.setFeature('building_type_swh', building_type_swh) end user_data_updated = true end end unless user_data_updated OpenStudio.logFree(OpenStudio::Info, 'prm.log', "WaterUseEquipment name #{wateruse_equipment.name.get} was not found in user data file: #{UserDataFiles::WATERUSE_EQUIPMENT}; default building swh type #{default_type} applied.") end end # No user data updated, use default type unless user_data_updated wateruse_equipment.additionalProperties.setFeature('building_type_swh', default_type) end end end # A function to load water use connections schedules from user data csv files # The file name is userdata_wateruse_connections.csv # @param [OpenStudio::Model::Model] model def handle_wateruse_connections_user_input_data(model) user_data_wateruse_connections = get_userdata(UserDataFiles::WATERUSE_CONNECTIONS) model.getWaterUseConnectionss.each do |wateruse_connections| if user_data_wateruse_connections user_data_updated = false user_data_wateruse_connections.each do |user_wateruse| next unless UserData.compare(wateruse_connections.name.get, user_wateruse['name']) hot_water_supply_temperature_schedule_name = prm_read_user_data(user_wateruse, 'hot_water_supply_temperature_schedule', '') # verify the schedule exist in the model prm_raise(model.getScheduleRulesetByName(hot_water_supply_temperature_schedule_name) || model.getScheduleCompactByName(hot_water_supply_temperature_schedule_name) || model.getScheduleConstantByName(hot_water_supply_temperature_schedule_name), @sizing_run_dir, "Cannot find #{hot_water_supply_temperature_schedule_name} in the model. Note, such schedule shall be one of the following type: RuleSet, Compact and Constant") wateruse_connections.additionalProperties.setFeature('hot_water_supply_temperature_schedule', hot_water_supply_temperature_schedule_name) cold_water_supply_temperature_schedule_name = prm_read_user_data(user_wateruse, 'cold_water_supply_temperature_schedule', '') # verify the schedule exist in the model prm_raise(model.getScheduleRulesetByName(cold_water_supply_temperature_schedule_name) || model.getScheduleCompactByName(cold_water_supply_temperature_schedule_name) || model.getScheduleConstantByName(cold_water_supply_temperature_schedule_name), @sizing_run_dir, "Cannot find #{cold_water_supply_temperature_schedule_name} in the model. Note, such schedule shall be one of the following type: RuleSet, Compact and Constant") wateruse_connections.additionalProperties.setFeature('cold_water_supply_temperature_schedule', cold_water_supply_temperature_schedule_name) user_data_updated = true end unless user_data_updated OpenStudio.logFree(OpenStudio::Info, 'prm.log', "WaterUseConnections name #{wateruse_connections.name.get} was not found in user data file: #{UserDataFiles::WATERUSE_CONNECTIONS}; No user data applied.") end end end end # A function to load exterior lighting data from user data csv files # The file name is userdata_exterior_lighting.csv # @param [OpenStudio::Model::Model] model def handle_exterior_lighting_user_input_data(model) user_data_exterior_lighting_objects = get_userdata(UserDataFiles::EXTERIOR_LIGHTS) search_criteria = { 'template' => template } ext_ltg_baseline_values = standards_lookup_table_first(table_name: 'prm_exterior_lighting', search_criteria: search_criteria) model.getExteriorLightss.each do |exterior_light| if user_data_exterior_lighting_objects user_data_updated = false # get exterior lighting object. user_data_exterior_lighting_objects.each do |user_exterior_lighting| next unless UserData.compare(exterior_light.name.get, user_exterior_lighting['name']) num_cats = prm_read_user_data(user_exterior_lighting, 'num_ext_lights_subcats', '0').to_i # Make sure none of the categories are nontradeable and not a mix of tradeable and nontradeable num_trade = 0 num_notrade = 0 ext_ltg_cats = {} (1..num_cats).each do |icat| cat_key = format('end_use_subcategory_%02d', icat) # validated subcat = user_exterior_lighting[cat_key] # handle the userdata missing value issue. if UserDataNonTradableLightsCategory.matched_any?(subcat) num_notrade += 1 else num_trade += 1 meas_val_key = format('end_use_measurement_value_%02d', icat) meas_val = prm_read_user_data(user_exterior_lighting, meas_val_key, '0.0').to_f unless meas_val == 0 OpenStudio.logFree(OpenStudio::Info, 'prm.log', "End use subcategory #{subcat} has either missing measurement value or invalid measurement value, set to 0.0") end ext_ltg_cats[subcat] = meas_val end end # skip this if all lights are non-tradeable if num_trade == 0 exterior_light.additionalProperties.setFeature('design_level', 0.0) next end if (num_trade > 0) && (num_notrade > 0) OpenStudio.logFree(OpenStudio::Warn, 'prm.log', "ExteriorLights object named #{user_exterior_lighting['name']} from user data file has a mix of tradeable and non-tradeable lighting types. All will be treated as non-tradeable.") next end ext_ltg_pwr = 0 ext_ltg_cats.each do |subcat, meas_val| # Get baseline power for this type of exterior lighting baseline_value = ext_ltg_baseline_values[subcat].to_f ext_ltg_pwr += baseline_value * meas_val end exterior_light.additionalProperties.setFeature('design_level', ext_ltg_pwr) user_data_updated = true end unless user_data_updated OpenStudio.logFree(OpenStudio::Info, 'prm.log', "Exterior Lights name #{exterior_light.name.get} was not found in user data file: #{UserDataFiles::EXTERIOR_LIGHTS}; No user data applied.") end end end end # A function to load electric equipment csv files # The file name is userdata_electric_equipment.csv # @param [OpenStudio::Model::Model] model def handle_electric_equipment_user_input_data(model) user_data_plug_load = get_userdata(UserDataFiles::ELECTRIC_EQUIPMENT) model.getElectricEquipments.each do |elevator_equipment| if user_data_plug_load user_data_updated = false user_data_plug_load.each do |user_plug_load| next unless UserData.compare(elevator_equipment.name.get, user_plug_load['name']) fraction_of_controlled_receptacles = prm_read_user_data(user_plug_load, 'fraction_of_controlled_receptacles', '0.0').to_f elevator_equipment.additionalProperties.setFeature('fraction_of_controlled_receptacles', fraction_of_controlled_receptacles) receptacle_power_savings = prm_read_user_data(user_plug_load, 'receptacle_power_savings', '0.0').to_f elevator_equipment.additionalProperties.setFeature('receptacle_power_savings', receptacle_power_savings) num_lifts = prm_read_user_data(user_plug_load, 'elevator_number_of_lifts', '0').to_i if num_lifts > 0 elevator_equipment.additionalProperties.setFeature('elevator_number_of_lifts', num_lifts) number_of_levels = prm_read_user_data(user_plug_load, 'elevator_number_of_stories', '0').to_i elevator_equipment.additionalProperties.setFeature('elevator_number_of_stories', number_of_levels) elevator_weight_of_car = prm_read_user_data(user_plug_load, 'elevator_weight_of_car', '0.0').to_f elevator_equipment.additionalProperties.setFeature('elevator_weight_of_car', elevator_weight_of_car) elevator_weight_of_car = prm_read_user_data(user_plug_load, 'elevator_counter_weight_of_car', '0.0').to_f elevator_equipment.additionalProperties.setFeature('elevator_counter_weight_of_car', elevator_weight_of_car) elevator_rated_load = prm_read_user_data(user_plug_load, 'elevator_rated_load', '0.0').to_f elevator_equipment.additionalProperties.setFeature('elevator_rated_load', elevator_rated_load) elevator_speed_of_car = prm_read_user_data(user_plug_load, 'elevator_speed_of_car', '0.0').to_f elevator_equipment.additionalProperties.setFeature('elevator_speed_of_car', elevator_speed_of_car) elevator_ventilation_cfm = prm_read_user_data(user_plug_load, 'elevator_ventilation_cfm', '0.0').to_f elevator_equipment.additionalProperties.setFeature('elevator_ventilation_cfm', elevator_ventilation_cfm) elevator_area_ft2 = prm_read_user_data(user_plug_load, 'elevator_area_ft2', '0.0').to_f elevator_equipment.additionalProperties.setFeature('elevator_area_ft2', elevator_area_ft2) end user_data_updated = true end unless user_data_updated OpenStudio.logFree(OpenStudio::Info, 'prm.log', "Electric equipment name #{elevator_equipment.name.get} was not found in user data file: #{UserDataFiles::ELECTRIC_EQUIPMENT}; No user data applied.") end end end end # A function to load gas equipment csv files # The file name is userdata_gas_equipment.csv # @param [OpenStudio::Model::Model] model def handle_gas_equipment_user_input_data(model) user_data_gas_equipment = get_userdata(UserDataFiles::GAS_EQUIPMENT) model.getGasEquipments.each do |gas_equipment| if user_data_gas_equipment user_data_updated = false user_data_gas_equipment.each do |user_gas_equipment| next unless UserData.compare(gas_equipment.name.get, user_gas_equipment['name']) fraction_of_controlled_receptacles = prm_read_user_data(user_gas_equipment, 'fraction_of_controlled_receptacles', '0.0').to_f prm_raise(fraction_of_controlled_receptacles > 1.0, 'The fraction of all controlled receptacles cannot be higher than 1.0') gas_equipment.additionalProperties.setFeature('fraction_of_controlled_receptacles', fraction_of_controlled_receptacles) receptacle_power_savings = prm_read_user_data(user_gas_equipment, 'receptacle_power_savings', '0.0').to_f gas_equipment.additionalProperties.setFeature('receptacle_power_savings', receptacle_power_savings) user_data_updated = true end unless user_data_updated OpenStudio.logFree(OpenStudio::Info, 'prm.log', "Gas equipment name #{gas_equipment.name.get} was not found in user data file: #{UserDataFiles::GAS_EQUIPMENT}; No user data applied.") end end end end # A function to load outdoor air data from user data csv files # The file name is userdata_design_specification_outdoor_air.csv # @param [OpenStudio::Model::Model] model def handle_outdoor_air_user_input_data(model) user_data_oas = get_userdata(UserDataFiles::DESIGN_SPECIFICATION_OUTDOOR_AIR) model.getDesignSpecificationOutdoorAirs.each do |zone_oa| if user_data_oas user_data_updated = false user_data_oas.each do |user_oa| next unless UserData.compare(zone_oa.name.get, user_oa['name']) user_oa.keys.each do |info_key| if info_key == 'name' zone_oa.additionalProperties.setFeature('has_user_data', true) else # this will capture the invalid string to 0.0, need to add note OpenStudio.logFree(OpenStudio::Info, 'prm.log', "Add user provided outdoor air field: #{info_key}, value: #{user_oa[info_key].to_f} to DesignSpecification:OutdoorAir #{zone_oa.name.get} ") zone_oa.additionalProperties.setFeature(info_key, user_oa[info_key].to_f) end end user_data_updated = true end unless user_data_updated OpenStudio.logFree(OpenStudio::Info, 'prm.log', "Zone outdoor air name #{zone_oa.name.get} was not found in user data file: #{UserDataFiles::DESIGN_SPECIFICATION_OUTDOOR_AIR}; No user data applied.") end end end end # A function to load airloop data from userdata csv files # The function works with validated user data only. # # @param model [OpenStudio::Model::Model] OpenStudio model object def handle_airloop_user_input_data(model) # ============================Process airloop info ============================================ user_airloops = get_userdata(UserDataFiles::AIRLOOP_HVAC) model.getAirLoopHVACs.each do |air_loop| if user_airloops user_data_updated = false user_airloops.each do |user_airloop| next unless UserData.compare(air_loop.name.get, user_airloop['name']) air_loop.thermalZones.each do |thermal_zone| # gas phase air cleaning is system base - add proposed hvac system name to zones economizer_exception_for_gas_phase_air_cleaning = user_airloop['economizer_exception_for_gas_phase_air_cleaning'] economizer_exception_for_open_refrigerated_cases = user_airloop['economizer_exception_for_open_refrigerated_cases'] user_airloop.keys.each do |info_key| if info_key.include?('has_fan_power_credit') && UserData.compare(user_airloop[info_key], UserDataBoolean::TRUE) current_value = get_additional_property_as_double(thermal_zone, info_key, 0.0) thermal_zone.additionalProperties.setFeature(info_key, current_value + 1.0) elsif info_key.include?('fan_power_credit') # Case 2: user provided value fan_power_credit = prm_read_user_data(user_airloop, info_key, '0.0').to_f current_value = get_additional_property_as_double(thermal_zone, info_key, 0.0) thermal_zone.additionalProperties.setFeature(info_key, current_value + fan_power_credit) end # Exhaust air energy recovery if info_key.include?('exhaust_energy_recovery_exception') if UserData.compare(user_airloop[info_key], UserDataBoolean::TRUE) thermal_zone.additionalProperties.setFeature(info_key, true) else thermal_zone.additionalProperties.setFeature(info_key, false) end end end if UserData.compare(economizer_exception_for_gas_phase_air_cleaning, UserDataBoolean::TRUE) thermal_zone.additionalProperties.setFeature('economizer_exception_for_gas_phase_air_cleaning', true) else thermal_zone.additionalProperties.setFeature('economizer_exception_for_gas_phase_air_cleaning', false) end if UserData.compare(economizer_exception_for_open_refrigerated_cases, UserDataBoolean::TRUE) thermal_zone.additionalProperties.setFeature('economizer_exception_for_open_refrigerated_cases', true) else thermal_zone.additionalProperties.setFeature('economizer_exception_for_open_refrigerated_cases', false) end end user_data_updated = true end unless user_data_updated OpenStudio.logFree(OpenStudio::Info, 'prm.log', "Air loop name #{air_loop.name.get} was not found in user data file: #{UserDataFiles::AIRLOOP_HVAC}; No user data applied.") end end end end # Retrieve zone HVAC user specified compliance inputs from CSV file # # @param model [OpenStudio::Model::Model] OpenStudio model object def handle_zone_hvac_user_input_data(model) user_zone_hvac = get_userdata(UserDataFiles::ZONE_HVAC) return unless user_zone_hvac && !user_zone_hvac.empty? zone_hvac_equipment = model.getZoneHVACComponents if zone_hvac_equipment.empty? OpenStudio.logFree(OpenStudio::Error, 'prm.log', 'No zone HVAC equipment is present in the proposed model, user provided information cannot be used to generate the baseline building model.') return end user_zone_hvac.each do |zone_hvac_eqp_info| user_defined_zone_hvac_obj_name = zone_hvac_eqp_info['name'] user_defined_zone_hvac_obj_type_name = zone_hvac_eqp_info['zone_hvac_object_type_name'] # Check that the object type name do exist begin user_defined_zone_hvac_obj_type_name_idd = user_defined_zone_hvac_obj_type_name.to_IddObjectType rescue StandardError => e OpenStudio.logFree(OpenStudio::Error, 'prm.log', "#{user_defined_zone_hvac_obj_type_name}, provided in the user zone HVAC user data, is not a valid OpenStudio model object.") end # Retrieve zone HVAC object(s) by name zone_hvac_eqp = model.getZoneHVACComponentsByName(user_defined_zone_hvac_obj_name, false) # If multiple object have the same name if zone_hvac_eqp.empty? OpenStudio.logFree(OpenStudio::Error, 'prm.log', "The #{user_defined_zone_hvac_obj_type_name} object named #{user_defined_zone_hvac_obj_name} provided in the user zone HVAC user data could not be found in the model.") elsif zone_hvac_eqp.length == 1 zone_hvac_eqp = zone_hvac_eqp[0] zone_hvac_eqp_idd = zone_hvac_eqp.iddObjectType.to_s if zone_hvac_eqp_idd != user_defined_zone_hvac_obj_type_name OpenStudio.logFree(OpenStudio::Error, 'prm.log', "The object type name provided in the zone HVAC user data (#{user_defined_zone_hvac_obj_type_name}) does not match with the one in the model: #{zone_hvac_eqp_idd}.") end else zone_hvac_eqp.each do |eqp| zone_hvac_eqp_idd = eqp.iddObjectType if zone_hvac_eqp_idd == user_defined_zone_hvac_obj_type_name zone_hvac_eqp = eqp break end end OpenStudio.logFree(OpenStudio::Error, 'prm.log', "A #{user_defined_zone_hvac_obj_type_name} object named #{user_defined_zone_hvac_obj_name} (as specified in the user zone HVAC data) could not be found in the model.") end if zone_hvac_eqp.thermalZone.is_initialized thermal_zone = zone_hvac_eqp.thermalZone.get zone_hvac_eqp_info.keys.each do |info_key| if info_key.include?('fan_power_credit') if !zone_hvac_eqp_info[info_key].to_s.empty? if info_key.include?('has_') if thermal_zone.additionalProperties.hasFeature(info_key) current_value = thermal_zone.additionalProperties.getFeatureAsDouble(info_key).to_f thermal_zone.additionalProperties.setFeature(info_key, current_value + 1.0) else thermal_zone.additionalProperties.setFeature(info_key, 1.0) end else if thermal_zone.additionalProperties.hasFeature(info_key) current_value = thermal_zone.additionalProperties.getFeatureAsDouble(info_key).to_f thermal_zone.additionalProperties.setFeature(info_key, current_value + zone_hvac_eqp_info[info_key]) else thermal_zone.additionalProperties.setFeature(info_key, zone_hvac_eqp_info[info_key]) end end end end end end end end # A function to load airloop DOAS data from userdata csv files # @param model [OpenStudio::Model::Model] OpenStudio model object def handle_airloop_doas_user_input_data(model) # Get user data user_airloop_doass = get_userdata(UserDataFiles::AIRLOOP_HVAC_DOAS) model.getAirLoopHVACDedicatedOutdoorAirSystems.each do |air_loop_doas| if user_airloop_doass user_data_updated = false user_airloop_doass.each do |user_airloop_doas| next unless UserData.compare(user_airloop_doas['name'], air_loop_doas.name.get) # Parse fan power credits data user_airloop_doas.keys.each do |info_key| if info_key.include?('has_fan_power_credit') && UserDataBoolean.compare(user_airloop_doas[info_key], UserDataBoolean::TRUE) air_loop_doas.airLoops.each do |air_loop| air_loop.thermalZones.each do |thermal_zone| current_value = get_additional_property_as_double(thermal_zone, info_key, 0.0) thermal_zone.additionalProperties.setFeature(info_key, current_value + 1.0) end end elsif info_key.include?('fan_power_credit') # Case 2: user provided value air_loop_doas.airLoops.each do |air_loop| air_loop.thermalZones.each do |thermal_zone| current_value = get_additional_property_as_double(thermal_zone, info_key, 0.0) thermal_zone.additionalProperties.setFeature(info_key, current_value + prm_read_user_data(user_airloop_doas, info_key, '0.0').to_f) end end end end user_data_updated = true end unless user_data_updated OpenStudio.logFree(OpenStudio::Info, 'prm.log', "Air Loop DOAS name #{air_loop_doas.name.get} was not found in user data file: #{UserDataFiles::AIRLOOP_HVAC_DOAS}; No user data applied.") end end end end # A function to load thermal zone data from userdata csv files # @param model [OpenStudio::Model::Model] OpenStudio model object def handle_thermal_zone_user_input_data(model) userdata_thermal_zones = get_userdata(UserDataFiles::THERMAL_ZONE) model.getThermalZones.each do |thermal_zone| nightcycle_exception = false if userdata_thermal_zones user_data_updated = false userdata_thermal_zones.each do |row| next unless UserData.compare(row['name'], thermal_zone.name.get) if UserData.compare(row['has_health_safety_night_cycle_exception'], UserDataBoolean::TRUE) nightcycle_exception = true break end user_data_updated = true end unless user_data_updated OpenStudio.logFree(OpenStudio::Info, 'prm.log', "Thermal Zone name #{thermal_zone.name.get} was not found in user data file: #{UserDataFiles::THERMAL_ZONE}.") end end if nightcycle_exception thermal_zone.additionalProperties.setFeature('has_health_safety_night_cycle_exception', true) end end # mark unmarked zones model.getThermalZones.each do |zone| next if zone.additionalProperties.hasFeature('has_health_safety_night_cycle_exception') zone.additionalProperties.setFeature('has_health_safety_night_cycle_exception', false) end end # Analyze HVAC, window-to-wall ratio and SWH building (area) types from user data inputs in the @standard_data library # This function returns True, but the values are stored in the multi-building_data argument. # The hierarchy for process the building types # 1. Highest: PRM rules - if rules applied against user inputs, the function will use the calculated value to reset the building type # 2. Second: User defined building type in the csv file. # 3. Third: User defined userdata_building.csv file. If an object (e.g. space, thermalzone) are not defined in their correspondent userdata csv file, use the building csv file # 4. Fourth: Dropdown list in the measure GUI. If none presented, use the data from the dropdown list. # NOTE! This function will add building types to OpenStudio objects as an additional features for hierarchy 1-3 # The object additional feature is empty when the function determined it uses fourth hierarchy. # # @param model [OpenStudio::Model::Model] OpenStudio model object # @param climate_zone [String] ASHRAE climate zone, e.g. 'ASHRAE 169-2013-4A' # @param default_hvac_building_type [String] (Fourth Hierarchy hvac building type) # @param default_wwr_building_type [String] (Fourth Hierarchy wwr building type) # @param default_swh_building_type [String] (Fourth Hierarchy swh building type) # @param bldg_type_hvac_zone_hash [Hash] An empty hash that maps building type for hvac to a list of thermal zones # @return [Boolean] returns true if successful, false if not def handle_multi_building_area_types(model, climate_zone, default_hvac_building_type, default_wwr_building_type, default_swh_building_type, bldg_type_hvac_zone_hash) # Construct the user_building hashmap user_buildings = get_userdata(UserDataFiles::BUILDING) # Build up a hvac_building_type : thermal zone hash map # =============================HVAC user data process=========================================== user_thermal_zones = get_userdata(UserDataFiles::THERMAL_ZONE) # First construct hvac building type -> thermal Zone hash and hvac building type -> floor area bldg_type_zone_hash = {} bldg_type_zone_area_hash = {} model.getThermalZones.each do |thermal_zone| # get climate zone to check the conditioning category thermal_zone_condition_category = thermal_zone_conditioning_category(thermal_zone, climate_zone) if thermal_zone_condition_category == 'Semiheated' || thermal_zone_condition_category == 'Unconditioned' next end # Check for Second hierarchy hvac_building_type = nil if user_thermal_zones user_thermal_zone_index = user_thermal_zones.index { |user_thermal_zone| UserData.compare(user_thermal_zone['name'], thermal_zone.name.get) } # make sure the thermal zone has assigned a building_type_for_hvac unless user_thermal_zone_index.nil? || user_thermal_zones[user_thermal_zone_index]['building_type_for_hvac'].nil? # Only thermal zone in the user data and have building_type_for_hvac data will be assigned. hvac_building_type = user_thermal_zones[user_thermal_zone_index]['building_type_for_hvac'] end end # Second hierarchy does not apply, check Third hierarchy if hvac_building_type.nil? && user_buildings building_name = prm_get_optional_handler(thermal_zone.model, @sizing_run_dir, 'building', 'name') user_building_index = user_buildings.index { |user_building| UserData.compare(user_building['name'], building_name) } unless user_building_index.nil? || user_buildings[user_building_index]['building_type_for_hvac'].nil? # Only thermal zone in the buildings user data and have building_type_for_hvac data will be assigned. hvac_building_type = user_buildings[user_building_index]['building_type_for_hvac'] end end # Third hierarchy does not apply, apply Fourth hierarchy if hvac_building_type.nil? hvac_building_type = default_hvac_building_type end # Add data to the hash map unless bldg_type_zone_hash.key?(hvac_building_type) bldg_type_zone_hash[hvac_building_type] = [] end unless bldg_type_zone_area_hash.key?(hvac_building_type) bldg_type_zone_area_hash[hvac_building_type] = 0.0 end # calculate floor area for the thermal zone part_of_floor_area = false thermal_zone.spaces.sort.each do |space| next unless space.partofTotalFloorArea # a space in thermal zone is part of floor area. part_of_floor_area = true bldg_type_zone_area_hash[hvac_building_type] += space.floorArea * space.multiplier end if part_of_floor_area # Only add the thermal_zone if it is part of the floor area bldg_type_zone_hash[hvac_building_type].append(thermal_zone) end end if bldg_type_zone_hash.empty? # Build hash with all zones assigned to default hvac building type zone_array = [] model.getThermalZones.each do |thermal_zone| zone_array.append(thermal_zone) thermal_zone.additionalProperties.setFeature('building_type_for_hvac', default_hvac_building_type) end bldg_type_hvac_zone_hash[default_hvac_building_type] = zone_array else # Calculate the total floor area. # If the max tie, this algorithm will pick the first encountered hvac building type as the maximum. total_floor_area = 0.0 hvac_bldg_type_with_max_floor = nil hvac_bldg_type_max_floor_area = 0.0 bldg_type_zone_area_hash.each do |key, value| if value > hvac_bldg_type_max_floor_area hvac_bldg_type_with_max_floor = key hvac_bldg_type_max_floor_area = value end total_floor_area += value end # Reset the thermal zones by going through the hierarchy 1 logics bldg_type_hvac_zone_hash.clear # Add the thermal zones for the maximum floor (primary system) bldg_type_hvac_zone_hash[hvac_bldg_type_with_max_floor] = bldg_type_zone_hash[hvac_bldg_type_with_max_floor] bldg_type_zone_hash.each do |bldg_type, bldg_type_zone| # loop the rest bldg_types unless bldg_type.eql? hvac_bldg_type_with_max_floor if OpenStudio.convert(total_floor_area, 'm^2', 'ft^2').get <= 40000 # Building is smaller than 40k sqft, it could only have one hvac_building_type, reset all the thermal zones. bldg_type_hvac_zone_hash[hvac_bldg_type_with_max_floor].push(*bldg_type_zone) OpenStudio.logFree(OpenStudio::Info, 'prm.log', "The building floor area is less than 40,000 square foot. Thermal zones under hvac building type #{bldg_type} is reset to #{hvac_bldg_type_with_max_floor}") else if OpenStudio.convert(bldg_type_zone_area_hash[bldg_type], 'm^2', 'ft^2').get < 20000 # in this case, all thermal zones shall be categorized as the primary hvac_building_type bldg_type_hvac_zone_hash[hvac_bldg_type_with_max_floor].push(*bldg_type_zone) OpenStudio.logFree(OpenStudio::Info, 'prm.log', "The floor area in hvac building type #{bldg_type} is less than 20,000 square foot. Thermal zones under this hvac building type is reset to #{hvac_bldg_type_with_max_floor}") else bldg_type_hvac_zone_hash[bldg_type] = bldg_type_zone end end end end # Write in hvac building type thermal zones by thermal zone bldg_type_hvac_zone_hash.each do |h1_bldg_type, bldg_type_zone_array| bldg_type_zone_array.each do |thermal_zone| thermal_zone.additionalProperties.setFeature('building_type_for_hvac', h1_bldg_type) end end end # =============================SPACE user data process=========================================== user_spaces = get_userdata(UserDataFiles::SPACE) model.getSpaces.each do |space| type_for_wwr = nil # Check for 2nd level hierarchy if user_spaces user_spaces.each do |user_space| unless user_space['building_type_for_wwr'].nil? if UserData.compare(space.name.get, user_space['name']) type_for_wwr = user_space['building_type_for_wwr'] end end end end if type_for_wwr.nil? # 2nd Hierarchy does not apply, check for 3rd level hierarchy building_name = prm_get_optional_handler(space.model, @sizing_run_dir, 'building', 'name') if user_buildings user_buildings.each do |user_building| unless user_building['building_type_for_wwr'].nil? if UserData.compare(user_building['name'], building_name) type_for_wwr = user_building['building_type_for_wwr'] end end end end end if type_for_wwr.nil? # 3rd level hierarchy does not apply, Apply 4th level hierarchy type_for_wwr = default_wwr_building_type end # add wwr type to space: space.additionalProperties.setFeature('building_type_for_wwr', type_for_wwr) end # =============================SWH user data process=========================================== user_wateruse_equipments = get_userdata(UserDataFiles::WATERUSE_EQUIPMENT) model.getWaterUseEquipments.each do |wateruse_equipment| type_for_swh = nil # Check for 2nd hierarchy if user_wateruse_equipments user_wateruse_equipments.each do |user_wateruse_equipment| unless user_wateruse_equipment['building_type_for_swh'].nil? if UserData.compare(wateruse_equipment.name.get, user_wateruse_equipment['name']) type_for_swh = user_wateruse_equipment['building_type_for_swh'] end end end end if type_for_swh.nil? # 2nd hierarchy does not apply, check for 3rd hierarchy # get space building type building_name = prm_get_optional_handler(wateruse_equipment.model, @sizing_run_dir, 'building', 'name') if user_buildings user_buildings.each do |user_building| unless user_building['building_type_for_swh'].nil? if UserData.compare(user_building['name'], building_name) type_for_swh = user_building['building_type_for_swh'] end end end end end if type_for_swh.nil? # 3rd hierarchy does not apply, apply 4th hierarchy type_for_swh = default_swh_building_type end # add swh type to wateruse equipment: wateruse_equipment.additionalProperties.setFeature('building_type_for_swh', type_for_swh) end return true end # Modify the existing service water heating loops to match the baseline required heating type. # # @param model [OpenStudio::Model::Model] OpenStudio model object # @param building_type [String] the building type # @return [Boolean] returns true if successful, false if not def model_apply_baseline_swh_loops(model, building_type) model.getPlantLoops.each do |plant_loop| # Skip non service water heating loops next unless plant_loop_swh_loop?(plant_loop) # Rename the loop to avoid accidentally hooking up the HVAC systems to this loop later. plant_loop.setName('Service Water Heating Loop') htg_fuels, combination_system, storage_capacity, total_heating_capacity = plant_loop_swh_system_type(plant_loop) electric = true if htg_fuels.include?('NaturalGas') || htg_fuels.include?('PropaneGas') || htg_fuels.include?('FuelOilNo1') || htg_fuels.include?('FuelOilNo2') || htg_fuels.include?('Coal') || htg_fuels.include?('Diesel') || htg_fuels.include?('Gasoline') electric = false end # Per Table G3.1 11.e, if the baseline system was a combination of heating and service water heating, # delete all heating equipment and recreate a WaterHeater:Mixed. if combination_system a = plant_loop.supplyComponents b = plant_loop.demandComponents plantloop_components = a += b plantloop_components.each do |component| # Get the object type obj_type = component.iddObjectType.valueName.to_s next if ['OS_Node', 'OS_Pump_ConstantSpeed', 'OS_Pump_VariableSpeed', 'OS_Connector_Splitter', 'OS_Connector_Mixer', 'OS_Pipe_Adiabatic'].include?(obj_type) component.remove end water_heater = OpenStudio::Model::WaterHeaterMixed.new(model) water_heater.setName('Baseline Water Heater') water_heater.setHeaterMaximumCapacity(total_heating_capacity) water_heater.setTankVolume(storage_capacity) plant_loop.addSupplyBranchForComponent(water_heater) if electric # G3.1.11.b: If electric, WaterHeater:Mixed with electric resistance water_heater.setHeaterFuelType('Electricity') water_heater.setHeaterThermalEfficiency(1.0) else # @todo for now, just get the first fuel that isn't Electricity # A better way would be to count the capacities associated # with each fuel type and use the preponderant one fuels = htg_fuels - ['Electricity'] fossil_fuel_type = fuels[0] water_heater.setHeaterFuelType(fossil_fuel_type) water_heater.setHeaterThermalEfficiency(0.8) end # If it's not a combination heating and service water heating system # just change the fuel type of all water heaters on the system # to electric resistance if it's electric else # Per Table G3.1 11.i, piping losses was deleted plant_loop_adiabatic_pipes_only(plant_loop) if electric plant_loop.supplyComponents.each do |component| next unless component.to_WaterHeaterMixed.is_initialized water_heater = component.to_WaterHeaterMixed.get # G3.1.11.b: If electric, WaterHeater:Mixed with electric resistance water_heater.setHeaterFuelType('Electricity') water_heater.setHeaterThermalEfficiency(1.0) end end end end # Set the water heater fuel types if it's 90.1-2013 model.getWaterHeaterMixeds.sort.each do |water_heater| water_heater_mixed_apply_prm_baseline_fuel_type(water_heater, building_type) end return true end # Check whether the baseline model generation needs to run all four orientations # The default shall be true # The orientation takes priority of: # 1. Appx G # 2. Method user input. # 3. User data override. # # @param run_all_orients [Boolean] user inputs to indicate whether it is required to run all orientations # @param user_model [OpenStudio::Model::Model] OpenStudio model # @return [Boolean] True if run all orientation is required, false otherwise def run_all_orientations(run_all_orients, user_model) run_orients_flag = false # Step 1 check orientation variations - priority 3 fenestration_area_hash = get_model_fenestration_area_by_orientation(user_model) fenestration_area_hash.each do |orientation, fenestration_area| OpenStudio.logFree(OpenStudio::Info, 'prm.log', "#{orientation} orientation has total fenestration area of #{fenestration_area} m2") fenestration_area_hash.each do |other_orientation, other_fenestration_area| next unless orientation != other_orientation variance = (other_fenestration_area - fenestration_area) / fenestration_area if variance.abs > 0.05 # if greater then 0.05 OpenStudio.logFree(OpenStudio::Info, 'prm.log', "#{orientation} has total fenestration area of #{fenestration_area} m2, which is higher than 5% variance compare to #{other_fenestration_area} at #{other_orientation}") run_orients_flag = true end end end # Step 2, assign method user input if it is provided as false. unless run_all_orients OpenStudio.logFree(OpenStudio::Error, 'prm.log', 'The run_all_orientation flag is set to False, update the run to a single orientation PRM generation.') run_orients_flag = run_all_orients end # Step 3 read user data - priority 1 - user data will override the priority 2 user_buildings = @standards_data.key?('userdata_building') ? @standards_data['userdata_building'] : nil if user_buildings building_name = user_model.building.get.name.get user_building_index = user_buildings.index { |user_building| building_name.include? user_building['name'] } unless user_building_index.nil? || user_buildings[user_building_index]['is_exempt_from_rotations'].nil? # user data exempt the rotation, No indicates true for running orients. OpenStudio.logFree(OpenStudio::Error, 'prm.log', "User data in the userdata_building.csv indicate building #{building_name} is exempted from rotation. Update the run to a single orientation PRM generation.") # @todo need to use user data enums later. run_orients_flag = user_buildings[user_building_index]['is_exempt_from_rotations'].casecmp('False') == 0 end end return run_orients_flag end # Function that extract the total fenestration area from a model by orientations. # Orientation is identified as N (North), S (South), E (East), W (West) # # @param user_model [OpenStudio::Model::Model] OpenStudio model # @return [Hash] Hash map that contains the total area of fenestration at each orientation (N, S, E, W) def get_model_fenestration_area_by_orientation(user_model) # First index is wall, second index is window fenestration_area_hash = { 'N' => 0.0, 'S' => 0.0, 'E' => 0.0, 'W' => 0.0 } user_model.getSpaces.each do |space| space_cond_type = space_conditioning_category(space) next if space_cond_type == 'Unconditioned' # Get zone multiplier multiplier = prm_get_optional_handler(space, @sizing_run_dir, 'thermalZone').multiplier space.surfaces.each do |surface| next if surface.surfaceType != 'Wall' next if surface.outsideBoundaryCondition != 'Outdoors' orientation = OpenstudioStandards::Geometry.surface_get_cardinal_direction(surface) surface.subSurfaces.each do |subsurface| subsurface_type = subsurface.subSurfaceType.to_s.downcase # Do not count doors next unless (subsurface_type.include? 'window') || (subsurface_type.include? 'glass') fenestration_area_hash[orientation] += subsurface.grossArea * subsurface.multiplier * multiplier end end end return fenestration_area_hash end # Apply the standard construction to each surface in the model, based on the construction type currently assigned. # # @param model [OpenStudio::Model::Model] OpenStudio model object # @param climate_zone [String] ASHRAE climate zone, e.g. 'ASHRAE 169-2013-4A' # @return [Boolean] returns true if successful, false if not def model_apply_constructions(model, climate_zone, wwr_building_type, wwr_info) model_apply_standard_constructions(model, climate_zone, wwr_building_type: wwr_building_type, wwr_info: wwr_info) return true end # Update ground temperature profile based on the weather file specified in the model # # @param model [OpenStudio::Model::Model] OpenStudio model object # @param climate_zone [String] ASHRAE climate zone, e.g. 'ASHRAE 169-2013-4A' # @return [Boolean] surfaces_with_fc_factor_boundary, returns true if successful, false if not def model_update_ground_temperature_profile(model, climate_zone) # Check if the ground temperature profile is needed surfaces_with_fc_factor_boundary = false model.getSurfaces.each do |surface| if surface.outsideBoundaryCondition.to_s == 'GroundFCfactorMethod' surfaces_with_fc_factor_boundary = true break end end if surfaces_with_fc_factor_boundary # Remove existing FCFactor temperature profile model.getSiteGroundTemperatureFCfactorMethod.remove # Get path to weather file specified in the model weather_file_path = prm_get_optional_handler(model.getWeatherFile, @sizing_run_dir, 'path').to_s # Look for stat file corresponding to the weather file stat_file_path = weather_file_path.sub('.epw', '.stat').to_s if !File.exist? stat_file_path # When the stat file corresponding with the weather file in the model is missing, # use the weather file that represent the climate zone climate_zone_weather_file_map = OpenstudioStandards::Weather.climate_zone_weather_file_map prm_raise(climate_zone_weather_file_map.key?(climate_zone), @sizing_run_dir, "Failed to find a matching climate zone #{climate_zone} from the climate zone weather files.") weather_file = climate_zone_weather_file_map[climate_zone] stat_file_path = OpenstudioStandards::Weather.get_standards_weather_file_path(weather_file).sub('.epw', '.stat').to_s end ground_temp = OpenStudio::Model::SiteGroundTemperatureFCfactorMethod.new(model) stat_file = OpenstudioStandards::Weather::StatFile.load(stat_file_path) ground_temperatures = stat_file.monthly_lagged_dry_bulb unless ground_temperatures.empty? # set the site ground temperature building surface ground_temp.setAllMonthlyTemperatures(ground_temperatures) end end return surfaces_with_fc_factor_boundary end # Generate baseline log to a specific file directory # @param file_directory [String] file directory def generate_baseline_log(file_directory) log_messages_to_file_prm("#{file_directory}/prm.log", false) end # This function checks whether it is required to adjust the window to wall ratio based on the model WWR and wwr limit. # @param wwr_limit [Double] window to wall ratio limit # @param wwr_list [Array] list of wwr of zone conditioning category in a building area type category - residential, nonresidential and semiheated # @return [Boolean] True, require adjustment, false not require adjustment def model_does_require_wwr_adjustment?(wwr_limit, wwr_list) # 90.1 PRM routine requires return true end # For 2019, it is required to adjusted wwr based on building categories for all other types # # @param bat [String] building category # @param wwr_list [Array] list of zone conditioning category-based WWR - residential, nonresidential and semiheated # @return [Double] return adjusted wwr_limit def model_get_bat_wwr_target(bat, wwr_list) wwr_limit = 40.0 # Lookup WWR target from stable baseline table wwr_lib = standards_data['prm_wwr_bldg_type'] search_criteria = { 'template' => template, 'wwr_building_type' => bat } wwr_limit_bat = model_find_object(wwr_lib, search_criteria) # If building type isn't found, assume that it's # the same as 'All Others' if wwr_limit_bat.nil? || bat.casecmp?('all others') wwr = wwr_list.max # All others type # use the min of 40% and the max wwr in the ZCC-wwr list. wwr_limit = [wwr_limit, wwr].min else # Matched type: use WWR from database. wwr_limit = wwr_limit_bat['wwr'] * 100.0 end return wwr_limit end # Calculate the window to wall ratio reduction factor # # @param multiplier [Double] multiplier of the wwr # @param surface [OpenStudio::Model:Surface] the surface object # @param wwr_building_type[String] building type for wwr # @param wwr_target [Double] target window to wall ratio # @param total_wall_m2 [Double] total wall area of the category in m2. # @param total_wall_with_fene_m2 [Double] total wall area of the category with fenestrations in m2. # @param total_fene_m2 [Double] total fenestration area # @return [Double] reduction factor def surface_get_wwr_reduction_ratio(multiplier, surface, wwr_building_type: 'All others', wwr_target: nil, total_wall_m2: 0.0, # prevent 0.0 division total_wall_with_fene_m2: 0.0, total_fene_m2: 0.0, total_plenum_wall_m2: 0.0) surface_name = surface.name.get surface_wwr = OpenstudioStandards::Geometry.surface_get_window_to_wall_ratio(surface) surface_dr = OpenstudioStandards::Geometry.surface_get_door_to_wall_ratio(surface) if multiplier < 1.0 # Case when reduction is required reduction_ratio = 1.0 - multiplier OpenStudio.logFree(OpenStudio::Info, 'prm.log', "Surface #{surface_name} WWR is #{surface_wwr}. Reduce its WWR to #{surface_wwr * reduction_ratio}%") else # Case when increase is required - takes the door area into consideration. # The target is to increase each surface to maximum 90% WWR deduct the total door area. exist_max_wwr = 0.0 if total_wall_m2 > 0 then exist_max_wwr = total_wall_with_fene_m2 * 0.9 / total_wall_m2 end if exist_max_wwr < wwr_target # In this case, it is required to add vertical fenestration to other surfaces if surface_wwr == 0.0 # delta_fenestration_surface_area / delta_wall_surface_area + 1.0 = increase_ratio for a surface with no windows. # ASSUMPTION!! assume adding windows to surface with no windows will never be window_m2 + door_m2 > surface_m2. reduction_ratio = (wwr_target - exist_max_wwr) * total_wall_m2 / (total_wall_m2 - total_wall_with_fene_m2 - total_plenum_wall_m2) + 1.0 OpenStudio.logFree(OpenStudio::Info, 'prm.log', "The max window to wall ratio is #{exist_max_wwr}, smaller than the target window to wall ratio #{wwr_target}. Surface #{surface_name} has no fenestration subsurfaces. Adding new fenestration band with WWR of #{(reduction_ratio - 1) * 100}%") else # surface has fenestration - expand it to 90% WWR or surface area minus door area, whichever is smaller. if (1.0 - surface_dr) < 0.9 # A negative reduction ratio as a flat to main function that this reduction ratio is adjusted by doors # and it is needed to adjust the WWR of the no fenestration surfaces to meet the lost reduction_ratio = (surface_dr - 1.0) / surface_wwr else reduction_ratio = 0.9 / surface_wwr end OpenStudio.logFree(OpenStudio::Info, 'prm.log', "The max window to wall ratio is #{exist_max_wwr}, smaller than the target window to wall ratio #{wwr_target}. Surface #{surface_name} will expand its WWR to 90%") end else # multiplier will be negative number thus resulting in > 1 reduction_ratio if surface_wwr == 0.0 # 1.0 means remain the original form reduction_ratio = 1.0 else reduction_ratio = multiplier end end end return reduction_ratio end # Readjusted the WWR for surfaces previously has no windows to meet the # overall WWR requirement. # This function shall only be called if the maximum WWR value for surfaces with fenestration is lower than 90% due to # accommodating the total door surface areas # # @param residual_ratio [Double] the ratio of residual surfaces among the total wall surface area with no fenestrations # @param space [OpenStudio::Model:Space] a space # @param model [OpenStudio::Model::Model] openstudio model # @return [Boolean] returns true if successful, false if not def model_readjust_surface_wwr(residual_ratio, space, model) # In this loop, we will focus on the surfaces with newly added a fenestration. space.surfaces.sort.each do |surface| next unless surface.additionalProperties.hasFeature('added_wwr') added_wwr = surface.additionalProperties.getFeatureAsDouble('added_wwr').to_f # The full calculation of adjustment is: # ((residual_ratio * surface_area + added_wwr * surface_area) / surface_area ) / added_wwr adjustment_ratio = residual_ratio / added_wwr + 1.0 surface_adjust_fenestration_in_a_surface(surface, adjustment_ratio, model) end end # Assign spaces to system groups based on building area type # Get zone groups separately for each hvac building type # # @param model [OpenStudio::Model::Model] openstudio model # @param custom [String] identifier for custom programs, not used here, but included for backwards compatibility # @param bldg_type_hvac_zone_hash [Hash of bldg_type:list of zone objects] association of zones to each hvac building type # @return [Array] an array of hashes of area information, # with keys area_ft2, type, fuel, and zones (an array of zones) def model_prm_baseline_system_groups(model, custom, bldg_type_hvac_zone_hash) bldg_groups = [] bldg_type_hvac_zone_hash.keys.each do |hvac_building_type, zones_in_building_type| # Get all groups for this hvac building type new_groups = get_baseline_system_groups_for_one_building_type(model, hvac_building_type, zones_in_building_type) # Add the groups for this hvac building type to the full list new_groups.each do |group| bldg_groups << group end end return bldg_groups end # Assign spaces to system groups for one hvac building type # One group contains all zones associated with one HVAC type # Separate groups are made for laboratories, computer rooms, district cooled zones, heated-only zones, or hybrids of these # Groups may include zones from multiple floors; separating by floor is handled later # For stable baseline, heating type is based on climate, not proposed heating type # Isolate zones that have heating-only or district (purchased) heat or chilled water # @param hvac_building_type [String] Chosen by user via measure interface or user data files # @param zones_in_building_type [Array] array of thermal zones # @return [Array] an array of hashes of area information, # with keys area_ft2, type, fuel, and zones (an array of zones) def get_baseline_system_groups_for_one_building_type(model, hvac_building_type, zones_in_building_type) # Build zones hash of [zone, zone area, occupancy type, building type, fuel] zones = model_zones_with_occ_and_fuel_type(model, 'custom') # Ensure that there is at least one conditioned zone prm_raise(!zones.empty?, @sizing_run_dir, 'The building does not appear to have any conditioned zones. Make sure zones have thermostat with appropriate heating and cooling setpoint schedules.') # Consider special rules for computer rooms # need load of all # Get cooling load of all computer rooms to establish system types comp_room_loads = {} bldg_comp_room_load = 0 zones.each do |zn| zone_load = 0.0 has_computer_room = false # First check if any space in zone has a computer room zn['zone'].spaces.each do |space| if prm_get_optional_handler(space, @sizing_run_dir, 'spaceType', 'standardsSpaceType') == 'computer room' has_computer_room = true break end end if has_computer_room # Collect load for entire zone if zn['zone'].model.version < OpenStudio::VersionString.new('3.6.0') OpenStudio.logFree(OpenStudio::Error, 'openstudio.ashrae_90_1_prm.Model', 'Required ThermalZone method .autosizedCoolingDesignLoad is not available in pre-OpenStudio 3.6.0 versions. Use a more recent version of OpenStudio.') end zone_load_w = zn['zone'].autosizedCoolingDesignLoad.get zone_load_w *= zn['zone'].multiplier zone_load = OpenStudio.convert(zone_load_w, 'W', 'Btu/hr').get end comp_room_loads[zn['zone'].name.get] = zone_load bldg_comp_room_load += zone_load end # Lab zones are grouped separately if total lab exhaust in building > 15000 cfm # Make list of zone objects that contain laboratory spaces lab_zones = [] has_lab_spaces = {} model.getThermalZones.each do |zone| # Check if this zone includes laboratory space zone.spaces.each do |space| space_type = prm_get_optional_handler(space, @sizing_run_dir, 'spaceType', 'standardsSpaceType') zone_name = zone.name.get has_lab_spaces[zone_name] = false if space_type == 'laboratory' lab_zones << zone has_lab_spaces[zone_name] = true break end end end lab_exhaust_si = 0 lab_relief_si = 0 if !lab_zones.empty? # Build a hash of return_node:zone_name node_list = {} zone_return_flow_si = Hash.new(0) var_name = 'System Node Standard Density Volume Flow Rate' frequency = 'hourly' model.getThermalZones.each do |zone| port_list = zone.returnPortList port_list_objects = port_list.modelObjects port_list_objects.each do |node| node_name = node.nameString node_list[node_name] = zone.name.get end zone_return_flow_si[zone.name.get] = 0 end # Get return air flow for each zone (even non-lab zones are needed) # Take from hourly reports created during sizing run node_list.each do |node_name, zone_name| sql = model.sqlFile prm_raise(sql.is_initialized, @sizing_run_dir, 'Model is missing SQL file. It is likely caused by: 1. unsuccessful simulation, 2. SQL is not set as one of the output file.') sql = sql.get query = "SELECT ReportDataDictionaryIndex FROM ReportDataDictionary WHERE KeyValue = '#{node_name}' COLLATE NOCASE" val = sql.execAndReturnFirstDouble(query) prm_raise(val.is_initialized, @sizing_run_dir, "No hourly return air flow data reported for node #{node_name}") report_data_dict_index = val.get query = "SELECT MAX(Value) FROM ReportData WHERE ReportDataDictionaryIndex = '#{report_data_dict_index}'" val = sql.execAndReturnFirstDouble(query) prm_raise(val.is_initialized, @sizing_run_dir, "No hourly return air flow data reported at report index #{report_data_dict_index}") zone_return_flow_si[zone_name] += OpenStudio::OptionalDouble.new(val.get).to_f end # Calc ratio of Air Loop relief to sum of zone return for each air loop # and store in zone hash # For each air loop, get relief air flow and calculate lab exhaust from the central air handler # Take from hourly reports created during sizing run zone_relief_flow_si = {} model.getAirLoopHVACs.each do |air_loop_hvac| # First get relief air flow from sizing run sql file relief_node = prm_get_optional_handler(air_loop_hvac, @sizing_run_dir, 'reliefAirNode') node_name = relief_node.nameString relief_flow_si = 0 relief_fraction = 0 sql = model.sqlFile prm_raise(sql.is_initialized, @sizing_run_dir, 'Model is missing SQL file. It is likely caused by: 1. unsuccessful simulation, 2. SQL is not set as one of the output file.') sql = sql.get query = "SELECT ReportDataDictionaryIndex FROM ReportDataDictionary WHERE KeyValue = '#{node_name}' COLLATE NOCASE" val = sql.execAndReturnFirstDouble(query) query = "SELECT MAX(Value) FROM ReportData WHERE ReportDataDictionaryIndex = '#{val.get}'" val = sql.execAndReturnFirstDouble(query) if val.is_initialized result = OpenStudio::OptionalDouble.new(val.get) end relief_flow_si = result.to_f # Get total flow of zones on this air loop total_zone_return_si = 0 air_loop_hvac.thermalZones.each do |zone| total_zone_return_si += zone_return_flow_si[zone.name.get] end relief_fraction = relief_flow_si / total_zone_return_si unless total_zone_return_si == 0 # For each zone calc total effective exhaust air_loop_hvac.thermalZones.each do |zone| zone_relief_flow_si[zone.name.get] = relief_fraction * zone_return_flow_si[zone.name.get] end end # Now check for exhaust driven by zone exhaust fans lab_zones.each do |zone| zone.equipment.each do |zone_equipment| # Get tally of exhaust fan flow if zone_equipment.to_FanZoneExhaust.is_initialized zone_exh_fan = zone_equipment.to_FanZoneExhaust.get # Check if any spaces in this zone are laboratory lab_exhaust_si += zone_exh_fan.maximumFlowRate.get end end # Also account for outdoor air exhausted from this zone via return/relief lab_relief_si += zone_relief_flow_si[zone.name.get] end end lab_exhaust_si += lab_relief_si lab_exhaust_cfm = OpenStudio.convert(lab_exhaust_si, 'm^3/s', 'cfm').get # Isolate computer rooms onto separate groups # Computer rooms may need to be split to two groups, depending on load # Isolate heated-only and destrict cooling zones onto separate groups # District heating does not require separate group final_groups = [] # Initialize arrays of zone objects by category heated_only_zones = [] heated_cooled_zones = [] district_cooled_zones = [] comp_room_svav_zones = [] comp_room_psz_zones = [] dist_comp_room_svav_zones = [] dist_comp_room_psz_zones = [] lab_zones = [] total_area_ft2 = 0 zones.each do |zn| if OpenstudioStandards::ThermalZone.thermal_zone_heated?(zn['zone']) && !OpenstudioStandards::ThermalZone.thermal_zone_cooled?(zn['zone']) # this will occur when there is no cooling tstat, or when min cooling setpoint is above 91 F heated_only_zones << zn['zone'] elsif comp_room_loads[zn['zone'].name.get] > 0 # This is a computer room zone if bldg_comp_room_load > 3_000_000 || comp_room_loads[zn['zone'].name.get] > 600_000 # System 11 if zn['fuel'].include?('DistrictCooling') dist_comp_room_svav_zones << zn['zone'] else comp_room_svav_zones << zn['zone'] end else # PSZ if zn['fuel'].include?('DistrictCooling') dist_comp_room_psz_zones << zn['zone'] else comp_room_psz_zones << zn['zone'] end end elsif has_lab_spaces[zn['zone'].name.get] && lab_exhaust_cfm > 15_000 lab_zones << zn['zone'] elsif zn['fuel'].include?('DistrictCooling') district_cooled_zones << zn['zone'] else heated_cooled_zones << zn['zone'] end # Collect total floor area of all zones for this building area type area_m2 = zn['zone'].floorArea * zn['zone'].multiplier total_area_ft2 += OpenStudio.convert(area_m2, 'm^2', 'ft^2').get end # Build final_groups array unless heated_only_zones.empty? htd_only_group = {} htd_only_group['occ'] = 'heated-only storage' htd_only_group['fuel'] = 'any' htd_only_group['zone_group_type'] = 'heated_only_zones' area_m2 = 0 heated_only_zones.each do |zone| area_m2 += zone.floorArea * zone.multiplier end area_ft2 = OpenStudio.convert(area_m2, 'm^2', 'ft^2').get htd_only_group['group_area_ft2'] = area_ft2 htd_only_group['building_area_type_ft2'] = total_area_ft2 htd_only_group['zones'] = heated_only_zones final_groups << htd_only_group end unless district_cooled_zones.empty? district_cooled_group = {} district_cooled_group['occ'] = hvac_building_type district_cooled_group['fuel'] = 'districtcooling' district_cooled_group['zone_group_type'] = 'district_cooled_zones' area_m2 = 0 district_cooled_zones.each do |zone| area_m2 += zone.floorArea * zone.multiplier end area_ft2 = OpenStudio.convert(area_m2, 'm^2', 'ft^2').get district_cooled_group['group_area_ft2'] = area_ft2 district_cooled_group['building_area_type_ft2'] = total_area_ft2 district_cooled_group['zones'] = district_cooled_zones # store info if any zone has district, fuel, or electric heating district_cooled_group['fuel'] = get_group_heat_types(model, district_cooled_zones) final_groups << district_cooled_group end unless heated_cooled_zones.empty? heated_cooled_group = {} heated_cooled_group['occ'] = hvac_building_type heated_cooled_group['fuel'] = 'any' heated_cooled_group['zone_group_type'] = 'heated_cooled_zones' area_m2 = 0 heated_cooled_zones.each do |zone| area_m2 += zone.floorArea * zone.multiplier end area_ft2 = OpenStudio.convert(area_m2, 'm^2', 'ft^2').get heated_cooled_group['group_area_ft2'] = area_ft2 heated_cooled_group['building_area_type_ft2'] = total_area_ft2 heated_cooled_group['zones'] = heated_cooled_zones # store info if any zone has district, fuel, or electric heating heated_cooled_group['fuel'] = get_group_heat_types(model, heated_cooled_zones) final_groups << heated_cooled_group end unless lab_zones.empty? lab_group = {} lab_group['occ'] = hvac_building_type lab_group['fuel'] = 'any' lab_group['zone_group_type'] = 'lab_zones' area_m2 = 0 lab_zones.each do |zone| area_m2 += zone.floorArea * zone.multiplier end area_ft2 = OpenStudio.convert(area_m2, 'm^2', 'ft^2').get lab_group['group_area_ft2'] = area_ft2 lab_group['building_area_type_ft2'] = total_area_ft2 lab_group['zones'] = lab_zones # store info if any zone has district, fuel, or electric heating lab_group['fuel'] = get_group_heat_types(model, lab_zones) final_groups << lab_group end unless comp_room_svav_zones.empty? comp_room_svav_group = {} comp_room_svav_group['occ'] = 'computer room szvav' comp_room_svav_group['fuel'] = 'any' comp_room_svav_group['zone_group_type'] = 'computer_zones' area_m2 = 0 comp_room_svav_zones.each do |zone| area_m2 += zone.floorArea * zone.multiplier end area_ft2 = OpenStudio.convert(area_m2, 'm^2', 'ft^2').get comp_room_svav_group['group_area_ft2'] = area_ft2 comp_room_svav_group['building_area_type_ft2'] = total_area_ft2 comp_room_svav_group['zones'] = comp_room_svav_zones # store info if any zone has district, fuel, or electric heating comp_room_svav_group['fuel'] = get_group_heat_types(model, comp_room_svav_zones) final_groups << comp_room_svav_group end unless comp_room_psz_zones.empty? comp_room_psz_group = {} comp_room_psz_group['occ'] = 'computer room psz' comp_room_psz_group['fuel'] = 'any' comp_room_psz_group['zone_group_type'] = 'computer_zones' area_m2 = 0 comp_room_psz_zones.each do |zone| area_m2 += zone.floorArea * zone.multiplier end area_ft2 = OpenStudio.convert(area_m2, 'm^2', 'ft^2').get comp_room_psz_group['group_area_ft2'] = area_ft2 comp_room_psz_group['building_area_type_ft2'] = total_area_ft2 comp_room_psz_group['zones'] = comp_room_psz_zones # store info if any zone has district, fuel, or electric heating comp_room_psz_group['fuel'] = get_group_heat_types(model, comp_room_psz_zones) final_groups << comp_room_psz_group end unless dist_comp_room_svav_zones.empty? dist_comp_room_svav_group = {} dist_comp_room_svav_group['occ'] = hvac_building_type dist_comp_room_svav_group['fuel'] = 'districtcooling' dist_comp_room_svav_group['zone_group_type'] = 'computer_zones' area_m2 = 0 dist_comp_room_svav_zones.each do |zone| area_m2 += zone.floorArea * zone.multiplier end area_ft2 = OpenStudio.convert(area_m2, 'm^2', 'ft^2').get dist_comp_room_svav_group['group_area_ft2'] = area_ft2 dist_comp_room_svav_group['building_area_type_ft2'] = total_area_ft2 dist_comp_room_svav_group['zones'] = dist_comp_room_svav_zones # store info if any zone has district, fuel, or electric heating dist_comp_room_svav_group['fuel'] = get_group_heat_types(model, dist_comp_room_svav_zones) final_groups << dist_comp_room_svav_group end unless dist_comp_room_psz_zones.empty? dist_comp_room_psz_group = {} dist_comp_room_psz_group['occ'] = hvac_building_type dist_comp_room_psz_group['fuel'] = 'districtcooling' dist_comp_room_psz_group['zone_group_type'] = 'computer_zones' area_m2 = 0 dist_comp_room_psz_zones.each do |zone| end area_ft2 = OpenStudio.convert(area_m2, 'm^2', 'ft^2').get dist_comp_room_psz_group['group_area_ft2'] = area_ft2 dist_comp_room_psz_group['building_area_type_ft2'] = total_area_ft2 dist_comp_room_psz_group['zones'] = dist_comp_room_psz_zones # store info if any zone has district, fuel, or electric heating dist_comp_room_psz_group['fuel'] = get_group_heat_types(model, dist_comp_room_psz_zones) final_groups << dist_comp_room_psz_group end ngrps = final_groups.count # Determine the number of stories spanned by each group and report out info. final_groups.each do |group| # Determine the number of stories this group spans group['stories'] = OpenstudioStandards::Geometry.thermal_zones_get_number_of_stories_spanned(group['zones']) # Report out the final grouping OpenStudio.logFree(OpenStudio::Info, 'openstudio.standards.Model', "Final system type group: occ = #{group['occ']}, fuel = #{group['fuel']}, area = #{group['group_area_ft2'].round} ft2, num stories = #{group['stories']}, zones:") group['zones'].sort.each_slice(5) do |zone_list| zone_names = [] zone_list.each do |zone| zone_names << zone.name.get.to_s end OpenStudio.logFree(OpenStudio::Info, 'openstudio.standards.Model', "--- #{zone_names.join(', ')}") end end return final_groups end # Alternate method for 2016 and later stable baseline # Limits for each building area type are taken from data table # Heating fuel is based on climate zone, unless district heat is in proposed # # @note Select system type from data table base on key parameters # @param climate_zone [String] id code for the climate # @param sys_group [Hash] Hash defining a group of zones that have the same Appendix G system type # @param custom [String] included here for backwards compatibility (not used here) # @param hvac_building_type [String] Chosen by user via measure interface or user data files # @param district_heat_zones [Hash] of zone name => true for has district heat, false for has not # @return [String] The system type. Possibilities are PTHP, PTAC, PSZ_AC, PSZ_HP, PVAV_Reheat, PVAV_PFP_Boxes, # VAV_Reheat, VAV_PFP_Boxes, Gas_Furnace, Electric_Furnace def model_prm_baseline_system_type(model, climate_zone, sys_group, custom, hvac_building_type, district_heat_zones) area_type = sys_group['occ'] fuel_type = sys_group['fuel'] area_ft2 = sys_group['building_area_type_ft2'] num_stories = sys_group['stories'] # [type, central_heating_fuel, zone_heating_fuel, cooling_fuel] system_type = [nil, nil, nil, nil] # Find matching record from prm baseline hvac table # First filter by number of stories i_story_group = 0 props = {} 0.upto(9) do |i| i_story_group += 1 props = model_find_object(standards_data['prm_baseline_hvac'], 'template' => template, 'hvac_building_type' => area_type, 'flrs_range_group' => i_story_group, 'area_range_group' => 1) prm_raise(props, @sizing_run_dir, "Could not find baseline HVAC type for: #{template}-#{area_type}.") if num_stories <= props['bldg_flrs_max'] # Story Group Is found break end end # Next filter by floor area i_area_group = 0 loop do i_area_group += 1 props = model_find_object(standards_data['prm_baseline_hvac'], 'template' => template, 'hvac_building_type' => area_type, 'flrs_range_group' => i_story_group, 'area_range_group' => i_area_group) prm_raise(props && i_area_group <= 9, @sizing_run_dir, "Could not find baseline HVAC type for: #{template}-#{area_type}.") below_max = false above_min = false # check if actual building floor area is within range for this area group if props['max_area_qual'] == 'LT' if area_ft2 < props['bldg_area_max'] below_max = true end elsif props['max_area_qual'] == 'LE' if area_ft2 <= props['bldg_area_max'] below_max = true end end if props['min_area_qual'] == 'GT' if area_ft2 > props['bldg_area_min'] above_min = true end elsif props['min_area_qual'] == 'GE' if area_ft2 >= props['bldg_area_min'] above_min = true end end if above_min && below_max # break condition. break end end heat_type = find_prm_heat_type(hvac_building_type, climate_zone) # hash to relate apx G systype categories to sys types for model sys_hash = {} if heat_type == 'fuel' sys_hash['PTAC'] = 'PTAC' sys_hash['PSZ'] = 'PSZ_AC' sys_hash['SZ-CV'] = 'SZ_CV' sys_hash['Heating and ventilation'] = 'Gas_Furnace' sys_hash['PSZ-AC'] = 'PSZ_AC' sys_hash['Packaged VAV'] = 'PVAV_Reheat' sys_hash['VAV'] = 'VAV_Reheat' sys_hash['Unconditioned'] = 'None' sys_hash['SZ-VAV'] = 'SZ_VAV' else sys_hash['PTAC'] = 'PTHP' sys_hash['PSZ'] = 'PSZ_HP' sys_hash['SZ-CV'] = 'SZ_CV' sys_hash['Heating and ventilation'] = 'Electric_Furnace' sys_hash['PSZ-AC'] = 'PSZ_HP' sys_hash['Packaged VAV'] = 'PVAV_PFP_Boxes' sys_hash['VAV'] = 'VAV_PFP_Boxes' sys_hash['Unconditioned'] = 'None' sys_hash['SZ-VAV'] = 'SZ_VAV' end model_sys_type = sys_hash[props['system_type']] if /districtheating/i =~ fuel_type central_heat = 'DistrictHeating' elsif heat_type =~ /fuel/i central_heat = 'NaturalGas' else central_heat = 'Electricity' end if /districtheating/i =~ fuel_type && /elec/i !~ fuel_type && /fuel/i !~ fuel_type # if no zone has fuel or elect, set default to district for zones zone_heat = 'DistrictHeating' elsif heat_type =~ /fuel/i zone_heat = 'NaturalGas' else zone_heat = 'Electricity' end if /districtcooling/i =~ fuel_type cool_type = 'DistrictCooling' elsif props['system_type'] =~ /Heating and ventilation/i || props['system_type'] =~ /unconditioned/i cool_type = nil end system_type = [model_sys_type, central_heat, zone_heat, cool_type] return system_type end # For a multizone system, create the fan schedule based on zone occupancy/fan schedules # @author Doug Maddox, PNNL # @param model [OpenStudio::Model::Model] openstudio model # @param zone_op_hrs [Hash] of hash of zoneName zone_op_hrs # @param pri_zones [Array] names of zones served by the multizone system # @param system_name [String] name of air loop def model_create_multizone_fan_schedule(model, zone_op_hrs, pri_zones, system_name) # Create fan schedule for multizone system fan_8760 = [] # If any zone is on for an hour, then the system fan must be on for that hour pri_zones.each do |zone| zone_name = zone.name.get.to_s if fan_8760.empty? fan_8760 = zone_op_hrs[zone_name] else (0..fan_8760.size - 1).each do |ihr| if zone_op_hrs[zone_name][ihr] > 0 fan_8760[ihr] = 1 end end end end # Convert 8760 array to schedule ruleset fan_sch_limits = model.getScheduleTypeLimitsByName('fan schedule limits for prm') if fan_sch_limits.empty? fan_sch_limits = OpenStudio::Model::ScheduleTypeLimits.new(model) fan_sch_limits.setName('fan schedule limits for prm') fan_sch_limits.setNumericType('DISCRETE') fan_sch_limits.setUnitType('Dimensionless') fan_sch_limits.setLowerLimitValue(0) fan_sch_limits.setUpperLimitValue(1) else fan_sch_limits = fan_sch_limits.get end sch_name = "#{system_name} fan schedule" make_ruleset_sched_from_8760(model, fan_8760, sch_name, fan_sch_limits) air_loop = model.getAirLoopHVACByName(system_name).get air_loop.additionalProperties.setFeature('fan_sched_name', sch_name) end # For a multizone system, identify any zones to isolate to separate PSZ systems # isolated zones are on the 'secondary' list # This version of the method applies to standard years 2016 and later (stable baseline) # @author Doug Maddox, PNNL # @param model # @param zones [Array] # @param zone_fan_scheds [Hash] hash of zoneName 8760FanSchedPerZone # @return [Hash] A hash of two arrays of ThermalZones, # where the keys are 'primary' and 'secondary' def model_differentiate_primary_secondary_thermal_zones(model, zones, zone_fan_scheds) pri_zones = [] sec_zones = [] pri_zone_names = [] sec_zone_names = [] zone_op_hrs = {} # hash of zoneName: 8760 array of operating hours # If there is only one zone, then set that as primary if zones.size == 1 zones.each do |zone| pri_zones << zone pri_zone_names << zone.name.get.to_s zone_name = zone.name.get.to_s if zone_fan_scheds.key?(zone_name) zone_fan_sched = zone_fan_scheds[zone_name] else zone_fan_sched = nil end zone_op_hrs[zone.name.get.to_s] = thermal_zone_get_annual_operating_hours(model, zone, zone_fan_sched) end # Report out the primary vs. secondary zones unless sec_zone_names.empty? OpenStudio.logFree(OpenStudio::Info, 'openstudio.standards.Model', "Secondary system zones = #{sec_zone_names.join(', ')}.") end return { 'primary' => pri_zones, 'secondary' => sec_zones, 'zone_op_hrs' => zone_op_hrs } end zone_eflh = {} # hash of zoneName: eflh for zone zone_max_load = {} # hash of zoneName: coincident max internal load load_limit = 10 # differ by 10 Btu/hr-sf or more eflh_limit = 40 # differ by more than 40 EFLH/week from average of other zones zone_area = {} # hash of zoneName:area # Get coincident peak internal load for each zone zones.each do |zone| zone_name = zone.name.get.to_s if zone_fan_scheds.key?(zone_name) zone_fan_sched = zone_fan_scheds[zone_name] else zone_fan_sched = nil end zone_op_hrs[zone_name] = thermal_zone_get_annual_operating_hours(model, zone, zone_fan_sched) zone_eflh[zone_name] = thermal_zone_occupancy_eflh(zone, zone_op_hrs[zone_name]) zone_max_load_w = thermal_zone_peak_internal_load(model, zone) zone_max_load_w_m2 = zone_max_load_w / zone.floorArea zone_max_load[zone_name] = OpenStudio.convert(zone_max_load_w_m2, 'W/m^2', 'Btu/hr*ft^2').get zone_area[zone_name] = zone.floorArea end # Eliminate all zones for which both max load and EFLH exceed limits zones.each do |zone| zone_name = zone.name.get.to_s max_load = zone_max_load[zone_name] avg_max_load = get_wtd_avg_of_other_zones(zone_max_load, zone_area, zone_name) max_load_diff = (max_load - avg_max_load).abs avg_eflh = get_avg_of_other_zones(zone_eflh, zone_name) eflh_diff = (avg_eflh - zone_eflh[zone_name]).abs if max_load_diff >= load_limit && eflh_diff > eflh_limit # Add zone to secondary list, and remove from hashes OpenStudio.logFree(OpenStudio::Warn, 'openstudio.standards.Model', "Zone moved to PSZ due to load AND eflh: #{zone_name}; load limit = #{load_limit}, eflh_limit = #{eflh_limit}") OpenStudio.logFree(OpenStudio::Warn, 'openstudio.standards.Model', "load diff = #{max_load_diff}, this zone load = #{max_load}, avg zone load = #{avg_max_load}") OpenStudio.logFree(OpenStudio::Warn, 'openstudio.standards.Model', "eflh diff = #{eflh_diff}, this zone load = #{zone_eflh[zone_name]}, avg zone eflh = #{avg_eflh}") sec_zones << zone sec_zone_names << zone_name zone_eflh.delete(zone_name) zone_max_load.delete(zone_name) end end # Eliminate worst zone where EFLH exceeds limit # Repeat until all zones are within limit num_zones = zone_eflh.size avg_eflh_save = 0 max_zone_name = '' max_eflh_diff = 0 max_zone = nil (1..num_zones).each do |izone| # This loop is to iterate to eliminate one zone at a time max_eflh_diff = 0 zones.each do |zone| # This loop finds the worst remaining zone to eliminate if above threshold zone_name = zone.name.get.to_s next if !zone_eflh.key?(zone_name) avg_eflh = get_avg_of_other_zones(zone_eflh, zone_name) eflh_diff = (avg_eflh - zone_eflh[zone_name]).abs if eflh_diff > max_eflh_diff max_eflh_diff = eflh_diff max_zone_name = zone_name max_zone = zone avg_eflh_save = avg_eflh end end if max_eflh_diff > eflh_limit # Move the max Zone to the secondary list OpenStudio.logFree(OpenStudio::Warn, 'openstudio.standards.Model', "Zone moved to PSZ due to eflh: #{max_zone_name}; limit = #{eflh_limit}") OpenStudio.logFree(OpenStudio::Warn, 'openstudio.standards.Model', "eflh diff = #{max_eflh_diff}, this zone load = #{zone_eflh[max_zone_name]}, avg zone eflh = #{avg_eflh_save}") sec_zones << max_zone sec_zone_names << max_zone_name zone_eflh.delete(max_zone_name) zone_max_load.delete(max_zone_name) else # All zones are now within the limit, exit the iteration break end end # Eliminate worst zone where max load exceeds limit and repeat until all pass num_zones = zone_eflh.size highest_max_load_diff = -1 highest_zone = nil highest_zone_name = '' highest_max_load = 0 avg_max_load_save = 0 (1..num_zones).each do |izone| # This loop is to iterate to eliminate one zone at a time highest_max_load_diff = 0 zones.each do |zone| # This loop finds the worst remaining zone to eliminate if above threshold zone_name = zone.name.get.to_s next if !zone_max_load.key?(zone_name) max_load = zone_max_load[zone_name] avg_max_load = get_wtd_avg_of_other_zones(zone_max_load, zone_area, zone_name) max_load_diff = (max_load - avg_max_load).abs if max_load_diff >= highest_max_load_diff highest_max_load_diff = max_load_diff highest_zone_name = zone_name highest_zone = zone highest_max_load = max_load avg_max_load_save = avg_max_load end end if highest_max_load_diff > load_limit # Move the max Zone to the secondary list OpenStudio.logFree(OpenStudio::Warn, 'openstudio.standards.Model', "Zone moved to PSZ due to load: #{highest_zone_name}; load limit = #{load_limit}") OpenStudio.logFree(OpenStudio::Warn, 'openstudio.standards.Model', "load diff = #{highest_max_load_diff}, this zone load = #{highest_max_load}, avg zone load = #{avg_max_load_save}") sec_zones << highest_zone sec_zone_names << highest_zone_name zone_eflh.delete(highest_zone_name) zone_max_load.delete(highest_zone_name) else # All zones are now within the limit, exit the iteration break end end # Place remaining zones in multizone system list zone_eflh.each_key do |key| zones.each do |zone| if key == zone.name.get.to_s pri_zones << zone pri_zone_names << key end end end # Report out the primary vs. secondary zones unless pri_zone_names.empty? OpenStudio.logFree(OpenStudio::Info, 'openstudio.standards.Model', "Primary system zones = #{pri_zone_names.join(', ')}.") end unless sec_zone_names.empty? OpenStudio.logFree(OpenStudio::Info, 'openstudio.standards.Model', "Secondary system zones = #{sec_zone_names.join(', ')}.") end return { 'primary' => pri_zones, 'secondary' => sec_zones, 'zone_op_hrs' => zone_op_hrs } end # This method is a catch-all run at the end of create-baseline to make final adjustements to HVAC capacities # to account for recent model changes # @author Doug Maddox, PNNL # @param model # @return [Boolean] returns true if successful, false if not def model_refine_size_dependent_values(model, sizing_run_dir) # Final sizing run before refining size-dependent values if model_run_sizing_run(model, "#{sizing_run_dir}/SR3") == false return false end model.getAirLoopHVACs.sort.each do |air_loop_hvac| # Reset secondary design secondary flow rate based on updated primary flow air_loop_hvac.demandComponents.each do |dc| next if dc.to_AirTerminalSingleDuctParallelPIUReheat.empty? pfp_term = dc.to_AirTerminalSingleDuctParallelPIUReheat.get sec_flow_frac = 0.5 # Get the maximum flow rate through the terminal max_primary_air_flow_rate = nil if pfp_term.maximumPrimaryAirFlowRate.is_initialized max_primary_air_flow_rate = pfp_term.maximumPrimaryAirFlowRate.get elsif pfp_term.autosizedMaximumPrimaryAirFlowRate.is_initialized max_primary_air_flow_rate = pfp_term.autosizedMaximumPrimaryAirFlowRate.get end max_sec_flow_rate_m3_per_s = max_primary_air_flow_rate * sec_flow_frac pfp_term.setMaximumSecondaryAirFlowRate(max_sec_flow_rate_m3_per_s) end end return true end # Determine whether heating type is fuel or electric # @param hvac_building_type [String] Key for lookup of baseline system type # @param climate_zone [String] full name of climate zone # @return [String] fuel or electric def find_prm_heat_type(hvac_building_type, climate_zone) climate_code = climate_zone.split('-')[-1] heat_type_props = model_find_object(standards_data['prm_heat_type'], 'template' => template, 'hvac_building_type' => hvac_building_type, 'climate_zone' => climate_code) if !heat_type_props # try again with wild card for climate heat_type_props = model_find_object(standards_data['prm_heat_type'], 'template' => template, 'hvac_building_type' => hvac_building_type, 'climate_zone' => 'any') end if !heat_type_props # try again with wild card for building type heat_type_props = model_find_object(standards_data['prm_heat_type'], 'template' => template, 'hvac_building_type' => 'all others', 'climate_zone' => climate_code) end prm_raise(heat_type_props, @sizing_run_dir, "Could not find baseline heat type for: #{template}-#{hvac_building_type}-#{climate_zone}.") return heat_type_props['heat_type'] end private # Check if the PRM process uses user data. The function returns a hash when # 1. There is a matching user data # 2. The matching user data is not nil saved in the @standards_data # 3. The matching user data hash is not empty # The function returns nil if none of the above matched. # # @param user_data_csv [String] the name of the user data csv file # @return [hash | nil] Returns hash or nil. def get_userdata(user_data_csv) return @standards_data.key?(user_data_csv) && @standards_data[user_data_csv].length >= 1 ? @standards_data[user_data_csv] : nil end end