Module: OpenstudioStandards::QAQC

Defined in:
lib/openstudio-standards/qaqc/eui.rb,
lib/openstudio-standards/qaqc/hvac.rb,
lib/openstudio-standards/qaqc/envelope.rb,
lib/openstudio-standards/qaqc/reporting.rb,
lib/openstudio-standards/qaqc/schedules.rb,
lib/openstudio-standards/qaqc/calibration.rb,
lib/openstudio-standards/qaqc/weather_files.rb,
lib/openstudio-standards/qaqc/create_results.rb,
lib/openstudio-standards/qaqc/internal_loads.rb,
lib/openstudio-standards/qaqc/zone_conditions.rb,
lib/openstudio-standards/qaqc/service_water_heating.rb

Energy Use Intensity (EUI) collapse

HVAC collapse

Envelope collapse

QAQC HTML reporting collapse

Schedules collapse

Calibration collapse

Weather File collapse

Make Results collapse

Internal Loads collapse

Zone Conditions collapse

Service Water Heating collapse

Class Method Details

.check_air_loop_fan_power(category, target_standard, max_pct_delta: 0.3, name_only: false) ⇒ OpenStudio::Attribute

Check the fan power (W/cfm) for each air loop fan in the model to identify unrealistically sized fans.

Parameters:

  • category (String)

    category to bin this check into

  • target_standard (String)

    standard template, e.g. ‘90.1-2013’

  • max_pct_delta (Double) (defaults to: 0.3)

    threshold for throwing an error for percent difference

  • name_only (Boolean) (defaults to: false)

    If true, only return the name of this check

Returns:

  • (OpenStudio::Attribute)

    OpenStudio Attribute object containing check results



253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
# 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_air_loop_temperatures(category, max_sizing_temp_delta: 2.0, max_operating_temp_delta: 5.0, name_only: false) ⇒ OpenStudio::Attribute

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.

Parameters:

  • category (String)

    category to bin this check into

  • max_sizing_temp_delta (Double) (defaults to: 2.0)

    threshold for throwing an error for design sizing temperatures

  • max_operating_temp_delta (Double) (defaults to: 5.0)

    threshold for throwing an error on operating temperatures

  • name_only (Boolean) (defaults to: false)

    If true, only return the name of this check

Returns:

  • (OpenStudio::Attribute)

    OpenStudio Attribute object containing check results



14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
# 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_calibration(category, target_standard, max_nmbe: 5.0, max_cvrmse: 15.0, name_only: false) ⇒ OpenStudio::Attribute

Check the calibration against utility bills

Parameters:

  • category (String)

    category to bin this check into

  • target_standard (String)

    standard template, e.g. ‘90.1-2013’

  • max_nmbe (Double) (defaults to: 5.0)

    maximum allowable normalized mean bias error (NMBE), default 5.0%

  • max_cvrmse (Double) (defaults to: 15.0)

    maximum allowable coefficient of variation of the root mean square error (CVRMSE), default 15.0%

  • name_only (Boolean) (defaults to: false)

    If true, only return the name of this check

Returns:

  • (OpenStudio::Attribute)

    OpenStudio Attribute object containing check results



14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
# 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_envelope_conductance(category, target_standard, min_pass_pct: 0.2, max_pass_pct: 0.2, name_only: false) ⇒ OpenStudio::Attribute

TODO:

unique tolerance ranges for conductance, reflectance, and shgc

Check the envelope conductance against a standard

Parameters:

  • category (String)

    category to bin this check into

  • target_standard (String)

    standard template, e.g. ‘90.1-2013’

  • min_pass_pct (Double) (defaults to: 0.2)

    threshold for throwing an error for percent difference

  • max_pass_pct (Double) (defaults to: 0.2)

    threshold for throwing an error for percent difference

  • name_only (Boolean) (defaults to: false)

    If true, only return the name of this check

Returns:

  • (OpenStudio::Attribute)

    OpenStudio Attribute object containing check results



