Module: OsLib_QAQC

Defined in:
lib/openstudio/extension/core/check_fan_pwr.rb,
lib/openstudio/extension/core/check_cond_zns.rb,
lib/openstudio/extension/core/check_pump_pwr.rb,
lib/openstudio/extension/core/check_plant_cap.rb,
lib/openstudio/extension/core/check_sch_coord.rb,
lib/openstudio/extension/core/check_schedules.rb,
lib/openstudio/extension/core/check_part_loads.rb,
lib/openstudio/extension/core/check_calibration.rb,
lib/openstudio/extension/core/check_placeholder.rb,
lib/openstudio/extension/core/check_plant_temps.rb,
lib/openstudio/extension/core/check_plenum_loads.rb,
lib/openstudio/extension/core/check_air_sys_temps.rb,
lib/openstudio/extension/core/check_mech_sys_type.rb,
lib/openstudio/extension/core/check_weather_files.rb,
lib/openstudio/extension/core/check_eui_by_end_use.rb,
lib/openstudio/extension/core/check_internal_loads.rb,
lib/openstudio/extension/core/check_mech_sys_capacity.rb,
lib/openstudio/extension/core/check_domestic_hot_water.rb,
lib/openstudio/extension/core/check_eui_reasonableness.rb,
lib/openstudio/extension/core/check_mech_sys_efficiency.rb,
lib/openstudio/extension/core/check_envelope_conductance.rb,
lib/openstudio/extension/core/check_mech_sys_part_load_eff.rb,
lib/openstudio/extension/core/check_simultaneous_heating_and_cooling.rb,
lib/openstudio/extension/core/check_supply_air_and_thermostat_temp_difference.rb

Overview

******************************************************************************* OpenStudio®, Copyright © Alliance for Sustainable Energy, LLC. See also openstudio.net/license *******************************************************************************

Instance Method Summary collapse

Instance Method Details

#bin_part_loads_by_ten_pcts(hrly_plrs) ⇒ Object

Bin the hourly part load ratios into 10% bins



8
9
10
11
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
# File 'lib/openstudio/extension/core/check_part_loads.rb', line 8

def bin_part_loads_by_ten_pcts(hrly_plrs)
  bins = Array.new(10, 0)
  op_hrs = 0.0
  hrly_plrs.each do |plr|
    op_hrs += 1.0 if plr > 0
    if plr <= 0.1 # add below-zero % PLRs to final bin
      bins[0] += 1
    elsif plr > 0.1 && plr <= 0.2
      bins[1] += 1
    elsif plr > 0.2 && plr <= 0.3
      bins[2] += 1
    elsif plr > 0.3 && plr <= 0.4
      bins[3] += 1
    elsif plr > 0.4 && plr <= 0.5
      bins[4] += 1
    elsif plr > 0.5 && plr <= 0.6
      bins[5] += 1
    elsif plr > 0.6 && plr <= 0.7
      bins[6] += 1
    elsif plr > 0.7 && plr <= 0.8
      bins[7] += 1
    elsif plr > 0.8 && plr <= 0.9
      bins[8] += 1
    elsif plr > 0.9 # add over-100% PLRs to final bin
      bins[9] += 1
    end
  end

  # Convert bins from hour counts to % of operating hours.
  bins.each_with_index do |bin, i|
    bins[i] = bins[i] / op_hrs
  end

  return bins
end

#check_air_sys_temps(category, target_standard, max_sizing_temp_delta = 0.1, name_only = false) ⇒ Object

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.



10
11
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
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
# File 'lib/openstudio/extension/core/check_air_sys_temps.rb', line 10

