tbd 3.2.3 → 3.3.0

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