15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
# 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 intended_surface_type.include? 'ExteriorWall' || 'ExteriorFloor' || 'ExteriorDoor'
          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.include? '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
          else
            # currently only used for surfaces with outdoor boundary condition
          end
          film_coefficients_r_value = std.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'
          u_factor_si = std.construction_calculated_u_factor(sub_surface_detail[:construction].to_LayeredConstruction.get.to_Construction.get)
          u_factor_ip = OpenStudio.convert(u_factor_si, source_units, target_units).get
          shgc = std.construction_calculated_solar_heat_gain_coefficient(sub_surface_detail[:construction].to_LayeredConstruction.get.to_Construction.get)

          # 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
            else
              # currently only used for surfaces with outdoor boundary condition
            end
            film_coefficients_r_value = std.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
              # currently only used for surfaces with outdoor boundary condition
            end
            film_coefficients_r_value = std.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 intended_surface_type.include? 'ExteriorWall' || 'ExteriorFloor' || 'ExteriorDoor'
              assembly_maximum_u_value = data['assembly_maximum_u_value']
              target_reflectance = 0.30
            elsif intended_surface_type.include? '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

.check_eui(category, target_standard, min_pass_pct: 0.1, max_pass_pct: 0.1, name_only: false) ⇒ OpenStudio::Attribute

Checks EUI for reasonableness

Parameters:

  • category (String)

    category to bin this check into

  • target_standard (String)

    standard template, e.g. ‘90.1-2013’

  • min_pass_pct (Double) (defaults to: 0.1)

    threshold for throwing an error for percent difference

  • max_pass_pct (Double) (defaults to: 0.1)

    threshold for throwing an error for percent difference

  • name_only (Boolean) (defaults to: false)

    If true, only return the name of this check

Returns:

  • (OpenStudio::Attribute)

    OpenStudio Attribute object containing check results



14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
# 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

.check_eui_by_end_use(category, target_standard, min_pass_pct: 0.2, max_pass_pct: 0.2, name_only: false) ⇒ OpenStudio::Attribute

Checks end use EUIs for reasonableness

Parameters:

  • category (String)

    category to bin this check into

  • target_standard (String)

    standard template, e.g. ‘90.1-2013’

  • min_pass_pct (Double) (defaults to: 0.2)

    threshold for throwing an error for percent difference

  • max_pass_pct (Double) (defaults to: 0.2)

    threshold for throwing an error for percent difference

  • name_only (Boolean) (defaults to: false)

    If true, only return the name of this check

Returns:

  • (OpenStudio::Attribute)

    OpenStudio Attribute object containing check results



104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
# 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_hvac_capacity(category, target_standard, name_only: false) ⇒ OpenStudio::Attribute

Check mechanical equipment capacity against typical sizing

Parameters:

  • category (String)

    category to bin this check into

  • target_standard (String)

    standard template, e.g. ‘90.1-2013’

  • name_only (Boolean) (defaults to: false)

    If true, only return the name of this check

Returns:

  • (OpenStudio::Attribute)

    OpenStudio Attribute object containing check results



396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
# 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_hvac_efficiency(category, target_standard, min_pass_pct: 0.3, max_pass_pct: 0.3, name_only: false) ⇒ OpenStudio::Attribute

Check the mechanical system efficiencies against a standard

Parameters:

  • category (String)

    category to bin this check into

  • target_standard (String)

    standard template, e.g. ‘90.1-2013’

  • min_pass_pct (Double) (defaults to: 0.3)

    threshold for throwing an error for percent difference

  • max_pass_pct (Double) (defaults to: 0.3)

    threshold for throwing an error for percent difference

  • name_only (Boolean) (defaults to: false)

    If true, only return the name of this check

Returns:

  • (OpenStudio::Attribute)

    OpenStudio Attribute object containing check results



581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
# 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_hvac_equipment_part_load_ratios(category, name_only: false) ⇒ OpenStudio::Attribute