def check_air_sys_temps(category, target_standard, max_sizing_temp_delta = 0.1, 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

  std = Standard.build(target_standard)

  begin
    # Check each air loop in the model
    @model.getAirLoopHVACs.sort.each do |airloop|
      loop_name = airloop.name.to_s

      # Get the central heating and cooling SAT for sizing
      sizing_system = airloop.sizingSystem
      loop_siz_htg_f = OpenStudio.convert(sizing_system.centralHeatingDesignSupplyAirTemperature, 'C', 'F').get
      loop_siz_clg_f = OpenStudio.convert(sizing_system.centralCoolingDesignSupplyAirTemperature, 'C', 'F').get

      # Compare air loop to zone sizing temperatures
      airloop.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 |equip|
          obj_type = equip.iddObjectType.valueName.to_s
          case obj_type
          when 'OS_AirTerminal_SingleDuct_ConstantVolume_Reheat'
            term = equip.to_AirTerminalSingleDuctConstantVolumeReheat.get
            reheat_op_f = OpenStudio.convert(term.maximumReheatAirTemperature, 'C', 'F').get
            reheat_zone = true
          when 'OS_AirTerminal_SingleDuct_VAV_HeatAndCool_Reheat'
            term = equip.to_AirTerminalSingleDuctVAVHeatAndCoolReheat.get
            reheat_op_f = OpenStudio.convert(term.maximumReheatAirTemperature, 'C', 'F').get
            reheat_zone = true
          when 'OS_AirTerminal_SingleDuct_VAV_Reheat'
            term = equip.to_AirTerminalSingleDuctVAVReheat.get
            reheat_op_f = OpenStudio.convert(term.maximumReheatAirTemperature, 'C', 'F').get
            reheat_zone = true
          when 'OS_AirTerminal_SingleDuct_ParallelPIU_Reheat'
            term = equip.to_AirTerminalSingleDuctParallelPIUReheat.get
            # reheat_op_f = # Not an OpenStudio input
            reheat_zone = true
          when 'OS_AirTerminal_SingleDuct_SeriesPIU_Reheat'
            term = equip.to_AirTerminalSingleDuctSeriesPIUReheat.get
            # 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 ((loop_siz_clg_f - zone_siz_clg_f) / loop_siz_clg_f).abs > max_sizing_temp_delta
          check_elems << OpenStudio::Attribute.new('flag', "For #{zone.name}, the sizing for the air loop is done with a cooling supply air temp of #{loop_siz_clg_f.round(2)}F, but the sizing for the zone is done with a cooling supply air temp of #{zone_siz_clg_f.round(2)}F. These are farther apart than the acceptable #{(max_sizing_temp_delta * 100.0).round(2)}% difference.")
        end

        # Check heating temperatures
        if reheat_zone && reheat_op_f
          if ((reheat_op_f - zone_siz_htg_f) / reheat_op_f).abs > max_sizing_temp_delta
            check_elems << OpenStudio::Attribute.new('flag', "For #{zone.name}, the reheat air temp is set to #{reheat_op_f.round(2)}F, but the sizing for the zone is done with a heating supply air temp of #{zone_siz_htg_f.round(2)}F. These are farther apart than the acceptable #{(max_sizing_temp_delta * 100.0).round(2)}% difference.")
          end
        elsif reheat_zone && !reheat_op_f
          # Don't perform the check if it is a reheat zone but the reheat temperature
          # is not available from the model inputs
        else
          if ((loop_siz_htg_f - zone_siz_htg_f) / loop_siz_htg_f).abs > max_sizing_temp_delta
            check_elems << OpenStudio::Attribute.new('flag', "For #{zone.name}, the sizing for the air loop is done with a heating supply air temp of #{loop_siz_htg_f.round(2)}F, but the sizing for the zone is done with a heating supply air temp of #{zone_siz_htg_f.round(2)}F. These are farther apart than the acceptable #{(max_sizing_temp_delta * 100.0).round(2)}% difference.")
          end
        end
      end

      # Determine the min and max operational temperatures
      loop_op_min_f = nil
      loop_op_max_f = nil
      airloop.supplyOutletNode.setpointManagers.each do |spm|
        obj_type = spm.iddObjectType.valueName.to_s
        case obj_type
        when 'OS_SetpointManager_Scheduled'
          sch = spm.to_SetpointManagerScheduled.get.schedule
          if sch.to_ScheduleRuleset.is_initialized
            min_c = std.schedule_ruleset_annual_min_max_value(sch.to_ScheduleRuleset.get)['min']
            max_c = std.schedule_ruleset_annual_min_max_value(sch.to_ScheduleRuleset.get)['max']
          elsif sch.to_ScheduleConstant.is_initialized
            min_c = std.schedule_constant_annual_min_max_value(sch.to_ScheduleConstant.get)['min']
            max_c = std.schedule_constant_annual_min_max_value(sch.to_ScheduleConstant.get)['max']
          else
            next
          end
          loop_op_min_f = OpenStudio.convert(min_c, 'C', 'F').get
          loop_op_max_f = OpenStudio.convert(max_c, 'C', 'F').get
        when 'OS_SetpointManager_SingleZoneReheat'
          spm = spm.to_SetpointManagerSingleZoneReheat.get
          loop_op_min_f = OpenStudio.convert(spm.minimumSupplyAirTemperature, 'C', 'F').get
          loop_op_max_f = OpenStudio.convert(spm.maximumSupplyAirTemperature, 'C', 'F').get
        when 'OS_SetpointManager_Warmest'
          spm = spm.to_SetpointManagerSingleZoneReheat.get
          loop_op_min_f = OpenStudio.convert(spm.minimumSetpointTemperature, 'C', 'F').get
          loop_op_max_f = OpenStudio.convert(spm.maximumSetpointTemperature, 'C', 'F').get
        when 'OS_SetpointManager_WarmestTemperatureFlow'
          spm = spm.to_SetpointManagerSingleZoneReheat.get
          loop_op_min_f = OpenStudio.convert(spm.minimumSetpointTemperature, 'C', 'F').get
          loop_op_max_f = OpenStudio.convert(spm.maximumSetpointTemperature, 'C', 'F').get
        else
          next # Only check the commonly used setpoint managers
        end
      end

      # Compare air loop sizing temperatures to operational temperatures

      # Cooling
      if loop_op_min_f
        if ((loop_op_min_f - loop_siz_clg_f) / loop_op_min_f).abs > max_sizing_temp_delta
          check_elems << OpenStudio::Attribute.new('flag', "For #{airloop.name}, the sizing is done with a cooling supply air temp of #{loop_siz_clg_f.round(2)}F, but the setpoint manager controlling the loop operates down to #{loop_op_min_f.round(2)}F. These are farther apart than the acceptable #{(max_sizing_temp_delta * 100.0).round(2)}% difference.")
        end
      end

      # Heating
      if loop_op_max_f
        if ((loop_op_max_f - loop_siz_htg_f) / loop_op_max_f).abs > max_sizing_temp_delta
          check_elems << OpenStudio::Attribute.new('flag', "For #{airloop.name}, the sizing is done with a heating supply air temp of #{loop_siz_htg_f.round(2)}F, but the setpoint manager controlling the loop operates up to #{loop_op_max_f.round(2)}F. These are farther apart than the acceptable #{(max_sizing_temp_delta * 100.0).round(2)}% difference.")
        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
  # note: registerWarning and registerValue will be added for checks downstream using os_lib_reporting_qaqc.rb
end

#check_calibration(category, target_standard, max_nmbe, max_cvrmse, name_only = false) ⇒ Object

Check the calibration against utility bills.



8
9
10
11
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
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
# File 'lib/openstudio/extension/core/check_calibration.rb', line 8

def check_calibration(category, target_standard, max_nmbe, max_cvrmse, 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
  # note: registerWarning and registerValue will be added for checks downstream using os_lib_reporting_qaqc.rb
end

#check_cond_zns(category, target_standard, name_only = false) ⇒ Object

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



8
9
10
11
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
# File 'lib/openstudio/extension/core/check_cond_zns.rb', line 8

def check_cond_zns(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.each do |zone|
      # Only check zones that have 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 set the setpoints
      # such that the system never comes on.  This check attempts to catch that.
      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
  # note: registerWarning and registerValue will be added for checks downstream using os_lib_reporting_qaqc.rb
end

#check_domestic_hot_water(category, target_standard, min_pass, max_pass, name_only = false) ⇒ Object

checks the number of unmet hours in the model



10
11
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
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
# File 'lib/openstudio/extension/core/check_domestic_hot_water.rb', line 10

def check_domestic_hot_water(category, target_standard, min_pass, max_pass, 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

  # Versions of OpenStudio greater than 2.4.0 use a modified version of
  # openstudio-standards with different method calls.  These methods
  # require a "Standard" object instead of the standard being passed into method calls.
  # This Standard object is used throughout the QAQC check.
  if OpenStudio::VersionString.new(OpenStudio.openStudioVersion) < OpenStudio::VersionString.new('2.4.3')
    use_old_gem_code = true
  else
    use_old_gem_code = false
    std = Standard.build(target_standard)
  end

  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
          if use_old_gem_code
            annual_equiv_flow_rate = schedule_inst.to_ScheduleRuleset.get.annual_equivalent_full_load_hrs
          else
            annual_equiv_flow_rate = std.schedule_ruleset_annual_equivalent_full_load_hrs(schedule_inst.to_ScheduleRuleset.get)
          end
        elsif schedule_inst.to_ScheduleConstant.is_initialized
          if use_old_gem_code
            annual_equiv_flow_rate = schedule_inst.to_ScheduleConstant.get.annual_equivalent_full_load_hrs
          else
            annual_equiv_flow_rate = std.schedule_constant_annual_equivalent_full_load_hrs(schedule_inst.to_ScheduleConstant.get)
          end
        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.
      if use_old_gem_code
        target_consumption = @model.find_icc_iecc_2015_hot_water_demand(num_units, bedrooms_per_unit)
      else
        target_consumption = std.model_find_icc_iecc_2015_hot_water_demand(@model, num_units, bedrooms_per_unit)
      end

    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
      if use_old_gem_code
        ashrae_hot_water_demand = @model.find_ashrae_hot_water_demand
      else
        ashrae_hot_water_demand = std.model_find_ashrae_hot_water_demand(@model)
      end

      # 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
                if use_old_gem_code
                  annual_equiv_flow_rate = inst_schedule.to_ScheduleRuleset.get.annual_equivalent_full_load_hrs
                else
                  annual_equiv_flow_rate = std.schedule_ruleset_annual_equivalent_full_load_hrs(inst_schedule.to_ScheduleRuleset.get)
                end
              elsif inst_schedule.to_ScheduleConstant.is_initialized
                if use_old_gem_code
                  annual_equiv_flow_rate = inst_schedule.to_ScheduleConstant.get.annual_equivalent_full_load_hrs
                else
                  annual_equiv_flow_rate = std.schedule_constant_annual_equivalent_full_load_hrs(inst_schedule.to_ScheduleConstant.get)
                end
              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
                if use_old_gem_code
                  annual_equiv_flow_rate = inst_schedule.to_ScheduleRuleset.get.annual_equivalent_full_load_hrs
                else
                  annual_equiv_flow_rate = std.schedule_ruleset_annual_equivalent_full_load_hrs(inst_schedule.to_ScheduleRuleset.get)
                end
              elsif inst_schedule.to_ScheduleConstant.is_initialized
                if use_old_gem_code
                  annual_equiv_flow_rate = inst_schedule.to_ScheduleConstant.get.annual_equivalent_full_load_hrs
                else
                  annual_equiv_flow_rate = std.schedule_constant_annual_equivalent_full_load_hrs(inst_schedule.to_ScheduleConstant.get)
                end
              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)
      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 * 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)
      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 * 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
  # note: registerWarning and registerValue will be added for checks downstream using os_lib_reporting_qaqc.rb
end

#check_envelope_conductance(category, target_standard, min_pass, max_pass, name_only = false) ⇒ Object

checks the number of unmet hours in the model todo - do I need unique tolerance ranges for conductance, reflectance, and shgc



11
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
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
# File 'lib/openstudio/extension/core/check_envelope_conductance.rb', line 11

def check_envelope_conductance(category, target_standard, min_pass, max_pass, 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 relfectance 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 relfectance 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

  # 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

  # Versions of OpenStudio greater than 2.4.0 use a modified version of
  # openstudio-standards with different method calls.  These methods
  # require a "Standard" object instead of the standard being passed into method calls.
  # This Standard object is used throughout the QAQC check.
  if OpenStudio::VersionString.new(OpenStudio.openStudioVersion) < OpenStudio::VersionString.new('2.4.3')
    use_old_gem_code = true
  else
    use_old_gem_code = false
    std = Standard.build(target_standard)
  end

  begin
    # loop through all space types used in the model
    @model.getSpaceTypes.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] = {}
        if use_old_gem_code
          data = space_type.get_construction_properties(target_standard, intended_surface_type, standards_construction_type)
        else
          data = std.space_type_get_construction_properties(space_type, intended_surface_type, standards_construction_type)
        end
        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_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_constructions << sub_surface.name.get
            end
          end
        end
      end

      if !missing_surface_constructions.empty?
        check_elems << OpenStudio::Attribute.new('flag', "#{missing_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_constructions.size} sub surfaces are missing constructions in #{space_type.name}. Spaces and can't be checked.")
      end

      # gather targer 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 arary combinations
      surface_details.uniq.each do |surface_detail|
        if surface_detail[:construction].thermalConductance.is_initialized

          # don't use intened 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'
            if surface_type.to_s == 'Wall' then intended_surface_type = 'ExteriorWall' end
            if surface_type == 'RoofCeiling' then intended_surface_type = 'ExteriorRoof' end
            if surface_type == 'Floor' then intended_surface_type = 'ExteriorFloor' end
          else
            # currently only used for surfaces with outdoor boundary condition
          end
          if use_old_gem_code
            film_coefficients_r_value = surface_detail[:construction].to_LayeredConstruction.get.to_Construction.get.film_coefficients_r_value(intended_surface_type)
          else
            film_coefficients_r_value = std.film_coefficients_r_value(intended_surface_type, includes_int_film = true, includes_ext_film = true)
          end
          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 optional first does what happens with ext. 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)
            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 * 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)
            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 * 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)) && (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 * 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)) && (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 * 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 arary 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'

          if use_old_gem_code
            u_factor_si = sub_surface_detail[:construction].to_LayeredConstruction.get.to_Construction.get.calculated_u_factor
          else
            u_factor_si = std.construction_calculated_u_factor(sub_surface_detail[:construction].to_LayeredConstruction.get.to_Construction.get)
          end
          u_factor_ip = OpenStudio.convert(u_factor_si, source_units, target_units).get
          if use_old_gem_code
            shgc = sub_surface_detail[:construction].to_LayeredConstruction.get.to_Construction.get.calculated_solar_heat_gain_coefficient
          else
            shgc = std.construction_calculated_solar_heat_gain_coefficient(sub_surface_detail[:construction].to_LayeredConstruction.get.to_Construction.get)
          end

          # 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)
            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 * 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)
            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 * 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)
            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 * 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)
            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 * 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 intened 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
            if use_old_gem_code
              film_coefficients_r_value = sub_surface_detail[:construction].to_LayeredConstruction.get.to_Construction.get.film_coefficients_r_value(intended_surface_type)
            else
              film_coefficients_r_value = std.film_coefficients_r_value(intended_surface_type, includes_int_film = true, includes_ext_film = true)
            end

            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 optional first does what happens with ext. 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)
              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 * 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)
              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 * 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)) && (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 * 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)) && (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 * 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.each do |space|
      if !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_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_constructions << sub_surface.name.get
            end
          end
        end

        if !missing_surface_constructions.empty?
          check_elems << OpenStudio::Attribute.new('flag', "#{missing_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_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 intened 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'
              if surface_type.to_s == 'Wall'
                intended_surface_type = 'ExteriorWall'
                standards_construction_type = 'SteelFramed'
              elsif surface_type == 'RoofCeiling'
                intended_surface_type = 'ExteriorRoof'
                standards_construction_type = 'IEAD'
              else surface_type == 'Floor'
                   intended_surface_type = 'ExteriorFloor'
                   standards_construction_type = 'Mass'
              end
            else
              # currently only used for surfaces with outdoor boundary condition
            end
            if use_old_gem_code
              film_coefficients_r_value = surface_detail[:construction].to_LayeredConstruction.get.to_Construction.get.film_coefficients_r_value(intended_surface_type)
            else
              film_coefficients_r_value = std.film_coefficients_r_value(intended_surface_type, includes_int_film = true, includes_ext_film = true)
            end

            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 optional first does what happens with ext. air wall

            # calculate target_r_value_ip
            target_reflectance = nil
            if use_old_gem_code
              data = @model.get_construction_properties(target_standard, intended_surface_type, standards_construction_type)
            else
              data = std.model_get_construction_properties(@model, intended_surface_type, standards_construction_type)
            end

            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)
              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 * 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)
              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 * 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)) && (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 * 100} % below the expected value of #{(target_reflectance * 100).round} %.")
            elsif (solar_reflectance > target_reflectance * (1.0 + max_pass)) && (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 * 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
  # note: registerWarning and registerValue will be added for checks downstream using os_lib_reporting_qaqc.rb
end

#check_eui_by_end_use(category, target_standard, min_pass, max_pass, name_only = false) ⇒ Object

checks the number of unmet hours in the model



10
11
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
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
# File 'lib/openstudio/extension/core/check_eui_by_end_use.rb', line 10

def check_eui_by_end_use(category, target_standard, min_pass, max_pass, 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

  # Versions of OpenStudio greater than 2.4.0 use a modified version of
  # openstudio-standards with different method calls.  These methods
  # require a "Standard" object instead of the standard being passed into method calls.
  # This Standard object is used throughout the QAQC check.
  if OpenStudio::VersionString.new(OpenStudio.openStudioVersion) < OpenStudio::VersionString.new('2.4.3')
    use_old_gem_code = true
  else
    use_old_gem_code = false
    std = Standard.build(target_standard)
  end

  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
    if use_old_gem_code
      target_eui_by_end_use = @model.find_target_eui_by_end_use(target_standard)
    else
      std = Standard.build(target_standard)
      target_eui_by_end_use = std.model_find_target_eui_by_end_use(@model)
    end

    # 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)) && !skip
          check_elems << OpenStudio::Attribute.new('flag', "#{end_use} EUI of #{eui_ip_neat} (#{target_units}) is less than #{min_pass * 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)) && !skip
          check_elems << OpenStudio::Attribute.new('flag', "#{end_use} EUI of #{eui_ip_neat} (#{target_units}) is more than #{max_pass * 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
  # note: registerWarning and registerValue will be added for checks downstream using os_lib_reporting_qaqc.rb
end

#check_eui_reasonableness(category, target_standard, min_pass, max_pass, name_only = false) ⇒ Object

checks the number of unmet hours in the model



10
11
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
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/extension/core/check_eui_reasonableness.rb', line 10

def check_eui_reasonableness(category, target_standard, min_pass, max_pass, 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

  # Versions of OpenStudio greater than 2.4.0 use a modified version of
  # openstudio-standards with different method calls.  These methods
  # require a "Standard" object instead of the standard being passed into method calls.
  # This Standard object is used throughout the QAQC check.
  if OpenStudio::VersionString.new(OpenStudio.openStudioVersion) < OpenStudio::VersionString.new('2.4.3')
    use_old_gem_code = true
  else
    use_old_gem_code = false
    std = Standard.build(target_standard)
  end

  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
    if use_old_gem_code
      target_eui = @model.find_target_eui(target_standard)
    else
      std = Standard.build(target_standard)
      target_eui = std.model_find_target_eui(@model)
    end

    # 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)
        check_elems << OpenStudio::Attribute.new('flag', "Model EUI of #{eui_ip_neat} (#{target_units}) is less than #{min_pass * 100} % below the expected EUI of #{target_eui_ip_neat} (#{target_units}) for #{target_standard}.")
      elsif eui > target_eui * (1.0 + max_pass)
        check_elems << OpenStudio::Attribute.new('flag', "Model EUI of #{eui_ip_neat} (#{target_units}) is more than #{max_pass * 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
  # note: registerWarning and registerValue will be added for checks downstream using os_lib_reporting_qaqc.rb
end

#check_fan_pwr(category, target_standard, max_pwr_delta = 0.1, name_only = false) ⇒ Object

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



9
10
11
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
58
59
60
61
62
63
64
65
66
67
# File 'lib/openstudio/extension/core/check_fan_pwr.rb', line 9

def check_fan_pwr(category, target_standard, max_pwr_delta = 0.1, 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.each do |plant_loop|
      # Set the expected W/cfm
      expected_w_per_cfm = 1.1

      # Check the W/cfm for each fan on each air loop
      plant_loop.supplyComponents.each do |sc|
        # Get the W/cfm for the fan
        obj_type = sc.iddObjectType.valueName.to_s
        case obj_type
        when 'OS_Fan_ConstantVolume'
          actual_w_per_cfm = std.fan_rated_w_per_cfm(sc.to_FanConstantVolume.get)
        when 'OS_Fan_OnOff'
          actual_w_per_cfm = std.fan_rated_w_per_cfm(sc.to_FanOnOff.get)
        when 'OS_Fan_VariableVolume'
          actual_w_per_cfm = std.fan_rated_w_per_cfm(sc.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_pwr_delta
          check_elems << OpenStudio::Attribute.new('flag', "For #{sc.name} on #{plant_loop.name}, the actual fan power of #{actual_w_per_cfm.round(1)} W/cfm is more than #{(max_pwr_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
  # note: registerWarning and registerValue will be added for checks downstream using os_lib_reporting_qaqc.rb
end

#check_internal_loads(category, target_standard, min_pass, max_pass, name_only = false) ⇒ Object

checks the number of unmet hours in the model



10
11
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
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
# File 'lib/openstudio/extension/core/check_internal_loads.rb', line 10

def check_internal_loads(category, target_standard, min_pass, max_pass, 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

  # Versions of OpenStudio greater than 2.4.0 use a modified version of
  # openstudio-standards with different method calls.  These methods
  # require a "Standard" object instead of the standard being passed into method calls.
  # This Standard object is used throughout the QAQC check.
  if OpenStudio::VersionString.new(OpenStudio.openStudioVersion) < OpenStudio::VersionString.new('2.4.3')
    use_old_gem_code = true
  else
    use_old_gem_code = false
    std = Standard.build(target_standard)
  end

  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.
      if use_old_gem_code
        target_loads_hash = @model.find_icc_iecc_2015_internal_loads(num_units, bedrooms_per_unit)
      else
        target_loads_hash = std.model_find_icc_iecc_2015_internal_loads(@model, num_units, bedrooms_per_unit)
      end

      # 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)
        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 * 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)
        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 * 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)
        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 * 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)
        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 * 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.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)

        # load in standard info for this space type
        if use_old_gem_code
          data = space_type.get_standards_data(target_standard)
        else
          data = std.space_type_get_standards_data(space_type)
        end

        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|
            if use_old_gem_code
              if !space.plenum?
                all_spaces_plenums = false
                next
              end
            else
              if !std.space_plenum?(space)
                all_spaces_plenums = false
                next
              end
            end
          end

          if !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)
          check_elems << OpenStudio::Attribute.new('flag', "#{load_type} of #{model_ip_neat} (#{target_units}) for #{space_type.name} is more than #{min_pass * 100} % below the expected value of #{target_ip_neat} (#{target_units}) for #{display_standard}.")
        elsif model_ip > target_ip * (1.0 + max_pass)
          check_elems << OpenStudio::Attribute.new('flag', "#{load_type} of #{model_ip_neat} (#{target_units}) for #{space_type.name} is more than #{max_pass * 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)
          check_elems << OpenStudio::Attribute.new('flag', "#{load_type} of #{model_ip_neat} (#{target_units}) for #{space_type.name} is more than #{min_pass * 100} % below the expected value of #{target_ip_neat} (#{target_units}) for #{display_standard}.")
        elsif model_ip > target_ip * (1.0 + max_pass)
          check_elems << OpenStudio::Attribute.new('flag', "#{load_type} of #{model_ip_neat} (#{target_units}) for #{space_type.name} is more than #{max_pass * 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)
          check_elems << OpenStudio::Attribute.new('flag', "#{load_type} of #{model_ip_neat} (#{target_units}) for #{space_type.name} is more than #{min_pass * 100} % below the expected value of #{target_ip_neat} (#{target_units}) for #{display_standard}.")
        elsif model_ip > target_ip * (1.0 + max_pass)
          check_elems << OpenStudio::Attribute.new('flag', "#{load_type} of #{model_ip_neat} (#{target_units}) for #{space_type.name} is more than #{max_pass * 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)
          check_elems << OpenStudio::Attribute.new('flag', "#{load_type} of #{model_ip_neat} (#{target_units}) for #{space_type.name} is more than #{min_pass * 100} % below the expected value of #{target_ip_neat} (#{target_units}) for #{display_standard}.")
        elsif model_ip > target_ip * (1.0 + max_pass)
          check_elems << OpenStudio::Attribute.new('flag', "#{load_type} of #{model_ip_neat} (#{target_units}) for #{space_type.name} is more than #{max_pass * 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_other = 0.0
          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
          oa_other_total = 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)
          check_elems << OpenStudio::Attribute.new('flag', "#{load_type} of #{model_ip_neat} (#{target_units}) for #{space_type.name} is more than #{min_pass * 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)
          check_elems << OpenStudio::Attribute.new('flag', "#{load_type} of #{model_ip_neat} (#{target_units}) for #{space_type.name} is more than #{max_pass * 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)
          check_elems << OpenStudio::Attribute.new('flag', "#{load_type} of #{model_ip_neat} (#{target_units}) for #{space_type.name} is more than #{min_pass * 100} % below the expected value of #{target_ip_neat} (#{target_units}) for #{display_standard}.")
        elsif oa_total > target_oa_total * (1.0 + max_pass)
          check_elems << OpenStudio::Attribute.new('flag', "#{load_type} of #{model_ip_neat} (#{target_units}) for #{space_type.name} is more than #{max_pass * 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.each do |space|
        if use_old_gem_code
          next if space.plenum?
        else
          next if std.space_plenum?(space)
        end

        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
  # note: registerWarning and registerValue will be added for checks downstream using os_lib_reporting_qaqc.rb
end

#check_mech_sys_capacity(category, options, target_standard, name_only = false) ⇒ Object

checks the number of unmet hours in the model



10
11
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
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
# File 'lib/openstudio/extension/core/check_mech_sys_capacity.rb', line 10

def check_mech_sys_capacity(category, options, 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

  # Versions of OpenStudio greater than 2.4.0 use a modified version of
  # openstudio-standards with different method calls.  These methods
  # require a "Standard" object instead of the standard being passed into method calls.
  # This Standard object is used throughout the QAQC check.
  if OpenStudio::VersionString.new(OpenStudio.openStudioVersion) < OpenStudio::VersionString.new('2.4.3')
    use_old_gem_code = true
  else
    use_old_gem_code = false
    std = Standard.build(target_standard)
  end

  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_target = options['chiller_max_flow_rate']['target']
      chiller_max_flow_rate_fraction_min = options['chiller_max_flow_rate']['min']
      chiller_max_flow_rate_fraction_max = options['chiller_max_flow_rate']['max']
      chiller_max_flow_rate_units_ip = options['chiller_max_flow_rate']['units'] # gal/ton*min
      # string above or display only, for converstion 12000 Btu/h per ton

      # get capacity of loop (not individual chiller but entire loop)
      if use_old_gem_code
        total_cooling_capacity_w = plant_loop.total_cooling_capacity
      else
        total_cooling_capacity_w = std.plant_loop_total_cooling_capacity(plant_loop)
      end
      total_cooling_capacity_ton = OpenStudio.convert(total_cooling_capacity_w, 'W', 'Btu/h').get / 12000.0

      # get the max flow rate (through plant, not specific chiller)
      if use_old_gem_code
        maximum_loop_flow_rate = plant_loop.find_maximum_loop_flow_rate
      else
        maximum_loop_flow_rate = std.plant_loop_find_maximum_loop_flow_rate(plant_loop)
      end
      maximum_loop_flow_rate_ip = OpenStudio.convert(maximum_loop_flow_rate, 'm^3/s', 'gal/min').get

      # 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_target * (1.0 - chiller_max_flow_rate_fraction_min)
        check_elems <<  OpenStudio::Attribute.new('flag', "Flow Rate of #{model_flow_rate_per_ton_cooling_ip.round(2)} #{chiller_max_flow_rate_units_ip} for #{plant_loop.name.get} is more than #{chiller_max_flow_rate_fraction_min * 100} % below the typical value of #{chiller_max_flow_rate_target.round(2)} #{chiller_max_flow_rate_units_ip}.")
      elsif model_flow_rate_per_ton_cooling_ip > chiller_max_flow_rate_target * (1.0 + chiller_max_flow_rate_fraction_max)
        check_elems <<  OpenStudio::Attribute.new('flag', "Flow Rate of #{model_flow_rate_per_ton_cooling_ip.round(2)} #{chiller_max_flow_rate_units_ip} for #{plant_loop.name.get} is more than #{chiller_max_flow_rate_fraction_max * 100} % above the typical value of #{chiller_max_flow_rate_target.round(2)} #{chiller_max_flow_rate_units_ip}.")
      end
    end

    # loop through air loops to get max flor rate and cooling capacity.
    @model.getAirLoopHVACs.sort.each do |air_loop|
      # TODO: - check if DOAS, don't check airflow or cooling capacity if it is (why not check OA for DOAS? would it be different target)

      # gather argument options for air_loop_max_flow_rate checks
      air_loop_max_flow_rate_target = options['air_loop_max_flow_rate']['target']
      air_loop_max_flow_rate_fraction_min = options['air_loop_max_flow_rate']['min']
      air_loop_max_flow_rate_fraction_max = options['air_loop_max_flow_rate']['max']
      air_loop_max_flow_rate_units_ip = options['air_loop_max_flow_rate']['units']
      air_loop_max_flow_rate_units_si = 'm^3/m^2*s'

      # get values from model for air loop checks
      if use_old_gem_code
        floor_area_served = air_loop.floor_area_served # m^2
      else
        floor_area_served = std.air_loop_hvac_floor_area_served(air_loop) # m^2
      end

      if use_old_gem_code
        design_supply_air_flow_rate = air_loop.find_design_supply_air_flow_rate # m^3/s
      else
        design_supply_air_flow_rate = std.air_loop_hvac_find_design_supply_air_flow_rate(air_loop) # m^3/s
      end

      # 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, air_loop_max_flow_rate_units_si, air_loop_max_flow_rate_units_ip).get
      if model_normalized_flow_rate_ip < air_loop_max_flow_rate_target * (1.0 - air_loop_max_flow_rate_fraction_min)
        check_elems <<  OpenStudio::Attribute.new('flag', "Flow Rate of #{model_normalized_flow_rate_ip.round(2)} #{air_loop_max_flow_rate_units_ip} for #{air_loop.name.get} is more than #{air_loop_max_flow_rate_fraction_min * 100} % below the typical value of #{air_loop_max_flow_rate_target.round(2)} #{air_loop_max_flow_rate_units_ip}.")
      elsif model_normalized_flow_rate_ip > air_loop_max_flow_rate_target * (1.0 + air_loop_max_flow_rate_fraction_max)
        check_elems <<  OpenStudio::Attribute.new('flag', "Flow Rate of #{model_normalized_flow_rate_ip.round(2)} #{air_loop_max_flow_rate_units_ip} for #{air_loop.name.get} is more than #{air_loop_max_flow_rate_fraction_max * 100} % above the typical value of #{air_loop_max_flow_rate_target.round(2)} #{air_loop_max_flow_rate_units_ip}.")
      end
    end

    # loop through air loops to get max flor 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_target = options['air_loop_cooling_capacity']['target']
      air_loop_cooling_capacity_fraction_min = options['air_loop_cooling_capacity']['min']
      air_loop_cooling_capacity_fraction_max = options['air_loop_cooling_capacity']['max']
      air_loop_cooling_capacity_units_ip = options['air_loop_cooling_capacity']['units'] # tons/ft^2
      # string above or display only, for converstion 12000 Btu/h per ton
      air_loop_cooling_capacity_units_si = 'W/m^2'

      # get values from model for air loop checks
      if use_old_gem_code
        floor_area_served = air_loop.floor_area_served # m^2
      else
        floor_area_served = std.air_loop_hvac_floor_area_served(air_loop) # m^2
      end

      if use_old_gem_code
        capacity = air_loop.total_cooling_capacity # W
      else
        capacity = std.air_loop_hvac_total_cooling_capacity(air_loop) # W
      end

      # check cooling capacity of air loops in the model
      model_normalized_capacity_si = capacity / floor_area_served
      model_normalized_capacity_ip = OpenStudio.convert(model_normalized_capacity_si, air_loop_cooling_capacity_units_si, 'Btu/ft^2*h').get / 12000.0 # hard coded to get tons from Btu/h

      # 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
      target_tons_per_area_ip = 1.0 / air_loop_cooling_capacity_target
      inverted_units = 'ft^2/ton'

      if model_tons_per_area_ip < target_tons_per_area_ip * (1.0 - air_loop_cooling_capacity_fraction_max)
        check_elems <<  OpenStudio::Attribute.new('flag', "Cooling Capacity of #{model_tons_per_area_ip.round} #{inverted_units} for #{air_loop.name.get} is more than #{air_loop_cooling_capacity_fraction_max * 100} % below the typical value of #{target_tons_per_area_ip.round} #{inverted_units}.")
      elsif model_tons_per_area_ip > target_tons_per_area_ip * (1.0 + air_loop_cooling_capacity_fraction_min)
        check_elems <<  OpenStudio::Attribute.new('flag', "Cooling Capacity of #{model_tons_per_area_ip.round} #{inverted_units} for #{air_loop.name.get} is more than #{air_loop_cooling_capacity_fraction_min * 100} % above the typical value of #{target_tons_per_area_ip.round} #{inverted_units}.")
      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'
    target = options['zone_heating_capacity']['target']
    fraction_min = options['zone_heating_capacity']['min']
    fraction_max = options['zone_heating_capacity']['max']
    units_ip = options['zone_heating_capacity']['units']
    units_si = 'W/m^2'
    @model.getThermalZones.sort.each do |thermal_zone|
      next if thermal_zone.canBePlenum
      next if thermal_zone.exteriorSurfaceArea == 0.0
      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) # W/m^2
      model_zone_heating_capacity_ip = OpenStudio.convert(results.to_f, units_si, units_ip).get
      # check actual against target
      if model_zone_heating_capacity_ip < target * (1.0 - fraction_min)
        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 more than #{fraction_min * 100} % below the typical value of #{target.round(2)} Btu/ft^2*h.")
      elsif model_zone_heating_capacity_ip > target * (1.0 + fraction_max)
        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 more than #{fraction_max * 100} % above the typical value of #{target.round(2)} 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
  # note: registerWarning and registerValue will be added for checks downstream using os_lib_reporting_qaqc.rb
end

#check_mech_sys_efficiency(category, target_standard, min_pass, max_pass, name_only = false) ⇒ Object

checks the number of unmet hours in the model



10
11
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
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
# File 'lib/openstudio/extension/core/check_mech_sys_efficiency.rb', line 10

def check_mech_sys_efficiency(category, target_standard, min_pass, max_pass, 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')
    display_standard = "ASHRAE #{target_standard}"
    check_elems << OpenStudio::Attribute.new('description', "Check against #{display_standard} Tables 6.8.1 A-K for the following component types: #{component_type_array.join(', ')}.")
  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 against #{display_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

  # Versions of OpenStudio greater than 2.4.0 use a modified version of
  # openstudio-standards with different method calls.  These methods
  # require a "Standard" object instead of the standard being passed into method calls.
  # This Standard object is used throughout the QAQC check.
  if OpenStudio::VersionString.new(OpenStudio.openStudioVersion) < OpenStudio::VersionString.new('2.4.3')
    use_old_gem_code = true
  else
    use_old_gem_code = false
    std = Standard.build(target_standard)
  end

  begin
    # check ChillerElectricEIR objects (will also have curve check in different script)
    @model.getChillerElectricEIRs.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)

      if use_old_gem_code
        standard_minimum_full_load_efficiency = component.standard_minimum_full_load_efficiency(target_standard)
      else
        standard_minimum_full_load_efficiency = std.chiller_electric_eir_standard_minimum_full_load_efficiency(component)
      end

      # 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)
        check_elems <<  OpenStudio::Attribute.new('flag', "COP of #{reference_COP.round(2)} for #{component.name} is more than #{min_pass * 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)
        check_elems <<  OpenStudio::Attribute.new('flag', "COP  of #{reference_COP.round(2)} for #{component.name} is more than #{max_pass * 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
      if use_old_gem_code
        standard_minimum_cop = component.standard_minimum_cop(target_standard)
      else
        standard_minimum_cop = std.coil_cooling_dx_single_speed_standard_minimum_cop(component)
      end

      # 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)
        check_elems <<  OpenStudio::Attribute.new('flag', "The COP of #{rated_COP.round(2)} for #{component.name} is more than #{min_pass * 100} % below the expected value of #{standard_minimum_cop.round(2)} for #{display_standard}.")
      elsif rated_COP > standard_minimum_cop * (1.0 + max_pass)
        check_elems <<  OpenStudio::Attribute.new('flag', "The COP of  #{rated_COP.round(2)} for #{component.name} is more than #{max_pass * 100} % above the expected value of #{standard_minimum_cop.round(2)} for #{display_standard}.")
      end
    end

    # check CoilCoolingDXTwoSpeed objects (will also have curve check in different script)
    @model.getCoilCoolingDXTwoSpeeds.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
      if use_old_gem_code
        standard_minimum_cop = component.standard_minimum_cop(target_standard)
      else
        standard_minimum_cop = std.coil_cooling_dx_two_speed_standard_minimum_cop(component)
      end

      # 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)
        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 * 100} % below the expected value of #{standard_minimum_cop.round(2)} for #{display_standard}.")
      elsif rated_high_speed_COP > standard_minimum_cop * (1.0 + max_pass)
        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 * 100} % above the expected value of #{standard_minimum_cop.round(2)} for #{display_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)
        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 * 100} % below the expected value of #{standard_minimum_cop.round(2)} for #{display_standard}.")
      elsif rated_low_speed_COP > standard_minimum_cop * (1.0 + max_pass)
        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 * 100} % above the expected value of #{standard_minimum_cop.round(2)} for #{display_standard}.")
      end
    end

    # check CoilHeatingDXSingleSpeed objects
    # todo - need to test this once json file populated for this data
    @model.getCoilHeatingDXSingleSpeeds.each do |component|
      # eff values from model
      rated_COP = component.ratedCOP

      # get eff values from standards
      if use_old_gem_code
        standard_minimum_cop = component.standard_minimum_cop(target_standard)
      else
        standard_minimum_cop = std.coil_heating_dx_single_speed_standard_minimum_cop(component)
      end

      # 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)
        check_elems <<  OpenStudio::Attribute.new('flag', "The COP of #{rated_COP.round(2)} for #{component.name} is more than #{min_pass * 100} % below the expected value of #{standard_minimum_cop.round(2)} for #{display_standard}.")
      elsif rated_COP > standard_minimum_cop * (1.0 + max_pass)
        check_elems <<  OpenStudio::Attribute.new('flag', "The COP of  #{rated_COP.round(2)} for #{component.name} is more than #{max_pass * 100} % above the expected value of #{standard_minimum_cop.round(2)}. for #{display_standard}")
      end
    end

    # check BoilerHotWater
    @model.getBoilerHotWaters.each do |component|
      # eff values from model
      nominal_thermal_efficiency = component.nominalThermalEfficiency

      # get eff values from standards
      if use_old_gem_code
        standard_minimum_thermal_efficiency = component.standard_minimum_thermal_efficiency(target_standard)
      else
        standard_minimum_thermal_efficiency = std.boiler_hot_water_standard_minimum_thermal_efficiency(component)
      end

      # 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)
        check_elems <<  OpenStudio::Attribute.new('flag', "Nominal thermal efficiency of #{nominal_thermal_efficiency.round(2)} for #{component.name} is more than #{min_pass * 100} % below the expected value of #{standard_minimum_thermal_efficiency.round(2)} for #{display_standard}.")
      elsif nominal_thermal_efficiency > standard_minimum_thermal_efficiency * (1.0 + max_pass)
        check_elems <<  OpenStudio::Attribute.new('flag', "Nominal thermal efficiency of  #{nominal_thermal_efficiency.round(2)} for #{component.name} is more than #{max_pass * 100} % above the expected value of #{standard_minimum_thermal_efficiency.round(2)} for #{display_standard}.")
      end
    end

    # check FanConstantVolume
    @model.getFanConstantVolumes.each do |component|
      # eff values from model
      motor_eff = component.motorEfficiency

      # get eff values from standards
      if use_old_gem_code
        motor_bhp = component.brake_horsepower
      else
        motor_bhp = std.fan_brake_horsepower(component)
      end

      if use_old_gem_code
        standard_minimum_motor_efficiency_and_size = component.standard_minimum_motor_efficiency_and_size(target_standard, motor_bhp)[0]
      else
        standard_minimum_motor_efficiency_and_size = std.fan_standard_minimum_motor_efficiency_and_size(component, motor_bhp)[0]
      end

      # check actual against target
      if motor_eff < standard_minimum_motor_efficiency_and_size * (1.0 - min_pass)
        check_elems <<  OpenStudio::Attribute.new('flag', "Motor efficiency of #{motor_eff.round(2)} for #{component.name} is more than #{min_pass * 100} % below the expected value of #{standard_minimum_motor_efficiency_and_size.round(2)} for #{display_standard}.")
      elsif motor_eff > standard_minimum_motor_efficiency_and_size * (1.0 + max_pass)
        check_elems <<  OpenStudio::Attribute.new('flag', "Motor efficiency of #{motor_eff.round(2)} for #{component.name} is more than #{max_pass * 100} % above the expected value of #{standard_minimum_motor_efficiency_and_size.round(2)} for #{display_standard}.")
      end
    end

    # check FanVariableVolume
    @model.getFanVariableVolumes.each do |component|
      # eff values from model
      motor_eff = component.motorEfficiency

      # get eff values from standards
      if use_old_gem_code
        motor_bhp = component.brake_horsepower
      else
        motor_bhp = std.fan_brake_horsepower(component)
       end
      if use_old_gem_code
        standard_minimum_motor_efficiency_and_size = component.standard_minimum_motor_efficiency_and_size(target_standard, motor_bhp)[0]
      else
        standard_minimum_motor_efficiency_and_size = std.fan_standard_minimum_motor_efficiency_and_size(component, motor_bhp)[0]
      end

      # check actual against target
      if motor_eff < standard_minimum_motor_efficiency_and_size * (1.0 - min_pass)
        check_elems <<  OpenStudio::Attribute.new('flag', "Motor efficiency of #{motor_eff.round(2)} for #{component.name} is more than #{min_pass * 100} % below the expected value of #{standard_minimum_motor_efficiency_and_size.round(2)} for #{display_standard}.")
      elsif motor_eff > standard_minimum_motor_efficiency_and_size * (1.0 + max_pass)
        check_elems <<  OpenStudio::Attribute.new('flag', "Motor efficiency of #{motor_eff.round(2)} for #{component.name} is more than #{max_pass * 100} % above the expected value of #{standard_minimum_motor_efficiency_and_size.round(2)} for #{display_standard}.")
      end
    end

    # check PumpConstantSpeed
    @model.getPumpConstantSpeeds.each do |component|
      # eff values from model
      motor_eff = component.motorEfficiency

      # get eff values from standards
      if use_old_gem_code
        motor_bhp = component.brake_horsepower
      else
        motor_bhp = std.pump_brake_horsepower(component)
      end
      next if motor_bhp == 0.0
      if use_old_gem_code
        standard_minimum_motor_efficiency_and_size = component.standard_minimum_motor_efficiency_and_size(target_standard, motor_bhp)[0]
      else
        standard_minimum_motor_efficiency_and_size = std.pump_standard_minimum_motor_efficiency_and_size(component, motor_bhp)[0]
      end

      # check actual against target
      if motor_eff < standard_minimum_motor_efficiency_and_size * (1.0 - min_pass)
        check_elems <<  OpenStudio::Attribute.new('flag', "Motor efficiency of #{motor_eff.round(2)} for #{component.name} is more than #{min_pass * 100} % below the expected value of #{standard_minimum_motor_efficiency_and_size.round(2)} for #{display_standard}.")
      elsif motor_eff > standard_minimum_motor_efficiency_and_size * (1.0 + max_pass)
        check_elems <<  OpenStudio::Attribute.new('flag', "Motor efficiency of #{motor_eff.round(2)} for #{component.name} is more than #{max_pass * 100} % above the expected value of #{standard_minimum_motor_efficiency_and_size.round(2)} for #{display_standard}.")
      end
    end

    # check PumpVariableSpeed
    @model.getPumpVariableSpeeds.each do |component|
      # eff values from model
      motor_eff = component.motorEfficiency

      # get eff values from standards
      if use_old_gem_code
        motor_bhp = component.brake_horsepower
      else
        motor_bhp = std.pump_brake_horsepower(component)
      end
      next if motor_bhp == 0.0
      if use_old_gem_code
        standard_minimum_motor_efficiency_and_size = component.standard_minimum_motor_efficiency_and_size(target_standard, motor_bhp)[0]
      else
        standard_minimum_motor_efficiency_and_size = std.pump_standard_minimum_motor_efficiency_and_size(component, motor_bhp)[0]
      end

      # check actual against target
      if motor_eff < standard_minimum_motor_efficiency_and_size * (1.0 - min_pass)
        check_elems <<  OpenStudio::Attribute.new('flag', "Motor efficiency of #{motor_eff.round(2)} for #{component.name} is more than #{min_pass * 100} % below the expected value of #{standard_minimum_motor_efficiency_and_size.round(2)} for #{display_standard}.")
      elsif motor_eff > standard_minimum_motor_efficiency_and_size * (1.0 + max_pass)
        check_elems <<  OpenStudio::Attribute.new('flag', "Motor efficiency of #{motor_eff.round(2)} for #{component.name} is more than #{max_pass * 100} % above the expected value of #{standard_minimum_motor_efficiency_and_size.round(2)} for #{display_standard}.")
      end
    end

    # TODO: - should I throw flag if any other component types are in the model

    # BasicOfficeTest_Mueller.osm test model current exercises the following component types
    # (CoilCoolingDXTwoSpeed,FanVariableVolume,PumpConstantSpeed)

    # BasicOfficeTest_Mueller_altHVAC_a checks these component types
    # (ChillerElectricEIR,CoilCoolingDXSingleSpeed,CoilHeatingDXSingleSpeed,BoilerHotWater,FanConstantVolume,PumpVariableSpeed)
  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
  # note: registerWarning and registerValue will be added for checks downstream using os_lib_reporting_qaqc.rb
end

#check_mech_sys_part_load_eff(category, target_standard, min_pass, max_pass, name_only = false) ⇒ Object

checks the number of unmet hours in the model



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
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
# File 'lib/openstudio/extension/core/check_mech_sys_part_load_eff.rb', line 12

def check_mech_sys_part_load_eff(category, target_standard, min_pass, max_pass, name_only = false)
  if target_standard.include?('90.1')
    display_standard = "ASHRAE #{target_standard}"
  else
    display_standard = target_standard
  end

  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 #{display_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.")
  # TODO: - add in check for VAV fan

  # 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

  # Versions of OpenStudio greater than 2.4.0 use a modified version of
  # openstudio-standards with different method calls.  These methods
  # require a "Standard" object instead of the standard being passed into method calls.
  # This Standard object is used throughout the QAQC check.
  if OpenStudio::VersionString.new(OpenStudio.openStudioVersion) < OpenStudio::VersionString.new('2.4.3')
    use_old_gem_code = true
  else
    use_old_gem_code = false
    std = Standard.build(target_standard)
  end

  begin
    # TODO: - in future would be nice to dynamically genrate list of possible options from 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.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
      if use_old_gem_code
        search_criteria = component.find_search_criteria(target_standard)
      else
        search_criteria = std.chiller_electric_eir_find_search_criteria(component)
      end

      # 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
      if use_old_gem_code
        capacity_w = component.find_capacity
      else
        capacity_w = std.chiller_electric_eir_find_capacity(component)
      end
      capacity_tons = OpenStudio.convert(capacity_w, 'W', 'ton').get

      if use_old_gem_code
        chlr_props = component.model.find_object($os_standards['chillers'], search_criteria, capacity_tons, Date.today)
        chlr_props
        chlr_props = std.model_find_object(std.standards_data['chillers'], search_criteria, capacity_tons, Date.today)
      end
      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
      if use_old_gem_code
        temp_curve = model_temp.add_curve(target_curve_name)
      else
        temp_curve = std.model_add_curve(model_temp, target_curve_name)
      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)
        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 * 100} % below the typical value of #{target_curve_40_pct.round(2)} for #{display_standard}.")
      elsif curve_40_pct > target_curve_40_pct * (1.0 + max_pass)
        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 * 100} % above the typical value of #{target_curve_40_pct.round(2)} for #{display_standard}.")
      end
      if curve_80_pct < target_curve_80_pct * (1.0 - min_pass)
        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 * 100} % below the typical value of #{target_curve_80_pct.round(2)} for #{display_standard}.")
      elsif curve_80_pct > target_curve_80_pct * (1.0 + max_pass)
        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 * 100} % above the typical value of #{target_curve_80_pct.round(2)} for #{display_standard}.")
      end
    end

    # check getCoilCoolingDXSingleSpeeds objects (will also have curve check in different script)
    @model.getCoilCoolingDXSingleSpeeds.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
      if use_old_gem_code
        search_criteria = component.find_search_criteria(target_standard)
      else
        search_criteria = std.coil_dx_find_search_criteria(component)
      end

      if use_old_gem_code
        capacity_w = component.find_capacity
      else
        capacity_w = std.coil_cooling_dx_single_speed_find_capacity(component)
      end
      capacity_btu_per_hr = OpenStudio.convert(capacity_w, 'W', 'Btu/hr').get

      if use_old_gem_code
        if component.heat_pump?
          ac_props = component.model.find_object($os_standards['heat_pumps'], search_criteria, capacity_btu_per_hr, Date.today)
        else
          ac_props = component.model.find_object($os_standards['unitary_acs'], search_criteria, capacity_btu_per_hr, Date.today)
        end
      else
        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
      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
      if use_old_gem_code
        temp_curve = model_temp.add_curve(target_curve_name)
      else
        temp_curve = std.model_add_curve(model_temp, target_curve_name)
      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)
        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 * 100} % below the typical value of #{target_curve_40_pct.round(2)} for #{display_standard}.")
      elsif curve_40_pct > target_curve_40_pct * (1.0 + max_pass)
        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 * 100} % above the typical value of #{target_curve_40_pct.round(2)} for #{display_standard}.")
      end
      if curve_80_pct < target_curve_80_pct * (1.0 - min_pass)
        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 * 100} % below the typical value of #{target_curve_80_pct.round(2)} for #{display_standard}.")
      elsif curve_80_pct > target_curve_80_pct * (1.0 + max_pass)
        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 * 100} % above the typical value of #{target_curve_80_pct.round(2)} for #{display_standard}.")
      end
    end

    # check CoilCoolingDXTwoSpeed objects (will also have curve check in different script)
    @model.getCoilCoolingDXTwoSpeeds.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
      if use_old_gem_code
        search_criteria = component.find_search_criteria(target_standard)
      else
        search_criteria = std.coil_dx_find_search_criteria(component)
      end

      if use_old_gem_code
        capacity_w = component.find_capacity
      else
        capacity_w = std.coil_cooling_dx_two_speed_find_capacity(component)
      end
      capacity_btu_per_hr = OpenStudio.convert(capacity_w, 'W', 'Btu/hr').get

      if use_old_gem_code
        ac_props = component.model.find_object($os_standards['unitary_acs'], 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_flow curve for #{component.name}")
        next # don't go past here in loop if can't find curve
      end
      if use_old_gem_code
        temp_curve = model_temp.add_curve(target_curve_name)
      else
        temp_curve = std.model_add_curve(model_temp, target_curve_name)
      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)
        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 * 100} % below the typical value of #{target_curve_40_pct.round(2)} for #{display_standard}.")
      elsif curve_40_pct > target_curve_40_pct * (1.0 + max_pass)
        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 * 100} % above the typical value of #{target_curve_40_pct.round(2)} for #{display_standard}.")
      end
      if curve_80_pct < target_curve_80_pct * (1.0 - min_pass)
        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 * 100} % below the typical value of #{target_curve_80_pct.round(2)} for #{display_standard}.")
      elsif curve_80_pct > target_curve_80_pct * (1.0 + max_pass)
        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 * 100} % above the typical value of #{target_curve_80_pct.round(2)} for #{display_standard}.")
      end
    end

    # check CoilCoolingDXTwoSpeed objects (will also have curve check in different script)
    @model.getCoilHeatingDXSingleSpeeds.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
      if use_old_gem_code
        search_criteria = component.find_search_criteria(target_standard)
      else
        search_criteria = std.coil_dx_find_search_criteria(component)
      end

      if use_old_gem_code
        capacity_w = component.find_capacity
      else
        capacity_w = std.coil_heating_dx_single_speed_find_capacity(component)
      end
      capacity_btu_per_hr = OpenStudio.convert(capacity_w, 'W', 'Btu/hr').get

      if use_old_gem_code
        ac_props = component.model.find_object($os_standards['heat_pumps_heating'], search_criteria, capacity_btu_per_hr, Date.today)
      else
        ac_props = std.model_find_object(std.standards_data['heat_pumps_heating'], search_criteria, capacity_btu_per_hr, Date.today)
      end
      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
      if use_old_gem_code
        temp_curve = model_temp.add_curve(target_curve_name)
      else
        temp_curve = std.model_add_curve(model_temp, target_curve_name)
      end

      # 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)
        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 * 100} % below the typical value of #{target_curve_40_pct.round(2)} for #{display_standard}.")
      elsif curve_40_pct > target_curve_40_pct * (1.0 + max_pass)
        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 * 100} % above the typical value of #{target_curve_40_pct.round(2)} for #{display_standard}.")
      end
      if curve_80_pct < target_curve_80_pct * (1.0 - min_pass)
        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 * 100} % below the typical value of #{target_curve_80_pct.round(2)} for #{display_standard}.")
      elsif curve_80_pct > target_curve_80_pct * (1.0 + max_pass)
        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 * 100} % above the typical value of #{target_curve_80_pct.round(2)} for #{display_standard}.")
      end
    end

    # check
    @model.getFanVariableVolumes.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
      if use_old_gem_code
        next if component.brake_horsepower == 0.0
      else
        next if std.fan_brake_horsepower(component) == 0.0
      end

      # 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)
      if use_old_gem_code
        target_fan.set_control_type('Multi Zone VAV with Static Pressure Reset')
      else
        std.fan_variable_volume_set_control_type(target_fan, 'Multi Zone VAV with VSD and Static Pressure Reset')
      end

      # 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)
        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 * 100} % below the typical value of #{target_curve_40_pct.round(2)} for #{display_standard}.")
      elsif curve_40_pct > target_curve_40_pct * (1.0 + max_pass)
        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 * 100} % above the typical value of #{target_curve_40_pct.round(2)} for #{display_standard}.")
      end
      if curve_80_pct < target_curve_80_pct * (1.0 - min_pass)
        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 * 100} % below the typical value of #{target_curve_80_pct.round(2)} for #{display_standard}.")
      elsif curve_80_pct > target_curve_80_pct * (1.0 + max_pass)
        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 * 100} % above the typical value of #{target_curve_80_pct.round(2)} for #{display_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
  # note: registerWarning and registerValue will be added for checks downstream using os_lib_reporting_qaqc.rb
