tbd 3.2.3 → 3.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/lib/tbd/geo.rb CHANGED
@@ -22,31 +22,33 @@
22
22
 
23
23
  module TBD
24
24
  ##
25
- # Check for matching Topolys vertex pairs between edges.
25
+ # Checks whether 2 edges share Topolys vertex pairs.
26
26
  #
27
- # @param e1 [Hash] first edge
28
- # @param e2 [Hash] second edge
29
- # @param tol [Float] user-set tolerance (> TOL) in m
27
+ # @param [Hash] e1 first edge
28
+ # @param [Hash] e2 second edge
29
+ # @option e1 [Topolys::Point3D] :v0 origin vertex
30
+ # @option e1 [Topolys::Point3D] :v1 terminal vertex
31
+ # @param tol [Numeric] tolerance (OSut::TOL) in m
30
32
  #
31
- # @return [Bool] true if edges share vertex pairs
32
- # @return [Bool] false if invalid input
33
+ # @return [Bool] whether edges share vertex pairs
34
+ # @return [false] if invalid input (see logs)
33
35
  def matches?(e1 = {}, e2 = {}, tol = TOL)
34
36
  mth = "TBD::#{__callee__}"
35
37
  cl = Topolys::Point3D
36
38
  a = false
39
+ return mismatch("e1", e1, Hash, mth, DBG, a) unless e1.is_a?(Hash)
40
+ return mismatch("e2", e2, Hash, mth, DBG, a) unless e2.is_a?(Hash)
41
+ return mismatch("e2", e2, Hash, mth, DBG, a) unless e2.is_a?(Hash)
37
42
 
38
- return mismatch("e1", e1, Hash, mth, DBG, a) unless e1.is_a?(Hash)
39
- return mismatch("e2", e2, Hash, mth, DBG, a) unless e2.is_a?(Hash)
43
+ return hashkey("e1", e1, :v0, mth, DBG, a) unless e1.key?(:v0)
44
+ return hashkey("e1", e1, :v1, mth, DBG, a) unless e1.key?(:v1)
45
+ return hashkey("e2", e2, :v0, mth, DBG, a) unless e2.key?(:v0)
46
+ return hashkey("e2", e2, :v1, mth, DBG, a) unless e2.key?(:v1)
40
47
 
41
- return hashkey("e1", e1, :v0, mth, DBG, a) unless e1.key?(:v0)
42
- return hashkey("e1", e1, :v1, mth, DBG, a) unless e1.key?(:v1)
43
- return hashkey("e2", e2, :v0, mth, DBG, a) unless e2.key?(:v0)
44
- return hashkey("e2", e2, :v1, mth, DBG, a) unless e2.key?(:v1)
45
-
46
- return mismatch("e1 :v0", e1[:v0], cl, mth, DBG, a) unless e1[:v0].is_a?(cl)
47
- return mismatch("e1 :v1", e1[:v1], cl, mth, DBG, a) unless e1[:v1].is_a?(cl)
48
- return mismatch("e2 :v0", e2[:v0], cl, mth, DBG, a) unless e2[:v0].is_a?(cl)
49
- return mismatch("e2 :v1", e2[:v1], cl, mth, DBG, a) unless e2[:v1].is_a?(cl)
48
+ return mismatch("e1:v0", e1[:v0], cl, mth, DBG, a) unless e1[:v0].is_a?(cl)
49
+ return mismatch("e1:v1", e1[:v1], cl, mth, DBG, a) unless e1[:v1].is_a?(cl)
50
+ return mismatch("e2:v0", e2[:v0], cl, mth, DBG, a) unless e2[:v0].is_a?(cl)
51
+ return mismatch("e2:v1", e2[:v1], cl, mth, DBG, a) unless e2[:v1].is_a?(cl)
50
52
 
51
53
  e1_vector = e1[:v1] - e1[:v0]
52
54
  e2_vector = e2[:v1] - e2[:v0]
@@ -54,8 +56,8 @@ module TBD
54
56
  return zero("e1", mth, DBG, a) if e1_vector.magnitude < TOL
55
57
  return zero("e2", mth, DBG, a) if e2_vector.magnitude < TOL
56
58
 
57
- return mismatch("e1", e1, Hash, mth, DBG, a) unless tol.is_a?(Numeric)
58
- return zero("tol", mth, DBG, a) if tol < TOL
59
+ return mismatch("e1", e1, Hash, mth, DBG, a) unless tol.is_a?(Numeric)
60
+ return zero("tol", mth, DBG, a) if tol < TOL
59
61
 
60
62
  return true if
