openstudio-extension 0.1.2 → 0.1.3

Sign up to get free protection for your applications and to get access to all the features.
Files changed (81) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +29 -26
  3. data/.rspec +3 -3
  4. data/.rubocop.yml +10 -9
  5. data/CHANGELOG.md +16 -0
  6. data/Gemfile +6 -6
  7. data/Jenkinsfile +10 -10
  8. data/README.md +251 -250
  9. data/Rakefile +119 -119
  10. data/bin/console +14 -14
  11. data/bin/setup +8 -8
  12. data/doc_templates/LICENSE.md +26 -26
  13. data/doc_templates/README.md.erb +41 -41
  14. data/doc_templates/copyright_erb.txt +35 -35
  15. data/doc_templates/copyright_js.txt +3 -3
  16. data/doc_templates/copyright_ruby.txt +33 -33
  17. data/init_templates/README.md +37 -37
  18. data/init_templates/gemspec.txt +32 -32
  19. data/init_templates/openstudio_module.rb +50 -50
  20. data/init_templates/spec.rb +47 -47
  21. data/init_templates/spec_helper.rb +49 -49
  22. data/init_templates/template_gemfile.txt +17 -17
  23. data/init_templates/template_rakefile.txt +15 -15
  24. data/init_templates/version.rb +40 -40
  25. data/lib/files/openstudio-extension-gem-test.ddy +536 -536
  26. data/lib/files/openstudio-extension-gem-test.epw +8768 -8768
  27. data/lib/files/openstudio-extension-gem-test.stat +554 -554
  28. data/lib/measures/openstudio_extension_test_measure/LICENSE.md +26 -26
  29. data/lib/measures/openstudio_extension_test_measure/README.md +26 -26
  30. data/lib/measures/openstudio_extension_test_measure/README.md.erb +41 -41
  31. data/lib/measures/openstudio_extension_test_measure/measure.rb +72 -72
  32. data/lib/measures/openstudio_extension_test_measure/measure.xml +83 -83
  33. data/lib/measures/openstudio_extension_test_measure/resources/os_lib_helper_methods.rb +399 -399
  34. data/lib/measures/openstudio_extension_test_measure/tests/{OpenStudioExtensionTestMeasure_Test.rb → openstudio_extension_test_measure_test.rb} +74 -75
  35. data/lib/openstudio-extension.rb +1 -1
  36. data/lib/openstudio/extension.rb +234 -229
  37. data/lib/openstudio/extension/core/CreateResults.rb +886 -886
  38. data/lib/openstudio/extension/core/check_air_sys_temps.rb +190 -190
  39. data/lib/openstudio/extension/core/check_calibration.rb +155 -155
  40. data/lib/openstudio/extension/core/check_cond_zns.rb +84 -84
  41. data/lib/openstudio/extension/core/check_domestic_hot_water.rb +334 -334
  42. data/lib/openstudio/extension/core/check_envelope_conductance.rb +453 -453
  43. data/lib/openstudio/extension/core/check_eui_by_end_use.rb +162 -162
  44. data/lib/openstudio/extension/core/check_eui_reasonableness.rb +135 -135
  45. data/lib/openstudio/extension/core/check_fan_pwr.rb +98 -98
  46. data/lib/openstudio/extension/core/check_internal_loads.rb +393 -393
  47. data/lib/openstudio/extension/core/check_mech_sys_capacity.rb +226 -226
  48. data/lib/openstudio/extension/core/check_mech_sys_efficiency.rb +326 -326
  49. data/lib/openstudio/extension/core/check_mech_sys_part_load_eff.rb +464 -464
  50. data/lib/openstudio/extension/core/check_mech_sys_type.rb +139 -139
  51. data/lib/openstudio/extension/core/check_part_loads.rb +451 -451
  52. data/lib/openstudio/extension/core/check_placeholder.rb +75 -75
  53. data/lib/openstudio/extension/core/check_plant_cap.rb +123 -123
  54. data/lib/openstudio/extension/core/check_plant_temps.rb +159 -159
  55. data/lib/openstudio/extension/core/check_plenum_loads.rb +87 -87
  56. data/lib/openstudio/extension/core/check_pump_pwr.rb +108 -108
  57. data/lib/openstudio/extension/core/check_sch_coord.rb +241 -241
  58. data/lib/openstudio/extension/core/check_schedules.rb +311 -311
  59. data/lib/openstudio/extension/core/check_simultaneous_heating_and_cooling.rb +158 -158
  60. data/lib/openstudio/extension/core/check_supply_air_and_thermostat_temp_difference.rb +148 -148
  61. data/lib/openstudio/extension/core/check_weather_files.rb +132 -132
  62. data/lib/openstudio/extension/core/deer_vintages.rb +311 -311
  63. data/lib/openstudio/extension/core/os_lib_aedg_measures.rb +491 -491
  64. data/lib/openstudio/extension/core/os_lib_cofee.rb +258 -258
  65. data/lib/openstudio/extension/core/os_lib_constructions.rb +378 -378
  66. data/lib/openstudio/extension/core/os_lib_geometry.rb +1022 -1022
  67. data/lib/openstudio/extension/core/os_lib_helper_methods.rb +399 -399
  68. data/lib/openstudio/extension/core/os_lib_hvac.rb +2171 -2171
  69. data/lib/openstudio/extension/core/os_lib_lighting_and_equipment.rb +214 -214
  70. data/lib/openstudio/extension/core/os_lib_model_generation.rb +817 -817
  71. data/lib/openstudio/extension/core/os_lib_model_simplification.rb +1049 -1049
  72. data/lib/openstudio/extension/core/os_lib_outdoorair_and_infiltration.rb +165 -165
  73. data/lib/openstudio/extension/core/os_lib_reporting.rb +4651 -4651
  74. data/lib/openstudio/extension/core/os_lib_reporting_qaqc.rb +200 -200
  75. data/lib/openstudio/extension/core/os_lib_schedules.rb +963 -963
  76. data/lib/openstudio/extension/rake_task.rb +159 -149
  77. data/lib/openstudio/extension/runner.rb +667 -644
  78. data/lib/openstudio/extension/runner_config.rb +114 -0
  79. data/lib/openstudio/extension/version.rb +40 -40
  80. data/openstudio-extension.gemspec +34 -33
  81. metadata +39 -37
@@ -1,1049 +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
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