Check primary heating and cooling equipment part load ratios to find equipment that is significantly oversized or undersized.

Parameters:

  • category (String)

    category to bin this check into

  • name_only (Boolean) (defaults to: false)

    If true, only return the name of this check

Returns:

  • (OpenStudio::Attribute)

    OpenStudio Attribute object containing check results



1706
1707
1708
1709
1710
1711
1712
1713
1714
1715
1716
1717
1718
1719
1720
1721
1722
1723
1724
1725
1726
1727
1728
1729
1730
1731
1732
1733
1734
1735
1736
1737
1738
1739
1740
1741
1742
1743
1744
1745
1746
1747
1748
1749
1750
1751
1752
1753
1754
1755
1756
1757
1758
1759
1760
1761
1762
1763
1764
1765
1766
1767
1768
1769
1770
1771
1772
1773
1774
1775
1776
1777
1778
1779
1780
1781
1782
1783
1784
1785
1786
1787
1788
1789
1790
1791
1792
1793
1794
1795
1796
1797
1798
1799
1800
1801
1802
1803
1804
1805
1806
1807
1808
1809
1810
1811
1812
1813
1814
1815
1816
1817
1818
1819
1820
1821
1822
1823
1824
1825
1826
1827
1828
1829
1830
1831
1832
1833
1834
1835
1836
1837
1838
1839
1840
1841
1842
1843
1844
1845
1846
1847
1848
1849
1850
1851
1852
1853
1854
1855
1856
1857
1858
1859
1860
1861
1862
1863
1864
1865
1866
1867
1868
1869
1870
1871
1872
1873
1874
1875
1876
1877
1878
1879
1880
1881
1882
1883
1884
1885
1886
1887
1888
1889
1890
1891
1892
1893
1894
1895
1896
1897
1898
1899
1900
1901
1902
1903
1904
1905
1906
1907
1908
1909
1910
1911
1912
1913
1914
1915
1916
1917
1918
1919
1920
1921
1922
1923
1924
1925
1926
1927
1928
1929
1930
1931
1932
1933
1934
1935
1936
1937
1938
1939
1940
1941
# 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_hvac_part_load_efficiency(category, target_standard, min_pass_pct: 0.3, max_pass_pct: 0.3, name_only: false) ⇒ OpenStudio::Attribute

Check the mechanical system part load efficiencies against a standard

Parameters:

  • category (String)

    category to bin this check into

  • target_standard (String)

    standard template, e.g. ‘90.1-2013’

  • min_pass_pct (Double) (defaults to: 0.3)

    threshold for throwing an error for percent difference

  • max_pass_pct (Double) (defaults to: 0.3)

    threshold for throwing an error for percent difference

  • name_only (Boolean) (defaults to: false)

    If true, only return the name of this check

Returns:

  • (OpenStudio::Attribute)

    OpenStudio Attribute object containing check results



807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
# 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

.check_hvac_system_type(category, target_standard, name_only: false) ⇒ OpenStudio::Attribute

checks the HVAC system type against 90.1 baseline system type

Parameters:

  • category (String)

    category to bin this check into

  • target_standard (String)

    standard template, e.g. ‘90.1-2013’

  • name_only (Boolean) (defaults to: false)

    If true, only return the name of this check

Returns:

  • (OpenStudio::Attribute)

    OpenStudio Attribute object containing check results



323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
# 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 std.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_internal_loads(category, target_standard, min_pass_pct: 0.2, max_pass_pct: 0.2, name_only: false) ⇒ OpenStudio::Attribute

Check the internal loads against a standard

Parameters:

  • category (String)

    category to bin this check into

  • target_standard (String)

    standard template, e.g. ‘90.1-2013’

  • min_pass_pct (Double) (defaults to: 0.2)

    threshold for throwing an error for percent difference

  • max_pass_pct (Double) (defaults to: 0.2)

    threshold for throwing an error for percent difference

  • name_only (Boolean) (defaults to: false)

    If true, only return the name of this check