61
63
  (
@@ -85,25 +87,26 @@ module TBD
85
87
  end
86
88
 
87
89
  ##
88
- # Return Topolys vertices and a Topolys wire from Topolys points. As a side
89
- # effect, it will - if successful - also populate the Topolys model with the
90
- # vertices and wire.
90
+ # Returns Topolys vertices and a Topolys wire from Topolys points. If
91
+ # missing, it populates the Topolys model with the vertices and wire.
91
92
  #
92
93
  # @param model [Topolys::Model] a model
93
- # @param pts [Array] a 1D array of 3D Topolys points (min 3x)
94
+ # @param pts [Array<Topolys::Point3D>] 3D points
94
95
  #
95
- # @return [Hash] vx: 3D Topolys vertices Array; w: corresponding Topolys::Wire
96
- # @return [Hash] vx: nil; w: nil (if invalid input)
96
+ # @return [Hash] vx: (Array<Topolys::Vertex>); w: (Topolys::Wire)
97
+ # @return [Hash] vx: nil, w: nil if invalid input (see logs)
97
98
  def objects(model = nil, pts = [])
98
99
  mth = "TBD::#{__callee__}"
99
- cl = Topolys::Model
100
+ cl1 = Topolys::Model
101
+ cl2 = Array
102
+ cl3 = Topolys::Point3D
100
103
  obj = { vx: nil, w: nil }
104
+ return mismatch("model", model, cl1, mth, DBG, obj) unless model.is_a?(cl1)
105
+ return mismatch("points", pts, cl2, mth, DBG, obj) unless pts.is_a?(cl2)
101
106
 
102
- return mismatch("model", model, cl, mth, DBG, obj) unless model.is_a?(cl)
103
- return mismatch("points", pts, Array, mth, DBG, obj) unless pts.is_a?(Array)
104
-
105
- log(DBG, "#{pts.size}? need +3 Topolys points (#{mth})") unless pts.size > 2
106
- return obj unless pts.size > 2
107
+ pts.each do |pt|
108
+ return mismatch("point", pt, cl3, mth, DBG, obj) unless pt.is_a?(cl3)
109
+ end
107
110
 
108
111
  obj[:vx] = model.get_vertices(pts)
109
112
  obj[:w ] = model.get_wire(obj[:vx])
@@ -112,31 +115,36 @@ module TBD
112
115
  end
113
116
 
114
117
  ##
115
- # Populate collection of TBD hinged 'kids' (subsurfaces), relying on Topolys.
116
- # As a side effect, it will - if successful - also populate a Topolys 'model'
117
- # with Topolys vertices, wires, holes. In rare cases such as domes of tubular
118
- # daylighting devices (TDDs), kids may be 'unhinged', i.e. not on same 3D
119
- # plane as 'dad(s)' - TBD corrects such cases elsewhere.
118
+ # Adds a collection of TBD sub surfaces ('kids') to a Topolys model,
119
+ # including vertices, wires & holes. A sub surface is typically 'hinged',
120
+ # i.e. along the same 3D plane as its base surface (or 'dad'). In rare cases
121
+ # such as domes of tubular daylighting devices (TDDs), a sub surface may be
122
+ # 'unhinged'.
120
123
  #
121
124
  # @param model [Topolys::Model] a model
122
- # @param boys [Hash] a collection of TBD subsurfaces
125
+ # @param [Hash] boys a collection of TBD subsurfaces
126
+ # @option boys [Array<Topolys::Point3D>] :points sub surface 3D points
127
+ # @option boys [Bool] :unhinged whether same 3D plane as base surface
128
+ # @option boys [OpenStudio::Vector3d] :n outward normal
123
129
  #
124
- # @return [Array] 3D Topolys wires of 'holes' (made by kids)
130
+ # @return [Array<Topolys::Wire>] holes cut out by kids (see logs if empty)
125
131
  def kids(model = nil, boys = {})
126
- mth = "TBD::#{__callee__}"
127
- cl = Topolys::Model
132
+ mth = "TBD::#{__callee__}"
133
+ cl1 = Topolys::Model
134
+ cl2 = Hash
128
135
  holes = []
129
-
130
- return mismatch("model", model, cl, mth, DBG, holes) unless model.is_a?(cl)
131
- return mismatch("boys", boys, Hash, mth, DBG, holes) unless boys.is_a?(Hash)
136
+ return mismatch("model", model, cl1, mth, DBG, {}) unless model.is_a?(cl1)
137
+ return mismatch("boys", boys, cl2, mth, DBG, {}) unless boys.is_a?(cl2)
132
138
 
133
139
  boys.each do |id, props|
134
140
  obj = objects(model, props[:points])
135
141
  next unless obj[:w]
142
+
136
143
  obj[:w].attributes[:id ] = id
137
144
  obj[:w].attributes[:unhinged] = props[:unhinged] if props.key?(:unhinged)
138
- obj[:w].attributes[:n ] = props[:n] if props.key?(:n)
139
- props[:hole] = obj[:w]
145
+ obj[:w].attributes[:n ] = props[:n ] if props.key?(:n)
146
+
147
+ props[:hole] = obj[:w]
140
148
  holes << obj[:w]
141
149
  end
142
150
 
@@ -144,37 +152,49 @@ module TBD
144
152
  end
145
153
 
146
154
  ##
147
- # Populate hash of TBD 'dads' (parent) surfaces, relying on Topolys. As a side
148
- # effect, it will - if successful - also populate the main Topolys model with
149
- # Topolys vertices, wires, holes & faces.
155
+ # Adds a collection of bases surfaces ('dads') to a Topolys model, including
156
+ # vertices, wires, holes & faces. Also populates the model with sub surfaces
157
+ # ('kids').
150
158
  #
151
159
  # @param model [Topolys::Model] a model
152
- # @param pops [Hash] a collection of TBD (parent) surfaces
160
+ # @param [Hash] pops base surfaces
161
+ # @option pops [OpenStudio::Point3dVector] :points base surface 3D points
162
+ # @option pops [Hash] :windows incorporated windows (see kids)
163
+ # @option pops [Hash] :doors incorporated doors (see kids)
164
+ # @option pops [Hash] :skylights incorporated skylights (see kids)
165
+ # @option pops [OpenStudio::Vector3D] :n outward normal
153
166
  #
154
- # @return [Array] 3D Topolys wires of 'holes' (made by kids)
167
+ # @return [Hash] 3D Topolys wires of 'holes' (made by kids)
155
168
  def dads(model = nil, pops = {})
156
169
  mth = "TBD::#{__callee__}"
157
- cl = Topolys::Model
170
+ cl1 = Topolys::Model
171
+ cl2 = Hash
158
172
  holes = {}
159
-
160
- return mismatch("model", model, cl, mth, DBG, holes) unless model.is_a?(cl)
161
- return mismatch("pops", pops, Hash, mth, DBG, holes) unless pops.is_a?(Hash)
173
+ return mismatch("model", model, cl2, mth, DBG, {}) unless model.is_a?(cl1)
174
+ return mismatch("pops", pops, cl2, mth, DBG, {}) unless pops.is_a?(cl2)
162
175
 
163
176
  pops.each do |id, props|
164
177
  hols = []
165
178
  hinged = []
166
179
  obj = objects(model, props[:points])
167
180
  next unless obj[:vx] && obj[:w]
168
- hols += kids(model, props[:windows ]) if props.key?(:windows )
169
- hols += kids(model, props[:doors ]) if props.key?(:doors )
170
- hols += kids(model, props[:skylights]) if props.key?(:skylights)
181
+
182
+ hols += kids(model, props[:windows ]) if props.key?(:windows)
183
+ hols += kids(model, props[:doors ]) if props.key?(:doors)
184
+ hols += kids(model, props[:skylights]) if props.key?(:skylights)
185
+
171
186
  hols.each { |hol| hinged << hol unless hol.attributes[:unhinged] }
187
+
172
188
  face = model.get_face(obj[:w], hinged)
173
- log(DBG, "Unable to retrieve valid 'dad' (#{mth})") unless face
174
- next unless face
189
+ msg = "Unable to retrieve valid 'dad' (#{mth})"
190
+ log(DBG, msg) unless face
191
+ next unless face
192
+
175
193
  face.attributes[:id] = id
176
- face.attributes[:n] = props[:n] if props.key?(:n)
177
- props[:face] = face
194
+ face.attributes[:n ] = props[:n] if props.key?(:n)
195
+
196
+ props[:face] = face
197
+
178
198
  hols.each { |hol| holes[hol.attributes[:id]] = hol }
179
199
  end
180
200
 
@@ -182,22 +202,27 @@ module TBD
182
202
  end
183
203
 
184
204
  ##
185
- # Populate TBD edges with linked Topolys faces.
205
+ # Populates TBD edges with linked Topolys faces.
186
206
  #
187
- # @param s [Hash] a collection of TBD surfaces
188
- # @param e [Hash] a collection TBD edges
207
+ # @param [Hash] s TBD surfaces
208
+ # @option s [Topolys::Face] :face a Topolys face
209
+ # @param [Hash] e TBD edges
210
+ # @option e [Numeric] :length edge length
211
+ # @option e [Topolys::Vertex] :v0 edge origin vertex
212
+ # @option e [Topolys::Vertex] :v1 edge terminal vertex
189
213
  #
190
- # @return [Bool] true if successful
191
- # @return [Bool] false if invalid input
214
+ # @return [Bool] whether successful in populating faces
215
+ # @return [false] if invalid input (see logs)
192
216
  def faces(s = {}, e = {})
193
217
  mth = "TBD::#{__callee__}"
194
-
195
- return mismatch("surfaces", s, Hash, mth, DBG, false) unless s.is_a?(Hash)
196
- return mismatch("edges", e, Hash, mth, DBG, false) unless e.is_a?(Hash)
218
+ return mismatch("surfaces", s, Hash, mth, DBG, false) unless s.is_a?(Hash)
219
+ return mismatch("edges", e, Hash, mth, DBG, false) unless e.is_a?(Hash)
197
220
 
198
221
  s.each do |id, props|
199
- log(DBG, "Missing Topolys face '#{id}' (#{mth})") unless props.key?(:face)
200
- next unless props.key?(:face)
222
+ unless props.key?(:face)
223
+ log(DBG, "Missing Topolys face '#{id}' (#{mth})")
224
+ next
225
+ end
201
226
 
202
227
  props[:face].wires.each do |wire|
203
228
  wire.edges.each do |edge|
@@ -219,19 +244,18 @@ module TBD
219
244
  end
220
245
 
221
246
  ##
222
- # Return site-specific (or true) Topolys normal vector of OpenStudio surface.
247
+ # Returns site (or true) Topolys normal vector of OpenStudio surface.
223
248
  #
224
249
  # @param s [OpenStudio::Model::PlanarSurface] a planar surface
225
- # @param r [Float] a group/site rotation angle [0,2PI) radians
250
+ # @param r [#to_f] a group/site rotation angle [0,2PI) radians
226
251
  #
227
- # @return [Topolys::Vector3D] normal (Topolys) vector <x,y,z> of s
228
- # @return [NilClass] if invalid input
252
+ # @return [Topolys::Vector3D] true normal vector of s
253
+ # @return [nil] if invalid input (see logs)
229
254
  def trueNormal(s = nil, r = 0)
230
255
  mth = "TBD::#{__callee__}"
231
256
  cl = OpenStudio::Model::PlanarSurface
232
-
233
- return mismatch("surface", s, cl, mth) unless s.is_a?(cl)
234
- return invalid("rotation angle", mth, 2) unless r.respond_to?(:to_f)
257
+ return mismatch("surface", s, cl, mth) unless s.is_a?(cl)
258
+ return invalid("rotation angle", mth, 2) unless r.respond_to?(:to_f)
235
259
 
236
260
  r = -r.to_f * Math::PI / 180.0
237
261
  vx = s.outwardNormal.x * Math.cos(r) - s.outwardNormal.y * Math.sin(r)
@@ -241,45 +265,46 @@ module TBD
241
265
  end
242
266
 
243
267
  ##
244
- # Fetch OpenStudio surface properties, including opening areas & vertices.
268
+ # Fetches OpenStudio surface properties, including opening areas & vertices.
245
269
  #
246
- # @param model [OpenStudio::Model::Model] a model
247
270
  # @param surface [OpenStudio::Model::Surface] a surface
271
+ # @param [Hash] argh TBD arguments
272
+ # @option argh [Bool] :setpoints whether model holds thermal zone setpoints
248
273
  #
249
- # @return [Hash] TBD surface with key attributes, including openings
250
- # @return [NilClass] if invalid input
251
- def properties(model = nil, surface = nil)
274
+ # @return [Hash] TBD surface with key attributes (see )
275
+ # @return [nil] if invalid input (see logs)
276
+ def properties(surface = nil, argh = {})
252
277
  mth = "TBD::#{__callee__}"
253
- cl1 = OpenStudio::Model::Model
254
- cl2 = OpenStudio::Model::Surface
255
- cl3 = OpenStudio::Model::LayeredConstruction
256
-
257
- return mismatch("model", model, cl1, mth) unless model.is_a?(cl1)
258
- return mismatch("surface", surface, cl2, mth) unless surface.is_a?(cl2)
259
- return nil unless surface_valid?(surface)
278
+ cl1 = OpenStudio::Model::Surface
279
+ cl2 = OpenStudio::Model::LayeredConstruction
280
+ cl3 = Hash
281
+ return mismatch("surface", surface, cl1, mth) unless surface.is_a?(cl1)
282
+ return mismatch("argh" , argh , cl3, mth) unless argh.is_a?(cl3)
260
283
 
261
284
  nom = surface.nameString
262
285
  surf = {}
263
286
  subs = {}
264
287
  fd = false
265
- return empty("'#{nom}' space", mth, ERR) if surface.space.empty?
288
+ return invalid("#{nom}", mth, 1, FTL) if poly(surface).empty?
289
+ return empty("#{nom} space", mth, ERR) if surface.space.empty?
266
290
 
267
291
  space = surface.space.get
268
292
  stype = space.spaceType
269
293
  story = space.buildingStory
270
- tr = transforms(model, space)
271
- return invalid("'#{nom}' transform", mth, 0, FTL) unless tr[:t] && tr[:r]
294
+ tr = transforms(space)
295
+ return invalid("#{nom} transform", mth, 0, FTL) unless tr[:t] && tr[:r]
272
296
 
273
297
  t = tr[:t]
274
298
  n = trueNormal(surface, tr[:r])
275
- return invalid("'#{nom}' normal", mth, 0, FTL) unless n
299
+ return invalid("#{nom} normal", mth, 0, FTL) unless n
276
300
 
277
301
  type = surface.surfaceType.downcase
278
302
  facing = surface.outsideBoundaryCondition
303
+ setpts = setpoints(space)
279
304
 
280
305
  if facing.downcase == "surface"
281
306
  empty = surface.adjacentSurface.empty?
282
- return invalid("'#{nom}': adjacent surface", mth, 0, ERR) if empty
307
+ return invalid("#{nom}: adjacent surface", mth, 0, ERR) if empty
283
308
 
284
309
  facing = surface.adjacentSurface.get.nameString
285
310
  end
@@ -290,9 +315,9 @@ module TBD
290
315
  unless construction.empty?
291
316
  construction = construction.get
292
317
  lyr = insulatingLayer(construction)
293
- lyr[:index] = nil unless lyr[:index].is_a?(Numeric)
294
- lyr[:index] = nil unless lyr[:index] >= 0
295
- lyr[:index] = nil unless lyr[:index] < construction.layers.size
318
+ lyr[:index] = nil unless lyr[:index].is_a?(Numeric)
319
+ lyr[:index] = nil unless lyr[:index] >= 0
320
+ lyr[:index] = nil unless lyr[:index] < construction.layers.size
296
321
 
297
322
  if lyr[:index]
298
323
  surf[:construction] = construction
@@ -306,52 +331,140 @@ module TBD
306
331
  end
307
332
  end
308
333
 
309
- surf[:conditioned] = true
334
+ unless argh.key?(:setpoints)
335
+ heat = heatingTemperatureSetpoints?(model)
336
+ cool = coolingTemperatureSetpoints?(model)
337
+ argh[:setpoints] = heat || cool
338
+ end
339
+
340
+ if argh[:setpoints]
341
+ surf[:heating] = setpts[:heating] unless setpts[:heating].nil?
342
+ surf[:cooling] = setpts[:cooling] unless setpts[:cooling].nil?
343
+ else
344
+ surf[:heating] = 21.0
345
+ surf[:cooling] = 24.0
346
+ end
347
+
348
+ surf[:conditioned] = surf.key?(:heating) || surf.key?(:cooling)
310
349
  surf[:space ] = space
311
350
  surf[:boundary ] = facing
312
351
  surf[:ground ] = surface.isGroundSurface
313
352
  surf[:type ] = :floor
314
- surf[:type ] = :ceiling if type.include?("ceiling")
315
- surf[:type ] = :wall if type.include?("wall" )
316
- surf[:stype ] = stype.get unless stype.empty?
317
- surf[:story ] = story.get unless story.empty?
353
+ surf[:type ] = :ceiling if type.include?("ceiling")
354
+ surf[:type ] = :wall if type.include?("wall" )
355
+ surf[:stype ] = stype.get unless stype.empty?
356
+ surf[:story ] = story.get unless story.empty?
318
357
  surf[:n ] = n
319
358
  surf[:gross ] = surface.grossArea
320
359
  surf[:filmRSI ] = surface.filmResistance
360
+ surf[:spandrel ] = spandrel?(surface)
321
361
 
322
362
  surface.subSurfaces.sort_by { |s| s.nameString }.each do |s|
323
- next unless surface_valid?(s)
324
-
325
- id = s.nameString
326
- valid = s.vertices.size == 3 || s.vertices.size == 4
327
- log(ERR, "Skipping '#{id}': vertex # 3 or 4 (#{mth})") unless valid
328
- next unless valid
329
- vec = s.vertices
330
- area = s.grossArea
331
- mult = s.multiplier
332
- typ = s.subSurfaceType.downcase
333
- type = :skylight
334
- type = :window if typ.include?("window" )
335
- type = :door if typ.include?("door" )
336
- glazed = type == :door && typ.include?("glass" )
337
- tubular = typ.include?("tubular")
338
- domed = typ.include?("dome" )
339
- unhinged = false
340
-
341
- # Determine if TDD dome subsurface is unhinged i.e. unconnected to parent.
363
+ next if poly(s).empty?
364
+
365
+ id = s.nameString
366
+ typ = surface.surfaceType.downcase
367
+
368
+ unless (3..4).cover?(s.vertices.size)
369
+ log(ERR, "Skipping '#{id}': vertex # 3 or 4 (#{mth})")
370
+ next
371
+ end
372
+
373
+ vec = s.vertices
374
+ area = s.grossArea
375
+ mult = s.multiplier
376
+
377
+ # An OpenStudio subsurface has a "type" (string), either defaulted during
378
+ # initialization or explicitely set by the user (from a built-in list):
379
+ #
380
+ # OpenStudio::Model::SubSurface.validSubSurfaceTypeValues
381
+ # - "FixedWindow"
382
+ # - "OperableWindow"
383
+ # - "Door"
384
+ # - "GlassDoor"
385
+ # - "OverheadDoor"
386
+ # - "Skylight"
387
+ # - "TubularDaylightDome"
388
+ # - "TubularDaylightDiffuser"
389
+ typ = s.subSurfaceType.downcase
390
+
391
+ # An OpenStudio default subsurface construction set can hold unique
392
+ # constructions assigned for each of these admissible types. In addition,
393
+ # type assignment determines whether frame/divider attributes can be
394
+ # linked to a subsurface (this shortlist has evolved between OpenStudio
395
+ # releases). Type assignment is relied upon when calculating (admissible)
396
+ # fenestration areas. TBD also relies on OpenStudio subsurface type
397
+ # assignment, with resulting TBD tags being a bit more concise, e.g.:
398
+ #
399
+ # - :window includes "FixedWindow" and "OperableWindow"
400
+ # - :door includes "Door", "OverheadWindow" and "GlassDoor"
401
+ # ... a (roof) access roof hatch should be assigned as a "Door"
402
+ # - :skylight includes "Skylight", "TubularDaylightDome", etc.
403
+ #
404
+ type = :skylight
405
+ type = :window if typ.include?("window") # operable or not
406
+ type = :door if typ.include?("door") # fenestrated or not
407
+
408
+ # In fact, ANY subsurface other than :window or :door is tagged as
409
+ # :skylight, e.g. a glazed floor opening (CN, Calgary, Tokyo towers). This
410
+ # happens to reflect OpenStudio default initialization behaviour. For
411
+ # instance, a subsurface added to an exposed (horizontal) floor in
412
+ # OpenStudio is automatically assigned a "Skylight" type. This is similar
413
+ # to the auto-assignment of (opaque) walls, roof/ceilings and floors
414
+ # (based on surface tilt) in OpenStudio.
415
+ #
416
+ # When it comes to major thermal bridging, ASHRAE 90.1 (2022) makes a
417
+ # clear distinction between "vertical fenestration" (a defined term) and
418
+ # all other subsurfaces. "Vertical fenestration" would include both
419
+ # instances of "Window", as well as "GlassDoor". It would exclude however
420
+ # a non-fenestrated "door" (another defined term), like "Door" &
421
+ # "OverheadDoor", as well as skylights. TBD tracks relevant subsurface
422
+ # attributes via a handful of boolean variables:
423
+ glazed = type == :door && typ.include?("glass") # fenestrated door
424
+ tubular = typ.include?("tubular") # dome or diffuser
425
+ domed = typ.include?("dome") # (tubular) dome
426
+ unhinged = false # (tubular) dome
427
+
428
+ # It would be tempting (and simple) to have TBD further validate whether a
429
+ # "GlassDoor" is actually integrated within a (vertical) wall. The
430
+ # automated type assignment in OpenStudio is very simple and reliable (as
431
+ # discussed in the preceding paragraphs), yet users can nonetheless reset
432
+ # this explicitly. For instance, while a vertical surface may indeed be
433
+ # auto-assigned "Wall", a modeller can just as easily reset its type as
434
+ # "Floor". Although OpenStudio supports 90.1 rules by default, it's not
435
+ # enforced. TBD retains the same approach: for whatever osbcur reason a
436
+ # modeller may decide (and hopefully the "authority having jurisdiction"
437
+ # may authorize) to reset a wall as a "Floor" or a roof skylight as a
438
+ # "GlassDoor", TBD maintains the same OpenStudio policy. Either OpenStudio
439
+ # (and consequently EnergyPlus) sub/surface type assignment is reliable,
440
+ # or it is not.
441
+
442
+ # Determine if TDD dome subsurface is 'unhinged', i.e. unconnected to its
443
+ # base surface (not same 3D plane).
342
444
  if domed
343
- unhinged = true unless s.plane.equal(surface.plane)
344
- n = s.outwardNormal if unhinged
445
+ unhinged = true unless s.plane.equal(surface.plane)
446
+ n = s.outwardNormal if unhinged
447
+ end
448
+
449
+ if area < TOL
450
+ log(ERR, "Skipping '#{id}': gross area ~zero (#{mth})")
451
+ next
345
452
  end
346
453
 
347
- log(ERR, "Skipping '#{id}': gross area ~zero (#{mth})") if area < TOL
348
- next if area < TOL
349
454
  c = s.construction
350
- log(ERR, "Skipping '#{id}': missing construction (#{mth})") if c.empty?
351
- next if c.empty?
455
+
456
+ if c.empty?
457
+ log(ERR, "Skipping '#{id}': missing construction (#{mth})")
458
+ next
459
+ end
460
+
352
461
  c = c.get.to_LayeredConstruction
353
- log(WRN, "Skipping '#{id}': subs limited to #{cl3} (#{mth})") if c.empty?
354
- next if c.empty?
462
+
463
+ if c.empty?
464
+ log(WRN, "Skipping '#{id}': subs limited to #{cl2} (#{mth})")
465
+ next
466
+ end
467
+
355
468
  c = c.get
356
469
 
357
470
  # A subsurface may have an overall U-factor set by the user - a less
@@ -364,7 +477,7 @@ module TBD
364
477
  # window-calculation-module.html#simple-window-model
365
478
  #
366
479
  # TBD will instead rely on Tubular Daylighting Device (TDD) effective
367
- # dome-to-diffuser RSi values (if valid).
480
+ # dome-to-diffuser RSi-factors (if valid).
368
481
  #
369
482
  # https://bigladdersoftware.com/epx/docs/9-6/engineering-reference/
370
483
  # daylighting-devices.html#tubular-daylighting-devices
@@ -379,12 +492,12 @@ module TBD
379
492
  # resistances). This is the least reliable option, especially if
380
493
  # subsurfaces have Frame & Divider objects, or irregular geometry.
381
494
  u = s.uFactor
382
- u = u.get unless u.empty?
495
+ u = u.get unless u.empty?
383
496
 
384
- if tubular & s.respond_to?(:daylightingDeviceTubular) # OSM > v3.3.0
497
+ if tubular & s.respond_to?(:daylightingDeviceTubular) # OSM > v3.3.0
385
498
  unless s.daylightingDeviceTubular.empty?
386
499
  r = s.daylightingDeviceTubular.get.effectiveThermalResistance
387
- u = 1 / r if r > TOL
500
+ u = 1 / r if r > TOL
388
501
  end
389
502
  end
390
503
 
@@ -394,8 +507,12 @@ module TBD
394
507
 
395
508
  unless u.is_a?(Numeric)
396
509
  r = rsi(c, surface.filmResistance)
397
- log(ERR, "Skipping '#{id}': U-factor unavailable (#{mth})") if r < TOL
398
- next if r < TOL
510
+
511
+ if r < TOL
512
+ log(ERR, "Skipping '#{id}': U-factor unavailable (#{mth})")
513
+ next
514
+ end
515
+
399
516
  u = 1 / r
400
517
  end
401
518
 
@@ -407,8 +524,12 @@ module TBD
407
524
  width = s.windowPropertyFrameAndDivider.get.frameWidth
408
525
  vec = offset(vec, width, 300)
409
526
  area = OpenStudio.getArea(vec)
410
- log(ERR, "Skipping '#{id}': invalid offset (#{mth})") if area.empty?
411
- next if area.empty?
527
+
528
+ if area.empty?
529
+ log(ERR, "Skipping '#{id}': invalid offset (#{mth})")
530
+ next
531
+ end
532
+
412
533
  area = area.get
413
534
  end
414
535
 
@@ -423,7 +544,7 @@ module TBD
423
544
  unhinged: unhinged }
