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,473 @@
1
+ require 'json'
2
+ require_relative 'costing_database_wrapper.rb'
3
+ require_relative 'common_paths.rb'
4
+
5
+ class SimpleLinearRegression
6
+ #https://gist.github.com/rweald/3516193#file-full-slr-class-snippet-rb
7
+ def initialize(xs, ys)
8
+ @xs, @ys = xs, ys
9
+ if @xs.length != @ys.length
10
+ raise "Unbalanced data. xs need to be same length as ys"
11
+ end
12
+ end
13
+
14
+ def y_intercept
15
+ return mean(@ys) - (slope * mean(@xs))
16
+ end
17
+
18
+ def slope
19
+ x_mean = mean(@xs)
20
+ y_mean = mean(@ys)
21
+
22
+ numerator = (0...@xs.length).reduce(0) do |sum, i|
23
+ sum + ((@xs[i] - x_mean) * (@ys[i] - y_mean))
24
+ end
25
+
26
+ denominator = @xs.reduce(0) do |sum, x|
27
+ sum + ((x - x_mean) ** 2)
28
+ end
29
+
30
+ return (numerator / denominator)
31
+ end
32
+
33
+ def mean(values)
34
+ total = values.reduce(0) { |sum, x| x + sum }
35
+ return Float(total) / Float(values.length)
36
+ end
37
+ end
38
+
39
+
40
+ class BTAPCosting
41
+ # May be initialized with custom databases:
42
+ # costs_csv: Path to custom costing
43
+ # factors_csv: Path to custom localization factors
44
+ def initialize(costs_csv: nil, factors_csv: nil)
45
+ @cp = CommonPaths.instance
46
+ @costing_database = CostingDatabase.instance
47
+
48
+ # If the path for custom costing is defined, use custom costing.
49
+ if (not costs_csv.nil?) and File.exist?(costs_csv)
50
+ @cp.costs_path = costs_csv
51
+ end
52
+
53
+ # If the path for custom factors is defined, use custom factors.
54
+ if (not factors_csv.nil?) and File.exist?(factors_csv)
55
+ @cp.costs_local_factors_path = factors_csv
56
+ end
57
+ end
58
+
59
+ def load_database()
60
+ @costing_database.load_database
61
+ end
62
+
63
+ def validate_database()
64
+ @costing_database.validate_database
65
+ end
66
+
67
+ def generate_construction_cost_database_for_all_cities()
68
+ result = Array.new
69
+ @costing_database['raw']['locations'].each do |location|
70
+ province_state = location["province_state"]
71
+ city = location['city']
72
+ result.concat(generate_construction_cost_database_for_city(city, province_state))
73
+ end
74
+ return result
75
+ end
76
+
77
+ def generate_construction_cost_database_for_city(city, province_state)
78
+ @costing_database['constructions_costs'] = Array.new
79
+ puts "Costing for: #{province_state},#{city}"
80
+ @costing_database["raw"]['constructions_opaque'].each do |construction|
81
+ cost_construction(construction, {"province_state" => province_state, "city" => city}, 'opaque')
82
+ end
83
+ @costing_database["raw"]['constructions_glazing'].each do |construction|
84
+ cost_construction(construction, {"province_state" => province_state, "city" => city}, 'glazing')
85
+ end
86
+ puts "#{@costing_database['constructions_costs'].size} Costed Constructions for #{province_state},#{city}."
87
+ return @costing_database['constructions_costs']
88
+ end
89
+
90
+
91
+ def cost_audit_all(model:,
92
+ prototype_creator:,
93
+ envelope_costing: true,
94
+ lighting_costing: true,
95
+ boilers_costing: true,
96
+ chillers_costing: true,
97
+ cooling_towers_costing: true,
98
+ shw_costing: true,
99
+ ventilation_costing: true,
100
+ zone_system_costing: true,
101
+ renewables_costing: true,
102
+ template_type: nil
103
+ )
104
+ # Create a Hash to collect costing data.
105
+ @costing_report = {}
106
+
107
+ #Use closest city.
108
+ closest_loc = get_closest_cost_location(model.getWeatherFile.latitude, model.getWeatherFile.longitude)
109
+ @costing_report['city'] = closest_loc['city']
110
+ @costing_report['province_state'] = closest_loc['province_state']
111
+
112
+ # Create array to collect costed item information. First element is the costing location.
113
+ @cost_items = {
114
+ 'City' => closest_loc['city'],
115
+ 'Province' => closest_loc['province_state'],
116
+ 'Items' => []
117
+ }
118
+
119
+ # Create a Hash in the hash for categories of costing.
120
+ @costing_report['envelope'] = {}
121
+ @costing_report['lighting'] = {}
122
+ @costing_report['lighting']['daylighting_sensor_control'] = []
123
+ @costing_report['lighting']['led_lighting'] = []
124
+ @costing_report['heating_and_cooling'] = {}
125
+ @costing_report['heating_and_cooling']['plant_equipment'] = []
126
+ @costing_report['heating_and_cooling']['zonal_systems'] = []
127
+ @costing_report['shw'] = {}
128
+ @costing_report['ventilation'] = {}
129
+ @costing_report['renewables'] = {}
130
+ @costing_report['renewables']['pv'] = []
131
+ @costing_report['totals'] = {}
132
+
133
+ # Check to see if standards building type and the number of stories has been defined. The former may be omitted in the future.
134
+ if model.getBuilding.standardsBuildingType.empty? or model.getBuilding.standardsNumberOfAboveGroundStories.empty?
135
+ raise("Building information is not complete, please ensure that the standardsBuildingType and standardsNumberOfAboveGroundStories are entered in the model. ")
136
+ end
137
+
138
+ # Find the mechanical room
139
+ mech_room, cond_spaces = prototype_creator.find_mech_room(model)
140
+
141
+ envCost = envelope_costing ? self.cost_audit_envelope(model, prototype_creator) : 0.0
142
+ lgtCost = lighting_costing ? self.cost_audit_lighting(model, prototype_creator) : 0.0
143
+ boilerCost = boilers_costing ? self.boiler_costing(model, prototype_creator) : 0.0
144
+ chillerCost = chillers_costing ? self.chiller_costing(model, prototype_creator) : 0.0
145
+ coolingTowerCost = cooling_towers_costing ? self.coolingtower_costing(model, prototype_creator) : 0.0
146
+ shwCost = shw_costing ? self.shw_costing(model, prototype_creator) : 0.0
147
+ ventCost = ventilation_costing ? self.ventilation_costing(model, prototype_creator,template_type, mech_room, cond_spaces) : 0.0
148
+ zonalSystemCost = zone_system_costing ? self.zonalsys_costing(model, prototype_creator, mech_room, cond_spaces) : 0.0
149
+ pvGroundCost = renewables_costing ? self.cost_audit_pv_ground(model, prototype_creator) : 0.0
150
+ thermalBridgingCost = 0.0
151
+
152
+ @costing_report["totals"] = {
153
+ 'envelope' => envCost.round(0),
154
+ 'thermal_bridging' => thermalBridgingCost.round(0),
155
+ 'lighting' => lgtCost.round(0),
156
+ 'heating_and_cooling' => (boilerCost + chillerCost + coolingTowerCost + zonalSystemCost).round(0),
157
+ 'shw' => shwCost.round(0),
158
+ 'ventilation' => ventCost.round(0),
159
+ 'renewables' => pvGroundCost.round(0),
160
+ 'grand_total' => (envCost + thermalBridgingCost + lgtCost + boilerCost + chillerCost + coolingTowerCost +
161
+ shwCost + ventCost + zonalSystemCost + pvGroundCost).round(0)
162
+ }
163
+
164
+ return @costing_report, @cost_items
165
+ end
166
+
167
+ def get_regional_cost_factors(provinceState, city, material)
168
+ @costing_database['localization_factors'].select { |code|
169
+ code['province_state'] == provinceState && code['city'] == city }.each do |code|
170
+ prefix_id = material['id'][0..1]
171
+ prefix_stored = code['code_prefix']
172
+ if prefix_id == prefix_stored
173
+ return code['material'], code['installation'], code['total']
174
+ end
175
+
176
+ end
177
+ error = [material, "Could not find regional adjustment factor for material used in #{city}, #{provinceState}."]
178
+ @costing_database['db_errors'] << error unless @costing_database['db_errors'].include?(error)
179
+ return 100.0, 100.0, 100.0
180
+ end
181
+
182
+ # Interpolate array of hashes that contain 2 values (key=rsi, data=cost)
183
+ def interpolate(x_y_array:, x2:, exterpolate_percentage_range: 30.0)
184
+ ratio_range = exterpolate_percentage_range / 100.0
185
+ array = x_y_array.uniq.sort { |a, b| a[0] <=> b[0] }
186
+ #if there is only one...return what you got.
187
+ if array.size == 1
188
+ return array.first[1].to_f
189
+ end
190
+ # Check if value x2 is within range of array for interpolation
191
+ # Extrapolate when x2 is out-of-range by +/- 10% of end values.
192
+ if array.empty? || x2 < ((1.0 - ratio_range) * array.first[0].to_f) || x2 > ((1.0 + ratio_range) * array.last[0].to_f)
193
+ return nil
194
+ elsif x2 < array.first[0].to_f
195
+ # Extrapolate down using first and second cost value to this out-of-range input
196
+ x_array = [array[0][0].to_f, array[1][0].to_f]
197
+ y_array = [array[0][1].to_f, array[1][1].to_f]
198
+ linear_model = SimpleLinearRegression.new(x_array, y_array)
199
+ y2 = linear_model.y_intercept + linear_model.slope * x2
200
+ return y2
201
+ elsif x2 > array.last[0].to_f
202
+ # Extrapolate up using second to last and last cost value to this out-of-range input
203
+ x_array = [array[-2][0].to_f, array[-1][0].to_f]
204
+ y_array = [array[-2][1].to_f, array[-1][1].to_f]
205
+ linear_model = SimpleLinearRegression.new(x_array, y_array)
206
+ y2 = linear_model.y_intercept + linear_model.slope * x2
207
+ return y2
208
+ else
209
+ array.each_index do |counter|
210
+
211
+ # skip last value.
212
+ next if array[counter] == array.last
213
+
214
+ x0 = array[counter][0]
215
+ y0 = array[counter][1]
216
+ x1 = array[counter + 1][0]
217
+ y1 = array[counter + 1][1]
218
+
219
+ # skip to next if x2 is not between x0 and x1
220
+ next if x2 < x0 || x2 > x1
221
+
222
+ # Do interpolation
223
+ y2 = y0 # just in-case x0, x1 and x2 are identical!
224
+ if (x1 - x0) > 0.0
225
+ y2 = y0.to_f + ((y1 - y0).to_f * (x2 - x0).to_f / (x1 - x0).to_f)
226
+ end
227
+ return y2
228
+ end
229
+ end
230
+ end
231
+
232
+ # Enter in [latitude, longitude] for each loc and this method will return the distance.
233
+ def distance(loc1, loc2)
234
+ rad_per_deg = Math::PI / 180 # PI / 180
235
+ rkm = 6371 # Earth radius in kilometers
236
+ rm = rkm * 1000 # Radius in meters
237
+
238
+ dlat_rad = (loc2[0] - loc1[0]) * rad_per_deg # Delta, converted to rad
239
+ dlon_rad = (loc2[1] - loc1[1]) * rad_per_deg
240
+
241
+ lat1_rad, lon1_rad = loc1.map { |i| i * rad_per_deg }
242
+ lat2_rad, lon2_rad = loc2.map { |i| i * rad_per_deg }
243
+
244
+ a = Math.sin(dlat_rad / 2) ** 2 + Math.cos(lat1_rad) * Math.cos(lat2_rad) * Math.sin(dlon_rad / 2) ** 2
245
+ c = 2 * Math::atan2(Math::sqrt(a), Math::sqrt(1 - a))
246
+ rm * c # Delta in meters
247
+ end
248
+
249
+ def get_closest_cost_location(lat, long)
250
+ dist = 1000000000000000000000.0
251
+ closest_loc = nil
252
+ # province_state city latitude longitude source
253
+ @costing_database['raw']['locations'].each do |location|
254
+ if distance([lat, long], [location['latitude'].to_f, location['longitude'].to_f]) < dist
255
+ closest_loc = location
256
+ dist = distance([lat, long], [location['latitude'].to_f, location['longitude'].to_f])
257
+ end
258
+ end
259
+ return closest_loc
260
+ end
261
+
262
+ # This will expand the two letter province abbreviation to a full uppercase province name
263
+ def expandProvAbbrev(abbrev)
264
+
265
+ # Note that the proper abbreviation for Quebec is QC not PQ. However, we've used PQ in openstudio-standards!
266
+ Hash provAbbrev = {"AB" => "ALBERTA",
267
+ "BC" => "BRITISH COLUMBIA",
268
+ "MB" => "MANITOBA",
269
+ "NB" => "NEW BRUNSWICK",
270
+ "NL" => "NEWFOUNDLAND AND LABRADOR",
271
+ "NT" => "NORTHWEST TERRITORIES",
272
+ "NS" => "NOVA SCOTIA",
273
+ "NU" => "NUNAVUT",
274
+ "ON" => "ONTARIO",
275
+ "PE" => "PRINCE EDWARD ISLAND",
276
+ "PQ" => "QUEBEC",
277
+ "SK" => "SASKATCHEWAN",
278
+ "YT" => "YUKON"
279
+ }
280
+ return provAbbrev[abbrev]
281
+ end
282
+
283
+ def read_mech_sizing()
284
+ file = File.read(@cp.mech_sizing_data_file)
285
+ return JSON.parse(file)
286
+ end
287
+
288
+ # This adds costed items to the array of cousted items which end up in btap_itmes.json. Note that the array this
289
+ # method uses is created in the cost_audit_all method. The array is created with an initial element that contains the
290
+ # city and province whose localiazation factors are used for costing.
291
+ # The inputs are:
292
+ # id: (string) The costing database id for the item being costed.
293
+ # quantity: (float) The total amount of the item being costed in whatever units the item is costed in. This should
294
+ # include all multiplier used to determine this cost (e.g. such as thermal_zone multipliers). As
295
+ # an example, if 32 ft. of wire were required for a piece of equipment used in a thermal zone with
296
+ # a multiplier of 10, the quantity would be 3.2 (32 ft. * 10 / 100 since wire is costed per
297
+ # 100 ft.).
298
+ # material_mult: (float) The multiplier used to estimate the cost of an item from the base cost. For example, high
299
+ # efficiency SHW tanks are estimated to cost 30% higher than regular SHW tanks so the cost of these
300
+ # tanks are calculated by the cost * 1.3. Thus, the material_mult for high efficiency tanks
301
+ # would be 1.3. This is defauted to 1.0 if it is not provided.
302
+ # labour_mult: (float) Similar to material_mult only applied to labour costs. The labour_mult can be different than
303
+ # the material_mult. This is defaulted to 1.0 if it is not provided.
304
+ # equipment_mult: (float) Similar to material_mult and labour_mult only for equipment. This will always be 1.0 until
305
+ # equipment costs are supported.
306
+ # tags: (array of strings) This is an array which links the costed item to a component of the model that is being
307
+ # costed. For example, a material_id related to a boiler pump might have tags like ["boiler", "pump"].
308
+ #
309
+ def add_costed_item(material_id:, quantity:, material_mult: 1.0, labour_mult: 1.0, equip_mult: 1.0, tags: [])
310
+ # Do some error handling for the tags argument
311
+ tags_out = [tags] if tags.kind_of?(String)
312
+ tags_out = tags if tags.kind_of?(Array)
313
+
314
+ # Validate the type of the arguments.
315
+ if (tags_out.kind_of?(Array) == false)
316
+ raise("The tags for the item #{material_id} were not properly defined. Please search for where the item is being added to the @cost_items hash via the add_costed_item method and correct the entry.")
317
+ end
318
+
319
+ if (material_id.kind_of?(String) == false)
320
+ raise("The material_id for the item #{material_id} is not a string. Please search for where the item is being added to the @cost_items hash via the add_costed_item method and correct the entry.")
321
+ end
322
+
323
+ if (quantity.kind_of?(Float) == false)
324
+ raise("The quantity for the item #{material_id} is not a float. Please search for where the item is being added to the @cost_items hash via the add_costed_item method and correct the entry.")
325
+ end
326
+
327
+ if (material_mult.kind_of?(Float) == false)
328
+ raise("The material_mult for the item #{material_id} is not a float. Please search for where the item is being added to the @cost_items hash via the add_costed_item method and correct the entry.")
329
+ end
330
+
331
+ if (labour_mult.kind_of?(Float) == false)
332
+ raise("The labour_mult for the item #{material_id} is not a float. Please search for where the item is being added to the @cost_items hash via the add_costed_item method and correct the entry.")
333
+ end
334
+
335
+ if (equip_mult.kind_of?(Float) == false)
336
+ raise("The equip_mult for the item #{material_id} is not a float. Please search for where the item is being added to the @cost_items hash via the add_costed_item method and correct the entry.")
337
+ end
338
+
339
+ # Add the costed item to the output output hash.
340
+ @cost_items['Items'] << {
341
+ 'id' => material_id,
342
+ 'quantity' => quantity,
343
+ 'material_mult' => material_mult,
344
+ 'labour_mult' => labour_mult,
345
+ 'equipment_mult' => equip_mult,
346
+ 'tags' => tags_out
347
+ }
348
+ end
349
+
350
+ # This method takes the list of costed items in the building generated with the help of the above add_costed_item
351
+ # method and finds the costs for the list of items. It takes in:
352
+ # btap_items: (array of hashes) This array contains all the items that must be costed. The first element of the
353
+ # array is:
354
+ # {
355
+ # City: (string) City used for cost lacalization factor
356
+ # Province: (string) Province used for cost localization factor
357
+ # }
358
+ # The remaining arrays look like:
359
+ # {
360
+ # id: (string) ID of the coested item in question.
361
+ # quantity: (float) Amount of costed item (should include all multipliers except localization factors,
362
+ # material_mult, labour_mult, equipment_mult).
363
+ # material_mult: (float) Material multiplier from cost spreadsheet used mainly for higher performance equipment
364
+ # (for example, regular and high performance boilers share the same id but high performance
365
+ # boilers have a material_mult of around 1.3-that is they are estimated to be 1.3 times as
366
+ # expensive as regular boilers).
367
+ # labour_mult: (float) Same idea as material_mult only for labour (often this will be 1.0 even if material_mult
368
+ # is something else).
369
+ # equipment_mult: (float) Same idea as labour_mult only for equipment. It will always be 1.0 until equipment
370
+ # costs are implemented in costing.
371
+ # tags: (array of strings) An array of strings used to define what part of the building is being costed (e.g.
372
+ # an component for a ccashp might have these tags: "Ventilation", "CCASHP", "ccashp_condensor")
373
+ # }
374
+ # custom_costing: (array of hashes) A custom costing database if you do not want to use the default one. This must
375
+ # have the same format as that found by @costing_database['costs']
376
+ # custCity: (string) A custom cost localization city if you do not want to use the one in the first item in the
377
+ # btap_itmes hash.
378
+ # custProvince: (string) A custom cost localization province if you do not want to use the one in the first item in
379
+ # the btap_items hash.
380
+ #
381
+ # The output of the method is a hash containing these summary costs:
382
+ # costRetHash = {
383
+ # envelope: (float) Building envelope costs (to 2 decimal places).
384
+ # lighting: (float) Ligting costs (to 2 decimal places).
385
+ # heating_and_cooling: (float) Heating and cooling costs (not related to ventilation) (to 2 decimal places).
386
+ # shw: (float) Service hot water costs (to 2 decimal places).
387
+ # ventilation: (float) Ventilation (including ventilation air heating and cooling) costs (to 2 decimal places).
388
+ # grand_total: (float) Total costs (to 2 decimal places).
389
+ # }
390
+ #
391
+ def cost_list_items(btap_items:, custom_costing: nil, custCity: nil, custProvince: nil)
392
+ # Check if costing is for a custom city and province. If not use the city and province found in the first entry
393
+ # of the array of costed items.
394
+ if custCity.nil? || custProvince.nil?
395
+ costCity = btap_items['City'].to_s
396
+ costProvince = btap_items['Province'].to_s
397
+ else
398
+ costCity = custCity
399
+ costProvince = custProvince
400
+ end
401
+
402
+ # Initialize cost counters
403
+ totCost = 0.0
404
+ envCost = 0.0
405
+ lightCost = 0.0
406
+ heatCoolCost = 0.0
407
+ shwCost = 0.0
408
+ ventCost = 0.0
409
+ renewCost = 0.0
410
+
411
+ custom_costing.nil? ? costingDB = @costing_database['costs'] : costingDB = custom_costing
412
+
413
+ btap_items['Items'].each do |costing_item|
414
+ # Look for the costing information for the piece of equipment in the costing database.
415
+ costing_data = costingDB.detect {|data| data['id'].to_s.upcase == costing_item['id'].to_s.upcase}
416
+ # If no costing information is found then return an error.
417
+ if costing_data.nil?
418
+ raise "Error: no costing information available for material id #{costing_item['id']}!"
419
+ elsif costing_data['baseCosts']['materialOpCost'].nil? && costing_data['baseCosts']['laborOpCost'].nil?
420
+ #This is a stub for some work that needs to be done to account for equipment costing. For now this is zeroed out.
421
+ # A similar test is done on reading the data from the database and collected in the error file when the
422
+ # costing database is generated.
423
+ raise "Error: costing information for material id #{costing_item['id']} is nil. Please check costing data."
424
+ end
425
+ costing_data['baseCosts']['equipmentOpCost'].nil? ? equip_base_cost = 0.0 : equip_base_cost = costing_data['baseCosts']['equipmentOpCost'].to_f
426
+ costing_data['baseCosts']['materialOpCost'].nil? ? mat_base_cost = 0.0 : mat_base_cost = costing_data['baseCosts']['materialOpCost'].to_f
427
+ costing_data['baseCosts']['laborOpCost'].nil? ? lab_base_cost = 0.0 : lab_base_cost = costing_data['baseCosts']['laborOpCost'].to_f
428
+
429
+ # The costs from the costing database are US national average costs (for placeholder costs) or whatever is in the
430
+ # 'province_state' and 'city' fields (for custom costs). These costs need to be adjusted to reflect the costs
431
+ # expected in the location of interest. The 'get_regional_cost_factors' method finds the appropriate cost
432
+ # adjustment factors.
433
+
434
+ mat_mult, inst_mult, eq_mult = get_regional_cost_factors(costProvince, costCity, costing_item)
435
+ if mat_mult.nil? || inst_mult.nil?
436
+ raise("Error: no localization information available for material id #{costing_item['material_id']}!")
437
+ end
438
+ # Get any associated material or labour multiplier for the equipment present in the 'materials_hvac' sheet in the
439
+ # costing spreadsheet.
440
+ costing_item['material_mult'].to_f == 0 ? mat_quant = 1.0 : mat_quant = costing_item['material_mult'].to_f
441
+ costing_item['labour_mult'].to_f == 0 ? lab_quant = 1.0 : lab_quant = costing_item['labour_mult'].to_f
442
+ costing_item['equipment_mult'].to_f == 0 || costing_item['equipment_mult'].nil? ? eq_quant = 1.0 : eq_quant = costing_item['equipment_mult'].to_f
443
+ # Calculate the adjusted material and labour costs.
444
+ mat_cost = mat_base_cost*(mat_mult/100.0)*mat_quant
445
+ lab_cost = lab_base_cost*(inst_mult/100.0)*lab_quant
446
+ eq_cost = equip_base_cost*(eq_mult/100.0)*eq_quant
447
+ # Calculate the total item cost.
448
+ item_cost = (mat_cost + lab_cost + eq_cost)*(costing_item["quantity"].to_f)
449
+
450
+ # Add cost to sub-type counters
451
+ envCost += item_cost unless (costing_item['tags'].select{|data| data.to_s.upcase == "ENVELOPE"}).empty?
452
+ lightCost += item_cost unless (costing_item['tags'].select{|data| data.to_s.upcase == "LIGHTING"}).empty?
453
+ heatCoolCost += item_cost unless (costing_item['tags'].select{|data| data.to_s.upcase == "HEATING_COOLING"}).empty?
454
+ shwCost += item_cost unless (costing_item['tags'].select{|data| data.to_s.upcase == "SHW"}).empty?
455
+ ventCost += item_cost unless (costing_item['tags'].select{|data| data.to_s.upcase == "VENTILATION"}).empty?
456
+ renewCost += item_cost unless (costing_item['tags'].select{|data| data.to_s.upcase == "RENEWABLES"}).empty?
457
+ totCost += item_cost
458
+ end
459
+
460
+ # Create and return hash containing costing results
461
+ costRetHash = {
462
+ 'envelope' => envCost.round(2),
463
+ 'lighting' => lightCost.round(2),
464
+ 'heating_and_cooling' => heatCoolCost.round(2),
465
+ 'shw' => shwCost.round(2),
466
+ 'ventilation' => ventCost.round(2),
467
+ 'renewables' => renewCost.round(2),
468
+ 'grand_total' => totCost.round(2)
469
+ }
470
+ return costRetHash
471
+ end
472
+
473
+ end