Class: BTAPCosting

Inherits:
Object
  • Object
show all
Defined in:
lib/openstudio-standards/btap/costing/nv_costing.rb,
lib/openstudio-standards/btap/costing/dcv_costing.rb,
lib/openstudio-standards/btap/costing/shw_costing.rb,
lib/openstudio-standards/btap/costing/btap_costing.rb,
lib/openstudio-standards/btap/costing/envelope_costing.rb,
lib/openstudio-standards/btap/costing/lighting_costing.rb,
lib/openstudio-standards/btap/costing/pv_ground_costing.rb,
lib/openstudio-standards/btap/costing/ventilation_costing.rb,
lib/openstudio-standards/btap/costing/led_lighting_costing.rb,
lib/openstudio-standards/btap/costing/heating_cooling_costing.rb,
lib/openstudio-standards/btap/costing/daylighting_sensor_control_costing.rb

Instance Method Summary collapse

Constructor Details

#initialize(costs_csv: nil, factors_csv: nil) ⇒ BTAPCosting

May be initialized with custom databases:

costs_csv:   Path to custom costing
factors_csv: Path to custom localization factors


44
45
46
47
48
49
50
51
52
53
54
55
56
57
# File 'lib/openstudio-standards/btap/costing/btap_costing.rb', line 44

def initialize(costs_csv: nil, factors_csv: nil)
  @cp               = CommonPaths.instance
  @costing_database = CostingDatabase.instance

  # If the path for custom costing is defined, use custom costing.
  if (not costs_csv.nil?) and File.exist?(costs_csv)
    @cp.costs_path = costs_csv
  end

  # If the path for custom factors is defined, use custom factors.
  if (not factors_csv.nil?) and File.exist?(factors_csv)
    @cp.costs_local_factors_path = factors_csv
  end
end

Instance Method Details

#add_costed_item(material_id:, quantity:, material_mult: 1.0, labour_mult: 1.0, equip_mult: 1.0, tags: []) ⇒ Object

This adds costed items to the array of cousted items which end up in btap_itmes.json. Note that the array this method uses is created in the cost_audit_all method. The array is created with an initial element that contains the city and province whose localiazation factors are used for costing. The inputs are: id: (string) The costing database id for the item being costed. quantity: (float) The total amount of the item being costed in whatever units the item is costed in. This should

include all multiplier used to determine this cost (e.g. such as thermal_zone multipliers).  As
an example, if 32 ft. of wire were required for a piece of equipment used in a thermal zone with
a multiplier of 10, the quantity would be 3.2 (32 ft. * 10 / 100 since wire is costed per
100 ft.).

material_mult: (float) The multiplier used to estimate the cost of an item from the base cost. For example, high

efficiency SHW tanks are estimated to cost 30% higher than regular SHW tanks so the cost of these
tanks are calculated by the cost * 1.3.  Thus, the material_mult for high efficiency tanks
would be 1.3.  This is defauted to 1.0 if it is not provided.

labour_mult: (float) Similar to material_mult only applied to labour costs. The labour_mult can be different than

the material_mult.  This is defaulted to 1.0 if it is not provided.

equipment_mult: (float) Similar to material_mult and labour_mult only for equipment. This will always be 1.0 until

equipment costs are supported.

tags: (array of strings) This is an array which links the costed item to a component of the model that is being

costed.  For example, a material_id related to a boiler pump might have tags like ["boiler", "pump"].


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
# File 'lib/openstudio-standards/btap/costing/btap_costing.rb', line 309

def add_costed_item(material_id:, quantity:, material_mult: 1.0, labour_mult: 1.0, equip_mult: 1.0, tags: [])
  # Do some error handling for the tags argument
  tags_out = [tags] if tags.kind_of?(String)
  tags_out = tags if tags.kind_of?(Array)

  # Validate the type of the arguments.
  if (tags_out.kind_of?(Array) == false)
    raise("The tags for the item #{material_id} were not properly defined.  Please search for where the item is being added to the @cost_items hash via the add_costed_item method and correct the entry.")
  end

  if (material_id.kind_of?(String) == false)
    raise("The material_id for the item #{material_id} is not a string.  Please search for where the item is being added to the @cost_items hash via the add_costed_item method and correct the entry.")
  end

  if (quantity.kind_of?(Float) == false)
    raise("The quantity for the item #{material_id} is not a float.  Please search for where the item is being added to the @cost_items hash via the add_costed_item method and correct the entry.")
  end

  if (material_mult.kind_of?(Float) == false)
    raise("The material_mult for the item #{material_id} is not a float.  Please search for where the item is being added to the @cost_items hash via the add_costed_item method and correct the entry.")
  end

  if (labour_mult.kind_of?(Float) == false)
    raise("The labour_mult for the item #{material_id} is not a float.  Please search for where the item is being added to the @cost_items hash via the add_costed_item method and correct the entry.")
  end

  if (equip_mult.kind_of?(Float) == false)
    raise("The equip_mult for the item #{material_id} is not a float.  Please search for where the item is being added to the @cost_items hash via the add_costed_item method and correct the entry.")
  end

  # Add the costed item to the output output hash.
  @cost_items['Items'] << {
    'id' => material_id,
    'quantity' => quantity,
    'material_mult' => material_mult,
    'labour_mult' => labour_mult,
    'equipment_mult' => equip_mult,
    'tags' => tags_out
  }
end

#add_floor_sys(hvac_floors:, tz_floor_sys:) ⇒ Object



1772
1773
1774
1775
1776
1777
1778
1779
1780
1781
1782
1783
1784
1785
1786
1787
1788
1789
1790
1791
1792
1793
1794
1795
1796
1797
1798
1799
1800
1801
1802
1803
1804
1805
1806
1807
1808
# File 'lib/openstudio-standards/btap/costing/ventilation_costing.rb', line 1772

def add_floor_sys(hvac_floors:, tz_floor_sys:)
  if hvac_floors.empty?
    hvac_floors << {
        story_name: tz_floor_sys[:story_name],
        story: tz_floor_sys[:story],
        supply_air_m3ps: tz_floor_sys[:tz_floor_supp_air_m3ps],
        return_air_m3ps: tz_floor_sys[:tz_floor_ret_air_m3ps],
        tz_mult: tz_floor_sys[:tz_mult],
        tz_num: 1,
        floor_tz: [tz_floor_sys]
    }
  else
    found_story = false
    hvac_floors.each do |hvac_floor|
      if hvac_floor[:story_name].to_s.upcase == tz_floor_sys[:story_name].to_s.upcase
        hvac_floor[:supply_air_m3ps] += tz_floor_sys[:tz_floor_supp_air_m3ps]
        hvac_floor[:return_air_m3ps] += tz_floor_sys[:tz_floor_ret_air_m3ps]
        hvac_floor[:tz_mult] += tz_floor_sys[:tz_mult]
        hvac_floor[:tz_num] += 1
        hvac_floor[:floor_tz] << tz_floor_sys
        found_story = true
      end
    end
    if found_story == false
      hvac_floors << {
          story_name: tz_floor_sys[:story_name],
          story: tz_floor_sys[:story],
          supply_air_m3ps: tz_floor_sys[:tz_floor_supp_air_m3ps],
          return_air_m3ps: tz_floor_sys[:tz_floor_ret_air_m3ps],
          tz_mult: tz_floor_sys[:tz_mult],
          tz_num: 1,
          floor_tz: [tz_floor_sys]
      }
    end
  end
  return hvac_floors
end

#add_heat_cool_to_report(equipment_info:, heat_cool_cost:, al_eq_reporting_info:) ⇒ Object

This method collects air loop heating and cooling costing information into the al_eq_reporting_info hash. This hash will be included in the ventilation costing report. It collects air loops by system type.



2442
2443
2444
2445
2446
2447
2448
2449
2450
2451
2452
2453
2454
2455
2456
2457
2458
2459
2460
2461
2462
2463
2464
2465
2466
2467
2468
2469
2470
2471
2472
# File 'lib/openstudio-standards/btap/costing/ventilation_costing.rb', line 2442

def add_heat_cool_to_report(equipment_info:, heat_cool_cost:, al_eq_reporting_info:)
  # If there is no air loop heating or cooling equipment casting information add it to the hash.
  if al_eq_reporting_info.empty?
    al_eq_reporting_info << {
        eq_category: equipment_info[:obj_type][3..-1],
        heating_fuel: equipment_info[:heating_fuel],
        cooling_type: equipment_info[:cooling_type],
        total_modeled_capacity_kw: equipment_info[:mech_capacity_kw].round(3),
        cost: heat_cool_cost.round(2)
    }
  else
    # look for an air loop with the appropriate system type.
    ahu_heat_cool = al_eq_reporting_info.select {|aloop|
      aloop[:eq_category] == equipment_info[:obj_type][3..-1]
    }
    # If air loops with that system type are present add a new one.
    if ahu_heat_cool.empty?
      al_eq_reporting_info << {
          eq_category: equipment_info[:obj_type][3..-1],
          heating_fuel: equipment_info[:heating_fuel],
          cooling_type: equipment_info[:cooling_type],
          total_modeled_capacity_kw: equipment_info[:mech_capacity_kw].round(3),
          cost: heat_cool_cost.round(2)
      }
    else
      # If there is an air loop with the appropriate system type add the capacity and cost to the hash.
      ahu_heat_cool[0][:total_modeled_capacity_kw] += equipment_info[:mech_capacity_kw].round(3)
      ahu_heat_cool[0][:cost] += heat_cool_cost.round(2)
    end
  end
end

#add_tz_to_air_sys(air_system:, air_system_total:, air_system_totals:, floor_tz:) ⇒ Object



2287
2288
2289
2290
2291
2292
2293
2294
2295
2296
2297
2298
2299
2300
2301
2302
2303
2304
2305
2306
2307
2308
2309
2310
2311
2312
2313
2314
2315
2316
# File 'lib/openstudio-standards/btap/costing/ventilation_costing.rb', line 2287

def add_tz_to_air_sys(air_system:, air_system_total:, air_system_totals:, floor_tz:)
  if air_system_totals.empty?
    air_system_totals << {
        air_system: air_system[:air_sys],
        hrv_air_m3ps: air_system_total[:hrv_air_m3ps],
        dist_to_roof_m: air_system_total[:dist_to_roof_m],
        num_systems: air_system_total[:num_systems],
        hrv_info: air_system[:hrv_info],
        floor_tz: [floor_tz]
    }
  else
    curr_air_sys = air_system_totals.select {|air_sys| air_sys[:air_system] == air_system[:air_sys]}
    if curr_air_sys.empty?
      air_system_totals << {
          air_system: air_system[:air_sys],
          hrv_air_m3ps: air_system_total[:hrv_air_m3ps],
          dist_to_roof_m: air_system_total[:dist_to_roof_m],
          num_systems: air_system_total[:num_systems],
          hrv_info: air_system[:hrv_info],
          floor_tz: [floor_tz]
      }
    else
      curr_air_sys[0][:hrv_air_m3ps] += air_system_total[:hrv_air_m3ps]
      curr_air_sys[0][:dist_to_roof_m] = [curr_air_sys[0][:dist_to_roof_m], air_system_total[:dist_to_roof_m]].max
      curr_air_sys[0][:num_systems] += air_system_total[:num_systems]
      curr_air_sys[0][:floor_tz] << floor_tz
    end
  end
  return air_system_totals
end

#ahu_costing(model:, prototype_creator:, template_type:, mech_room:, roof_cent:, mech_sizing_info:, min_space:) ⇒ Object



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
# File 'lib/openstudio-standards/btap/costing/ventilation_costing.rb', line 25

def ahu_costing(model:, prototype_creator:, template_type:, mech_room:, roof_cent:, mech_sizing_info:, min_space:)
  ahu_cost = 0
  hrv_total_cost = 0
  heat_type = {
      'HP' => 0,
      'elec' => 0,
      'Gas' => 0,
      'HW' => 0,
  }
  cool_type = {
      'DX' => 0,
      'CHW' => 0,
  }

  rt_unit_num = 0
  total_vent_flow_m3_per_s = 0
  sys_1_4 = true
  hvac_floors = []
  # Go through each air loop in the model and cost it
  model.getAirLoopHVACs.sort.each do |airloop|
    @airloop_info = nil
    airloop_name = airloop.nameString
    # Look for the system type from the name of the air loop
    sys_name_loc = airloop_name.to_s.upcase.index("SYS_")
    if sys_name_loc.nil?
      puts "The name of airloop #{airloop_name} does not start with a valid NECB system type described as \"Sys_\" and then an NECB system number."
      puts "Please rename the airloop appropriately or do not cost the ventilation system until ventilation costing can handle non-NECB ventilation systems."
      next
    else
      sys_type = airloop_name[(sys_name_loc+4)].to_i
      sys_type_real = sys_type
      # For costing, treat system types 1 and 4 the same (treat both as system 1)
      sys_type = 1 if sys_type == 4
      next if sys_type == 2
    end
    ahu_tags = [
      "ventilation",
      airloop_name,
      "system #{sys_type_real}"
    ]
    rt_unit_num += 1

    @airloop_info = {sys_type: sys_type}
    @airloop_info[:name] = airloop_name

    # Get the air loop supply airflow rate (used for sizing the ahu for costing)
    if airloop.isDesignSupplyAirFlowRateAutosized
      airloop_flow_m3_per_s = airloop.autosizedDesignSupplyAirFlowRate.to_f
    else
      airloop_flow_m3_per_s = airloop.designSupplyAirFlowRate.to_f
    end
    airloop_flow_cfm = (OpenStudio.convert(airloop_flow_m3_per_s, 'm^3/s', 'cfm').get)
    airloop_flow_lps = (OpenStudio.convert(airloop_flow_m3_per_s, 'm^3/s', 'L/s').get)
    total_vent_flow_m3_per_s += airloop_flow_m3_per_s
    # Set up hash to record heating and cooling capacities.  If more than one heating or cooling source is present this will be used to determine which is predominant one since ahu costing is done based on one heating fuel and cooling type
    heat_cap = {
        'HP' => 0,
        'elec' => 0,
        'Gas' => 0,
        'HW' => 0,
        'CCASHP' => 0
    }
    cool_cap = {
        'DX' => 0,
        'CHW' => 0,
    }
    @airloop_info[:airloop_flow_m3_per_s] = airloop_flow_m3_per_s.round(3)
    total_heat_cool_cost = 0
    airloop_equipment = []
    #@airloop_info[:equipment_info] = []
    # Find HRVs in the air loop so they can be costed if present
    hrv_info = get_hrv_info(airloop: airloop, model: model)
    # Sort through all of the supply components in the air loop and collect heating and cooling equipment
    airloop.supplyComponents.sort.each do |supplycomp|
      # Get the OS object type of the supply component
      obj_type = supplycomp.iddObjectType.valueName.to_s
      mech_capacity = 0
      heating_fuel = 'none'
      cooling_type = 'none'
      adv_dx_clg_eqpt = false
      cat_search = nil
      # Based on the object type determine how to handle it.
      case obj_type
        # Determine what to do (if anything) with a piece of air loop heating/cooling equipment.  Note the comment for the first type applies to the rest.
      when /OS_Coil_Heating_DX_VariableSpeed/
        # Get the object and make sure it is cast correctly
        suppcomp = supplycomp.to_CoilHeatingDXVariableSpeed.get
        # Determine the size of the object if either autosized or manualy sized
        if suppcomp.isRatedHeatingCapacityAtSelectedNominalSpeedLevelAutosized
          mech_capacity = suppcomp.autosizedRatedHeatingCapacityAtSelectedNominalSpeedLevel.to_f/1000.0
        else
          mech_capacity = suppcomp.ratedHeatingCapacityAtSelectedNominalSpeedLevel.to_f/1000.0
        end
        # Determine from the name if it is a CCASHP
        if suppcomp.name.to_s.upcase.include?("CCASHP")
          # Set the heating equipment type (used to determine how to cost the equipment)
          heating_fuel = 'CCASHP'
          # Set the term used to search the 'hvac_costing' sheet in the costing spreadsheet to get costing information
          cat_search = 'coils'
          # Set the heating capacity (used to determine the predominant heating type for the air loop)
          heat_cap['CCASHP'] += mech_capacity
        else
          heating_fuel = 'HP'
          cat_search = 'ashp'
          heat_cap['HP'] += mech_capacity
        end
      when /OS_Coil_Heating_DX_SingleSpeed/
        suppcomp = supplycomp.to_CoilHeatingDXSingleSpeed.get
        if suppcomp.isRatedTotalHeatingCapacityAutosized
          mech_capacity = suppcomp.autosizedRatedTotalHeatingCapacity.to_f/1000.0
        else
          mech_capacity = suppcomp.ratedTotalHeatingCapacity.to_f/1000.0
        end
        if suppcomp.name.to_s.upcase.include?("CCASHP")
          heating_fuel = 'CCASHP'
          # There is a separate method which costs additional CCASHP cost information.  The 'coils' category is only
          # one of the pieces of equipment that goes into CCASHP costing.
          cat_search = 'coils'
          heat_cap['CCASHP'] += mech_capacity
        else
          heating_fuel = 'HP'
          cat_search = 'ashp'
          heat_cap['HP'] += mech_capacity
        end
      when 'OS_Coil_Heating_Electric'
        heating_fuel = 'elec'
        suppcomp = supplycomp.to_CoilHeatingElectric.get
        if suppcomp.isNominalCapacityAutosized
          mech_capacity = suppcomp.autosizedNominalCapacity.to_f/1000.0
        else
          mech_capacity = suppcomp.nominalCapacity.to_f/1000.0
        end
        cat_search = 'elecheat'
        heat_cap['elec'] += mech_capacity
      when /OS_Coil_Heating_Gas/
        heating_fuel = 'Gas'
        suppcomp = supplycomp.to_CoilHeatingGas.get
        if suppcomp.isNominalCapacityAutosized
          mech_capacity = suppcomp.autosizedNominalCapacity.to_f/1000.0
        else
          mech_capacity = suppcomp.nominalCapacity.to_f/1000.0
        end
        cat_search = 'FurnaceGas'
        heat_cap['Gas'] += mech_capacity
      when /OS_Coil_Heating_Water/
        heating_fuel = 'HW'
        suppcomp = supplycomp.to_CoilHeatingWater.get
        if suppcomp.isRatedCapacityAutosized
          mech_capacity = suppcomp.autosizedRatedCapacity.to_f/1000.0
        else
          suppcomp.ratedCapacity.to_f/1000.0
        end
        cat_search = 'coils'
        heat_cap['HW'] += mech_capacity
      when /OS_Coil_Cooling_DX_SingleSpeed/
        suppcomp = supplycomp.to_CoilCoolingDXSingleSpeed.get
        if suppcomp.isRatedTotalCoolingCapacityAutosized
          mech_capacity = suppcomp.autosizedRatedTotalCoolingCapacity.to_f/1000.0
        else
          mech_capacity = suppcomp.ratedTotalCoolingCapacity.to_f/1000.0
        end
        if suppcomp.name.to_s.upcase.include?('DX-ADV')
          cooling_type = 'DX-adv'
          cat_search = 'coils'
          cool_cap['DX'] += mech_capacity
        else
          cooling_type = 'DX'
          cat_search = 'coils'
          cool_cap['DX'] += mech_capacity
        end
      when /OS_Coil_Cooling_DX_VariableSpeed/
        suppcomp = supplycomp.to_CoilCoolingDXVariableSpeed.get
        if suppcomp.isGrossRatedTotalCoolingCapacityAtSelectedNominalSpeedLevelAutosized
          mech_capacity = suppcomp.autosizedGrossRatedTotalCoolingCapacityAtSelectedNominalSpeedLevel.to_f/1000.0
        else
          mech_capacity = suppcomp.grossRatedTotalCoolingCapacityAtSelectedNominalSpeedLevel.to_f/1000.0
        end
        if suppcomp.name.to_s.upcase.include?('DX-ADV')
          cooling_type = 'DX-adv'
          cat_search = 'coils'
          cool_cap['DX'] += mech_capacity
        else
          cooling_type = 'DX'
          cat_search = 'coils'
          cool_cap['DX'] += mech_capacity
        end
      when /Coil_Cooling_Water/
        cooling_type = 'CHW'
        suppcomp = supplycomp.to_CoilCoolingWater.get
        mech_capacity = suppcomp.autosizedDesignCoilLoad.to_f/1000.0
        cat_search = 'coils'
        cool_cap['CHW'] += mech_capacity
      when /OS_AirLoopHVAC_UnitaryHeatPump_AirToAir/
        suppcomp = supplycomp.to_AirLoopHVACUnitaryHeatPumpAirToAir.get
        htg_coil = suppcomp.heatingCoil
        if htg_coil.to_CoilHeatingDXSingleSpeed.is_initialized
          htg_coil = htg_coil.to_CoilHeatingDXSingleSpeed.get
          if htg_coil.isRatedTotalHeatingCapacityAutosized
            mech_capacity = htg_coil.autosizedRatedTotalHeatingCapacity.to_f/1000.0
          else
            mech_capacity = htg_coil.ratedTotalHeatingCapacity.to_f/1000.0
          end
          heating_fuel = 'HP'
          cat_search = 'ashp'
          heat_cap['HP'] += mech_capacity
        end
        clg_coil = suppcomp.coolingCoil
        if clg_coil.to_CoilCoolingDXSingleSpeed.is_initialized
          clg_coil = clg_coil.to_CoilCoolingDXSingleSpeed.get
          if clg_coil.isRatedTotalCoolingCapacityAutosized
            mech_capacity = clg_coil.autosizedRatedTotalCoolingCapacity.to_f/1000.0
          else
            mech_capacity = clg_coil.ratedTotalCoolingCapacity.to_f/1000.0
          end
          cooling_type = 'DX'
          cat_search = 'coils'
          cool_cap['DX'] += mech_capacity
        end
        supp_htg_coil = suppcomp.supplementalHeatingCoil
        if supp_htg_coil.to_CoilHeatingElectric.is_initialized
          supp_htg_coil = supp_htg_coil.to_CoilHeatingElectric.get
        elsif supp_htg_coil.to_CoilHeatingGas.is_initialized
          supp_htg_coil = supp_htg_coil.to_CoilHeatingGas.get
        end
        if supp_htg_coil.isNominalCapacityAutosized
          mech_capacity = supp_htg_coil.autosizedNominalCapacity.to_f/1000.0
        else
          mech_capacity = supp_htg_coil.nominalCapacity.to_f/1000.0
        end
        if supp_htg_coil.class.name.include? 'CoilHeatingElectric'
          cat_search = 'elecheat'
          heat_cap['elec'] += mech_capacity
        elsif supp_htg_coil.class.name.include? 'CoilHeatingGas'
          cat_search = 'FurnaceGas'
          heat_cap['Gas'] += mech_capacity
        end
      end
      # This hash contains all of the pertinent information required for costing a piece of air loop heating/cooling equipment
      equipment_info = {
        sys_type: sys_type,
        obj_type: obj_type,
        supply_comp: supplycomp,
        heating_fuel: heating_fuel,
        cooling_type: cooling_type,
        adv_dx_clg_eqpt: adv_dx_clg_eqpt,
        mech_capacity_kw: mech_capacity,
        cat_search: cat_search
      }
      unless equipment_info[:mech_capacity_kw].to_f <= 0
        # Add the piece of air loop equipment to an array for costing if the equipment does something (that is has a size larger than 0)
        airloop_equipment << equipment_info
      end
    end

    # Determine the predominant heating and cooling fuel type.
    ahu_heat_cool_info = determine_ahu_htg_clg_fuel(heat_cap: heat_cap, cool_cap: cool_cap, heat_type: heat_type, cool_type: cool_type)
    heat_type = ahu_heat_cool_info[:heat_type]
    cool_type = ahu_heat_cool_info[:cool_type]
    # Cost rooftop ventilation unit.
    costed_ahu_info = cost_ahu(sys_type: sys_type, airloop_flow_lps: airloop_flow_lps, airloop_flow_cfm: airloop_flow_cfm, mech_sizing_info: mech_sizing_info, heating_fuel: ahu_heat_cool_info[:heating_fuel], cooling_type: ahu_heat_cool_info[:cooling_type], airloop_name: airloop_name, vent_tags: ahu_tags)
    # Get ventilation heating and cooling equipment costs.
    air_loop_equip_return_info = airloop_equipment_costing(airloop_equipment: airloop_equipment, ahu_mult: costed_ahu_info[:mult].to_f, vent_tags: ahu_tags)
    # Get the air loop equipment reporting information from the air loop equipment costing method return hash
    al_eq_reporting_info = air_loop_equip_return_info[:al_eq_reporting_info]
    # Add the air loop equipment costing to the total air loop cost
    total_heat_cool_cost += air_loop_equip_return_info[:heat_cool_cost]

    # Determine information about thermal zones supplied by this air loop and sort it by building floor
    hvac_floors = gen_hvac_info_by_floor(hvac_floors: hvac_floors, model: model, prototype_creator: prototype_creator, airloop: airloop, sys_type: sys_type, hrv_info: hrv_info)
    sys_1_4 = false unless (sys_type == 1 || sys_type == 4)

    reheat_cost, reheat_array = reheat_recool_cost(airloop: airloop, prototype_creator: prototype_creator, model: model, roof_cent: roof_cent, mech_sizing_info: mech_sizing_info, vent_tags: ahu_tags, report_mult: 1.0)

    if hrv_info[:hrv_present]
      hrv_rep = hrv_cost(hrv_info: hrv_info, airloop: airloop, vent_tags: ahu_tags, report_mult: 1.0)
      hrv_total_cost += hrv_rep[:revised_hrv_cost].to_f
    else
      hrv_rep = {}
    end

    @airloop_info[:hrv] = hrv_rep
    ahu_cost +=  costed_ahu_info[:adjusted_base_ahu_cost] + reheat_cost + total_heat_cool_cost
    @airloop_info[:equipment_info] = al_eq_reporting_info
    @airloop_info[:reheat_recool] = reheat_array
    @costing_report['ventilation'].each {|key, value| value << @airloop_info if key.to_s == ('system_' + sys_type.to_s)}
  end
  if total_vent_flow_m3_per_s == 0 || total_vent_flow_m3_per_s.nil?
    puts "No ventilation system is present which can currently be costed."
    @costing_report['ventilation'] = {
        error: "No ventilation system is present which can currently be costed."
    }
    return 0
  end
  @costing_report['ventilation'][:hrv_total_cost] = hrv_total_cost.round(2)
  mech_roof_cost, mech_roof_rep = mech_to_roof_cost(heat_type: heat_type, cool_type: cool_type, mech_room: mech_room, roof_cent: roof_cent, rt_unit_num: rt_unit_num)
  @costing_report['ventilation'][:mech_to_roof] = mech_roof_rep
  trunk_duct_cost, trunk_duct_info = vent_trunk_duct_cost(tot_air_m3pers: total_vent_flow_m3_per_s, min_space: min_space, roof_cent: roof_cent, mech_sizing_info: mech_sizing_info, sys_1_4: sys_1_4)
  @costing_report['ventilation'][:trunk_duct] << trunk_duct_info
  floor_dist_cost, build_floor_trunk_info = floor_vent_dist_cost(hvac_floors: hvac_floors, prototype_creator: prototype_creator, roof_cent: roof_cent, mech_sizing_info: mech_sizing_info)
  @costing_report['ventilation'][:floor_trunk_ducts] << build_floor_trunk_info
  tz_dist_cost, duct_dist_rep = tz_vent_dist_cost(hvac_floors: hvac_floors, mech_sizing_info: mech_sizing_info)
  @costing_report['ventilation'][:tz_distribution] << duct_dist_rep
  hrv_ducting_cost, hrv_ret_duct_report = hrv_duct_cost(prototype_creator: prototype_creator, roof_cent: roof_cent, mech_sizing_info: mech_sizing_info, hvac_floors: hvac_floors)
  @costing_report['ventilation'][:hrv_return_ducting] = hrv_ret_duct_report
  ahu_cost += tz_dist_cost  + trunk_duct_cost + floor_dist_cost  + hrv_ducting_cost + hrv_total_cost + mech_roof_cost
  return ahu_cost.round(2)
end

#airloop_equipment_costing(airloop_equipment:, ahu_mult:, vent_tags: []) ⇒ Object

This method oversees the costing of heating and cooling equipment in an air loop. It takes in: airloop_equipment: A hash containing all heating and cooling supply equipment in the air loop The method retruns the airloop_equip_return_info hash which contains: al_eq_reporting_info: A hash containing information that will be included in the ventilation costing report heat_cool_cost: The total cost of heating and cooling equipment in the air loop



2479
2480
2481
2482
2483
2484
2485
2486
2487
2488
2489
2490
2491
2492
2493
2494
2495
2496
2497
2498
2499
2500
2501
2502
2503
2504
2505
2506
2507
2508
2509
2510
2511
2512
2513
2514
2515
2516
2517
2518
2519
2520
2521
2522
2523
2524
2525
2526
2527
2528
2529
2530
2531
2532
# File 'lib/openstudio-standards/btap/costing/ventilation_costing.rb', line 2479

def airloop_equipment_costing(airloop_equipment:, ahu_mult:, vent_tags: [])
  # Initialize return data
  ret_heat_cool_cost = 0
  al_eq_reporting_info = []
  ccashp_cost = 0
  vent_equip_tags = vent_tags.clone
  vent_equip_tags << "air loop equipment"

  # Look for a heat pump.  Heat pump air loop equipment costing is treated differently.
  heat_pumps = airloop_equipment.select{|airloop_eq| airloop_eq[:heating_fuel].to_s.include?('HP')}
  unless heat_pumps.empty?
    cool_eq = airloop_equipment.select{|airloop_eq| airloop_eq[:cooling_type].to_s.include?("DX")}
    unless cool_eq.empty?
      heat_pumps[0][:mech_capacity_kw] = cool_eq[0][:mech_capacity_kw].to_f if cool_eq[0][:mech_capacity_kw].to_f > heat_pumps[0][:mech_capacity_kw].to_f
      heat_pumps[0][:cooling_type] = heat_pumps[0][:heating_fuel]
      airloop_equipment.delete_if{|data| data[:cooling_type].to_s.include?("DX")}
    end
    if heat_pumps[0][:heating_fuel].to_s == "CCASHP"
      ccashp_cost = cost_ccashp_additional_components(ahu_mult: ahu_mult, heat_pump: heat_pumps[0], vent_tags: vent_equip_tags)
    end
    elec_eq = airloop_equipment.select{|airloop_eq| airloop_eq[:heating_fuel] == 'elec'}
    # If a backup electric heating coil is present look for a different item in the 'hvac_materials' costing sheet
    # than if the coil where part of an air loop without a heat pump.
    elec_eq.each do |el_eq|
      el_eq[:cat_search] = 'elecduct'
    end
    #airloop_equipment.select.with_index{|airloop_eq, index| airloop_eq[:cooling_type] == 'DX' || airloop_eq[:cooling_type] == 'CCASHP'}
  end

  # Cost all of the heating and cooling equipment in the air loop
  airloop_equipment.each do |airloop_eq|
    # Costing of air loop equipment should be done on a per air handler basis.  Thus, divide the total capacity of the
    # piece of air loop equipment by the number of air handlers required.
    total_modeled_capacity = airloop_eq[:mech_capacity_kw].to_f
    airloop_eq[:mech_capacity_kw] = total_modeled_capacity / ahu_mult
    # Get ventilation heating and cooling equipment costs.
    heat_cool_cost = cost_heat_cool_equip(equipment_info: airloop_eq, vent_tags: vent_equip_tags, report_mult: ahu_mult) * ahu_mult
    heat_cool_cost += ccashp_cost if airloop_eq[:heating_fuel].to_s == "CCASHP"
    # Add the equipment cost to the total air loop equipment cost
    ret_heat_cool_cost += heat_cool_cost
    # Only the total modeled capacity of the piece of air loop equipment should be reported to the user rather than
    # the capacity per air handler.
    airloop_eq[:mech_capacity_kw] = total_modeled_capacity
    # Add the air loop hetaing/cooling equipment information to the total air loop heating/cooling equipment report hash
    al_eq_reporting_info = add_heat_cool_to_report(equipment_info: airloop_eq, heat_cool_cost: heat_cool_cost, al_eq_reporting_info: al_eq_reporting_info)
  end

  # Create the return hash and return it.
  airloop_equip_return_info = {
    al_eq_reporting_info: al_eq_reporting_info,
    heat_cool_cost: ret_heat_cool_cost
  }
  return airloop_equip_return_info
end

#assembly_cost(cost_info:, sheet_name:, column_1:, column_2:, quantity:, tags:) ⇒ Object



626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
# File 'lib/openstudio-standards/btap/costing/pv_ground_costing.rb', line 626

def assembly_cost(cost_info:, sheet_name:, column_1:, column_2:, quantity:, tags:)
  #-------------------------------------------------------------------------------------------------------------------
  ### Step I: find mat_id
  mat_data = nil
  mat_data = @costing_database['raw'][sheet_name].select { |data|
    data[column_1].to_s.upcase == cost_info[:row_id_1].to_s.upcase and
        data[column_2].to_f.round(1) == cost_info[:row_id_2].to_f.round(1)
  }.first
  mat_id = mat_data['id']
  material_adjust = mat_data['material_mult']
  labour_adjust = mat_data['labour_mult']
  material_adjust = 1.0 if material_adjust.nil?
  labour_adjust = 1.0 if labour_adjust.nil?
  #-------------------------------------------------------------------------------------------------------------------
  ### Step II: calculate unit cost
  mat_cost_info = @costing_database['costs'].select { |data| data['id'] == mat_id.to_s.upcase }.first
  regional_material, regional_installation, regional_equipment = get_regional_cost_factors(@costing_report["province_state"], @costing_report["city"], mat_cost_info)
  # puts "regional_material, regional_installation, regional_equipment #{regional_material}, #{regional_installation}, #{regional_equipment}"

  if mat_cost_info['baseCosts']['materialOpCost'].nil?
    cost_material = 0.0
  else
    cost_material = mat_cost_info['baseCosts']['materialOpCost'] * (regional_material / 100.0) * material_adjust.to_f
  end
  if mat_cost_info['baseCosts']['laborOpCost'].nil?
    cost_labour = 0.0
  else
    cost_labour = mat_cost_info['baseCosts']['laborOpCost'] * (regional_installation / 100.0) * labour_adjust.to_f
  end
  if mat_cost_info['baseCosts']['equipmentOpCost'].nil?
    cost_equipment = 0.0
  else
    cost_equipment = mat_cost_info['baseCosts']['equipmentOpCost'] * (regional_equipment / 100.0)
  end

  cost_unit = cost_material + cost_labour + cost_equipment
  # puts "cost_unit is #{cost_unit}"
  #-------------------------------------------------------------------------------------------------------------------
  ### Step III: calculate total cost
  cost_total = cost_unit * quantity
  # puts "cost_total is #{cost_total}"
  #-------------------------------------------------------------------------------------------------------------------
  # Gather info for costed items output file
  unless mat_data['Material'].nil?
    tags << mat_data['Material']
  end
  unless mat_data['description'].nil?
    tags << mat_data['description']
  end
  if not tags.empty?
    add_costed_item(material_id: mat_id.to_s,
                    quantity: quantity,
                    material_mult: material_adjust.to_f,
                    labour_mult: labour_adjust.to_f,
                    equip_mult: 1.0,
                    tags: tags)
  end

  return cost_total
end

#boiler_costing(model, prototype_creator) ⇒ Object


This function gets all costs associated with boilers (i.e., boilers, pumps, flues, electrical lines and boxes, fuel lines and distribution piping to zonal heating units)




7
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
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
# File 'lib/openstudio-standards/btap/costing/heating_cooling_costing.rb', line 7

def boiler_costing(model, prototype_creator)

  #Global flag to determine if a GSHP is present
  @gshp_flag = false

  #Global flag to determine if a AWHP is present
  @awhp_flag = false

  totalCost = 0.0

  # Get regional cost factors for this province and city
  materials_hvac = @costing_database["raw"]["materials_hvac"]
  hvac_material = materials_hvac.select {|data|
    data['Material'].to_s == "GasBoilers"}.first  # Get any row from spreadsheet in case of region error
  regional_material, regional_installation =
      get_regional_cost_factors(@costing_report['province_state'], @costing_report['city'], hvac_material)

  # Get regional electric cost factors for this province and city
  hvac_material = materials_hvac.select {|data|
    data['Material'].to_s.upcase == "BOX" && data['Size'].to_i == 1}.first
  reg_mat_elec, reg_lab_elec =
    get_regional_cost_factors(@costing_report['province_state'], @costing_report['city'], hvac_material)

  # Store some geometry data for use below...
  util_dist, ht_roof, nom_flr_hght, horz_dist, numAGFlrs, mechRmInBsmt = getGeometryData(model, prototype_creator)

  template_type = prototype_creator.template

  plant_loop_info = {}
  plant_loop_info[:boilers] = []
  plant_loop_info[:boilerpumps] = []

  # Iterate through the plant loops to get boiler & pump data...
  model.getPlantLoops.each do |plant_loop|
    next unless (plant_loop.name.get.to_s.downcase == "hot water loop") || (plant_loop.name.get.to_s.downcase == "hw plantloop")
    plant_loop.supplyComponents.each do |supply_comp|
      if supply_comp.to_BoilerHotWater.is_initialized
        boiler = supply_comp.to_BoilerHotWater.get
        boiler_info = {}
        plant_loop_info[:boilers] << boiler_info
        boiler_info[:name] = boiler.name.get
        # 2020-09-01 CK Include efficiency for boiler upgrade costing
        boiler_info[:efficiency] = boiler.nominalThermalEfficiency.to_f
        if boiler.fuelType =~ /Electric/i
          boiler_info[:fueltype] = 'ElecBoilers'
        elsif boiler.fuelType =~ /NaturalGas/i
          boiler_info[:fueltype] = 'GasBoilers'
          #2020-09-01 CK Include modifications for condensing and pulse gas boilers
          if boiler_info[:efficiency] >= 0.827 && boiler_info[:efficiency] < 0.9
            boiler_info[:fueltype] = "CondensingBoilers"
          elsif boiler_info[:efficiency] >= 0.9
            boiler_info[:fueltype] = "PulseBoilers"
          end
        elsif boiler.fuelType =~ /Oil/i       # Oil, FuelOil, FuelOil#2
          boiler_info[:fueltype] = 'OilBoilers'
        end
        boiler_info[:nominal_capacity] = boiler.nominalCapacity.to_f / 1000 # kW
      elsif supply_comp.to_PumpConstantSpeed.is_initialized
        csPump = supply_comp.to_PumpConstantSpeed.get
        csPump_info = {}
        plant_loop_info[:boilerpumps] << csPump_info
        csPump_info[:name] = csPump.name.get
        if csPump.isRatedPowerConsumptionAutosized.to_bool
          csPumpSize = csPump.autosizedRatedPowerConsumption.to_f
        else
          csPumpSize = csPump.ratedPowerConsumption.to_f
        end
        csPump_info[:size] = csPumpSize.to_f # Watts
        if csPump.isRatedFlowRateAutosized.to_bool
          csPump_info[:water_flow_m3_per_s] = csPump.autosizedRatedFlowRate.to_f
        else
          csPump_info[:water_flow_m3_per_s] = csPump.ratedFlowRate.to_f
        end
      elsif supply_comp.to_PumpVariableSpeed.is_initialized
        vsPump = supply_comp.to_PumpVariableSpeed.get
        vsPump_info = {}
        plant_loop_info[:boilerpumps] << vsPump_info
        vsPump_info[:name] = vsPump.name.get
        if vsPump.isRatedPowerConsumptionAutosized.to_bool
          vsPumpSize = vsPump.autosizedRatedPowerConsumption.to_f
        else
          vsPumpSize = vsPump.ratedPowerConsumption.to_f
        end
        vsPump_info[:size] = vsPumpSize.to_f # Watts
        if vsPump.isRatedFlowRateAutosized.to_bool
          vsPump_info[:water_flow_m3_per_s] = vsPump.autosizedRatedFlowRate.to_f
        else
          vsPump_info[:water_flow_m3_per_s] = vsPump.ratedFlowRate.to_f
        end
      elsif supply_comp.to_HeatPumpWaterToWaterEquationFitHeating.is_initialized
        gshp = supply_comp.to_HeatPumpWaterToWaterEquationFitHeating.get
        gshp_info = {}
        gshp_info[:fueltype] = 'wshp'
        gshp_info[:name] = gshp.name.to_s
        if gshp.isRatedHeatingCapacityAutosized.to_bool
          gshp_info[:nominal_capacity] = gshp.autosizedRatedHeatingCapacity.to_f/1000.0
        else
          gshp_info[:nominal_capacity] = gshp.ratedHeatingCapacity.to_f/1000.0
        end
        plant_loop_info[:boilers] << gshp_info
        @gshp_flag = true
      elsif supply_comp.to_HeatPumpPlantLoopEIRHeating.is_initialized
        awhp = supply_comp.to_HeatPumpPlantLoopEIRHeating.get
        awhp_info = {}
        awhp_info[:fueltype] = 'Airtowaterhp'
        awhp_info[:name] = awhp.name.to_s
        if awhp.isReferenceCapacityAutosized.to_bool
          awhp_info[:nominal_capacity] = awhp.autosizedReferenceCapacity.to_f/1000.0
        else
          awhp_info[:nominal_capacity] = awhp.referenceCapacity.to_f/1000.0
        end
        plant_loop_info[:boilers] << awhp_info
        @awhp_flag = true
      end
    end
  end

  boilerCost = 0.0 ; thisBoilerCost = 0.0 ; flueCost = 0.0 ; utilCost = 0.0 ; fuelFittingCost = 0.0
  numBoilers = 0 ; multiplier = 1.0 ; primaryFuel = ''; primaryCap = 0 ; backupBoiler = false

  # Get costs associated with each boiler
  plant_loop_info[:boilers].each do |boiler|

    # Get primary/secondary/backup boiler cost based on fuel type and capacity for each boiler
    # 06-Sep-2019 JTB: Added check for no 'Primary' or 'Secondary' label and assume primary.
    #    This boiler prefix name seemed to disappear after the heat pump work was committed.
    numBoilers += 1
    if boiler[:name] =~ /primary/i || (boiler[:name] !~ /primary/i && boiler[:name] !~ /secondary/ && numBoilers == 1) || (boiler[:fuel_type] == 'wshp') || (boiler[:fuel_type] == 'Airtowaterhp')
      primaryFuel = boiler[:fueltype]
      primaryCap = boiler[:nominal_capacity]
      matCost, labCost = getHVACCost(boiler[:name], boiler[:fueltype], boiler[:nominal_capacity], false)

      # 2020-09-02 CK: Assume condensing oil boilers cost twice as much as non-condensing oil boilers
      if boiler[:fueltype] == 'OilBoilers' && boiler[:efficiency] >= 0.9
        thisBoilerCost = matCost * 2 * regional_material / 100.0 + labCost * regional_installation / 100.0
      else
        thisBoilerCost = matCost * regional_material / 100.0 + labCost * regional_installation / 100.0
      end

      # Flue and utility component costs (for gas and oil boilers only)
      # 2020-09-02 CK:  Include pulse and condensing gas boilers to those with utility component costs
      if boiler[:fueltype] == 'GasBoilers' || boiler[:fueltype] == 'OilBoilers' || boiler[:fueltype] == 'CondensingBoilers' || boiler[:fueltype] == 'PulseBoilers'
        # Calculate flue costs once for all boilers since flues combined by header when multiple boilers
        # 6 inch diameter flue (#384)
        materialHash = get_cost_info(mat: 'Venting', size: '6')
        matCost, labCost = getCost('flue', materialHash, multiplier)
        flueVentCost = matCost * regional_material / 100.0 + labCost * regional_installation / 100.0

        #6 inch elbow fitting (#386)
        materialHash = get_cost_info(mat: 'VentingElbow', size: '6')
        matCost, labCost = getCost('flue elbow', materialHash, multiplier)
        flueElbowCost = matCost * regional_material / 100.0 + labCost * regional_installation / 100.0

        # 6 inch top (#392)
        materialHash = get_cost_info(mat: 'VentingTop', size: '6')
        matCost, labCost = getCost('flue top', materialHash, multiplier)
        flueTopCost = matCost * regional_material / 100.0 + labCost * regional_installation / 100.0

        # Gas/Oil line piping cost per ft (#1)
        materialHash = get_cost_info(mat: 'GasLine', unit: 'L.F.')
        matCost, labCost = getCost('fuel line', materialHash, multiplier)
        fuelLineCost = matCost * regional_material / 100.0 + labCost * regional_installation / 100.0

        # Gas/Oil line fitting connection per boiler (#2)
        materialHash = get_cost_info(mat: 'GasLine', unit: 'each')
        matCost, labCost = getCost('fuel line fitting connection', materialHash, multiplier)
        fuelFittingCost = matCost * regional_material / 100.0 + labCost * regional_installation / 100.0

        # Header cost only non-zero if there is a secondary/backup gas/oil boiler
        headerCost = 0.0
      else  # Electric has no flue
        flueVentCost = 0.0 ; flueElbowCost = 0.0 ; flueTopCost = 0.0 ; headerCost = 0.0
      end

      # Electric utility cost components (i.e., power lines).
      # Calculate utility cost for primary boiler only since multiple boilers use common utilities

      # elec 600V #14 wire /100 ft (#848)
      materialHash = get_cost_info(mat: 'Wiring', size: 14)
      matCost, labCost = getCost('electrical wire - 600V #14', materialHash, multiplier)
      elecWireCost = matCost * reg_mat_elec / 100.0 + labCost * reg_lab_elec / 100.0

      # 1 inch metal conduit (#851)
      materialHash = get_cost_info(mat: 'Conduit', unit: 'L.F.')
      matCost, labCost = getCost('1 inch metal conduit', materialHash, multiplier)
      metalConduitCost = matCost * reg_mat_elec / 100.0 + labCost * reg_lab_elec / 100.0

      # 2020-09-02 CK Adding Condensing and Pulse boilers to those that need additional connections
      if boiler[:fueltype] == 'GasBoilers' || boiler[:fueltype] == 'CondensingBoilers' || boiler[:fueltype] == 'PulseBoilers'
        # Gas boilers require fuel line+valves+connectors and electrical conduit
        utilCost += (fuelLineCost + metalConduitCost) * util_dist + fuelFittingCost +
            elecWireCost * util_dist / 100

      elsif boiler[:fueltype] == 'OilBoilers'
        # Oil boilers require fuel line+valves+connectors and electrical conduit

        # Oil filtering system (#4)
        materialHash = get_cost_info(mat: 'OilLine', unit: 'each')
        matCost, labCost = getCost('Oil filtering system', materialHash, multiplier)
        oilFilterCost = matCost * regional_material / 100.0 + labCost * regional_installation / 100.0

        # 2000 USG above ground tank (#5)
        materialHash = get_cost_info(mat: 'OilTanks', size: 2000)
        matCost, labCost = getCost('Oil tank (2000 USG)', materialHash, multiplier)
        oilTankCost = matCost * regional_material / 100.0 + labCost * regional_installation / 100.0

        utilCost += (fuelLineCost + metalConduitCost) * util_dist + fuelFittingCost +
            elecWireCost * util_dist / 100 + oilFilterCost + oilTankCost

      elsif boiler[:fueltype].to_s.downcase == 'elecboilers' || boiler[:fueltype].to_s.downcase == 'wshp'
        # Electric boilers require only conduit
        utilCost += metalConduitCost * util_dist + elecWireCost * util_dist / 100
      elsif boiler[:fuel_type].to_s.downcase == 'airtowaterhp'
        # Add heating buffer tank for awhp
        materialHash = get_cost_info(mat: 'solartank', size: 450)
        matCost, labCost = getCost('solartank', materialHash, multiplier)
        utilCost = matCost * regional_material / 100.0 + labCost * regional_installation / 100.0
      end

    elsif boiler[:name] =~ /secondary/i || numBoilers > 1
      if boiler[:nominal_capacity] > 0.1
        # A secondary boiler exists so use it for costing
        matCost, labCost = getHVACCost(boiler[:name], boiler[:fueltype], boiler[:nominal_capacity], false)
        # 2020-09-02 CK: Assume condensing oil boilers cost twice as much as non-condensing oil boilers
        if boiler[:fueltype] == 'OilBoilers' && boiler[:efficiency] >= 0.9
          thisBoilerCost = matCost * 2 * regional_material / 100.0 + labCost * regional_installation / 100.0
        else
          thisBoilerCost = matCost * regional_material / 100.0 + labCost * regional_installation / 100.0
        end
      else
        # Use existing value of thisBoilerCost to represent a backup boiler!
        # This just doubles the cost of the primary boiler.
        # 2023-04-25:  Leaving backup boiler in energy model but no longer costing.
        backupBoiler = false
        thisBoilerCost = 0.0
        numBoilers -= 1
      end

      # Flue costs set to zero if secondary boiler since already calculated in primary
      flueVentCost = 0.0; flueElbowCost = 0.0; flueTopCost = 0.0

      # Check if need a flue header (i.e., there are both primary and secondary/backup boilers)
      if thisBoilerCost > 0.0 && ( (backupBoiler && primaryFuel != 'ElecBoilers') || (boiler[:fueltype] != 'ElecBoilers') || (boiler[:fueltype] != 'wshp') || (boiler[:fueltype] != 'Airtowaterhp'))
        # 6 inch diameter header (#384)
        materialHash = get_cost_info(mat: 'Venting', size: 6)
        matCost, labCost = getCost('flue header', materialHash, multiplier)
        headerVentCost = matCost * regional_material / 100.0 + labCost * regional_installation / 100.0

        #6 inch elbow fitting for header (#386)
        materialHash = get_cost_info(mat: 'VentingElbow', size: 6)
        matCost, labCost = getCost('flue header elbow', materialHash, multiplier)
        headerElbowCost = matCost * regional_material / 100.0 + labCost * regional_installation / 100.0

        # Assume a header length of 20 ft and an elbow fitting for each boiler connected to the header
        headerCost = (headerVentCost * 20  + headerElbowCost) * numBoilers
      else
        headerCost = 0.0
      end
    end
    boilerCost += thisBoilerCost
    flueCost += flueVentCost * ht_roof + flueElbowCost + flueTopCost + headerCost
    if numBoilers > 1
      # Adjust utility cost for extra fuel line fitting cost
      utilCost += fuelFittingCost * (numBoilers - 1)
    end
  end

  # Boiler pump costs
  pumpCost = 0.0; pipingToPumpCost = 0.0; numPumps = 0; pumpName = ''; pumpSize = 0.0 ; pumpFlow = 0.0
  plant_loop_info[:boilerpumps].each do |pump|
    numPumps += 1
    # Cost variable and constant volume pumps the same (the difference is in extra cost for VFD controller)
    pumpSize = pump[:size]; pumpName = pump[:name]
    pumpFlow += pump[:water_flow_m3_per_s].to_f
    matCost, labCost = getHVACCost(pumpName, 'Pumps', pumpSize, false)
    pumpCost += matCost * regional_material / 100.0 + labCost * regional_installation / 100.0
    if pump[:name] =~ /variable/i
      # Cost the VFD controller for the variable pump
      matCost, labCost = getHVACCost(pumpName, 'VFD', pumpSize, false)
      pumpCost += matCost * reg_mat_elec / 100.0 + labCost * reg_lab_elec / 100.0
    end
  end
  if numBoilers > 1 && numPumps < 2
    # Add pump costing for the backup boiler pump.
    pumpCost *= 2.0
    numPumps = 2  # reset the number of pumps for piping costs below
  end
  # Double the pump costs to accomodate the costing of a backup pumps for each boiler!
  # No longer costing backup pumps.
  # pumpCost *= 2.0

  # Boiler water piping to pumps cost: Add piping elbows, valves and insulation from the boiler(s)
  # to the pumps(s) assuming a pipe diameter of 1” and a distance of 10 ft per pump

  if numBoilers > 0
    # 1 inch Steel pipe
    matCost, labCost = getHVACCost('1 inch steel pipe', 'SteelPipe', 1)
    pipingToPumpCost += 10.0 * numPumps * (matCost * regional_material / 100.0 + labCost * regional_installation / 100.0)

    # 1 inch Steel pipe insulation
    matCost, labCost = getHVACCost('1 inch pipe insulation', 'PipeInsulation', 1)
    pipingToPumpCost += 10.0 * numPumps * (matCost * regional_material / 100.0 + labCost * regional_installation / 100.0)

    # 1 inch Steel pipe elbow
    matCost, labCost = getHVACCost('1 inch steel pipe elbow', 'SteelPipeElbow', 1)
    pipingToPumpCost += 2.0 * numPumps * (matCost * regional_material / 100.0 + labCost * regional_installation / 100.0)

    # 1 inch gate valves
    matCost, labCost = getHVACCost('1 inch gate valves', 'ValvesGate', 1)
    pipingToPumpCost += 1.0 * numPumps * (matCost * regional_material / 100.0 + labCost * regional_installation / 100.0)
  end

  if numBoilers > 0
    # Double pump piping cost to account for second boiler
    pipingToPumpCost *= numBoilers

    hdrDistributionCost = getHeaderPipingDistributionCost(numAGFlrs, mechRmInBsmt, regional_material, regional_installation, reg_mat_elec, reg_lab_elec,
                                                          pumpFlow, horz_dist, nom_flr_hght)
  else
    pipingToPumpCost = 0
    hdrDistributionCost = 0
  end

  totalCost = boilerCost + flueCost + utilCost + pumpCost + pipingToPumpCost + hdrDistributionCost

      @costing_report['heating_and_cooling']['plant_equipment']  << {
      'type' => 'boilers',
      'nom_flr2flr_hght_ft' => nom_flr_hght.round(1),
      'ht_roof_ft' => ht_roof.round(1),
      'longest_distance_to_ext_ft' => horz_dist.round(1),
      'wiring_and_gas_connections_distance_ft' => util_dist.round(1),
      'equipment_cost' => boilerCost.round(0),
      'flue_cost' => flueCost.round(0),
      'wiring_and_gas_connections_cost' => utilCost.round(0),
      'pump_cost' => pumpCost.round(0),
      'piping_to_pump_cost' => pipingToPumpCost.round(0),
      'header_distribution_cost' => hdrDistributionCost.round(0),
      'total_cost' => totalCost.round(0)
  }
  puts "\nHVAC Boiler costing data successfully generated. Total boiler costs: $#{totalCost.round(0)}"

  return totalCost
end

#chiller_costing(model, prototype_creator) ⇒ Object


Chiller costing is similar to boiler costing above




354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
# File 'lib/openstudio-standards/btap/costing/heating_cooling_costing.rb', line 354

def chiller_costing(model, prototype_creator)

  totalCost = 0.0

  # Get regional cost factors for this province and city
  materials_hvac = @costing_database["raw"]["materials_hvac"]
  hvac_material = materials_hvac.select {|data|
    data['Material'].to_s == "GasBoilers"}.first  # Get any row from spreadsheet in case of region error
  regional_material, regional_installation =
      get_regional_cost_factors(@costing_report['province_state'], @costing_report['city'], hvac_material)

  # Get regional electric cost factors for this province and city
  hvac_material = materials_hvac.select {|data|
    data['Material'].to_s.upcase == "BOX" && data['Size'].to_i == 1}.first
  reg_mat_elec, reg_lab_elec =
    get_regional_cost_factors(@costing_report['province_state'], @costing_report['city'], hvac_material)

  # Store some geometry data for use below...
  util_dist, ht_roof, nom_flr_hght, horz_dist, numAGFlrs, mechRmInBsmt = getGeometryData(model, prototype_creator)

  template_type = prototype_creator.template

  chillerCost = 0.0 ; thisChillerCost = 0.0 ; flueCost = 0.0 ; utilCost = 0.0
  plant_loop_info = {}
  plant_loop_info[:chillers] = []
  plant_loop_info[:chillerpumps] = []
  awhp_chiller = false

  # Iterate through the plant loops to get chiller & pump data...
  model.getPlantLoops.each do |plant_loop|
    next unless (plant_loop.name.get.to_s.downcase == "chilled water loop") || (plant_loop.name.get.to_s.downcase == "chw plantloop")
    plant_loop.supplyComponents.each do |supply_comp|
      if supply_comp.to_ChillerElectricEIR.is_initialized #|| supply_comp.to_ChillerGasEIR.is_initialized
        chiller = supply_comp.to_ChillerElectricEIR.get
        chiller_info = {}
        plant_loop_info[:chillers] << chiller_info
        chiller_info[:name] = chiller.name.get
        if chiller_info[:name] =~ /WaterCooled/i
          if chiller_info[:name] =~ /Absorption/i
            chiller_info[:type] = 'HotAbsChiller'
            chiller_info[:fuel] = 'NaturalGas'
          elsif chiller_info[:name] =~ /Direct Gas/i
            chiller_info[:type] = 'GasAbsChiller'
            chiller_info[:fuel] = 'NaturalGas'
          elsif chiller_info[:name] =~ /Centrifugal/i
            chiller_info[:type] = 'CentChillerWater'
            chiller_info[:fuel] = 'Electric'
          elsif chiller_info[:name] =~ /Reciprocating/i
            chiller_info[:type] = 'RecChillerWater'
            chiller_info[:fuel] = 'Electric'
          elsif chiller_info[:name] =~ /Scroll/i
            chiller_info[:type] = 'ScrollChillerWater'
            chiller_info[:fuel] = 'Electric'
          elsif chiller_info[:name] =~ /Screw/i
            chiller_info[:type] = 'ScrewChillerWater'
            chiller_info[:fuel] = 'Electric'
          end
        elsif chiller_info[:name] =~ /AirCooled/i
          if chiller_info[:name] =~ /Reciprocating/i
            chiller_info[:type] = 'RecChillerAir'
            chiller_info[:fuel] = 'Electric'
          elsif chiller_info[:name] =~ /Scroll/i
            chiller_info[:type] = 'ScrollChillerAir'
            chiller_info[:fuel] = 'Electric'
          elsif chiller_info[:name] =~ /Screw/i
            chiller_info[:type] = 'ScrewChillerAir'
            chiller_info[:fuel] = 'Electric'
          elsif chiller_info[:name] =~ /DX/i
            chiller_info[:type] = 'DXChiller'
            chiller_info[:fuel] = 'Electric'
          end
        end
        chiller_info[:reference_capacity] = chiller.referenceCapacity.to_f / 1000 # kW
      elsif supply_comp.to_HeatPumpPlantLoopEIRCooling.is_initialized
        chiller = supply_comp.to_HeatPumpPlantLoopEIRCooling.get
        chiller_info = {}
        chiller_info[:name] = chiller.name.get
        chiller_info[:type] = 'Airtowaterhp'
        chiller_info[:fuel] = 'Electric'
        if chiller.isReferenceCapacityAutosized
          chiller_info[:reference_capacity] = chiller.autosizedReferenceCapacity.to_f / 1000 # kW
        else
          chiller_info[:reference_capacity] = chiller.referenceCapacity.to_f / 1000 # kW
        end
        awhp_chiller = true
        plant_loop_info[:chillers] << chiller_info
      elsif supply_comp.to_PumpConstantSpeed.is_initialized
        csPump = supply_comp.to_PumpConstantSpeed.get
        csPump_info = {}
        plant_loop_info[:chillerpumps] << csPump_info
        csPump_info[:name] = csPump.name.get
        if csPump.isRatedPowerConsumptionAutosized.to_bool
          csPumpSize = csPump.autosizedRatedPowerConsumption.to_f
        else
          csPumpSize = csPump.ratedPowerConsumption.to_f
        end
        csPump_info[:size] = csPumpSize.to_f # Watts
        if csPump.isRatedFlowRateAutosized.to_bool
          csPump_info[:water_flow_m3_per_s] = csPump.autosizedRatedFlowRate.to_f
        else
          csPump_info[:water_flow_m3_per_s] = csPump.ratedFlowRate.to_f
        end
      elsif supply_comp.to_PumpVariableSpeed.is_initialized
        vsPump = supply_comp.to_PumpVariableSpeed.get
        vsPump_info = {}
        plant_loop_info[:chillerpumps] << vsPump_info
        vsPump_info[:name] = vsPump.name.get
        if vsPump.isRatedPowerConsumptionAutosized.to_bool
          vsPumpSize = vsPump.autosizedRatedPowerConsumption.to_f
        else
          vsPumpSize = vsPump.ratedPowerConsumption.to_f
        end
        vsPump_info[:size] = vsPumpSize.to_f # Watts
        if vsPump.isRatedFlowRateAutosized.to_bool
          vsPump_info[:water_flow_m3_per_s] = vsPump.autosizedRatedFlowRate.to_f
        else
          vsPump_info[:water_flow_m3_per_s] = vsPump.ratedFlowRate.to_f
        end
      end
    end
  end

  # Get costs associated with each chiller
  numChillers = 0 ; multiplier = 1.0
  primaryFuel = ''; primaryCap = 0

  plant_loop_info[:chillers].each do |chiller|

    # Get primary/secondary/backup chiller cost based on type and capacity for each chiller
    # 06-Sep-2019 JTB: Added check for no 'Primary' or 'Secondary' label and assume primary.
    #    This chiller prefix name seemed to disappear after the heat pump work was committed.
    numChillers += 1
    if chiller[:type].to_s.downcase == 'airtowaterhp'
      primaryFuel = chiller[:fuel]
      primaryCap = chiller[:reference_capacity] #kW
      # Add cooling buffer tank for awhp
      materialHash = get_cost_info(mat: 'solartank', size: 450)
      matCost, labCost = getCost('solartank', materialHash, multiplier) #Costing for AWHP only buffer tank, AWHP included in boiler cost
      thisChillerCost = matCost * regional_material / 100.0 + labCost * regional_installation / 100.0
      # Include 2 expansion tanks for awhp
      materialHash = get_cost_info(mat: 'ExpansionTanks', size: 60)
      matCost, labCost = getCost('ExpansionTanks', materialHash, multiplier) #Costing for AWHP only buffer tank, AWHP included in boiler cost
      thisChillerCost += (matCost * regional_material / 100.0 + labCost * regional_installation / 100.0) * 2
      # Inclide glycol cost
      materalHash = get_cost_info(mat: 'glycol')
      matCost, labCost = getCost('solartank', materialHash, multiplier) #Costing for AWHP only buffer tank, AWHP included in boiler cost
      thisChillerCost += (matCost * regional_material / 100.0 + labCost * regional_installation / 100.0) * 2

      flueVentCost = 0.0 ; flueElbowCost = 0.0 ; flueTopCost = 0.0 ; headerCost = 0.0
    elsif ((chiller[:name].to_s.downcase =~ /primary/i || (chiller[:name] !~ /primary/i && chiller[:name] !~ /secondary/i && numChillers == 1)) || (@gshp_flag))
      primaryFuel = chiller[:fuel]
      primaryCap = chiller[:reference_capacity] #kW
      if not chiller[:name].include?("ChillerElectricEIR_VSDCentrifugalWaterChiller")
        matCost, labCost = getHVACCost(chiller[:name], chiller[:type], chiller[:reference_capacity], false)
        thisChillerCost = matCost * regional_material / 100.0 + labCost * regional_installation / 100.0
      elsif chiller[:name].include?("ChillerElectricEIR_VSDCentrifugalWaterChiller")
        thisChillerCost = vsd_chiller_cost(primaryCap: primaryCap)
      end
      # Flue cost for gas (absorption) chillers!
      if chiller[:fuel] == 'NaturalGas'
        # Calculate flue costs once for all chillets since flues combined by header when multiple chillers
        # 6 inch diameter flue (#384)
        materialHash = get_cost_info(mat: 'Venting', size: 6)
        matCost, labCost = getCost('flue', materialHash, multiplier)
        flueVentCost = matCost * regional_material / 100.0 + labCost * regional_installation / 100.0

        #6 inch elbow fitting (#386)
        materialHash = get_cost_info(mat: 'VentingElbow', size: 6)
        matCost, labCost = getCost('flue elbow', materialHash, multiplier)
        flueElbowCost = matCost * regional_material / 100.0 + labCost * regional_installation / 100.0

        # 6 inch top (#392)
        materialHash = get_cost_info(mat: 'VentingTop', size: 6)
        matCost, labCost = getCost('flue top', materialHash, multiplier)
        flueTopCost = matCost * regional_material / 100.0 + labCost * regional_installation / 100.0

        # Gas line piping cost per ft (#1)
        materialHash = get_cost_info(mat: 'GasLine', unit: 'L.F.')
        matCost, labCost = getCost('fuel line', materialHash, multiplier)
        fuelLineCost = matCost * regional_material / 100.0 + labCost * regional_installation / 100.0

        # Gas line fitting connection per boiler (#2)
        materialHash = get_cost_info(mat: 'GasLine', unit: 'each')
        matCost, labCost = getCost('fuel line fitting connection', materialHash, multiplier)
        fuelFittingCost = matCost * regional_material / 100.0 + labCost * regional_installation / 100.0


        # Header cost only non-zero if there is a secondary/backup gas/oil boiler
        headerCost = 0.0
      else  # Electric
        flueVentCost = 0.0 ; flueElbowCost = 0.0 ; flueTopCost = 0.0 ; headerCost = 0.0
      end

      # Electric utility costs (i.e., power lines).
      # Calculate utility cost components for primary chiller only since multiple chillers use common utilities

      # elec 600V #14 wire /100 ft (#848)
      materialHash = get_cost_info(mat: 'Wiring', size: 14)
      matCost, labCost = getCost('electrical wire - 600V #14', materialHash, multiplier)
      elecWireCost = matCost * reg_mat_elec / 100.0 + labCost * reg_lab_elec / 100.0

      # 1 inch metal conduit (#851)
      materialHash = get_cost_info(mat: 'Conduit', unit: 'L.F.')
      matCost, labCost = getCost('1 inch metal conduit', materialHash, multiplier)
      metalConduitCost = matCost * reg_mat_elec / 100.0 + labCost * reg_lab_elec / 100.0

      if chiller[:fuel] == 'NaturalGas'
        # Gas chillers require fuel line+valves+connectors and electrical conduit
        utilCost += (fuelLineCost + metalConduitCost) * util_dist + fuelFittingCost + elecWireCost * util_dist / 100

      else # Electric
        # Electric chillers require only conduit
        utilCost += metalConduitCost * util_dist + elecWireCost * util_dist / 100
      end

    elsif (chiller[:name].to_s.downcase =~ /secondary/i || numChillers > 1)
      if chiller[:reference_capacity] <= 0.1
        # Chiller cost is zero!
        thisChillerCost = 0.0
        numChillers -= 1
      else
        # A secondary chiller exists so use it for costing
        if not chiller[:name].include?("ChillerElectricEIR_VSDCentrifugalWaterChiller")
          matCost, labCost = getHVACCost(chiller[:name], chiller[:type], chiller[:reference_capacity], false)
          thisChillerCost = matCost * regional_material / 100.0 + labCost * regional_installation / 100.0
        elsif chiller[:name].include?("ChillerElectricEIR_VSDCentrifugalWaterChiller")
          thisChillerCost = vsd_chiller_cost(primaryCap: primaryCap)
        end
      end

      # Flue costs set to zero if secondary chiler since already calculated in primary (if gas absorption)
      flueVentCost = 0.0; flueElbowCost = 0.0; flueTopCost = 0.0

      # Check if need a flue header (i.e., both primary and secondary chillers are gas absorption)
      if thisChillerCost > 0.0 && primaryFuel == 'NaturalGas' && chiller[:fuel] == 'NaturalGas'
        # 6 inch diameter header (#384)
        materialHash = get_cost_info(mat: 'Venting', size: 6)
        matCost, labCost = getCost('flue header', materialHash, multiplier)
        headerVentCost = matCost * regional_material / 100.0 + labCost * regional_installation / 100.0

        #6 inch elbow fitting for header (#386)
        materialHash = get_cost_info(mat: 'VentingElbow', size: 6)
        matCost, labCost = getCost('flue header elbow', materialHash, multiplier)
        headerElbowCost = matCost * regional_material / 100.0 + labCost * regional_installation / 100.0

        # Assume a header length of 20 ft and an elbow fitting for each boiler connected to the header
        headerCost = (headerVentCost * 20 + headerElbowCost) * numChillers
      else
        headerCost = 0.0
      end
    end
    chillerCost += thisChillerCost
    flueCost += flueVentCost * ht_roof + flueElbowCost + flueTopCost + headerCost
    if numChillers > 1 && primaryFuel == 'NaturalGas'
      # Adjust utility cost for extra fuel line fitting cost
      utilCost += fuelFittingCost * (numChillers - 1)
    end
    if numChillers < 2
      # Create a cost for a backup chiller by doubling cost of primary chiller
      # 2023-04-25:  Although backup chillers may be modeled we are no longer counting them.
      #chillerCost *= 2.0
      numChillers = 1
    end
  end

  # Chiller pump costs
  pumpCost = 0.0; pipingToPumpCost = 0.0; numPumps = 0; pumpName = ''; pumpSize = 0.0 ; pumpFlow = 0.0
  plant_loop_info[:chillerpumps].each do |pump|
    numPumps += 1
    # Cost variable and constant volume pumps the same (the difference is in extra cost for VFD controller)
    pumpSize = pump[:size]; pumpName = pump[:name]
    pumpFlow += pump[:water_flow_m3_per_s].to_f
    matCost, labCost = getHVACCost(pumpName, 'Pumps', pumpSize, false)
    indpumpCost = matCost * regional_material / 100.0 + labCost * regional_installation / 100.0
    if pump[:name] =~ /variable/i
      # Cost the VFD controller for the variable pump costed above
      matCost, labCost = getHVACCost(pumpName, 'VFD', pumpSize, false)
      indpumpCost += matCost * reg_mat_elec / 100.0 + labCost * reg_lab_elec / 100.0
    end
    if awhp_chiller
      pumpCost += indpumpCost * 2
    else
      pumpCost += indpumpCost
    end
  end
  if (numChillers > 1 && numPumps < 2)
    # Add pump costing for additional chillers
    pumpCost *= 2.0
    numPumps = 2  # reset the number of pumps for piping costs below
    numChillers = 2
  end
  # Double the pump costs to accomodate the costing of backup pumps for each chiller!
  # No longer costing backup pump CK 2023-06-23
  #pumpCost *= 2.0

  # Chiller water piping cost: Add piping elbows, valves and insulation from the chiller(s)
  # to the pumps(s) assuming a pipe diameter of 1” and a distance of 10 ft per pump
  if numChillers > 0
    # 1 inch Steel pipe
    matCost, labCost = getHVACCost('1 inch steel pipe', 'SteelPipe', 1)
    pipingToPumpCost = 10.0 * numPumps * (matCost * regional_material / 100.0 + labCost * regional_installation / 100.0)

    # 1 inch Steel pipe insulation
    matCost, labCost = getHVACCost('1 inch pipe insulation', 'PipeInsulation', 1)
    pipingToPumpCost += 10.0 * numPumps * (matCost * regional_material / 100.0 + labCost * regional_installation / 100.0)

    # 1 inch Steel pipe elbow
    matCost, labCost = getHVACCost('1 inch steel pipe elbow', 'SteelPipeElbow', 1)
    pipingToPumpCost += 2.0 * numPumps * (matCost * regional_material / 100.0 + labCost * regional_installation / 100.0)

    # 1 inch gate valves
    matCost, labCost = getHVACCost('1 inch gate valves', 'ValvesGate', 1)
    pipingToPumpCost += 1.0 * numPumps * (matCost * regional_material / 100.0 + labCost * regional_installation / 100.0)
  end

  if numChillers > 0
    # Double pump piping cost to account for second chiller
    pipingToPumpCost *= 2 if awhp_chiller
    pipingToPumpCost *= numChillers

    hdrDistributionCost = getHeaderPipingDistributionCost(numAGFlrs, mechRmInBsmt, regional_material, regional_installation, reg_mat_elec, reg_lab_elec,
                                                          pumpFlow, horz_dist, nom_flr_hght)
  else
    pipingToPumpCost = 0
    hdrDistributionCost = 0
  end

  totalCost = chillerCost + flueCost + utilCost + pumpCost + pipingToPumpCost + hdrDistributionCost

  @costing_report['heating_and_cooling']['plant_equipment']  << {
      'type' => 'chillers',
      'nom_flr2flr_hght_ft' => nom_flr_hght.round(1),
      'ht_roof_ft' => ht_roof.round(1),
      'longest_distance_to_ext_ft' => horz_dist.round(1),
      'wiring_and_gas_connections_distance_ft' => util_dist.round(1),
      'equipment_cost' => chillerCost.round(0),
      'flue_cost' => flueCost.round(0),
      'wiring_and_gas_connections_cost' => utilCost.round(0),
      'pump_cost' => pumpCost.round(0),
      'piping_to_pump_cost' => pipingToPumpCost.round(0),
      'header_distribution_cost' => hdrDistributionCost.round(0),
      'total_cost' => totalCost.round(0)
  }

  puts "\nHVAC Chiller costing data successfully generated. Total chiller costs: $#{totalCost.round(0)}"

  return totalCost
end

#compileZonalVRFFloors(vrfSystemFloors:, tzFloor:) ⇒ Object

This method takes information about a thermal zone served by a VRF system (tzFloor) and adds it to the collection of thermal zones also served by VRF systems on the same floor. The ultimate output (once all thermal zones are read) is an array of hashes. Each entry in the array represents a floor of the building. Each floor entry contains information about thermal zones served by VRF systems on that floor.

The information on the thermal zone served by a VRF system on a given floor is contained in tzFloor. The overall collection of thermal zone information by floors is contained in vrfSystemFloors. This method modifies vrfSystemFloors which is why that is an input and output for the method. Both tzFloor and vrfSystemFloors are described below: Input:

tzFloor = {
  tzName(string): Name of the thermal zone.
  tzFloorName(string): Name of the floor the thermal zone is on (or if the TZ is on multiple floors the
                       name of the current floor being looked at for the thermal zone).
  tzFloorCeilingAream2(float): The ceiling area (m2) of the thermal zone on the given floor (this is for the
                               current thermal zone only and does not include multiples).
  tzMult(float): The multiplier for the thermal zone (that is the thermal zone is modeled as tzMult number
                 of identical thermal zones).
  tzSpaces(array): An array containing all of the space objects contained by the thermal zone on the given
                   floor.
  tzSpaceMults(array): An array containing all of the multipliers for the spaces in tzSpaces (probably all the
                       same as tzMult but I added it anyway).
  tzFloorArea_m2(float): The floor area (m2) of all of the spaces in the thermal zone on the given floor (this
                         is for the current thermal zone only and does not include multiples).
  tzFloorCapkW(float): The capacity (kW) of the thermal zone VRF system for the current floor.  It is the
                       highest of the heating and cooling capacities for the VRF system.  It dose not include
                       multipliers and is only for the current floor.  If the same thermal zone spans multiple
                       floors then this is the total copacity for the thermal zone times tzFloorArea_m2
                       divided by the total floor area for the thermal zone.
  tzCentroid(array):  This is an array containing three items.  These items ore x, y and z coordinates of the
                      centroid of the current thermal zone on the current floor referenced to the global
                      origin for the building.  The units are in m.  Note that, depending on the shape of the
                      thermal zone on the current floor, the centroid may not actually lie in the thermal
                      zone (e.g. for an L shaped thermal zone the centroid may be outside the L).
  vrfCeilMountInfo(hash): This is a hash containing the costing information, from the costing spreadsheet, for
                          the VRF ceiling mounts serving the thermal zone on the current floor.  It is only
                          for the current floor and does not include tz multipliers.
  vrfCeilMountCost(float): The cost of the VRF ceiling mounts serving the thermal zone on the current floor.
                           It is only for the current floor and does not include tz multipliers.
  vrfSysContCost: The cost of the VRF system controllers that are associated with each VRF ceiling mount.  It
                  is only for the current floor and does not include tz multipliers.
}

vrfSystemFloors = {
  maxCeil(float):  The height of the thermal zone served by a VRF system with the highest ceiling in the
                   building.  This is referenced to the global origin for the building.  The units are m.
  lowCeil(float):  The height of the thermal zone served by a VRF system with the lowest ceiling in the
                   building.  This is referenced to the global origin for the building.  The units are m.
  vrfFloors(array):  [
    storyName(string):  Name of the current floor (story).
    buildStoryObj(Obj):  The OpenStudio object associated with the current floor (story).
    floorAream2:  Total floor area (m2) of thermal zones on the current floor served by VRF systems.  Does not
                  include multipliers.
    floorCeillingAream2:  The Total ceiling area (m2) of thermal zones on the current floor served by VRF
                          systems.  Does not include multipliers.
    floorTZs(array):  An array containing each tzFloor hash described above for the current floor.

  ]
}


2194
2195
2196
2197
2198
2199
2200
2201
2202
2203
2204
2205
2206
2207
2208
2209
2210
2211
2212
2213
2214
2215
2216
2217
2218
2219
2220
2221
2222
2223
2224
2225
2226
2227
2228
# File 'lib/openstudio-standards/btap/costing/heating_cooling_costing.rb', line 2194

def compileZonalVRFFloors(vrfSystemFloors:, tzFloor:)
  # Check for the highest ceiling and lowest ceiling.
  vrfSystemFloors[:maxCeil] = tzFloor[:tzCentroid][2].to_f if tzFloor[:tzCentroid][2].to_f >= vrfSystemFloors[:maxCeil].to_f
  vrfSystemFloors[:lowCeil] = tzFloor[:tzCentroid][2].to_f if tzFloor[:tzCentroid][2].to_f <= vrfSystemFloors[:lowCeil].to_f
  # If this is the first time vrfSystemFloors has been used add a new floor and enter the information for tzFloor in
  # it.
  if vrfSystemFloors[:vrfFloors].empty?
    vrfSystemFloors[:vrfFloors] << {
      storyName: tzFloor[:tzFloorName],
      buildStoryObj: tzFloor[:tzSpaces][0].buildingStory.get,
      floorAream2: tzFloor[:tzFloorArea_m2],
      floorCeilingAream2: tzFloor[:tzFloorCeilingAream2],
      floorTZs: [tzFloor]
    }
  else
    # If vrfSystemFloors has been used check if the the floor that tzFloor is on has an entry already.
    vrfFloor = vrfSystemFloors[:vrfFloors].select{|sysFloor| sysFloor[:storyName].to_s.upcase == tzFloor[:tzFloorName].to_s.upcase}
    # If no entry has been made for the floor tzFloor is on then add a new floor and include the tzFloor info.
    if vrfFloor.empty?
      vrfSystemFloors[:vrfFloors] << {
        storyName: tzFloor[:tzFloorName],
        buildStoryObj: tzFloor[:tzSpaces][0].buildingStory.get,
        floorAream2: tzFloor[:tzFloorArea_m2],
        floorCeilingAream2: tzFloor[:tzFloorCeilingAream2],
        floorTZs: [tzFloor]
      }
    else
      # If the floor that tzFloor is on has already been made then adjust the floor information to include tzFloor.
      vrfFloor[0][:floorAream2] += tzFloor[:tzFloorArea_m2]
      vrfFloor[0][:floorCeilingAream2] += tzFloor[:tzFloorCeilingAream2]
      vrfFloor[0][:floorTZs] << tzFloor
    end
  end
  return vrfSystemFloors
end

#coolingtower_costing(model, prototype_creator) ⇒ Object


Cooling tower (i.e., chiller condensor loop cooling) costing




706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
# File 'lib/openstudio-standards/btap/costing/heating_cooling_costing.rb', line 706

def coolingtower_costing(model, prototype_creator)

  totalCost = 0.0

  # Get regional cost factors for this province and city
  materials_hvac = @costing_database["raw"]["materials_hvac"]
  hvac_material = materials_hvac.select {|data|
    data['Material'].to_s == "GasBoilers"}.first  # Get any row from spreadsheet in case of region error
  regional_material, regional_installation =
      get_regional_cost_factors(@costing_report['province_state'], @costing_report['city'], hvac_material)

  # Get regional electric cost factors for this province and city
  hvac_material = materials_hvac.select {|data|
    data['Material'].to_s.upcase == "BOX" && data['Size'].to_i == 1}.first
  reg_mat_elec, reg_lab_elec =
    get_regional_cost_factors(@costing_report['province_state'], @costing_report['city'], hvac_material)

  # Store some geometry data for use below...
  util_dist, ht_roof, nom_flr_hght, horz_dist, numAGFlrs, mechRmInBsmt = getGeometryData(model, prototype_creator)

  template_type = prototype_creator.template

  cltowerCost = 0.0
  thisClTowerCost = 0.0
  utilCost = 0.0
  plant_loop_info = {}
  plant_loop_info[:coolingtowers] = []
  plant_loop_info[:coolingtowerpumps] = []
  plant_loop_info[:groundloops] = []
  cltowertype = 'cooling_towers'

  # Iterate through the plant loops to get cooling tower & pump data...
  model.getPlantLoops.each do |plant_loop|
    next unless (plant_loop.name.get.to_s =~ /Condenser Water Loop/i) || (plant_loop.name.get.to_s =~ /Condenser PlantLoop GLHX/i)
    plant_loop.supplyComponents.each do |supply_comp|
      if supply_comp.to_CoolingTowerSingleSpeed.is_initialized
        cltower = supply_comp.to_CoolingTowerSingleSpeed.get
        cltower_info = {}
        plant_loop_info[:coolingtowers] << cltower_info
        cltower_info[:name] = cltower.name.get
        cltower_info[:type] = 'ClgTwr'  # Material lookup name
        cltower_info[:fanPoweratDesignAirFlowRate] = cltower.fanPoweratDesignAirFlowRate.to_f / 1000 # kW
        cltower_info[:capacity] = model.sqlFile().get().execAndReturnFirstDouble("SELECT Value FROM " +
          "TabularDataWithStrings WHERE ReportName='EquipmentSummary' AND ReportForString='Entire Facility' AND " +
          "TableName='Central Plant' AND ColumnName='Nominal Capacity' AND " +
          "RowName='#{cltower_info[:name].upcase}' ").to_f / 1000 # kW
      elsif supply_comp.to_PumpConstantSpeed.is_initialized
        csPump = supply_comp.to_PumpConstantSpeed.get
        csPump_info = {}
        plant_loop_info[:coolingtowerpumps] << csPump_info
        csPump_info[:name] = csPump.name.get
        if csPump.isRatedPowerConsumptionAutosized.to_bool
          csPumpSize = csPump.autosizedRatedPowerConsumption.to_f
        else
          csPumpSize = csPump.ratedPowerConsumption.to_f
        end
        csPump_info[:size] = csPumpSize.to_f # Watts
      elsif supply_comp.to_PumpVariableSpeed.is_initialized
        vsPump = supply_comp.to_PumpVariableSpeed.get
        vsPump_info = {}
        plant_loop_info[:coolingtowerpumps] << vsPump_info
        vsPump_info[:name] = vsPump.name.get
        if vsPump.isRatedPowerConsumptionAutosized.to_bool
          vsPumpSize = vsPump.autosizedRatedPowerConsumption.to_f
        else
          vsPumpSize = vsPump.ratedPowerConsumption.to_f
        end
        vsPump_info[:size] = vsPumpSize.to_f # Watts
      elsif supply_comp.to_DistrictHeating.is_initialized
        groundLoop = supply_comp.to_DistrictHeating.get
        groundLoop_info = {}
        groundLoop_info[:name] = groundLoop.name.to_s
        if groundLoop.isNominalCapacityAutosized.to_bool
          groundLoop_info[:nominal_capacity] = groundLoop.autosizedNominalCapacity.to_f/1000.0
        else
          groundLoop_info[:nominal_capacity] = groundLoop.nominalCapacity.to_f/1000.0
        end
        # Get flow rate to ground loop
        if plant_loop.isMaximumLoopFlowRateAutosized.to_bool
          groundLoop_info[:plant_loop_flow_rate_m3ps] = plant_loop.autosizedMaximumLoopFlowRate.to_f
        else
          groundLoop_info[:plant_loop_flow_rate_m3ps] = plant_loop.maximumLoopFlowRate.to_f
        end
        plant_loop_info[:groundloops] << groundLoop_info
      elsif supply_comp.to_DistrictCooling.is_initialized
        groundLoop = supply_comp.to_DistrictCooling.get
        groundLoop_info = {}
        groundLoop_info[:name] = groundLoop.name.to_s
        if groundLoop.isNominalCapacityAutosized.to_bool
          groundLoop_info[:nominal_capacity] = groundLoop.autosizedNominalCapacity.to_f/1000.0
        else
          groundLoop_info[:nominal_capacity] = groundLoop.nominalCapacity.to_f/1000.0
        end
        # Get flow rate to ground loop
        if plant_loop.isMaximumLoopFlowRateAutosized.to_bool
          groundLoop_info[:plant_loop_flow_rate_m3ps] = plant_loop.autosizedMaximumLoopFlowRate.to_f
        else
          groundLoop_info[:plant_loop_flow_rate_m3ps] = plant_loop.maximumLoopFlowRate.to_f
        end
        plant_loop_info[:groundloops] << groundLoop_info
      end
    end
  end

  # Get costs associated with each cooling tower
  numTowers = 0 ; multiplier = 1.0

  plant_loop_info[:coolingtowers].each do |cltower|
    # Get cooling tower cost based on capacity
    numTowers += 1
    if numTowers == 1
      matCost, labCost = getHVACCost(cltower[:name], cltower[:type], cltower[:capacity], false)
      thisClTowerCost = matCost * regional_material / 100.0 + labCost * regional_installation / 100.0
    else  # Multiple cooling towers
      if cltower[:capacity] <= 0.1
        # Cooling tower cost is zero!
        thisClTowerCost = 0.0
      else
        # A second cooling tower exists so use it for costing
        matCost, labCost = getHVACCost(cltower[:name], cltower[:type], cltower[:capacity], false)
        thisClTowerCost = matCost * regional_material / 100.0 + labCost * regional_installation / 100.0
      end
    end
    cltowerCost += thisClTowerCost

    # Electric utility costs (i.e., power lines) for cooling tower(s).

    # elec 600V #14 wire /100 ft (#848)
    materialHash = get_cost_info(mat: 'Wiring', size: 14)
    matCost, labCost = getCost('electrical wire - 600V #14', materialHash, multiplier)
    elecWireCost = matCost * reg_mat_elec / 100.0 + labCost * reg_lab_elec / 100.0

    # 1 inch metal conduit (#851)
    materialHash = get_cost_info(mat: 'Conduit', unit: 'L.F.')
    matCost, labCost = getCost('1 inch metal conduit', materialHash, multiplier)
    metalConduitCost = matCost * reg_mat_elec / 100.0 + labCost * reg_lab_elec / 100.0

    utilCost += metalConduitCost * (ht_roof + 20) + elecWireCost * (ht_roof + 20) / 100
  end

  # Cooling Tower (condensor) pump costs
  pumpCost = 0.0; pipingCost = 0.0; numPumps = 0; pumpName = ''; pumpSize = 0.0
  plant_loop_info[:coolingtowerpumps].each do |pump|
    numPumps += 1
    # Cost variable and constant volume pumps the same (VFD controller added if variable)
    pumpSize = pump[:size]; pumpName = pump[:name]
    matCost, labCost = getHVACCost(pumpName, 'Pumps', pumpSize, false)
    pumpCost += matCost * regional_material / 100.0 + labCost * regional_installation / 100.0
    if pump[:name] =~ /variable/i
      # Cost the VFD controller for the variable pump costed above
      pumpSize = pump[:size]; pumpName = pump[:name]
      matCost, labCost = getHVACCost(pumpName, 'VFD', pumpSize, false)
      pumpCost += matCost * reg_mat_elec / 100.0 + labCost * reg_lab_elec / 100.0
    end
  end
  if numTowers > 1 && numPumps < 2
    # Add pump costing for the backup pump.
    # 2023-04-25:  Not including backup tower or pump costs
    # pumpCost *= 2.0
  end
  # Double the pump costs to accomodate the costing of a backup pump(s)!
  # 2023-04-25 No longer including backup pumps
  #pumpCost *= 2.0
  #numPumps = 2  # reset the number of pumps for piping costs below


  # Chiller water piping cost: Add piping elbows, valves and insulation from the chiller(s)
  # to the pumps(s) assuming a pipe diameter of 1” and a distance of 10 ft per pump
  if numTowers > 0
    # 4 inch Steel pipe (vertical + horizontal)
    matCost, labCost = getHVACCost('4 inch steel pipe', 'SteelPipe', 4)
    pipingCost += (ht_roof * 2 + 10 * numPumps) * (matCost * regional_material / 100.0 + labCost * regional_installation / 100.0)

    # 4 inch Steel pipe insulation (vertical + horizontal)
    matCost, labCost = getHVACCost('1 inch pipe insulation', 'PipeInsulation', 4)
    pipingCost += (ht_roof * 2 + 10 * numPumps) * (matCost * regional_material / 100.0 + labCost * regional_installation / 100.0)

    # 4 inch Steel pipe elbow
    matCost, labCost = getHVACCost('4 inch steel pipe tee', 'SteelPipeTee', 4)
    pipingCost += 1.0 * numPumps * (matCost * regional_material / 100.0 + labCost * regional_installation / 100.0)

    # 4 inch valves
    matCost, labCost = getHVACCost('4 inch BFly valves', 'ValvesBFly', 4)
    pipingCost += 1.0 * numPumps * (matCost * regional_material / 100.0 + labCost * regional_installation / 100.0)
  end

  # Note: No extra costs for piping for backup condenser pump or multiple cooling towers.

  # Calculate GSHP ground loop cost and interior piping
  unless plant_loop_info[:groundloops].empty?
    # GSHP ground loop cost
    # Not applying localization because costs are currently national estimates only (2023-06-19)
    cltowertype = 'ground_loop'
    largest_loop = plant_loop_info[:groundloops].max_by { |groundloop| groundloop[:nominal_capacity] }
    nominal_capacity = largest_loop[:nominal_capacity].to_f
    matCost, labCost = getHVACCost('GSHP ground loop', 'gshp_ground_loop', '')
    cltowerCost = matCost * nominal_capacity

    # GSHP piping from building to bore field cost
    # Not applying localization because costs are currently national estimates only (2023-06-19)
    gshp_dist = 50
    loop_flow = largest_loop[:plant_loop_flow_rate_m3ps]
    pipe_dia_mm = d = Math.sqrt(1.273*loop_flow/2.0)*1000
    matCost, labCost = getHVACCost('GSHP outdoor piping cost', 'gshp_buried_pipe', pipe_dia_mm, false)
    pipingCost = matCost * gshp_dist

    # Interior piping cost
    # Get mechanical room lacotion (assume this is where the GSHP is)
    mech_room, cond_spaces = prototype_creator.find_mech_room(model)
    mech_centroid = mech_room["space_centroid"]
    # Determine the length to the largest exterior wall
    ground_spaces = []
    pipe_dists = []
    # Determine the exterior ground walls touching the ground
    # Get the spaces
    model.getSpaces.sort.each do |space|
      ground_surf = false
      # Get the surfaces for the space and determine if any are contacting the ground.  If one is add it to the arroy
      # of spaces
      space.surfaces.sort.each do |surface|
        if surface.isGroundSurface
          ground_surf = true
        end
      end
      ground_spaces << space if ground_surf == true
    end
    # Go through all of the spaces contacting the ground
    ground_spaces.sort.each do |ground_space|
      # Go through all of the surfaces for the space and determine which are exterior or foundation walls
      ext_walls = ground_space.surfaces.select{ |surf| surf.surfaceType == 'Wall' && (surf.outsideBoundaryCondition == 'Outdoors' || surf.outsideBoundaryCondition == 'Foundation')}
      # Get the largest exterior wall and the distance to its centroid to the mech room centroid
      unless ext_walls.empty?
        ext_wall = ext_walls.max_by { |ext_wall| ext_wall.grossArea.to_f }
        pipe_dists << {
          wall: ext_wall,
          pipe_dist: ((ext_wall.centroid.x.to_f + ground_space.xOrigin.to_f - mech_centroid[0].to_f).abs + (ext_wall.centroid.y.to_f + ground_space.yOrigin.to_f - mech_centroid[1].to_f).abs + (ext_wall.centroid.z.to_f + ground_space.zOrigin.to_f - mech_centroid[2].to_f).abs)
        }
      end
    end
    # Find the shortest distance to the 3 largest walls and pick the shortest one
    largest_walls = pipe_dists.max_by(3) { |wall| wall[:wall].grossArea.to_f }
    pipe_dist = largest_walls.min_by { |wall| wall[:pipe_dist].to_f }
    pipe_dist_ft = (OpenStudio.convert(pipe_dist[:pipe_dist], 'm', 'ft').get)
    pipe_dia_mm = d = Math.sqrt(1.273*loop_flow/2.0)*1000
    pipe_dia_inch = (OpenStudio.convert(pipe_dia_mm/1000, 'm', 'ft').get)*12.0
    pipe_dia_inch = 8.0 if pipe_dia_inch >= 8.0

    # Include localization foctors in interior piping and fixtures
    # Cost the interior pipe
    matCost, labCost = getHVACCost('GSHP indoor piping cost', 'SteelPipe', pipe_dia_inch, false)
    pipingCost += (matCost*regional_material + labCost*regional_installation)*pipe_dist_ft*2.0/100.0

    # Cost the pipe insulation
    pipe_dia_inch = 4.0 if pipe_dia_inch > 4.0
    matCost, labCost = getHVACCost('GSHP indoor pipe insulation', 'PipeInsulation', pipe_dia_inch, false)
    pipingCost += (matCost*regional_material + labCost*regional_installation)*pipe_dist_ft*2.0/100.0

    # Cost 1 valve
    matCost, labCost = getHVACCost('GSHP indoor pipe valve', 'ValvesBig', 4.0)
    pipingCost += (matCost*regional_material + labCost*regional_installation)/100.0

    # Cost 2 pipe tees
    matCost, labCost = getHVACCost('GSHP indoor pipe tees', 'SteelPipeTee', 4.0)
    pipingCost += (matCost*regional_material + labCost*regional_installation)*2.0/100.0

    # Cost 8 pipe tees
    matCost, labCost = getHVACCost('GSHP indoor pipe elbows', 'SteelPipeElbow', 4.0)
    pipingCost += (matCost*regional_material + labCost*regional_installation)*8.0/100.0
  end
  totalCost = cltowerCost + utilCost + pumpCost + pipingCost

  @costing_report['heating_and_cooling']['plant_equipment']  << {
      'type' => cltowertype,
      'nom_flr2flr_hght_ft' => nom_flr_hght.round(1),
      'ht_roof_ft' => ht_roof.round(1),
      'longest_distance_to_ext_ft' => horz_dist.round(1),
      'wiring_and_gas_connections_distance_ft' => util_dist.round(1),
      'equipment_cost' => cltowerCost.round(0),
      'wiring_and_gas_connections_cost' => utilCost.round(0),
      'pump_cost' => pumpCost.round(0),
      'piping_cost' => pipingCost.round(0),
      'total_cost' => totalCost.round(0)
  }

  puts "\nHVAC Cooling Tower costing data successfully generated. Total cooling tower costs: $#{totalCost.round(0)}"

  return totalCost
end

#cost_ahu(sys_type:, airloop_flow_lps:, airloop_flow_cfm:, mech_sizing_info:, heating_fuel:, cooling_type:, airloop_name:, vent_tags: []) ⇒ Object

This method looks for an air handler in the ‘hvac_vent_ahu’ sheet of the costing spreadsheet. The inputs it uses to find the air handler are: sys_type: HVAC system type (can handle NECB systems 1, 3, 4 or 6) airloop_flow_lps: Air loop design air flow rate (L/s) heating_fuel: The predominant heating fuel used by the air loop (HP, CCASHP, HW, Gas, Propane, Oil) cooling_type: The predominant cooling type used by the air loop (DX, HP, CCASHP, CHW) airloop_name: The name of the air loop (only used for error messages)

If no air handler with matching characteristics are found it assumes that all of the ones in the ‘hvac_vent_ahu’ ore too small. I then calls get_ahu_mult to find the largest air handler with the appropriate characteristics and finds how many of those are required to meet the load (see get_ahu_mult for more information). Once the appropriate air handler is selected from the ‘hvac_vent_ahu’ the method then reads the numbers in column K (id_layers) and column N (id_layers_quantity_multipliers). The numbers in ‘id_layers’ are indexes that match column A (material_id) in the ‘material_hvac’ costing spreadsheet sheet. The numbers in ‘id_layers_quantity_multipliers’ define how many pieces of equipment defined in the id_layer. The method then calls the ‘vent_assembly_cost’ method which takes the set of id_layers, the ‘id_layers_quantity_multipliers’ and the overall_mult. This costs each item in ‘id_layers’, multiplies the cost by the number in ‘id_layers_quantity_multipliers’ and multiplies everything by ‘overall_mult’. The returned cast is then multiplied by the number of air handlers present (mult) and returns the cost.

The method now also also includes the call to the ‘gas_burner_cost’ method to adjust for burner costs. It also includes the ahu size adjustement previously done in the main ‘ahu_costing’ method.



848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
# File 'lib/openstudio-standards/btap/costing/ventilation_costing.rb', line 848

def cost_ahu(sys_type:, airloop_flow_lps:, airloop_flow_cfm:, mech_sizing_info:, heating_fuel:, cooling_type:, airloop_name:, vent_tags: [])
  # Assmue one air handler to start
  mult = 1.0
  # Find an air handler in the 'hvac_vent_ahu' sheet that matches the system_type, air flow rate, heating type and
  # cooling type.
  ahu = @costing_database['raw']['hvac_vent_ahu'].select {|data|
    data['Sys_type'].to_f.round(0) == sys_type.to_f.round(0) and
        data['Supply_air'].to_f >= airloop_flow_lps and
        data['Htg'].to_s == heating_fuel and
        data['Clg'].to_s == cooling_type
  }.min_by{|info| info['Supply_air'].to_f}
  # If none are there assume that none had a big enough air flow rate.  Create a data structure with the pertinent
  # air handler information.
  if ahu.nil? || ahu.empty?
    loop_equip = {
        sys_type: sys_type,
        heating_fuel: heating_fuel,
        cooling_type: cooling_type,
        airloop_flow_lps: airloop_flow_lps,
        airloop_name: airloop_name
    }
    # Find send the air handler information to the 'get_ahu_mult' method which returns the air handler information and
    # the number which will meet the supply air rate.
    ahu, mult, rev_airloop_flow_lps = get_ahu_mult(loop_equip: loop_equip)
    # If one air handler which meets the requirements is found then use that one.
  else
    rev_airloop_flow_lps = airloop_flow_lps
  end
  # set the number of air hondlers in @airloop_info which is included in the ventilation costing report.
  @airloop_info[:num_rooftop_units] = mult.to_i
  # Calculate the ahu cost modifier for systems other than the largest (recreation of modifier originally applied in
  # the 'ahu_costing' method).
  ahu['Supply_air'].to_f.round(0) == 15000 ? ahu_cost_mod = 1.0 : ahu_cost_mod = (rev_airloop_flow_lps/(ahu['Supply_air'].to_f))
  # Get the 'id_layers' from the 'hvac_vent_ahu' sheet and put them into an array
  ids = ahu['id_layers'].to_s.split(',')
  # Get the quantity of each of the preceding 'id_layers'.  To do this, get the 'id_layers_quantity_multipliers'
  # numbers from the 'hvac_vent_ahu' and convert them into an array
  id_quants = ahu['Id_layers_quantity_multipliers'].to_s.split(',')
  # Check that the number of ids is the same as the number of id_quants.  If it isn't something is wrong and raise an
  # error.
  raise "The number of id_layers does not match the number of id_layer_quantity_multipliers in the hvac_vent_auh sheet of the costing spreadsheet.  Please look for the air handler in the costing spreadsheet and check the appropriate columns.  The air handler characteristics are: #{ahu}" if ids.size != id_quants.size
  # Get the overall_mult.  This used to be used but does not seem to be used anymore.  I left it in just in case
  # (probably a bad idea).
  overall_mult = ahu['material_mult'].to_f
  overall_mult = 1.0 if overall_mult == 0

  # Create tags that will be added to the cost list output
  new_tags = vent_tags.clone
  new_tags << heating_fuel
  new_tags << cooling_type
  new_tags << "Required Air Flow (L/s): #{airloop_flow_lps.to_f.round(2)}"
  new_tags << "Total AHU Air Flow with Multipliers(L/s): #{(ahu['Supply_air'].to_f*mult).to_f.round(2)}"
  new_tags << "AHU Equipment"

  # Cost the ids (multiplied by the number associated id_quants) and maltiply everything by the number of air handlers
  # (if one was too small).
  ind_ahu_cost = vent_assembly_cost(ids: ids, id_quants: id_quants, overall_mult: overall_mult, vent_tags: new_tags, report_mult: (overall_mult*ahu_cost_mod*mult))
  # This is the total ahu cost without adjusting cost with airflow
  calc_ahu_cost = ind_ahu_cost*mult
  # Create the start of the return hash (done here because it is used in the 'gas_burner_cost' method)
  costed_ahu_info = {
    ahu: ahu,
    mult: mult,
    air_loop_flow_lps: airloop_flow_lps,
    ind_ahu_cost: ind_ahu_cost
  }
  new_tags.pop
  # Remove gas burner cost from ahu cost because it is accounted for in the heating and cooling equipment calculated later.
  new_tags << "AHU Cost Adjustment"
  ahu_mech_adj = gas_burner_cost(heating_fuel: heating_fuel, sys_type: sys_type, airloop_flow_cfm: airloop_flow_cfm, mech_sizing_info: mech_sizing_info, costed_ahu_info: costed_ahu_info, vent_tags: new_tags, report_mult: ahu_cost_mod)
  base_ahu_cost = calc_ahu_cost - ahu_mech_adj
  # Caclculate the adjusted ahu cost
  adj_ahu_cost = (ind_ahu_cost*mult- ahu_mech_adj)*ahu_cost_mod
  # Add costs to costing output
  @airloop_info[:ind_ahu_max_airflow_l_per_s] = ahu['Supply_air'].to_f.round(0)
  @airloop_info[:base_ahu_cost] = base_ahu_cost.round(2)
  @airloop_info[:revised_base_ahu_cost] = adj_ahu_cost.round(2)

  # Add ahu costs to return hash
  costed_ahu_info[:base_ahu_cost] = base_ahu_cost
  costed_ahu_info[:adjusted_base_ahu_cost] = adj_ahu_cost

  return costed_ahu_info
end

#cost_audit_all(model:, prototype_creator:, envelope_costing: true, lighting_costing: true, boilers_costing: true, chillers_costing: true, cooling_towers_costing: true, shw_costing: true, ventilation_costing: true, zone_system_costing: true, renewables_costing: true, template_type: nil) ⇒ Object



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
# File 'lib/openstudio-standards/btap/costing/btap_costing.rb', line 91

def cost_audit_all(model:,
                   prototype_creator:,
                   envelope_costing: true,
                   lighting_costing: true,
                   boilers_costing: true,
                   chillers_costing: true,
                   cooling_towers_costing: true,
                   shw_costing: true,
                   ventilation_costing: true,
                   zone_system_costing: true,
                   renewables_costing: true,
                   template_type: nil
)
  # Create a Hash to collect costing data.
  @costing_report = {}

  #Use closest city.
  closest_loc = get_closest_cost_location(model.getWeatherFile.latitude, model.getWeatherFile.longitude)
  @costing_report['city'] = closest_loc['city']
  @costing_report['province_state'] = closest_loc['province_state']

  # Create array to collect costed item information.  First element is the costing location.
  @cost_items = {
    'City' => closest_loc['city'],
    'Province' => closest_loc['province_state'],
    'Items' => []
  }

  # Create a Hash in the hash for categories of costing.
  @costing_report['envelope'] = {}
  @costing_report['lighting'] = {}
  @costing_report['lighting']['daylighting_sensor_control'] = []
  @costing_report['lighting']['led_lighting'] = []
  @costing_report['heating_and_cooling'] = {}
  @costing_report['heating_and_cooling']['plant_equipment'] = []
  @costing_report['heating_and_cooling']['zonal_systems'] = []
  @costing_report['shw'] = {}
  @costing_report['ventilation'] = {}
  @costing_report['renewables'] = {}
  @costing_report['renewables']['pv'] = []
  @costing_report['totals'] = {}

  # Check to see if standards building type and the number of stories has been defined.  The former may be omitted in the future.
  if model.getBuilding.standardsBuildingType.empty? or model.getBuilding.standardsNumberOfAboveGroundStories.empty?
    raise("Building information is not complete, please ensure that the standardsBuildingType and standardsNumberOfAboveGroundStories are entered in the model. ")
  end

  # Find the mechanical room
  mech_room, cond_spaces = prototype_creator.find_mech_room(model)

  envCost = envelope_costing ? self.cost_audit_envelope(model, prototype_creator) : 0.0
  lgtCost = lighting_costing ? self.cost_audit_lighting(model, prototype_creator) : 0.0
  boilerCost = boilers_costing ? self.boiler_costing(model, prototype_creator) : 0.0
  chillerCost = chillers_costing ? self.chiller_costing(model, prototype_creator) : 0.0
  coolingTowerCost = cooling_towers_costing ? self.coolingtower_costing(model, prototype_creator) : 0.0
  shwCost = shw_costing ? self.shw_costing(model, prototype_creator) : 0.0
  ventCost = ventilation_costing ? self.ventilation_costing(model, prototype_creator,template_type, mech_room, cond_spaces) : 0.0
  zonalSystemCost = zone_system_costing ? self.zonalsys_costing(model, prototype_creator, mech_room, cond_spaces) : 0.0
  pvGroundCost = renewables_costing ? self.cost_audit_pv_ground(model, prototype_creator) : 0.0
  thermalBridgingCost = 0.0

  @costing_report["totals"] = {
    'envelope' => envCost.round(0),
    'thermal_bridging' => thermalBridgingCost.round(0),
    'lighting' => lgtCost.round(0),
    'heating_and_cooling' => (boilerCost + chillerCost + coolingTowerCost + zonalSystemCost).round(0),
    'shw' => shwCost.round(0),
    'ventilation' => ventCost.round(0),
    'renewables' => pvGroundCost.round(0),
    'grand_total' => (envCost + thermalBridgingCost + lgtCost + boilerCost + chillerCost + coolingTowerCost +
      shwCost + ventCost + zonalSystemCost + pvGroundCost).round(0)
  }

  return @costing_report, @cost_items
end

#cost_audit_daylighting_sensor_control(model:, prototype_creator:) ⇒ Object



3
4
5
6
7
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
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
# File 'lib/openstudio-standards/btap/costing/daylighting_sensor_control_costing.rb', line 3

def cost_audit_daylighting_sensor_control(model:, prototype_creator:)
  @costing_report["lighting"]["daylighting_sensor_control"] = []
  # NOTE: Number of daylighting sensors is based on how many a daylighted space needs sensors as per Mike Lubun's costing spec, rather than daylighting sensor control measure.
  standards_template = model.building.get.standardsTemplate.to_s
  if standards_template.include?('NECB')
    standards_template = standards_template.gsub(/(?<=\p{L})(?=\d)/, ' ') #insert a space between NECB and 2011/2015/2017
  end

  #-------------------------------------------------------------------------------------------------------------------
  dsc_cost_total = 0.0
  all_tz_primary_sidelighted_quatity = 0.0
  all_tz_skylight_quatity = 0.0
  #-------------------------------------------------------------------------------------------------------------------
  model.getThermalZones.sort.each do |tz|
    if tz.primaryDaylightingControl.is_initialized
      tz_cost_primary_sidelighted = 0.0
      tz_cost_skylight = 0.0
      tz_multiplier = tz.multiplier()
      daylight_spaces = []
      primary_sidelighted_area_hash = {}
      daylighted_area_under_skylights_hash = {}
      primary_sidelighted_area = 0.0
      daylighted_under_skylight_area = 0.0
      tz_area = 0.0
      tz_number_fixtures = 0.0
      tz_primary_sidelighted_ratio_daylight_area = 0.0
      tz_primary_sidelighted_number_fixtures = 0.0
      tz_primary_sidelighted_number_sensors = 0.0
      tz_skylights_ratio_daylight_area = 0.0
      tz_skylights_number_fixtures = 0.0
      tz_skylights_number_sensors = 0.0
      if !tz.primaryDaylightingControl.get.name().empty? && tz.fractionofZoneControlledbyPrimaryDaylightingControl() > 0.00
        tz.spaces().sort.each do |space|
          daylight_spaces << space
        end
      end

      #-------------------------------------------------------------------------------------------------------------------
      ##### Calculate tz_primary_sidelighted_area AND tz_daylighted_area_under_skylights.
      ##### The above two area values are required for the calculation of tz_primary_sidelighted_number_fixtures AND tz_skylights_number_fixtures
      daylight_spaces.sort.each do |daylight_space|
        # Go to the next space if the current space's space type is undefined.
        next if daylight_space.spaceType.get.name.to_s.downcase.include? "undefined"

        area_weighted_vt_handle = 0.0
        window_area_sum = 0.0
        skylight_area_weighted_vt_handle = 0.0
        skylight_area_sum = 0.0

        ##### Find lights_type in each daylight_space
        led_lights = 0
        daylight_space_type = daylight_space.spaceType()
        daylight_space_type.get.lights.sort.each do |inst|
          daylight_space_lights_definition = inst.lightsDefinition
          daylight_space_lights_definition_name = daylight_space_lights_definition.name
          if daylight_space_lights_definition_name.to_s.include?('LED lighting')
            led_lights += 1
          end
        end
        if (led_lights > 0) or (standards_template == 'NECB 2020')
          lights_type = 'LED'
        else
          lights_type = 'CFL'
        end

        ##### Find height of daylight_space
        max_space_height_m = 0.0
        daylight_space.surfaces.sort.select { |surface| surface.surfaceType == 'Wall' }.each do |wall_surface|
          # Find the vertex with the max z value.
          vertex_with_max_height = wall_surface.vertices.max_by(&:z)
          # Replace max if this surface has something bigger.
          max_space_height_m = vertex_with_max_height.z if vertex_with_max_height.z > max_space_height_m
        end
        max_space_height_ft = (OpenStudio.convert(max_space_height_m, 'm', 'ft').get) #Convert height to ft

        ##### Find area, floor_surface, and floor_vertices of daylight_space
        floor_surface = nil
        floor_area = 0.0
        floor_vertices = []
        daylight_space.surfaces.sort.each do |surface|
          if surface.surfaceType == 'Floor'
            floor_surface = surface
            floor_area += surface.netArea
            floor_vertices << surface.vertices
          end
        end

        ##### COSTING-related step: Find fixture type that should be used in the daylight_space based on space_type, template, and lights_type
        search_fixture_type = {
            row_id_1: daylight_space.spaceType.get.standardsSpaceType.to_s, #space_type
            row_id_2: standards_template,
            row_id_3: lights_type
        }
        sheet_name = 'lighting_sets'
        if max_space_height_ft < 7.88
          column_search = 'Fixture_type_less_than_7.88ft_ht'
        elsif max_space_height_ft >= 7.88 && max_space_height_ft < 15.75
          column_search = 'Fixture_type_7.88_to_15.75ft_ht'
        else #i.e. max_space_height_ft >= 15.75ft_ht
          column_search = 'Fixture_type_greater_than_>15.75ft_ht'
        end
        row_search_1 = 'space_type'
        row_search_2 = 'template'
        row_search_3 = 'Type'
        fixture_type = get_fixture_type_id(fixture_info: search_fixture_type, sheet_name: sheet_name, row_name_1: row_search_1, row_name_2: row_search_2, row_name_3: row_search_3, column_search: column_search)

        ##### COSTING-related step: Find number_fixtures_per_1000_ft2 that should be considered in the daylight_space based on fixture_type
        search_fixtures_per_1000_ft2 = @costing_database['raw']['lighting'].select { |data|
          data['lighting_type_id'].to_f.round(1) == fixture_type.to_f.round(1)
        }.first
        if search_fixtures_per_1000_ft2.nil?
          puts("No data found for #{search_fixtures_per_1000_ft2}!")
          raise
        end
        number_fixtures_per_1000_ft2 = search_fixtures_per_1000_ft2['Fix_1000ft'].to_i

        ##### COSTING-related step: Calculate number_fixtures_space that should be considered in the daylight_space based on number_fixtures_per_1000_ft2 and area of daylight_space
        floor_area_ft2 = (OpenStudio.convert(floor_area, 'm^2', 'ft^2').get) #convert floor_area to ft2
        number_fixtures_space = (floor_area_ft2 / 1000) * number_fixtures_per_1000_ft2
        number_fixtures_space = number_fixtures_space.ceil
        tz_number_fixtures += number_fixtures_space

        #-----------------------------------------------------------------------------------------------------------------
        ############################## Calculate 'primary_sidelighted_area' of the thermal zone ##########################
        primary_sidelighted_area, area_weighted_vt_handle, window_area_sum =
            prototype_creator.get_parameters_sidelighting(daylight_space: daylight_space,
                                                 floor_surface: floor_surface,
                                                 floor_vertices: floor_vertices,
                                                 floor_area: floor_area,
                                                 primary_sidelighted_area: primary_sidelighted_area,
                                                 area_weighted_vt_handle: area_weighted_vt_handle,
                                                 window_area_sum: window_area_sum)

        primary_sidelighted_area_hash[daylight_space.name.to_s] = primary_sidelighted_area
        #-----------------------------------------------------------------------------------------------------------------
        ########################### Calculate 'daylighted_under_skylight_area' of the thermal zone #########################
        ##### Loop through the surfaces of each daylight_space to calculate daylighted_area_under_skylights and skylight_effective_aperture for each daylight_space
        daylighted_under_skylight_area, skylight_area_weighted_vt_handle, skylight_area_sum =
            prototype_creator.get_parameters_skylight(daylight_space: daylight_space,
                                             skylight_area_weighted_vt_handle: skylight_area_weighted_vt_handle,
                                             skylight_area_sum: skylight_area_sum,
                                             daylighted_under_skylight_area: daylighted_under_skylight_area)

        daylighted_area_under_skylights_hash[daylight_space.name.to_s] = daylighted_under_skylight_area
        #-----------------------------------------------------------------------------------------------------------------

        tz_area += floor_area

      end #daylight_spaces.sort.each do |daylight_space|

      #-------------------------------------------------------------------------------------------------------------------
      # If no fixtures or daylighting is defined then go to the next thermal zone
      next if tz_number_fixtures.to_f == 0.0 || tz_primary_sidelighted_ratio_daylight_area.to_f.nan?
      ##### COSTING-related step: Calculate number of fixtures in thermal zones with window(s)-------------------------------------------------
      tz_primary_sidelighted_ratio_daylight_area = primary_sidelighted_area / tz_area
      tz_primary_sidelighted_number_fixtures = (tz_number_fixtures * tz_primary_sidelighted_ratio_daylight_area).ceil
      tz_primary_sidelighted_number_sensors = (tz_primary_sidelighted_number_fixtures / 4.0).ceil
      all_tz_primary_sidelighted_quatity += tz_primary_sidelighted_number_sensors * tz_multiplier

      if tz_primary_sidelighted_number_sensors > 0.0
        tags = ['lighting', 'daylighting_sensor_control']
        # cost of daylighting sensor
        quantity_tz_primary_sidelighted_daylighting_sensor = 1.0 * tz_primary_sidelighted_number_sensors * tz_multiplier
        search_tz_primary_sidelighted_daylighting_sensor = {
            row_id_1: 'Ea',
            row_id_2: 407
        }
        sheet_name = 'materials_lighting'
        column_1 = 'unit'
        column_2 = 'lighting_type_id'
        cost_tz_primary_sidelighted_daylighting_sensor = assembly_cost(cost_info:search_tz_primary_sidelighted_daylighting_sensor,
                                                                       sheet_name:sheet_name,
                                                                       column_1:column_1,
                                                                       column_2:column_2,
                                                                       quantity:quantity_tz_primary_sidelighted_daylighting_sensor,
                                                                       tags: tags)
        # cost of wiring
        quantity_tz_primary_sidelighted_wiring = (30.0 / 100.0) * tz_primary_sidelighted_number_sensors * tz_multiplier
        search_tz_primary_sidelighted_wiring = {
            row_id_1: 'CLF',
            row_id_2: 10
        }
        sheet_name = 'materials_lighting'
        column_1 = 'unit'
        column_2 = 'lighting_type_id'
        cost_tz_primary_sidelighted_wiring = assembly_cost(cost_info:search_tz_primary_sidelighted_wiring,
                                                           sheet_name:sheet_name,
                                                           column_1:column_1,
                                                           column_2:column_2,
                                                           quantity:quantity_tz_primary_sidelighted_wiring,
                                                           tags: tags)
        # cost of pvc conduit
        quantity_tz_primary_sidelighted_pvc_conduit = 30.0 * tz_primary_sidelighted_number_sensors * tz_multiplier
        search_tz_primary_sidelighted_pvc_conduit = {
            row_id_1: 'LF',
            row_id_2: 17
        }
        sheet_name = 'materials_lighting'
        column_1 = 'unit'
        column_2 = 'lighting_type_id'
        cost_tz_primary_sidelighted_pvc_conduit = assembly_cost(cost_info:search_tz_primary_sidelighted_pvc_conduit,
                                                                sheet_name:sheet_name,
                                                                column_1:column_1,
                                                                column_2:column_2,
                                                                quantity:quantity_tz_primary_sidelighted_pvc_conduit,
                                                                tags: tags)
        # cost of box
        quantity_tz_primary_sidelighted_box = 1.0 * tz_primary_sidelighted_number_sensors * tz_multiplier
        search_tz_primary_sidelighted_box = {
            row_id_1: 'Ea',
            row_id_2: 14
        }
        sheet_name = 'materials_lighting'
        column_1 = 'unit'
        column_2 = 'lighting_type_id'
        cost_tz_primary_sidelighted_box = assembly_cost(cost_info:search_tz_primary_sidelighted_box,
                                                        sheet_name:sheet_name,
                                                        column_1:column_1,
                                                        column_2:column_2,
                                                        quantity:quantity_tz_primary_sidelighted_box,
                                                        tags: tags)
        # total cost for this zone
        tz_cost_primary_sidelighted = cost_tz_primary_sidelighted_daylighting_sensor +
                                      cost_tz_primary_sidelighted_wiring +
                                      cost_tz_primary_sidelighted_pvc_conduit +
                                      cost_tz_primary_sidelighted_box
        dsc_cost_total += tz_cost_primary_sidelighted
      end

      ##### COSTING-related step: Calculate number of fixtures in thermal zones with skylight(s)-------------------------------------------------
      tz_skylights_ratio_daylight_area = daylighted_under_skylight_area / tz_area
      tz_skylights_number_fixtures = (tz_number_fixtures * tz_skylights_ratio_daylight_area).ceil
      tz_skylights_number_sensors = (tz_skylights_number_fixtures / 4.0).ceil
      all_tz_skylight_quatity += tz_skylights_number_sensors * tz_multiplier

      if tz_skylights_number_sensors > 0.0
        tags = ['lighting', 'daylighting_sensor_control']
        # cost of daylighting sensor
        quantity_tz_skylights_daylighting_sensor = 1.0 * tz_skylights_number_sensors * tz_multiplier
        search_tz_skylights_daylighting_sensor = {
            row_id_1: 'Ea',
            row_id_2: 407
        }
        sheet_name = 'materials_lighting'
        column_1 = 'unit'
        column_2 = 'lighting_type_id'
        cost_tz_skylights_daylighting_sensor = assembly_cost(cost_info:search_tz_skylights_daylighting_sensor,
                                                             sheet_name:sheet_name,
                                                             column_1:column_1,
                                                             column_2:column_2,
                                                             quantity:quantity_tz_skylights_daylighting_sensor,
                                                             tags: tags)

        # cost of wiring
        quantity_tz_skylights_wiring = (30.0 / 100.0) * tz_skylights_number_sensors * tz_multiplier
        search_tz_skylights_wiring = {
            row_id_1: 'CLF',
            row_id_2: 10
        }
        sheet_name = 'materials_lighting'
        column_1 = 'unit'
        column_2 = 'lighting_type_id'
        cost_tz_skylights_wiring = assembly_cost(cost_info:search_tz_skylights_wiring,
                                                 sheet_name:sheet_name,
                                                 column_1:column_1,
                                                 column_2:column_2,
                                                 quantity:quantity_tz_skylights_wiring,
                                                 tags: tags)

        # cost of pvc conduit
        quantity_tz_skylights_pvc_conduit = 30.0 * tz_skylights_number_sensors * tz_multiplier
        search_tz_skylights_pvc_conduit = {
            row_id_1: 'LF',
            row_id_2: 17
        }
        sheet_name = 'materials_lighting'
        column_1 = 'unit'
        column_2 = 'lighting_type_id'
        cost_tz_skylights_pvc_conduit = assembly_cost(cost_info:search_tz_skylights_pvc_conduit,
                                                      sheet_name:sheet_name,
                                                      column_1:column_1,
                                                      column_2:column_2,
                                                      quantity:quantity_tz_skylights_pvc_conduit,
                                                      tags: tags)

        # cost of box
        quantity_tz_skylights_box = 1.0 * tz_skylights_number_sensors * tz_multiplier
        search_tz_skylights_box = {
            row_id_1: 'Ea',
            row_id_2: 14
        }
        sheet_name = 'materials_lighting'
        column_1 = 'unit'
        column_2 = 'lighting_type_id'
        cost_tz_skylights_box = assembly_cost(cost_info:search_tz_skylights_box,
                                              sheet_name:sheet_name,
                                              column_1:column_1,
                                              column_2:column_2,
                                              quantity:quantity_tz_skylights_box,
                                              tags: tags)

        # total cost for this zone
        tz_cost_skylight = cost_tz_skylights_daylighting_sensor +
                           cost_tz_skylights_wiring +
                           cost_tz_skylights_pvc_conduit +
                           cost_tz_skylights_box

        dsc_cost_total += tz_cost_skylight
      end

      ##### Gather information for reporting
      @costing_report["lighting"]["daylighting_sensor_control"] << {
          'zone' => tz.name.to_s,
          'zone_area' => tz_area,
          'zone_multiplier' => tz_multiplier,
          'number_of_fixtures_required_without_considering_daylighted_area_under_sidelighting_and_skylights' => tz_number_fixtures,
          'primary_sidelighted_area' => primary_sidelighted_area,
          'primary_sidelighted_number_fixtures' => tz_primary_sidelighted_number_fixtures,
          'primary_sidelighted_number_sensors' => tz_primary_sidelighted_number_sensors,
          'skylights_daylighted_area' => daylighted_under_skylight_area,
          'skylights_number_fixtures' => tz_skylights_number_fixtures,
          'skylights_number_sensors' => tz_skylights_number_sensors,
          'daylighting_sensor_control_cost_for_this_zone' => tz_cost_primary_sidelighted + tz_cost_skylight
      }

    end #tz.primaryDaylightingControl.is_initialized
  end #model.getThermalZones.sort.each do |tz|
  #-------------------------------------------------------------------------------------------------------------------

  puts "\nDaylighting sensor controls costing data successfully generated. Total DSC costs: $#{dsc_cost_total.round(2)}"

  return dsc_cost_total

end

#cost_audit_dcv(model:, prototype_creator:) ⇒ Object



3
4
5
6
7
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
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
# File 'lib/openstudio-standards/btap/costing/dcv_costing.rb', line 3

def cost_audit_dcv(model:, prototype_creator:)
  @costing_report['ventilation'][:demand_controlled_ventilation] = []
  a = 0.0 # This is for reporting purposes.

  ##### Initialize cost of various parts of DCV
  dcv_cost_zone_occupancy = 0.0
  dcv_cost_zone_co2 = 0.0
  dcv_cost_box = 0.0
  dcv_cost_vertical_conduit = 0.0
  dcv_cost_roof = 0.0
  dcv_cost_control = 0.0
  dcv_cost_total = 0.0

  ##### Get number of stories and nominal floor to floor height.
  ##### These inputs are required for the calculation of the length of conduit for each AirLoopHVAC.
  util_dist, ht_roof, nominal_flr2flr_height_ft, horizontal_dist, standards_number_of_stories, mechRmInBsmt = getGeometryData(model, prototype_creator)
  nominal_flr2flr_height_m = (OpenStudio.convert(nominal_flr2flr_height_ft, 'ft', 'm').get)

  number_of_dcv_controller_for_all_air_loops = 0

  model.getAirLoopHVACs.sort.each do |air_loop|
    number_of_thermal_zones_served_by_air_loop = 0
    number_floors_served_by_air_loop = 0
    number_of_junction_boxes_for_air_loop = 0

    ##### Step A: Calculate number of thermal zones and floors served by each AirLoopHVAC (air_loop)
    building_story_list = []
    air_loop.thermalZones.sort.each do |thermal_zone|
      thermal_zone.spaces().sort.each do |space|
        building_story_list << space.buildingStory.get.name()
      end
      number_of_thermal_zones_served_by_air_loop += thermal_zone.multiplier()
    end
    building_story_list = building_story_list.uniq()
    number_floors_served_by_air_loop = building_story_list.length

    ##### Loop through AirLoopHVAC's supply nodes to:
    ##### (1) Find its AirLoopHVAC:OutdoorAirSystem using the supply node;
    ##### (2) Find Controller:OutdoorAir using AirLoopHVAC:OutdoorAirSystem;
    ##### (3) Get "Controller Mechanical Ventilation" from Controller:OutdoorAir.
    air_loop.supplyComponents.sort.each do |supply_component|
      ##### Find AirLoopHVAC:OutdoorAirSystem of AirLoopHVAC using the supply node.
      hvac_component = supply_component.to_AirLoopHVACOutdoorAirSystem

      if !hvac_component.empty?

        tags = ['ventilation', 'demand_controlled_ventilation']

        ##### Find Controller:OutdoorAir using AirLoopHVAC:OutdoorAirSystem.
        hvac_component = hvac_component.get
        hvac_component_name = hvac_component.name()
        controller_outdoorair = hvac_component.getControllerOutdoorAir
        controller_outdoorair_name = controller_outdoorair.name()

        ##### Get "Controller Mechanical Ventilation" from Controller:OutdoorAir.
        controller_mechanical_ventilation = controller_outdoorair.controllerMechanicalVentilation
        controller_mechanical_ventilation_name = controller_mechanical_ventilation.name()

        ##### Check if "Demand Controlled Ventilation" is "Yes" in Controller:MechanicalVentilation depending on dcv_type.
        controller_mechanical_ventilation_demand_controlled_ventilation_status = controller_mechanical_ventilation.demandControlledVentilation

        controller_mechanical_ventilation_system_outdoor_air_method = controller_mechanical_ventilation.systemOutdoorAirMethod()
        if controller_mechanical_ventilation_demand_controlled_ventilation_status == true && (controller_mechanical_ventilation_system_outdoor_air_method == 'ZoneSum' || controller_mechanical_ventilation_system_outdoor_air_method == 'IndoorAirQualityProcedure')

          ##### Step B: Calculate costs of the installation of a single junction box for each floor within that AirLoopHVAC (air_loop) to accumulate the wiring
          quantity_ahu_floors_junction_box = 1.0 * number_floors_served_by_air_loop
          search_ahu_floors_junction_box = {
              row_id_1: 'Ea',
              row_id_2: 14
          }
          sheet_name = 'materials_lighting'
          column_1 = 'unit'
          column_2 = 'lighting_type_id'
          dcv_cost_box = assembly_cost(cost_info:search_ahu_floors_junction_box,
                                       sheet_name:sheet_name,
                                       column_1:column_1,
                                       column_2:column_2,
                                       quantity:quantity_ahu_floors_junction_box,
                                       tags: tags)

          ##### Step C: Calculate costs of the installation of a single conduit that runs the entire height of the building for each AirLoopHVAC to accumulate the wiring
          quantity_ahu_vertical_wiring = 1.0/100.0 * standards_number_of_stories * nominal_flr2flr_height_ft
          search_ahu_vertical_wiring = {
              row_id_1: 'CLF',
              row_id_2: 10
          }
          sheet_name = 'materials_lighting'
          column_1 = 'unit'
          column_2 = 'lighting_type_id'
          cost_ahu_vertical_wiring = assembly_cost(cost_info:search_ahu_vertical_wiring,
                                                   sheet_name:sheet_name,
                                                   column_1:column_1,
                                                   column_2:column_2,
                                                   quantity:quantity_ahu_vertical_wiring,
                                                   tags: tags)
          quantity_ahu_vertical_conduit = 1.0 * standards_number_of_stories * nominal_flr2flr_height_ft
          search_ahu_vertical_conduit = {
              row_id_1: 'LF',
              row_id_2: 13
          }
          sheet_name = 'materials_lighting'
          column_1 = 'unit'
          column_2 = 'lighting_type_id'
          cost_ahu_vertical_conduit = assembly_cost(cost_info:search_ahu_vertical_conduit,
                                                    sheet_name:sheet_name,
                                                    column_1:column_1,
                                                    column_2:column_2,
                                                    quantity:quantity_ahu_vertical_conduit,
                                                    tags: tags)
          dcv_cost_vertical_conduit = cost_ahu_vertical_wiring + cost_ahu_vertical_conduit

          ##### Step D: Calculate the roof conduit and wiring for each AirLoopHVAC.
          quantity_ahu_roof_wiring = 20.0/100.0
          search_ahu_roof_wiring = {
              row_id_1: 'CLF',
              row_id_2: 10
          }
          sheet_name = 'materials_lighting'
          column_1 = 'unit'
          column_2 = 'lighting_type_id'
          cost_ahu_roof_wiring = assembly_cost(cost_info:search_ahu_roof_wiring,
                                               sheet_name:sheet_name,
                                               column_1:column_1,
                                               column_2:column_2,
                                               quantity:quantity_ahu_roof_wiring,
                                               tags: tags)
          quantity_ahu_roof_conduit = 20.0
          search_ahu_roof_conduit = {
              row_id_1: 'LF',
              row_id_2: 13
          }
          sheet_name = 'materials_lighting'
          column_1 = 'unit'
          column_2 = 'lighting_type_id'
          cost_ahu_roof_conduit = assembly_cost(cost_info:search_ahu_roof_conduit,
                                                sheet_name:sheet_name,
                                                column_1:column_1,
                                                column_2:column_2,
                                                quantity:quantity_ahu_roof_conduit,
                                                tags: tags)
          quantity_ahu_roof_junction_box = 1.0
          search_ahu_roof_junction_box = {
              row_id_1: 'Ea',
              row_id_2: 14
          }
          sheet_name = 'materials_lighting'
          column_1 = 'unit'
          column_2 = 'lighting_type_id'
          cost_ahu_roof_junction_box = assembly_cost(cost_info:search_ahu_roof_junction_box,
                                                     sheet_name:sheet_name,
                                                     column_1:column_1,
                                                     column_2:column_2,
                                                     quantity:quantity_ahu_roof_junction_box,
                                                     tags: tags)
          dcv_cost_roof = cost_ahu_roof_wiring + cost_ahu_roof_conduit + cost_ahu_roof_junction_box

          ##### Step E: Calculate DCV controller for each AirLoopHVAC.
          number_of_dcv_controller_for_all_air_loops += 1
          quantity_ahu_contorller = 1.0
          search_ahu_contorller = {
              row_id_1: 'Ea',
              row_id_2: 400
          }
          sheet_name = 'materials_lighting'
          column_1 = 'unit'
          column_2 = 'lighting_type_id'
          dcv_cost_control = assembly_cost(cost_info:search_ahu_contorller,
                                           sheet_name:sheet_name,
                                           column_1:column_1,
                                           column_2:column_2,
                                           quantity:quantity_ahu_contorller,
                                           tags: tags)

          if controller_mechanical_ventilation_system_outdoor_air_method == 'ZoneSum'
            ##### Step F: Calculate total Cost for each AirLoopHVAC
            # Calculate occupancy sensor-related costs of each thermal zone served by each AirLoopHVAC (air_loop)
            quantity_tz_occupancy_sensor = 1.0 * number_of_thermal_zones_served_by_air_loop.to_f
            search_tz_occupancy_sensor = {
                row_id_1: 'Ea',
                row_id_2: 404
            }
            sheet_name = 'materials_lighting'
            column_1 = 'unit'
            column_2 = 'lighting_type_id'
            cost_tz_occupancy_sensor = assembly_cost(cost_info:search_tz_occupancy_sensor,
                                                     sheet_name:sheet_name,
                                                     column_1:column_1,
                                                     column_2:column_2,
                                                     quantity:quantity_tz_occupancy_sensor,
                                                     tags: tags)
            quantity_tz_occupancy_sensor_wiring = 30.0/100.0 * number_of_thermal_zones_served_by_air_loop.to_f
            search_tz_occupancy_sensor_wiring = {
                row_id_1: 'CLF',
                row_id_2: 10
            }
            sheet_name = 'materials_lighting'
            column_1 = 'unit'
            column_2 = 'lighting_type_id'
            cost_tz_occupancy_sensor_wiring = assembly_cost(cost_info:search_tz_occupancy_sensor_wiring,
                                                            sheet_name:sheet_name,
                                                            column_1:column_1,
                                                            column_2:column_2,
                                                            quantity:quantity_tz_occupancy_sensor_wiring,
                                                            tags: tags)
            quantity_tz_occupancy_sensor_pvc_conduit = 30.0 * number_of_thermal_zones_served_by_air_loop.to_f
            search_tz_occupancy_sensor_pvc_conduit = {
                row_id_1: 'LF',
                row_id_2: 17
            }
            sheet_name = 'materials_lighting'
            column_1 = 'unit'
            column_2 = 'lighting_type_id'
            cost_tz_occupancy_sensor_pvc_conduit = assembly_cost(cost_info:search_tz_occupancy_sensor_pvc_conduit,
                                                                 sheet_name:sheet_name,
                                                                 column_1:column_1,
                                                                 column_2:column_2,
                                                                 quantity:quantity_tz_occupancy_sensor_pvc_conduit,
                                                                 tags: tags)
            dcv_cost_zone_occupancy = cost_tz_occupancy_sensor + cost_tz_occupancy_sensor_wiring + cost_tz_occupancy_sensor_pvc_conduit
            dcv_cost_total += dcv_cost_zone_occupancy + dcv_cost_box + dcv_cost_vertical_conduit + dcv_cost_roof + dcv_cost_control
            total_cost_for_air_loop = dcv_cost_zone_occupancy + dcv_cost_box + dcv_cost_vertical_conduit + dcv_cost_roof + dcv_cost_control
          elsif controller_mechanical_ventilation_system_outdoor_air_method == 'IndoorAirQualityProcedure'
            ##### Step F: Calculate total Cost for each AirLoopHVAC
            # Calculate CO2 sensor-related costs of each thermal zone served by each AirLoopHVAC (air_loop)
            quantity_tz_co2_sensor = 1.0 * number_of_thermal_zones_served_by_air_loop.to_f
            search_tz_co2_sensor = {
                row_id_1: nil,
                row_id_2: 1316
            }
            sheet_name = 'materials_hvac'
            column_1 = nil
            column_2 = 'material_id'
            cost_tz_co2_sensor = assembly_cost(cost_info:search_tz_co2_sensor,
                                               sheet_name:sheet_name,
                                               column_1:column_1,
                                               column_2:column_2,
                                               quantity:quantity_tz_co2_sensor,
                                               tags: tags)
            quantity_tz_co2_sensor_wiring = 30.0/100.0 * number_of_thermal_zones_served_by_air_loop.to_f
            search_tz_co2_sensor_wiring = {
                row_id_1: 'CLF',
                row_id_2: 10
            }
            sheet_name = 'materials_lighting'
            column_1 = 'unit'
            column_2 = 'lighting_type_id'
            cost_tz_co2_sensor_wiring = assembly_cost(cost_info:search_tz_co2_sensor_wiring,
                                                      sheet_name:sheet_name,
                                                      column_1:column_1,
                                                      column_2:column_2,
                                                      quantity:quantity_tz_co2_sensor_wiring,
                                                      tags: tags)
            quantity_tz_co2_sensor_pvc_conduit = 30.0 * number_of_thermal_zones_served_by_air_loop.to_f
            search_tz_co2_sensor_pvc_conduit = {
                row_id_1: 'LF',
                row_id_2: 17
            }
            sheet_name = 'materials_lighting'
            column_1 = 'unit'
            column_2 = 'lighting_type_id'
            cost_tz_co2_sensor_pvc_conduit = assembly_cost(cost_info:search_tz_co2_sensor_pvc_conduit,
                                                           sheet_name:sheet_name,
                                                           column_1:column_1,
                                                           column_2:column_2,
                                                           quantity:quantity_tz_co2_sensor_pvc_conduit,
                                                           tags: tags)
            dcv_cost_zone_co2 = cost_tz_co2_sensor + cost_tz_co2_sensor_wiring + cost_tz_co2_sensor_pvc_conduit
            dcv_cost_total += dcv_cost_zone_co2 + dcv_cost_box + dcv_cost_vertical_conduit + dcv_cost_roof + dcv_cost_control
            total_cost_for_air_loop = dcv_cost_zone_co2 + dcv_cost_box + dcv_cost_vertical_conduit + dcv_cost_roof + dcv_cost_control
          end

          ##### Gather information for reporting
          @costing_report['ventilation'][:demand_controlled_ventilation] << {
              air_loop_name: air_loop.name().to_s,
              controller_Outdoor_air: controller_outdoorair_name.to_s,
              controller_mechanical_ventilation_name: controller_mechanical_ventilation_name.to_s,
              controller_mechanical_ventilation_demand_controlled_ventilation_status: controller_mechanical_ventilation_demand_controlled_ventilation_status.to_s,
              controller_mechanical_ventilation_system_outdoor_air_method: controller_mechanical_ventilation_system_outdoor_air_method.to_s,
              number_of_floors_served_by_air_loop: number_floors_served_by_air_loop.to_f,
              number_of_thermal_zones_served_by_air_loop: number_of_thermal_zones_served_by_air_loop.to_f,
              number_of_junction_boxes_for_air_loop: number_floors_served_by_air_loop.to_f,
              total_cost_for_air_loop: total_cost_for_air_loop.to_f.round(2)
          }
          a += 1.0

        end

            # puts dcv_cost_total

      end #if !hvac_component.empty?

    end #air_loop.supplyComponents.each do |supply_component|

  end #model.getAirLoopHVACs.each do |air_loop|

  if a > 0.0
    ###### Gather information for reporting
    @costing_report['ventilation'][:demand_controlled_ventilation] << {
        standards_number_of_building_stories: standards_number_of_stories.to_f,
        nominal_floor_to_floor_height: nominal_flr2flr_height_m.to_f.round(2),
        total_cost_for_all_dcvs: dcv_cost_total.to_f.round(2)
    }
  end


  puts "\nDemand-controlled ventilation costing data successfully generated. Total DCV costs: $#{dcv_cost_total.round(2)}"

  return dcv_cost_total
end

#cost_audit_envelope(model, prototype_creator) ⇒ Object



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
# File 'lib/openstudio-standards/btap/costing/envelope_costing.rb', line 12

def cost_audit_envelope(model, prototype_creator)
  # These are the only envelope costing items we are considering for envelopes..
  costed_surfaces = [
      "ExteriorWall",
      "ExteriorRoof",
      "ExteriorFloor",
      "ExteriorFixedWindow",
      "ExteriorOperableWindow",
      "ExteriorSkylight",
      "ExteriorTubularDaylightDiffuser",
      "ExteriorTubularDaylightDome",
      "ExteriorDoor",
      "ExteriorGlassDoor",
      "ExteriorOverheadDoor",
      "GroundContactWall",
      "GroundContactRoof",
      "GroundContactFloor"
  ]
  costed_surfaces.each do |surface_type|
    @costing_report["envelope"]["#{surface_type.underscore}_cost"] = 0.00
    @costing_report["envelope"]["#{surface_type.underscore}_area_m2"] = 0.0
    @costing_report["envelope"]["#{surface_type.underscore}_cost_per_m2"] = 0.00
  end

  @costing_report["envelope"]["construction_costs"] = []

  # Store number of stories. Required for envelope costing logic.
  num_of_above_ground_stories = model.getBuilding.standardsNumberOfAboveGroundStories.to_i

  template_type = prototype_creator.template

  closest_loc = get_closest_cost_location(model.getWeatherFile.latitude, model.getWeatherFile.longitude)
  generate_construction_cost_database_for_city(@costing_report["city"], @costing_report["province_state"])

  totEnvCost = 0

  # Iterate through the thermal zones.
  model.getThermalZones.sort.each do |zone|
    # Iterate through spaces.
    zone.spaces.sort.each do |space|
      # Get SpaceType defined for space.. if not defined it will skip the spacetype. May have to deal with Attic spaces.
      if space.spaceType.empty? or space.spaceType.get.standardsSpaceType.empty? or space.spaceType.get.standardsBuildingType.empty?
        raise ("standards Space type and building type is not defined for space:#{space.name.get}. Skipping this space for costing.")
      end

      # Get space type standard names.
      space_type = space.spaceType.get.standardsSpaceType
      building_type = space.spaceType.get.standardsBuildingType

      # Get standard constructions based on collected information (spacetype, no of stories, etc..)
      # This is a standard way to search a hash.
      construction_set = @costing_database['raw']['construction_sets'].select { |data|
        data['template'].to_s.gsub(/\s*/, '') == template_type and
            data['building_type'].to_s.downcase == building_type.to_s.downcase and
            data['space_type'].to_s.downcase == space_type.to_s.downcase and
            data['min_stories'].to_i <= num_of_above_ground_stories and
            data['max_stories'].to_i >= num_of_above_ground_stories
      }.first


      # Create Hash to store surfaces for this space by surface type
      surfaces = {}
      #Exterior
      exterior_surfaces = BTAP::Geometry::Surfaces::filter_by_boundary_condition(space.surfaces, "Outdoors")
      surfaces["ExteriorWall"] = BTAP::Geometry::Surfaces::filter_by_surface_types(exterior_surfaces, "Wall")
      surfaces["ExteriorRoof"] = BTAP::Geometry::Surfaces::filter_by_surface_types(exterior_surfaces, "RoofCeiling")
      surfaces["ExteriorFloor"] = BTAP::Geometry::Surfaces::filter_by_surface_types(exterior_surfaces, "Floor")
      # Exterior Subsurface
      exterior_subsurfaces = exterior_surfaces.flat_map(&:subSurfaces)
      surfaces["ExteriorFixedWindow"] = BTAP::Geometry::Surfaces::filter_subsurfaces_by_types(exterior_subsurfaces, ["FixedWindow"])
      surfaces["ExteriorOperableWindow"] = BTAP::Geometry::Surfaces::filter_subsurfaces_by_types(exterior_subsurfaces, ["OperableWindow"])
      surfaces["ExteriorSkylight"] = BTAP::Geometry::Surfaces::filter_subsurfaces_by_types(exterior_subsurfaces, ["Skylight"])
      surfaces["ExteriorTubularDaylightDiffuser"] = BTAP::Geometry::Surfaces::filter_subsurfaces_by_types(exterior_subsurfaces, ["TubularDaylightDiffuser"])
      surfaces["ExteriorTubularDaylightDome"] = BTAP::Geometry::Surfaces::filter_subsurfaces_by_types(exterior_subsurfaces, ["TubularDaylightDome"])
      surfaces["ExteriorDoor"] = BTAP::Geometry::Surfaces::filter_subsurfaces_by_types(exterior_subsurfaces, ["Door"])
      surfaces["ExteriorGlassDoor"] = BTAP::Geometry::Surfaces::filter_subsurfaces_by_types(exterior_subsurfaces, ["GlassDoor"])
      surfaces["ExteriorOverheadDoor"] = BTAP::Geometry::Surfaces::filter_subsurfaces_by_types(exterior_subsurfaces, ["OverheadDoor"])

      # Ground Surfaces
      ground_surfaces = BTAP::Geometry::Surfaces::filter_by_boundary_condition(space.surfaces, "Ground")
      ground_surfaces += BTAP::Geometry::Surfaces::filter_by_boundary_condition(space.surfaces, "Foundation")
      surfaces["GroundContactWall"] = BTAP::Geometry::Surfaces::filter_by_surface_types(ground_surfaces, "Wall")
      surfaces["GroundContactRoof"] = BTAP::Geometry::Surfaces::filter_by_surface_types(ground_surfaces, "RoofCeiling")
      surfaces["GroundContactFloor"] = BTAP::Geometry::Surfaces::filter_by_surface_types(ground_surfaces, "Floor")


      # Iterate through
      costed_surfaces.each do |surface_type|
        # Get Costs for this construction type. This will get the cost for the particular construction type
        # for all rsi levels for this location. This has been collected by the API costs data. Note that a space_type
        # of "- undefined -" will create a nil construction_set!


        if construction_set.nil?
          cost_range_hash = {}
        else
          cost_range_hash = @costing_database['constructions_costs'].select { |construction|
            construction['construction_type_name'] == construction_set[surface_type] &&
                construction['province_state'] == @costing_report["province_state"] &&
                construction['city'] == @costing_report["city"]
          }
        end

        # We don't need all the information, just the rsi and cost. However, for windows rsi = 1/u_w_per_m2_k
        surfaceIsGlazing = (surface_type == 'ExteriorFixedWindow' || surface_type == 'ExteriorOperableWindow' ||
            surface_type == 'ExteriorSkylight' || surface_type == 'ExteriorTubularDaylightDiffuser' ||
            surface_type == 'ExteriorTubularDaylightDome' || surface_type == 'ExteriorGlassDoor')
        if surfaceIsGlazing
          cost_range_array = cost_range_hash.map { |cost|
            [
                (1.0 / cost['u_w_per_m2_k'].to_f),
                cost['total_cost_with_op']
            ]
          }
        else
          cost_range_array = cost_range_hash.map { |cost|
            [
                cost['rsi_k_m2_per_w'],
                cost['total_cost_with_op']
            ]
          }
        end
        # Sorted based on rsi.
        cost_range_array.sort! { |a, b| a[0] <=> b[0] }

        # Iterate through actual surfaces in the model of surface_type.
        numSurfType = 0
        surfaces[surface_type].sort.each do |surface|
          numSurfType = numSurfType + 1

          # Get RSI of existing model surface (actually returns rsi for glazings too!).
          # Make an array of constructions to use with surfaces_get_conductance method which replaces the get_rsi
          # method
          rsi = 1 / (OpenstudioStandards::Constructions.construction_get_conductance(OpenStudio::Model::getConstructionByName(surface.model, surface.construction.get.name.to_s).get))


          #Check to see if it is in range


          # Use the cost_range_array to interpolate the estimated cost for the given rsi.
          # Note that window costs in the API data use U-value, which was converted to rsi for cost_range_array above
          exterpolate_percentage_range = 30.0
          cost = interpolate(x_y_array: cost_range_array, x2: rsi, exterpolate_percentage_range: exterpolate_percentage_range)


          # If the cost is nil, that means the rsi is out of range. Flag in the report.
          if cost.nil?
            if !cost_range_array.empty?
              notes = "Warning! RSI out of the range (#{'%.2f' % rsi}) or cost is 0!. Range for #{construction_set[surface_type]} is #{'%.2f' % cost_range_array.first[0]}-#{'%.2f' % cost_range_array.last[0]}."
              cost = 0.0
            else
              notes = "No cost found for this! So Cost is set to 0.0!"
              cost = 0.0
            end
          elsif cost.nan?
            raise("the values for cost and conductance for #{construction_set[surface_type]} cannot be interpolated...cannot create an equation of a line from #{cost_range_array.sort.uniq}. Check construction database and either eliminate the errant row, or set the x value to an appropriate number. ")
          else
            #Tell user if we are extrapolating outside of library.
            array = cost_range_array.sort { |a, b| a[0] <=> b[0] }
            if rsi < (array.first[0].to_f) || rsi > (array.last[0].to_f)
              notes = "RSI out of the range (#{'%.2f' % rsi}). Range for #{construction_set[surface_type]} is #{'%.2f' % cost_range_array.first[0]}-#{'%.2f' % cost_range_array.last[0]}.Using extrapolation up to +/-30% of library boundaries. "
            else
              notes = "OK"
            end
          end

          # Calculate SHGC/film cost
          film_cost = 0.0
          if surfaceIsGlazing
            #Get SHGC from surface.
            shgc = OpenstudioStandards::Constructions.construction_get_solar_transmittance(surface.construction.get.to_Construction.get)
            # Get the closest value in materials_glazing sheet of SolarFilms.
            material_row = @costing_database["raw"]["materials_glazing"].select{ |row| row['material_type'] == 'Solarfilms' }.min_by {|row| (shgc.to_f - row['solar_heat_gain_coefficient'].to_f).abs}
            standard_film_cost = getCost(material_row['description'], material_row, 1.0)
            regional_factors = get_regional_cost_factors(@costing_report['province_state'], @costing_report['city'], material_row)
            # mult regional cost and sum costs. Zip adds the arrays together, map multiplies each row and divides by 100.0 since the regional factor is in percents.
            film_cost = standard_film_cost.zip(regional_factors).map{|cost,region_factor| cost * region_factor / 100.0}.inject(0, :+)
          end


          testSurfName = surface.name.to_s
          testSpaceName = space.name.to_s
          surfArea = surface.netArea * zone.multiplier
          surfAreaft = (OpenStudio.convert(surfArea, "m^2", "ft^2").get).to_f
          surfCost = (cost + film_cost) * surfAreaft
          totEnvCost = totEnvCost + surfCost
          name = ""

          # Bin the costing by construction standard type and rsi
          if construction_set.nil?
            name = "undefined space type_#{(1.0 / rsi).round(3)}"
          else
            name = "#{construction_set[surface_type]}"
          end
          row = @costing_report["envelope"]["construction_costs"].detect { |row| (row['name'] == name) && (row['conductance'].round(3) == ((1.0 / rsi).round(3))) }
          if row.nil?
            @costing_report["envelope"]["construction_costs"] << {'name' => name, 'conductance' => ((1.0 / rsi).round(3)), 'area' => (surfArea.round(2)), 'cost' => (surfCost.round(2)), 'cost_per_area' => (surfCost / surfArea).round(2), 'note' => "Surf ##{numSurfType}: #{notes}"}
          else
            # Not using += for @costing_report additions so that output can be properly rounded
            row['area'] = (row['area'] + surfArea).round(2)
            row['cost'] = (row['cost'] + surfCost).round(2)
            row['cost_per_area'] = ((row['cost'] / row['area']).to_f.round(2))
            row['note'] += " / #{numSurfType}: #{notes}"
          end
          # Not using += for @costing_report additions so that output can be properly rounded
          @costing_report["envelope"]["#{surface_type.underscore}_cost"] = (@costing_report["envelope"]["#{surface_type.underscore}_cost"] + surfCost).round(2)
          @costing_report["envelope"]["#{surface_type.underscore}_area_m2"] = (@costing_report["envelope"]["#{surface_type.underscore}_area_m2"] + surfArea).round(2)
          @costing_report["envelope"]["#{surface_type.underscore}_cost_per_m2"] = (@costing_report["envelope"]["#{surface_type.underscore}_cost"] / @costing_report["envelope"]["#{surface_type.underscore}_area_m2"]).round(2)
        end # surfaces of surface type
      end # surface_type
    end # spaces
  end # thermalzone

  @costing_report["envelope"]['total_envelope_cost'] = totEnvCost.to_f.round(2)
  puts "\nEnvelope costing data successfully generated. Total envelope cost is $#{totEnvCost.to_f.round(2)}"

  return totEnvCost
end

#cost_audit_led_lighting(model:, prototype_creator:) ⇒ Object



3
4
5
6
7
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
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
# File 'lib/openstudio-standards/btap/costing/led_lighting_costing.rb', line 3

def cost_audit_led_lighting(model:, prototype_creator:)
  a = 0 # This is for reporting purposes.
  standards_template = model.building.get.standardsTemplate.to_s
  if standards_template.include?('NECB')
    standards_template = standards_template.gsub(/(?<=\p{L})(?=\d)/, ' ') #insert a space between NECB and 2011/2015/2017
  end
  # puts standards_template

  #-------------------------------------------------------------------------------------------------------------------
  led_cost_total = 0.0
  #-------------------------------------------------------------------------------------------------------------------
  model.getSpaces.sort.each do |space|

    ##### Find height of the space
    max_space_height_m = 0.0
    space.surfaces.sort.select { |surface| surface.surfaceType == 'Wall' }.each do |wall_surface|
      # Find the vertex with the max z value.
      vertex_with_max_height = wall_surface.vertices.max_by(&:z)
      # Replace max if this surface has something bigger.
      max_space_height_m = vertex_with_max_height.z if vertex_with_max_height.z > max_space_height_m
    end
    # puts "max_space_height_m - #{max_space_height_m}"
    max_space_height_ft = (OpenStudio.convert(max_space_height_m, 'm', 'ft').get) #Convert height to ft
    # puts "max_space_height_ft - #{max_space_height_ft}"

    ##### Find space's floor area
    floor_surface = nil
    floor_area_m2 = 0.0
    floor_vertices = []
    space.surfaces.sort.each do |surface|
      # puts floor_area_m2
      if surface.surfaceType == 'Floor'
        floor_surface = surface
        floor_area_m2 += surface.netArea
        # puts floor_area_m2
      end
    end
    floor_area_ft2 = (OpenStudio.convert(floor_area_m2, 'm^2', 'ft^2').get) #convert floor_area_m2 to ft2
    # puts "floor_area_m2 - #{floor_area_m2}"
    # puts "floor_area_ft2 - #{floor_area_ft2}"

    ##### Find type of the space
    space_type = space.spaceType()
    # puts space_type.get
    space_type_name = space_type.get.standardsSpaceType
    # puts "space_type_name - #{space_type_name}"

    ##### Figure out if the space has LED lighting; and calculate its associated cost
    space_type.get.lights.sort.each do |light|
      space_lights_definition = light.lightsDefinition
      space_lights_definition_name = space_lights_definition.name
      # puts space_lights_definition_name

      if space_lights_definition_name.to_s.include?('LED lighting')
        led_cost_space = 0.0
        ##### COSTING-related step: Find fixture type that should be used in the space based on space_type, template, and lights_type
        search_fixture_type = {
            row_id_1: space_type_name,
            row_id_2: standards_template,
            row_id_3: 'LED'
        }
        sheet_name = 'lighting_sets'
        if max_space_height_ft < 7.88
          column_search = 'Fixture_type_less_than_7.88ft_ht'
        elsif max_space_height_ft >= 7.88 && max_space_height_ft < 15.75
          column_search = 'Fixture_type_7.88_to_15.75ft_ht'
        else #i.e. max_space_height_ft >= 15.75ft_ht
          column_search = 'Fixture_type_greater_than_>15.75ft_ht'
        end
        row_search_1 = 'space_type'
        row_search_2 = 'template'
        row_search_3 = 'Type'
        fixture_type = get_fixture_type_id(fixture_info: search_fixture_type, sheet_name: sheet_name, row_name_1: row_search_1, row_name_2: row_search_2, row_name_3: row_search_3, column_search: column_search)
        # puts "fixture_type - #{fixture_type}"

        ##### COSTING-related step: Find 'id_layers' and 'Id_layers_quantity_multipliers' based on fixture_type; and calculate LED cost
        search_id_layers = @costing_database['raw']['lighting'].select { |data|
          data['lighting_type_id'].to_f.round(1) == fixture_type.to_f.round(1)
        }.first
        if search_id_layers.nil?
          puts("No data found for #{search_id_layers}!")
          raise
        end
        ids = search_id_layers['id_layers'].to_s.split(',')
        # puts "id_layers - #{ids}"

        search_id_layers_quantity_multipliers = @costing_database['raw']['lighting'].select { |data|
          data['lighting_type_id'].to_f.round(1) == fixture_type.to_f.round(1)
        }.first
        if search_id_layers_quantity_multipliers.nil?
          puts("No data found for #{search_id_layers_quantity_multipliers}!")
          raise
        end
        id_quants = search_id_layers_quantity_multipliers['Id_layers_quantity_multipliers'].to_s.split(',')
        # puts "id_layers_quantity_multipliers - #{id_quants}"

        overall_mult = 1.0

        index_id_quant = 0.0
        ids.each do |id|
          quantity_led = id_quants[index_id_quant].to_f * overall_mult * floor_area_ft2
          # id_description_search = @costing_database['raw']['materials_lighting'].select { |data|
          #   data['lighting_type_id'].to_f.round(1) == id.to_f.round(1)
          # }.first
          # id_description = id_description_search['description']

          search_led = {
              row_id_1: nil,
              row_id_2: id
          }
          sheet_name = 'materials_lighting'
          column_1 = nil
          column_2 = 'lighting_type_id'
          tags = ['lighting', 'led_lighting']
          led_costing = assembly_cost(cost_info:search_led,
                                      sheet_name:sheet_name,
                                      column_1:column_1,
                                      column_2:column_2,
                                      quantity:quantity_led,
                                      tags: tags)
          led_cost_space += led_costing
          index_id_quant += 1.0
        end

        led_cost_total += led_cost_space

        @costing_report["lighting"]["led_lighting"] << {
            'space' => space.name.to_s,
            'led_costing' => led_cost_space,
        }

        a += 1

      end #if space_lights_definition_name.to_s.include?('LED lighting')
    end #space_type.get.lights.sort.each do |light|
    #-------------------------------------------------------------------------------------------------------------------

  end #model.getSpaces.sort.each do |space|
  #-------------------------------------------------------------------------------------------------------------------

  if a > 0
    @costing_report["lighting"]["led_lighting"] << {
        'total_cost' => led_cost_total
    }
  end

  puts "\nLED lighting costing data successfully generated. Total LED lighting costs: $#{led_cost_total.round(2)}"

  return led_cost_total

end

#cost_audit_lighting(model, prototype_creator) ⇒ Object



3
4
5
6
7
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
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
# File 'lib/openstudio-standards/btap/costing/lighting_costing.rb', line 3

def cost_audit_lighting(model, prototype_creator)
  # Store number of stories. Required for envelope costing logic.
  num_of_above_ground_stories = model.getBuilding.standardsNumberOfAboveGroundStories.to_i

  template_type = prototype_creator.template

  closest_loc = get_closest_cost_location(model.getWeatherFile.latitude, model.getWeatherFile.longitude)
  generate_construction_cost_database_for_city(@costing_report["city"],@costing_report["province_state"])

  totLgtCost = 0

  # daylighting sensor control costing
  dsc_cost_total = cost_audit_daylighting_sensor_control(model: model, prototype_creator: prototype_creator)

  # led lighting costing
  led_cost_total = cost_audit_led_lighting(model: model, prototype_creator: prototype_creator)

  totLgtCost += dsc_cost_total

  # Iterate through the thermal zones.

  #Create Zonal report.
  @costing_report["lighting"]["fixture_report"] = []
  @costing_report["lighting"]["space_report"] = []
  model.getThermalZones.sort.each do |zone|
    # Iterate through spaces.
    spaceNum = 0  # Counting number of spaces for reporting
    total_with_region = 0
    zone.spaces.sort.each do |space|
      spaceNum += 1  # Counting number of spaces for reporting
      # Get SpaceType defined for space.. if not defined it will skip the spacetype. May have to deal with Attic spaces.
      if space.spaceType.empty? or space.spaceType.get.standardsSpaceType.empty? or space.spaceType.get.standardsBuildingType.empty?
        raise ("standards Space type and building type is not defined for space:#{space.name.get}. Skipping this space for costing.")
      end

      # Get space type standard names.
      space_type = space.spaceType.get.standardsSpaceType
      building_type = space.spaceType.get.standardsBuildingType
      space_light = space.spaceType.get.lights
      space_has_no_light = space_light.empty?

      # Figure out what light_type is used in the building
      light_type = 'Nil'
      if space_has_no_light == false
        space.spaceType.get.lights.sort.each do |light|
          space_lights_definition = light.lightsDefinition
          space_lights_definition_name = space_lights_definition.name
          if space_lights_definition_name.to_s.include?('LED lighting') or template_type=='NECB2020' # Note: NECB2020's lights default is LED
            light_type = 'LED'
          else
            light_type = 'CFL'
          end
        end
      end

      # Get standard lighting sets based on collected information (spacetype, no of stories, etc..)
      if space_has_no_light == false
        lighting_set = @costing_database['raw']['lighting_sets'].detect {|data|
          data['template'].to_s.gsub(/\s*/, '') == template_type and
            data['building_type'].to_s.downcase == building_type.to_s.downcase and
            data['space_type'].to_s.downcase == space_type.to_s.downcase and
            data['Type'].to_s.downcase == light_type.to_s.downcase
        }
      else
        lighting_set = @costing_database['raw']['lighting_sets'].detect {|data|
          data['template'].to_s.gsub(/\s*/, '') == template_type and
            data['building_type'].to_s.downcase == building_type.to_s.downcase and
            data['space_type'].to_s.downcase == space_type.to_s.downcase
        }
      end

      # Determine average space height using space volume and floor area (convert to feet)
      ceilHgt, flrArea = 0
      if space.floorArea > 0
        ceilHgt = space.volume / space.floorArea
        ceilHgt = OpenStudio.convert(ceilHgt,"m","ft").get
        flrArea = OpenStudio.convert(space.floorArea,"m^2","ft^2").get
      end

      # Find Fixture type for this space ceiling height (ft)
      fixtureType = 'Nil'
      fixture_description = ""
      if lighting_set.nil?
        raise("Error: lighting_set empty for zone #{zone.name.to_s} and space type #{building_type} #{space_type.to_s}!")
      else
        if ceilHgt > 0 && ceilHgt < 7.88
          fixtureType = lighting_set["Fixture_type_less_than_7.88ft_ht"]
        elsif ceilHgt >= 7.88 && ceilHgt <= 15.75
          fixtureType = lighting_set["Fixture_type_7.88_to_15.75ft_ht"]
        elsif ceilHgt > 15.75
          fixtureType = lighting_set["Fixture_type_greater_than_>15.75ft_ht"]
        end
      end

      # Costs are 0 for 'Nil' because no fixture type due to either zero floor area, zero ceiling height or a 'Nil'
      # setting for fixture type in lighting_sets sheet ("- undefined -" space)
      if fixtureType != 'Nil'
        # Get lighting type sets based on fixtureType
        lighting_type = @costing_database['raw']['lighting'].select {|lighting_layer_data|
          lighting_layer_data['lighting_type_id'].to_s == fixtureType.to_s
        }.first

        # Scan through layer IDs in id_layers field to get API data from materials_lighting sheet
        materials_lighting_database = @costing_database["raw"]["materials_lighting"]

        layer_type_IDs = []
        layer_type_mult = []
        layer_MaterialCost = 0
        layer_LabourCost = 0

        if lighting_type["id_layers"].empty?
          raise ("Lighting type layers list for lighting type ID #{fixtureType} is empty.")
        else
          layer_type_IDs = lighting_type["id_layers"].split(/\s*,\s*/)
          layer_type_mult = lighting_type["Id_layers_quantity_multipliers"].split(/\s*,\s*/)
          lighting_layers = layer_type_IDs.zip(layer_type_mult).to_h

          lighting_layers.each do |layer_id, layer_mult|
            # Note: The column in the spreadsheet labelled "lighting_type_id" is mislabelled and should
            # really be "lighting_type_layer_id" but left it as-is (below).
            lighting_material = materials_lighting_database.find do |data|
              data["lighting_type_id"].to_s == layer_id.to_s
            end
            if lighting_material.nil?
              puts "Lighting material error..could not find lighting material #{layer_id} in #{materials_lighting_database}"
              raise
            else
              costing_data = @costing_database['costs'].detect {|data| data['id'].to_s.upcase == lighting_material['id'].to_s.upcase}
              if costing_data.nil?
                puts "Lighting material id #{lighting_material['id']} not found in costing data. Skipping."
                raise
              else
                # Get cost information from lookup.
                material_cost = costing_data['baseCosts']['materialOpCost'].to_f * layer_mult.to_f * flrArea * zone.multiplier
                labour_cost = costing_data['baseCosts']['laborOpCost'].to_f * layer_mult.to_f * flrArea * zone.multiplier
                layer_MaterialCost += material_cost
                layer_LabourCost += labour_cost

                regional_material, regional_installation =
                  get_regional_cost_factors(@costing_report["province_state"], @costing_report["city"], lighting_material)
                total_with_region = layer_MaterialCost * regional_material / 100.0 + layer_LabourCost * regional_installation / 100.0

                # Gather info for costed items output file
                tags = ['lighting','necb_default']
                add_costed_item(material_id: costing_data['id'],
                                quantity: layer_mult.to_f * flrArea * zone.multiplier,
                                material_mult: costing_data['material_mult'].to_f,
                                labour_mult: costing_data['labour_mult'].to_f,
                                equip_mult: 1.0,
                                tags: tags)


              end # costing_data Nil check
            end # lighting_material Nil check
          end # lighting layer ids loop

          totLgtCost += total_with_region
          fixture_description = lighting_type["description"]
        end # lighting layer ids check
      end # fixtureType Nil check

      zName = zone.name.to_s

      # Create Lighting space report.
      @costing_report["lighting"]["space_report"] << {
        'space' => space.name.to_s,
        'zone' => zone.name.to_s,
        'building_type' =>space.spaceType.get.standardsBuildingType.to_s,
        'space_type' => space.spaceType.get.standardsSpaceType.to_s,
        'zone_multiplier' => space.multiplier,
        'fixture_type' => fixtureType,
        'fixture_desciption' => fixture_description,
        'height_avg_ft' => ceilHgt.round(1),
        'floor_area_ft2' => (flrArea * space.multiplier).round(1),
        'cost' => total_with_region.round(2),
        'cost_per_ft2' => (total_with_region / ( flrArea * space.multiplier )).round(2),
        'note' => ""
      }

      # Create Lighting Zonal report.
      lighting_fixture_report = @costing_report["lighting"]["fixture_report"].detect {|fixture_report| fixture_report["fixture_type"] == fixtureType}
      unless lighting_fixture_report.nil?
        lighting_fixture_report['floor_area_ft2'] = (lighting_fixture_report['floor_area_ft2'] + (flrArea * space.multiplier)).round(1)
        lighting_fixture_report['cost'] = (lighting_fixture_report['cost'] + total_with_region).round(2)
        lighting_fixture_report['cost_per_ft2'] = (lighting_fixture_report['cost'] / lighting_fixture_report['floor_area_ft2']).round(2)
        lighting_fixture_report['spaces'] << space.name.get
        lighting_fixture_report['number_of_spaces'] = lighting_fixture_report['spaces'].size
      else
        @costing_report["lighting"]["fixture_report"] << {
          'fixture_type' => fixtureType,
          'fixture_description' => fixture_description,
          'floor_area_ft2' => (flrArea * space.multiplier).round(1),
          'cost' => total_with_region.round(2),
          'cost_per_ft2' => (total_with_region / (flrArea * space.multiplier)).round(2),
          'spaces' => [space.name.get],
          'number_of_spaces' => 1
        }
      end
    end # Spaces loop
  end # thermalzone loop

  @costing_report["lighting"]['total_lighting_cost'] = totLgtCost.round(2)
  puts "\nLighting costing data successfully generated. Total lighting cost is $#{totLgtCost.round(2)}"

  return totLgtCost
end

#cost_audit_nv(model:, prototype_creator:) ⇒ Object



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

def cost_audit_nv(model:, prototype_creator:)
  @costing_report['ventilation'][:natural_ventilation] = []
  #TODO: expected results file will be updated once labour cost for zn controller, sensor, and usb are updated in costing database

  nv_total_cost = 0.0
  nv_total_cost_tz = 0.0
  nv_total_cost_ahu = 0.0

  nv_airloop_vertical_conduit_hash = {}
  nv_airloop_vertical_conduit_wiring_hash = {}
  nv_airloop_vertical_conduit_box_hash = {}
  nv_airloop_roof_conduit_hash = {}
  nv_airloop_roof_wiring_hash = {}
  nv_airloop_roof_box_hash = {}
  nv_airloop_controller_hash = {}

  #-----------------------------------------------------------------------------------------------------------------
  ##### Get number of stories and nominal floor to floor height.
  ##### These inputs are required for the calculation of the length of conduit for each AirLoopHVAC
  ##### if any thermal zone served by that AirLoopHVAC has the potential for NV.
  util_dist, ht_roof, nominal_flr2flr_height_ft, horizontal_dist, standards_number_of_stories, mechRmInBsmt = getGeometryData(model, prototype_creator)

  #-----------------------------------------------------------------------------------------------------------------
  ##### Find which airloop serves the thermal zone
  thermal_zones_airloop_hash = {}
  model.getAirLoopHVACs.sort.each do |air_loop|
    # puts air_loop.name().to_s
    air_loop.thermalZones.sort.each do |thermal_zone|
      # puts thermal_zone.name().to_s
      thermal_zones_airloop_hash[thermal_zone.name.to_s] = air_loop.name.to_s
    end
  end
  # puts "thermal_zones_airloop_hash is #{thermal_zones_airloop_hash}"

  #-----------------------------------------------------------------------------------------------------------------
  ##### Loop through ZoneHVACEquipmentLists to see which thermal zone(s) has(have) been set to use NV where applicable.
  model.getZoneHVACEquipmentLists.sort.each do |zone_hvac_equipment_list|
    # puts "zone_hvac_equipment_list.name  is #{zone_hvac_equipment_list.name}"

    nv_exist = 0.0 #this variable is to check the thermal zone has been set to use NV where applicable. (1.0: NV is allowed; 0.0: NV is not allowed)

    ### Loop through ZoneHVACEquipmentLists to see which thermal zone(s) has(have) ZoneVentilationWindandStackOpenArea
    hvac_equipment = zone_hvac_equipment_list.equipment
    for i in 0..hvac_equipment.length()
      unless hvac_equipment[i].nil?
        if hvac_equipment[i].to_ZoneVentilationWindandStackOpenArea.is_initialized
          nv_exist = 1.0 #this means that the thermal zone has NV.
        end
      end
    end
    # puts "Is NV allowed? #{nv_exist}"

    ### Loop through thermal zone's spaces to count how many spaces of them have windows to exterior
    if nv_exist == 1.0
      tags = ['ventilation', 'natural_ventilation']
      thermal_zone = zone_hvac_equipment_list.thermalZone
      thermal_zone_name = thermal_zone.name
      thermal_zone_multiplier = thermal_zone.multiplier()
      # puts "thermal_zone_name is #{thermal_zone.name}"

      ##### Find which airloop serves the thermal zone
      thermal_zone_sys = thermal_zones_airloop_hash[thermal_zone_name.to_s]
      # puts "thermal_zone_sys is #{thermal_zone_sys}"

      if !thermal_zone_sys.nil?

        ################################################## Step I: costing for each thermal zone ############################################################
        ##### costing for each thermal zone:  natural ventilation controller -------------------------------------------------------------------------------------------------------------------
        quantity_tz_nv_controller = 1.0 * thermal_zone_multiplier
        # puts "quantity_tz_nv_controller is #{quantity_tz_nv_controller}"
        search_tz_nv_controller = {
            row_id_1: 'nat_vent_control',
            row_id_2: 1537
        }
        sheet_name = 'materials_hvac'
        column_1 = 'Material'
        column_2 = 'material_id'
        nv_costing_tz_nv_controller = assembly_cost(cost_info:search_tz_nv_controller,
                                                    sheet_name:sheet_name,
                                                    column_1:column_1,
                                                    column_2:column_2,
                                                    quantity: quantity_tz_nv_controller,
                                                    tags: tags)
        # puts "quantity_tz_nv_controller is #{quantity_tz_nv_controller}"
        # puts "nv_costing_tz_nv_controller is #{nv_costing_tz_nv_controller}"

        ##### costing for each thermal zone: natural ventilation sensor -------------------------------------------------------------------------------------------------------------------
        quantity_tz_nv_sensor = 1.0 * thermal_zone_multiplier
        search_tz_nv_sensor = {
            row_id_1: 'nat_vent_sensor',
            row_id_2: 1538
        }
        sheet_name = 'materials_hvac'
        column_1 = 'Material'
        column_2 = 'material_id'
        nv_costing_tz_nv_sensor = assembly_cost(cost_info:search_tz_nv_sensor,
                                                sheet_name:sheet_name,
                                                column_1:column_1,
                                                column_2:column_2,
                                                quantity: quantity_tz_nv_sensor,
                                                tags: tags)
        # puts "quantity_tz_nv_sensor is #{quantity_tz_nv_sensor}"
        # puts "nv_costing_tz_nv_sensor is #{nv_costing_tz_nv_sensor}"

        ##### costing for each thermal zone: natural ventilation USB -------------------------------------------------------------------------------------------------------------------
        quantity_tz_nv_usb = 1.0 * thermal_zone_multiplier
        search_tz_nv_usb = {
            row_id_1: 'nat_vent_usb',
            row_id_2: 1539
        }
        sheet_name = 'materials_hvac'
        column_1 = 'Material'
        column_2 = 'material_id'
        nv_costing_tz_nv_usb = assembly_cost(cost_info:search_tz_nv_usb,
                                             sheet_name:sheet_name,
                                             column_1:column_1,
                                             column_2:column_2,
                                             quantity: quantity_tz_nv_usb,
                                             tags: tags)
        # puts "quantity_tz_nv_usb is #{quantity_tz_nv_usb}"
        # puts "nv_costing_tz_nv_usb is #{nv_costing_tz_nv_usb}"

        ##### costing for each thermal zone: Tin_sensor_wiring -------------------------------------------------------------------------------------------------------------------
        # Note: assuming distance of 30 ft to each thermal zone per floor
        # for each outdoor sensor (Tout and wind speed) and
        # Tin sensor to the natural ventilation controller,
        # total wiring is 90 ft in below equation.
        quantity_tz_Tin_sensor_wiring = (90.0/100.0) * thermal_zone_multiplier #unit: CLF (hundred linear feet)
        search_tz_Tin_sensor_wiring = {
            row_id_1: 'CLF',
            row_id_2: 10
        }
        sheet_name = 'materials_lighting'
        column_1 = 'unit'
        column_2 = 'lighting_type_id'
        nv_costing_tz_Tin_sensor_wiring = assembly_cost(cost_info:search_tz_Tin_sensor_wiring,
                                                        sheet_name:sheet_name,
                                                        column_1:column_1,
                                                        column_2:column_2,
                                                        quantity: quantity_tz_Tin_sensor_wiring,
                                                        tags: tags)
        # puts "quantity_tz_Tin_sensor_wiring is #{quantity_tz_Tin_sensor_wiring}"
        # puts "nv_costing_tz_Tin_sensor_wiring is #{nv_costing_tz_Tin_sensor_wiring}"

        ##### costing for each thermal zone: PVC_conduit -------------------------------------------------------------------------------------------------------------------
        quantity_tz_Tin_sensor_conduit = 2.0 * 30.0 * thermal_zone_multiplier #unit: LF (linear feet)
        search_tz_Tin_sensor_conduit = {
            row_id_1: 'LF',
            row_id_2: 17
        }
        sheet_name = 'materials_lighting'
        column_1 = 'unit'
        column_2 = 'lighting_type_id'
        nv_costing_tz_Tin_sensor_conduit = assembly_cost(cost_info:search_tz_Tin_sensor_conduit,
                                                         sheet_name:sheet_name,
                                                         column_1:column_1,
                                                         column_2:column_2,
                                                         quantity: quantity_tz_Tin_sensor_conduit,
                                                         tags: tags)
        # puts "quantity_tz_Tin_sensor_conduit is #{quantity_tz_Tin_sensor_conduit}"
        # puts "nv_costing_tz_Tin_sensor_conduit is #{nv_costing_tz_Tin_sensor_conduit}"

        ##### costing for each thermal zone: junction box -------------------------------------------------------------------------------------------------------------------
        quantity_tz_Tin_sensor_box = 1.0 * thermal_zone_multiplier #unit: Ea
        search_tz_Tin_sensor_box = {
            row_id_1: 'Ea',
            row_id_2: 14
        }
        sheet_name = 'materials_lighting'
        column_1 = 'unit'
        column_2 = 'lighting_type_id'
        nv_costing_tz_Tin_sensor_box = assembly_cost(cost_info:search_tz_Tin_sensor_box,
                                                     sheet_name:sheet_name,
                                                     column_1:column_1,
                                                     column_2:column_2,
                                                     quantity: quantity_tz_Tin_sensor_box,
                                                     tags: tags)
        # puts "quantity_tz_Tin_sensor_box is #{quantity_tz_Tin_sensor_box}"
        # puts "nv_costing_tz_Tin_sensor_box is #{nv_costing_tz_Tin_sensor_box}"

        ##### costing for each thermal zone: duct_damper_motor -------------------------------------------------------------------------------------------------------------------
        # calculate how many dampers are needed, depending on the system type
        nv_a = ['sys_1', 'sys_2', 'sys_3', 'sys_4', 'sys_5', 'sys_7']
        nv_b = ['sys_6']
        nv_c = thermal_zone_sys
        if nv_a.any? { |s| nv_c.include? s}
          damper_mult = 1.0
        elsif nv_b.any? { |s| nv_c.include? s}
          damper_mult = 2.0
        end
        # puts "damper_mult is #{damper_mult}"

        quantity_tz_duct_damper_motor = 1.0 * damper_mult * thermal_zone_multiplier #unit: Ea
        search_tz_duct_damper_motor = {
            row_id_1: 'duct_damper_motor',
            row_id_2: 6
        }
        sheet_name = 'materials_hvac'
        column_1 = 'Material'
        column_2 = 'Size'
        nv_costing_tz_duct_damper_motor = assembly_cost(cost_info:search_tz_duct_damper_motor,
                                                        sheet_name:sheet_name,
                                                        column_1:column_1,
                                                        column_2:column_2,
                                                        quantity: quantity_tz_duct_damper_motor,
                                                        tags: tags)
        # puts "quantity_tz_duct_damper_motor is #{quantity_tz_duct_damper_motor}"
        # puts "nv_costing_tz_duct_damper_motor is #{nv_costing_tz_duct_damper_motor}"

        ##### costing for each thermal zone: duct_damper_wiring -------------------------------------------------------------------------------------------------------------------
        quantity_tz_duct_damper_wiring = (30.0/100.0) * damper_mult * thermal_zone_multiplier #unit: CLF
        search_tz_duct_damper_wiring = {
            row_id_1: 'CLF',
            row_id_2: 10
        }
        sheet_name = 'materials_lighting'
        column_1 = 'unit'
        column_2 = 'lighting_type_id'
        nv_costing_tz_duct_damper_wiring = assembly_cost(cost_info:search_tz_duct_damper_wiring,
                                                         sheet_name:sheet_name,
                                                         column_1:column_1,
                                                         column_2:column_2,
                                                         quantity: quantity_tz_duct_damper_wiring,
                                                         tags: tags)
        # puts "quantity_tz_duct_damper_wiring is #{quantity_tz_duct_damper_wiring}"
        # puts "nv_costing_tz_duct_damper_wiring is #{nv_costing_tz_duct_damper_wiring}"

        ##### costing for each thermal zone: duct_damper_conduit -------------------------------------------------------------------------------------------------------------------
        quantity_tz_duct_damper_conduit = 30.0 * damper_mult * thermal_zone_multiplier #unit: CLF
        search_tz_duct_damper_conduit = {
            row_id_1: 'LF',
            row_id_2: 17
        }
        sheet_name = 'materials_lighting'
        column_1 = 'unit'
        column_2 = 'lighting_type_id'
        nv_costing_tz_duct_damper_conduit = assembly_cost(cost_info:search_tz_duct_damper_conduit,
                                                          sheet_name:sheet_name,
                                                          column_1:column_1,
                                                          column_2:column_2,
                                                          quantity: quantity_tz_duct_damper_conduit,
                                                          tags: tags)
        # puts "quantity_tz_duct_damper_conduit is #{quantity_tz_duct_damper_conduit}"
        # puts "nv_costing_tz_duct_damper_conduit is #{nv_costing_tz_duct_damper_conduit}"

        ##### costing for each thermal zone: duct_damper_box -------------------------------------------------------------------------------------------------------------------
        quantity_tz_duct_damper_box = 1.0 * damper_mult * thermal_zone_multiplier #unit: CLF
        search_tz_duct_damper_box = {
            row_id_1: 'Ea',
            row_id_2: 14
        }
        sheet_name = 'materials_lighting'
        column_1 = 'unit'
        column_2 = 'lighting_type_id'
        nv_costing_tz_duct_damper_box = assembly_cost(cost_info:search_tz_duct_damper_box,
                                                      sheet_name:sheet_name,
                                                      column_1:column_1,
                                                      column_2:column_2,
                                                      quantity: quantity_tz_duct_damper_box,
                                                      tags: tags)
        # puts "quantity_tz_duct_damper_box is #{quantity_tz_duct_damper_box}"
        # puts "nv_costing_tz_duct_damper_box is #{nv_costing_tz_duct_damper_box}"

        ################################################## Step II: costing for each AirLoopHVAC - Vertical Conduit ############################################################
        ##### Note: This is completed twice for each AirLoopHVAC:
        # (1) for the wiring and conduit to the rooftop AHU
        # (2) for the wiring and conduit to the rooftop outdoor air temperature and wind speed sensors
        ##### costing for each AirLoopHVAC-VerticalConduit: a single conduit runs the entire height of the building -------------------------------------------------------------------------------------------------------------------
        quantity_nv_vertical_conduit = 2.0 * nominal_flr2flr_height_ft * standards_number_of_stories
        search_nv_vertical_conduit = {
            row_id_1: 'LF',
            row_id_2: 13
        }
        sheet_name = 'materials_lighting'
        column_1 = 'unit'
        column_2 = 'lighting_type_id'
        if nv_airloop_vertical_conduit_hash.key?(thermal_zone_sys.to_s) == false
          nv_costing_vertical_conduit = assembly_cost(cost_info:search_nv_vertical_conduit,
                                                      sheet_name:sheet_name,
                                                      column_1:column_1,
                                                      column_2:column_2,
                                                      quantity: quantity_nv_vertical_conduit,
                                                      tags: tags)
          nv_airloop_vertical_conduit_hash[thermal_zone_sys.to_s] = nv_costing_vertical_conduit
        else
          nv_costing_vertical_conduit = 0.0
        end
        # puts "quantity_nv_vertical_conduit is #{quantity_nv_vertical_conduit}"
        # puts "nv_costing_vertical_conduit is #{nv_costing_vertical_conduit}"

        ##### costing for each AirLoopHVAC-VerticalConduit: wiring -------------------------------------------------------------------------------------------------------------------
        quantity_nv_vertical_conduit_wiring = 2.0 * nominal_flr2flr_height_ft * standards_number_of_stories / 100.0
        search_nv_vertical_conduit_wiring = {
            row_id_1: 'CLF',
            row_id_2: 10
        }
        sheet_name = 'materials_lighting'
        column_1 = 'unit'
        column_2 = 'lighting_type_id'
        if nv_airloop_vertical_conduit_wiring_hash.key?(thermal_zone_sys.to_s) == false
          nv_costing_vertical_conduit_wiring = assembly_cost(cost_info:search_nv_vertical_conduit_wiring,
                                                             sheet_name:sheet_name,
                                                             column_1:column_1,
                                                             column_2:column_2,
                                                             quantity: quantity_nv_vertical_conduit_wiring,
                                                             tags: tags)
          nv_airloop_vertical_conduit_wiring_hash[thermal_zone_sys.to_s] = nv_costing_vertical_conduit_wiring
        else
          nv_costing_vertical_conduit_wiring = 0.0
        end
        # puts "quantity_nv_vertical_conduit_wiring is #{quantity_nv_vertical_conduit_wiring}"
        # puts "nv_costing_vertical_conduit_wiring is #{nv_costing_vertical_conduit_wiring}"

        ##### costing for each AirLoopHVAC-VerticalConduit: box -------------------------------------------------------------------------------------------------------------------
        quantity_nv_vertical_conduit_box = 2.0 * standards_number_of_stories
        search_nv_vertical_conduit_box = {
            row_id_1: 'Ea',
            row_id_2: 14
        }
        sheet_name = 'materials_lighting'
        column_1 = 'unit'
        column_2 = 'lighting_type_id'
        if nv_airloop_vertical_conduit_box_hash.key?(thermal_zone_sys.to_s) == false
          nv_costing_vertical_conduit_box = assembly_cost(cost_info:search_nv_vertical_conduit_box,
                                                          sheet_name:sheet_name,
                                                          column_1:column_1,
                                                          column_2:column_2,
                                                          quantity: quantity_nv_vertical_conduit_box,
                                                          tags: tags)
          nv_airloop_vertical_conduit_box_hash[thermal_zone_sys.to_s] = nv_costing_vertical_conduit_box
        else
          nv_costing_vertical_conduit_box = 0.0
        end
        # puts "quantity_nv_vertical_conduit_box is #{quantity_nv_vertical_conduit_box}"
        # puts "nv_costing_vertical_conduit_box is #{nv_costing_vertical_conduit_box}"

        ################################################## Step III: costing for each AirLoopHVAC - Roof  ############################################################
        ##### costing for each AirLoopHVAC-Roof: conduit -------------------------------------------------------------------------------------------------------------------
        quantity_nv_roof_conduit = 20.0
        search_nv_roof_conduit = {
            row_id_1: 'LF',
            row_id_2: 13
        }
        sheet_name = 'materials_lighting'
        column_1 = 'unit'
        column_2 = 'lighting_type_id'
        if nv_airloop_roof_conduit_hash.key?(thermal_zone_sys.to_s) == false
          nv_costing_roof_conduit = assembly_cost(cost_info:search_nv_roof_conduit,
                                                  sheet_name:sheet_name,
                                                  column_1:column_1,
                                                  column_2:column_2,
                                                  quantity: quantity_nv_roof_conduit,
                                                  tags: tags)
          nv_airloop_roof_conduit_hash[thermal_zone_sys.to_s] = nv_costing_roof_conduit
        else
          nv_costing_roof_conduit = 0.0
        end
        # puts "quantity_nv_roof_conduit is #{quantity_nv_roof_conduit}"
        # puts "nv_costing_roof_conduit is #{nv_costing_roof_conduit}"

        ##### costing for each AirLoopHVAC-Roof: wiring -------------------------------------------------------------------------------------------------------------------
        quantity_nv_roof_wiring = 20.0 / 100.0
        search_nv_roof_wiring = {
            row_id_1: 'CLF',
            row_id_2: 10
        }
        sheet_name = 'materials_lighting'
        column_1 = 'unit'
        column_2 = 'lighting_type_id'
        if nv_airloop_roof_wiring_hash.key?(thermal_zone_sys.to_s) == false
          nv_costing_roof_wiring = assembly_cost(cost_info:search_nv_roof_wiring,
                                                 sheet_name:sheet_name,
                                                 column_1:column_1,
                                                 column_2:column_2,
                                                 quantity: quantity_nv_roof_wiring,
                                                 tags: tags)
          nv_airloop_roof_wiring_hash[thermal_zone_sys.to_s] = nv_costing_roof_wiring
        else
          nv_costing_roof_wiring = 0.0
        end
        # puts "quantity_nv_roof_wiring is #{quantity_nv_roof_wiring}"
        # puts "nv_costing_roof_wiring is #{nv_costing_roof_wiring}"

        ##### costing for each AirLoopHVAC-Roof: box -------------------------------------------------------------------------------------------------------------------
        quantity_nv_roof_box = 1.0
        search_nv_roof_box = {
            row_id_1: 'Ea',
            row_id_2: 14
        }
        sheet_name = 'materials_lighting'
        column_1 = 'unit'
        column_2 = 'lighting_type_id'
        if nv_airloop_roof_box_hash.key?(thermal_zone_sys.to_s) == false
          nv_costing_roof_box = assembly_cost(cost_info:search_nv_roof_box,
                                              sheet_name:sheet_name,
                                              column_1:column_1,
                                              column_2:column_2,
                                              quantity: quantity_nv_roof_box,
                                              tags: tags)
          nv_airloop_roof_box_hash[thermal_zone_sys.to_s] = nv_costing_roof_box
        else
          nv_costing_roof_box = 0.0
        end
        # puts "quantity_nv_roof_box is #{quantity_nv_roof_box}"
        # puts "nv_costing_roof_box is #{nv_costing_roof_box}"

        ################################################## Step IV: costing for each AirLoopHVAC - Controller  ############################################################
        quantity_nv_ahu_controller = 1.0
        search_nv_ahu_controller = {
            row_id_1: 'Ea',
            row_id_2: 400
        }
        sheet_name = 'materials_lighting'
        column_1 = 'unit'
        column_2 = 'lighting_type_id'
        if nv_airloop_controller_hash.key?(thermal_zone_sys.to_s) == false
          nv_costing_ahu_controller = assembly_cost(cost_info:search_nv_ahu_controller,
                                                    sheet_name:sheet_name,
                                                    column_1:column_1,
                                                    column_2:column_2,
                                                    quantity: quantity_nv_ahu_controller,
                                                    tags: tags)
          nv_airloop_controller_hash[thermal_zone_sys.to_s] = nv_costing_ahu_controller
        else
          nv_costing_ahu_controller = 0.0
        end
        # puts "quantity_nv_ahu_controller is #{quantity_nv_ahu_controller}"
        # puts "nv_costing_ahu_controller is #{nv_costing_ahu_controller}"

        ################################################## Step V: costing for each thermal zone (total); also all previous thermal zones including current thermal zone ############################################################
        costing_for_each_ThermalZone = nv_costing_tz_nv_controller +
                                       nv_costing_tz_nv_sensor +
                                       nv_costing_tz_nv_usb +
                                       nv_costing_tz_Tin_sensor_wiring +
                                       nv_costing_tz_Tin_sensor_conduit +
                                       nv_costing_tz_Tin_sensor_box +
                                       nv_costing_tz_duct_damper_motor +
                                       nv_costing_tz_duct_damper_wiring +
                                       nv_costing_tz_duct_damper_conduit +
                                       nv_costing_tz_duct_damper_box
        # puts "costing_for_each_ThermalZone is #{costing_for_each_ThermalZone}"
        nv_total_cost_tz += costing_for_each_ThermalZone

        ################################################## Step VI: costing for each AirLoopHVAC - Total  ############################################################
        costing_for_each_AirLoopHVAC = nv_costing_vertical_conduit +
                                       nv_costing_vertical_conduit_wiring +
                                       nv_costing_vertical_conduit_box +
                                       nv_costing_roof_conduit +
                                       nv_costing_roof_wiring +
                                       nv_costing_roof_box +
                                       nv_costing_ahu_controller

        ##### Gather information for reporting
        @costing_report['ventilation'][:natural_ventilation] << {
            zone: thermal_zone_name.to_s,
            ahu_serves_the_zone: thermal_zone_sys,
            costing_for_the_zone: costing_for_each_ThermalZone,
            costing_for_the_ahu: costing_for_each_AirLoopHVAC
        }
        ########################################################################################################################################################
      end #if !thermal_zone_sys.nil?
    end #if nv_exist == 1.0
  end #model.getZoneHVACEquipmentLists.sort.each do |zone_hvac_equipment_list|

  ########################################################################################################################################################
  # costing for all AirLoopHVACs if they serve at least one thermal zone with the potential for using NV
  nv_airloop_vertical_conduit_hash.each do |k, v|
    nv_total_cost_ahu += v
  end
  nv_airloop_vertical_conduit_wiring_hash.each do |k, v|
    nv_total_cost_ahu += v
  end
  nv_airloop_vertical_conduit_box_hash.each do |k, v|
    nv_total_cost_ahu += v
  end
  nv_airloop_roof_conduit_hash.each do |k, v|
    nv_total_cost_ahu += v
  end
  nv_airloop_roof_wiring_hash.each do |k, v|
    nv_total_cost_ahu += v
  end
  nv_airloop_roof_box_hash.each do |k, v|
    nv_total_cost_ahu += v
  end
  nv_airloop_controller_hash.each do |k, v|
    nv_total_cost_ahu += v
  end
  # puts "nv_total_cost_ahu is #{nv_total_cost_ahu}"

  ########################################################################################################################################################
  ##### costing for the roof-top outdoor air temperature sensor and wind speed sensor
  if nv_total_cost_tz > 0.0
    tags = ['ventilation', 'natural_ventilation']
    ### roof-top outdoor air temperature sensor ----------------------------------------------------------------------------------------
    quantity_nv_rooftop_sensor_Tout = 1.0
    search_nv_rooftop_sensor_Tout = {
        row_id_1: 'Temperaturesensor',
        row_id_2: 1326
    }
    sheet_name = 'materials_hvac'
    column_1 = 'Material'
    column_2 = 'material_id'
    nv_costing_nv_rooftop_sensor_Tout = assembly_cost(cost_info:search_nv_rooftop_sensor_Tout,
                                                      sheet_name:sheet_name,
                                                      column_1:column_1,
                                                      column_2:column_2,
                                                      quantity: quantity_nv_rooftop_sensor_Tout,
                                                      tags: tags)
    # puts "nv_costing_nv_rooftop_sensor_Tout is #{nv_costing_nv_rooftop_sensor_Tout}"

    ### roof-top wind speed sensor -----------------------------------------------------------------------------------------------------
    quantity_nv_rooftop_sensor_wind_speed = 1.0
    search_nv_rooftop_sensor_wind_speed = {
        row_id_1: 'Ea',
        row_id_2: 407
    }
    sheet_name = 'materials_lighting'
    column_1 = 'unit'
    column_2 = 'lighting_type_id'
    nv_costing_nv_rooftop_sensor_wind_speed = assembly_cost(cost_info:search_nv_rooftop_sensor_wind_speed,
                                                            sheet_name:sheet_name,
                                                            column_1:column_1,
                                                            column_2:column_2,
                                                            quantity: quantity_nv_rooftop_sensor_wind_speed,
                                                            tags: tags)
    # puts "nv_costing_nv_rooftop_sensor_wind_speed is #{nv_costing_nv_rooftop_sensor_wind_speed}"

    ### roof-top sensors -----------------------------------------------------------------------------------------------------
    nv_total_cost_rooftop_sensors = nv_costing_nv_rooftop_sensor_Tout + nv_costing_nv_rooftop_sensor_wind_speed
    ########################################################################################################################################################
    nv_total_cost = nv_total_cost_tz + nv_total_cost_ahu + nv_total_cost_rooftop_sensors
    ##### Gather information for reporting
    @costing_report['ventilation'][:natural_ventilation] << {
        costing_for_rooftop_sensors: nv_total_cost_rooftop_sensors,
        nv_total_cost: nv_total_cost
    }
  end

  puts "\nNatural ventilation costing data successfully generated. Total NV costs: $#{nv_total_cost.round(2)}"

  return nv_total_cost
end

#cost_audit_pv_ground(model, prototype_creator) ⇒ Object



3
4
5
6
7
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
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
# File 'lib/openstudio-standards/btap/costing/pv_ground_costing.rb', line 3

def cost_audit_pv_ground(model, prototype_creator)
  @costing_report['renewables']['pv'] = []
  a = 0 # This is for reporting purposes.
  pv_ground_total_cost = 0.0
  #-------------------------------------------------------------------------------------------------------------------
  # summary of all steps as per Mike Lubun's spec:
  # Step 2: costing of concrete base
  # Step 3: pv modules' racking costing
  # Step 4: pv module costing
  # Step 5: pv wiring costing
  # Step 6: pv inverter costing
  # Step 7: transformer costing
  # Step 8: circuit breakers costing
  # Step 9: circuit breaker fuses costing
  # Step 10: PV fuses costing
  # Step 11: disconnects costing
  # Step 12: total cost (sum of Step 2 to 11)
  #-------------------------------------------------------------------------------------------------------------------
  ##### Gather PV information from the model
  model.getGeneratorPVWattss.sort.each do |generator_PVWatt|

    tags = ['renewables','pv']

    dc_system_capacity_w = generator_PVWatt.dcSystemCapacity()
    module_type = generator_PVWatt.moduleType()
    dc_system_capacity_kw = dc_system_capacity_w/1000
    #-----------------------------------------------------------------------------------------------------------------
    ##### Step 2: costing of concrete base (#Note: steps' numbers are based on Mike Lubun's spec document)
    ### Step 2a: costing of concrete
    ### Step 2b: costing of excavation
    ### Step 2c: costing of concrete footing
    ### Step 2d: costing of backfill
    ### Step 2e: costing of compaction
    ### Step 2f: costing of underground electrical duct
    ### Step 2g: costing of grounding
    ### Step 2h: sum of 2a,b,c,d,e,f,g
    ### Step 2a: costing of concrete -------------------------------
    quantity_concrete = 0.5 * dc_system_capacity_kw #unit: yd3
    search_concrete_3000psi = {
        row_id_1: 'concrete',
        row_id_2: 3000.0
    }
    sheet_name = 'materials_hvac'
    column_1 = 'Material'
    column_2 = 'Size'
    pv_ground_costing_concrete = assembly_cost(cost_info:search_concrete_3000psi,
                                               sheet_name:sheet_name,
                                               column_1:column_1,
                                               column_2:column_2,
                                               quantity: quantity_concrete,
                                               tags: tags)
    # puts "quantity_concrete is #{quantity_concrete}"
    # puts "pv_ground_costing_concrete is #{pv_ground_costing_concrete}"

    ### Step 2b: costing of excavation -------------------------------
    quantity_excavation = 3.0 * dc_system_capacity_kw #unit: yd3
    search_excavation = {
        row_id_1: 'Excavation_trench_4_6',
        row_id_2: 0.75
    }
    sheet_name = 'materials_hvac'
    column_1 = 'Material'
    column_2 = 'Size'
    pv_ground_costing_excavation = assembly_cost(cost_info:search_excavation,
                                                 sheet_name:sheet_name,
                                                 column_1:column_1,
                                                 column_2:column_2,
                                                 quantity: quantity_excavation,
                                                 tags: tags)
    # puts "quantity_excavation is #{quantity_excavation}"
    # puts "pv_ground_costing_excavation is #{pv_ground_costing_excavation}"

    ### Step 2c: costing of concrete footing -------------------------------
    quantity_concrete_form = 1.0 * dc_system_capacity_kw #unit: each
    search_concrete_footing = {
        row_id_1: 'concreteforms',
        row_id_2: nil
    }
    sheet_name = 'materials_hvac'
    column_1 = 'Material'
    column_2 = nil
    pv_ground_concrete_footing = assembly_cost(cost_info:search_concrete_footing,
                                               sheet_name:sheet_name,
                                               column_1:column_1,
                                               column_2:column_2,
                                               quantity: quantity_concrete_form,
                                               tags: tags)
    # puts "quantity_concrete_form is #{quantity_concrete_form}"
    # puts "pv_ground_concrete_footing is #{pv_ground_concrete_footing}"

    ### Step 2d: costing of backfill -------------------------------
    quantity_backfill = 3.0 * dc_system_capacity_kw #unit: each
    search_backfill = {
        row_id_1: 'Backfill ',
        row_id_2: nil
    }
    sheet_name = 'materials_hvac'
    column_1 = 'Material'
    column_2 = nil
    pv_ground_backfill = assembly_cost(cost_info:search_backfill,
                                       sheet_name:sheet_name,
                                       column_1:column_1,
                                       column_2:column_2,
                                       quantity: quantity_backfill,
                                       tags: tags)
    # puts "quantity_backfill is #{quantity_backfill}"
    # puts "pv_ground_backfill is #{pv_ground_backfill}"

    ### Step 2e: costing of compaction -------------------------------
    quantity_compaction = 3.0 * dc_system_capacity_kw #unit: yd3
    search_compaction = {
        row_id_1: 'Compaction_WalkBehind',
        row_id_2: 4.0
    }
    sheet_name = 'materials_hvac'
    column_1 = 'Material'
    column_2 = 'Size'
    pv_ground_compaction = assembly_cost(cost_info:search_compaction,
                                         sheet_name:sheet_name,
                                         column_1:column_1,
                                         column_2:column_2,
                                         quantity: quantity_compaction,
                                         tags: tags)
    # puts "quantity_compaction is #{quantity_compaction}"
    # puts "pv_ground_compaction is #{pv_ground_compaction}"

    ### Step 2f: costing of underground electrical duct -------------------------------
    quantity_underground_electrical_duct = 100.0
    search_underground_electrical_duct = {
        row_id_1: 'groundconduit',
        row_id_2: 1470.0
    }
    sheet_name = 'materials_hvac'
    column_1 = 'Material'
    column_2 = 'material_id'
    pv_ground_underground_electrical_duct = assembly_cost(cost_info:search_underground_electrical_duct,
                                                          sheet_name:sheet_name,
                                                          column_1:column_1,
                                                          column_2:column_2,
                                                          quantity: quantity_underground_electrical_duct,
                                                          tags: tags)
    # puts "quantity_underground_electrical_duct is #{quantity_underground_electrical_duct}"
    # puts "pv_ground_underground_electrical_duct is #{pv_ground_underground_electrical_duct}"

    ### Step 2g: costing of grounding -------------------------------
    # Step 2g-1: costing of ground rod
    quantity_grounding_ground_rod = 1.0
    search_grounding_ground_rod = {
        row_id_1: 'Ground_Rod',
        row_id_2: 1356.0
    }
    sheet_name = 'materials_hvac'
    column_1 = 'Material'
    column_2 = 'material_id'
    pv_ground_grounding_ground_rod = assembly_cost(cost_info:search_grounding_ground_rod,
                                                   sheet_name:sheet_name,
                                                   column_1:column_1,
                                                   column_2:column_2,
                                                   quantity: quantity_grounding_ground_rod,
                                                   tags: tags)
    # puts "quantity_grounding_ground_rod is #{quantity_grounding_ground_rod}"
    # puts "pv_grounding_ground_rod is #{pv_ground_grounding_ground_rod}"

    # Step 2g-2: costing of exo weld
    quantity_grounding_exo_weld = 2.0
    search_grounding_exo_weld = {
        row_id_1: 'Exo_weld',
        row_id_2: 1373.0
    }
    sheet_name = 'materials_hvac'
    column_1 = 'Material'
    column_2 = 'material_id'
    pv_ground_grounding_exo_weld = assembly_cost(cost_info:search_grounding_exo_weld,
                                                 sheet_name:sheet_name,
                                                 column_1:column_1,
                                                 column_2:column_2,
                                                 quantity: quantity_grounding_exo_weld,
                                                 tags: tags)
    # puts "quantity_grounding_exo_weld is #{quantity_grounding_exo_weld}"
    # puts "pv_grounding_exo_weld is #{pv_ground_grounding_exo_weld}"

    # Step 2g-3: costing of ground wire #4
    quantity_grounding_ground_wire_4 = 100.0 / 100.0
    search_grounding_ground_wire_4 = {
        row_id_1: 'Wire_copper_stranded',
        row_id_2: 1372.0
    }
    sheet_name = 'materials_hvac'
    column_1 = 'Material'
    column_2 = 'material_id'
    pv_ground_grounding_ground_wire_4 = assembly_cost(cost_info:search_grounding_ground_wire_4,
                                                      sheet_name:sheet_name,
                                                      column_1:column_1,
                                                      column_2:column_2,
                                                      quantity: quantity_grounding_ground_wire_4,
                                                      tags: tags)
    # puts "quantity_grounding_ground_wire_4 is #{quantity_grounding_ground_wire_4}"
    # puts "pv_grounding_ground_wire_4 is #{pv_ground_grounding_ground_wire_4}"

    # Step 2g-4: costing of ground wire #6
    quantity_grounding_ground_wire_6 = 20.0
    search_grounding_ground_wire_6 = {
        row_id_1: 'Wire_copper_solid',
        row_id_2: 1361.0
    }
    sheet_name = 'materials_hvac'
    column_1 = 'Material'
    column_2 = 'material_id'
    pv_ground_grounding_ground_wire_6 = assembly_cost(cost_info:search_grounding_ground_wire_6,
                                                      sheet_name:sheet_name,
                                                      column_1:column_1,
                                                      column_2:column_2,
                                                      quantity: quantity_grounding_ground_wire_6,
                                                      tags: tags)
    # puts "quantity_grounding_ground_wire_6 is #{quantity_grounding_ground_wire_6}"
    # puts "pv_grounding_ground_wire_6 is #{pv_ground_grounding_ground_wire_6}"

    # Step 2g-5: costing of wire brazing
    quantity_grounding_wire_brazing = 1.0
    search_grounding_wire_brazing = {
        row_id_1: 'Brazed_connection',
        row_id_2: 1374.0
    }
    sheet_name = 'materials_hvac'
    column_1 = 'Material'
    column_2 = 'material_id'
    pv_ground_grounding_wire_brazing = assembly_cost(cost_info:search_grounding_wire_brazing,
                                                     sheet_name:sheet_name,
                                                     column_1:column_1,
                                                     column_2:column_2,
                                                     quantity: quantity_grounding_wire_brazing,
                                                     tags: tags)
    # puts "quantity_grounding_wire_brazing is #{quantity_grounding_wire_brazing}"
    # puts "pv_grounding_wire_brazing is #{pv_ground_grounding_wire_brazing}"

    # total cost of grounding
    pv_ground_grounding = pv_ground_grounding_ground_rod +
                          pv_ground_grounding_exo_weld +
                          pv_ground_grounding_ground_wire_4 +
                          pv_ground_grounding_ground_wire_6 +
                          pv_ground_grounding_wire_brazing
    # puts "pv_ground_grounding is #{pv_ground_grounding}"

    ### Step 2h: sum of 2a,b,c,d,e,f,g ------------------------------
    # calculate total cost of concrete base (2a + 2b + 2c + 2d + 2e + 2f + 2g)
    costing_of_concrete_base = pv_ground_costing_concrete +
                              pv_ground_costing_excavation +
                              pv_ground_concrete_footing +
                              pv_ground_backfill +
                              pv_ground_compaction +
                              pv_ground_underground_electrical_duct +
                              pv_ground_grounding
    # puts "costing_of_concrete_base is #{costing_of_concrete_base}"
    #-----------------------------------------------------------------------------------------------------------------
    ##### Step 3: pv modules' racking costing
    quantity_racking = dc_system_capacity_kw * 1.0
    search_pv_racking = {
        row_id_1: 'pvgroundmount',
        row_id_2: 6.0
    }
    sheet_name = 'materials_hvac'
    column_1 = 'Material'
    column_2 = 'Size'
    pv_ground_racking = assembly_cost(cost_info:search_pv_racking,
                                      sheet_name:sheet_name,
                                      column_1:column_1,
                                      column_2:column_2,
                                      quantity:quantity_racking,
                                      tags: tags)
    # puts "quantity_racking is #{quantity_racking}"
    # puts "pv_ground_racking is #{pv_ground_racking}"
    #-----------------------------------------------------------------------------------------------------------------
    ##### Step 4: pv module costing
    # Note: osm file does not show total area of PV panels; instead it shows the total wattage of PV panels.
    # So, for the calculation of number of PV panels, we have assumed that a specific module is being used based on
    # the module type that we can get from the osm file. In this way, we know the wattage of each PV panel;
    # so, we can calculate number of PV panels.
    if module_type == 'Standard' #As per Mike Lubun's comment, we assume using 'HES-160-36PV 26.6  x 58.3 x 1.38' as 'Standard' (i.e. 'poly'/'perc')
      row_id_1 = 'HES-160-36PV 26.6  x 58.3 x 1.38'
      row_id_2 = 160.0 #wattage of each module of 'HES-160-36PV 26.6  x 58.3 x 1.38'
      quantity_number_of_panels = dc_system_capacity_w / row_id_2
    elsif module_type == 'Premium'   #As per Mike Lubun's comment, we assume using 'Heliene 36HD 26.6  x 58.6 x 1.6' as 'Premium' (i.e. 'mono')
      row_id_1 = 'Heliene 36HD 26.6  x 58.6 x 1.6'
      row_id_2 = 160.0  #wattage of each module of 'Heliene 36HD 26.6  x 58.6 x 1.6'
      quantity_number_of_panels = dc_system_capacity_w / row_id_2
    elsif module_type == 'ThinFilm'  #As per Mike Lubun's comment, we assume using 'Powerfilm, Soltronic Semi-Flex with Sunpower cells, 21 x 44.5 x 0.08' as 'ThinFilm' (i.e. 'thin')
      row_id_1 = 'Powerfilm, Soltronic Semi-Flex with Sunpower cells, 21 x 44.5 x 0.08'
      row_id_2 = 100.0   #wattage of each module of 'Powerfilm, Soltronic Semi-Flex with Sunpower cells, 21 x 44.5 x 0.08'
      quantity_number_of_panels = dc_system_capacity_w / row_id_2
    end
    search_pv_module = {
        row_id_1: row_id_1,
        row_id_2: row_id_2
    }
    sheet_name = 'materials_hvac'
    column_1 = 'description'
    column_2 = 'Size'
    pv_ground_costing_pv_module = assembly_cost(cost_info:search_pv_module,
                                                sheet_name:sheet_name,
                                                column_1:column_1,
                                                column_2:column_2,
                                                quantity:quantity_number_of_panels,
                                                tags: tags)
    # puts "quantity_number_of_panels is #{quantity_number_of_panels}"
    # puts "pv_ground_costing_pv_module is #{pv_ground_costing_pv_module}"

    #-----------------------------------------------------------------------------------------------------------------
    ##### Step 5: pv wiring costing
    # Step 5-1
    quantity_wiring_wire = dc_system_capacity_kw * 1.0#unit: CLF (Hundred Linear Feet)
    search_pv_wire = {
        row_id_1: 'Wire_copper_stranded',
        row_id_2: 6.0
    }
    sheet_name = 'materials_hvac'
    column_1 = 'Material'
    column_2 = 'Size'
    pv_ground_wiring_wire = assembly_cost(cost_info:search_pv_wire,
                                          sheet_name:sheet_name,
                                          column_1:column_1,
                                          column_2:column_2,
                                          quantity:quantity_wiring_wire,
                                          tags: tags)
    # puts "quantity_wiring_wire is #{quantity_wiring_wire}"
    # puts "pv_ground_wiring_wire is #{pv_ground_wiring_wire}"

    # Step 5-2
    quantity_wiring_conduit = dc_system_capacity_kw * 27.0 #unit: LF
    search_pv_conduit = {
        row_id_1: 'Conduit',
        row_id_2: 851.0
    }
    sheet_name = 'materials_hvac'
    column_1 = 'Material'
    column_2 = 'material_id'
    pv_ground_wiring_conduit = assembly_cost(cost_info:search_pv_conduit,
                                             sheet_name:sheet_name,
                                             column_1:column_1,
                                             column_2:column_2,
                                             quantity:quantity_wiring_conduit,
                                             tags: tags)
    # puts "quantity_wiring_conduit is #{quantity_wiring_conduit}"
    # puts "pv_ground_wiring_conduit is #{pv_ground_wiring_conduit}"

    pv_ground_wiring = pv_ground_wiring_wire + pv_ground_wiring_conduit
    # puts "pv_ground_wiring is #{pv_ground_wiring}"

    #-----------------------------------------------------------------------------------------------------------------
    ##### Step 6: pv inverter costing
    # Step 6-1: inverters themselves
    if dc_system_capacity_kw < 4.0
      inverter_size = 3.0
      inverter_multiplier = 1.0
    elsif dc_system_capacity_kw == 4.0
      inverter_size = 4.0
      inverter_multiplier = 1.0
    elsif dc_system_capacity_kw > 4.0
      inverter_size = 4.0
      inverter_multiplier = dc_system_capacity_kw / 4.0
      inverter_multiplier = inverter_multiplier.ceil
    end
    # puts "inverter_multiplier is #{inverter_multiplier}"
    quantity_inverter_itself = inverter_multiplier.to_f
    search_pv_inverter_itself = {
        row_id_1: 'inverter24',
        row_id_2: inverter_size
    }
    sheet_name = 'materials_hvac'
    column_1 = 'Material'
    column_2 = 'Size'
    pv_ground_inverter_itself = assembly_cost(cost_info:search_pv_inverter_itself,
                                              sheet_name:sheet_name,
                                              column_1:column_1,
                                              column_2:column_2,
                                              quantity:quantity_inverter_itself,
                                              tags: tags)
    # puts "quantity_inverter_itself is #{quantity_inverter_itself}"
    # puts "pv_ground_inverter_itself is #{pv_ground_inverter_itself}"

    # Step 6-2: inverters' boxes
    quantity_inverter_box = inverter_multiplier.to_f
    search_pv_inverter_box = {
        row_id_1: 'pvbox',
        row_id_2: 1135.0
    }
    sheet_name = 'materials_hvac'
    column_1 = 'Material'
    column_2 = 'material_id'
    pv_ground_inverter_box = assembly_cost(cost_info:search_pv_inverter_box,
                                           sheet_name:sheet_name,
                                           column_1:column_1,
                                           column_2:column_2,
                                           quantity:quantity_inverter_box,
                                           tags: tags)
    # puts "quantity_inverter_box is #{quantity_inverter_box}"
    # puts "pv_ground_inverter_box is #{pv_ground_inverter_box}"

    pv_ground_inverter = pv_ground_inverter_itself + pv_ground_inverter_box
    # puts "pv_ground_inverter is #{pv_ground_inverter}"

    #-----------------------------------------------------------------------------------------------------------------
    ##### Step 7: transformer costing
    transformer_types = [1.0, 2.0, 3.0, 5.0, 7.5, 10.0, 15.0, 25.0, 37.5, 50.0, 75.0, 100.0, 167.0] #based on Mike Lubun's costing spreadsheet
    transformer_closet_to_pv_kw = transformer_types.sort_by { |item| (dc_system_capacity_kw-item).abs }.first(1)
    # puts "transformer_closet_to_pv_kw is #{transformer_closet_to_pv_kw}"
    if dc_system_capacity_kw <= 167.0
      transformer_multiplier = 1.0
      row_id_2 = transformer_closet_to_pv_kw[0]
    else #i.e. dc_system_capacity_kw > 167.0
      transformer_multiplier = dc_system_capacity_kw / 167.0
      transformer_multiplier = transformer_multiplier.ceil
      row_id_2 = 167.0
    end
    # puts "transformer_multiplier is #{transformer_multiplier}"
    quantity_transformer = transformer_multiplier.to_f
    search_transformer = {
        row_id_1: 'Transformer_dry_low_voltage',
        row_id_2: row_id_2
    }
    sheet_name = 'materials_hvac'
    column_1 = 'Material'
    column_2 = 'Size'
    pv_ground_transformer = assembly_cost(cost_info:search_transformer,
                                          sheet_name:sheet_name,
                                          column_1:column_1,
                                          column_2:column_2,
                                          quantity:quantity_transformer,
                                          tags: tags)
    # puts "quantity_transformer is #{quantity_transformer}"
    # puts "pv_ground_transformer is #{pv_ground_transformer}"

    #-----------------------------------------------------------------------------------------------------------------
    ##### Step 8: circuit breakers costing
    circuit_breaker240_types = [30.0, 60.0, 100.0, 200.0, 400.0, 600.0]
    circuit_breaker_amps = (dc_system_capacity_kw * 1000.0 * 1.5 / 24.0) * 1.25
    # puts "circuit_breaker_amps is #{circuit_breaker_amps}"
    circuit_breaker_closet_to_pv_amps = circuit_breaker240_types.sort_by { |item| (circuit_breaker_amps-item).abs }.first(1)
    # puts "circuit_breaker_closet_to_pv_amps is #{circuit_breaker_closet_to_pv_amps}"
    if circuit_breaker_amps <= 600.0
      circuit_breaker_multiplier = 1.0
      row_id_2 = circuit_breaker_closet_to_pv_amps[0]
    else #i.e. circuit_breaker_amps > 600.0
      circuit_breaker_multiplier = circuit_breaker_amps / 600.0
      circuit_breaker_multiplier = circuit_breaker_multiplier.ceil
      row_id_2 = 600.0
    end
    quantity_circuit_breakers = circuit_breaker_multiplier.to_f
    search_circuit_breakers = {
        row_id_1: 'Circuit_breaker240',
        row_id_2: row_id_2
    }
    sheet_name = 'materials_hvac'
    column_1 = 'Material'
    column_2 = 'Size'
    pv_ground_circuit_breakers = assembly_cost(cost_info:search_circuit_breakers,
                                               sheet_name:sheet_name,
                                               column_1:column_1,
                                               column_2:column_2,
                                               quantity:quantity_circuit_breakers,
                                               tags: tags)
    # puts "quantity_circuit_breakers is #{quantity_circuit_breakers}"
    # puts "pv_ground_circuit_breakers is #{pv_ground_circuit_breakers}"

    #-----------------------------------------------------------------------------------------------------------------
    ##### Step 9: circuit breaker fuses costing
    circuit_breaker_fuse = circuit_breaker_amps
    circuit_breaker_fuse_250V_timedelay_types = [30.0, 5.0, 60.0, 100.0, 200.0, 400.0, 600.0]
    circuit_breaker_fuse_closet_to_pv_amps = circuit_breaker_fuse_250V_timedelay_types.sort_by { |item| (circuit_breaker_fuse-item).abs }.first(1)
    # puts "circuit_breaker_fuse_closet_to_pv_amps is #{circuit_breaker_fuse_closet_to_pv_amps}"
    if circuit_breaker_fuse <= 600.0
      circuit_breaker_fuse_multiplier = 1.0
      row_id_2 = circuit_breaker_fuse_closet_to_pv_amps[0]
    else #i.e. circuit_breaker_fuse > 600.0
      circuit_breaker_fuse_multiplier = circuit_breaker_fuse / 600.0
      circuit_breaker_fuse_multiplier = circuit_breaker_fuse_multiplier.ceil
      row_id_2 = 600.0
    end
    quantity_circuit_breaker_fuses = circuit_breaker_fuse_multiplier.to_f
    search_circuit_breaker_fuses = {
        row_id_1: 'fuse_250V_timedelay',
        row_id_2: row_id_2
    }
    sheet_name = 'materials_hvac'
    column_1 = 'Material'
    column_2 = 'Size'
    pv_ground_circuit_breaker_fuses = assembly_cost(cost_info:search_circuit_breaker_fuses,
                                                    sheet_name:sheet_name,
                                                    column_1:column_1,
                                                    column_2:column_2,
                                                    quantity:quantity_circuit_breaker_fuses,
                                                    tags: tags)
    # puts "quantity_circuit_breaker_fuses is #{quantity_circuit_breaker_fuses}"
    # puts "pv_ground_circuit_breaker_fuses is #{pv_ground_circuit_breaker_fuses}"

    #-----------------------------------------------------------------------------------------------------------------
    ##### Step 10: PV fuses costing
    # Step 10-1: PV fuses
    quantity_pv_fuse_itself = (dc_system_capacity_kw * 1.0).ceil.to_f
    search_pv_fuse = {
        row_id_1: 'fuse_120V',
        row_id_2: 15.0
    }
    sheet_name = 'materials_hvac'
    column_1 = 'Material'
    column_2 = 'Size'
    pv_ground_pv_fuse_itself = assembly_cost(cost_info:search_pv_fuse,
                                             sheet_name:sheet_name,
                                             column_1:column_1,
                                             column_2:column_2,
                                             quantity:quantity_pv_fuse_itself,
                                             tags: tags)
    # puts "quantity_pv_fuse_itself is #{quantity_pv_fuse_itself}"
    # puts "pv_ground_pv_fuse_itself is #{pv_ground_pv_fuse_itself}"

    # Step 10-2: PV combiner box
    quantity_pv_combiner_box = (dc_system_capacity_kw / 10.0).ceil.to_f
    search_pv_combiner_box = {
        row_id_1: 'pvcombinerbox',
        row_id_2: 1125.0
    }
    sheet_name = 'materials_hvac'
    column_1 = 'Material'
    column_2 = 'material_id'
    pv_ground_pv_combiner_box = assembly_cost(cost_info:search_pv_combiner_box,
                                              sheet_name:sheet_name,
                                              column_1:column_1,
                                              column_2:column_2,
                                              quantity:quantity_pv_combiner_box,
                                              tags: tags)
    # puts "quantity_pv_combiner_box is #{quantity_pv_combiner_box}"
    # puts "pv_ground_pv_combiner_box is #{pv_ground_pv_combiner_box}"

    pv_ground_pv_fuses = pv_ground_pv_fuse_itself + pv_ground_pv_combiner_box
    # puts "pv_ground_pv_fuses is #{pv_ground_pv_fuses}"

    #-----------------------------------------------------------------------------------------------------------------
    ##### Step 11: disconnects costing
    quantity_disconnect_before_inverter = 1.0
    search_disconnect_before_inverter = {
        row_id_1: 'Circuit_breaker240',
        row_id_2: 1403.0
    }
    sheet_name = 'materials_hvac'
    column_1 = 'Material'
    column_2 = 'material_id'
    pv_ground_disconnect_before_inverter = assembly_cost(cost_info:search_disconnect_before_inverter,
                                                         sheet_name:sheet_name,
                                                         column_1:column_1,
                                                         column_2:column_2,
                                                         quantity:quantity_disconnect_before_inverter,
                                                         tags: tags)
    # puts "quantity_disconnect_before_inverter is #{quantity_disconnect_before_inverter}"
    # puts "pv_ground_disconnect_before_inverter is #{pv_ground_disconnect_before_inverter}"


    quantity_disconnect_after_transformer = 1.0
    search_disconnect_after_transformer = {
        row_id_1: 'Circuit_breaker240',
        row_id_2: 1407.0
    }
    sheet_name = 'materials_hvac'
    column_1 = 'Material'
    column_2 = 'material_id'
    pv_ground_disconnect_after_transformer = assembly_cost(cost_info:search_disconnect_after_transformer,
                                                           sheet_name:sheet_name,
                                                           column_1:column_1,
                                                           column_2:column_2,
                                                           quantity:quantity_disconnect_after_transformer,
                                                           tags: tags)
    # puts "quantity_disconnect_after_transformer is #{quantity_disconnect_after_transformer}"
    # puts "pv_ground_disconnect_after_transformer is #{pv_ground_disconnect_after_transformer}"

    pv_ground_disconnects = pv_ground_disconnect_before_inverter + pv_ground_disconnect_after_transformer
    # puts "pv_ground_disconnects is #{pv_ground_disconnects}"

    #-----------------------------------------------------------------------------------------------------------------
    ##### Step 12: calculate total cost of the ground mount PV system (sum of steps 2 to 11)
    pv_ground_total_cost_handle = costing_of_concrete_base +
                                   pv_ground_racking +
                                   pv_ground_costing_pv_module +
                                   pv_ground_wiring +
                                   pv_ground_inverter +
                                   pv_ground_transformer +
                                   pv_ground_circuit_breakers +
                                   pv_ground_circuit_breaker_fuses +
                                   pv_ground_pv_fuses +
                                   pv_ground_disconnects

    pv_ground_total_cost += pv_ground_total_cost_handle

    # puts "pv_ground_total_cost_handle is #{pv_ground_total_cost_handle}"

    ##### Gather information for reporting
    @costing_report['renewables']['pv'] << {
        'generator_PVWatt_name' => generator_PVWatt.name.to_s,
        'costing_of_concrete_base' => costing_of_concrete_base,
        'pv_ground_racking' => pv_ground_racking,
        'pv_ground_costing_pv_module' => pv_ground_costing_pv_module,
        'pv_ground_wiring' => pv_ground_wiring,
        'pv_ground_inverter' => pv_ground_inverter,
        'pv_ground_transformer' => pv_ground_transformer,
        'pv_ground_circuit_breakers' => pv_ground_circuit_breakers,
        'pv_ground_circuit_breaker_fuses' => pv_ground_circuit_breaker_fuses,
        'pv_ground_pv_fuses' => pv_ground_pv_fuses,
        'pv_ground_disconnects' => pv_ground_disconnects,
        'the_generator_PVWatt_total_cost' => pv_ground_total_cost_handle
    }

    a += 1

  end
  if a > 0
    @costing_report['renewables']['pv'] << {
        'pv_ground_total_cost' => pv_ground_total_cost
    }
  end

  puts "\nGround-mounted PV costing data successfully generated. Total PV costs: $#{pv_ground_total_cost.round(2)}"

  return pv_ground_total_cost
end

#cost_ccashp_additional_components(ahu_mult:, heat_pump:, vent_tags: [], report_mult: 1.0) ⇒ Object

This method calculates the costs of CCASHP equipment beyond the coil cost and any backup heating costs. It takes in ahu_mult: The number of air handlers required to meet the model air loop flow rate, cooling type, heating type and system type. heat_pumps: The heat pump hash for the ccashp which contains the OpenStudio heat pump object and the size of the heat pump in kW. The method uses a number of different costing methods to get equipment costs. The methods used depend on what best suits the costing. For example evaporator costing is found by size and material so the get_vent_cost_data method is most appropriate. Wiring has a material and size but the size should be an exact match so the get_comp_cost method is used. Finally, a number of pieces of equipment with no size are costed. The esiest way to cost these items was to refer to their ‘materials_hvac’ sheet ‘material_id’ column numbers and associated quantities and use the vent_assembly_cost method.



2545
2546
2547
2548
2549
2550
2551
2552
2553
2554
2555
2556
2557
2558
2559
2560
2561
2562
2563
2564
2565
2566
2567
2568
2569
2570
2571
2572
2573
2574
2575
2576
2577
2578
2579
2580
2581
2582
2583
2584
2585
2586
2587
2588
2589
2590
2591
2592
2593
2594
2595
2596
2597
2598
2599
2600
2601
2602
2603
2604
2605
2606
2607
2608
2609
2610
2611
2612
2613
2614
2615
# File 'lib/openstudio-standards/btap/costing/ventilation_costing.rb', line 2545

def cost_ccashp_additional_components(ahu_mult:, heat_pump:, vent_tags: [], report_mult: 1.0)
  ccashp_tags = vent_tags.clone
  # Initialize the ccashp additional equipment cost.
  ccashp_add_cost = 0
  # Set a variable to represent the capacity of each heat pump per air handler
  cap = heat_pump[:mech_capacity_kw].to_f/ahu_mult
  # Set a variable to represent the capacity in tons of cooling (for costing the refrigerent line).
  # cap_tonc = (OpenStudio.convert(cap.to_f, 'kW', 'kBtu/hr').get)/12.0 # No longer needed but keeping for future reference

  # This variable holds the number of condensing units.
  cond_mult = 1.0

  # An array of hashes containing the information required to cost the heat pump evaporator valve and condenser.
  ccashp_lrg_equips = []
  ccashp_lrg_equips << {
    supply_comp: heat_pump[:supply_comp],
    mech_capacity_kw: cap,
    cat_search: "EV_valve"
  }
  ccashp_lrg_equips << {
    supply_comp: heat_pump[:supply_comp],
    mech_capacity_kw: cap,
    cat_search: "ccashp_condensor"
  }

  # Cost the heat pump evaporator valve and condenser.
  ccashp_lrg_equips.each do |ccashp_lrg_equip|
    equip_mult, cost_info = get_vent_cost_data(equipment_info: ccashp_lrg_equip)
    ccashp_add_cost += get_vent_mat_cost(mat_cost_info: cost_info, vent_tags: ccashp_tags, report_mult: (report_mult*equip_mult*ahu_mult)) * equip_mult * ahu_mult
    # cond_mult is supposed to be the number of condensors there are.  It is set to be the multiplier if one condensor
    # is not enough.  It should be set to the number of condesors because the condensors should be the last item in
    # this loop to be costed.
    cond_mult = equip_mult
  end

  # Cost the wiring per heat pump condenser.  Correcting to use 20 ft rather than 20 m.
  #ccashp_wiring_dist = (OpenStudio.convert(20, 'm', 'ft').get)/100.0
  ccashp_add_equip = [
    {
      mat: "Wiring",
      unit: "CLF",
      size: 10,
      mult: 0.2 * ahu_mult * cond_mult
  }
  ]
  # Get the Wiring costs.
  ccashp_add_cost += get_comp_cost(cost_info: ccashp_add_equip, vent_tags: ccashp_tags)

  # Set an array containing the equipment 'material_id' references to search in the costing spreadsheet
  # 'materials_hvac' sheet.
  ids = [
    #1307, #Low Temperature Kit this belongs with the air handlers not the equipment
    1295, # Remote Condensor Controller
    1662, # Refrigerant tubing-large, 20' of 0.5" supply and 1-1/8" return
    30, # 1.25" pipe insulation for refrigerant tubing
    1415 # Safety Switch
  ]

  # Set the quantities associated with the above ids.  Note that ahu_mult is included when getting the cost.
  id_quants = [
    #1.0,
    cond_mult,
    cond_mult,
    cond_mult * 20 * 2, # 20' of supply and return pipe insulation for refrigerant tubing
    cond_mult
  ]

  # Get the costs for equipment in the ids with id_quants quantities above.
  ccashp_add_cost += vent_assembly_cost(ids: ids, id_quants: id_quants, overall_mult: ahu_mult, vent_tags: ccashp_tags)
  return ccashp_add_cost
end

#cost_construction(construction, location, type = 'opaque') ⇒ Object



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
# File 'lib/openstudio-standards/btap/costing/envelope_costing.rb', line 231

def cost_construction(construction, location, type = 'opaque')

  material_layers = "material_#{type}_id_layers"
  material_id = "materials_#{type}_id"
  materials_database = @costing_database["raw"]["materials_#{type}"]

  total_with_op = 0.0
  material_cost_pairs = []
  construction[material_layers].split(',').reject { |c| c.empty? }.each do |material_index|
    material = materials_database.find { |data| data[material_id].to_s == material_index.to_s }
    if material.nil?
      puts "material error..could not find material #{material_index} in #{materials_database}"
      raise()
    else
      costing_data = @costing_database['costs'].detect { |data| data['id'].to_s.upcase == material['id'].to_s.upcase }
      if costing_data.nil?
        puts "This material id #{material['id']} was not found in the costing database. Skipping. This construction will be inaccurate. "
        raise()
      else
        regional_material, regional_installation = get_regional_cost_factors(location['province_state'], location['city'], material)

        # Get cost information from lookup.
        # Note that "glazing" types don't have a 'quantity' hash entry!
        # Don't need "and" below but using in-case this hash field is added in the future.
        if type == 'glazing' and material['quantity'].to_f == 0.0
          material['quantity'] = '1.0'
        end
        material_cost = costing_data['baseCosts']['materialOpCost'].to_f * material['material_mult'].to_f
        labour_cost = costing_data['baseCosts']['laborOpCost'].to_f * material['labour_mult'].to_f
        equipment_cost = costing_data['baseCosts']['equipmentOpCost'].to_f
        layer_cost = (((material_cost * regional_material / 100.0) + (labour_cost * regional_installation / 100.0) + equipment_cost) * material['quantity'].to_f).round(2)
        material_cost_pairs << {material_id.to_s => material_index,
                                'cost' => layer_cost}
        total_with_op += layer_cost
      end
    end
  end
  new_construction = {
      'province_state' => location['province_state'],
      'city' => location['city'],
      "construction_type_name" => construction["construction_type_name"],
      'description' => construction["description"],
      'intended_surface_type' => construction["intended_surface_type"],
      'standards_construction_type' => construction["standards_construction_type"],
      'rsi_k_m2_per_w' => construction['rsi_k_m2_per_w'].to_f,
      'zone' => construction['climate_zone'],
      'fenestration_type' => construction['fenestration_type'],
      'u_w_per_m2_k' => construction['u_w_per_m2_k'],
      'materials' => material_cost_pairs,
      'total_cost_with_op' => total_with_op}

  @costing_database['constructions_costs'] << new_construction
end

#cost_heat_cool_equip(equipment_info:, vent_tags: [], report_mult: 1.0) ⇒ Object



681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
# File 'lib/openstudio-standards/btap/costing/ventilation_costing.rb', line 681

def cost_heat_cool_equip(equipment_info:, vent_tags: [], report_mult: 1.0)
  equip_tags = vent_tags.clone
  total_cost = 0
  multiplier, heat_cool_cost_info = get_vent_cost_data(equipment_info: equipment_info)
  total_cost += (get_vent_mat_cost(mat_cost_info: heat_cool_cost_info, vent_tags: equip_tags, report_mult: (report_mult*multiplier)))*multiplier
  if equipment_info[:cooling_type] == 'DX' || equipment_info[:cooling_type] == 'DX-adv'
    equipment_info[:cooling_type].include?("-adv") ? search_suff = "-adv" : search_suff = ""
    equipment_info[:cat_search] = "CondensingUnit" + search_suff
    equip_tags << equipment_info[:cat_search] unless equip_tags.empty?
    multiplier, heat_cool_cost_info = get_vent_cost_data(equipment_info: equipment_info)
    total_cost += get_vent_mat_cost(mat_cost_info: heat_cool_cost_info, vent_tags: equip_tags, report_mult: (report_mult*multiplier))*multiplier
    equip_tags << "piping" unless equip_tags.empty?
    piping_search = []

    piping_search << {
      mat: 'SteelPipe',
      unit: 'L.F.',
      size: 1.25,
      mult: 32.8
    }
    piping_search << {
      mat: 'PipeInsulationsilica',
      unit: 'L.F.',
      size: 1.25,
      mult: 32.8
    }
    piping_search << {
      mat: 'SteelPipeElbow',
      unit: 'each',
      size: 1.25,
      mult: 8
    }
    total_cost += get_comp_cost(cost_info: piping_search, vent_tags: equip_tags, report_mult: (report_mult*multiplier))*multiplier
    return total_cost
  end
  # This needs to be revised as currently the costing spreadsheet may not inculde heating and cooling coil costs in
  # the ahu definition sheet.  This is commented out for now but will need to be revisited.  See btap_tasks issue 156.
=begin
  if equipment_info[:heating_fuel] == 'HP'
    if sys_type == 3 || sys_type == 6
      # Remove the DX cooling unit for ashp in type 3 and 6 systems
      heat_cool_cost = @costing_database['raw']['materials_hvac'].select {|data|
        data['Material'].to_s.upcase == 'DX' and
            data['Size'].to_f.round(8) >= equipment_info[:mech_capacity_kw].to_f
      }.first
      if heat_cool_cost.nil?
        heat_cool_cost, multiplier = get_vent_system_mult(loop_equip: equipment_info)
      end
      total_cost -= (get_vent_mat_cost(mat_cost_info: heat_cool_cost))*multiplier

      # Remove the heating coil for ashp in type 3 and 6 systems
      heat_cool_cost = @costing_database['raw']['materials_hvac'].select {|data|
        data['Material'].to_s.upcase == 'COILS' and
            data['Size'].to_f.round(8) >= equipment_info[:mech_capacity_kw].to_f
      }.first
      if heat_cool_cost.nil?
        heat_cool_cost, multiplier = get_vent_system_mult(loop_equip: equipment_info)
      end
      total_cost -= (get_vent_mat_cost(mat_cost_info: heat_cool_cost))*multiplier
      puts 'hello'
    end
    # Add pre-heat for ashp in all cases
    # This needs to be refined as well.  Only add the cost of an electric heat if a heater (presumably of any type) if
    # one is not already explicitly modeled in the air loop (and thus costed already as part of this method).  This is
    # also part of btap_tasks issue 156.
    heat_cool_cost = @costing_database['raw']['materials_hvac'].select {|data|
      data['Material'].to_s.upcase == 'ELECHEAT' and
          data['Size'].to_f.round(8) >= equipment_info[:mech_capacity_kw].to_f
    }.first
    if heat_cool_cost.nil?
      heat_cool_cost, multiplier = get_vent_system_mult(loop_equip: equipment_info)
    end
    total_cost += (get_vent_mat_cost(mat_cost_info: heat_cool_cost))*multiplier
  end
=end
  return total_cost
end

#cost_list_items(btap_items:, custom_costing: nil, custCity: nil, custProvince: nil) ⇒ Object

This method takes the list of costed items in the building generated with the help of the above add_costed_item method and finds the costs for the list of items. It takes in: btap_items: (array of hashes) This array contains all the items that must be costed. The first element of the

      array is:
{
  City: (string) City used for cost lacalization factor
  Province: (string) Province used for cost localization factor
}

The remaining arrays look like: {

  id: (string)  ID of the coested item in question.
  quantity: (float) Amount of costed item (should include all multipliers except localization factors,
            material_mult, labour_mult, equipment_mult).
  material_mult: (float) Material multiplier from cost spreadsheet used mainly for higher performance equipment
                 (for example, regular and high performance boilers share the same id but high performance
                 boilers have a material_mult of around 1.3-that is they are estimated to be 1.3 times as
                 expensive as regular boilers).
  labour_mult: (float) Same idea as material_mult only for labour (often this will be 1.0 even if material_mult
               is something else).
  equipment_mult: (float) Same idea as labour_mult only for equipment.  It will always be 1.0 until equipment
                  costs are implemented in costing.
  tags: (array of strings) An array of strings used to define what part of the building is being costed (e.g.
        an component for a ccashp might have these tags: "Ventilation", "CCASHP", "ccashp_condensor")
}

custom_costing: (array of hashes) A custom costing database if you do not want to use the default one. This must

have the same format as that found by @costing_database['costs']

custCity: (string) A custom cost localization city if you do not want to use the one in the first item in the

btap_itmes hash.

custProvince: (string) A custom cost localization province if you do not want to use the one in the first item in

the btap_items hash.

The output of the method is a hash containing these summary costs:

costRetHash = {
  envelope: (float) Building envelope costs (to 2 decimal places).
  lighting: (float) Ligting costs (to 2 decimal places).
  heating_and_cooling: (float) Heating and cooling costs (not related to ventilation) (to 2 decimal places).
  shw: (float) Service hot water costs (to 2 decimal places).
  ventilation: (float) Ventilation (including ventilation air heating and cooling) costs (to 2 decimal places).
  grand_total: (float) Total costs (to 2 decimal places).
}


391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
# File 'lib/openstudio-standards/btap/costing/btap_costing.rb', line 391

def cost_list_items(btap_items:, custom_costing: nil, custCity: nil, custProvince: nil)
  # Check if costing is for a custom city and province.  If not use the city and province found in the first entry
  # of the array of costed items.
  if custCity.nil? || custProvince.nil?
    costCity = btap_items['City'].to_s
    costProvince = btap_items['Province'].to_s
  else
    costCity = custCity
    costProvince = custProvince
  end

  # Initialize cost counters
  totCost = 0.0
  envCost = 0.0
  lightCost = 0.0
  heatCoolCost = 0.0
  shwCost = 0.0
  ventCost = 0.0
  renewCost = 0.0

  custom_costing.nil? ? costingDB = @costing_database['costs'] : costingDB = custom_costing

  btap_items['Items'].each do |costing_item|
    # Look for the costing information for the piece of equipment in the costing database.
    costing_data = costingDB.detect {|data| data['id'].to_s.upcase == costing_item['id'].to_s.upcase}
    # If no costing information is found then return an error.
    if costing_data.nil?
      raise "Error: no costing information available for material id #{costing_item['id']}!"
    elsif costing_data['baseCosts']['materialOpCost'].nil? && costing_data['baseCosts']['laborOpCost'].nil?
      #This is a stub for some work that needs to be done to account for equipment costing. For now this is zeroed out.
      # A similar test is done on reading the data from the database and collected in the error file when the
      # costing database is generated.
      raise "Error: costing information for material id #{costing_item['id']} is nil.  Please check costing data."
    end
    costing_data['baseCosts']['equipmentOpCost'].nil? ? equip_base_cost = 0.0 : equip_base_cost = costing_data['baseCosts']['equipmentOpCost'].to_f
    costing_data['baseCosts']['materialOpCost'].nil? ? mat_base_cost = 0.0 : mat_base_cost = costing_data['baseCosts']['materialOpCost'].to_f
    costing_data['baseCosts']['laborOpCost'].nil? ? lab_base_cost = 0.0 : lab_base_cost = costing_data['baseCosts']['laborOpCost'].to_f

    # The costs from the costing database are US national average costs (for placeholder costs) or whatever is in the
    # 'province_state' and 'city' fields (for custom costs).  These costs need to be adjusted to reflect the costs
    # expected in the location of interest.  The 'get_regional_cost_factors' method finds the appropriate cost
    # adjustment factors.

    mat_mult, inst_mult, eq_mult = get_regional_cost_factors(costProvince, costCity, costing_item)
    if mat_mult.nil? || inst_mult.nil?
      raise("Error: no localization information available for material id #{costing_item['material_id']}!")
    end
    # Get any associated material or labour multiplier for the equipment present in the 'materials_hvac' sheet in the
    # costing spreadsheet.
    costing_item['material_mult'].to_f == 0 ? mat_quant = 1.0 : mat_quant = costing_item['material_mult'].to_f
    costing_item['labour_mult'].to_f == 0 ? lab_quant = 1.0 : lab_quant = costing_item['labour_mult'].to_f
    costing_item['equipment_mult'].to_f == 0 || costing_item['equipment_mult'].nil? ? eq_quant = 1.0 : eq_quant = costing_item['equipment_mult'].to_f
    # Calculate the adjusted material and labour costs.
    mat_cost = mat_base_cost*(mat_mult/100.0)*mat_quant
    lab_cost = lab_base_cost*(inst_mult/100.0)*lab_quant
    eq_cost = equip_base_cost*(eq_mult/100.0)*eq_quant
    # Calculate the total item cost.
    item_cost = (mat_cost + lab_cost + eq_cost)*(costing_item["quantity"].to_f)

    # Add cost to sub-type counters
    envCost += item_cost unless (costing_item['tags'].select{|data| data.to_s.upcase == "ENVELOPE"}).empty?
    lightCost += item_cost unless (costing_item['tags'].select{|data| data.to_s.upcase == "LIGHTING"}).empty?
    heatCoolCost += item_cost unless (costing_item['tags'].select{|data| data.to_s.upcase == "HEATING_COOLING"}).empty?
    shwCost += item_cost unless (costing_item['tags'].select{|data| data.to_s.upcase == "SHW"}).empty?
    ventCost += item_cost unless (costing_item['tags'].select{|data| data.to_s.upcase == "VENTILATION"}).empty?
    renewCost += item_cost unless (costing_item['tags'].select{|data| data.to_s.upcase == "RENEWABLES"}).empty?
    totCost += item_cost
  end

  # Create and return hash containing costing results
  costRetHash = {
    'envelope' => envCost.round(2),
    'lighting' => lightCost.round(2),
    'heating_and_cooling' => heatCoolCost.round(2),
    'shw' => shwCost.round(2),
    'ventilation' => ventCost.round(2),
    'renewables' => renewCost.round(2),
    'grand_total' => totCost.round(2)
  }
  return costRetHash
end

#cost_shw_main(mech_room:, roof_cent:, min_space:) ⇒ Object



503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
# File 'lib/openstudio-standards/btap/costing/shw_costing.rb', line 503

def cost_shw_main(mech_room:, roof_cent:, min_space:)
  shw_dist_search = []
  building_height_m = (roof_cent[:roof_centroid][2] - min_space[:roof_cent][2]).abs
  mech_to_cent_dist_m = (roof_cent[:roof_centroid][0] - mech_room['space_centroid'][0]).abs + (roof_cent[:roof_centroid][1] - mech_room['space_centroid'][1]).abs
  #Twice the distance to account for supply and return shw piping.
  total_dist_m = 2*(building_height_m + mech_to_cent_dist_m)
  total_dist_ft = OpenStudio.convert(total_dist_m, 'm', 'ft').get
  shw_dist_search << {
      mat: 'CopperPipe',
      unit: 'L.F.',
      size: 0.75,
      mult: total_dist_ft
  }
  total_comp_cost = get_comp_cost(cost_info: shw_dist_search)
  return {
      length_m: total_dist_m,
      cost: total_comp_cost
  }
end

#costVRFCondenser(model:, maxHeightDiff:, regMat:, regLab:, regMatElec:, regLabElec:, roofHeight:) ⇒ Object

Costing for the VRF Condenser(s) and the wiring and piping conecting the Condenser(s) to the branch distributors on each floor of the building with thermal zones served by a VRF system.

Taking in: model(hash): OpenStudio building model maxHeightDiff(float, m): Difference between height of highest ceiling and ceiling of lowest space served by a VRF

system.

regMat(float): HVAC regional cost factor for material. regLab(float): HVAC regional cost factor for labour. RegMatElec(float): Electrical regional cost factor for material. RegLabElec(float): Electrical regional cost factor for labour.

Returns: Total VRF condensor cost (also adds information to @costing_report).



2300
2301
2302
2303
2304
2305
2306
2307
2308
2309
2310
2311
2312
2313
2314
2315
2316
2317
2318
2319
2320
2321
2322
2323
2324
2325
2326
2327
2328
2329
2330
2331
2332
2333
2334
2335
2336
2337
2338
2339
2340
2341
2342
2343
2344
2345
2346
2347
2348
2349
2350
2351
2352
2353
2354
2355
2356
2357
2358
2359
2360
2361
2362
2363
2364
2365
2366
2367
2368
2369
2370
2371
2372
2373
2374
2375
2376
2377
2378
2379
2380
2381
2382
2383
2384
2385
2386
2387
2388
2389
2390
2391
2392
2393
2394
2395
2396
2397
2398
2399
2400
2401
2402
2403
# File 'lib/openstudio-standards/btap/costing/heating_cooling_costing.rb', line 2300

def costVRFCondenser(model:, maxHeightDiff:, regMat:, regLab:, regMatElec:, regLabElec:, roofHeight:)
  # VRF systems have a maximum height difference of 50m.  If the height difference calculated above is greater than
  # 50m then determine how many VRF condensers are required (one every 50m)
  numVRFheight = 1.0
  if maxHeightDiff > 50.0
    (maxHeightDiff % 50.0).round(1) > 0.0 ? numVRFheight = (maxHeightDiff / 50.0).to_i.to_f + 1.0 : numVRFheight = (maxHeightDiff / 50.0).to_f.round(0)
  end
  # Get the VRF condenser and calculate the overall capacity as the largest of the heating or cooling capacities.
  vrfCond = model.getAirConditionerVariableRefrigerantFlows[0]
  if vrfCond.isGrossRatedHeatingCapacityAutosized.to_bool
    heatingCapkW = vrfCond.autosizedGrossRatedHeatingCapacity.to_f/1000.0
  else
    heatingCapkW = vrfCond.grossRatedHeatingCapacity.to_f/1000.0
  end
  if vrfCond.isGrossRatedTotalCoolingCapacityAutosized.to_bool
    coolingCapkW = vrfCond.autosizedGrossRatedTotalCoolingCapacity.to_f/1000.0
  else
    coolingCapkW = vrfCond.grossRatedTotalCoolingCapacity.to_f/1000.0
  end
  heatingCapkW >= coolingCapkW ? vrfCondCapkW = heatingCapkW : vrfCondCapkW = coolingCapkW
  # If more than one VRF condenser is present because of a large height difference then assume each VRF condenser will
  # serve the same fraction of the load.  Divide the revised capacity as the original capacity divided by the number
  # of VRF condensers required to compensate for a height difference (the default is 1).
  modVRFCondCapkW = vrfCondCapkW / numVRFheight
  vrfCondInfo = getHVACDBInfo(name: "VRF Condenser Unit", materialLookup: "VRF-HP-HRV-Outdoor", materialSize: modVRFCondCapkW, exactMatch: false)
  vrfSizeMult = vrfCondInfo[:multiplier]
  vrfMat, vrfLab = getCost(vrfCondInfo[:name], vrfCondInfo[:hvac_material], vrfCondInfo[:multiplier])
  vrfCondCost = (vrfMat * regMat + vrfLab * regLab) / 100

  # Cost the refrigerant tubing (assume 20' of 0.5" supply and 1.0833" return tubing)
  # vrfCondSizeTon = (OpenStudio.convert(modVRFCondCapkW.to_f, 'kW', 'kBtu/hr').get)/12.0
  refrigPipeMat, refrigPipeLab = getHVACCost("Refrigerant Piping", 'refrig-tubing-large', 20, true)
  refrigPipingCost = (refrigPipeMat * regMat + refrigPipeLab * regLab) / 100

  # Cost insulation for tubing (assume 20' of 1.25" pipe insulation for both the supply and return refrigerant tubing)
  refrigInsMat, refrigInsLab = getHVACCost('Refrigerant Insulation', 'pipeinsulation', 1.25, true)
  refrigInsulationCost = (refrigInsMat * regMat + refrigInsLab * regLab) * 2 * 20 / 100

  # Cost the wiring
  vrfWireMat, vrfWireLab = getHVACCost('VRF Wiring', 'wiring', 10, true)
  vrfWireLength = 20
  vrfWiringCost = ((vrfWireMat*regMatElec + vrfWireLab*regLabElec)/100)*vrfWireLength/100

  # Cost the Conduit
  vrfConduitMat, vrfConduitLab = getHVACCost('VRF Wiring Conduit', 'Conduit', nil, true)
  vrfConduitCost = ((vrfConduitMat*regMatElec + vrfConduitLab*regLabElec)/100)*vrfWireLength

  # Cost the disconnect
  vrfDiscMat, vrfDiscLab = getHVACCost('VRF Wiring Disconnect', 'Safety_switch', 60, true)
  vrfDiscCost = (vrfDiscMat*regMatElec + vrfDiscLab*regLabElec)/100

  # Determine the Tubing and wiring between the branch distributors and the condenser unit on the roof.  This cost is
  # included with the condenser cost since this is for the entire building and only depends on the distance between
  # the height difference between the lowest space served by a VRF system and the roof center height.  It will be the
  # same cost even if there are several condensers because of height restrictions).
  #
  # Get the refrigerant tubing cost.  Exterior refrigerant tubing is given in 10' lengths of 0.5" supply and 1-1/8"
  # return tubing.  The tubing is assumed to run inside the building so no insulation or all-weather protection is
  # provided.
  buildRefrigTubingMat, buildRefrigTubingLab = getHVACCost("VRF Building Refrigerant Tubing", 'refrig-tubing-large', 10, true)
  # Get distance between lowest floor served by the VRF system and the roof in feet
  maxHeightDiffFt = OpenStudio.convert(maxHeightDiff, 'm', 'ft').get
  # Tubing cost divided by ten because tubing costing is provided in 10 ft rolls
  buildRefrigCost = ((buildRefrigTubingMat * regMat + buildRefrigTubingLab * regLab) / 100) * maxHeightDiffFt / 10.0

  # Get the cost of condensate tubing for the whole building.  A different height is used than the that for
  # refrigerant tubing since the condensate line must extend from the height of the roof (where the condenser is) to
  # the ground floor (if maxHeightDiff does not extend the entire building height) or basement ()if maxHeightDiff
  # includes a basement).
  buildCondMat, buildCondLab = getHVACCost('Building Condensate pipe', 'PEX_tubing', 0.5, true)
  buildCondCost = ((buildCondMat * regMat + buildCondLab * regLab) / 100) * maxHeightDiffFt

  # Get the wiring cost (note wiring comes in 100 ft lengths)
  buildWiringMat, buildWiringLab = getHVACCost('VRF Wiring', 'wiring', 10, true)
  buildWiringCost = ((buildWiringMat * regMatElec + buildWiringLab * regLabElec) / 100) * maxHeightDiffFt / 100.0

  # Get the conduit cost
  buildConduitMat, buildConduitLab = getHVACCost("VRF Building Conduit", 'Conduit', nil, true)
  buildConduitCost = ((buildConduitMat * regMatElec + buildConduitLab * regLabElec) / 100) * maxHeightDiffFt

  # Find totals
  totalVRFCondCost = vrfCondCost * numVRFheight
  totalVRFPipingCost = (refrigPipingCost + refrigInsulationCost) * numVRFheight * vrfSizeMult + buildRefrigCost + buildCondCost
  totalVRFWiringCost = (vrfWiringCost + vrfConduitCost + vrfDiscCost) * numVRFheight * vrfSizeMult + buildWiringCost + buildConduitCost
  totalVRFEquipCost = totalVRFCondCost + totalVRFPipingCost + totalVRFWiringCost


  # Add to VRF Condenser cost report.  I was not sure where to put this since it was really neither a plant unit or a
  # zonal unit.  I guess it supplies several zones so that makes it plant equipment.
  @costing_report['heating_and_cooling']['plant_equipment']  << {
    'type' => 'VRF Zonal System Condenser',
    'nom_flr2flr_hght_ft' => 0.0,
    'ht_roof_ft' => maxHeightDiffFt.round(1),
    'longest_distance_to_ext_ft' => 0.0,
    'wiring_and_gas_connections_distance_ft' => (vrfWireLength*numVRFheight*vrfSizeMult + maxHeightDiffFt).round(1),
    'equipment_cost' => totalVRFCondCost.round(0),
    'wiring_and_gas_connections_cost' => totalVRFWiringCost.round(0),
    'pump_cost' => 0.00,
    'piping_cost' => totalVRFPipingCost.round(0),
    'total_cost' => totalVRFEquipCost.round(0)
  }

  return totalVRFEquipCost
end

#determine_ahu_htg_clg_fuel(heat_cap:, cool_cap:, heat_type:, cool_type:) ⇒ Object

This method determines the main heating fuel and cooling type used by an air handling unit (a given model’s air loop). The method also determines the ahu’s supplementary heating type (if any) if the primary heater is a heat pump. All capacities are in KW.

Inputs:

heat_cap: The capacity of heaters in the supply side of the air loop. This is used to determine the main heating

type used by the ahu.  They can be the following types:
 HP (Heat Pump)
 elec (Electricity)
 Gas
 HW (Hot Water)
 CCASHP (Cold Climate Air Source Heat Pump)

cool_cap: The capacity of cooling units in the supply side of the air loop. This is used to determine the main

cooling type used by the ahu.  They can be the following types:
DX (Direct Expansion)
CHW (Chilled Water)
Note that HP and CCASHP are not inculded.  If the the main heating type is a HP or CCASHP and the main
cooling type is DX then the the main cooling type will be reported as being the same as the main heating
type.

heat_type: This is a hash of counters used to determine what services (electrical lines, hot water pipes, chilled

water pipes, etc.) need to be run from the main mechanical room (where they are assumed to originate) to
the roof of the building (where the ahu's are located).  The following tpes are used:
HP:  Heat pump (also used for CCASHP, esentially just an electircal line is needed which is always
inculded anyway)
elec: Electricity (an electrical line is needed which is always inculded anyway)
Gas: Gas (a gas line is needed)
HW: Hot water (a hot water line is needed)

cool_type: This is the same as heat_type only for cooling. This is really just used to determine if a chilled water

line is needed since an electrical line is always inculded.
DX:  Direct Exchange (also used for HP and CCASHP since only the defaul electrical line is needed)
CHW:  Chilled water (a chilled water pipe is required)

Outputs: heat_cool_info: This is a hash that contains the return information which inculdes:

heating_fuel:  The primary heating fuel used by the ahu (and supplemental heating fuel if used by a HP
               or CCASHP).  This is used when searching the 'hvac_vent_ahu' sheet in the costing
               spreasheet when costing the ahu.
cooling_type:  The primary cooling type used by the ahu.  This is used when searching the
               'hvac_vent_ahu' sheet in the costing spreadsheet when costing the ahu.
heat_type:  See above (only counters adjusted)
cool_type:  See above (only counters adjusted)


376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
# File 'lib/openstudio-standards/btap/costing/ventilation_costing.rb', line 376

def determine_ahu_htg_clg_fuel(heat_cap:, cool_cap:, heat_type:, cool_type:)
  # Determine the predominant heating and cooling type by looking for the key associated with the largest value in the
  # heat_cap and cool_cap hashes.  For heating it returns HP, elec, Gas, HW or CCASHP and for cooling it returns CHW
  # or DX.
  heating_fuel = heat_cap.max_by{|key, value| value}[0]
  cooling_type = cool_cap.max_by{|key, value| value}[0]

  # Increase the counter of the associated cooling type by 1
  cool_type[cooling_type] += 1


  # If a variety of heat pump (regular HP or CCASHP) is present then, for costing, it is assumed to be the primary
  # heating type for the ahu.
  if heat_cap['HP'] > 0 || heat_cap['CCASHP'] > 0
    # Increase the heat_type counter for heat pump by 1.
    heat_type['HP'] += 1

    # Get the capacities of just the HP and CCASHP.
    pri_hp_type = {
      'HP' => heat_cap['HP'],
      'CCASHP' => heat_cap['CCASHP']
    }
    # Use the same technique for heating_fuel and cooling_type to determine which type of heat pump has the largest
    # capacity.  This is used in the off chance that more than one heat pump type is present (I'm not even sure that
    # is possible in OpenStudio air loops but I include this little bit of edge case handling anyway).
    hp_type = pri_hp_type.max_by{|key, value| value}[0].to_s
    heating_fuel = hp_type
    # It is possible to heat your building with an ASHP and use chilled water to cool your building.  I don't know why
    # you would do that but we can cost the ahu if you do.  If the main cooling type is DX (which is highly likely
    # if you are heating your air loop with an ASHP) then the main cooling type is set to be the main heating type
    # of the air loop (either regular HP or fancy CCAHP).
    if cooling_type == 'DX'
      cooling_type = hp_type
    end
    # This determines if supplemental heating is used with your heat pump (very likely in most of Canada if you heat
    # with a HP).
    unless (heat_cap['elec'] == 0) && (heat_cap['Gas'] == 0) && (heat_cap['HW'] == 0)
      # Create a hash of just the fuel heating in the air loop and change the hash key to match what we will look for
      # in the hvac_vent_ahu sheet in the costing speadsheet
      hp_supp_cap = {
        '-e' => heat_cap['elec'],
        '-g' => heat_cap['Gas'],
        '-hw' => heat_cap['HW'],
      }
      # Look for the key (which is the fuel type) with the largest associated value (which is the capacity).
      hp_supp = hp_supp_cap.max_by{|key, value| value}[0].to_s
      # Increase the heat_type count for the associated supplement heat type.  This is necessary since if gas heating
      # is used as supplemental heatnig for a heat pump then a gas line will be required between the mechanical room
      # and the roof.
      case hp_supp
      when '-e'
        heat_type['elec'] += 1
      when '-g'
        heat_type['Gas'] += 1
      when 'hw'
        heat_type['HW'] += 1
      end
    end
    # Get the heating fuel by appending the supplementary heating type just determined (if any) to the heat pump type
    heating_fuel = hp_type
    heating_fuel += hp_supp  unless hp_supp.nil?
  else
    # If you do not use a heat pump then increase the heat_type counter for whatever fuel you use to heat the air loop
    # by 1.
    heat_type[heating_fuel] += 1
  end
  # Create the hash with the results and return it (I use a hash to return a bunch of results because it seems
  # cleaner).
  heat_cool_info = {
    heating_fuel: heating_fuel,
    cooling_type: cooling_type,
    heat_type: heat_type,
    cool_type: cool_type,
  }
  return heat_cool_info
end

#distance(loc1, loc2) ⇒ Object

Enter in [latitude, longitude] for each loc and this method will return the distance.



233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
# File 'lib/openstudio-standards/btap/costing/btap_costing.rb', line 233

def distance(loc1, loc2)
  rad_per_deg = Math::PI / 180 # PI / 180
  rkm = 6371 # Earth radius in kilometers
  rm = rkm * 1000 # Radius in meters

  dlat_rad = (loc2[0] - loc1[0]) * rad_per_deg # Delta, converted to rad
  dlon_rad = (loc2[1] - loc1[1]) * rad_per_deg

  lat1_rad, lon1_rad = loc1.map { |i| i * rad_per_deg }
  lat2_rad, lon2_rad = loc2.map { |i| i * rad_per_deg }

  a = Math.sin(dlat_rad / 2) ** 2 + Math.cos(lat1_rad) * Math.cos(lat2_rad) * Math.sin(dlon_rad / 2) ** 2
  c = 2 * Math::atan2(Math::sqrt(a), Math::sqrt(1 - a))
  rm * c # Delta in meters
end

#expandProvAbbrev(abbrev) ⇒ Object

This will expand the two letter province abbreviation to a full uppercase province name



263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
# File 'lib/openstudio-standards/btap/costing/btap_costing.rb', line 263

def expandProvAbbrev(abbrev)

  # Note that the proper abbreviation for Quebec is QC not PQ. However, we've used PQ in openstudio-standards!
  Hash provAbbrev = {"AB" => "ALBERTA",
                     "BC" => "BRITISH COLUMBIA",
                     "MB" => "MANITOBA",
                     "NB" => "NEW BRUNSWICK",
                     "NL" => "NEWFOUNDLAND AND LABRADOR",
                     "NT" => "NORTHWEST TERRITORIES",
                     "NS" => "NOVA SCOTIA",
                     "NU" => "NUNAVUT",
                     "ON" => "ONTARIO",
                     "PE" => "PRINCE EDWARD ISLAND",
                     "PQ" => "QUEBEC",
                     "SK" => "SASKATCHEWAN",
                     "YT" => "YUKON"
  }
  return provAbbrev[abbrev]
end

#floor_vent_dist_cost(hvac_floors:, prototype_creator:, roof_cent:, mech_sizing_info:) ⇒ Object



1810
1811
1812
1813
1814
1815
1816
1817
1818
1819
1820
1821
1822
1823
1824
1825
# File 'lib/openstudio-standards/btap/costing/ventilation_costing.rb', line 1810

def floor_vent_dist_cost(hvac_floors:, prototype_creator:, roof_cent:, mech_sizing_info:)
  floor_duct_cost = 0
  build_floor_trunk_info = []
  mech_table = get_mech_table(mech_size_info: mech_sizing_info, table_name: 'vel_prof')
  hvac_floors.each do |hvac_floor|
    next if hvac_floor[:tz_num] < 2 && hvac_floor[:floor_tz][0][:sys_type] == 3
    tz_floor_mult = (hvac_floor[:tz_mult].to_f)/(hvac_floor[:tz_num].to_f)
    floor_trunk_line = get_story_cent_to_edge(building_story: hvac_floor[:story], prototype_creator: prototype_creator, target_cent: roof_cent[:roof_centroid], full_length: true)
    current_floor_duct_cost, floor_trunk_info = get_floor_trunk_cost(mech_table: mech_table, hvac_floor: hvac_floor, prototype_creator: prototype_creator, floor_trunk_dist_m: floor_trunk_line[:end_point][:line][:dist])
    floor_duct_cost += current_floor_duct_cost*tz_floor_mult
    floor_trunk_info[:Floor] = hvac_floor[:story_name]
    floor_trunk_info[:Multiplier] = tz_floor_mult
    build_floor_trunk_info << floor_trunk_info
  end
  return floor_duct_cost, build_floor_trunk_info
end

#gas_burner_cost(heating_fuel:, sys_type:, airloop_flow_cfm:, mech_sizing_info:, costed_ahu_info:, vent_tags: [], report_mult: 1.0) ⇒ Object



795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
# File 'lib/openstudio-standards/btap/costing/ventilation_costing.rb', line 795

def gas_burner_cost(heating_fuel:, sys_type:, airloop_flow_cfm:, mech_sizing_info:, costed_ahu_info:, vent_tags: [], report_mult: 1.0)
  ahu_airflow_lps = costed_ahu_info[:ahu]["Supply_air"].to_f
  report_mult_mod = report_mult*(-1.0)
  burner_tags = vent_tags.clone
  if (sys_type == 3 || sys_type == 6)
    return 0
    mech_table = get_mech_table(mech_size_info: mech_sizing_info, table_name: 'ahu_airflow')
    coil_sizing_info = mech_table.select{|data| (data['ahu_airflow_range_lps'][0].to_f <= ahu_airflow_lps) && (data['ahu_airflow_range_lps'][1].to_f > ahu_airflow_lps) }
    if coil_sizing_info.empty?
      coil_sizing_kW = mech_table.max_by{|data| data['ahu_airflow_range_lps'][1]}
    else
      coil_sizing_kW = coil_sizing_info[0]
    end
    heating_kw = coil_sizing_kW['htg_coil_sizing_kW'].to_f
    cooling_kw = coil_sizing_kW['DX_coil_sizing_kW'].to_f
    heat_mech_eq_mult, heat_cost_info = get_vent_cost_data(equipment_info: {cat_search: 'coils', mech_capacity_kw: heating_kw})
    cool_mech_eq_mult, cool_cost_info = get_vent_cost_data(equipment_info: {cat_search: 'coils', mech_capacity_kw: cooling_kw})
    heating_coil_cost = heat_mech_eq_mult*get_vent_mat_cost(mat_cost_info: heat_cost_info, vent_tags: burner_tags, report_mult: (report_mult_mod*heat_mech_eq_mult))
    dx_coil_cost = cool_mech_eq_mult*get_vent_mat_cost(mat_cost_info: cool_cost_info, vent_tags: burner_tags, report_mult: (report_mult_mod*cool_mech_eq_mult))
    return heating_coil_cost + dx_coil_cost
  else
    if airloop_flow_cfm >= 1000 && airloop_flow_cfm <= 1500
      mult, mech_info = get_vent_cost_data(equipment_info: {cat_search: 'DuctFurGasExt', mech_capacity_kw: 88})
      return get_vent_mat_cost(mat_cost_info: mech_info, vent_tags: burner_tags, report_mult: (report_mult_mod*mult))*mult
    elsif airloop_flow_cfm > 1500
      mult, mech_info = get_vent_cost_data(equipment_info: {cat_search: 'DuctFurGasExt', mech_capacity_kw: 132})
      return get_vent_mat_cost(mat_cost_info: mech_info, vent_tags: burner_tags, report_mult: (report_mult_mod*mult))*mult
    end
  end
  return 0.0
end

#gen_hvac_info_by_floor(hvac_floors:, model:, prototype_creator:, airloop:, sys_type:, hrv_info:) ⇒ Object



1720
1721
1722
1723
1724
1725
1726
1727
1728
1729
1730
1731
1732
1733
1734
1735
1736
1737
1738
1739
1740
1741
1742
1743
1744
1745
1746
1747
1748
1749
1750
1751
1752
1753
1754
1755
1756
1757
1758
1759
1760
1761
1762
1763
1764
1765
1766
1767
1768
1769
1770
# File 'lib/openstudio-standards/btap/costing/ventilation_costing.rb', line 1720

def gen_hvac_info_by_floor(hvac_floors:, model:, prototype_creator:, airloop:, sys_type:, hrv_info:)
  airloop.thermalZones.sort.each do |tz|
    tz.equipment.sort.each do |eq|
      tz_mult = tz.multiplier.to_f
      terminal, box_name = get_airloop_terminal_type(eq: eq)
      next if terminal.nil?
      if terminal.isMaximumAirFlowRateAutosized.to_bool
        query = "SELECT Value FROM ComponentSizes WHERE CompName='#{eq.name.to_s.upcase}' AND Description='Design Size Maximum Air Flow Rate'"
        tz_air = model.sqlFile().get().execAndReturnFirstDouble(query).to_f/tz_mult
      else
        tz_air = terminal.maximumAirFlowRate.to_f/tz_mult
      end
      tz_cents = prototype_creator.thermal_zone_get_centroid_per_floor(tz)
      tz_cents.each do |tz_cent|
        story_floor_area = 0
        tz_outdoor_air_m3ps = 0
        tz_cent[:spaces].each do |space|
          # Note that space.floorArea gets the floor area for the space only and does not include a thermal zone multiplier.
          # Thus the outdoor air flow rate totaled here will be for only one thermal zone and will not include thermal zone multipliers.
          story_floor_area += space.floorArea.to_f
          outdoor_air_obj = space.designSpecificationOutdoorAir
          outdoor_air_obj.is_initialized ? outdoor_air_m3ps = (outdoor_air_obj.get.outdoorAirFlowperFloorArea)*(space.floorArea.to_f) : outdoor_air_m3ps = 0
          tz_outdoor_air_m3ps += outdoor_air_m3ps
        end
        story_obj = tz_cent[:spaces][0].buildingStory.get
        floor_area_frac = (story_floor_area/tz.floorArea).round(2)
        tz_floor_air = floor_area_frac*tz_air
        (sys_type == 1 || sys_type == 4) ? tz_floor_return = 0 : tz_floor_return = tz_floor_air
        tz_floor_system = {
            story_name: tz_cent[:story_name],
            story: story_obj,
            sys_name: airloop.nameString,
            sys_type: sys_type,
            sys_info: airloop,
            tz: tz,
            tz_mult: tz_mult,
            terminal: terminal,
            floor_area_frac: floor_area_frac,
            tz_floor_area: story_floor_area,
            tz_floor_supp_air_m3ps: tz_floor_air,
            tz_floor_ret_air_m3ps: tz_floor_return,
            tz_floor_outdoor_air_m3ps: tz_outdoor_air_m3ps,
            hrv_info: hrv_info,
            tz_cent: tz_cent
        }
        hvac_floors = add_floor_sys(hvac_floors: hvac_floors, tz_floor_sys: tz_floor_system)
      end
    end
  end
  return hvac_floors
end

#generate_construction_cost_database_for_all_citiesObject



67
68
69
70
71
72
73
74
75
# File 'lib/openstudio-standards/btap/costing/btap_costing.rb', line 67

def generate_construction_cost_database_for_all_cities()
  result = Array.new
  @costing_database['raw']['locations'].each do |location|
    province_state = location["province_state"]
    city = location['city']
    result.concat(generate_construction_cost_database_for_city(city, province_state))
  end
  return result
end

#generate_construction_cost_database_for_city(city, province_state) ⇒ Object



77
78
79
80
81
82
83
84
85
86
87
88
# File 'lib/openstudio-standards/btap/costing/btap_costing.rb', line 77

def generate_construction_cost_database_for_city(city, province_state)
  @costing_database['constructions_costs'] = Array.new
  puts "Costing for: #{province_state},#{city}"
  @costing_database["raw"]['constructions_opaque'].each do |construction|
    cost_construction(construction, {"province_state" => province_state, "city" => city}, 'opaque')
  end
  @costing_database["raw"]['constructions_glazing'].each do |construction|
    cost_construction(construction, {"province_state" => province_state, "city" => city}, 'glazing')
  end
  puts "#{@costing_database['constructions_costs'].size} Costed Constructions for #{province_state},#{city}."
  return @costing_database['constructions_costs']
end

#get_ahu_mult(loop_equip:) ⇒ Object

This method finds ahu with the largest supply air capacity based on the heating and cooling characteristics defined by loop_equip. Loop_equip is a hash which includes: sys_type: the NECB HVAC system type (one of 1, 3, 4, or 6) heating_fuel: The predominant heating fuel cooling_type: The predominant cooling type airloop_flow_lps: The air loop flow rate (L/s) airloop_name: The name of the air loop (used in an error message)

If no air handler is found that meets the above requirements raise an error telling the user that something is wrong. If one or more air handlers are found choose the one with the larges ‘Supply_air’. This defines the largest air handler of the given type. Then divide the air loop air flow by the maximum air flow available. Round up and this number defines how many air handlers are required to meet the load.

In some cases, the air loop flow rate is only a little larger than that available by the largest air handler. For example, an air loop may have a flow rate of 16000 L/s but the largest available air handler is 15000 L/s. Rather than costing two 15000 L/s air handlers it would be cheaper to cost two 8000 L/s air handlers. To do this, the method divides the air_loop_flow_lps by the number of required air handlers. It then looks for air handlers which meet the required characteristics and revised air flow rate. If more than one are found it selects the smallest one available. It then returns this new air handler along with the raw number of air handlens (which may be a fraction) and the maximum number (which will be an integer).



568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
# File 'lib/openstudio-standards/btap/costing/ventilation_costing.rb', line 568

def get_ahu_mult(loop_equip:)
  # Look for the largest air handler that matches the system type, heating fuel, and cooling type requirements
  ahu = @costing_database['raw']['hvac_vent_ahu'].select {|data|
    data['Sys_type'].to_f.round(0) == loop_equip[:sys_type].to_f.round(0) and
        data['Htg'].to_s.upcase == loop_equip[:heating_fuel].to_s.upcase and
        data['Clg'].to_s.upcase == loop_equip[:cooling_type].to_s.upcase
  }.max_by {|element| element['Supply_air'].to_f}
  # If none are found something has gone wrong.  Tell the user.
  if ahu.nil? || ahu.empty?
    raise "Error: no ahu information available for equipment #{loop_equip[:airloop_name]}!"
  end
  # I probably don't need to check this but make sure that the air handler has a size larger than 0.
  if ahu['Supply_air'].to_f <= 0
    raise "Error: #{loop_equip[:airloop_name]} has a size of 0 or less.  Please check that the correct costing_database.json file is being used or check the costing spreadsheet!"
  end
  # Determine the number of air handlers to be the air loop flow rate divided by the maximum air handler size.  This
  # will likely not be a whole number.
  mult = (loop_equip[:airloop_flow_lps].to_f) / (ahu['Supply_air'].to_f)
  # Since air handlers only come in integer numbers (half and air handler would not be too useful) round up to the
  # next whole number (the if statement is for the off chance that the required number ended up being an integer).
  mult > (mult.to_i).to_f.round(0) ? multiplier = (mult.to_i).to_f.round(0) + 1 : multiplier = mult.round(0)
  # Get the revised required air flow rate by dividing the air loop air flow by the number of air handlers
  rev_air_flow = loop_equip[:airloop_flow_lps].to_f / multiplier
  # Find air handlers that can meet that air flow and choose the smallest one that meets the requirement.
  rev_ahu = @costing_database['raw']['hvac_vent_ahu'].select {|data|
    data['Sys_type'].to_f.round(0) == loop_equip[:sys_type].to_f.round(0) and
      data['Htg'].to_s.upcase == loop_equip[:heating_fuel].to_s.upcase and
      data['Clg'].to_s.upcase == loop_equip[:cooling_type].to_s.upcase and
      data['Supply_air'].to_f >= rev_air_flow
  }.min_by{|info| info['Supply_air'].to_f}
  # If none are found something weird is happening so keep the one you already found.
  if rev_ahu.nil? || rev_ahu.empty?
    # Something weird happened, keep the ahu you found before.
  else
    ahu = rev_ahu
  end
  return ahu, multiplier, rev_air_flow
end

#get_airloop_terminal_type(eq:) ⇒ Object



1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
# File 'lib/openstudio-standards/btap/costing/ventilation_costing.rb', line 1075

def get_airloop_terminal_type(eq:)
  case eq.iddObject.name
  when /OS:AirTerminal:SingleDuct:ConstantVolume:Reheat/
    terminal = eq.to_AirTerminalSingleDuctConstantVolumeReheat.get
    box_name = 'CVMixingBoxes'
  when /OS:AirTerminal:SingleDuct:VAV:NoReheat/
    terminal = eq.to_AirTerminalSingleDuctVavNoReheat.get
    box_name = 'VAVFanMixingBoxesClg'
  when /OS:AirTerminal:SingleDuct:VAV:Reheat/
    terminal = eq.to_AirTerminalSingleDuctVAVReheat.get
    box_name = 'VAVFanMixingBoxesHtg'
  when /OS:AirTerminal:SingleDuct:ConstantVolume:NoReheat/
    terminal = eq.to_AirTerminalSingleDuctConstantVolumeNoReheat.get
    box_nam = nil
  else
    terminal = nil
    box_name = nil
  end
  return terminal, box_name
end

#get_closest_cost_location(lat, long) ⇒ Object



249
250
251
252
253
254
255
256
257
258
259
260
# File 'lib/openstudio-standards/btap/costing/btap_costing.rb', line 249

def get_closest_cost_location(lat, long)
  dist = 1000000000000000000000.0
  closest_loc = nil
  # province_state	city	latitude	longitude	source
  @costing_database['raw']['locations'].each do |location|
    if distance([lat, long], [location['latitude'].to_f, location['longitude'].to_f]) < dist
      closest_loc = location
      dist = distance([lat, long], [location['latitude'].to_f, location['longitude'].to_f])
    end
  end
  return closest_loc
end

#get_comp_cost(cost_info:, vent_tags: [], report_mult: 1.0) ⇒ Object



1282
1283
1284
1285
1286
1287
1288
1289
1290
1291
1292
1293
1294
1295
1296
1297
1298
1299
1300
1301
1302
1303
1304
1305
1306
1307
1308
1309
1310
1311
1312
# File 'lib/openstudio-standards/btap/costing/ventilation_costing.rb', line 1282

def get_comp_cost(cost_info:, vent_tags: [], report_mult: 1.0)
  vent_comp_tags = vent_tags.clone
  cost = 0
  cost_info.each do |comp|
    comp_info = nil
    if comp[:unit].to_s == 'none'
      comp_info = @costing_database['raw']['materials_hvac'].select {|data|
        data['Material'].to_s.upcase == comp[:mat].to_s.upcase and
            data['Size'].to_f.round(2) == comp[:size].to_f.round(2)
      }.first
    elsif comp[:size].to_f == 0
      comp_info = @costing_database['raw']['materials_hvac'].select {|data|
        data['Material'].to_s.upcase == comp[:mat].to_s.upcase and
            data['unit'].to_s.upcase == comp[:unit].to_s.upcase
      }.first
    else
      comp_info = @costing_database['raw']['materials_hvac'].select {|data|
        data['Material'].to_s.upcase == comp[:mat].to_s.upcase and
            data['Size'].to_f.round(2) == comp[:size].to_f.round(2) and
            data['unit'].to_s.upcase == comp[:unit].to_s.upcase
      }.first
    end
    if comp_info.nil?
      puts("No data found for #{comp}!")
      raise
    end
    # report_mult included for cost list output.
    cost += get_vent_mat_cost(mat_cost_info: comp_info, vent_tags: vent_comp_tags, report_mult: (comp[:mult].to_f*report_mult))*(comp[:mult].to_f)
  end
  return cost
end

#get_cost_info(mat:, size: nil, unit: nil) ⇒ Object



675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
# File 'lib/openstudio-standards/btap/costing/shw_costing.rb', line 675

def get_cost_info(mat:, size: nil, unit: nil)
  comp_info = nil
  if unit.nil?
    comp_info = @costing_database['raw']['materials_hvac'].select {|data|
      data['Material'].to_s.upcase == mat.to_s.upcase and
        data['Size'].to_f.round(2) == size.to_f.round(2)
    }.first
  elsif size.nil?
    comp_info = @costing_database['raw']['materials_hvac'].select {|data|
      data['Material'].to_s.upcase == mat.to_s.upcase and
        data['unit'].to_s.upcase == unit.to_s.upcase
    }.first
  elsif size.nil? && unit.nil?
    comp_info = @costing_database['raw']['materials_hvac'].select {|data|
      data['Material'].to_s.upcase == mat.to_s.upcase
    }.first
  else
    comp_info = @costing_database['raw']['materials_hvac'].select {|data|
      data['Material'].to_s.upcase == mat.to_s.upcase and
        data['Size'].to_f.round(2) == size.to_f.round(2) and
        data['unit'].to_s.upcase == unit.to_s.upcase
    }.first
  end
  if comp_info.nil?
    puts("No data found for material: #{mat}, size: #{size}, with unit: #{unit}")
    raise
  end
  return comp_info
end

#get_duct_cost(cost_info:) ⇒ Object



1904
1905
1906
1907
1908
1909
1910
1911
1912
1913
1914
1915
1916
1917
1918
1919
1920
1921
1922
1923
1924
1925
1926
1927
# File 'lib/openstudio-standards/btap/costing/ventilation_costing.rb', line 1904

def get_duct_cost(cost_info:)
  comp_info = nil
  comp_info_all = @costing_database['raw']['materials_hvac'].select {|data|
    data['Material'].to_s.upcase == cost_info[:mat].to_s.upcase and
        data['Size'].to_f.round(1) >= cost_info[:size].to_f.round(1) and
        data['unit'].to_s.upcase == cost_info[:unit].to_s.upcase
  }
  if comp_info_all.nil? || comp_info_all.empty?
    max_size_info = @costing_database['raw']['materials_hvac'].select {|data|
      data['Material'].to_s.upcase == cost_info[:mat].to_s.upcase
    }
    if max_size_info.nil?
      puts("No data found for #{cost_info}!")
      raise
    end
    comp_info = max_size_info.max_by {|element| element['Size'].to_f}
  elsif comp_info_all.size == 1
    comp_info = comp_info_all[0]
  else
    comp_info = comp_info_all.min_by{|data| data['Size'].to_f}
  end
  cost = get_vent_mat_cost(mat_cost_info: comp_info)*cost_info[:mult].to_f
  return cost, comp_info
end

#get_fan_cap(fan:, model:) ⇒ Object



2104
2105
2106
2107
2108
2109
2110
2111
2112
2113
2114
2115
2116
2117
2118
2119
2120
2121
2122
2123
2124
2125
# File 'lib/openstudio-standards/btap/costing/ventilation_costing.rb', line 2104

def get_fan_cap(fan:, model:)
  fan_type = fan.iddObjectType.valueName.to_s
  case fan_type
  when /OS_Fan_VariableVolume/
    fan_obj = fan.to_FanVariableVolume.get
    if fan_obj.isMaximumFlowRateAutosized
      fan_cap_m3ps = fan_obj.autosizedMaximumFlowRate.to_f
    else
      fan_cap_m3ps = fan_obj.maximumFlowRate.to_f
    end
  when /OS_Fan_ConstantVolume/
    fan_obj = fan.to_FanConstantVolume.get
    if fan_obj.isMaximumFlowRateAutosized
      fan_cap_m3ps = fan_obj.autosizedMaximumFlowRate.to_f
    else
      fan_cap_m3ps = fan_obj.maximumFlowRate.to_f
    end
  else
    fan_cap_m3ps = 0
  end
  return fan_cap_m3ps
end

#get_fixture_type_id(fixture_info:, sheet_name:, row_name_1:, row_name_2:, row_name_3:, column_search:) ⇒ Object



339
340
341
342
343
344
345
346
347
348
349
350
351
# File 'lib/openstudio-standards/btap/costing/daylighting_sensor_control_costing.rb', line 339

def get_fixture_type_id(fixture_info:, sheet_name:, row_name_1:, row_name_2:, row_name_3:, column_search:)
  fixture_type = nil
  fixture_type = @costing_database['raw'][sheet_name].select { |data|
    data[row_name_1].to_s.upcase == fixture_info[:row_id_1].to_s.upcase and
        data[row_name_2].to_s.upcase == fixture_info[:row_id_2].to_s.upcase and
        data[row_name_3].to_s.upcase == fixture_info[:row_id_3].to_s.upcase
  }.first
  if fixture_type.nil?
    puts("No data found for #{fixture_type}!")
    raise
  end
  return fixture_type[column_search]
end

#get_floor_trunk_cost(mech_table:, hvac_floor:, prototype_creator:, floor_trunk_dist_m:, fric_allow: 1) ⇒ Object



1827
1828
1829
1830
1831
1832
1833
1834
1835
1836
1837
1838
1839
1840
1841
1842
1843
1844
1845
1846
1847
1848
1849
1850
1851
1852
1853
1854
1855
1856
1857
1858
1859
1860
1861
1862
1863
1864
1865
1866
1867
1868
1869
1870
1871
1872
1873
1874
1875
1876
1877
1878
1879
1880
1881
1882
1883
1884
1885
1886
1887
1888
1889
1890
1891
1892
1893
1894
1895
1896
1897
1898
1899
1900
1901
1902
# File 'lib/openstudio-standards/btap/costing/ventilation_costing.rb', line 1827

def get_floor_trunk_cost(mech_table:, hvac_floor:, prototype_creator:, floor_trunk_dist_m:, fric_allow: 1)
  floor_trunk_info = {
      Floor: '',
      Predominant_space_type: 0,
      SupplyDuctSize_in: 0,
      SupplyDuctLength_m: 0,
      ReturnDuctSize_in: 0,
      ReturnDuctLength_m: 0,
      TotalDuctCost: 0,
      Multiplier: 1
  }
  floor_trunk_cost = 0
  duct_comp_search = []
  floor_trunk_dist = (OpenStudio.convert(floor_trunk_dist_m, 'm', 'ft').get)
  space_type = get_predominant_floor_space_type_area(hvac_floor: hvac_floor, prototype_creator: prototype_creator)
  floor_trunk_info[:Predominant_space_type] = space_type[:space_type]
  loor_vel_fpm = nil
  mech_table.each do |vel_prof|
    spc_type_name = nil
    spc_type_name = vel_prof['space_types'].select {|spc_type|
      spc_type.to_s.upcase == space_type[:space_type].to_s.upcase
    }.first
    floor_vel_fpm = vel_prof['vel_fpm'].to_f unless spc_type_name.nil?
  end
  floor_vel_fpm = mech_table[mech_table.size - 1]['vel_fpm'].to_f if floor_vel_fpm.nil?
  supply_flow_cfm = (OpenStudio.convert(hvac_floor[:supply_air_m3ps], 'm^3/s', 'cfm').get)
  sup_cross_in2 = ((supply_flow_cfm*fric_allow)/floor_vel_fpm)*144
  sup_dia_in = 2*Math.sqrt(sup_cross_in2/Math::PI)
  duct_cost_search = {
      mat: 'Ductwork-S',
      unit: 'L.F.',
      size: sup_dia_in,
      mult: floor_trunk_dist
  }
  duct_cost, comp_info = get_duct_cost(cost_info: duct_cost_search)
  floor_trunk_info[:SupplyDuctSize_in] = sup_dia_in.round(2)
  floor_trunk_info[:SupplyDuctLength_m] = floor_trunk_dist_m.round(1)
  floor_trunk_cost += duct_cost
  sup_area_sqrft = (comp_info['Size'].to_f/12)*Math::PI*floor_trunk_dist
  duct_comp_search << {
      mat: 'Ductinsulation',
      unit: 'ft2',
      size: 1.5,
      mult: sup_area_sqrft
  }
  if hvac_floor[:return_air_m3ps] == hvac_floor[:supply_air_m3ps]
    floor_trunk_cost += duct_cost
    duct_comp_search[0][:mult] = sup_area_sqrft*2
    floor_trunk_info[:ReturnDuctSize_in] = floor_trunk_info[:SupplyDuctSize_in]
    floor_trunk_info[:ReturnDuctLength_m] = floor_trunk_info[:SupplyDuctLength_m]
  elsif hvac_floor[:return_air_m3ps].to_f > 0
    return_flow_cfm = (OpenStudio.convert(hvac_floor[:return_air_m3ps], 'm^3/s', 'cfm').get)
    ret_cross_in2 = ((return_flow_cfm*fric_allow)/floor_vel_fpm)*144
    ret_dia_in = 2*Math.sqrt(ret_cross_in2/Math::PI)
    duct_cost_search = {
        mat: 'Ductwork-S',
        unit: 'L.F.',
        size: ret_dia_in,
        mult: floor_trunk_dist
    }
    duct_cost, comp_info = get_duct_cost(cost_info: duct_cost_search)
    floor_trunk_cost += duct_cost
    ret_area_sqrft = (comp_info['Size'].to_f/12)*Math::PI*floor_trunk_dist
    duct_comp_search << {
        mat: 'Ductinsulation',
        unit: 'ft2',
        size: 1.5,
        mult: ret_area_sqrft
    }
    floor_trunk_info[:ReturnDuctSize_in] = ret_dia_in.round(2)
    floor_trunk_info[:ReturnDuctLength_m] = floor_trunk_dist_m.round(1)
  end
  floor_trunk_cost += get_comp_cost(cost_info: duct_comp_search)
  floor_trunk_info[:TotalDuctCost] = floor_trunk_cost.round(2)
  return floor_trunk_cost, floor_trunk_info
end

#get_hrv_floor_trunk_cost(mech_table:, air_system:, floor_trunk_dist_m:) ⇒ Object



2318
2319
2320
2321
2322
2323
2324
2325
2326
2327
2328
2329
2330
2331
2332
2333
2334
2335
2336
2337
2338
2339
2340
2341
2342
2343
2344
2345
2346
2347
2348
# File 'lib/openstudio-standards/btap/costing/ventilation_costing.rb', line 2318

def get_hrv_floor_trunk_cost(mech_table:, air_system:, floor_trunk_dist_m:)
  return 0 if air_system[:sys_hrv_flow_m3ps].round(2) == 0.0
  hrv_trunk_cost = 0
  duct_comp_search = []
  floor_trunk_dist = (OpenStudio.convert(floor_trunk_dist_m, 'm', 'ft').get)
  trunk_duct_sz = mech_table.select {|sz_range|
    air_system[:sys_hrv_flow_m3ps] > sz_range['max_flow_range_m3pers'][0] && air_system[:sys_hrv_flow_m3ps] <= sz_range['max_flow_range_m3pers'][1]
  }
  trunk_duct_sz << mech_table[mech_table.size-1] if trunk_duct_sz.empty?
  trunk_dia_in = (trunk_duct_sz[0]['duct_dia_inch'])
  duct_comp_search << {
      mat: 'Ductwork-S',
      unit: 'L.F.',
      size: trunk_dia_in,
      mult: floor_trunk_dist
  }
  trunk_area_sqrft = (trunk_dia_in.to_f/12)*Math::PI*floor_trunk_dist
  duct_comp_search << {
      mat: 'Ductinsulation',
      unit: 'ft2',
      size: 1.5,
      mult: trunk_area_sqrft
  }
  hrv_trunk_cost += get_comp_cost(cost_info: duct_comp_search)
  hrv_trunk_cost_rep = {
      duct_length_m: floor_trunk_dist_m.round(1),
      dia_in: trunk_dia_in.round(2),
      cost: hrv_trunk_cost.round(2)
  }
  return hrv_trunk_cost, hrv_trunk_cost_rep
end

#get_hrv_info(airloop:, model:) ⇒ Object



2071
2072
2073
2074
2075
2076
2077
2078
2079
2080
2081
2082
2083
2084
2085
2086
2087
2088
2089
2090
2091
2092
2093
2094
2095
2096
2097
2098
2099
2100
2101
2102
# File 'lib/openstudio-standards/btap/costing/ventilation_costing.rb', line 2071

def get_hrv_info(airloop:, model:)
  hrv_present = false
  hrv_data = nil
  hrv_design_flow_m3ps = 0
  airloop.oaComponents.each do |oaComp|
    if oaComp.iddObjectType.valueName.to_s == 'OS_HeatExchanger_AirToAir_SensibleAndLatent'
      hrv_present = true
      hrv_data = oaComp.to_HeatExchangerAirToAirSensibleAndLatent.get
      if hrv_data.isNominalSupplyAirFlowRateAutosized
        hrv_design_flow_m3ps = hrv_data.autosizedNominalSupplyAirFlowRate.to_f
      else
        hrv_design_flow_m3ps = hrv_data.nominalSupplyAirFlowRate.to_f
      end
    end
  end
  return {
    hrv_present: hrv_present,
    hrv_data: hrv_data,
    hrv_size_m3ps: hrv_design_flow_m3ps,
    supply_cap_m3ps: 0,
    return_cap_m3ps: 0
  } unless hrv_present
  airloop.supplyFan.is_initialized ? supply_fan_cap = get_fan_cap(fan: airloop.supplyFan.get, model: model) : supply_fan_cap = 0
  airloop.returnFan.is_initialized ? return_fan_cap = get_fan_cap(fan: airloop.returnFan.get, model: model) : return_fan_cap = 0
  return {
    hrv_present: hrv_present,
    hrv_data: hrv_data,
    hrv_size_m3ps: hrv_design_flow_m3ps,
    supply_cap_m3ps: supply_fan_cap,
    return_cap_m3ps: return_fan_cap
  }
end

#get_HVAC_multiplier(materialLookup, materialSize) ⇒ Object

This method determines how many pieces of equipment are required to satisfy the required size if 1 piece is not enough. It takes in: materialLookup(String): The name to search for in the ‘Material’ column of the materials_hvac sheet of the costing

spreadsheet.

materialSize(Float): The size to search for in the ‘Size’ column of teh materials_hvac sheet of the costing

spreadsheet.

It returns: multiplier(Float): Number of materialLookup with the largest size required to meet the materialSize.



1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
# File 'lib/openstudio-standards/btap/costing/heating_cooling_costing.rb', line 1004

def get_HVAC_multiplier(materialLookup, materialSize)
  multiplier = 1.0
  materials_hvac = @costing_database['raw']['materials_hvac'].select {|data|
    data['Material'].to_s.upcase == materialLookup.to_s.upcase
  }
  if materials_hvac.nil?
    puts("Error: no hvac information available for equipment #{materialLookup}!")
    raise
  elsif materials_hvac.empty?
    puts("Error: no hvac information available for equipment #{materialLookup}!")
    raise
  end
  materials_hvac.length == 1 ? max_size = materials_hvac[0] : max_size = materials_hvac.max_by {|d| d['Size'].to_f}
  if max_size['Size'].to_f <= 0
    puts("Error: #{materialLookup} has a size of 0 or less.  Please check that the correct costing_database.json file is being used or check the costing spreadsheet!")
    raise
  end
  mult = materialSize.to_f / (max_size['Size'].to_f)
  multiplier = (mult.to_i).to_f.round(0) + 1  # Use next largest integer for multiplier
  return multiplier.to_f
end

#get_line_eq(a:, b:, tol: 8) ⇒ Object



1515
1516
1517
1518
1519
1520
1521
1522
1523
1524
1525
1526
1527
1528
1529
1530
1531
1532
1533
1534
1535
1536
1537
# File 'lib/openstudio-standards/btap/costing/ventilation_costing.rb', line 1515

def get_line_eq(a:, b:, tol: 8)
  if a[0].round(tol) == b[0].round(tol) and a[1].round(tol) == b[1].round(tol)
    return {
        slope: 0,
        int: 0,
        inf: true
    }
  elsif a[0].round(tol) == b[0].round(tol)
    return {
        slope: a[0].round(tol),
        int: 1,
        inf: true
    }
  else
    slope = (b[1].round(tol) - a[1].round(tol))/(b[0].round(tol) - a[0].round(tol))
    int = a[1].round(tol) - (slope*a[0].round(tol))
  end
  return {
      slope: slope,
      int: int,
      inf: false
  }
end

#get_lowest_space(spaces:) ⇒ Object



1666
1667
1668
1669
1670
1671
1672
1673
1674
1675
1676
1677
1678
1679
1680
1681
1682
# File 'lib/openstudio-standards/btap/costing/ventilation_costing.rb', line 1666

def get_lowest_space(spaces:)
  cents = []
  spaces.each do |space|
    test = space['space']
    origin = [space['space'].xOrigin.to_f, space['space'].yOrigin.to_f, space['space'].zOrigin.to_f]
    space['space'].surfaces.each do |surface|
      if surface.surfaceType.to_s.upcase == 'ROOFCEILING'
        cents <<{
            space: space['space'],
            roof_cent: [surface.centroid.x.to_f + origin[0], surface.centroid.y.to_f + origin[1], surface.centroid.z.to_f + origin[2]]
        }
      end
    end
  end
  min_space = cents.min_by{|cent| cent[:roof_cent][2]}
  return min_space
end

#get_mech_costing(mech_name:, size:, terminal:, use_mult: true, vent_tags: [], report_mult: 1.0) ⇒ Object

This method gets the cost of a piece of equipment. I takes the following in: mech_name: The category or type of equipment that is being searched for in the ‘Material’ column of the ‘materials_hvac’ sheet of the costing spreadsheet. size: The size of the piece of equipment being searched for. terminal: The openstudio object being costed (used to let the user know if there is an issue finding costing info). mult: A switch which is used to determine if you want to cost multiple pieces of equipment. If it is set to true (the default) then if a piece of equipment is too large to be costed, then multiple smaller pieces of equipment will be costed. If it is set to false, then only 1 of the largest piece of equipment will be costed.



1180
1181
1182
1183
1184
1185
1186
1187
1188
1189
1190
1191
1192
1193
1194
# File 'lib/openstudio-standards/btap/costing/ventilation_costing.rb', line 1180

def get_mech_costing(mech_name:, size:, terminal:, use_mult: true, vent_tags: [], report_mult: 1.0)
  mech_cost_tags = vent_tags.clone
  # Turn the input into something that the get_vent_cost_data method can use.
  mech_info = {
    cat_search: mech_name,
    mech_capacity_kw: size,
    supply_component: terminal
  }
  # Get the costing information and multiplier (if the piece of equipment is too large) for the equipment.
  mech_mult, cost_info = get_vent_cost_data(equipment_info: mech_info)
  # Use only one piece of equipment if use_mult is set to false
  mech_mult = 1.0 unless use_mult
  # Return the total cost for the piece of equipment.
  return get_vent_mat_cost(mat_cost_info: cost_info, vent_tags: mech_cost_tags, report_mult: (mech_mult*report_mult))*mech_mult
end

#get_mech_table(mech_size_info:, table_name:) ⇒ Object



1314
1315
1316
1317
1318
1319
# File 'lib/openstudio-standards/btap/costing/ventilation_costing.rb', line 1314

def get_mech_table(mech_size_info:, table_name:)
  table = mech_size_info.select {|hash|
    hash['component'].to_s.upcase == table_name.to_s.upcase
  }.first
  return table['table']
end

#get_orient(p:, q:, r:, tol: 8) ⇒ Object



1651
1652
1653
1654
1655
# File 'lib/openstudio-standards/btap/costing/ventilation_costing.rb', line 1651

def get_orient(p:, q:, r:, tol: 8)
  orient = (q[1].round(tol) - p[1].round(tol))*(r[0].round(tol) - q[0].round(tol)) - (q[0].round(tol) - p[0].round(tol))*(r[1].round(tol) - q[1].round(tol))
  return 0 if orient == 0
  orient > 0 ? (return 1) : (return 2)
end

#get_predominant_floor_space_type_area(hvac_floor:, prototype_creator:) ⇒ Object



1929
1930
1931
1932
1933
1934
1935
1936
1937
1938
1939
1940
1941
1942
1943
1944
1945
1946
1947
1948
1949
1950
1951
1952
1953
1954
1955
1956
1957
1958
1959
1960
1961
# File 'lib/openstudio-standards/btap/costing/ventilation_costing.rb', line 1929

def get_predominant_floor_space_type_area(hvac_floor:, prototype_creator:)
  spaces = hvac_floor[:story].spaces
  space_list = []
  space_mod = OpenstudioStandards::Space
  spaces.sort.each do |space|
    if (space_mod.space_cooled?(space) || space_mod.space_heated?(space)) && !space_mod.space_plenum?(space)
      space_type = space.spaceType.get.nameString[15..-1]
      if space_list.empty?
        space_list << {
            space_type: space_type,
            floor_area: space.floorArea
        }
      else
        new_space = nil
        space_list.each do |spc_lst|
          if space_type.upcase == spc_lst[:space_type]
            spc_lst[:floor_area] += space.floorArea
          else
            new_space = {
                space_type: space_type,
                floor_area: space.floorArea
            }
          end
        end
        unless new_space.nil?
          space_list << new_space
        end
      end
    end
  end
  max_space_type = space_list.max_by {|spc_lst| spc_lst[:floor_area]}
  return max_space_type
end

#get_regional_cost_factors(provinceState, city, material) ⇒ Object



167
168
169
170
171
172
173
174
175
176
177
178
179
180
# File 'lib/openstudio-standards/btap/costing/btap_costing.rb', line 167

def get_regional_cost_factors(provinceState, city, material)
  @costing_database['localization_factors'].select { |code|
    code['province_state'] == provinceState && code['city'] == city }.each do |code|
    prefix_id = material['id'][0..1]
    prefix_stored = code['code_prefix']
    if prefix_id == prefix_stored
      return code['material'], code['installation'], code['total']
    end

  end
  error = [material, "Could not find regional adjustment factor for material used in #{city}, #{provinceState}."]
  @costing_database['db_errors'] << error unless @costing_database['db_errors'].include?(error)
  return 100.0, 100.0, 100.0
end

#get_shw_dist_cost(space:, roof_cent:) ⇒ Object



485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
# File 'lib/openstudio-standards/btap/costing/shw_costing.rb', line 485

def get_shw_dist_cost(space:, roof_cent:)
  shw_dist_search = []
  space_cent = get_space_floor_centroid(space: space)
  dist_m = (roof_cent[:roof_centroid][0] - space_cent[:centroid][0]).abs + (roof_cent[:roof_centroid][1] - space_cent[:centroid][1]).abs
  dist_ft = OpenStudio.convert(dist_m, 'm', 'ft').get
  shw_dist_search << {
      mat: 'CopperPipe',
      unit: 'L.F.',
      size: 0.75,
      mult: dist_ft
  }
  total_comp_cost = get_comp_cost(cost_info: shw_dist_search)
  return {
      length_m: dist_m,
      cost: total_comp_cost
  }
end

#get_SHW_vol_multiplier(materialLookup:, materialSize:, materialVol:) ⇒ Object

This method is a copy of get_HVAC_multiplier but searches for volume in the ‘Fuel’ column of the materials_hvac sheet. The ‘Fuel’ column is where tank volume information is kept for electric and oil SHW tanks.



655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
# File 'lib/openstudio-standards/btap/costing/shw_costing.rb', line 655

def get_SHW_vol_multiplier(materialLookup:, materialSize:, materialVol:)
  multiplier = 1.0
  materials_hvac = @costing_database['raw']['materials_hvac'].select {|data|
    data['Material'].to_s.upcase == materialLookup.to_s.upcase && data['Size'].to_f >= materialSize
  }
  if materials_hvac.nil? || materials_hvac.empty?
    puts("Error: no hvac information available for equipment #{materialLookup}!")
    raise
  end
  materials_hvac.length == 1 ? max_size = materials_hvac[0] : max_size = materials_hvac.max_by {|d| d['Fuel'].to_f}
  if max_size['Fuel'].to_f <= 0
    puts("Error: #{materialLookup} has a volume of 0 or less.  Please check that the correct costing_database.json file is being used or check the costing spreadsheet!")
    raise
  end
  mult = materialVol.to_f / (max_size['Fuel'].to_f)

  multiplier = (mult.to_i).to_f + 1.0  # Use next largest integer for multiplier
  return multiplier.to_f, max_size['Fuel'].to_f
end

#get_space_floor_centroid(space:) ⇒ Object



455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
# File 'lib/openstudio-standards/btap/costing/shw_costing.rb', line 455

def get_space_floor_centroid(space:)
  # Determine the bottom surface of the space and calculate it's centroid.
  # Get the coordinates of the origin for the space (the coordinates of points in the space are relative to this).
  xOrigin = space.xOrigin
  yOrigin = space.yOrigin
  zOrigin = space.zOrigin
  # Get the surfaces for the space.
  space_surfaces = space.surfaces
  # Find the floor (aka the surface with the lowest centroid).
  min_surf = space_surfaces.min_by{|sp_surface| (sp_surface.centroid.z.to_f)}
  # The following is added to determine the overall floor centroid because some spaces have floors composed of more than one surface.
  floor_centroid = [0, 0, 0]
  space_surfaces.each do |sp_surface|
    if min_surf.centroid.z.to_f.round(8) == sp_surface.centroid.z.to_f.round(8)
      floor_centroid[0] = floor_centroid[0] + sp_surface.centroid.x.to_f*sp_surface.grossArea.to_f
      floor_centroid[1] = floor_centroid[1] + sp_surface.centroid.y.to_f*sp_surface.grossArea.to_f
      floor_centroid[2] = floor_centroid[2] + sp_surface.grossArea
    end
  end

  # Determine the floor centroid
  floor_centroid[0] = floor_centroid[0]/floor_centroid[2]
  floor_centroid[1] = floor_centroid[1]/floor_centroid[2]

  return {
      centroid: [floor_centroid[0] + xOrigin, floor_centroid[1] + yOrigin, min_surf.centroid.z.to_f + zOrigin],
      floor_area_m2: floor_centroid[2]
  }
end

#get_story_cent_to_edge(building_story:, prototype_creator:, target_cent:, tol: 8, full_length: false) ⇒ Object

This method finds the centroid of the ceiling line on a given story furthest from the specified point. It only takes into account ceilings that above conditioned spaces that are not plenums. A line can be defined between the supplied point (we’ll call it point O) and the ceiling line centroid furthest from that point(we’ll call it point A). We will call this line AO. If the full_length input argument is set to true the method will also return the point where line AO intercepts the ceiling line on the other side of the building. Note that the method only looks at x and y coordinates and ignores the z coordinate of the point you pass it. The method assumes that the ceilings of all of the spaces on the floor you pass it are flat so generally ignores their z components as well. This was done to avoid further complicating things with 3D geometry. If the ceilings of all of the spaces in the building story you pass the method are not flat it will still work but pretend as though the ceilings are flat by ignoring the z coordinate.

The method works by going through each space in the supplied building story and finding the ones which are conditioned (either heated or cooled) and which are not considered plenums. It then goes through the surfaces of the conditioned spaces and finds the ones which have an OpenStudio SurfaceType of ‘RoofCeiling’. It then goes through each point on that surface and makes lines going from the current point (CP) to the previous point (PP). It calculates the centroid (LC) of the line formed between PP and CP by averaging each coordinate of PP and CP. It then determines which LC is furthest from the supplied point (point O) and this becomes point A. Note that point A is not necessarily on the outside of a building since no checks are made on where line P lies in the building (only that it is on a RoofCeiling above a conditioned space that is not a plenum). For example in the LargeOffice building archetype point P generally lies on one of the short edges of the trapezoids forming the perimeter spaces. This is if this reference point (O) is the center of the building.

The inputs arguments are are: building_story: OpenStudio BuildingStory object. A building story defined in OpenStudio. prototype_creator: The Openstudio-standards object, containing all of the methods etc. in the nrcan branch of

Openstudio-standards.

target_cent: Array. The point you supply from which you want to find the furthest ceiling line centroid (point O

in the description above).  This point should be a one dimensional array containing at least two
elements target_cent[0] = x, target_cent[1] = y.  The array can have more points but they will be
ignored.  This point should be inside the building.

tol: Float. The tolerence used by the method when rounding geometry (default is 8 digits after decimal). full_length: Boolean true/false

The switch which tells the method whether or not it should find, and supply, the point where line AO (
as defined above) intercepts the other side of the building.  It is defaulted to false, meaning it
will only return points A and O.  If it set to 'true' it will return the point where line AO
intercepts the other side of the building.  It does this by going through all of the ceiling lines
in the specified building story and determining if any intercept line AO (let us call each intercepts
point C).  It then runs through each intercept (point C) and determines which C makes line AOC the
longest.

The output is the following hash.

start_point:  Hash.  A hash which defines point A and provides a bunch of other information (see below),
mid_point:  Hash.  This is a hash containing the array defining the point you passed the method in the first
            place.,
end_point:  Hash.  If full_length was set to true then this defines point C and provides a bunch of other
            information (see below).  If full_length was not set to false or undefined then this is set to nil.

The structure of the hashes start_point and end_point are identical. I will only define the hash start_point below noting differences for end_point.

start_point: {

space:  OpenStudio Space object.  The space that contains point A (or point C if in the end_point hash).,
surface:  OpenStudio Surface object.  The surface in space that contains point A (should have a RoofCeiling
          SpaceType).  In the case of the end_point hash this is the surface that contains point C.,
verts:  Two dimmensional array.  The points defining ':surface'.  These points are in the building coordinate
        system (rather than the space coordinate system).  These points are ordered clockwise when viewed with the
        surface normal pointed towards the viewer.  The array would be structured as follows:
        [1st point, 2nd point, ..., last point].  Each point is an array as follows:  [x coord, y coord, z coord].
        The points are in meters.,
line:  Hash.  A hash defining the line containing point A (point C if this is in the 'end_point' hash).  See
       definition below.

‘line’ has the identical structure in the start_point and end_point hashes. I will define it once but note any differences for when it is containing in the start_point and end_point hashes.

line:

verta:  Array.  The end point of the line containing point A (when in the start_point hash) or point C (when in
        the end_point hash).  It is formed as [x, y, z].  It is in the building coordinate system, in meters.
ventb:  Array.  The start point of the line containing point A (when in the start_point hash) or point C (when in
        the end_point hash).  It is formed as [x, y, z].  It is in the building coordinate system, in meters.
int:  Array.  If this is in the start_point hash then this is the centre of the line from vertb to verta.  If this
      is in the end_point hash then this is the intercept of the line AO with the line starting with vertb and
      ending with verta.  It is formed as [x, y, z].  It is in the building coordinate system, in meters.  If in
      the start_point hash then the z coordinate is the average of the z coordinates of verta and vertb.  If in
      the end_point hash then the z coordinate is calculated by first determining of the distance of the line
      between vertb and verta when only using their x and y coordinates (we will call it the xy_dist).  Then the
      distance from just the x and y coordinates of ventb to the x and y coordinates (the only ones provided) of
      point C is determined (we will call it the c_dist).  The fraction c_dist/xy_dist is then found and added to
      the z coordinate of ventb thus providing the z coordinate of point C.
i:    Integer.  The index of verta in the verts array.
ip:   Integer.  The index of vertb in the verts array.
dist:  If in the start_point hash this is the distance between point A and point O using only the x and y
       coordinates of the respective points.  If in the end_point hash this is the distance between point A and
       point C using only the x and y coordinates of the respective points.  In meters.



1410
1411
1412
1413
1414
1415
1416
1417
1418
1419
1420
1421
1422
1423
1424
1425
1426
1427
1428
1429
1430
1431
1432
1433
1434
1435
1436
1437
1438
1439
1440
1441
1442
1443
1444
1445
1446
1447
1448
1449
1450
1451
1452
1453
1454
1455
1456
1457
1458
1459
1460
1461
1462
1463
1464
1465
1466
1467
1468
1469
1470
1471
1472
1473
1474
1475
1476
1477
1478
1479
1480
1481
1482
1483
1484
1485
1486
1487
1488
1489
1490
1491
1492
1493
1494
1495
1496
1497
1498
1499
1500
1501
1502
1503
1504
1505
1506
1507
1508
1509
1510
1511
1512
1513
# File 'lib/openstudio-standards/btap/costing/ventilation_costing.rb', line 1410

def get_story_cent_to_edge(building_story:, prototype_creator:, target_cent:, tol: 8, full_length: false)
  ceiling_start = []
  space_mod = OpenstudioStandards::Space
  building_story.spaces.sort.each do |space|
    if (space_mod.space_heated?(space) || space_mod.space_cooled?(space)) && !space_mod.space_plenum?(space)
      origin = [space.xOrigin.to_f, space.yOrigin.to_f, space.zOrigin.to_f]
      space.surfaces.each do |surface|
        if surface.surfaceType.to_s.upcase == 'ROOFCEILING'
          verts = surface.vertices
          dists = []
          surf_verts = []
          for index in 1..verts.length
            index == verts.length ? i = 0 : i = index
            i == 0 ? ip = verts.length - 1 : ip = i - 1
            verta = [verts[i].x.to_f + origin[0], verts[i].y.to_f + origin[1], verts[i].z.to_f + origin[2]]
            vertb = [verts[ip].x.to_f + origin[0], verts[ip].y.to_f + origin[1], verts[ip].z.to_f + origin[2]]
            cent = [(verta[0] + vertb[0])/2.0 , (verta[1] + vertb[1])/2.0, (verta[2] + vertb[2])/2.0]
            dist = Math.sqrt((target_cent[0].to_f - cent[0])**2 + (target_cent[1].to_f - cent[1])**2)
            dists << {
                verta: verta,
                vertb: vertb,
                int: cent,
                i: i,
                ip: ip,
                dist: dist
            }
            surf_verts << vertb
          end
          max_dist = dists.max_by{|dist_el| dist_el[:dist].to_f}
          ceiling_start << {
              space: space,
              surface: surface,
              verts: surf_verts,
              line: max_dist
          }
        end
      end
    end
  end

  return nil if ceiling_start.empty?

  furthest_line = ceiling_start.max_by{|wall| wall[:line][:dist].to_f}

  return {start_point: furthest_line, mid_point: target_cent, end_point: nil} unless full_length

  x_dist_ref = (furthest_line[:line][:int][0].round(tol) - target_cent[0].round(tol))
  x_dist_ref == 1 if x_dist_ref == 0
  y_dist_ref = (furthest_line[:line][:int][1].round(tol) - target_cent[1].round(tol))
  y_dist_ref == 1 if y_dist_ref == 0
  x_side_ref = x_dist_ref/x_dist_ref.abs
  y_side_ref = y_dist_ref/y_dist_ref.abs
  linea_eq = get_line_eq(a: target_cent, b: furthest_line[:line][:int], tol: tol)
  ints = []
  ceiling_start.each do |side|
    verts = side[:verts]
    for index in 1..(verts.length)
      index == verts.length ? i = 0 : i = index
      i == 0 ? ip = verts.length-1 : ip = i - 1
      lineb = [verts[i], verts[ip]]
      int = line_int(line_seg: lineb, line: linea_eq, tol: tol)
      next if int.nil?
      x_dist = (int[0].round(tol) - target_cent[0].round(tol))
      x_dist = 1 if x_dist == 0
      y_dist = (int[1].round(tol) - target_cent[1].round(tol))
      y_dist = 1 if y_dist == 0
      x_side = x_dist/x_dist.abs
      y_side = y_dist/y_dist.abs
      next if x_side == x_side_ref && y_side == y_side_ref
      ceil_dist = Math.sqrt((furthest_line[:line][:int][0] - int[0])**2 + (furthest_line[:line][:int][1] - int[1])**2)
      int_dist = Math.sqrt((int[0] - verts[ip][0])**2 + (int[1] - verts[ip][1])**2)
      line_dist = Math.sqrt((verts[i][0] - verts[ip][0])**2 + (verts[i][1] - verts[ip][1])**2)
      z_coord = verts[ip][2] + ((verts[i][2] - verts[ip][2])*int_dist/line_dist)
      ints << {
          ceiling_info: side,
          line: lineb,
          int: [int[0], int[1], z_coord],
          i: i,
          ip: ip,
          dist: ceil_dist
      }
    end
  end

  return nil if ints.empty?
  end_wall = ints.max_by{|wall| wall[:dist].to_f}
  return {
      start_point: furthest_line,
      mid_point: target_cent,
      end_point: {
          space: end_wall[:ceiling_info][:space],
          surface: end_wall[:ceiling_info][:surface],
          verts: end_wall[:ceiling_info][:verts],
          line: {
              verta: end_wall[:line][0],
              vertb: end_wall[:line][1],
              int: end_wall[:int],
              i: end_wall[:i],
              ip: end_wall[:ip],
              dist: end_wall[:dist]
          },
      }
  }
end

#get_vent_cost_data(equipment_info:) ⇒ Object

This method collects information related a piece of equipment from the ‘materials_hvac’ sheet in the costing spreadsheet. This information is then used to determine the cost of a piece of equipment. It takes in the equipment_info hash. This hash contains the following information: equipment_info = { cat_search: This is the category or type of mechanical equipment that is being costed. It is used to match items in the ‘Material’ column of the ‘materials_hvac’ sheet. mech_capacity_kw: This is the capacity of the piece of mechanical equipment being costed. Although it has kw in the name this is not always the case. It is compared against information in the ‘Size’ column of the ‘materials_hvac’ sheet. supply_comp: This is the OpenStudio object being costed. If there is an error this is used to tell which piece of the model had the issue.

The method tries to find the smallest piece of equipment that matches the equipment type and that can satisfy the capacity requirements. If it cannot find one then it then it assumes that the largest matching piece of equipment cannot meet the required capacity and tries to determine how many would be need to meet the required capacity. It then returns the information it found in the costing spreadsheet and the number of piece of equipment would be required (if applicable)



776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
# File 'lib/openstudio-standards/btap/costing/ventilation_costing.rb', line 776

def get_vent_cost_data(equipment_info:)
  # Assume one piece of equipment is enough.
  multiplier = 1.0
  # Find the smallest piece of equipment in 'materials_hvac' sheet that matches the equipment type and meets the
  # capacity requirement.
  heat_cool_cost_data = @costing_database['raw']['materials_hvac'].select {|data|
    data['Material'].to_s.upcase == equipment_info[:cat_search].to_s.upcase and
      data['Size'].to_f.round(8) >= equipment_info[:mech_capacity_kw].to_f
  }.min_by{|heat_cool| heat_cool[:mech_capcity_kw].to_f}
  # If it cannot find any then assume the largest piece of equipment in the costing spreadsheet is too small and
  # figure out how many of a smaller piece of equipment are required and what the smaller piece of equipment would be.
  if heat_cool_cost_data.nil? || heat_cool_cost_data.empty?
    heat_cool_cost_data, multiplier = get_vent_system_mult(loop_equip: equipment_info)
  end
  # Return the number of equipment necessary and the informatino required to find the piece of equipment in the
  # costing database.
  return multiplier, heat_cool_cost_data
end

#get_vent_mat_cost(mat_cost_info:, vent_tags: [], report_mult: 1.0) ⇒ Object

This method costs a piece of mechanical equipment. The mat_cost_info is a hash that contains the information for the piece of equipment from the ‘materials_hvac’ sheet of the costing spreadsheet. It contains: material_id: An index sometimes used to refer to find a specific piece of equipment material: The type of equipment. description: A description of the piece of equipment. Size: The size of the piece of equipment (see units for the unit this is in). Fuel: Sometimes this is indicates the fuel type, sometimes it is an additional size criteria. source: The source to look for the costing information. This can be placeholder or custom. id: The unique id of the costing information associated with this piece of equipment. unit: The units of the given Size (can be one of many units). province_state: For custom costing data, this is the province or state that the costing data is given for (used to adjust the costing data so it can be used nationally). city: For custom costing data, this is the city that the costing data is given for (used to adjust the costing so it can be used nationally). year: The year the costing information is provided for (it should be the same for everything but some costs are only available in some years and not others). material_cost: The custom cost for material (e.g. the cost of a pipe). Not used for placeholder costs. labour_cost: The custom cost for labour (e.g. the labour to install the pipe). Not used for placeholder costs. equipment_cost: The custom cost of equipment required (e.g. the cost of any machinery required to install the pipe, often this is 0). This is not used for placeholder costs. material_op_factor: Ask Mike or Phylroy. Probably not for placeholder costs. labour_op_factor: Ask Mike or Phylroy. Probably not for placeholder costs. equipment_op_factor: Ask Mike or Phylroy. Probably not for placeholder costs. comments: comments. material_mult: A fixed multiplier to multiply the material cost by. labour_mult: A fixed multiplier to multiply the labour costs by. The method uses the id from ‘mat_cost_info’ to find costing information for the piece of equipment in the costing database. It then adjusts the material and equipment cost by the regional cost factor for the location the model is supposed to be in. The resulting adjusted equipment and material costs are then multiplied by any associated multipliers and the total amount is returned.



637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
# File 'lib/openstudio-standards/btap/costing/ventilation_costing.rb', line 637

def get_vent_mat_cost(mat_cost_info:, vent_tags: [], report_mult: 1.0)
  cost_tags =vent_tags.clone
  if mat_cost_info.nil?
    raise("Error: no assembly information available for material!")
  end
  # Look for the costing information for the piece of equipment in the costing database.
  costing_data = @costing_database['costs'].detect {|data| data['id'].to_s.upcase == mat_cost_info['id'].to_s.upcase}
  # If no costing information is found then return an error.
  if costing_data.nil?
    raise "Error: no costing information available for material id #{mat_cost_info['id']}!"
  elsif costing_data['baseCosts']['materialOpCost'].nil? || costing_data['baseCosts']['laborOpCost'].nil?
    #This is a stub for some work that needs to be done to account for equipment costing. For now this is zeroed out.
    # A similar test is done on reading the data from the database and collected in the error file when the
    # costing database is generated.
    puts("Error: costing information for material id #{mat_cost_info['id']} is nil.  Please check costing data.")
    return 0.0
  end
  # The costs from the costing database are US national average costs (for placeholder costs) or whatever is in the
  # 'province_state' and 'city' fieleds (for custom costs).  These costs need to be adjusted to reflect the costs
  # expected in the location of interest.  The 'get_regional_cost_factors' method finds the appropriate cost
  # adjustment factors.
  mat_mult, inst_mult = get_regional_cost_factors(@costing_report['province_state'], @costing_report['city'], mat_cost_info)
  if mat_mult.nil? || inst_mult.nil?
    raise("Error: no localization information available for material id #{id}!")
  end
  # Get any associated material or labour multiplier for the equipment present in the 'materials_hvac' sheet in the
  # costing spreadsheet.
  mat_cost_info['material_mult'].to_f == 0 ? mat_quant = 1.0 : mat_quant = mat_cost_info['material_mult'].to_f
  mat_cost_info['labour_mult'].to_f == 0 ? lab_quant = 1.0 : lab_quant = mat_cost_info['labour_mult'].to_f
  # Calculate the adjusted material and labour costs.
  mat_cost = costing_data['baseCosts']['materialOpCost']*(mat_mult/100.0)*mat_quant
  lab_cost = costing_data['baseCosts']['laborOpCost']*(inst_mult/100.0)*lab_quant
  # Add information to report output if tags provided.
  unless cost_tags.empty?
    cost_tags << mat_cost_info['Material'].to_s
    cost_tags << mat_cost_info['description'].to_s
    # Add support for equipment_multiplier (if used in the future).
    mat_cost_info['equipment_mult'].nil? || mat_cost_info['equipment_mult'].to_f == 0 ? equip_quant = 1.0 : equip_quant = mat_cost_info['equipment_mult'].to_f
    add_costed_item(material_id: mat_cost_info['id'], quantity: report_mult.to_f, material_mult: mat_quant, labour_mult: lab_quant, equip_mult: equip_quant, tags: cost_tags)
  end
  # Return the total.
  return (mat_cost+lab_cost)
end

#get_vent_system_mult(loop_equip:, mult_floor: nil) ⇒ Object

This method finds how many pieces of costed equipment are required to meet a given load if no one piece of costed equipment can do it. It takes in two hashes: mult_floor: This should probably be mult_ceiling. It is the maximum size of mechanical equipment that should be selected. This is used if you really want to make sure that a given piece of mechanical equipment does not exceed this size. It is normally not used. loop_equip: This hash must have the following information in it: cat_search: The category or type of the mechanical equipment that is being searched for in the ‘Material’ column of the ‘materials_hvac’ sheet in the costing spreadsheet. supply_comp: This is the oir loop supply component from the OpenStudio model. It is really just used to give a name in any error messages. mech_capacity: This is the capacity of the piece of the supply component being costed.

The method first looks for all of the items in the ‘materials_hvac’ sheet whose ‘Material’ match the ‘cat_search’ criteria. If none are found then something has gone wrong so an error is generated telling the user what happened. Assuming it found some items it then finds the largest one (or the largest one that does not exceed the mult_floor category). It then divides the mech_capacity by the size of the costed equipment it found to determine the minimum number of pieces of costed equipment meets the model equipment capacity (the multiplier). With this information it then rounds the multiplier to the next largest whole number and divides the modeled equipment capacity by this size to determine the revised size of equipment (this may be smaller than the largest piece of equipment). It then looks for the smallest piece of costed equipment that meets this requirement and returns the result.



506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
# File 'lib/openstudio-standards/btap/costing/ventilation_costing.rb', line 506

def get_vent_system_mult(loop_equip:, mult_floor: nil)
  # Look for all of the equipment in the materials_hvac sheet that has a 'Material' that matches the cat_search
  # criteria.
  heat_cool_cost = @costing_database['raw']['materials_hvac'].select {|data|
    data['Material'].to_s.upcase == loop_equip[:cat_search].to_s.upcase
  }

  # In some cases loop_equip[:supply_comp] may not be an object but a string.  If this is the case then the string
  # should be given rather than a message that nameString does not exist.
  equip_name = loop_equip[:supply_comp].nameString rescue equip_name = loop_equip[:supply_comp].to_s

  # If it cannot find any then return an error telling the user what happened.  This is likely the result of a
  # spelling mistake somewhere but it is something the user will have to deal with.
  if heat_cool_cost.nil? || heat_cool_cost.empty?
    raise "Error: no equipment could be found whose type matches the name #{loop_equip[:cat_search]} for the #{equip_name} air loop supply component!"
  end
  # Set the maximum size to be a really large number if it is not defined.
  mult_floor.nil? ? max_eq_size = 99999999999999999999.0 : max_eq_size = mult_floor.to_f
  # Find the largest piece of equipment that is smaller than the size ceiling.
  max_size = heat_cool_cost.select {|element| element['Size'].to_f <= max_eq_size}.max_by{|data| data['Size'].to_f}
  # If you cannot find any then the size ceiling is too small.  Raise an error telling the user
  if max_size.nil? || max_size.empty?
    raise "Error no equipment of the type #{loop_equip[:cat_search]} could be found with a size less than #{max_eq_size} for the #{equip_name} air loop supply component!"
  end
  # Make sure the piece of equipment has a capacity larger than 0.
  if max_size['Size'].to_f <= 0
    raise "Error: #{loop_equip[:cat_search]} has a size of 0 or less.  Please check that the correct costing_database.json file is being used or check the costing spreadsheet!"
  end
  # Find the revised number of pieces of equipment and round to the next largest whole number.
  mult = (loop_equip[:mech_capacity_kw].to_f) / (max_size['Size'].to_f)
  # This is to handle the small possibility that the revised capacity is a whole number.
  mult > (mult.to_i).to_f.round(0) ? multiplier = (mult.to_i).to_f.round(0) + 1 : multiplier = mult.round(0)
  # Find the new capacity of the pieces of equipment
  new_cap = loop_equip[:mech_capacity_kw].to_f/multiplier.to_f
  # Find the smallest piece of costed equimpent that meets the new size requirement.
  return_equip = heat_cool_cost.select{|data| data['Size'].to_f >= new_cap}.min_by{|element| element['Size'.to_f]}
  # If no costed equipment can be found that matches this new size then something is wrong and use the largest piece
  # you found before.
  return_equip = max_size if (return_equip.nil? || return_equip.empty?)
  return return_equip, multiplier.to_f
end

#getCost(materialType, materialHash, multiplier) ⇒ Object



1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
# File 'lib/openstudio-standards/btap/costing/heating_cooling_costing.rb', line 1123

def getCost(materialType, materialHash, multiplier)
  material_cost = 0.0 ; labour_cost = 0.0
  costing_data = @costing_database['costs'].detect do |data|
    data['id'].to_s.upcase == materialHash['id'].to_s.upcase
  end
  if costing_data.nil?
    puts "HVAC #{materialType} with id #{materialHash['id']} not found in the costing database. Skipping."
    raise
  else
    # Get cost information from lookup.
    # Adjust for material and labour multiplier in costing spreadsheet 'materials_hvac' sheet 'material_mult' and
    # 'labour_mult' columns.
    (materialHash['material_mult'].nil?) || (materialHash['material_mult'].empty?) ? mat_mult = 1.0 : mat_mult = materialHash['material_mult'].to_f
    (materialHash['labour_mult'].nil?) || (materialHash['labour_mult'].empty?) ? lab_mult = 1.0 : lab_mult = materialHash['labour_mult'].to_f
    material_cost = costing_data['baseCosts']['materialOpCost'].to_f * multiplier * mat_mult
    labour_cost = costing_data['baseCosts']['laborOpCost'].to_f * multiplier * lab_mult
  end
  return material_cost, labour_cost
end

#getGeometryData(model, prototype_creator) ⇒ Object



1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
1165
1166
1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178
1179
1180
1181
1182
1183
1184
1185
1186
1187
1188
1189
1190
1191
1192
1193
1194
1195
1196
1197
1198
1199
1200
1201
1202
1203
1204
1205
1206
1207
1208
1209
1210
# File 'lib/openstudio-standards/btap/costing/heating_cooling_costing.rb', line 1143

def getGeometryData(model, prototype_creator)
  num_of_above_ground_stories = model.getBuilding.standardsNumberOfAboveGroundStories.to_i
  space_mod = OpenstudioStandards::Space
  if model.building.get.nominalFloortoFloorHeight().empty?
    volume = model.building.get.airVolume()
    flrArea = 0.0
    if model.building.get.conditionedFloorArea.empty?
      model.getThermalZones.sort.each do |tz|
        tz.spaces.sort.each do |tz_space|
          flrArea += tz_space.floorArea.to_f if ( (space_mod.space_cooled?(tz_space)) || (space_mod.space_heated?(tz_space)) )
        end
        flrArea += tz.floorArea
      end
    else
      flrArea = model.building.get.conditionedFloorArea().get
    end
    nominal_flr2flr_height = 0.0
    nominal_flr2flr_height = volume / flrArea unless flrArea <= 0.01
  else
    nominal_flr2flr_height = model.building.get.nominalFloortoFloorHeight.get
  end

  # Location of mechanical room and utility distances for use below (space_centroid is an array
  # in mech_room hash containing the x,y and z coordinates of space centroid). Utility distance
  # uses the distance from the mech room centroid to the perimeter of the building.
  mech_room, cond_spaces = prototype_creator.find_mech_room(model)
  mech_room_story = nil
  target_cent = [mech_room['space_centroid'][0], mech_room['space_centroid'][1]]
  found = false
  model.getBuildingStorys.sort.each do |story|
    story.spaces.sort.each do |space|
      if space.nameString == mech_room['space_name']
        mech_room_story = story
        found = true
        break
      end
    end
    break if found
  end
  distance_info_hash = get_story_cent_to_edge( building_story: mech_room_story, prototype_creator: prototype_creator,
                                               target_cent: target_cent, full_length: false )
  horizontal_dist = distance_info_hash[:start_point][:line][:dist]  # in metres

  ht_roof = 0.0
  util_dist = 0.0
  mechRmInBsmt = false
  if mech_room['space_centroid'][2] < 0
    # Mechanical room is in the basement (z dimension is negative).
    mechRmInBsmt = true
    ht_roof = (num_of_above_ground_stories + 1) * nominal_flr2flr_height
    util_dist = nominal_flr2flr_height + horizontal_dist
  elsif mech_room['space_centroid'][2] == 0
    # Mech room on ground floor
    ht_roof = num_of_above_ground_stories * nominal_flr2flr_height
    util_dist = horizontal_dist
  else
    # Mech room on some other floor
    ht_roof = (num_of_above_ground_stories - (mech_room['space_centroid'][2]/nominal_flr2flr_height).round(0)) * nominal_flr2flr_height
    util_dist = ht_roof + horizontal_dist
  end

  util_dist = OpenStudio.convert(util_dist,"m","ft").get
  nominal_flr2flr_height = OpenStudio.convert(nominal_flr2flr_height,"m","ft").get
  ht_roof = OpenStudio.convert(ht_roof,"m","ft").get
  horizontal_dist = OpenStudio.convert(horizontal_dist,"m","ft").get

  return util_dist, ht_roof, nominal_flr2flr_height, horizontal_dist, num_of_above_ground_stories, mechRmInBsmt
end

#getHeaderPipingDistributionCost(numAGFlrs, mechRmInBsmt, regional_material, regional_installation, reg_elec_mat, reg_elec_inst, pumpFlow, horz_dist, nom_flr_hght) ⇒ Object



1713
1714
1715
1716
1717
1718
1719
1720
1721
1722
1723
1724
1725
1726
1727
1728
1729
1730
1731
1732
1733
1734
1735
1736
1737
1738
1739
1740
1741
1742
1743
1744
1745
1746
1747
1748
1749
1750
1751
1752
1753
1754
1755
1756
1757
1758
1759
1760
1761
1762
1763
1764
1765
1766
1767
1768
1769
1770
1771
1772
1773
1774
1775
1776
1777
1778
1779
1780
1781
1782
1783
1784
1785
1786
1787
1788
1789
1790
1791
1792
1793
1794
1795
1796
1797
1798
1799
1800
1801
1802
1803
1804
1805
1806
1807
# File 'lib/openstudio-standards/btap/costing/heating_cooling_costing.rb', line 1713

def getHeaderPipingDistributionCost(numAGFlrs, mechRmInBsmt, regional_material, regional_installation, reg_elec_mat, reg_elec_inst, pumpFlow, horz_dist, nom_flr_hght)
  # Hot water central header piping distribution costs. Note that the piping distribution cost
  # of zone piping is done in the zonalsys_costing function

  # Central header piping Cost
  supHdrCost = 0; retHdrCost = 0
  mechRmInBsmt ? numFlrs = numAGFlrs + 1 : numFlrs = numAGFlrs
  if numFlrs < 3
    # Header pipe is same diameter as distribution pipes to zone floors
    supHdrLen = numFlrs * nom_flr_hght

    # 1.25 inch Steel pipe
    matCost, labCost = getHVACCost('Header 1.25 inch steel pipe', 'SteelPipe', 1.25)
    supHdrpipingCost = supHdrLen * (matCost * regional_material / 100.0 + labCost * regional_installation / 100.0)

    # 1.25 inch Steel pipe insulation
    matCost, labCost = getHVACCost('Header 1.25 inch pipe insulation', 'PipeInsulation', 1.25)
    supHdrInsulCost = supHdrLen * (matCost * regional_material / 100.0 + labCost * regional_installation / 100.0)

    # 1.25 inch gate valves
    matCost, labCost = getHVACCost('Header 1.25 inch gate valves', 'ValvesGate', 1.25)
    supHdrValvesCost = (matCost * regional_material / 100.0 + labCost * regional_installation / 100.0)

    # 1.25 inch tee
    matCost, labCost = getHVACCost('Header 1.25 inch steel tee', 'SteelPipeTee', 1.25)
    supHdrTeeCost = (matCost * regional_material / 100.0 + labCost * regional_installation / 100.0)

    supHdrCost = supHdrpipingCost + supHdrInsulCost + supHdrValvesCost + supHdrTeeCost
    retHdrCost = supHdrCost
  else  # Greater than 3 floors (including basement)
    # Use pumpFlow to determine pipe size
    if pumpFlow <= 0.0001262
      hdrPipeSize = 0.5
    elsif pumpFlow > 0.0001262 && pumpFlow <= 0.0002524
      hdrPipeSize = 0.75
    elsif pumpFlow > 0.0002524 && pumpFlow <= 0.0005047
      hdrPipeSize = 1.0
    elsif pumpFlow > 0.0005047 && pumpFlow <= 0.0010090
      hdrPipeSize = 1.25
    elsif pumpFlow > 0.0010090 && pumpFlow <= 0.0015773
      hdrPipeSize = 1.5
    elsif pumpFlow > 0.0015773 && pumpFlow <= 0.0031545
      hdrPipeSize = 2.0
    elsif pumpFlow > 0.0031545
      hdrPipeSize = 2.5
    end

    hdrPipeLen = horz_dist + nom_flr_hght * numFlrs

    # Steel pipe
    matCost, labCost = getHVACCost("Header Steel Pipe - #{hdrPipeSize} inch", 'SteelPipe', hdrPipeSize)
    supHdrpipingCost = hdrPipeLen * (matCost * regional_material / 100.0 + labCost * regional_installation / 100.0)

    # Steel pipe insulation
    matCost, labCost = getHVACCost("Header Pipe Insulation - #{hdrPipeSize} inch", 'PipeInsulation', hdrPipeSize)
    supHdrInsulCost = hdrPipeLen * (matCost * regional_material / 100.0 + labCost * regional_installation / 100.0)

    # Gate valves
    matCost, labCost = getHVACCost("Header Gate Valves - #{hdrPipeSize} inch", 'ValvesGate', hdrPipeSize)
    supHdrValvesCost = (matCost * regional_material / 100.0 + labCost * regional_installation / 100.0)

    # Tee
    matCost, labCost = getHVACCost("Header Steel Tee - #{hdrPipeSize} inch", 'SteelPipeTee', hdrPipeSize)
    supHdrTeeCost = (matCost * regional_material / 100.0 + labCost * regional_installation / 100.0)

    supHdrCost = supHdrpipingCost + supHdrInsulCost + supHdrValvesCost + supHdrTeeCost
    retHdrCost = supHdrCost
  end

  hdrPipeCost = supHdrCost + retHdrCost

  # Electrical header costs. Central electric header cost for zonal heatingunits
  hdrLen = numFlrs * nom_flr_hght

  # Conduit - only one spreadsheet entry
  matCost, labCost = getHVACCost('Header Metal conduit', 'Conduit', '')
  hdrConduitCost = hdrLen * (matCost * reg_elec_mat / 100.0 + labCost * reg_elec_inst / 100.0)

  # Wiring - size 10
  matCost, labCost = getHVACCost('Header No 10 Wiring', 'Wiring', 10)
  hdrWireCost = hdrLen / 100 * (matCost * reg_elec_mat / 100.0 + labCost * reg_elec_inst / 100.0)

  # Box - size 4
  matCost, labCost = getHVACCost('Header 4 inch deep Box', 'Box', 4)
  hdrBoxCost = numFlrs * (matCost * reg_elec_mat / 100.0 + labCost * reg_elec_inst / 100.0)

  elecHdrCost = hdrConduitCost + hdrWireCost + hdrBoxCost

  # Central gas header cost will be determined in zonalsys_costing function since
  # this cost depends on existence of at least one gas-fired unit heater in building.

  hdrDistributionCost = hdrPipeCost + elecHdrCost

  return hdrDistributionCost
end

#getHVACCost(name, materialLookup, materialSize, exactMatch = true) ⇒ Object

This method provides the material and labour cost for a required piece of equimpment. It takes in: name(String): The name of a piece of equipment. Is only used for error reporting and is not linked to anything

else.

materialLookup(String): The material type used to search hte ‘Material’ column of the materials_hvac sheet of the

costing spreadsheet.

materialSize(float): The size of the equipment in whichever units are required when searching the ‘Size’ column of

the costing spreadsheet.

exactMatch(true/false): A flag to indicate if the hvac equipment must match the size provided exactly or if the

size is a minimum equipment size.

It returns the material cost ond labor cost for the equipment including any multipliers.



1036
1037
1038
1039
# File 'lib/openstudio-standards/btap/costing/heating_cooling_costing.rb', line 1036

def getHVACCost(name, materialLookup, materialSize, exactMatch=true)
  eqCostInfo = getHVACDBInfo(name: name, materialLookup: materialLookup, materialSize: materialSize, exactMatch: exactMatch)
  return getCost(eqCostInfo[:name], eqCostInfo[:hvac_material], eqCostInfo[:multiplier])
end

#getHVACDBInfo(name:, materialLookup:, materialSize:, exactMatch: true) ⇒ Object

This method was originally part of getHVACCOST but was split out because in some cases the information from the materials_hvac sheet of the costing spreadsheet was required but not tho cost. The method takes in: name(String): The name of a piece of equipment. Is only used for error reporting and is not linked to anything

else.

materialLookup(String): The material type used to search hte ‘Material’ column of the materials_hvac sheet of the

costing spreadsheet.

materialSize(float): The size of the equipment in whichever units are required when searching the ‘Size’ column of

the costing spreadsheet.

exactMatch(true/false): A flag to indicate if the hvac equipment must match the size provided exactly or if the

size is a minimum equipment size.

The method returns a hash with the following composition: { name(string): Same as above. hvac_material(hash): The costing spreadsheet information for the hvac equipment being searched for. multiplier(float): Default is 1. Will be higher if exactMatch is false, and no materialLookup could be found with

a large enough materialSize in the costing spreadsheet. In this case, it is assumed that several
pieced of equipment defined by hvact_material are used to satisfy the required materialSize.
The multiplier defines the number of hvac_material required to meet the materialSize

}



1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
# File 'lib/openstudio-standards/btap/costing/heating_cooling_costing.rb', line 1062

def getHVACDBInfo(name:, materialLookup:, materialSize:, exactMatch: true)
  multiplier = 1.0
  materials_hvac = @costing_database["raw"]["materials_hvac"]
  if materialSize == 'nil' || materialSize == '' || materialSize == nil
    # When materialSize is blank because there is only one row in the data sheet, the value is nil
    hvac_material = materials_hvac.select { |data| data['Material'].to_s.upcase == materialLookup.to_s.upcase }.first
  else
    if exactMatch
      hvac_material = materials_hvac.select {|data|
        data['Material'].to_s.upcase == materialLookup.to_s.upcase && data['Size'].to_f == materialSize
      }.first
    else
      hvac_material_info = materials_hvac.select {|data|
        data['Material'].to_s.upcase == materialLookup.to_s.upcase && data['Size'].to_f >= materialSize
      }
      if hvac_material_info.empty?
        hvac_material = nil
      elsif hvac_material_info.size == 1
        hvac_material = hvac_material_info[0]
      else
        hvac_material = hvac_material_info.min_by{|data| data['Size'].to_f}
      end
    end
  end
  if hvac_material.nil?
    if exactMatch
      puts "HVAC material error! Could not find #{name} in materials_hvac!"
      raise
    else
      # There is no exact match in the costing spreadsheet so redo search for next largest size
      hvac_material = materials_hvac.select {|data|
        data['Material'].to_s.upcase == materialLookup.to_s.upcase && data['Size'].to_f >= materialSize.to_f
      }.min_by{|mat_info| mat_info['Size'].to_f}
      if hvac_material.nil?
        # The nominal capacity is greater than the maximum value in the API data for this boiler!
        # Lookup cost for a capacity divided by the multiple of req'd size/max size.
        multiplier = get_HVAC_multiplier( materialLookup, materialSize )
        hvac_materials = materials_hvac.select {|data|
          data['Material'].to_s.upcase == materialLookup.to_s.upcase && data['Size'].to_f >= materialSize.to_f / multiplier.to_f
        }
        if hvac_materials.size == 0
          puts "HVAC material error! Could not find next largest size for #{name} in #{materials_hvac}"
          raise
        elsif hvac_materials.size == 1
          hvac_material = hvac_materials[0]
        else
          hvac_material = hvac_materials.min_by{|data| data['Size'].to_f}
        end
      end
    end
  end
  # Create the return hash.
  costDBInfo = {
    name: name,
    hvac_material: hvac_material,
    multiplier: multiplier
  }
  return costDBInfo
end

#getHVACMultiSizeDBInfo(name:, materialLookup:, materialCap:, materialCon:) ⇒ Object

This method was originally part of getHVACCOST but was split out because in some cases the information from the materials_hvac sheet of the costing spreadsheet was required but not tho cost. The method takes in: name(String): The name of a piece of equipment. Is only used for error reporting and is not linked to anything

else.

materialLookup(String): The material type used to search hte ‘Material’ column of the materials_hvac sheet of the

costing spreadsheet.

materialSize(float): The size of the equipment in whichever units are required when searching the ‘Size’ column of

the costing spreadsheet.

exactMatch(true/false): A flag to indicate if the hvac equipment must match the size provided exactly or if the

size is a minimum equipment size.

The method returns a hash with the following composition: { name(string): Same as above. hvac_material(hash): The costing spreadsheet information for the hvac equipment being searched for. multiplier(float): Default is 1. Will be higher if exactMatch is false, and no materialLookup could be found with

a large enough materialSize in the costing spreadsheet. In this case, it is assumed that several
pieced of equipment defined by hvact_material are used to satisfy the required materialSize.
The multiplier defines the number of hvac_material required to meet the materialSize

}



2426
2427
2428
2429
2430
2431
2432
2433
2434
2435
2436
2437
2438
2439
2440
2441
2442
2443
2444
2445
2446
2447
2448
2449
2450
2451
2452
2453
2454
2455
2456
2457
2458
2459
2460
2461
2462
2463
2464
2465
2466
2467
2468
2469
2470
2471
2472
2473
2474
2475
2476
2477
2478
2479
2480
2481
2482
2483
2484
2485
2486
2487
2488
2489
2490
2491
2492
2493
2494
2495
2496
2497
2498
2499
2500
2501
2502
2503
2504
2505
2506
2507
2508
2509
2510
2511
2512
2513
2514
2515
2516
2517
2518
2519
2520
2521
2522
2523
2524
2525
2526
2527
2528
2529
2530
2531
2532
2533
2534
2535
2536
2537
2538
2539
2540
2541
2542
2543
2544
2545
2546
2547
2548
2549
# File 'lib/openstudio-standards/btap/costing/heating_cooling_costing.rb', line 2426

def getHVACMultiSizeDBInfo(name:, materialLookup:, materialCap:, materialCon:)
  multiplier = 1.0
  numConLoops = 1.0
  # Get the materials_hvac sheet info from the costing spreadsheet
  materials_hvac = @costing_database["raw"]["materials_hvac"]
  # Find material that meet the materialCon requirement
  hvac_material_con_info = materials_hvac.select {|data|
    data['Material'].to_s.upcase == materialLookup.to_s.upcase && data['Fuel'].to_f >= materialCon
  }
  hvac_material = []
  unless hvac_material_con_info.empty?
    # If the materialCon criteria check if any of the selected material meet the mateterialCap criteria
    hvac_material_cap_info = hvac_material_con_info.select {|data| data['Size'].to_f >= materialCap}
    if hvac_material_cap_info.nil? || hvac_material_cap_info.empty?
      # If none do then select the material with the largest capacity that met the materialCon criteria
      hvac_material << hvac_material_con_info.max_by {|data| data['Size'].to_f}
      con_per_loop = hvac_material[0]['Fuel'].to_f
    else
      # If something met both the materialCon and materialCap then return a hash containing the material and other
      # information and we are done.
      hvac_material = hvac_material_cap_info.min_by {|data| data['Size'].to_f}
      ret_hash = {
        name: name,
        hvac_material: hvac_material,
        multiplier: 1
      }
      return ret_hash
    end
  end
  if hvac_material.empty?
    # If no equipment met the materialCon criteria then find all with the material type we want
    hvac_material_info = materials_hvac.select {|data| data['Material'].to_s.upcase == materialLookup.to_s.upcase}
    # If you cannot find even the material type then something has gone very wrong.  Stop everything and tell the user.
    raise "HVAC material error! Could not find next largest size for #{name} in the materials_hvac sheet of the costing spreadsheet of #{materialLookup} type." if hvac_material_info.empty?
    # Find the equipment with the largest 'Fuel' (this is what defines the materialCon options for this material type)
    hvac_material = hvac_material_info.max_by{|data| data['Fuel'].to_f}
    # Find the number of pieces of equipment will be needed to meet the materialCon requirement
    (((materialCon.to_f) % (hvac_material['Fuel'].to_f)).round(3) > 0.0) ? numConLoops = ((materialCon.to_f/(hvac_material['Fuel'].to_f)).to_i + 1).to_f.round(0) : numConLoops = (materialCon.to_f/(hvac_material['Fuel'].to_f)).round(0)
    # Revise the materialCon requirement now that several pieces of equipment are being used
    con_per_loop = materialCon / numConLoops
    # Find all the appropriate equipment in the costing spreadsheet that meet the revised materialCon requirement
    hvac_material = hvac_material_info.select{|data| data['Fuel'].to_f >= con_per_loop}
  end

  # Now that we have some equipment that meet the required materialCon requirement revise the materialCap requirement
  # in case multiple pieces of equipment were to meet the materialCon requirement.
  reqMatSize = materialCap/numConLoops
  # Of the equipment that met the (modified or original) materialCan requirement select the equipment the meets the
  # (modified or original) capacity requirement.
  material_cap = hvac_material.select{|data| data['Size'].to_f >= reqMatSize}
  if material_cap.empty?
    # If none of the selected materials meet the materialCap requirement find the one with the largest capacity
    largestMat = hvac_material.max_by{|data| data['Size'].to_f}
    maxAvailCap = largestMat['Size'].to_f
    # Find out how many are required to meet the materialCap requirement
    (reqMatSize%maxAvailCap).to_f.round(3) > 0 ? numCapLoops = (((reqMatSize/maxAvailCap).to_i) + 1).to_f.round(0) : numCapLoops = (reqMatSize/maxAvailCap).to_f.round(0)
    # Calculate how many pieces of equipment are now required to meet both the materialCon and materialCap
    # requirements
    totLoops = numConLoops*numCapLoops
    # Revise the materialCap and materialCon requirements to reflect that even more pieces of equipment will be used
    # to meet both requirements
    modMatCap = (materialCap / totLoops).to_f
    modMatCon = (materialCon / totLoops).to_f
    # Search for equipment that meet both the modMatCap and modMatCon criteria
    hvac_material_info = materials_hvac.select {|data| data['Material'].to_s.upcase == materialLookup.to_s.upcase}
    material_cap = hvac_material_info.select{|data| (data['Size'].to_f >= modMatCap) && (data['Fuel'].to_f >= modMatCon)}
    if material_cap.empty?
      # It should have gotten something.  If it didn't then select the one with largest capacity and use that.
      ret_mat = largestMat
    else
      # Find the equipment with the smallest capacity that meets the requirement
      ret_mat = material_cap.min_by{|data| data['Size'].to_f}
    end
  else
    # If something now meets the materialCon and materialCap requirement select the one with the smallest capacity.
    totLoops = numConLoops
    ret_mat = material_cap.min_by{|data| data['Size'].to_f}
  end
  # If multiple branch distributors are required check if the last one can be smaller than the others and return that
  # in addition to the other branch distributors.
  if totLoops.round(0) > 1.0
    # Check check the remaining size requirements for the last branch distributor
    (materialCon - (totLoops - 1.0)*(ret_mat['Fuel'].to_f)) > 0 ? redCon = (materialCon - (totLoops - 1.0)*(ret_mat['Fuel'].to_f)) : redCon = 0.0
    (materialCap - (totLoops - 1.0)*(ret_mat['Size'].to_f)) > 0 ? redCap = (materialCap - (totLoops - 1.0)*(ret_mat['Size'].to_f)) : redCap = 0.0
    # If either are greater than zero (as should be the case) then look for equipment that can meet the remaining
    # connection or capacity requipments.
    if (redCon > 0) || (redCap > 0)
      # Find material that meet the remaining connection and capacity requirements.
      hvac_material_red = materials_hvac.select {|data|
        data['Material'].to_s.upcase == materialLookup.to_s.upcase && data['Fuel'].to_f >= redCon && data['Size'].to_f >= redCap
      }
      if hvac_material_red.size == 0
        # If no equipment could be found which meet the remaining connection and capacity requirements then return
        # the the number and type of equipment without adjust for a smaller final piece of equipment.
        red_ret_hash = nil
      else
        # If equipment could be found then select the one with the minimum number of connections.
        red_ret = hvac_material_red.min_by{|data| data['Fuel'].to_f}
        min_hvac_sel = hvac_material_red.select{|data| data['Fuel'].to_f == red_ret['Fuel'].to_f}
        # If more than one piece of equipment can meet the minimum connection requirement then select the one with the
        # minimum capacity requirement.
        if min_hvac_sel.size > 1
          red_ret = min_hvac_sel.min_by{|data| data['Size'].to_f}
        end
        red_ret_hash = {
          red_ret_hash: red_ret,
          numCon: redCon,
          numCap: redCap
        }
      end
    end
  end
  # If multiple pieces of equipment are required to meet the connection and capacity requirements check if a smaller
  # piece of equipment was found to get the final remaining requirements.  If one is found then adjust the multiplier
  # for the main equipment to be reduced by one and return the smaller remaining piece of equipment.
  red_ret_hash.nil? ? retLoops = totLoops : retLoops = totLoops - 1.0
  # Create a hash with the results and return it.
  ret_hash = {
    name: name,
    hvac_material: ret_mat,
    multiplier: retLoops
  }
  return ret_hash, red_ret_hash
end

#getPerimDistPipingCost(zone, nom_flr_hght, regional_material, regional_installation) ⇒ Object



1809
1810
1811
1812
1813
1814
1815
1816
1817
1818
1819
1820
1821
1822
1823
1824
1825
1826
1827
1828
1829
1830
# File 'lib/openstudio-standards/btap/costing/heating_cooling_costing.rb', line 1809

def getPerimDistPipingCost(zone, nom_flr_hght, regional_material, regional_installation)
  # Get perimeter distribution piping cost
  extWallArea = 0.0
  perimPipingCost = 0.0
  zone.spaces.sort.each do |space|
    if space.spaceType.empty? or space.spaceType.get.standardsSpaceType.empty? or space.spaceType.get.standardsBuildingType.empty?
      raise ("standards Space type and building type is not defined for space:#{space.name.get}. Skipping this space for costing.")
    end
    extWallArea += OpenStudio.convert(space.exteriorWallArea.to_f,"m^2","ft^2").get  # sq.ft.
  end
  perimTotal = ( extWallArea / nom_flr_hght ) * zone.multiplier

  # 1.25 inch Steel pipe
  matCost, labCost = getHVACCost('Perimeter Distribution - 1.25 inch steel pipe', 'SteelPipe', 1.25)
  perimPipingCost = perimTotal * (matCost * regional_material / 100.0 + labCost * regional_installation / 100.0)

  # 1.25 inch Steel pipe insulation
  matCost, labCost = getHVACCost('Perimeter Distribution - 1.25 inch pipe insulation', 'PipeInsulation', 1.25)
  perimPipingCost += perimTotal * (matCost * regional_material / 100.0 + labCost * regional_installation / 100.0)

  return perimPipingCost
end

#getPerimDistWiringCost(zone, nom_flr_hght, regional_material, regional_installation) ⇒ Object



1832
1833
1834
1835
1836
1837
1838
1839
1840
1841
1842
1843
1844
1845
1846
1847
1848
1849
1850
1851
1852
1853
# File 'lib/openstudio-standards/btap/costing/heating_cooling_costing.rb', line 1832

def getPerimDistWiringCost(zone, nom_flr_hght, regional_material, regional_installation)
  # Get perimeter distribution wiring cost
  extWallArea = 0.0
  perimWiringCost = 0.0
  zone.spaces.sort.each do |space|
    if space.spaceType.empty? or space.spaceType.get.standardsSpaceType.empty? or space.spaceType.get.standardsBuildingType.empty?
      raise ("standards Space type and building type is not defined for space:#{space.name.get}. Skipping this space for costing.")
    end
    extWallArea += OpenStudio.convert(space.exteriorWallArea.to_f,"m^2","ft^2").get  # sq.ft.
  end
  perimTotal = ( extWallArea / nom_flr_hght ) * zone.multiplier

  # Conduit - only one spreadsheet entry
  matCost, labCost = getHVACCost('Perimeter Distribution - Metal conduit', 'Conduit', '')
  perimWiringCost = perimTotal * (matCost * regional_material / 100.0 + labCost * regional_installation / 100.0)

  # Wiring - size 10
  matCost, labCost = getHVACCost('Perimeter Distribution - No 10 Wiring', 'Wiring', 10)
  perimWiringCost += perimTotal / 100 * (matCost * regional_material / 100.0 + labCost * regional_installation / 100.0)

  return perimWiringCost
end

#getSHWTankCost(name:, materialLookup:, materialSize:, tankVol:) ⇒ Object

Getting cost for SHW Tanks. This method is different from the getHVACCost method used everywhere else in that it accepts a tank volume argument in addition to the tank capacity (materialSize in this case). This additional argument means that the method must search for a SHW tank heated with the right fuel that has a large enough capacity and volume.

IMPORTANT: This method assumes that when SHW tanks are retrieved from the model their capacity is checked against the capacities of costed tanks. If the modeled capacity is too large then it is costed as though multiple smaller tanks are present. Thus, this method assumes that anything passed to it will be small enough to be costed. This is another difference from the getHVACCost method which includes a call to get_HVAC_multiplier that checks if a costed item is too big and should be replaced by several smaller items.

Note that the multiplier is always set to 1.0 when claculating the cost. That is because the multiplier is applied to the cost in the main shw_costing method.



536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
# File 'lib/openstudio-standards/btap/costing/shw_costing.rb', line 536

def getSHWTankCost(name:, materialLookup:, materialSize:, tankVol:)
  multiplier = 1.0
  materials_hvac = @costing_database['raw']['materials_hvac']
  # Get costing spreadsheet data for gas and oil fired mixed shw tanks
  if tankVol.nil?
    # If no tank volume is provided then only look at capacity.
    # Get all capacities hor that type of tank.
    hvac_materials = materials_hvac.select {|data|
      data['Material'].to_s == materialLookup.to_s && data['Size'].to_f >= materialSize.to_f
    }
    if hvac_materials.empty?
      # If no tanks have a big enough capacity then something is amiss and return an error (this should never happen
      # because tanks capacity should be checked before this method is called).
      puts "HVAC material error! Could not find next largest size for #{name} in #{materials_hvac}"
      raise
    elsif hvac_materials.size == 1
      # Only one tank has an appropriate capacity find it's cost and return it.
      matCost, labCost = getCost(name, hvac_materials[0], 1.0)
      ret_hash = {
          matCost: matCost,
          labCost: labCost,
          multiplier: multiplier,
          Vol_USGal: tankVol,
          Cap_kW: hvac_materials[0]['Size'].to_f

      }
      return ret_hash
    else
      # More than one tank has a big enough capacity.  Find the cost of the one with teh smallest capacity and return
      # it.
      hvac_material = hvac_materials.min_by {|data| data['Size'].to_f}
      matCost, labCost = getCost(name, hvac_material, 1.0)
      ret_hash = {
          matCost: matCost,
          labCost: labCost,
          multiplier: multiplier,
          Vol_USGal: tankVol,
          Cap_kW: hvac_material['Size'].to_f
      }
      return ret_hash
    end
  else
    # We need to find a tank with a big enough capacity and volume.
    # First see if a unique tank with a large enough capacity and volume exists
    hvac_materials = materials_hvac.select {|data|
      data['Material'].to_s == materialLookup.to_s && data['Size'].to_f >= materialSize.to_f && data['Fuel'].to_f >= tankVol
    }
    if hvac_materials.empty?
      # If none exists see if the tank volume is big enough.  Note that tank capacity was checked earlier so capacity
      # should not be an issue.  However, volume was not checked.  It is possible that tanks with a big enough
      # capacity are in the costing database but not a big enough volume.
      #
      # Find the largest volume tank with a big enough capacity.  Find out how many of those tanks are needed to
      # satisfy the volume requirement.
      multiplier, revVol = get_SHW_vol_multiplier(materialLookup: materialLookup, materialSize: materialSize, materialVol: tankVol)
      materialSize /= multiplier
      tankVol = revVol
      # Try again to get tanks with a large enough size and capacity
      hvac_materials = materials_hvac.select {|data|
        data['Material'].to_s == materialLookup.to_s && data['Size'].to_f >= materialSize.to_f && data['Fuel'].to_f >= tankVol
      }
      # You may notice that there is no handling for cases where there is more than one tank with a large enough
      # capacity and volume.  There actually is, it is just a little further below.
      if hvac_materials.empty?
        puts "HVAC material error! Could not find a #{name} tank with a capacity >= #{materialSize} kW and a volume >= #{tankVol} US Gal in #{materials_hvac}"
      elsif hvac_materials.size == 1
        matCost, labCost = getCost(name, hvac_materials[0], 1.0)
        ret_hash = {
            matCost: matCost,
            labCost: labCost,
            multiplier: multiplier,
            Vol_USGal: hvac_materials[0]['Fuel'].to_f,
            Cap_kW: hvac_materials[0]['Size'].to_f
        }
        return ret_hash
      end
    elsif hvac_materials.size == 1
      matCost, labCost = getCost(name, hvac_materials[0], 1.0)
      ret_hash = {
          matCost: matCost,
          labCost: labCost,
          multiplier: multiplier,
          Vol_USGal: hvac_materials[0]['Fuel'].to_f,
          Cap_kW: hvac_materials[0]['Size'].to_f
      }
      return ret_hash
    end
    # If mare than one tank has a lorge enough capacity and volume then find the one with the smallest volume.
    hvac_materials_min_vol = hvac_materials.min_by {|data| data['Fuel'].to_f}
    if hvac_materials_min_vol.nil?
      # Well, something went horribly wrong.  You should have gotten this far only if there were several tanks that
      # had a large enough capacity and volume.  Now we can't find the smallest one.  Not sure what happened but
      # whatever it was it is not good.
      puts "HVAC material error! Could not find a #{name} tank with a capacity >= #{materialSize} kW and a volume >= #{materialVol} US Gal in costing database."
      raise
    else
      # Find how many tanks have the lowest volume.
      hvac_materials_vol = hvac_materials.select {|data| data['Fuel'].to_f == hvac_materials_min_vol['Fuel'].to_f}
      if hvac_materials_vol.size == 1
        hvac_material = hvac_materials_vol[0]
      else
        # If more than one tank as a small enough volume choose the one with the smallest capacity.
        hvac_material = hvac_materials_vol.min_by {|data| data['Size'].to_f}
      end
      matCost, labCost = getCost(name, hvac_material, 1.0)
      ret_hash = {
          matCost: matCost,
          labCost: labCost,
          multiplier: multiplier,
          Vol_USGal: hvac_material['Fuel'].to_f,
          Cap_kW: hvac_material['Size'].to_f
      }
      return ret_hash
    end
  end
end

#getWallWithLargestArea(currFloor:) ⇒ Object

This method finds and returns the outside wall with the largest area on a given building story. It takes in:

currFloor = {
  storyName(string):  Name of the current floor (story).
  buildStoryObj(Obj):  The OpenStudio object associated with the current floor (story).
  floorAream2:  Total floor area (m2) of thermal zones on the current floor served by VRF systems.  Does not
                include multipliers.
  floorCeillingAream2:  The Total ceiling area (m2) of thermal zones on the current floor served by VRF
                        systems.  Does not include multipliers.
  floorTZs(array):  An array containing each tzFloor hash described above for the current floor.

}

It returns a hash containing the following: wallRetHash =

largestOutsideWallObj(OS Object):  OpenStudio surface object with the largest gross area that has a 'wall'
                                   surface type and an 'Outdoors' outside boundary condition.
wallCentObj(array of floats):  OpenStudio point3d object containing the x, y, z coordinates of the wall above
                         wall's centroid.  In local coordinate system.
wallCentOrigin(array of OS Objects):  Coordinates of the local wall origin in the absolute building
                                      coordinate system.
wallCent(array of floats):  Coordinates of the wall's centroid in floats referenced to the building
                            coordinate system.



2252
2253
2254
2255
2256
2257
2258
2259
2260
2261
2262
2263
2264
2265
2266
2267
2268
2269
2270
2271
2272
2273
2274
2275
2276
2277
2278
2279
2280
2281
2282
2283
2284
# File 'lib/openstudio-standards/btap/costing/heating_cooling_costing.rb', line 2252

def getWallWithLargestArea(currFloor:)
  outsideWalls = []
  # Get all the spaces associated with the building story
  floorSpaces = currFloor[:buildStoryObj].spaces
  # Cycle through each space associated with the building story.
  floorSpaces.each do |floorSpace|
    # Get the surfaces in the spcae which have a 'Wall' Surface Type and an 'Outdoors' outside boundary condition.
    spaceOutWalls = floorSpace.surfaces.select{|surf| surf.surfaceType.to_s.upcase == 'WALL' && ((surf.outsideBoundaryCondition.to_s.upcase == 'OUTDOORS') || (surf.outsideBoundaryCondition.to_s.upcase == 'GROUND') || (surf.outsideBoundaryCondition.to_s.upcase == 'FOUNDATION'))}
    # Add these surfaces to the array containing outdoor walls.
    spaceOutWalls.each{|outWall| outsideWalls << outWall}
  end
  # Find and return the outside wall object with the largest gross area.
  largestWall = outsideWalls.sort.max_by{|outWall| outWall.grossArea.to_f}
  largestWallSpace = largestWall.space.get
  largestWallSpaceOrigin = [
    largestWallSpace.xOrigin,
    largestWallSpace.yOrigin,
    largestWallSpace.zOrigin
  ]
  wallCentObj = largestWall.centroid
  wallCent = [
    wallCentObj.x.to_f + largestWallSpaceOrigin[0].to_f,
    wallCentObj.y.to_f + largestWallSpaceOrigin[1].to_f,
    wallCentObj.z.to_f + largestWallSpaceOrigin[2].to_f
  ]
  wallRetHash = {
    largestOutsideWallObj: largestWall,
    wallCentObj: wallCentObj,
    wallCentOrigin: largestWallSpaceOrigin,
    wallCent: wallCent
  }
  return wallRetHash
end

#getZonalVRFCosting(vrfSystemFloors:, model:, prototype_creator:, regMat:, regLab:, cumulCost:) ⇒ Object

Cost zonal VRF systems including zone equipment (ceiling units and associated tubing and wiring), floor equipment ( branch distributor on each floor), VRF system condenser (assumed one on rooftop and another every 50m), and piping and wiring linking the branch distributors to one another and to the condensers.

Takes in: vrfSystemFloors = {

  maxCeil(float):  The height of the thermal zone served by a VRF system with the highest ceiling in the
                   building.  This is referenced to the global origin for the building.  The units are m.
  lowCeil(float):  The height of the thermal zone served by a VRF system with the lowest ceiling in the
                   building.  This is referenced to the global origin for the building.  The units are m.
  vrfFloors(array):  [
    storyName(string):  Name of the current floor (story).
    buildStoryObj(Obj):  The OpenStudio object associated with the current floor (story).
    floorAream2:  Total floor area (m2) of thermal zones on the current floor served by VRF systems.  Does not
                  include multipliers.
    floorCeillingAream2:  The Total ceiling area (m2) of thermal zones on the current floor served by VRF
                          systems.  Does not include multipliers.
    floorTZs(array):  An array containing each tzFloor hash described above for the current floor.

  ]
}

model(hash): OpenStudio building model. prototype_creator(object): OpenStudio-Standards object for whichever version of NECB was used to create the model. regMat(float): HVAC regional cost factor for materiel. regLab(float): HVAC regional cost factor for labour. cumulCost(float): Cumulative zonal system cost.

Output(float): Total cost for VRF system. Also adds information to @costing_report for condenser(s), VRF zonal

systems, and branch distributors.


1965
1966
1967
1968
1969
1970
1971
1972
1973
1974
1975
1976
1977
1978
1979
1980
1981
1982
1983
1984
1985
1986
1987
1988
1989
1990
1991
1992
1993
1994
1995
1996
1997
1998
1999
2000
2001
2002
2003
2004
2005
2006
2007
2008
2009
2010
2011
2012
2013
2014
2015
2016
2017
2018
2019
2020
2021
2022
2023
2024
2025
2026
2027
2028
2029
2030
2031
2032
2033
2034
2035
2036
2037
2038
2039
2040
2041
2042
2043
2044
2045
2046
2047
2048
2049
2050
2051
2052
2053
2054
2055
2056
2057
2058
2059
2060
2061
2062
2063
2064
2065
2066
2067
2068
2069
2070
2071
2072
2073
2074
2075
2076
2077
2078
2079
2080
2081
2082
2083
2084
2085
2086
2087
2088
2089
2090
2091
2092
2093
2094
2095
2096
2097
2098
2099
2100
2101
2102
2103
2104
2105
2106
2107
2108
2109
2110
2111
2112
2113
2114
2115
2116
2117
2118
2119
2120
2121
2122
2123
2124
2125
2126
2127
2128
2129
2130
2131
2132
# File 'lib/openstudio-standards/btap/costing/heating_cooling_costing.rb', line 1965

def getZonalVRFCosting(vrfSystemFloors:, model:, prototype_creator:, regMat:, regLab:, cumulCost:)
  # Include empty array in costing report for branch distributor costs on each floor
  @costing_report['heating_and_cooling']['floor_systems'] = []
  total_cost = 0
  vrfWireInfo = getHVACDBInfo(name: "VRF Wiring", materialLookup: "wiring", materialSize: 10, exactMatch: true)
  regMatElec, regLabElec = get_regional_cost_factors(@costing_report['province_state'], @costing_report['city'], vrfWireInfo[:hvac_material])

  # Find the center of the highest roof
  roof_cent_info = prototype_creator.find_highest_roof_centre(model)
  roof_cent = roof_cent_info[:roof_centroid]
  # Find the distance between the highest roof and the ceiling of the lowest space served by a VRF system
  maxHeightDiff = (roof_cent[2] - vrfSystemFloors[:lowCeil]).to_f.round(8)
  # Find the roof height for the condenensate line cost.  If the maxHeightDiff includes basement spaces use it,
  # otherwise use the height of the roof.
  maxHeightDiff > roof_cent[2].to_f.round(8) ? roofHeight = maxHeightDiff : roofHeight = roof_cent[2].to_f.round(8)
  # Get the condenser cost
  vrfCondenserCost = costVRFCondenser(model: model, maxHeightDiff: maxHeightDiff, regMat: regMat, regLab: regLab, regMatElec: regMatElec, regLabElec:regLabElec, roofHeight: roofHeight)
  total_cost += vrfCondenserCost
  vrfSystemFloors[:vrfFloors].each do |currFloor|
    vrfDistWallInfo = getWallWithLargestArea(currFloor: currFloor)
    vrfDistWallCent = vrfDistWallInfo[:wallCent]
    totalFloorCapkW = 0
    totalFloorCeilUnits = 0
    floorMults = []
    currFloor[:floorTZs].each do |floorTZ|
      zoneWallLengthm = (vrfDistWallCent[0] - floorTZ[:tzCentroid][0]).abs + (vrfDistWallCent[1] - floorTZ[:tzCentroid][1]).abs
      zoneWallLengthft = (OpenStudio.convert(zoneWallLengthm, 'm', 'ft').get)
      elecLength = zoneWallLengthft + 10.0 * (floorTZ[:vrfCeilMountInfo][:multiplier] - 1)

      # Get the zone refrigerant tubing cost (tubing running from the ceiling units to the brancd distributers).  A
      # Size of 50 is used for interior refrigerant tubing while 10 is used for the exterior tubing used between the
      # branch distributors and the condensers.
      zoneRefrigTubingMat, zoneRefrigTubingLab = getHVACCost("VRF Zone Refrigerant Tubing", 'Refrig-tubing', 50, true)
      # Refrigerant tubing comes in 50 ft rolls
      zoneRefrigTubingCost = ((zoneRefrigTubingMat * regMat + zoneRefrigTubingLab * regLab) / 100) * elecLength / 50.0

      # Include condensate line tubing cost
      zoneCondTubingMat, zoneCondTubingLab = getHVACCost('VRF Zone Condensate Line Tubing', 'PEX_tubing', 0.5, true)
      zoneCondTubingCost = ((zoneCondTubingMat * regMat + zoneCondTubingLab * regLab) / 100) * elecLength

      # Include coupler cost for condensate line
      zoneCondCouplingMat, zoneCondCouplingLab = getHVACCost('VRF Zone Condensate Line Couplers', 'PVC_coupling', 0.5, true)
      zoneCondCouplingCost = ((zoneCondCouplingMat * regMat + zoneCondCouplingLab * regLab) / 100) * floorTZ[:vrfCeilMountInfo][:multiplier]

      # Include tee cost for condensate line
      zoneCondTeeMat, zoneCondTeeLab = getHVACCost('VRF Zone Condensate Line Tees', 'PVC_tee', 0.5, true)
      zoneCondTeeCost = ((zoneCondTeeMat * regMat + zoneCondTeeLab * regLab) / 100) * floorTZ[:vrfCeilMountInfo][:multiplier]

      # Total condensate line cost
      zoneCondLineCost = zoneCondTubingCost + zoneCondCouplingCost + zoneCondTeeCost

      # Get the wiring cost
      zoneWiringMat, zoneWiringLab = getCost(vrfWireInfo[:name], vrfWireInfo[:hvac_material], vrfWireInfo[:multiplier])
      zoneWiringCost = ((zoneWiringMat*regMatElec + zoneWiringLab*regLabElec)/100)*elecLength/100

      # Get the conduit cost
      zoneConduitMat, zoneConduitLab = getHVACCost("VRF Zone Conduit", 'Conduit', nil, true)
      zoneConduitCost = ((zoneConduitMat*regMatElec + zoneConduitLab*regLabElec)/100)*elecLength

      # Get the total cost
      totalZoneCost = (zoneRefrigTubingCost + zoneCondLineCost + zoneWiringCost + zoneConduitCost + floorTZ[:vrfCeilMountCost] + floorTZ[:vrfSysContCost])*floorTZ[:tzMult]

      total_cost += totalZoneCost

      cumulCost += total_cost
      # Add zonal cost to report
      @costing_report['heating_and_cooling']['zonal_systems'] << {
        'systype' => 'zonalVRF',
        'zone_number' => floorTZ[:tzNum],
        'zone_name' => floorTZ[:tzName],
        'zone_multiple' => floorTZ[:tzMult],
        'heat_capacity(kW)' => floorTZ[:tzFloorCapkW].round(1),
        'cool_capacity(kW)' => floorTZ[:tzFloorCapkW].round(1),
        'heat_cost' => 0.00,
        'cool_cost' => 0.00,
        'heatcool_cost' => ((floorTZ[:vrfCeilMountCost] + floorTZ[:vrfSysContCost]) * floorTZ[:tzMult]).round(0),
        'piping_cost' => ((zoneRefrigTubingCost + zoneCondLineCost) * floorTZ[:tzMult]).round(0),
        'wiring_cost' => ((zoneWiringCost + zoneConduitCost) * floorTZ[:tzMult]).round(0),
        'num_units' => floorTZ[:vrfCeilMountInfo][:multiplier],
        'cummultive_zonal_cost' => cumulCost.round(0)
      }
      totalFloorCapkW += floorTZ[:tzFloorCapkW]
      totalFloorCeilUnits += floorTZ[:vrfCeilMountInfo][:multiplier]
      # Determine the distribution of thermal zone multipliers on the floor.  For each thermal zone on the floor get
      # the multiplier.  If the same multiplier is already in the arry then add 1 to the number of occurrences of that
      # multiplier.  If the multiplier is not in the array then add the multiplier to the array with an occurrence of
      # one.
      if floorMults.empty?
        floorMults << {
          zoneMult: floorTZ[:tzMult],
          numMults: 1
        }
      else
        numFloorMult = floorMults.select{|data| data[:zoneMult] == floorTZ[:tzMult]}
        if numFloorMult.empty?
          floorMults << {
            zoneMult: floorTZ[:tzMult],
            numMults: 1
          }
        else
          numFloorMult[0][:numMults] += 1
        end
      end
    end
    # Find the number of and type of branch distributors which meet the number of ceiling unit criteria and capacity
    # criteria.  Costing for a separate, smaller, branch distributor may be returned if multiple branch distributors
    # are required to meet the connection or load requirements.  In this case this smaller branch distributor will
    # meet any connection or capacity remaining from the main equipment*(multiplier - 1).  In some cases most of the
    # requirements may be met by the (multiplier - 1)*equipment and a much smaller piece of equipment can be used for
    # the remaining requipments.
    vrfBranchDistInfo, vrfBranchDistInfoRed= getHVACMultiSizeDBInfo(name: 'VRF Branch Distributors', materialLookup: 'VRF-Solenoid', materialCap: totalFloorCapkW, materialCon: totalFloorCeilUnits)
    # Get the branch distributor cost
    vrfBranchDistMat, vrfBranchDistLab = getCost(vrfBranchDistInfo[:name], vrfBranchDistInfo[:hvac_material], vrfBranchDistInfo[:multiplier])
    vrfBranchDistCost = (vrfBranchDistMat*regMat + vrfBranchDistLab*regLab)/100
    # Using the distribution of zone multipliers on the floor find which appears most often.
    initMaxMult = floorMults.max_by{|data| data[:numMults]}
    # If several different zone multipliers appear the same number of times then choose the largest zone multiplier
    floorMultsMatch = floorMults.select{|data| data[:numMults] == initMaxMult[:numMults]}
    if floorMultsMatch.size > 1
      maxMult = floorMultsMatch.max_by{|data| data[:zoneMult]}[:zoneMult]
    else
      maxMult = initMaxMult[:zoneMult]
    end
    # multiply the branch distributer cost by the floor multiplier
    totalVRFBranchDistCost = vrfBranchDistCost*maxMult

    # Add branch distributor cost to report
    @costing_report['heating_and_cooling']['floor_systems'] << {
      'systype' => 'VRFBranchDistributor',
      'floor_name' => currFloor[:storyName],
      'floor_multiple' => maxMult,
      'heat_capacity(kW)' => totalFloorCapkW.round(1),
      'cool_capacity(kW)' => totalFloorCapkW.round(1),
      'num_ceiling_units' => totalFloorCeilUnits,
      'heatcool_cost' => totalVRFBranchDistCost.round(0),
      'num_units' => vrfBranchDistInfo[:multiplier],
      'total_floor_cost' => totalVRFBranchDistCost.round(0)
    }

    total_cost += totalVRFBranchDistCost

    # Check if a smaller piece of equipment was found to meet the requirements remaining after removing the
    # (multiplier-1) requirements.
    unless vrfBranchDistInfoRed.nil?
      # Get the branch distributor cost
      vrfBranchDistRedMat, vrfBranchDistRedLab = getCost(vrfBranchDistInfo[:name], vrfBranchDistInfoRed[:red_ret_hash], 1.0)
      vrfBranchDistRedCost = (vrfBranchDistRedMat*regMat + vrfBranchDistRedLab*regLab)/100

      # multiply the branch distributer cost by the floor multiplier
      totalVRFBranchDistRedCost = vrfBranchDistRedCost*maxMult

      # Add branch distributor cost to report
      @costing_report['heating_and_cooling']['floor_systems'] << {
        'systype' => 'VRFBranchDistributor',
        'floor_name' => currFloor[:storyName],
        'floor_multiple' => maxMult,
        'heat_capacity(kW)' => vrfBranchDistInfoRed[:numCap].to_f.round(1),
        'cool_capacity(kW)' => vrfBranchDistInfoRed[:numCap].to_f.round(1),
        'num_ceiling_units' => vrfBranchDistInfoRed[:numCon].to_f.round(1),
        'heatcool_cost' => totalVRFBranchDistRedCost.round(0),
        'num_units' => 1.0,
        'total_floor_cost' => totalVRFBranchDistRedCost.round(0)
      }
      total_cost += totalVRFBranchDistRedCost
    end
  end
  return total_cost
end

#getZonalVRFInfo(zone:, model:, prototype_creator:, zonalSys:, vrfSystemFloors:, regMat:, regLab:, numZones:) ⇒ Object

Get information on Zonal VRF System in a Thermal Zone



1856
1857
1858
1859
1860
1861
1862
1863
1864
1865
1866
1867
1868
1869
1870
1871
1872
1873
1874
1875
1876
1877
1878
1879
1880
1881
1882
1883
1884
1885
1886
1887
1888
1889
1890
1891
1892
1893
1894
1895
1896
1897
1898
1899
1900
1901
1902
1903
1904
1905
1906
1907
1908
1909
1910
1911
1912
1913
1914
1915
1916
1917
1918
1919
1920
1921
1922
1923
1924
1925
1926
1927
1928
1929
1930
1931
1932
1933
1934
# File 'lib/openstudio-standards/btap/costing/heating_cooling_costing.rb', line 1856

def getZonalVRFInfo(zone:, model:, prototype_creator:, zonalSys:, vrfSystemFloors:, regMat:, regLab:, numZones:)
  #Get heating and cooling coil objects
  heatingCoil = zonalSys.heatingCoil.get.to_CoilHeatingDXVariableRefrigerantFlow.get
  coolingCoil = zonalSys.coolingCoil.get.to_CoilCoolingDXVariableRefrigerantFlow.get

  # Get heating capacity
  if heatingCoil.isRatedTotalHeatingCapacityAutosized.to_bool
    heatingCapkW = heatingCoil.autosizedRatedTotalHeatingCapacity.to_f/1000.0
  else
    heatingCapkW = (heatingCoil.ratedTotalHeatingCapacity).to_f/1000.0
  end

  # Get cooling capacity
  if coolingCoil.isRatedTotalCoolingCapacityAutosized.to_bool
    coolingCapkW = coolingCoil.autosizedRatedTotalCoolingCapacity.to_f/1000.0
  else
    coolingCapkW = coolingCoil.grossRatedTotalCoolingCapacityAtSelectedNominalSpeedLevel.to_f/1000.0
  end

  # Get the multiplier for the thermal zone
  zoneMult = zone.multiplier

  # Set capacity to the highest of the heating or cooling capacity and adjust for the thermal zone multiplier
  heatingCapkW >= coolingCapkW ? totalCapkW = heatingCapkW/zoneMult : totalCapkW = coolingCapkW/zoneMult

  # Get the thermal zone (TZ) and collect information on:
  # The TZ name.
  # The TZ capacity.
  # The spaces associated with the TZ.
  # The floors the spaces occupy.
  # The total ceiling area and floor area for each floor the spaces occupy.
  # The centroid of all of the spaces on a given floor (this may be outside of the spaces for some geometries)
  # The building story names and objects associated with a thermal zone.
  # the ceiling and floor area of the spaces associated with it for each floor they occupy
  # The capacity of the TZ on a given floor (calculated at total capacity * floor area of TZ spaces on floor/total TZ floor area)
  # The VRF ceiling mounts required and associated cost
  # The VRF system controllers cost
  tzFloorsInfo = prototype_creator.thermal_zone_get_centroid_per_floor(zone)
  tzFloorsInfo.each do |tzFloor|
    tzStoryFloorArea_m2 = 0
    tzSpaceMults = []
    tzFloor[:spaces].each do |tz_space|
      tzStoryFloorArea_m2 += tz_space.floorArea.to_f
      tzSpaceMults << tz_space.multiplier.to_f
    end
    tzFloorCapkW = totalCapkW * tzStoryFloorArea_m2 / zone.floorArea.to_f
    # Get the VRF Ceiling mount information and cost
    vrfCeilMountInfo = getHVACDBInfo(name: "VRF Ceiling Mount", materialLookup: "VRF-CeilingMount", materialSize: tzFloorCapkW, exactMatch: false)
    # Get VRF Ceiling mount cost.
    material, labour = getCost(vrfCeilMountInfo[:name], vrfCeilMountInfo[:hvac_material], vrfCeilMountInfo[:multiplier])
    # The multiplier (number of units required to meet the demand) is already included in the material and labour cost.
    vrfCeilMountCost = (material*regMat + labour*regLab) / 100.0
    # Get the VRF System Controller Cost (for 1 controller)
    material, labour = getHVACCost("VRF System Controller", 'VRF-Sys-Controller', nil, true)
    # Since material and labour are for 1 controller the multiplier is added here.
    vrfSysContCost = (material * regMat + labour * regLab) * vrfCeilMountInfo[:multiplier] / 100.0

    # Put the thermal zone information for the floor into a hash.
    tzFloorInfo = {
      tzName: zone.name.to_s,
      tzNum: numZones,
      tzFloorName: tzFloor[:story_name],
      tzFloorCeilingAream2: tzFloor[:ceiling_area],
      tzMult: zoneMult,
      tzSpaces: tzFloor[:spaces],
      tzSpaceMults: tzSpaceMults,
      tzFloorArea_m2: tzStoryFloorArea_m2,
      tzFloorCapkW: tzFloorCapkW,
      tzCentroid: tzFloor[:centroid],
      vrfCeilMountInfo: vrfCeilMountInfo,
      vrfCeilMountCost: vrfCeilMountCost,
      vrfSysContCost: vrfSysContCost
    }

    # Add the TZ information for the floor to a hash containing all of the thermal zones.
    vrfSystemFloors = compileZonalVRFFloors(vrfSystemFloors: vrfSystemFloors, tzFloor: tzFloorInfo)
  end
  return vrfSystemFloors
end

#hrv_cost(hrv_info:, airloop:, vent_tags: [], report_mult: 1.0) ⇒ Object

This method consumes the following: hrv_info: (hash) Information about the modeled HRV. airloop: (OpenStudio Object) The OpenStudio air loop object. vent_tags: (array of strings) Tags used to associate the costing output list with whichever component of the

building is being costed.

report_mult: (float) When recreating the cost of items from the costing output list this multiplier is used to

multiply the total of the localized material and labour costs.


2376
2377
2378
2379
2380
2381
2382
2383
2384
2385
2386
2387
2388
2389
2390
2391
2392
2393
2394
2395
2396
2397
2398
2399
2400
2401
2402
2403
2404
2405
2406
2407
2408
2409
2410
2411
2412
2413
2414
2415
2416
2417
2418
2419
2420
2421
2422
2423
2424
2425
2426
2427
2428
2429
2430
2431
2432
2433
2434
2435
2436
2437
2438
# File 'lib/openstudio-standards/btap/costing/ventilation_costing.rb', line 2376

def hrv_cost(hrv_info:, airloop:, vent_tags: [], report_mult: 1.0)
  hrv_tags = vent_tags.clone
  hrv_tags << "ERV duct cost"
  hrv_cost_tot = 0
  number_zones = 0
  duct_comp_search = []
  # Calculate the number of thermal zones served by the ERV
  airloop.thermalZones.each do |tz|
    number_zones += tz.multiplier
  end

  # Get additional ductwork costs
  duct_comp_search << {
      mat: 'Ductwork-Fitting',
      unit: 'each',
      size: 8,
      mult: number_zones
  }
  hrv_cost_tot += get_comp_cost(cost_info: duct_comp_search, vent_tags: hrv_tags, report_mult: report_mult)
  hrv_tags.pop

  # Get the return air fan cost (if applicable)
  hrv_info[:return_cap_m3ps] >= hrv_info[:hrv_size_m3ps] ? hrv_return_flow_m3ps = 0.0 : hrv_return_flow_m3ps = hrv_info[:hrv_size_m3ps] - hrv_info[:return_cap_m3ps]
  hrv_tags << "ERV return air fan"
  unless hrv_return_flow_m3ps.round(2) == 0
    hrv_return_flow_cfm = (OpenStudio.convert(hrv_return_flow_m3ps, 'm^3/s', 'cfm').get)
    if hrv_return_flow_cfm < 800
      hrv_cost_tot += get_mech_costing(mech_name: 'FansDD-LP', size: hrv_return_flow_cfm, terminal: hrv_info[:hrv_data], use_mult: true, vent_tags: hrv_tags, report_mult: report_mult)
    else
      hrv_cost_tot += get_mech_costing(mech_name: 'FansBelt', size: hrv_return_flow_cfm, terminal: hrv_info[:hrv_data], use_mult: true, vent_tags: hrv_tags, report_mult: report_mult)
    end
  end


  hrv_tags.pop
  hrv_tags << "ERV with adjustment factor"

  hrv_size_cfm = (OpenStudio.convert(hrv_info[:hrv_size_m3ps], 'm^3/s', 'cfm').get)
  # Turn the HRV information into something the 'get_vent_cost_data' method expects.
  hrv_requirements = {
    cat_search: 'ERV',
    mech_capacity_kw: hrv_size_cfm, # This key really should just be called mech_capacity since the units vary.
    supply_component: hrv_info[:hrv_data]
  }
  # Get the HRV costing information
  hrv_mult, hrv_cost_info = get_vent_cost_data(equipment_info: hrv_requirements)
  # Calculate the HRV cost adjustment factor
  hrv_cost_adj = hrv_size_cfm*hrv_mult/(hrv_cost_info['Size'].to_f)
  ind_hrv_cost = get_vent_mat_cost(mat_cost_info: hrv_cost_info, vent_tags: hrv_tags, report_mult: hrv_cost_adj)

  ind_hrv_cost_rep = hrv_cost_tot + ind_hrv_cost
  hrv_cost_tot += ind_hrv_cost*hrv_cost_adj
  hrv_rep = {
    hrv_type: (hrv_info[:hrv_data].iddObjectType.valueName.to_s)[3..-1],
    hrv_name: hrv_info[:hrv_data].nameString,
    hrv_size_m3ps: hrv_info[:hrv_size_m3ps].round(3),
    hrv_return_fan_size_m3ps: hrv_return_flow_m3ps.round(3),
    hrv_cost: ind_hrv_cost_rep.round(2),
    revised_hrv_cost: hrv_cost_tot.round(2)
  }

  return hrv_rep
end

#hrv_duct_cost(prototype_creator:, roof_cent:, mech_sizing_info:, hvac_floors:) ⇒ Object



2127
2128
2129
2130
2131
2132
2133
2134
2135
2136
2137
2138
2139
2140
2141
2142
2143
2144
2145
2146
2147
2148
2149
2150
2151
2152
2153
2154
2155
2156
2157
2158
2159
2160
2161
2162
2163
2164
2165
2166
2167
2168
2169
2170
2171
2172
2173
2174
2175
2176
2177
2178
2179
2180
2181
2182
2183
2184
2185
2186
2187
2188
2189
2190
2191
2192
2193
2194
2195
2196
2197
2198
2199
2200
2201
2202
2203
2204
2205
2206
2207
2208
2209
2210
2211
2212
2213
2214
2215
2216
2217
2218
2219
2220
2221
2222
2223
2224
2225
2226
2227
2228
2229
2230
2231
2232
2233
2234
2235
2236
2237
2238
2239
2240
2241
2242
2243
2244
2245
2246
2247
2248
# File 'lib/openstudio-standards/btap/costing/ventilation_costing.rb', line 2127

def hrv_duct_cost(prototype_creator:, roof_cent:, mech_sizing_info:, hvac_floors:)
  hrv_cost_tot = 0
  mech_table = get_mech_table(mech_size_info: mech_sizing_info, table_name: 'trunk')
  air_system_totals = []
  hrv_dist_rep = []
  hvac_floors.each_with_index do |hvac_floor, floor_index|
    hrv_dist_rep << {
        floor: hvac_floor[:story_name],
        air_systems: []
    }
    floor_systems = sort_tzs_by_air_system(hvac_floor: hvac_floor)
    floor_systems.each_with_index do |air_system, air_index|
      next if air_system[:sys_hrv_flow_m3ps].round(2) == 0.0 || air_system[:hrv_info][:hrv_present] == false
      floor_trunk_line = nil
      floor_air_sys = {
          air_system: air_system[:air_sys].nameString,
          hrv: air_system[:hrv_info][:hrv_data].nameString,
          floor_mult: 1,
          hrv_ret_trunk: {},
          tz_dist: [],
      }
      if air_system[:num_tz] > 1
        sys_floor_mult = air_system[:tz_mult]/(air_system[:num_tz])
        floor_trunk_line = get_story_cent_to_edge(building_story: hvac_floor[:story], prototype_creator: prototype_creator, target_cent: roof_cent[:roof_centroid], full_length: true)
        hrv_trunk_cost, floor_air_sys[:hrv_ret_trunk] = get_hrv_floor_trunk_cost(mech_table: mech_table, air_system: air_system, floor_trunk_dist_m: floor_trunk_line[:end_point][:line][:dist])
        hrv_cost_tot += hrv_trunk_cost*sys_floor_mult
        floor_air_sys[:floor_mult] = sys_floor_mult
      end
      air_system[:floor_tz].each do |floor_tz|
        floor_tz[:tz_floor_ret_air_m3ps] >= floor_tz[:tz_floor_outdoor_air_m3ps] ? hrv_air = 0 : hrv_air = (floor_tz[:tz_floor_outdoor_air_m3ps] - floor_tz[:tz_floor_ret_air_m3ps]).abs
        next if hrv_air.round(2) == 0.0
        air_system_total = {
            dist_to_roof_m: (roof_cent[:roof_centroid][2] - floor_tz[:tz_cent][:centroid][2]).abs,
            hrv_air_m3ps: hrv_air*floor_tz[:tz_mult],
            num_systems: floor_tz[:tz_mult]
        }
        if floor_trunk_line.nil?
          floor_duct_coords = [roof_cent[:roof_centroid][0] - floor_tz[:tz_cent][:centroid][0], roof_cent[:roof_centroid][1] - floor_tz[:tz_cent][:centroid][1], roof_cent[:roof_centroid][2] - floor_tz[:tz_cent][:centroid][2]]
          floor_duct_dist_m = floor_duct_coords[0].abs + floor_duct_coords[1].abs
        else
          line = {
              start: floor_trunk_line[:start_point][:line][:int],
              end: floor_trunk_line[:end_point][:line][:int]
          }
          floor_duct_dist_m = short_dist_point_and_line(point: floor_tz[:tz_cent][:centroid], line: line).abs
          if floor_duct_dist_m.nil?
            floor_duct_dist_m = (line[:start][0] - floor_tz[:tz_cent][:centroid][0]).abs + (line[:start][1] - floor_tz[:tz_cent][:centroid][1]).abs
          end
        end
        if floor_duct_dist_m.round(2) > 0.1
          floor_duct_dist_ft = (OpenStudio.convert(floor_duct_dist_m, 'm', 'ft').get)
          branch_duct_sz = mech_table.select {|sz_range|
            hrv_air > sz_range['max_flow_range_m3pers'][0] && hrv_air <= sz_range['max_flow_range_m3pers'][1]
          }
          branch_duct_sz << mech_table[mech_table.size-1] if branch_duct_sz.empty?
          duct_comp_search = []
          duct_dia_in = branch_duct_sz[0]['duct_dia_inch']
          duct_surface_area = floor_duct_dist_ft*(duct_dia_in.to_f/12)*Math::PI
          duct_comp_search << {
              mat: 'Ductinsulation',
              unit: 'ft2',
              size: 1.5,
              mult: duct_surface_area
          }
          duct_comp_search << {
              mat: 'Ductwork-S',
              unit: 'L.F.',
              size: duct_dia_in,
              mult: floor_duct_dist_ft
          }
          hrv_branch_cost = get_comp_cost(cost_info: duct_comp_search)
          hrv_cost_tot += hrv_branch_cost*floor_tz[:tz_mult]
          floor_air_sys[:tz_dist] << {
              tz: floor_tz[:tz].nameString,
              tz_mult: floor_tz[:tz_mult],
              hrv_ret_dist_m: floor_duct_dist_m.round(1),
              hrv_ret_size_in: duct_dia_in.round(2),
              cost: hrv_branch_cost.round(2)
          }
        end
        air_system_totals = add_tz_to_air_sys(air_system: air_system, air_system_total: air_system_total, air_system_totals: air_system_totals, floor_tz: floor_tz)
      end
      hrv_dist_rep[floor_index][:air_systems] << floor_air_sys
    end
  end
  unless air_system_totals.empty?
    air_system_totals.each do |air_system|
      next if air_system[:hrv_air_m3ps].round(2) == 0
      # In addition to distance from floor to roof add 20' of duct from roof centre to box
      main_trunk_dist_ft = (OpenStudio.convert(air_system[:dist_to_roof_m], 'm', 'ft').get) + 20
      main_trunk_sz = mech_table.select {|sz_range|
        air_system[:hrv_air_m3ps] > sz_range['max_flow_range_m3pers'][0] && air_system[:hrv_air_m3ps] <= sz_range['max_flow_range_m3pers'][1]
      }
      main_trunk_sz << mech_table[mech_table.size-1] if main_trunk_sz.empty?
      duct_comp_search = []
      duct_dia_in = main_trunk_sz[0]['duct_dia_inch']
      duct_surf_area_ft2 = main_trunk_dist_ft*(duct_dia_in.to_f/12)*Math::PI
      duct_comp_search << {
          mat: 'Ductinsulation',
          unit: 'ft2',
          size: 1.5,
          mult: duct_surf_area_ft2
      }
      duct_comp_search << {
          mat: 'Ductwork-S',
          unit: 'L.F.',
          size: duct_dia_in,
          mult: main_trunk_dist_ft
      }
      main_trunk_cost = get_comp_cost(cost_info: duct_comp_search)
      hrv_cost_tot += main_trunk_cost
      hrv_dist_rep << {
          air_system: air_system[:air_system].nameString,
          hrv: air_system[:hrv_info][:hrv_data].nameString,
          hrv_building_trunk_length_m: air_system[:dist_to_roof_m].round(1),
          hrv_building_trunk_dia_in: duct_dia_in.round(2),
          cost: main_trunk_cost.round(2)
      }
    end
  end
  return hrv_cost_tot, hrv_dist_rep
end

#interpolate(x_y_array:, x2:, exterpolate_percentage_range: 30.0) ⇒ Object

Interpolate array of hashes that contain 2 values (key=rsi, data=cost)



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
# File 'lib/openstudio-standards/btap/costing/btap_costing.rb', line 183

def interpolate(x_y_array:, x2:, exterpolate_percentage_range: 30.0)
  ratio_range = exterpolate_percentage_range / 100.0
  array = x_y_array.uniq.sort { |a, b| a[0] <=> b[0] }
  #if there is only one...return what you got.
  if array.size == 1
    return array.first[1].to_f
  end
  # Check if value x2 is within range of array for interpolation
  # Extrapolate when x2 is out-of-range by +/- 10% of end values.
  if array.empty? || x2 < ((1.0 - ratio_range) * array.first[0].to_f) || x2 > ((1.0 + ratio_range) * array.last[0].to_f)
    return nil
  elsif x2 < array.first[0].to_f
    # Extrapolate down using first and second cost value to this out-of-range input
    x_array = [array[0][0].to_f, array[1][0].to_f]
    y_array = [array[0][1].to_f, array[1][1].to_f]
    linear_model = SimpleLinearRegression.new(x_array, y_array)
    y2 = linear_model.y_intercept + linear_model.slope * x2
    return y2
  elsif x2 > array.last[0].to_f
    # Extrapolate up using second to last and last cost value to this out-of-range input
    x_array = [array[-2][0].to_f, array[-1][0].to_f]
    y_array = [array[-2][1].to_f, array[-1][1].to_f]
    linear_model = SimpleLinearRegression.new(x_array, y_array)
    y2 = linear_model.y_intercept + linear_model.slope * x2
    return y2
  else
    array.each_index do |counter|

      # skip last value.
      next if array[counter] == array.last

      x0 = array[counter][0]
      y0 = array[counter][1]
      x1 = array[counter + 1][0]
      y1 = array[counter + 1][1]

      # skip to next if x2 is not between x0 and x1
      next if x2 < x0 || x2 > x1

      # Do interpolation
      y2 = y0 # just in-case x0, x1 and x2 are identical!
      if (x1 - x0) > 0.0
        y2 = y0.to_f + ((y1 - y0).to_f * (x2 - x0).to_f / (x1 - x0).to_f)
      end
      return y2
    end
  end
end

#line_int(line_seg:, line:, tol: 8) ⇒ Object



1539
1540
1541
1542
1543
1544
1545
1546
1547
1548
1549
1550
1551
1552
1553
1554
1555
1556
1557
1558
1559
1560
1561
1562
1563
1564
1565
1566
1567
1568
1569
1570
1571
1572
1573
1574
1575
1576
1577
1578
1579
1580
1581
1582
1583
1584
1585
1586
1587
1588
1589
1590
1591
1592
1593
1594
1595
1596
1597
1598
1599
1600
1601
1602
1603
1604
1605
1606
1607
1608
# File 'lib/openstudio-standards/btap/costing/ventilation_costing.rb', line 1539

def line_int(line_seg:, line:, tol: 8)
  line[:inf] == true && line[:int] == 1 ? x_cross = line[:slope] : x_cross = nil
  if line_seg[0][0].round(tol) == line_seg[1][0].round(tol) && line_seg[0][1].round(tol) == line_seg[1][1].round(tol)
    if x_cross.nil?
      y_val = line[:slope]*line_seg[0][0] + line[:int]
      y_val.round(tol) == line_seg[0][1].round(tol) ? (return line_seg[0]) : (return nil)
    else
      x_cross.round(tol) == line_seg[0][0].round(tol) ? (return line_seg[0]) : (return nil)
    end
  elsif line_seg[0][0].round(tol) == line_seg[1][0]
    if x_cross.nil?
      y_val = line[:slope]*line_seg[0][0] + line[:int]
      if (line_seg[0][1].round(tol) >= y_val.round(tol) && y_val.round(tol) >= line_seg[1][1].round(tol)) ||
          (line_seg[0][1].round(tol) <= y_val.round(tol) && y_val.round(tol) <= line_seg[1][1].round(tol))
        return [line_seg[0][0] , y_val, line_seg[0][2]]
      else
        return nil
      end
    else
      if x_cross.round(tol) == line_seg[0][0]
        y_val = (line_seg[0][1] + line_seg[1][1])/2
        return [line_seg[0][0] , y_val, line_seg[0][2]]
      else
        return nil
      end
    end
  end
  lineb = get_line_eq(a: line_seg[0], b: line_seg[1], tol: tol)
  if lineb[:slope].round(tol) == 0 && line[:slope].round(tol) == 0
    if x_cross.nil?
      if lineb[:int].round(tol) == line[:int].round(tol)
        x_val = (line_seg[0][0] + line_seg[1][0])/2
        return [x_val, lineb[:slope], line_seg[0][2]]
      else
        return nil
      end
    else
      if (line_seg[0][0].round(tol) <= x_cross.round(tol) && x_cross.round(tol) <= line_seg[1][0].round(tol)) ||
          (line_seg[0][0].round(tol) >= x_cross.round(tol) && x_cross.round(tol) >= line_seg[1][0].round(tol))
        [x_cross, lineb[:slope]]
      else
        return nil
      end
    end
  end
  unless x_cross.nil?
    if (line_seg[0][0].round(tol) <= x_cross.round(tol) && x_cross.round(tol) <= line_seg[1][0].round(tol)) ||
        (line_seg[0][0].round(tol) >= x_cross.round(tol) && x_cross.round(tol) >= line_seg[1][0].round(tol))
      y_val = lineb[:slope]*x_cross + lineb[:int]
      return [x_cross , y_val, line_seg[0][2]]
    else
      return nil
    end
  end
  if lineb[:inf] == true && lineb[:int] == 1
    x_int = lineb[:slope]
    y_int = line[:slope].to_f*x_int + line[:int].to_f
  else
    x_int = (lineb[:int].to_f - line[:int].to_f)/(line[:slope].to_f - lineb[:slope].to_f)
    y_int = lineb[:slope].to_f*x_int + lineb[:int].to_f
  end
  if (line_seg[0][0].round(tol) <= x_int.round(tol) && x_int.round(tol) <= line_seg[1][0].round(tol)) ||
      (line_seg[0][0].round(tol) >= x_int.round(tol) && x_int.round(tol) >= line_seg[1][0].round(tol))
    if (line_seg[0][1].round(tol) >= y_int.round(tol) && y_int.round(tol) >= line_seg[1][1].round(tol)) ||
        (line_seg[0][1].round(tol) <= y_int.round(tol) && y_int.round(tol) <= line_seg[1][1].round(tol))
      return [x_int, y_int, line_seg[0][2]]
    end
  end
  return nil
end

#line_seg_int(linea:, lineb:, tol: 8) ⇒ Object



1610
1611
1612
1613
1614
1615
1616
1617
1618
1619
1620
1621
1622
1623
1624
1625
1626
1627
1628
1629
1630
1631
1632
1633
1634
1635
1636
1637
1638
1639
1640
1641
1642
1643
1644
1645
1646
1647
1648
1649
# File 'lib/openstudio-standards/btap/costing/ventilation_costing.rb', line 1610

def line_seg_int(linea:, lineb:, tol: 8)
  if linea[0][0].round(tol) == lineb[0][0].round(tol) && linea[0][1].round(tol) == lineb[0][1].round(tol) &&
  linea[1][0].round(tol) == lineb[1][0].round(tol) && linea[1][1].round(tol) == lineb[1][1].round(tol)
    return [(linea[0][0] + linea[1][0])/2 , (linea[0][1] + linea[1][1])/2]
  elsif linea[0][0].round(tol) == linea[1][0].round(tol) && linea[0][1].round(tol) == linea[1][1].round(tol)
    return linea[0]
  elsif lineb[0][0].round(tol) == lineb[1][0].round(tol) && lineb[0][1].round(tol) == lineb[1][1].round(tol)
    return lineb[0]
  end

  o1 = get_orient(p: linea[0], q: linea[1], r: lineb[0], tol: tol)
  o2 = get_orient(p: linea[0], q: linea[1], r: lineb[1], tol: tol)
  o3 = get_orient(p: lineb[0], q: lineb[1], r: linea[0], tol: tol)
  o4 = get_orient(p: lineb[0], q: lineb[1], r: linea[1], tol: tol)

  int_sect = 0
  int_sect = 1 if o1 != o2 && o3 != o4
  return lineb[0] if o1 == 0 && point_on_line(p: linea[0], q: lineb[0], r: linea[1], tol: tol)
  return lineb[1] if o2 == 0 && point_on_line(p: linea[0], q: lineb[1], r: linea[1], tol: tol)
  return linea[0] if o3 == 0 && point_on_line(p: lineb[0], q: linea[0], r: lineb[1], tol: tol)
  return linea[1] if o4 == 0 && point_on_line(p: lineb[0], q: linea[1], r: lineb[1], tol: tol)

  return nil if int_sect == 0

  eq_linea = get_line_eq(a: linea[0], b: linea[1], tol: tol)
  eq_lineb = get_line_eq(a: lineb[0], b: lineb[1], tol: tol)
  if eq_linea[:inf] == true && eq_linea[:slope].to_f == 1
    x_int = linea[0][0]
    y_int = eq_lineb[:slope].to_f*x_int + eq_lineb[:int].to_f
    return [x_int, y_int]
  elsif eq_lineb[:inf] == true && eq_lineb[:slope].to_f == 1
    x_int = lineb[0][0]
    y_int = eq_linea[:slope].to_f*x_int + eq_linea[:int].to_f
    return [x_int, y_int]
  else
    x_int = (eq_lineb[:int].to_f - eq_linea[:int].to_f) / (eq_linea[:slope].to_f - eq_lineb[:slope].to_f)
    y_int = eq_lineb[:slope].to_f*x_int + eq_lineb[:int].to_f
    return [x_int, y_int]
  end
end

#load_databaseObject



59
60
61
# File 'lib/openstudio-standards/btap/costing/btap_costing.rb', line 59

def load_database()
  @costing_database.load_database
end

#mech_to_roof_cost(heat_type:, cool_type:, mech_room:, roof_cent:, rt_unit_num:) ⇒ Object



933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
# File 'lib/openstudio-standards/btap/costing/ventilation_costing.rb', line 933

def mech_to_roof_cost(heat_type:, cool_type:, mech_room:, roof_cent:, rt_unit_num:)
  mech_to_roof_rep = {
      Gas_Line_m: 0.0,
      HW_Line_m: 0.0,
      CHW_Line_m: 0.0,
      Elec_Line_m: 0.0,
      Total_cost: 0.0
  }
  mech_dist = [(roof_cent[:roof_centroid][0] - mech_room['space_centroid'][0]), (roof_cent[:roof_centroid][1] - mech_room['space_centroid'][1]), (roof_cent[:roof_centroid][2] - mech_room['space_centroid'][2])]
  utility_dist = 0
  ut_search = []
  rt_roof_dist = OpenStudio.convert(10, 'm', 'ft').get
  mech_dist.each{|dist| utility_dist+= dist.abs}
  utility_dist = OpenStudio.convert(utility_dist, 'm', 'ft').get
  heat_type.each do |key, value|
    if value >= 1
      case key
      when 'HP'
        next
      when 'elec'
        next
      when 'Gas'
        ut_search << {
            mat: 'GasLine',
            unit: 'L.F.',
            size: 0,
            mult: utility_dist + rt_roof_dist*value
        }
        heat_type['Gas'] = 0
        mech_to_roof_rep[:Gas_Line_m] == (utility_dist + rt_roof_dist*value).round(1)
      when 'HW'
        ut_search << {
            mat: 'SteelPipe',
            unit: 'L.F.',
            size: 4,
            mult: 2*utility_dist + 2*rt_roof_dist*value
        }
        mech_to_roof_rep[:HW_Line_m] = (2*utility_dist + 2*rt_roof_dist*value).round(1)
        ut_search << {
            mat: 'PipeInsulation',
            unit: 'none',
            size: 4,
            mult: 2*utility_dist + 2*rt_roof_dist*value
        }
        ut_search << {
            mat: 'PipeJacket',
            unit: 'none',
            size: 4,
            mult: 2*utility_dist + 2*rt_roof_dist*value
        }
      end
    end
  end

  cool_type.each do |key, value|
    if value >= 1
      case key
      when 'DX'
        next
      when 'CHW'
        ut_search << {
            mat: 'SteelPipe',
            unit: 'L.F.',
            size: 4,
            mult: 2*utility_dist + 2*rt_roof_dist*value
        }
        mech_to_roof_rep[:CHW_Line_m] = (2*utility_dist + 2*rt_roof_dist*value).round(1)
        ut_search << {
            mat: 'PipeInsulation',
            unit: 'none',
            size: 4,
            mult: 2*utility_dist + 2*rt_roof_dist*value
        }
        ut_search << {
            mat: 'PipeJacket',
            unit: 'none',
            size: 4,
            mult: 2*utility_dist + 2*rt_roof_dist*value
        }
      end
    end
  end
  mech_to_roof_rep[:Elec_Line_m] = (utility_dist + rt_unit_num*rt_roof_dist).round(1)
  ut_search << {
      mat: 'Wiring',
      unit: 'CLF',
      size: 10,
      mult: (utility_dist + rt_unit_num*rt_roof_dist)/100
  }
  ut_search << {
      mat: 'Conduit',
      unit: 'L.F.',
      size: 0,
      mult: utility_dist + rt_unit_num*rt_roof_dist
  }
  total_comp_cost = get_comp_cost(cost_info: ut_search)
  mech_to_roof_rep[:Total_cost] = total_comp_cost.round(2)
  return total_comp_cost, mech_to_roof_rep
end

#piping_cost(pipe_dist_m:, mech_sizing_info:, air_m3_per_s:, is_cool: false, vent_tags: [], report_mult: 1.0) ⇒ Object



1196
1197
1198
1199
1200
1201
1202
1203
1204
1205
1206
1207
1208
1209
1210
1211
1212
1213
1214
1215
1216
1217
1218
1219
1220
1221
1222
1223
1224
1225
1226
1227
1228
1229
1230
1231
1232
1233
1234
1235
1236
1237
1238
1239
1240
1241
1242
1243
1244
1245
1246
1247
1248
# File 'lib/openstudio-standards/btap/costing/ventilation_costing.rb', line 1196

def piping_cost(pipe_dist_m:, mech_sizing_info:, air_m3_per_s:, is_cool: false, vent_tags: [], report_mult: 1.0)
  piping_tags = vent_tags.clone
  piping_tags << "piping" unless piping_tags.nil?
  pipe_dist = OpenStudio.convert(pipe_dist_m, 'm', 'ft').get
  air_flow = (OpenStudio.convert(air_m3_per_s, 'm^3/s', 'L/s').get)
  air_flow = 15000 if air_flow > 15000
  mech_table = get_mech_table(mech_size_info: mech_sizing_info, table_name: 'piping')
  pipe_sz_info = mech_table.select {|pipe_choice|
    pipe_choice['ahu_airflow_range_Literpers'][0].to_f.round(0) < air_flow.round(0) and
        pipe_choice['ahu_airflow_range_Literpers'][1].to_f.round(0) >= air_flow.round(0)
  }.first
  pipe_dia = pipe_sz_info['heat_valve_pipe_dia_inch'].to_f.round(2)
  pipe_dia = pipe_sz_info['cool_valve_pipe_dia_inch'].to_f.round(2) if is_cool == true
  pipe_cost_search = []
  pipe_cost_search << {
      mat: 'Steelpipe',
      unit: 'L.F.',
      size: pipe_dia,
      mult: 2*pipe_dist
  }
  pipe_cost_search << {
      mat: 'SteelPipeElbow',
      unit: 'none',
      size: pipe_dia,
      mult: 2
  }
  pipe_cost_search << {
      mat: 'SteelPipeTee',
      unit: 'none',
      size: pipe_dia,
      mult: 2
  }
  pipe_cost_search << {
      mat: 'SteelPipeTeeRed',
      unit: 'none',
      size: pipe_dia,
      mult: 2
  }
  pipe_cost_search << {
      mat: 'SteelPipeRed',
      unit: 'none',
      size: pipe_dia,
      mult: 2
  }
  pipe_dia > 3 ? pipe_dia_union = 3 : pipe_dia_union = pipe_dia
  pipe_cost_search << {
      mat: 'SteelPipeUnion',
      unit: 'none',
      size: pipe_dia_union,
      mult: 2
  }
  return get_comp_cost(cost_info: pipe_cost_search, vent_tags: piping_tags, report_mult: report_mult)
end

#point_on_line(p:, q:, r:, tol: 8) ⇒ Object



1657
1658
1659
1660
1661
1662
1663
1664
# File 'lib/openstudio-standards/btap/costing/ventilation_costing.rb', line 1657

def point_on_line(p:, q:, r:, tol: 8)
  q[0].round(tol) <= [p[0].round(tol), r[0].round(tol)].max ? crita = true : crita = false
  q[0].round(tol) >= [p[0].round(tol), r[0].round(tol)].min ? critb = true : critb = false
  q[1].round(tol) <= [p[1].round(tol), r[1].round(tol)].max ? critc = true : critc = false
  q[1].round(tol) >= [p[1].round(tol), r[1].round(tol)].min ? critd = true : critd = false
  return true if crita && critb && critc && critd
  return false
end

#read_mech_sizingObject



283
284
285
286
# File 'lib/openstudio-standards/btap/costing/btap_costing.rb', line 283

def read_mech_sizing()
  file = File.read(@cp.mech_sizing_data_file)
  return JSON.parse(file)
end

#reheat_coil_costing(terminal:, tz_centroids:, model:, tz:, roof_cent:, tz_mult:, mech_sizing_info:, air_m3_per_s:, box_name:, vent_tags: [], report_mult: 1.0) ⇒ Object



1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
# File 'lib/openstudio-standards/btap/costing/ventilation_costing.rb', line 1096

def reheat_coil_costing(terminal:, tz_centroids:, model:, tz:, roof_cent:, tz_mult:, mech_sizing_info:, air_m3_per_s:, box_name:, vent_tags: [], report_mult: 1.0)
  coil_tags = vent_tags.clone
  coil_mat = 'none'
  coil_cost = 0
  coil = terminal.reheatCoil
  case coil.iddObject.name
  when /Water/
    coil = coil.to_CoilHeatingWater.get
    if coil.isRatedCapacityAutosized
      capacity = coil.autosizedRatedCapacity.to_f/(1000.0*tz_mult)
    else
      capacity = coil.ratedCapacity.to_f/(1000.0*tz_mult)
    end
    coil_mat = 'Coils'
    coil_tags << "water coil" unless coil_tags.empty?
  when /Electric/
    coil = coil.to_CoilHeatingElectric.get
    if coil.isNominalCapacityAutosized.to_bool
      capacity = (coil.autosizedNominalCapacity.to_f)/(1000.0*tz_mult)
    else
      capacity = (coil.nominalCapacity.to_f)/(1000.0*tz_mult)
    end
    coil_mat = 'ElecDuct'
    coil_tags << "electric duct heater" unless coil_tags.empty?
  end
  return 0, {size_kw: 0.0, air_flow_m3_per_s: 0.0, pipe_dist_m: 0.0, elect_dist_m: 0.0, num_units: 0} if coil_mat == 'none'
  pipe_length_m = 0
  elect_length_m = 0
  num_coils = 0
  tz_centroids.sort.each do |tz_cent|
    coil_tags << tz_cent[:story_name]
    story_floor_area = 0
    num_coils += 1
    tz_cent[:spaces].each { |space| story_floor_area += space.floorArea.to_f }
    floor_area_frac = (story_floor_area/tz.floorArea).round(2)
    floor_cap = floor_area_frac*capacity
    coil_cost += get_mech_costing(mech_name: coil_mat, size: floor_cap, terminal: terminal, vent_tags: coil_tags, report_mult: report_mult)
    coil_cost += get_mech_costing(mech_name: box_name, size: floor_area_frac*(OpenStudio.convert(air_m3_per_s, 'm^3/s', 'cfm').get), terminal: terminal, vent_tags: coil_tags, report_mult: report_mult)
    ut_dist = (tz_cent[:centroid][0].to_f - roof_cent[:roof_centroid][0].to_f).abs + (tz_cent[:centroid][1].to_f - roof_cent[:roof_centroid][1].to_f).abs
    if coil_mat == 'Coils'
      pipe_length_m += ut_dist
      coil_cost += piping_cost(pipe_dist_m: ut_dist, mech_sizing_info: mech_sizing_info, air_m3_per_s: air_m3_per_s, vent_tags: coil_tags, report_mult: report_mult)
    end
    elect_length_m += ut_dist
    coil_cost += vent_box_elec_cost(cond_dist_m: ut_dist, vent_tags: coil_tags, report_mult: report_mult)
    coil_tags.pop()
  end
  box_info = {size_kw: capacity.round(3), air_flow_m3_per_s: air_m3_per_s.round(3), pipe_dist_m: pipe_length_m.round(1), elect_dist_m: elect_length_m.round(1), num_units: num_coils}
  return coil_cost, box_info
end

#reheat_recool_cost(airloop:, prototype_creator:, model:, roof_cent:, mech_sizing_info:, vent_tags: [], report_mult: 1.0) ⇒ Object



1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
# File 'lib/openstudio-standards/btap/costing/ventilation_costing.rb', line 1033

def reheat_recool_cost(airloop:, prototype_creator:, model:, roof_cent:, mech_sizing_info:, vent_tags: [], report_mult: 1.0)
  reheat_recool_tags = vent_tags.clone
  heat_cost = 0
  out_reheat_array = []
  airloop.thermalZones.sort.each do |thermalzone|
    tz_mult = thermalzone.multiplier.to_f
    thermalzone.equipment.sort.each do |eq|
      tz_eq_cost = 0
      terminal, box_name = get_airloop_terminal_type(eq: eq)
      next if box_name.nil?
      if terminal.isMaximumAirFlowRateAutosized.to_bool
        query = "SELECT Value FROM ComponentSizes WHERE CompName='#{eq.name.to_s.upcase}' AND Description='Design Size Maximum Air Flow Rate'"
        air_m3_per_s = model.sqlFile().get().execAndReturnFirstDouble(query).to_f/tz_mult
      else
        air_m3_per_s = terminal.maximumAirFlowRate.to_f/tz_mult
      end
      tz_centroids = prototype_creator.thermal_zone_get_centroid_per_floor(thermalzone)
      reheat_recool_tags << thermalzone.name.to_s
      if box_name == 'CVMixingBoxes'
        reheat_recool_tags << "Contant Volume Mixing Box" unless vent_tags.empty?
        tz_eq_cost, box_info = reheat_coil_costing(terminal: terminal, tz_centroids: tz_centroids, model: model, tz: thermalzone, roof_cent: roof_cent, tz_mult: tz_mult, mech_sizing_info: mech_sizing_info, air_m3_per_s: air_m3_per_s, box_name: box_name, vent_tags: reheat_recool_tags, report_mult: (tz_mult*report_mult))
        reheat_recool_tags.pop()
      else
        reheat_recool_tags << "VAV" unless vent_tags.empty?
        tz_eq_cost, box_info = vav_cost(terminal: terminal, tz_centroids: tz_centroids, tz: thermalzone, roof_cent: roof_cent, mech_sizing_info: mech_sizing_info, air_flow_m3_per_s: air_m3_per_s, box_name: box_name, vent_tags: reheat_recool_tags, report_mult: (tz_mult*report_mult))
        reheat_recool_tags.pop()
      end
      reheat_recool_tags.pop()
      heat_cost += tz_mult*tz_eq_cost
      out_reheat_array << {
          terminal: (terminal.iddObjectType.valueName.to_s)[3..-1],
          zone_mult: tz_mult,
          box_type: box_name,
          box_name: terminal.nameString,
          unit_info: box_info,
          cost: tz_eq_cost.round(2)
      }
    end
  end
  return heat_cost, out_reheat_array
end

#short_dist_point_and_line(point:, line:) ⇒ Object



2350
2351
2352
2353
2354
2355
2356
2357
2358
2359
2360
2361
2362
2363
2364
2365
2366
2367
# File 'lib/openstudio-standards/btap/costing/ventilation_costing.rb', line 2350

def short_dist_point_and_line(point:, line:)
  line_eq = get_line_eq(a: line[:start], b: line[:end])
  if line_eq[:int] == 1 and line_eq[:inf] == true
    dist = point[0] - line_eq[:slope]
  elsif line_eq[:int] == 0 and line_eq[:inf] == true
    dist = nil
  else
    # Turn equation of line as:  y = slope*x + intercept
    # into:  a*x + b*y + c = 0
    # a = slope, b = -1, c = intercept
    a = line_eq[:slope]
    b = -1
    c = line_eq[:int]
    # Use dot product to get shortest distance from point to line
    dist = (a*point[0] + b*point[1] + c) / Math.sqrt(a**2 + b**2)
  end
  return dist
end

#shw_costing(model, prototype_creator) ⇒ Object


This function gets all costs associated with SHW/DHW (i.e., tanks, pumps, flues, piping and utility costs)




7
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
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
# File 'lib/openstudio-standards/btap/costing/shw_costing.rb', line 7

def shw_costing(model, prototype_creator)

  @costing_report['shw'] = {}
  totalCost = 0.0

  # Get regional cost factors for this province and city
  materials_hvac = @costing_database["raw"]["materials_hvac"]
  hvac_material = materials_hvac.select {|data|
    data['Material'].to_s == "WaterGas"}.first  # Get any row from spreadsheet in case of region error
  regional_material, regional_installation =
      get_regional_cost_factors(@costing_report['province_state'], @costing_report['city'], hvac_material)
  # Use wiring to get regional cost factors for electrical equipment such as conduit and VFDs
  hvac_material_elec = get_cost_info(mat: 'Wiring', size: 14, unit: nil)
  regional_material_elec, regional_installation_elec =
      get_regional_cost_factors(@costing_report['province_state'], @costing_report['city'], hvac_material_elec)

  # Store some geometry data for use below...
  util_dist, ht_roof, nominal_flr2flr_height, horizontal_dist = getGeometryData(model, prototype_creator)

  template_type = prototype_creator.template

  plant_loop_info = {}
  plant_loop_info[:shwtanks] = []
  plant_loop_info[:shwpumps] = []
  hphw_tank_names = []

  num_reg_gas_tanks = 0
  num_reg_oil_tanks = 0
  num_elec_tanks = 0
  num_hphw_tanks = 0
  num_high_eff_gas_tanks = 0
  num_high_eff_oil_tanks = 0

  # HPHW heaters are stored outside of the plant loop
  # Iterate through these first to determine if their are HPHW heaters
  model.getWaterHeaterHeatPumps.each do |hphw|
    if hphw.to_WaterHeaterHeatPump.is_initialized
      hphw_tank_name = hphw.tank.name.get
      hphw_tank_names << hphw_tank_name
    end
  end
  # Iterate through the plant loops to get shw tank & pump data...
  model.getPlantLoops.each do |plant_loop|
    next unless plant_loop.name.get.to_s =~ /Main Service Water Loop/i
    plant_loop.supplyComponents.each do |supply_comp|
      if supply_comp.to_WaterHeaterMixed.is_initialized
        tank = supply_comp.to_WaterHeaterMixed.get
        tank_info = {}
        plant_loop_info[:shwtanks] << tank_info
        tank_info[:name] = tank.name.get
        tank_info[:type] = "WaterHeater:Mixed"
        tank_info[:heater_thermal_efficiency] = tank.heaterThermalEfficiency.get unless tank.heaterThermalEfficiency.empty?
        tank_info[:heater_fuel_type] = tank.heaterFuelType
        tank_info[:nominal_capacity] = tank.heaterMaximumCapacity.to_f / 1000 # kW
        tank_info[:heater_volume_gal] = (OpenStudio.convert(tank.tankVolume.to_f, 'm^3', 'gal').get)
        tank_info[:eff_mult] = 1.0
        if tank.heaterFuelType =~ /Electric/i
          # Check if the tank is associated with a HPHW heater
          if hphw_tank_names.include?(tank.name.get)
            tank_info[:heater_fuel_type] = 'HPHW_Heater'
            tank_info[:tank_mult] = get_HVAC_multiplier(tank_info[:heater_fuel_type], tank_info[:nominal_capacity])
            tank_info[:nominal_capacity] /= tank_info[:tank_mult]
            tank_info[:heater_volume_gal] /= tank_info[:tank_mult]
            num_hphw_tanks += tank_info[:tank_mult]
          elsif !hphw_tank_names.include?(tank.name.get)
            tank_info[:heater_fuel_type] = 'WaterElec'
            tank_info[:tank_mult] = get_HVAC_multiplier(tank_info[:heater_fuel_type], tank_info[:nominal_capacity])
            tank_info[:nominal_capacity] /= tank_info[:tank_mult]
            tank_info[:heater_volume_gal] /= tank_info[:tank_mult]
            num_elec_tanks += tank_info[:tank_mult]
          end
        elsif tank.heaterFuelType =~ /NaturalGas/i
          tank_info[:heater_fuel_type] = 'WaterGas'
          tank_info[:tank_mult] = get_HVAC_multiplier(tank_info[:heater_fuel_type], tank_info[:nominal_capacity])
          tank_info[:nominal_capacity] /= tank_info[:tank_mult]
          tank_info[:heater_volume_gal] /= tank_info[:tank_mult]
          if tank_info[:heater_thermal_efficiency] >= 0.85
            tank_info[:heater_fuel_type] = 'WaterGas_HE'
            tank_info[:eff_mult] = 1.3
            num_high_eff_gas_tanks += tank_info[:tank_mult]
          else
            num_reg_gas_tanks += tank_info[:tank_mult]
          end
        elsif tank.heaterFuelType =~ /Oil/i       # Oil, FuelOil, FuelOil#2
          tank_info[:heater_fuel_type] = 'WaterOil'
          tank_info[:tank_mult] = get_HVAC_multiplier(tank_info[:heater_fuel_type], tank_info[:nominal_capacity])
          tank_info[:nominal_capacity] /= tank_info[:tank_mult]
          tank_info[:heater_volume_gal] /= tank_info[:tank_mult]
          if tank_info[:heater_thermal_efficiency] >= 0.85
            tank_info[:heater_fuel_type] = 'WaterOil_HE'
            tank_info[:eff_mult] = 1.3
            num_high_eff_oil_tanks += tank_info[:tank_mult]
          else
            num_reg_oil_tanks += tank_info[:tank_mult]
          end
        end
      elsif supply_comp.to_PumpConstantSpeed.is_initialized
        csPump = supply_comp.to_PumpConstantSpeed.get
        csPump_info = {}
        plant_loop_info[:shwpumps] << csPump_info
        csPump_info[:name] = csPump.name.get
        if csPump.isRatedPowerConsumptionAutosized.to_bool
          csPumpSize = csPump.autosizedRatedPowerConsumption.to_f
        else
          csPumpSize = csPump.ratedPowerConsumption.to_f
        end
        csPump_info[:size] = csPumpSize.to_f # Watts
      elsif supply_comp.to_PumpVariableSpeed.is_initialized
        vsPump = supply_comp.to_PumpVariableSpeed.get
        vsPump_info = {}
        plant_loop_info[:shwpumps] << vsPump_info
        vsPump_info[:name] = vsPump.name.get
        if vsPump.isRatedPowerConsumptionAutosized.to_bool
          vsPumpSize = vsPump.autosizedRatedPowerConsumption.to_f
        else
          vsPumpSize = vsPump.ratedPowerConsumption.to_f
        end
        vsPump_info[:size] = vsPumpSize.to_f # Watts
      end
    end
  end

  # Get costs associated with each shw tank
  tankCost = 0.0 ; flueCost = 0.0 ; utilCost = 0.0 ; fuelFittingCost = 0.0; fuelLineCost = 0.0
  multiplier = 1.0 ; primaryFuel = ''; primaryCap = 0

  plant_loop_info[:shwtanks].each do |tank|
    # Get primary/secondary/backup tank cost based on fuel type and capacity for each tank
    #set to local variables.
    primaryFuel = tank[:heater_fuel_type]
    primaryCap = tank[:nominal_capacity].to_f
    heaterVolGal = tank[:heater_volume_gal].to_f

    #Get tank cost.
    if primaryFuel.include?("WaterGas")
      # For gas fired shw tanks we don't have to bother with volume.  However, we have to accept a revised tank volume
      # which is there for electric and oil tanks even though we won't use it.
      shwTankCostInfo = getSHWTankCost(name: tank[:name], materialLookup: primaryFuel, materialSize: primaryCap, tankVol: nil)
      tank[:nominal_cacacity] = shwTankCostInfo[:Cap_kW]
    else
      # If the SHW tank is electric or oil need to find the cost for one with a large enough capacity and volume. If
      # no tanks have a large enough volume then get_SHWTankCost will find the tank with the largest volume and find
      # how many tanks of that size are needed (multiplier).  Below, if this multiplier is larger than one then
      # the tank volume is adjusted to be the largest one that was found (by revVol) and the tank required capacity is
      # reduced by dividing by the multiplier.
      shwTankCostInfo  = getSHWTankCost(name: tank[:name], materialLookup: primaryFuel, materialSize: primaryCap, tankVol: heaterVolGal)
      tank[:heater_volume_gal] = shwTankCostInfo[:Vol_USGal]
      tank[:nominal_capacity] = shwTankCostInfo[:Cap_kW]
      if shwTankCostInfo[:multiplier] > 1.0
        if primaryFuel.include?("WaterElec")
          num_elec_tanks -= tank[:tank_mult]
          tank[:tank_mult] *= shwTankCostInfo[:multiplier]
          num_elec_tanks += tank[:tank_mult]
        elsif primaryFuel.include?("HPHW_Heater")
          num_hphw_tanks -= tank[:tank_mult]
          tank[:tank_mult] *= shwTankCostInfo[:multiplier]
          num_hphw_tanks += tank[:tank_mult]
        else
          if tank[:heater_thermal_efficiency] >= 0.85
            num_high_eff_oil_tanks -= tank[:tank_mult]
            tank[:tank_mult] *= shwTankCostInfo[:multiplier]
            num_high_eff_oil_tanks += tank[:tank_mult]
          else
            num_reg_oil_tanks -= tank[:tank_mult]
            tank[:tank_mult] *= shwTankCostInfo[:multiplier]
            num_reg_oil_tanks += tank[:tank_mult]
          end
        end
      end
    end
    matCost = shwTankCostInfo[:matCost]*tank[:tank_mult].to_f
    labCost = shwTankCostInfo[:labCost]*tank[:tank_mult].to_f

    thisTankCost = matCost * regional_material / 100.0 + labCost * regional_installation / 100.0
    tankCost += thisTankCost

    # Determine power venting costs for high efficiency tanks.  Doing this here because tank multiplier and capacity
    # may have changed.
    if tank[:eff_mult] > 1.1
      if shwTankCostInfo[:Cap_kW] < 200
        # 1/8 hp power vent
        materialHash = materials_hvac.find {|data|
          data['Material'].to_s == 'Waterheater_power_vent' && data['Size'].to_s == '0.125'}
        matCost, labCost = getCost('1/8 hp power vent', materialHash, multiplier)
        flueCost += (matCost * regional_material / 100.0 + labCost * regional_installation / 100.0) * tank[:tank_mult]
      else
        # 1/2 hp power vent
        materialHash = materials_hvac.find {|data|
          data['Material'].to_s == 'Waterheater_power_vent' && data['Size'].to_s == '0.5'}
        matCost, labCost = getCost('1/2 hp power vent', materialHash, multiplier)
        flueCost += (matCost * regional_material / 100.0 + labCost * regional_installation / 100.0) * tank[:tank_mult]
      end
    end
  end

  numTanks = num_elec_tanks + num_hphw_tanks + num_reg_gas_tanks + num_high_eff_gas_tanks + num_reg_oil_tanks + num_high_eff_oil_tanks
  numFuelTanks = num_reg_gas_tanks + num_high_eff_gas_tanks + num_reg_oil_tanks + num_high_eff_oil_tanks

  if numTanks > 0
    # Electric utility cost components (i.e., power lines).

    # elec 600V #14 wire /100 ft (#848)
    materialHash = get_cost_info(mat: 'Wiring', size: 14)
    matCost, labCost = getCost('electrical wire - 600V #14', materialHash, multiplier)
    elecWireCost = matCost * regional_material_elec / 100.0 + labCost * regional_installation_elec / 100.0

    # 1 inch metal conduit (#851)
    materialHash = get_cost_info(mat: 'Conduit', unit: 'L.F.')
    matCost, labCost = getCost('1 inch metal conduit', materialHash, multiplier)
    metalConduitCost = matCost * regional_material_elec / 100.0 + labCost * regional_installation_elec / 100.0

    # Electric utility wire and conduit cost used by all tanks except HPHW
    utilCost += (metalConduitCost * util_dist + elecWireCost * util_dist / 100) * (numTanks - num_hphw_tanks)

    # Get costs condition on fuel types.
    if numFuelTanks> 0
      numRegFuelTanks = num_reg_gas_tanks + num_reg_oil_tanks
      numHighEffFuelTanks = num_high_eff_gas_tanks + num_high_eff_oil_tanks

      # Gas/Oil line piping cost per ft (#1)
      materialHash = get_cost_info(mat: 'GasLine', unit: 'L.F.')
      matCost, labCost = getCost('fuel line', materialHash, multiplier)
      fuelLineCost = matCost * regional_material / 100.0 + labCost * regional_installation / 100.0

      # Gas/Oil line fitting connection per tank (#2)
      materialHash = get_cost_info(mat: 'GasLine', unit: 'each')
      matCost, labCost = getCost('fuel line fitting connection', materialHash, multiplier)
      fuelFittingCost = matCost * regional_material / 100.0 + labCost * regional_installation / 100.0

      if numRegFuelTanks > 0
        # Flue and utility component costs (for gas and oil tanks only)
        # Calculate flue costs once for all tanks since flues combined by header when multiple tanks
        # 6 inch diameter flue (#384)
        materialHash = get_cost_info(mat: 'Venting', size: 6)
        matCost, labCost = getCost('flue', materialHash, multiplier)
        flueVentCost = matCost * regional_material / 100.0 + labCost * regional_installation / 100.0

        #6 inch elbow fitting (#386)
        materialHash = get_cost_info(mat: 'VentingElbow', size: 6)
        matCost, labCost = getCost('flue elbow', materialHash, multiplier)
        flueElbowCost = matCost * regional_material / 100.0 + labCost * regional_installation / 100.0

        # 6 inch top (#392)
        materialHash = get_cost_info(mat: 'VentingTop', size: 6)
        matCost, labCost = getCost('flue top', materialHash, multiplier)
        flueTopCost = matCost * regional_material / 100.0 + labCost * regional_installation / 100.0

        # Adding one regular flue if any regular efficiency shw tanks are present
        flueCost += flueVentCost * ht_roof + flueElbowCost + flueTopCost

        # Header cost only non-zero if there is a secondary/backup gas/oil tank
        if numRegFuelTanks > 1
          # Check if need a flue header (i.e., there are both primary and secondary/backup tanks)
          # 6 inch diameter header (#384)
          materialHash = get_cost_info(mat: 'Venting', size: 6)
          matCost, labCost = getCost('flue header', materialHash, multiplier)
          headerVentCost = matCost * regional_material / 100.0 + labCost * regional_installation / 100.0

          #6 inch elbow fitting for header (#386)
          materialHash = get_cost_info(mat: 'VentingElbow', size: 6)
          matCost, labCost = getCost('flue header elbow', materialHash, multiplier)
          headerElbowCost = matCost * regional_material / 100.0 + labCost * regional_installation / 100.0

          # Adding a regular tank header for every additional regular efficiency SHW tank present
          # Assume a header length of 20 ft and an elbow fitting for each tank connected to the header
          flueCost += (headerVentCost * 20  + headerElbowCost) * (numRegFuelTanks - 1)
        end
      end

      # If high efficiency fuel fired shw tanks are present add flues (1 per tank)
      if numHighEffFuelTanks > 0
        #6 inch PVC pipe (#1327)
        materialHash = get_cost_info(mat: 'Vent_pvc', size: 6)
        matCost, labCost = getCost('flue', materialHash, multiplier)
        pvcFluePipe = (matCost * regional_material / 100.0 + labCost * regional_installation / 100.0)

        #6 inch PVC Coupling (#1319)
        materialHash = get_cost_info(mat: 'Vent_pvc_coupling', size: 6)
        matCost, labCost = getCost('flue elbow', materialHash, multiplier)
        pvcFlueCoupling = matCost * regional_material / 100.0 + labCost * regional_installation / 100.0

        # 6 inch PVC elbow (#1329)
        materialHash = get_cost_info(mat: 'Vent_pvc_elbow', size: 6)
        matCost, labCost = getCost('flue top', materialHash, multiplier)
        pvcFlueElbow = matCost * regional_material / 100.0 + labCost * regional_installation / 100.0

        # Adding PVC flue costs for all high efficiency fuel fired SHW tanks
        flueCost += (pvcFluePipe * 20.0 + pvcFlueCoupling + pvcFlueElbow) * numHighEffFuelTanks
      end

      # If natural gas tanks are present include fuel line and connectors
      if (num_reg_gas_tanks + num_reg_gas_tanks) > 0
        # Gas tanks require fuel line+valves+connectors
        utilCost += (fuelLineCost * util_dist + fuelFittingCost) * (num_reg_gas_tanks + num_high_eff_gas_tanks)

      elsif (num_reg_oil_tanks + num_high_eff_oil_tanks) > 0
        # Oil tanks require fuel line+valves+connectors and electrical conduit

        # Oil filtering system (#4)
        materialHash = get_cost_info(mat: 'OilLine', unit: 'each')
        matCost, labCost = getCost('Oil filtering system', materialHash, multiplier)
        oilFilterCost = matCost * regional_material / 100.0 + labCost * regional_installation / 100.0

        # 2000 USG above ground tank (#5)
        materialHash = get_cost_info(mat: 'OilTanks', size: 2000)
        matCost, labCost = getCost('Oil tank (2000 USG)', materialHash, multiplier)
        oilTankCost = matCost * regional_material / 100.0 + labCost * regional_installation / 100.0

        utilCost += (fuelLineCost * util_dist + fuelFittingCost) * (num_reg_oil_tanks + num_high_eff_oil_tanks) + oilFilterCost + oilTankCost
      end
    end
  end

  # Tank pump costs
  pumpCost = 0.0; pipingCost = 0.0; numPumps = 0; pumpName = ''; pumpSize = 0.0
  plant_loop_info[:shwpumps].each do |pump|
    numPumps += 1
    # Cost variable and constant volume pumps the same (the difference is in extra cost for VFD controller)
    pumpSize = pump[:size]; pumpName = pump[:name]
    matCost, labCost = getHVACCost(pumpName, 'Pumps', pumpSize, false)
    pumpCost += matCost * regional_material / 100.0 + labCost * regional_installation / 100.0
    if pump[:name] =~ /variable/i
      # Cost the VFD controller for the variable pump costed above
      pumpSize = pump[:size]; pumpName = pump[:name]
      matCost, labCost = getHVACCost(pumpName, 'VFD', pumpSize, false)
      pumpCost += matCost * regional_material_elec / 100.0 + labCost * regional_installation_elec / 100.0
    end
  end
  #if numTanks > 1 && numPumps < 2
    # Add pump costing for the backup tank pump.
    # 2024-04-25:  No longer including redundant costs.
    #pumpCost *= 2.0
    #numPumps = 2  # reset the number of pumps for piping costs below
  #end
  # Double the pump costs to accomodate the costing of a backup pumps for each tank!
  # 2024-04-25:  No longer including redundant casts.
  # pumpCost *= 2.0

  # Tank water piping cost: Add piping elbows, valves and insulation from the tank(s)
  # to the pumps(s) assuming a pipe diameter of 1” and a distance of 10 ft per pump
  if numTanks > 0
    # 1 inch Steel pipe
    matCost, labCost = getHVACCost('1 inch steel pipe', 'SteelPipe', 1)
    pipingCost += 10.0 * numPumps * (matCost * regional_material / 100.0 + labCost * regional_installation / 100.0)

    # 1 inch Steel pipe insulation
    matCost, labCost = getHVACCost('1 inch pipe insulation', 'PipeInsulation', 1)
    pipingCost += 10.0 * numPumps * (matCost * regional_material / 100.0 + labCost * regional_installation / 100.0)

    # 1 inch Steel pipe elbow
    matCost, labCost = getHVACCost('1 inch steel pipe elbow', 'SteelPipeElbow', 1)
    pipingCost += 2.0 * numPumps * (matCost * regional_material / 100.0 + labCost * regional_installation / 100.0)

    # 1 inch gate valves
    matCost, labCost = getHVACCost('1 inch gate valves', 'ValvesGate', 1)
    pipingCost += 1.0 * numPumps * (matCost * regional_material / 100.0 + labCost * regional_installation / 100.0)
  end

  # 2023-04-25:  Removing costing for redundant equipment and piping.
  #if numTanks > 1
    # Double pump piping cost to account for second tank
  #  pipingCost *= 2
  #end

  # ckirney, 2019-04-12:  shw_distribution_costing mostly completed however priorities have changed for now so
  # completion and testing will be delayed.  Adding code to master for now but it will not be called until it is
  # ready.
  # distCost = shw_distribution_costing(model: model, prototype_creator: prototype_creator)

  totalCost = tankCost + flueCost + utilCost + pumpCost + pipingCost

  @costing_report['shw'] = {
      'shw_nom_flr2flr_hght_ft' => nominal_flr2flr_height.round(1),
      'shw_ht_roof' => ht_roof.round(1),
      'shw_longest_distance_to_ext_ft' => horizontal_dist.round(1),
      'shw_utility_distance_ft' => util_dist.round(1),
      'shw_tanks' => tankCost.round(2),
      'shw_num_of_modeled_tanks' => plant_loop_info[:shwtanks].size,
      'num_elec_tanks' => num_elec_tanks,
      'num_hphw_tanks' => num_hphw_tanks,
      'shw_num_reg_eff_gas_tanks' => num_reg_gas_tanks,
      'shw_num_high_eff_gas_tanks' => num_high_eff_gas_tanks,
      'shw_num_reg_eff_oil_tanks' => num_reg_oil_tanks,
      'shw_num_high_eff_oil_tanks' => num_high_eff_oil_tanks,
      'shw_num_of_costed_tanks' => numTanks,
      'shw_flues' => flueCost.round(2),
      'shw_utilties' => utilCost.round(2),
      'shw_pumps' => pumpCost.round(2),
      'shw_num_of_pumps' => plant_loop_info[:shwpumps].size,
      'shw_piping' => pipingCost.round(2),
      'shw_total' => totalCost.round(2)
  }
  puts "\nHVAC SHW costing data successfully generated. Total shw costs: $#{totalCost.round(2)}"

  return totalCost
end

#shw_distribution_costing(model:, prototype_creator:) ⇒ Object



404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
# File 'lib/openstudio-standards/btap/costing/shw_costing.rb', line 404

def shw_distribution_costing(model:, prototype_creator:)
  total_shw_dist_cost = 0
  roof_cent = prototype_creator.find_highest_roof_centre(model)
  mech_room, cond_spaces = prototype_creator.find_mech_room(model)
  min_space = get_lowest_space(spaces: cond_spaces)
  mech_sizing_info = read_mech_sizing()
  shw_sp_types = get_mech_table(mech_size_info: mech_sizing_info, table_name: 'shw_space_types')
  excl_sp_types = get_mech_table(mech_size_info: mech_sizing_info, table_name: 'exclusive_shw_space_types')
  shw_main_cost = cost_shw_main(mech_room: mech_room, roof_cent: roof_cent, min_space: min_space)
  total_shw_dist_cost += shw_main_cost[:cost]
  #determine if space is wet:  prototype_creator.is_an_necb_wet_space?(space)
  #Sort spaces by floor and conditioned spaces
  space_mod = OpenstudioStandards::Space
  model.getBuildingStorys.sort.each do |build_story|
    public_wash = false
    other_public_wash = false
    build_story.spaces.sort.each do |space|
      next unless (space_mod.space_heated?(space) || space_mod.space_cooled?(space)) && !space_mod.space_plenum?(space)
      sp_type_name = space.spaceType.get.nameString
      shw_neccesary = shw_sp_types.select {|table_sp_type|
        !/#{table_sp_type.upcase}/.match(sp_type_name.upcase).nil?
      }
      if shw_neccesary.empty?
        public_wash = true
      else
        shw_dist_cost = get_shw_dist_cost(space: space, roof_cent: roof_cent)
        total_shw_dist_cost += shw_dist_cost[:cost]
        public_shw = excl_sp_types.select {|ex_table_sp_type|
          !/#{ex_table_sp_type.upcase}/.match(sp_type_name.upcase).nil?
        }
        other_public_wash = true
      end
    end
    if public_wash == true && other_public_wash == false
      #Cost two shw piping to two washrooms in the center of the story.  Assume each has 20 feet of supply and return
      #shw piping to the story center (10 feet supply, 10 feet return).
      dist_ft = 40
      shw_dist_search = []
      shw_dist_search << {
          mat: 'CopperPipe',
          unit: 'L.F.',
          size: 0.75,
          mult: dist_ft
      }
      washroom_shw_cost = get_comp_cost(cost_info: shw_dist_search)
      total_shw_dist_cost += washroom_shw_cost
    end
  end
  return total_shw_dist_cost
end

#sort_tzs_by_air_system(hvac_floor:) ⇒ Object



2250
2251
2252
2253
2254
2255
2256
2257
2258
2259
2260
2261
2262
2263
2264
2265
2266
2267
2268
2269
2270
2271
2272
2273
2274
2275
2276
2277
2278
2279
2280
2281
2282
2283
2284
2285
# File 'lib/openstudio-standards/btap/costing/ventilation_costing.rb', line 2250

def sort_tzs_by_air_system(hvac_floor:)
  floor_systems = []
  hvac_floor[:floor_tz].each do |floor_tz|
    air_sys = floor_tz[:sys_info]
    next if floor_tz[:hrv_info][:hrv_present] == false
    floor_tz[:tz_floor_ret_air_m3ps] >= floor_tz[:tz_floor_outdoor_air_m3ps] ? hrv_ret_air_m3ps = 0 : hrv_ret_air_m3ps = (floor_tz[:tz_floor_outdoor_air_m3ps] - floor_tz[:tz_floor_ret_air_m3ps]).abs
    if floor_systems.empty?
      floor_systems << {
          air_sys: air_sys,
          sys_hrv_flow_m3ps: hrv_ret_air_m3ps,
          num_tz: 1,
          tz_mult: floor_tz[:tz_mult],
          hrv_info: floor_tz[:hrv_info],
          floor_tz: [floor_tz]
      }
    else
      current_sys = floor_systems.select {|floor_sys| floor_sys[:air_sys] == air_sys}
      if current_sys.empty?
        floor_systems << {
            air_sys: air_sys,
            sys_hrv_flow_m3ps: hrv_ret_air_m3ps,
            num_tz: 1,
            tz_mult: floor_tz[:tz_mult],
            hrv_info: floor_tz[:hrv_info],
            floor_tz: [floor_tz]
        }
      else
        current_sys[0][:sys_hrv_flow_m3ps] += hrv_ret_air_m3ps
        current_sys[0][:num_tz] += 1
        current_sys[0][:tz_mult] += floor_tz[:tz_mult]
        current_sys[0][:floor_tz] << floor_tz
      end
    end
  end
  return floor_systems
end

#tz_vent_dist_cost(hvac_floors:, mech_sizing_info:) ⇒ Object



1963
1964
1965
1966
1967
1968
1969
1970
1971
1972
1973
1974
1975
1976
1977
1978
1979
1980
1981
1982
1983
1984
1985
1986
1987
1988
1989
1990
1991
1992
1993
1994
1995
1996
1997
1998
1999
2000
2001
2002
2003
2004
2005
2006
2007
2008
2009
2010
2011
2012
2013
2014
2015
2016
2017
2018
2019
2020
2021
2022
2023
2024
2025
2026
2027
2028
2029
2030
2031
2032
2033
2034
2035
2036
2037
2038
2039
2040
2041
2042
2043
2044
2045
2046
2047
2048
2049
2050
2051
2052
2053
2054
2055
2056
2057
2058
2059
2060
2061
2062
2063
2064
2065
2066
2067
2068
2069
# File 'lib/openstudio-standards/btap/costing/ventilation_costing.rb', line 1963

def tz_vent_dist_cost(hvac_floors:, mech_sizing_info:)
  dist_reporting = []
  vent_dist_cost = 0
  mech_table = get_mech_table(mech_size_info: mech_sizing_info, table_name: 'tz_dist_info')
  flexduct_table = get_mech_table(mech_size_info: mech_sizing_info, table_name: 'flex_duct')
  hvac_floors.each_with_index do |hvac_floor, index|
    dist_reporting << {
        Story: hvac_floor[:story_name],
        thermal_zones: []
    }
    hvac_floor[:floor_tz].each do |floor_tz|
      floor_vent_cost = 0
      airflow_m3ps = []
      airflow_m3ps << floor_tz[:tz_floor_supp_air_m3ps]*floor_tz[:floor_area_frac]
      airflow_m3ps << floor_tz[:tz_floor_ret_air_m3ps]*floor_tz[:floor_area_frac] if floor_tz[:tz_floor_ret_air_m3ps].to_f.round(6) > 0.0
      airflow_m3ps.each_with_index do |max_air_m3ps, flow_index|
        # Using max supply air flow rather than breathing zone outdoor airflow.  Keep breathing zone outdoor airflow in
        # case we change our minds.
        # breathing_zone_outdoor_airflow_vbz= model.sqlFile().get().execAndReturnFirstDouble("SELECT Value FROM TabularDataWithStrings WHERE ReportName='Standard62.1Summary' AND ReportForString='Entire Facility' AND TableName='Zone Ventilation Parameters' AND ColumnName='Breathing Zone Outdoor Airflow - Vbz' AND Units='m3/s' AND RowName='#{tz.nameString.to_s.upcase}' ")
        # bz_outdoor_airflow_m3_s = breathing_zone_outdoor_airflow_vbz.get unless breathing_zone_outdoor_airflow_vbz.empty?
        tz_dist_sz = mech_table.select {|size_range|
          max_air_m3ps > size_range['airflow_m3ps'][0] && max_air_m3ps <= size_range['airflow_m3ps'][1]
        }
        if tz_dist_sz.empty?
          size_range = mech_table[mech_table.size - 1]
          diffusers = (max_air_m3ps/size_range["diffusers"]).round(0)
          tz_dist_sz << {
              "airflow_m3ps" => size_range['airflow_m3ps'],
              "diffusers" => diffusers,
              "ducting_lbs" => (diffusers*size_range["ducting_lbs"]).round(0),
              "duct_insulation_ft2" => (diffusers*size_range["duct_insulation_ft2"]).round(0),
              "flex_duct_ft" => (diffusers*size_range["flex_duct_ft"]).round(0)
          }
        elsif tz_dist_sz[0] == mech_table[mech_table.size - 1]
          diffusers = (max_air_m3ps/tz_dist_sz[0]['diffusers']).round(0)
          tz_dist_sz[0] = {
              "airflow_m3ps" => tz_dist_sz[0]['airflow_m3ps'],
              "diffusers" => diffusers,
              "ducting_lbs" => (diffusers*tz_dist_sz[0]['ducting_lbs']).round(0),
              "duct_insulation_ft2" => (diffusers*tz_dist_sz[0]['duct_insulation_ft2']).round(0),
              "flex_duct_ft" => (diffusers*tz_dist_sz[0]['flex_duct_ft']).round(0)
          }
        end
        duct_cost_search = []
        duct_cost_search << {
            mat: 'Diffusers',
            unit: 'each',
            size: 36,
            mult: tz_dist_sz[0]['diffusers']
        }
        if tz_dist_sz[0]["ducting_lbs"] < 200
          duct_cost_search << {
              mat: 'Ductwork',
              unit: 'lb.',
              size: 199,
              mult: tz_dist_sz[0]['ducting_lbs']
          }
        else
          duct_cost_search << {
              mat: 'Ductwork',
              unit: 'lb.',
              size: 200,
              mult: tz_dist_sz[0]['ducting_lbs']
          }
        end
        duct_cost_search << {
            mat: 'DuctInsulation',
            unit: 'ft2',
            size: 1.5,
            mult: tz_dist_sz[0]['duct_insulation_ft2']
        }
        floor_vent_cost = get_comp_cost(cost_info: duct_cost_search)*floor_tz[:tz_mult]
        flex_duct_sz = flexduct_table.select {|flex_duct|
          max_air_m3ps > flex_duct['airflow_m3ps'][0] && max_air_m3ps <= flex_duct['airflow_m3ps'][1]
        }
        flex_duct_sz << flexduct_table[flexduct_table.size-1] if flex_duct_sz.empty?
        duct_cost_search = {
            mat: 'Ductwork-M',
            unit: 'L.F.',
            size: flex_duct_sz[0]['diameter_in'],
            mult: tz_dist_sz[0]['flex_duct_ft']
        }
        duct_cost, comp_info = get_duct_cost(cost_info: duct_cost_search)
        floor_vent_cost += duct_cost*floor_tz[:tz_mult]
        vent_dist_cost += floor_vent_cost
        if flow_index == 0
          flow_dir = 'Supply'
        else
          flow_dir = 'Return'
        end
        dist_reporting[index][:thermal_zones] << {
            ThermalZone: floor_tz[:tz].nameString,
            ducting_direction: flow_dir,
            tz_mult: floor_tz[:tz_mult],
            airflow_m3ps: max_air_m3ps.round(3),
            num_diff: tz_dist_sz[0]['diffusers'],
            ducting_lbs: tz_dist_sz[0]['ducting_lbs'],
            duct_insulation_ft2: tz_dist_sz[0]['duct_insulation_ft2'],
            flex_duct_sz_in: flex_duct_sz[0]['diameter_in'],
            flex_duct_length_ft: tz_dist_sz[0]['flex_duct_ft'],
            cost: floor_vent_cost.round(2)
        }
      end
    end
  end
  return vent_dist_cost, dist_reporting
end

#validate_databaseObject



63
64
65
# File 'lib/openstudio-standards/btap/costing/btap_costing.rb', line 63

def validate_database()
  @costing_database.validate_database
end

#vav_cost(terminal:, tz_centroids:, tz:, roof_cent:, mech_sizing_info:, air_flow_m3_per_s:, box_name:, vent_tags: [], report_mult: 1.0) ⇒ Object



1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
1165
1166
1167
1168
1169
1170
# File 'lib/openstudio-standards/btap/costing/ventilation_costing.rb', line 1147

def vav_cost(terminal:, tz_centroids:, tz:, roof_cent:, mech_sizing_info:, air_flow_m3_per_s:, box_name:, vent_tags: [], report_mult: 1.0)
  cost = 0
  pipe_length_m = 0
  elect_length_m = 0
  num_coils = 0
  tz_centroids.sort.each do |tz_cent|
    vav_tags = vent_tags.clone
    vav_tags << tz_cent[:story_name] unless vav_tags.empty?
    num_coils += 1
    story_floor_area = 0
    tz_cent[:spaces].each { |space| story_floor_area += space.floorArea.to_f }
    floor_area_frac = (story_floor_area/tz.floorArea).round(2)
    cost += get_mech_costing(mech_name: box_name, size: floor_area_frac*(OpenStudio.convert(air_flow_m3_per_s, 'm^3/s', 'cfm').get), terminal: terminal, vent_tags: vav_tags, report_mult: report_mult)
    ut_dist = (tz_cent[:centroid][0].to_f - roof_cent[:roof_centroid][0].to_f).abs + (tz_cent[:centroid][1].to_f - roof_cent[:roof_centroid][1].to_f).abs
    if /Htg/.match(box_name)
      pipe_length_m += ut_dist
      cost += piping_cost(pipe_dist_m: ut_dist, mech_sizing_info: mech_sizing_info, air_m3_per_s: floor_area_frac*air_flow_m3_per_s, vent_tags: vav_tags, report_mult: report_mult)
    end
    elect_length_m += ut_dist
    cost += vent_box_elec_cost(cond_dist_m: ut_dist, vent_tags: vav_tags, report_mult: report_mult)
  end
  box_info = {size_kw: 0.0, air_flow_m3_per_s: air_flow_m3_per_s.round(3), pipe_dist_m: pipe_length_m.round(1), elect_dist_m: elect_length_m.round(1), num_units: num_coils}
  return cost, box_info
end

#vent_assembly_cost(ids:, id_quants:, overall_mult: 1.0, vent_tags: [], report_mult: 1.0) ⇒ Object

This method tokes in: ids: The list of material ids to look for in the ‘material_id’ column of the materials_hvac sheet. The number of ids should match the number of id_quants (this is checked earlier). id_quants: The number of the piece of equipment defined by the ids above required. Like ids this should be an array taken from the ‘id_layers_quantity_multipliers’ column of the ‘hvac_vent_ahu’ sheet for the air handler that matches the required criteria. The number of ids should match the number of id_quants (this is checked earlier). overall_mult: An multiplier to apply to all ids and id_quants (I’m not sure if this is used anymore). This method cycles through each of the ids and searches for it in the ‘material_id’ column of the ‘material_hvac’ sheet in the costing spreadsheet. The equipment information found in the ‘materials_hvac’ is then costed. The cost is then multiplied by the associated id_quants. For example, if the ids contains 5 elements the method searches for each one. In our example, when we get ot the 4th element of the ids we multiply its associated cost by the 4th element of the id_quants array. The total cost is then summed and multiplied by the ‘overall_mult’ and returned.



464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
# File 'lib/openstudio-standards/btap/costing/ventilation_costing.rb', line 464

def vent_assembly_cost(ids:, id_quants:, overall_mult: 1.0, vent_tags: [], report_mult: 1.0)
  assembly_tags = vent_tags.clone
  total_cost = 0
  # Cycle through each of the ids.  The index is used to select the correct element of the id_quants array.
  ids.each_with_index do |id, index|
    # Get the equipment information from the costing spreadsheet's 'material_hvac' sheet whose 'material_id' matches
    # the id.
    mat_cost_info = @costing_database['raw']['materials_hvac'].select {|data|
      data['material_id'].to_f.round(0) == id.to_f.round(0)
    }.first
    # If it cannot find it there is an issue with either the 'materials_hvac' sheet or the 'hvac_vent_ahu' sheet which
    # the user has to deal with.
    if mat_cost_info.nil?
      raise "Error: no assembly information available for material id #{id}!"
    end
    # Get the cost for the piece of equipment, multiply it by the associated id_quants element and add to the total
    total_cost += get_vent_mat_cost(mat_cost_info: mat_cost_info, report_mult: (overall_mult*id_quants[index].to_f*report_mult), vent_tags: assembly_tags)*id_quants[index].to_f
  end
  # multiply the total by the overal_mult (which is probably always 1.0 now but I'm not sure) and return the cost.
  return (total_cost*overall_mult)
end

#vent_box_elec_cost(cond_dist_m:, vent_tags: [], report_mult: 1.0) ⇒ Object



1250
1251
1252
1253
1254
1255
1256
1257
1258
1259
1260
1261
1262
1263
1264
1265
1266
1267
1268
1269
1270
1271
1272
1273
1274
1275
1276
1277
1278
1279
1280
# File 'lib/openstudio-standards/btap/costing/ventilation_costing.rb', line 1250

def vent_box_elec_cost(cond_dist_m:, vent_tags: [], report_mult: 1.0)
  elec_tags = vent_tags.clone
  elec_tags << "electrical" unless elec_tags.empty?
  cond_dist = OpenStudio.convert(cond_dist_m, 'm', 'ft').get
  elec_cost_search = []
  elec_cost_search << {
      mat: 'Wiring',
      unit: 'CLF',
      size: 14,
      mult: cond_dist/100
  }
  elec_cost_search << {
      mat: 'Conduit',
      unit: 'L.F.',
      size: 0,
      mult: cond_dist
  }
  elec_cost_search << {
      mat: 'Box',
      unit: 'none',
      size: 4,
      mult: 1
  }
  elec_cost_search << {
      mat: 'Box',
      unit: 'none',
      size: 1,
      mult: 1
  }
  return get_comp_cost(cost_info: elec_cost_search, vent_tags: elec_tags, report_mult: report_mult)
end

#vent_trunk_duct_cost(tot_air_m3pers:, min_space:, roof_cent:, mech_sizing_info:, sys_1_4:) ⇒ Object



1684
1685
1686
1687
1688
1689
1690
1691
1692
1693
1694
1695
1696
1697
1698
1699
1700
1701
1702
1703
1704
1705
1706
1707
1708
1709
1710
1711
1712
1713
1714
1715
1716
1717
1718
# File 'lib/openstudio-standards/btap/costing/ventilation_costing.rb', line 1684

def vent_trunk_duct_cost(tot_air_m3pers:, min_space:, roof_cent:, mech_sizing_info:, sys_1_4:)
  sys_1_4 ? overall_mult = 1 : overall_mult = 2
  duct_cost_search = []
  mech_table = get_mech_table(mech_size_info: mech_sizing_info, table_name: 'trunk')
  max_trunk_line = mech_table.max_by {|entry| entry['max_flow_range_m3pers'][0]}
  tot_air_m3pers = max_trunk_line['max_flow_range_m3pers'][0].to_f.round(2) if tot_air_m3pers.round(2) > max_trunk_line['max_flow_range_m3pers'][1].to_f.round(2)
  trunk_sz_info = mech_table.select {|trunk_choice|
    trunk_choice['max_flow_range_m3pers'][0].to_f.round(2) < tot_air_m3pers.round(2) and
        trunk_choice['max_flow_range_m3pers'][1].to_f.round(2) >= tot_air_m3pers.round(2)
  }.first
  duct_dia = trunk_sz_info['duct_dia_inch']
  duct_length_m = (roof_cent[:roof_centroid][2].to_f - min_space[:roof_cent][2].to_f).abs
  duct_length = (OpenStudio.convert(duct_length_m, 'm', 'ft').get)
  duct_cost_search << {
      mat: 'Ductwork-S',
      unit: 'L.F.',
      size: duct_dia,
      mult: duct_length*overall_mult
  }
  duct_area = (duct_dia/12)*Math::PI*duct_length*overall_mult
  duct_cost_search << {
      mat: 'Ductinsulation',
      unit: 'ft2',
      size: 1.5,
      mult: duct_area
  }
  duct_cost = get_comp_cost(cost_info: duct_cost_search)
  trunk_duct_info = {
      DuctSize_in: duct_dia.round(1),
      DuctLength_m: duct_length_m.round(1),
      NumberRuns: overall_mult,
      DuctCost: duct_cost.round(2)
  }
  return duct_cost, trunk_duct_info
end

#ventilation_costing(model, prototype_creator, template_type, mech_room, cond_spaces) ⇒ Object



2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# File 'lib/openstudio-standards/btap/costing/ventilation_costing.rb', line 2

def ventilation_costing(model, prototype_creator, template_type, mech_room, cond_spaces)
  # Set up reporting hash
  @costing_report['ventilation'] = {system_1: [], system_2: [], system_3: [], system_4: [], system_5: [], system_6: [], system_7: [], mech_to_roof: [], trunk_duct: [], floor_trunk_ducts: [], tz_distribution: [], hrv_return_ducting: [], natural_ventilation: [], demand_controlled_ventilation: []}
  # Get mechanical sizing for costing information from mech_sizing.json
  mech_sizing_info = read_mech_sizing()
  # Find the mechanical room in the model and conditioned spaces - moved to btap_costing.rb
  # mech_room, cond_spaces = prototype_creator.find_mech_room(model)
  # Find the center of the highest roof in the model (this will be surrounded by roof top mechancial equipment and is where utility lines will be sent)
  roof_cent = prototype_creator.find_highest_roof_centre(model)
  # Find the lowest space in the building (trunk duct runs from here to the highest space).
  min_space = get_lowest_space(spaces: cond_spaces)
  vent_cost = 0
  # Start ventilation costing
  vent_cost += ahu_costing(model: model, prototype_creator: prototype_creator, template_type: template_type, mech_room: mech_room, roof_cent: roof_cent, mech_sizing_info: mech_sizing_info, min_space: min_space)
  #  natural ventilation costing
  nv_total_cost = cost_audit_nv(model: model, prototype_creator: prototype_creator)
  # demand-controlled ventilation costing
  dcv_cost_total = cost_audit_dcv(model: model, prototype_creator: prototype_creator)
  # total ventilation cost
  vent_cost += nv_total_cost + dcv_cost_total
  return vent_cost
end

#vsd_chiller_cost(primaryCap:) ⇒ Object

This method is for the calculation of VSD chiller cost



2552
2553
2554
2555
2556
2557
2558
2559
2560
2561
2562
2563
2564
2565
2566
2567
2568
2569
2570
2571
2572
2573
2574
2575
2576
2577
2578
2579
2580
2581
2582
2583
# File 'lib/openstudio-standards/btap/costing/heating_cooling_costing.rb', line 2552

def vsd_chiller_cost(primaryCap:)
  # Gather a list of VSD chillers that exist in the costing spreadsheet
  vsd_chiller_sizes = []
  vsd_chiller_options = @costing_database['raw']['materials_hvac'].select {|data|
    data['Material'].to_s.upcase == 'ChillerElectricEIR_VSDCentrifugalWaterChiller'.upcase
  }
  vsd_chiller_options[0..-1].each do |a,b|
    a.each do |key,value|
      vsd_chiller_sizes << value.to_f if key=='Size'
    end
  end

  # Look for a VSD in the list of VSDs that has the closest size, and calculate its cost
  vsd_chiller_closet_to_current_kw = vsd_chiller_sizes.sort_by { |item| (primaryCap-item).abs }.first(1)
  quantity_chiller_electric_eir = 1.0
  search_chiller_electric_eir = {
      row_id_1: 'ChillerElectricEIR_VSDCentrifugalWaterChiller',
      row_id_2: vsd_chiller_closet_to_current_kw[0].to_s
  }
  sheet_name = 'materials_hvac'
  column_1 = 'Material'
  column_2 = 'Size'
  tags = ['heating_and_cooling','plant_equipment','chiller']
  thisChillerCost = assembly_cost(cost_info:search_chiller_electric_eir,
                                  sheet_name:sheet_name,
                                  column_1:column_1,
                                  column_2:column_2,
                                  quantity:quantity_chiller_electric_eir,
                                  tags: tags)
  # puts "thisVSDChillerCost is #{thisChillerCost}"
  return thisChillerCost
end

#zonalsys_costing(model, prototype_creator, mech_room, cond_spaces) ⇒ Object


This function gets all costs associated zonal heating and cooling systems (i.e., zonal units, pumps, flues & utility costs)




1216
1217
1218
1219
1220
1221
1222
1223
1224
1225
1226
1227
1228
1229
1230
1231
1232
1233
1234
1235
1236
1237
1238
1239
1240
1241
1242
1243
1244
1245
1246
1247
1248
1249
1250
1251
1252
1253
1254
1255
1256
1257
1258
1259
1260
1261
1262
1263
1264
1265
1266
1267
1268
1269
1270
1271
1272
1273
1274
1275
1276
1277
1278
1279
1280
1281
1282
1283
1284
1285
1286
1287
1288
1289
1290
1291
1292
1293
1294
1295
1296
1297
1298
1299
1300
1301
1302
1303
1304
1305
1306
1307
1308
1309
1310
1311
1312
1313
1314
1315
1316
1317
1318
1319
1320
1321
1322
1323
1324
1325
1326
1327
1328
1329
1330
1331
1332
1333
1334
1335
1336
1337
1338
1339
1340
1341
1342
1343
1344
1345
1346
1347
1348
1349
1350
1351
1352
1353
1354
1355
1356
1357
1358
1359
1360
1361
1362
1363
1364
1365
1366
1367
1368
1369
1370
1371
1372
1373
1374
1375
1376
1377
1378
1379
1380
1381
1382
1383
1384
1385
1386
1387
1388
1389
1390
1391
1392
1393
1394
1395
1396
1397
1398
1399
1400
1401
1402
1403
1404
1405
1406
1407
1408
1409
1410
1411
1412
1413
1414
1415
1416
1417
1418
1419
1420
1421
1422
1423
1424
1425
1426
1427
1428
1429
1430
1431
1432
1433
1434
1435
1436
1437
1438
1439
1440
1441
1442
1443
1444
1445
1446
1447
1448
1449
1450
1451
1452
1453
1454
1455
1456
1457
1458
1459
1460
1461
1462
1463
1464
1465
1466
1467
1468
1469
1470
1471
1472
1473
1474
1475
1476
1477
1478
1479
1480
1481
1482
1483
1484
1485
1486
1487
1488
1489
1490
1491
1492
1493
1494
1495
1496
1497
1498
1499
1500
1501
1502
1503
1504
1505
1506
1507
1508
1509
1510
1511
1512
1513
1514
1515
1516
1517
1518
1519
1520
1521
1522
1523
1524
1525
1526
1527
1528
1529
1530
1531
1532
1533
1534
1535
1536
1537
1538
1539
1540
1541
1542
1543
1544
1545
1546
1547
1548
1549
1550
1551
1552
1553
1554
1555
1556
1557
1558
1559
1560
1561
1562
1563
1564
1565
1566
1567
1568
1569
1570
1571
1572
1573
1574
1575
1576
1577
1578
1579
1580
1581
1582
1583
1584
1585
1586
1587
1588
1589
1590
1591
1592
1593
1594
1595
1596
1597
1598
1599
1600
1601
1602
1603
1604
1605
1606
1607
1608
1609
1610
1611
1612
1613
1614
1615
1616
1617
1618
1619
1620
1621
1622
1623
1624
1625
1626
1627
1628
1629
1630
1631
1632
1633
1634
1635
1636
1637
1638
1639
1640
1641
1642
1643
1644
1645
1646
1647
1648
1649
1650
1651
1652
1653
1654
1655
1656
1657
1658
1659
1660
1661
1662
1663
1664
1665
1666
1667
1668
1669
1670
1671
1672
1673
1674
1675
1676
1677
1678
1679
1680
1681
1682
1683
1684
1685
1686
1687
1688
1689
1690
1691
1692
1693
1694
1695
1696
1697
1698
1699
1700
1701
1702
1703
1704
1705
1706
1707
1708
1709
1710
1711
# File 'lib/openstudio-standards/btap/costing/heating_cooling_costing.rb', line 1216

def zonalsys_costing(model, prototype_creator, mech_room, cond_spaces)

  totalCost = 0.0

  # Get regional cost factors for this province and city
  materials_hvac = @costing_database["raw"]["materials_hvac"]
  hvac_material = materials_hvac.select {|data|
    data['Material'].to_s == "GasBoilers"}.first  # Get any row from spreadsheet in case of region error
  regional_material, regional_installation =
      get_regional_cost_factors(@costing_report['province_state'], @costing_report['city'], hvac_material)

  # Get regional electric cost factors for this province and city
  hvac_material = materials_hvac.select {|data|
    data['Material'].to_s.upcase == "BOX" && data['Size'].to_i == 1}.first
  reg_mat_elec, reg_lab_elec =
    get_regional_cost_factors(@costing_report['province_state'], @costing_report['city'], hvac_material)

  # Store some geometry data for use below...
  util_dist, ht_roof, nom_flr_hght, horz_dist, numAGFlrs, mechRmInBsmt = getGeometryData(model, prototype_creator)

  template_type = prototype_creator.template

  zone_loop_info = {}
  zone_loop_info[:zonesys] = []
  numZones = 0; floorNumber = 0
  vrfSystemFloors = {
    maxCeil: -9999999999999,
    lowCeil: 9999999999999,
    vrfFloors: []
  }

  model.getThermalZones.sort.each do |zone|
    numZones += 1
    zone.equipment.each do |equipment|
      obj_type = equipment.iddObjectType.valueName.to_s
      if equipment.to_ZoneHVACComponent.is_initialized
        # This is a zonal HVAC component
        zone_info = {}
        zone_loop_info[:zonesys] << zone_info

        # Get floor number from zone name string using regexp (Flr-N, where N is the storey number)
        zone_info[:zonename] = zone.name.get
        zone_info[:zonename].scan(/.*Flr-(\d+).*/) {|num| zone_info[:flrnum] = num[0].to_i}

        unless zone.isConditioned.empty?
          zone_info[:is_conditioned] = zone.isConditioned.get
        else
          zone_info[:is_conditioned] = 'N/A'
          puts "Warning: zone.isConditioned is empty for #{zone.name.get}!"
        end

        zone_info[:multiplier] = zone.multiplier

        # Get the zone ceiling height value from the sql file...
        query = "SELECT CeilingHeight FROM Zones WHERE ZoneName='#{zone_info[:zonename].upcase}'"
        ceilHeight = model.sqlFile().get().execAndReturnFirstDouble(query)
        zone_info[:ceilingheight] = OpenStudio.convert(ceilHeight.to_f,"m","ft").get  # feet

        zone_info[:heatcost] = 0.0
        zone_info[:coolcost] = 0.0
        zone_info[:heatcoolcost] = 0.0
        zone_info[:pipingcost] = 0.0
        zone_info[:wiringcost] = 0.0
        zone_info[:multiplier] = zone.multiplier
        zone_info[:sysname] = equipment.name.get

        # Get the heat capacity values from the sql file - ZoneSizes table...
        query = "SELECT UserDesLoad FROM ZoneSizes WHERE ZoneName='#{zone_info[:zonename].upcase}' AND LoadType='Heating'"
        heatCapVal = model.sqlFile().get().execAndReturnFirstDouble(query)
        zone_info[:heatcapacity] = heatCapVal.to_f / 1000.0 # Watts -> kW

        component = equipment.to_ZoneHVACComponent.get
        if component.to_ZoneHVACPackagedTerminalAirConditioner.is_initialized
          heating_coil_name = component.to_ZoneHVACPackagedTerminalAirConditioner.get.heatingCoil.name.to_s
          query = "SELECT Value FROM TabularDataWithStrings WHERE ReportName='CoilSizingDetails' AND RowName='#{heating_coil_name.upcase}' AND ColumnName='Coil Final Gross Total Capacity'"
          zone_info[:heatcapacity] = model.sqlFile.get.execAndReturnFirstDouble(query).to_f/1000.0
          cooling_coil_name = component.to_ZoneHVACPackagedTerminalAirConditioner.get.coolingCoil.name.to_s
          query = "SELECT Value FROM TabularDataWithStrings WHERE ReportName='CoilSizingDetails' AND RowName='#{cooling_coil_name.upcase}' AND ColumnName='Coil Final Gross Total Capacity'"
          zone_info[:coolcapacity] = model.sqlFile.get.execAndReturnFirstDouble(query).to_f/1000.0
        elsif component.to_ZoneHVACFourPipeFanCoil.is_initialized # 2PFC & 4PFC
          heating_coil_name = component.to_ZoneHVACFourPipeFanCoil.get.heatingCoil.name.to_s
          query = "SELECT Value FROM TabularDataWithStrings WHERE ReportName='CoilSizingDetails' AND RowName='#{heating_coil_name.upcase}' AND ColumnName='Coil Final Gross Total Capacity'"
          zone_info[:heatcapacity] = model.sqlFile.get.execAndReturnFirstDouble(query).to_f/1000.0
          cooling_coil_name = component.to_ZoneHVACFourPipeFanCoil.get.coolingCoil.name.to_s
          query = "SELECT Value FROM TabularDataWithStrings WHERE ReportName='CoilSizingDetails' AND RowName='#{cooling_coil_name.upcase}' AND ColumnName='Coil Final Gross Total Capacity'"
          zone_info[:coolcapacity] = model.sqlFile.get.execAndReturnFirstDouble(query).to_f/1000.0
        elsif component.to_ZoneHVACPackagedTerminalHeatPump.is_initialized
          heating_coil_name = component.to_ZoneHVACPackagedTerminalHeatPump.get.heatingCoil.name.to_s
          query = "SELECT Value FROM TabularDataWithStrings WHERE ReportName='CoilSizingDetails' AND RowName='#{heating_coil_name.upcase}' AND ColumnName='Coil Final Gross Total Capacity'"
          zone_info[:heatcapacity] = model.sqlFile.get.execAndReturnFirstDouble(query).to_f/1000.0
          cooling_coil_name = component.to_ZoneHVACPackagedTerminalHeatPump.get.coolingCoil.name.to_s
          query = "SELECT Value FROM TabularDataWithStrings WHERE ReportName='CoilSizingDetails' AND RowName='#{cooling_coil_name.upcase}' AND ColumnName='Coil Final Gross Total Capacity'"
          zone_info[:coolcapacity] = model.sqlFile.get.execAndReturnFirstDouble(query).to_f/1000.0
        elsif component.to_ZoneHVACTerminalUnitVariableRefrigerantFlow.is_initialized
          # Use separate method to get zonal VRF system info
          zonalSys = component.to_ZoneHVACTerminalUnitVariableRefrigerantFlow.get
          vrfSystemFloors = getZonalVRFInfo(zone: zone, model: model, prototype_creator: prototype_creator, zonalSys: zonalSys, vrfSystemFloors: vrfSystemFloors, regMat: regional_material, regLab: regional_installation, numZones: numZones)
          # When done, go to the next piece of equipment.  Will do VRF costing once all thermal zones are
          # investigated.
          next
        else
          cooling_coil_name = 'nil'
        end

        unless (obj_type.to_s == 'OS_ZoneHVAC_FourPipeFanCoil') || (obj_type.to_s == 'OS_ZoneHVAC_PackagedTerminalHeatPump') || (obj_type.to_s == 'OS_ZoneHVAC_PackagedTerminalAirConditioner')
          # Get the cooling total capacity (sen+lat) value from the sql file - ComponentSizes table
          query = "SELECT Value FROM ComponentSizes WHERE CompName='#{cooling_coil_name.upcase}' AND Units='W'"
          coolCapVal = model.sqlFile().get().execAndReturnFirstDouble(query)
          zone_info[:coolcapacity] = coolCapVal.to_f / 1000.0 # Watts -> kW
        end

        if (zone_info[:sysname] =~ /Baseboard Convective Water/i) or (zone_info[:sysname] =~ /BaseboardConvectiveWater/i)
          zone_info[:systype] = 'HW'
          # HW convector length based on 0.425 kW/foot
          if zone_info[:heatcapacity] > 0
            heatCapacity = zone_info[:heatcapacity] / zone.multiplier
            convLength = (heatCapacity / 0.425).round(0)
            # HW convector 1" copper core pipe cost
            matCost, labCost = getHVACCost(zone_info[:sysname], 'ConvectCopper', 1.25, true)
            convPipeCost = (matCost * regional_material / 100.0 + labCost * regional_installation / 100.0) * convLength
            # For each convector there will be a shut-off valve, 2 Tee connections and 2 elbows to
            # isolate the convector from the hot water loop distribution for servicing and balancing.
            # Hot water convectors are manufactured in maximum 8 ft lengths, therefore the number of
            # convectors per thermal zone is (rounded up to nearest integer):
            ratio = (convLength.to_f / 8.0).to_f
            numConvectors = (ratio - ratio.to_i) > 0.10 ? (ratio + 0.5).round(0) : ratio
            # Cost of valves:
            matCost, labCost = getHVACCost('1.25 inch gate valve', 'ValvesGate', 1.25, true)
            convValvesCost = (matCost * regional_material / 100.0 + labCost * regional_installation / 100.0) * numConvectors
            # Cost of tees:
            matCost, labCost = getHVACCost('1.25 inch copper tees', 'CopperPipeTee', 1.25, true)
            convTeesCost = (matCost * regional_material / 100.0 + labCost * regional_installation / 100.0) * 2 * numConvectors
            # Cost of elbows:
            matCost, labCost = getHVACCost('1.25 inch copper elbows', 'CopperPipeElbow', 1.25, true)
            convElbowsCost = (matCost * regional_material / 100.0 + labCost * regional_installation / 100.0) * 2 * numConvectors
            # Total convector cost for this zone (excluding distribution piping):
            convCost = (convPipeCost + convValvesCost + convTeesCost + convElbowsCost) * zone.multiplier
            zone_info[:heatcost] = convCost
            zone_info[:num_units] = numConvectors

            # Single pipe supply and return
            perimPipingCost = getPerimDistPipingCost(zone, nom_flr_hght, regional_material, regional_installation)
            zone_info[:pipingcost] = perimPipingCost

            totalCost += convCost + perimPipingCost
          end

        elsif (zone_info[:sysname]=~ /Baseboard Convective Electric/i) or (zone_info[:sysname]=~ /BaseboardConvectiveElectric/i)
          zone_info[:systype] = 'BB'
          # BB number based on 0.935 kW/unit
          if zone_info[:heatcapacity] > 0
            heatCapacity = zone_info[:heatcapacity] / zone.multiplier
            ratio = (heatCapacity / 0.935).to_f
            numConvectors = (ratio - ratio.to_i) > 0.10 ? (ratio + 0.5).round(0) : ratio
            # BB electric convector unit cost (Just one in sheet)
            matCost, labCost = getHVACCost(zone_info[:sysname], 'ElectricBaseboard', 'nil', true)
            elecBBCost = (matCost * regional_material / 100.0 + labCost * regional_installation / 100.0) * numConvectors
            # For each baseboard there will be an electrical junction box
            matCost, labCost = getHVACCost('Electrical Outlet Box', 'Box', 1, true)
            elecBoxCost = (matCost * reg_mat_elec / 100.0 + labCost * reg_lab_elec / 100.0) * numConvectors
            # Total electric basbeboard cost for this zone:
            elecConvCost = (elecBBCost + elecBoxCost) * zone.multiplier
            zone_info[:heatcost] = elecConvCost
            zone_info[:num_units] = numConvectors

            perimWiringCost = getPerimDistWiringCost(zone, nom_flr_hght, reg_mat_elec, reg_lab_elec)
            zone_info[:wiringcost] = perimWiringCost

            totalCost += elecConvCost + perimWiringCost
          end

        elsif (zone_info[:sysname] =~ /PTAC/i) || (obj_type.to_s == 'OS_ZoneHVAC_PackagedTerminalAirConditioner')
          zone_info[:systype] = 'PTAC'
          # Heating cost of PTAC is handled by Baseboard Convective Electric Heater entry in Equipment list!
          # Cooling cost of PTAC ...
          if zone_info[:coolcapacity] > 0
            # DX cooling unit
            coolCapacity = zone_info[:coolcapacity] / zone.multiplier
            numUnits = get_HVAC_multiplier('PTAC', coolCapacity)
            # PTAC unit cost (Note that same numUnits multiple applied within getHVACCost())
            matCost, labCost = getHVACCost(zone_info[:sysname], 'PTAC', coolCapacity, false)
            thePTACUnitCost = (matCost * regional_material / 100.0 + labCost * regional_installation / 100.0)
            # For each PTAC unit there will be an electrical junction box (wiring costed with distribution - not here)
            matCost, labCost = getHVACCost('Electrical Outlet Box', 'Box', 1, true)
            elecBoxCost = (matCost * reg_mat_elec / 100.0 + labCost * reg_lab_elec / 100.0) * numUnits
            # Total PTAC cost for this zone (excluding distribution piping):
            thePTACCost = (thePTACUnitCost + elecBoxCost) * zone.multiplier
            zone_info[:coolcost] = thePTACCost
            zone_info[:num_units] = numUnits

            perimWiringCost = getPerimDistWiringCost(zone, nom_flr_hght, reg_mat_elec, reg_lab_elec)
            zone_info[:wiringcost] = perimWiringCost

            totalCost += thePTACCost + perimWiringCost
          end

        elsif (zone_info[:sysname] =~ /PTHP/i) || (obj_type.to_s =~ /OS_ZoneHVAC_PackagedTerminalHeatPump/)
          zone_info[:systype] = 'HP'
          # Cost of PTAC based on heating capacity...
          if zone_info[:heatcapacity] > 0
            # DX heat pump unit
            capacityHPUnit = zone_info[:coolcapacity] > zone_info[:heatcapacity] ?
                               zone_info[:coolcapacity] / zone.multiplier : zone_info[:heatcapacity] / zone.multiplier
            numUnits = get_HVAC_multiplier('ashp', capacityHPUnit)
            # HP unit cost (Note that same numUnits multiple applied within getHVACCost())
            matCost, labCost = getHVACCost(zone_info[:sysname], 'ashp', capacityHPUnit, false)
            theHPUnitCost = (matCost * regional_material / 100.0 + labCost * regional_installation / 100.0)
            # For each HP unit there will be an electrical junction box (wiring costed with distribution - not here)
            matCost, labCost = getHVACCost('Electrical Outlet Box', 'Box', 1, true)
            elecBoxCost = (matCost * reg_mat_elec / 100.0 + labCost * reg_lab_elec / 100.0) * numUnits
            # Total HP cost for this zone (excluding distribution piping):
            theHPCost = (theHPUnitCost + elecBoxCost) * zone.multiplier
            zone_info[:heatcoolcost] = theHPUnitCost
            zone_info[:num_units] = numUnits

            perimWiringCost = getPerimDistWiringCost(zone, nom_flr_hght, reg_mat_elec, reg_lab_elec)
            zone_info[:wiringcost] = perimWiringCost

            totalCost += theHPCost + perimWiringCost
          end

        elsif zone_info[:sysname] =~ /2-pipe Fan Coil/i
          zone_info[:sfurnaceystype] = '2FC'
          if zone_info[:heatcapacity] > 0 || zone_info[:coolcapacity] > 0
            # Hot water heating and chilled water cooling type fan coil unit
            capacityFCUnit = zone_info[:coolcapacity] > zone_info[:heatcapacity] ?
                             zone_info[:coolcapacity] / zone.multiplier : zone_info[:heatcapacity] / zone.multiplier
            numFCUnits = get_HVAC_multiplier('FanCoilHtgClgVent', capacityFCUnit)
            # 2PFC unit cost (Note that same numFCUnits multiple applied within getHVACCost())
            matCost, labCost = getHVACCost(zone_info[:sysname], 'FanCoilHtgClgVent', capacityFCUnit, false)
            fcUnitCost = (matCost * regional_material / 100.0 + labCost * regional_installation / 100.0)
            # For each 2PFC unit there will be a shut-off valve, 2 Tee connections and 2 elbows to
            # isolate the convector from the hot water loop distribution for servicing and balancing.
            # Assumed unit piping is 1.25 inches in diameter.
            # Cost of valves:
            matCost, labCost = getHVACCost('1.25 inch gate valve', 'ValvesGate', 1.25, true)
            fcValvesCost = (matCost * regional_material / 100.0 + labCost * regional_installation / 100.0) * numFCUnits
            # Cost of tees:
            matCost, labCost = getHVACCost('1.25 inch copper tees', 'CopperPipeTee', 1.25, true)
            fcTeesCost = (matCost * regional_material / 100.0 + labCost * regional_installation / 100.0) * 2 * numFCUnits
            # Cost of elbows:
            matCost, labCost = getHVACCost('1.25 inch copper elbows', 'CopperPipeElbow', 1.25, true)
            fcElbowsCost = (matCost * regional_material / 100.0 + labCost * regional_installation / 100.0) * 2 * numFCUnits
            # For each 2PFC unit there will be an electrical junction box (wiring costed with distribution - not here)
            matCost, labCost = getHVACCost('Electrical Outlet Box', 'Box', 1, true)
            elecBoxCost = (matCost * reg_mat_elec / 100.0 + labCost * reg_lab_elec / 100.0) * numFCUnits
            # Total 2PFC cost for this zone (excluding distribution piping):
            fcCost = (fcUnitCost + fcValvesCost + fcTeesCost + fcElbowsCost + elecBoxCost) * zone.multiplier
            zone_info[:heatcoolcost] = fcCost
            zone_info[:num_units] = numFCUnits

            # Cost for one set supply/return piping
            perimPipingCost = getPerimDistPipingCost(zone, nom_flr_hght, regional_material, regional_installation)
            zone_info[:pipingcost] = perimPipingCost

            perimWiringCost = getPerimDistWiringCost(zone, nom_flr_hght, reg_mat_elec, reg_lab_elec)
            zone_info[:wiringcost] = perimWiringCost

            totalCost += fcCost + perimPipingCost + perimWiringCost
          end

        elsif (zone_info[:sysname] =~ /4-pipe Fan Coil/i) || (obj_type =~ /OS_ZoneHVAC_FourPipeFanCoil/)
          zone_info[:systype] = '4FC'
          if (zone_info[:heatcapacity] > 0) || (zone_info[:coolcapacity] > 0)
            # Hot water heating and chilled water cooling type fan coil unit
            capacityFCUnit = zone_info[:coolcapacity] > zone_info[:heatcapacity] ?
                                 zone_info[:coolcapacity] / zone.multiplier : zone_info[:heatcapacity] / zone.multiplier
            numFCUnits = get_HVAC_multiplier('FanCoilHtgClgVent', capacityFCUnit)
            # 4PFC unit cost (Note that same numFCUnits multiple applied within getHVACCost())
            matCost, labCost = getHVACCost(zone_info[:sysname], 'FanCoilHtgClgVent', capacityFCUnit, false)
            fcUnitCost = (matCost * regional_material / 100.0 + labCost * regional_installation / 100.0)
            # For each 4PFC unit there will be 2 shut-off valves, 4 Tee connections and 4 elbows to
            # isolate the convector from the hot water loop distribution for servicing and balancing.
            # Assumed unit piping is 1.25 inches in diameter.
            # Cost of valves:
            matCost, labCost = getHVACCost('1.25 inch gate valve', 'ValvesGate', 1.25, true)
            fcValvesCost = (matCost * regional_material / 100.0 + labCost * regional_installation / 100.0) * 2 * numFCUnits
            # Cost of tees:
            matCost, labCost = getHVACCost('1.25 inch copper tees', 'CopperPipeTee', 1.25, true)
            fcTeesCost = (matCost * regional_material / 100.0 + labCost * regional_installation / 100.0) * 4 * numFCUnits
            # Cost of elbows:
            matCost, labCost = getHVACCost('1.25 inch copper elbows', 'CopperPipeElbow', 1.25, true)
            fcElbowsCost = (matCost * regional_material / 100.0 + labCost * regional_installation / 100.0) * 4 * numFCUnits
            # For each 4PFC unit there will be an electrical junction box (wiring costed with distribution - not here)
            matCost, labCost = getHVACCost('Electrical Outlet Box', 'Box', 1, true)
            elecBoxCost = (matCost * reg_mat_elec / 100.0 + labCost * reg_lab_elec / 100.0) * numFCUnits
            # Total 4PFC cost for this zone (excluding distribution piping):
            fcCost = (fcUnitCost + fcValvesCost + fcTeesCost + fcElbowsCost + elecBoxCost) * zone.multiplier
            zone_info[:heatcoolcost] = fcCost
            zone_info[:num_units] = numFCUnits

            # Cost for two sets supply/return piping
            perimPipingCost = 2 * getPerimDistPipingCost(zone, nom_flr_hght, regional_material, regional_installation)
            zone_info[:pipingcost] = perimPipingCost

            perimWiringCost = getPerimDistWiringCost(zone, nom_flr_hght, reg_mat_elec, reg_lab_elec)
            zone_info[:wiringcost] = perimWiringCost

            totalCost += fcCost + perimPipingCost + perimWiringCost
          end

        elsif zone_info[:sysname] =~ /Unit Heater/i || zone_info[:sysname] =~ /Unitary/i
          zone_info[:systype] = 'FUR'
          # Two types of unit heaters: electric and gas
          unitHeater = component.to_ZoneHVACUnitHeater.get
          heatCoil = unitHeater.heatingCoil
          if heatCoil.to_CoilHeatingGas.is_initialized   # TODO: Need to test this!
            # The gas unit heaters are cabinet type with a burner and blower rather than the radiant type
            gasCoil = heatCoil.to_CoilHeatingGas.get
            if heatCoil.isNominalCapacityAutosized.to_bool
              zone_info[:heatcapacity] = gasCoil.autosizedNominalCapacity.to_f/1000.0
            else
              zone_info[:heatcapacity] = gasCoil.nominalCapacity.to_f/1000.0
            end
            if zone_info[:heatcapacity] > 0
              heatCapacity = zone_info[:heatcapacity] / zone.multiplier
              numUnits = get_HVAC_multiplier('gasheater', heatCapacity)
              # Unit cost (Note that same unit multiple applied within getHVACCost())
              matCost, labCost = getHVACCost(zone_info[:sysname], 'gasheater', heatCapacity, false)
              unitCost = (matCost * regional_material / 100.0 + labCost * regional_installation / 100.0)
              # For each unit heater there will be an electrical junction box (wiring costed with distribution - not here)
              matCost, labCost = getHVACCost('Electrical Outlet Box', 'Box', 1, true)
              elecBoxCost = (matCost * reg_mat_elec / 100.0 + labCost * reg_lab_elec / 100.0) * numUnits
              # It is assumed that the gas unit heater(s) are located in the centre of this zone. An 8 in exhaust duct
              # must be costed from the unit heater to the exterior via the roof. The centroid of this zone:
              if zone_info[:flrnum] > 1
                zoneCentroidToRoof_Ft = 10 + nom_flr_hght * zone_info[:flrnum]
              else
                zoneCentroidToRoof_Ft = 10
              end
              matCost, labCost = getHVACCost('Unit heater exhaust duct', 'Ductwork-S', 8, true)
              exhaustductCost = (matCost * regional_material / 100.0 + labCost * regional_installation / 100.0) * zoneCentroidToRoof_Ft
              zone_info[:heatcost] = (unitCost + elecBoxCost + exhaustductCost) * zone.multiplier
              zone_info[:num_units] = numUnits

              # Cost of gas line header for zone. Header is located in the centre of this zone's floor
              mechRmInBsmt ? numFlrs = numAGFlrs + 1 : numFlrs = numAGFlrs
              hdrGasLen = numFlrs * nom_flr_hght
              # Gas line - first one in spreadsheet
              matCost, labCost = getHVACCost('Central header gas line', 'GasLine', '')
              hdrGasLineCost = hdrGasLen * (matCost * regional_material / 100.0 + labCost * regional_installation / 100.0)

              # Cost of gas line from header (centre of flr) to unit heater (centre of zone)
              #centreOfFloor = get_story_cent_to_edge( building_story: zone_info[:flrnum], prototype_creator: prototype_creator, target_cent: target_cent, full_length: false )
              #centreOfSpace = get_space_floor_centroid(space:)
              #gasLineLen = ABS(centreOfFloor - centreOfSpace)

              # Cost of wiring header for zone

              # Cost of wiring from header to unit heater


              totalCost += zone_info[:heatcost] + hdrGasLineCost
            end
          elsif heatCoil.to_CoilHeatingElectric.is_initialized # Electric Unit Heater
            elecCoil = heatCoil.to_CoilHeatingElectric.get
            if elecCoil.isNominalCapacityAutosized.to_bool
              zone_info[:heatcapacity] = elecCoil.autosizedNominalCapacity.to_f/1000.0
            else
              zone_info[:heatcapacity] = elecCoil.nominalCapacity.to_f/1000.0
            end
            if zone_info[:heatcapacity] > 0
              heatCapacity = zone_info[:heatcapacity] / zone.multiplier
              numUnits = get_HVAC_multiplier('elecheat', heatCapacity)
              # Unit cost (Note that same unit multiple applied within getHVACCost())
              matCost, labCost = getHVACCost(zone_info[:sysname], 'elecheat', heatCapacity, false)
              unitCost = (matCost * regional_material / 100.0 + labCost * regional_installation / 100.0)
              # For each unit heater there will be an electrical junction box (wiring costed with distribution - not here)
              matCost, labCost = getHVACCost('Electrical Outlet Box', 'Box', 1, true)
              elecBoxCost = (matCost * reg_mat_elec / 100.0 + labCost * reg_lab_elec / 100.0) * numUnits
              zone_info[:heatcost] = (unitCost + elecBoxCost) * zone.multiplier
              zone_info[:num_units] = numUnits
              # Cost of wiring to electric unit heater


              totalCost += zone_info[:heatcost]
            end
          elsif heatCoil.to_CoilHeatingWater.is_initialized  # Hot water unit heater
            waterCoil = heatCoil.to_CoilHeatingWater.get
            if waterCoil.isRatedCapacityAutosized.to_bool
              zone_info[:heatcapacity] = waterCoil.autosizedRatedCapacity.to_f/1000.0
            else
              zone_info[:heatcapacity] = waterCoil.ratedCapacity.to_f/1000.0
            end
            if zone_info[:heatcapacity] > 0
              heatCapacity = zone_info[:heatcapacity] / zone.multiplier
              # Max capacity for hot water heater is 75300 Watts
              numUnits = get_HVAC_multiplier('hotwateruh', heatCapacity)
              matCost, labCost = getHVACCost(zone_info[:sysname], 'hotwateruh', heatCapacity, false)
              unitHtrCost = (matCost * regional_material / 100.0 + labCost * regional_installation / 100.0)
              # For each unit heater there will be a shut-off valve, 2 Tee connections and 2 elbows to
              # isolate the convector from the hot water loop distribution for servicing and balancing.
              # Cost of valves:
              matCost, labCost = getHVACCost('1.25 inch gate valve', 'ValvesGate', 1.25, true)
              unitHtrValvesCost = (matCost * regional_material / 100.0 + labCost * regional_installation / 100.0) * numUnits
              # Cost of tees:
              matCost, labCost = getHVACCost('1.25 inch copper tees', 'CopperPipeTee', 1.25, true)
              unitHtrTeesCost = (matCost * regional_material / 100.0 + labCost * regional_installation / 100.0) * 2 * numUnits
              # Cost of elbows:
              matCost, labCost = getHVACCost('1.25 inch copper elbows', 'CopperPipeElbow', 1.25, true)
              unitHtrElbowsCost = (matCost * regional_material / 100.0 + labCost * regional_installation / 100.0) * 2 * numUnits
              # For each unit heater there will be an electrical junction box (wiring costed with distribution - not here)
              matCost, labCost = getHVACCost('Electrical Outlet Box', 'Box', 1, true)
              elecBoxCost = (matCost * reg_mat_elec / 100.0 + labCost * reg_lab_elec / 100.0) * numUnits
              # Total convector cost for this zone (excluding distribution piping):
              unitHeaterCost = (unitHtrCost + unitHtrValvesCost + unitHtrTeesCost + unitHtrElbowsCost + elecBoxCost) * zone.multiplier
              zone_info[:heatcost] = unitHeaterCost
              zone_info[:num_units] = numUnits
              # Cost of distribution piping from header to unit heater


              totalCost += unitHeaterCost
            end
          end
        elsif zone_info[:sysname] =~ /WindowAC/i
          zone_info[:systype] = 'WinAC'
          # Cooling cost of WindowAC ...
          if cooling_coil_name == 'nil'
            # The cooling coil name doesn't exist so must use a different method to determine cooling
            # capacity for window AC units!
            query = "SELECT Value FROM ComponentSizes WHERE CompName='WindowAC' AND Units='W'"
            coolCapVal = model.sqlFile().get().execAndReturnFirstDouble(query)
            zone_info[:coolcapacity] = coolCapVal.to_f / 1000.0 # Watts -> kW
          end
          if zone_info[:coolcapacity] > 0
            # DX cooling unit
            coolCapacity = zone_info[:coolcapacity] / zone.multiplier
            numUnits = get_HVAC_multiplier('WINAC', coolCapacity)
            # Window AC unit cost (Note that same numUnits multiple applied within getHVACCost())
            matCost, labCost = getHVACCost(zone_info[:sysname], 'WINAC', coolCapacity, false)
            unitWinACCost = (matCost * regional_material / 100.0 + labCost * regional_installation / 100.0)
            # For each WinAC unit there will be an electrical junction box (wiring costed with distribution - not here)
            matCost, labCost = getHVACCost('Electrical Outlet Box', 'Box', 1, true)
            elecBoxCost = (matCost * reg_mat_elec / 100.0 + labCost * reg_lab_elec / 100.0) * numUnits
            # Total WinAC cost for this zone:
            theWinACCost = (unitWinACCost + elecBoxCost) * zone.multiplier
            zone_info[:coolcost] = theWinACCost
            zone_info[:num_units] = numUnits
            totalCost += theWinACCost
          end
        elsif zone_info[:sysname] =~ /Split/i
          zone_info[:systype] = 'MiniSplit'
          # Cooling cost of Mini-split AC ...
          if cooling_coil_name == 'nil'
            # The cooling coil name doesn't exist so must use a different method to determine cooling
            # capacity for mini-spli units!
            query = "SELECT Value FROM ComponentSizes WHERE CompName='WindowAC' AND Units='W'"
            coolCapVal = model.sqlFile().get().execAndReturnFirstDouble(query)
            zone_info[:coolcapacity] = coolCapVal.to_f / 1000.0 # Watts -> kW
          end
          if zone_info[:coolcapacity] > 0
            # Mini-splt cooling unit
            coolCapacity = zone_info[:coolcapacity] / zone.multiplier
            numUnits = get_HVAC_multiplier('SplitSZWall', coolCapacity)
            # PTAC unit cost (Note that same numUnits multiple applied within getHVACCost())
            matCost, labCost = getHVACCost(zone_info[:sysname], 'PTAC', coolCapacity, false)
            theMiniSplitUnitCost = (matCost * regional_material / 100.0 + labCost * regional_installation / 100.0)
            # For each PTAC unit there will be an electrical junction box (wiring costed with distribution - not here)
            matCost, labCost = getHVACCost('Electrical Outlet Box', 'Box', 1, true)
            elecBoxCost = (matCost * reg_mat_elec / 100.0 + labCost * reg_lab_elec / 100.0) * numUnits
            # Total PTAC cost for this zone (excluding distribution piping):
            theMiniSplitCost = (theMiniSplitUnitCost + elecBoxCost) * zone.multiplier
            zone_info[:coolcost] = theMiniSplitCost
            zone_info[:num_units] = numUnits
            totalCost += theMiniSplitCost
          end

        end
        # Add information to zonal costing report.
        @costing_report['heating_and_cooling']['zonal_systems'] << {
          'systype' => zone_info[:systype],
          'zone_number' => numZones,
          'zone_name' => zone_info[:zonename],
          'zone_multiple' => zone_info[:multiplier],
          'heat_capacity(kW)' => zone_info[:heatcapacity].round(1),
          'cool_capacity(kW)' => zone_info[:coolcapacity].round(1),
          'heat_cost' => zone_info[:heatcost].round(0),
          'cool_cost' => zone_info[:coolcost].round(0),
          'heatcool_cost' => zone_info[:heatcoolcost].round(0),
          'piping_cost' => zone_info[:pipingcost].round(0),
          'wiring_cost' => zone_info[:wiringcost].round(0),
          'num_units' => zone_info[:num_units],
          'cummultive_zonal_cost' => totalCost.round(0)
        }
      end # End of check of check of if zonal equipment exists
    end # End of equipment loop
  end # End of zone loop

  # Get cost of zonal vrf systems
  unless vrfSystemFloors[:vrfFloors].empty?
    totalCost += getZonalVRFCosting(vrfSystemFloors: vrfSystemFloors, model: model, prototype_creator: prototype_creator, regMat: regional_material, regLab: regional_installation, cumulCost: totalCost)
  end
  puts "\nZonal systems costing data successfully generated. Total zonal systems costs: $#{totalCost.round(0)}"

  return totalCost
end