424
545
 
425
546
  sub[:glazed] = true if glazed
426
- subs[id] = sub
547
+ subs[id ] = sub
427
548
  end
428
549
 
429
550
  valid = true
@@ -433,24 +554,27 @@ module TBD
433
554
  # also inadvertently catch pre-existing (yet nonetheless invalid)
434
555
  # OpenStudio inputs (without Frame & Dividers).
435
556
  subs.each do |id, sub|
436
- break unless fd
437
- break unless valid
438
- valid = fits?(sub[:points], surface.vertices, id, nom)
439
- log(ERR, "Skipping '#{id}': can't fit in '#{nom}' (#{mth})") unless valid
557
+ break unless fd
558
+ break unless valid
559
+
560
+ valid = fits?(sub[:points], surface.vertices)
561
+ log(ERR, "Skipping '#{id}': can't fit in '#{nom}' (#{mth})") unless valid
440
562
 
441
563
  subs.each do |i, sb|
442
- break unless valid
443
- next if i == id
444
- oops = overlaps?(sb[:points], sub[:points], id, nom)
445
- log(ERR, "Skipping '#{id}': overlaps sibling '#{i}' (#{mth})") if oops
446
- valid = false if oops
564
+ break unless valid
565
+ next if i == id
566
+
567
+ if overlaps?(sb[:points], sub[:points])
568
+ log(ERR, "Skipping '#{id}': overlaps sibling '#{i}' (#{mth})")
569
+ valid = false
570
+ end
447
571
  end