Returns:

  • (OpenStudio::Attribute)

    OpenStudio Attribute object containing check results



14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
# 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 std.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 std.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_internal_loads_schedules(category, target_standard, min_pass_pct: 0.2, max_pass_pct: 0.2, name_only: false) ⇒ OpenStudio::Attribute

Check the internal load schedules against template prototypes

Parameters:

  • category (String)

    category to bin this check into

  • target_standard (String)

    standard template, e.g. ‘90.1-2013’

  • min_pass_pct (Double) (defaults to: 0.2)

    threshold for throwing an error for percent difference

  • max_pass_pct (Double) (defaults to: 0.2)

    threshold for throwing an error for percent difference

  • name_only (Boolean) (defaults to: false)

    If true, only return the name of this check

Returns:

  • (OpenStudio::Attribute)

    OpenStudio Attribute object containing check results



347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
# 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 std.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 = std.schedule_ruleset_annual_equivalent_full_load_hrs(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 = std.schedule_ruleset_annual_equivalent_full_load_hrs(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 = std.schedule_ruleset_annual_equivalent_full_load_hrs(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 = std.schedule_ruleset_annual_equivalent_full_load_hrs(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 std.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

.check_la_weather_files(category, zip_code, name_only: false) ⇒ OpenStudio::Attribute

checks the weather files matches the appropriate weather file for the Los Angeles zip code

Parameters:

  • category (String)

    category to bin this check into

  • zip_code (String)

    Los Angeles zip code

  • name_only (Boolean) (defaults to: false)

    If true, only return the name of this check

Returns:

  • (OpenStudio::Attribute)

    OpenStudio Attribute object containing check results



112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
# File 'lib/openstudio-standards/qaqc/weather_files.rb', line 112

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_occupied_zones_conditioned(category, target_standard, name_only: false) ⇒ OpenStudio::Attribute

Check that all zones with people are conditioned (have a thermostat with setpoints)

Parameters:

  • category (String)

    category to bin this check into

  • target_standard (String)

    standard template, e.g. ‘90.1-2013’

  • name_only (Boolean) (defaults to: false)

    If true, only return the name of this check

Returns:

  • (OpenStudio::Attribute)

    OpenStudio Attribute object containing check results



165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
# File 'lib/openstudio-standards/qaqc/zone_conditions.rb', line 165

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 std.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_plant_loop_capacity(category, target_standard, max_pct_delta: 0.3, name_only: false) ⇒ OpenStudio::Attribute

Check primary plant loop heating and cooling equipment capacity against coil loads to find equipment that is significantly oversized or undersized.

Parameters:

  • category (String)

    category to bin this check into

  • target_standard (String)

    standard template, e.g. ‘90.1-2013’

  • max_pct_delta (Double) (defaults to: 0.3)

    threshold for throwing an error for percent difference

  • name_only (Boolean) (defaults to: false)

    If true, only return the name of this check

Returns:

  • (OpenStudio::Attribute)

    OpenStudio Attribute object containing check results



1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
1165
1166
1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178
1179
1180
1181
1182
1183
1184
1185
1186
1187
1188
1189
1190
1191
1192
1193
1194
1195
1196
1197
1198
1199
1200
1201
1202
1203
1204
1205
1206
1207
1208
1209
1210
1211
1212
1213
1214
1215
1216
1217
# 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_plant_loop_temperatures(category, max_sizing_temp_delta: 2.0, max_operating_temp_delta: 5.0, name_only: false) ⇒ OpenStudio::Attribute

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.

Parameters:

  • category (String)

    category to bin this check into

  • max_sizing_temp_delta (Double) (defaults to: 2.0)

    threshold for throwing an error for design sizing temperatures

  • max_operating_temp_delta (Double) (defaults to: 5.0)

    threshold for throwing an error on operating temperatures

  • name_only (Boolean) (defaults to: false)

    If true, only return the name of this check