end

#check_mech_sys_type(category, target_standard, name_only = false) ⇒ Object

checks the number of unmet hours in the model



10
11
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
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
# File 'lib/openstudio/extension/core/check_mech_sys_type.rb', line 10

def check_mech_sys_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

  # Versions of OpenStudio greater than 2.4.0 use a modified version of
  # openstudio-standards with different method calls.  These methods
  # require a "Standard" object instead of the standard being passed into method calls.
  # This Standard object is used throughout the QAQC check.
  if OpenStudio::VersionString.new(OpenStudio.openStudioVersion) < OpenStudio::VersionString.new('2.4.3')
    use_old_gem_code = true
  else
    use_old_gem_code = false
    std = Standard.build(target_standard)
  end

  begin
    # Get the actual system type for all zones in the model
    act_zone_to_sys_type = {}
    @model.getThermalZones.each do |zone|
      if use_old_gem_code
        act_zone_to_sys_type[zone] = zone.infer_system_type
      else
        act_zone_to_sys_type[zone] = std.thermal_zone_infer_system_type(zone)
      end
    end

    # Get the baseline system type for all zones in the model
    if use_old_gem_code
      climate_zone = @model.get_building_climate_zone_and_building_type['climate_zone']
    else
      climate_zone = std.model_get_building_properties(@model)['climate_zone']
    end

    if use_old_gem_code
      req_zone_to_sys_type = @model.get_baseline_system_type_by_zone(target_standard, climate_zone)
    else
      req_zone_to_sys_type = std.model_get_baseline_system_type_by_zone(@model, climate_zone)
    end

    # Compare the actual to the correct
    @model.getThermalZones.each do |zone|
      # TODO: - skip if plenum
      is_plenum = false
      zone.spaces.each do |space|
        if use_old_gem_code
          if space.plenum?
            is_plenum = true
          end
        else
          if std.space_plenum?(space)
            is_plenum = true
          end
        end
      end
      next if is_plenum

      req_sys_type = req_zone_to_sys_type[zone]
      act_sys_type = act_zone_to_sys_type[zone]

      if act_sys_type == req_sys_type
        puts "#{zone.name} system type = #{act_sys_type}"
      else
        if req_sys_type == '' then req_sys_type = 'Unknown' end
        puts "#{zone.name} baseline system type is incorrect. Supposed to be #{req_sys_type}, but was #{act_sys_type} instead."
        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
  # note: registerWarning and registerValue will be added for checks downstream using os_lib_reporting_qaqc.rb