448
572
  end
449
573
 
450
574
  if fd
451
- subs.values.each { |sub| sub[:gross ] = sub[:area ] } if valid
452
- subs.values.each { |sub| sub[:points] = sub[:v ] } unless valid
453
- subs.values.each { |sub| sub[:area ] = sub[:gross] } unless valid
575
+ subs.values.each { |sub| sub[:gross ] = sub[:area ] } if valid
576
+ subs.values.each { |sub| sub[:points] = sub[:v ] } unless valid
577
+ subs.values.each { |sub| sub[:area ] = sub[:gross] } unless valid
454
578
  end
455
579
 
456
580
  subarea = 0
@@ -461,21 +585,22 @@ module TBD
461
585
 
462
586
  # Tranform final Point 3D sets, and store.
463
587
  pts = (t * surface.vertices).map { |v| Topolys::Point3D.new(v.x, v.y, v.z) }
588
+
464
589
  surf[:points] = pts
465
590
  surf[:minz ] = ( pts.map { |pt| pt.z } ).min
466
591
 
467
592
  subs.each do |id, sub|
468
593
  pts = (t * sub[:points]).map { |v| Topolys::Point3D.new(v.x, v.y, v.z) }
594
+
469
595
  sub[:points] = pts
470
596
  sub[:minz ] = ( pts.map { |p| p.z } ).min
