openstudio-standards 0.8.3 → 0.8.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (87) hide show
  1. checksums.yaml +4 -4
  2. data/lib/openstudio-standards/btap/costing/README.md +502 -0
  3. data/lib/openstudio-standards/btap/costing/btap_costing.rb +473 -0
  4. data/lib/openstudio-standards/btap/costing/btap_measure_helper.rb +359 -0
  5. data/lib/openstudio-standards/btap/costing/btap_workflow.rb +117 -0
  6. data/lib/openstudio-standards/btap/costing/common_paths.rb +78 -0
  7. data/lib/openstudio-standards/btap/costing/common_resources/ConstructionProperties.csv +52 -0
  8. data/lib/openstudio-standards/btap/costing/common_resources/Constructions.csv +37 -0
  9. data/lib/openstudio-standards/btap/costing/common_resources/construction_sets.csv +1270 -0
  10. data/lib/openstudio-standards/btap/costing/common_resources/constructions_glazing.csv +61 -0
  11. data/lib/openstudio-standards/btap/costing/common_resources/constructions_opaque.csv +2256 -0
  12. data/lib/openstudio-standards/btap/costing/common_resources/costs.csv +1904 -0
  13. data/lib/openstudio-standards/btap/costing/common_resources/costs_local_factors.csv +2315 -0
  14. data/lib/openstudio-standards/btap/costing/common_resources/hvac_vent_ahu.csv +925 -0
  15. data/lib/openstudio-standards/btap/costing/common_resources/lighting.csv +364 -0
  16. data/lib/openstudio-standards/btap/costing/common_resources/lighting_sets.csv +2667 -0
  17. data/lib/openstudio-standards/btap/costing/common_resources/locations.csv +75 -0
  18. data/lib/openstudio-standards/btap/costing/common_resources/materials_glazing.csv +35 -0
  19. data/lib/openstudio-standards/btap/costing/common_resources/materials_hvac.csv +1699 -0
  20. data/lib/openstudio-standards/btap/costing/common_resources/materials_lighting.csv +267 -0
  21. data/lib/openstudio-standards/btap/costing/common_resources/materials_opaque.csv +164 -0
  22. data/lib/openstudio-standards/btap/costing/copy_test_results_files_to_expected_results.rb +11 -0
  23. data/lib/openstudio-standards/btap/costing/cost_building_from_file.rb +136 -0
  24. data/lib/openstudio-standards/btap/costing/costing_database_wrapper.rb +177 -0
  25. data/lib/openstudio-standards/btap/costing/daylighting_sensor_control_costing.rb +353 -0
  26. data/lib/openstudio-standards/btap/costing/dcv_costing.rb +314 -0
  27. data/lib/openstudio-standards/btap/costing/dummy.epw +8768 -0
  28. data/lib/openstudio-standards/btap/costing/dummy.osm +5320 -0
  29. data/lib/openstudio-standards/btap/costing/envelope_costing.rb +284 -0
  30. data/lib/openstudio-standards/btap/costing/heating_cooling_costing.rb +2584 -0
  31. data/lib/openstudio-standards/btap/costing/led_lighting_costing.rb +155 -0
  32. data/lib/openstudio-standards/btap/costing/lighting_costing.rb +209 -0
  33. data/lib/openstudio-standards/btap/costing/mech_sizing.json +502 -0
  34. data/lib/openstudio-standards/btap/costing/neb_end_use_prices.csv +42 -0
  35. data/lib/openstudio-standards/btap/costing/necb_2011_spacetype_info.csv +225 -0
  36. data/lib/openstudio-standards/btap/costing/necb_reference_runs.csv +28705 -0
  37. data/lib/openstudio-standards/btap/costing/nv_costing.rb +547 -0
  38. data/lib/openstudio-standards/btap/costing/parallel_tests.rb +92 -0
  39. data/lib/openstudio-standards/btap/costing/pv_ground_costing.rb +687 -0
  40. data/lib/openstudio-standards/btap/costing/shw_costing.rb +705 -0
  41. data/lib/openstudio-standards/btap/costing/test_list.txt +17 -0
  42. data/lib/openstudio-standards/btap/costing/test_run_all_test_locally.rb +26 -0
  43. data/lib/openstudio-standards/btap/costing/test_run_costing_tests.rb +80 -0
  44. data/lib/openstudio-standards/btap/costing/ventilation_costing.rb +2616 -0
  45. data/lib/openstudio-standards/constructions/modify.rb +2 -1
  46. data/lib/openstudio-standards/standards/Standards.Model.rb +39 -9
  47. data/lib/openstudio-standards/standards/ashrae_90_1_prm/ashrae_90_1_prm.Model.rb +2 -2
  48. data/lib/openstudio-standards/standards/ashrae_90_1_prm/userdata_csv/ashrae_90_1_prm.UserData.rb +6 -1
  49. data/lib/openstudio-standards/standards/necb/BTAPPRE1980/btap_pre1980.rb +2 -27
  50. data/lib/openstudio-standards/standards/necb/BTAPPRE1980/hvac_system_3_and_8_single_speed.rb +68 -27
  51. data/lib/openstudio-standards/standards/necb/BTAPPRE1980/hvac_system_4.rb +64 -25
  52. data/lib/openstudio-standards/standards/necb/BTAPPRE1980/hvac_system_6.rb +9 -14
  53. data/lib/openstudio-standards/standards/necb/ECMS/hvac_systems.rb +46 -20
  54. data/lib/openstudio-standards/standards/necb/NECB2011/autozone.rb +635 -248
  55. data/lib/openstudio-standards/standards/necb/NECB2011/data/constants.json +43 -7
  56. data/lib/openstudio-standards/standards/necb/NECB2011/data/fuel_type_sets.json +7 -1
  57. data/lib/openstudio-standards/standards/necb/NECB2011/data/geometry/HighriseApartmentMult.osm +14272 -0
  58. data/lib/openstudio-standards/standards/necb/NECB2011/data/necb_2015_table_c1.json +1 -1
  59. data/lib/openstudio-standards/standards/necb/NECB2011/data/space_types.json +437 -437
  60. data/lib/openstudio-standards/standards/necb/NECB2011/data/systems.json +516 -0
  61. data/lib/openstudio-standards/standards/necb/NECB2011/data/systems_including_sys5.json +588 -0
  62. data/lib/openstudio-standards/standards/necb/NECB2011/hvac_namer.rb +489 -0
  63. data/lib/openstudio-standards/standards/necb/NECB2011/hvac_system_1_single_speed.rb +16 -6
  64. data/lib/openstudio-standards/standards/necb/NECB2011/hvac_system_2_and_5.rb +48 -5
  65. data/lib/openstudio-standards/standards/necb/NECB2011/hvac_system_3_and_8_multi_speed.rb +2 -2
  66. data/lib/openstudio-standards/standards/necb/NECB2011/hvac_system_3_and_8_single_speed.rb +35 -27
  67. data/lib/openstudio-standards/standards/necb/NECB2011/hvac_system_4.rb +34 -23
  68. data/lib/openstudio-standards/standards/necb/NECB2011/hvac_system_6.rb +8 -6
  69. data/lib/openstudio-standards/standards/necb/NECB2011/hvac_systems.rb +42 -13
  70. data/lib/openstudio-standards/standards/necb/NECB2011/necb_2011.rb +214 -25
  71. data/lib/openstudio-standards/standards/necb/NECB2011/system_fuels.rb +61 -1
  72. data/lib/openstudio-standards/standards/necb/NECB2015/data/space_types.json +636 -636
  73. data/lib/openstudio-standards/standards/necb/NECB2015/data/unitary_acs.json +38 -38
  74. data/lib/openstudio-standards/standards/necb/NECB2015/hvac_systems.rb +15 -6
  75. data/lib/openstudio-standards/standards/necb/NECB2017/data/space_types.json +636 -636
  76. data/lib/openstudio-standards/standards/necb/NECB2020/data/chillers.json +71 -71
  77. data/lib/openstudio-standards/standards/necb/README.md +343 -0
  78. data/lib/openstudio-standards/standards/necb/common/btap_data.rb +190 -28
  79. data/lib/openstudio-standards/standards/necb/common/btap_datapoint.rb +14 -5
  80. data/lib/openstudio-standards/standards/necb/common/eccc_electric_grid_intensity_20250311.csv +14 -0
  81. data/lib/openstudio-standards/standards/necb/common/nir_gas_grid_intensity_20250311.csv +14 -0
  82. data/lib/openstudio-standards/standards/necb/common/system_types.yaml +0 -0
  83. data/lib/openstudio-standards/utilities/logging.rb +18 -14
  84. data/lib/openstudio-standards/version.rb +1 -1
  85. data/lib/openstudio-standards/weather/modify.rb +2 -2
  86. data/lib/openstudio-standards.rb +12 -0
  87. metadata +53 -2