end

#check_part_loads(category, target_standard, max_pct_delta = 0.1, name_only = false) ⇒ Object

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



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
# File 'lib/openstudio/extension/core/check_part_loads.rb', line 46

def check_part_loads(category, target_standard, max_pct_delta = 0.1, 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

  std = Standard.build(target_standard)

  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.each do |equip|
      # Get the timeseries part load ratio data
      key_value =  equip.name.get.to_s.upcase # must be in all caps.
      time_step = 'Hourly'
      variable_name = 'Boiler Part Load Ratio'
      ts = @sql.timeSeries(ann_env_pd, time_step, variable_name, key_value)
      if ts.empty?
        check_elems << OpenStudio::Attribute.new('flag', "#{variable_name} Timeseries not found for #{key_value}.")
        next
      end

      # Convert to array
      ts = ts.get.values
      plrs = []
      for i in 0..(ts.size - 1)
        plrs << ts[i]
      end

      # Bin part load ratios
      pct_hrs_above_90 = bin_part_loads_by_ten_pcts(plrs)[9]

      # Check top-end part load ratio bins
      if ((pct_hrs_above_90 - expected_pct_hrs_above_90) / pct_hrs_above_90).abs > max_pct_delta
        check_elems << OpenStudio::Attribute.new('flag', "For #{equip.name}, the actual hrs above 90% part load of #{(pct_hrs_above_90 * 100).round(2)}% is more than #{(max_pct_delta * 100.0).round(2)}% different from the expected #{(expected_pct_hrs_above_90 * 100).round(2)}% of hrs above 90% part load.  This could indicate significantly oversized or undersized equipment.")
      end
    end

    # Chillers
    @model.getChillerElectricEIRs.each do |equip|
      # Get the timeseries part load ratio data
      key_value =  equip.name.get.to_s.upcase # must be in all caps.
      time_step = 'Hourly'
      variable_name = 'Chiller Part Load Ratio'
      ts = @sql.timeSeries(ann_env_pd, time_step, variable_name, key_value)
      if ts.empty?
        check_elems << OpenStudio::Attribute.new('flag', "#{variable_name} Timeseries not found for #{key_value}.")
        next
      end

      # Convert to array
      ts = ts.get.values
      plrs = []
      for i in 0..(ts.size - 1)
        plrs << ts[i]
      end

      # Bin part load ratios
      pct_hrs_above_90 = bin_part_loads_by_ten_pcts(plrs)[9]

      # Check top-end part load ratio bins
      if ((pct_hrs_above_90 - expected_pct_hrs_above_90) / pct_hrs_above_90).abs > max_pct_delta
        check_elems << OpenStudio::Attribute.new('flag', "For #{equip.name}, the actual hrs above 90% part load of #{(pct_hrs_above_90 * 100).round(2)}% is more than #{(max_pct_delta * 100.0).round(2)}% different from the expected #{(expected_pct_hrs_above_90 * 100).round(2)}% of hrs above 90% part load.  This could indicate significantly oversized or undersized equipment.")
      end
    end

    # Cooling Towers (Single Speed)
    @model.getCoolingTowerSingleSpeeds.each do |equip|
      # Get the design fan power
      if equip.fanPoweratDesignAirFlowRate.is_initialized
        dsn_pwr = equip.fanPoweratDesignAirFlowRate.get
      elsif equip.autosizedFanPoweratDesignAirFlowRate.is_initialized
        dsn_pwr = equip.autosizedFanPoweratDesignAirFlowRate.get
      else
        check_elems << OpenStudio::Attribute.new('flag', "Could not determine peak power for #{equip.name}, cannot check part load ratios.")
        next
      end

      # Get the timeseries fan power
      key_value = equip.name.get.to_s.upcase # must be in all caps.
      time_step = 'Hourly'
      variable_name = 'Cooling Tower Fan Electric Power'
      ts = @sql.timeSeries(ann_env_pd, time_step, variable_name, key_value)
      if ts.empty?
        check_elems << OpenStudio::Attribute.new('flag', "#{variable_name} Timeseries not found for #{key_value}.")
        next
      end

      # Convert to array
      ts = ts.get.values
      plrs = []
      for i in 0..(ts.size - 1)
        plrs << ts[i] / dsn_pwr.to_f
      end

      # Bin part load ratios
      pct_hrs_above_90 = bin_part_loads_by_ten_pcts(plrs)[9]

      # Check top-end part load ratio bins
      if ((pct_hrs_above_90 - expected_pct_hrs_above_90) / pct_hrs_above_90).abs > max_pct_delta
        check_elems << OpenStudio::Attribute.new('flag', "For #{equip.name}, the actual hrs above 90% part load of #{(pct_hrs_above_90 * 100).round(2)}% is more than #{(max_pct_delta * 100.0).round(2)}% different from the expected #{(expected_pct_hrs_above_90 * 100).round(2)}% of hrs above 90% part load.  This could indicate significantly oversized or undersized equipment.")
      end
    end

    # Cooling Towers (Two Speed)
    @model.getCoolingTowerTwoSpeeds.each do |equip|
      # Get the design fan power
      if equip.highFanSpeedFanPower.is_initialized
        dsn_pwr = equip.highFanSpeedFanPower.get
      elsif equip.autosizedHighFanSpeedFanPower.is_initialized
        dsn_pwr = equip.autosizedHighFanSpeedFanPower.get
      else
        check_elems << OpenStudio::Attribute.new('flag', "Could not determine peak power for #{equip.name}, cannot check part load ratios.")
        next
      end

      # Get the timeseries fan power
      key_value = equip.name.get.to_s.upcase # must be in all caps.
      time_step = 'Hourly'
      variable_name = 'Cooling Tower Fan Electric Power'
      ts = @sql.timeSeries(ann_env_pd, time_step, variable_name, key_value)
      if ts.empty?
        check_elems << OpenStudio::Attribute.new('flag', "#{variable_name} Timeseries not found for #{key_value}.")
        next
      end

      # Convert to array
      ts = ts.get.values
      plrs = []
      for i in 0..(ts.size - 1)
        plrs << ts[i] / dsn_pwr.to_f
      end

      # Bin part load ratios
      pct_hrs_above_90 = bin_part_loads_by_ten_pcts(plrs)[9]

      # Check top-end part load ratio bins
      if ((pct_hrs_above_90 - expected_pct_hrs_above_90) / pct_hrs_above_90).abs > max_pct_delta
        check_elems << OpenStudio::Attribute.new('flag', "For #{equip.name}, the actual hrs above 90% part load of #{(pct_hrs_above_90 * 100).round(2)}% is more than #{(max_pct_delta * 100.0).round(2)}% different from the expected #{(expected_pct_hrs_above_90 * 100).round(2)}% of hrs above 90% part load.  This could indicate significantly oversized or undersized equipment.")
      end
    end

    # Cooling Towers (Variable Speed)
    @model.getCoolingTowerVariableSpeeds.each do |equip|
      # Get the design fan power
      if equip.designFanPower.is_initialized
        dsn_pwr = equip.designFanPower.get
      elsif equip.autosizedDesignFanPower.is_initialized
        dsn_pwr = equip.autosizedDesignFanPower.get
      else
        check_elems << OpenStudio::Attribute.new('flag', "Could not determine peak power for #{equip.name}, cannot check part load ratios.")
        next
      end

      # Get the timeseries fan power
      key_value = equip.name.get.to_s.upcase # must be in all caps.
      time_step = 'Hourly'
      variable_name = 'Cooling Tower Fan Electric Power'
      ts = @sql.timeSeries(ann_env_pd, time_step, variable_name, key_value)
      if ts.empty?
        check_elems << OpenStudio::Attribute.new('flag', "#{variable_name} Timeseries not found for #{key_value}.")
        next
      end

      # Convert to array
      ts = ts.get.values
      plrs = []
      for i in 0..(ts.size - 1)
        plrs << ts[i] / dsn_pwr.to_f
      end

      # Bin part load ratios
      pct_hrs_above_90 = bin_part_loads_by_ten_pcts(plrs)[9]

      # Check top-end part load ratio bins
      if ((pct_hrs_above_90 - expected_pct_hrs_above_90) / pct_hrs_above_90).abs > max_pct_delta
        check_elems << OpenStudio::Attribute.new('flag', "For #{equip.name}, the actual hrs above 90% part load of #{(pct_hrs_above_90 * 100).round(2)}% is more than #{(max_pct_delta * 100.0).round(2)}% different from the expected #{(expected_pct_hrs_above_90 * 100).round(2)}% of hrs above 90% part load.  This could indicate significantly oversized or undersized equipment.")
      end
    end

    # DX Cooling Coils (Single Speed)
    @model.getCoilCoolingDXSingleSpeeds.each do |equip|
      # Get the design coil capacity
      if equip.grossRatedTotalCoolingCapacity.is_initialized
        dsn_pwr = equip.grossRatedTotalCoolingCapacity.get
      elsif equip.autosizedGrossRatedTotalCoolingCapacity.is_initialized
        dsn_pwr = equip.autosizedGrossRatedTotalCoolingCapacity.get
      else
        check_elems << OpenStudio::Attribute.new('flag', "Could not determine capacity for #{equip.name}, cannot check part load ratios.")
        next
      end

      # Get the timeseries coil capacity
      key_value = equip.name.get.to_s.upcase # must be in all caps.
      time_step = 'Hourly'
      variable_name = 'Cooling Coil Total Cooling Rate'
      ts = @sql.timeSeries(ann_env_pd, time_step, variable_name, key_value)
      if ts.empty?
        check_elems << OpenStudio::Attribute.new('flag', "#{variable_name} Timeseries not found for #{key_value}.")
        next
      end

      # Convert to array
      ts = ts.get.values
      plrs = []
      for i in 0..(ts.size - 1)
        plrs << ts[i] / dsn_pwr.to_f
      end

      # Bin part load ratios
      pct_hrs_above_90 = bin_part_loads_by_ten_pcts(plrs)[9]

      # Check top-end part load ratio bins
      if ((pct_hrs_above_90 - expected_pct_hrs_above_90) / pct_hrs_above_90).abs > max_pct_delta
        check_elems << OpenStudio::Attribute.new('flag', "For #{equip.name}, the actual hrs above 90% part load of #{(pct_hrs_above_90 * 100).round(2)}% is more than #{(max_pct_delta * 100.0).round(2)}% different from the expected #{(expected_pct_hrs_above_90 * 100).round(2)}% of hrs above 90% part load.  This could indicate significantly oversized or undersized equipment.")
      end
    end

    # DX Cooling Coils (Two Speed)
    @model.getCoilCoolingDXTwoSpeeds.each do |equip|
      # Get the design coil capacity
      if equip.ratedHighSpeedTotalCoolingCapacity.is_initialized
        dsn_pwr = equip.ratedHighSpeedTotalCoolingCapacity.get
      elsif equip.autosizedRatedHighSpeedTotalCoolingCapacity.is_initialized
        dsn_pwr = equip.autosizedRatedHighSpeedTotalCoolingCapacity.get
      else
        check_elems << OpenStudio::Attribute.new('flag', "Could not determine capacity for #{equip.name}, cannot check part load ratios.")
        next
      end

      # Get the timeseries coil capacity
      key_value = equip.name.get.to_s.upcase # must be in all caps.
      time_step = 'Hourly'
      variable_name = 'Cooling Coil Total Cooling Rate'
      ts = @sql.timeSeries(ann_env_pd, time_step, variable_name, key_value)
      if ts.empty?
        check_elems << OpenStudio::Attribute.new('flag', "#{variable_name} Timeseries not found for #{key_value}.")
        next
      end

      # Convert to array
      ts = ts.get.values
      plrs = []
      for i in 0..(ts.size - 1)
        plrs << ts[i] / dsn_pwr.to_f
      end

      # Bin part load ratios
      pct_hrs_above_90 = bin_part_loads_by_ten_pcts(plrs)[9]

      # Check top-end part load ratio bins
      if ((pct_hrs_above_90 - expected_pct_hrs_above_90) / pct_hrs_above_90).abs > max_pct_delta
        check_elems << OpenStudio::Attribute.new('flag', "For #{equip.name}, the actual hrs above 90% part load of #{(pct_hrs_above_90 * 100).round(2)}% is more than #{(max_pct_delta * 100.0).round(2)}% different from the expected #{(expected_pct_hrs_above_90 * 100).round(2)}% of hrs above 90% part load.  This could indicate significantly oversized or undersized equipment.")
      end
    end

    # DX Cooling Coils (Variable Speed)
    @model.getCoilCoolingDXVariableSpeeds.each do |equip|
      # Get the design coil capacity
      if equip.grossRatedTotalCoolingCapacityAtSelectedNominalSpeedLevel.is_initialized
        dsn_pwr = equip.grossRatedTotalCoolingCapacityAtSelectedNominalSpeedLevel.get
      elsif equip.autosizedGrossRatedTotalCoolingCapacityAtSelectedNominalSpeedLevel.is_initialized
        dsn_pwr = equip.autosizedGrossRatedTotalCoolingCapacityAtSelectedNominalSpeedLevel.get
      else
        check_elems << OpenStudio::Attribute.new('flag', "Could not determine capacity for #{equip.name}, cannot check part load ratios.")
        next
      end

      # Get the timeseries coil capacity
      key_value = equip.name.get.to_s.upcase # must be in all caps.
      time_step = 'Hourly'
      variable_name = 'Cooling Coil Total Cooling Rate'
      ts = @sql.timeSeries(ann_env_pd, time_step, variable_name, key_value)
      if ts.empty?
        check_elems << OpenStudio::Attribute.new('flag', "#{variable_name} Timeseries not found for #{key_value}.")
        next
      end

      # Convert to array
      ts = ts.get.values
      plrs = []
      for i in 0..(ts.size - 1)
        plrs << ts[i] / dsn_pwr.to_f
      end

      # Bin part load ratios
      pct_hrs_above_90 = bin_part_loads_by_ten_pcts(plrs)[9]

      # Check top-end part load ratio bins
      if ((pct_hrs_above_90 - expected_pct_hrs_above_90) / pct_hrs_above_90).abs > max_pct_delta
        check_elems << OpenStudio::Attribute.new('flag', "For #{equip.name}, the actual hrs above 90% part load of #{(pct_hrs_above_90 * 100).round(2)}% is more than #{(max_pct_delta * 100.0).round(2)}% different from the expected #{(expected_pct_hrs_above_90 * 100).round(2)}% of hrs above 90% part load.  This could indicate significantly oversized or undersized equipment.")
      end
    end

    # Gas Heating Coils
    @model.getCoilHeatingGass.each do |equip|
      # Get the design coil capacity
      if equip.nominalCapacity.is_initialized
        dsn_pwr = equip.nominalCapacity.get
      elsif equip.autosizedNominalCapacity.is_initialized
        dsn_pwr = equip.autosizedNominalCapacity.get
      else
        check_elems << OpenStudio::Attribute.new('flag', "Could not determine capacity for #{equip.name}, cannot check part load ratios.")
        next
      end

      # Get the timeseries coil capacity
      key_value = equip.name.get.to_s.upcase # must be in all caps.
      time_step = 'Hourly'
      variable_name = 'Heating Coil Air Heating Rate'
      ts = @sql.timeSeries(ann_env_pd, time_step, variable_name, key_value)
      if ts.empty?
        check_elems << OpenStudio::Attribute.new('flag', "#{variable_name} Timeseries not found for #{key_value}.")
        next
      end

      # Convert to array
      ts = ts.get.values
      plrs = []
      for i in 0..(ts.size - 1)
        plrs << ts[i] / dsn_pwr.to_f
      end

      # Bin part load ratios
      pct_hrs_above_90 = bin_part_loads_by_ten_pcts(plrs)[9]

      # Check top-end part load ratio bins
      if ((pct_hrs_above_90 - expected_pct_hrs_above_90) / pct_hrs_above_90).abs > max_pct_delta
        check_elems << OpenStudio::Attribute.new('flag', "For #{equip.name}, the actual hrs above 90% part load of #{(pct_hrs_above_90 * 100).round(2)}% is more than #{(max_pct_delta * 100.0).round(2)}% different from the expected #{(expected_pct_hrs_above_90 * 100).round(2)}% of hrs above 90% part load.  This could indicate significantly oversized or undersized equipment.")
      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
  # note: registerWarning and registerValue will be added for checks downstream using os_lib_reporting_qaqc.rb
end

#check_placeholder(category, name_only = false) ⇒ Object

checks the number of unmet hours in the model



10
11
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
# File 'lib/openstudio/extension/core/check_placeholder.rb', line 10

def check_placeholder(category, name_only = false)
  # summary of the check
  check_elems = OpenStudio::AttributeVector.new
  check_elems << OpenStudio::Attribute.new('name', 'Place Holder Check')
  check_elems << OpenStudio::Attribute.new('category', category)
  check_elems << OpenStudio::Attribute.new('description', 'This does nothing, it will just throw a flag until I add real check code to the method.')

  # 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
    # TODO: - implement QAQC check code here

    # remove this once code is written to do real checks
    check_elems << OpenStudio::Attribute.new('flag', 'Code has not been implemented yet for this QAQC check')
  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
  # note: registerWarning and registerValue will be added for checks downstream using os_lib_reporting_qaqc.rb
end

#check_plant_cap(category, target_standard, max_pct_delta = 0.1, name_only = false) ⇒ Object

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



9
10
11
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
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
# File 'lib/openstudio/extension/core/check_plant_cap.rb', line 9

def check_plant_cap(category, target_standard, max_pct_delta = 0.1, 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.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)

      # Heating and cooling loads
      plant_loop.demandComponents.each do |dc|
        # Get the load for each coil
        htg_load_w = 0.0
        clg_load_w = 0.0
        obj_type = sc.iddObjectType.valueName.to_s
        case obj_type
        when 'OS_Coil_Heating_Water'
          coil = sc.to_CoilHeatingWater.get
          if coil.ratedCapacity.is_initialized
            clg_load_w += coil.ratedCapacity.get
          elsif coil.autosizedRatedCapacity.is_initialized
            clg_load_w += coil.autosizedRatedCapacity.get
          end
        when 'OS_Coil_Cooling_Water'
          coil = sc.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_cap_kbtu_per_hr} tons is more than #{(max_pct_delta * 100.0).round(2)}% different from the combined coil load of #{clg_load_kbtu_per_hr} 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
  # note: registerWarning and registerValue will be added for checks downstream using os_lib_reporting_qaqc.rb
