openstudio-extension 0.1.0 → 0.1.1

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 (80) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +9 -0
  3. data/.rubocop.yml +9 -0
  4. data/Gemfile +3 -1
  5. data/Jenkinsfile +10 -0
  6. data/README.md +230 -12
  7. data/Rakefile +88 -3
  8. data/bin/console +3 -3
  9. data/doc_templates/LICENSE.md +27 -0
  10. data/doc_templates/README.md.erb +42 -0
  11. data/doc_templates/copyright_erb.txt +36 -0
  12. data/doc_templates/copyright_js.txt +4 -0
  13. data/doc_templates/copyright_ruby.txt +34 -0
  14. data/init_templates/README.md +37 -0
  15. data/init_templates/gemspec.txt +32 -0
  16. data/init_templates/openstudio_module.rb +50 -0
  17. data/init_templates/spec.rb +47 -0
  18. data/init_templates/spec_helper.rb +49 -0
  19. data/init_templates/template_gemfile.txt +17 -0
  20. data/init_templates/template_rakefile.txt +15 -0
  21. data/init_templates/version.rb +40 -0
  22. data/lib/files/openstudio-extension-gem-test.ddy +536 -0
  23. data/lib/files/openstudio-extension-gem-test.epw +8768 -0
  24. data/lib/files/openstudio-extension-gem-test.stat +554 -0
  25. data/lib/measures/openstudio_extension_test_measure/LICENSE.md +27 -0
  26. data/lib/measures/openstudio_extension_test_measure/README.md +26 -0
  27. data/lib/measures/openstudio_extension_test_measure/README.md.erb +42 -0
  28. data/lib/measures/openstudio_extension_test_measure/measure.rb +72 -0
  29. data/lib/measures/openstudio_extension_test_measure/measure.xml +83 -0
  30. data/lib/measures/openstudio_extension_test_measure/resources/os_lib_helper_methods.rb +399 -0
  31. data/lib/measures/openstudio_extension_test_measure/tests/OpenStudioExtensionTestMeasure_Test.rb +75 -0
  32. data/lib/openstudio/extension.rb +220 -0
  33. data/lib/openstudio/extension/core/CreateResults.rb +879 -0
  34. data/lib/openstudio/extension/core/check_air_sys_temps.rb +190 -0
  35. data/lib/openstudio/extension/core/check_calibration.rb +155 -0
  36. data/lib/openstudio/extension/core/check_cond_zns.rb +84 -0
  37. data/lib/openstudio/extension/core/check_domestic_hot_water.rb +334 -0
  38. data/lib/openstudio/extension/core/check_envelope_conductance.rb +453 -0
  39. data/lib/openstudio/extension/core/check_eui_by_end_use.rb +162 -0
  40. data/lib/openstudio/extension/core/check_eui_reasonableness.rb +135 -0
  41. data/lib/openstudio/extension/core/check_fan_pwr.rb +98 -0
  42. data/lib/openstudio/extension/core/check_internal_loads.rb +393 -0
  43. data/lib/openstudio/extension/core/check_mech_sys_capacity.rb +226 -0
  44. data/lib/openstudio/extension/core/check_mech_sys_efficiency.rb +326 -0
  45. data/lib/openstudio/extension/core/check_mech_sys_part_load_eff.rb +464 -0
  46. data/lib/openstudio/extension/core/check_mech_sys_type.rb +139 -0
  47. data/lib/openstudio/extension/core/check_part_loads.rb +451 -0
  48. data/lib/openstudio/extension/core/check_placeholder.rb +75 -0
  49. data/lib/openstudio/extension/core/check_plant_cap.rb +123 -0
  50. data/lib/openstudio/extension/core/check_plant_temps.rb +159 -0
  51. data/lib/openstudio/extension/core/check_plenum_loads.rb +87 -0
  52. data/lib/openstudio/extension/core/check_pump_pwr.rb +108 -0
  53. data/lib/openstudio/extension/core/check_sch_coord.rb +241 -0
  54. data/lib/openstudio/extension/core/check_schedules.rb +311 -0
  55. data/lib/openstudio/extension/core/check_simultaneous_heating_and_cooling.rb +158 -0
  56. data/lib/openstudio/extension/core/check_supply_air_and_thermostat_temp_difference.rb +148 -0
  57. data/lib/openstudio/extension/core/check_weather_files.rb +132 -0
  58. data/lib/openstudio/extension/core/deer_vintages.rb +311 -0
  59. data/lib/openstudio/extension/core/os_lib_aedg_measures.rb +491 -0
  60. data/lib/openstudio/extension/core/os_lib_cofee.rb +259 -0
  61. data/lib/openstudio/extension/core/os_lib_constructions.rb +378 -0
  62. data/lib/openstudio/extension/core/os_lib_geometry.rb +1022 -0
  63. data/lib/openstudio/extension/core/os_lib_helper_methods.rb +399 -0
  64. data/lib/openstudio/extension/core/os_lib_hvac.rb +2171 -0
  65. data/lib/openstudio/extension/core/os_lib_lighting_and_equipment.rb +214 -0
  66. data/lib/openstudio/extension/core/os_lib_model_generation.rb +817 -0
  67. data/lib/openstudio/extension/core/os_lib_model_simplification.rb +1049 -0
  68. data/lib/openstudio/extension/core/os_lib_outdoorair_and_infiltration.rb +165 -0
  69. data/lib/openstudio/extension/core/os_lib_reporting.rb +4652 -0
  70. data/lib/openstudio/extension/core/os_lib_reporting_qaqc.rb +200 -0
  71. data/lib/openstudio/extension/core/os_lib_schedules.rb +963 -0
  72. data/lib/openstudio/extension/rake_task.rb +149 -0
  73. data/lib/openstudio/extension/runner.rb +644 -0
  74. data/lib/openstudio/extension/version.rb +40 -0
  75. data/openstudio-extension.gemspec +20 -15
  76. metadata +150 -14
  77. data/.travis.yml +0 -7
  78. data/lib/OpenStudio/Extension/rake_task.rb +0 -84
  79. data/lib/OpenStudio/Extension/version.rb +0 -33
  80. data/lib/OpenStudio/extension.rb +0 -65