Returns:

  • (OpenStudio::Attribute)

    OpenStudio Attribute object containing check results



1227
1228
1229
1230
1231
1232
1233
1234
1235
1236
1237
1238
1239
1240
1241
1242
1243
1244
1245
1246
1247
1248
1249
1250
1251
1252
1253
1254
1255
1256
1257
1258
1259
1260
1261
1262
1263
1264
1265
1266
1267
1268
1269
1270
1271
1272
1273
1274
1275
1276
1277
1278
1279
1280
1281
1282
1283
1284
1285
1286
1287
1288
1289
1290
1291
1292
1293
1294
1295
1296
1297
1298
1299
1300
1301
1302
1303
1304
1305
1306
1307
1308
1309
1310
1311
1312
1313
1314
1315
1316
1317
1318
1319
1320
1321
1322
1323
1324
1325
1326
1327
1328
1329
1330
1331
1332
1333
1334
1335
1336
1337
1338
1339
1340
1341
1342
1343
1344
1345
1346
1347
1348
1349
1350
1351
1352
1353
1354
1355
1356
1357
1358
1359
1360
1361
1362
1363
1364
1365
1366
1367
1368
1369
1370
1371
1372
1373
1374
1375
1376
1377
1378
1379
1380
1381
1382
1383
1384
1385
1386
1387
1388
1389
1390
# 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_plenum_loads(category, target_standard, name_only: false) ⇒ OpenStudio::Attribute

Check that there are no people or lights in plenums.

Parameters:

  • category (String)

    category to bin this check into

  • target_standard (String)

    standard template, e.g. ‘90.1-2013’

  • name_only (Boolean) (defaults to: false)

    If true, only return the name of this check

Returns:

  • (OpenStudio::Attribute)

    OpenStudio Attribute object containing check results



12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
# 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 std.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_pump_power(category, target_standard, max_pct_delta: 0.3, name_only: false) ⇒ OpenStudio::Attribute

Check the pumping power (W/gpm) for each pump in the model to identify unrealistically sized pumps.

Parameters:

  • category (String)

    category to bin this check into

  • target_standard (String)

    standard template, e.g. ‘90.1-2013’

  • max_pct_delta (Double) (defaults to: 0.3)

    threshold for throwing an error for percent difference

  • name_only (Boolean) (defaults to: false)

    If true, only return the name of this check

Returns:

  • (OpenStudio::Attribute)

    OpenStudio Attribute object containing check results



1399
1400
1401
1402
1403
1404
1405
1406
1407
1408
1409
1410
1411
1412
1413
1414
1415
1416
1417
1418
1419
1420
1421
1422
1423
1424
1425
1426
1427
1428
1429
1430
1431
1432
1433
1434
1435
1436
1437
1438
1439
1440
1441
1442
1443
1444
1445
1446
1447
1448
1449
1450
1451
1452
1453
1454
1455
1456
1457
1458
1459
1460
1461
1462
1463
1464
1465
1466
1467
1468
1469
1470
1471
# 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_schedule_coordination(category, target_standard, max_hrs: 2.0, name_only: false) ⇒ OpenStudio::Attribute

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.

Parameters:

  • category (String)

    category to bin this check into

  • target_standard (String)

    standard template, e.g. ‘90.1-2013’

  • max_hrs (Double) (defaults to: 2.0)

    threshold for throwing an error for schedule coordination

  • name_only (Boolean) (defaults to: false)

    If true, only return the name of this check

Returns:

  • (OpenStudio::Attribute)

    OpenStudio Attribute object containing check results



14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
# 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

.check_service_hot_water(category, target_standard, min_pass_pct: 0.25, max_pass_pct: 0.25, name_only: false) ⇒ OpenStudio::Attribute

Checks the hot water use in a model for typical values

Parameters:

  • category (String)

    category to bin this check into

  • target_standard (String)

    standard template, e.g. ‘90.1-2013’

  • min_pass_pct (Double) (defaults to: 0.25)

    threshold for throwing an error for percent difference

  • max_pass_pct (Double) (defaults to: 0.25)

    threshold for throwing an error for percent difference

  • name_only (Boolean) (defaults to: false)

    If true, only return the name of this check