471
597
 
472
598
  [:windows, :doors, :skylights].each do |types|
473
599
  type = types.slice(0..-2).to_sym
600
+ next unless sub[:type] == type
474
601
 
475
- if sub[:type] == type
476
- surf[types] = {} unless surf.key?(types)
477
- surf[types][id] = sub
478
- end
602
+ surf[types] = {} unless surf.key?(types)
603
+ surf[types][id] = sub
479
604
  end
480
605
  end
481
606
 
@@ -483,33 +608,37 @@ module TBD
483
608
  end
484
609
 
485
610
  ##
486
- # Validate whether edge surfaces form a concave angle, as seen from outside.
611
+ # Validates whether edge surfaces form a concave angle, as seen from outside.
487
612
  #
488
- # @param s1 [Surface] first TBD surface
489
- # @param s2 [Surface] second TBD surface
613
+ # @param [Hash] s1 first TBD surface
614
+ # @param [Hash] s2 second TBD surface
615
+ # @option s1 [Topolys::Vector3D] :normal surface normal vector
616
+ # @option s1 [Topolys::Vector3D] :polar vector around edge
617
+ # @option s1 [Numeric] :angle polar angle vs reference (e.g. North, Zenith)
490
618
  #
491
619
  # @return [Bool] true if angle between surfaces is concave