@@ -0,0 +1,1049 @@
1
+ # *******************************************************************************
2
+ # OpenStudio(R), Copyright (c) 2008-2019, Alliance for Sustainable Energy, LLC.
3
+ # All rights reserved.
4
+ # Redistribution and use in source and binary forms, with or without
5
+ # modification, are permitted provided that the following conditions are met:
6
+ #
7
+ # (1) Redistributions of source code must retain the above copyright notice,
8
+ # this list of conditions and the following disclaimer.
9
+ #
10
+ # (2) Redistributions in binary form must reproduce the above copyright notice,
11
+ # this list of conditions and the following disclaimer in the documentation
12
+ # and/or other materials provided with the distribution.
13
+ #
14
+ # (3) Neither the name of the copyright holder nor the names of any contributors
15
+ # may be used to endorse or promote products derived from this software without
16
+ # specific prior written permission from the respective party.
17
+ #
18
+ # (4) Other than as required in clauses (1) and (2), distributions in any form
19
+ # of modifications or other derivative works may not use the "OpenStudio"
20
+ # trademark, "OS", "os", or any other confusingly similar designation without
21
+ # specific prior written permission from Alliance for Sustainable Energy, LLC.
22
+ #
23
+ # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDER(S) AND ANY CONTRIBUTORS
24
+ # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
25
+ # THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
26
+ # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER(S), ANY CONTRIBUTORS, THE
27
+ # UNITED STATES GOVERNMENT, OR THE UNITED STATES DEPARTMENT OF ENERGY, NOR ANY OF
28
+ # THEIR EMPLOYEES, BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
29
+ # EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT
30
+ # OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
31
+ # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
32
+ # STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
33
+ # OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
34
+ # *******************************************************************************
35
+
36
+ module OsLib_ModelSimplification
37
+ # get all loads for a space_or_space_type and place in hash by type
38
+ def gather_internal_loads(space_or_space_type)
39
+ internal_load_hash = {}
40
+
41
+ # gather different load types (all vectors except dsoa which will be turned into an array)
42
+ internal_load_hash[:internal_mass] = space_or_space_type.internalMass
43
+ internal_load_hash[:people] = space_or_space_type.people
44
+ internal_load_hash[:lights] = space_or_space_type.lights
45
+ internal_load_hash[:luminaires] = space_or_space_type.luminaires
46
+ internal_load_hash[:electric_equipment] = space_or_space_type.electricEquipment
47
+ internal_load_hash[:gas_equipment] = space_or_space_type.gasEquipment
48
+ internal_load_hash[:hot_water_equipment] = space_or_space_type.hotWaterEquipment
49
+ internal_load_hash[:steam_equipment] = space_or_space_type.steamEquipment
50
+ internal_load_hash[:other_equipment] = space_or_space_type.otherEquipment
51
+ internal_load_hash[:space_infiltration_design_flow_rates] = space_or_space_type.spaceInfiltrationDesignFlowRates
52
+ internal_load_hash[:space_infiltration_effective_leakage_areas] = space_or_space_type.spaceInfiltrationEffectiveLeakageAreas
53
+ if space_or_space_type.designSpecificationOutdoorAir.nil?
54
+ internal_load_hash[:design_specification_outdoor_air] = []
55
+ else
56
+ internal_load_hash[:design_specification_outdoor_air] = [space_or_space_type.designSpecificationOutdoorAir]
57
+ end
58
+ if space_or_space_type.class.to_s == 'OpenStudio::Model::Space'
59
+ internal_load_hash[:water_use_equipment] = space_or_space_type.waterUseEquipment # don't think this reports
60
+ internal_load_hash[:daylighting_controls] = space_or_space_type.daylightingControls
61
+ end
62
+
63
+ # TODO: - warn if daylighting controls in spaces (should I alter fraction controled based on lighting per area ratio)
64
+
65
+ return internal_load_hash
66
+ end
67
+
68
+ # blend_space_types_from_floor_area_ratio used when working from space type ratio and un-assigned space types
69
+ def blend_space_types_from_floor_area_ratio(runner, model, space_type_ratio_hash)
70
+ # create stub blended space type
71
+ blended_space_type = OpenStudio::Model::SpaceType.new(model)
72
+ blended_space_type.setName('Blended Space Type')
73
+
74
+ # TODO: - inspect people instances and see if any defs are not normalized per area. If find any issue warning
75
+
76
+ # gather inputs
77
+ sum_of_num_people_per_m_2 = 0.0
78
+ space_type_ratio_hash.each do |space_type, ratios|
79
+ # get number of peple per m 2 for space type. Can do this without looking at instances
80
+ sum_of_num_people_per_m_2 += space_type.getPeoplePerFloorArea(1.0)
81
+ end
82
+
83
+ # raw num_people_ratios
84
+ sum_area_adj_num_people_ratio = 0.0
85
+ space_type_ratio_hash.each do |space_type, ratios|
86
+ # calculate num_people_ratios
87
+ area_adj_num_people_ratio = (space_type.getPeoplePerFloorArea(1.0) / sum_of_num_people_per_m_2) * ratios[:floor_area_ratio]
88
+ sum_area_adj_num_people_ratio += area_adj_num_people_ratio
89
+ end
90
+
91
+ # set ratios
92
+ largest_space_type = nil
93
+ largest_space_type_ratio = 0.00
94
+ space_type_ratio_hash.each do |space_type, ratios|
95
+ # calculate num_people_ratios
96
+ area_adj_num_people_ratio = (space_type.getPeoplePerFloorArea(1.0) / sum_of_num_people_per_m_2) * ratios[:floor_area_ratio]
97
+ normalized_area_adj_num_people_ratio = area_adj_num_people_ratio / sum_area_adj_num_people_ratio
98
+
99
+ # ratios[:floor_area_ratio] is already defined
100
+ ratios[:num_people_ratio] = normalized_area_adj_num_people_ratio.round(4)
101
+ ratios[:ext_surface_area_ratio] = ratios[:floor_area_ratio]
102
+ ratios[:ext_wall_area_ratio] = ratios[:floor_area_ratio]
103
+ ratios[:volume_ratio] = ratios[:floor_area_ratio]
104
+
105
+ # update largest space type values
106
+ if largest_space_type.nil?
107
+ largest_space_type = space_type
108
+ largest_space_type_ratio = ratios[:floor_area_ratio]
109
+ elsif ratios[:floor_area_ratio] > largest_space_type_ratio
110
+ largest_space_type = space_type
111
+ largest_space_type_ratio = ratios[:floor_area_ratio]
112
+ end
113
+ end
114
+
115
+ if largest_space_type.nil?
116
+ runner.registerError("Didn't find any space types in model matching user argument string.")
117
+ return nil
118
+ end
119
+
120
+ # set standards info for space type based on largest ratio (for use to apply HVAC system)
121
+ standards_building_type = largest_space_type.standardsBuildingType
122
+ standards_space_type = largest_space_type.standardsSpaceType
123
+ if standards_building_type.is_initialized
124
+ blended_space_type.setStandardsBuildingType(standards_building_type.get)
125
+ end
126
+ if standards_space_type.is_initialized
127
+ blended_space_type.setStandardsSpaceType(standards_space_type.get)
128
+ end
129
+
130
+ # loop therough space types to get instances from and then remove
131
+ space_type_ratio_hash.each do |space_type, ratios|
132
+ # blend internal loads (nil is space_hash)
133
+ space_type_load_instances = blend_internal_loads(runner, model, space_type, blended_space_type, ratios, model.getBuilding.floorArea, nil)
134
+ runner.registerInfo("Blending #{space_type.name.get} with floor area ratio of #{ratios[:floor_area_ratio]} and number of people ratio of #{ratios[:num_people_ratio]}.")
135
+
136
+ # delete space type. Don't want to leave in model since internal loads have been removed from it
137
+ space_type.remove
138
+ end
139
+
140
+ return blended_space_type
141
+ end
142
+
143
+ # takes in space type hash where each hash value is a colleciton of space types. Each collection is blended into it's own space type
144
+ # If key for any collection is "Building" it will also opererate on spaces that don't have space type assigned
145
+ # where a space assigned to a space type from a collection has space loads, those space loads are normalized and added to the blended space type
146
+ # load instances are maintained so that they can haave unique schedules, and can have EE measures selectivly applied.
147
+ def blend_space_type_collections(runner, model, space_type_hash)
148
+ # loop through building type hash to create multiple blends
149
+ space_type_hash.each do |collection_name, space_types|
150
+ if collection_name == 'Building'
151
+ space_array = model.getSpaces # use all space types, not just space types passed in
152
+ else
153
+ space_array = []
154
+ space_types.each do |space_type|
155
+ space_array.concat(space_type.spaces)
156
+ end
157
+ end
158
+
159
+ # calculate metrics for all spaces included in building area to pass into space_type and space hash
160
+ # note: in the future this may be a subset of spaces if blending into multiple space types vs. just one.
161
+ collection_totals = {}
162
+ collection_totals[:floor_area] = 0.0
163
+ collection_totals[:num_people] = 0.0
164
+ collection_totals[:ext_surface_area] = 0.0
165
+ collection_totals[:ext_wall_area] = 0.0
166
+ collection_totals[:volume] = 0.0
167
+ space_array.each do |space|
168
+ next if !space.partofTotalFloorArea
169
+ collection_totals[:floor_area] += space.floorArea * space.multiplier
170
+ collection_totals[:num_people] += space.numberOfPeople * space.multiplier
171
+ collection_totals[:ext_surface_area] += space.exteriorArea * space.multiplier
172
+ collection_totals[:ext_wall_area] += space.exteriorWallArea * space.multiplier
173
+ collection_totals[:volume] += space.volume * space.multiplier
174
+ end
175
+ area_ip = OpenStudio.convert(collection_totals[:floor_area], 'm^2', 'ft^2').get
176
+ area_ip_neat = OpenStudio.toNeatString(area_ip, 2, true)
177
+ runner.registerInfo("#{collection_name} area is #{area_ip_neat} ft^2, number of people is #{collection_totals[:num_people].round(0)}.")
178
+
179
+ # create hash of space types and floor area for all space types with area > 0 when spaces included in floor area
180
+ # code to gather space type areas came from openstudio_results measure.
181
+ space_type_hash = {}
182
+ largest_space_type = nil
183
+ largest_space_type_ratio = 0.00
184
+ space_types.each do |space_type|
185
+ next if space_type.floorArea == 0
186
+ space_type_totals = {}
187
+ space_type_totals[:floor_area] = 0.0
188
+ space_type_totals[:num_people] = 0.0
189
+ space_type_totals[:ext_surface_area] = 0.0
190
+ space_type_totals[:ext_wall_area] = 0.0
191
+ space_type_totals[:volume] = 0.0
192
+ # loop through spaces so I can skip if not included in floor area
193
+ space_type.spaces.each do |space|
194
+ next if !space.partofTotalFloorArea
195
+ space_type_totals[:floor_area] += space.floorArea * space.multiplier
196
+ space_type_totals[:num_people] += space.numberOfPeople * space.multiplier
197
+ space_type_totals[:ext_surface_area] += space.exteriorArea * space.multiplier
198
+ space_type_totals[:ext_wall_area] += space.exteriorWallArea * space.multiplier
199
+ space_type_totals[:volume] += space.volume * space.multiplier
200
+ end
201
+
202
+ # update largest space type values
203
+ if largest_space_type.nil?
204
+ largest_space_type = space_type
205
+ largest_space_type_ratio = space_type_totals[:floor_area]
206
+ elsif space_type_totals[:floor_area] > largest_space_type_ratio
207
+ largest_space_type = space_type
208
+ largest_space_type_ratio = space_type_totals[:floor_area]
209
+ end
210
+
211
+ # gather internal loads
212
+ space_type_loads_hash = gather_internal_loads(space_type)
213
+
214
+ # don't add to hash if no spaces used for space type are included in building area (e.g. plenum and attic)
215
+ # todo - log these and decide what to do for them. Leave loads alone or remove, do they add to blend at all?
216
+ next if space_type_totals[:floor_area] == 0
217
+
218
+ if !space_type_totals[:floor_area] = space_type.floorArea # TODO: - not sure if these would ever show as different
219
+ runner.registerWarning("Some but not all spaces of #{space_type.name} space type are not included in the building floor area. May have unexpected results")
220
+ end
221
+
222
+ # populate space type hash
223
+ space_type_hash[space_type] = { int_loads: space_type_loads_hash, totals: space_type_totals }
224
+ end
225
+
226
+ # report initial condition of model
227
+ runner.registerInfo("#{collection_name} accounts for #{space_type_hash.size} space types.")
228
+
229
+ if collection_name == 'Building'
230
+ # count area of spaces that have no space type
231
+ no_space_type_area_counter = 0
232
+ model.getSpaces.each do |space|
233
+ if space.spaceType.empty?
234
+ next if !space.partofTotalFloorArea
235
+ no_space_type_area_counter += space.floorArea * space.multiplier
236
+ end
237
+ end
238
+ floor_area_ratio = no_space_type_area_counter / collection_totals[:floor_area]
239
+ if floor_area_ratio > 0
240
+ runner.registerInfo("#{floor_area_ratio} fraction of building area is composed of spaces without space type assignments.")
241
+ end
242
+ end
243
+
244
+ # report the space ratio for hard spaces
245
+ space_hash = {}
246
+ space_array.each do |space|
247
+ next if !space.partofTotalFloorArea
248
+ space_loads_hash = gather_internal_loads(space)
249
+ space_totals = {}
250
+ space_totals[:floor_area] = space.floorArea * space.multiplier
251
+ space_totals[:num_people] = space.numberOfPeople * space.multiplier
252
+ space_totals[:ext_surface_area] = space.exteriorArea * space.multiplier
253
+ space_totals[:ext_wall_area] = space.exteriorWallArea * space.multiplier
254
+ space_totals[:volume] = space.volume * space.multiplier
255
+ if !space_loads_hash[:daylighting_controls].empty?
256
+ runner.registerWarning("#{space.name} has one or more daylighting controls. Lighting loads from blended space type may affect lighting reduction from daylighting controls.")
257
+ end
258
+ if !space_loads_hash[:water_use_equipment].empty?
259
+ runner.registerInfo("One ore more water use equipment objects are associated with space #{space.name}. This can't be moved to a space type.")
260
+ end
261
+ # note: If generating ratios without geometry can calculate people_ratio given space_types floor_area_ratio
262
+ space_hash[space] = { int_loads: space_loads_hash, totals: space_totals }
263
+ end
264
+
265
+ # create stub blended space type
266
+ blended_space_type = OpenStudio::Model::SpaceType.new(model)
267
+ blended_space_type.setName("#{collection_name} Blended Space Type")
268
+
269
+ # set standards info for space type based on largest ratio (for use to apply HVAC system)
270
+ standards_building_type = largest_space_type.standardsBuildingType
271
+ standards_space_type = largest_space_type.standardsSpaceType
272
+ if standards_building_type.is_initialized
273
+ blended_space_type.setStandardsBuildingType(standards_building_type.get)
274
+ end
275
+ if standards_space_type.is_initialized
276
+ blended_space_type.setStandardsSpaceType(standards_space_type.get)
277
+ end
278
+
279
+ # values from collection hash
280
+ collection_floor_area = collection_totals[:floor_area]
281
+ collection_num_people = collection_totals[:num_people]
282
+ collection_ext_surface_area = collection_totals[:ext_surface_area]
283
+ collection_ext_wall_area = collection_totals[:ext_wall_area]
284
+ collection_volume = collection_totals[:volume]
285
+
286
+ # loop through space that have one or more spaces included in the building area
287
+ space_type_hash.each do |space_type, hash|
288
+ # hard assign space load schedules before re-assign instances to blended space type
289
+ space_type.hardApplySpaceLoadSchedules
290
+
291
+ # vaules from space or space_type
292
+ floor_area = hash[:totals][:floor_area]
293
+ num_people = hash[:totals][:num_people]
294
+ ext_surface_area = hash[:totals][:ext_surface_area]
295
+ ext_wall_area = hash[:totals][:ext_wall_area]
296
+ volume = hash[:totals][:volume]
297
+
298
+ # ratios
299
+ ratios = {}
300
+ if collection_floor_area > 0
301
+ ratios[:floor_area_ratio] = floor_area / collection_floor_area
302
+ else
303
+ ratios[:floor_area_ratio] = 0.0
304
+ end
305
+ if collection_num_people > 0
306
+ ratios[:num_people_ratio] = num_people / collection_num_people
307
+ else
308
+ ratios[:num_people_ratio] = 0.0
309
+ end
310
+ if collection_ext_surface_area > 0
311
+ ratios[:ext_surface_area_ratio] = ext_surface_area / collection_ext_surface_area
312
+ else
313
+ ratios[:ext_surface_area_ratio] = 0.0
314
+ end
315
+ if collection_ext_wall_area > 0
316
+ ratios[:ext_wall_area_ratio] = ext_wall_area / collection_ext_wall_area
317
+ else
318
+ ratios[:ext_wall_area_ratio] = 0.0
319
+ end
320
+ if collection_volume > 0
321
+ ratios[:volume_ratio] = volume / collection_volume
322
+ else
323
+ ratios[:volume_ratio] = 0.0
324
+ end
325
+
326
+ # populate blended space type with space type loads
327
+ space_type_load_instances = blend_internal_loads(runner, model, space_type, blended_space_type, ratios, collection_floor_area, space_hash)
328
+ runner.registerInfo("Blending space type #{space_type.name}. Floor area ratio is #{(hash[:totals][:floor_area] / collection_totals[:floor_area]).round(3)}. People ratio is #{(hash[:totals][:num_people] / collection_totals[:num_people]).round(3)}")
329
+
330
+ # hard assign any constructions assigned by space types, except for space not included in the building area
331
+ if space_type.defaultConstructionSet.is_initialized
332
+ runner.registerInfo("Hard assigning constructions for #{space_type.name}.")
333
+ space_type.spaces.each(&:hardApplyConstructions)
334
+ end
335
+
336
+ # remove all space type assignments, except for spaces not included in building area.
337
+ space_type.spaces.each do |space|
338
+ next if !space.partofTotalFloorArea
339
+ space.resetSpaceType
340
+ end
341
+
342
+ # delete space type. Don't want to leave in model since internal loads have been removed from it
343
+ space_type.remove
344
+ end
345
+
346
+ # loop through spaces that are included in building area
347
+ space_hash.each do |space, hash|
348
+ # hard assign space load schedules before re-assign instances to blended space type
349
+ space.hardApplySpaceLoadSchedules
350
+
351
+ # vaules from space or space_type
352
+ floor_area = hash[:totals][:floor_area]
353
+ num_people = hash[:totals][:num_people]
354
+ ext_surface_area = hash[:totals][:ext_surface_area]
355
+ ext_wall_area = hash[:totals][:ext_wall_area]
356
+ volume = hash[:totals][:volume]
357
+
358
+ # ratios
359
+ ratios = {}
360
+ if collection_floor_area > 0
361
+ ratios[:floor_area_ratio] = floor_area / collection_floor_area
362
+ else
363
+ ratios[:floor_area_ratio] = 0.0
364
+ end
365
+ if collection_num_people > 0
366
+ ratios[:num_people_ratio] = num_people / collection_num_people
367
+ else
368
+ ratios[:num_people_ratio] = 0.0
369
+ end
370
+ if collection_ext_surface_area > 0
371
+ ratios[:ext_surface_area_ratio] = ext_surface_area / collection_ext_surface_area
372
+ else
373
+ ratios[:ext_surface_area_ratio] = 0.0
374
+ end
375
+ if collection_ext_wall_area > 0
376
+ ratios[:ext_wall_area_ratio] = ext_wall_area / collection_ext_wall_area
377
+ else
378
+ ratios[:ext_wall_area_ratio] = 0.0
379
+ end
380
+ if collection_volume > 0
381
+ ratios[:volume_ratio] = volume / collection_volume
382
+ else
383
+ ratios[:volume_ratio] = 0.0
384
+ end
385
+
386
+ # populate blended space type with space loads
387
+ space_load_instances = blend_internal_loads(runner, model, space, blended_space_type, ratios, collection_floor_area, space_hash)
388
+ next if space_load_instances.empty?
389
+ runner.registerInfo("Blending space #{space.name}. Floor area ratio is #{(hash[:totals][:floor_area] / collection_totals[:floor_area]).round(3)}. People ratio is #{(hash[:totals][:num_people] / collection_totals[:num_people]).round(3)}")
390
+ end
391
+
392
+ if collection_name == 'Building'
393
+ # assign blended space type to building
394
+ model.getBuilding.setSpaceType(blended_space_type)
395
+ building_space_type = model.getBuilding.spaceType
396
+ else
397
+ space_array.each do |space|
398
+ space.setSpaceType(blended_space_type)
399
+ end
400
+ end
401
+ end
402
+
403
+ return model.getSpaceTypes
404
+ end
405
+
406
+ # blend internal loads used when working from existing model
407
+ def blend_internal_loads(runner, model, source_space_or_space_type, target_space_type, ratios, collection_floor_area, space_hash)
408
+ # ratios
409
+ floor_area_ratio = ratios[:floor_area_ratio]
410
+ num_people_ratio = ratios[:num_people_ratio]
411
+ ext_surface_area_ratio = ratios[:ext_surface_area_ratio]
412
+ ext_wall_area_ratio = ratios[:ext_wall_area_ratio]
413
+ volume_ratio = ratios[:volume_ratio]
414
+
415
+ # for normalizing design level loads I need to know effective number of spaces instance is applied to
416
+ if source_space_or_space_type.to_Space.is_initialized
417
+ eff_num_spaces = source_space_or_space_type.multiplier
418
+ else
419
+ eff_num_spaces = 0
420
+ source_space_or_space_type.spaces.each do |space|
421
+ eff_num_spaces += space.multiplier
422
+ end
423
+ end
424
+
425
+ # array of load instacnes re-assigned to blended space
426
+ instances_array = []
427
+
428
+ # internal_mass
429
+ source_space_or_space_type.internalMass.each do |load_inst|
430
+ load_def = load_inst.definition.to_InternalMassDefinition.get
431
+ if load_def.surfaceArea.is_initialized
432
+ # edit and assign a clone of definition and normalize per area based on floor area ratio
433
+ if collection_floor_area == 0
434
+ runner.registerWarning("Can't determine building floor area to normalize #{load_def}. #{load_inst} will be asigned the the blended space without altering its values.")
435
+ else
436
+ cloned_load_def = load_def.clone(model).to_InternalMass.get
437
+ orig_design_level = cloned_load_def.surfaceArea.get
438
+ cloned_load_def.setSurfaceAreaperSpaceFloorArea(eff_num_spaces * orig_design_level / collection_floor_area)
439
+ cloned_load_def.setName("#{cloned_load_def.name} - pre-normalized value was #{orig_design_level.round} m^2.")
440
+ load_inst.setInternalMassDefinition(cloned_load_def)
441
+ end
442
+ elsif load_def.surfaceAreaperSpaceFloorArea.is_initialized
443
+ load_inst.setMultiplier(load_inst.multiplier * floor_area_ratio)
444
+ elsif load_def.surfaceAreaperPerson.is_initialized
445
+ if num_people_ratio.nil?
446
+ runner.registerError("#{load_def} has value defined per person, but people ratio wasn't passed in")
447
+ return false
448
+ else
449
+ load_inst.setMultiplier(load_inst.multiplier * num_people_ratio)
450
+ end
451
+ else
452
+ runner.registerError("Unexpected value type for #{load_def.name}")
453
+ return false
454
+ end
455
+ load_inst.setSpaceType(target_space_type)
456
+ instances_array << load_inst
457
+ end
458
+
459
+ # people
460
+ source_space_or_space_type.people.each do |load_inst|
461
+ load_def = load_inst.definition.to_PeopleDefinition.get
462
+ if load_def.numberofPeople.is_initialized
463
+ # edit and assign a clone of definition and normalize per area based on floor area ratio
464
+ if collection_floor_area == 0
465
+ runner.registerWarning("Can't determine building floor area to normalize #{load_def}. #{load_inst} will be asigned the the blended space without altering its values.")
466
+ else
467
+ cloned_load_def = load_def.clone(model).to_PeopleDefinition.get
468
+ orig_design_level = cloned_load_def.numberofPeople.get
469
+ cloned_load_def.setPeopleperSpaceFloorArea(eff_num_spaces * orig_design_level / collection_floor_area)
470
+ cloned_load_def.setName("#{cloned_load_def.name} - pre-normalized value was #{orig_design_level.round} people.")
471
+ load_inst.setPeopleDefinition(cloned_load_def)
472
+ end
473
+ elsif load_def.peopleperSpaceFloorArea.is_initialized
474
+ load_inst.setMultiplier(load_inst.multiplier * floor_area_ratio)
475
+ elsif load_def.spaceFloorAreaperPerson.is_initialized
476
+ load_inst.setMultiplier(load_inst.multiplier * floor_area_ratio)
477
+ else
478
+ runner.registerError("Unexpected value type for #{load_def.name}")
479
+ return false
480
+ end
481
+ load_inst.setSpaceType(target_space_type)
482
+ instances_array << load_inst
483
+ end
484
+
485
+ # lights
486
+ source_space_or_space_type.lights.each do |load_inst|
487
+ load_def = load_inst.definition.to_LightsDefinition.get
488
+ if load_def.lightingLevel.is_initialized
489
+ # edit and assign a clone of definition and normalize per area based on floor area ratio
490
+ if collection_floor_area == 0
491
+ runner.registerWarning("Can't determine building floor area to normalize #{load_def}. #{load_inst} will be asigned the the blended space without altering its values.")
492
+ else
493
+ cloned_load_def = load_def.clone(model).to_LightsDefinition.get
494
+ orig_design_level = cloned_load_def.lightingLevel.get
495
+ cloned_load_def.setWattsperSpaceFloorArea(eff_num_spaces * orig_design_level / collection_floor_area)
496
+ cloned_load_def.setName("#{cloned_load_def.name} - pre-normalized value was #{orig_design_level.round} W.")
497
+ load_inst.setLightsDefinition(cloned_load_def)
498
+ end
499
+ elsif load_def.wattsperSpaceFloorArea.is_initialized
500
+ load_inst.setMultiplier(load_inst.multiplier * floor_area_ratio)
501
+ elsif load_def.wattsperPerson.is_initialized
502
+ if num_people_ratio.nil?
503
+ runner.registerError("#{load_def} has value defined per person, but people ratio wasn't passed in")
504
+ return false
505
+ else
506
+ load_inst.setMultiplier(load_inst.multiplier * num_people_ratio)
507
+ end
508
+ else
509
+ runner.registerError("Unexpected value type for #{load_def.name}")
510
+ return false
511
+ end
512
+ load_inst.setSpaceType(target_space_type)
513
+ instances_array << load_inst
514
+ end
515
+
516
+ # luminaires
517
+ source_space_or_space_type.luminaires.each do |load_inst|
518
+ # TODO: - can't normalize luminaire. Replace it with similar normalized lights def and instance
519
+ runner.registerWarning("Can't area normalize luminaire. Instance will be applied to every space using the blended space type")
520
+ instances_array << load_inst
521
+ end
522
+
523
+ # electric_equipment
524
+ source_space_or_space_type.electricEquipment.each do |load_inst|
525
+ load_def = load_inst.definition.to_ElectricEquipmentDefinition.get
526
+ if load_def.designLevel.is_initialized
527
+ # edit and assign a clone of definition and normalize per area based on floor area ratio
528
+ if collection_floor_area == 0
529
+ runner.registerWarning("Can't determine building floor area to normalize #{load_def}. #{load_inst} will be asigned the the blended space without altering its values.")
530
+ else
531
+ cloned_load_def = load_def.clone(model).to_ElectricEquipmentDefinition.get
532
+ orig_design_level = cloned_load_def.designLevel.get
533
+ cloned_load_def.setWattsperSpaceFloorArea(eff_num_spaces * orig_design_level / collection_floor_area)
534
+ cloned_load_def.setName("#{cloned_load_def.name} - pre-normalized value was #{orig_design_level.round} W.")
535
+ load_inst.setElectricEquipmentDefinition(cloned_load_def)
536
+ end
537
+ elsif load_def.wattsperSpaceFloorArea.is_initialized
538
+ load_inst.setMultiplier(load_inst.multiplier * floor_area_ratio)
539
+ elsif load_def.wattsperPerson.is_initialized
540
+ if num_people_ratio.nil?
541
+ runner.registerError("#{load_def} has value defined per person, but people ratio wasn't passed in")
542
+ return false
543
+ else
544
+ load_inst.setMultiplier(load_inst.multiplier * num_people_ratio)
545
+ end
546
+ else
547
+ runner.registerError("Unexpected value type for #{load_def.name}")
548
+ return false
549
+ end
550
+ load_inst.setSpaceType(target_space_type)
551
+ instances_array << load_inst
552
+ end
553
+
554
+ # gas_equipment
555
+ source_space_or_space_type.gasEquipment.each do |load_inst|
556
+ load_def = load_inst.definition.to_GasEquipmentDefinition.get
557
+ if load_def.designLevel.is_initialized
558
+ # edit and assign a clone of definition and normalize per area based on floor area ratio
559
+ if collection_floor_area == 0
560
+ runner.registerWarning("Can't determine building floor area to normalize #{load_def}. #{load_inst} will be asigned the the blended space without altering its values.")
561
+ else
562
+ cloned_load_def = load_def.clone(model).to_GasEquipmentDefinition.get
563
+ orig_design_level = cloned_load_def.designLevel.get
564
+ cloned_load_def.setWattsperSpaceFloorArea(eff_num_spaces * orig_design_level / collection_floor_area)
565
+ cloned_load_def.setName("#{cloned_load_def.name} - pre-normalized value was #{orig_design_level.round} W.")
566
+ load_inst.setGasEquipmentDefinition(cloned_load_def)
567
+ end
568
+ elsif load_def.wattsperSpaceFloorArea.is_initialized
569
+ load_inst.setMultiplier(load_inst.multiplier * floor_area_ratio)
570
+ elsif load_def.wattsperPerson.is_initialized
571
+ if num_people_ratio.nil?
572
+ runner.registerError("#{load_def} has value defined per person, but people ratio wasn't passed in")
573
+ return false
574
+ else
575
+ load_inst.setMultiplier(load_inst.multiplier * num_people_ratio)
576
+ end
577
+ else
578
+ runner.registerError("Unexpected value type for #{load_def.name}")
579
+ return false
580
+ end
581
+ load_inst.setSpaceType(target_space_type)
582
+ instances_array << load_inst
583
+ end
584
+
585
+ # hot_water_equipment
586
+ source_space_or_space_type.hotWaterEquipment.each do |load_inst|
587
+ load_def = load_inst.definition.to_HotWaterDefinition.get
588
+ if load_def.designLevel.is_initialized
589
+ # edit and assign a clone of definition and normalize per area based on floor area ratio
590
+ if collection_floor_area == 0
591
+ runner.registerWarning("Can't determine building floor area to normalize #{load_def}. #{load_inst} will be asigned the the blended space without altering its values.")
592
+ else
593
+ cloned_load_def = load_def.clone(model).to_HotWaterEquipmentDefinition.get
594
+ orig_design_level = cloned_load_def.designLevel.get
595
+ cloned_load_def.setWattsperSpaceFloorArea(eff_num_spaces * orig_design_level / collection_floor_area)
596
+ cloned_load_def.setName("#{cloned_load_def.name} - pre-normalized value was #{orig_design_level.round} W.")
597
+ load_inst.setHotWaterEquipmentDefinition(cloned_load_def)
598
+ end
599
+ elsif load_def.wattsperSpaceFloorArea.is_initialized
600
+ load_inst.setMultiplier(load_inst.multiplier * floor_area_ratio)
601
+ elsif load_def.wattsperPerson.is_initialized
602
+ if num_people_ratio.nil?
603
+ runner.registerError("#{load_def} has value defined per person, but people ratio wasn't passed in")
604
+ return false
605
+ else
606
+ load_inst.setMultiplier(load_inst.multiplier * num_people_ratio)
607
+ end
608
+ else
609
+ runner.registerError("Unexpected value type for #{load_def.name}")
610
+ return false
611
+ end
612
+ load_inst.setSpaceType(target_space_type)
613
+ instances_array << load_inst
614
+ end
615
+
616
+ # steam_equipment
617
+ source_space_or_space_type.steamEquipment.each do |load_inst|
618
+ load_def = load_inst.definition.to_SteamDefinition.get
619
+ if load_def.designLevel.is_initialized
620
+ # edit and assign a clone of definition and normalize per area based on floor area ratio
621
+ if collection_floor_area == 0
622
+ runner.registerWarning("Can't determine building floor area to normalize #{load_def}. #{load_inst} will be asigned the the blended space without altering its values.")
623
+ else
624
+ cloned_load_def = load_def.clone(model).to_SteamEquipmentDefinition.get
625
+ orig_design_level = cloned_load_def.designLevel.get
626
+ cloned_load_def.setWattsperSpaceFloorArea(eff_num_spaces * orig_design_level / collection_floor_area)
627
+ cloned_load_def.setName("#{cloned_load_def.name} - pre-normalized value was #{orig_design_level.round} W.")
628
+ load_inst.setSteamEquipmentDefinition(cloned_load_def)
629
+ end
630
+ elsif load_def.wattsperSpaceFloorArea.is_initialized
631
+ load_inst.setMultiplier(load_inst.multiplier * floor_area_ratio)
632
+ elsif load_def.wattsperPerson.is_initialized
633
+ if num_people_ratio.nil?
634
+ runner.registerError("#{load_def} has value defined per person, but people ratio wasn't passed in")
635
+ return false
636
+ else
637
+ load_inst.setMultiplier(load_inst.multiplier * num_people_ratio)
638
+ end
639
+ else
640
+ runner.registerError("Unexpected value type for #{load_def.name}")
641
+ return false
642
+ end
643
+ load_inst.setSpaceType(target_space_type)
644
+ instances_array << load_inst
645
+ end
646
+
647
+ # other_equipment
648
+ source_space_or_space_type.otherEquipment.each do |load_inst|
649
+ load_def = load_inst.definition.to_OtherDefinition.get
650
+ if load_def.designLevel.is_initialized
651
+ # edit and assign a clone of definition and normalize per area based on floor area ratio
652
+ if collection_floor_area == 0
653
+ runner.registerWarning("Can't determine building floor area to normalize #{load_def}. #{load_inst} will be asigned the the blended space without altering its values.")
654
+ else
655
+ cloned_load_def = load_def.clone(model).to_OtherEquipmentDefinition.get
656
+ orig_design_level = cloned_load_def.designLevel.get
657
+ cloned_load_def.setWattsperSpaceFloorArea(eff_num_spaces * orig_design_level / collection_floor_area)
658
+ cloned_load_def.setName("#{cloned_load_def.name} - pre-normalized value was #{orig_design_level.round} W.")
659
+ load_inst.setOtherEquipmentDefinition(cloned_load_def)
660
+ end
661
+ elsif load_def.wattsperSpaceFloorArea.is_initialized
662
+ load_inst.setMultiplier(load_inst.multiplier * floor_area_ratio)
663
+ elsif load_def.wattsperPerson.is_initialized
664
+ if num_people_ratio.nil?
665
+ runner.registerError("#{load_def} has value defined per person, but people ratio wasn't passed in")
666
+ return false
667
+ else
668
+ load_inst.setMultiplier(load_inst.multiplier * num_people_ratio)
669
+ end
670
+ else
671
+ runner.registerError("Unexpected value type for #{load_def.name}")
672
+ return false
673
+ end
674
+ load_inst.setSpaceType(target_space_type)
675
+ instances_array << load_inst
676
+ end
677
+
678
+ # space_infiltration_design_flow_rates
679
+ source_space_or_space_type.spaceInfiltrationDesignFlowRates.each do |load_inst|
680
+ if load_inst.designFlowRateCalculationMethod == 'Flow/Space'
681
+ # edit load so normalized for building area
682
+ if collection_floor_area == 0
683
+ runner.registerWarning("Can't determine building floor area to normalize #{load_def}. #{load_inst} will be asigned the the blended space without altering its values.")
684
+ else
685
+ orig_design_level = load_inst.designFlowRate.get
686
+ load_inst.setFlowperSpaceFloorArea(eff_num_spaces * orig_design_level / collection_floor_area)
687
+ load_inst.setName("#{load_inst.name} - pre-normalized value was #{orig_design_level} m^3/sec")
688
+ end
689
+ elsif load_inst.designFlowRateCalculationMethod == 'Flow/Area'
690
+ load_inst.setFlowperSpaceFloorArea(load_inst.flowperSpaceFloorArea.get * floor_area_ratio)
691
+ elsif load_inst.designFlowRateCalculationMethod == 'Flow/ExteriorArea'
692
+ load_inst.setFlowperExteriorSurfaceArea(load_inst.flowperExteriorSurfaceArea.get * ext_surface_area_ratio)
693
+ elsif load_inst.designFlowRateCalculationMethod == 'Flow/ExteriorWallArea'
694
+ load_inst.setFlowperExteriorWallArea(load_inst.flowperExteriorWallArea.get * ext_wall_area_ratio)
695
+ elsif load_inst.designFlowRateCalculationMethod == 'AirChanges/Hour'
696
+ load_inst.setAirChangesperHour (load_inst.airChangesperHour.get * volume_ratio)
697
+ else
698
+ runner.registerError("Unexpected value type for #{load_inst.name}")
699
+ return false
700
+ end
701
+ load_inst.setSpaceType(target_space_type)
702
+ instances_array << load_inst
703
+ end
704
+
705
+ # space_infiltration_effective_leakage_areas
706
+ source_space_or_space_type.spaceInfiltrationEffectiveLeakageAreas.each do |load|
707
+ # TODO: - can't normalize space_infiltration_effective_leakage_areas. Come up with logic to address this
708
+ runner.registerWarning("Can't area normalize space_infiltration_effective_leakage_areas. It will be applied to every space using the blended space type")
709
+ load.setSpaceType(target_space_type)
710
+ instances_array << load
711
+ end
712
+
713
+ # add OA object if it doesn't already exist
714
+ if target_space_type.designSpecificationOutdoorAir.is_initialized
715
+ blended_oa = target_space_type.designSpecificationOutdoorAir.get
716
+ else
717
+ blended_oa = OpenStudio::Model::DesignSpecificationOutdoorAir.new(model)
718
+ blended_oa.setName('Blended OA')
719
+ blended_oa.setOutdoorAirMethod('Sum')
720
+ target_space_type.setDesignSpecificationOutdoorAir(blended_oa)
721
+ instances_array << blended_oa
722
+ end
723
+
724
+ # update OA object
725
+ if source_space_or_space_type.designSpecificationOutdoorAir.is_initialized
726
+ oa = source_space_or_space_type.designSpecificationOutdoorAir.get
727
+ oa_sch = nil
728
+ if oa.outdoorAirFlowRateFractionSchedule.is_initialized
729
+ # TODO: - improve logic to address multiple schedules
730
+ runner.registerWarning("Schedule #{oa.outdoorAirFlowRateFractionSchedule.get.name} assigned to #{oa.name} will be ignored. New OA object will not have a schedule assigned")
731
+ end
732
+ if oa.outdoorAirMethod == 'Maximum'
733
+ # TODO: - see if way to address this by pre-calculating the max and only entering that value for space type
734
+ runner.registerWarning("Outdoor air method of Maximum will be ignored for #{oa.name}. New OA object will have outdoor air method of Sum.")
735
+ end
736
+ # adjusted ratios for oa (lowered for space type if there is hard assigned oa load for one or more spaces)
737
+ oa_floor_area_ratio = floor_area_ratio
738
+ oa_num_people_ratio = num_people_ratio
739
+ if source_space_or_space_type.class.to_s == 'OpenStudio::Model::SpaceType'
740
+ source_space_or_space_type.spaces.each do |space|
741
+ if !space.isDesignSpecificationOutdoorAirDefaulted
742
+ if space_hash.nil?
743
+ runner.registerWarning('No space_hash passed in and model has OA designed at space level.')
744
+ else
745
+ oa_floor_area_ratio -= space_hash[space][:floor_area_ratio]
746
+ oa_num_people_ratio -= space_hash[space][:num_people_ratio]
747
+ end
748
+ end
749
+ end
750
+ end
751
+ # add to values of blended OA load
752
+ if oa.outdoorAirFlowperPerson > 0
753
+ blended_oa.setOutdoorAirFlowperPerson(blended_oa.outdoorAirFlowperPerson + oa.outdoorAirFlowperPerson * oa_num_people_ratio)
754
+ end
755
+ if oa.outdoorAirFlowperFloorArea > 0
756
+ blended_oa.setOutdoorAirFlowperFloorArea(blended_oa.outdoorAirFlowperFloorArea + oa.outdoorAirFlowperFloorArea * oa_floor_area_ratio)
757
+ end
758
+ if oa.outdoorAirFlowRate > 0
759
+
760
+ # calculate quantity for instance (doesn't exist as a method in api)
761
+ if source_space_or_space_type.class.to_s == 'OpenStudio::Model::SpaceType'
762
+ quantity = 0
763
+ source_space_or_space_type.spaces.each do |space|
764
+ if !space.isDesignSpecificationOutdoorAirDefaulted
765
+ quantity += space.multiplier
766
+ end
767
+ end
768
+ else
769
+ quantity = source_space_or_space_type.multiplier
770
+ end
771
+
772
+ # can't normalize air flow rate, convert to air flow rate per floor area
773
+ blended_oa.setOutdoorAirFlowperFloorArea(blended_oa.outdoorAirFlowperFloorArea + quantity * oa.outdoorAirFlowRate / collection_floor_area)
774
+ end
775
+ if oa.outdoorAirFlowAirChangesperHour > 0
776
+ # floor area should be good approximation of area for multiplier
777
+ blended_oa.setOutdoorAirFlowAirChangesperHour(blended_oa.outdoorAirFlowAirChangesperHour + oa.outdoorAirFlowAirChangesperHour * oa_floor_area_ratio)
778
+ end
779
+ end
780
+
781
+ # note: water_use_equipment can't be assigned to a space type. Leave it as is, if assigned to space type
782
+ # todo - if we use this measure with new geometry need to find a way to pull water use equipment loads into new model
783
+
784
+ return instances_array
785
+ end
786
+
787
+ # sort building stories
788
+ def sort_building_stories_and_get_min_multiplier(model)
789
+ sorted_building_stories = {}
790
+ # loop through stories
791
+ model.getBuildingStorys.each do |story|
792
+ story_min_z = nil
793
+ # loop through spaces in story.
794
+ story.spaces.each do |space|
795
+ space_z_min = OsLib_Geometry.getSurfaceZValues(space.surfaces.to_a).min + space.zOrigin
796
+ if story_min_z.nil? || (story_min_z > space_z_min)
797
+ story_min_z = space_z_min
798
+ end
799
+ end
800
+ sorted_building_stories[story] = story_min_z
801
+ end
802
+
803
+ return sorted_building_stories
804
+ end
805
+
806
+ # gather_envelope_data for envelope simplification
807
+ def gather_envelope_data(runner, model)
808
+ runner.registerInfo('Gathering envelope data.')
809
+
810
+ # hash to contain envelope data
811
+ envelope_data_hash = {}
812
+
813
+ # used for overhang and party wall orientation catigorization
814
+ facade_options = {
815
+ 'northEast' => 45,
816
+ 'southEast' => 125,
817
+ 'southWest' => 225,
818
+ 'northWest' => 315
819
+ }
820
+
821
+ # get building level inputs
822
+ envelope_data_hash[:north_axis] = model.getBuilding.northAxis
823
+ envelope_data_hash[:building_floor_area] = model.getBuilding.floorArea
824
+ envelope_data_hash[:building_exterior_surface_area] = model.getBuilding.exteriorSurfaceArea
825
+ envelope_data_hash[:building_exterior_wall_area] = model.getBuilding.exteriorWallArea
826
+ envelope_data_hash[:building_exterior_roof_area] = envelope_data_hash[:building_exterior_surface_area] - envelope_data_hash[:building_exterior_wall_area]
827
+ envelope_data_hash[:building_air_volume] = model.getBuilding.airVolume
828
+ envelope_data_hash[:building_perimeter] = nil # will be applied for first story without ground walls
829
+
830
+ # get bounding_box
831
+ bounding_box = OpenStudio::BoundingBox.new
832
+ model.getSpaces.each do |space|
833
+ space.surfaces.each do |spaceSurface|
834
+ bounding_box.addPoints(space.transformation * spaceSurface.vertices)
835
+ end
836
+ end
837
+ min_x = bounding_box.minX.get
838
+ min_y = bounding_box.minY.get
839
+ min_z = bounding_box.minZ.get
840
+ max_x = bounding_box.maxX.get
841
+ max_y = bounding_box.maxY.get
842
+ max_z = bounding_box.maxZ.get
843
+ envelope_data_hash[:building_min_xyz] = [min_x, min_y, min_z]
844
+ envelope_data_hash[:building_max_xyz] = [max_x, max_y, max_z]
845
+
846
+ # add orientation specific wwr
847
+ ext_surfaces_hash = OsLib_Geometry.getExteriorWindowAndWllAreaByOrientation(model, model.getSpaces.to_a)
848
+ envelope_data_hash[:building_wwr_n] = ext_surfaces_hash['northWindow'] / ext_surfaces_hash['northWall']
849
+ envelope_data_hash[:building_wwr_s] = ext_surfaces_hash['southWindow'] / ext_surfaces_hash['southWall']
850
+ envelope_data_hash[:building_wwr_e] = ext_surfaces_hash['eastWindow'] / ext_surfaces_hash['eastWall']
851
+ envelope_data_hash[:building_wwr_w] = ext_surfaces_hash['westWindow'] / ext_surfaces_hash['westWall']
852
+ envelope_data_hash[:stories] = {} # each entry will be hash with buildingStory as key and attributes has values
853
+ envelope_data_hash[:space_types] = {} # each entry will be hash with spaceType as key and attributes has values
854
+
855
+ # as rough estimate overhang area / glazing area should be close to projection factor assuming overhang is same width as windows
856
+ # will only add building shading surfaces assoicated with a sub-surface.
857
+ building_overhang_area_n = 0.0
858
+ building_overhang_area_s = 0.0
859
+ building_overhang_area_e = 0.0
860
+ building_overhang_area_w = 0.0
861
+
862
+ # loop through stories based on mine z height of surfaces.
863
+ sorted_stories = sort_building_stories_and_get_min_multiplier(model).sort_by { |k, v| v }
864
+ sorted_stories.each do |story, story_min_z|
865
+ story_min_multiplier = nil
866
+ story_footprint = nil
867
+ story_multiplied_floor_area = OsLib_HelperMethods.getAreaOfSpacesInArray(model, story.spaces, 'floorArea')['totalArea']
868
+ # goal of footprint calc is to count multiplier for hotel room on facade,but not to count what is intended as a story multiplier
869
+ story_multiplied_exterior_surface_area = OsLib_HelperMethods.getAreaOfSpacesInArray(model, story.spaces, 'exteriorArea')['totalArea']
870
+ story_multiplied_exterior_wall_area = OsLib_HelperMethods.getAreaOfSpacesInArray(model, story.spaces, 'exteriorWallArea')['totalArea']
871
+ story_multiplied_exterior_roof_area = story_multiplied_exterior_surface_area - story_multiplied_exterior_wall_area
872
+ story_has_ground_walls = []
873
+ story_has_adiabatic_walls = []
874
+ story_included_in_building_area = false # will be true if any spaces on story are inclued in building area
875
+ story_max_z = nil
876
+
877
+ # loop through spaces for story gathering information
878
+ story.spaces.each do |space|
879
+ # get min multiplier value
880
+ multiplier = space.multiplier
881
+ if story_min_multiplier.nil? || (story_min_multiplier > multiplier)
882
+ story_min_multiplier = multiplier
883
+ end
884
+
885
+ # calculate footprint
886
+ story_footprint = story_multiplied_floor_area / story_min_multiplier
887
+
888
+ # see if part of floor area
889
+ if space.partofTotalFloorArea
890
+ story_included_in_building_area = true
891
+
892
+ # add to space type ratio hash when space is included in building floor area
893
+ if space.spaceType.is_initialized
894
+ space_type = space.spaceType.get
895
+ space_floor_area = space.floorArea * space.multiplier
896
+ if envelope_data_hash[:space_types].key?(space_type)
897
+ envelope_data_hash[:space_types][space_type][:floor_area] += space_floor_area
898
+ else
899
+ envelope_data_hash[:space_types][space_type] = {}
900
+ envelope_data_hash[:space_types][space_type][:floor_area] = space_floor_area
901
+
902
+ # make hash for heating and cooling setpoints
903
+ envelope_data_hash[:space_types][space_type][:htg_setpoint] = {}
904
+ envelope_data_hash[:space_types][space_type][:clg_setpoint] = {}
905
+
906
+ end
907
+
908
+ # add heating and cooling setpoints
909
+ if space.thermalZone.is_initialized && space.thermalZone.get.thermostatSetpointDualSetpoint.is_initialized
910
+ thermostat = space.thermalZone.get.thermostatSetpointDualSetpoint.get
911
+
912
+ # log heating schedule
913
+ if thermostat.heatingSetpointTemperatureSchedule.is_initialized
914
+ htg_sch = thermostat.heatingSetpointTemperatureSchedule.get
915
+ if envelope_data_hash[:space_types][space_type][:htg_setpoint].key?(htg_sch)
916
+ envelope_data_hash[:space_types][space_type][:htg_setpoint][htg_sch] += space_floor_area
917
+ else
918
+ envelope_data_hash[:space_types][space_type][:htg_setpoint][htg_sch] = space_floor_area
919
+ end
920
+ else
921
+ runner.registerWarning("#{space.thermalZone.get.name} containing #{space.name} doesn't have a heating setpoint schedule.")
922
+ end
923
+
924
+ # log cooling schedule
925
+ if thermostat.coolingSetpointTemperatureSchedule.is_initialized
926
+ clg_sch = thermostat.coolingSetpointTemperatureSchedule.get
927
+ if envelope_data_hash[:space_types][space_type][:clg_setpoint].key?(clg_sch)
928
+ envelope_data_hash[:space_types][space_type][:clg_setpoint][clg_sch] += space_floor_area
929
+ else
930
+ envelope_data_hash[:space_types][space_type][:clg_setpoint][clg_sch] = space_floor_area
931
+ end
932
+ else
933
+ runner.registerWarning("#{space.thermalZone.get.name} containing #{space.name} doesn't have a heating setpoint schedule.")
934
+ end
935
+
936
+ else
937
+ runner.registerWarning("#{space.name} either isn't in a thermal zone or doesn't have a thermostat assigned")
938
+ end
939
+
940
+ else
941
+ runner.regsiterWarning("#{space.name} is included in the building floor area but isn't assigned a space type.")
942
+ end
943
+
944
+ end
945
+
946
+ # check for walls with adiabatic and ground boundary condition
947
+ space.surfaces.each do |surface|
948
+ next if surface.surfaceType != 'Wall'
949
+ if surface.outsideBoundaryCondition == 'Ground'
950
+ story_has_ground_walls << surface
951
+ elsif surface.outsideBoundaryCondition == 'Adiabatic'
952
+ story_has_adiabatic_walls << surface
953
+ end
954
+ end
955
+
956
+ # populate overhang values
957
+ space.surfaces.each do |surface|
958
+ surface.subSurfaces.each do |sub_surface|
959
+ sub_surface.shadingSurfaceGroups.each do |shading_surface_group|
960
+ shading_surface_group.shadingSurfaces.each do |shading_surface|
961
+ absoluteAzimuth = OpenStudio.convert(sub_surface.azimuth, 'rad', 'deg').get + sub_surface.space.get.directionofRelativeNorth + model.getBuilding.northAxis
962
+ absoluteAzimuth -= 360.0 until absoluteAzimuth < 360.0
963
+ # add to hash based on orientation
964
+ if (facade_options['northEast'] <= absoluteAzimuth) && (absoluteAzimuth < facade_options['southEast']) # East overhang
965
+ building_overhang_area_e += shading_surface.grossArea * space.multiplier
966
+ elsif (facade_options['southEast'] <= absoluteAzimuth) && (absoluteAzimuth < facade_options['southWest']) # South overhang
967
+ building_overhang_area_s += shading_surface.grossArea * space.multiplier
968
+ elsif (facade_options['southWest'] <= absoluteAzimuth) && (absoluteAzimuth < facade_options['northWest']) # West overhang
969
+ building_overhang_area_w += shading_surface.grossArea * space.multiplier
970
+ else # North overhang
971
+ building_overhang_area_n += shading_surface.grossArea * space.multiplier
972
+ end
973
+ end
974
+ end
975
+ end
976
+ end
977
+
978
+ # get max z
979
+ space_z_max = OsLib_Geometry.getSurfaceZValues(space.surfaces.to_a).max + space.zOrigin
980
+ if story_max_z.nil? || (story_max_z > space_z_max)
981
+ story_max_z = space_z_max
982
+ end
983
+ end
984
+
985
+ # populate hash for story data
986
+ envelope_data_hash[:stories][story] = {}
987
+ envelope_data_hash[:stories][story][:story_min_height] = story_min_z
988
+ envelope_data_hash[:stories][story][:story_max_height] = story_max_z
989
+ envelope_data_hash[:stories][story][:story_min_multiplier] = story_min_multiplier
990
+ envelope_data_hash[:stories][story][:story_has_ground_walls] = story_has_ground_walls
991
+ envelope_data_hash[:stories][story][:story_has_adiabatic_walls] = story_has_adiabatic_walls
992
+ envelope_data_hash[:stories][story][:story_included_in_building_area] = story_included_in_building_area
993
+ envelope_data_hash[:stories][story][:story_footprint] = story_footprint
994
+ envelope_data_hash[:stories][story][:story_multiplied_floor_area] = story_multiplied_floor_area
995
+ envelope_data_hash[:stories][story][:story_exterior_surface_area] = story_multiplied_exterior_surface_area
996
+ envelope_data_hash[:stories][story][:story_multiplied_exterior_wall_area] = story_multiplied_exterior_wall_area
997
+ envelope_data_hash[:stories][story][:story_multiplied_exterior_roof_area] = story_multiplied_exterior_roof_area
998
+
999
+ # get perimeter and adiabatic walls that appear to be party walls
1000
+ perimeter_and_party_walls = OsLib_Geometry.calculate_story_exterior_wall_perimeter(runner, story, story_min_multiplier, ['Outdoors', 'Ground', 'Adiabatic'], bounding_box)
1001
+ envelope_data_hash[:stories][story][:story_perimeter] = perimeter_and_party_walls[:perimeter]
1002
+ envelope_data_hash[:stories][story][:story_party_walls] = []
1003
+ east = false
1004
+ south = false
1005
+ west = false
1006
+ north = false
1007
+ perimeter_and_party_walls[:party_walls].each do |surface|
1008
+ absoluteAzimuth = OpenStudio.convert(surface.azimuth, 'rad', 'deg').get + surface.space.get.directionofRelativeNorth + model.getBuilding.northAxis
1009
+ absoluteAzimuth -= 360.0 until absoluteAzimuth < 360.0
1010
+
1011
+ # add to hash based on orientation (initially added array of sourfaces, but swtiched to just true/false flag)
1012
+ if (facade_options['northEast'] <= absoluteAzimuth) && (absoluteAzimuth < facade_options['southEast']) # East party walls
1013
+ east = true
1014
+ elsif (facade_options['southEast'] <= absoluteAzimuth) && (absoluteAzimuth < facade_options['southWest']) # South party walls
1015
+ south = true
1016
+ elsif (facade_options['southWest'] <= absoluteAzimuth) && (absoluteAzimuth < facade_options['northWest']) # West party walls
1017
+ west = true
1018
+ else # North party walls
1019
+ north = true
1020
+ end
1021
+ end
1022
+
1023
+ if east then envelope_data_hash[:stories][story][:story_party_walls] << 'east' end
1024
+ if south then envelope_data_hash[:stories][story][:story_party_walls] << 'south' end
1025
+ if west then envelope_data_hash[:stories][story][:story_party_walls] << 'west' end
1026
+ if north then envelope_data_hash[:stories][story][:story_party_walls] << 'north' end
1027
+
1028
+ # store perimeter from first story that doesn't have ground walls
1029
+ if story_has_ground_walls.empty? && envelope_data_hash[:building_perimeter].nil?
1030
+ envelope_data_hash[:building_perimeter] = envelope_data_hash[:stories][story][:story_perimeter]
1031
+ runner.registerInfo(" * #{story.name} is the first above grade story and will be used for the building perimeter.")
1032
+ end
1033
+ end
1034
+
1035
+ envelope_data_hash[:building_overhang_proj_factor_n] = building_overhang_area_n / ext_surfaces_hash['northWindow']
1036
+ envelope_data_hash[:building_overhang_proj_factor_s] = building_overhang_area_s / ext_surfaces_hash['southWindow']
1037
+ envelope_data_hash[:building_overhang_proj_factor_e] = building_overhang_area_e / ext_surfaces_hash['eastWindow']
1038
+ envelope_data_hash[:building_overhang_proj_factor_w] = building_overhang_area_w / ext_surfaces_hash['westWindow']
1039
+
1040
+ # warn for spaces that are not on a story (in future could infer stories for these)
1041
+ model.getSpaces.each do |space|
1042
+ if !space.buildingStory.is_initialized
1043
+ runner.registerWarning("#{space.name} is not on a building story, may have unexpected results.")
1044
+ end
1045
+ end
1046
+
1047
+ return envelope_data_hash
1048
+ end
1049
+ end