module OpenstudioStandards::QAQC
Public Class Methods
Check the fan power (W/cfm) for each air loop fan in the model to identify unrealistically sized fans.
@param category [String] category to bin this check into @param target_standard [String] standard template, e.g. ‘90.1-2013’ @param max_pct_delta [Double] threshold for throwing an error for percent difference @param name_only [Boolean] If true, only return the name of this check @return [OpenStudio::Attribute] OpenStudio Attribute object containing check results
# File lib/openstudio-standards/qaqc/hvac.rb, line 253 def self.check_air_loop_fan_power(category, target_standard, max_pct_delta: 0.3, name_only: false) # summary of the check check_elems = OpenStudio::AttributeVector.new check_elems << OpenStudio::Attribute.new('name', 'Fan Power') check_elems << OpenStudio::Attribute.new('category', category) check_elems << OpenStudio::Attribute.new('description', 'Check that fan power vs flow makes sense.') # stop here if only name is requested this is used to populate display name for arguments if name_only == true results = [] check_elems.each do |elem| results << elem.valueAsString end return results end std = Standard.build(target_standard) begin # Check each air loop @model.getAirLoopHVACs.sort.each do |air_loop| # Set the expected W/cfm if air_loop.thermalZones.size.to_i == 1 # expect single zone systems to be lower expected_w_per_cfm = 0.5 else expected_w_per_cfm = 1.1 end # Check the W/cfm for each fan on each air loop air_loop.supplyComponents.each do |component| # Get the W/cfm for the fan obj_type = component.iddObjectType.valueName.to_s case obj_type when 'OS_Fan_ConstantVolume' actual_w_per_cfm = std.fan_rated_w_per_cfm(component.to_FanConstantVolume.get) when 'OS_Fan_OnOff' actual_w_per_cfm = std.fan_rated_w_per_cfm(component.to_FanOnOff.get) when 'OS_Fan_VariableVolume' actual_w_per_cfm = std.fan_rated_w_per_cfm(component.to_FanVariableVolume.get) else next # Skip non-fan objects end # Compare W/cfm to expected/typical values if ((expected_w_per_cfm - actual_w_per_cfm) / actual_w_per_cfm).abs > max_pct_delta check_elems << OpenStudio::Attribute.new('flag', "For #{component.name} on #{air_loop.name}, the actual fan power of #{actual_w_per_cfm.round(1)} W/cfm is more than #{(max_pct_delta * 100.0).round(2)}% different from the expected #{expected_w_per_cfm} W/cfm.") end end end rescue StandardError => e # brief description of ruby error check_elems << OpenStudio::Attribute.new('flag', "Error prevented QAQC check from running (#{e}).") # backtrace of ruby error for diagnostic use if @error_backtrace then check_elems << OpenStudio::Attribute.new('flag', e.backtrace.join("\n").to_s) end end # add check_elms to new attribute check_elem = OpenStudio::Attribute.new('check', check_elems) return check_elem end
Check the air loop and zone operational vs. sizing temperatures and make sure everything is coordinated. This identifies problems caused by sizing to one set of conditions and operating at a different set.
@param category [String] category to bin this check into @param max_sizing_temp_delta [Double] threshold for throwing an error for design sizing temperatures @param max_operating_temp_delta [Double] threshold for throwing an error on operating temperatures @param name_only [Boolean] If true, only return the name of this check @return [OpenStudio::Attribute] OpenStudio Attribute object containing check results
# File lib/openstudio-standards/qaqc/hvac.rb, line 14 def self.check_air_loop_temperatures(category, max_sizing_temp_delta: 2.0, max_operating_temp_delta: 5.0, name_only: false) # summary of the check check_elems = OpenStudio::AttributeVector.new check_elems << OpenStudio::Attribute.new('name', 'Air System Temperatures') check_elems << OpenStudio::Attribute.new('category', category) check_elems << OpenStudio::Attribute.new('description', 'Check that air system sizing and operation temperatures are coordinated.') # stop here if only name is requested this is used to populate display name for arguments if name_only == true results = [] check_elems.each do |elem| results << elem.valueAsString end return results end begin # get the weather file run period (as opposed to design day run period) ann_env_pd = nil @sql = @model.sqlFile.get @sql.availableEnvPeriods.each do |env_pd| env_type = @sql.environmentType(env_pd) if env_type.is_initialized if env_type.get == OpenStudio::EnvironmentType.new('WeatherRunPeriod') ann_env_pd = env_pd break end end end # only try to get the annual timeseries if an annual simulation was run if ann_env_pd.nil? check_elems << OpenStudio::Attribute.new('flag', 'Cannot find the annual simulation run period, cannot check equipment part load ratios.') return check_elems end @model.getAirLoopHVACs.sort.each do |air_loop| supply_outlet_node_name = air_loop.supplyOutletNode.name.to_s design_cooling_sat = air_loop.sizingSystem.centralCoolingDesignSupplyAirTemperature design_cooling_sat = OpenStudio.convert(design_cooling_sat, 'C', 'F').get design_heating_sat = air_loop.sizingSystem.centralHeatingDesignSupplyAirTemperature design_heating_sat = OpenStudio.convert(design_heating_sat, 'C', 'F').get # check if the system is a unitary system is_unitary_system = OpenstudioStandards::HVAC.air_loop_hvac_unitary_system?(air_loop) is_direct_evap = OpenstudioStandards::HVAC.air_loop_hvac_direct_evap?(air_loop) if is_unitary_system && !is_direct_evap unitary_system_name = nil unitary_system_type = '<unspecified>' unitary_min_temp_f = nil unitary_max_temp_f = nil air_loop.supplyComponents.each do |component| obj_type = component.iddObjectType.valueName.to_s case obj_type when 'OS_AirLoopHVAC_UnitarySystem', 'OS_AirLoopHVAC_UnitaryHeatPump_AirToAir', 'OS_AirLoopHVAC_UnitaryHeatPump_AirToAir_MultiSpeed', 'OS_AirLoopHVAC_UnitaryHeatCool_VAVChangeoverBypass' unitary_system_name = component.name.to_s unitary_system_type = obj_type unitary_system_temps = OpenstudioStandards::HVAC.unitary_system_min_max_temperature_value(component) unitary_min_temp_f = unitary_system_temps['min_temp'] unitary_max_temp_f = unitary_system_temps['max_temp'] end end # set expected minimums for operating temperatures expected_min = unitary_min_temp_f.nil? ? design_cooling_sat : [design_cooling_sat, unitary_min_temp_f].min expected_max = unitary_max_temp_f.nil? ? design_heating_sat : [design_heating_sat, unitary_max_temp_f].max else # get setpoint manager spm_name = nil spm_type = '<unspecified>' spm_min_temp_f = nil spm_max_temp_f = nil @model.getSetpointManagers.each do |spm| if spm.setpointNode.is_initialized spm_node = spm.setpointNode.get if spm_node.name.to_s == supply_outlet_node_name spm_name = spm.name spm_type = spm.iddObjectType.valueName.to_s spm_temps_f = OpenstudioStandards::HVAC.setpoint_manager_min_max_temperature(spm) spm_min_temp_f = spm_temps_f['min_temp'] spm_max_temp_f = spm_temps_f['max_temp'] break end end end # check setpoint manager temperatures against design temperatures if spm_min_temp_f if (spm_min_temp_f - design_cooling_sat).abs > max_sizing_temp_delta check_elems << OpenStudio::Attribute.new('flag', "Minor Error: Air loop '#{air_loop.name}' sizing uses a #{design_cooling_sat.round(1)}F design cooling supply air temperature, but the setpoint manager operates down to #{spm_min_temp_f.round(1)}F.") end end if spm_max_temp_f if (spm_max_temp_f - design_heating_sat).abs > max_sizing_temp_delta check_elems << OpenStudio::Attribute.new('flag', "Minor Error: Air loop '#{air_loop.name}' sizing uses a #{design_heating_sat.round(1)}F design heating supply air temperature, but the setpoint manager operates up to #{spm_max_temp_f.round(1)}F.") end end # set expected minimums for operating temperatures expected_min = spm_min_temp_f.nil? ? design_cooling_sat : [design_cooling_sat, spm_min_temp_f].min expected_max = spm_max_temp_f.nil? ? design_heating_sat : [design_heating_sat, spm_max_temp_f].max # check zone sizing temperature against air loop design temperatures air_loop.thermalZones.each do |zone| # if this zone has a reheat terminal, get the reheat temp for comparison reheat_op_f = nil reheat_zone = false zone.equipment.each do |equipment| obj_type = equipment.iddObjectType.valueName.to_s case obj_type when 'OS_AirTerminal_SingleDuct_ConstantVolume_Reheat' term = equipment.to_AirTerminalSingleDuctConstantVolumeReheat.get reheat_op_f = OpenStudio.convert(term.maximumReheatAirTemperature, 'C', 'F').get reheat_zone = true when 'OS_AirTerminal_SingleDuct_VAV_HeatAndCool_Reheat' term = equipment.to_AirTerminalSingleDuctVAVHeatAndCoolReheat.get reheat_op_f = OpenStudio.convert(term.maximumReheatAirTemperature, 'C', 'F').get reheat_zone = true when 'OS_AirTerminal_SingleDuct_VAV_Reheat' term = equipment.to_AirTerminalSingleDuctVAVReheat.get reheat_op_f = OpenStudio.convert(term.maximumReheatAirTemperature, 'C', 'F').get reheat_zone = true when 'OS_AirTerminal_SingleDuct_ParallelPIU_Reheat' # reheat_op_f = # Not an OpenStudio input reheat_zone = true when 'OS_AirTerminal_SingleDuct_SeriesPIU_Reheat' # reheat_op_f = # Not an OpenStudio input reheat_zone = true end end # get the zone heating and cooling SAT for sizing sizing_zone = zone.sizingZone zone_siz_htg_f = OpenStudio.convert(sizing_zone.zoneHeatingDesignSupplyAirTemperature, 'C', 'F').get zone_siz_clg_f = OpenStudio.convert(sizing_zone.zoneCoolingDesignSupplyAirTemperature, 'C', 'F').get # check cooling temperatures if (design_cooling_sat - zone_siz_clg_f).abs > max_sizing_temp_delta check_elems << OpenStudio::Attribute.new('flag', "Minor Error: Air loop '#{air_loop.name}' sizing uses a #{design_cooling_sat.round(1)}F design cooling supply air temperature but the sizing for zone #{zone.name} uses a cooling supply air temperature of #{zone_siz_clg_f.round(1)}F.") end # check heating temperatures if reheat_zone && reheat_op_f if (reheat_op_f - zone_siz_htg_f).abs > max_sizing_temp_delta check_elems << OpenStudio::Attribute.new('flag', "Minor Error: For zone '#{zone.name}', the reheat air temperature is set to #{reheat_op_f.round(1)}F, but the sizing for the zone is done with a heating supply air temperature of #{zone_siz_htg_f.round(1)}F.") end elsif reheat_zone && !reheat_op_f # reheat zone but no reheat temperature available from terminal object elsif (design_heating_sat - zone_siz_htg_f).abs > max_sizing_temp_delta check_elems << OpenStudio::Attribute.new('flag', "Minor Error: Air loop '#{air_loop.name}' sizing uses a #{design_heating_sat.round(1)}F design heating supply air temperature but the sizing for zone #{zone.name} uses a heating supply air temperature of #{zone_siz_htg_f.round(1)}F.") end end end # get supply air temperatures for supply outlet node supply_temp_timeseries = @sql.timeSeries(ann_env_pd, 'Timestep', 'System Node Temperature', supply_outlet_node_name) if supply_temp_timeseries.empty? check_elems << OpenStudio::Attribute.new('flag', "Warning: No supply node temperature timeseries found for '#{air_loop.name}'") next else # convert to ruby array temperatures = [] supply_temp_vector = supply_temp_timeseries.get.values for i in (0..supply_temp_vector.size - 1) temperatures << supply_temp_vector[i] end end # get supply air flow rates for supply outlet node supply_flow_timeseries = @sql.timeSeries(ann_env_pd, 'Timestep', 'System Node Standard Density Volume Flow Rate', supply_outlet_node_name) if supply_flow_timeseries.empty? check_elems << OpenStudio::Attribute.new('flag', "Warning: No supply node temperature timeseries found for '#{air_loop.name}'") next else # convert to ruby array flowrates = [] supply_flow_vector = supply_flow_timeseries.get.values for i in (0..supply_flow_vector.size - 1) flowrates << supply_flow_vector[i] end end # check reasonableness of supply air temperatures when supply air flow rate is operating flow_tolerance = OpenStudio.convert(10.0, 'cfm', 'm^3/s').get operating_temperatures = temperatures.select.with_index { |_t, k| flowrates[k] > flow_tolerance } operating_temperatures = operating_temperatures.map { |t| (t * 1.8 + 32.0) } next if operating_temperatures.empty? runtime_fraction = operating_temperatures.size.to_f / temperatures.size temps_out_of_bounds = operating_temperatures.select { |t| ((t < 40.0) || (t > 110.0) || ((t + max_operating_temp_delta) < expected_min) || ((t - max_operating_temp_delta) > expected_max)) } next if temps_out_of_bounds.empty? min_op_temp_f = temps_out_of_bounds.min max_op_temp_f = temps_out_of_bounds.max # avg_F = temps_out_of_bounds.inject(:+).to_f / temps_out_of_bounds.size err = [] err << 'Major Error:' err << "Expected supply air temperatures out of bounds for air loop '#{air_loop.name}'" err << "with #{design_cooling_sat.round(1)}F design cooling SAT" err << "and #{design_heating_sat.round(1)}F design heating SAT." unless is_unitary_system && !is_direct_evap err << "Air loop setpoint manager '#{spm_name}' of type '#{spm_type}' with a" err << "#{spm_min_temp_f.round(1)}F minimum setpoint temperature and" err << "#{spm_max_temp_f.round(1)}F maximum setpoint temperature." end if is_unitary_system && !is_direct_evap err << "Unitary system '#{unitary_system_name}' of type '#{unitary_system_type}' with" temp_str = unitary_min_temp_f.nil? ? 'no' : "#{unitary_min_temp_f.round(1)}F" err << "#{temp_str} minimum setpoint temperature and" temp_str = unitary_max_temp_f.nil? ? 'no' : "#{unitary_max_temp_f.round(1)}F" err << "#{temp_str} maximum setpoint temperature." end err << "Out of #{operating_temperatures.size}/#{temperatures.size} (#{(runtime_fraction * 100.0).round(1)}%) operating supply air temperatures" err << "#{temps_out_of_bounds.size}/#{operating_temperatures.size} (#{((temps_out_of_bounds.size.to_f / operating_temperatures.size) * 100.0).round(1)}%)" err << "are out of bounds with #{min_op_temp_f.round(1)}F min and #{max_op_temp_f.round(1)}F max." check_elems << OpenStudio::Attribute.new('flag', err.join(' ').gsub(/\n/, '')) end rescue StandardError => e # brief description of ruby error check_elems << OpenStudio::Attribute.new('flag', "Major Error: Error prevented QAQC check from running (#{e}).") # backtrace of ruby error for diagnostic use if @error_backtrace then check_elems << OpenStudio::Attribute.new('flag', e.backtrace.join("\n").to_s) end end # add check_elms to new attribute check_elem = OpenStudio::Attribute.new('check', check_elems) return check_elem end
Check the calibration against utility bills
@param category [String] category to bin this check into @param target_standard [String] standard template, e.g. ‘90.1-2013’ @param max_nmbe [Double] maximum allowable normalized mean bias error (NMBE), default 5.0% @param max_cvrmse [Double] maximum allowable coefficient of variation of the root mean square error (CVRMSE), default 15.0% @param name_only [Boolean] If true, only return the name of this check @return [OpenStudio::Attribute] OpenStudio Attribute object containing check results
# File lib/openstudio-standards/qaqc/calibration.rb, line 14 def self.check_calibration(category, target_standard, max_nmbe: 5.0, max_cvrmse: 15.0, name_only: false) # summary of the check check_elems = OpenStudio::AttributeVector.new check_elems << OpenStudio::Attribute.new('name', 'Calibration') check_elems << OpenStudio::Attribute.new('category', category) check_elems << OpenStudio::Attribute.new('description', 'Check that the model is calibrated to the utility bills.') # stop here if only name is requested this is used to populate display name for arguments if name_only == true results = [] check_elems.each do |elem| results << elem.valueAsString end return results end std = Standard.build(target_standard) begin # Check that there are utility bills in the model if @model.getUtilityBills.empty? check_elems << OpenStudio::Attribute.new('flag', 'Model contains no utility bills, cannot check calibration.') end # Check the calibration for each utility bill @model.getUtilityBills.each do |bill| bill_name = bill.name.get fuel = bill.fuelType.valueDescription # Consumption # NMBE if bill.NMBE.is_initialized nmbe = bill.NMBE.get if nmbe > max_nmbe || nmbe < -1.0 * max_nmbe check_elems << OpenStudio::Attribute.new('flag', "For the #{fuel} bill called #{bill_name}, the consumption NMBE of #{nmbe.round(1)}% is outside the limit of +/- #{max_nmbe}%, so the model is not calibrated.") end end # CVRMSE if bill.CVRMSE.is_initialized cvrmse = bill.CVRMSE.get if cvrmse > max_cvrmse check_elems << OpenStudio::Attribute.new('flag', "For the #{fuel} bill called #{bill_name}, the consumption CVRMSE of #{cvrmse.round(1)}% is above the limit of #{max_cvrmse}%, so the model is not calibrated.") end end # Peak Demand (for some fuels) if bill.peakDemandUnitConversionFactor.is_initialized peak_conversion = bill.peakDemandUnitConversionFactor.get # Get modeled and actual values actual_vals = [] modeled_vals = [] bill.billingPeriods.each do |billing_period| actual_peak = billing_period.peakDemand if actual_peak.is_initialized actual_vals << actual_peak.get end modeled_peak = billing_period.modelPeakDemand if modeled_peak.is_initialized modeled_vals << modeled_peak.get end end # Check that both arrays are the same size unless actual_vals.size == modeled_vals.size check_elems << OpenStudio::Attribute.new('flag', "For the #{fuel} bill called #{bill_name}, cannot get the same number of modeled and actual peak demand values, cannot check peak demand calibration.") end # NMBE and CMRMSE ysum = 0 sum_err = 0 squared_err = 0 n = actual_vals.size actual_vals.each_with_index do |actual, i| modeled = modeled_vals[i] actual *= peak_conversion # Convert actual demand to model units ysum += actual sum_err += (actual - modeled) squared_err += (actual - modeled)**2 end if n > 1 ybar = ysum / n # NMBE demand_nmbe = 100.0 * (sum_err / (n - 1)) / ybar if demand_nmbe > max_nmbe || demand_nmbe < -1.0 * max_nmbe check_elems << OpenStudio::Attribute.new('flag', "For the #{fuel} bill called #{bill_name}, the peak demand NMBE of #{demand_nmbe.round(1)}% is outside the limit of +/- #{max_nmbe}%, so the model is not calibrated.") end # CVRMSE demand_cvrmse = 100.0 * (squared_err / (n - 1))**0.5 / ybar if demand_cvrmse > max_cvrmse check_elems << OpenStudio::Attribute.new('flag', "For the #{fuel} bill called #{bill_name}, the peak demand CVRMSE of #{demand_cvrmse.round(1)}% is above the limit of #{max_cvrmse}%, so the model is not calibrated.") end end end end rescue StandardError => e # brief description of ruby error check_elems << OpenStudio::Attribute.new('flag', "Error prevented QAQC check from running (#{e}).") # backtrace of ruby error for diagnostic use if @error_backtrace then check_elems << OpenStudio::Attribute.new('flag', e.backtrace.join("\n").to_s) end end # add check_elms to new attribute check_elem = OpenStudio::Attribute.new('check', check_elems) return check_elem end
Check the envelope conductance against a standard
@param category [String] category to bin this check into @param target_standard [String] standard template, e.g. ‘90.1-2013’ @param min_pass_pct [Double] threshold for throwing an error for percent difference @param max_pass_pct [Double] threshold for throwing an error for percent difference @param name_only [Boolean] If true, only return the name of this check @return [OpenStudio::Attribute] OpenStudio Attribute object containing check results @todo unique tolerance ranges for conductance, reflectance, and shgc
# File lib/openstudio-standards/qaqc/envelope.rb, line 15 def self.check_envelope_conductance(category, target_standard, min_pass_pct: 0.2, max_pass_pct: 0.2, name_only: false) # summary of the check check_elems = OpenStudio::AttributeVector.new check_elems << OpenStudio::Attribute.new('name', 'Envelope R-Value') check_elems << OpenStudio::Attribute.new('category', category) if target_standard == 'ICC IECC 2015' dislay_standard = target_standard check_elems << OpenStudio::Attribute.new('description', "Check envelope against Table R402.1.2 and R402.1.4 in #{dislay_standard} Residential Provisions.") elsif target_standard.include?('90.1-2013') display_standard = "ASHRAE #{target_standard}" check_elems << OpenStudio::Attribute.new('description', "Check envelope against #{display_standard} Table 5.5.2, Table G2.1.5 b,c,d,e, Section 5.5.3.1.1a. Roof reflectance of 55%, wall reflectance of 30%.") else # @todo could add more elsifs if want to dsiplay tables and sections for additional 90.1 standards if target_standard.include?('90.1') display_standard = "ASHRAE #{target_standard}" else display_standard = target_standard end check_elems << OpenStudio::Attribute.new('description', "Check envelope against #{display_standard}. Roof reflectance of 55%, wall reflectance of 30%.") end # stop here if only name is requested this is used to populate display name for arguments if name_only == true results = [] check_elems.each do |elem| results << elem.valueAsString end return results end std = Standard.build(target_standard) # list of surface types to identify for each space type for surfaces and sub-surfaces construction_type_array = [] construction_type_array << ['ExteriorWall', 'SteelFramed'] construction_type_array << ['ExteriorRoof', 'IEAD'] construction_type_array << ['ExteriorFloor', 'Mass'] construction_type_array << ['ExteriorDoor', 'Swinging'] construction_type_array << ['ExteriorWindow', 'Metal framing (all other)'] construction_type_array << ['Skylight', 'Glass with Curb'] # overhead door doesn't show in list, or glass door begin # loop through all space types used in the model @model.getSpaceTypes.sort.each do |space_type| next if space_type.floorArea <= 0 space_type_const_properties = {} construction_type_array.each do |const_type| # gather data for exterior wall intended_surface_type = const_type[0] standards_construction_type = const_type[1] space_type_const_properties[intended_surface_type] = {} data = std.space_type_get_construction_properties(space_type, intended_surface_type, standards_construction_type) if data.nil? puts "lookup for #{target_standard},#{intended_surface_type},#{standards_construction_type}" check_elems << OpenStudio::Attribute.new('flag', "Didn't find construction for #{standards_construction_type} #{intended_surface_type} for #{space_type.name}.") elsif ['ExteriorWall', 'ExteriorFloor', 'ExteriorDoor'].include? intended_surface_type space_type_const_properties[intended_surface_type]['u_value'] = data['assembly_maximum_u_value'] space_type_const_properties[intended_surface_type]['reflectance'] = 0.30 # hard coded value elsif intended_surface_type == 'ExteriorRoof' space_type_const_properties[intended_surface_type]['u_value'] = data['assembly_maximum_u_value'] space_type_const_properties[intended_surface_type]['reflectance'] = 0.55 # hard coded value else space_type_const_properties[intended_surface_type]['u_value'] = data['assembly_maximum_u_value'] space_type_const_properties[intended_surface_type]['shgc'] = data['assembly_maximum_solar_heat_gain_coefficient'] end end # make array of construction details for surfaces surface_details = [] missing_surface_constructions = [] sub_surface_details = [] missing_sub_surface_constructions = [] # loop through spaces space_type.spaces.each do |space| space.surfaces.each do |surface| next if surface.outsideBoundaryCondition != 'Outdoors' if surface.construction.is_initialized surface_details << { boundary_condition: surface.outsideBoundaryCondition, surface_type: surface.surfaceType, construction: surface.construction.get } else missing_surface_constructions << surface.name.get end # make array of construction details for sub_surfaces surface.subSurfaces.each do |sub_surface| if sub_surface.construction.is_initialized sub_surface_details << { boundary_condition: sub_surface.outsideBoundaryCondition, surface_type: sub_surface.subSurfaceType, construction: sub_surface.construction.get } else missing_sub_surface_constructions << sub_surface.name.get end end end end if !missing_surface_constructions.empty? check_elems << OpenStudio::Attribute.new('flag', "#{missing_surface_constructions.size} surfaces are missing constructions in #{space_type.name}. Spaces and can't be checked.") end if !missing_sub_surface_constructions.empty? check_elems << OpenStudio::Attribute.new('flag', "#{missing_sub_surface_constructions.size} sub surfaces are missing constructions in #{space_type.name}. Spaces and can't be checked.") end # gather target values for this space type # @todo address support for other surface types e.g. overhead door glass door target_r_value_ip = {} target_reflectance = {} target_u_value_ip = {} target_shgc = {} target_r_value_ip['Wall'] = 1.0 / space_type_const_properties['ExteriorWall']['u_value'].to_f target_reflectance['Wall'] = space_type_const_properties['ExteriorWall']['reflectance'].to_f target_r_value_ip['RoofCeiling'] = 1.0 / space_type_const_properties['ExteriorRoof']['u_value'].to_f target_reflectance['RoofCeiling'] = space_type_const_properties['ExteriorRoof']['reflectance'].to_f target_r_value_ip['Floor'] = 1.0 / space_type_const_properties['ExteriorFloor']['u_value'].to_f target_reflectance['Floor'] = space_type_const_properties['ExteriorFloor']['reflectance'].to_f target_r_value_ip['Door'] = 1.0 / space_type_const_properties['ExteriorDoor']['u_value'].to_f target_reflectance['Door'] = space_type_const_properties['ExteriorDoor']['reflectance'].to_f target_u_value_ip['FixedWindow'] = space_type_const_properties['ExteriorWindow']['u_value'].to_f target_shgc['FixedWindow'] = space_type_const_properties['ExteriorWindow']['shgc'].to_f target_u_value_ip['OperableWindow'] = space_type_const_properties['ExteriorWindow']['u_value'].to_f target_shgc['OperableWindow'] = space_type_const_properties['ExteriorWindow']['shgc'].to_f target_u_value_ip['Skylight'] = space_type_const_properties['Skylight']['u_value'].to_f target_shgc['Skylight'] = space_type_const_properties['Skylight']['shgc'].to_f # loop through unique construction array combinations surface_details.uniq.each do |surface_detail| if surface_detail[:construction].thermalConductance.is_initialized # don't use intended surface type of construction, look map based on surface type and boundary condition boundary_condition = surface_detail[:boundary_condition] surface_type = surface_detail[:surface_type] intended_surface_type = '' if boundary_condition.to_s == 'Outdoors' case surface_type.to_s when 'Wall' intended_surface_type = 'ExteriorWall' when 'RoofCeiling' intended_surface_type = 'ExteriorRoof' when 'Floor' intended_surface_type = 'ExteriorFloor' end end film_coefficients_r_value = OpenstudioStandards::Constructions.film_coefficients_r_value(intended_surface_type, includes_int_film = true, includes_ext_film = true) thermal_conductance = surface_detail[:construction].thermalConductance.get r_value_with_film = 1 / thermal_conductance + film_coefficients_r_value source_units = 'm^2*K/W' target_units = 'ft^2*h*R/Btu' r_value_ip = OpenStudio.convert(r_value_with_film, source_units, target_units).get solar_reflectance = surface_detail[:construction].to_LayeredConstruction.get.layers[0].to_OpaqueMaterial.get.solarReflectance.get # @todo check with exterior air wall # stop if didn't find values (0 or infinity) next if target_r_value_ip[surface_detail[:surface_type]] == 0.0 next if target_r_value_ip[surface_detail[:surface_type]] == Float::INFINITY # check r avlues if r_value_ip < target_r_value_ip[surface_detail[:surface_type]] * (1.0 - min_pass_pct) check_elems << OpenStudio::Attribute.new('flag', "R value of #{r_value_ip.round(2)} (#{target_units}) for #{surface_detail[:construction].name} in #{space_type.name} is more than #{min_pass_pct * 100} % below the expected value of #{target_r_value_ip[surface_detail[:surface_type]].round(2)} (#{target_units}) for #{display_standard}.") elsif r_value_ip > target_r_value_ip[surface_detail[:surface_type]] * (1.0 + max_pass_pct) check_elems << OpenStudio::Attribute.new('flag', "R value of #{r_value_ip.round(2)} (#{target_units}) for #{surface_detail[:construction].name} in #{space_type.name} is more than #{max_pass_pct * 100} % above the expected value of #{target_r_value_ip[surface_detail[:surface_type]].round(2)} (#{target_units}) for #{display_standard}.") end # check solar reflectance if (solar_reflectance < target_reflectance[surface_detail[:surface_type]] * (1.0 - min_pass_pct)) && (target_standard != 'ICC IECC 2015') check_elems << OpenStudio::Attribute.new('flag', "Solar Reflectance of #{(solar_reflectance * 100).round} % for #{surface_detail[:construction].name} in #{space_type.name} is more than #{min_pass_pct * 100} % below the expected value of #{(target_reflectance[surface_detail[:surface_type]] * 100).round} %.") elsif (solar_reflectance > target_reflectance[surface_detail[:surface_type]] * (1.0 + max_pass_pct)) && (target_standard != 'ICC IECC 2015') check_elems << OpenStudio::Attribute.new('flag', "Solar Reflectance of #{(solar_reflectance * 100).round} % for #{surface_detail[:construction].name} in #{space_type.name} is more than #{max_pass_pct * 100} % above the expected value of #{(target_reflectance[surface_detail[:surface_type]] * 100).round} %.") end else check_elems << OpenStudio::Attribute.new('flag', "Can't calculate R value for #{surface_detail[:construction].name}.") end end # loop through unique construction array combinations sub_surface_details.uniq.each do |sub_surface_detail| if sub_surface_detail[:surface_type] == 'FixedWindow' || sub_surface_detail[:surface_type] == 'OperableWindow' || sub_surface_detail[:surface_type] == 'Skylight' # check for non opaque sub surfaces source_units = 'W/m^2*K' target_units = 'Btu/ft^2*h*R' surface_construction = sub_surface_detail[:construction].to_LayeredConstruction.get u_factor_si = OpenstudioStandards::Constructions.construction_get_conductance(surface_construction) u_factor_ip = OpenStudio.convert(u_factor_si, source_units, target_units).get shgc = OpenstudioStandards::Constructions.construction_get_solar_transmittance(surface_construction) # stop if didn't find values (0 or infinity) next if target_u_value_ip[sub_surface_detail[:surface_type]] == 0.0 next if target_u_value_ip[sub_surface_detail[:surface_type]] == Float::INFINITY # check u avlues if u_factor_ip < target_u_value_ip[sub_surface_detail[:surface_type]] * (1.0 - min_pass_pct) check_elems << OpenStudio::Attribute.new('flag', "U value of #{u_factor_ip.round(2)} (#{target_units}) for #{sub_surface_detail[:construction].name} in #{space_type.name} is more than #{min_pass_pct * 100} % below the expected value of #{target_u_value_ip[sub_surface_detail[:surface_type]].round(2)} (#{target_units}) for #{display_standard}.") elsif u_factor_ip > target_u_value_ip[sub_surface_detail[:surface_type]] * (1.0 + max_pass_pct) check_elems << OpenStudio::Attribute.new('flag', "U value of #{u_factor_ip.round(2)} (#{target_units}) for #{sub_surface_detail[:construction].name} in #{space_type.name} is more than #{max_pass_pct * 100} % above the expected value of #{target_u_value_ip[sub_surface_detail[:surface_type]].round(2)} (#{target_units}) for #{display_standard}.") end # check shgc if shgc < target_shgc[sub_surface_detail[:surface_type]] * (1.0 - min_pass_pct) check_elems << OpenStudio::Attribute.new('flag', "SHGC of #{shgc.round(2)} % for #{sub_surface_detail[:construction].name} in #{space_type.name} is more than #{min_pass_pct * 100} % below the expected value of #{target_shgc[sub_surface_detail[:surface_type]].round(2)} %.") elsif shgc > target_shgc[sub_surface_detail[:surface_type]] * (1.0 + max_pass_pct) check_elems << OpenStudio::Attribute.new('flag', "SHGC of #{shgc.round(2)} % for #{sub_surface_detail[:construction].name} in #{space_type.name} is more than #{max_pass_pct * 100} % above the expected value of #{target_shgc[sub_surface_detail[:surface_type]].round(2)} %.") end else # check for opaque sub surfaces if sub_surface_detail[:construction].thermalConductance.is_initialized # don't use intended surface type of construction, look map based on surface type and boundary condition boundary_condition = sub_surface_detail[:boundary_condition] surface_type = sub_surface_detail[:surface_type] intended_surface_type = '' if boundary_condition.to_s == 'Outdoors' # @todo add additional intended surface types if surface_type.to_s == 'Door' then intended_surface_type = 'ExteriorDoor' end end film_coefficients_r_value = OpenstudioStandards::Constructions.film_coefficients_r_value(intended_surface_type, includes_int_film = true, includes_ext_film = true) thermal_conductance = sub_surface_detail[:construction].thermalConductance.get r_value_with_film = 1 / thermal_conductance + film_coefficients_r_value source_units = 'm^2*K/W' target_units = 'ft^2*h*R/Btu' r_value_ip = OpenStudio.convert(r_value_with_film, source_units, target_units).get solar_reflectance = sub_surface_detail[:construction].to_LayeredConstruction.get.layers[0].to_OpaqueMaterial.get.solarReflectance.get # @todo check what happens with exterior air wall # stop if didn't find values (0 or infinity) next if target_r_value_ip[sub_surface_detail[:surface_type]] == 0.0 next if target_r_value_ip[sub_surface_detail[:surface_type]] == Float::INFINITY # check r avlues if r_value_ip < target_r_value_ip[sub_surface_detail[:surface_type]] * (1.0 - min_pass_pct) check_elems << OpenStudio::Attribute.new('flag', "R value of #{r_value_ip.round(2)} (#{target_units}) for #{sub_surface_detail[:construction].name} in #{space_type.name} is more than #{min_pass_pct * 100} % below the expected value of #{target_r_value_ip[sub_surface_detail[:surface_type]].round(2)} (#{target_units}) for #{display_standard}.") elsif r_value_ip > target_r_value_ip[sub_surface_detail[:surface_type]] * (1.0 + max_pass_pct) check_elems << OpenStudio::Attribute.new('flag', "R value of #{r_value_ip.round(2)} (#{target_units}) for #{sub_surface_detail[:construction].name} in #{space_type.name} is more than #{max_pass_pct * 100} % above the expected value of #{target_r_value_ip[sub_surface_detail[:surface_type]].round(2)} (#{target_units}) for #{display_standard}.") end # check solar reflectance if (solar_reflectance < target_reflectance[sub_surface_detail[:surface_type]] * (1.0 - min_pass_pct)) && (target_standard != 'ICC IECC 2015') check_elems << OpenStudio::Attribute.new('flag', "Solar Reflectance of #{(solar_reflectance * 100).round} % for #{sub_surface_detail[:construction].name} in #{space_type.name} is more than #{min_pass_pct * 100} % below the expected value of #{(target_reflectance[sub_surface_detail[:surface_type]] * 100).round} %.") elsif (solar_reflectance > target_reflectance[sub_surface_detail[:surface_type]] * (1.0 + max_pass_pct)) && (target_standard != 'ICC IECC 2015') check_elems << OpenStudio::Attribute.new('flag', "Solar Reflectance of #{(solar_reflectance * 100).round} % for #{sub_surface_detail[:construction].name} in #{space_type.name} is more than #{max_pass_pct * 100} % above the expected value of #{(target_reflectance[sub_surface_detail[:surface_type]] * 100).round} %.") end else check_elems << OpenStudio::Attribute.new('flag', "Can't calculate R value for #{sub_surface_detail[:construction].name}.") end end end end # check spaces without space types against Nonresidential for this climate zone @model.getSpaces.sort.each do |space| unless space.spaceType.is_initialized # make array of construction details for surfaces surface_details = [] missing_surface_constructions = [] sub_surface_details = [] missing_sub_surface_constructions = [] space.surfaces.each do |surface| next if surface.outsideBoundaryCondition != 'Outdoors' if surface.construction.is_initialized surface_details << { boundary_condition: surface.outsideBoundaryCondition, surface_type: surface.surfaceType, construction: surface.construction.get } else missing_surface_constructions << surface.name.get end # make array of construction details for sub_surfaces surface.subSurfaces.each do |sub_surface| if sub_surface.construction.is_initialized sub_surface_details << { boundary_condition: sub_surface.outsideBoundaryCondition, surface_type: sub_surface.subSurfaceType, construction: sub_surface.construction.get } else missing_sub_surface_constructions << sub_surface.name.get end end end unless missing_surface_constructions.empty? check_elems << OpenStudio::Attribute.new('flag', "#{missing_surface_constructions.size} surfaces are missing constructions in #{space_type.name}. Spaces and can't be checked.") end unless missing_sub_surface_constructions.empty? check_elems << OpenStudio::Attribute.new('flag', "#{missing_sub_surface_constructions.size} sub surfaces are missing constructions in #{space_type.name}. Spaces and can't be checked.") end surface_details.uniq.each do |surface_detail| if surface_detail[:construction].thermalConductance.is_initialized # don't use intended surface type of construction, look map based on surface type and boundary condition boundary_condition = surface_detail[:boundary_condition] surface_type = surface_detail[:surface_type] intended_surface_type = '' if boundary_condition.to_s == 'Outdoors' case surface_type.to_s when 'Wall' intended_surface_type = 'ExteriorWall' standards_construction_type = 'SteelFramed' when 'RoofCeiling' intended_surface_type = 'ExteriorRoof' standards_construction_type = 'IEAD' when 'Floor' intended_surface_type = 'ExteriorFloor' standards_construction_type = 'Mass' end end film_coefficients_r_value = OpenstudioStandards::Constructions.film_coefficients_r_value(intended_surface_type, includes_int_film = true, includes_ext_film = true) thermal_conductance = surface_detail[:construction].thermalConductance.get r_value_with_film = 1 / thermal_conductance + film_coefficients_r_value source_units = 'm^2*K/W' target_units = 'ft^2*h*R/Btu' r_value_ip = OpenStudio.convert(r_value_with_film, source_units, target_units).get solar_reflectance = surface_detail[:construction].to_LayeredConstruction.get.layers[0].to_OpaqueMaterial.get.solarReflectance.get # @todo check what happens with exterior air wall # calculate target_r_value_ip target_reflectance = nil data = std.model_get_construction_properties(@model, intended_surface_type, standards_construction_type) if data.nil? check_elems << OpenStudio::Attribute.new('flag', "Didn't find construction for #{standards_construction_type} #{intended_surface_type} for #{space.name}.") next elsif ['ExteriorWall', 'ExteriorFloor', 'ExteriorDoor'].include? intended_surface_type assembly_maximum_u_value = data['assembly_maximum_u_value'] target_reflectance = 0.30 elsif intended_surface_type == 'ExteriorRoof' assembly_maximum_u_value = data['assembly_maximum_u_value'] target_reflectance = 0.55 else assembly_maximum_u_value = data['assembly_maximum_u_value'] assembly_maximum_solar_heat_gain_coefficient = data['assembly_maximum_solar_heat_gain_coefficient'] end assembly_maximum_r_value_ip = 1 / assembly_maximum_u_value # stop if didn't find values (0 or infinity) next if assembly_maximum_r_value_ip == 0.0 next if assembly_maximum_r_value_ip == Float::INFINITY # check r avlues if r_value_ip < assembly_maximum_r_value_ip * (1.0 - min_pass_pct) check_elems << OpenStudio::Attribute.new('flag', "R value of #{r_value_ip.round(2)} (#{target_units}) for #{surface_detail[:construction].name} in #{space.name} is more than #{min_pass_pct * 100} % below the expected value of #{assembly_maximum_r_value_ip.round(2)} (#{target_units}) for #{display_standard}.") elsif r_value_ip > assembly_maximum_r_value_ip * (1.0 + max_pass_pct) check_elems << OpenStudio::Attribute.new('flag', "R value of #{r_value_ip.round(2)} (#{target_units}) for #{surface_detail[:construction].name} in #{space.name} is more than #{max_pass_pct * 100} % above the expected value of #{assembly_maximum_r_value_ip.round(2)} (#{target_units}) for #{display_standard}.") end # check solar reflectance if (solar_reflectance < target_reflectance * (1.0 - min_pass_pct)) && (target_standard != 'ICC IECC 2015') check_elems << OpenStudio::Attribute.new('flag', "Solar Reflectance of #{(solar_reflectance * 100).round} % for #{surface_detail[:construction].name} in #{space.name} is more than #{min_pass_pct * 100} % below the expected value of #{(target_reflectance * 100).round} %.") elsif (solar_reflectance > target_reflectance * (1.0 + max_pass_pct)) && (target_standard != 'ICC IECC 2015') check_elems << OpenStudio::Attribute.new('flag', "Solar Reflectance of #{(solar_reflectance * 100).round} % for #{surface_detail[:construction].name} in #{space.name} is more than #{max_pass_pct * 100} % above the expected value of #{(target_reflectance * 100).round} %.") end else check_elems << OpenStudio::Attribute.new('flag', "Can't calculate R value for #{surface_detail[:construction].name}.") end end sub_surface_details.uniq.each do |sub_surface_detail| # @todo update this so it works for doors and windows check_elems << OpenStudio::Attribute.new('flag', "Not setup to check sub-surfaces of spaces without space types. Can't check properties for #{sub_surface_detail[:construction].name}.") end end end rescue StandardError => e # brief description of ruby error check_elems << OpenStudio::Attribute.new('flag', "Error prevented QAQC check from running (#{e}).") # backtrace of ruby error for diagnostic use if @error_backtrace then check_elems << OpenStudio::Attribute.new('flag', e.backtrace.join("\n").to_s) end end # add check_elms to new attribute check_elem = OpenStudio::Attribute.new('check', check_elems) return check_elem end
Checks EUI for reasonableness
@param category [String] category to bin this check into @param target_standard [String] standard template, e.g. ‘90.1-2013’ @param min_pass_pct [Double] threshold for throwing an error for percent difference @param max_pass_pct [Double] threshold for throwing an error for percent difference @param name_only [Boolean] If true, only return the name of this check @return [OpenStudio::Attribute] OpenStudio Attribute object containing check results
# File lib/openstudio-standards/qaqc/eui.rb, line 14 def self.check_eui(category, target_standard, min_pass_pct: 0.1, max_pass_pct: 0.1, name_only: false) # summary of the check check_elems = OpenStudio::AttributeVector.new check_elems << OpenStudio::Attribute.new('name', 'EUI Reasonableness') check_elems << OpenStudio::Attribute.new('category', category) check_elems << OpenStudio::Attribute.new('description', "Check EUI for model against #{target_standard} DOE prototype buildings.") # stop here if only name is requested this is used to populate display name for arguments if name_only == true results = [] check_elems.each do |elem| results << elem.valueAsString end return results end std = Standard.build(target_standard) begin # total building area query = 'SELECT Value FROM tabulardatawithstrings WHERE ' query << "ReportName='AnnualBuildingUtilityPerformanceSummary' and " query << "ReportForString='Entire Facility' and " query << "TableName='Building Area' and " query << "RowName='Total Building Area' and " query << "ColumnName='Area' and " query << "Units='m2';" query_results = @sql.execAndReturnFirstDouble(query) if query_results.empty? check_elems << OpenStudio::Attribute.new('flag', "Can't calculate EUI, SQL query for building area failed.") return OpenStudio::Attribute.new('check', check_elems) else energy_plus_area = query_results.get end # temp code to check OS vs. E+ area open_studio_area = @model.getBuilding.floorArea if (energy_plus_area - open_studio_area).abs >= 0.1 check_elems << OpenStudio::Attribute.new('flag', "EnergyPlus reported area is #{energy_plus_area} (m^2). OpenStudio reported area is #{@model.getBuilding.floorArea} (m^2).") end # EUI source_units = 'GJ/m^2' target_units = 'kBtu/ft^2' if energy_plus_area > 0.0 # don't calculate EUI if building doesn't have any area # todo - netSiteEnergy deducts for renewable. May want to update this to show gross consumption vs. net consumption eui = @sql.netSiteEnergy.get / energy_plus_area else check_elems << OpenStudio::Attribute.new('flag', "Can't calculate model EUI, building doesn't have any floor area.") return OpenStudio::Attribute.new('check', check_elems) end # test using new method std = Standard.build(target_standard) target_eui = std.model_find_target_eui(@model) # check model vs. target for user specified tolerance. if !target_eui.nil? eui_ip_neat = OpenStudio.toNeatString(OpenStudio.convert(eui, source_units, target_units).get, 1, true) target_eui_ip_neat = OpenStudio.toNeatString(OpenStudio.convert(target_eui, source_units, target_units).get, 1, true) if eui < target_eui * (1.0 - min_pass_pct) check_elems << OpenStudio::Attribute.new('flag', "Model EUI of #{eui_ip_neat} (#{target_units}) is less than #{min_pass_pct * 100} % below the expected EUI of #{target_eui_ip_neat} (#{target_units}) for #{target_standard}.") elsif eui > target_eui * (1.0 + max_pass_pct) check_elems << OpenStudio::Attribute.new('flag', "Model EUI of #{eui_ip_neat} (#{target_units}) is more than #{max_pass_pct * 100} % above the expected EUI of #{target_eui_ip_neat} (#{target_units}) for #{target_standard}.") end else check_elems << OpenStudio::Attribute.new('flag', "Can't calculate target EUI. Make sure model has expected climate zone and building type.") end rescue StandardError => e # brief description of ruby error check_elems << OpenStudio::Attribute.new('flag', "Error prevented QAQC check from running (#{e}).") # backtrace of ruby error for diagnostic use if @error_backtrace then check_elems << OpenStudio::Attribute.new('flag', e.backtrace.join("\n").to_s) end end # add check_elms to new attribute check_elem = OpenStudio::Attribute.new('check', check_elems) return check_elem end
Checks end use EUIs for reasonableness
@param category [String] category to bin this check into @param target_standard [String] standard template, e.g. ‘90.1-2013’ @param min_pass_pct [Double] threshold for throwing an error for percent difference @param max_pass_pct [Double] threshold for throwing an error for percent difference @param name_only [Boolean] If true, only return the name of this check @return [OpenStudio::Attribute] OpenStudio Attribute object containing check results
# File lib/openstudio-standards/qaqc/eui.rb, line 104 def self.check_eui_by_end_use(category, target_standard, min_pass_pct: 0.2, max_pass_pct: 0.2, name_only: false) # summary of the check check_elems = OpenStudio::AttributeVector.new check_elems << OpenStudio::Attribute.new('name', 'End Use by Category') check_elems << OpenStudio::Attribute.new('category', category) check_elems << OpenStudio::Attribute.new('description', "Check end use by category against #{target_standard} DOE prototype buildings.") # stop here if only name is requested this is used to populate display name for arguments if name_only == true results = [] check_elems.each do |elem| results << elem.valueAsString end return results end std = Standard.build(target_standard) begin # total building area query = 'SELECT Value FROM tabulardatawithstrings WHERE ' query << "ReportName='AnnualBuildingUtilityPerformanceSummary' and " query << "ReportForString='Entire Facility' and " query << "TableName='Building Area' and " query << "RowName='Total Building Area' and " query << "ColumnName='Area' and " query << "Units='m2';" query_results = @sql.execAndReturnFirstDouble(query) if query_results.empty? check_elems << OpenStudio::Attribute.new('flag', "Can't calculate EUI, SQL query for building area failed.") return OpenStudio::Attribute.new('check', check_elems) else energy_plus_area = query_results.get end # temp code to check OS vs. E+ area open_studio_area = @model.getBuilding.floorArea if (energy_plus_area - open_studio_area).abs >= 0.1 check_elems << OpenStudio::Attribute.new('flag', "EnergyPlus reported area is #{energy_plus_area} (m^2). OpenStudio reported area is #{@model.getBuilding.floorArea} (m^2).") end # loop through end uses and gather consumption, normalized by floor area actual_eui_by_end_use = {} OpenStudio::EndUseCategoryType.getValues.each do |end_use| # get end uses end_use = OpenStudio::EndUseCategoryType.new(end_use).valueDescription query_elec = "SELECT Value FROM tabulardatawithstrings WHERE ReportName='AnnualBuildingUtilityPerformanceSummary' and TableName='End Uses' and RowName= '#{end_use}' and ColumnName= 'Electricity'" query_gas = "SELECT Value FROM tabulardatawithstrings WHERE ReportName='AnnualBuildingUtilityPerformanceSummary' and TableName='End Uses' and RowName= '#{end_use}' and ColumnName= 'Natural Gas'" query_add = "SELECT Value FROM tabulardatawithstrings WHERE ReportName='AnnualBuildingUtilityPerformanceSummary' and TableName='End Uses' and RowName= '#{end_use}' and ColumnName= 'Additional Fuel'" query_dc = "SELECT Value FROM tabulardatawithstrings WHERE ReportName='AnnualBuildingUtilityPerformanceSummary' and TableName='End Uses' and RowName= '#{end_use}' and ColumnName= 'District Cooling'" query_dh = "SELECT Value FROM tabulardatawithstrings WHERE ReportName='AnnualBuildingUtilityPerformanceSummary' and TableName='End Uses' and RowName= '#{end_use}' and ColumnName= 'District Heating'" results_elec = @sql.execAndReturnFirstDouble(query_elec).get results_gas = @sql.execAndReturnFirstDouble(query_gas).get results_add = @sql.execAndReturnFirstDouble(query_add).get results_dc = @sql.execAndReturnFirstDouble(query_dc).get results_dh = @sql.execAndReturnFirstDouble(query_dh).get total_end_use = results_elec + results_gas + results_add + results_dc + results_dh # populate hash for actual end use normalized by area actual_eui_by_end_use[end_use] = total_end_use / energy_plus_area end # gather target end uses for given standard as hash std = Standard.build(target_standard) target_eui_by_end_use = std.model_find_target_eui_by_end_use(@model) # units for flag display text and unit conversion source_units = 'GJ/m^2' target_units = 'kBtu/ft^2' # check acutal vs. target against tolerance if !target_eui_by_end_use.nil? actual_eui_by_end_use.each do |end_use, value| # this should have value of 0 in model. This page change in the future. It doesn't exist in target lookup next if end_use == 'Exterior Equipment' # perform check and issue flags as needed target_value = target_eui_by_end_use[end_use] eui_ip_neat = OpenStudio.toNeatString(OpenStudio.convert(value, source_units, target_units).get, 5, true) target_eui_ip_neat = OpenStudio.toNeatString(OpenStudio.convert(target_value, source_units, target_units).get, 1, true) # add in use case specific logic to skip checks when near 0 actual and target skip = false if (end_use == 'Heat Recovery') && (value < 0.05) && (target_value < 0.05) then skip = true end if (end_use == 'Pumps') && (value < 0.05) && (target_value < 0.05) then skip = true end if (value < target_value * (1.0 - min_pass_pct)) && !skip check_elems << OpenStudio::Attribute.new('flag', "#{end_use} EUI of #{eui_ip_neat} (#{target_units}) is less than #{min_pass_pct * 100} % below the expected #{end_use} EUI of #{target_eui_ip_neat} (#{target_units}) for #{target_standard}.") elsif (value > target_value * (1.0 + max_pass_pct)) && !skip check_elems << OpenStudio::Attribute.new('flag', "#{end_use} EUI of #{eui_ip_neat} (#{target_units}) is more than #{max_pass_pct * 100} % above the expected #{end_use} EUI of #{target_eui_ip_neat} (#{target_units}) for #{target_standard}.") end end else check_elems << OpenStudio::Attribute.new('flag', "Can't calculate target end use EUIs. Make sure model has expected climate zone and building type.") end rescue StandardError => e # brief description of ruby error check_elems << OpenStudio::Attribute.new('flag', "Error prevented QAQC check from running (#{e}).") # backtrace of ruby error for diagnostic use if @error_backtrace then check_elems << OpenStudio::Attribute.new('flag', e.backtrace.join("\n").to_s) end end # add check_elms to new attribute check_elem = OpenStudio::Attribute.new('check', check_elems) return check_elem end
Check mechanical equipment capacity against typical sizing
@param category [String] category to bin this check into @param target_standard [String] standard template, e.g. ‘90.1-2013’ @param name_only [Boolean] If true, only return the name of this check @return [OpenStudio::Attribute] OpenStudio Attribute object containing check results
# File lib/openstudio-standards/qaqc/hvac.rb, line 396 def self.check_hvac_capacity(category, target_standard, name_only: false) # summary of the check check_elems = OpenStudio::AttributeVector.new check_elems << OpenStudio::Attribute.new('name', 'Mechanical System Capacity') check_elems << OpenStudio::Attribute.new('category', category) check_elems << OpenStudio::Attribute.new('description', 'Check HVAC capacity against ASHRAE rules of thumb for chiller max flow rate, air loop max flow rate, air loop cooling capciaty, and zone heating capcaity. Zone heating check will skip thermal zones without any exterior exposure, and thermal zones that are not conditioned.') # stop here if only name is requested this is used to populate display name for arguments if name_only == true results = [] check_elems.each do |elem| results << elem.valueAsString end return results end std = Standard.build(target_standard) # Sizing benchmarks. Each option has a target value, min and max fractional tolerance, and units. # In the future climate zone specific targets may be in standards sizing_benchmarks = {} sizing_benchmarks['chiller_max_flow_rate'] = { 'min_error' => 1.5, 'min_warning' => 2.0, 'max_warning' => 3.0, 'max_error' => 3.5, 'units' => 'gal/ton*min' } sizing_benchmarks['air_loop_max_flow_rate'] = { 'min_error' => 0.2, 'min_warning' => 0.5, 'max_warning' => 2.0, 'max_error' => 4.0, 'units' => 'cfm/ft^2' } sizing_benchmarks['air_loop_cooling_capacity'] = { 'min_error' => 200.0, 'min_warning' => 300.0, 'max_warning' => 1500.0, 'max_error' => 2000.0, 'units' => 'ft^2/ton' } sizing_benchmarks['zone_heating_capacity'] = { 'min_error' => 4.0, 'min_warning' => 8.0, 'max_warning' => 30.0, 'max_error' => 60.0, 'units' => 'Btu/ft^2*h' } begin # check max flow rate of chillers in model @model.getPlantLoops.sort.each do |plant_loop| # next if no chiller on plant loop chillers = [] plant_loop.supplyComponents.each do |sc| if sc.to_ChillerElectricEIR.is_initialized chillers << sc.to_ChillerElectricEIR.get end end next if chillers.empty? # gather targets for chiller capacity chiller_max_flow_rate_min_error = sizing_benchmarks['chiller_max_flow_rate']['min_error'] chiller_max_flow_rate_min_warning = sizing_benchmarks['chiller_max_flow_rate']['min_warning'] chiller_max_flow_rate_max_warning = sizing_benchmarks['chiller_max_flow_rate']['max_warning'] chiller_max_flow_rate_max_error = sizing_benchmarks['chiller_max_flow_rate']['max_error'] chiller_max_flow_rate_units_ip = options['chiller_max_flow_rate']['units'] # get capacity of loop (not individual chiller but entire loop) total_cooling_capacity_w = std.plant_loop_total_cooling_capacity(plant_loop) total_cooling_capacity_ton = OpenStudio.convert(total_cooling_capacity_w, 'W', 'Btu/h').get / 12_000.0 # get the max flow rate (not individual chiller) maximum_loop_flow_rate = std.plant_loop_find_maximum_loop_flow_rate(plant_loop) maximum_loop_flow_rate_ip = OpenStudio.convert(maximum_loop_flow_rate, 'm^3/s', 'gal/min').get if total_cooling_capacity_ton < 0.01 check_elems << OpenStudio::Attribute.new('flag', "Cooling capacity for #{plant_loop.name.get} is too small for flow rate #{maximum_loop_flow_rate_ip.round(2)} gal/min.") end # calculate the flow per tons of cooling model_flow_rate_per_ton_cooling_ip = maximum_loop_flow_rate_ip / total_cooling_capacity_ton # check flow rate per capacity if model_flow_rate_per_ton_cooling_ip < chiller_max_flow_rate_min_error check_elems << OpenStudio::Attribute.new('flag', "Error: Flow Rate of #{model_flow_rate_per_ton_cooling_ip.round(2)} #{chiller_max_flow_rate_units_ip} for #{plant_loop.name.get} is below #{chiller_max_flow_rate_min_error.round(2)} #{chiller_max_flow_rate_units_ip}.") elsif model_flow_rate_per_ton_cooling_ip < chiller_max_flow_rate_min_warning check_elems << OpenStudio::Attribute.new('flag', "Warning: Flow Rate of #{model_flow_rate_per_ton_cooling_ip.round(2)} #{chiller_max_flow_rate_units_ip} for #{plant_loop.name.get} is below #{chiller_max_flow_rate_min_warning.round(2)} #{chiller_max_flow_rate_units_ip}.") elsif model_flow_rate_per_ton_cooling_ip > chiller_max_flow_rate_max_warning check_elems << OpenStudio::Attribute.new('flag', "Warning: Flow Rate of #{model_flow_rate_per_ton_cooling_ip.round(2)} #{chiller_max_flow_rate_units_ip} for #{plant_loop.name.get} is above #{chiller_max_flow_rate_max_warning.round(2)} #{chiller_max_flow_rate_units_ip}.") elsif model_flow_rate_per_ton_cooling_ip > chiller_max_flow_rate_max_error check_elems << OpenStudio::Attribute.new('flag', "Error: Flow Rate of #{model_flow_rate_per_ton_cooling_ip.round(2)} #{chiller_max_flow_rate_units_ip} for #{plant_loop.name.get} is above #{chiller_max_flow_rate_max_error.round(2)} #{chiller_max_flow_rate_units_ip}.") end end # loop through air loops to get max flow rate and cooling capacity. @model.getAirLoopHVACs.sort.each do |air_loop| # skip DOAS systems for now sizing_system = air_loop.sizingSystem next if sizing_system.typeofLoadtoSizeOn.to_s == 'VentilationRequirement' # gather argument sizing_benchmarks for air_loop_max_flow_rate checks air_loop_max_flow_rate_min_error = sizing_benchmarks['air_loop_max_flow_rate']['min_error'] air_loop_max_flow_rate_min_warning = sizing_benchmarks['air_loop_max_flow_rate']['min_warning'] air_loop_max_flow_rate_max_warning = sizing_benchmarks['air_loop_max_flow_rate']['max_warning'] air_loop_max_flow_rate_max_error = sizing_benchmarks['air_loop_max_flow_rate']['max_error'] air_loop_max_flow_rate_units_ip = sizing_benchmarks['air_loop_max_flow_rate']['units'] # get values from model for air loop checks floor_area_served = std.air_loop_hvac_floor_area_served(air_loop) design_supply_air_flow_rate = std.air_loop_hvac_find_design_supply_air_flow_rate(air_loop) # check max flow rate of air loops in the model model_normalized_flow_rate_si = design_supply_air_flow_rate / floor_area_served model_normalized_flow_rate_ip = OpenStudio.convert(model_normalized_flow_rate_si, 'm^3/m^2*s', air_loop_max_flow_rate_units_ip).get if model_normalized_flow_rate_ip < air_loop_max_flow_rate_min_error check_elems << OpenStudio::Attribute.new('flag', "Error: Flow Rate of #{model_normalized_flow_rate_ip.round(2)} #{air_loop_max_flow_rate_units_ip} for #{air_loop.name.get} is below #{air_loop_max_flow_rate_min_error.round(2)} #{air_loop_max_flow_rate_units_ip}.") elsif model_normalized_flow_rate_ip < air_loop_max_flow_rate_min_warning check_elems << OpenStudio::Attribute.new('flag', "Warning: Flow Rate of #{model_normalized_flow_rate_ip.round(2)} #{air_loop_max_flow_rate_units_ip} for #{air_loop.name.get} is below #{air_loop_max_flow_rate_min_warning.round(2)} #{air_loop_max_flow_rate_units_ip}.") elsif model_normalized_flow_rate_ip > air_loop_max_flow_rate_max_warning check_elems << OpenStudio::Attribute.new('flag', "Warning: Flow Rate of #{model_normalized_flow_rate_ip.round(2)} #{air_loop_max_flow_rate_units_ip} for #{air_loop.name.get} is above #{air_loop_max_flow_rate_max_warning.round(2)} #{air_loop_max_flow_rate_units_ip}.") elsif model_normalized_flow_rate_ip > air_loop_max_flow_rate_max_error check_elems << OpenStudio::Attribute.new('flag', "Error: Flow Rate of #{model_normalized_flow_rate_ip.round(2)} #{air_loop_max_flow_rate_units_ip} for #{air_loop.name.get} is above #{air_loop_max_flow_rate_max_error.round(2)} #{air_loop_max_flow_rate_units_ip}.") end end # loop through air loops to get max flow rate and cooling capacity. @model.getAirLoopHVACs.sort.each do |air_loop| # check if DOAS, don't check airflow or cooling capacity if it is sizing_system = air_loop.sizingSystem next if sizing_system.typeofLoadtoSizeOn.to_s == 'VentilationRequirement' # gather argument options for air_loop_cooling_capacity checks air_loop_cooling_capacity_min_error = sizing_benchmarks['air_loop_cooling_capacity']['min_error'] air_loop_cooling_capacity_min_warning = sizing_benchmarks['air_loop_cooling_capacity']['min_warning'] air_loop_cooling_capacity_max_warning = sizing_benchmarks['air_loop_cooling_capacity']['max_warning'] air_loop_cooling_capacity_max_error = sizing_benchmarks['air_loop_cooling_capacity']['max_error'] air_loop_cooling_capacity_units_ip = sizing_benchmarks['air_loop_cooling_capacity']['units'] # check cooling capacity of air loops in the model floor_area_served = std.air_loop_hvac_floor_area_served(air_loop) capacity = std.air_loop_hvac_total_cooling_capacity(air_loop) model_normalized_capacity_si = capacity / floor_area_served model_normalized_capacity_ip = OpenStudio.convert(model_normalized_capacity_si, 'W/m^2', 'Btu/ft^2*h').get / 12_000.0 # want to display in tons/ft^2 so invert number and display for checks model_tons_per_area_ip = 1.0 / model_normalized_capacity_ip if model_tons_per_area_ip < air_loop_cooling_capacity_min_error check_elems << OpenStudio::Attribute.new('flag', "Cooling Capacity of #{model_tons_per_area_ip.round} #{air_loop_cooling_capacity_units_ip} for #{air_loop.name.get} is below #{air_loop_cooling_capacity_min_error.round} #{air_loop_cooling_capacity_units_ip}.") elsif model_tons_per_area_ip < air_loop_cooling_capacity_min_warning check_elems << OpenStudio::Attribute.new('flag', "Cooling Capacity of #{model_tons_per_area_ip.round} #{air_loop_cooling_capacity_units_ip} for #{air_loop.name.get} is below #{air_loop_cooling_capacity_min_warning.round} #{air_loop_cooling_capacity_units_ip}.") elsif model_tons_per_area_ip > air_loop_cooling_capacity_max_warning check_elems << OpenStudio::Attribute.new('flag', "Cooling Capacity of #{model_tons_per_area_ip.round} #{air_loop_cooling_capacity_units_ip} for #{air_loop.name.get} is above #{air_loop_cooling_capacity_max_warning.round} #{air_loop_cooling_capacity_units_ip}.") elsif model_tons_per_area_ip > air_loop_cooling_capacity_max_error check_elems << OpenStudio::Attribute.new('flag', "Cooling Capacity of #{model_tons_per_area_ip.round} #{air_loop_cooling_capacity_units_ip} for #{air_loop.name.get} is above #{air_loop_cooling_capacity_max_error.round} #{air_loop_cooling_capacity_units_ip}.") end end # check heating capacity of thermal zones in the model with exterior exposure report_name = 'HVACSizingSummary' table_name = 'Zone Sensible Heating' column_name = 'User Design Load per Area' min_error = sizing_benchmarks['zone_heating_capacity']['min_error'] min_warning = sizing_benchmarks['zone_heating_capacity']['min_warning'] max_warning = sizing_benchmarks['zone_heating_capacity']['max_warning'] max_error = sizing_benchmarks['zone_heating_capacity']['max_error'] units_ip = sizing_benchmarks['zone_heating_capacity']['units'] @model.getThermalZones.sort.each do |thermal_zone| next if thermal_zone.canBePlenum next if thermal_zone.exteriorSurfaceArea == 0.0 # check actual against target query = "SELECT Value FROM tabulardatawithstrings WHERE ReportName='#{report_name}' and TableName='#{table_name}' and RowName= '#{thermal_zone.name.get.upcase}' and ColumnName= '#{column_name}'" results = @sql.execAndReturnFirstDouble(query) model_zone_heating_capacity_ip = OpenStudio.convert(results.to_f, 'W/m^2', units_ip).get if model_zone_heating_capacity_ip < min_error check_elems << OpenStudio::Attribute.new('flag', "Heating Capacity of #{model_zone_heating_capacity_ip.round(2)} Btu/ft^2*h for #{thermal_zone.name.get} is below #{min_error.round(1)} Btu/ft^2*h.") elsif model_zone_heating_capacity_ip < min_warning check_elems << OpenStudio::Attribute.new('flag', "Heating Capacity of #{model_zone_heating_capacity_ip.round(2)} Btu/ft^2*h for #{thermal_zone.name.get} is below #{min_warning.round(1)} Btu/ft^2*h.") elsif model_zone_heating_capacity_ip > max_warning check_elems << OpenStudio::Attribute.new('flag', "Heating Capacity of #{model_zone_heating_capacity_ip.round(2)} Btu/ft^2*h for #{thermal_zone.name.get} is above #{max_warning.round(1)} Btu/ft^2*h.") elsif model_zone_heating_capacity_ip > max_error check_elems << OpenStudio::Attribute.new('flag', "Heating Capacity of #{model_zone_heating_capacity_ip.round(2)} Btu/ft^2*h for #{thermal_zone.name.get} is above #{max_error.round(1)} Btu/ft^2*h.") end end rescue StandardError => e # brief description of ruby error check_elems << OpenStudio::Attribute.new('flag', "Error prevented QAQC check from running (#{e}).") # backtrace of ruby error for diagnostic use if @error_backtrace then check_elems << OpenStudio::Attribute.new('flag', e.backtrace.join("\n").to_s) end end # add check_elms to new attribute check_elem = OpenStudio::Attribute.new('check', check_elems) return check_elem end
Check the mechanical system efficiencies against a standard
@param category [String] category to bin this check into @param target_standard [String] standard template, e.g. ‘90.1-2013’ @param min_pass_pct [Double] threshold for throwing an error for percent difference @param max_pass_pct [Double] threshold for throwing an error for percent difference @param name_only [Boolean] If true, only return the name of this check @return [OpenStudio::Attribute] OpenStudio Attribute object containing check results
# File lib/openstudio-standards/qaqc/hvac.rb, line 581 def self.check_hvac_efficiency(category, target_standard, min_pass_pct: 0.3, max_pass_pct: 0.3, name_only: false) component_type_array = ['ChillerElectricEIR', 'CoilCoolingDXSingleSpeed', 'CoilCoolingDXTwoSpeed', 'CoilHeatingDXSingleSpeed', 'BoilerHotWater', 'FanConstantVolume', 'FanVariableVolume', 'PumpConstantSpeed', 'PumpVariableSpeed'] # summary of the check check_elems = OpenStudio::AttributeVector.new check_elems << OpenStudio::Attribute.new('name', 'Mechanical System Efficiency') check_elems << OpenStudio::Attribute.new('category', category) if target_standard.include?('90.1-2013') check_elems << OpenStudio::Attribute.new('description', "Check against #{target_standard} Tables 6.8.1 A-K for the following component types: #{component_type_array.join(', ')}.") else check_elems << OpenStudio::Attribute.new('description', "Check against #{target_standard} for the following component types: #{component_type_array.join(', ')}.") end # stop here if only name is requested this is used to populate display name for arguments if name_only == true results = [] check_elems.each do |elem| results << elem.valueAsString end return results end std = Standard.build(target_standard) begin # check ChillerElectricEIR objects (will also have curve check in different script) @model.getChillerElectricEIRs.sort.each do |component| # eff values from model reference_cop = component.referenceCOP # get eff values from standards (if name doesn't have expected strings find object returns first object of multiple) standard_minimum_full_load_efficiency = std.chiller_electric_eir_standard_minimum_full_load_efficiency(component) # check actual against target if standard_minimum_full_load_efficiency.nil? check_elems << OpenStudio::Attribute.new('flag', "Can't find target full load efficiency for #{component.name}.") elsif reference_cop < standard_minimum_full_load_efficiency * (1.0 - min_pass_pct) check_elems << OpenStudio::Attribute.new('flag', "COP of #{reference_cop.round(2)} for #{component.name} is more than #{min_pass_pct * 100} % below the expected value of #{standard_minimum_full_load_efficiency.round(2)}.") elsif reference_cop > standard_minimum_full_load_efficiency * (1.0 + max_pass_pct) check_elems << OpenStudio::Attribute.new('flag', "COP of #{reference_cop.round(2)} for #{component.name} is more than #{max_pass_pct * 100} % above the expected value of #{standard_minimum_full_load_efficiency.round(2)}.") end end # check CoilCoolingDXSingleSpeed objects (will also have curve check in different script) @model.getCoilCoolingDXSingleSpeeds.each do |component| # eff values from model rated_cop = component.ratedCOP.get # get eff values from standards standard_minimum_cop = std.coil_cooling_dx_single_speed_standard_minimum_cop(component) # check actual against target if standard_minimum_cop.nil? check_elems << OpenStudio::Attribute.new('flag', "Can't find target COP for #{component.name}.") elsif rated_cop < standard_minimum_cop * (1.0 - min_pass_pct) check_elems << OpenStudio::Attribute.new('flag', "The COP of #{rated_cop.round(2)} for #{component.name} is more than #{min_pass_pct * 100} % below the expected value of #{standard_minimum_cop.round(2)} for #{target_standard}.") elsif rated_cop > standard_minimum_cop * (1.0 + max_pass_pct) check_elems << OpenStudio::Attribute.new('flag', "The COP of #{rated_cop.round(2)} for #{component.name} is more than #{max_pass_pct * 100} % above the expected value of #{standard_minimum_cop.round(2)} for #{target_standard}.") end end # check CoilCoolingDXTwoSpeed objects (will also have curve check in different script) @model.getCoilCoolingDXTwoSpeeds.sort.each do |component| # eff values from model rated_high_speed_cop = component.ratedHighSpeedCOP.get rated_low_speed_cop = component.ratedLowSpeedCOP.get # get eff values from standards standard_minimum_cop = std.coil_cooling_dx_two_speed_standard_minimum_cop(component) # check actual against target if standard_minimum_cop.nil? check_elems << OpenStudio::Attribute.new('flag', "Can't find target COP for #{component.name}.") elsif rated_high_speed_cop < standard_minimum_cop * (1.0 - min_pass_pct) check_elems << OpenStudio::Attribute.new('flag', "The high speed COP of #{rated_high_speed_cop.round(2)} for #{component.name} is more than #{min_pass_pct * 100} % below the expected value of #{standard_minimum_cop.round(2)} for #{target_standard}.") elsif rated_high_speed_cop > standard_minimum_cop * (1.0 + max_pass_pct) check_elems << OpenStudio::Attribute.new('flag', "The high speed COP of #{rated_high_speed_cop.round(2)} for #{component.name} is more than #{max_pass_pct * 100} % above the expected value of #{standard_minimum_cop.round(2)} for #{target_standard}.") end if standard_minimum_cop.nil? check_elems << OpenStudio::Attribute.new('flag', "Can't find target COP for #{component.name}.") elsif rated_low_speed_cop < standard_minimum_cop * (1.0 - min_pass_pct) check_elems << OpenStudio::Attribute.new('flag', "The low speed COP of #{rated_low_speed_cop.round(2)} for #{component.name} is more than #{min_pass_pct * 100} % below the expected value of #{standard_minimum_cop.round(2)} for #{target_standard}.") elsif rated_low_speed_cop > standard_minimum_cop * (1.0 + max_pass_pct) check_elems << OpenStudio::Attribute.new('flag', "The low speed COP of #{rated_low_speed_cop.round(2)} for #{component.name} is more than #{max_pass_pct * 100} % above the expected value of #{standard_minimum_cop.round(2)} for #{target_standard}.") end end # check CoilHeatingDXSingleSpeed objects # @todo need to test this once json file populated for this data @model.getCoilHeatingDXSingleSpeeds.sort.each do |component| # eff values from model rated_cop = component.ratedCOP # get eff values from standards standard_minimum_cop = std.coil_heating_dx_single_speed_standard_minimum_cop(component) # check actual against target if standard_minimum_cop.nil? check_elems << OpenStudio::Attribute.new('flag', "Can't find target COP for #{component.name}.") elsif rated_cop < standard_minimum_cop * (1.0 - min_pass_pct) check_elems << OpenStudio::Attribute.new('flag', "The COP of #{rated_cop.round(2)} for #{component.name} is more than #{min_pass_pct * 100} % below the expected value of #{standard_minimum_cop.round(2)} for #{target_standard}.") elsif rated_cop > standard_minimum_cop * (1.0 + max_pass_pct) check_elems << OpenStudio::Attribute.new('flag', "The COP of #{rated_cop.round(2)} for #{component.name} is more than #{max_pass_pct * 100} % above the expected value of #{standard_minimum_cop.round(2)}. for #{target_standard}") end end # check BoilerHotWater @model.getBoilerHotWaters.sort.each do |component| # eff values from model nominal_thermal_efficiency = component.nominalThermalEfficiency # get eff values from standards standard_minimum_thermal_efficiency = std.boiler_hot_water_standard_minimum_thermal_efficiency(component) # check actual against target if standard_minimum_thermal_efficiency.nil? check_elems << OpenStudio::Attribute.new('flag', "Can't find target thermal efficiency for #{component.name}.") elsif nominal_thermal_efficiency < standard_minimum_thermal_efficiency * (1.0 - min_pass_pct) check_elems << OpenStudio::Attribute.new('flag', "Nominal thermal efficiency of #{nominal_thermal_efficiency.round(2)} for #{component.name} is more than #{min_pass_pct * 100} % below the expected value of #{standard_minimum_thermal_efficiency.round(2)} for #{target_standard}.") elsif nominal_thermal_efficiency > standard_minimum_thermal_efficiency * (1.0 + max_pass_pct) check_elems << OpenStudio::Attribute.new('flag', "Nominal thermal efficiency of #{nominal_thermal_efficiency.round(2)} for #{component.name} is more than #{max_pass_pct * 100} % above the expected value of #{standard_minimum_thermal_efficiency.round(2)} for #{target_standard}.") end end # check FanConstantVolume @model.getFanConstantVolumes.sort.each do |component| # eff values from model motor_eff = component.motorEfficiency # get eff values from standards motor_bhp = std.fan_brake_horsepower(component) standard_minimum_motor_efficiency_and_size = std.fan_standard_minimum_motor_efficiency_and_size(component, motor_bhp)[0] # check actual against target if standard_minimum_motor_efficiency_and_size.nil? check_elems << OpenStudio::Attribute.new('flag', "Can't find target motor efficiency for #{component.name}.") elsif motor_eff < standard_minimum_motor_efficiency_and_size * (1.0 - min_pass_pct) check_elems << OpenStudio::Attribute.new('flag', "Motor efficiency of #{motor_eff.round(2)} for #{component.name} is more than #{min_pass_pct * 100} % below the expected value of #{standard_minimum_motor_efficiency_and_size.round(2)} for #{target_standard}.") elsif motor_eff > standard_minimum_motor_efficiency_and_size * (1.0 + max_pass_pct) check_elems << OpenStudio::Attribute.new('flag', "Motor efficiency of #{motor_eff.round(2)} for #{component.name} is more than #{max_pass_pct * 100} % above the expected value of #{standard_minimum_motor_efficiency_and_size.round(2)} for #{target_standard}.") end end # check FanVariableVolume @model.getFanVariableVolumes.sort.each do |component| # eff values from model motor_eff = component.motorEfficiency # get eff values from standards motor_bhp = std.fan_brake_horsepower(component) standard_minimum_motor_efficiency_and_size = std.fan_standard_minimum_motor_efficiency_and_size(component, motor_bhp)[0] # check actual against target if standard_minimum_motor_efficiency_and_size.nil? check_elems << OpenStudio::Attribute.new('flag', "Can't find target motor efficiency for #{component.name}.") elsif motor_eff < standard_minimum_motor_efficiency_and_size * (1.0 - min_pass_pct) check_elems << OpenStudio::Attribute.new('flag', "Motor efficiency of #{motor_eff.round(2)} for #{component.name} is more than #{min_pass_pct * 100} % below the expected value of #{standard_minimum_motor_efficiency_and_size.round(2)} for #{target_standard}.") elsif motor_eff > standard_minimum_motor_efficiency_and_size * (1.0 + max_pass_pct) check_elems << OpenStudio::Attribute.new('flag', "Motor efficiency of #{motor_eff.round(2)} for #{component.name} is more than #{max_pass_pct * 100} % above the expected value of #{standard_minimum_motor_efficiency_and_size.round(2)} for #{target_standard}.") end end # check PumpConstantSpeed @model.getPumpConstantSpeeds.sort.each do |component| # eff values from model motor_eff = component.motorEfficiency # get eff values from standards motor_bhp = std.pump_brake_horsepower(component) next if motor_bhp == 0.0 standard_minimum_motor_efficiency_and_size = std.pump_standard_minimum_motor_efficiency_and_size(component, motor_bhp)[0] # check actual against target if standard_minimum_motor_efficiency_and_size.nil? check_elems << OpenStudio::Attribute.new('flag', "Can't find target motor efficiency for #{component.name}.") elsif motor_eff < standard_minimum_motor_efficiency_and_size * (1.0 - min_pass_pct) check_elems << OpenStudio::Attribute.new('flag', "Motor efficiency of #{motor_eff.round(2)} for #{component.name} is more than #{min_pass_pct * 100} % below the expected value of #{standard_minimum_motor_efficiency_and_size.round(2)} for #{target_standard}.") elsif motor_eff > standard_minimum_motor_efficiency_and_size * (1.0 + max_pass_pct) check_elems << OpenStudio::Attribute.new('flag', "Motor efficiency of #{motor_eff.round(2)} for #{component.name} is more than #{max_pass_pct * 100} % above the expected value of #{standard_minimum_motor_efficiency_and_size.round(2)} for #{target_standard}.") end end # check PumpVariableSpeed @model.getPumpVariableSpeeds.sort.each do |component| # eff values from model motor_eff = component.motorEfficiency # get eff values from standards motor_bhp = std.pump_brake_horsepower(component) next if motor_bhp == 0.0 standard_minimum_motor_efficiency_and_size = std.pump_standard_minimum_motor_efficiency_and_size(component, motor_bhp)[0] # check actual against target if standard_minimum_motor_efficiency_and_size.nil? check_elems << OpenStudio::Attribute.new('flag', "Can't find target motor efficiency for #{component.name}.") elsif motor_eff < standard_minimum_motor_efficiency_and_size * (1.0 - min_pass_pct) check_elems << OpenStudio::Attribute.new('flag', "Motor efficiency of #{motor_eff.round(2)} for #{component.name} is more than #{min_pass_pct * 100} % below the expected value of #{standard_minimum_motor_efficiency_and_size.round(2)} for #{target_standard}.") elsif motor_eff > standard_minimum_motor_efficiency_and_size * (1.0 + max_pass_pct) check_elems << OpenStudio::Attribute.new('flag', "Motor efficiency of #{motor_eff.round(2)} for #{component.name} is more than #{max_pass_pct * 100} % above the expected value of #{standard_minimum_motor_efficiency_and_size.round(2)} for #{target_standard}.") end end rescue StandardError => e # brief description of ruby error check_elems << OpenStudio::Attribute.new('flag', "Error prevented QAQC check from running (#{e}).") # backtrace of ruby error for diagnostic use if @error_backtrace then check_elems << OpenStudio::Attribute.new('flag', e.backtrace.join("\n").to_s) end end # add check_elms to new attribute check_elem = OpenStudio::Attribute.new('check', check_elems) return check_elem end
Check primary heating and cooling equipment part load ratios to find equipment that is significantly oversized or undersized.
@param category [String] category to bin this check into @param name_only [Boolean] If true, only return the name of this check @return [OpenStudio::Attribute] OpenStudio Attribute object containing check results
# File lib/openstudio-standards/qaqc/hvac.rb, line 1706 def self.check_hvac_equipment_part_load_ratios(category, name_only: false) # summary of the check check_elems = OpenStudio::AttributeVector.new check_elems << OpenStudio::Attribute.new('name', 'Part Load') check_elems << OpenStudio::Attribute.new('category', category) check_elems << OpenStudio::Attribute.new('description', 'Check that equipment operates at reasonable part load ranges.') # stop here if only name is requested this is used to populate display name for arguments if name_only == true results = [] check_elems.each do |elem| results << elem.valueAsString end return results end begin # Establish limits for % of operating hrs expected above 90% part load expected_pct_hrs_above_90 = 0.1 # get the weather file run period (as opposed to design day run period) ann_env_pd = nil @sql.availableEnvPeriods.each do |env_pd| env_type = @sql.environmentType(env_pd) if env_type.is_initialized if env_type.get == OpenStudio::EnvironmentType.new('WeatherRunPeriod') ann_env_pd = env_pd break end end end # only try to get the annual timeseries if an annual simulation was run if ann_env_pd.nil? check_elems << OpenStudio::Attribute.new('flag', 'Cannot find the annual simulation run period, cannot check equipment part load ratios.') return check_elem end # Boilers @model.getBoilerHotWaters.sort.each do |boiler| msg = OpenstudioStandards::HVAC.hvac_equipment_part_load_ratio_message(@sql, ann_env_pd, 'Hourly', 'Boiler Part Load Ratio', boiler, 1.0) unless msg.nil? check_elems << OpenStudio::Attribute.new('flag', msg) end end # Chillers @model.getChillerElectricEIRs.sort.each do |chiller| msg = OpenstudioStandards::HVAC.hvac_equipment_part_load_ratio_message(@sql, ann_env_pd, 'Hourly', 'Chiller Part Load Ratio', chiller, 1.0) unless msg.nil? check_elems << OpenStudio::Attribute.new('flag', msg) end end # Cooling Towers (Single Speed) @model.getCoolingTowerSingleSpeeds.sort.each do |cooling_tower| # Get the design fan power if cooling_tower.fanPoweratDesignAirFlowRate.is_initialized design_power = cooling_tower.fanPoweratDesignAirFlowRate.get elsif cooling_tower.autosizedFanPoweratDesignAirFlowRate.is_initialized design_power = cooling_tower.autosizedFanPoweratDesignAirFlowRate.get else check_elems << OpenStudio::Attribute.new('flag', "Could not determine peak power for #{cooling_tower.name}, cannot check part load ratios.") next end msg = OpenstudioStandards::HVAC.hvac_equipment_part_load_ratio_message(@sql, ann_env_pd, 'Hourly', 'Cooling Tower Fan Electric Power', cooling_tower, design_power, units: 'W') unless msg.nil? check_elems << OpenStudio::Attribute.new('flag', msg) end end # Cooling Towers (Two Speed) @model.getCoolingTowerTwoSpeeds.sort.each do |cooling_tower| # Get the design fan power if cooling_tower.highFanSpeedFanPower.is_initialized design_power = cooling_tower.highFanSpeedFanPower.get elsif cooling_tower.autosizedHighFanSpeedFanPower.is_initialized design_power = cooling_tower.autosizedHighFanSpeedFanPower.get else check_elems << OpenStudio::Attribute.new('flag', "Could not determine peak power for #{cooling_tower.name}, cannot check part load ratios.") next end msg = OpenstudioStandards::HVAC.hvac_equipment_part_load_ratio_message(@sql, ann_env_pd, 'Hourly', 'Cooling Tower Fan Electric Power', cooling_tower, design_power, units: 'W') unless msg.nil? check_elems << OpenStudio::Attribute.new('flag', msg) end end # Cooling Towers (Variable Speed) @model.getCoolingTowerVariableSpeeds.sort.each do |cooling_tower| # Get the design fan power if cooling_tower.designFanPower.is_initialized design_power = cooling_tower.designFanPower.get elsif cooling_tower.autosizedDesignFanPower.is_initialized design_power = cooling_tower.autosizedDesignFanPower.get else check_elems << OpenStudio::Attribute.new('flag', "Could not determine peak power for #{cooling_tower.name}, cannot check part load ratios.") next end msg = OpenstudioStandards::HVAC.hvac_equipment_part_load_ratio_message(@sql, ann_env_pd, 'Hourly', 'Cooling Tower Fan Electric Power', cooling_tower, design_power, units: 'W') unless msg.nil? check_elems << OpenStudio::Attribute.new('flag', msg) end end # DX Cooling Coils (Single Speed) @model.getCoilCoolingDXSingleSpeeds.sort.each do |dx_coil| # Get the design coil capacity if dx_coil.ratedTotalCoolingCapacity.is_initialized design_power = dx_coil.ratedTotalCoolingCapacity.get elsif dx_coil.autosizedRatedTotalCoolingCapacity.is_initialized design_power = dx_coil.autosizedRatedTotalCoolingCapacity.get else check_elems << OpenStudio::Attribute.new('flag', "Could not determine capacity for #{dx_coil.name}, cannot check part load ratios.") next end msg = OpenstudioStandards::HVAC.hvac_equipment_part_load_ratio_message(@sql, ann_env_pd, 'Hourly', 'Cooling Coil Total Cooling Rate', dx_coil, design_power, units: 'W') unless msg.nil? check_elems << OpenStudio::Attribute.new('flag', msg) end end # DX Cooling Coils (Two Speed) @model.getCoilCoolingDXTwoSpeeds.sort.each do |dx_coil| # Get the design coil capacity if dx_coil.ratedHighSpeedTotalCoolingCapacity.is_initialized design_power = dx_coil.ratedHighSpeedTotalCoolingCapacity.get elsif dx_coil.autosizedRatedHighSpeedTotalCoolingCapacity.is_initialized design_power = dx_coil.autosizedRatedHighSpeedTotalCoolingCapacity.get else check_elems << OpenStudio::Attribute.new('flag', "Could not determine capacity for #{dx_coil.name}, cannot check part load ratios.") next end msg = OpenstudioStandards::HVAC.hvac_equipment_part_load_ratio_message(@sql, ann_env_pd, 'Hourly', 'Cooling Coil Total Cooling Rate', dx_coil, design_power, units: 'W') unless msg.nil? check_elems << OpenStudio::Attribute.new('flag', msg) end end # DX Cooling Coils (Variable Speed) @model.getCoilCoolingDXVariableSpeeds.sort.each do |dx_coil| # Get the design coil capacity if dx_coil.grossRatedTotalCoolingCapacityAtSelectedNominalSpeedLevel.is_initialized design_power = dx_coil.grossRatedTotalCoolingCapacityAtSelectedNominalSpeedLevel.get elsif dx_coil.autosizedGrossRatedTotalCoolingCapacityAtSelectedNominalSpeedLevel.is_initialized design_power = dx_coil.autosizedGrossRatedTotalCoolingCapacityAtSelectedNominalSpeedLevel.get else check_elems << OpenStudio::Attribute.new('flag', "Could not determine capacity for #{dx_coil.name}, cannot check part load ratios.") next end msg = OpenstudioStandards::HVAC.hvac_equipment_part_load_ratio_message(@sql, ann_env_pd, 'Hourly', 'Cooling Coil Total Cooling Rate', dx_coil, design_power, units: 'W') unless msg.nil? check_elems << OpenStudio::Attribute.new('flag', msg) end end # Gas Heating Coils @model.getCoilHeatingGass.sort.each do |gas_coil| # Get the design coil capacity if gas_coil.nominalCapacity.is_initialized design_power = gas_coil.nominalCapacity.get elsif gas_coil.autosizedNominalCapacity.is_initialized design_power = gas_coil.autosizedNominalCapacity.get else check_elems << OpenStudio::Attribute.new('flag', "Could not determine capacity for #{gas_coil.name}, cannot check part load ratios.") next end if (gas_coil.name.to_s.include? 'Backup') || (gas_coil.name.to_s.include? 'Supplemental') msg = OpenstudioStandards::HVAC.hvac_equipment_part_load_ratio_message(@sql, ann_env_pd, 'Hourly', 'Heating Coil Heating Rate', gas_coil, design_power, units: 'W', expect_low_plr: true) else msg = OpenstudioStandards::HVAC.hvac_equipment_part_load_ratio_message(@sql, ann_env_pd, 'Hourly', 'Heating Coil Heating Rate', gas_coil, design_power, units: 'W') end unless msg.nil? check_elems << OpenStudio::Attribute.new('flag', msg) end end # Electric Heating Coils @model.getCoilHeatingElectrics.sort.each do |electric_coil| # Get the design coil capacity if electric_coil.nominalCapacity.is_initialized design_power = electric_coil.nominalCapacity.get elsif electric_coil.autosizedNominalCapacity.is_initialized design_power = electric_coil.autosizedNominalCapacity.get else check_elems << OpenStudio::Attribute.new('flag', "Could not determine capacity for #{electric_coil.name}, cannot check part load ratios.") next end if (electric_coil.name.to_s.include? 'Backup') || (electric_coil.name.to_s.include? 'Supplemental') msg = OpenstudioStandards::HVAC.hvac_equipment_part_load_ratio_message(@sql, ann_env_pd, 'Hourly', 'Heating Coil Heating Rate', electric_coil, design_power, units: 'W', expect_low_plr: true) else msg = OpenstudioStandards::HVAC.hvac_equipment_part_load_ratio_message(@sql, ann_env_pd, 'Hourly', 'Heating Coil Heating Rate', electric_coil, design_power, units: 'W') end unless msg.nil? check_elems << OpenStudio::Attribute.new('flag', msg) end end # DX Heating Coils (Single Speed) @model.getCoilHeatingDXSingleSpeeds.sort.each do |dx_coil| # Get the design coil capacity if dx_coil.ratedTotalHeatingCapacity.is_initialized design_power = dx_coil.ratedTotalHeatingCapacity.get elsif dx_coil.autosizedRatedTotalHeatingCapacity.is_initialized design_power = dx_coil.autosizedRatedTotalHeatingCapacity.get else check_elems << OpenStudio::Attribute.new('flag', "Could not determine capacity for #{dx_coil.name}, cannot check part load ratios.") next end msg = OpenstudioStandards::HVAC.hvac_equipment_part_load_ratio_message(@sql, ann_env_pd, 'Hourly', 'Heating Coil Heating Rate', dx_coil, design_power, units: 'W') unless msg.nil? check_elems << OpenStudio::Attribute.new('flag', msg) end end rescue StandardError => e # brief description of ruby error check_elems << OpenStudio::Attribute.new('flag', "Error prevented QAQC check from running (#{e}).") # backtrace of ruby error for diagnostic use if @error_backtrace then check_elems << OpenStudio::Attribute.new('flag', e.backtrace.join("\n").to_s) end end # add check_elms to new attribute check_elem = OpenStudio::Attribute.new('check', check_elems) return check_elem end
Check the mechanical system part load efficiencies against a standard
@param category [String] category to bin this check into @param target_standard [String] standard template, e.g. ‘90.1-2013’ @param min_pass_pct [Double] threshold for throwing an error for percent difference @param max_pass_pct [Double] threshold for throwing an error for percent difference @param name_only [Boolean] If true, only return the name of this check @return [OpenStudio::Attribute] OpenStudio Attribute object containing check results
# File lib/openstudio-standards/qaqc/hvac.rb, line 807 def self.check_hvac_part_load_efficiency(category, target_standard, min_pass_pct: 0.3, max_pass_pct: 0.3, name_only: false) component_type_array = ['ChillerElectricEIR', 'CoilCoolingDXSingleSpeed', 'CoilCoolingDXTwoSpeed', 'CoilHeatingDXSingleSpeed'] # summary of the check check_elems = OpenStudio::AttributeVector.new check_elems << OpenStudio::Attribute.new('name', 'Mechanical System Part Load Efficiency') check_elems << OpenStudio::Attribute.new('category', category) check_elems << OpenStudio::Attribute.new('description', "Check 40% and 80% part load efficency against #{target_standard} for the following compenent types: #{component_type_array.join(', ')}. Checking EIR Function of Part Load Ratio curve for chiller and EIR Function of Flow Fraction for DX coils.") # stop here if only name is requested this is used to populate display name for arguments if name_only == true results = [] check_elems.each do |elem| results << elem.valueAsString end return results end std = Standard.build(target_standard) # @todo add in check for VAV fan begin # @todo dynamically generate a list of possible options from the standards json chiller_air_cooled_condenser_types = ['WithCondenser', 'WithoutCondenser'] chiller_water_cooled_compressor_types = ['Reciprocating', 'Scroll', 'Rotary Screw', 'Centrifugal'] absorption_types = ['Single Effect', 'Double Effect Indirect Fired', 'Double Effect Direct Fired'] # check getChillerElectricEIRs objects (will also have curve check in different script) @model.getChillerElectricEIRs.sort.each do |component| # get curve and evaluate electric_input_to_cooling_output_ratio_function_of_plr = component.electricInputToCoolingOutputRatioFunctionOfPLR curve_40_pct = electric_input_to_cooling_output_ratio_function_of_plr.evaluate(0.4) curve_80_pct = electric_input_to_cooling_output_ratio_function_of_plr.evaluate(0.8) # find ac properties search_criteria = std.chiller_electric_eir_find_search_criteria(component) # extend search_criteria for absorption_type absorption_types.each do |absorption_type| if component.name.to_s.include?(absorption_type) search_criteria['absorption_type'] = absorption_type next end end # extend search_criteria for condenser type or compressor type if search_criteria['cooling_type'] == 'AirCooled' chiller_air_cooled_condenser_types.each do |condenser_type| if component.name.to_s.include?(condenser_type) search_criteria['condenser_type'] = condenser_type next end end # if no match and also no absorption_type then issue warning if !search_criteria.key?('condenser_type') || search_criteria['condenser_type'].nil? if !search_criteria.key?('absorption_type') || search_criteria['absorption_type'].nil? check_elems << OpenStudio::Attribute.new('flag', "Can't find unique search criteria for #{component.name}. #{search_criteria}") next # don't go past here end end elsif search_criteria['cooling_type'] == 'WaterCooled' chiller_air_cooled_condenser_types.each do |compressor_type| if component.name.to_s.include?(compressor_type) search_criteria['compressor_type'] = compressor_type next end end # if no match and also no absorption_type then issue warning if !search_criteria.key?('compressor_type') || search_criteria['compressor_type'].nil? if !search_criteria.key?('absorption_type') || search_criteria['absorption_type'].nil? check_elems << OpenStudio::Attribute.new('flag', "Can't find unique search criteria for #{component.name}. #{search_criteria}") next # don't go past here end end end # lookup chiller capacity_w = std.chiller_electric_eir_find_capacity(component) capacity_tons = OpenStudio.convert(capacity_w, 'W', 'ton').get chlr_props = std.model_find_object(std.standards_data['chillers'], search_criteria, capacity_tons, Date.today) if chlr_props.nil? check_elems << OpenStudio::Attribute.new('flag', "Didn't find chiller for #{component.name}. #{search_criteria}") next # don't go past here in loop if can't find curve end # temp model to hold temp curve model_temp = OpenStudio::Model::Model.new # create temp curve target_curve_name = chlr_props['eirfplr'] if target_curve_name.nil? check_elems << OpenStudio::Attribute.new('flag', "Can't find target eirfplr curve for #{component.name}") next # don't go past here in loop if can't find curve end temp_curve = std.model_add_curve(model_temp, target_curve_name) target_curve_40_pct = temp_curve.evaluate(0.4) target_curve_80_pct = temp_curve.evaluate(0.8) # check curve at two points if curve_40_pct < target_curve_40_pct * (1.0 - min_pass_pct) check_elems << OpenStudio::Attribute.new('flag', "The curve value at 40% of #{curve_40_pct.round(2)} for #{component.name} is more than #{min_pass_pct * 100} % below the typical value of #{target_curve_40_pct.round(2)} for #{target_standard}.") elsif curve_40_pct > target_curve_40_pct * (1.0 + max_pass_pct) check_elems << OpenStudio::Attribute.new('flag', "The curve value at 40% of #{curve_40_pct.round(2)} for #{component.name} is more than #{max_pass_pct * 100} % above the typical value of #{target_curve_40_pct.round(2)} for #{target_standard}.") end if curve_80_pct < target_curve_80_pct * (1.0 - min_pass_pct) check_elems << OpenStudio::Attribute.new('flag', "The curve value at 80% of #{curve_80_pct.round(2)} for #{component.name} is more than #{min_pass_pct * 100} % below the typical value of #{target_curve_80_pct.round(2)} for #{target_standard}.") elsif curve_80_pct > target_curve_80_pct * (1.0 + max_pass_pct) check_elems << OpenStudio::Attribute.new('flag', "The curve value at 80% of #{curve_80_pct.round(2)} for #{component.name} is more than #{max_pass_pct * 100} % above the typical value of #{target_curve_80_pct.round(2)} for #{target_standard}.") end end # check getCoilCoolingDXSingleSpeeds objects (will also have curve check in different script) @model.getCoilCoolingDXSingleSpeeds.sort.each do |component| # get curve and evaluate eir_function_of_flow_fraction_curve = component.energyInputRatioFunctionOfFlowFractionCurve curve_40_pct = eir_function_of_flow_fraction_curve.evaluate(0.4) curve_80_pct = eir_function_of_flow_fraction_curve.evaluate(0.8) # find ac properties search_criteria = std.coil_dx_find_search_criteria(component) capacity_w = std.coil_cooling_dx_single_speed_find_capacity(component) capacity_btu_per_hr = OpenStudio.convert(capacity_w, 'W', 'Btu/hr').get if std.coil_dx_heat_pump?(component) ac_props = std.model_find_object(std.standards_data['heat_pumps'], search_criteria, capacity_btu_per_hr, Date.today) else ac_props = std.model_find_object(std.standards_data['unitary_acs'], search_criteria, capacity_btu_per_hr, Date.today) end # temp model to hold temp curve model_temp = OpenStudio::Model::Model.new # create temp curve target_curve_name = ac_props['cool_eir_fflow'] if target_curve_name.nil? check_elems << OpenStudio::Attribute.new('flag', "Can't find target cool_eir_fflow curve for #{component.name}") next # don't go past here in loop if can't find curve end temp_curve = std.model_add_curve(model_temp, target_curve_name) target_curve_40_pct = temp_curve.evaluate(0.4) target_curve_80_pct = temp_curve.evaluate(0.8) # check curve at two points if curve_40_pct < target_curve_40_pct * (1.0 - min_pass_pct) check_elems << OpenStudio::Attribute.new('flag', "The curve value at 40% of #{curve_40_pct.round(2)} for #{component.name} is more than #{min_pass_pct * 100} % below the typical value of #{target_curve_40_pct.round(2)} for #{target_standard}.") elsif curve_40_pct > target_curve_40_pct * (1.0 + max_pass_pct) check_elems << OpenStudio::Attribute.new('flag', "The curve value at 40% of #{curve_40_pct.round(2)} for #{component.name} is more than #{max_pass_pct * 100} % above the typical value of #{target_curve_40_pct.round(2)} for #{target_standard}.") end if curve_80_pct < target_curve_80_pct * (1.0 - min_pass_pct) check_elems << OpenStudio::Attribute.new('flag', "The curve value at 80% of #{curve_80_pct.round(2)} for #{component.name} is more than #{min_pass_pct * 100} % below the typical value of #{target_curve_80_pct.round(2)} for #{target_standard}.") elsif curve_80_pct > target_curve_80_pct * (1.0 + max_pass_pct) check_elems << OpenStudio::Attribute.new('flag', "The curve value at 80% of #{curve_80_pct.round(2)} for #{component.name} is more than #{max_pass_pct * 100} % above the typical value of #{target_curve_80_pct.round(2)} for #{target_standard}.") end end # check CoilCoolingDXTwoSpeed objects (will also have curve check in different script) @model.getCoilCoolingDXTwoSpeeds.sort.each do |component| # get curve and evaluate eir_function_of_flow_fraction_curve = component.energyInputRatioFunctionOfFlowFractionCurve curve_40_pct = eir_function_of_flow_fraction_curve.evaluate(0.4) curve_80_pct = eir_function_of_flow_fraction_curve.evaluate(0.8) # find ac properties search_criteria = std.coil_dx_find_search_criteria(component) capacity_w = std.coil_cooling_dx_two_speed_find_capacity(component) capacity_btu_per_hr = OpenStudio.convert(capacity_w, 'W', 'Btu/hr').get ac_props = std.model_find_object(std.standards_data['unitary_acs'], search_criteria, capacity_btu_per_hr, Date.today) # temp model to hold temp curve model_temp = OpenStudio::Model::Model.new # create temp curve target_curve_name = ac_props['cool_eir_fflow'] if target_curve_name.nil? check_elems << OpenStudio::Attribute.new('flag', "Can't find target cool_eir_flow curve for #{component.name}") next # don't go past here in loop if can't find curve end temp_curve = std.model_add_curve(model_temp, target_curve_name) target_curve_40_pct = temp_curve.evaluate(0.4) target_curve_80_pct = temp_curve.evaluate(0.8) # check curve at two points if curve_40_pct < target_curve_40_pct * (1.0 - min_pass_pct) check_elems << OpenStudio::Attribute.new('flag', "The curve value at 40% of #{curve_40_pct.round(2)} for #{component.name} is more than #{min_pass_pct * 100} % below the typical value of #{target_curve_40_pct.round(2)} for #{target_standard}.") elsif curve_40_pct > target_curve_40_pct * (1.0 + max_pass_pct) check_elems << OpenStudio::Attribute.new('flag', "The curve value at 40% of #{curve_40_pct.round(2)} for #{component.name} is more than #{max_pass_pct * 100} % above the typical value of #{target_curve_40_pct.round(2)} for #{target_standard}.") end if curve_80_pct < target_curve_80_pct * (1.0 - min_pass_pct) check_elems << OpenStudio::Attribute.new('flag', "The curve value at 80% of #{curve_80_pct.round(2)} for #{component.name} is more than #{min_pass_pct * 100} % below the typical value of #{target_curve_80_pct.round(2)} for #{target_standard}.") elsif curve_80_pct > target_curve_80_pct * (1.0 + max_pass_pct) check_elems << OpenStudio::Attribute.new('flag', "The curve value at 80% of #{curve_80_pct.round(2)} for #{component.name} is more than #{max_pass_pct * 100} % above the typical value of #{target_curve_80_pct.round(2)} for #{target_standard}.") end end # check CoilCoolingDXTwoSpeed objects (will also have curve check in different script) @model.getCoilHeatingDXSingleSpeeds.sort.each do |component| # get curve and evaluate eir_function_of_flow_fraction_curve = component.energyInputRatioFunctionofFlowFractionCurve # why lowercase of here but not in CoilCoolingDX objects curve_40_pct = eir_function_of_flow_fraction_curve.evaluate(0.4) curve_80_pct = eir_function_of_flow_fraction_curve.evaluate(0.8) # find ac properties search_criteria = std.coil_dx_find_search_criteria(component) capacity_w = std.coil_heating_dx_single_speed_find_capacity(component) capacity_btu_per_hr = OpenStudio.convert(capacity_w, 'W', 'Btu/hr').get ac_props = std.model_find_object(std.standards_data['heat_pumps_heating'], search_criteria, capacity_btu_per_hr, Date.today) if ac_props.nil? target_curve_name = nil else target_curve_name = ac_props['heat_eir_fflow'] end # temp model to hold temp curve model_temp = OpenStudio::Model::Model.new # create temp curve if target_curve_name.nil? check_elems << OpenStudio::Attribute.new('flag', "Can't find target curve for #{component.name}") next # don't go past here in loop if can't find curve end temp_curve = std.model_add_curve(model_temp, target_curve_name) # Ensure that the curve was found in standards before attempting to evaluate if temp_curve.nil? check_elems << OpenStudio::Attribute.new('flag', "Can't find coefficients of curve called #{target_curve_name} for #{component.name}, cannot check part-load performance.") next end target_curve_40_pct = temp_curve.evaluate(0.4) target_curve_80_pct = temp_curve.evaluate(0.8) # check curve at two points if curve_40_pct < target_curve_40_pct * (1.0 - min_pass_pct) check_elems << OpenStudio::Attribute.new('flag', "The curve value at 40% of #{curve_40_pct.round(2)} for #{component.name} is more than #{min_pass_pct * 100} % below the typical value of #{target_curve_40_pct.round(2)} for #{target_standard}.") elsif curve_40_pct > target_curve_40_pct * (1.0 + max_pass_pct) check_elems << OpenStudio::Attribute.new('flag', "The curve value at 40% of #{curve_40_pct.round(2)} for #{component.name} is more than #{max_pass_pct * 100} % above the typical value of #{target_curve_40_pct.round(2)} for #{target_standard}.") end if curve_80_pct < target_curve_80_pct * (1.0 - min_pass_pct) check_elems << OpenStudio::Attribute.new('flag', "The curve value at 80% of #{curve_80_pct.round(2)} for #{component.name} is more than #{min_pass_pct * 100} % below the typical value of #{target_curve_80_pct.round(2)} for #{target_standard}.") elsif curve_80_pct > target_curve_80_pct * (1.0 + max_pass_pct) check_elems << OpenStudio::Attribute.new('flag', "The curve value at 80% of #{curve_80_pct.round(2)} for #{component.name} is more than #{max_pass_pct * 100} % above the typical value of #{target_curve_80_pct.round(2)} for #{target_standard}.") end end # check @model.getFanVariableVolumes.sort.each do |component| # skip if not on multi-zone system. if component.airLoopHVAC.is_initialized airloop = component.airLoopHVAC.get next unless airloop.thermalZones.size > 1.0 end # skip of brake horsepower is 0 next if std.fan_brake_horsepower(component) == 0.0 # temp model for use by temp model and target curve model_temp = OpenStudio::Model::Model.new # get coeficents for fan model_fan_coefs = [] model_fan_coefs << component.fanPowerCoefficient1.get model_fan_coefs << component.fanPowerCoefficient2.get model_fan_coefs << component.fanPowerCoefficient3.get model_fan_coefs << component.fanPowerCoefficient4.get model_fan_coefs << component.fanPowerCoefficient5.get # make model curve model_curve = OpenStudio::Model::CurveQuartic.new(model_temp) model_curve.setCoefficient1Constant(model_fan_coefs[0]) model_curve.setCoefficient2x(model_fan_coefs[1]) model_curve.setCoefficient3xPOW2(model_fan_coefs[2]) model_curve.setCoefficient4xPOW3(model_fan_coefs[3]) model_curve.setCoefficient5xPOW4(model_fan_coefs[4]) curve_40_pct = model_curve.evaluate(0.4) curve_80_pct = model_curve.evaluate(0.8) # get target coefs target_fan = OpenStudio::Model::FanVariableVolume.new(model_temp) std.fan_variable_volume_set_control_type(target_fan, 'Multi Zone VAV with VSD and Static Pressure Reset') # get coeficents for fan target_fan_coefs = [] target_fan_coefs << target_fan.fanPowerCoefficient1.get target_fan_coefs << target_fan.fanPowerCoefficient2.get target_fan_coefs << target_fan.fanPowerCoefficient3.get target_fan_coefs << target_fan.fanPowerCoefficient4.get target_fan_coefs << target_fan.fanPowerCoefficient5.get # make model curve target_curve = OpenStudio::Model::CurveQuartic.new(model_temp) target_curve.setCoefficient1Constant(target_fan_coefs[0]) target_curve.setCoefficient2x(target_fan_coefs[1]) target_curve.setCoefficient3xPOW2(target_fan_coefs[2]) target_curve.setCoefficient4xPOW3(target_fan_coefs[3]) target_curve.setCoefficient5xPOW4(target_fan_coefs[4]) target_curve_40_pct = target_curve.evaluate(0.4) target_curve_80_pct = target_curve.evaluate(0.8) # check curve at two points if curve_40_pct < target_curve_40_pct * (1.0 - min_pass_pct) check_elems << OpenStudio::Attribute.new('flag', "The curve value at 40% of #{curve_40_pct.round(2)} for #{component.name} is more than #{min_pass_pct * 100} % below the typical value of #{target_curve_40_pct.round(2)} for #{target_standard}.") elsif curve_40_pct > target_curve_40_pct * (1.0 + max_pass_pct) check_elems << OpenStudio::Attribute.new('flag', "The curve value at 40% of #{curve_40_pct.round(2)} for #{component.name} is more than #{max_pass_pct * 100} % above the typical value of #{target_curve_40_pct.round(2)} for #{target_standard}.") end if curve_80_pct < target_curve_80_pct * (1.0 - min_pass_pct) check_elems << OpenStudio::Attribute.new('flag', "The curve value at 80% of #{curve_80_pct.round(2)} for #{component.name} is more than #{min_pass_pct * 100} % below the typical value of #{target_curve_80_pct.round(2)} for #{target_standard}.") elsif curve_80_pct > target_curve_80_pct * (1.0 + max_pass_pct) check_elems << OpenStudio::Attribute.new('flag', "The curve value at 80% of #{curve_80_pct.round(2)} for #{component.name} is more than #{max_pass_pct * 100} % above the typical value of #{target_curve_80_pct.round(2)} for #{target_standard}.") end end rescue StandardError => e # brief description of ruby error check_elems << OpenStudio::Attribute.new('flag', "Error prevented QAQC check from running (#{e}).") # backtrace of ruby error for diagnostic use if @error_backtrace then check_elems << OpenStudio::Attribute.new('flag', e.backtrace.join("\n").to_s) end end # add check_elms to new attribute check_elem = OpenStudio::Attribute.new('check', check_elems) return check_elem end
checks the HVAC
system type against 90.1 baseline system type
@param category [String] category to bin this check into @param target_standard [String] standard template, e.g. ‘90.1-2013’ @param name_only [Boolean] If true, only return the name of this check @return [OpenStudio::Attribute] OpenStudio Attribute object containing check results
# File lib/openstudio-standards/qaqc/hvac.rb, line 323 def self.check_hvac_system_type(category, target_standard, name_only: false) # summary of the check check_elems = OpenStudio::AttributeVector.new check_elems << OpenStudio::Attribute.new('name', 'Mechanical System Type') check_elems << OpenStudio::Attribute.new('category', category) # add ASHRAE to display of target standard if includes with 90.1 if target_standard.include?('90.1 2013') check_elems << OpenStudio::Attribute.new('description', 'Check against ASHRAE 90.1 2013 Tables G3.1.1 A-B. Infers the baseline system type based on the equipment serving the zone and their heating/cooling fuels. Only does a high-level inference; does not look for the presence/absence of required controls, etc.') else check_elems << OpenStudio::Attribute.new('description', 'Check against ASHRAE 90.1. Infers the baseline system type based on the equipment serving the zone and their heating/cooling fuels. Only does a high-level inference; does not look for the presence/absence of required controls, etc.') end # stop here if only name is requested this is used to populate display name for arguments if name_only == true results = [] check_elems.each do |elem| results << elem.valueAsString end return results end std = Standard.build(target_standard) begin # Get the actual system type for all zones in the model act_zone_to_sys_type = {} @model.getThermalZones.each do |zone| act_zone_to_sys_type[zone] = std.thermal_zone_infer_system_type(zone) end # Get the baseline system type for all zones in the model climate_zone = std.model_get_building_properties(@model)['climate_zone'] req_zone_to_sys_type = std.model_get_baseline_system_type_by_zone(@model, climate_zone) # Compare the actual to the correct @model.getThermalZones.each do |zone| is_plenum = false zone.spaces.each do |space| if OpenstudioStandards::Space.space_plenum?(space) is_plenum = true end end next if is_plenum req_sys_type = req_zone_to_sys_type[zone] act_sys_type = act_zone_to_sys_type[zone] unless act_sys_type == req_sys_type if req_sys_type == '' then req_sys_type = 'Unknown' end check_elems << OpenStudio::Attribute.new('flag', "#{zone.name} baseline system type is incorrect. Supposed to be #{req_sys_type}, but was #{act_sys_type} instead.") end end rescue StandardError => e # brief description of ruby error check_elems << OpenStudio::Attribute.new('flag', "Error prevented QAQC check from running (#{e}).") # backtrace of ruby error for diagnostic use if @error_backtrace then check_elems << OpenStudio::Attribute.new('flag', e.backtrace.join("\n").to_s) end end # add check_elms to new attribute check_elem = OpenStudio::Attribute.new('check', check_elems) return check_elem end
Check the internal loads against a standard
@param category [String] category to bin this check into @param target_standard [String] standard template, e.g. ‘90.1-2013’ @param min_pass_pct [Double] threshold for throwing an error for percent difference @param max_pass_pct [Double] threshold for throwing an error for percent difference @param name_only [Boolean] If true, only return the name of this check @return [OpenStudio::Attribute] OpenStudio Attribute object containing check results
# File lib/openstudio-standards/qaqc/internal_loads.rb, line 14 def self.check_internal_loads(category, target_standard, min_pass_pct: 0.2, max_pass_pct: 0.2, name_only: false) # summary of the check check_elems = OpenStudio::AttributeVector.new check_elems << OpenStudio::Attribute.new('name', 'Internal Loads') check_elems << OpenStudio::Attribute.new('category', category) if target_standard == 'ICC IECC 2015' check_elems << OpenStudio::Attribute.new('description', 'Check internal loads against Table R405.5.2(1) in ICC IECC 2015 Residential Provisions.') else if target_standard.include?('90.1') display_standard = "ASHRAE #{target_standard}" else display_standard = target_standard end check_elems << OpenStudio::Attribute.new('description', "Check LPD, ventilation rates, occupant density, plug loads, and equipment loads against #{display_standard} and DOE Prototype buildings.") end # stop here if only name is requested this is used to populate display name for arguments if name_only == true results = [] check_elems.each do |elem| results << elem.valueAsString end return results end std = Standard.build(target_standard) begin if target_standard == 'ICC IECC 2015' num_people = 0.0 @model.getSpaceTypes.each do |space_type| next if !space_type.standardsSpaceType.is_initialized next if space_type.standardsSpaceType.get != 'Apartment' # currently only supports midrise apt space type space_type_floor_area = space_type.floorArea space_type_num_people = space_type.getNumberOfPeople(space_type_floor_area) num_people += space_type_num_people end # lookup iecc internal loads for the building bedrooms_per_unit = 2.0 # assumption num_units = num_people / 2.5 # Avg 2.5 units per person. target_loads_hash = std.model_find_icc_iecc_2015_internal_loads(@model, num_units, bedrooms_per_unit) # get model internal gains for lights, elec equipment, and gas equipment model_internal_gains_si = 0.0 query_eleint_lights = "SELECT Value FROM tabulardatawithstrings WHERE ReportName='AnnualBuildingUtilityPerformanceSummary' and TableName='End Uses' and RowName= 'Interior Lighting' and ColumnName= 'Electricity'" query_elec_equip = "SELECT Value FROM tabulardatawithstrings WHERE ReportName='AnnualBuildingUtilityPerformanceSummary' and TableName='End Uses' and RowName= 'Interior Equipment' and ColumnName= 'Electricity'" query_gas_equip = "SELECT Value FROM tabulardatawithstrings WHERE ReportName='AnnualBuildingUtilityPerformanceSummary' and TableName='End Uses' and RowName= 'Interior Equipment' and ColumnName= 'Natural Gas'" model_internal_gains_si += results_elec = @sql.execAndReturnFirstDouble(query_eleint_lights).get model_internal_gains_si += results_elec = @sql.execAndReturnFirstDouble(query_elec_equip).get model_internal_gains_si += results_elec = @sql.execAndReturnFirstDouble(query_gas_equip).get model_internal_gains_si_kbtu_per_day = OpenStudio.convert(model_internal_gains_si, 'GJ', 'kBtu').get / 365.0 # assumes annual run # get target internal loads target_igain_btu_per_day = target_loads_hash['igain_btu_per_day'] target_igain_kbtu_per_day = OpenStudio.convert(target_igain_btu_per_day, 'Btu', 'kBtu').get # check internal loads if model_internal_gains_si_kbtu_per_day < target_igain_kbtu_per_day * (1.0 - min_pass_pct) check_elems << OpenStudio::Attribute.new('flag', "The model average of #{OpenStudio.toNeatString(model_internal_gains_si_kbtu_per_day, 2, true)} (kBtu/day) is more than #{min_pass_pct * 100} % below the expected value of #{OpenStudio.toNeatString(target_igain_kbtu_per_day, 2, true)} (kBtu/day) for #{target_standard}.") elsif model_internal_gains_si_kbtu_per_day > target_igain_kbtu_per_day * (1.0 + max_pass_pct) check_elems << OpenStudio::Attribute.new('flag', "The model average of #{OpenStudio.toNeatString(model_internal_gains_si_kbtu_per_day, 2, true)} (kBtu/day) is more than #{max_pass_pct * 100} % above the expected value of #{OpenStudio.toNeatString(target_igain_kbtu_per_day, 2, true)} k(Btu/day) for #{target_standard}.") end # get target mech vent target_mech_vent_cfm = target_loads_hash['mech_vent_cfm'] # get model mech vent model_mech_vent_si = 0 @model.getSpaceTypes.each do |space_type| next if space_type.floorArea <= 0 # get necessary space type information floor_area = space_type.floorArea num_people = space_type.getNumberOfPeople(floor_area) # get volume for space type for use with ventilation and infiltration space_type_volume = 0.0 space_type_exterior_area = 0.0 space_type_exterior_wall_area = 0.0 space_type.spaces.each do |space| space_type_volume += space.volume * space.multiplier space_type_exterior_area = space.exteriorArea * space.multiplier space_type_exterior_wall_area = space.exteriorWallArea * space.multiplier end # get design spec OA object if space_type.designSpecificationOutdoorAir.is_initialized oa = space_type.designSpecificationOutdoorAir.get oa_method = oa.outdoorAirMethod oa_per_person = oa.outdoorAirFlowperPerson * num_people oa_ach = oa.outdoorAirFlowAirChangesperHour * space_type_volume oa_per_area = oa.outdoorAirFlowperFloorArea * floor_area oa_flow_rate = oa.outdoorAirFlowRate oa_space_type_total = oa_per_person + oa_ach + oa_per_area + oa_flow_rate value_count = 0 if oa_per_person > 0 then value_count += 1 end if oa_ach > 0 then value_count += 1 end if oa_per_area > 0 then value_count += 1 end if oa_flow_rate > 0 then value_count += 1 end if (oa_method != 'Sum') && (value_count > 1) check_elems << OpenStudio::Attribute.new('flag', "Outdoor Air Method for #{space_type.name} was #{oa_method}. Expected value was Sum.") end else oa_space_type_total = 0.0 end # add to building total oa model_mech_vent_si += oa_space_type_total end # check oa model_mech_vent_cfm = OpenStudio.convert(model_mech_vent_si, 'm^3/s', 'cfm').get if model_mech_vent_cfm < target_mech_vent_cfm * (1.0 - min_pass_pct) check_elems << OpenStudio::Attribute.new('flag', "The model mechanical ventilation of #{OpenStudio.toNeatString(model_mech_vent_cfm, 2, true)} cfm is more than #{min_pass_pct * 100} % below the expected value of #{OpenStudio.toNeatString(target_mech_vent_cfm, 2, true)} cfm for #{target_standard}.") elsif model_mech_vent_cfm > target_mech_vent_cfm * (1.0 + max_pass_pct) check_elems << OpenStudio::Attribute.new('flag', "The model mechanical ventilation of #{OpenStudio.toNeatString(model_mech_vent_cfm, 2, true)} cfm is more than #{max_pass_pct * 100} % above the expected value of #{OpenStudio.toNeatString(target_mech_vent_cfm, 2, true)} cfm for #{target_standard}.") end else # loop through all space types used in the model @model.getSpaceTypes.sort.each do |space_type| next if space_type.floorArea <= 0 next if space_type.name.to_s == 'Plenum' # get necessary space type information floor_area = space_type.floorArea num_people = space_type.getNumberOfPeople(floor_area) # load in standard info for this space type data = std.space_type_get_standards_data(space_type) if data.nil? || data.empty? # skip if all spaces using this space type are plenums all_spaces_plenums = true space_type.spaces.each do |space| unless OpenstudioStandards::Space.space_plenum?(space) all_spaces_plenums = false next end end unless all_spaces_plenums check_elems << OpenStudio::Attribute.new('flag', "Unexpected standards type for #{space_type.name}, can't validate internal loads.") end next end # check lpd for space type model_lights_si = space_type.getLightingPowerPerFloorArea(floor_area, num_people) data['lighting_per_area'].nil? ? (target_lights_ip = 0.0) : (target_lights_ip = data['lighting_per_area']) source_units = 'W/m^2' target_units = 'W/ft^2' load_type = 'Lighting Power Density' model_ip = OpenStudio.convert(model_lights_si, source_units, target_units).get target_ip = target_lights_ip.to_f model_ip_neat = OpenStudio.toNeatString(model_ip, 2, true) target_ip_neat = OpenStudio.toNeatString(target_ip, 2, true) if model_ip < target_ip * (1.0 - min_pass_pct) check_elems << OpenStudio::Attribute.new('flag', "#{load_type} of #{model_ip_neat} (#{target_units}) for #{space_type.name} is more than #{min_pass_pct * 100} % below the expected value of #{target_ip_neat} (#{target_units}) for #{display_standard}.") elsif model_ip > target_ip * (1.0 + max_pass_pct) check_elems << OpenStudio::Attribute.new('flag', "#{load_type} of #{model_ip_neat} (#{target_units}) for #{space_type.name} is more than #{max_pass_pct * 100} % above the expected value of #{target_ip_neat} (#{target_units}) for #{display_standard}.") end # check electric equipment model_elec_si = space_type.getElectricEquipmentPowerPerFloorArea(floor_area, num_people) data['electric_equipment_per_area'].nil? ? (target_elec_ip = 0.0) : (target_elec_ip = data['electric_equipment_per_area']) source_units = 'W/m^2' target_units = 'W/ft^2' load_type = 'Electric Power Density' model_ip = OpenStudio.convert(model_elec_si, source_units, target_units).get target_ip = target_elec_ip.to_f model_ip_neat = OpenStudio.toNeatString(model_ip, 2, true) target_ip_neat = OpenStudio.toNeatString(target_ip, 2, true) if model_ip < target_ip * (1.0 - min_pass_pct) check_elems << OpenStudio::Attribute.new('flag', "#{load_type} of #{model_ip_neat} (#{target_units}) for #{space_type.name} is more than #{min_pass_pct * 100} % below the expected value of #{target_ip_neat} (#{target_units}) for #{display_standard}.") elsif model_ip > target_ip * (1.0 + max_pass_pct) check_elems << OpenStudio::Attribute.new('flag', "#{load_type} of #{model_ip_neat} (#{target_units}) for #{space_type.name} is more than #{max_pass_pct * 100} % above the expected value of #{target_ip_neat} (#{target_units}) for #{display_standard}.") end # check gas equipment model_gas_si = space_type.getGasEquipmentPowerPerFloorArea(floor_area, num_people) data['gas_equipment_per_area'].nil? ? (target_gas_ip = 0.0) : (target_gas_ip = data['gas_equipment_per_area']) source_units = 'W/m^2' target_units = 'Btu/hr*ft^2' load_type = 'Gas Power Density' model_ip = OpenStudio.convert(model_gas_si, source_units, target_units).get target_ip = target_gas_ip.to_f model_ip_neat = OpenStudio.toNeatString(model_ip, 2, true) target_ip_neat = OpenStudio.toNeatString(target_ip, 2, true) if model_ip < target_ip * (1.0 - min_pass_pct) check_elems << OpenStudio::Attribute.new('flag', "#{load_type} of #{model_ip_neat} (#{target_units}) for #{space_type.name} is more than #{min_pass_pct * 100} % below the expected value of #{target_ip_neat} (#{target_units}) for #{display_standard}.") elsif model_ip > target_ip * (1.0 + max_pass_pct) check_elems << OpenStudio::Attribute.new('flag', "#{load_type} of #{model_ip_neat} (#{target_units}) for #{space_type.name} is more than #{max_pass_pct * 100} % above the expected value of #{target_ip_neat} (#{target_units}) for #{display_standard}.") end # check people model_occ_si = space_type.getPeoplePerFloorArea(floor_area) data['occupancy_per_area'].nil? ? (target_occ_ip = 0.0) : (target_occ_ip = data['occupancy_per_area']) source_units = '1/m^2' # people/m^2 target_units = '1/ft^2' # people per ft^2 (can't add *1000) to the bottom, need to do later load_type = 'Occupancy per Area' model_ip = OpenStudio.convert(model_occ_si, source_units, target_units).get * 1000.0 target_ip = target_occ_ip.to_f model_ip_neat = OpenStudio.toNeatString(model_ip, 2, true) target_ip_neat = OpenStudio.toNeatString(target_ip, 2, true) # for people need to update target units just for display. Can't be used for converstion. target_units = 'People/1000 ft^2' if model_ip < target_ip * (1.0 - min_pass_pct) check_elems << OpenStudio::Attribute.new('flag', "#{load_type} of #{model_ip_neat} (#{target_units}) for #{space_type.name} is more than #{min_pass_pct * 100} % below the expected value of #{target_ip_neat} (#{target_units}) for #{display_standard}.") elsif model_ip > target_ip * (1.0 + max_pass_pct) check_elems << OpenStudio::Attribute.new('flag', "#{load_type} of #{model_ip_neat} (#{target_units}) for #{space_type.name} is more than #{max_pass_pct * 100} % above the expected value of #{target_ip_neat} (#{target_units}) for #{display_standard}.") end # get volume for space type for use with ventilation and infiltration space_type_volume = 0.0 space_type_exterior_area = 0.0 space_type_exterior_wall_area = 0.0 space_type.spaces.each do |space| space_type_volume += space.volume * space.multiplier space_type_exterior_area = space.exteriorArea * space.multiplier space_type_exterior_wall_area = space.exteriorWallArea * space.multiplier end # get design spec OA object if space_type.designSpecificationOutdoorAir.is_initialized oa = space_type.designSpecificationOutdoorAir.get oa_method = oa.outdoorAirMethod oa_per_person = oa.outdoorAirFlowperPerson oa_ach = oa.outdoorAirFlowAirChangesperHour * space_type_volume oa_per_area = oa.outdoorAirFlowperFloorArea * floor_area oa_flow_rate = oa.outdoorAirFlowRate oa_total = oa_ach + oa_per_area + oa_flow_rate value_count = 0 if oa_per_person > 0 then value_count += 1 end if oa_ach > 0 then value_count += 1 end if oa_per_area > 0 then value_count += 1 end if oa_flow_rate > 0 then value_count += 1 end if (oa_method != 'Sum') && (value_count > 1) check_elems << OpenStudio::Attribute.new('flag', "Outdoor Air Method for #{space_type.name} was #{oa_method}. Expected value was Sum.") end else oa_per_person = 0.0 end # get target values for OA target_oa_per_person_ip = data['ventilation_per_person'].to_f # ft^3/min*person target_oa_ach_ip = data['ventilation_air_changes'].to_f # ach target_oa_per_area_ip = data['ventilation_per_area'].to_f # ft^3/min*ft^2 if target_oa_per_person_ip.nil? target_oa_per_person_si = 0.0 else target_oa_per_person_si = OpenStudio.convert(target_oa_per_person_ip, 'cfm', 'm^3/s').get end if target_oa_ach_ip.nil? target_oa_ach_si = 0.0 else target_oa_ach_si = target_oa_ach_ip * space_type_volume end if target_oa_per_area_ip.nil? target_oa_per_area_si = 0.0 else target_oa_per_area_si = OpenStudio.convert(target_oa_per_area_ip, 'cfm/ft^2', 'm^3/s*m^2').get * floor_area end target_oa_total = target_oa_ach_si + target_oa_per_area_si # check oa per person source_units = 'm^3/s' target_units = 'cfm' load_type = 'Outdoor Air Per Person' model_ip_neat = OpenStudio.toNeatString(OpenStudio.convert(oa_per_person, source_units, target_units).get, 2, true) target_ip_neat = OpenStudio.toNeatString(target_oa_per_person_ip, 2, true) if oa_per_person < target_oa_per_person_si * (1.0 - min_pass_pct) check_elems << OpenStudio::Attribute.new('flag', "#{load_type} of #{model_ip_neat} (#{target_units}) for #{space_type.name} is more than #{min_pass_pct * 100} % below the expected value of #{target_ip_neat} (#{target_units}) for #{display_standard}.") elsif oa_per_person > target_oa_per_person_si * (1.0 + max_pass_pct) check_elems << OpenStudio::Attribute.new('flag', "#{load_type} of #{model_ip_neat} (#{target_units}) for #{space_type.name} is more than #{max_pass_pct * 100} % above the expected value of #{target_ip_neat} (#{target_units}) for #{display_standard}.") end # check other oa source_units = 'm^3/s' target_units = 'cfm' load_type = 'Outdoor Air (Excluding per Person Value)' model_ip_neat = OpenStudio.toNeatString(OpenStudio.convert(oa_total, source_units, target_units).get, 2, true) target_ip_neat = OpenStudio.toNeatString(OpenStudio.convert(target_oa_total, source_units, target_units).get, 2, true) if oa_total < target_oa_total * (1.0 - min_pass_pct) check_elems << OpenStudio::Attribute.new('flag', "#{load_type} of #{model_ip_neat} (#{target_units}) for #{space_type.name} is more than #{min_pass_pct * 100} % below the expected value of #{target_ip_neat} (#{target_units}) for #{display_standard}.") elsif oa_total > target_oa_total * (1.0 + max_pass_pct) check_elems << OpenStudio::Attribute.new('flag', "#{load_type} of #{model_ip_neat} (#{target_units}) for #{space_type.name} is more than #{max_pass_pct * 100} % above the expected value of #{target_ip_neat} (#{target_units}) for #{display_standard}.") end end # warn if there are spaces in model that don't use space type unless they appear to be plenums @model.getSpaces.sort.each do |space| next if OpenstudioStandards::Space.space_plenum?(space) if !space.spaceType.is_initialized check_elems << OpenStudio::Attribute.new('flag', "#{space.name} doesn't have a space type assigned, can't validate internal loads.") end end # @todo need to address internal loads where fuel is variable like cooking and laundry # @todo For now we are not going to loop through spaces looking for loads beyond what comes from space type # @todo space infiltration end rescue StandardError => e # brief description of ruby error check_elems << OpenStudio::Attribute.new('flag', "Error prevented QAQC check from running (#{e}).") # backtrace of ruby error for diagnostic use if @error_backtrace then check_elems << OpenStudio::Attribute.new('flag', e.backtrace.join("\n").to_s) end end # add check_elms to new attribute check_elem = OpenStudio::Attribute.new('check', check_elems) return check_elem end
Check the internal load schedules against template prototypes
@param category [String] category to bin this check into @param target_standard [String] standard template, e.g. ‘90.1-2013’ @param min_pass_pct [Double] threshold for throwing an error for percent difference @param max_pass_pct [Double] threshold for throwing an error for percent difference @param name_only [Boolean] If true, only return the name of this check @return [OpenStudio::Attribute] OpenStudio Attribute object containing check results
# File lib/openstudio-standards/qaqc/internal_loads.rb, line 347 def self.check_internal_loads_schedules(category, target_standard, min_pass_pct: 0.2, max_pass_pct: 0.2, name_only: false) # summary of the check check_elems = OpenStudio::AttributeVector.new check_elems << OpenStudio::Attribute.new('name', 'Schedules') check_elems << OpenStudio::Attribute.new('category', category) check_elems << OpenStudio::Attribute.new('description', 'Check schedules for lighting, ventilation, occupant density, plug loads, and equipment based on DOE reference building schedules in terms of full load hours per year.') # stop here if only name is requested this is used to populate display name for arguments if name_only == true results = [] check_elems.each do |elem| results << elem.valueAsString end return results end std = Standard.build(target_standard) begin # loop through all space types used in the model @model.getSpaceTypes.each do |space_type| next if space_type.floorArea <= 0 # load in standard info for this space type data = std.space_type_get_standards_data(space_type) if data.nil? || data.empty? # skip if all spaces using this space type are plenums all_spaces_plenums = true space_type.spaces.each do |space| unless OpenstudioStandards::Space.space_plenum?(space) all_spaces_plenums = false break end end unless all_spaces_plenums check_elems << OpenStudio::Attribute.new('flag', "Unexpected standards type for #{space_type.name}, can't validate schedules.") end next end # temp model to hold schedules to check model_temp = OpenStudio::Model::Model.new # check lighting schedules data['lighting_per_area'].nil? ? (target_ip = 0.0) : (target_ip = data['lighting_per_area']) if target_ip.to_f > 0 schedule_target = std.model_add_schedule(model_temp, data['lighting_schedule']) if !schedule_target check_elems << OpenStudio::Attribute.new('flag', "Didn't find schedule named #{data['lighting_schedule']} in standards json.") elsif !schedule_target.to_ScheduleRuleset.is_initialized check_elems << OpenStudio::Attribute.new('flag', "Schedule named #{schedule_target.name} is a #{schedule_target.class}, not a ScheduleRuleset schedule.") else schedule_target = schedule_target.to_ScheduleRuleset.get # loop through and test individual load instances expected_hours = OpenstudioStandards::Schedules.schedule_ruleset_get_equivalent_full_load_hours(schedule_target) space_type.lights.each do |space_load_instance| inst_sch_check = OpenstudioStandards::QAQC.space_load_instance_schedule_check(space_load_instance, expected_hours, std: std, min_pass_pct: min_pass_pct, max_pass_pct: max_pass_pct) if inst_sch_check then check_elems << inst_sch_check end end end end # check electric equipment schedules data['electric_equipment_per_area'].nil? ? (target_ip = 0.0) : (target_ip = data['electric_equipment_per_area']) if target_ip.to_f > 0 schedule_target = std.model_add_schedule(model_temp, data['electric_equipment_schedule']) if !schedule_target check_elems << OpenStudio::Attribute.new('flag', "Didn't find schedule named #{data['electric_equipment_schedule']} in standards json.") elsif !schedule_target.to_ScheduleRuleset.is_initialized check_elems << OpenStudio::Attribute.new('flag', "Schedule named #{schedule_target.name} is a #{schedule_target.class}, not a ScheduleRuleset schedule.") else schedule_target = schedule_target.to_ScheduleRuleset.get # loop through and test individual load instances expected_hours = OpenstudioStandards::Schedules.schedule_ruleset_get_equivalent_full_load_hours(schedule_target) space_type.electricEquipment.each do |space_load_instance| inst_sch_check = OpenstudioStandards::QAQC.space_load_instance_schedule_check(space_load_instance, expected_hours, std: std, min_pass_pct: min_pass_pct, max_pass_pct: max_pass_pct) if inst_sch_check then check_elems << inst_sch_check end end end end # check gas equipment schedules # @todo - update measure test to with space type to check this data['gas_equipment_per_area'].nil? ? (target_ip = 0.0) : (target_ip = data['gas_equipment_per_area']) if target_ip.to_f > 0 schedule_target = std.model_add_schedule(model_temp, data['gas_equipment_schedule']) if !schedule_target check_elems << OpenStudio::Attribute.new('flag', "Didn't find schedule named #{data['gas_equipment_schedule']} in standards json.") elsif !schedule_target.to_ScheduleRuleset.is_initialized check_elems << OpenStudio::Attribute.new('flag', "Schedule named #{schedule_target.name} is a #{schedule_target.class}, not a ScheduleRuleset schedule.") else schedule_target = schedule_target.to_ScheduleRuleset.get # loop through and test individual load instances expected_hours = OpenstudioStandards::Schedules.schedule_ruleset_get_equivalent_full_load_hours(schedule_target) space_type.gasEquipment.each do |space_load_instance| inst_sch_check = OpenstudioStandards::QAQC.space_load_instance_schedule_check(space_load_instance, expected_hours, std: std, min_pass_pct: min_pass_pct, max_pass_pct: max_pass_pct) if inst_sch_check then check_elems << inst_sch_check end end end end # check occupancy schedules data['occupancy_per_area'].nil? ? (target_ip = 0.0) : (target_ip = data['occupancy_per_area']) if target_ip.to_f > 0 schedule_target = std.model_add_schedule(model_temp, data['occupancy_schedule']) if !schedule_target check_elems << OpenStudio::Attribute.new('flag', "Didn't find schedule named #{data['occupancy_schedule']} in standards json.") elsif !schedule_target.to_ScheduleRuleset.is_initialized check_elems << OpenStudio::Attribute.new('flag', "Schedule named #{schedule_target.name} is a #{schedule_target.class}, not a ScheduleRuleset schedule.") else schedule_target = schedule_target.to_ScheduleRuleset.get # loop through and test individual load instances expected_hours = OpenstudioStandards::Schedules.schedule_ruleset_get_equivalent_full_load_hours(schedule_target) space_type.people.each do |space_load_instance| inst_sch_check = OpenstudioStandards::QAQC.space_load_instance_schedule_check(space_load_instance, expected_hours, std: std, min_pass_pct: min_pass_pct, max_pass_pct: max_pass_pct) if inst_sch_check then check_elems << inst_sch_check end end end end # @todo: check ventilation schedules # if objects are in the model should they just be always on schedule, or have a 8760 annual equivalent value # oa_schedule should not exist, or if it does shoudl be always on or have 8760 annual equivalent value if space_type.designSpecificationOutdoorAir.is_initialized oa = space_type.designSpecificationOutdoorAir.get if oa.outdoorAirFlowRateFractionSchedule.is_initialized # @todo: update measure test to check this expected_hours = 8760 inst_sch_check = OpenstudioStandards::QAQC.space_load_instance_schedule_check(oa, expected_hours, std: std, min_pass_pct: min_pass_pct, max_pass_pct: max_pass_pct) if inst_sch_check then check_elems << inst_sch_check end end end # notes # current logic only looks at 8760 values and not design days # when multiple instances of a type currently check every schedule by itself. In future could do weighted avgerage merge # not looking at infiltration schedules # not looking at luminaires # not looking at space loads, only loads at space type # only checking schedules where standard shows non zero load value # model load for space type where standards doesn't have one wont throw flag about mis-matched schedules end # warn if there are spaces in model that don't use space type unless they appear to be plenums @model.getSpaces.sort.each do |space| next if OpenstudioStandards::Space.space_plenum?(space) if !space.spaceType.is_initialized check_elems << OpenStudio::Attribute.new('flag', "#{space.name} doesn't have a space type assigned, can't validate schedules.") end end rescue StandardError => e # brief description of ruby error check_elems << OpenStudio::Attribute.new('flag', "Error prevented QAQC check from running (#{e}).") # backtrace of ruby error for diagnostic use if @error_backtrace then check_elems << OpenStudio::Attribute.new('flag', e.backtrace.join("\n").to_s) end end # add check_elms to new attribute check_elem = OpenStudio::Attribute.new('check', check_elems) return check_elem end
checks the weather files matches the appropriate weather file for the Los Angeles zip code
@param category [String] category to bin this check into @param zip_code [String] Los Angeles zip code @param name_only [Boolean] If true, only return the name of this check @return [OpenStudio::Attribute] OpenStudio Attribute object containing check results
# File lib/openstudio-standards/qaqc/weather_files.rb, line 111 def self.check_la_weather_files(category, zip_code, name_only: false) # summary of the check check_elems = OpenStudio::AttributeVector.new check_elems << OpenStudio::Attribute.new('name', 'LA Weather Files') check_elems << OpenStudio::Attribute.new('category', category) check_elems << OpenStudio::Attribute.new('description', 'Check that correct weather file was used for the selected zip code.') # stop here if only name is requested this is used to populate display name for arguments if name_only == true results = [] check_elems.each do |elem| results << elem.valueAsString end return results end begin # get weather file model_epw = nil if @model.getWeatherFile.url.is_initialized model_epw = @model.getWeatherFile.url.get model_epw = model_epw.gsub('file:', '') model_epw = model_epw.gsub('files/', '') end # Get the correct weather file based on the zip code zip_to_epw = { '90001' => 'USA_CA_Hawthorne-Jack.Northrop.Field.722956_TMY3.epw', '90002' => 'USA_CA_Hawthorne-Jack.Northrop.Field.722956_TMY3.epw', '90003' => 'USA_CA_Hawthorne-Jack.Northrop.Field.722956_TMY3.epw', '90004' => 'LOS-ANGELES-DOWNTOWN_722874_CZ2010.epw', '90005' => 'LOS-ANGELES-DOWNTOWN_722874_CZ2010.epw', '90006' => 'LOS-ANGELES-DOWNTOWN_722874_CZ2010.epw', '90007' => 'LOS-ANGELES-DOWNTOWN_722874_CZ2010.epw', '90008' => 'USA_CA_Hawthorne-Jack.Northrop.Field.722956_TMY3.epw', '90010' => 'LOS-ANGELES-DOWNTOWN_722874_CZ2010.epw', '90011' => 'USA_CA_Hawthorne-Jack.Northrop.Field.722956_TMY3.epw', '90012' => 'LOS-ANGELES-DOWNTOWN_722874_CZ2010.epw', '90013' => 'LOS-ANGELES-DOWNTOWN_722874_CZ2010.epw', '90014' => 'LOS-ANGELES-DOWNTOWN_722874_CZ2010.epw', '90015' => 'LOS-ANGELES-DOWNTOWN_722874_CZ2010.epw', '90016' => 'LOS-ANGELES-DOWNTOWN_722874_CZ2010.epw', '90017' => 'LOS-ANGELES-DOWNTOWN_722874_CZ2010.epw', '90018' => 'LOS-ANGELES-DOWNTOWN_722874_CZ2010.epw', '90019' => 'LOS-ANGELES-DOWNTOWN_722874_CZ2010.epw', '90020' => 'LOS-ANGELES-DOWNTOWN_722874_CZ2010.epw', '90021' => 'LOS-ANGELES-DOWNTOWN_722874_CZ2010.epw', '90022' => 'LOS-ANGELES-DOWNTOWN_722874_CZ2010.epw', '90023' => 'LOS-ANGELES-DOWNTOWN_722874_CZ2010.epw', '90024' => 'USA_CA_Santa.Monica.Muni.AP.722885_TMY3.epw', '90025' => 'USA_CA_Santa.Monica.Muni.AP.722885_TMY3.epw', '90026' => 'LOS-ANGELES-DOWNTOWN_722874_CZ2010.epw', '90027' => 'USA_CA_Burbank-Glendale-Pasadena.Bob.Hope.AP.722880_ TMY3.epw', '90028' => 'USA_CA_Burbank-Glendale-Pasadena.Bob.Hope.AP.722880_ TMY3.epw', '90029' => 'LOS-ANGELES-DOWNTOWN_722874_CZ2010.epw', '90031' => 'LOS-ANGELES-DOWNTOWN_722874_CZ2010.epw', '90032' => 'LOS-ANGELES-DOWNTOWN_722874_CZ2010.epw', '90033' => 'LOS-ANGELES-DOWNTOWN_722874_CZ2010.epw', '90034' => 'USA_CA_Santa.Monica.Muni.AP.722885_TMY3.epw', '90035' => 'LOS-ANGELES-DOWNTOWN_722874_CZ2010.epw', '90036' => 'LOS-ANGELES-DOWNTOWN_722874_CZ2010.epw', '90037' => 'USA_CA_Hawthorne-Jack.Northrop.Field.722956_TMY3.epw', '90038' => 'LOS-ANGELES-DOWNTOWN_722874_CZ2010.epw', '90039' => 'LOS-ANGELES-DOWNTOWN_722874_CZ2010.epw', '90040' => 'LOS-ANGELES-DOWNTOWN_722874_CZ2010.epw', '90041' => 'LOS-ANGELES-DOWNTOWN_722874_CZ2010.epw', '90042' => 'LOS-ANGELES-DOWNTOWN_722874_CZ2010.epw', '90043' => 'USA_CA_Hawthorne-Jack.Northrop.Field.722956_TMY3.epw', '90044' => 'USA_CA_Hawthorne-Jack.Northrop.Field.722956_TMY3.epw', '90045' => 'USA_CA_Los.Angeles.Intl.AP.722950_TMY3.epw', '90046' => 'USA_CA_Burbank-Glendale-Pasadena.Bob.Hope.AP.722880_ TMY3.epw', '90047' => 'USA_CA_Hawthorne-Jack.Northrop.Field.722956_TMY3.epw', '90048' => 'LOS-ANGELES-DOWNTOWN_722874_CZ2010.epw', '90049' => 'USA_CA_Santa.Monica.Muni.AP.722885_TMY3.epw', '90056' => 'USA_CA_Los.Angeles.Intl.AP.722950_TMY3.epw', '90057' => 'LOS-ANGELES-DOWNTOWN_722874_CZ2010.epw', '90058' => 'USA_CA_Hawthorne-Jack.Northrop.Field.722956_TMY3.epw', '90059' => 'USA_CA_Hawthorne-Jack.Northrop.Field.722956_TMY3.epw', '90061' => 'USA_CA_Hawthorne-Jack.Northrop.Field.722956_TMY3.epw', '90062' => 'USA_CA_Hawthorne-Jack.Northrop.Field.722956_TMY3.epw', '90063' => 'LOS-ANGELES-DOWNTOWN_722874_CZ2010.epw', '90064' => 'USA_CA_Santa.Monica.Muni.AP.722885_TMY3.epw', '90065' => 'LOS-ANGELES-DOWNTOWN_722874_CZ2010.epw', '90066' => 'USA_CA_Santa.Monica.Muni.AP.722885_TMY3.epw', '90067' => 'LOS-ANGELES-DOWNTOWN_722874_CZ2010.epw', '90068' => 'USA_CA_Burbank-Glendale-Pasadena.Bob.Hope.AP.722880_ TMY3.epw', '90069' => 'USA_CA_Burbank-Glendale-Pasadena.Bob.Hope.AP.722880_ TMY3.epw', '90071' => 'LOS-ANGELES-DOWNTOWN_722874_CZ2010.epw', '90073' => 'USA_CA_Santa.Monica.Muni.AP.722885_TMY3.epw', '90077' => 'USA_CA_Santa.Monica.Muni.AP.722885_TMY3.epw', '90089' => 'USA_CA_Hawthorne-Jack.Northrop.Field.722956_TMY3.epw', '90094' => 'USA_CA_Santa.Monica.Muni.AP.722885_TMY3.epw', '90095' => 'USA_CA_Santa.Monica.Muni.AP.722885_TMY3.epw', '90201' => 'USA_CA_Hawthorne-Jack.Northrop.Field.722956_TMY3.epw', '90210' => 'USA_CA_Burbank-Glendale-Pasadena.Bob.Hope.AP.722880_ TMY3.epw', '90211' => 'LOS-ANGELES-DOWNTOWN_722874_CZ2010.epw', '90212' => 'USA_CA_Burbank-Glendale-Pasadena.Bob.Hope.AP.722880_ TMY3.epw', '90222' => 'USA_CA_Hawthorne-Jack.Northrop.Field.722956_TMY3.epw', '90230' => 'USA_CA_Santa.Monica.Muni.AP.722885_TMY3.epw', '90232' => 'USA_CA_Santa.Monica.Muni.AP.722885_TMY3.epw', '90240' => 'LOS-ANGELES-DOWNTOWN_722874_CZ2010.epw', '90245' => 'USA_CA_Los.Angeles.Intl.AP.722950_TMY3.epw', '90247' => 'USA_CA_Hawthorne-Jack.Northrop.Field.722956_TMY3.epw', '90248' => 'USA_CA_Hawthorne-Jack.Northrop.Field.722956_TMY3.epw', '90249' => 'USA_CA_Hawthorne-Jack.Northrop.Field.722956_TMY3.epw', '90250' => 'USA_CA_Hawthorne-Jack.Northrop.Field.722956_TMY3.epw', '90254' => 'USA_CA_Los.Angeles.Intl.AP.722950_TMY3.epw', '90255' => 'USA_CA_Hawthorne-Jack.Northrop.Field.722956_TMY3.epw', '90260' => 'USA_CA_Hawthorne-Jack.Northrop.Field.722956_TMY3.epw', '90262' => 'USA_CA_Hawthorne-Jack.Northrop.Field.722956_TMY3.epw', '90263' => 'USA_CA_Santa.Monica.Muni.AP.722885_TMY3.epw', '90265' => 'USA_CA_Van.Nuys.AP.722886_TMY3.epw', '90266' => 'USA_CA_Los.Angeles.Intl.AP.722950_TMY3.epw', '90270' => 'USA_CA_Hawthorne-Jack.Northrop.Field.722956_TMY3.epw', '90272' => 'USA_CA_Santa.Monica.Muni.AP.722885_TMY3.epw', '90274' => 'TORRANCE_722955_CZ2010.epw', '90275' => 'TORRANCE_722955_CZ2010.epw', '90277' => 'TORRANCE_722955_CZ2010.epw', '90278' => 'USA_CA_Hawthorne-Jack.Northrop.Field.722956_TMY3.epw', '90280' => 'USA_CA_Hawthorne-Jack.Northrop.Field.722956_TMY3.epw', '90290' => 'USA_CA_Santa.Monica.Muni.AP.722885_TMY3.epw', '90291' => 'USA_CA_Santa.Monica.Muni.AP.722885_TMY3.epw', '90292' => 'USA_CA_Santa.Monica.Muni.AP.722885_TMY3.epw', '90293' => 'USA_CA_Los.Angeles.Intl.AP.722950_TMY3.epw', '90301' => 'USA_CA_Hawthorne-Jack.Northrop.Field.722956_TMY3.epw', '90302' => 'USA_CA_Hawthorne-Jack.Northrop.Field.722956_TMY3.epw', '90303' => 'USA_CA_Hawthorne-Jack.Northrop.Field.722956_TMY3.epw', '90304' => 'USA_CA_Hawthorne-Jack.Northrop.Field.722956_TMY3.epw', '90305' => 'USA_CA_Hawthorne-Jack.Northrop.Field.722956_TMY3.epw', '90401' => 'USA_CA_Santa.Monica.Muni.AP.722885_TMY3.epw', '90402' => 'USA_CA_Santa.Monica.Muni.AP.722885_TMY3.epw', '90403' => 'USA_CA_Santa.Monica.Muni.AP.722885_TMY3.epw', '90404' => 'USA_CA_Santa.Monica.Muni.AP.722885_TMY3.epw', '90405' => 'USA_CA_Santa.Monica.Muni.AP.722885_TMY3.epw', '90501' => 'TORRANCE_722955_CZ2010.epw', '90502' => 'TORRANCE_722955_CZ2010.epw', '90503' => 'TORRANCE_722955_CZ2010.epw', '90504' => 'USA_CA_Hawthorne-Jack.Northrop.Field.722956_TMY3.epw', '90505' => 'TORRANCE_722955_CZ2010.epw', '90506' => 'USA_CA_Hawthorne-Jack.Northrop.Field.722956_TMY3.epw', '90601' => 'LOS-ANGELES-DOWNTOWN_722874_CZ2010.epw', '90602' => 'LOS-ANGELES-DOWNTOWN_722874_CZ2010.epw', '90603' => 'LOS-ANGELES-DOWNTOWN_722874_CZ2010.epw', '90604' => 'LOS-ANGELES-DOWNTOWN_722874_CZ2010.epw', '90605' => 'LOS-ANGELES-DOWNTOWN_722874_CZ2010.epw', '90606' => 'LOS-ANGELES-DOWNTOWN_722874_CZ2010.epw', '90621' => 'LOS-ANGELES-DOWNTOWN_722874_CZ2010.epw', '90631' => 'LOS-ANGELES-DOWNTOWN_722874_CZ2010.epw', '90638' => 'LOS-ANGELES-DOWNTOWN_722874_CZ2010.epw', '90639' => 'LOS-ANGELES-DOWNTOWN_722874_CZ2010.epw', '90640' => 'LOS-ANGELES-DOWNTOWN_722874_CZ2010.epw', '90650' => 'LOS-ANGELES-DOWNTOWN_722874_CZ2010.epw', '90660' => 'LOS-ANGELES-DOWNTOWN_722874_CZ2010.epw', '90670' => 'LOS-ANGELES-DOWNTOWN_722874_CZ2010.epw', '90680' => 'TORRANCE_722955_CZ2010.epw', '90710' => 'TORRANCE_722955_CZ2010.epw', '90717' => 'TORRANCE_722955_CZ2010.epw', '90720' => 'TORRANCE_722955_CZ2010.epw', '90731' => 'TORRANCE_722955_CZ2010.epw', '90732' => 'TORRANCE_722955_CZ2010.epw', '90740' => 'TORRANCE_722955_CZ2010.epw', '90742' => 'TORRANCE_722955_CZ2010.epw', '90743' => 'TORRANCE_722955_CZ2010.epw', '90744' => 'TORRANCE_722955_CZ2010.epw', '90745' => 'TORRANCE_722955_CZ2010.epw', '90746' => 'TORRANCE_722955_CZ2010.epw', '90755' => 'TORRANCE_722955_CZ2010.epw', '90802' => 'TORRANCE_722955_CZ2010.epw', '90803' => 'TORRANCE_722955_CZ2010.epw', '90804' => 'TORRANCE_722955_CZ2010.epw', '90806' => 'TORRANCE_722955_CZ2010.epw', '90807' => 'TORRANCE_722955_CZ2010.epw', '90810' => 'TORRANCE_722955_CZ2010.epw', '90813' => 'TORRANCE_722955_CZ2010.epw', '90814' => 'TORRANCE_722955_CZ2010.epw', '90815' => 'TORRANCE_722955_CZ2010.epw', '90840' => 'TORRANCE_722955_CZ2010.epw', '91001' => 'USA_CA_Burbank-Glendale-Pasadena.Bob.Hope.AP.722880_ TMY3.epw', '91006' => 'LOS-ANGELES-DOWNTOWN_722874_CZ2010.epw', '91007' => 'LOS-ANGELES-DOWNTOWN_722874_CZ2010.epw', '91008' => 'LOS-ANGELES-DOWNTOWN_722874_CZ2010.epw', '91010' => 'LOS-ANGELES-DOWNTOWN_722874_CZ2010.epw', '91011' => 'USA_CA_Burbank-Glendale-Pasadena.Bob.Hope.AP.722880_ TMY3.epw', '91016' => 'LOS-ANGELES-DOWNTOWN_722874_CZ2010.epw', '91020' => 'USA_CA_Burbank-Glendale-Pasadena.Bob.Hope.AP.722880_ TMY3.epw', '91024' => 'LOS-ANGELES-DOWNTOWN_722874_CZ2010.epw', '91030' => 'LOS-ANGELES-DOWNTOWN_722874_CZ2010.epw', '91040' => 'USA_CA_Burbank-Glendale-Pasadena.Bob.Hope.AP.722880_ TMY3.epw', '91042' => 'USA_CA_Burbank-Glendale-Pasadena.Bob.Hope.AP.722880_ TMY3.epw', '91101' => 'LOS-ANGELES-DOWNTOWN_722874_CZ2010.epw', '91103' => 'USA_CA_Burbank-Glendale-Pasadena.Bob.Hope.AP.722880_ TMY3.epw', '91104' => 'LOS-ANGELES-DOWNTOWN_722874_CZ2010.epw', '91105' => 'LOS-ANGELES-DOWNTOWN_722874_CZ2010.epw', '91106' => 'LOS-ANGELES-DOWNTOWN_722874_CZ2010.epw', '91107' => 'LOS-ANGELES-DOWNTOWN_722874_CZ2010.epw', '91108' => 'LOS-ANGELES-DOWNTOWN_722874_CZ2010.epw', '91123' => 'LOS-ANGELES-DOWNTOWN_722874_CZ2010.epw', '91201' => 'USA_CA_Burbank-Glendale-Pasadena.Bob.Hope.AP.722880_ TMY3.epw', '91202' => 'USA_CA_Burbank-Glendale-Pasadena.Bob.Hope.AP.722880_ TMY3.epw', '91203' => 'USA_CA_Burbank-Glendale-Pasadena.Bob.Hope.AP.722880_ TMY3.epw', '91204' => 'USA_CA_Burbank-Glendale-Pasadena.Bob.Hope.AP.722880_ TMY3.epw', '91205' => 'LOS-ANGELES-DOWNTOWN_722874_CZ2010.epw', '91206' => 'USA_CA_Burbank-Glendale-Pasadena.Bob.Hope.AP.722880_ TMY3.epw', '91207' => 'USA_CA_Burbank-Glendale-Pasadena.Bob.Hope.AP.722880_ TMY3.epw', '91208' => 'USA_CA_Burbank-Glendale-Pasadena.Bob.Hope.AP.722880_ TMY3.epw', '91214' => 'USA_CA_Burbank-Glendale-Pasadena.Bob.Hope.AP.722880_ TMY3.epw', '91301' => 'USA_CA_Van.Nuys.AP.722886_TMY3.epw', '91302' => 'USA_CA_Santa.Monica.Muni.AP.722885_TMY3.epw', '91303' => 'USA_CA_Van.Nuys.AP.722886_TMY3.epw', '91304' => 'USA_CA_Van.Nuys.AP.722886_TMY3.epw', '91306' => 'USA_CA_Van.Nuys.AP.722886_TMY3.epw', '91307' => 'USA_CA_Van.Nuys.AP.722886_TMY3.epw', '91311' => 'USA_CA_Van.Nuys.AP.722886_TMY3.epw', '91316' => 'USA_CA_Van.Nuys.AP.722886_TMY3.epw', '91320' => 'USA_CA_Van.Nuys.AP.722886_TMY3.epw', '91321' => 'USA_CA_Van.Nuys.AP.722886_TMY3.epw', '91324' => 'USA_CA_Van.Nuys.AP.722886_TMY3.epw', '91325' => 'USA_CA_Van.Nuys.AP.722886_TMY3.epw', '91326' => 'USA_CA_Van.Nuys.AP.722886_TMY3.epw', '91330' => 'USA_CA_Van.Nuys.AP.722886_TMY3.epw', '91331' => 'USA_CA_Van.Nuys.AP.722886_TMY3.epw', '91335' => 'USA_CA_Van.Nuys.AP.722886_TMY3.epw', '91340' => 'USA_CA_Van.Nuys.AP.722886_TMY3.epw', '91342' => 'USA_CA_Van.Nuys.AP.722886_TMY3.epw', '91343' => 'USA_CA_Van.Nuys.AP.722886_TMY3.epw', '91344' => 'USA_CA_Van.Nuys.AP.722886_TMY3.epw', '91345' => 'USA_CA_Van.Nuys.AP.722886_TMY3.epw', '91350' => 'USA_CA_Van.Nuys.AP.722886_TMY3.epw', '91351' => 'USA_CA_Van.Nuys.AP.722886_TMY3.epw', '91352' => 'USA_CA_Burbank-Glendale-Pasadena.Bob.Hope.AP.722880_ TMY3.epw', '91354' => 'USA_CA_Van.Nuys.AP.722886_TMY3.epw', '91355' => 'USA_CA_Van.Nuys.AP.722886_TMY3.epw', '91356' => 'USA_CA_Van.Nuys.AP.722886_TMY3.epw', '91360' => 'USA_CA_Van.Nuys.AP.722886_TMY3.epw', '91361' => 'USA_CA_Van.Nuys.AP.722886_TMY3.epw', '91362' => 'USA_CA_Van.Nuys.AP.722886_TMY3.epw', '91364' => 'USA_CA_Van.Nuys.AP.722886_TMY3.epw', '91367' => 'USA_CA_Van.Nuys.AP.722886_TMY3.epw', '91371' => 'USA_CA_Van.Nuys.AP.722886_TMY3.epw', '91377' => 'USA_CA_Van.Nuys.AP.722886_TMY3.epw', '91381' => 'USA_CA_Van.Nuys.AP.722886_TMY3.epw', '91384' => 'USA_CA_Van.Nuys.AP.722886_TMY3.epw', '91387' => 'USA_CA_Van.Nuys.AP.722886_TMY3.epw', '91390' => 'USA_CA_Van.Nuys.AP.722886_TMY3.epw', '91401' => 'USA_CA_Van.Nuys.AP.722886_TMY3.epw', '91402' => 'USA_CA_Van.Nuys.AP.722886_TMY3.epw', '91403' => 'USA_CA_Van.Nuys.AP.722886_TMY3.epw', '91405' => 'USA_CA_Van.Nuys.AP.722886_TMY3.epw', '91406' => 'USA_CA_Van.Nuys.AP.722886_TMY3.epw', '91411' => 'USA_CA_Van.Nuys.AP.722886_TMY3.epw', '91423' => 'USA_CA_Van.Nuys.AP.722886_TMY3.epw', '91436' => 'USA_CA_Van.Nuys.AP.722886_TMY3.epw', '91501' => 'USA_CA_Burbank-Glendale-Pasadena.Bob.Hope.AP.722880_ TMY3.epw', '91502' => 'USA_CA_Burbank-Glendale-Pasadena.Bob.Hope.AP.722880_ TMY3.epw', '91504' => 'USA_CA_Burbank-Glendale-Pasadena.Bob.Hope.AP.722880_ TMY3.epw', '91505' => 'USA_CA_Burbank-Glendale-Pasadena.Bob.Hope.AP.722880_ TMY3.epw', '91506' => 'USA_CA_Burbank-Glendale-Pasadena.Bob.Hope.AP.722880_ TMY3.epw', '91521' => 'USA_CA_Burbank-Glendale-Pasadena.Bob.Hope.AP.722880_ TMY3.epw', '91522' => 'USA_CA_Burbank-Glendale-Pasadena.Bob.Hope.AP.722880_ TMY3.epw', '91523' => 'USA_CA_Burbank-Glendale-Pasadena.Bob.Hope.AP.722880_ TMY3.epw', '91601' => 'USA_CA_Burbank-Glendale-Pasadena.Bob.Hope.AP.722880_ TMY3.epw', '91602' => 'USA_CA_Burbank-Glendale-Pasadena.Bob.Hope.AP.722880_ TMY3.epw', '91604' => 'USA_CA_Burbank-Glendale-Pasadena.Bob.Hope.AP.722880_ TMY3.epw', '91605' => 'USA_CA_Burbank-Glendale-Pasadena.Bob.Hope.AP.722880_ TMY3.epw', '91606' => 'USA_CA_Burbank-Glendale-Pasadena.Bob.Hope.AP.722880_ TMY3.epw', '91607' => 'USA_CA_Burbank-Glendale-Pasadena.Bob.Hope.AP.722880_ TMY3.epw', '91608' => 'USA_CA_Burbank-Glendale-Pasadena.Bob.Hope.AP.722880_ TMY3.epw', '91702' => 'LOS-ANGELES-DOWNTOWN_722874_CZ2010.epw', '91706' => 'LOS-ANGELES-DOWNTOWN_722874_CZ2010.epw', '91709' => 'LOS-ANGELES-DOWNTOWN_722874_CZ2010.epw', '91710' => 'LOS-ANGELES-DOWNTOWN_722874_CZ2010.epw', '91711' => 'LOS-ANGELES-DOWNTOWN_722874_CZ2010.epw', '91722' => 'LOS-ANGELES-DOWNTOWN_722874_CZ2010.epw', '91723' => 'LOS-ANGELES-DOWNTOWN_722874_CZ2010.epw', '91724' => 'LOS-ANGELES-DOWNTOWN_722874_CZ2010.epw', '91731' => 'LOS-ANGELES-DOWNTOWN_722874_CZ2010.epw', '91732' => 'LOS-ANGELES-DOWNTOWN_722874_CZ2010.epw', '91733' => 'LOS-ANGELES-DOWNTOWN_722874_CZ2010.epw', '91740' => 'LOS-ANGELES-DOWNTOWN_722874_CZ2010.epw', '91741' => 'LOS-ANGELES-DOWNTOWN_722874_CZ2010.epw', '91744' => 'LOS-ANGELES-DOWNTOWN_722874_CZ2010.epw', '91745' => 'LOS-ANGELES-DOWNTOWN_722874_CZ2010.epw', '91746' => 'LOS-ANGELES-DOWNTOWN_722874_CZ2010.epw', '91748' => 'LOS-ANGELES-DOWNTOWN_722874_CZ2010.epw', '91750' => 'LOS-ANGELES-DOWNTOWN_722874_CZ2010.epw', '91754' => 'LOS-ANGELES-DOWNTOWN_722874_CZ2010.epw', '91755' => 'LOS-ANGELES-DOWNTOWN_722874_CZ2010.epw', '91763' => 'LOS-ANGELES-DOWNTOWN_722874_CZ2010.epw', '91765' => 'LOS-ANGELES-DOWNTOWN_722874_CZ2010.epw', '91766' => 'LOS-ANGELES-DOWNTOWN_722874_CZ2010.epw', '91767' => 'LOS-ANGELES-DOWNTOWN_722874_CZ2010.epw', '91768' => 'LOS-ANGELES-DOWNTOWN_722874_CZ2010.epw', '91770' => 'LOS-ANGELES-DOWNTOWN_722874_CZ2010.epw', '91773' => 'LOS-ANGELES-DOWNTOWN_722874_CZ2010.epw', '91775' => 'LOS-ANGELES-DOWNTOWN_722874_CZ2010.epw', '91776' => 'LOS-ANGELES-DOWNTOWN_722874_CZ2010.epw', '91780' => 'LOS-ANGELES-DOWNTOWN_722874_CZ2010.epw', '91784' => 'LOS-ANGELES-DOWNTOWN_722874_CZ2010.epw', '91789' => 'LOS-ANGELES-DOWNTOWN_722874_CZ2010.epw', '91790' => 'LOS-ANGELES-DOWNTOWN_722874_CZ2010.epw', '91791' => 'LOS-ANGELES-DOWNTOWN_722874_CZ2010.epw', '91792' => 'LOS-ANGELES-DOWNTOWN_722874_CZ2010.epw', '91801' => 'LOS-ANGELES-DOWNTOWN_722874_CZ2010.epw', '91803' => 'LOS-ANGELES-DOWNTOWN_722874_CZ2010.epw', '92603' => 'TORRANCE_722955_CZ2010.epw', '92612' => 'TORRANCE_722955_CZ2010.epw', '92614' => 'TORRANCE_722955_CZ2010.epw', '92617' => 'TORRANCE_722955_CZ2010.epw', '92624' => 'TORRANCE_722955_CZ2010.epw', '92625' => 'TORRANCE_722955_CZ2010.epw', '92626' => 'TORRANCE_722955_CZ2010.epw', '92627' => 'TORRANCE_722955_CZ2010.epw', '92629' => 'TORRANCE_722955_CZ2010.epw', '92637' => 'TORRANCE_722955_CZ2010.epw', '92646' => 'TORRANCE_722955_CZ2010.epw', '92647' => 'TORRANCE_722955_CZ2010.epw', '92648' => 'TORRANCE_722955_CZ2010.epw', '92649' => 'TORRANCE_722955_CZ2010.epw', '92651' => 'TORRANCE_722955_CZ2010.epw', '92653' => 'TORRANCE_722955_CZ2010.epw', '92655' => 'TORRANCE_722955_CZ2010.epw', '92656' => 'TORRANCE_722955_CZ2010.epw', '92657' => 'TORRANCE_722955_CZ2010.epw', '92660' => 'TORRANCE_722955_CZ2010.epw', '92661' => 'TORRANCE_722955_CZ2010.epw', '92662' => 'TORRANCE_722955_CZ2010.epw', '92663' => 'TORRANCE_722955_CZ2010.epw', '92672' => 'TORRANCE_722955_CZ2010.epw', '92673' => 'TORRANCE_722955_CZ2010.epw', '92675' => 'TORRANCE_722955_CZ2010.epw', '92677' => 'TORRANCE_722955_CZ2010.epw', '92683' => 'TORRANCE_722955_CZ2010.epw', '92691' => 'TORRANCE_722955_CZ2010.epw', '92692' => 'TORRANCE_722955_CZ2010.epw', '92697' => 'TORRANCE_722955_CZ2010.epw', '92703' => 'TORRANCE_722955_CZ2010.epw', '92704' => 'TORRANCE_722955_CZ2010.epw', '92707' => 'TORRANCE_722955_CZ2010.epw', '92708' => 'TORRANCE_722955_CZ2010.epw', '92821' => 'LOS-ANGELES-DOWNTOWN_722874_CZ2010.epw', '92823' => 'LOS-ANGELES-DOWNTOWN_722874_CZ2010.epw', '92833' => 'LOS-ANGELES-DOWNTOWN_722874_CZ2010.epw', '92841' => 'TORRANCE_722955_CZ2010.epw', '92843' => 'TORRANCE_722955_CZ2010.epw', '92844' => 'TORRANCE_722955_CZ2010.epw', '92845' => 'TORRANCE_722955_CZ2010.epw', '93001' => 'USA_CA_Van.Nuys.AP.722886_TMY3.epw', '93003' => 'USA_CA_Van.Nuys.AP.722886_TMY3.epw', '93004' => 'USA_CA_Van.Nuys.AP.722886_TMY3.epw', '93012' => 'USA_CA_Van.Nuys.AP.722886_TMY3.epw', '93013' => 'USA_CA_Van.Nuys.AP.722886_TMY3.epw', '93015' => 'USA_CA_Van.Nuys.AP.722886_TMY3.epw', '93021' => 'USA_CA_Van.Nuys.AP.722886_TMY3.epw', '93022' => 'USA_CA_Van.Nuys.AP.722886_TMY3.epw', '93023' => 'USA_CA_Van.Nuys.AP.722886_TMY3.epw', '93060' => 'USA_CA_Van.Nuys.AP.722886_TMY3.epw', '93063' => 'USA_CA_Van.Nuys.AP.722886_TMY3.epw', '93065' => 'USA_CA_Van.Nuys.AP.722886_TMY3.epw', '93066' => 'USA_CA_Van.Nuys.AP.722886_TMY3.epw', '93108' => 'USA_CA_Van.Nuys.AP.722886_TMY3.epw', '93510' => 'USA_CA_Burbank-Glendale-Pasadena.Bob.Hope.AP.722880_ TMY3.epw' } correct_epw = zip_to_epw[zip_code] if correct_epw.nil? check_elems << OpenStudio::Attribute.new('flag', "There is no correct weather file specified for the zip code #{zip_code}") end unless model_epw == correct_epw check_elems << OpenStudio::Attribute.new('flag', "The selected weather file #{model_epw} is incorrect for zip code #{zip_code}. The correct weather file is #{correct_epw}.") end rescue StandardError => e # brief description of ruby error check_elems << OpenStudio::Attribute.new('flag', "Error prevented QAQC check from running (#{e}).") # backtrace of ruby error for diagnostic use if @error_backtrace then check_elems << OpenStudio::Attribute.new('flag', e.backtrace.join("\n").to_s) end end # add check_elms to new attribute check_elem = OpenStudio::Attribute.new('check', check_elems) return check_elem end
Check that all zones with people are conditioned (have a thermostat with setpoints)
@param category [String] category to bin this check into @param target_standard [String] standard template, e.g. ‘90.1-2013’ @param name_only [Boolean] If true, only return the name of this check @return [OpenStudio::Attribute] OpenStudio Attribute object containing check results
# File lib/openstudio-standards/qaqc/zone_conditions.rb, line 159 def self.check_occupied_zones_conditioned(category, target_standard, name_only: false) # summary of the check check_elems = OpenStudio::AttributeVector.new check_elems << OpenStudio::Attribute.new('name', 'Conditioned Zones') check_elems << OpenStudio::Attribute.new('category', category) check_elems << OpenStudio::Attribute.new('description', 'Check that all zones with people have thermostats.') # stop here if only name is requested this is used to populate display name for arguments if name_only == true results = [] check_elems.each do |elem| results << elem.valueAsString end return results end std = Standard.build(target_standard) begin @model.getThermalZones.sort.each do |zone| # only check zones with people num_ppl = zone.numberOfPeople next unless zone.numberOfPeople > 0 # Check that the zone is heated (at a minimum) by checking that the heating setpoint is at least 41F. # Sometimes people include thermostats but use setpoints such that the system never comes on. unless OpenstudioStandards::ThermalZone.thermal_zone_heated?(zone) check_elems << OpenStudio::Attribute.new('flag', "#{zone.name} has #{num_ppl} people but is not heated. Zones containing people are expected to be conditioned, heated-only at a minimum. Heating setpoint must be at least 41F to be considered heated.") end end rescue StandardError => e # brief description of ruby error check_elems << OpenStudio::Attribute.new('flag', "Error prevented QAQC check from running (#{e}).") # backtrace of ruby error for diagnostic use if @error_backtrace then check_elems << OpenStudio::Attribute.new('flag', e.backtrace.join("\n").to_s) end end # add check_elms to new attribute check_elem = OpenStudio::Attribute.new('check', check_elems) return check_elem end
Check primary plant loop heating and cooling equipment capacity against coil loads to find equipment that is significantly oversized or undersized.
@param category [String] category to bin this check into @param target_standard [String] standard template, e.g. ‘90.1-2013’ @param max_pct_delta [Double] threshold for throwing an error for percent difference @param name_only [Boolean] If true, only return the name of this check @return [OpenStudio::Attribute] OpenStudio Attribute object containing check results
# File lib/openstudio-standards/qaqc/hvac.rb, line 1138 def self.check_plant_loop_capacity(category, target_standard, max_pct_delta: 0.3, name_only: false) # summary of the check check_elems = OpenStudio::AttributeVector.new check_elems << OpenStudio::Attribute.new('name', 'Plant Capacity') check_elems << OpenStudio::Attribute.new('category', category) check_elems << OpenStudio::Attribute.new('description', 'Check that plant equipment capacity matches loads.') # stop here if only name is requested this is used to populate display name for arguments if name_only == true results = [] check_elems.each do |elem| results << elem.valueAsString end return results end std = Standard.build(target_standard) begin # Check the heating and cooling capacity of the plant loops against their coil loads @model.getPlantLoops.sort.each do |plant_loop| # Heating capacity htg_cap_w = std.plant_loop_total_heating_capacity(plant_loop) # Cooling capacity clg_cap_w = std.plant_loop_total_cooling_capacity(plant_loop) # Sum the load for each coil on the loop htg_load_w = 0.0 clg_load_w = 0.0 plant_loop.demandComponents.each do |dc| obj_type = dc.iddObjectType.valueName.to_s case obj_type when 'OS_Coil_Heating_Water' coil = dc.to_CoilHeatingWater.get if coil.ratedCapacity.is_initialized htg_load_w += coil.ratedCapacity.get elsif coil.autosizedRatedCapacity.is_initialized htg_load_w += coil.autosizedRatedCapacity.get end when 'OS_Coil_Cooling_Water' coil = dc.to_CoilCoolingWater.get if coil.autosizedDesignCoilLoad.is_initialized clg_load_w += coil.autosizedDesignCoilLoad.get end end end # Don't check loops with no loads. These are probably SWH or non-typical loops that can't be checked by simple methods. # Heating if htg_load_w > 0 htg_cap_kbtu_per_hr = OpenStudio.convert(htg_cap_w, 'W', 'kBtu/hr').get.round(1) htg_load_kbtu_per_hr = OpenStudio.convert(htg_load_w, 'W', 'kBtu/hr').get.round(1) if ((htg_cap_w - htg_load_w) / htg_cap_w).abs > max_pct_delta check_elems << OpenStudio::Attribute.new('flag', "For #{plant_loop.name}, the total heating capacity of #{htg_cap_kbtu_per_hr} kBtu/hr is more than #{(max_pct_delta * 100.0).round(2)}% different from the combined coil load of #{htg_load_kbtu_per_hr} kBtu/hr. This could indicate significantly oversized or undersized equipment.") end end # Cooling if clg_load_w > 0 clg_cap_tons = OpenStudio.convert(clg_cap_w, 'W', 'ton').get.round(1) clg_load_tons = OpenStudio.convert(clg_load_w, 'W', 'ton').get.round(1) if ((clg_cap_w - clg_load_w) / clg_cap_w).abs > max_pct_delta check_elems << OpenStudio::Attribute.new('flag', "For #{plant_loop.name}, the total cooling capacity of #{clg_load_tons} tons is more than #{(max_pct_delta * 100.0).round(2)}% different from the combined coil load of #{clg_load_tons} tons. This could indicate significantly oversized or undersized equipment.") end end end rescue StandardError => e # brief description of ruby error check_elems << OpenStudio::Attribute.new('flag', "Error prevented QAQC check from running (#{e}).") # backtrace of ruby error for diagnostic use if @error_backtrace then check_elems << OpenStudio::Attribute.new('flag', e.backtrace.join("\n").to_s) end end # add check_elms to new attribute check_elem = OpenStudio::Attribute.new('check', check_elems) return check_elem end
Check the plant loop operational vs. sizing temperatures and make sure everything is coordinated. This identifies problems caused by sizing to one set of conditions and operating at a different set.
@param category [String] category to bin this check into @param max_sizing_temp_delta [Double] threshold for throwing an error for design sizing temperatures @param max_operating_temp_delta [Double] threshold for throwing an error on operating temperatures @param name_only [Boolean] If true, only return the name of this check @return [OpenStudio::Attribute] OpenStudio Attribute object containing check results
# File lib/openstudio-standards/qaqc/hvac.rb, line 1227 def self.check_plant_loop_temperatures(category, max_sizing_temp_delta: 2.0, max_operating_temp_delta: 5.0, name_only: false) # summary of the check check_elems = OpenStudio::AttributeVector.new check_elems << OpenStudio::Attribute.new('name', 'Plant Loop Temperatures') check_elems << OpenStudio::Attribute.new('category', category) check_elems << OpenStudio::Attribute.new('description', 'Check that plant loop sizing and operation temperatures are coordinated.') # stop here if only name is requested this is used to populate display name for arguments if name_only == true results = [] check_elems.each do |elem| results << elem.valueAsString end return results end begin # get the weather file run period (as opposed to design day run period) ann_env_pd = nil @sql.availableEnvPeriods.each do |env_pd| env_type = @sql.environmentType(env_pd) if env_type.is_initialized if env_type.get == OpenStudio::EnvironmentType.new('WeatherRunPeriod') ann_env_pd = env_pd break end end end # only try to get the annual timeseries if an annual simulation was run if ann_env_pd.nil? check_elems << OpenStudio::Attribute.new('flag', 'Cannot find the annual simulation run period, cannot check equipment part load ratios.') return check_elems end # Check each plant loop in the model @model.getPlantLoops.sort.each do |plant_loop| supply_outlet_node_name = plant_loop.supplyOutletNode.name.to_s design_supply_temperature = plant_loop.sizingPlant.designLoopExitTemperature design_supply_temperature = OpenStudio.convert(design_supply_temperature, 'C', 'F').get design_temperature_difference = plant_loop.sizingPlant.loopDesignTemperatureDifference design_temperature_difference = OpenStudio.convert(design_temperature_difference, 'K', 'R').get # get min and max temperatures from setpoint manager spm_name = '' spm_type = '<unspecified>' spm_min_temp_f = nil spm_max_temp_f = nil spms = plant_loop.supplyOutletNode.setpointManagers unless spms.empty? spm = spms[0] # assume first setpoint manager is only setpoint manager spm_name = spm.name spm_type = spm.iddObjectType.valueName.to_s spm_temps_f = OpenstudioStandards::HVAC.setpoint_manager_min_max_temperature(spm) spm_min_temp_f = spm_temps_f['min_temp'] spm_max_temp_f = spm_temps_f['max_temp'] end # check setpoint manager temperatures against design temperatures case plant_loop.sizingPlant.loopType when 'Heating' if spm_max_temp_f if (spm_max_temp_f - design_supply_temperature).abs > max_sizing_temp_delta check_elems << OpenStudio::Attribute.new('flag', "Minor Error: #{plant_loop.name} sizing uses a #{design_supply_temperature.round(1)}F supply water temperature, but the setpoint manager operates up to #{spm_max_temp_f.round(1)}F.") end end when 'Cooling' if spm_min_temp_f if (spm_min_temp_f - design_supply_temperature).abs > max_sizing_temp_delta check_elems << OpenStudio::Attribute.new('flag', "Minor Error: #{plant_loop.name} sizing uses a #{design_supply_temperature.round(1)}F supply water temperature, but the setpoint manager operates down to #{spm_min_temp_f.round(1)}F.") end end end # get supply water temperatures for supply outlet node supply_temp_timeseries = @sql.timeSeries(ann_env_pd, 'Timestep', 'System Node Temperature', supply_outlet_node_name) if supply_temp_timeseries.empty? check[:items] << { type: 'warning', msg: "No supply node temperature timeseries found for '#{plant_loop.name}'" } next else # convert to ruby array temperatures = [] supply_temp_vector = supply_temp_timeseries.get.values for i in (0..supply_temp_vector.size - 1) temperatures << supply_temp_vector[i] end end # get supply water flow rates for supply outlet node supply_flow_timeseries = @sql.timeSeries(ann_env_pd, 'Timestep', 'System Node Standard Density Volume Flow Rate', supply_outlet_node_name) if supply_flow_timeseries.empty? check_elems << OpenStudio::Attribute.new('flag', "Warning: No supply node temperature timeseries found for '#{plant_loop.name}'") next else # convert to ruby array flowrates = [] supply_flow_vector = supply_flow_timeseries.get.values for i in (0..supply_flow_vector.size - 1) flowrates << supply_flow_vector[i].to_f end end # check reasonableness of supply water temperatures when supply water flow rate is operating operating_temperatures = temperatures.select.with_index { |_t, k| flowrates[k] > 1e-8 } operating_temperatures = operating_temperatures.map { |t| (t * 1.8 + 32.0) } if operating_temperatures.empty? check_elems << OpenStudio::Attribute.new('flag', "Warning: Flowrates are all zero in supply node timeseries for '#{plant_loop.name}'") next end runtime_fraction = operating_temperatures.size.to_f / temperatures.size.to_f temps_out_of_bounds = [] case plant_loop.sizingPlant.loopType when 'Heating' design_return_temperature = design_supply_temperature - design_temperature_difference expected_max = spm_max_temp_f.nil? ? design_supply_temperature : [design_supply_temperature, spm_max_temp_f].max expected_min = spm_min_temp_f.nil? ? design_return_temperature : [design_return_temperature, spm_min_temp_f].min temps_out_of_bounds = (operating_temperatures.select { |t| (((t + max_operating_temp_delta) < expected_min) || ((t - max_operating_temp_delta) > expected_max)) }) when 'Cooling' design_return_temperature = design_supply_temperature + design_temperature_difference expected_max = spm_max_temp_f.nil? ? design_return_temperature : [design_return_temperature, spm_max_temp_f].max expected_min = spm_min_temp_f.nil? ? design_supply_temperature : [design_supply_temperature, spm_min_temp_f].min temps_out_of_bounds = (operating_temperatures.select { |t| (((t + max_operating_temp_delta) < expected_min) || ((t - max_operating_temp_delta) > expected_max)) }) when 'Condenser' design_return_temperature = design_supply_temperature + design_temperature_difference expected_max = spm_max_temp_f.nil? ? design_return_temperature : [design_return_temperature, spm_max_temp_f].max temps_out_of_bounds = (operating_temperatures.select { |t| ((t < 35.0) || (t > 100.0) || ((t - max_operating_temp_delta) > expected_max)) }) end next if temps_out_of_bounds.empty? min_op_temp_f = temps_out_of_bounds.min max_op_temp_f = temps_out_of_bounds.max # avg_F = temps_out_of_bounds.inject(:+).to_f / temps_out_of_bounds.size spm_min_temp_f = spm_min_temp_f.round(1) unless spm_min_temp_f.nil? spm_max_temp_f = spm_max_temp_f.round(1) unless spm_max_temp_f.nil? err = [] err << 'Major Error:' err << 'Expected supply water temperatures out of bounds for' err << "#{plant_loop.sizingPlant.loopType} plant loop '#{plant_loop.name}'" err << "with a #{design_supply_temperature.round(1)}F design supply temperature and" err << "#{design_return_temperature.round(1)}F design return temperature and" err << "a setpoint manager '#{spm_name}' of type '#{spm_type}' with a" err << "#{spm_min_temp_f}F minimum setpoint temperature and" err << "#{spm_max_temp_f}F maximum setpoint temperature." err << "Out of #{operating_temperatures.size}/#{temperatures.size} (#{(runtime_fraction * 100.0).round(1)}%) operating supply water temperatures" err << "#{temps_out_of_bounds.size}/#{operating_temperatures.size} (#{((temps_out_of_bounds.size.to_f / operating_temperatures.size) * 100.0).round(1)}%)" err << "are out of bounds with #{min_op_temp_f.round(1)}F min and #{max_op_temp_f.round(1)}F max." check_elems << OpenStudio::Attribute.new('flag', err.join(' ').gsub(/\n/, '')) end rescue StandardError => e # brief description of ruby error check_elems << OpenStudio::Attribute.new('flag', "Error prevented QAQC check from running (#{e}).") # backtrace of ruby error for diagnostic use if @error_backtrace then check_elems << OpenStudio::Attribute.new('flag', e.backtrace.join("\n").to_s) end end # add check_elms to new attribute check_elem = OpenStudio::Attribute.new('check', check_elems) return check_elem end
Check that there are no people or lights in plenums.
@param category [String] category to bin this check into @param target_standard [String] standard template, e.g. ‘90.1-2013’ @param name_only [Boolean] If true, only return the name of this check @return [OpenStudio::Attribute] OpenStudio Attribute object containing check results
# File lib/openstudio-standards/qaqc/zone_conditions.rb, line 12 def self.check_plenum_loads(category, target_standard, name_only: false) # summary of the check check_elems = OpenStudio::AttributeVector.new check_elems << OpenStudio::Attribute.new('name', 'Plenum Loads') check_elems << OpenStudio::Attribute.new('category', category) check_elems << OpenStudio::Attribute.new('description', 'Check that the plenums do not have people or lights.') # stop here if only name is requested this is used to populate display name for arguments if name_only == true results = [] check_elems.each do |elem| results << elem.valueAsString end return results end std = Standard.build(target_standard) begin @model.getThermalZones.sort.each do |zone| next unless OpenstudioStandards::ThermalZone.thermal_zone_plenum?(zone) # people num_people = zone.numberOfPeople if num_people > 0 check_elems << OpenStudio::Attribute.new('flag', "#{zone.name} is a plenum, but has #{num_people.round(1)} people. Plenums should not contain people.") end # lights lights_w = zone.lightingPower if lights_w > 0 check_elems << OpenStudio::Attribute.new('flag', "#{zone.name} is a plenum, but has #{lights_w.round(1)} W of lights. Plenums should not contain lights.") end end rescue StandardError => e # brief description of ruby error check_elems << OpenStudio::Attribute.new('flag', "Error prevented QAQC check from running (#{e}).") # backtrace of ruby error for diagnostic use if @error_backtrace then check_elems << OpenStudio::Attribute.new('flag', e.backtrace.join("\n").to_s) end end # add check_elms to new attribute check_elem = OpenStudio::Attribute.new('check', check_elems) return check_elem end
Check the pumping power (W/gpm) for each pump in the model to identify unrealistically sized pumps.
@param category [String] category to bin this check into @param target_standard [String] standard template, e.g. ‘90.1-2013’ @param max_pct_delta [Double] threshold for throwing an error for percent difference @param name_only [Boolean] If true, only return the name of this check @return [OpenStudio::Attribute] OpenStudio Attribute object containing check results
# File lib/openstudio-standards/qaqc/hvac.rb, line 1399 def self.check_pump_power(category, target_standard, max_pct_delta: 0.3, name_only: false) # summary of the check check_elems = OpenStudio::AttributeVector.new check_elems << OpenStudio::Attribute.new('name', 'Pump Power') check_elems << OpenStudio::Attribute.new('category', category) check_elems << OpenStudio::Attribute.new('description', 'Check that pump power vs flow makes sense.') # stop here if only name is requested this is used to populate display name for arguments if name_only == true results = [] check_elems.each do |elem| results << elem.valueAsString end return results end std = Standard.build(target_standard) begin # Check each plant loop @model.getPlantLoops.sort.each do |plant_loop| # Set the expected/typical W/gpm loop_type = plant_loop.sizingPlant.loopType case loop_type when 'Heating' expected_w_per_gpm = 19.0 when 'Cooling' expected_w_per_gpm = 22.0 when 'Condenser' expected_w_per_gpm = 19.0 end # Check the W/gpm for each pump on each plant loop plant_loop.supplyComponents.each do |component| # Get the W/gpm for the pump obj_type = component.iddObjectType.valueName.to_s case obj_type when 'OS_Pump_ConstantSpeed' actual_w_per_gpm = std.pump_rated_w_per_gpm(component.to_PumpConstantSpeed.get) when 'OS_Pump_VariableSpeed' actual_w_per_gpm = std.pump_rated_w_per_gpm(component.to_PumpVariableSpeed.get) when 'OS_HeaderedPumps_ConstantSpeed' actual_w_per_gpm = std.pump_rated_w_per_gpm(component.to_HeaderedPumpsConstantSpeed.get) when 'OS_HeaderedPumps_VariableSpeed' actual_w_per_gpm = std.pump_rated_w_per_gpm(component.to_HeaderedPumpsVariableSpeed.get) else next # Skip non-pump objects end # Compare W/gpm to expected/typical values if ((expected_w_per_gpm - actual_w_per_gpm) / actual_w_per_gpm).abs > max_pct_delta if plant_loop.name.get.to_s.downcase.include? 'service water loop' # some service water loops use just water main pressure and have a dummy pump check_elems << OpenStudio::Attribute.new('flag', "Warning: For #{component.name} on #{plant_loop.name}, the pumping power is #{actual_w_per_gpm.round(1)} W/gpm.") else check_elems << OpenStudio::Attribute.new('flag', "For #{component.name} on #{plant_loop.name}, the actual pumping power of #{actual_w_per_gpm.round(1)} W/gpm is more than #{(max_pct_delta * 100.0).round(2)}% different from the expected #{expected_w_per_gpm} W/gpm for a #{loop_type} plant loop.") end end end end rescue StandardError => e # brief description of ruby error check_elems << OpenStudio::Attribute.new('flag', "Error prevented QAQC check from running (#{e}).") # backtrace of ruby error for diagnostic use if @error_backtrace then check_elems << OpenStudio::Attribute.new('flag', e.backtrace.join("\n").to_s) end end # add check_elms to new attribute check_elem = OpenStudio::Attribute.new('check', check_elems) return check_elem end
Check that the lighting, equipment, and HVAC
setpoint schedules coordinate with the occupancy schedules. This is defined as having start and end times within the specified number of hours away from the occupancy schedule.
@param category [String] category to bin this check into @param target_standard [String] standard template, e.g. ‘90.1-2013’ @param max_hrs [Double] threshold for throwing an error for schedule coordination @param name_only [Boolean] If true, only return the name of this check @return [OpenStudio::Attribute] OpenStudio Attribute object containing check results
# File lib/openstudio-standards/qaqc/schedules.rb, line 14 def self.check_schedule_coordination(category, target_standard, max_hrs: 2.0, name_only: false) # summary of the check check_elems = OpenStudio::AttributeVector.new check_elems << OpenStudio::Attribute.new('name', 'Schedule Coordination') check_elems << OpenStudio::Attribute.new('category', category) check_elems << OpenStudio::Attribute.new('description', 'Check that lighting, equipment, and HVAC schedules coordinate with occupancy.') # stop here if only name is requested this is used to populate display name for arguments if name_only == true results = [] check_elems.each do |elem| results << elem.valueAsString end return results end std = Standard.build(target_standard) begin # Convert max hr limit to OpenStudio Time max_hrs = OpenStudio::Time.new(0, max_hrs.to_i, 0, 0) # Check schedules in each space @model.getSpaces.sort.each do |space| # Occupancy, Lighting, and Equipment Schedules coord_schs = [] occ_schs = [] # Get the space type (optional) space_type = space.spaceType # Occupancy occs = [] occs += space.people # From space directly occs += space_type.get.people if space_type.is_initialized # Inherited from space type occs.each do |occ| occ_schs << occ.numberofPeopleSchedule.get if occ.numberofPeopleSchedule.is_initialized end # Lights lts = [] lts += space.lights # From space directly lts += space_type.get.lights if space_type.is_initialized # Inherited from space type lts.each do |lt| coord_schs << lt.schedule.get if lt.schedule.is_initialized end # Equip plugs = [] plugs += space.electricEquipment # From space directly plugs += space_type.get.electricEquipment if space_type.is_initialized # Inherited from space type plugs.each do |plug| coord_schs << plug.schedule.get if plug.schedule.is_initialized end # HVAC Schedule (airloop-served zones only) if space.thermalZone.is_initialized zone = space.thermalZone.get if zone.airLoopHVAC.is_initialized coord_schs << zone.airLoopHVAC.get.availabilitySchedule end end # Cannot check spaces with no occupancy schedule to compare against next if occ_schs.empty? # Get start and end occupancy times from the first occupancy schedule times = OpenstudioStandards::Schedules.schedule_ruleset_get_start_and_end_times(occ_schs[0]) occ_start_time = times['start_time'] occ_end_time = times['end_time'] # Cannot check a space where the occupancy start time or end time cannot be determined next if occ_start_time.nil? || occ_end_time.nil? # Check all schedules against occupancy # Lights should have a start and end within X hrs of the occupancy start and end coord_schs.each do |coord_sch| # Get start and end time of load/HVAC schedule times = OpenstudioStandards::Schedules.schedule_ruleset_get_start_and_end_times(coord_sch) start_time = times['start_time'] end_time = times['end_time]'] if start_time.nil? check_elems << OpenStudio::Attribute.new('flag', "Could not determine start time of a schedule called #{coord_sch.name}, cannot determine if schedule coordinates with occupancy schedule.") next elsif end_time.nil? check_elems << OpenStudio::Attribute.new('flag', "Could not determine end time of a schedule called #{coord_sch.name}, cannot determine if schedule coordinates with occupancy schedule.") next end # Check start time if (occ_start_time - start_time) > max_hrs || (start_time - occ_start_time) > max_hrs check_elems << OpenStudio::Attribute.new('flag', "The start time of #{coord_sch.name} is #{start_time}, which is more than #{max_hrs} away from the occupancy schedule start time of #{occ_start_time} for #{occ_schs[0].name} in #{space.name}. Schedules do not coordinate.") end # Check end time if (occ_end_time - end_time) > max_hrs || (end_time - occ_end_time) > max_hrs check_elems << OpenStudio::Attribute.new('flag', "The end time of #{coord_sch.name} is #{end_time}, which is more than #{max_hrs} away from the occupancy schedule end time of #{occ_end_time} for #{occ_schs[0].name} in #{space.name}. Schedules do not coordinate.") end end end rescue StandardError => e # brief description of ruby error check_elems << OpenStudio::Attribute.new('flag', "Error prevented QAQC check from running (#{e}).") # backtrace of ruby error for diagnostic use if @error_backtrace then check_elems << OpenStudio::Attribute.new('flag', e.backtrace.join("\n").to_s) end end # add check_elms to new attribute check_elem = OpenStudio::Attribute.new('check', check_elems) return check_elem end
Checks the hot water use in a model for typical values
@param category [String] category to bin this check into @param target_standard [String] standard template, e.g. ‘90.1-2013’ @param min_pass_pct [Double] threshold for throwing an error for percent difference @param max_pass_pct [Double] threshold for throwing an error for percent difference @param name_only [Boolean] If true, only return the name of this check @return [OpenStudio::Attribute] OpenStudio Attribute object containing check results
# File lib/openstudio-standards/qaqc/service_water_heating.rb, line 14 def self.check_service_hot_water(category, target_standard, min_pass_pct: 0.25, max_pass_pct: 0.25, name_only: false) # @todo could expose meal turnover and people per unit for res and hotel into arguments # summary of the check check_elems = OpenStudio::AttributeVector.new check_elems << OpenStudio::Attribute.new('name', 'Domestic Hot Water') check_elems << OpenStudio::Attribute.new('category', category) if target_standard == 'ICC IECC 2015' check_elems << OpenStudio::Attribute.new('description', 'Check service water heating consumption against Table R405.5.2(1) in ICC IECC 2015 Residential Provisions.') else check_elems << OpenStudio::Attribute.new('description', 'Check against the 2011 ASHRAE Handbook - HVAC Applications, Table 7 section 50.14.') end # stop here if only name is requested this is used to populate display name for arguments if name_only == true results = [] check_elems.each do |elem| results << elem.valueAsString end return results end std = Standard.build(target_standard) begin # loop through water_use_equipment service_water_consumption_daily_avg_gal = 0.0 @model.getWaterUseEquipments.each do |water_use_equipment| # get peak flow rate from def peak_flow_rate_si = water_use_equipment.waterUseEquipmentDefinition.peakFlowRate source_units = 'm^3/s' target_units = 'gal/min' peak_flow_rate_ip = OpenStudio.convert(peak_flow_rate_si, source_units, target_units).get # get value from flow rate schedule if water_use_equipment.flowRateFractionSchedule.is_initialized # get annual equiv for model schedule schedule_inst = water_use_equipment.flowRateFractionSchedule.get annual_equiv_flow_rate = OpenstudioStandards::Schedules.schedule_get_equivalent_full_load_hours(schedule_inst) if annual_equiv_flow_rate.nil? check_elems << OpenStudio::Attribute.new('flag', "#{schedule_inst.name} isn't a Ruleset or Constant schedule. Can't calculate annual equivalent full load hours.") next end else # issue flag check_elems << OpenStudio::Attribute.new('flag', "#{water_use_equipment.name} doesn't have a schedule. Can't identify hot water consumption.") next end # add to global service water consumpiton value service_water_consumption_daily_avg_gal += 60.0 * peak_flow_rate_ip * annual_equiv_flow_rate / 365.0 end if target_standard == 'ICC IECC 2015' num_people = 0.0 @model.getSpaceTypes.each do |space_type| next if !space_type.standardsSpaceType.is_initialized next if space_type.standardsSpaceType.get != 'Apartment' # currently only supports midrise apt space type space_type_floor_area = space_type.floorArea space_type_num_people = space_type.getNumberOfPeople(space_type_floor_area) num_people += space_type_num_people end # lookup target gal/day for the building bedrooms_per_unit = 2.0 # assumption num_units = num_people / 2.5 # Avg 2.5 units per person. target_consumption = std.model_find_icc_iecc_2015_hot_water_demand(@model, num_units, bedrooms_per_unit) else # only other path for now is 90.1-2013 # get building type building_type = '' if @model.getBuilding.standardsBuildingType.is_initialized building_type = @model.getBuilding.standardsBuildingType.get end # lookup data from standards ashrae_hot_water_demand = std.model_find_ashrae_hot_water_demand(@model) # building type specific logic for water consumption # todo - update test to exercise various building types if !ashrae_hot_water_demand.empty? if building_type == 'FullServiceRestaurant' num_people_hours = 0.0 @model.getSpaceTypes.each do |space_type| next if !space_type.standardsSpaceType.is_initialized next if space_type.standardsSpaceType.get != 'Dining' space_type_floor_area = space_type.floorArea space_type_num_people_hours = 0.0 # loop through peole instances space_type.peoples.each do |inst| inst_num_people = inst.getNumberOfPeople(space_type_floor_area) inst_schedule = inst.numberofPeopleSchedule.get # sim will fail prior to this if doesn't have it annual_equivalent_people = OpenstudioStandards::Schedules.schedule_get_equivalent_full_load_hours(inst_schedule) if annual_equivalent_people.nil? check_elems << OpenStudio::Attribute.new('flag', "#{inst_schedule.name} isn't a Ruleset or Constant schedule. Can't calculate annual equivalent full load hours.") annual_equivalent_people = 0.0 end inst_num_people_hours = annual_equivalent_people * inst_num_people space_type_num_people_hours += inst_num_people_hours end num_people_hours += space_type_num_people_hours end num_meals = num_people_hours / 365.0 * 1.5 # 90 minute meal target_consumption = num_meals * ashrae_hot_water_demand.first[:avg_day_unit] elsif ['LargeHotel', 'SmallHotel'].include? building_type num_people = 0.0 @model.getSpaceTypes.each do |space_type| next if !space_type.standardsSpaceType.is_initialized next if space_type.standardsSpaceType.get != 'GuestRoom' space_type_floor_area = space_type.floorArea space_type_num_people = space_type.getNumberOfPeople(space_type_floor_area) num_people += space_type_num_people end # find best fit from returned results num_units = num_people / 2.0 # 2 people per room design load, not typical occupancy avg_day_unit = nil fit = nil ashrae_hot_water_demand.each do |block| if fit.nil? avg_day_unit = block[:avg_day_unit] fit = (avg_day_unit - block[:block]).abs elsif (avg_day_unit - block[:block]).abs - fit avg_day_unit = block[:avg_day_unit] fit = (avg_day_unit - block[:block]).abs end end target_consumption = num_units * avg_day_unit elsif building_type == 'MidriseApartment' num_people = 0.0 @model.getSpaceTypes.each do |space_type| next if !space_type.standardsSpaceType.is_initialized next if space_type.standardsSpaceType.get != 'Apartment' space_type_floor_area = space_type.floorArea space_type_num_people = space_type.getNumberOfPeople(space_type_floor_area) num_people += space_type_num_people end # find best fit from returned results num_units = num_people / 2.5 # Avg 2.5 units per person. avg_day_unit = nil fit = nil ashrae_hot_water_demand.each do |block| if fit.nil? avg_day_unit = block[:avg_day_unit] fit = (avg_day_unit - block[:block]).abs elsif (avg_day_unit - block[:block]).abs - fit avg_day_unit = block[:avg_day_unit] fit = (avg_day_unit - block[:block]).abs end end target_consumption = num_units * avg_day_unit elsif ['Office', 'LargeOffice', 'MediumOffice', 'SmallOffice'].include? building_type num_people = @model.getBuilding.numberOfPeople target_consumption = num_people * ashrae_hot_water_demand.first[:avg_day_unit] elsif building_type == 'PrimarySchool' num_people = 0.0 @model.getSpaceTypes.each do |space_type| next if !space_type.standardsSpaceType.is_initialized next if space_type.standardsSpaceType.get != 'Classroom' space_type_floor_area = space_type.floorArea space_type_num_people = space_type.getNumberOfPeople(space_type_floor_area) num_people += space_type_num_people end target_consumption = num_people * ashrae_hot_water_demand.first[:avg_day_unit] elsif building_type == 'QuickServiceRestaurant' num_people_hours = 0.0 @model.getSpaceTypes.each do |space_type| next if !space_type.standardsSpaceType.is_initialized next if space_type.standardsSpaceType.get != 'Dining' space_type_floor_area = space_type.floorArea space_type_num_people_hours = 0.0 # loop through peole instances space_type.peoples.each do |inst| inst_num_people = inst.getNumberOfPeople(space_type_floor_area) inst_schedule = inst.numberofPeopleSchedule.get # sim will fail prior to this if doesn't have it annual_equivalent_people = OpenstudioStandards::Schedules.schedule_get_equivalent_full_load_hours(inst_schedule) if annual_equivalent_people.nil? check_elems << OpenStudio::Attribute.new('flag', "#{inst_schedule.name} isn't a Ruleset or Constant schedule. Can't calculate annual equivalent full load hours.") annual_equivalent_people = 0.0 end inst_num_people_hours = annual_equivalent_people * inst_num_people space_type_num_people_hours += inst_num_people_hours end num_people_hours += space_type_num_people_hours end num_meals = num_people_hours / 365.0 * 0.5 # 30 minute leal # todo - add logic to address drive through traffic target_consumption = num_meals * ashrae_hot_water_demand.first[:avg_day_unit] elsif building_type == 'SecondarySchool' num_people = 0.0 @model.getSpaceTypes.each do |space_type| next if !space_type.standardsSpaceType.is_initialized next if space_type.standardsSpaceType.get != 'Classroom' space_type_floor_area = space_type.floorArea space_type_num_people = space_type.getNumberOfPeople(space_type_floor_area) num_people += space_type_num_people end target_consumption = num_people * ashrae_hot_water_demand.first[:avg_day_unit] else check_elems << OpenStudio::Attribute.new('flag', "No rule of thumb values exist for #{building_type}. Hot water consumption was not checked.") end else check_elems << OpenStudio::Attribute.new('flag', "No rule of thumb values exist for #{building_type}. Hot water consumption was not checked.") end end # check actual against target if service_water_consumption_daily_avg_gal < target_consumption * (1.0 - min_pass_pct) check_elems << OpenStudio::Attribute.new('flag', "Annual average of #{service_water_consumption_daily_avg_gal.round} gallons per day of hot water is more than #{min_pass_pct * 100} % below the expected value of #{target_consumption.round} gallons per day.") elsif service_water_consumption_daily_avg_gal > target_consumption * (1.0 + max_pass_pct) check_elems << OpenStudio::Attribute.new('flag', "Annual average of #{service_water_consumption_daily_avg_gal.round} gallons per day of hot water is more than #{max_pass_pct * 100} % above the expected value of #{target_consumption.round} gallons per day.") end rescue StandardError => e # brief description of ruby error check_elems << OpenStudio::Attribute.new('flag', "Error prevented QAQC check from running (#{e}).") # backtrace of ruby error for diagnostic use if @error_backtrace then check_elems << OpenStudio::Attribute.new('flag', e.backtrace.join("\n").to_s) end end # add check_elms to new attribute check_elem = OpenStudio::Attribute.new('check', check_elems) return check_elem end
Check for excess simulataneous heating and cooling
@param category [String] category to bin this check into @param max_pass_pct [Double] threshold for throwing an error for percent difference @param name_only [Boolean] If true, only return the name of this check @return [OpenStudio::Attribute] OpenStudio Attribute object containing check results
# File lib/openstudio-standards/qaqc/hvac.rb, line 1479 def self.check_simultaneous_heating_and_cooling(category, max_pass_pct: 0.1, name_only: false) # summary of the check check_elems = OpenStudio::AttributeVector.new check_elems << OpenStudio::Attribute.new('name', 'Simultaneous Heating and Cooling') check_elems << OpenStudio::Attribute.new('category', category) check_elems << OpenStudio::Attribute.new('description', 'Check for simultaneous heating and cooling by looping through all Single Duct VAV Reheat Air Terminals and analyzing hourly data when there is a cooling load. ') # stop here if only name is requested this is used to populate display name for arguments if name_only == true results = [] check_elems.each do |elem| results << elem.valueAsString end return results end begin # get the weather file run period (as opposed to design day run period) ann_env_pd = nil @sql.availableEnvPeriods.each do |env_pd| env_type = @sql.environmentType(env_pd) if env_type.is_initialized if env_type.get == OpenStudio::EnvironmentType.new('WeatherRunPeriod') ann_env_pd = env_pd break end end end # only try to get the annual timeseries if an annual simulation was run if ann_env_pd.nil? check_elems << OpenStudio::Attribute.new('flag', 'Cannot find the annual simulation run period, cannot determine simultaneous heating and cooling.') return check_elem end # For each VAV reheat terminal, calculate # the annual total % reheat hours. @model.getAirTerminalSingleDuctVAVReheats.sort.each do |term| # Reheat coil heating rate rht_coil = term.reheatCoil key_value = rht_coil.name.get.to_s.upcase # must be in all caps. time_step = 'Hourly' # "Zone Timestep", "Hourly", "HVAC System Timestep" variable_name = 'Heating Coil Heating Rate' variable_name_alt = 'Heating Coil Air Heating Rate' rht_rate_ts = @sql.timeSeries(ann_env_pd, time_step, variable_name, key_value) # key value would go at the end if we used it. # try and alternate variable name if rht_rate_ts.empty? rht_rate_ts = @sql.timeSeries(ann_env_pd, time_step, variable_name_alt, key_value) # key value would go at the end if we used it. end if rht_rate_ts.empty? check_elems << OpenStudio::Attribute.new('flag', "Heating Coil (Air) Heating Rate Timeseries not found for #{key_value}.") else rht_rate_ts = rht_rate_ts.get.values # Put timeseries into array rht_rate_vals = [] for i in 0..(rht_rate_ts.size - 1) rht_rate_vals << rht_rate_ts[i] end # Zone Air Terminal Sensible Heating Rate key_value = "ADU #{term.name.get.to_s.upcase}" # must be in all caps. time_step = 'Hourly' # "Zone Timestep", "Hourly", "HVAC System Timestep" variable_name = 'Zone Air Terminal Sensible Cooling Rate' clg_rate_ts = @sql.timeSeries(ann_env_pd, time_step, variable_name, key_value) # key value would go at the end if we used it. if clg_rate_ts.empty? check_elems << OpenStudio::Attribute.new('flag', "Zone Air Terminal Sensible Cooling Rate Timeseries not found for #{key_value}.") else clg_rate_ts = clg_rate_ts.get.values # Put timeseries into array clg_rate_vals = [] for i in 0..(clg_rate_ts.size - 1) clg_rate_vals << clg_rate_ts[i] end # Loop through each timestep and calculate the hourly # % reheat value. ann_rht_hrs = 0 ann_clg_hrs = 0 ann_pcts = [] rht_rate_vals.zip(clg_rate_vals).each do |rht_w, clg_w| # Skip hours with no cooling (in heating mode) next if clg_w == 0 pct_overcool_rht = rht_w / (rht_w + clg_w) ann_rht_hrs += pct_overcool_rht # implied * 1hr b/c hrly results ann_clg_hrs += 1 ann_pcts << pct_overcool_rht.round(3) end # Calculate annual % reheat hours ann_pct_reheat = ((ann_rht_hrs / ann_clg_hrs) * 100).round(1) # Compare to limit if ann_pct_reheat > max_pass_pct * 100.0 check_elems << OpenStudio::Attribute.new('flag', "#{term.name} has #{ann_pct_reheat}% overcool-reheat, which is greater than the limit of #{max_pass_pct * 100.0}%. This terminal is in cooling mode for #{ann_clg_hrs} hours of the year.") end end end end rescue StandardError => e # brief description of ruby error check_elems << OpenStudio::Attribute.new('flag', "Error prevented QAQC check from running (#{e}).") # backtrace of ruby error for diagnostic use if @error_backtrace then check_elems << OpenStudio::Attribute.new('flag', e.backtrace.join("\n").to_s) end end # add check_elms to new attribute check_elem = OpenStudio::Attribute.new('check', check_elems) return check_elem end
Check for excess simulataneous heating and cooling
@param category [String] category to bin this check into @param target_standard [String] standard template, e.g. ‘90.1-2013’ @param max_delta [Double] threshold for throwing an error for temperature difference @param name_only [Boolean] If true, only return the name of this check @return [OpenStudio::Attribute] OpenStudio Attribute object containing check results
# File lib/openstudio-standards/qaqc/zone_conditions.rb, line 66 def self.check_supply_air_and_thermostat_temperature_difference(category, target_standard, max_delta: 2.0, name_only: false) # G3.1.2.9 requires a 20 degree F delta between supply air temperature and zone temperature. target_clg_delta = 20.0 # summary of the check check_elems = OpenStudio::AttributeVector.new check_elems << OpenStudio::Attribute.new('name', 'Supply and Zone Air Temperature') check_elems << OpenStudio::Attribute.new('category', category) if @utility_name.nil? check_elems << OpenStudio::Attribute.new('description', "Check if fans modeled to ASHRAE 90.1 2013 Section G3.1.2.9 requirements. Compare the supply air temperature for each thermal zone against the thermostat setpoints. Throw flag if temperature difference excedes threshold of #{target_clg_delta}F plus the selected tolerance.") else check_elems << OpenStudio::Attribute.new('description', "Check if fans modeled to ASHRAE 90.1 2013 Section G3.1.2.9 requirements. Compare the supply air temperature for each thermal zone against the thermostat setpoints. Throw flag if temperature difference excedes threshold set by #{@utility_name}.") end # stop here if only name is requested this is used to populate display name for arguments if name_only == true results = [] check_elems.each do |elem| results << elem.valueAsString end return results end std = Standard.build(target_standard) begin # loop through thermal zones @model.getThermalZones.sort.each do |thermal_zone| # skip plenums next if OpenstudioStandards::ThermalZone.thermal_zone_plenum?(thermal_zone) # skip zones without thermostats next unless thermal_zone.thermostatSetpointDualSetpoint.is_initialized # populate thermostat ranges model_clg_min = nil thermostat = thermal_zone.thermostatSetpointDualSetpoint.get if thermostat.coolingSetpointTemperatureSchedule.is_initialized clg_sch = thermostat.coolingSetpointTemperatureSchedule.get schedule_values = nil if clg_sch.to_ScheduleRuleset.is_initialized schedule_values = OpenstudioStandards::Schedules.schedule_ruleset_get_min_max(clg_sch.to_ScheduleRuleset.get) elsif clg_sch.to_ScheduleConstant.is_initialized schedule_values = OpenstudioStandards::Schedules.schedule_constant_get_min_max(clg_sch.to_ScheduleConstant.get) end unless schedule_values.nil? model_clg_min = schedule_values['min'] end end # flag if there is setpoint schedule can't be inspected (isn't ruleset) if model_clg_min.nil? check_elems << OpenStudio::Attribute.new('flag', "Can't inspect thermostat schedules for #{thermal_zone.name}") else # get supply air temps from thermal zone sizing sizing_zone = thermal_zone.sizingZone clg_supply_air_temp = sizing_zone.zoneCoolingDesignSupplyAirTemperature # convert model values to IP model_clg_min_ip = OpenStudio.convert(model_clg_min, 'C', 'F').get clg_supply_air_temp_ip = OpenStudio.convert(clg_supply_air_temp, 'C', 'F').get # check supply air against zone temperature (only check against min setpoint, assume max is night setback) if model_clg_min_ip - clg_supply_air_temp_ip > target_clg_delta + max_delta check_elems << OpenStudio::Attribute.new('flag', "For #{thermal_zone.name} the delta temp between the cooling supply air temp of #{clg_supply_air_temp_ip.round(2)} (F) and the minimum thermostat cooling temp of #{model_clg_min_ip.round(2)} (F) is more than #{max_delta} (F) larger than the expected delta of #{target_clg_delta} (F)") elsif model_clg_min_ip - clg_supply_air_temp_ip < target_clg_delta - max_delta check_elems << OpenStudio::Attribute.new('flag', "For #{thermal_zone.name} the delta temp between the cooling supply air temp of #{clg_supply_air_temp_ip.round(2)} (F) and the minimum thermostat cooling temp of #{model_clg_min_ip.round(2)} (F) is more than #{max_delta} (F) smaller than the expected delta of #{target_clg_delta} (F)") end end end rescue StandardError => e # brief description of ruby error check_elems << OpenStudio::Attribute.new('flag', "Error prevented QAQC check from running (#{e}).") # backtrace of ruby error for diagnostic use if @error_backtrace then check_elems << OpenStudio::Attribute.new('flag', e.backtrace.join("\n").to_s) end end # add check_elms to new attribute check_elem = OpenStudio::Attribute.new('check', check_elems) return check_elem end
Check unmet hours
@param category [String] category to bin this check into @param target_standard [String] standard template, e.g. ‘90.1-2013’ @param max_unmet_hrs [Double] threshold for unmet hours reporting @param expect_clg_unmet_hrs [Bool] boolean on whether to expect unmet cooling hours for a model without a cooling system @param expect_htg_unmet_hrs [Bool] boolean on whether to expect unmet heating hours for a model without a heating system @param name_only [Boolean] If true, only return the name of this check @return [OpenStudio::Attribute] OpenStudio Attribute object containing check results
# File lib/openstudio-standards/qaqc/zone_conditions.rb, line 212 def self.check_unmet_hours(category, target_standard, max_unmet_hrs: 550.0, expect_clg_unmet_hrs: false, expect_htg_unmet_hrs: false, name_only: false) # summary of the check check_elems = OpenStudio::AttributeVector.new check_elems << OpenStudio::Attribute.new('name', 'Unmet Hours') check_elems << OpenStudio::Attribute.new('category', category) check_elems << OpenStudio::Attribute.new('description', 'Check model unmet hours.') # stop here if only name is requested this is used to populate display name for arguments if name_only == true results = [] check_elems.each do |elem| results << elem.valueAsString end return results end std = Standard.build(target_standard) begin unmet_heating_hrs = OpenstudioStandards::SqlFile.model_get_annual_occupied_unmet_heating_hours(@model) unmet_cooling_hrs = OpenstudioStandards::SqlFile.model_get_annual_occupied_unmet_cooling_hours(@model) unmet_hrs = OpenstudioStandards::SqlFile.model_get_annual_occupied_unmet_hours(@model) if unmet_hrs if unmet_hrs > max_unmet_hrs if expect_clg_unmet_hrs && expect_htg_unmet_hrs check_elems << OpenStudio::Attribute.new('flag', "Warning: Unmet heating and cooling hours expected. There were #{unmet_heating_hrs.round(1)} unmet occupied heating hours and #{unmet_cooling_hrs.round(1)} unmet occupied cooling hours (total: #{unmet_hrs.round(1)}).") elsif expect_clg_unmet_hrs && !expect_htg_unmet_hrs && unmet_heating_hrs >= max_unmet_hrs check_elems << OpenStudio::Attribute.new('flag', "Major Error: Unmet cooling hours expected, but unmet heating hours exceeds limit of #{max_unmet_hrs}. There were #{unmet_heating_hrs.round(1)} unmet occupied heating hours and #{unmet_cooling_hrs.round(1)} unmet occupied cooling hours (total: #{unmet_hrs.round(1)}).") elsif expect_clg_unmet_hrs && !expect_htg_unmet_hrs && unmet_heating_hrs < max_unmet_hrs check_elems << OpenStudio::Attribute.new('flag', "Warning: Unmet cooling hours expected. There were #{unmet_heating_hrs.round(1)} unmet occupied heating hours and #{unmet_cooling_hrs.round(1)} unmet occupied cooling hours (total: #{unmet_hrs.round(1)}).") elsif expect_htg_unmet_hrs && !expect_clg_unmet_hrs && unmet_cooling_hrs >= max_unmet_hrs check_elems << OpenStudio::Attribute.new('flag', "Major Error: Unmet heating hours expected, but unmet cooling hours exceeds limit of #{max_unmet_hrs}. There were #{unmet_heating_hrs.round(1)} unmet occupied heating hours and #{unmet_cooling_hrs.round(1)} unmet occupied cooling hours (total: #{unmet_hrs.round(1)}).") elsif expect_htg_unmet_hrs && !expect_clg_unmet_hrs && unmet_cooling_hrs < max_unmet_hrs check_elems << OpenStudio::Attribute.new('flag', "Warning: Unmet heating hours expected. There were #{unmet_heating_hrs.round(1)} unmet occupied heating hours and #{unmet_cooling_hrs.round(1)} unmet occupied cooling hours (total: #{unmet_hrs.round(1)}).") else check_elems << OpenStudio::Attribute.new('flag', "Major Error: There were #{unmet_heating_hrs.round(1)} unmet occupied heating hours and #{unmet_cooling_hrs.round(1)} unmet occupied cooling hours (total: #{unmet_hrs.round(1)}), more than the limit of #{max_unmet_hrs}.") end end else check_elems << OpenStudio::Attribute.new('flag', 'Warning: Could not determine unmet hours; simulation may have failed.') end rescue StandardError => e # brief description of ruby error check_elems << OpenStudio::Attribute.new('flag', "Error prevented QAQC check from running (#{e}).") # backtrace of ruby error for diagnostic use if @error_backtrace then check_elems << OpenStudio::Attribute.new('flag', e.backtrace.join("\n").to_s) end end # add check_elms to new attribute check_elem = OpenStudio::Attribute.new('check', check_elems) return check_elem end
Check the weather file design days and climate zone
@param category [String] category to bin this check into @param options [Hash] Hash
with epw file as a string, with child objects, ‘summer’ and ‘winter’ for each design day (strings), and ‘climate_zone’ for the climate zone number @param name_only [Boolean] If true, only return the name of this check @return [OpenStudio::Attribute] OpenStudio Attribute object containing check results
# File lib/openstudio-standards/qaqc/weather_files.rb, line 14 def self.check_weather_files(category, options, name_only: false) # summary of the check check_elems = OpenStudio::AttributeVector.new check_elems << OpenStudio::Attribute.new('name', 'Weather Files') check_elems << OpenStudio::Attribute.new('category', category) check_elems << OpenStudio::Attribute.new('description', "Check weather file, design days, and climate zone against #{@utility_name} list of allowable options.") # stop here if only name is requested this is used to populate display name for arguments if name_only == true results = [] check_elems.each do |elem| results << elem.valueAsString end return results end begin # get weather file model_epw = nil if @model.getWeatherFile.url.is_initialized raw_epw = @model.getWeatherFile.url.get end_path_index = raw_epw.rindex('/') model_epw = raw_epw.slice!(end_path_index + 1, raw_epw.length) # everything right of last forward slash end # check design days (model must have one or more of the required summer and winter design days) # get design days names from model model_summer_dd_names = [] model_winter_dd_names = [] @model.getDesignDays.each do |design_day| if design_day.dayType == 'SummerDesignDay' model_summer_dd_names << design_day.name.to_s elsif design_day.dayType == 'WinterDesignDay' model_winter_dd_names << design_day.name.to_s else puts "unexpected day type of #{design_day.dayType} wont' be included in check" end end # find matching weather file from options, as well as design days and climate zone if options.key?(model_epw) required_summer_dd = options[model_epw]['summer'] required_winter_dd = options[model_epw]['winter'] valid_climate_zones = [options[model_epw]['climate_zone']] # check for intersection betwen model valid design days summer_intersection = (required_summer_dd & model_summer_dd_names) winter_intersection = (required_winter_dd & model_winter_dd_names) if summer_intersection.empty? && !required_summer_dd.empty? check_elems << OpenStudio::Attribute.new('flag', "Didn't find any of the expected summer design days for #{model_epw}") end if winter_intersection.empty? && !required_winter_dd.empty? check_elems << OpenStudio::Attribute.new('flag', "Didn't find any of the expected winter design days for #{model_epw}") end else check_elems << OpenStudio::Attribute.new('flag', "#{model_epw} is not a an expected weather file.") check_elems << OpenStudio::Attribute.new('flag', "Model doesn't have expected epw file, as a result can't validate design days.") valid_climate_zones = [] options.each do |lookup_epw, value| valid_climate_zones << value['climate_zone'] end end # get ashrae climate zone from model model_climate_zone = nil @model.getClimateZones.climateZones.each do |climate_zone| if climate_zone.institution == 'ASHRAE' model_climate_zone = climate_zone.value next end end if model_climate_zone == '' check_elems << OpenStudio::Attribute.new('flag', "The model's ASHRAE climate zone has not been defined. Expected climate zone was #{valid_climate_zones.uniq.join(',')}.") elsif !valid_climate_zones.include?(model_climate_zone) check_elems << OpenStudio::Attribute.new('flag', "The model's ASHRAE climate zone was #{model_climate_zone}. Expected climate zone was #{valid_climate_zones.uniq.join(',')}.") end rescue StandardError => e # brief description of ruby error check_elems << OpenStudio::Attribute.new('flag', "Error prevented QAQC check from running (#{e}).") # backtrace of ruby error for diagnostic use if @error_backtrace then check_elems << OpenStudio::Attribute.new('flag', e.backtrace.join("\n").to_s) end end # add check_elms to new attribute check_elem = OpenStudio::Attribute.new('check', check_elems) return check_elem end
Cleanup and prepare HTML measures calling this must add the following require calls: require ‘json’ require ‘erb’
@param html_in_path [String] HTML input path @param sections [Array] sections from create_sections_from_check_attributes
@param name [String] the name that a user will see @return [String] HTML output path
# File lib/openstudio-standards/qaqc/reporting.rb, line 15 def self.create_qaqc_html(html_in_path, sections, name) # read in template if File.exist?(html_in_path) html_in_path = html_in_path else html_in_path = "#{File.dirname(__FILE__)}/report.html.erb" end html_in = '' File.open(html_in_path, 'r') do |file| html_in = file.read end # configure template with variable values # instance variables for erb @sections = sections @name = name renderer = ERB.new(html_in) html_out = renderer.result(binding) # write html file html_out_path = './report.html' File.open(html_out_path, 'w') do |file| file << html_out # make sure data is written to the disk one way or the other begin file.fsync rescue StandardError file.flush end end return html_out_path end
Make HTML sections from a collection of QAQC
checks
@param check_elems [OpenStudio::AttributeVector.new] vector of check elements @return [Array] Array
of HTML sections
# File lib/openstudio-standards/qaqc/reporting.rb, line 53 def self.create_sections_from_check_attributes(check_elems) # developer notes # method below is custom version of standard OpenStudio results methods. It passes an array of sections vs. a single section. # It doesn't use the model or SQL file. It just gets data form OpenStudio attributes passed in # It doesn't have a name_only section since it doesn't populate user arguments # inspecting check attributes # make single table with checks. # make second table with flag description (with column for where it came from) # array to hold sections sections = [] # gather data for section qaqc_check_summary = {} qaqc_check_summary[:title] = 'List of Checks in Measure' qaqc_check_summary[:header] = ['Name', 'Category', 'Flags', 'Description'] qaqc_check_summary[:data] = [] qaqc_check_summary[:data_color] = [] @qaqc_check_section = {} @qaqc_check_section[:title] = 'QAQC Check Summary' @qaqc_check_section[:tables] = [qaqc_check_summary] # add sections to array sections << @qaqc_check_section # counter for flags thrown num_flags = 0 check_elems.each do |check| # gather data for section qaqc_flag_details = {} qaqc_flag_details[:title] = "List of Flags Triggered for #{check.valueAsAttributeVector.first.valueAsString}." qaqc_flag_details[:header] = ['Flag Detail'] qaqc_flag_details[:data] = [] @qaqc_flag_section = {} @qaqc_flag_section[:title] = check.valueAsAttributeVector.first.valueAsString.to_s @qaqc_flag_section[:tables] = [qaqc_flag_details] check_name = nil check_cat = nil check_desc = nil flags = [] # loop through attributes (name,category,description,then optionally one or more flag attributes) check.valueAsAttributeVector.each_with_index do |value, index| if index == 0 check_name = value.valueAsString elsif index == 1 check_cat = value.valueAsString elsif index == 2 check_desc = value.valueAsString else # should be flag flags << value.valueAsString qaqc_flag_details[:data] << [value.valueAsString] OpenStudio.logFree(OpenStudio::Warn, 'openstudio.standards.QAQC', "#{check_name} - #{value.valueAsString}") num_flags += 1 end end # add row to table for this check qaqc_check_summary[:data] << [check_name, check_cat, flags.size, check_desc] # add info message for check if no flags found (this way user still knows what ran) if check.valueAsAttributeVector.size < 4 OpenStudio.logFree(OpenStudio::Info, 'openstudio.standards.QAQC', "#{check_name} - no flags.") end # color cells based and add logging messages based on flag status if !flags.empty? qaqc_check_summary[:data_color] << ['', '', 'indianred', ''] OpenStudio.logFree(OpenStudio::Info, 'openstudio.standards.QAQC', "#{check_name.downcase.tr(' ', '_')} #{flags.size} flags") else qaqc_check_summary[:data_color] << ['', '', 'lightgreen', ''] OpenStudio.logFree(OpenStudio::Info, 'openstudio.standards.QAQC', "#{check_name.downcase.tr(' ', '_')} #{flags.size} flags") end # add table for this check if there are flags if !qaqc_flag_details[:data].empty? sections << @qaqc_flag_section end end # add total flags registerValue OpenStudio.logFree(OpenStudio::Info, 'openstudio.standards.QAQC', "total flags: #{num_flags}") return sections end
Bin the hourly part load ratios into 10% bins
@param hourly_part_load_ratios @return [Array<Integer>] Array
of 11 integers for each bin
# File lib/openstudio-standards/qaqc/hvac.rb, line 1602 def self.hourly_part_load_ratio_bins(hourly_part_load_ratios) bins = Array.new(11, 0) hourly_part_load_ratios.each do |plr| if plr <= 0 bins[0] += 1 elsif plr > 0 && plr <= 0.1 bins[1] += 1 elsif plr > 0.1 && plr <= 0.2 bins[2] += 1 elsif plr > 0.2 && plr <= 0.3 bins[3] += 1 elsif plr > 0.3 && plr <= 0.4 bins[4] += 1 elsif plr > 0.4 && plr <= 0.5 bins[5] += 1 elsif plr > 0.5 && plr <= 0.6 bins[6] += 1 elsif plr > 0.6 && plr <= 0.7 bins[7] += 1 elsif plr > 0.7 && plr <= 0.8 bins[8] += 1 elsif plr > 0.8 && plr <= 0.9 bins[9] += 1 elsif plr > 0.9 # add over-100% PLRs to final bin bins[10] += 1 end end # Convert bins from hour counts to % of operating hours. bins.each_with_index do |bin, i| bins[i] = bins[i] * 1.0 / hourly_part_load_ratios.size end return bins end
Checks part loads ratios for a piece of equipment using the part load timeseries
@param sql [OpenStudio::SqlFile] OpenStudio SqlFile
@param ann_env_pd [String] EnvPeriod, typically ‘WeatherRunPeriod’ @param time_step [String] timestep, typically ‘Hourly’ @param variable_name [String] part load ratio variable name @param equipment [OpenStudio::Model::ModelObject] OpenStudio ModelObject, usually an HVACComponent @param design_power [Double] equipment design power, typically in watts @param units [String] design_power units, typically ‘W’, default ” @param expect_low_plr [Boolean] toggle for whether to expect very low part load ratios and not report a message if found @return [String] string with error message, or nil if none
# File lib/openstudio-standards/qaqc/hvac.rb, line 1649 def self.hvac_equipment_part_load_ratio_message(sql, ann_env_pd, time_step, variable_name, equipment, design_power, units: '', expect_low_plr: false) msg = nil key_value = equipment.name.get.to_s.upcase # must be in all caps ts = sql.timeSeries(ann_env_pd, time_step, variable_name, key_value) if ts.empty? msg = "Warning: #{variable_name} Timeseries not found for #{key_value}." return msg end if design_power.zero? return msg end # Convert to array ts = ts.get.values plrs = [] for i in 0..(ts.size - 1) plrs << ts[i] / design_power.to_f end # Bin part load ratios bins = OpenstudioStandards::HVAC.hourly_part_load_ratio_bins(plrs) frac_hrs_above_90 = bins[10] frac_hrs_above_80 = frac_hrs_above_90 + bins[9] frac_hrs_above_70 = frac_hrs_above_80 + bins[8] frac_hrs_above_60 = frac_hrs_above_70 + bins[7] frac_hrs_above_50 = frac_hrs_above_60 + bins[6] frac_hrs_zero = bins[0] pretty_bins = bins.map { |x| (x * 100).round(2) } # Check top-end part load ratio bins if expect_low_plr msg = "Warning: For #{equipment.name} with design size #{design_power.round(2)} #{units} is expected to have a low part load ratio. Bins of PLR [0%,0%-10%,...]: #{pretty_bins}." elsif frac_hrs_zero == 1.0 msg = "Warning: For #{equipment.name}, all hrs are zero; equipment never runs." elsif frac_hrs_above_50 < 0.01 msg = "Major Error: For #{equipment.name} with design size #{design_power.round(2)} #{units}, #{(frac_hrs_above_50 * 100).round(2)}% of hrs are above 50% part load. This indicates significantly oversized equipment. Bins of PLR [0%,0%-10%,...]: #{pretty_bins}." elsif frac_hrs_above_60 < 0.01 msg = "Minor Error: For #{equipment.name} with design size #{design_power.round(2)} #{units}, #{(frac_hrs_above_60 * 100).round(2)}% of hrs are above 60% part load. This indicates significantly oversized equipment. Bins of PLR [0%,0%-10%,...]: #{pretty_bins}." elsif frac_hrs_above_80 < 0.01 msg = "Warning: For #{equipment.name} with design size #{design_power.round(2)} #{units}, #{(frac_hrs_above_80 * 100).round(2)}% of hrs are above 80% part load. This indicates oversized equipment. Bins of PLR [0%,0%-10%,...]: #{pretty_bins}." elsif frac_hrs_above_90 > 0.05 msg = "Warning: For #{equipment.name} with design size #{design_power.round(2)} #{units}, #{(frac_hrs_above_90 * 100).round(2)}% of hrs are above 90% part load. This indicates undersized equipment. Bins of PLR [0%,0%-10%,...]: #{pretty_bins}." elsif frac_hrs_above_90 > 0.1 msg = "Minor Error: For #{equipment.name} with design size #{design_power.round(2)} #{units}, #{(frac_hrs_above_90 * 100).round(2)}% of hrs are above 90% part load. This indicates significantly undersized equipment. Bins of PLR [0%,0%-10%,...]: #{pretty_bins}." elsif frac_hrs_above_90 > 0.2 msg = "Major Error: For #{equipment.name} with design size #{design_power.round(2)} #{units}, #{(frac_hrs_above_90 * 100).round(2)}% of hrs are above 90% part load. This indicates significantly undersized equipment. Bins of PLR [0%,0%-10%,...]: #{pretty_bins}." end return msg end
Reports out the detailed simulation results needed by EDAPT and other QAQC
programs Results are output as OpenStudio::Attributes
@param skip_weekends [Bool] if true, weekends will not be included in the peak demand window @param skip_holidays [Bool] if true, holidays will not be included in the peak demand window @param start_mo [String] the start month for the peak demand window @param start_day [Integer] the start day for the peak demand window @param start_hr [Integer] the start hour for the peak demand window, using 24-hr clock @param end_mo [String] the end month for the peak demand window @param end_day [Integer] the end day for the peak demand window @param end_hr [Integer] the end hour for the peak demand window, using 24-hr clock @param electricity_consumption_tou_periods [Array<Hash>] optional array of hashes to add time-of-use electricity consumption values to the annual consumption information. Periods may overlap, but should be listed in the order in which they must be checked, where the value will be assigned to the first encountered period it falls into. An example hash looks like this:
{ 'tou_name' => 'system_peak', 'tou_id' => 1, 'skip_weekends' => true, 'skip_holidays' => true, 'start_mo' => 'July', 'start_day' => 1, 'start_hr' => 14, 'end_mo' => 'August', 'end_day' => 31, 'end_hr' => 18 }
@return [OpenStudio::AttributeVector] a vector of results needed by EDAPT
# File lib/openstudio-standards/qaqc/create_results.rb, line 35 def self.make_qaqc_results_vector(skip_weekends = true, skip_holidays = true, start_mo = 'June', start_day = 1, start_hr = 14, end_mo = 'September', end_day = 30, end_hr = 18, electricity_consumption_tou_periods = []) # get the current version of OS being used to determine if sql query # changes are needed (for when E+ changes). os_version = OpenStudio::VersionString.new(OpenStudio.openStudioVersion) # make an attribute vector to hold results result_elems = OpenStudio::AttributeVector.new # floor_area floor_area_query = "SELECT Value FROM tabulardatawithstrings WHERE ReportName='AnnualBuildingUtilityPerformanceSummary' AND ReportForString='Entire Facility' AND TableName='Building Area' AND RowName='Net Conditioned Building Area' AND ColumnName='Area' AND Units='m2'" floor_area = @sql.execAndReturnFirstDouble(floor_area_query) if floor_area.is_initialized result_elems << OpenStudio::Attribute.new('floor_area', floor_area.get, 'm^2') else OpenStudio.logFree(OpenStudio::Warn, 'openstudio.standards.QAQC', 'Building floor area not found') return false end # inflation approach inf_appr_query = "SELECT Value FROM tabulardatawithstrings WHERE ReportName='Life-Cycle Cost Report' AND ReportForString='Entire Facility' AND TableName='Life-Cycle Cost Parameters' AND RowName='Inflation Approach' AND ColumnName='Value'" inf_appr = @sql.execAndReturnFirstString(inf_appr_query) if inf_appr.is_initialized if inf_appr.get == 'ConstantDollar' inf_appr = 'Constant Dollar' elsif inf_appr.get == 'CurrentDollar' inf_appr = 'Current Dollar' else OpenStudio.logFree(OpenStudio::Error, 'openstudio.standards.QAQC', "Inflation approach: #{inf_appr.get} not recognized") return OpenStudio::Attribute.new('report', result_elems) end OpenStudio.logFree(OpenStudio::Info, 'openstudio.standards.QAQC', "Inflation approach = #{inf_appr}") else OpenStudio.logFree(OpenStudio::Error, 'openstudio.standards.QAQC', 'Could not determine inflation approach used') return OpenStudio::Attribute.new('report', result_elems) end # base year base_yr_query = "SELECT Value FROM tabulardatawithstrings WHERE ReportName='Life-Cycle Cost Report' AND ReportForString='Entire Facility' AND TableName='Life-Cycle Cost Parameters' AND RowName='Base Date' AND ColumnName='Value'" base_yr = @sql.execAndReturnFirstString(base_yr_query) if base_yr.is_initialized if base_yr.get =~ /\d\d\d\d/ base_yr = base_yr.get.match(/\d\d\d\d/)[0].to_f else OpenStudio.logFree(OpenStudio::Error, 'openstudio.standards.QAQC', "Could not determine the analysis start year from #{base_yr.get}") return OpenStudio::Attribute.new('report', result_elems) end else OpenStudio.logFree(OpenStudio::Error, 'openstudio.standards.QAQC', 'Could not determine analysis start year') return OpenStudio::Attribute.new('report', result_elems) end # analysis length length_yrs_query = "SELECT Value FROM tabulardatawithstrings WHERE ReportName='Life-Cycle Cost Report' AND ReportForString='Entire Facility' AND TableName='Life-Cycle Cost Parameters' AND RowName='Length of Study Period in Years' AND ColumnName='Value'" length_yrs = @sql.execAndReturnFirstInt(length_yrs_query) if length_yrs.is_initialized OpenStudio.logFree(OpenStudio::Error, "Analysis length = #{length_yrs.get} yrs") length_yrs = length_yrs.get else OpenStudio.logFree(OpenStudio::Error, 'openstudio.standards.QAQC', 'Could not determine analysis length') return OpenStudio::Attribute.new('report', result_elems) end # cash flows cash_flow_elems = OpenStudio::AttributeVector.new # setup a vector for each type of cash flow cap_cash_flow_elems = OpenStudio::AttributeVector.new om_cash_flow_elems = OpenStudio::AttributeVector.new energy_cash_flow_elems = OpenStudio::AttributeVector.new water_cash_flow_elems = OpenStudio::AttributeVector.new tot_cash_flow_elems = OpenStudio::AttributeVector.new # add the type to the element cap_cash_flow_elems << OpenStudio::Attribute.new('type', "#{inf_appr} Capital Costs") om_cash_flow_elems << OpenStudio::Attribute.new('type', "#{inf_appr} Operating Costs") energy_cash_flow_elems << OpenStudio::Attribute.new('type', "#{inf_appr} Energy Costs") water_cash_flow_elems << OpenStudio::Attribute.new('type', "#{inf_appr} Water Costs") tot_cash_flow_elems << OpenStudio::Attribute.new('type', "#{inf_appr} Total Costs") # record the cash flow in these hashes cap_cash_flow = {} om_cash_flow = {} energy_cash_flow = {} water_cash_flow = {} tot_cash_flow = {} # loop through each year and record the cash flow for i in 0..(length_yrs - 1) do new_yr = base_yr + i yr = nil if os_version > OpenStudio::VersionString.new('1.5.3') yr = "January #{new_yr.round}" else yr = "January #{new_yr.round}" end ann_cap_cash = 0.0 ann_om_cash = 0.0 ann_energy_cash = 0.0 ann_water_cash = 0.0 ann_tot_cash = 0.0 # capital cash flow cap_cash_query = "SELECT Value FROM tabulardatawithstrings WHERE ReportName='Life-Cycle Cost Report' AND ReportForString='Entire Facility' AND TableName='Capital Cash Flow by Category (Without Escalation)' AND RowName='#{yr}' AND ColumnName='Total'" cap_cash = @sql.execAndReturnFirstDouble(cap_cash_query) if cap_cash.is_initialized ann_cap_cash += cap_cash.get ann_tot_cash += cap_cash.get end # o&m cash flow (excluding utility costs) om_types = ['Maintenance', 'Repair', 'Operation', 'Replacement', 'MinorOverhaul', 'MajorOverhaul', 'OtherOperational'] om_types.each do |om_type| om_cash_query = "SELECT Value FROM tabulardatawithstrings WHERE ReportName='Life-Cycle Cost Report' AND ReportForString='Entire Facility' AND TableName='Operating Cash Flow by Category (Without Escalation)' AND RowName='#{yr}' AND ColumnName='#{om_type}'" om_cash = @sql.execAndReturnFirstDouble(om_cash_query) if om_cash.is_initialized ann_om_cash += om_cash.get ann_tot_cash += om_cash.get end end # energy cash flow energy_cash_query = "SELECT Value FROM tabulardatawithstrings WHERE ReportName='Life-Cycle Cost Report' AND ReportForString='Entire Facility' AND TableName='Operating Cash Flow by Category (Without Escalation)' AND RowName='#{yr}' AND ColumnName='Energy'" energy_cash = @sql.execAndReturnFirstDouble(energy_cash_query) if energy_cash.is_initialized ann_energy_cash += energy_cash.get ann_tot_cash += energy_cash.get end # water cash flow water_cash_query = "SELECT Value FROM tabulardatawithstrings WHERE ReportName='Life-Cycle Cost Report' AND ReportForString='Entire Facility' AND TableName='Operating Cash Flow by Category (Without Escalation)' AND RowName='#{yr}' AND ColumnName='Water'" water_cash = @sql.execAndReturnFirstDouble(water_cash_query) if water_cash.is_initialized ann_water_cash += water_cash.get ann_tot_cash += water_cash.get end # log the values for this year cap_cash_flow[yr] = ann_cap_cash om_cash_flow[yr] = ann_om_cash energy_cash_flow[yr] = ann_energy_cash water_cash_flow[yr] = ann_water_cash tot_cash_flow[yr] = ann_tot_cash cap_cash_flow_elems << OpenStudio::Attribute.new('year', ann_cap_cash, 'dollars') om_cash_flow_elems << OpenStudio::Attribute.new('year', ann_om_cash, 'dollars') energy_cash_flow_elems << OpenStudio::Attribute.new('year', ann_energy_cash, 'dollars') water_cash_flow_elems << OpenStudio::Attribute.new('year', ann_water_cash, 'dollars') tot_cash_flow_elems << OpenStudio::Attribute.new('year', ann_tot_cash, 'dollars') end # end cash flows cash_flow_elems << OpenStudio::Attribute.new('cash_flow', cap_cash_flow_elems) cash_flow_elems << OpenStudio::Attribute.new('cash_flow', om_cash_flow_elems) cash_flow_elems << OpenStudio::Attribute.new('cash_flow', energy_cash_flow_elems) cash_flow_elems << OpenStudio::Attribute.new('cash_flow', water_cash_flow_elems) cash_flow_elems << OpenStudio::Attribute.new('cash_flow', tot_cash_flow_elems) result_elems << OpenStudio::Attribute.new('cash_flows', cash_flow_elems) # list of all end uses in OpenStudio end_use_cat_types = [] OpenStudio::EndUseCategoryType.getValues.each do |end_use_val| end_use_cat_types << OpenStudio::EndUseCategoryType.new(end_use_val) end # list of all end use fule types in OpenStudio end_use_fuel_types = [] OpenStudio::EndUseFuelType.getValues.each do |end_use_fuel_type_val| end_use_fuel_types << OpenStudio::EndUseFuelType.new(end_use_fuel_type_val) end # list of the 12 months of the year in OpenStudio months = [] OpenStudio::MonthOfYear.getValues.each do |month_of_year_val| if (month_of_year_val >= 1) && (month_of_year_val <= 12) months << OpenStudio::MonthOfYear.new(month_of_year_val) end end # map each end use category type to the name that will be used in the xml end_use_map = { OpenStudio::EndUseCategoryType.new('Heating').value => 'heating', OpenStudio::EndUseCategoryType.new('Cooling').value => 'cooling', OpenStudio::EndUseCategoryType.new('InteriorLights').value => 'lighting_interior', OpenStudio::EndUseCategoryType.new('ExteriorLights').value => 'lighting_exterior', OpenStudio::EndUseCategoryType.new('InteriorEquipment').value => 'equipment_interior', OpenStudio::EndUseCategoryType.new('ExteriorEquipment').value => 'equipment_exterior', OpenStudio::EndUseCategoryType.new('Fans').value => 'fans', OpenStudio::EndUseCategoryType.new('Pumps').value => 'pumps', OpenStudio::EndUseCategoryType.new('HeatRejection').value => 'heat_rejection', OpenStudio::EndUseCategoryType.new('Humidifier').value => 'humidification', OpenStudio::EndUseCategoryType.new('HeatRecovery').value => 'heat_recovery', OpenStudio::EndUseCategoryType.new('WaterSystems').value => 'water_systems', OpenStudio::EndUseCategoryType.new('Refrigeration').value => 'refrigeration', OpenStudio::EndUseCategoryType.new('Generators').value => 'generators' } # map each fuel type in EndUseFuelTypes to a specific FuelTypes fuel_type_map = { OpenStudio::EndUseFuelType.new('Electricity').value => OpenStudio::FuelType.new('Electricity'), OpenStudio::EndUseFuelType.new('Gas').value => OpenStudio::FuelType.new('Gas'), OpenStudio::EndUseFuelType.new('AdditionalFuel').value => OpenStudio::FuelType.new('Diesel'), # TODO: add other fuel types OpenStudio::EndUseFuelType.new('DistrictCooling').value => OpenStudio::FuelType.new('DistrictCooling'), OpenStudio::EndUseFuelType.new('DistrictHeating').value => OpenStudio::FuelType.new('DistrictHeating'), OpenStudio::EndUseFuelType.new('Water').value => OpenStudio::FuelType.new('Water') } # map each fuel type in EndUseFuelTypes to a specific FuelTypes fuel_type_alias_map = { OpenStudio::EndUseFuelType.new('Electricity').value => 'electricity', OpenStudio::EndUseFuelType.new('Gas').value => 'gas', OpenStudio::EndUseFuelType.new('AdditionalFuel').value => 'other_energy', OpenStudio::EndUseFuelType.new('DistrictCooling').value => 'district_cooling', OpenStudio::EndUseFuelType.new('DistrictHeating').value => 'district_heating', OpenStudio::EndUseFuelType.new('Water').value => 'water' } # annual "annual" annual_elems = OpenStudio::AttributeVector.new # consumption "consumption" cons_elems = OpenStudio::AttributeVector.new # electricity electricity = @sql.electricityTotalEndUses if electricity.is_initialized cons_elems << OpenStudio::Attribute.new('electricity', electricity.get, 'GJ') else cons_elems << OpenStudio::Attribute.new('electricity', 0.0, 'GJ') end # gas gas = @sql.naturalGasTotalEndUses if gas.is_initialized cons_elems << OpenStudio::Attribute.new('gas', gas.get, 'GJ') else cons_elems << OpenStudio::Attribute.new('gas', 0.0, 'GJ') end # other_energy other_energy = @sql.otherFuelTotalEndUses if other_energy.is_initialized cons_elems << OpenStudio::Attribute.new('other_energy', other_energy.get, 'GJ') else cons_elems << OpenStudio::Attribute.new('other_energy', 0.0, 'GJ') end # district_cooling district_cooling = @sql.districtCoolingTotalEndUses if district_cooling.is_initialized cons_elems << OpenStudio::Attribute.new('district_cooling', district_cooling.get, 'GJ') else cons_elems << OpenStudio::Attribute.new('district_cooling', 0.0, 'GJ') end # district_heating district_heating = @sql.districtHeatingTotalEndUses if district_heating.is_initialized cons_elems << OpenStudio::Attribute.new('district_heating', district_heating.get, 'GJ') else cons_elems << OpenStudio::Attribute.new('district_heating', 0.0, 'GJ') end # water water = @sql.waterTotalEndUses if water.is_initialized cons_elems << OpenStudio::Attribute.new('water', water.get, 'm^3') else cons_elems << OpenStudio::Attribute.new('water', 0.0, 'm^3') end # end consumption annual_elems << OpenStudio::Attribute.new('consumption', cons_elems) # demand "demand" demand_elems = OpenStudio::AttributeVector.new # get the weather file run period (as opposed to design day run period) ann_env_pd = nil @sql.availableEnvPeriods.each do |env_pd| env_type = @sql.environmentType(env_pd) if env_type.is_initialized if env_type.get == OpenStudio::EnvironmentType.new('WeatherRunPeriod') ann_env_pd = env_pd end end end # only try to get the annual peak demand if an annual simulation was run if ann_env_pd # make some units to use joule_unit = OpenStudio.createUnit('J').get gigajoule_unit = OpenStudio.createUnit('GJ').get hrs_unit = OpenStudio.createUnit('h').get kilowatt_unit = OpenStudio.createUnit('kW').get # get the annual hours simulated hrs_sim = '(0 - no partial annual simulation)' if @sql.hoursSimulated.is_initialized hrs_sim = @sql.hoursSimulated.get if hrs_sim != 8760 OpenStudio.logFree(OpenStudio::Error, 'openstudio.standards.QAQC', "Simulation was only #{hrs_sim} hrs; EDA requires an annual simulation (8760 hrs)") return OpenStudio::Attribute.new('report', result_elems) end end # Get the electricity timeseries to determine the year used elec = @sql.timeSeries(ann_env_pd, 'Zone Timestep', 'Electricity:Facility', '') timeseries_yr = nil if elec.is_initialized timeseries_yr = elec.get.dateTimes[0].date.year else OpenStudio.logFree(OpenStudio::Error, 'openstudio.standards.QAQC', 'Peak Demand timeseries (Electricity:Facility at zone timestep) could not be found, cannot determine the informatino needed to calculate savings or incentives.') end # Setup the peak demand time window based on input arguments. # Note that holidays and weekends are not excluded because # of a bug in EnergyPlus dates. # This will only impact corner-case buildings that have # peak demand on weekends or holidays, which is unusual. OpenStudio.logFree(OpenStudio::Info, 'openstudio.standards.QAQC', "Peak Demand window is #{start_mo} #{start_day} to #{end_mo} #{end_day} from #{start_hr}:00 to #{end_hr}:00.") start_date = OpenStudio::DateTime.new(OpenStudio::Date.new(OpenStudio::MonthOfYear.new(start_mo), start_day, timeseries_yr), OpenStudio::Time.new(0, 0, 0, 0)) end_date = OpenStudio::DateTime.new(OpenStudio::Date.new(OpenStudio::MonthOfYear.new(end_mo), end_day, timeseries_yr), OpenStudio::Time.new(0, 24, 0, 0)) start_time = OpenStudio::Time.new(0, start_hr, 0, 0) end_time = OpenStudio::Time.new(0, end_hr, 0, 0) # Get the day type timeseries. day_types = nil day_type_indices = @sql.timeSeries(ann_env_pd, 'Zone Timestep', 'Site Day Type Index', 'Environment') if day_type_indices.is_initialized # Put values into array day_types = [] day_type_vals = day_type_indices.get.values for i in 0..(day_type_vals.size - 1) day_types << day_type_vals[i] end else OpenStudio.logFree(OpenStudio::Error, 'openstudio.standards.QAQC', 'Day Type timeseries (Site Day Type Index at zone timestep) could not be found, cannot accurately determine the peak demand.') end # electricity_peak_demand electricity_peak_demand = -1.0 electricity_peak_demand_time = nil # deduce the timestep based on the hours simulated and the number of datapoints in the timeseries if elec.is_initialized && day_types elec = elec.get num_int = elec.values.size int_len_hrs = OpenStudio::Quantity.new(hrs_sim / num_int, hrs_unit) # Put timeseries into array elec_vals = [] ann_elec_vals = elec.values for i in 0..(ann_elec_vals.size - 1) elec_vals << ann_elec_vals[i] end # Put values into array elec_times = [] ann_elec_times = elec.dateTimes for i in 0..(ann_elec_times.size - 1) elec_times << ann_elec_times[i] end # Loop through the time/value pairs and find the peak # excluding the times outside of the Xcel peak demand window elec_times.zip(elec_vals).each_with_index do |vs, ind| date_time = vs[0] val = vs[1] day_type = day_types[ind] time = date_time.time date = date_time.date day_of_week = date.dayOfWeek # Convert the peak demand to kW val_j_per_hr = val / int_len_hrs.value val_kw = OpenStudio.convert(val_j_per_hr, 'J/h', 'kW').get # puts("#{val_kw}kW; #{date}; #{time}; #{day_of_week.valueName}") # Skip times outside of the correct months next if date_time < start_date || date_time > end_date # Skip times before 2pm and after 6pm next if time < start_time || time > end_time # Skip weekends if asked if skip_weekends # Sunday = 1, Saturday = 7 next if day_type == 1 || day_type == 7 end # Skip holidays if asked if skip_holidays # Holiday = 8 next if day_type == 8 end # puts("VALID #{val_kw}kW; #{date}; #{time}; #{day_of_week.valueName}") # Check peak demand against this timestep # and update if this timestep is higher. if val > electricity_peak_demand electricity_peak_demand = val electricity_peak_demand_time = date_time end end elec_peak_demand_timestep_j = OpenStudio::Quantity.new(electricity_peak_demand, joule_unit) num_int = elec.values.size int_len_hrs = OpenStudio::Quantity.new(hrs_sim / num_int, hrs_unit) elec_peak_demand_hourly_j_per_hr = elec_peak_demand_timestep_j / int_len_hrs electricity_peak_demand = OpenStudio.convert(elec_peak_demand_hourly_j_per_hr, kilowatt_unit).get.value demand_elems << OpenStudio::Attribute.new('electricity_peak_demand', electricity_peak_demand, 'kW') OpenStudio.logFree(OpenStudio::Info, 'openstudio.standards.QAQC', "Peak Demand = #{electricity_peak_demand.round(2)}kW on #{electricity_peak_demand_time}") else OpenStudio.logFree(OpenStudio::Error, 'openstudio.standards.QAQC', 'Peak Demand timeseries (Electricity:Facility at zone timestep) could not be found, cannot determine the informatino needed to calculate savings or incentives.') demand_elems << OpenStudio::Attribute.new('electricity_peak_demand', 0.0, 'kW') end # Describe the TOU periods electricity_consumption_tou_periods.each do |tou_pd| OpenStudio.logFree(OpenStudio::Info, 'openstudio.standards.QAQC', "TOU period #{tou_pd['tou_id']} represents #{tou_pd['tou_name']} and covers #{tou_pd['start_mo']}-#{tou_pd['start_day']} to #{tou_pd['end_mo']}-#{tou_pd['end_day']} from #{tou_pd['start_hr']} to #{tou_pd['end_hr']}, skip weekends = #{tou_pd['skip_weekends']}, skip holidays = #{tou_pd['skip_holidays']}") end # electricity time-of-use periods elec = @sql.timeSeries(ann_env_pd, 'Zone Timestep', 'Electricity:Facility', '') if elec.is_initialized && day_types elec = elec.get # Put timeseries into array elec_vals = [] ann_elec_vals = elec.values for i in 0..(ann_elec_vals.size - 1) elec_vals << ann_elec_vals[i] end # Put values into array elec_times = [] ann_elec_times = elec.dateTimes for i in 0..(ann_elec_times.size - 1) elec_times << ann_elec_times[i] end # Loop through the time/value pairs and find the peak # excluding the times outside of the Xcel peak demand window electricity_tou_vals = Hash.new(0) elec_times.zip(elec_vals).each_with_index do |vs, ind| date_time = vs[0] joules = vs[1] day_type = day_types[ind] time = date_time.time date = date_time.date # puts("#{val_kw}kW; #{date}; #{time}; #{day_of_week.valueName}") # Determine which TOU period this hour falls into tou_period_assigned = false electricity_consumption_tou_periods.each do |tou_pd| pd_start_date = OpenStudio::DateTime.new(OpenStudio::Date.new(OpenStudio::MonthOfYear.new(tou_pd['start_mo']), tou_pd['start_day'], timeseries_yr), OpenStudio::Time.new(0, 0, 0, 0)) pd_end_date = OpenStudio::DateTime.new(OpenStudio::Date.new(OpenStudio::MonthOfYear.new(tou_pd['end_mo']), tou_pd['end_day'], timeseries_yr), OpenStudio::Time.new(0, 24, 0, 0)) pd_start_time = OpenStudio::Time.new(0, tou_pd['start_hr'], 0, 0) pd_end_time = OpenStudio::Time.new(0, tou_pd['end_hr'], 0, 0) # Skip times outside of the correct months next if date_time < pd_start_date || date_time > pd_end_date # Skip times before some time and after another time next if time < pd_start_time || time > pd_end_time # Skip weekends if asked if tou_pd['skip_weekends'] # Sunday = 1, Saturday = 7 next if day_type == 1 || day_type == 7 end # Skip holidays if asked if tou_pd['skip_holidays'] # Holiday = 8 next if day_type == 8 end # If here, this hour falls into the specified period tou_period_assigned = true electricity_tou_vals[tou_pd['tou_id']] += joules break end # Ensure that the value fell into a period unless tou_period_assigned OpenStudio.logFree(OpenStudio::Error, 'openstudio.standards.QAQC', "Did not find a TOU period covering #{time} on #{date}, kWh will not be included in any TOU period.") end end # Register values for any time-of-use period with kWh electricity_tou_vals.each do |tou_pd_id, joules_in_pd| gj_in_pd = OpenStudio.convert(joules_in_pd, 'J', 'GJ').get kwh_in_pd = OpenStudio.convert(joules_in_pd, 'J', 'kWh').get OpenStudio.logFree(OpenStudio::Info, 'openstudio.standards.QAQC', "TOU period #{tou_pd_id} annual electricity consumption = #{kwh_in_pd} kWh.") end else OpenStudio.logFree(OpenStudio::Error, 'openstudio.standards.QAQC', 'Electricity timeseries (Electricity:Facility at zone timestep) could not be found, cannot determine the information needed to calculate savings or incentives.') end # electricity_annual_avg_peak_demand val = @sql.electricityTotalEndUses if val.is_initialized ann_elec_gj = OpenStudio::Quantity.new(val.get, gigajoule_unit) ann_hrs = OpenStudio::Quantity.new(hrs_sim, hrs_unit) elec_ann_avg_peak_demand_hourly_GJ_per_hr = ann_elec_gj / ann_hrs electricity_annual_avg_peak_demand = OpenStudio.convert(elec_ann_avg_peak_demand_hourly_GJ_per_hr, kilowatt_unit).get.value demand_elems << OpenStudio::Attribute.new('electricity_annual_avg_peak_demand', electricity_annual_avg_peak_demand, 'kW') else demand_elems << OpenStudio::Attribute.new('electricity_annual_avg_peak_demand', 0.0, 'kW') end # district_cooling_peak_demand district_cooling_peak_demand = -1.0 ann_dist_clg_peak_demand_time = nil dist_clg = @sql.timeSeries(ann_env_pd, 'Zone Timestep', 'DistrictCooling:Facility', '') # deduce the timestep based on the hours simulated and the number of datapoints in the timeseries if dist_clg.is_initialized && day_types dist_clg = dist_clg.get num_int = dist_clg.values.size int_len_hrs = OpenStudio::Quantity.new(hrs_sim / num_int, hrs_unit) # Put timeseries into array dist_clg_vals = [] ann_dist_clg_vals = dist_clg.values for i in 0..(ann_dist_clg_vals.size - 1) dist_clg_vals << ann_dist_clg_vals[i] end # Put values into array dist_clg_times = [] ann_dist_clg_times = dist_clg.dateTimes for i in 0..(ann_dist_clg_times.size - 1) dist_clg_times << ann_dist_clg_times[i] end # Loop through the time/value pairs and find the peak # excluding the times outside of the Xcel peak demand window dist_clg_times.zip(dist_clg_vals).each_with_index do |vs, ind| date_time = vs[0] val = vs[1] day_type = day_types[ind] time = date_time.time date = date_time.date day_of_week = date.dayOfWeek # Convert the peak demand to kW val_j_per_hr = val / int_len_hrs.value val_kw = OpenStudio.convert(val_j_per_hr, 'J/h', 'kW').get # puts("#{val_kw}kW; #{date}; #{time}; #{day_of_week.valueName}") # Skip times outside of the correct months next if date_time < start_date || date_time > end_date # Skip times before 2pm and after 6pm next if time < start_time || time > end_time # Skip weekends if asked if skip_weekends # Sunday = 1, Saturday = 7 next if day_type == 1 || day_type == 7 end # Skip holidays if asked if skip_holidays # Holiday = 8 next if day_type == 8 end # puts("VALID #{val_kw}kW; #{date}; #{time}; #{day_of_week.valueName}") # Check peak demand against this timestep # and update if this timestep is higher. if val > district_cooling_peak_demand district_cooling_peak_demand = val ann_dist_clg_peak_demand_time = date_time end end dist_clg_peak_demand_timestep_j = OpenStudio::Quantity.new(district_cooling_peak_demand, joule_unit) num_int = dist_clg.values.size int_len_hrs = OpenStudio::Quantity.new(hrs_sim / num_int, hrs_unit) dist_clg_peak_demand_hourly_j_per_hr = dist_clg_peak_demand_timestep_j / int_len_hrs district_cooling_peak_demand = OpenStudio.convert(dist_clg_peak_demand_hourly_j_per_hr, kilowatt_unit).get.value demand_elems << OpenStudio::Attribute.new('district_cooling_peak_demand', district_cooling_peak_demand, 'kW') OpenStudio.logFree(OpenStudio::Info, 'openstudio.standards.QAQC', "District Cooling Peak Demand = #{district_cooling_peak_demand.round(2)}kW on #{ann_dist_clg_peak_demand_time}") else demand_elems << OpenStudio::Attribute.new('district_cooling_peak_demand', 0.0, 'kW') end # district cooling time-of-use periods dist_clg = @sql.timeSeries(ann_env_pd, 'Zone Timestep', 'DistrictCooling:Facility', '') if dist_clg.is_initialized && day_types dist_clg = dist_clg.get # Put timeseries into array dist_clg_vals = [] ann_dist_clg_vals = dist_clg.values for i in 0..(ann_dist_clg_vals.size - 1) dist_clg_vals << ann_dist_clg_vals[i] end # Put values into array dist_clg_times = [] ann_dist_clg_times = dist_clg.dateTimes for i in 0..(ann_dist_clg_times.size - 1) dist_clg_times << ann_dist_clg_times[i] end # Loop through the time/value pairs and find the peak # excluding the times outside of the Xcel peak demand window dist_clg_tou_vals = Hash.new(0) dist_clg_times.zip(dist_clg_vals).each_with_index do |vs, ind| date_time = vs[0] joules = vs[1] day_type = day_types[ind] time = date_time.time date = date_time.date # puts("#{val_kw}kW; #{date}; #{time}; #{day_of_week.valueName}") # Determine which TOU period this hour falls into tou_period_assigned = false electricity_consumption_tou_periods.each do |tou_pd| pd_start_date = OpenStudio::DateTime.new(OpenStudio::Date.new(OpenStudio::MonthOfYear.new(tou_pd['start_mo']), tou_pd['start_day'], timeseries_yr), OpenStudio::Time.new(0, 0, 0, 0)) pd_end_date = OpenStudio::DateTime.new(OpenStudio::Date.new(OpenStudio::MonthOfYear.new(tou_pd['end_mo']), tou_pd['end_day'], timeseries_yr), OpenStudio::Time.new(0, 24, 0, 0)) pd_start_time = OpenStudio::Time.new(0, tou_pd['start_hr'], 0, 0) pd_end_time = OpenStudio::Time.new(0, tou_pd['end_hr'], 0, 0) # Skip times outside of the correct months next if date_time < pd_start_date || date_time > pd_end_date # Skip times before some time and after another time next if time < pd_start_time || time > pd_end_time # Skip weekends if asked if tou_pd['skip_weekends'] # Sunday = 1, Saturday = 7 next if day_type == 1 || day_type == 7 end # Skip holidays if asked if tou_pd['skip_holidays'] # Holiday = 8 next if day_type == 8 end # If here, this hour falls into the specified period tou_period_assigned = true dist_clg_tou_vals[tou_pd['tou_id']] += joules break end # Ensure that the value fell into a period unless tou_period_assigned OpenStudio.logFree(OpenStudio::Error, 'openstudio.standards.QAQC', "Did not find a TOU period covering #{time} on #{date}, kWh will not be included in any TOU period.") end end # Register values for any time-of-use period with kWh dist_clg_tou_vals.each do |tou_pd_id, joules_in_pd| gj_in_pd = OpenStudio.convert(joules_in_pd, 'J', 'GJ').get kwh_in_pd = OpenStudio.convert(joules_in_pd, 'J', 'kWh').get OpenStudio.logFree(OpenStudio::Info, 'openstudio.standards.QAQC', "TOU period #{tou_pd_id} annual district cooling consumption = #{kwh_in_pd} kWh.") end else # If TOU periods were specified but this model has no district cooling, report zeroes if !electricity_consumption_tou_periods.empty? # Get the TOU ids tou_ids = [] electricity_consumption_tou_periods.each do |tou_pd| tou_ids << tou_pd['tou_id'] end end end else OpenStudio.logFree(OpenStudio::Error, 'openstudio.standards.QAQC', 'Could not find an annual run period') return OpenStudio::Attribute.new('report', result_elems) end # end demand annual_elems << OpenStudio::Attribute.new('demand', demand_elems) # utility_cost utility_cost_elems = OpenStudio::AttributeVector.new annual_utility_cost_map = {} # electricity electricity = @sql.annualTotalCost(OpenStudio::FuelType.new('Electricity')) if electricity.is_initialized utility_cost_elems << OpenStudio::Attribute.new('electricity', electricity.get, 'dollars') annual_utility_cost_map[OpenStudio::EndUseFuelType.new('Electricity').valueName] = electricity.get else utility_cost_elems << OpenStudio::Attribute.new('electricity', 0.0, 'dollars') annual_utility_cost_map[OpenStudio::EndUseFuelType.new('Electricity').valueName] = 0.0 end # electricity_consumption_charge and electricity_demand_charge electric_consumption_charge = 0.0 electric_demand_charge = 0.0 electric_rate_query = "SELECT value FROM tabulardatawithstrings WHERE ReportName='LEEDsummary' AND ReportForString='Entire Facility' AND TableName='EAp2-3. Energy Type Summary' AND RowName='Electricity' AND ColumnName='Utility Rate'" electric_rate_name = @sql.execAndReturnFirstString(electric_rate_query) if electric_rate_name.is_initialized electric_rate_name = electric_rate_name.get.strip # electricity_consumption_charge electric_consumption_charge_query = "SELECT value FROM tabulardatawithstrings WHERE ReportName='Tariff Report' AND ReportForString='#{electric_rate_name}' AND TableName='Categories' AND RowName='EnergyCharges (~~$~~)' AND ColumnName='Sum'" val = @sql.execAndReturnFirstDouble(electric_consumption_charge_query) if val.is_initialized electric_consumption_charge = val.get end # electricity_demand_charge electric_demand_charge_query = "SELECT value FROM tabulardatawithstrings WHERE ReportName='Tariff Report' AND ReportForString='#{electric_rate_name}' AND TableName='Categories' AND RowName='DemandCharges (~~$~~)' AND ColumnName='Sum'" val = @sql.execAndReturnFirstDouble(electric_demand_charge_query) if val.is_initialized electric_demand_charge = val.get end end utility_cost_elems << OpenStudio::Attribute.new('electricity_consumption_charge', electric_consumption_charge, 'dollars') utility_cost_elems << OpenStudio::Attribute.new('electricity_demand_charge', electric_demand_charge, 'dollars') # gas gas = @sql.annualTotalCost(OpenStudio::FuelType.new('Gas')) if gas.is_initialized annual_utility_cost_map[OpenStudio::EndUseFuelType.new('Gas').valueName] = gas.get else annual_utility_cost_map[OpenStudio::EndUseFuelType.new('Gas').valueName] = 0.0 end # district_cooling district_cooling_charge = 0.0 district_cooling_rate_query = "SELECT value FROM tabulardatawithstrings WHERE ReportName='LEEDsummary' AND ReportForString='Entire Facility' AND TableName='EAp2-3. Energy Type Summary' AND RowName='District Cooling' AND ColumnName='Utility Rate'" district_cooling_rate_name = @sql.execAndReturnFirstString(district_cooling_rate_query) if district_cooling_rate_name.is_initialized district_cooling_rate_name = district_cooling_rate_name.get.strip # district_cooling_charge district_cooling_charge_query = "SELECT value FROM tabulardatawithstrings WHERE ReportName='Tariff Report' AND ReportForString='#{district_cooling_rate_name}' AND TableName='Categories' AND RowName='Basis (~~$~~)' AND ColumnName='Sum'" val = @sql.execAndReturnFirstDouble(district_cooling_charge_query) if val.is_initialized district_cooling_charge = val.get end end annual_utility_cost_map[OpenStudio::EndUseFuelType.new('DistrictCooling').valueName] = district_cooling_charge # district_heating district_heating_charge = 0.0 district_heating_rate_query = "SELECT value FROM tabulardatawithstrings WHERE ReportName='LEEDsummary' AND ReportForString='Entire Facility' AND TableName='EAp2-3. Energy Type Summary' AND RowName='District Heating' AND ColumnName='Utility Rate'" district_heating_rate_name = @sql.execAndReturnFirstString(district_heating_rate_query) if district_heating_rate_name.is_initialized district_heating_rate_name = district_heating_rate_name.get.strip # district_heating_charge district_heating_charge_query = "SELECT value FROM tabulardatawithstrings WHERE ReportName='Tariff Report' AND ReportForString='#{district_heating_rate_name}' AND TableName='Categories' AND RowName='Basis (~~$~~)' AND ColumnName='Sum'" val = @sql.execAndReturnFirstDouble(district_heating_charge_query) if val.is_initialized district_heating_charge = val.get end end annual_utility_cost_map[OpenStudio::EndUseFuelType.new('DistrictHeating').valueName] = district_heating_charge # water water = @sql.annualTotalCost(OpenStudio::FuelType.new('Water')) if water.is_initialized annual_utility_cost_map[OpenStudio::EndUseFuelType.new('Water').valueName] = water.get else annual_utility_cost_map[OpenStudio::EndUseFuelType.new('Water').valueName] = 0.0 end # total total_query = "SELECT Value from tabulardatawithstrings where (reportname = 'Economics Results Summary Report') and (ReportForString = 'Entire Facility') and (TableName = 'Annual Cost') and (ColumnName ='Total') and (((RowName = 'Cost') and (Units = '~~$~~')) or (RowName = 'Cost (~~$~~)'))" total = @sql.execAndReturnFirstDouble(total_query) # other_energy # Subtract off the already accounted for fuel types from the total # to account for fuels on custom meters where the fuel type is not known. prev_tot = 0.0 annual_utility_cost_map.each do |fuel, value| prev_tot += value end if total.is_initialized other_val = total.get - prev_tot annual_utility_cost_map[OpenStudio::EndUseFuelType.new('AdditionalFuel').valueName] = other_val else annual_utility_cost_map[OpenStudio::EndUseFuelType.new('AdditionalFuel').valueName] = 0.0 end # export remaining costs in the correct order # gas utility_cost_elems << OpenStudio::Attribute.new('gas', annual_utility_cost_map[OpenStudio::EndUseFuelType.new('Gas').valueName], 'dollars') # other_energy utility_cost_elems << OpenStudio::Attribute.new('other_energy', annual_utility_cost_map[OpenStudio::EndUseFuelType.new('AdditionalFuel').valueName], 'dollars') # district_cooling utility_cost_elems << OpenStudio::Attribute.new('district_cooling', annual_utility_cost_map[OpenStudio::EndUseFuelType.new('DistrictCooling').valueName], 'dollars') # district_heating utility_cost_elems << OpenStudio::Attribute.new('district_heating', annual_utility_cost_map[OpenStudio::EndUseFuelType.new('DistrictHeating').valueName], 'dollars') # water utility_cost_elems << OpenStudio::Attribute.new('water', annual_utility_cost_map[OpenStudio::EndUseFuelType.new('Water').valueName], 'dollars') # total if total.is_initialized utility_cost_elems << OpenStudio::Attribute.new('total', total.get, 'dollars') else utility_cost_elems << OpenStudio::Attribute.new('total', 0.0, 'dollars') end # end_uses - utility costs by end use using average blended cost end_uses_elems = OpenStudio::AttributeVector.new # map to store the costs by end use cost_by_end_use = {} # fill the map with 0.0's to start end_use_cat_types.each do |end_use_cat_type| cost_by_end_use[end_use_cat_type] = 0.0 end # only attempt to get monthly data if enduses table is available if @sql.endUses.is_initialized end_uses_table = @sql.endUses.get # loop through all the fuel types end_use_fuel_types.each do |end_use_fuel_type| # get the annual total cost for this fuel type ann_cost = annual_utility_cost_map[end_use_fuel_type.valueName] # get the total annual usage for this fuel type in all end use categories # loop through all end uses, adding the annual usage value to the aggregator ann_usg = 0.0 end_use_cat_types.each do |end_use_cat_type| ann_usg += end_uses_table.getEndUse(end_use_fuel_type, end_use_cat_type) end # figure out the annual blended rate for this fuel type avg_ann_rate = 0.0 if ann_cost > 0 && ann_usg > 0 avg_ann_rate = ann_cost / ann_usg end # for each end use category, figure out the cost if using # the avg ann rate; add this cost to the map end_use_cat_types.each do |end_use_cat_type| cost_by_end_use[end_use_cat_type] += end_uses_table.getEndUse(end_use_fuel_type, end_use_cat_type) * avg_ann_rate end end # loop through the end uses and record the annual total cost based on the avg annual rate end_use_cat_types.each do |end_use_cat_type| # record the value end_uses_elems << OpenStudio::Attribute.new(end_use_map[end_use_cat_type.value], cost_by_end_use[end_use_cat_type], 'dollars') end else OpenStudio.logFree(OpenStudio::Error, 'openstudio.standards.QAQC', 'End-Use table not available in results; could not retrieve monthly costs by end use') return OpenStudio::Attribute.new('report', result_elems) end # end end_uses utility_cost_elems << OpenStudio::Attribute.new('end_uses', end_uses_elems) # end utility_costs annual_elems << OpenStudio::Attribute.new('utility_cost', utility_cost_elems) # end annual result_elems << OpenStudio::Attribute.new('annual', annual_elems) # monthly monthly_elems = OpenStudio::AttributeVector.new # consumption cons_elems = OpenStudio::AttributeVector.new # loop through all end uses end_use_cat_types.each do |end_use_cat| end_use_elems = OpenStudio::AttributeVector.new end_use_name = end_use_map[end_use_cat.value] # in each end use, loop through all fuel types end_use_fuel_types.each do |end_use_fuel_type| fuel_type_elems = OpenStudio::AttributeVector.new fuel_type_name = fuel_type_alias_map[end_use_fuel_type.value] ann_energy_cons = 0.0 # in each end use, loop through months and get monthly enedy consumption months.each_with_index do |month, i| mon_energy_cons = 0.0 val = @sql.energyConsumptionByMonth(end_use_fuel_type, end_use_cat, month) if val.is_initialized monthly_consumption_j = OpenStudio::Quantity.new(val.get, joule_unit) monthly_consumption_gj = OpenStudio.convert(monthly_consumption_j, gigajoule_unit).get.value mon_energy_cons = monthly_consumption_gj ann_energy_cons += monthly_consumption_gj end # record the monthly value if end_use_fuel_type == OpenStudio::EndUseFuelType.new('Water') fuel_type_elems << OpenStudio::Attribute.new('month', mon_energy_cons, 'm^3') else fuel_type_elems << OpenStudio::Attribute.new('month', mon_energy_cons, 'GJ') end end # record the annual total fuel_type_elems << OpenStudio::Attribute.new('year', ann_energy_cons, 'GJ') # add this fuel type end_use_elems << OpenStudio::Attribute.new(fuel_type_alias_map[end_use_fuel_type.value], fuel_type_elems) end # add this end use cons_elems << OpenStudio::Attribute.new(end_use_map[end_use_cat.value], end_use_elems) end # end consumption monthly_elems << OpenStudio::Attribute.new('consumption', cons_elems) # create a unit to use watt_unit = OpenStudio.createUnit('W').get kilowatt_unit = OpenStudio.createUnit('kW').get # demand demand_elems = OpenStudio::AttributeVector.new # loop through all end uses end_use_cat_types.each do |end_use_cat| end_use_elems = OpenStudio::AttributeVector.new end_use_name = end_use_map[end_use_cat.value] # in each end use, loop through all fuel types end_use_fuel_types.each do |end_use_fuel_type| fuel_type_elems = OpenStudio::AttributeVector.new fuel_type_name = fuel_type_alias_map[end_use_fuel_type.value] ann_peak_demand = 0.0 # in each end use, loop through months and get monthly enedy consumption months.each_with_index do |month, month_index| mon_peak_demand = 0.0 val = @sql.peakEnergyDemandByMonth(end_use_fuel_type, end_use_cat, month) if val.is_initialized mon_peak_demand_w = OpenStudio::Quantity.new(val.get, watt_unit) mon_peak_demand = OpenStudio.convert(mon_peak_demand_w, kilowatt_unit).get.value end # record the monthly value fuel_type_elems << OpenStudio::Attribute.new('month', mon_peak_demand, 'kW') # if month peak demand > ann peak demand make this new ann peak demand if mon_peak_demand > ann_peak_demand ann_peak_demand = mon_peak_demand end end # record the annual peak demand fuel_type_elems << OpenStudio::Attribute.new('year', ann_peak_demand, 'kW') # add this fuel type end_use_elems << OpenStudio::Attribute.new(fuel_type_alias_map[end_use_fuel_type.value], fuel_type_elems) end # add this end use demand_elems << OpenStudio::Attribute.new(end_use_map[end_use_cat.value], end_use_elems) end # end demand monthly_elems << OpenStudio::Attribute.new('demand', demand_elems) # end monthly result_elems << OpenStudio::Attribute.new('monthly', monthly_elems) result_elem = OpenStudio::Attribute.new('results', result_elems) return result_elem end
Check the schedule for a space load instance will return false or a single attribute
@param space_load_instance [OpenStudio::Model::SpaceLoadInstance] Openstudio SpaceLoadInstance object @param expected_hours [Double] expected number of equivalent full load hours @param std [String] openstudio-standards Standard
Class @param min_pass_pct [Double] threshold for throwing an error for percent difference @param max_pass_pct [Double] threshold for throwing an error for percent difference @return [OpenStudio::Attribute, false] OpenStudio Attribute object containing check results, or false if no error
# File lib/openstudio-standards/qaqc/internal_loads.rb, line 527 def self.space_load_instance_schedule_check(space_load_instance, expected_hours, std: nil, min_pass_pct: 0.2, max_pass_pct: 0.2) if std.nil? std = Standard.build('90.1-2013') end if space_load_instance.spaceType.is_initialized space_type = space_load_instance end # get schedule if (space_load_instance.class.to_s == 'OpenStudio::Model::People') && space_load_instance.numberofPeopleSchedule.is_initialized schedule_inst = space_load_instance.numberofPeopleSchedule.get elsif (space_load_instance.class.to_s == 'OpenStudio::Model::DesignSpecificationOutdoorAir') && space_load_instance.outdoorAirFlowRateFractionSchedule.is_initialized schedule_inst = space_load_instance.outdoorAirFlowRateFractionSchedule .get elsif space_load_instance.schedule.is_initialized schedule_inst = space_load_instance.schedule.get else return OpenStudio::Attribute.new('flag', "#{space_load_instance.name} in #{space_type.name} doesn't have a schedule assigned.") end # get annual equiv for model schedule inst_hrs = OpenstudioStandards::Schedules.schedule_get_equivalent_full_load_hours(schedule_inst) if inst_hrs.nil? return OpenStudio::Attribute.new('flag', "#{schedule_inst.name} isn't a Ruleset or Constant schedule. Can't calculate annual equivalent full load hours.") end # check instance against target if inst_hrs < expected_hours * (1.0 - min_pass_pct) return OpenStudio::Attribute.new('flag', "#{inst_hrs.round} annual equivalent full load hours for #{schedule_inst.name} in #{space_type.name} is more than #{min_pass_pct * 100} (%) below the typical value of #{expected_hours.round} hours from the DOE Prototype building.") elsif inst_hrs > expected_hours * (1.0 + max_pass_pct) return OpenStudio::Attribute.new('flag', "#{inst_hrs.round} annual equivalent full load hours for #{schedule_inst.name} in #{space_type.name} is more than #{max_pass_pct * 100} (%) above the typical value of #{expected_hours.round} hours DOE Prototype building.") end # will get to this if no flag was thrown return false end