openstudio-extension 0.1.0 → 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
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