Returns:

  • (OpenStudio::Attribute)

    OpenStudio Attribute object containing check results



14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
# 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
        if schedule_inst.to_ScheduleRuleset.is_initialized
          annual_equiv_flow_rate = std.schedule_ruleset_annual_equivalent_full_load_hrs(schedule_inst.to_ScheduleRuleset.get)
        elsif schedule_inst.to_ScheduleConstant.is_initialized
          annual_equiv_flow_rate = std.schedule_constant_annual_equivalent_full_load_hrs(schedule_inst.to_ScheduleConstant.get)
        else
          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

              if inst_schedule.to_ScheduleRuleset.is_initialized
                annual_equiv_flow_rate = std.schedule_ruleset_annual_equivalent_full_load_hrs(inst_schedule.to_ScheduleRuleset.get)
              elsif inst_schedule.to_ScheduleConstant.is_initialized
                annual_equiv_flow_rate = std.schedule_constant_annual_equivalent_full_load_hrs(inst_schedule.to_ScheduleConstant.get)
              else
                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_equiv_flow_rate = 0.0
              end

              inst_num_people_horus = annual_equiv_flow_rate * inst_num_people
              space_type_num_people_hours += inst_num_people_horus
            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

              if inst_schedule.to_ScheduleRuleset.is_initialized
                annual_equiv_flow_rate = std.schedule_ruleset_annual_equivalent_full_load_hrs(inst_schedule.to_ScheduleRuleset.get)
              elsif inst_schedule.to_ScheduleConstant.is_initialized
                annual_equiv_flow_rate = std.schedule_constant_annual_equivalent_full_load_hrs(inst_schedule.to_ScheduleConstant.get)
              else
                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_equiv_flow_rate = 0.0
              end

              inst_num_people_horus = annual_equiv_flow_rate * inst_num_people
              space_type_num_people_hours += inst_num_people_horus
            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_simultaneous_heating_and_cooling(category, max_pass_pct: 0.1, name_only: false) ⇒ OpenStudio::Attribute

Check for excess simulataneous heating and cooling

Parameters:

  • category (String)

    category to bin this check into

  • max_pass_pct (Double) (defaults to: 0.1)

    threshold for throwing an error for percent difference

  • name_only (Boolean) (defaults to: false)

    If true, only return the name of this check

Returns:

  • (OpenStudio::Attribute)

    OpenStudio Attribute object containing check results



1479
1480
1481
1482
1483
1484
1485
1486
1487
1488
1489
1490
1491
1492
1493
1494
1495
1496
1497
1498
1499
1500
1501
1502
1503
1504
1505
1506
1507
1508
1509
1510
1511
1512
1513
1514
1515
1516
1517
1518
1519
1520
1521
1522
1523
1524
1525
1526
1527
1528
1529
1530
1531
1532
1533
1534
1535
1536
1537
1538
1539
1540
1541
1542
1543
1544
1545
1546
1547
1548
1549
1550
1551
1552
1553
1554
1555
1556
1557
1558
1559
1560
1561
1562
1563
1564
1565
1566
1567
1568
1569
1570
1571
1572
1573
1574
1575
1576
1577
1578
1579
1580
1581
1582
1583
1584
1585
1586
1587
1588
1589
1590
1591
1592
1593
1594
1595
1596
# 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_supply_air_and_thermostat_temperature_difference(category, target_standard, max_delta: 2.0, name_only: false) ⇒ OpenStudio::Attribute

Check for excess simulataneous heating and cooling

Parameters:

  • category (String)

    category to bin this check into

  • target_standard (String)

    standard template, e.g. ‘90.1-2013’

  • max_delta (Double) (defaults to: 2.0)

    threshold for throwing an error for temperature difference

  • name_only (Boolean) (defaults to: false)

    If true, only return the name of this check

