openstudio-extension 0.1.0 → 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (80) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +9 -0
  3. data/.rubocop.yml +9 -0
  4. data/Gemfile +3 -1
  5. data/Jenkinsfile +10 -0
  6. data/README.md +230 -12
  7. data/Rakefile +88 -3
  8. data/bin/console +3 -3
  9. data/doc_templates/LICENSE.md +27 -0
  10. data/doc_templates/README.md.erb +42 -0
  11. data/doc_templates/copyright_erb.txt +36 -0
  12. data/doc_templates/copyright_js.txt +4 -0
  13. data/doc_templates/copyright_ruby.txt +34 -0
  14. data/init_templates/README.md +37 -0
  15. data/init_templates/gemspec.txt +32 -0
  16. data/init_templates/openstudio_module.rb +50 -0
  17. data/init_templates/spec.rb +47 -0
  18. data/init_templates/spec_helper.rb +49 -0
  19. data/init_templates/template_gemfile.txt +17 -0
  20. data/init_templates/template_rakefile.txt +15 -0
  21. data/init_templates/version.rb +40 -0
  22. data/lib/files/openstudio-extension-gem-test.ddy +536 -0
  23. data/lib/files/openstudio-extension-gem-test.epw +8768 -0
  24. data/lib/files/openstudio-extension-gem-test.stat +554 -0
  25. data/lib/measures/openstudio_extension_test_measure/LICENSE.md +27 -0
  26. data/lib/measures/openstudio_extension_test_measure/README.md +26 -0
  27. data/lib/measures/openstudio_extension_test_measure/README.md.erb +42 -0
  28. data/lib/measures/openstudio_extension_test_measure/measure.rb +72 -0
  29. data/lib/measures/openstudio_extension_test_measure/measure.xml +83 -0
  30. data/lib/measures/openstudio_extension_test_measure/resources/os_lib_helper_methods.rb +399 -0
  31. data/lib/measures/openstudio_extension_test_measure/tests/OpenStudioExtensionTestMeasure_Test.rb +75 -0
  32. data/lib/openstudio/extension.rb +220 -0
  33. data/lib/openstudio/extension/core/CreateResults.rb +879 -0
  34. data/lib/openstudio/extension/core/check_air_sys_temps.rb +190 -0
  35. data/lib/openstudio/extension/core/check_calibration.rb +155 -0
  36. data/lib/openstudio/extension/core/check_cond_zns.rb +84 -0
  37. data/lib/openstudio/extension/core/check_domestic_hot_water.rb +334 -0
  38. data/lib/openstudio/extension/core/check_envelope_conductance.rb +453 -0
  39. data/lib/openstudio/extension/core/check_eui_by_end_use.rb +162 -0
  40. data/lib/openstudio/extension/core/check_eui_reasonableness.rb +135 -0
  41. data/lib/openstudio/extension/core/check_fan_pwr.rb +98 -0
  42. data/lib/openstudio/extension/core/check_internal_loads.rb +393 -0
  43. data/lib/openstudio/extension/core/check_mech_sys_capacity.rb +226 -0
  44. data/lib/openstudio/extension/core/check_mech_sys_efficiency.rb +326 -0
  45. data/lib/openstudio/extension/core/check_mech_sys_part_load_eff.rb +464 -0
  46. data/lib/openstudio/extension/core/check_mech_sys_type.rb +139 -0
  47. data/lib/openstudio/extension/core/check_part_loads.rb +451 -0
  48. data/lib/openstudio/extension/core/check_placeholder.rb +75 -0
  49. data/lib/openstudio/extension/core/check_plant_cap.rb +123 -0
  50. data/lib/openstudio/extension/core/check_plant_temps.rb +159 -0
  51. data/lib/openstudio/extension/core/check_plenum_loads.rb +87 -0
  52. data/lib/openstudio/extension/core/check_pump_pwr.rb +108 -0
  53. data/lib/openstudio/extension/core/check_sch_coord.rb +241 -0
  54. data/lib/openstudio/extension/core/check_schedules.rb +311 -0
  55. data/lib/openstudio/extension/core/check_simultaneous_heating_and_cooling.rb +158 -0
  56. data/lib/openstudio/extension/core/check_supply_air_and_thermostat_temp_difference.rb +148 -0
  57. data/lib/openstudio/extension/core/check_weather_files.rb +132 -0
  58. data/lib/openstudio/extension/core/deer_vintages.rb +311 -0
  59. data/lib/openstudio/extension/core/os_lib_aedg_measures.rb +491 -0
  60. data/lib/openstudio/extension/core/os_lib_cofee.rb +259 -0
  61. data/lib/openstudio/extension/core/os_lib_constructions.rb +378 -0
  62. data/lib/openstudio/extension/core/os_lib_geometry.rb +1022 -0
  63. data/lib/openstudio/extension/core/os_lib_helper_methods.rb +399 -0
  64. data/lib/openstudio/extension/core/os_lib_hvac.rb +2171 -0
  65. data/lib/openstudio/extension/core/os_lib_lighting_and_equipment.rb +214 -0
  66. data/lib/openstudio/extension/core/os_lib_model_generation.rb +817 -0
  67. data/lib/openstudio/extension/core/os_lib_model_simplification.rb +1049 -0
  68. data/lib/openstudio/extension/core/os_lib_outdoorair_and_infiltration.rb +165 -0
  69. data/lib/openstudio/extension/core/os_lib_reporting.rb +4652 -0
  70. data/lib/openstudio/extension/core/os_lib_reporting_qaqc.rb +200 -0
  71. data/lib/openstudio/extension/core/os_lib_schedules.rb +963 -0
  72. data/lib/openstudio/extension/rake_task.rb +149 -0
  73. data/lib/openstudio/extension/runner.rb +644 -0
  74. data/lib/openstudio/extension/version.rb +40 -0
  75. data/openstudio-extension.gemspec +20 -15
  76. metadata +150 -14
  77. data/.travis.yml +0 -7
  78. data/lib/OpenStudio/Extension/rake_task.rb +0 -84
  79. data/lib/OpenStudio/Extension/version.rb +0 -33
  80. data/lib/OpenStudio/extension.rb +0 -65