492
- # @return [Bool] false if invalid input
620
+ # @return [false] if invalid input (see logs)
493
621
  def concave?(s1 = nil, s2 = nil)
494
622
  mth = "TBD::#{__callee__}"
623
+ return mismatch("s1", s1, Hash, mth, DBG, false) unless s1.is_a?(Hash)
624
+ return mismatch("s2", s2, Hash, mth, DBG, false) unless s2.is_a?(Hash)
625
+ return false if s1 == s2
626
+
627
+ return hashkey("s1", s1, :angle, mth, DBG, false) unless s1.key?(:angle)
628
+ return hashkey("s2", s2, :angle, mth, DBG, false) unless s2.key?(:angle)
629
+ return hashkey("s1", s1, :normal, mth, DBG, false) unless s1.key?(:normal)
630
+ return hashkey("s2", s2, :normal, mth, DBG, false) unless s2.key?(:normal)
631
+ return hashkey("s1", s1, :polar, mth, DBG, false) unless s1.key?(:polar)
632
+ return hashkey("s2", s2, :polar, mth, DBG, false) unless s2.key?(:polar)
495
633
 
496
- return mismatch("s1", s1, Hash, mth, DBG, false) unless s1.is_a?(Hash)
497
- return mismatch("s2", s2, Hash, mth, DBG, false) unless s2.is_a?(Hash)
498
- return hashkey("s1", s1, :angle, mth, DBG, false) unless s1.key?(:angle)
499
- return hashkey("s2", s2, :angle, mth, DBG, false) unless s2.key?(:angle)
500
- return hashkey("s1", s1, :normal, mth, DBG, false) unless s1.key?(:normal)
501
- return hashkey("s2", s2, :normal, mth, DBG, false) unless s2.key?(:normal)
502
- return hashkey("s1", s1, :polar, mth, DBG, false) unless s1.key?(:polar)
503
- return hashkey("s2", s2, :polar, mth, DBG, false) unless s2.key?(:polar)
504
634
  valid1 = s1[:angle].is_a?(Numeric)
505
635
  valid2 = s2[:angle].is_a?(Numeric)
506
- return mismatch("s1 angle", s1[:angle], Numeric, DBG, false) unless valid1
507
- return mismatch("s1 angle", s1[:angle], Numeric, DBG, false) unless valid2
636
+ return mismatch("s1 angle", s1[:angle], Numeric, DBG, false) unless valid1
637
+ return mismatch("s1 angle", s1[:angle], Numeric, DBG, false) unless valid2
508
638
 
509
639
  angle = 0
510
640
  angle = s2[:angle] - s1[:angle] if s2[:angle] > s1[:angle]
511
641
  angle = s1[:angle] - s2[:angle] if s1[:angle] > s2[:angle]
512
-
513
642
  return false if angle < TOL
514
643
  return false unless (2 * Math::PI - angle).abs > TOL
515
644
  return false if angle > 3 * Math::PI / 4 && angle < 5 * Math::PI / 4
@@ -522,33 +651,37 @@ module TBD
522
651
  end
523
652
 
524
653
  ##
525
- # Validate whether edge surfaces form a convex angle, as seen from outside.
654
+ # Validates whether edge surfaces form a convex angle, as seen from outside.
526
655
  #
527
- # @param s1 [Surface] first TBD surface
528
- # @param s2 [Surface] second TBD surface
656
+ # @param [Hash] s1 first TBD surface
657
+ # @param [Hash] s2 second TBD surface
658
+ # @option s1 [Topolys::Vector3D] :normal surface normal vector
659
+ # @option s1 [Topolys::Vector3D] :polar vector around edge
660
+ # @option s1 [Numeric] :angle polar angle vs reference (e.g. North, Zenith)
529
661
  #
530
662
  # @return [Bool] true if angle between surfaces is convex
531
- # @return [Bool] false if invalid input
663
+ # @return [false] if invalid input (see logs)
532
664
  def convex?(s1 = nil, s2 = nil)
533
665
  mth = "TBD::#{__callee__}"
666
+ return mismatch("s1", s1, Hash, mth, DBG, false) unless s1.is_a?(Hash)
667
+ return mismatch("s2", s2, Hash, mth, DBG, false) unless s2.is_a?(Hash)
668
+ return false if s1 == s2
669
+
670
+ return hashkey("s1", s1, :angle, mth, DBG, false) unless s1.key?(:angle)
671
+ return hashkey("s2", s2, :angle, mth, DBG, false) unless s2.key?(:angle)
672
+ return hashkey("s1", s1, :normal, mth, DBG, false) unless s1.key?(:normal)
673
+ return hashkey("s2", s2, :normal, mth, DBG, false) unless s2.key?(:normal)
674
+ return hashkey("s1", s1, :polar, mth, DBG, false) unless s1.key?(:polar)
675
+ return hashkey("s2", s2, :polar, mth, DBG, false) unless s2.key?(:polar)
534
676
 
535
- return mismatch("s1", s1, Hash, mth, DBG, false) unless s1.is_a?(Hash)
536
- return mismatch("s2", s2, Hash, mth, DBG, false) unless s2.is_a?(Hash)
537
- return hashkey("s1", s1, :angle, mth, DBG, false) unless s1.key?(:angle)
538
- return hashkey("s2", s2, :angle, mth, DBG, false) unless s2.key?(:angle)
539
- return hashkey("s1", s1, :normal, mth, DBG, false) unless s1.key?(:normal)
540
- return hashkey("s2", s2, :normal, mth, DBG, false) unless s2.key?(:normal)
541
- return hashkey("s1", s1, :polar, mth, DBG, false) unless s1.key?(:polar)
542
- return hashkey("s2", s2, :polar, mth, DBG, false) unless s2.key?(:polar)
543
677
  valid1 = s1[:angle].is_a?(Numeric)