end

#check_plant_temps(category, target_standard, max_sizing_temp_delta = 0.1, name_only = false) ⇒ Object

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.



10
11
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
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
# File 'lib/openstudio/extension/core/check_plant_temps.rb', line 10

def check_plant_temps(category, target_standard, max_sizing_temp_delta = 0.1, 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

  std = Standard.build(target_standard)

  begin
    # Check each plant loop in the model
    @model.getPlantLoops.sort.each do |plant_loop|
      loop_name = plant_loop.name.to_s

      # Get the central heating and cooling SAT for sizing
      sizing_plant = plant_loop.sizingPlant
      loop_siz_f = OpenStudio.convert(sizing_plant.designLoopExitTemperature, 'C', 'F').get

      # Determine the min and max operational temperatures
      loop_op_min_f = nil
      loop_op_max_f = nil
      plant_loop.supplyOutletNode.setpointManagers.each do |spm|
        obj_type = spm.iddObjectType.valueName.to_s
        case obj_type
        when 'OS_SetpointManager_Scheduled'
          sch = spm.to_SetpointManagerScheduled.get.schedule
          if sch.to_ScheduleRuleset.is_initialized
            min_c = std.schedule_ruleset_annual_min_max_value(sch.to_ScheduleRuleset.get)['min']
            max_c = std.schedule_ruleset_annual_min_max_value(sch.to_ScheduleRuleset.get)['max']
          elsif sch.to_ScheduleConstant.is_initialized
            min_c = std.schedule_constant_annual_min_max_value(sch.to_ScheduleConstant.get)['min']
            max_c = std.schedule_constant_annual_min_max_value(sch.to_ScheduleConstant.get)['max']
          else
            next
          end
          loop_op_min_f = OpenStudio.convert(min_c, 'C', 'F').get
          loop_op_max_f = OpenStudio.convert(max_c, 'C', 'F').get
        when 'OS_SetpointManager_Scheduled_DualSetpoint'
          spm = spm.to_SetpointManagerSingleZoneReheat.get
          # Lowest setpoint is minimum of low schedule
          low_sch = spm.to_SetpointManagerScheduled.get.lowSetpointSchedule
          next if low_sch.empty?
          low_sch = low_sch.get
          if low_sch.to_ScheduleRuleset.is_initialized
            min_c = std.schedule_ruleset_annual_min_max_value(low_sch.to_ScheduleRuleset.get)['min']
            max_c = std.schedule_ruleset_annual_min_max_value(low_sch.to_ScheduleRuleset.get)['max']
          elsif low_sch.to_ScheduleConstant.is_initialized
            min_c = std.schedule_constant_annual_min_max_value(low_sch.to_ScheduleConstant.get)['min']
            max_c = std.schedule_constant_annual_min_max_value(low_sch.to_ScheduleConstant.get)['max']
          else
            next
          end
          loop_op_min_f = OpenStudio.convert(min_c, 'C', 'F').get
          # Highest setpoint it maximum of high schedule
          high_sch = spm.to_SetpointManagerScheduled.get.highSetpointSchedule
          next if high_sch.empty?
          high_sch = high_sch.get
          if high_sch.to_ScheduleRuleset.is_initialized
            min_c = std.schedule_ruleset_annual_min_max_value(high_sch.to_ScheduleRuleset.get)['min']
            max_c = std.schedule_ruleset_annual_min_max_value(high_sch.to_ScheduleRuleset.get)['max']
          elsif high_sch.to_ScheduleConstant.is_initialized
            min_c = std.schedule_constant_annual_min_max_value(high_sch.to_ScheduleConstant.get)['min']
            max_c = std.schedule_constant_annual_min_max_value(high_sch.to_ScheduleConstant.get)['max']
          else
            next
          end
          loop_op_max_f = OpenStudio.convert(max_c, 'C', 'F').get
        when 'OS_SetpointManager_OutdoorAirReset'
          spm = spm.to_SetpointManagerOutdoorAirReset.get
          temp_1_f = OpenStudio.convert(spm.setpointatOutdoorHighTemperature, 'C', 'F').get
          temp_2_f = OpenStudio.convert(spm.setpointatOutdoorLowTemperature, 'C', 'F').get
          loop_op_min_f = [temp_1_f, temp_2_f].min
          loop_op_max_f = [temp_1_f, temp_2_f].max
        else
          next # Only check the commonly used setpoint managers
        end
      end

      # Compare plant loop sizing temperatures to operational temperatures
      case sizing_plant.loopType
      when 'Heating'
        if loop_op_max_f
          if ((loop_op_max_f - loop_siz_f) / loop_op_max_f).abs > max_sizing_temp_delta
            check_elems << OpenStudio::Attribute.new('flag', "For #{plant_loop.name}, the sizing is done with a supply water temp of #{loop_siz_f.round(2)}F, but the setpoint manager controlling the loop operates up to #{loop_op_max_f.round(2)}F. These are farther apart than the acceptable #{(max_sizing_temp_delta * 100.0).round(2)}% difference.")
          end
        end
      when 'Cooling'
        if loop_op_min_f
          if ((loop_op_min_f - loop_siz_f) / loop_op_min_f).abs > max_sizing_temp_delta
            check_elems << OpenStudio::Attribute.new('flag', "For #{plant_loop.name}, the sizing is done with a supply water temp of #{loop_siz_f.round(2)}F, but the setpoint manager controlling the loop operates down to #{loop_op_min_f.round(2)}F. These are farther apart than the acceptable #{(max_sizing_temp_delta * 100.0).round(2)}% difference.")
          end
        end
      when 'Condenser'
        # Not checking sizing of condenser loops
      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
  # note: registerWarning and registerValue will be added for checks downstream using os_lib_reporting_qaqc.rb
end

#check_plenum_loads(category, target_standard, name_only = false) ⇒ Object

Check that there are no people or lights in plenums.



8
9
10
11
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
# File 'lib/openstudio/extension/core/check_plenum_loads.rb', line 8

def 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.each do |zone|
      # Only check plenums
      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
  # note: registerWarning and registerValue will be added for checks downstream using os_lib_reporting_qaqc.rb
end

#check_pump_pwr(category, target_standard, max_pwr_delta = 0.1, name_only = false) ⇒ Object

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



9
10
11
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
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
# File 'lib/openstudio/extension/core/check_pump_pwr.rb', line 9

def check_pump_pwr(category, target_standard, max_pwr_delta = 0.1, 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.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 |sc|
        # Get the W/gpm for the pump
        obj_type = sc.iddObjectType.valueName.to_s
        case obj_type
        when 'OS_Pump_ConstantSpeed'
          actual_w_per_gpm = std.pump_rated_w_per_gpm(sc.to_PumpConstantSpeed.get)
        when 'OS_Pump_VariableSpeed'
          actual_w_per_gpm = std.pump_rated_w_per_gpm(sc.to_PumpVariableSpeed.get)
        when 'OS_HeaderedPumps_ConstantSpeed'
          actual_w_per_gpm = std.pump_rated_w_per_gpm(sc.to_HeaderedPumpsConstantSpeed.get)
        when 'OS_HeaderedPumps_VariableSpeed'
          actual_w_per_gpm = std.pump_rated_w_per_gpm(sc.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_pwr_delta
          check_elems << OpenStudio::Attribute.new('flag', "For #{sc.name} on #{plant_loop.name}, the actual pumping power of #{actual_w_per_gpm.round(1)} W/gpm is more than #{(max_pwr_delta * 100.0).round(2)}% different from the expected #{expected_w_per_gpm} W/gpm for a #{loop_type} plant loop.")
        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
  # note: registerWarning and registerValue will be added for checks downstream using os_lib_reporting_qaqc.rb
end

#check_sch_coord(category, target_standard, max_hrs, name_only = false) ⇒ Object

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.



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
# File 'lib/openstudio/extension/core/check_sch_coord.rb', line 101

def check_sch_coord(category, target_standard, max_hrs, 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 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, 0, 0)

    # Check schedules in each space
    @model.getSpaces.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
      occ_start_time, occ_end_time = get_start_and_end_times(occ_schs[0])

      # 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
        start_time, end_time = get_start_and_end_times(coord_sch)
        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
  # note: registerWarning and registerValue will be added for checks downstream using os_lib_reporting_qaqc.rb
end

#check_schedules(category, target_standard, min_pass, max_pass, name_only = false) ⇒ Object

checks the number of unmet hours in the model



10
11
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
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
# File 'lib/openstudio/extension/core/check_schedules.rb', line 10

def check_schedules(category, target_standard, min_pass, max_pass, 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

  # Versions of OpenStudio greater than 2.4.0 use a modified version of
  # openstudio-standards with different method calls.  These methods
  # require a "Standard" object instead of the standard being passed into method calls.
  # This Standard object is used throughout the QAQC check.
  if OpenStudio::VersionString.new(OpenStudio.openStudioVersion) < OpenStudio::VersionString.new('2.4.3')
    use_old_gem_code = true
  else
    use_old_gem_code = false
    std = Standard.build(target_standard)
  end

  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
      if use_old_gem_code
        data = space_type.get_standards_data(target_standard)
      else
        data = std.space_type_get_standards_data(space_type)
      end

      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|
          if use_old_gem_code
            if !space.plenum?
              all_spaces_plenums = false
              next
            end
          else
            if !std.space_plenum?(space)
              all_spaces_plenums = false
              next
            end
          end
        end

        if !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
        if use_old_gem_code
          schedule_target = model_temp.add_schedule(data['lighting_schedule'])
        else
          schedule_target = std.model_add_schedule(model_temp, data['lighting_schedule'])
        end
        if !schedule_target
          check_elems << OpenStudio::Attribute.new('flag', "Didn't find schedule named #{data['lighting_schedule']} in standards json.")
        else
          # loop through and test individual load instances
          if use_old_gem_code
            target_hrs = schedule_target.annual_equivalent_full_load_hrs
          else
            target_hrs = std.schedule_ruleset_annual_equivalent_full_load_hrs(schedule_target)
          end
          space_type.lights.each do |load_inst|
            inst_sch_check = generate_load_insc_sch_check_attribute(target_hrs, load_inst, space_type, check_elems, min_pass, max_pass)
            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
        if use_old_gem_code
          schedule_target = model_temp.add_schedule(data['electric_equipment_schedule'])
        else
          schedule_target = std.model_add_schedule(model_temp, data['electric_equipment_schedule'])
        end
        if !schedule_target
          check_elems << OpenStudio::Attribute.new('flag', "Didn't find schedule named #{data['electric_equipment_schedule']} in standards json.")
        else
          # loop through and test individual load instances
          if use_old_gem_code
            target_hrs = schedule_target.annual_equivalent_full_load_hrs
          else
            target_hrs = std.schedule_ruleset_annual_equivalent_full_load_hrs(schedule_target)
          end

          space_type.electricEquipment.each do |load_inst|
            inst_sch_check = generate_load_insc_sch_check_attribute(target_hrs, load_inst, space_type, check_elems, min_pass, max_pass)
            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
        if use_old_gem_code
          schedule_target = model_temp.add_schedule(data['gas_equipment_schedule'])
        else
          schedule_target = std.model_add_schedule(model_temp, data['gas_equipment_schedule'])
        end
        if !schedule_target
          check_elems << OpenStudio::Attribute.new('flag', "Didn't find schedule named #{data['gas_equipment_schedule']} in standards json.")
        else
          # loop through and test individual load instances
          if use_old_gem_code
            target_hrs = schedule_target.annual_equivalent_full_load_hrs
          else
            target_hrs = std.schedule_ruleset_annual_equivalent_full_load_hrs(schedule_target)
          end
          space_type.gasEquipment.each do |load_inst|
            inst_sch_check = generate_load_insc_sch_check_attribute(target_hrs, load_inst, space_type, check_elems, min_pass, max_pass)
            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
        if use_old_gem_code
          schedule_target = model_temp.add_schedule(data['occupancy_schedule'])
        else
          schedule_target = std.model_add_schedule(model_temp, data['occupancy_schedule'])
        end
        if !schedule_target
          check_elems << OpenStudio::Attribute.new('flag', "Didn't find schedule named #{data['occupancy_schedule']} in standards json.")
        else
          # loop through and test individual load instances
          if use_old_gem_code
            target_hrs = schedule_target.annual_equivalent_full_load_hrs
          else
            target_hrs = std.schedule_ruleset_annual_equivalent_full_load_hrs(schedule_target)
          end
          space_type.people.each do |load_inst|
            inst_sch_check = generate_load_insc_sch_check_attribute(target_hrs, load_inst, space_type, check_elems, min_pass, max_pass)
            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 equiv value
      # oa_schedule should not exist, or if it does shoudl be always on or have 8760 annual equiv value
      if space_type.designSpecificationOutdoorAir.is_initialized
        oa = space_type.designSpecificationOutdoorAir.get
        if oa.outdoorAirFlowRateFractionSchedule.is_initialized
          # TODO: - update measure test to check this
          target_hrs = 8760
          inst_sch_check = generate_load_insc_sch_check_attribute(target_hrs, oa, space_type, check_elems, min_pass, max_pass)
          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 avg. 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.each do |space|
      if use_old_gem_code
        next if space.plenum?
      else
        next if std.space_plenum?(space)
      end
      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
  # note: registerWarning and registerValue will be added for checks downstream using os_lib_reporting_qaqc.rb
end

#check_simultaneous_heating_and_cooling(category, max_pass, name_only = false) ⇒ Object

checks the number of unmet hours in the model



10
11
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
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/extension/core/check_simultaneous_heating_and_cooling.rb', line 10

def check_simultaneous_heating_and_cooling(category, max_pass, 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.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 * 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 * 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
  # note: registerWarning and registerValue will be added for checks downstream using os_lib_reporting_qaqc.rb
end

#check_supply_air_and_thermostat_temp_difference(category, target_standard, max_delta, name_only = false) ⇒ Object

checks the number of unmet hours in the model



10
11
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
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
# File 'lib/openstudio/extension/core/check_supply_air_and_thermostat_temp_difference.rb', line 10

def check_supply_air_and_thermostat_temp_difference(category, target_standard, max_delta, 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

  # Versions of OpenStudio greater than 2.4.0 use a modified version of
  # openstudio-standards with different method calls.  These methods
  # require a "Standard" object instead of the standard being passed into method calls.
  # This Standard object is used throughout the QAQC check.
  if OpenStudio::VersionString.new(OpenStudio.openStudioVersion) < OpenStudio::VersionString.new('2.4.3')
    use_old_gem_code = true
  else
    use_old_gem_code = false
    std = Standard.build(target_standard)
  end

  begin
    # loop through thermal zones
    @model.getThermalZones.sort.each do |thermal_zone|
      model_clg_min = nil

      # populate thermostat ranges
      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
            if use_old_gem_code
              schedule_values = clg_sch.to_ScheduleRuleset.get.annual_min_max_value
            else
              schedule_values = std.schedule_ruleset_annual_min_max_value(clg_sch.to_ScheduleRuleset.get)
            end
          elsif clg_sch.to_ScheduleConstant.is_initialized
            if use_old_gem_code
              schedule_values = clg_sch.to_ScheduleConstant.get.annual_min_max_value
            else
              schedule_values = std.schedule_constant_annual_min_max_value(clg_sch.to_ScheduleConstant.get)
            end
          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
  # note: registerWarning and registerValue will be added for checks downstream using os_lib_reporting_qaqc.rb
end

#check_weather_files(category, options, name_only = false) ⇒ Object

checks the number of unmet hours in the model



10
11
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
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
# File 'lib/openstudio/extension/core/check_weather_files.rb', line 10

def 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
  # note: registerWarning and registerValue will be added for checks downstream using os_lib_reporting_qaqc.rb
end

#generate_load_insc_sch_check_attribute(target_hrs, load_inst, space_type, check_elems, min_pass, max_pass) ⇒ Object

code for each load instance for different load types will pass through here will return nill or a single attribute



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
# File 'lib/openstudio/extension/core/check_schedules.rb', line 228

def generate_load_insc_sch_check_attribute(target_hrs, load_inst, space_type, check_elems, min_pass, max_pass)
  # Versions of OpenStudio greater than 2.4.0 use a modified version of
  # openstudio-standards with different method calls.  These methods
  # require a "Standard" object instead of the standard being passed into method calls.
  # This Standard object is used throughout the QAQC check.
  if OpenStudio::VersionString.new(OpenStudio.openStudioVersion) < OpenStudio::VersionString.new('2.4.3')
    use_old_gem_code = true
  else
    use_old_gem_code = false
    std = Standard.build('90.1-2013')
  end

  schedule_inst = nil
  inst_hrs = nil

  # get schedule
  if (load_inst.class.to_s == 'OpenStudio::Model::People') && load_inst.numberofPeopleSchedule.is_initialized
    schedule_inst = load_inst.numberofPeopleSchedule.get
  elsif (load_inst.class.to_s == 'OpenStudio::Model::DesignSpecificationOutdoorAir') && load_inst.outdoorAirFlowRateFractionSchedule.is_initialized
    schedule_inst = load_inst.outdoorAirFlowRateFractionSchedule .get
  elsif load_inst.schedule.is_initialized
    schedule_inst = load_inst.schedule.get
  else
    return OpenStudio::Attribute.new('flag', "#{load_inst.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
    if use_old_gem_code
      inst_hrs = schedule_inst.to_ScheduleRuleset.get.annual_equivalent_full_load_hrs
    else
      inst_hrs = std.schedule_ruleset_annual_equivalent_full_load_hrs(schedule_inst.to_ScheduleRuleset.get)
    end
  elsif schedule_inst.to_ScheduleConstant.is_initialized
    if use_old_gem_code
      inst_hrs = schedule_inst.to_ScheduleConstant.get.annual_equivalent_full_load_hrs
    else
      inst_hrs = std.schedule_constant_annual_equivalent_full_load_hrs(schedule_inst.to_ScheduleConstant.get)
    end
  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 < target_hrs * (1.0 - min_pass)
    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 * 100} (%) below the typical value of #{target_hrs.round} hours from the DOE Prototype building.")
  elsif inst_hrs > target_hrs * (1.0 + max_pass)
    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 * 100} (%) above the typical value of #{target_hrs.round} hours DOE Prototype building.")
  end

  # will get to this if no flag was thrown
  return false
end

#get_start_and_end_times(schedule_ruleset) ⇒ Object

Determine the hour when the schedule first exceeds the starting value and when it goes back down to the ending value at the end of the day. This method only works for ScheduleRuleset schedules.



10
11
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
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
# File 'lib/openstudio/extension/core/check_sch_coord.rb', line 10

def get_start_and_end_times(schedule_ruleset)
  # Ensure that this is a ScheduleRuleset
  schedule_ruleset = schedule_ruleset.to_ScheduleRuleset
  return [nil, nil] if schedule_ruleset.empty?
  schedule_ruleset = schedule_ruleset.get

  # Define the start and end date
  year_start_date = nil
  year_end_date = nil
  if schedule_ruleset.model.yearDescription.is_initialized
    year_description = schedule_ruleset.model.yearDescription.get
    year = year_description.assumedYear
    year_start_date = OpenStudio::Date.new(OpenStudio::MonthOfYear.new('January'), 1, year)
    year_end_date = OpenStudio::Date.new(OpenStudio::MonthOfYear.new('December'), 31, year)
  else
    year_start_date = OpenStudio::Date.new(OpenStudio::MonthOfYear.new('January'), 1, 2009)
    year_end_date = OpenStudio::Date.new(OpenStudio::MonthOfYear.new('December'), 31, 2009)
  end

  # Get the ordered list of all the day schedules that are used by this schedule ruleset
  day_schs = schedule_ruleset.getDaySchedules(year_start_date, year_end_date)

  # Get a 365-value array of which schedule is used on each day of the year,
  day_schs_used_each_day = schedule_ruleset.getActiveRuleIndices(year_start_date, year_end_date)

  # Create a map that shows how many days each schedule is used
  day_sch_freq = day_schs_used_each_day.group_by { |n| n }
  day_sch_freq = day_sch_freq.sort_by { |freq| freq[1].size }
  common_day_freq = day_sch_freq.last

  # Build a hash that maps schedule day index to schedule day
  schedule_index_to_day = {}
  day_schs.each_with_index do |day_sch, i|
    schedule_index_to_day[day_schs_used_each_day[i]] = day_sch
  end

  # Get the most common day schedule
  sch_index = common_day_freq[0]
  number_of_days_sch_used = common_day_freq[1].size

  # Get the day schedule at this index
  day_sch = if sch_index == -1 # If index = -1, this day uses the default day schedule (not a rule)
              schedule_ruleset.defaultDaySchedule
            else
              schedule_index_to_day[sch_index]
            end

  # Determine the full load hours for just one day
  values = []
  times = []
  day_sch.times.each_with_index do |time, i|
    times << day_sch.times[i]
    values << day_sch.values[i]
  end

  # Get the minimum value
  start_val = values.first
  end_val = values.last

  # Get the start time (first time value goes above minimum)
  start_time = nil
  values.each_with_index do |val, i|
    break if i == values.size - 1 # Stop if we reach end of array
    if val == start_val && values[i + 1] > start_val
      start_time = times[i + 1]
      break
    end
  end

  # Get the end time (first time value goes back down to minimum)
  end_time = nil
  values.each_with_index do |val, i|
    if i < values.size - 1
      if val > end_val && values[i + 1] == end_val
        end_time = times[i]
        break
      end
    else
      if val > end_val && values[0] == start_val # Check first hour of day for schedules that end at midnight
        end_time = OpenStudio::Time.new(0, 24, 0, 0)
        break
      end
    end
  end

  return [start_time, end_time]
end