Returns:

  • (OpenStudio::Attribute)

    OpenStudio Attribute object containing check results



66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
# 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 std.thermal_zone_plenum?(thermal_zone)

      # populate thermostat ranges
      model_clg_min = nil
      if thermal_zone.thermostatSetpointDualSetpoint.is_initialized

        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 = std.schedule_ruleset_annual_min_max_value(clg_sch.to_ScheduleRuleset.get)
          elsif clg_sch.to_ScheduleConstant.is_initialized
            schedule_values = std.schedule_constant_annual_min_max_value(clg_sch.to_ScheduleConstant.get)
          end

          unless schedule_values.nil?
            model_clg_min = schedule_values['min']
          end
        end

      else
        # go to next zone if not conditioned
        next

      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(category, target_standard, max_unmet_hrs: 550.0, expect_clg_unmet_hrs: false, expect_htg_unmet_hrs: false, name_only: false) ⇒ OpenStudio::Attribute

Check unmet hours

Parameters:

  • category (String)

    category to bin this check into

  • target_standard (String)

    standard template, e.g. ‘90.1-2013’

  • max_unmet_hrs (Double) (defaults to: 550.0)

    threshold for unmet hours reporting

  • expect_clg_unmet_hrs (Bool) (defaults to: false)

    boolean on whether to expect unmet cooling hours for a model without a cooling system

  • expect_htg_unmet_hrs (Bool) (defaults to: false)

    boolean on whether to expect unmet heating hours for a model without a heating system

  • name_only (Boolean) (defaults to: false)

    If true, only return the name of this check

Returns:

  • (OpenStudio::Attribute)

    OpenStudio Attribute object containing check results



218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
# File 'lib/openstudio-standards/qaqc/zone_conditions.rb', line 218

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 = std.model_annual_occupied_unmet_heating_hours(@model)
    unmet_cooling_hrs = std.model_annual_occupied_unmet_cooling_hours(@model)
    unmet_hrs = std.model_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_weather_files(category, options, name_only: false) ⇒ OpenStudio::Attribute

Check the weather file design days and climate zone

with child objects, ‘summer’ and ‘winter’ for each design day (strings), and ‘climate_zone’ for the climate zone number

Parameters:

  • category (String)

    category to bin this check into

  • options (Hash)

    Hash with epw file as a string,

  • name_only (Boolean) (defaults to: false)

    If true, only return the name of this check

Returns:

  • (OpenStudio::Attribute)

    OpenStudio Attribute object containing check results



14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
# 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
    climateZones = @model.getClimateZones
    climateZones.climateZones.each do |climateZone|
      if climateZone.institution == 'ASHRAE'
        model_climate_zone = climateZone.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

.create_qaqc_html(html_in_path, sections, name) ⇒ String

Cleanup and prepare HTML measures calling this must add the following require calls: require ‘json’ require ‘erb’

Parameters:

  • html_in_path (String)

    HTML input path

  • sections (Array)

    sections from create_sections_from_check_attributes

  • name (String)

    the name that a user will see

Returns:

  • (String)

    HTML output path



15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
# 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

.create_sections_from_check_attributes(check_elems) ⇒ Array

Make HTML sections from a collection of QAQC checks

Parameters:

  • check_elems (OpenStudio::AttributeVector.new)

    vector of check elements

Returns:

  • (Array)

    Array of HTML sections



53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
# 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

.hourly_part_load_ratio_bins(hourly_part_load_ratios) ⇒ Array<Integer>

Bin the hourly part load ratios into 10% bins

Parameters:

  • hourly_part_load_ratios

Returns:



1602
1603
1604
1605
1606
1607
1608
1609
1610
1611
1612
1613
1614
1615
1616
1617
1618
1619
1620
1621
1622
1623
1624
1625
1626
1627
1628
1629
1630
1631
1632
1633
1634
1635
1636
# 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