544
678
  valid2 = s2[:angle].is_a?(Numeric)
545
- return mismatch("s1 angle", s1[:angle], Numeric, DBG, false) unless valid1
546
- return mismatch("s1 angle", s1[:angle], Numeric, DBG, false) unless valid2
679
+ return mismatch("s1 angle", s1[:angle], Numeric, DBG, false) unless valid1
680
+ return mismatch("s1 angle", s1[:angle], Numeric, DBG, false) unless valid2
547
681
 
548
682
  angle = 0
549
683
  angle = s2[:angle] - s1[:angle] if s2[:angle] > s1[:angle]
550
684
  angle = s1[:angle] - s2[:angle] if s1[:angle] > s2[:angle]
551
-
552
685
  return false if angle < TOL
553
686
  return false unless (2 * Math::PI - angle).abs > TOL
554
687
  return false if angle > 3 * Math::PI / 4 && angle < 5 * Math::PI / 4
@@ -561,7 +694,64 @@ module TBD
561
694
  end
562
695
 
563
696
  ##
564
- # Generate Kiva settings and objects if model surfaces have 'foundation'
697
+ # Purge existing KIVA-related objects in an OpenStudio model. Resets ground-
698
+ # facing surface outside boundary condition to "Ground" or "Foundation".
699
+ #
700
+ # @param model [OpenStudio::Model::Model] a model
701
+ # @param boundary ["Ground", "Foundation"] new outside boundary condition
702
+ #
703
+ # @return [Bool] true if model is free of KIVA-related objects
704
+ # @return [false] if invalid input (see logs)
705
+ def resetKIVA(model = nil, boundary = "Foundation")
706
+ mth = "TBD::#{__callee__}"
707
+ cl = OpenStudio::Model::Model
708
+ ck1 = model.is_a?(cl)
709
+ ck2 = boundary.respond_to?(:to_s)
710
+ kva = false
711
+ b = ["Ground", "Foundation"]
712
+ return mismatch("model" , model , cl , mth, DBG, kva) unless ck1
713
+ return mismatch("boundary", boundary, String, mth, DBG, kva) unless ck2
714
+
715
+ boundary.capitalize!
716
+ return invalid("boundary", mth, 2, DBG, kva) unless b.include?(boundary)
717
+
718
+ # Reset surface KIVA-related objects.
719
+ model.getSurfaces.each do |surface|
720
+ kva = true unless surface.adjacentFoundation.empty?
721
+ kva = true unless surface.surfacePropertyExposedFoundationPerimeter.empty?
722
+ surface.resetAdjacentFoundation
723
+ surface.resetSurfacePropertyExposedFoundationPerimeter
724
+ next unless surface.isGroundSurface
725
+ next if surface.outsideBoundaryCondition.capitalize == boundary
726
+
727
+ lc = surface.construction.empty? ? nil : surface.construction.get
728
+ surface.setOutsideBoundaryCondition(boundary)
729
+ next if boundary == "Ground"
730
+ next if lc.nil?
731
+
732
+ surface.setConstruction(lc) if surface.construction.empty?
733
+ end
734
+
735
+ perimeters = model.getSurfacePropertyExposedFoundationPerimeters
736
+
737
+ kva = true unless perimeters.empty?
738
+
739
+ # Remove KIVA exposed perimeters.
740
+ perimeters.each { |perimeter| perimeter.remove }
741
+
742
+ # Remove KIVA custom blocks, & foundations.
743
+ model.getFoundationKivas.each do |kiva|
744
+ kiva.removeAllCustomBlocks
745
+ kiva.remove
746
+ end
747
+
748
+ log(INF, "Purged KIVA objects from model (#{mth})") if kva
749
+
750
+ true
751
+ end
752
+
753
+ ##
754
+ # Generates Kiva settings and objects if model surfaces have 'foundation'
565
755
  # boundary conditions.
566
756
  #
567
757
  # @param model [OpenStudio::Model::Model] a model
@@ -570,19 +760,28 @@ module TBD
570
760
  # @param edges [Hash] TBD edges (many linking floors & walls
571
761
  #
572
762
  # @return [Bool] true if Kiva foundations are successfully generated
573
- # @return [Bool] false if invalid input
763
+ # @return [false] if invalid input (see logs)
574
764
  def kiva(model = nil, walls = {}, floors = {}, edges = {})
575
765
  mth = "TBD::#{__callee__}"
576
766
  cl1 = OpenStudio::Model::Model
577
767
  cl2 = Hash
578
768
  a = false
579
-
580
- return mismatch("model", model, cl1, mth, DBG, a) unless model.is_a?(cl1)
581
- return mismatch("walls", walls, cl2, mth, DBG, a) unless walls.is_a?(cl2)
769
+ return mismatch("model" , model, cl1, mth, DBG, a) unless model.is_a?(cl1)
770
+ return mismatch("walls" , walls, cl2, mth, DBG, a) unless walls.is_a?(cl2)
582
771
  return mismatch("floors", floors, cl2, mth, DBG, a) unless floors.is_a?(cl2)
583
- return mismatch("edges", edges, cl2, mth, DBG, a) unless edges.is_a?(cl2)
584
-
585
- kva = true
772
+ return mismatch("edges" , edges, cl2, mth, DBG, a) unless edges.is_a?(cl2)
773
+
774
+ # Check for existing KIVA objects.
775
+ kva = false
776
+ kva = true unless model.getSurfacePropertyExposedFoundationPerimeters.empty?
777
+ kva = true unless model.getFoundationKivas.empty?
778
+
779
+ if kva
780
+ log(ERR, "Exiting - KIVA objects in model (#{mth})")
781
+ return a
782
+ else
783
+ kva = true
784
+ end
586
785
 
587
786
  # Pre-validate foundation-facing constructions.
588
787
  model.getSurfaces.each do |s|
@@ -591,20 +790,20 @@ module TBD
591
790
  next unless s.outsideBoundaryCondition.downcase == "foundation"
592
791
 
593
792
  if construction.empty?
594
- log(ERR, "Invalid construction for KIVA (see #{id})")
595
- kva = false if kva
793
+ log(ERR, "Invalid construction for #{id} (#{mth})")
794
+ kva = false
596
795
  else
597
796
  construction = construction.get.to_LayeredConstruction
598
797
 
599
798
  if construction.empty?
600
- log(ERR, "KIVA requires layered constructions (see #{id})")
601
- kva = false if kva
799
+ log(ERR, "Invalid layered constructions for #{id} (#{mth})")
800
+ kva = false
602
801
  else