@@ -0,0 +1,1022 @@
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_Geometry
37
+ # lower z value of vertices with starting value above x to new value of y
38
+ def self.lowerSurfaceZvalue(surfaceArray, zValueTarget)
39
+ counter = 0
40
+
41
+ # loop over all surfaces
42
+ surfaceArray.each do |surface|
43
+ # create a new set of vertices
44
+ newVertices = OpenStudio::Point3dVector.new
45
+
46
+ # get the existing vertices for this interior partition
47
+ vertices = surface.vertices
48
+ flag = false
49
+ vertices.each do |vertex|
50
+ # initialize new vertex to old vertex
51
+ x = vertex.x
52
+ y = vertex.y
53
+ z = vertex.z
54
+
55
+ # if this z vertex is not on the z = 0 plane
56
+ if z > zValueTarget
57
+ z = zValueTarget
58
+ flag = true
59
+ end
60
+
61
+ # add point to new vertices
62
+ newVertices << OpenStudio::Point3d.new(x, y, z)
63
+ end
64
+
65
+ # set vertices to new vertices
66
+ surface.setVertices(newVertices) # todo check if this was made, and issue warning if it was not. Could happen if resulting surface not planer.
67
+
68
+ if flag then counter += 1 end
69
+ end
70
+
71
+ result = counter
72
+ return result
73
+ end
74
+
75
+ # return an array of z values for surfaces passed in. The values will be relative to the parent origin. This was intended for spaces.
76
+ def self.getSurfaceZValues(surfaceArray)
77
+ zValueArray = []
78
+
79
+ # loop over all surfaces
80
+ surfaceArray.each do |surface|
81
+ # get the existing vertices
82
+ vertices = surface.vertices
83
+ vertices.each do |vertex|
84
+ # push z value to array
85
+ zValueArray << vertex.z
86
+ end
87
+ end
88
+
89
+ result = zValueArray
90
+ return result
91
+ end
92
+
93
+ def self.createPointAtCenterOfFloor(model, space, zOffset)
94
+ # find floors
95
+ floors = []
96
+ space.surfaces.each do |surface|
97
+ next if surface.surfaceType != 'Floor'
98
+ floors << surface
99
+ end
100
+
101
+ # this method only works for flat (non-inclined) floors
102
+ boundingBox = OpenStudio::BoundingBox.new
103
+ floors.each do |floor|
104
+ boundingBox.addPoints(floor.vertices)
105
+ end
106
+ xmin = boundingBox.minX.get
107
+ ymin = boundingBox.minY.get
108
+ zmin = boundingBox.minZ.get
109
+ xmax = boundingBox.maxX.get
110
+ ymax = boundingBox.maxY.get
111
+
112
+ x_pos = (xmin + xmax) / 2
113
+ y_pos = (ymin + ymax) / 2
114
+ z_pos = zmin + zOffset
115
+
116
+ floorSurfacesInSpace = []
117
+ space.surfaces.each do |surface|
118
+ if surface.surfaceType == 'Floor'
119
+ floorSurfacesInSpace << surface
120
+ end
121
+ end
122
+
123
+ pointIsOnFloor = OsLib_Geometry.checkIfPointIsOnSurfaceInArray(OpenStudio::Point3d.new(x_pos, y_pos, zmin), floorSurfacesInSpace)
124
+
125
+ if pointIsOnFloor
126
+ new_point = OpenStudio::Point3d.new(x_pos, y_pos, z_pos)
127
+ else
128
+ # don't make point, it doesn't appear to be inside of the space
129
+ new_point = nil
130
+ end
131
+
132
+ result = new_point
133
+ return result
134
+ end
135
+
136
+ def self.createPointInFromSubSurfaceAtSpecifiedHeight(model, subSurface, referenceFloor, distanceInFromWindow, heightAboveBottomOfSubSurface)
137
+ window_outward_normal = subSurface.outwardNormal
138
+ window_centroid = OpenStudio.getCentroid(subSurface.vertices).get
139
+ window_outward_normal.setLength(distanceInFromWindow)
140
+ vertex = window_centroid + window_outward_normal.reverseVector
141
+ vertex_on_floorplane = referenceFloor.plane.project(vertex)
142
+ floor_outward_normal = referenceFloor.outwardNormal
143
+ floor_outward_normal.setLength(heightAboveBottomOfSubSurface)
144
+
145
+ floorSurfacesInSpace = []
146
+ subSurface.space.get.surfaces.each do |surface|
147
+ if surface.surfaceType == 'Floor'
148
+ floorSurfacesInSpace << surface
149
+ end
150
+ end
151
+
152
+ pointIsOnFloor = OsLib_Geometry.checkIfPointIsOnSurfaceInArray(vertex_on_floorplane, floorSurfacesInSpace)
153
+
154
+ if pointIsOnFloor
155
+ new_point = vertex_on_floorplane + floor_outward_normal.reverseVector
156
+ else
157
+ # don't make point, it doesn't appear to be inside of the space
158
+ new_point = vertex_on_floorplane + floor_outward_normal.reverseVector # nil
159
+ end
160
+
161
+ result = new_point
162
+ return result
163
+ end
164
+
165
+ def self.checkIfPointIsOnSurfaceInArray(point, surfaceArray)
166
+ onSurfacesFlag = false
167
+
168
+ surfaceArray.each do |surface|
169
+ # Check if sensor is on floor plane (I need to loop through all floors)
170
+ plane = surface.plane
171
+ point_on_plane = plane.project(point)
172
+
173
+ faceTransform = OpenStudio::Transformation.alignFace(surface.vertices)
174
+ faceVertices = faceTransform * surface.vertices
175
+ facePointOnPlane = faceTransform * point_on_plane
176
+
177
+ if OpenStudio.pointInPolygon(facePointOnPlane, faceVertices.reverse, 0.01)
178
+ # initial_sensor location lands in this surface's polygon
179
+ onSurfacesFlag = true
180
+ end
181
+ end
182
+
183
+ if onSurfacesFlag
184
+ result = true
185
+ else
186
+ result = false
187
+ end
188
+
189
+ return result
190
+ end
191
+
192
+ def self.getExteriorWindowToWallRatio(spaceArray)
193
+ # counters
194
+ total_gross_ext_wall_area = 0
195
+ total_ext_window_area = 0
196
+
197
+ spaceArray.each do |space|
198
+ # get surface area adjusting for zone multiplier
199
+ zone = space.thermalZone
200
+ if !zone.empty?
201
+ zone_multiplier = zone.get.multiplier
202
+ if zone_multiplier > 1
203
+ end
204
+ else
205
+ zone_multiplier = 1 # space is not in a thermal zone
206
+ end
207
+
208
+ space.surfaces.each do |s|
209
+ next if s.surfaceType != 'Wall'
210
+ next if s.outsideBoundaryCondition != 'Outdoors'
211
+
212
+ surface_gross_area = s.grossArea * zone_multiplier
213
+
214
+ # loop through sub surfaces and add area including multiplier
215
+ ext_window_area = 0
216
+ s.subSurfaces.each do |subSurface|
217
+ ext_window_area += subSurface.grossArea * subSurface.multiplier * zone_multiplier
218
+ end
219
+
220
+ total_gross_ext_wall_area += surface_gross_area
221
+ total_ext_window_area += ext_window_area
222
+ end
223
+ end
224
+
225
+ if total_gross_ext_wall_area > 0
226
+ result = total_ext_window_area / total_gross_ext_wall_area
227
+ else
228
+ result = 0.0 # TODO: - this should not happen if the building has geometry
229
+ end
230
+
231
+ return result
232
+ end
233
+
234
+ # create core and perimeter polygons from length width and origin
235
+ def self.make_core_and_perimeter_polygons(runner, length, width, footprint_origin = OpenStudio::Point3d.new(0, 0, 0), perimeter_zone_depth = OpenStudio.convert(15, 'ft', 'm').get)
236
+ hash_of_point_vectors = {} # key is name, value is a hash, one item of which is polygon. Another could be space type
237
+
238
+ # determine if core and perimeter zoning can be used
239
+ if !(length > perimeter_zone_depth * 2.5 && width > perimeter_zone_depth * 2.5)
240
+ perimeter_zone_depth = 0 # if any size is to small then just model floor as single zone, issue warning
241
+ runner.registerWarning('Due to the size of the building modeling each floor as a single zone.')
242
+ end
243
+
244
+ x_delta = footprint_origin.x - length / 2.0
245
+ y_delta = footprint_origin.y - width / 2.0
246
+ z = 0
247
+ nw_point = OpenStudio::Point3d.new(x_delta, y_delta + width, z)
248
+ ne_point = OpenStudio::Point3d.new(x_delta + length, y_delta + width, z)
249
+ se_point = OpenStudio::Point3d.new(x_delta + length, y_delta, z)
250
+ sw_point = OpenStudio::Point3d.new(x_delta, y_delta, z)
251
+
252
+ # Define polygons for a rectangular building
253
+ if perimeter_zone_depth > 0
254
+ perimeter_nw_point = nw_point + OpenStudio::Vector3d.new(perimeter_zone_depth, -perimeter_zone_depth, 0)
255
+ perimeter_ne_point = ne_point + OpenStudio::Vector3d.new(-perimeter_zone_depth, -perimeter_zone_depth, 0)
256
+ perimeter_se_point = se_point + OpenStudio::Vector3d.new(-perimeter_zone_depth, perimeter_zone_depth, 0)
257
+ perimeter_sw_point = sw_point + OpenStudio::Vector3d.new(perimeter_zone_depth, perimeter_zone_depth, 0)
258
+
259
+ west_polygon = OpenStudio::Point3dVector.new
260
+ west_polygon << sw_point
261
+ west_polygon << nw_point
262
+ west_polygon << perimeter_nw_point
263
+ west_polygon << perimeter_sw_point
264
+ hash_of_point_vectors['West Perimeter Space'] = {}
265
+ hash_of_point_vectors['West Perimeter Space'][:space_type] = nil # other methods being used by makeSpacesFromPolygons may have space types associated with each polygon but this doesn't.
266
+ hash_of_point_vectors['West Perimeter Space'][:polygon] = west_polygon
267
+
268
+ north_polygon = OpenStudio::Point3dVector.new
269
+ north_polygon << nw_point
270
+ north_polygon << ne_point
271
+ north_polygon << perimeter_ne_point
272
+ north_polygon << perimeter_nw_point
273
+ hash_of_point_vectors['North Perimeter Space'] = {}
274
+ hash_of_point_vectors['North Perimeter Space'][:space_type] = nil
275
+ hash_of_point_vectors['North Perimeter Space'][:polygon] = north_polygon
276
+
277
+ east_polygon = OpenStudio::Point3dVector.new
278
+ east_polygon << ne_point
279
+ east_polygon << se_point
280
+ east_polygon << perimeter_se_point
281
+ east_polygon << perimeter_ne_point
282
+ hash_of_point_vectors['East Perimeter Space'] = {}
283
+ hash_of_point_vectors['East Perimeter Space'][:space_type] = nil
284
+ hash_of_point_vectors['East Perimeter Space'][:polygon] = east_polygon
285
+
286
+ south_polygon = OpenStudio::Point3dVector.new
287
+ south_polygon << se_point
288
+ south_polygon << sw_point
289
+ south_polygon << perimeter_sw_point
290
+ south_polygon << perimeter_se_point
291
+ hash_of_point_vectors['South Perimeter Space'] = {}
292
+ hash_of_point_vectors['South Perimeter Space'][:space_type] = nil
293
+ hash_of_point_vectors['South Perimeter Space'][:polygon] = south_polygon
294
+
295
+ core_polygon = OpenStudio::Point3dVector.new
296
+ core_polygon << perimeter_sw_point
297
+ core_polygon << perimeter_nw_point
298
+ core_polygon << perimeter_ne_point
299
+ core_polygon << perimeter_se_point
300
+ hash_of_point_vectors['Core Space'] = {}
301
+ hash_of_point_vectors['Core Space'][:space_type] = nil
302
+ hash_of_point_vectors['Core Space'][:polygon] = core_polygon
303
+
304
+ # Minimal zones
305
+ else
306
+ whole_story_polygon = OpenStudio::Point3dVector.new
307
+ whole_story_polygon << sw_point
308
+ whole_story_polygon << nw_point
309
+ whole_story_polygon << ne_point
310
+ whole_story_polygon << se_point
311
+ hash_of_point_vectors['Whole Story Space'] = {}
312
+ hash_of_point_vectors['Whole Story Space'][:space_type] = nil
313
+ hash_of_point_vectors['Whole Story Space'][:polygon] = whole_story_polygon
314
+ end
315
+
316
+ return hash_of_point_vectors
317
+ end
318
+
319
+ # sliced bar multi creates and array of multiple sliced bar simple hashes
320
+ def self.make_sliced_bar_multi_polygons(runner, space_types, length, width, footprint_origin = OpenStudio::Point3d.new(0, 0, 0), story_hash)
321
+ # total building floor area to calculate ratios from space type floor areas
322
+ total_floor_area = 0.0
323
+ target_per_space_type = {}
324
+ space_types.each do |space_type, space_type_hash|
325
+ total_floor_area += space_type_hash[:floor_area]
326
+ target_per_space_type[space_type] = space_type_hash[:floor_area]
327
+ end
328
+
329
+ # sort array by floor area, this hash will be altered to reduce floor area for each space type to 0
330
+ space_types_running_count = space_types.sort_by { |k, v| v[:floor_area] }
331
+
332
+ # array entry for each story
333
+ footprints = []
334
+
335
+ # variables for sliver check
336
+ valid_bar_width_min = OpenStudio.convert(3, 'ft', 'm').get # re-evaluate what this should be
337
+ bar_length = width # building width
338
+ valid_bar_area_min = valid_bar_width_min * bar_length
339
+
340
+ # loop through stories to populate footprints
341
+ story_hash.each_with_index do |(k, v), i|
342
+ # update the length and width for partial floors
343
+ if i + 1 == story_hash.size
344
+ area_multiplier = v[:partial_story_multiplier]
345
+ edge_multiplier = Math.sqrt(area_multiplier)
346
+ length *= edge_multiplier
347
+ width *= edge_multiplier
348
+ end
349
+
350
+ # this will be populated for each building story
351
+ target_footprint_area = v[:multiplier] * length * width
352
+ current_footprint_area = 0.0
353
+ space_types_local_count = {}
354
+
355
+ space_types_running_count.each do |space_type, space_type_hash|
356
+ # next if floor area is full or space type is empty
357
+ next if current_footprint_area >= target_footprint_area
358
+ next if space_type_hash[:floor_area] <= 0.0
359
+
360
+ # special test for when total floor area is smaller than valid_bar_area_min, just make bar smaller that valid min and warn user
361
+ if target_per_space_type[space_type] < valid_bar_area_min
362
+ sliver_override = true
363
+ runner.registerWarning("Floor area of #{space_type.name} results in a bar with smaller than target minimum width.")
364
+ else
365
+ sliver_override = false
366
+ end
367
+
368
+ # add entry for space type if it doesn't have one yet
369
+ if !space_types_local_count.key?(space_type)
370
+ space_types_local_count[space_type] = { floor_area: 0.0 }
371
+ end
372
+
373
+ # if there is enough of this space type to fill rest of floor area
374
+ remaining_in_footprint = target_footprint_area - current_footprint_area
375
+ if space_type_hash[:floor_area] > remaining_in_footprint
376
+
377
+ # add to local count for story and remove from running count from space type
378
+ raw_footprint_area_used = remaining_in_footprint
379
+
380
+ else
381
+ # if not then use up the rest of the floor area and move on to next space type
382
+ raw_footprint_area_used = space_type_hash[:floor_area]
383
+ end
384
+
385
+ # add to local hash
386
+ space_types_local_count[space_type][:floor_area] = raw_footprint_area_used / v[:multiplier].to_f
387
+
388
+ # adjust balance ot running and local counts
389
+ current_footprint_area += raw_footprint_area_used
390
+ space_type_hash[:floor_area] -= raw_footprint_area_used
391
+
392
+ # test if think slice left on current floor.
393
+ # fix by moving smallest space type to next floor and and the same amount more of the sliver space type to this story
394
+ raw_footprint_area_used < valid_bar_area_min && sliver_override == false ? (test_a = true) : (test_a = false)
395
+
396
+ # test if what would be left of the current space type would result in a sliver on the next story.
397
+ # fix by removing some of this space type so their is enough left for the next story, and replace the removed amount with the largest space type in the model
398
+ (space_type_hash[:floor_area] < valid_bar_area_min) && (space_type_hash[:floor_area] > 0.0001) ? (test_b = true) : (test_b = false)
399
+
400
+ # identify very small slices and re-arrange spaces to different stories to avoid this
401
+ if test_a
402
+
403
+ # get first/smallest space type to move to another story
404
+ first_space = space_types_local_count.first
405
+
406
+ # adjustments running counter for space type being removed from this story
407
+ space_types_running_count.each do |k2, v2|
408
+ next if k2 != first_space[0]
409
+ v2[:floor_area] += first_space[1][:floor_area] * v[:multiplier]
410
+ end
411
+
412
+ # adjust running count for current space type
413
+ space_type_hash[:floor_area] -= first_space[1][:floor_area] * v[:multiplier]
414
+
415
+ # add to local count for current space type
416
+ space_types_local_count[space_type][:floor_area] += first_space[1][:floor_area]
417
+
418
+ # remove from local count for removed space type
419
+ space_types_local_count.shift
420
+
421
+ elsif test_b
422
+
423
+ # swap size
424
+ swap_size = valid_bar_area_min * 5 # currently equal to default perimeter zone depth of 15'
425
+
426
+ # adjust running count for current space type
427
+ space_type_hash[:floor_area] += swap_size
428
+
429
+ # remove from local count for current space type
430
+ space_types_local_count[space_type][:floor_area] -= swap_size / v[:multiplier].to_f
431
+
432
+ # adjust footprint used
433
+ current_footprint_area -= swap_size
434
+
435
+ # the next larger space type will be brought down to fill out the footprint without any additional code
436
+
437
+ end
438
+ end
439
+
440
+ # creating footprint for story
441
+ footprints << OsLib_Geometry.make_sliced_bar_simple_polygons(runner, space_types_local_count, length, width, footprint_origin)
442
+ end
443
+
444
+ return footprints
445
+ end
446
+
447
+ # sliced bar simple creates a single sliced bar for space types passed in
448
+ # todo - look at length and width to adjust slicing direction
449
+ def self.make_sliced_bar_simple_polygons(runner, space_types, length, width, footprint_origin = OpenStudio::Point3d.new(0, 0, 0), perimeter_zone_depth = OpenStudio.convert(15, 'ft', 'm').get)
450
+ hash_of_point_vectors = {} # key is name, value is a hash, one item of which is polygon. Another could be space type
451
+
452
+ # determine if core and perimeter zoning can be used
453
+ if !(length > perimeter_zone_depth * 2.5 && width > perimeter_zone_depth * 2.5)
454
+ perimeter_zone_depth = 0 # if any size is to small then just model floor as single zone, issue warning
455
+ runner.registerWarning('Not modeling core and perimeter zones for some portion of the model.')
456
+ end
457
+
458
+ x_delta = footprint_origin.x - length / 2.0
459
+ y_delta = footprint_origin.y - width / 2.0
460
+ z = 0
461
+ # this represents the entire bar, not individual space type slices
462
+ nw_point = OpenStudio::Point3d.new(x_delta, y_delta + width, z)
463
+ sw_point = OpenStudio::Point3d.new(x_delta, y_delta, z)
464
+
465
+ # total building floor area to calculate ratios from space type floor areas
466
+ total_floor_area = 0.0
467
+ space_types.each do |space_type, space_type_hash|
468
+ total_floor_area += space_type_hash[:floor_area]
469
+ end
470
+
471
+ # sort array by floor area but shift largest object to front
472
+ space_types = space_types.sort_by { |k, v| v[:floor_area] }
473
+ space_types.insert(0, space_types.delete_at(space_types.size - 1))
474
+
475
+ # min and max bar end values
476
+ min_bar_end_multiplier = 0.75
477
+ max_bar_end_multiplier = 1.5
478
+
479
+ # sort_by results in arrays with two items , first is key, second is hash value
480
+ re_apply_largest_space_type_at_end = false
481
+ max_reduction = nil # used when looping through section_hash_for_space_type if first space type needs to also be at far end of bar
482
+ space_types.each do |space_type, space_type_hash|
483
+ # setup end perimeter zones if needed
484
+ start_perimeter_width_deduction = 0.0
485
+ end_perimeter_width_deduction = 0.0
486
+ if space_type == space_types.first[0]
487
+ if length * space_type_hash[:floor_area] / total_floor_area > max_bar_end_multiplier * perimeter_zone_depth
488
+ start_perimeter_width_deduction = perimeter_zone_depth
489
+ end
490
+ # see if last space type is too small for perimeter. If it is then save some of this space type
491
+ if length * space_types.last[1][:floor_area] / total_floor_area < perimeter_zone_depth * min_bar_end_multiplier
492
+ re_apply_largest_space_type_at_end = true
493
+ end
494
+ end
495
+ if space_type == space_types.last[0]
496
+ if length * space_type_hash[:floor_area] / total_floor_area > max_bar_end_multiplier * perimeter_zone_depth
497
+ end_perimeter_width_deduction = perimeter_zone_depth
498
+ end
499
+ end
500
+ non_end_adjusted_width = (length * space_type_hash[:floor_area] / total_floor_area) - start_perimeter_width_deduction - end_perimeter_width_deduction
501
+
502
+ # adjustment of end space type is too small and is replaced with largest space type
503
+ if (space_type == space_types.first[0]) && re_apply_largest_space_type_at_end
504
+ max_reduction = [perimeter_zone_depth, non_end_adjusted_width].min
505
+ non_end_adjusted_width -= max_reduction
506
+ end
507
+ if (space_type == space_types.last[0]) && re_apply_largest_space_type_at_end
508
+ end_perimeter_width_deduction = space_types.first[0]
509
+ end
510
+
511
+ # poulate data for core and perimeter of slice
512
+ section_hash_for_space_type = {}
513
+ section_hash_for_space_type['end_a'] = start_perimeter_width_deduction
514
+ section_hash_for_space_type[''] = non_end_adjusted_width
515
+ section_hash_for_space_type['end_b'] = end_perimeter_width_deduction
516
+
517
+ # loop through sections for space type (main and possibly one or two end perimeter sections)
518
+ section_hash_for_space_type.each do |k, width|
519
+ if width.class.to_s == 'OpenStudio::Model::SpaceType' # confirm this
520
+ space_type = width
521
+ max_reduction = [perimeter_zone_depth, max_reduction].min
522
+ width = max_reduction
523
+ end
524
+ if width == 0
525
+ next
526
+ end
527
+
528
+ ne_point = nw_point + OpenStudio::Vector3d.new(width, 0, 0)
529
+ se_point = sw_point + OpenStudio::Vector3d.new(width, 0, 0)
530
+
531
+ if perimeter_zone_depth > 0
532
+ polygon_a = OpenStudio::Point3dVector.new
533
+ polygon_a << sw_point
534
+ polygon_a << sw_point + OpenStudio::Vector3d.new(0, perimeter_zone_depth, 0)
535
+ polygon_a << se_point + OpenStudio::Vector3d.new(0, perimeter_zone_depth, 0)
536
+ polygon_a << se_point
537
+ hash_of_point_vectors["#{space_type.name} A #{k}"] = {}
538
+ hash_of_point_vectors["#{space_type.name} A #{k}"][:space_type] = space_type
539
+ hash_of_point_vectors["#{space_type.name} A #{k}"][:polygon] = polygon_a
540
+
541
+ polygon_b = OpenStudio::Point3dVector.new
542
+ polygon_b << sw_point + OpenStudio::Vector3d.new(0, perimeter_zone_depth, 0)
543
+ polygon_b << nw_point + OpenStudio::Vector3d.new(0, - perimeter_zone_depth, 0)
544
+ polygon_b << ne_point + OpenStudio::Vector3d.new(0, - perimeter_zone_depth, 0)
545
+ polygon_b << se_point + OpenStudio::Vector3d.new(0, perimeter_zone_depth, 0)
546
+ hash_of_point_vectors["#{space_type.name} B #{k}"] = {}
547
+ hash_of_point_vectors["#{space_type.name} B #{k}"][:space_type] = space_type
548
+ hash_of_point_vectors["#{space_type.name} B #{k}"][:polygon] = polygon_b
549
+
550
+ polygon_c = OpenStudio::Point3dVector.new
551
+ polygon_c << nw_point + OpenStudio::Vector3d.new(0, - perimeter_zone_depth, 0)
552
+ polygon_c << nw_point
553
+ polygon_c << ne_point
554
+ polygon_c << ne_point + OpenStudio::Vector3d.new(0, - perimeter_zone_depth, 0)
555
+ hash_of_point_vectors["#{space_type.name} C #{k}"] = {}
556
+ hash_of_point_vectors["#{space_type.name} C #{k}"][:space_type] = space_type
557
+ hash_of_point_vectors["#{space_type.name} C #{k}"][:polygon] = polygon_c
558
+ else
559
+ polygon_a = OpenStudio::Point3dVector.new
560
+ polygon_a << sw_point
561
+ polygon_a << nw_point
562
+ polygon_a << ne_point
563
+ polygon_a << se_point
564
+ hash_of_point_vectors["#{space_type.name} #{k}"] = {}
565
+ hash_of_point_vectors["#{space_type.name} #{k}"][:space_type] = space_type
566
+ hash_of_point_vectors["#{space_type.name} #{k}"][:polygon] = polygon_a
567
+ end
568
+
569
+ # update west points
570
+ nw_point = ne_point
571
+ sw_point = se_point
572
+ end
573
+ end
574
+
575
+ return hash_of_point_vectors
576
+ end
577
+
578
+ # take diagram made by make_core_and_perimeter_polygons and make multi-story building
579
+ # todo - add option to create shading surfaces when using multiplier. Mainly important for non rectangular buildings where self shading would be an issue
580
+ def self.makeSpacesFromPolygons(runner, model, footprints, typical_story_height, effective_num_stories, footprint_origin = OpenStudio::Point3d.new(0, 0, 0), story_hash = {})
581
+ # default story hash is for three stories with mid-story multiplier, but user can pass in custom versions
582
+ if story_hash.empty?
583
+ if effective_num_stories > 2
584
+ story_hash['Ground'] = { space_origin_z: footprint_origin.z, space_height: typical_story_height, multiplier: 1 }
585
+ story_hash['Mid'] = { space_origin_z: footprint_origin.z + typical_story_height + typical_story_height * (effective_num_stories.ceil - 3) / 2.0, space_height: typical_story_height, multiplier: effective_num_stories - 2 }
586
+ story_hash['Top'] = { space_origin_z: footprint_origin.z + typical_story_height * (effective_num_stories.ceil - 1), space_height: typical_story_height, multiplier: 1 }
587
+ elsif effective_num_stories > 1
588
+ story_hash['Ground'] = { space_origin_z: footprint_origin.z, space_height: typical_story_height, multiplier: 1 }
589
+ story_hash['Top'] = { space_origin_z: footprint_origin.z + typical_story_height * (effective_num_stories.ceil - 1), space_height: typical_story_height, multiplier: 1 }
590
+ else # one story only
591
+ story_hash['Ground'] = { space_origin_z: footprint_origin.z, space_height: typical_story_height, multiplier: 1 }
592
+ end
593
+ end
594
+
595
+ # loop through story_hash and polygons to generate all of the spaces
596
+ story_hash.each_with_index do |(story_name, story_data), index|
597
+ # make new story
598
+ story = OpenStudio::Model::BuildingStory.new(model)
599
+ story.setNominalFloortoFloorHeight(story_data[:space_height]) # not used for anything
600
+ story.setNominalZCoordinate (story_data[:space_origin_z]) # not used for anything
601
+ story.setName("Story #{story_name}")
602
+
603
+ # multiplier values for adjacent stories to be altered below as needed
604
+ multiplier_story_above = 1
605
+ multiplier_story_below = 1
606
+
607
+ if index == 0 # bottom floor, only check above
608
+ if story_hash.size > 1
609
+ multiplier_story_above = story_hash.values[index + 1][:multiplier]
610
+ end
611
+ elsif index == story_hash.size - 1 # top floor, check only below
612
+ multiplier_story_below = story_hash.values[index + -1][:multiplier]
613
+ else # mid floor, check above and below
614
+ multiplier_story_above = story_hash.values[index + 1][:multiplier]
615
+ multiplier_story_below = story_hash.values[index + -1][:multiplier]
616
+ end
617
+
618
+ # if adjacent story has multiplier > 1 then make appropriate surfaces adiabatic
619
+ adiabatic_ceilings = false
620
+ adiabatic_floors = false
621
+ if story_data[:multiplier] > 1
622
+ adiabatic_ceilings = true
623
+ adiabatic_floors = true
624
+ elsif multiplier_story_above > 1
625
+ adiabatic_ceilings = true
626
+ elsif multiplier_story_below > 1
627
+ adiabatic_floors = true
628
+ end
629
+
630
+ # get the right collection of polygons to make up footprint for each building story
631
+ if index > footprints.size - 1
632
+ # use last footprint
633
+ target_footprint = footprints.last
634
+ else
635
+ target_footprint = footprints[index]
636
+ end
637
+ target_footprint.each do |name, space_data|
638
+ # gather options
639
+ options = {
640
+ 'name' => "#{name} - #{story.name}",
641
+ 'spaceType' => space_data[:space_type],
642
+ 'story' => story,
643
+ 'makeThermalZone' => true,
644
+ 'thermalZoneMultiplier' => story_data[:multiplier],
645
+ 'floor_to_floor_height' => story_data[:space_height]
646
+ }
647
+
648
+ # make space
649
+ space = OsLib_Geometry.makeSpaceFromPolygon(model, space_data[:polygon].first, space_data[:polygon], options)
650
+
651
+ # set z origin to proper position
652
+ space.setZOrigin(story_data[:space_origin_z])
653
+
654
+ # loop through celings and floors to hard asssign constructions and set boundary condition
655
+ if adiabatic_ceilings || adiabatic_floors
656
+ space.surfaces.each do |surface|
657
+ if adiabatic_floors && (surface.surfaceType == 'Floor')
658
+ if surface.construction.is_initialized
659
+ surface.setConstruction(surface.construction.get)
660
+ end
661
+ surface.setOutsideBoundaryCondition('Adiabatic')
662
+ end
663
+ if adiabatic_ceilings && (surface.surfaceType == 'RoofCeiling')
664
+ if surface.construction.is_initialized
665
+ surface.setConstruction(surface.construction.get)
666
+ end
667
+ surface.setOutsideBoundaryCondition('Adiabatic')
668
+ end
669
+ end
670
+ end
671
+ end
672
+
673
+ # TODO: - in future add code to include plenums or raised floor to each/any story.
674
+ end
675
+
676
+ # any changes to wall boundary conditions will be handled by same code that calls this method.
677
+ # this method doesn't need to know about basements and party walls.
678
+
679
+ return model
680
+ end
681
+
682
+ # add def to create a space from input, optionally take a name, space type, story and thermal zone.
683
+ def self.makeSpaceFromPolygon(model, space_origin, point3dVector, options = {})
684
+ # set defaults to use if user inputs not passed in
685
+ defaults = {
686
+ 'name' => nil,
687
+ 'spaceType' => nil,
688
+ 'story' => nil,
689
+ 'makeThermalZone' => nil,
690
+ 'thermalZone' => nil,
691
+ 'thermalZoneMultiplier' => 1,
692
+ 'floor_to_floor_height' => OpenStudio.convert(10, 'ft', 'm').get
693
+ }
694
+
695
+ # merge user inputs with defaults
696
+ options = defaults.merge(options)
697
+
698
+ # Identity matrix for setting space origins
699
+ m = OpenStudio::Matrix.new(4, 4, 0)
700
+ m[0, 0] = 1
701
+ m[1, 1] = 1
702
+ m[2, 2] = 1
703
+ m[3, 3] = 1
704
+
705
+ # make space from floor print
706
+ space = OpenStudio::Model::Space.fromFloorPrint(point3dVector, options['floor_to_floor_height'], model)
707
+ space = space.get
708
+ m[0, 3] = space_origin.x
709
+ m[1, 3] = space_origin.y
710
+ m[2, 3] = space_origin.z
711
+ space.changeTransformation(OpenStudio::Transformation.new(m))
712
+ space.setBuildingStory(options['story'])
713
+ if !options['name'].nil?
714
+ space.setName(options['name'])
715
+ end
716
+
717
+ if !options['spaceType'].nil?
718
+ space.setSpaceType(options['spaceType'])
719
+ end
720
+
721
+ # create thermal zone if requested and assign
722
+ if options['makeThermalZone']
723
+ new_zone = OpenStudio::Model::ThermalZone.new(model)
724
+ new_zone.setMultiplier(options['thermalZoneMultiplier'])
725
+ space.setThermalZone(new_zone)
726
+ new_zone.setName("Zone #{space.name}")
727
+ else
728
+ if !options['thermalZone'].nil? then space.setThermalZone(options['thermalZone']) end
729
+ end
730
+
731
+ result = space
732
+ return result
733
+ end
734
+
735
+ def self.getExteriorWindowAndWllAreaByOrientation(model, spaceArray, options = {})
736
+ # set defaults to use if user inputs not passed in
737
+ defaults = {
738
+ 'northEast' => 45,
739
+ 'southEast' => 125,
740
+ 'southWest' => 225,
741
+ 'northWest' => 315
742
+ }
743
+
744
+ # merge user inputs with defaults
745
+ options = defaults.merge(options)
746
+
747
+ # counters
748
+ total_gross_ext_wall_area_North = 0
749
+ total_gross_ext_wall_area_South = 0
750
+ total_gross_ext_wall_area_East = 0
751
+ total_gross_ext_wall_area_West = 0
752
+ total_ext_window_area_North = 0
753
+ total_ext_window_area_South = 0
754
+ total_ext_window_area_East = 0
755
+ total_ext_window_area_West = 0
756
+
757
+ spaceArray.each do |space|
758
+ # get surface area adjusting for zone multiplier
759
+ zone = space.thermalZone
760
+ if !zone.empty?
761
+ zone_multiplier = zone.get.multiplier
762
+ if zone_multiplier > 1
763
+ end
764
+ else
765
+ zone_multiplier = 1 # space is not in a thermal zone
766
+ end
767
+
768
+ space.surfaces.each do |s|
769
+ next if s.surfaceType != 'Wall'
770
+ next if s.outsideBoundaryCondition != 'Outdoors'
771
+
772
+ surface_gross_area = s.grossArea * zone_multiplier
773
+
774
+ # loop through sub surfaces and add area including multiplier
775
+ ext_window_area = 0
776
+ s.subSurfaces.each do |subSurface|
777
+ ext_window_area += subSurface.grossArea * subSurface.multiplier * zone_multiplier
778
+ end
779
+
780
+ absoluteAzimuth = OpenStudio.convert(s.azimuth, 'rad', 'deg').get + s.space.get.directionofRelativeNorth + model.getBuilding.northAxis
781
+ absoluteAzimuth -= 360.0 until absoluteAzimuth < 360.0
782
+
783
+ # add to exterior wall counter if north or south
784
+ if (options['northEast'] <= absoluteAzimuth) && (absoluteAzimuth < options['southEast']) # East exterior walls
785
+ total_gross_ext_wall_area_East += surface_gross_area
786
+ total_ext_window_area_East += ext_window_area
787
+ elsif (options['southEast'] <= absoluteAzimuth) && (absoluteAzimuth < options['southWest']) # South exterior walls
788
+ total_gross_ext_wall_area_South += surface_gross_area
789
+ total_ext_window_area_South += ext_window_area
790
+ elsif (options['southWest'] <= absoluteAzimuth) && (absoluteAzimuth < options['northWest']) # West exterior walls
791
+ total_gross_ext_wall_area_West += surface_gross_area
792
+ total_ext_window_area_West += ext_window_area
793
+ else # North exterior walls
794
+ total_gross_ext_wall_area_North += surface_gross_area
795
+ total_ext_window_area_North += ext_window_area
796
+ end
797
+ end
798
+ end
799
+
800
+ result = { 'northWall' => total_gross_ext_wall_area_North,
801
+ 'northWindow' => total_ext_window_area_North,
802
+ 'southWall' => total_gross_ext_wall_area_South,
803
+ 'southWindow' => total_ext_window_area_South,
804
+ 'eastWall' => total_gross_ext_wall_area_East,
805
+ 'eastWindow' => total_ext_window_area_East,
806
+ 'westWall' => total_gross_ext_wall_area_West,
807
+ 'westWindow' => total_ext_window_area_West }
808
+ return result
809
+ end
810
+
811
+ def self.getAbsoluteAzimuthForSurface(surface, model)
812
+ absolute_azimuth = OpenStudio.convert(surface.azimuth, 'rad', 'deg').get + surface.space.get.directionofRelativeNorth + model.getBuilding.northAxis
813
+ absolute_azimuth -= 360.0 until absolute_azimuth < 360.0
814
+ return absolute_azimuth
815
+ end
816
+
817
+ # dont use this, use calculate_story_exterior_wall_perimeter instead
818
+ def self.estimate_perimeter(perim_story)
819
+ perimeter = 0
820
+ perim_story.spaces.each do |space|
821
+ space.surfaces.each do |surface|
822
+ next if (surface.outsideBoundaryCondition != 'Outdoors') || (surface.surfaceType != 'Wall')
823
+ area = surface.grossArea
824
+ z_value_array = OsLib_Geometry.getSurfaceZValues([surface])
825
+ next if z_value_array.max == z_value_array.min # shouldn't see this unless wall is horizontal
826
+ perimeter += area / (z_value_array.max - z_value_array.min)
827
+ end
828
+ end
829
+
830
+ return perimeter
831
+ end
832
+
833
+ # calculate story perimeter. Selected story should have above grade walls. If not perimeter may return zero.
834
+ # optional_multiplier_adjustment is used in special case when there are zone multipliers that represent additional zones within the same story
835
+ # the value entered represents the story_multiplier which reduces the adjustment by that factor over the full zone multiplier
836
+ # todo - this doesn't catch walls that are split that sit above floor surfaces that are not (e.g. main corridoor in secondary school model)
837
+ # todo - also odd with multi-height spaces
838
+ def self.calculate_story_exterior_wall_perimeter(runner, story, optional_multiplier_adjustment = nil, tested_wall_boundary_condition = ['Outdoors', 'Ground'], bounding_box = nil)
839
+ perimeter = 0
840
+ party_walls = []
841
+ story.spaces.each do |space|
842
+ # counter to use later
843
+ edge_hash = {}
844
+ edge_counter = 0
845
+ space.surfaces.each do |surface|
846
+ # get vertices
847
+ vertex_hash = {}
848
+ vertex_counter = 0
849
+ surface.vertices.each do |vertex|
850
+ vertex_counter += 1
851
+ vertex_hash[vertex_counter] = [vertex.x, vertex.y, vertex.z]
852
+ end
853
+ # make edges
854
+ counter = 0
855
+ vertex_hash.each do |k, v|
856
+ edge_counter += 1
857
+ counter += 1
858
+ if vertex_hash.size != counter
859
+ edge_hash[edge_counter] = [v, vertex_hash[counter + 1], surface, surface.outsideBoundaryCondition, surface.surfaceType]
860
+ else # different code for wrap around vertex
861
+ edge_hash[edge_counter] = [v, vertex_hash[1], surface, surface.outsideBoundaryCondition, surface.surfaceType]
862
+ end
863
+ end
864
+ end
865
+
866
+ # check edges for matches (need opposite vertices and proper boundary conditions)
867
+ edge_hash.each do |k1, v1|
868
+ # apply to any floor boundary condition. This supports used in floors above basements
869
+ next if v1[4] != 'Floor'
870
+ edge_hash.each do |k2, v2|
871
+ test_boundary_cond = false
872
+ next if !tested_wall_boundary_condition.include?(v2[3]) # method arg takes multiple conditions
873
+ next if v2[4] != 'Wall'
874
+
875
+ # see if edges have same geometry
876
+
877
+ # found cases where the two lines below removed edges and resulted in lower than actual perimeter. Added new code with tolerance.
878
+ # next if not v1[0] == v2[1] # next if not same geometry reversed
879
+ # next if not v1[1] == v2[0]
880
+
881
+ # these are three item array's add in tollerance for each array entry
882
+ tolerance = 0.0001
883
+ test_a = true
884
+ test_b = true
885
+ 3.times.each do |i|
886
+ if (v1[0][i] - v2[1][i]).abs > tolerance
887
+ test_a = false
888
+ end
889
+ if (v1[1][i] - v2[0][i]).abs > tolerance
890
+ test_b = false
891
+ end
892
+ end
893
+
894
+ next if test_a != true
895
+ next if test_b != true
896
+
897
+ # edge_bounding_box = OpenStudio::BoundingBox.new
898
+ # edge_bounding_box.addPoints(space.transformation() * v2[2].vertices)
899
+ # if not edge_bounding_box.intersects(bounding_box) doesn't seem to work reliably, writing custom code to check
900
+
901
+ point_one = OpenStudio::Point3d.new(v2[0][0], v2[0][1], v2[0][2])
902
+ point_one = (space.transformation * point_one)
903
+ point_two = OpenStudio::Point3d.new(v2[1][0], v2[1][1], v2[1][2])
904
+ point_two = (space.transformation * point_two)
905
+
906
+ if !bounding_box.nil? && (v2[3] == 'Adiabatic')
907
+
908
+ on_bounding_box = false
909
+ if ((bounding_box.minX.to_f - point_one.x).abs < tolerance) && ((bounding_box.minX.to_f - point_two.x).abs < tolerance)
910
+ on_bounding_box = true
911
+ elsif ((bounding_box.maxX.to_f - point_one.x).abs < tolerance) && ((bounding_box.maxX.to_f - point_two.x).abs < tolerance)
912
+ on_bounding_box = true
913
+ elsif ((bounding_box.minY.to_f - point_one.y).abs < tolerance) && ((bounding_box.minY.to_f - point_two.y).abs < tolerance)
914
+ on_bounding_box = true
915
+ elsif ((bounding_box.maxY.to_f - point_one.y).abs < tolerance) && ((bounding_box.maxY.to_f - point_two.y).abs < tolerance)
916
+ on_bounding_box = true
917
+ end
918
+
919
+ # if not edge_bounding_box.intersects(bounding_box) doesn't seem to work reliably, writing custom code to check
920
+ # todo - this is basic check for adiabatic party walls and won't catch all situations. Can be made more robust in the future
921
+ if on_bounding_box == true
922
+ length = OpenStudio::Vector3d.new(point_one - point_two).length
923
+ party_walls << v2[2]
924
+ length_ip_display = OpenStudio.convert(length, 'm', 'ft').get.round(2)
925
+ runner.registerInfo(" * #{v2[2].name} has an adiabatic boundary condition and sits in plane with the building bounding box. Adding #{length_ip_display} (ft) to perimeter length of #{story.name} for this surface, assuming it is a party wall.")
926
+ elsif space.multiplier == 1
927
+ length = OpenStudio::Vector3d.new(point_one - point_two).length
928
+ party_walls << v2[2]
929
+ length_ip_display = OpenStudio.convert(length, 'm', 'ft').get.round(2)
930
+ runner.registerInfo(" * #{v2[2].name} has an adiabatic boundary condition and is in a zone with a multiplier of 1. Adding #{length_ip_display} (ft) to perimeter length of #{story.name} for this surface, assuming it is a party wall.")
931
+ else
932
+ length = 0
933
+ end
934
+
935
+ else
936
+ length = OpenStudio::Vector3d.new(point_one - point_two).length
937
+ end
938
+
939
+ if optional_multiplier_adjustment.nil?
940
+ perimeter += length
941
+ else
942
+ # adjust for multiplier
943
+ non_story_multiplier = space.multiplier / optional_multiplier_adjustment.to_f
944
+ perimeter += length * non_story_multiplier
945
+ end
946
+ end
947
+ end
948
+ end
949
+
950
+ return { perimeter: perimeter, party_walls: party_walls }
951
+ end
952
+
953
+ # currently takes in model and checks for edges shared by a ground exposed floor and exterior exposed wall. Later could be updated for a specific story independent of floor boundary condition.
954
+ # todo - this doesn't catch walls that are split that sit above floor surfaces that are not (e.g. main corridoor in secondary school model)
955
+ # todo - also odd with multi-height spaces
956
+ def self.calculate_perimeter(model)
957
+ perimeter = 0
958
+ model.getSpaces.each do |space|
959
+ # counter to use later
960
+ edge_hash = {}
961
+ edge_counter = 0
962
+ space.surfaces.each do |surface|
963
+ # get vertices
964
+ vertex_hash = {}
965
+ vertex_counter = 0
966
+ surface.vertices.each do |vertex|
967
+ vertex_counter += 1
968
+ vertex_hash[vertex_counter] = [vertex.x, vertex.y, vertex.z]
969
+ end
970
+ # make edges
971
+ counter = 0
972
+ vertex_hash.each do |k, v|
973
+ edge_counter += 1
974
+ counter += 1
975
+ if vertex_hash.size != counter
976
+ edge_hash[edge_counter] = [v, vertex_hash[counter + 1], surface, surface.outsideBoundaryCondition, surface.surfaceType]
977
+ else # different code for wrap around vertex
978
+ edge_hash[edge_counter] = [v, vertex_hash[1], surface, surface.outsideBoundaryCondition, surface.surfaceType]
979
+ end
980
+ end
981
+ end
982
+
983
+ # check edges for matches (need opposite vertices and proper boundary conditions)
984
+ edge_hash.each do |k1, v1|
985
+ next if v1[3] != 'Ground' # skip if not ground exposed floor
986
+ next if v1[4] != 'Floor'
987
+ edge_hash.each do |k2, v2|
988
+ next if v2[3] != 'Outdoors' # skip if not exterior exposed wall (todo - update to handle basement)
989
+ next if v2[4] != 'Wall'
990
+
991
+ # see if edges have same geometry
992
+ # found cases where the two lines below removed edges and resulted in lower than actual perimeter. Added new code with tolerance.
993
+ # next if not v1[0] == v2[1] # next if not same geometry reversed
994
+ # next if not v1[1] == v2[0]
995
+
996
+ # these are three item array's add in tollerance for each array entry
997
+ tolerance = 0.0001
998
+ test_a = true
999
+ test_b = true
1000
+ 3.times.each do |i|
1001
+ if (v1[0][i] - v2[1][i]).abs > tolerance
1002
+ test_a = false
1003
+ end
1004
+ if (v1[1][i] - v2[0][i]).abs > tolerance
1005
+ test_b = false
1006
+ end
1007
+ end
1008
+
1009
+ next if test_a != true
1010
+ next if test_b != true
1011
+
1012
+ point_one = OpenStudio::Point3d.new(v1[0][0], v1[0][1], v1[0][2])
1013
+ point_two = OpenStudio::Point3d.new(v1[1][0], v1[1][1], v1[1][2])
1014
+ length = OpenStudio::Vector3d.new(point_one - point_two).length
1015
+ perimeter += length
1016
+ end
1017
+ end
1018
+ end
1019
+
1020
+ return perimeter
1021
+ end
1022
+ end