.hvac_equipment_part_load_ratio_message(sql, ann_env_pd, time_step, variable_name, equipment, design_power, units: '', expect_low_plr: false) ⇒ String

Checks part loads ratios for a piece of equipment using the part load timeseries

Parameters:

  • sql (OpenStudio::SqlFile)

    OpenStudio SqlFile

  • ann_env_pd (String)

    EnvPeriod, typically ‘WeatherRunPeriod’

  • time_step (String)

    timestep, typically ‘Hourly’

  • variable_name (String)

    part load ratio variable name

  • equipment (OpenStudio::Model::ModelObject)

    OpenStudio ModelObject, usually an HVACComponent

  • design_power (Double)

    equipment design power, typically in watts

  • units (String) (defaults to: '')

    design_power units, typically ‘W’, default ”

  • expect_low_plr (Boolean) (defaults to: false)

    toggle for whether to expect very low part load ratios and not report a message if found

Returns:

  • (String)

    string with error message, or nil if none



1649
1650
1651
1652
1653
1654
1655
1656
1657
1658
1659
1660
1661
1662
1663
1664
1665
1666
1667
1668
1669
1670
1671
1672
1673
1674
1675
1676
1677
1678
1679
1680
1681
1682
1683
1684
1685
1686
1687
1688
1689
1690
1691
1692
1693
1694
1695
1696
1697
1698
1699
# 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

.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 = []) ⇒ OpenStudio::AttributeVector

Reports out the detailed simulation results needed by EDAPT and other QAQC programs Results are output as OpenStudio::Attributes

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
}

Parameters:

  • skip_weekends (Bool) (defaults to: true)

    if true, weekends will not be included in the peak demand window

  • skip_holidays (Bool) (defaults to: true)

    if true, holidays will not be included in the peak demand window

  • start_mo (String) (defaults to: 'June')

    the start month for the peak demand window

  • start_day (Integer) (defaults to: 1)

    the start day for the peak demand window

  • start_hr (Integer) (defaults to: 14)

    the start hour for the peak demand window, using 24-hr clock

  • end_mo (String) (defaults to: 'September')

    the end month for the peak demand window

  • end_day (Integer) (defaults to: 30)

    the end day for the peak demand window

  • end_hr (Integer) (defaults to: 18)

    the end hour for the peak demand window, using 24-hr clock

  • electricity_consumption_tou_periods (Array<Hash>) (defaults to: [])

    optional array of hashes to add

Returns:

  • (OpenStudio::AttributeVector)

    a vector of results needed by EDAPT



35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
# 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, val|
    prev_tot += val
  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, i|
        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

.space_load_instance_schedule_check(space_load_instance, expected_hours, std: nil, min_pass_pct: 0.2, max_pass_pct: 0.2) ⇒ OpenStudio::Attribute, false

Check the schedule for a space load instance will return false or a single attribute

Parameters:

  • space_load_instance (OpenStudio::Model::SpaceLoadInstance)

    Openstudio SpaceLoadInstance object

  • expected_hours (Double)

    expected number of equivalent full load hours

  • std (String) (defaults to: nil)

    openstudio-standards Standard Class

  • min_pass_pct (Double) (defaults to: 0.2)

    threshold for throwing an error for percent difference

  • max_pass_pct (Double) (defaults to: 0.2)

    threshold for throwing an error for percent difference

Returns:

  • (OpenStudio::Attribute, false)

    OpenStudio Attribute object containing check results, or false if no error



528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
# File 'lib/openstudio-standards/qaqc/internal_loads.rb', line 528

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
  if schedule_inst.to_ScheduleRuleset.is_initialized
    inst_hrs = std.schedule_ruleset_annual_equivalent_full_load_hrs(schedule_inst.to_ScheduleRuleset.get)
  elsif schedule_inst.to_ScheduleConstant.is_initialized
    inst_hrs = std.schedule_constant_annual_equivalent_full_load_hrs(schedule_inst.to_ScheduleConstant.get)
  else
    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