603
802
  construction = construction.get
604
803
 
605
804
  unless standardOpaqueLayers?(construction)
606
- log(ERR, "KIVA requires standard materials (see #{id})")
607
- kva = false if kva
805
+ log(ERR, "Non-standard materials for #{id} (#{mth})")
806
+ kva = false
608
807
  end
609
808
  end
610
809
  end
@@ -613,12 +812,12 @@ module TBD
613
812
  return a unless kva
614
813
 
615
814
  # Strictly relying on Kiva's total exposed perimeter approach.
616
- arg = "TotalExposedPerimeter"
815
+ arg = "TotalExposedPerimeter"
617
816
  kiva = true
618
817
  # The following is loosely adapted from:
619
818
  #
620
- # github.com/NREL/OpenStudio-resources/blob/develop/model/simulationtests/
621
- # foundation_kiva.rb ... thanks.
819
+ # github.com/NREL/OpenStudio-resources/blob/develop/model/
820
+ # simulationtests/foundation_kiva.rb ... thanks.
622
821
  #
623
822
  # Access to KIVA settings. This is usually not required (the default KIVA
624
823
  # settings are fine), but its explicit inclusion in the model does offer
@@ -626,6 +825,7 @@ module TBD
626
825
  # required. Initial tests show slight differences in simulation results
627
826
  # w/w/o explcit inclusion of the KIVA settings template in the model.
628
827
  settings = model.getFoundationKivaSettings
828
+
629
829
  k = settings.soilConductivity
630
830
  settings.setSoilConductivity(k)
631
831
 
@@ -635,36 +835,43 @@ module TBD
635
835
  next unless floors.key?(id)
636
836
  next unless floors[id][:boundary].downcase == "foundation"
637
837
  next if floors[id].key?(:kiva)
638
- floors[id][:kiva ] = :slab # initially slabs-on-grade
639
- floors[id][:exposed] = 0.0 # slab-on-grade or basement walkout perimeter
640
838
 
641
- edge[:surfaces].keys.each do |i| # loop around current edge
839
+ floors[id][:kiva ] = :slab # initially slabs-on-grade
840
+ floors[id][:exposed] = 0.0 # slab-on-grade or walkout perimeter
841
+
842
+ # Loop around current edge.
843
+ edge[:surfaces].keys.each do |i|
642
844
  next if i == id
643
845
  next unless walls.key?(i)
644
846
  next unless walls[i][:boundary].downcase == "foundation"
645
847
  next if walls[i].key?(:kiva)
848
+
646
849
  floors[id][:kiva] = :basement
647
850
  walls[i ][:kiva] = id
648
851
  end
649
852
 
650
- edge[:surfaces].keys.each do |i| # loop around current edge
853
+ # Loop around current edge.
854
+ edge[:surfaces].keys.each do |i|
651
855
  next if i == id
652
856
  next unless walls.key?(i)
653
857
  next unless walls[i][:boundary].downcase == "outdoors"
858
+
654
859
  floors[id][:exposed] += edge[:length]
655
860
  end
656
861
 
657
- edges.each do |code2, e| # loop around other floor edges
658
- next if code1 == code2 # skip - same edge
862
+ # Loop around other floor edges.
863
+ edges.each do |code2, e|
864
+ next if code1 == code2 # skip - same edge
659
865
 
660
866
  e[:surfaces].keys.each do |i|
661
- next unless i == id # good - same floor
867
+ next unless i == id # good - same floor
662
868
 
663
869
  e[:surfaces].keys.each do |ii|
664
870
  next if i == ii
665
871
  next unless walls.key?(ii)
666
872
  next unless walls[ii][:boundary].downcase == "foundation"
667
873
  next if walls[ii].key?(:kiva)
874
+
668
875
  floors[id][:kiva] = :basement
669
876
  walls[ii ][:kiva] = id
670
877
  end
@@ -673,6 +880,7 @@ module TBD
673
880
  next if i == ii
674
881
  next unless walls.key?(ii)
675
882
  next unless walls[ii][:boundary].downcase == "outdoors"
883
+
676
884
  floors[id][:exposed] += e[:length]
677
885
  end
678
886
  end
@@ -680,32 +888,34 @@ module TBD
680
888
 
681
889
  foundation = OpenStudio::Model::FoundationKiva.new(model)
682
890
  foundation.setName("KIVA Foundation Floor #{id}")
683
-
684
891
  floor = model.getSurfaceByName(id)
685
892
  kiva = false if floor.empty?
686
893
  next if floor.empty?
894
+
687
895
  floor = floor.get
688
896
  construction = floor.construction
689
897
  kiva = false if construction.empty?
690
898
  next if construction.empty?
899
+
691
900
  construction = construction.get
692
901
  floor.setAdjacentFoundation(foundation)
693
902
  floor.setConstruction(construction)
694
-
695
903
  ep = floors[id][:exposed]
696
904
  per = floor.createSurfacePropertyExposedFoundationPerimeter(arg, ep)
697
- kiva = false if per.empty?
698
- next if per.empty?
699
- per = per.get
700
- perimeter = per.totalExposedPerimeter
701
- kiva = false if perimeter.empty?
702
- next if perimeter.empty?
703
- perimeter = perimeter.get
905
+ kiva = false if per.empty?
906
+ next if per.empty?
907
+
908
+ per = per.get
909
+ perimeter = per.totalExposedPerimeter
910
+ kiva = false if perimeter.empty?
911
+ next if perimeter.empty?
912
+
913
+ perimeter = perimeter.get
704
914
 
705
915
  if ep < 0.001
706
916
  ok = per.setTotalExposedPerimeter(0.000)
707
917
  ok = per.setTotalExposedPerimeter(0.001) unless ok
708
- kiva = false unless ok
918
+ kiva = false unless ok
709
919
  elsif (perimeter - ep).abs < TOL
710
920
  xps25 = model.getStandardOpaqueMaterialByName("XPS 25mm")
711
921
 
@@ -733,16 +943,20 @@ module TBD
733
943
 
734
944
  walls.each do |i, wall|
735
945
  next unless wall.key?(:kiva)
736
- id = walls[i][:kiva]
946
+
947
+ id = walls[i][:kiva]
737
948
  next unless floors.key?(id)
738
949
  next unless floors[id].key?(:foundation)
739
- mur = model.getSurfaceByName(i) # locate OpenStudio wall
950
+
951
+ mur = model.getSurfaceByName(i) # locate OpenStudio wall
740
952
  kiva = false if mur.empty?
741
953
  next if mur.empty?
954
+
742
955
  mur = mur.get
743
956
  construction = mur.construction
744
957
  kiva = false if construction.empty?
745
958
  next if construction.empty?
959
+
746
960
  construction = construction.get
747
961
  mur.setAdjacentFoundation(floors[id][:foundation])
748
962
  mur.setConstruction(construction)