@@ -0,0 +1,2584 @@
1
+ class BTAPCosting
2
+
3
+ # --------------------------------------------------------------------------------------------------
4
+ # This function gets all costs associated with boilers (i.e., boilers, pumps, flues, electrical
5
+ # lines and boxes, fuel lines and distribution piping to zonal heating units)
6
+ # --------------------------------------------------------------------------------------------------
7
+ def boiler_costing(model, prototype_creator)
8
+
9
+ #Global flag to determine if a GSHP is present
10
+ @gshp_flag = false
11
+
12
+ #Global flag to determine if a AWHP is present
13
+ @awhp_flag = false
14
+
15
+ totalCost = 0.0
16
+
17
+ # Get regional cost factors for this province and city
18
+ materials_hvac = @costing_database["raw"]["materials_hvac"]
19
+ hvac_material = materials_hvac.select {|data|
20
+ data['Material'].to_s == "GasBoilers"}.first # Get any row from spreadsheet in case of region error
21
+ regional_material, regional_installation =
22
+ get_regional_cost_factors(@costing_report['province_state'], @costing_report['city'], hvac_material)
23
+
24
+ # Get regional electric cost factors for this province and city
25
+ hvac_material = materials_hvac.select {|data|
26
+ data['Material'].to_s.upcase == "BOX" && data['Size'].to_i == 1}.first
27
+ reg_mat_elec, reg_lab_elec =
28
+ get_regional_cost_factors(@costing_report['province_state'], @costing_report['city'], hvac_material)
29
+
30
+ # Store some geometry data for use below...
31
+ util_dist, ht_roof, nom_flr_hght, horz_dist, numAGFlrs, mechRmInBsmt = getGeometryData(model, prototype_creator)
32
+
33
+ template_type = prototype_creator.template
34
+
35
+ plant_loop_info = {}
36
+ plant_loop_info[:boilers] = []
37
+ plant_loop_info[:boilerpumps] = []
38
+
39
+ # Iterate through the plant loops to get boiler & pump data...
40
+ model.getPlantLoops.each do |plant_loop|
41
+ next unless (plant_loop.name.get.to_s.downcase == "hot water loop") || (plant_loop.name.get.to_s.downcase == "hw plantloop")
42
+ plant_loop.supplyComponents.each do |supply_comp|
43
+ if supply_comp.to_BoilerHotWater.is_initialized
44
+ boiler = supply_comp.to_BoilerHotWater.get
45
+ boiler_info = {}
46
+ plant_loop_info[:boilers] << boiler_info
47
+ boiler_info[:name] = boiler.name.get
48
+ # 2020-09-01 CK Include efficiency for boiler upgrade costing
49
+ boiler_info[:efficiency] = boiler.nominalThermalEfficiency.to_f
50
+ if boiler.fuelType =~ /Electric/i
51
+ boiler_info[:fueltype] = 'ElecBoilers'
52
+ elsif boiler.fuelType =~ /NaturalGas/i
53
+ boiler_info[:fueltype] = 'GasBoilers'
54
+ #2020-09-01 CK Include modifications for condensing and pulse gas boilers
55
+ if boiler_info[:efficiency] >= 0.827 && boiler_info[:efficiency] < 0.9
56
+ boiler_info[:fueltype] = "CondensingBoilers"
57
+ elsif boiler_info[:efficiency] >= 0.9
58
+ boiler_info[:fueltype] = "PulseBoilers"
59
+ end
60
+ elsif boiler.fuelType =~ /Oil/i # Oil, FuelOil, FuelOil#2
61
+ boiler_info[:fueltype] = 'OilBoilers'
62
+ end
63
+ boiler_info[:nominal_capacity] = boiler.nominalCapacity.to_f / 1000 # kW
64
+ elsif supply_comp.to_PumpConstantSpeed.is_initialized
65
+ csPump = supply_comp.to_PumpConstantSpeed.get
66
+ csPump_info = {}
67
+ plant_loop_info[:boilerpumps] << csPump_info
68
+ csPump_info[:name] = csPump.name.get
69
+ if csPump.isRatedPowerConsumptionAutosized.to_bool
70
+ csPumpSize = csPump.autosizedRatedPowerConsumption.to_f
71
+ else
72
+ csPumpSize = csPump.ratedPowerConsumption.to_f
73
+ end
74
+ csPump_info[:size] = csPumpSize.to_f # Watts
75
+ if csPump.isRatedFlowRateAutosized.to_bool
76
+ csPump_info[:water_flow_m3_per_s] = csPump.autosizedRatedFlowRate.to_f
77
+ else
78
+ csPump_info[:water_flow_m3_per_s] = csPump.ratedFlowRate.to_f
79
+ end
80
+ elsif supply_comp.to_PumpVariableSpeed.is_initialized
81
+ vsPump = supply_comp.to_PumpVariableSpeed.get
82
+ vsPump_info = {}
83
+ plant_loop_info[:boilerpumps] << vsPump_info
84
+ vsPump_info[:name] = vsPump.name.get
85
+ if vsPump.isRatedPowerConsumptionAutosized.to_bool
86
+ vsPumpSize = vsPump.autosizedRatedPowerConsumption.to_f
87
+ else
88
+ vsPumpSize = vsPump.ratedPowerConsumption.to_f
89
+ end
90
+ vsPump_info[:size] = vsPumpSize.to_f # Watts
91
+ if vsPump.isRatedFlowRateAutosized.to_bool
92
+ vsPump_info[:water_flow_m3_per_s] = vsPump.autosizedRatedFlowRate.to_f
93
+ else
94
+ vsPump_info[:water_flow_m3_per_s] = vsPump.ratedFlowRate.to_f
95
+ end
96
+ elsif supply_comp.to_HeatPumpWaterToWaterEquationFitHeating.is_initialized
97
+ gshp = supply_comp.to_HeatPumpWaterToWaterEquationFitHeating.get
98
+ gshp_info = {}
99
+ gshp_info[:fueltype] = 'wshp'
100
+ gshp_info[:name] = gshp.name.to_s
101
+ if gshp.isRatedHeatingCapacityAutosized.to_bool
102
+ gshp_info[:nominal_capacity] = gshp.autosizedRatedHeatingCapacity.to_f/1000.0
103
+ else
104
+ gshp_info[:nominal_capacity] = gshp.ratedHeatingCapacity.to_f/1000.0
105
+ end
106
+ plant_loop_info[:boilers] << gshp_info
107
+ @gshp_flag = true
108
+ elsif supply_comp.to_HeatPumpPlantLoopEIRHeating.is_initialized
109
+ awhp = supply_comp.to_HeatPumpPlantLoopEIRHeating.get
110
+ awhp_info = {}
111
+ awhp_info[:fueltype] = 'Airtowaterhp'
112
+ awhp_info[:name] = awhp.name.to_s
113
+ if awhp.isReferenceCapacityAutosized.to_bool
114
+ awhp_info[:nominal_capacity] = awhp.autosizedReferenceCapacity.to_f/1000.0
115
+ else
116
+ awhp_info[:nominal_capacity] = awhp.referenceCapacity.to_f/1000.0
117
+ end
118
+ plant_loop_info[:boilers] << awhp_info
119
+ @awhp_flag = true
120
+ end
121
+ end
122
+ end
123
+
124
+ boilerCost = 0.0 ; thisBoilerCost = 0.0 ; flueCost = 0.0 ; utilCost = 0.0 ; fuelFittingCost = 0.0
125
+ numBoilers = 0 ; multiplier = 1.0 ; primaryFuel = ''; primaryCap = 0 ; backupBoiler = false
126
+
127
+ # Get costs associated with each boiler
128
+ plant_loop_info[:boilers].each do |boiler|
129
+
130
+ # Get primary/secondary/backup boiler cost based on fuel type and capacity for each boiler
131
+ # 06-Sep-2019 JTB: Added check for no 'Primary' or 'Secondary' label and assume primary.
132
+ # This boiler prefix name seemed to disappear after the heat pump work was committed.
133
+ numBoilers += 1
134
+ if boiler[:name] =~ /primary/i || (boiler[:name] !~ /primary/i && boiler[:name] !~ /secondary/ && numBoilers == 1) || (boiler[:fuel_type] == 'wshp') || (boiler[:fuel_type] == 'Airtowaterhp')
135
+ primaryFuel = boiler[:fueltype]
136
+ primaryCap = boiler[:nominal_capacity]
137
+ matCost, labCost = getHVACCost(boiler[:name], boiler[:fueltype], boiler[:nominal_capacity], false)
138
+
139
+ # 2020-09-02 CK: Assume condensing oil boilers cost twice as much as non-condensing oil boilers
140
+ if boiler[:fueltype] == 'OilBoilers' && boiler[:efficiency] >= 0.9
141
+ thisBoilerCost = matCost * 2 * regional_material / 100.0 + labCost * regional_installation / 100.0
142
+ else
143
+ thisBoilerCost = matCost * regional_material / 100.0 + labCost * regional_installation / 100.0
144
+ end
145
+
146
+ # Flue and utility component costs (for gas and oil boilers only)
147
+ # 2020-09-02 CK: Include pulse and condensing gas boilers to those with utility component costs
148
+ if boiler[:fueltype] == 'GasBoilers' || boiler[:fueltype] == 'OilBoilers' || boiler[:fueltype] == 'CondensingBoilers' || boiler[:fueltype] == 'PulseBoilers'
149
+ # Calculate flue costs once for all boilers since flues combined by header when multiple boilers
150
+ # 6 inch diameter flue (#384)
151
+ materialHash = get_cost_info(mat: 'Venting', size: '6')
152
+ matCost, labCost = getCost('flue', materialHash, multiplier)
153
+ flueVentCost = matCost * regional_material / 100.0 + labCost * regional_installation / 100.0
154
+
155
+ #6 inch elbow fitting (#386)
156
+ materialHash = get_cost_info(mat: 'VentingElbow', size: '6')
157
+ matCost, labCost = getCost('flue elbow', materialHash, multiplier)
158
+ flueElbowCost = matCost * regional_material / 100.0 + labCost * regional_installation / 100.0
159
+
160
+ # 6 inch top (#392)
161
+ materialHash = get_cost_info(mat: 'VentingTop', size: '6')
162
+ matCost, labCost = getCost('flue top', materialHash, multiplier)
163
+ flueTopCost = matCost * regional_material / 100.0 + labCost * regional_installation / 100.0
164
+
165
+ # Gas/Oil line piping cost per ft (#1)
166
+ materialHash = get_cost_info(mat: 'GasLine', unit: 'L.F.')
167
+ matCost, labCost = getCost('fuel line', materialHash, multiplier)
168
+ fuelLineCost = matCost * regional_material / 100.0 + labCost * regional_installation / 100.0
169
+
170
+ # Gas/Oil line fitting connection per boiler (#2)
171
+ materialHash = get_cost_info(mat: 'GasLine', unit: 'each')
172
+ matCost, labCost = getCost('fuel line fitting connection', materialHash, multiplier)
173
+ fuelFittingCost = matCost * regional_material / 100.0 + labCost * regional_installation / 100.0
174
+
175
+ # Header cost only non-zero if there is a secondary/backup gas/oil boiler
176
+ headerCost = 0.0
177
+ else # Electric has no flue
178
+ flueVentCost = 0.0 ; flueElbowCost = 0.0 ; flueTopCost = 0.0 ; headerCost = 0.0
179
+ end
180
+
181
+ # Electric utility cost components (i.e., power lines).
182
+ # Calculate utility cost for primary boiler only since multiple boilers use common utilities
183
+
184
+ # elec 600V #14 wire /100 ft (#848)
185
+ materialHash = get_cost_info(mat: 'Wiring', size: 14)
186
+ matCost, labCost = getCost('electrical wire - 600V #14', materialHash, multiplier)
187
+ elecWireCost = matCost * reg_mat_elec / 100.0 + labCost * reg_lab_elec / 100.0
188
+
189
+ # 1 inch metal conduit (#851)
190
+ materialHash = get_cost_info(mat: 'Conduit', unit: 'L.F.')
191
+ matCost, labCost = getCost('1 inch metal conduit', materialHash, multiplier)
192
+ metalConduitCost = matCost * reg_mat_elec / 100.0 + labCost * reg_lab_elec / 100.0
193
+
194
+ # 2020-09-02 CK Adding Condensing and Pulse boilers to those that need additional connections
195
+ if boiler[:fueltype] == 'GasBoilers' || boiler[:fueltype] == 'CondensingBoilers' || boiler[:fueltype] == 'PulseBoilers'
196
+ # Gas boilers require fuel line+valves+connectors and electrical conduit
197
+ utilCost += (fuelLineCost + metalConduitCost) * util_dist + fuelFittingCost +
198
+ elecWireCost * util_dist / 100
199
+
200
+ elsif boiler[:fueltype] == 'OilBoilers'
201
+ # Oil boilers require fuel line+valves+connectors and electrical conduit
202
+
203
+ # Oil filtering system (#4)
204
+ materialHash = get_cost_info(mat: 'OilLine', unit: 'each')
205
+ matCost, labCost = getCost('Oil filtering system', materialHash, multiplier)
206
+ oilFilterCost = matCost * regional_material / 100.0 + labCost * regional_installation / 100.0
207
+
208
+ # 2000 USG above ground tank (#5)
209
+ materialHash = get_cost_info(mat: 'OilTanks', size: 2000)
210
+ matCost, labCost = getCost('Oil tank (2000 USG)', materialHash, multiplier)
211
+ oilTankCost = matCost * regional_material / 100.0 + labCost * regional_installation / 100.0
212
+
213
+ utilCost += (fuelLineCost + metalConduitCost) * util_dist + fuelFittingCost +
214
+ elecWireCost * util_dist / 100 + oilFilterCost + oilTankCost
215
+
216
+ elsif boiler[:fueltype].to_s.downcase == 'elecboilers' || boiler[:fueltype].to_s.downcase == 'wshp'
217
+ # Electric boilers require only conduit
218
+ utilCost += metalConduitCost * util_dist + elecWireCost * util_dist / 100
219
+ elsif boiler[:fuel_type].to_s.downcase == 'airtowaterhp'
220
+ # Add heating buffer tank for awhp
221
+ materialHash = get_cost_info(mat: 'solartank', size: 450)
222
+ matCost, labCost = getCost('solartank', materialHash, multiplier)
223
+ utilCost = matCost * regional_material / 100.0 + labCost * regional_installation / 100.0
224
+ end
225
+
226
+ elsif boiler[:name] =~ /secondary/i || numBoilers > 1
227
+ if boiler[:nominal_capacity] > 0.1
228
+ # A secondary boiler exists so use it for costing
229
+ matCost, labCost = getHVACCost(boiler[:name], boiler[:fueltype], boiler[:nominal_capacity], false)
230
+ # 2020-09-02 CK: Assume condensing oil boilers cost twice as much as non-condensing oil boilers
231
+ if boiler[:fueltype] == 'OilBoilers' && boiler[:efficiency] >= 0.9
232
+ thisBoilerCost = matCost * 2 * regional_material / 100.0 + labCost * regional_installation / 100.0
233
+ else
234
+ thisBoilerCost = matCost * regional_material / 100.0 + labCost * regional_installation / 100.0
235
+ end
236
+ else
237
+ # Use existing value of thisBoilerCost to represent a backup boiler!
238
+ # This just doubles the cost of the primary boiler.
239
+ # 2023-04-25: Leaving backup boiler in energy model but no longer costing.
240
+ backupBoiler = false
241
+ thisBoilerCost = 0.0
242
+ numBoilers -= 1
243
+ end
244
+
245
+ # Flue costs set to zero if secondary boiler since already calculated in primary
246
+ flueVentCost = 0.0; flueElbowCost = 0.0; flueTopCost = 0.0
247
+
248
+ # Check if need a flue header (i.e., there are both primary and secondary/backup boilers)
249
+ if thisBoilerCost > 0.0 && ( (backupBoiler && primaryFuel != 'ElecBoilers') || (boiler[:fueltype] != 'ElecBoilers') || (boiler[:fueltype] != 'wshp') || (boiler[:fueltype] != 'Airtowaterhp'))
250
+ # 6 inch diameter header (#384)
251
+ materialHash = get_cost_info(mat: 'Venting', size: 6)
252
+ matCost, labCost = getCost('flue header', materialHash, multiplier)
253
+ headerVentCost = matCost * regional_material / 100.0 + labCost * regional_installation / 100.0
254
+
255
+ #6 inch elbow fitting for header (#386)
256
+ materialHash = get_cost_info(mat: 'VentingElbow', size: 6)
257
+ matCost, labCost = getCost('flue header elbow', materialHash, multiplier)
258
+ headerElbowCost = matCost * regional_material / 100.0 + labCost * regional_installation / 100.0
259
+
260
+ # Assume a header length of 20 ft and an elbow fitting for each boiler connected to the header
261
+ headerCost = (headerVentCost * 20 + headerElbowCost) * numBoilers
262
+ else
263
+ headerCost = 0.0
264
+ end
265
+ end
266
+ boilerCost += thisBoilerCost
267
+ flueCost += flueVentCost * ht_roof + flueElbowCost + flueTopCost + headerCost
268
+ if numBoilers > 1
269
+ # Adjust utility cost for extra fuel line fitting cost
270
+ utilCost += fuelFittingCost * (numBoilers - 1)
271
+ end
272
+ end
273
+
274
+ # Boiler pump costs
275
+ pumpCost = 0.0; pipingToPumpCost = 0.0; numPumps = 0; pumpName = ''; pumpSize = 0.0 ; pumpFlow = 0.0
276
+ plant_loop_info[:boilerpumps].each do |pump|
277
+ numPumps += 1
278
+ # Cost variable and constant volume pumps the same (the difference is in extra cost for VFD controller)
279
+ pumpSize = pump[:size]; pumpName = pump[:name]
280
+ pumpFlow += pump[:water_flow_m3_per_s].to_f
281
+ matCost, labCost = getHVACCost(pumpName, 'Pumps', pumpSize, false)
282
+ pumpCost += matCost * regional_material / 100.0 + labCost * regional_installation / 100.0
283
+ if pump[:name] =~ /variable/i
284
+ # Cost the VFD controller for the variable pump
285
+ matCost, labCost = getHVACCost(pumpName, 'VFD', pumpSize, false)
286
+ pumpCost += matCost * reg_mat_elec / 100.0 + labCost * reg_lab_elec / 100.0
287
+ end
288
+ end
289
+ if numBoilers > 1 && numPumps < 2
290
+ # Add pump costing for the backup boiler pump.
291
+ pumpCost *= 2.0
292
+ numPumps = 2 # reset the number of pumps for piping costs below
293
+ end
294
+ # Double the pump costs to accomodate the costing of a backup pumps for each boiler!
295
+ # No longer costing backup pumps.
296
+ # pumpCost *= 2.0
297
+
298
+ # Boiler water piping to pumps cost: Add piping elbows, valves and insulation from the boiler(s)
299
+ # to the pumps(s) assuming a pipe diameter of 1” and a distance of 10 ft per pump
300
+
301
+ if numBoilers > 0
302
+ # 1 inch Steel pipe
303
+ matCost, labCost = getHVACCost('1 inch steel pipe', 'SteelPipe', 1)
304
+ pipingToPumpCost += 10.0 * numPumps * (matCost * regional_material / 100.0 + labCost * regional_installation / 100.0)
305
+
306
+ # 1 inch Steel pipe insulation
307
+ matCost, labCost = getHVACCost('1 inch pipe insulation', 'PipeInsulation', 1)
308
+ pipingToPumpCost += 10.0 * numPumps * (matCost * regional_material / 100.0 + labCost * regional_installation / 100.0)
309
+
310
+ # 1 inch Steel pipe elbow
311
+ matCost, labCost = getHVACCost('1 inch steel pipe elbow', 'SteelPipeElbow', 1)
312
+ pipingToPumpCost += 2.0 * numPumps * (matCost * regional_material / 100.0 + labCost * regional_installation / 100.0)
313
+
314
+ # 1 inch gate valves
315
+ matCost, labCost = getHVACCost('1 inch gate valves', 'ValvesGate', 1)
316
+ pipingToPumpCost += 1.0 * numPumps * (matCost * regional_material / 100.0 + labCost * regional_installation / 100.0)
317
+ end
318
+
319
+ if numBoilers > 0
320
+ # Double pump piping cost to account for second boiler
321
+ pipingToPumpCost *= numBoilers
322
+
323
+ hdrDistributionCost = getHeaderPipingDistributionCost(numAGFlrs, mechRmInBsmt, regional_material, regional_installation, reg_mat_elec, reg_lab_elec,
324
+ pumpFlow, horz_dist, nom_flr_hght)
325
+ else
326
+ pipingToPumpCost = 0
327
+ hdrDistributionCost = 0
328
+ end
329
+
330
+ totalCost = boilerCost + flueCost + utilCost + pumpCost + pipingToPumpCost + hdrDistributionCost
331
+
332
+ @costing_report['heating_and_cooling']['plant_equipment'] << {
333
+ 'type' => 'boilers',
334
+ 'nom_flr2flr_hght_ft' => nom_flr_hght.round(1),
335
+ 'ht_roof_ft' => ht_roof.round(1),
336
+ 'longest_distance_to_ext_ft' => horz_dist.round(1),
337
+ 'wiring_and_gas_connections_distance_ft' => util_dist.round(1),
338
+ 'equipment_cost' => boilerCost.round(0),
339
+ 'flue_cost' => flueCost.round(0),
340
+ 'wiring_and_gas_connections_cost' => utilCost.round(0),
341
+ 'pump_cost' => pumpCost.round(0),
342
+ 'piping_to_pump_cost' => pipingToPumpCost.round(0),
343
+ 'header_distribution_cost' => hdrDistributionCost.round(0),
344
+ 'total_cost' => totalCost.round(0)
345
+ }
346
+ puts "\nHVAC Boiler costing data successfully generated. Total boiler costs: $#{totalCost.round(0)}"
347
+
348
+ return totalCost
349
+ end
350
+
351
+ # --------------------------------------------------------------------------------------------------
352
+ # Chiller costing is similar to boiler costing above
353
+ # --------------------------------------------------------------------------------------------------
354
+ def chiller_costing(model, prototype_creator)
355
+
356
+ totalCost = 0.0
357
+
358
+ # Get regional cost factors for this province and city
359
+ materials_hvac = @costing_database["raw"]["materials_hvac"]
360
+ hvac_material = materials_hvac.select {|data|
361
+ data['Material'].to_s == "GasBoilers"}.first # Get any row from spreadsheet in case of region error
362
+ regional_material, regional_installation =
363
+ get_regional_cost_factors(@costing_report['province_state'], @costing_report['city'], hvac_material)
364
+
365
+ # Get regional electric cost factors for this province and city
366
+ hvac_material = materials_hvac.select {|data|
367
+ data['Material'].to_s.upcase == "BOX" && data['Size'].to_i == 1}.first
368
+ reg_mat_elec, reg_lab_elec =
369
+ get_regional_cost_factors(@costing_report['province_state'], @costing_report['city'], hvac_material)
370
+
371
+ # Store some geometry data for use below...
372
+ util_dist, ht_roof, nom_flr_hght, horz_dist, numAGFlrs, mechRmInBsmt = getGeometryData(model, prototype_creator)
373
+
374
+ template_type = prototype_creator.template
375
+
376
+ chillerCost = 0.0 ; thisChillerCost = 0.0 ; flueCost = 0.0 ; utilCost = 0.0
377
+ plant_loop_info = {}
378
+ plant_loop_info[:chillers] = []
379
+ plant_loop_info[:chillerpumps] = []
380
+ awhp_chiller = false
381
+
382
+ # Iterate through the plant loops to get chiller & pump data...
383
+ model.getPlantLoops.each do |plant_loop|
384
+ next unless (plant_loop.name.get.to_s.downcase == "chilled water loop") || (plant_loop.name.get.to_s.downcase == "chw plantloop")
385
+ plant_loop.supplyComponents.each do |supply_comp|
386
+ if supply_comp.to_ChillerElectricEIR.is_initialized #|| supply_comp.to_ChillerGasEIR.is_initialized
387
+ chiller = supply_comp.to_ChillerElectricEIR.get
388
+ chiller_info = {}
389
+ plant_loop_info[:chillers] << chiller_info
390
+ chiller_info[:name] = chiller.name.get
391
+ if chiller_info[:name] =~ /WaterCooled/i
392
+ if chiller_info[:name] =~ /Absorption/i
393
+ chiller_info[:type] = 'HotAbsChiller'
394
+ chiller_info[:fuel] = 'NaturalGas'
395
+ elsif chiller_info[:name] =~ /Direct Gas/i
396
+ chiller_info[:type] = 'GasAbsChiller'
397
+ chiller_info[:fuel] = 'NaturalGas'
398
+ elsif chiller_info[:name] =~ /Centrifugal/i
399
+ chiller_info[:type] = 'CentChillerWater'
400
+ chiller_info[:fuel] = 'Electric'
401
+ elsif chiller_info[:name] =~ /Reciprocating/i
402
+ chiller_info[:type] = 'RecChillerWater'
403
+ chiller_info[:fuel] = 'Electric'
404
+ elsif chiller_info[:name] =~ /Scroll/i
405
+ chiller_info[:type] = 'ScrollChillerWater'
406
+ chiller_info[:fuel] = 'Electric'
407
+ elsif chiller_info[:name] =~ /Screw/i
408
+ chiller_info[:type] = 'ScrewChillerWater'
409
+ chiller_info[:fuel] = 'Electric'
410
+ end
411
+ elsif chiller_info[:name] =~ /AirCooled/i
412
+ if chiller_info[:name] =~ /Reciprocating/i
413
+ chiller_info[:type] = 'RecChillerAir'
414
+ chiller_info[:fuel] = 'Electric'
415
+ elsif chiller_info[:name] =~ /Scroll/i
416
+ chiller_info[:type] = 'ScrollChillerAir'
417
+ chiller_info[:fuel] = 'Electric'
418
+ elsif chiller_info[:name] =~ /Screw/i
419
+ chiller_info[:type] = 'ScrewChillerAir'
420
+ chiller_info[:fuel] = 'Electric'
421
+ elsif chiller_info[:name] =~ /DX/i
422
+ chiller_info[:type] = 'DXChiller'
423
+ chiller_info[:fuel] = 'Electric'
424
+ end
425
+ end
426
+ chiller_info[:reference_capacity] = chiller.referenceCapacity.to_f / 1000 # kW
427
+ elsif supply_comp.to_HeatPumpPlantLoopEIRCooling.is_initialized
428
+ chiller = supply_comp.to_HeatPumpPlantLoopEIRCooling.get
429
+ chiller_info = {}
430
+ chiller_info[:name] = chiller.name.get
431
+ chiller_info[:type] = 'Airtowaterhp'
432
+ chiller_info[:fuel] = 'Electric'
433
+ if chiller.isReferenceCapacityAutosized
434
+ chiller_info[:reference_capacity] = chiller.autosizedReferenceCapacity.to_f / 1000 # kW
435
+ else
436
+ chiller_info[:reference_capacity] = chiller.referenceCapacity.to_f / 1000 # kW
437
+ end
438
+ awhp_chiller = true
439
+ plant_loop_info[:chillers] << chiller_info
440
+ elsif supply_comp.to_PumpConstantSpeed.is_initialized
441
+ csPump = supply_comp.to_PumpConstantSpeed.get
442
+ csPump_info = {}
443
+ plant_loop_info[:chillerpumps] << csPump_info
444
+ csPump_info[:name] = csPump.name.get
445
+ if csPump.isRatedPowerConsumptionAutosized.to_bool
446
+ csPumpSize = csPump.autosizedRatedPowerConsumption.to_f
447
+ else
448
+ csPumpSize = csPump.ratedPowerConsumption.to_f
449
+ end
450
+ csPump_info[:size] = csPumpSize.to_f # Watts
451
+ if csPump.isRatedFlowRateAutosized.to_bool
452
+ csPump_info[:water_flow_m3_per_s] = csPump.autosizedRatedFlowRate.to_f
453
+ else
454
+ csPump_info[:water_flow_m3_per_s] = csPump.ratedFlowRate.to_f
455
+ end
456
+ elsif supply_comp.to_PumpVariableSpeed.is_initialized
457
+ vsPump = supply_comp.to_PumpVariableSpeed.get
458
+ vsPump_info = {}
459
+ plant_loop_info[:chillerpumps] << vsPump_info
460
+ vsPump_info[:name] = vsPump.name.get
461
+ if vsPump.isRatedPowerConsumptionAutosized.to_bool
462
+ vsPumpSize = vsPump.autosizedRatedPowerConsumption.to_f
463
+ else
464
+ vsPumpSize = vsPump.ratedPowerConsumption.to_f
465
+ end
466
+ vsPump_info[:size] = vsPumpSize.to_f # Watts
467
+ if vsPump.isRatedFlowRateAutosized.to_bool
468
+ vsPump_info[:water_flow_m3_per_s] = vsPump.autosizedRatedFlowRate.to_f
469
+ else
470
+ vsPump_info[:water_flow_m3_per_s] = vsPump.ratedFlowRate.to_f
471
+ end
472
+ end
473
+ end
474
+ end
475
+
476
+ # Get costs associated with each chiller
477
+ numChillers = 0 ; multiplier = 1.0
478
+ primaryFuel = ''; primaryCap = 0
479
+
480
+ plant_loop_info[:chillers].each do |chiller|
481
+
482
+ # Get primary/secondary/backup chiller cost based on type and capacity for each chiller
483
+ # 06-Sep-2019 JTB: Added check for no 'Primary' or 'Secondary' label and assume primary.
484
+ # This chiller prefix name seemed to disappear after the heat pump work was committed.
485
+ numChillers += 1
486
+ if chiller[:type].to_s.downcase == 'airtowaterhp'
487
+ primaryFuel = chiller[:fuel]
488
+ primaryCap = chiller[:reference_capacity] #kW
489
+ # Add cooling buffer tank for awhp
490
+ materialHash = get_cost_info(mat: 'solartank', size: 450)
491
+ matCost, labCost = getCost('solartank', materialHash, multiplier) #Costing for AWHP only buffer tank, AWHP included in boiler cost
492
+ thisChillerCost = matCost * regional_material / 100.0 + labCost * regional_installation / 100.0
493
+ # Include 2 expansion tanks for awhp
494
+ materialHash = get_cost_info(mat: 'ExpansionTanks', size: 60)
495
+ matCost, labCost = getCost('ExpansionTanks', materialHash, multiplier) #Costing for AWHP only buffer tank, AWHP included in boiler cost
496
+ thisChillerCost += (matCost * regional_material / 100.0 + labCost * regional_installation / 100.0) * 2
497
+ # Inclide glycol cost
498
+ materalHash = get_cost_info(mat: 'glycol')
499
+ matCost, labCost = getCost('solartank', materialHash, multiplier) #Costing for AWHP only buffer tank, AWHP included in boiler cost
500
+ thisChillerCost += (matCost * regional_material / 100.0 + labCost * regional_installation / 100.0) * 2
501
+
502
+ flueVentCost = 0.0 ; flueElbowCost = 0.0 ; flueTopCost = 0.0 ; headerCost = 0.0
503
+ elsif ((chiller[:name].to_s.downcase =~ /primary/i || (chiller[:name] !~ /primary/i && chiller[:name] !~ /secondary/i && numChillers == 1)) || (@gshp_flag))
504
+ primaryFuel = chiller[:fuel]
505
+ primaryCap = chiller[:reference_capacity] #kW
506
+ if not chiller[:name].include?("ChillerElectricEIR_VSDCentrifugalWaterChiller")
507
+ matCost, labCost = getHVACCost(chiller[:name], chiller[:type], chiller[:reference_capacity], false)
508
+ thisChillerCost = matCost * regional_material / 100.0 + labCost * regional_installation / 100.0
509
+ elsif chiller[:name].include?("ChillerElectricEIR_VSDCentrifugalWaterChiller")
510
+ thisChillerCost = vsd_chiller_cost(primaryCap: primaryCap)
511
+ end
512
+ # Flue cost for gas (absorption) chillers!
513
+ if chiller[:fuel] == 'NaturalGas'
514
+ # Calculate flue costs once for all chillets since flues combined by header when multiple chillers
515
+ # 6 inch diameter flue (#384)
516
+ materialHash = get_cost_info(mat: 'Venting', size: 6)
517
+ matCost, labCost = getCost('flue', materialHash, multiplier)
518
+ flueVentCost = matCost * regional_material / 100.0 + labCost * regional_installation / 100.0
519
+
520
+ #6 inch elbow fitting (#386)
521
+ materialHash = get_cost_info(mat: 'VentingElbow', size: 6)
522
+ matCost, labCost = getCost('flue elbow', materialHash, multiplier)
523
+ flueElbowCost = matCost * regional_material / 100.0 + labCost * regional_installation / 100.0
524
+
525
+ # 6 inch top (#392)
526
+ materialHash = get_cost_info(mat: 'VentingTop', size: 6)
527
+ matCost, labCost = getCost('flue top', materialHash, multiplier)
528
+ flueTopCost = matCost * regional_material / 100.0 + labCost * regional_installation / 100.0
529
+
530
+ # Gas line piping cost per ft (#1)
531
+ materialHash = get_cost_info(mat: 'GasLine', unit: 'L.F.')
532
+ matCost, labCost = getCost('fuel line', materialHash, multiplier)
533
+ fuelLineCost = matCost * regional_material / 100.0 + labCost * regional_installation / 100.0
534
+
535
+ # Gas line fitting connection per boiler (#2)
536
+ materialHash = get_cost_info(mat: 'GasLine', unit: 'each')
537
+ matCost, labCost = getCost('fuel line fitting connection', materialHash, multiplier)
538
+ fuelFittingCost = matCost * regional_material / 100.0 + labCost * regional_installation / 100.0
539
+
540
+
541
+ # Header cost only non-zero if there is a secondary/backup gas/oil boiler
542
+ headerCost = 0.0
543
+ else # Electric
544
+ flueVentCost = 0.0 ; flueElbowCost = 0.0 ; flueTopCost = 0.0 ; headerCost = 0.0
545
+ end
546
+
547
+ # Electric utility costs (i.e., power lines).
548
+ # Calculate utility cost components for primary chiller only since multiple chillers use common utilities
549
+
550
+ # elec 600V #14 wire /100 ft (#848)
551
+ materialHash = get_cost_info(mat: 'Wiring', size: 14)
552
+ matCost, labCost = getCost('electrical wire - 600V #14', materialHash, multiplier)
553
+ elecWireCost = matCost * reg_mat_elec / 100.0 + labCost * reg_lab_elec / 100.0
554
+
555
+ # 1 inch metal conduit (#851)
556
+ materialHash = get_cost_info(mat: 'Conduit', unit: 'L.F.')
557
+ matCost, labCost = getCost('1 inch metal conduit', materialHash, multiplier)
558
+ metalConduitCost = matCost * reg_mat_elec / 100.0 + labCost * reg_lab_elec / 100.0
559
+
560
+ if chiller[:fuel] == 'NaturalGas'
561
+ # Gas chillers require fuel line+valves+connectors and electrical conduit
562
+ utilCost += (fuelLineCost + metalConduitCost) * util_dist + fuelFittingCost + elecWireCost * util_dist / 100
563
+
564
+ else # Electric
565
+ # Electric chillers require only conduit
566
+ utilCost += metalConduitCost * util_dist + elecWireCost * util_dist / 100
567
+ end
568
+
569
+ elsif (chiller[:name].to_s.downcase =~ /secondary/i || numChillers > 1)
570
+ if chiller[:reference_capacity] <= 0.1
571
+ # Chiller cost is zero!
572
+ thisChillerCost = 0.0
573
+ numChillers -= 1
574
+ else
575
+ # A secondary chiller exists so use it for costing
576
+ if not chiller[:name].include?("ChillerElectricEIR_VSDCentrifugalWaterChiller")
577
+ matCost, labCost = getHVACCost(chiller[:name], chiller[:type], chiller[:reference_capacity], false)
578
+ thisChillerCost = matCost * regional_material / 100.0 + labCost * regional_installation / 100.0
579
+ elsif chiller[:name].include?("ChillerElectricEIR_VSDCentrifugalWaterChiller")
580
+ thisChillerCost = vsd_chiller_cost(primaryCap: primaryCap)
581
+ end
582
+ end
583
+
584
+ # Flue costs set to zero if secondary chiler since already calculated in primary (if gas absorption)
585
+ flueVentCost = 0.0; flueElbowCost = 0.0; flueTopCost = 0.0
586
+
587
+ # Check if need a flue header (i.e., both primary and secondary chillers are gas absorption)
588
+ if thisChillerCost > 0.0 && primaryFuel == 'NaturalGas' && chiller[:fuel] == 'NaturalGas'
589
+ # 6 inch diameter header (#384)
590
+ materialHash = get_cost_info(mat: 'Venting', size: 6)
591
+ matCost, labCost = getCost('flue header', materialHash, multiplier)
592
+ headerVentCost = matCost * regional_material / 100.0 + labCost * regional_installation / 100.0
593
+
594
+ #6 inch elbow fitting for header (#386)
595
+ materialHash = get_cost_info(mat: 'VentingElbow', size: 6)
596
+ matCost, labCost = getCost('flue header elbow', materialHash, multiplier)
597
+ headerElbowCost = matCost * regional_material / 100.0 + labCost * regional_installation / 100.0
598
+
599
+ # Assume a header length of 20 ft and an elbow fitting for each boiler connected to the header
600
+ headerCost = (headerVentCost * 20 + headerElbowCost) * numChillers
601
+ else
602
+ headerCost = 0.0
603
+ end
604
+ end
605
+ chillerCost += thisChillerCost
606
+ flueCost += flueVentCost * ht_roof + flueElbowCost + flueTopCost + headerCost
607
+ if numChillers > 1 && primaryFuel == 'NaturalGas'
608
+ # Adjust utility cost for extra fuel line fitting cost
609
+ utilCost += fuelFittingCost * (numChillers - 1)
610
+ end
611
+ if numChillers < 2
612
+ # Create a cost for a backup chiller by doubling cost of primary chiller
613
+ # 2023-04-25: Although backup chillers may be modeled we are no longer counting them.
614
+ #chillerCost *= 2.0
615
+ numChillers = 1
616
+ end
617
+ end
618
+
619
+ # Chiller pump costs
620
+ pumpCost = 0.0; pipingToPumpCost = 0.0; numPumps = 0; pumpName = ''; pumpSize = 0.0 ; pumpFlow = 0.0
621
+ plant_loop_info[:chillerpumps].each do |pump|
622
+ numPumps += 1
623
+ # Cost variable and constant volume pumps the same (the difference is in extra cost for VFD controller)
624
+ pumpSize = pump[:size]; pumpName = pump[:name]
625
+ pumpFlow += pump[:water_flow_m3_per_s].to_f
626
+ matCost, labCost = getHVACCost(pumpName, 'Pumps', pumpSize, false)
627
+ indpumpCost = matCost * regional_material / 100.0 + labCost * regional_installation / 100.0
628
+ if pump[:name] =~ /variable/i
629
+ # Cost the VFD controller for the variable pump costed above
630
+ matCost, labCost = getHVACCost(pumpName, 'VFD', pumpSize, false)
631
+ indpumpCost += matCost * reg_mat_elec / 100.0 + labCost * reg_lab_elec / 100.0
632
+ end
633
+ if awhp_chiller
634
+ pumpCost += indpumpCost * 2
635
+ else
636
+ pumpCost += indpumpCost
637
+ end
638
+ end
639
+ if (numChillers > 1 && numPumps < 2)
640
+ # Add pump costing for additional chillers
641
+ pumpCost *= 2.0
642
+ numPumps = 2 # reset the number of pumps for piping costs below
643
+ numChillers = 2
644
+ end
645
+ # Double the pump costs to accomodate the costing of backup pumps for each chiller!
646
+ # No longer costing backup pump CK 2023-06-23
647
+ #pumpCost *= 2.0
648
+
649
+ # Chiller water piping cost: Add piping elbows, valves and insulation from the chiller(s)
650
+ # to the pumps(s) assuming a pipe diameter of 1” and a distance of 10 ft per pump
651
+ if numChillers > 0
652
+ # 1 inch Steel pipe
653
+ matCost, labCost = getHVACCost('1 inch steel pipe', 'SteelPipe', 1)
654
+ pipingToPumpCost = 10.0 * numPumps * (matCost * regional_material / 100.0 + labCost * regional_installation / 100.0)
655
+
656
+ # 1 inch Steel pipe insulation
657
+ matCost, labCost = getHVACCost('1 inch pipe insulation', 'PipeInsulation', 1)
658
+ pipingToPumpCost += 10.0 * numPumps * (matCost * regional_material / 100.0 + labCost * regional_installation / 100.0)
659
+
660
+ # 1 inch Steel pipe elbow
661
+ matCost, labCost = getHVACCost('1 inch steel pipe elbow', 'SteelPipeElbow', 1)
662
+ pipingToPumpCost += 2.0 * numPumps * (matCost * regional_material / 100.0 + labCost * regional_installation / 100.0)
663
+
664
+ # 1 inch gate valves
665
+ matCost, labCost = getHVACCost('1 inch gate valves', 'ValvesGate', 1)
666
+ pipingToPumpCost += 1.0 * numPumps * (matCost * regional_material / 100.0 + labCost * regional_installation / 100.0)
667
+ end
668
+
669
+ if numChillers > 0
670
+ # Double pump piping cost to account for second chiller
671
+ pipingToPumpCost *= 2 if awhp_chiller
672
+ pipingToPumpCost *= numChillers
673
+
674
+ hdrDistributionCost = getHeaderPipingDistributionCost(numAGFlrs, mechRmInBsmt, regional_material, regional_installation, reg_mat_elec, reg_lab_elec,
675
+ pumpFlow, horz_dist, nom_flr_hght)
676
+ else
677
+ pipingToPumpCost = 0
678
+ hdrDistributionCost = 0
679
+ end
680
+
681
+ totalCost = chillerCost + flueCost + utilCost + pumpCost + pipingToPumpCost + hdrDistributionCost
682
+
683
+ @costing_report['heating_and_cooling']['plant_equipment'] << {
684
+ 'type' => 'chillers',
685
+ 'nom_flr2flr_hght_ft' => nom_flr_hght.round(1),
686
+ 'ht_roof_ft' => ht_roof.round(1),
687
+ 'longest_distance_to_ext_ft' => horz_dist.round(1),
688
+ 'wiring_and_gas_connections_distance_ft' => util_dist.round(1),
689
+ 'equipment_cost' => chillerCost.round(0),
690
+ 'flue_cost' => flueCost.round(0),
691
+ 'wiring_and_gas_connections_cost' => utilCost.round(0),
692
+ 'pump_cost' => pumpCost.round(0),
693
+ 'piping_to_pump_cost' => pipingToPumpCost.round(0),
694
+ 'header_distribution_cost' => hdrDistributionCost.round(0),
695
+ 'total_cost' => totalCost.round(0)
696
+ }
697
+
698
+ puts "\nHVAC Chiller costing data successfully generated. Total chiller costs: $#{totalCost.round(0)}"
699
+
700
+ return totalCost
701
+ end
702
+
703
+ # ----------------------------------------------------------------------------------------------
704
+ # Cooling tower (i.e., chiller condensor loop cooling) costing
705
+ # ----------------------------------------------------------------------------------------------
706
+ def coolingtower_costing(model, prototype_creator)
707
+
708
+ totalCost = 0.0
709
+
710
+ # Get regional cost factors for this province and city
711
+ materials_hvac = @costing_database["raw"]["materials_hvac"]
712
+ hvac_material = materials_hvac.select {|data|
713
+ data['Material'].to_s == "GasBoilers"}.first # Get any row from spreadsheet in case of region error
714
+ regional_material, regional_installation =
715
+ get_regional_cost_factors(@costing_report['province_state'], @costing_report['city'], hvac_material)
716
+
717
+ # Get regional electric cost factors for this province and city
718
+ hvac_material = materials_hvac.select {|data|
719
+ data['Material'].to_s.upcase == "BOX" && data['Size'].to_i == 1}.first
720
+ reg_mat_elec, reg_lab_elec =
721
+ get_regional_cost_factors(@costing_report['province_state'], @costing_report['city'], hvac_material)
722
+
723
+ # Store some geometry data for use below...
724
+ util_dist, ht_roof, nom_flr_hght, horz_dist, numAGFlrs, mechRmInBsmt = getGeometryData(model, prototype_creator)
725
+
726
+ template_type = prototype_creator.template
727
+
728
+ cltowerCost = 0.0
729
+ thisClTowerCost = 0.0
730
+ utilCost = 0.0
731
+ plant_loop_info = {}
732
+ plant_loop_info[:coolingtowers] = []
733
+ plant_loop_info[:coolingtowerpumps] = []
734
+ plant_loop_info[:groundloops] = []
735
+ cltowertype = 'cooling_towers'
736
+
737
+ # Iterate through the plant loops to get cooling tower & pump data...
738
+ model.getPlantLoops.each do |plant_loop|
739
+ next unless (plant_loop.name.get.to_s =~ /Condenser Water Loop/i) || (plant_loop.name.get.to_s =~ /Condenser PlantLoop GLHX/i)
740
+ plant_loop.supplyComponents.each do |supply_comp|
741
+ if supply_comp.to_CoolingTowerSingleSpeed.is_initialized
742
+ cltower = supply_comp.to_CoolingTowerSingleSpeed.get
743
+ cltower_info = {}
744
+ plant_loop_info[:coolingtowers] << cltower_info
745
+ cltower_info[:name] = cltower.name.get
746
+ cltower_info[:type] = 'ClgTwr' # Material lookup name
747
+ cltower_info[:fanPoweratDesignAirFlowRate] = cltower.fanPoweratDesignAirFlowRate.to_f / 1000 # kW
748
+ cltower_info[:capacity] = model.sqlFile().get().execAndReturnFirstDouble("SELECT Value FROM " +
749
+ "TabularDataWithStrings WHERE ReportName='EquipmentSummary' AND ReportForString='Entire Facility' AND " +
750
+ "TableName='Central Plant' AND ColumnName='Nominal Capacity' AND " +
751
+ "RowName='#{cltower_info[:name].upcase}' ").to_f / 1000 # kW
752
+ elsif supply_comp.to_PumpConstantSpeed.is_initialized
753
+ csPump = supply_comp.to_PumpConstantSpeed.get
754
+ csPump_info = {}
755
+ plant_loop_info[:coolingtowerpumps] << csPump_info
756
+ csPump_info[:name] = csPump.name.get
757
+ if csPump.isRatedPowerConsumptionAutosized.to_bool
758
+ csPumpSize = csPump.autosizedRatedPowerConsumption.to_f
759
+ else
760
+ csPumpSize = csPump.ratedPowerConsumption.to_f
761
+ end
762
+ csPump_info[:size] = csPumpSize.to_f # Watts
763
+ elsif supply_comp.to_PumpVariableSpeed.is_initialized
764
+ vsPump = supply_comp.to_PumpVariableSpeed.get
765
+ vsPump_info = {}
766
+ plant_loop_info[:coolingtowerpumps] << vsPump_info
767
+ vsPump_info[:name] = vsPump.name.get
768
+ if vsPump.isRatedPowerConsumptionAutosized.to_bool
769
+ vsPumpSize = vsPump.autosizedRatedPowerConsumption.to_f
770
+ else
771
+ vsPumpSize = vsPump.ratedPowerConsumption.to_f
772
+ end
773
+ vsPump_info[:size] = vsPumpSize.to_f # Watts
774
+ elsif supply_comp.to_DistrictHeating.is_initialized
775
+ groundLoop = supply_comp.to_DistrictHeating.get
776
+ groundLoop_info = {}
777
+ groundLoop_info[:name] = groundLoop.name.to_s
778
+ if groundLoop.isNominalCapacityAutosized.to_bool
779
+ groundLoop_info[:nominal_capacity] = groundLoop.autosizedNominalCapacity.to_f/1000.0
780
+ else
781
+ groundLoop_info[:nominal_capacity] = groundLoop.nominalCapacity.to_f/1000.0
782
+ end
783
+ # Get flow rate to ground loop
784
+ if plant_loop.isMaximumLoopFlowRateAutosized.to_bool
785
+ groundLoop_info[:plant_loop_flow_rate_m3ps] = plant_loop.autosizedMaximumLoopFlowRate.to_f
786
+ else
787
+ groundLoop_info[:plant_loop_flow_rate_m3ps] = plant_loop.maximumLoopFlowRate.to_f
788
+ end
789
+ plant_loop_info[:groundloops] << groundLoop_info
790
+ elsif supply_comp.to_DistrictCooling.is_initialized
791
+ groundLoop = supply_comp.to_DistrictCooling.get
792
+ groundLoop_info = {}
793
+ groundLoop_info[:name] = groundLoop.name.to_s
794
+ if groundLoop.isNominalCapacityAutosized.to_bool
795
+ groundLoop_info[:nominal_capacity] = groundLoop.autosizedNominalCapacity.to_f/1000.0
796
+ else
797
+ groundLoop_info[:nominal_capacity] = groundLoop.nominalCapacity.to_f/1000.0
798
+ end
799
+ # Get flow rate to ground loop
800
+ if plant_loop.isMaximumLoopFlowRateAutosized.to_bool
801
+ groundLoop_info[:plant_loop_flow_rate_m3ps] = plant_loop.autosizedMaximumLoopFlowRate.to_f
802
+ else
803
+ groundLoop_info[:plant_loop_flow_rate_m3ps] = plant_loop.maximumLoopFlowRate.to_f
804
+ end
805
+ plant_loop_info[:groundloops] << groundLoop_info
806
+ end
807
+ end
808
+ end
809
+
810
+ # Get costs associated with each cooling tower
811
+ numTowers = 0 ; multiplier = 1.0
812
+
813
+ plant_loop_info[:coolingtowers].each do |cltower|
814
+ # Get cooling tower cost based on capacity
815
+ numTowers += 1
816
+ if numTowers == 1
817
+ matCost, labCost = getHVACCost(cltower[:name], cltower[:type], cltower[:capacity], false)
818
+ thisClTowerCost = matCost * regional_material / 100.0 + labCost * regional_installation / 100.0
819
+ else # Multiple cooling towers
820
+ if cltower[:capacity] <= 0.1
821
+ # Cooling tower cost is zero!
822
+ thisClTowerCost = 0.0
823
+ else
824
+ # A second cooling tower exists so use it for costing
825
+ matCost, labCost = getHVACCost(cltower[:name], cltower[:type], cltower[:capacity], false)
826
+ thisClTowerCost = matCost * regional_material / 100.0 + labCost * regional_installation / 100.0
827
+ end
828
+ end
829
+ cltowerCost += thisClTowerCost
830
+
831
+ # Electric utility costs (i.e., power lines) for cooling tower(s).
832
+
833
+ # elec 600V #14 wire /100 ft (#848)
834
+ materialHash = get_cost_info(mat: 'Wiring', size: 14)
835
+ matCost, labCost = getCost('electrical wire - 600V #14', materialHash, multiplier)
836
+ elecWireCost = matCost * reg_mat_elec / 100.0 + labCost * reg_lab_elec / 100.0
837
+
838
+ # 1 inch metal conduit (#851)
839
+ materialHash = get_cost_info(mat: 'Conduit', unit: 'L.F.')
840
+ matCost, labCost = getCost('1 inch metal conduit', materialHash, multiplier)
841
+ metalConduitCost = matCost * reg_mat_elec / 100.0 + labCost * reg_lab_elec / 100.0
842
+
843
+ utilCost += metalConduitCost * (ht_roof + 20) + elecWireCost * (ht_roof + 20) / 100
844
+ end
845
+
846
+ # Cooling Tower (condensor) pump costs
847
+ pumpCost = 0.0; pipingCost = 0.0; numPumps = 0; pumpName = ''; pumpSize = 0.0
848
+ plant_loop_info[:coolingtowerpumps].each do |pump|
849
+ numPumps += 1
850
+ # Cost variable and constant volume pumps the same (VFD controller added if variable)
851
+ pumpSize = pump[:size]; pumpName = pump[:name]
852
+ matCost, labCost = getHVACCost(pumpName, 'Pumps', pumpSize, false)
853
+ pumpCost += matCost * regional_material / 100.0 + labCost * regional_installation / 100.0
854
+ if pump[:name] =~ /variable/i
855
+ # Cost the VFD controller for the variable pump costed above
856
+ pumpSize = pump[:size]; pumpName = pump[:name]
857
+ matCost, labCost = getHVACCost(pumpName, 'VFD', pumpSize, false)
858
+ pumpCost += matCost * reg_mat_elec / 100.0 + labCost * reg_lab_elec / 100.0
859
+ end
860
+ end
861
+ if numTowers > 1 && numPumps < 2
862
+ # Add pump costing for the backup pump.
863
+ # 2023-04-25: Not including backup tower or pump costs
864
+ # pumpCost *= 2.0
865
+ end
866
+ # Double the pump costs to accomodate the costing of a backup pump(s)!
867
+ # 2023-04-25 No longer including backup pumps
868
+ #pumpCost *= 2.0
869
+ #numPumps = 2 # reset the number of pumps for piping costs below
870
+
871
+
872
+ # Chiller water piping cost: Add piping elbows, valves and insulation from the chiller(s)
873
+ # to the pumps(s) assuming a pipe diameter of 1” and a distance of 10 ft per pump
874
+ if numTowers > 0
875
+ # 4 inch Steel pipe (vertical + horizontal)
876
+ matCost, labCost = getHVACCost('4 inch steel pipe', 'SteelPipe', 4)
877
+ pipingCost += (ht_roof * 2 + 10 * numPumps) * (matCost * regional_material / 100.0 + labCost * regional_installation / 100.0)
878
+
879
+ # 4 inch Steel pipe insulation (vertical + horizontal)
880
+ matCost, labCost = getHVACCost('1 inch pipe insulation', 'PipeInsulation', 4)
881
+ pipingCost += (ht_roof * 2 + 10 * numPumps) * (matCost * regional_material / 100.0 + labCost * regional_installation / 100.0)
882
+
883
+ # 4 inch Steel pipe elbow
884
+ matCost, labCost = getHVACCost('4 inch steel pipe tee', 'SteelPipeTee', 4)
885
+ pipingCost += 1.0 * numPumps * (matCost * regional_material / 100.0 + labCost * regional_installation / 100.0)
886
+
887
+ # 4 inch valves
888
+ matCost, labCost = getHVACCost('4 inch BFly valves', 'ValvesBFly', 4)
889
+ pipingCost += 1.0 * numPumps * (matCost * regional_material / 100.0 + labCost * regional_installation / 100.0)
890
+ end
891
+
892
+ # Note: No extra costs for piping for backup condenser pump or multiple cooling towers.
893
+
894
+ # Calculate GSHP ground loop cost and interior piping
895
+ unless plant_loop_info[:groundloops].empty?
896
+ # GSHP ground loop cost
897
+ # Not applying localization because costs are currently national estimates only (2023-06-19)
898
+ cltowertype = 'ground_loop'
899
+ largest_loop = plant_loop_info[:groundloops].max_by { |groundloop| groundloop[:nominal_capacity] }
900
+ nominal_capacity = largest_loop[:nominal_capacity].to_f
901
+ matCost, labCost = getHVACCost('GSHP ground loop', 'gshp_ground_loop', '')
902
+ cltowerCost = matCost * nominal_capacity
903
+
904
+ # GSHP piping from building to bore field cost
905
+ # Not applying localization because costs are currently national estimates only (2023-06-19)
906
+ gshp_dist = 50
907
+ loop_flow = largest_loop[:plant_loop_flow_rate_m3ps]
908
+ pipe_dia_mm = d = Math.sqrt(1.273*loop_flow/2.0)*1000
909
+ matCost, labCost = getHVACCost('GSHP outdoor piping cost', 'gshp_buried_pipe', pipe_dia_mm, false)
910
+ pipingCost = matCost * gshp_dist
911
+
912
+ # Interior piping cost
913
+ # Get mechanical room lacotion (assume this is where the GSHP is)
914
+ mech_room, cond_spaces = prototype_creator.find_mech_room(model)
915
+ mech_centroid = mech_room["space_centroid"]
916
+ # Determine the length to the largest exterior wall
917
+ ground_spaces = []
918
+ pipe_dists = []
919
+ # Determine the exterior ground walls touching the ground
920
+ # Get the spaces
921
+ model.getSpaces.sort.each do |space|
922
+ ground_surf = false
923
+ # Get the surfaces for the space and determine if any are contacting the ground. If one is add it to the arroy
924
+ # of spaces
925
+ space.surfaces.sort.each do |surface|
926
+ if surface.isGroundSurface
927
+ ground_surf = true
928
+ end
929
+ end
930
+ ground_spaces << space if ground_surf == true
931
+ end
932
+ # Go through all of the spaces contacting the ground
933
+ ground_spaces.sort.each do |ground_space|
934
+ # Go through all of the surfaces for the space and determine which are exterior or foundation walls
935
+ ext_walls = ground_space.surfaces.select{ |surf| surf.surfaceType == 'Wall' && (surf.outsideBoundaryCondition == 'Outdoors' || surf.outsideBoundaryCondition == 'Foundation')}
936
+ # Get the largest exterior wall and the distance to its centroid to the mech room centroid
937
+ unless ext_walls.empty?
938
+ ext_wall = ext_walls.max_by { |ext_wall| ext_wall.grossArea.to_f }
939
+ pipe_dists << {
940
+ wall: ext_wall,
941
+ 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)
942
+ }
943
+ end
944
+ end
945
+ # Find the shortest distance to the 3 largest walls and pick the shortest one
946
+ largest_walls = pipe_dists.max_by(3) { |wall| wall[:wall].grossArea.to_f }
947
+ pipe_dist = largest_walls.min_by { |wall| wall[:pipe_dist].to_f }
948
+ pipe_dist_ft = (OpenStudio.convert(pipe_dist[:pipe_dist], 'm', 'ft').get)
949
+ pipe_dia_mm = d = Math.sqrt(1.273*loop_flow/2.0)*1000
950
+ pipe_dia_inch = (OpenStudio.convert(pipe_dia_mm/1000, 'm', 'ft').get)*12.0
951
+ pipe_dia_inch = 8.0 if pipe_dia_inch >= 8.0
952
+
953
+ # Include localization foctors in interior piping and fixtures
954
+ # Cost the interior pipe
955
+ matCost, labCost = getHVACCost('GSHP indoor piping cost', 'SteelPipe', pipe_dia_inch, false)
956
+ pipingCost += (matCost*regional_material + labCost*regional_installation)*pipe_dist_ft*2.0/100.0
957
+
958
+ # Cost the pipe insulation
959
+ pipe_dia_inch = 4.0 if pipe_dia_inch > 4.0
960
+ matCost, labCost = getHVACCost('GSHP indoor pipe insulation', 'PipeInsulation', pipe_dia_inch, false)
961
+ pipingCost += (matCost*regional_material + labCost*regional_installation)*pipe_dist_ft*2.0/100.0
962
+
963
+ # Cost 1 valve
964
+ matCost, labCost = getHVACCost('GSHP indoor pipe valve', 'ValvesBig', 4.0)
965
+ pipingCost += (matCost*regional_material + labCost*regional_installation)/100.0
966
+
967
+ # Cost 2 pipe tees
968
+ matCost, labCost = getHVACCost('GSHP indoor pipe tees', 'SteelPipeTee', 4.0)
969
+ pipingCost += (matCost*regional_material + labCost*regional_installation)*2.0/100.0
970
+
971
+ # Cost 8 pipe tees
972
+ matCost, labCost = getHVACCost('GSHP indoor pipe elbows', 'SteelPipeElbow', 4.0)
973
+ pipingCost += (matCost*regional_material + labCost*regional_installation)*8.0/100.0
974
+ end
975
+ totalCost = cltowerCost + utilCost + pumpCost + pipingCost
976
+
977
+ @costing_report['heating_and_cooling']['plant_equipment'] << {
978
+ 'type' => cltowertype,
979
+ 'nom_flr2flr_hght_ft' => nom_flr_hght.round(1),
980
+ 'ht_roof_ft' => ht_roof.round(1),
981
+ 'longest_distance_to_ext_ft' => horz_dist.round(1),
982
+ 'wiring_and_gas_connections_distance_ft' => util_dist.round(1),
983
+ 'equipment_cost' => cltowerCost.round(0),
984
+ 'wiring_and_gas_connections_cost' => utilCost.round(0),
985
+ 'pump_cost' => pumpCost.round(0),
986
+ 'piping_cost' => pipingCost.round(0),
987
+ 'total_cost' => totalCost.round(0)
988
+ }
989
+
990
+ puts "\nHVAC Cooling Tower costing data successfully generated. Total cooling tower costs: $#{totalCost.round(0)}"
991
+
992
+ return totalCost
993
+ end
994
+
995
+
996
+ # This method determines how many pieces of equipment are required to satisfy the required size if 1 piece is not
997
+ # enough. It takes in:
998
+ # materialLookup(String): The name to search for in the 'Material' column of the materials_hvac sheet of the costing
999
+ # spreadsheet.
1000
+ # materialSize(Float): The size to search for in the 'Size' column of teh materials_hvac sheet of the costing
1001
+ # spreadsheet.
1002
+ # It returns:
1003
+ # multiplier(Float): Number of materialLookup with the largest size required to meet the materialSize.
1004
+ def get_HVAC_multiplier(materialLookup, materialSize)
1005
+ multiplier = 1.0
1006
+ materials_hvac = @costing_database['raw']['materials_hvac'].select {|data|
1007
+ data['Material'].to_s.upcase == materialLookup.to_s.upcase
1008
+ }
1009
+ if materials_hvac.nil?
1010
+ puts("Error: no hvac information available for equipment #{materialLookup}!")
1011
+ raise
1012
+ elsif materials_hvac.empty?
1013
+ puts("Error: no hvac information available for equipment #{materialLookup}!")
1014
+ raise
1015
+ end
1016
+ materials_hvac.length == 1 ? max_size = materials_hvac[0] : max_size = materials_hvac.max_by {|d| d['Size'].to_f}
1017
+ if max_size['Size'].to_f <= 0
1018
+ 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!")
1019
+ raise
1020
+ end
1021
+ mult = materialSize.to_f / (max_size['Size'].to_f)
1022
+ multiplier = (mult.to_i).to_f.round(0) + 1 # Use next largest integer for multiplier
1023
+ return multiplier.to_f
1024
+ end
1025
+
1026
+ # This method provides the material and labour cost for a required piece of equimpment. It takes in:
1027
+ # name(String): The name of a piece of equipment. Is only used for error reporting and is not linked to anything
1028
+ # else.
1029
+ # materialLookup(String): The material type used to search hte 'Material' column of the materials_hvac sheet of the
1030
+ # costing spreadsheet.
1031
+ # materialSize(float): The size of the equipment in whichever units are required when searching the 'Size' column of
1032
+ # the costing spreadsheet.
1033
+ # exactMatch(true/false): A flag to indicate if the hvac equipment must match the size provided exactly or if the
1034
+ # size is a minimum equipment size.
1035
+ # It returns the material cost ond labor cost for the equipment including any multipliers.
1036
+ def getHVACCost(name, materialLookup, materialSize, exactMatch=true)
1037
+ eqCostInfo = getHVACDBInfo(name: name, materialLookup: materialLookup, materialSize: materialSize, exactMatch: exactMatch)
1038
+ return getCost(eqCostInfo[:name], eqCostInfo[:hvac_material], eqCostInfo[:multiplier])
1039
+ end
1040
+
1041
+ # This method was originally part of getHVACCOST but was split out because in some cases the information from the
1042
+ # materials_hvac sheet of the costing spreadsheet was required but not tho cost.
1043
+ # The method takes in:
1044
+ # name(String): The name of a piece of equipment. Is only used for error reporting and is not linked to anything
1045
+ # else.
1046
+ # materialLookup(String): The material type used to search hte 'Material' column of the materials_hvac sheet of the
1047
+ # costing spreadsheet.
1048
+ # materialSize(float): The size of the equipment in whichever units are required when searching the 'Size' column of
1049
+ # the costing spreadsheet.
1050
+ # exactMatch(true/false): A flag to indicate if the hvac equipment must match the size provided exactly or if the
1051
+ # size is a minimum equipment size.
1052
+ #
1053
+ # The method returns a hash with the following composition:
1054
+ # {
1055
+ # name(string): Same as above.
1056
+ # hvac_material(hash): The costing spreadsheet information for the hvac equipment being searched for.
1057
+ # multiplier(float): Default is 1. Will be higher if exactMatch is false, and no materialLookup could be found with
1058
+ # a large enough materialSize in the costing spreadsheet. In this case, it is assumed that several
1059
+ # pieced of equipment defined by hvact_material are used to satisfy the required materialSize.
1060
+ # The multiplier defines the number of hvac_material required to meet the materialSize
1061
+ # }
1062
+ def getHVACDBInfo(name:, materialLookup:, materialSize:, exactMatch: true)
1063
+ multiplier = 1.0
1064
+ materials_hvac = @costing_database["raw"]["materials_hvac"]
1065
+ if materialSize == 'nil' || materialSize == '' || materialSize == nil
1066
+ # When materialSize is blank because there is only one row in the data sheet, the value is nil
1067
+ hvac_material = materials_hvac.select { |data| data['Material'].to_s.upcase == materialLookup.to_s.upcase }.first
1068
+ else
1069
+ if exactMatch
1070
+ hvac_material = materials_hvac.select {|data|
1071
+ data['Material'].to_s.upcase == materialLookup.to_s.upcase && data['Size'].to_f == materialSize
1072
+ }.first
1073
+ else
1074
+ hvac_material_info = materials_hvac.select {|data|
1075
+ data['Material'].to_s.upcase == materialLookup.to_s.upcase && data['Size'].to_f >= materialSize
1076
+ }
1077
+ if hvac_material_info.empty?
1078
+ hvac_material = nil
1079
+ elsif hvac_material_info.size == 1
1080
+ hvac_material = hvac_material_info[0]
1081
+ else
1082
+ hvac_material = hvac_material_info.min_by{|data| data['Size'].to_f}
1083
+ end
1084
+ end
1085
+ end
1086
+ if hvac_material.nil?
1087
+ if exactMatch
1088
+ puts "HVAC material error! Could not find #{name} in materials_hvac!"
1089
+ raise
1090
+ else
1091
+ # There is no exact match in the costing spreadsheet so redo search for next largest size
1092
+ hvac_material = materials_hvac.select {|data|
1093
+ data['Material'].to_s.upcase == materialLookup.to_s.upcase && data['Size'].to_f >= materialSize.to_f
1094
+ }.min_by{|mat_info| mat_info['Size'].to_f}
1095
+ if hvac_material.nil?
1096
+ # The nominal capacity is greater than the maximum value in the API data for this boiler!
1097
+ # Lookup cost for a capacity divided by the multiple of req'd size/max size.
1098
+ multiplier = get_HVAC_multiplier( materialLookup, materialSize )
1099
+ hvac_materials = materials_hvac.select {|data|
1100
+ data['Material'].to_s.upcase == materialLookup.to_s.upcase && data['Size'].to_f >= materialSize.to_f / multiplier.to_f
1101
+ }
1102
+ if hvac_materials.size == 0
1103
+ puts "HVAC material error! Could not find next largest size for #{name} in #{materials_hvac}"
1104
+ raise
1105
+ elsif hvac_materials.size == 1
1106
+ hvac_material = hvac_materials[0]
1107
+ else
1108
+ hvac_material = hvac_materials.min_by{|data| data['Size'].to_f}
1109
+ end
1110
+ end
1111
+ end
1112
+ end
1113
+ # Create the return hash.
1114
+ costDBInfo = {
1115
+ name: name,
1116
+ hvac_material: hvac_material,
1117
+ multiplier: multiplier
1118
+ }
1119
+ return costDBInfo
1120
+ end
1121
+
1122
+
1123
+ def getCost(materialType, materialHash, multiplier)
1124
+ material_cost = 0.0 ; labour_cost = 0.0
1125
+ costing_data = @costing_database['costs'].detect do |data|
1126
+ data['id'].to_s.upcase == materialHash['id'].to_s.upcase
1127
+ end
1128
+ if costing_data.nil?
1129
+ puts "HVAC #{materialType} with id #{materialHash['id']} not found in the costing database. Skipping."
1130
+ raise
1131
+ else
1132
+ # Get cost information from lookup.
1133
+ # Adjust for material and labour multiplier in costing spreadsheet 'materials_hvac' sheet 'material_mult' and
1134
+ # 'labour_mult' columns.
1135
+ (materialHash['material_mult'].nil?) || (materialHash['material_mult'].empty?) ? mat_mult = 1.0 : mat_mult = materialHash['material_mult'].to_f
1136
+ (materialHash['labour_mult'].nil?) || (materialHash['labour_mult'].empty?) ? lab_mult = 1.0 : lab_mult = materialHash['labour_mult'].to_f
1137
+ material_cost = costing_data['baseCosts']['materialOpCost'].to_f * multiplier * mat_mult
1138
+ labour_cost = costing_data['baseCosts']['laborOpCost'].to_f * multiplier * lab_mult
1139
+ end
1140
+ return material_cost, labour_cost
1141
+ end
1142
+
1143
+ def getGeometryData(model, prototype_creator)
1144
+ num_of_above_ground_stories = model.getBuilding.standardsNumberOfAboveGroundStories.to_i
1145
+ space_mod = OpenstudioStandards::Space
1146
+ if model.building.get.nominalFloortoFloorHeight().empty?
1147
+ volume = model.building.get.airVolume()
1148
+ flrArea = 0.0
1149
+ if model.building.get.conditionedFloorArea.empty?
1150
+ model.getThermalZones.sort.each do |tz|
1151
+ tz.spaces.sort.each do |tz_space|
1152
+ flrArea += tz_space.floorArea.to_f if ( (space_mod.space_cooled?(tz_space)) || (space_mod.space_heated?(tz_space)) )
1153
+ end
1154
+ flrArea += tz.floorArea
1155
+ end
1156
+ else
1157
+ flrArea = model.building.get.conditionedFloorArea().get
1158
+ end
1159
+ nominal_flr2flr_height = 0.0
1160
+ nominal_flr2flr_height = volume / flrArea unless flrArea <= 0.01
1161
+ else
1162
+ nominal_flr2flr_height = model.building.get.nominalFloortoFloorHeight.get
1163
+ end
1164
+
1165
+ # Location of mechanical room and utility distances for use below (space_centroid is an array
1166
+ # in mech_room hash containing the x,y and z coordinates of space centroid). Utility distance
1167
+ # uses the distance from the mech room centroid to the perimeter of the building.
1168
+ mech_room, cond_spaces = prototype_creator.find_mech_room(model)
1169
+ mech_room_story = nil
1170
+ target_cent = [mech_room['space_centroid'][0], mech_room['space_centroid'][1]]
1171
+ found = false
1172
+ model.getBuildingStorys.sort.each do |story|
1173
+ story.spaces.sort.each do |space|
1174
+ if space.nameString == mech_room['space_name']
1175
+ mech_room_story = story
1176
+ found = true
1177
+ break
1178
+ end
1179
+ end
1180
+ break if found
1181
+ end
1182
+ distance_info_hash = get_story_cent_to_edge( building_story: mech_room_story, prototype_creator: prototype_creator,
1183
+ target_cent: target_cent, full_length: false )
1184
+ horizontal_dist = distance_info_hash[:start_point][:line][:dist] # in metres
1185
+
1186
+ ht_roof = 0.0
1187
+ util_dist = 0.0
1188
+ mechRmInBsmt = false
1189
+ if mech_room['space_centroid'][2] < 0
1190
+ # Mechanical room is in the basement (z dimension is negative).
1191
+ mechRmInBsmt = true
1192
+ ht_roof = (num_of_above_ground_stories + 1) * nominal_flr2flr_height
1193
+ util_dist = nominal_flr2flr_height + horizontal_dist
1194
+ elsif mech_room['space_centroid'][2] == 0
1195
+ # Mech room on ground floor
1196
+ ht_roof = num_of_above_ground_stories * nominal_flr2flr_height
1197
+ util_dist = horizontal_dist
1198
+ else
1199
+ # Mech room on some other floor
1200
+ ht_roof = (num_of_above_ground_stories - (mech_room['space_centroid'][2]/nominal_flr2flr_height).round(0)) * nominal_flr2flr_height
1201
+ util_dist = ht_roof + horizontal_dist
1202
+ end
1203
+
1204
+ util_dist = OpenStudio.convert(util_dist,"m","ft").get
1205
+ nominal_flr2flr_height = OpenStudio.convert(nominal_flr2flr_height,"m","ft").get
1206
+ ht_roof = OpenStudio.convert(ht_roof,"m","ft").get
1207
+ horizontal_dist = OpenStudio.convert(horizontal_dist,"m","ft").get
1208
+
1209
+ return util_dist, ht_roof, nominal_flr2flr_height, horizontal_dist, num_of_above_ground_stories, mechRmInBsmt
1210
+ end
1211
+
1212
+ # --------------------------------------------------------------------------------------------------
1213
+ # This function gets all costs associated zonal heating and cooling systems
1214
+ # (i.e., zonal units, pumps, flues & utility costs)
1215
+ # --------------------------------------------------------------------------------------------------
1216
+ def zonalsys_costing(model, prototype_creator, mech_room, cond_spaces)
1217
+
1218
+ totalCost = 0.0
1219
+
1220
+ # Get regional cost factors for this province and city
1221
+ materials_hvac = @costing_database["raw"]["materials_hvac"]
1222
+ hvac_material = materials_hvac.select {|data|
1223
+ data['Material'].to_s == "GasBoilers"}.first # Get any row from spreadsheet in case of region error
1224
+ regional_material, regional_installation =
1225
+ get_regional_cost_factors(@costing_report['province_state'], @costing_report['city'], hvac_material)
1226
+
1227
+ # Get regional electric cost factors for this province and city
1228
+ hvac_material = materials_hvac.select {|data|
1229
+ data['Material'].to_s.upcase == "BOX" && data['Size'].to_i == 1}.first
1230
+ reg_mat_elec, reg_lab_elec =
1231
+ get_regional_cost_factors(@costing_report['province_state'], @costing_report['city'], hvac_material)
1232
+
1233
+ # Store some geometry data for use below...
1234
+ util_dist, ht_roof, nom_flr_hght, horz_dist, numAGFlrs, mechRmInBsmt = getGeometryData(model, prototype_creator)
1235
+
1236
+ template_type = prototype_creator.template
1237
+
1238
+ zone_loop_info = {}
1239
+ zone_loop_info[:zonesys] = []
1240
+ numZones = 0; floorNumber = 0
1241
+ vrfSystemFloors = {
1242
+ maxCeil: -9999999999999,
1243
+ lowCeil: 9999999999999,
1244
+ vrfFloors: []
1245
+ }
1246
+
1247
+ model.getThermalZones.sort.each do |zone|
1248
+ numZones += 1
1249
+ zone.equipment.each do |equipment|
1250
+ obj_type = equipment.iddObjectType.valueName.to_s
1251
+ if equipment.to_ZoneHVACComponent.is_initialized
1252
+ # This is a zonal HVAC component
1253
+ zone_info = {}
1254
+ zone_loop_info[:zonesys] << zone_info
1255
+
1256
+ # Get floor number from zone name string using regexp (Flr-N, where N is the storey number)
1257
+ zone_info[:zonename] = zone.name.get
1258
+ zone_info[:zonename].scan(/.*Flr-(\d+).*/) {|num| zone_info[:flrnum] = num[0].to_i}
1259
+
1260
+ unless zone.isConditioned.empty?
1261
+ zone_info[:is_conditioned] = zone.isConditioned.get
1262
+ else
1263
+ zone_info[:is_conditioned] = 'N/A'
1264
+ puts "Warning: zone.isConditioned is empty for #{zone.name.get}!"
1265
+ end
1266
+
1267
+ zone_info[:multiplier] = zone.multiplier
1268
+
1269
+ # Get the zone ceiling height value from the sql file...
1270
+ query = "SELECT CeilingHeight FROM Zones WHERE ZoneName='#{zone_info[:zonename].upcase}'"
1271
+ ceilHeight = model.sqlFile().get().execAndReturnFirstDouble(query)
1272
+ zone_info[:ceilingheight] = OpenStudio.convert(ceilHeight.to_f,"m","ft").get # feet
1273
+
1274
+ zone_info[:heatcost] = 0.0
1275
+ zone_info[:coolcost] = 0.0
1276
+ zone_info[:heatcoolcost] = 0.0
1277
+ zone_info[:pipingcost] = 0.0
1278
+ zone_info[:wiringcost] = 0.0
1279
+ zone_info[:multiplier] = zone.multiplier
1280
+ zone_info[:sysname] = equipment.name.get
1281
+
1282
+ # Get the heat capacity values from the sql file - ZoneSizes table...
1283
+ query = "SELECT UserDesLoad FROM ZoneSizes WHERE ZoneName='#{zone_info[:zonename].upcase}' AND LoadType='Heating'"
1284
+ heatCapVal = model.sqlFile().get().execAndReturnFirstDouble(query)
1285
+ zone_info[:heatcapacity] = heatCapVal.to_f / 1000.0 # Watts -> kW
1286
+
1287
+ component = equipment.to_ZoneHVACComponent.get
1288
+ if component.to_ZoneHVACPackagedTerminalAirConditioner.is_initialized
1289
+ heating_coil_name = component.to_ZoneHVACPackagedTerminalAirConditioner.get.heatingCoil.name.to_s
1290
+ query = "SELECT Value FROM TabularDataWithStrings WHERE ReportName='CoilSizingDetails' AND RowName='#{heating_coil_name.upcase}' AND ColumnName='Coil Final Gross Total Capacity'"
1291
+ zone_info[:heatcapacity] = model.sqlFile.get.execAndReturnFirstDouble(query).to_f/1000.0
1292
+ cooling_coil_name = component.to_ZoneHVACPackagedTerminalAirConditioner.get.coolingCoil.name.to_s
1293
+ query = "SELECT Value FROM TabularDataWithStrings WHERE ReportName='CoilSizingDetails' AND RowName='#{cooling_coil_name.upcase}' AND ColumnName='Coil Final Gross Total Capacity'"
1294
+ zone_info[:coolcapacity] = model.sqlFile.get.execAndReturnFirstDouble(query).to_f/1000.0
1295
+ elsif component.to_ZoneHVACFourPipeFanCoil.is_initialized # 2PFC & 4PFC
1296
+ heating_coil_name = component.to_ZoneHVACFourPipeFanCoil.get.heatingCoil.name.to_s
1297
+ query = "SELECT Value FROM TabularDataWithStrings WHERE ReportName='CoilSizingDetails' AND RowName='#{heating_coil_name.upcase}' AND ColumnName='Coil Final Gross Total Capacity'"
1298
+ zone_info[:heatcapacity] = model.sqlFile.get.execAndReturnFirstDouble(query).to_f/1000.0
1299
+ cooling_coil_name = component.to_ZoneHVACFourPipeFanCoil.get.coolingCoil.name.to_s
1300
+ query = "SELECT Value FROM TabularDataWithStrings WHERE ReportName='CoilSizingDetails' AND RowName='#{cooling_coil_name.upcase}' AND ColumnName='Coil Final Gross Total Capacity'"
1301
+ zone_info[:coolcapacity] = model.sqlFile.get.execAndReturnFirstDouble(query).to_f/1000.0
1302
+ elsif component.to_ZoneHVACPackagedTerminalHeatPump.is_initialized
1303
+ heating_coil_name = component.to_ZoneHVACPackagedTerminalHeatPump.get.heatingCoil.name.to_s
1304
+ query = "SELECT Value FROM TabularDataWithStrings WHERE ReportName='CoilSizingDetails' AND RowName='#{heating_coil_name.upcase}' AND ColumnName='Coil Final Gross Total Capacity'"
1305
+ zone_info[:heatcapacity] = model.sqlFile.get.execAndReturnFirstDouble(query).to_f/1000.0
1306
+ cooling_coil_name = component.to_ZoneHVACPackagedTerminalHeatPump.get.coolingCoil.name.to_s
1307
+ query = "SELECT Value FROM TabularDataWithStrings WHERE ReportName='CoilSizingDetails' AND RowName='#{cooling_coil_name.upcase}' AND ColumnName='Coil Final Gross Total Capacity'"
1308
+ zone_info[:coolcapacity] = model.sqlFile.get.execAndReturnFirstDouble(query).to_f/1000.0
1309
+ elsif component.to_ZoneHVACTerminalUnitVariableRefrigerantFlow.is_initialized
1310
+ # Use separate method to get zonal VRF system info
1311
+ zonalSys = component.to_ZoneHVACTerminalUnitVariableRefrigerantFlow.get
1312
+ vrfSystemFloors = getZonalVRFInfo(zone: zone, model: model, prototype_creator: prototype_creator, zonalSys: zonalSys, vrfSystemFloors: vrfSystemFloors, regMat: regional_material, regLab: regional_installation, numZones: numZones)
1313
+ # When done, go to the next piece of equipment. Will do VRF costing once all thermal zones are
1314
+ # investigated.
1315
+ next
1316
+ else
1317
+ cooling_coil_name = 'nil'
1318
+ end
1319
+
1320
+ unless (obj_type.to_s == 'OS_ZoneHVAC_FourPipeFanCoil') || (obj_type.to_s == 'OS_ZoneHVAC_PackagedTerminalHeatPump') || (obj_type.to_s == 'OS_ZoneHVAC_PackagedTerminalAirConditioner')
1321
+ # Get the cooling total capacity (sen+lat) value from the sql file - ComponentSizes table
1322
+ query = "SELECT Value FROM ComponentSizes WHERE CompName='#{cooling_coil_name.upcase}' AND Units='W'"
1323
+ coolCapVal = model.sqlFile().get().execAndReturnFirstDouble(query)
1324
+ zone_info[:coolcapacity] = coolCapVal.to_f / 1000.0 # Watts -> kW
1325
+ end
1326
+
1327
+ if (zone_info[:sysname] =~ /Baseboard Convective Water/i) or (zone_info[:sysname] =~ /BaseboardConvectiveWater/i)
1328
+ zone_info[:systype] = 'HW'
1329
+ # HW convector length based on 0.425 kW/foot
1330
+ if zone_info[:heatcapacity] > 0
1331
+ heatCapacity = zone_info[:heatcapacity] / zone.multiplier
1332
+ convLength = (heatCapacity / 0.425).round(0)
1333
+ # HW convector 1" copper core pipe cost
1334
+ matCost, labCost = getHVACCost(zone_info[:sysname], 'ConvectCopper', 1.25, true)
1335
+ convPipeCost = (matCost * regional_material / 100.0 + labCost * regional_installation / 100.0) * convLength
1336
+ # For each convector there will be a shut-off valve, 2 Tee connections and 2 elbows to
1337
+ # isolate the convector from the hot water loop distribution for servicing and balancing.
1338
+ # Hot water convectors are manufactured in maximum 8 ft lengths, therefore the number of
1339
+ # convectors per thermal zone is (rounded up to nearest integer):
1340
+ ratio = (convLength.to_f / 8.0).to_f
1341
+ numConvectors = (ratio - ratio.to_i) > 0.10 ? (ratio + 0.5).round(0) : ratio
1342
+ # Cost of valves:
1343
+ matCost, labCost = getHVACCost('1.25 inch gate valve', 'ValvesGate', 1.25, true)
1344
+ convValvesCost = (matCost * regional_material / 100.0 + labCost * regional_installation / 100.0) * numConvectors
1345
+ # Cost of tees:
1346
+ matCost, labCost = getHVACCost('1.25 inch copper tees', 'CopperPipeTee', 1.25, true)
1347
+ convTeesCost = (matCost * regional_material / 100.0 + labCost * regional_installation / 100.0) * 2 * numConvectors
1348
+ # Cost of elbows:
1349
+ matCost, labCost = getHVACCost('1.25 inch copper elbows', 'CopperPipeElbow', 1.25, true)
1350
+ convElbowsCost = (matCost * regional_material / 100.0 + labCost * regional_installation / 100.0) * 2 * numConvectors
1351
+ # Total convector cost for this zone (excluding distribution piping):
1352
+ convCost = (convPipeCost + convValvesCost + convTeesCost + convElbowsCost) * zone.multiplier
1353
+ zone_info[:heatcost] = convCost
1354
+ zone_info[:num_units] = numConvectors
1355
+
1356
+ # Single pipe supply and return
1357
+ perimPipingCost = getPerimDistPipingCost(zone, nom_flr_hght, regional_material, regional_installation)
1358
+ zone_info[:pipingcost] = perimPipingCost
1359
+
1360
+ totalCost += convCost + perimPipingCost
1361
+ end
1362
+
1363
+ elsif (zone_info[:sysname]=~ /Baseboard Convective Electric/i) or (zone_info[:sysname]=~ /BaseboardConvectiveElectric/i)
1364
+ zone_info[:systype] = 'BB'
1365
+ # BB number based on 0.935 kW/unit
1366
+ if zone_info[:heatcapacity] > 0
1367
+ heatCapacity = zone_info[:heatcapacity] / zone.multiplier
1368
+ ratio = (heatCapacity / 0.935).to_f
1369
+ numConvectors = (ratio - ratio.to_i) > 0.10 ? (ratio + 0.5).round(0) : ratio
1370
+ # BB electric convector unit cost (Just one in sheet)
1371
+ matCost, labCost = getHVACCost(zone_info[:sysname], 'ElectricBaseboard', 'nil', true)
1372
+ elecBBCost = (matCost * regional_material / 100.0 + labCost * regional_installation / 100.0) * numConvectors
1373
+ # For each baseboard there will be an electrical junction box
1374
+ matCost, labCost = getHVACCost('Electrical Outlet Box', 'Box', 1, true)
1375
+ elecBoxCost = (matCost * reg_mat_elec / 100.0 + labCost * reg_lab_elec / 100.0) * numConvectors
1376
+ # Total electric basbeboard cost for this zone:
1377
+ elecConvCost = (elecBBCost + elecBoxCost) * zone.multiplier
1378
+ zone_info[:heatcost] = elecConvCost
1379
+ zone_info[:num_units] = numConvectors
1380
+
1381
+ perimWiringCost = getPerimDistWiringCost(zone, nom_flr_hght, reg_mat_elec, reg_lab_elec)
1382
+ zone_info[:wiringcost] = perimWiringCost
1383
+
1384
+ totalCost += elecConvCost + perimWiringCost
1385
+ end
1386
+
1387
+ elsif (zone_info[:sysname] =~ /PTAC/i) || (obj_type.to_s == 'OS_ZoneHVAC_PackagedTerminalAirConditioner')
1388
+ zone_info[:systype] = 'PTAC'
1389
+ # Heating cost of PTAC is handled by Baseboard Convective Electric Heater entry in Equipment list!
1390
+ # Cooling cost of PTAC ...
1391
+ if zone_info[:coolcapacity] > 0
1392
+ # DX cooling unit
1393
+ coolCapacity = zone_info[:coolcapacity] / zone.multiplier
1394
+ numUnits = get_HVAC_multiplier('PTAC', coolCapacity)
1395
+ # PTAC unit cost (Note that same numUnits multiple applied within getHVACCost())
1396
+ matCost, labCost = getHVACCost(zone_info[:sysname], 'PTAC', coolCapacity, false)
1397
+ thePTACUnitCost = (matCost * regional_material / 100.0 + labCost * regional_installation / 100.0)
1398
+ # For each PTAC unit there will be an electrical junction box (wiring costed with distribution - not here)
1399
+ matCost, labCost = getHVACCost('Electrical Outlet Box', 'Box', 1, true)
1400
+ elecBoxCost = (matCost * reg_mat_elec / 100.0 + labCost * reg_lab_elec / 100.0) * numUnits
1401
+ # Total PTAC cost for this zone (excluding distribution piping):
1402
+ thePTACCost = (thePTACUnitCost + elecBoxCost) * zone.multiplier
1403
+ zone_info[:coolcost] = thePTACCost
1404
+ zone_info[:num_units] = numUnits
1405
+
1406
+ perimWiringCost = getPerimDistWiringCost(zone, nom_flr_hght, reg_mat_elec, reg_lab_elec)
1407
+ zone_info[:wiringcost] = perimWiringCost
1408
+
1409
+ totalCost += thePTACCost + perimWiringCost
1410
+ end
1411
+
1412
+ elsif (zone_info[:sysname] =~ /PTHP/i) || (obj_type.to_s =~ /OS_ZoneHVAC_PackagedTerminalHeatPump/)
1413
+ zone_info[:systype] = 'HP'
1414
+ # Cost of PTAC based on heating capacity...
1415
+ if zone_info[:heatcapacity] > 0
1416
+ # DX heat pump unit
1417
+ capacityHPUnit = zone_info[:coolcapacity] > zone_info[:heatcapacity] ?
1418
+ zone_info[:coolcapacity] / zone.multiplier : zone_info[:heatcapacity] / zone.multiplier
1419
+ numUnits = get_HVAC_multiplier('ashp', capacityHPUnit)
1420
+ # HP unit cost (Note that same numUnits multiple applied within getHVACCost())
1421
+ matCost, labCost = getHVACCost(zone_info[:sysname], 'ashp', capacityHPUnit, false)
1422
+ theHPUnitCost = (matCost * regional_material / 100.0 + labCost * regional_installation / 100.0)
1423
+ # For each HP unit there will be an electrical junction box (wiring costed with distribution - not here)
1424
+ matCost, labCost = getHVACCost('Electrical Outlet Box', 'Box', 1, true)
1425
+ elecBoxCost = (matCost * reg_mat_elec / 100.0 + labCost * reg_lab_elec / 100.0) * numUnits
1426
+ # Total HP cost for this zone (excluding distribution piping):
1427
+ theHPCost = (theHPUnitCost + elecBoxCost) * zone.multiplier
1428
+ zone_info[:heatcoolcost] = theHPUnitCost
1429
+ zone_info[:num_units] = numUnits
1430
+
1431
+ perimWiringCost = getPerimDistWiringCost(zone, nom_flr_hght, reg_mat_elec, reg_lab_elec)
1432
+ zone_info[:wiringcost] = perimWiringCost
1433
+
1434
+ totalCost += theHPCost + perimWiringCost
1435
+ end
1436
+
1437
+ elsif zone_info[:sysname] =~ /2-pipe Fan Coil/i
1438
+ zone_info[:sfurnaceystype] = '2FC'
1439
+ if zone_info[:heatcapacity] > 0 || zone_info[:coolcapacity] > 0
1440
+ # Hot water heating and chilled water cooling type fan coil unit
1441
+ capacityFCUnit = zone_info[:coolcapacity] > zone_info[:heatcapacity] ?
1442
+ zone_info[:coolcapacity] / zone.multiplier : zone_info[:heatcapacity] / zone.multiplier
1443
+ numFCUnits = get_HVAC_multiplier('FanCoilHtgClgVent', capacityFCUnit)
1444
+ # 2PFC unit cost (Note that same numFCUnits multiple applied within getHVACCost())
1445
+ matCost, labCost = getHVACCost(zone_info[:sysname], 'FanCoilHtgClgVent', capacityFCUnit, false)
1446
+ fcUnitCost = (matCost * regional_material / 100.0 + labCost * regional_installation / 100.0)
1447
+ # For each 2PFC unit there will be a shut-off valve, 2 Tee connections and 2 elbows to
1448
+ # isolate the convector from the hot water loop distribution for servicing and balancing.
1449
+ # Assumed unit piping is 1.25 inches in diameter.
1450
+ # Cost of valves:
1451
+ matCost, labCost = getHVACCost('1.25 inch gate valve', 'ValvesGate', 1.25, true)
1452
+ fcValvesCost = (matCost * regional_material / 100.0 + labCost * regional_installation / 100.0) * numFCUnits
1453
+ # Cost of tees:
1454
+ matCost, labCost = getHVACCost('1.25 inch copper tees', 'CopperPipeTee', 1.25, true)
1455
+ fcTeesCost = (matCost * regional_material / 100.0 + labCost * regional_installation / 100.0) * 2 * numFCUnits
1456
+ # Cost of elbows:
1457
+ matCost, labCost = getHVACCost('1.25 inch copper elbows', 'CopperPipeElbow', 1.25, true)
1458
+ fcElbowsCost = (matCost * regional_material / 100.0 + labCost * regional_installation / 100.0) * 2 * numFCUnits
1459
+ # For each 2PFC unit there will be an electrical junction box (wiring costed with distribution - not here)
1460
+ matCost, labCost = getHVACCost('Electrical Outlet Box', 'Box', 1, true)
1461
+ elecBoxCost = (matCost * reg_mat_elec / 100.0 + labCost * reg_lab_elec / 100.0) * numFCUnits
1462
+ # Total 2PFC cost for this zone (excluding distribution piping):
1463
+ fcCost = (fcUnitCost + fcValvesCost + fcTeesCost + fcElbowsCost + elecBoxCost) * zone.multiplier
1464
+ zone_info[:heatcoolcost] = fcCost
1465
+ zone_info[:num_units] = numFCUnits
1466
+
1467
+ # Cost for one set supply/return piping
1468
+ perimPipingCost = getPerimDistPipingCost(zone, nom_flr_hght, regional_material, regional_installation)
1469
+ zone_info[:pipingcost] = perimPipingCost
1470
+
1471
+ perimWiringCost = getPerimDistWiringCost(zone, nom_flr_hght, reg_mat_elec, reg_lab_elec)
1472
+ zone_info[:wiringcost] = perimWiringCost
1473
+
1474
+ totalCost += fcCost + perimPipingCost + perimWiringCost
1475
+ end
1476
+
1477
+ elsif (zone_info[:sysname] =~ /4-pipe Fan Coil/i) || (obj_type =~ /OS_ZoneHVAC_FourPipeFanCoil/)
1478
+ zone_info[:systype] = '4FC'
1479
+ if (zone_info[:heatcapacity] > 0) || (zone_info[:coolcapacity] > 0)
1480
+ # Hot water heating and chilled water cooling type fan coil unit
1481
+ capacityFCUnit = zone_info[:coolcapacity] > zone_info[:heatcapacity] ?
1482
+ zone_info[:coolcapacity] / zone.multiplier : zone_info[:heatcapacity] / zone.multiplier
1483
+ numFCUnits = get_HVAC_multiplier('FanCoilHtgClgVent', capacityFCUnit)
1484
+ # 4PFC unit cost (Note that same numFCUnits multiple applied within getHVACCost())
1485
+ matCost, labCost = getHVACCost(zone_info[:sysname], 'FanCoilHtgClgVent', capacityFCUnit, false)
1486
+ fcUnitCost = (matCost * regional_material / 100.0 + labCost * regional_installation / 100.0)
1487
+ # For each 4PFC unit there will be 2 shut-off valves, 4 Tee connections and 4 elbows to
1488
+ # isolate the convector from the hot water loop distribution for servicing and balancing.
1489
+ # Assumed unit piping is 1.25 inches in diameter.
1490
+ # Cost of valves:
1491
+ matCost, labCost = getHVACCost('1.25 inch gate valve', 'ValvesGate', 1.25, true)
1492
+ fcValvesCost = (matCost * regional_material / 100.0 + labCost * regional_installation / 100.0) * 2 * numFCUnits
1493
+ # Cost of tees:
1494
+ matCost, labCost = getHVACCost('1.25 inch copper tees', 'CopperPipeTee', 1.25, true)
1495
+ fcTeesCost = (matCost * regional_material / 100.0 + labCost * regional_installation / 100.0) * 4 * numFCUnits
1496
+ # Cost of elbows:
1497
+ matCost, labCost = getHVACCost('1.25 inch copper elbows', 'CopperPipeElbow', 1.25, true)
1498
+ fcElbowsCost = (matCost * regional_material / 100.0 + labCost * regional_installation / 100.0) * 4 * numFCUnits
1499
+ # For each 4PFC unit there will be an electrical junction box (wiring costed with distribution - not here)
1500
+ matCost, labCost = getHVACCost('Electrical Outlet Box', 'Box', 1, true)
1501
+ elecBoxCost = (matCost * reg_mat_elec / 100.0 + labCost * reg_lab_elec / 100.0) * numFCUnits
1502
+ # Total 4PFC cost for this zone (excluding distribution piping):
1503
+ fcCost = (fcUnitCost + fcValvesCost + fcTeesCost + fcElbowsCost + elecBoxCost) * zone.multiplier
1504
+ zone_info[:heatcoolcost] = fcCost
1505
+ zone_info[:num_units] = numFCUnits
1506
+
1507
+ # Cost for two sets supply/return piping
1508
+ perimPipingCost = 2 * getPerimDistPipingCost(zone, nom_flr_hght, regional_material, regional_installation)
1509
+ zone_info[:pipingcost] = perimPipingCost
1510
+
1511
+ perimWiringCost = getPerimDistWiringCost(zone, nom_flr_hght, reg_mat_elec, reg_lab_elec)
1512
+ zone_info[:wiringcost] = perimWiringCost
1513
+
1514
+ totalCost += fcCost + perimPipingCost + perimWiringCost
1515
+ end
1516
+
1517
+ elsif zone_info[:sysname] =~ /Unit Heater/i || zone_info[:sysname] =~ /Unitary/i
1518
+ zone_info[:systype] = 'FUR'
1519
+ # Two types of unit heaters: electric and gas
1520
+ unitHeater = component.to_ZoneHVACUnitHeater.get
1521
+ heatCoil = unitHeater.heatingCoil
1522
+ if heatCoil.to_CoilHeatingGas.is_initialized # TODO: Need to test this!
1523
+ # The gas unit heaters are cabinet type with a burner and blower rather than the radiant type
1524
+ gasCoil = heatCoil.to_CoilHeatingGas.get
1525
+ if heatCoil.isNominalCapacityAutosized.to_bool
1526
+ zone_info[:heatcapacity] = gasCoil.autosizedNominalCapacity.to_f/1000.0
1527
+ else
1528
+ zone_info[:heatcapacity] = gasCoil.nominalCapacity.to_f/1000.0
1529
+ end
1530
+ if zone_info[:heatcapacity] > 0
1531
+ heatCapacity = zone_info[:heatcapacity] / zone.multiplier
1532
+ numUnits = get_HVAC_multiplier('gasheater', heatCapacity)
1533
+ # Unit cost (Note that same unit multiple applied within getHVACCost())
1534
+ matCost, labCost = getHVACCost(zone_info[:sysname], 'gasheater', heatCapacity, false)
1535
+ unitCost = (matCost * regional_material / 100.0 + labCost * regional_installation / 100.0)
1536
+ # For each unit heater there will be an electrical junction box (wiring costed with distribution - not here)
1537
+ matCost, labCost = getHVACCost('Electrical Outlet Box', 'Box', 1, true)
1538
+ elecBoxCost = (matCost * reg_mat_elec / 100.0 + labCost * reg_lab_elec / 100.0) * numUnits
1539
+ # It is assumed that the gas unit heater(s) are located in the centre of this zone. An 8 in exhaust duct
1540
+ # must be costed from the unit heater to the exterior via the roof. The centroid of this zone:
1541
+ if zone_info[:flrnum] > 1
1542
+ zoneCentroidToRoof_Ft = 10 + nom_flr_hght * zone_info[:flrnum]
1543
+ else
1544
+ zoneCentroidToRoof_Ft = 10
1545
+ end
1546
+ matCost, labCost = getHVACCost('Unit heater exhaust duct', 'Ductwork-S', 8, true)
1547
+ exhaustductCost = (matCost * regional_material / 100.0 + labCost * regional_installation / 100.0) * zoneCentroidToRoof_Ft
1548
+ zone_info[:heatcost] = (unitCost + elecBoxCost + exhaustductCost) * zone.multiplier
1549
+ zone_info[:num_units] = numUnits
1550
+
1551
+ # Cost of gas line header for zone. Header is located in the centre of this zone's floor
1552
+ mechRmInBsmt ? numFlrs = numAGFlrs + 1 : numFlrs = numAGFlrs
1553
+ hdrGasLen = numFlrs * nom_flr_hght
1554
+ # Gas line - first one in spreadsheet
1555
+ matCost, labCost = getHVACCost('Central header gas line', 'GasLine', '')
1556
+ hdrGasLineCost = hdrGasLen * (matCost * regional_material / 100.0 + labCost * regional_installation / 100.0)
1557
+
1558
+ # Cost of gas line from header (centre of flr) to unit heater (centre of zone)
1559
+ #centreOfFloor = get_story_cent_to_edge( building_story: zone_info[:flrnum], prototype_creator: prototype_creator, target_cent: target_cent, full_length: false )
1560
+ #centreOfSpace = get_space_floor_centroid(space:)
1561
+ #gasLineLen = ABS(centreOfFloor - centreOfSpace)
1562
+
1563
+ # Cost of wiring header for zone
1564
+
1565
+ # Cost of wiring from header to unit heater
1566
+
1567
+
1568
+ totalCost += zone_info[:heatcost] + hdrGasLineCost
1569
+ end
1570
+ elsif heatCoil.to_CoilHeatingElectric.is_initialized # Electric Unit Heater
1571
+ elecCoil = heatCoil.to_CoilHeatingElectric.get
1572
+ if elecCoil.isNominalCapacityAutosized.to_bool
1573
+ zone_info[:heatcapacity] = elecCoil.autosizedNominalCapacity.to_f/1000.0
1574
+ else
1575
+ zone_info[:heatcapacity] = elecCoil.nominalCapacity.to_f/1000.0
1576
+ end
1577
+ if zone_info[:heatcapacity] > 0
1578
+ heatCapacity = zone_info[:heatcapacity] / zone.multiplier
1579
+ numUnits = get_HVAC_multiplier('elecheat', heatCapacity)
1580
+ # Unit cost (Note that same unit multiple applied within getHVACCost())
1581
+ matCost, labCost = getHVACCost(zone_info[:sysname], 'elecheat', heatCapacity, false)
1582
+ unitCost = (matCost * regional_material / 100.0 + labCost * regional_installation / 100.0)
1583
+ # For each unit heater there will be an electrical junction box (wiring costed with distribution - not here)
1584
+ matCost, labCost = getHVACCost('Electrical Outlet Box', 'Box', 1, true)
1585
+ elecBoxCost = (matCost * reg_mat_elec / 100.0 + labCost * reg_lab_elec / 100.0) * numUnits
1586
+ zone_info[:heatcost] = (unitCost + elecBoxCost) * zone.multiplier
1587
+ zone_info[:num_units] = numUnits
1588
+ # Cost of wiring to electric unit heater
1589
+
1590
+
1591
+ totalCost += zone_info[:heatcost]
1592
+ end
1593
+ elsif heatCoil.to_CoilHeatingWater.is_initialized # Hot water unit heater
1594
+ waterCoil = heatCoil.to_CoilHeatingWater.get
1595
+ if waterCoil.isRatedCapacityAutosized.to_bool
1596
+ zone_info[:heatcapacity] = waterCoil.autosizedRatedCapacity.to_f/1000.0
1597
+ else
1598
+ zone_info[:heatcapacity] = waterCoil.ratedCapacity.to_f/1000.0
1599
+ end
1600
+ if zone_info[:heatcapacity] > 0
1601
+ heatCapacity = zone_info[:heatcapacity] / zone.multiplier
1602
+ # Max capacity for hot water heater is 75300 Watts
1603
+ numUnits = get_HVAC_multiplier('hotwateruh', heatCapacity)
1604
+ matCost, labCost = getHVACCost(zone_info[:sysname], 'hotwateruh', heatCapacity, false)
1605
+ unitHtrCost = (matCost * regional_material / 100.0 + labCost * regional_installation / 100.0)
1606
+ # For each unit heater there will be a shut-off valve, 2 Tee connections and 2 elbows to
1607
+ # isolate the convector from the hot water loop distribution for servicing and balancing.
1608
+ # Cost of valves:
1609
+ matCost, labCost = getHVACCost('1.25 inch gate valve', 'ValvesGate', 1.25, true)
1610
+ unitHtrValvesCost = (matCost * regional_material / 100.0 + labCost * regional_installation / 100.0) * numUnits
1611
+ # Cost of tees:
1612
+ matCost, labCost = getHVACCost('1.25 inch copper tees', 'CopperPipeTee', 1.25, true)
1613
+ unitHtrTeesCost = (matCost * regional_material / 100.0 + labCost * regional_installation / 100.0) * 2 * numUnits
1614
+ # Cost of elbows:
1615
+ matCost, labCost = getHVACCost('1.25 inch copper elbows', 'CopperPipeElbow', 1.25, true)
1616
+ unitHtrElbowsCost = (matCost * regional_material / 100.0 + labCost * regional_installation / 100.0) * 2 * numUnits
1617
+ # For each unit heater there will be an electrical junction box (wiring costed with distribution - not here)
1618
+ matCost, labCost = getHVACCost('Electrical Outlet Box', 'Box', 1, true)
1619
+ elecBoxCost = (matCost * reg_mat_elec / 100.0 + labCost * reg_lab_elec / 100.0) * numUnits
1620
+ # Total convector cost for this zone (excluding distribution piping):
1621
+ unitHeaterCost = (unitHtrCost + unitHtrValvesCost + unitHtrTeesCost + unitHtrElbowsCost + elecBoxCost) * zone.multiplier
1622
+ zone_info[:heatcost] = unitHeaterCost
1623
+ zone_info[:num_units] = numUnits
1624
+ # Cost of distribution piping from header to unit heater
1625
+
1626
+
1627
+ totalCost += unitHeaterCost
1628
+ end
1629
+ end
1630
+ elsif zone_info[:sysname] =~ /WindowAC/i
1631
+ zone_info[:systype] = 'WinAC'
1632
+ # Cooling cost of WindowAC ...
1633
+ if cooling_coil_name == 'nil'
1634
+ # The cooling coil name doesn't exist so must use a different method to determine cooling
1635
+ # capacity for window AC units!
1636
+ query = "SELECT Value FROM ComponentSizes WHERE CompName='WindowAC' AND Units='W'"
1637
+ coolCapVal = model.sqlFile().get().execAndReturnFirstDouble(query)
1638
+ zone_info[:coolcapacity] = coolCapVal.to_f / 1000.0 # Watts -> kW
1639
+ end
1640
+ if zone_info[:coolcapacity] > 0
1641
+ # DX cooling unit
1642
+ coolCapacity = zone_info[:coolcapacity] / zone.multiplier
1643
+ numUnits = get_HVAC_multiplier('WINAC', coolCapacity)
1644
+ # Window AC unit cost (Note that same numUnits multiple applied within getHVACCost())
1645
+ matCost, labCost = getHVACCost(zone_info[:sysname], 'WINAC', coolCapacity, false)
1646
+ unitWinACCost = (matCost * regional_material / 100.0 + labCost * regional_installation / 100.0)
1647
+ # For each WinAC unit there will be an electrical junction box (wiring costed with distribution - not here)
1648
+ matCost, labCost = getHVACCost('Electrical Outlet Box', 'Box', 1, true)
1649
+ elecBoxCost = (matCost * reg_mat_elec / 100.0 + labCost * reg_lab_elec / 100.0) * numUnits
1650
+ # Total WinAC cost for this zone:
1651
+ theWinACCost = (unitWinACCost + elecBoxCost) * zone.multiplier
1652
+ zone_info[:coolcost] = theWinACCost
1653
+ zone_info[:num_units] = numUnits
1654
+ totalCost += theWinACCost
1655
+ end
1656
+ elsif zone_info[:sysname] =~ /Split/i
1657
+ zone_info[:systype] = 'MiniSplit'
1658
+ # Cooling cost of Mini-split AC ...
1659
+ if cooling_coil_name == 'nil'
1660
+ # The cooling coil name doesn't exist so must use a different method to determine cooling
1661
+ # capacity for mini-spli units!
1662
+ query = "SELECT Value FROM ComponentSizes WHERE CompName='WindowAC' AND Units='W'"
1663
+ coolCapVal = model.sqlFile().get().execAndReturnFirstDouble(query)
1664
+ zone_info[:coolcapacity] = coolCapVal.to_f / 1000.0 # Watts -> kW
1665
+ end
1666
+ if zone_info[:coolcapacity] > 0
1667
+ # Mini-splt cooling unit
1668
+ coolCapacity = zone_info[:coolcapacity] / zone.multiplier
1669
+ numUnits = get_HVAC_multiplier('SplitSZWall', coolCapacity)
1670
+ # PTAC unit cost (Note that same numUnits multiple applied within getHVACCost())
1671
+ matCost, labCost = getHVACCost(zone_info[:sysname], 'PTAC', coolCapacity, false)
1672
+ theMiniSplitUnitCost = (matCost * regional_material / 100.0 + labCost * regional_installation / 100.0)
1673
+ # For each PTAC unit there will be an electrical junction box (wiring costed with distribution - not here)
1674
+ matCost, labCost = getHVACCost('Electrical Outlet Box', 'Box', 1, true)
1675
+ elecBoxCost = (matCost * reg_mat_elec / 100.0 + labCost * reg_lab_elec / 100.0) * numUnits
1676
+ # Total PTAC cost for this zone (excluding distribution piping):
1677
+ theMiniSplitCost = (theMiniSplitUnitCost + elecBoxCost) * zone.multiplier
1678
+ zone_info[:coolcost] = theMiniSplitCost
1679
+ zone_info[:num_units] = numUnits
1680
+ totalCost += theMiniSplitCost
1681
+ end
1682
+
1683
+ end
1684
+ # Add information to zonal costing report.
1685
+ @costing_report['heating_and_cooling']['zonal_systems'] << {
1686
+ 'systype' => zone_info[:systype],
1687
+ 'zone_number' => numZones,
1688
+ 'zone_name' => zone_info[:zonename],
1689
+ 'zone_multiple' => zone_info[:multiplier],
1690
+ 'heat_capacity(kW)' => zone_info[:heatcapacity].round(1),
1691
+ 'cool_capacity(kW)' => zone_info[:coolcapacity].round(1),
1692
+ 'heat_cost' => zone_info[:heatcost].round(0),
1693
+ 'cool_cost' => zone_info[:coolcost].round(0),
1694
+ 'heatcool_cost' => zone_info[:heatcoolcost].round(0),
1695
+ 'piping_cost' => zone_info[:pipingcost].round(0),
1696
+ 'wiring_cost' => zone_info[:wiringcost].round(0),
1697
+ 'num_units' => zone_info[:num_units],
1698
+ 'cummultive_zonal_cost' => totalCost.round(0)
1699
+ }
1700
+ end # End of check of check of if zonal equipment exists
1701
+ end # End of equipment loop
1702
+ end # End of zone loop
1703
+
1704
+ # Get cost of zonal vrf systems
1705
+ unless vrfSystemFloors[:vrfFloors].empty?
1706
+ totalCost += getZonalVRFCosting(vrfSystemFloors: vrfSystemFloors, model: model, prototype_creator: prototype_creator, regMat: regional_material, regLab: regional_installation, cumulCost: totalCost)
1707
+ end
1708
+ puts "\nZonal systems costing data successfully generated. Total zonal systems costs: $#{totalCost.round(0)}"
1709
+
1710
+ return totalCost
1711
+ end
1712
+
1713
+ def getHeaderPipingDistributionCost(numAGFlrs, mechRmInBsmt, regional_material, regional_installation, reg_elec_mat, reg_elec_inst, pumpFlow, horz_dist, nom_flr_hght)
1714
+ # Hot water central header piping distribution costs. Note that the piping distribution cost
1715
+ # of zone piping is done in the zonalsys_costing function
1716
+
1717
+ # Central header piping Cost
1718
+ supHdrCost = 0; retHdrCost = 0
1719
+ mechRmInBsmt ? numFlrs = numAGFlrs + 1 : numFlrs = numAGFlrs
1720
+ if numFlrs < 3
1721
+ # Header pipe is same diameter as distribution pipes to zone floors
1722
+ supHdrLen = numFlrs * nom_flr_hght
1723
+
1724
+ # 1.25 inch Steel pipe
1725
+ matCost, labCost = getHVACCost('Header 1.25 inch steel pipe', 'SteelPipe', 1.25)
1726
+ supHdrpipingCost = supHdrLen * (matCost * regional_material / 100.0 + labCost * regional_installation / 100.0)
1727
+
1728
+ # 1.25 inch Steel pipe insulation
1729
+ matCost, labCost = getHVACCost('Header 1.25 inch pipe insulation', 'PipeInsulation', 1.25)
1730
+ supHdrInsulCost = supHdrLen * (matCost * regional_material / 100.0 + labCost * regional_installation / 100.0)
1731
+
1732
+ # 1.25 inch gate valves
1733
+ matCost, labCost = getHVACCost('Header 1.25 inch gate valves', 'ValvesGate', 1.25)
1734
+ supHdrValvesCost = (matCost * regional_material / 100.0 + labCost * regional_installation / 100.0)
1735
+
1736
+ # 1.25 inch tee
1737
+ matCost, labCost = getHVACCost('Header 1.25 inch steel tee', 'SteelPipeTee', 1.25)
1738
+ supHdrTeeCost = (matCost * regional_material / 100.0 + labCost * regional_installation / 100.0)
1739
+
1740
+ supHdrCost = supHdrpipingCost + supHdrInsulCost + supHdrValvesCost + supHdrTeeCost
1741
+ retHdrCost = supHdrCost
1742
+ else # Greater than 3 floors (including basement)
1743
+ # Use pumpFlow to determine pipe size
1744
+ if pumpFlow <= 0.0001262
1745
+ hdrPipeSize = 0.5
1746
+ elsif pumpFlow > 0.0001262 && pumpFlow <= 0.0002524
1747
+ hdrPipeSize = 0.75
1748
+ elsif pumpFlow > 0.0002524 && pumpFlow <= 0.0005047
1749
+ hdrPipeSize = 1.0
1750
+ elsif pumpFlow > 0.0005047 && pumpFlow <= 0.0010090
1751
+ hdrPipeSize = 1.25
1752
+ elsif pumpFlow > 0.0010090 && pumpFlow <= 0.0015773
1753
+ hdrPipeSize = 1.5
1754
+ elsif pumpFlow > 0.0015773 && pumpFlow <= 0.0031545
1755
+ hdrPipeSize = 2.0
1756
+ elsif pumpFlow > 0.0031545
1757
+ hdrPipeSize = 2.5
1758
+ end
1759
+
1760
+ hdrPipeLen = horz_dist + nom_flr_hght * numFlrs
1761
+
1762
+ # Steel pipe
1763
+ matCost, labCost = getHVACCost("Header Steel Pipe - #{hdrPipeSize} inch", 'SteelPipe', hdrPipeSize)
1764
+ supHdrpipingCost = hdrPipeLen * (matCost * regional_material / 100.0 + labCost * regional_installation / 100.0)
1765
+
1766
+ # Steel pipe insulation
1767
+ matCost, labCost = getHVACCost("Header Pipe Insulation - #{hdrPipeSize} inch", 'PipeInsulation', hdrPipeSize)
1768
+ supHdrInsulCost = hdrPipeLen * (matCost * regional_material / 100.0 + labCost * regional_installation / 100.0)
1769
+
1770
+ # Gate valves
1771
+ matCost, labCost = getHVACCost("Header Gate Valves - #{hdrPipeSize} inch", 'ValvesGate', hdrPipeSize)
1772
+ supHdrValvesCost = (matCost * regional_material / 100.0 + labCost * regional_installation / 100.0)
1773
+
1774
+ # Tee
1775
+ matCost, labCost = getHVACCost("Header Steel Tee - #{hdrPipeSize} inch", 'SteelPipeTee', hdrPipeSize)
1776
+ supHdrTeeCost = (matCost * regional_material / 100.0 + labCost * regional_installation / 100.0)
1777
+
1778
+ supHdrCost = supHdrpipingCost + supHdrInsulCost + supHdrValvesCost + supHdrTeeCost
1779
+ retHdrCost = supHdrCost
1780
+ end
1781
+
1782
+ hdrPipeCost = supHdrCost + retHdrCost
1783
+
1784
+ # Electrical header costs. Central electric header cost for zonal heatingunits
1785
+ hdrLen = numFlrs * nom_flr_hght
1786
+
1787
+ # Conduit - only one spreadsheet entry
1788
+ matCost, labCost = getHVACCost('Header Metal conduit', 'Conduit', '')
1789
+ hdrConduitCost = hdrLen * (matCost * reg_elec_mat / 100.0 + labCost * reg_elec_inst / 100.0)
1790
+
1791
+ # Wiring - size 10
1792
+ matCost, labCost = getHVACCost('Header No 10 Wiring', 'Wiring', 10)
1793
+ hdrWireCost = hdrLen / 100 * (matCost * reg_elec_mat / 100.0 + labCost * reg_elec_inst / 100.0)
1794
+
1795
+ # Box - size 4
1796
+ matCost, labCost = getHVACCost('Header 4 inch deep Box', 'Box', 4)
1797
+ hdrBoxCost = numFlrs * (matCost * reg_elec_mat / 100.0 + labCost * reg_elec_inst / 100.0)
1798
+
1799
+ elecHdrCost = hdrConduitCost + hdrWireCost + hdrBoxCost
1800
+
1801
+ # Central gas header cost will be determined in zonalsys_costing function since
1802
+ # this cost depends on existence of at least one gas-fired unit heater in building.
1803
+
1804
+ hdrDistributionCost = hdrPipeCost + elecHdrCost
1805
+
1806
+ return hdrDistributionCost
1807
+ end
1808
+
1809
+ def getPerimDistPipingCost(zone, nom_flr_hght, regional_material, regional_installation)
1810
+ # Get perimeter distribution piping cost
1811
+ extWallArea = 0.0
1812
+ perimPipingCost = 0.0
1813
+ zone.spaces.sort.each do |space|
1814
+ if space.spaceType.empty? or space.spaceType.get.standardsSpaceType.empty? or space.spaceType.get.standardsBuildingType.empty?
1815
+ raise ("standards Space type and building type is not defined for space:#{space.name.get}. Skipping this space for costing.")
1816
+ end
1817
+ extWallArea += OpenStudio.convert(space.exteriorWallArea.to_f,"m^2","ft^2").get # sq.ft.
1818
+ end
1819
+ perimTotal = ( extWallArea / nom_flr_hght ) * zone.multiplier
1820
+
1821
+ # 1.25 inch Steel pipe
1822
+ matCost, labCost = getHVACCost('Perimeter Distribution - 1.25 inch steel pipe', 'SteelPipe', 1.25)
1823
+ perimPipingCost = perimTotal * (matCost * regional_material / 100.0 + labCost * regional_installation / 100.0)
1824
+
1825
+ # 1.25 inch Steel pipe insulation
1826
+ matCost, labCost = getHVACCost('Perimeter Distribution - 1.25 inch pipe insulation', 'PipeInsulation', 1.25)
1827
+ perimPipingCost += perimTotal * (matCost * regional_material / 100.0 + labCost * regional_installation / 100.0)
1828
+
1829
+ return perimPipingCost
1830
+ end
1831
+
1832
+ def getPerimDistWiringCost(zone, nom_flr_hght, regional_material, regional_installation)
1833
+ # Get perimeter distribution wiring cost
1834
+ extWallArea = 0.0
1835
+ perimWiringCost = 0.0
1836
+ zone.spaces.sort.each do |space|
1837
+ if space.spaceType.empty? or space.spaceType.get.standardsSpaceType.empty? or space.spaceType.get.standardsBuildingType.empty?
1838
+ raise ("standards Space type and building type is not defined for space:#{space.name.get}. Skipping this space for costing.")
1839
+ end
1840
+ extWallArea += OpenStudio.convert(space.exteriorWallArea.to_f,"m^2","ft^2").get # sq.ft.
1841
+ end
1842
+ perimTotal = ( extWallArea / nom_flr_hght ) * zone.multiplier
1843
+
1844
+ # Conduit - only one spreadsheet entry
1845
+ matCost, labCost = getHVACCost('Perimeter Distribution - Metal conduit', 'Conduit', '')
1846
+ perimWiringCost = perimTotal * (matCost * regional_material / 100.0 + labCost * regional_installation / 100.0)
1847
+
1848
+ # Wiring - size 10
1849
+ matCost, labCost = getHVACCost('Perimeter Distribution - No 10 Wiring', 'Wiring', 10)
1850
+ perimWiringCost += perimTotal / 100 * (matCost * regional_material / 100.0 + labCost * regional_installation / 100.0)
1851
+
1852
+ return perimWiringCost
1853
+ end
1854
+
1855
+ # Get information on Zonal VRF System in a Thermal Zone
1856
+ def getZonalVRFInfo(zone:, model:, prototype_creator:, zonalSys:, vrfSystemFloors:, regMat:, regLab:, numZones:)
1857
+ #Get heating and cooling coil objects
1858
+ heatingCoil = zonalSys.heatingCoil.get.to_CoilHeatingDXVariableRefrigerantFlow.get
1859
+ coolingCoil = zonalSys.coolingCoil.get.to_CoilCoolingDXVariableRefrigerantFlow.get
1860
+
1861
+ # Get heating capacity
1862
+ if heatingCoil.isRatedTotalHeatingCapacityAutosized.to_bool
1863
+ heatingCapkW = heatingCoil.autosizedRatedTotalHeatingCapacity.to_f/1000.0
1864
+ else
1865
+ heatingCapkW = (heatingCoil.ratedTotalHeatingCapacity).to_f/1000.0
1866
+ end
1867
+
1868
+ # Get cooling capacity
1869
+ if coolingCoil.isRatedTotalCoolingCapacityAutosized.to_bool
1870
+ coolingCapkW = coolingCoil.autosizedRatedTotalCoolingCapacity.to_f/1000.0
1871
+ else
1872
+ coolingCapkW = coolingCoil.grossRatedTotalCoolingCapacityAtSelectedNominalSpeedLevel.to_f/1000.0
1873
+ end
1874
+
1875
+ # Get the multiplier for the thermal zone
1876
+ zoneMult = zone.multiplier
1877
+
1878
+ # Set capacity to the highest of the heating or cooling capacity and adjust for the thermal zone multiplier
1879
+ heatingCapkW >= coolingCapkW ? totalCapkW = heatingCapkW/zoneMult : totalCapkW = coolingCapkW/zoneMult
1880
+
1881
+ # Get the thermal zone (TZ) and collect information on:
1882
+ # The TZ name.
1883
+ # The TZ capacity.
1884
+ # The spaces associated with the TZ.
1885
+ # The floors the spaces occupy.
1886
+ # The total ceiling area and floor area for each floor the spaces occupy.
1887
+ # The centroid of all of the spaces on a given floor (this may be outside of the spaces for some geometries)
1888
+ # The building story names and objects associated with a thermal zone.
1889
+ # the ceiling and floor area of the spaces associated with it for each floor they occupy
1890
+ # The capacity of the TZ on a given floor (calculated at total capacity * floor area of TZ spaces on floor/total TZ floor area)
1891
+ # The VRF ceiling mounts required and associated cost
1892
+ # The VRF system controllers cost
1893
+ tzFloorsInfo = prototype_creator.thermal_zone_get_centroid_per_floor(zone)
1894
+ tzFloorsInfo.each do |tzFloor|
1895
+ tzStoryFloorArea_m2 = 0
1896
+ tzSpaceMults = []
1897
+ tzFloor[:spaces].each do |tz_space|
1898
+ tzStoryFloorArea_m2 += tz_space.floorArea.to_f
1899
+ tzSpaceMults << tz_space.multiplier.to_f
1900
+ end
1901
+ tzFloorCapkW = totalCapkW * tzStoryFloorArea_m2 / zone.floorArea.to_f
1902
+ # Get the VRF Ceiling mount information and cost
1903
+ vrfCeilMountInfo = getHVACDBInfo(name: "VRF Ceiling Mount", materialLookup: "VRF-CeilingMount", materialSize: tzFloorCapkW, exactMatch: false)
1904
+ # Get VRF Ceiling mount cost.
1905
+ material, labour = getCost(vrfCeilMountInfo[:name], vrfCeilMountInfo[:hvac_material], vrfCeilMountInfo[:multiplier])
1906
+ # The multiplier (number of units required to meet the demand) is already included in the material and labour cost.
1907
+ vrfCeilMountCost = (material*regMat + labour*regLab) / 100.0
1908
+ # Get the VRF System Controller Cost (for 1 controller)
1909
+ material, labour = getHVACCost("VRF System Controller", 'VRF-Sys-Controller', nil, true)
1910
+ # Since material and labour are for 1 controller the multiplier is added here.
1911
+ vrfSysContCost = (material * regMat + labour * regLab) * vrfCeilMountInfo[:multiplier] / 100.0
1912
+
1913
+ # Put the thermal zone information for the floor into a hash.
1914
+ tzFloorInfo = {
1915
+ tzName: zone.name.to_s,
1916
+ tzNum: numZones,
1917
+ tzFloorName: tzFloor[:story_name],
1918
+ tzFloorCeilingAream2: tzFloor[:ceiling_area],
1919
+ tzMult: zoneMult,
1920
+ tzSpaces: tzFloor[:spaces],
1921
+ tzSpaceMults: tzSpaceMults,
1922
+ tzFloorArea_m2: tzStoryFloorArea_m2,
1923
+ tzFloorCapkW: tzFloorCapkW,
1924
+ tzCentroid: tzFloor[:centroid],
1925
+ vrfCeilMountInfo: vrfCeilMountInfo,
1926
+ vrfCeilMountCost: vrfCeilMountCost,
1927
+ vrfSysContCost: vrfSysContCost
1928
+ }
1929
+
1930
+ # Add the TZ information for the floor to a hash containing all of the thermal zones.
1931
+ vrfSystemFloors = compileZonalVRFFloors(vrfSystemFloors: vrfSystemFloors, tzFloor: tzFloorInfo)
1932
+ end
1933
+ return vrfSystemFloors
1934
+ end
1935
+
1936
+ # Cost zonal VRF systems including zone equipment (ceiling units and associated tubing and wiring), floor equipment (
1937
+ # branch distributor on each floor), VRF system condenser (assumed one on rooftop and another every 50m), and piping
1938
+ # and wiring linking the branch distributors to one another and to the condensers.
1939
+ #
1940
+ # Takes in:
1941
+ # vrfSystemFloors = {
1942
+ # maxCeil(float): The height of the thermal zone served by a VRF system with the highest ceiling in the
1943
+ # building. This is referenced to the global origin for the building. The units are m.
1944
+ # lowCeil(float): The height of the thermal zone served by a VRF system with the lowest ceiling in the
1945
+ # building. This is referenced to the global origin for the building. The units are m.
1946
+ # vrfFloors(array): [
1947
+ # storyName(string): Name of the current floor (story).
1948
+ # buildStoryObj(Obj): The OpenStudio object associated with the current floor (story).
1949
+ # floorAream2: Total floor area (m2) of thermal zones on the current floor served by VRF systems. Does not
1950
+ # include multipliers.
1951
+ # floorCeillingAream2: The Total ceiling area (m2) of thermal zones on the current floor served by VRF
1952
+ # systems. Does not include multipliers.
1953
+ # floorTZs(array): An array containing each tzFloor hash described above for the current floor.
1954
+ #
1955
+ # ]
1956
+ # }
1957
+ # model(hash): OpenStudio building model.
1958
+ # prototype_creator(object): OpenStudio-Standards object for whichever version of NECB was used to create the model.
1959
+ # regMat(float): HVAC regional cost factor for materiel.
1960
+ # regLab(float): HVAC regional cost factor for labour.
1961
+ # cumulCost(float): Cumulative zonal system cost.
1962
+ #
1963
+ # Output(float): Total cost for VRF system. Also adds information to @costing_report for condenser(s), VRF zonal
1964
+ # systems, and branch distributors.
1965
+ def getZonalVRFCosting(vrfSystemFloors:, model:, prototype_creator:, regMat:, regLab:, cumulCost:)
1966
+ # Include empty array in costing report for branch distributor costs on each floor
1967
+ @costing_report['heating_and_cooling']['floor_systems'] = []
1968
+ total_cost = 0
1969
+ vrfWireInfo = getHVACDBInfo(name: "VRF Wiring", materialLookup: "wiring", materialSize: 10, exactMatch: true)
1970
+ regMatElec, regLabElec = get_regional_cost_factors(@costing_report['province_state'], @costing_report['city'], vrfWireInfo[:hvac_material])
1971
+
1972
+ # Find the center of the highest roof
1973
+ roof_cent_info = prototype_creator.find_highest_roof_centre(model)
1974
+ roof_cent = roof_cent_info[:roof_centroid]
1975
+ # Find the distance between the highest roof and the ceiling of the lowest space served by a VRF system
1976
+ maxHeightDiff = (roof_cent[2] - vrfSystemFloors[:lowCeil]).to_f.round(8)
1977
+ # Find the roof height for the condenensate line cost. If the maxHeightDiff includes basement spaces use it,
1978
+ # otherwise use the height of the roof.
1979
+ maxHeightDiff > roof_cent[2].to_f.round(8) ? roofHeight = maxHeightDiff : roofHeight = roof_cent[2].to_f.round(8)
1980
+ # Get the condenser cost
1981
+ vrfCondenserCost = costVRFCondenser(model: model, maxHeightDiff: maxHeightDiff, regMat: regMat, regLab: regLab, regMatElec: regMatElec, regLabElec:regLabElec, roofHeight: roofHeight)
1982
+ total_cost += vrfCondenserCost
1983
+ vrfSystemFloors[:vrfFloors].each do |currFloor|
1984
+ vrfDistWallInfo = getWallWithLargestArea(currFloor: currFloor)
1985
+ vrfDistWallCent = vrfDistWallInfo[:wallCent]
1986
+ totalFloorCapkW = 0
1987
+ totalFloorCeilUnits = 0
1988
+ floorMults = []
1989
+ currFloor[:floorTZs].each do |floorTZ|
1990
+ zoneWallLengthm = (vrfDistWallCent[0] - floorTZ[:tzCentroid][0]).abs + (vrfDistWallCent[1] - floorTZ[:tzCentroid][1]).abs
1991
+ zoneWallLengthft = (OpenStudio.convert(zoneWallLengthm, 'm', 'ft').get)
1992
+ elecLength = zoneWallLengthft + 10.0 * (floorTZ[:vrfCeilMountInfo][:multiplier] - 1)
1993
+
1994
+ # Get the zone refrigerant tubing cost (tubing running from the ceiling units to the brancd distributers). A
1995
+ # Size of 50 is used for interior refrigerant tubing while 10 is used for the exterior tubing used between the
1996
+ # branch distributors and the condensers.
1997
+ zoneRefrigTubingMat, zoneRefrigTubingLab = getHVACCost("VRF Zone Refrigerant Tubing", 'Refrig-tubing', 50, true)
1998
+ # Refrigerant tubing comes in 50 ft rolls
1999
+ zoneRefrigTubingCost = ((zoneRefrigTubingMat * regMat + zoneRefrigTubingLab * regLab) / 100) * elecLength / 50.0
2000
+
2001
+ # Include condensate line tubing cost
2002
+ zoneCondTubingMat, zoneCondTubingLab = getHVACCost('VRF Zone Condensate Line Tubing', 'PEX_tubing', 0.5, true)
2003
+ zoneCondTubingCost = ((zoneCondTubingMat * regMat + zoneCondTubingLab * regLab) / 100) * elecLength
2004
+
2005
+ # Include coupler cost for condensate line
2006
+ zoneCondCouplingMat, zoneCondCouplingLab = getHVACCost('VRF Zone Condensate Line Couplers', 'PVC_coupling', 0.5, true)
2007
+ zoneCondCouplingCost = ((zoneCondCouplingMat * regMat + zoneCondCouplingLab * regLab) / 100) * floorTZ[:vrfCeilMountInfo][:multiplier]
2008
+
2009
+ # Include tee cost for condensate line
2010
+ zoneCondTeeMat, zoneCondTeeLab = getHVACCost('VRF Zone Condensate Line Tees', 'PVC_tee', 0.5, true)
2011
+ zoneCondTeeCost = ((zoneCondTeeMat * regMat + zoneCondTeeLab * regLab) / 100) * floorTZ[:vrfCeilMountInfo][:multiplier]
2012
+
2013
+ # Total condensate line cost
2014
+ zoneCondLineCost = zoneCondTubingCost + zoneCondCouplingCost + zoneCondTeeCost
2015
+
2016
+ # Get the wiring cost
2017
+ zoneWiringMat, zoneWiringLab = getCost(vrfWireInfo[:name], vrfWireInfo[:hvac_material], vrfWireInfo[:multiplier])
2018
+ zoneWiringCost = ((zoneWiringMat*regMatElec + zoneWiringLab*regLabElec)/100)*elecLength/100
2019
+
2020
+ # Get the conduit cost
2021
+ zoneConduitMat, zoneConduitLab = getHVACCost("VRF Zone Conduit", 'Conduit', nil, true)
2022
+ zoneConduitCost = ((zoneConduitMat*regMatElec + zoneConduitLab*regLabElec)/100)*elecLength
2023
+
2024
+ # Get the total cost
2025
+ totalZoneCost = (zoneRefrigTubingCost + zoneCondLineCost + zoneWiringCost + zoneConduitCost + floorTZ[:vrfCeilMountCost] + floorTZ[:vrfSysContCost])*floorTZ[:tzMult]
2026
+
2027
+ total_cost += totalZoneCost
2028
+
2029
+ cumulCost += total_cost
2030
+ # Add zonal cost to report
2031
+ @costing_report['heating_and_cooling']['zonal_systems'] << {
2032
+ 'systype' => 'zonalVRF',
2033
+ 'zone_number' => floorTZ[:tzNum],
2034
+ 'zone_name' => floorTZ[:tzName],
2035
+ 'zone_multiple' => floorTZ[:tzMult],
2036
+ 'heat_capacity(kW)' => floorTZ[:tzFloorCapkW].round(1),
2037
+ 'cool_capacity(kW)' => floorTZ[:tzFloorCapkW].round(1),
2038
+ 'heat_cost' => 0.00,
2039
+ 'cool_cost' => 0.00,
2040
+ 'heatcool_cost' => ((floorTZ[:vrfCeilMountCost] + floorTZ[:vrfSysContCost]) * floorTZ[:tzMult]).round(0),
2041
+ 'piping_cost' => ((zoneRefrigTubingCost + zoneCondLineCost) * floorTZ[:tzMult]).round(0),
2042
+ 'wiring_cost' => ((zoneWiringCost + zoneConduitCost) * floorTZ[:tzMult]).round(0),
2043
+ 'num_units' => floorTZ[:vrfCeilMountInfo][:multiplier],
2044
+ 'cummultive_zonal_cost' => cumulCost.round(0)
2045
+ }
2046
+ totalFloorCapkW += floorTZ[:tzFloorCapkW]
2047
+ totalFloorCeilUnits += floorTZ[:vrfCeilMountInfo][:multiplier]
2048
+ # Determine the distribution of thermal zone multipliers on the floor. For each thermal zone on the floor get
2049
+ # the multiplier. If the same multiplier is already in the arry then add 1 to the number of occurrences of that
2050
+ # multiplier. If the multiplier is not in the array then add the multiplier to the array with an occurrence of
2051
+ # one.
2052
+ if floorMults.empty?
2053
+ floorMults << {
2054
+ zoneMult: floorTZ[:tzMult],
2055
+ numMults: 1
2056
+ }
2057
+ else
2058
+ numFloorMult = floorMults.select{|data| data[:zoneMult] == floorTZ[:tzMult]}
2059
+ if numFloorMult.empty?
2060
+ floorMults << {
2061
+ zoneMult: floorTZ[:tzMult],
2062
+ numMults: 1
2063
+ }
2064
+ else
2065
+ numFloorMult[0][:numMults] += 1
2066
+ end
2067
+ end
2068
+ end
2069
+ # Find the number of and type of branch distributors which meet the number of ceiling unit criteria and capacity
2070
+ # criteria. Costing for a separate, smaller, branch distributor may be returned if multiple branch distributors
2071
+ # are required to meet the connection or load requirements. In this case this smaller branch distributor will
2072
+ # meet any connection or capacity remaining from the main equipment*(multiplier - 1). In some cases most of the
2073
+ # requirements may be met by the (multiplier - 1)*equipment and a much smaller piece of equipment can be used for
2074
+ # the remaining requipments.
2075
+ vrfBranchDistInfo, vrfBranchDistInfoRed= getHVACMultiSizeDBInfo(name: 'VRF Branch Distributors', materialLookup: 'VRF-Solenoid', materialCap: totalFloorCapkW, materialCon: totalFloorCeilUnits)
2076
+ # Get the branch distributor cost
2077
+ vrfBranchDistMat, vrfBranchDistLab = getCost(vrfBranchDistInfo[:name], vrfBranchDistInfo[:hvac_material], vrfBranchDistInfo[:multiplier])
2078
+ vrfBranchDistCost = (vrfBranchDistMat*regMat + vrfBranchDistLab*regLab)/100
2079
+ # Using the distribution of zone multipliers on the floor find which appears most often.
2080
+ initMaxMult = floorMults.max_by{|data| data[:numMults]}
2081
+ # If several different zone multipliers appear the same number of times then choose the largest zone multiplier
2082
+ floorMultsMatch = floorMults.select{|data| data[:numMults] == initMaxMult[:numMults]}
2083
+ if floorMultsMatch.size > 1
2084
+ maxMult = floorMultsMatch.max_by{|data| data[:zoneMult]}[:zoneMult]
2085
+ else
2086
+ maxMult = initMaxMult[:zoneMult]
2087
+ end
2088
+ # multiply the branch distributer cost by the floor multiplier
2089
+ totalVRFBranchDistCost = vrfBranchDistCost*maxMult
2090
+
2091
+ # Add branch distributor cost to report
2092
+ @costing_report['heating_and_cooling']['floor_systems'] << {
2093
+ 'systype' => 'VRFBranchDistributor',
2094
+ 'floor_name' => currFloor[:storyName],
2095
+ 'floor_multiple' => maxMult,
2096
+ 'heat_capacity(kW)' => totalFloorCapkW.round(1),
2097
+ 'cool_capacity(kW)' => totalFloorCapkW.round(1),
2098
+ 'num_ceiling_units' => totalFloorCeilUnits,
2099
+ 'heatcool_cost' => totalVRFBranchDistCost.round(0),
2100
+ 'num_units' => vrfBranchDistInfo[:multiplier],
2101
+ 'total_floor_cost' => totalVRFBranchDistCost.round(0)
2102
+ }
2103
+
2104
+ total_cost += totalVRFBranchDistCost
2105
+
2106
+ # Check if a smaller piece of equipment was found to meet the requirements remaining after removing the
2107
+ # (multiplier-1) requirements.
2108
+ unless vrfBranchDistInfoRed.nil?
2109
+ # Get the branch distributor cost
2110
+ vrfBranchDistRedMat, vrfBranchDistRedLab = getCost(vrfBranchDistInfo[:name], vrfBranchDistInfoRed[:red_ret_hash], 1.0)
2111
+ vrfBranchDistRedCost = (vrfBranchDistRedMat*regMat + vrfBranchDistRedLab*regLab)/100
2112
+
2113
+ # multiply the branch distributer cost by the floor multiplier
2114
+ totalVRFBranchDistRedCost = vrfBranchDistRedCost*maxMult
2115
+
2116
+ # Add branch distributor cost to report
2117
+ @costing_report['heating_and_cooling']['floor_systems'] << {
2118
+ 'systype' => 'VRFBranchDistributor',
2119
+ 'floor_name' => currFloor[:storyName],
2120
+ 'floor_multiple' => maxMult,
2121
+ 'heat_capacity(kW)' => vrfBranchDistInfoRed[:numCap].to_f.round(1),
2122
+ 'cool_capacity(kW)' => vrfBranchDistInfoRed[:numCap].to_f.round(1),
2123
+ 'num_ceiling_units' => vrfBranchDistInfoRed[:numCon].to_f.round(1),
2124
+ 'heatcool_cost' => totalVRFBranchDistRedCost.round(0),
2125
+ 'num_units' => 1.0,
2126
+ 'total_floor_cost' => totalVRFBranchDistRedCost.round(0)
2127
+ }
2128
+ total_cost += totalVRFBranchDistRedCost
2129
+ end
2130
+ end
2131
+ return total_cost
2132
+ end
2133
+
2134
+ # This method takes information about a thermal zone served by a VRF system (tzFloor) and adds it to the collection of
2135
+ # thermal zones also served by VRF systems on the same floor. The ultimate output (once all thermal zones are read)
2136
+ # is an array of hashes. Each entry in the array represents a floor of the building. Each floor entry contains
2137
+ # information about thermal zones served by VRF systems on that floor.
2138
+ #
2139
+ # The information on the thermal zone served by a VRF system on a given floor is contained in tzFloor. The overall
2140
+ # collection of thermal zone information by floors is contained in vrfSystemFloors. This method modifies
2141
+ # vrfSystemFloors which is why that is an input and output for the method. Both tzFloor and vrfSystemFloors are
2142
+ # described below:
2143
+ # Input:
2144
+ # tzFloor = {
2145
+ # tzName(string): Name of the thermal zone.
2146
+ # tzFloorName(string): Name of the floor the thermal zone is on (or if the TZ is on multiple floors the
2147
+ # name of the current floor being looked at for the thermal zone).
2148
+ # tzFloorCeilingAream2(float): The ceiling area (m2) of the thermal zone on the given floor (this is for the
2149
+ # current thermal zone only and does not include multiples).
2150
+ # tzMult(float): The multiplier for the thermal zone (that is the thermal zone is modeled as tzMult number
2151
+ # of identical thermal zones).
2152
+ # tzSpaces(array): An array containing all of the space objects contained by the thermal zone on the given
2153
+ # floor.
2154
+ # tzSpaceMults(array): An array containing all of the multipliers for the spaces in tzSpaces (probably all the
2155
+ # same as tzMult but I added it anyway).
2156
+ # tzFloorArea_m2(float): The floor area (m2) of all of the spaces in the thermal zone on the given floor (this
2157
+ # is for the current thermal zone only and does not include multiples).
2158
+ # tzFloorCapkW(float): The capacity (kW) of the thermal zone VRF system for the current floor. It is the
2159
+ # highest of the heating and cooling capacities for the VRF system. It dose not include
2160
+ # multipliers and is only for the current floor. If the same thermal zone spans multiple
2161
+ # floors then this is the total copacity for the thermal zone times tzFloorArea_m2
2162
+ # divided by the total floor area for the thermal zone.
2163
+ # tzCentroid(array): This is an array containing three items. These items ore x, y and z coordinates of the
2164
+ # centroid of the current thermal zone on the current floor referenced to the global
2165
+ # origin for the building. The units are in m. Note that, depending on the shape of the
2166
+ # thermal zone on the current floor, the centroid may not actually lie in the thermal
2167
+ # zone (e.g. for an L shaped thermal zone the centroid may be outside the L).
2168
+ # vrfCeilMountInfo(hash): This is a hash containing the costing information, from the costing spreadsheet, for
2169
+ # the VRF ceiling mounts serving the thermal zone on the current floor. It is only
2170
+ # for the current floor and does not include tz multipliers.
2171
+ # vrfCeilMountCost(float): The cost of the VRF ceiling mounts serving the thermal zone on the current floor.
2172
+ # It is only for the current floor and does not include tz multipliers.
2173
+ # vrfSysContCost: The cost of the VRF system controllers that are associated with each VRF ceiling mount. It
2174
+ # is only for the current floor and does not include tz multipliers.
2175
+ # }
2176
+ #
2177
+ # vrfSystemFloors = {
2178
+ # maxCeil(float): The height of the thermal zone served by a VRF system with the highest ceiling in the
2179
+ # building. This is referenced to the global origin for the building. The units are m.
2180
+ # lowCeil(float): The height of the thermal zone served by a VRF system with the lowest ceiling in the
2181
+ # building. This is referenced to the global origin for the building. The units are m.
2182
+ # vrfFloors(array): [
2183
+ # storyName(string): Name of the current floor (story).
2184
+ # buildStoryObj(Obj): The OpenStudio object associated with the current floor (story).
2185
+ # floorAream2: Total floor area (m2) of thermal zones on the current floor served by VRF systems. Does not
2186
+ # include multipliers.
2187
+ # floorCeillingAream2: The Total ceiling area (m2) of thermal zones on the current floor served by VRF
2188
+ # systems. Does not include multipliers.
2189
+ # floorTZs(array): An array containing each tzFloor hash described above for the current floor.
2190
+ #
2191
+ # ]
2192
+ # }
2193
+ #
2194
+ def compileZonalVRFFloors(vrfSystemFloors:, tzFloor:)
2195
+ # Check for the highest ceiling and lowest ceiling.
2196
+ vrfSystemFloors[:maxCeil] = tzFloor[:tzCentroid][2].to_f if tzFloor[:tzCentroid][2].to_f >= vrfSystemFloors[:maxCeil].to_f
2197
+ vrfSystemFloors[:lowCeil] = tzFloor[:tzCentroid][2].to_f if tzFloor[:tzCentroid][2].to_f <= vrfSystemFloors[:lowCeil].to_f
2198
+ # If this is the first time vrfSystemFloors has been used add a new floor and enter the information for tzFloor in
2199
+ # it.
2200
+ if vrfSystemFloors[:vrfFloors].empty?
2201
+ vrfSystemFloors[:vrfFloors] << {
2202
+ storyName: tzFloor[:tzFloorName],
2203
+ buildStoryObj: tzFloor[:tzSpaces][0].buildingStory.get,
2204
+ floorAream2: tzFloor[:tzFloorArea_m2],
2205
+ floorCeilingAream2: tzFloor[:tzFloorCeilingAream2],
2206
+ floorTZs: [tzFloor]
2207
+ }
2208
+ else
2209
+ # If vrfSystemFloors has been used check if the the floor that tzFloor is on has an entry already.
2210
+ vrfFloor = vrfSystemFloors[:vrfFloors].select{|sysFloor| sysFloor[:storyName].to_s.upcase == tzFloor[:tzFloorName].to_s.upcase}
2211
+ # If no entry has been made for the floor tzFloor is on then add a new floor and include the tzFloor info.
2212
+ if vrfFloor.empty?
2213
+ vrfSystemFloors[:vrfFloors] << {
2214
+ storyName: tzFloor[:tzFloorName],
2215
+ buildStoryObj: tzFloor[:tzSpaces][0].buildingStory.get,
2216
+ floorAream2: tzFloor[:tzFloorArea_m2],
2217
+ floorCeilingAream2: tzFloor[:tzFloorCeilingAream2],
2218
+ floorTZs: [tzFloor]
2219
+ }
2220
+ else
2221
+ # If the floor that tzFloor is on has already been made then adjust the floor information to include tzFloor.
2222
+ vrfFloor[0][:floorAream2] += tzFloor[:tzFloorArea_m2]
2223
+ vrfFloor[0][:floorCeilingAream2] += tzFloor[:tzFloorCeilingAream2]
2224
+ vrfFloor[0][:floorTZs] << tzFloor
2225
+ end
2226
+ end
2227
+ return vrfSystemFloors
2228
+ end
2229
+
2230
+ # This method finds and returns the outside wall with the largest area on a given building story. It takes in:
2231
+ # currFloor = {
2232
+ # storyName(string): Name of the current floor (story).
2233
+ # buildStoryObj(Obj): The OpenStudio object associated with the current floor (story).
2234
+ # floorAream2: Total floor area (m2) of thermal zones on the current floor served by VRF systems. Does not
2235
+ # include multipliers.
2236
+ # floorCeillingAream2: The Total ceiling area (m2) of thermal zones on the current floor served by VRF
2237
+ # systems. Does not include multipliers.
2238
+ # floorTZs(array): An array containing each tzFloor hash described above for the current floor.
2239
+ #
2240
+ # }
2241
+ # It returns a hash containing the following:
2242
+ # wallRetHash = {
2243
+ # largestOutsideWallObj(OS Object): OpenStudio surface object with the largest gross area that has a 'wall'
2244
+ # surface type and an 'Outdoors' outside boundary condition.
2245
+ # wallCentObj(array of floats): OpenStudio point3d object containing the x, y, z coordinates of the wall above
2246
+ # wall's centroid. In local coordinate system.
2247
+ # wallCentOrigin(array of OS Objects): Coordinates of the local wall origin in the absolute building
2248
+ # coordinate system.
2249
+ # wallCent(array of floats): Coordinates of the wall's centroid in floats referenced to the building
2250
+ # coordinate system.
2251
+ # }
2252
+ def getWallWithLargestArea(currFloor:)
2253
+ outsideWalls = []
2254
+ # Get all the spaces associated with the building story
2255
+ floorSpaces = currFloor[:buildStoryObj].spaces
2256
+ # Cycle through each space associated with the building story.
2257
+ floorSpaces.each do |floorSpace|
2258
+ # Get the surfaces in the spcae which have a 'Wall' Surface Type and an 'Outdoors' outside boundary condition.
2259
+ 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'))}
2260
+ # Add these surfaces to the array containing outdoor walls.
2261
+ spaceOutWalls.each{|outWall| outsideWalls << outWall}
2262
+ end
2263
+ # Find and return the outside wall object with the largest gross area.
2264
+ largestWall = outsideWalls.sort.max_by{|outWall| outWall.grossArea.to_f}
2265
+ largestWallSpace = largestWall.space.get
2266
+ largestWallSpaceOrigin = [
2267
+ largestWallSpace.xOrigin,
2268
+ largestWallSpace.yOrigin,
2269
+ largestWallSpace.zOrigin
2270
+ ]
2271
+ wallCentObj = largestWall.centroid
2272
+ wallCent = [
2273
+ wallCentObj.x.to_f + largestWallSpaceOrigin[0].to_f,
2274
+ wallCentObj.y.to_f + largestWallSpaceOrigin[1].to_f,
2275
+ wallCentObj.z.to_f + largestWallSpaceOrigin[2].to_f
2276
+ ]
2277
+ wallRetHash = {
2278
+ largestOutsideWallObj: largestWall,
2279
+ wallCentObj: wallCentObj,
2280
+ wallCentOrigin: largestWallSpaceOrigin,
2281
+ wallCent: wallCent
2282
+ }
2283
+ return wallRetHash
2284
+ end
2285
+
2286
+ # Costing for the VRF Condenser(s) and the wiring and piping conecting the Condenser(s) to the branch distributors
2287
+ # on each floor of the building with thermal zones served by a VRF system.
2288
+ #
2289
+ # Taking in:
2290
+ # model(hash): OpenStudio building model
2291
+ # maxHeightDiff(float, m): Difference between height of highest ceiling and ceiling of lowest space served by a VRF
2292
+ # system.
2293
+ # regMat(float): HVAC regional cost factor for material.
2294
+ # regLab(float): HVAC regional cost factor for labour.
2295
+ # RegMatElec(float): Electrical regional cost factor for material.
2296
+ # RegLabElec(float): Electrical regional cost factor for labour.
2297
+ #
2298
+ # Returns:
2299
+ # Total VRF condensor cost (also adds information to @costing_report).
2300
+ def costVRFCondenser(model:, maxHeightDiff:, regMat:, regLab:, regMatElec:, regLabElec:, roofHeight:)
2301
+ # VRF systems have a maximum height difference of 50m. If the height difference calculated above is greater than
2302
+ # 50m then determine how many VRF condensers are required (one every 50m)
2303
+ numVRFheight = 1.0
2304
+ if maxHeightDiff > 50.0
2305
+ (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)
2306
+ end
2307
+ # Get the VRF condenser and calculate the overall capacity as the largest of the heating or cooling capacities.
2308
+ vrfCond = model.getAirConditionerVariableRefrigerantFlows[0]
2309
+ if vrfCond.isGrossRatedHeatingCapacityAutosized.to_bool
2310
+ heatingCapkW = vrfCond.autosizedGrossRatedHeatingCapacity.to_f/1000.0
2311
+ else
2312
+ heatingCapkW = vrfCond.grossRatedHeatingCapacity.to_f/1000.0
2313
+ end
2314
+ if vrfCond.isGrossRatedTotalCoolingCapacityAutosized.to_bool
2315
+ coolingCapkW = vrfCond.autosizedGrossRatedTotalCoolingCapacity.to_f/1000.0
2316
+ else
2317
+ coolingCapkW = vrfCond.grossRatedTotalCoolingCapacity.to_f/1000.0
2318
+ end
2319
+ heatingCapkW >= coolingCapkW ? vrfCondCapkW = heatingCapkW : vrfCondCapkW = coolingCapkW
2320
+ # If more than one VRF condenser is present because of a large height difference then assume each VRF condenser will
2321
+ # serve the same fraction of the load. Divide the revised capacity as the original capacity divided by the number
2322
+ # of VRF condensers required to compensate for a height difference (the default is 1).
2323
+ modVRFCondCapkW = vrfCondCapkW / numVRFheight
2324
+ vrfCondInfo = getHVACDBInfo(name: "VRF Condenser Unit", materialLookup: "VRF-HP-HRV-Outdoor", materialSize: modVRFCondCapkW, exactMatch: false)
2325
+ vrfSizeMult = vrfCondInfo[:multiplier]
2326
+ vrfMat, vrfLab = getCost(vrfCondInfo[:name], vrfCondInfo[:hvac_material], vrfCondInfo[:multiplier])
2327
+ vrfCondCost = (vrfMat * regMat + vrfLab * regLab) / 100
2328
+
2329
+ # Cost the refrigerant tubing (assume 20' of 0.5" supply and 1.0833" return tubing)
2330
+ # vrfCondSizeTon = (OpenStudio.convert(modVRFCondCapkW.to_f, 'kW', 'kBtu/hr').get)/12.0
2331
+ refrigPipeMat, refrigPipeLab = getHVACCost("Refrigerant Piping", 'refrig-tubing-large', 20, true)
2332
+ refrigPipingCost = (refrigPipeMat * regMat + refrigPipeLab * regLab) / 100
2333
+
2334
+ # Cost insulation for tubing (assume 20' of 1.25" pipe insulation for both the supply and return refrigerant tubing)
2335
+ refrigInsMat, refrigInsLab = getHVACCost('Refrigerant Insulation', 'pipeinsulation', 1.25, true)
2336
+ refrigInsulationCost = (refrigInsMat * regMat + refrigInsLab * regLab) * 2 * 20 / 100
2337
+
2338
+ # Cost the wiring
2339
+ vrfWireMat, vrfWireLab = getHVACCost('VRF Wiring', 'wiring', 10, true)
2340
+ vrfWireLength = 20
2341
+ vrfWiringCost = ((vrfWireMat*regMatElec + vrfWireLab*regLabElec)/100)*vrfWireLength/100
2342
+
2343
+ # Cost the Conduit
2344
+ vrfConduitMat, vrfConduitLab = getHVACCost('VRF Wiring Conduit', 'Conduit', nil, true)
2345
+ vrfConduitCost = ((vrfConduitMat*regMatElec + vrfConduitLab*regLabElec)/100)*vrfWireLength
2346
+
2347
+ # Cost the disconnect
2348
+ vrfDiscMat, vrfDiscLab = getHVACCost('VRF Wiring Disconnect', 'Safety_switch', 60, true)
2349
+ vrfDiscCost = (vrfDiscMat*regMatElec + vrfDiscLab*regLabElec)/100
2350
+
2351
+ # Determine the Tubing and wiring between the branch distributors and the condenser unit on the roof. This cost is
2352
+ # included with the condenser cost since this is for the entire building and only depends on the distance between
2353
+ # the height difference between the lowest space served by a VRF system and the roof center height. It will be the
2354
+ # same cost even if there are several condensers because of height restrictions).
2355
+ #
2356
+ # Get the refrigerant tubing cost. Exterior refrigerant tubing is given in 10' lengths of 0.5" supply and 1-1/8"
2357
+ # return tubing. The tubing is assumed to run inside the building so no insulation or all-weather protection is
2358
+ # provided.
2359
+ buildRefrigTubingMat, buildRefrigTubingLab = getHVACCost("VRF Building Refrigerant Tubing", 'refrig-tubing-large', 10, true)
2360
+ # Get distance between lowest floor served by the VRF system and the roof in feet
2361
+ maxHeightDiffFt = OpenStudio.convert(maxHeightDiff, 'm', 'ft').get
2362
+ # Tubing cost divided by ten because tubing costing is provided in 10 ft rolls
2363
+ buildRefrigCost = ((buildRefrigTubingMat * regMat + buildRefrigTubingLab * regLab) / 100) * maxHeightDiffFt / 10.0
2364
+
2365
+ # Get the cost of condensate tubing for the whole building. A different height is used than the that for
2366
+ # refrigerant tubing since the condensate line must extend from the height of the roof (where the condenser is) to
2367
+ # the ground floor (if maxHeightDiff does not extend the entire building height) or basement ()if maxHeightDiff
2368
+ # includes a basement).
2369
+ buildCondMat, buildCondLab = getHVACCost('Building Condensate pipe', 'PEX_tubing', 0.5, true)
2370
+ buildCondCost = ((buildCondMat * regMat + buildCondLab * regLab) / 100) * maxHeightDiffFt
2371
+
2372
+ # Get the wiring cost (note wiring comes in 100 ft lengths)
2373
+ buildWiringMat, buildWiringLab = getHVACCost('VRF Wiring', 'wiring', 10, true)
2374
+ buildWiringCost = ((buildWiringMat * regMatElec + buildWiringLab * regLabElec) / 100) * maxHeightDiffFt / 100.0
2375
+
2376
+ # Get the conduit cost
2377
+ buildConduitMat, buildConduitLab = getHVACCost("VRF Building Conduit", 'Conduit', nil, true)
2378
+ buildConduitCost = ((buildConduitMat * regMatElec + buildConduitLab * regLabElec) / 100) * maxHeightDiffFt
2379
+
2380
+ # Find totals
2381
+ totalVRFCondCost = vrfCondCost * numVRFheight
2382
+ totalVRFPipingCost = (refrigPipingCost + refrigInsulationCost) * numVRFheight * vrfSizeMult + buildRefrigCost + buildCondCost
2383
+ totalVRFWiringCost = (vrfWiringCost + vrfConduitCost + vrfDiscCost) * numVRFheight * vrfSizeMult + buildWiringCost + buildConduitCost
2384
+ totalVRFEquipCost = totalVRFCondCost + totalVRFPipingCost + totalVRFWiringCost
2385
+
2386
+
2387
+ # Add to VRF Condenser cost report. I was not sure where to put this since it was really neither a plant unit or a
2388
+ # zonal unit. I guess it supplies several zones so that makes it plant equipment.
2389
+ @costing_report['heating_and_cooling']['plant_equipment'] << {
2390
+ 'type' => 'VRF Zonal System Condenser',
2391
+ 'nom_flr2flr_hght_ft' => 0.0,
2392
+ 'ht_roof_ft' => maxHeightDiffFt.round(1),
2393
+ 'longest_distance_to_ext_ft' => 0.0,
2394
+ 'wiring_and_gas_connections_distance_ft' => (vrfWireLength*numVRFheight*vrfSizeMult + maxHeightDiffFt).round(1),
2395
+ 'equipment_cost' => totalVRFCondCost.round(0),
2396
+ 'wiring_and_gas_connections_cost' => totalVRFWiringCost.round(0),
2397
+ 'pump_cost' => 0.00,
2398
+ 'piping_cost' => totalVRFPipingCost.round(0),
2399
+ 'total_cost' => totalVRFEquipCost.round(0)
2400
+ }
2401
+
2402
+ return totalVRFEquipCost
2403
+ end
2404
+
2405
+ # This method was originally part of getHVACCOST but was split out because in some cases the information from the
2406
+ # materials_hvac sheet of the costing spreadsheet was required but not tho cost.
2407
+ # The method takes in:
2408
+ # name(String): The name of a piece of equipment. Is only used for error reporting and is not linked to anything
2409
+ # else.
2410
+ # materialLookup(String): The material type used to search hte 'Material' column of the materials_hvac sheet of the
2411
+ # costing spreadsheet.
2412
+ # materialSize(float): The size of the equipment in whichever units are required when searching the 'Size' column of
2413
+ # the costing spreadsheet.
2414
+ # exactMatch(true/false): A flag to indicate if the hvac equipment must match the size provided exactly or if the
2415
+ # size is a minimum equipment size.
2416
+ #
2417
+ # The method returns a hash with the following composition:
2418
+ # {
2419
+ # name(string): Same as above.
2420
+ # hvac_material(hash): The costing spreadsheet information for the hvac equipment being searched for.
2421
+ # multiplier(float): Default is 1. Will be higher if exactMatch is false, and no materialLookup could be found with
2422
+ # a large enough materialSize in the costing spreadsheet. In this case, it is assumed that several
2423
+ # pieced of equipment defined by hvact_material are used to satisfy the required materialSize.
2424
+ # The multiplier defines the number of hvac_material required to meet the materialSize
2425
+ # }
2426
+ def getHVACMultiSizeDBInfo(name:, materialLookup:, materialCap:, materialCon:)
2427
+ multiplier = 1.0
2428
+ numConLoops = 1.0
2429
+ # Get the materials_hvac sheet info from the costing spreadsheet
2430
+ materials_hvac = @costing_database["raw"]["materials_hvac"]
2431
+ # Find material that meet the materialCon requirement
2432
+ hvac_material_con_info = materials_hvac.select {|data|
2433
+ data['Material'].to_s.upcase == materialLookup.to_s.upcase && data['Fuel'].to_f >= materialCon
2434
+ }
2435
+ hvac_material = []
2436
+ unless hvac_material_con_info.empty?
2437
+ # If the materialCon criteria check if any of the selected material meet the mateterialCap criteria
2438
+ hvac_material_cap_info = hvac_material_con_info.select {|data| data['Size'].to_f >= materialCap}
2439
+ if hvac_material_cap_info.nil? || hvac_material_cap_info.empty?
2440
+ # If none do then select the material with the largest capacity that met the materialCon criteria
2441
+ hvac_material << hvac_material_con_info.max_by {|data| data['Size'].to_f}
2442
+ con_per_loop = hvac_material[0]['Fuel'].to_f
2443
+ else
2444
+ # If something met both the materialCon and materialCap then return a hash containing the material and other
2445
+ # information and we are done.
2446
+ hvac_material = hvac_material_cap_info.min_by {|data| data['Size'].to_f}
2447
+ ret_hash = {
2448
+ name: name,
2449
+ hvac_material: hvac_material,
2450
+ multiplier: 1
2451
+ }
2452
+ return ret_hash
2453
+ end
2454
+ end
2455
+ if hvac_material.empty?
2456
+ # If no equipment met the materialCon criteria then find all with the material type we want
2457
+ hvac_material_info = materials_hvac.select {|data| data['Material'].to_s.upcase == materialLookup.to_s.upcase}
2458
+ # If you cannot find even the material type then something has gone very wrong. Stop everything and tell the user.
2459
+ 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?
2460
+ # Find the equipment with the largest 'Fuel' (this is what defines the materialCon options for this material type)
2461
+ hvac_material = hvac_material_info.max_by{|data| data['Fuel'].to_f}
2462
+ # Find the number of pieces of equipment will be needed to meet the materialCon requirement
2463
+ (((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)
2464
+ # Revise the materialCon requirement now that several pieces of equipment are being used
2465
+ con_per_loop = materialCon / numConLoops
2466
+ # Find all the appropriate equipment in the costing spreadsheet that meet the revised materialCon requirement
2467
+ hvac_material = hvac_material_info.select{|data| data['Fuel'].to_f >= con_per_loop}
2468
+ end
2469
+
2470
+ # Now that we have some equipment that meet the required materialCon requirement revise the materialCap requirement
2471
+ # in case multiple pieces of equipment were to meet the materialCon requirement.
2472
+ reqMatSize = materialCap/numConLoops
2473
+ # Of the equipment that met the (modified or original) materialCan requirement select the equipment the meets the
2474
+ # (modified or original) capacity requirement.
2475
+ material_cap = hvac_material.select{|data| data['Size'].to_f >= reqMatSize}
2476
+ if material_cap.empty?
2477
+ # If none of the selected materials meet the materialCap requirement find the one with the largest capacity
2478
+ largestMat = hvac_material.max_by{|data| data['Size'].to_f}
2479
+ maxAvailCap = largestMat['Size'].to_f
2480
+ # Find out how many are required to meet the materialCap requirement
2481
+ (reqMatSize%maxAvailCap).to_f.round(3) > 0 ? numCapLoops = (((reqMatSize/maxAvailCap).to_i) + 1).to_f.round(0) : numCapLoops = (reqMatSize/maxAvailCap).to_f.round(0)
2482
+ # Calculate how many pieces of equipment are now required to meet both the materialCon and materialCap
2483
+ # requirements
2484
+ totLoops = numConLoops*numCapLoops
2485
+ # Revise the materialCap and materialCon requirements to reflect that even more pieces of equipment will be used
2486
+ # to meet both requirements
2487
+ modMatCap = (materialCap / totLoops).to_f
2488
+ modMatCon = (materialCon / totLoops).to_f
2489
+ # Search for equipment that meet both the modMatCap and modMatCon criteria
2490
+ hvac_material_info = materials_hvac.select {|data| data['Material'].to_s.upcase == materialLookup.to_s.upcase}
2491
+ material_cap = hvac_material_info.select{|data| (data['Size'].to_f >= modMatCap) && (data['Fuel'].to_f >= modMatCon)}
2492
+ if material_cap.empty?
2493
+ # It should have gotten something. If it didn't then select the one with largest capacity and use that.
2494
+ ret_mat = largestMat
2495
+ else
2496
+ # Find the equipment with the smallest capacity that meets the requirement
2497
+ ret_mat = material_cap.min_by{|data| data['Size'].to_f}
2498
+ end
2499
+ else
2500
+ # If something now meets the materialCon and materialCap requirement select the one with the smallest capacity.
2501
+ totLoops = numConLoops
2502
+ ret_mat = material_cap.min_by{|data| data['Size'].to_f}
2503
+ end
2504
+ # If multiple branch distributors are required check if the last one can be smaller than the others and return that
2505
+ # in addition to the other branch distributors.
2506
+ if totLoops.round(0) > 1.0
2507
+ # Check check the remaining size requirements for the last branch distributor
2508
+ (materialCon - (totLoops - 1.0)*(ret_mat['Fuel'].to_f)) > 0 ? redCon = (materialCon - (totLoops - 1.0)*(ret_mat['Fuel'].to_f)) : redCon = 0.0
2509
+ (materialCap - (totLoops - 1.0)*(ret_mat['Size'].to_f)) > 0 ? redCap = (materialCap - (totLoops - 1.0)*(ret_mat['Size'].to_f)) : redCap = 0.0
2510
+ # If either are greater than zero (as should be the case) then look for equipment that can meet the remaining
2511
+ # connection or capacity requipments.
2512
+ if (redCon > 0) || (redCap > 0)
2513
+ # Find material that meet the remaining connection and capacity requirements.
2514
+ hvac_material_red = materials_hvac.select {|data|
2515
+ data['Material'].to_s.upcase == materialLookup.to_s.upcase && data['Fuel'].to_f >= redCon && data['Size'].to_f >= redCap
2516
+ }
2517
+ if hvac_material_red.size == 0
2518
+ # If no equipment could be found which meet the remaining connection and capacity requirements then return
2519
+ # the the number and type of equipment without adjust for a smaller final piece of equipment.
2520
+ red_ret_hash = nil
2521
+ else
2522
+ # If equipment could be found then select the one with the minimum number of connections.
2523
+ red_ret = hvac_material_red.min_by{|data| data['Fuel'].to_f}
2524
+ min_hvac_sel = hvac_material_red.select{|data| data['Fuel'].to_f == red_ret['Fuel'].to_f}
2525
+ # If more than one piece of equipment can meet the minimum connection requirement then select the one with the
2526
+ # minimum capacity requirement.
2527
+ if min_hvac_sel.size > 1
2528
+ red_ret = min_hvac_sel.min_by{|data| data['Size'].to_f}
2529
+ end
2530
+ red_ret_hash = {
2531
+ red_ret_hash: red_ret,
2532
+ numCon: redCon,
2533
+ numCap: redCap
2534
+ }
2535
+ end
2536
+ end
2537
+ end
2538
+ # If multiple pieces of equipment are required to meet the connection and capacity requirements check if a smaller
2539
+ # piece of equipment was found to get the final remaining requirements. If one is found then adjust the multiplier
2540
+ # for the main equipment to be reduced by one and return the smaller remaining piece of equipment.
2541
+ red_ret_hash.nil? ? retLoops = totLoops : retLoops = totLoops - 1.0
2542
+ # Create a hash with the results and return it.
2543
+ ret_hash = {
2544
+ name: name,
2545
+ hvac_material: ret_mat,
2546
+ multiplier: retLoops
2547
+ }
2548
+ return ret_hash, red_ret_hash
2549
+ end
2550
+
2551
+ # This method is for the calculation of VSD chiller cost
2552
+ def vsd_chiller_cost(primaryCap:)
2553
+ # Gather a list of VSD chillers that exist in the costing spreadsheet
2554
+ vsd_chiller_sizes = []
2555
+ vsd_chiller_options = @costing_database['raw']['materials_hvac'].select {|data|
2556
+ data['Material'].to_s.upcase == 'ChillerElectricEIR_VSDCentrifugalWaterChiller'.upcase
2557
+ }
2558
+ vsd_chiller_options[0..-1].each do |a,b|
2559
+ a.each do |key,value|
2560
+ vsd_chiller_sizes << value.to_f if key=='Size'
2561
+ end
2562
+ end
2563
+
2564
+ # Look for a VSD in the list of VSDs that has the closest size, and calculate its cost
2565
+ vsd_chiller_closet_to_current_kw = vsd_chiller_sizes.sort_by { |item| (primaryCap-item).abs }.first(1)
2566
+ quantity_chiller_electric_eir = 1.0
2567
+ search_chiller_electric_eir = {
2568
+ row_id_1: 'ChillerElectricEIR_VSDCentrifugalWaterChiller',
2569
+ row_id_2: vsd_chiller_closet_to_current_kw[0].to_s
2570
+ }
2571
+ sheet_name = 'materials_hvac'
2572
+ column_1 = 'Material'
2573
+ column_2 = 'Size'
2574
+ tags = ['heating_and_cooling','plant_equipment','chiller']
2575
+ thisChillerCost = assembly_cost(cost_info:search_chiller_electric_eir,
2576
+ sheet_name:sheet_name,
2577
+ column_1:column_1,
2578
+ column_2:column_2,
2579
+ quantity:quantity_chiller_electric_eir,
2580
+ tags: tags)
2581
+ # puts "thisVSDChillerCost is #{thisChillerCost}"
2582
+ return thisChillerCost
2583
+ end
2584
+ end