tbd 3.2.2 → 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.
@@ -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,55 +244,18 @@ module TBD
219
244
  end
220
245
 
221
246
  ##
222
- # Validate whether an OpenStudio planar surface is safe for TBD to process.
223
- #
224
- # @param s [OpenStudio::Model::PlanarSurface] a surface
225
- #
226
- # @return [Bool] true if valid surface
227
- def validate(s = nil)
228
- mth = "TBD::#{__callee__}"
229
- cl = OpenStudio::Model::PlanarSurface
230
-
231
- return mismatch("surface", s, cl, mth, DBG, false) unless s.is_a?(cl)
232
-
233
- id = s.nameString
234
- size = s.vertices.size
235
- last = size - 1
236
-
237
- log(ERR, "#{id} #{size} vertices? need +3 (#{mth})") unless size > 2
238
- return false unless size > 2
239
-
240
- [0, last].each do |i|
241
- v1 = s.vertices[i]
242
- v2 = s.vertices[i + 1] unless i == last
243
- v2 = s.vertices.first if i == last
244
- vector = v2 - v1
245
- bad = vector.length < TOL
246
-
247
- # As is, this comparison also catches collinear vertices (< 10mm apart)
248
- # along an edge. Should avoid red-flagging such cases. TO DO.
249
- log(ERR, "#{id}: < #{TOL}m (#{mth})") if bad
250
- return false if bad
251
- end
252
-
253
- # Add as many extra tests as needed ...
254
- true
255
- end
256
-
257
- ##
258
- # Return site-specific (or true) Topolys normal vector of OpenStudio surface.
247
+ # Returns site (or true) Topolys normal vector of OpenStudio surface.
259
248
  #
260
249
  # @param s [OpenStudio::Model::PlanarSurface] a planar surface
261
- # @param r [Float] a group/site rotation angle [0,2PI) radians
250
+ # @param r [#to_f] a group/site rotation angle [0,2PI) radians
262
251
  #
263
- # @return [Topolys::Vector3D] normal (Topolys) vector <x,y,z> of s
264
- # @return [NilClass] if invalid input
252
+ # @return [Topolys::Vector3D] true normal vector of s
253
+ # @return [nil] if invalid input (see logs)
265
254
  def trueNormal(s = nil, r = 0)
266
255
  mth = "TBD::#{__callee__}"
267
256
  cl = OpenStudio::Model::PlanarSurface
268
-
269
- return mismatch("surface", s, cl, mth) unless s.is_a?(cl)
270
- 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)
271
259
 
272
260
  r = -r.to_f * Math::PI / 180.0
273
261
  vx = s.outwardNormal.x * Math.cos(r) - s.outwardNormal.y * Math.sin(r)
@@ -277,45 +265,46 @@ module TBD
277
265
  end
278
266
 
279
267
  ##
280
- # Fetch OpenStudio surface properties, including opening areas & vertices.
268
+ # Fetches OpenStudio surface properties, including opening areas & vertices.
281
269
  #
282
- # @param model [OpenStudio::Model::Model] a model
283
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
284
273
  #
285
- # @return [Hash] TBD surface with key attributes, including openings
286
- # @return [NilClass] if invalid input
287
- 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 = {})
288
277
  mth = "TBD::#{__callee__}"
289
- cl1 = OpenStudio::Model::Model
290
- cl2 = OpenStudio::Model::Surface
291
- cl3 = OpenStudio::Model::LayeredConstruction
292
-
293
- return mismatch("model", model, cl1, mth) unless model.is_a?(cl1)
294
- return mismatch("surface", surface, cl2, mth) unless surface.is_a?(cl2)
295
- return nil unless validate(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)
296
283
 
297
284
  nom = surface.nameString
298
285
  surf = {}
299
286
  subs = {}
300
287
  fd = false
301
- 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?
302
290
 
303
291
  space = surface.space.get
304
292
  stype = space.spaceType
305
293
  story = space.buildingStory
306
- tr = transforms(model, space)
307
- 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]
308
296
 
309
297
  t = tr[:t]
310
298
  n = trueNormal(surface, tr[:r])
311
- return invalid("'#{nom}' normal", mth, 0, FTL) unless n
299
+ return invalid("#{nom} normal", mth, 0, FTL) unless n
312
300
 
313
301
  type = surface.surfaceType.downcase
314
302
  facing = surface.outsideBoundaryCondition
303
+ setpts = setpoints(space)
315
304
 
316
305
  if facing.downcase == "surface"
317
306
  empty = surface.adjacentSurface.empty?
318
- return invalid("'#{nom}': adjacent surface", mth, 0, ERR) if empty
307
+ return invalid("#{nom}: adjacent surface", mth, 0, ERR) if empty
319
308
 
320
309
  facing = surface.adjacentSurface.get.nameString
321
310
  end
@@ -326,9 +315,9 @@ module TBD
326
315
  unless construction.empty?
327
316
  construction = construction.get
328
317
  lyr = insulatingLayer(construction)
329
- lyr[:index] = nil unless lyr[:index].is_a?(Numeric)
330
- lyr[:index] = nil unless lyr[:index] >= 0
331
- 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
332
321
 
333
322
  if lyr[:index]
334
323
  surf[:construction] = construction
@@ -342,52 +331,140 @@ module TBD
342
331
  end
343
332
  end
344
333
 
345
- 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)
346
349
  surf[:space ] = space
347
350
  surf[:boundary ] = facing
348
351
  surf[:ground ] = surface.isGroundSurface
349
352
  surf[:type ] = :floor
350
- surf[:type ] = :ceiling if type.include?("ceiling")
351
- surf[:type ] = :wall if type.include?("wall" )
352
- surf[:stype ] = stype.get unless stype.empty?
353
- 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?
354
357
  surf[:n ] = n
355
358
  surf[:gross ] = surface.grossArea
356
359
  surf[:filmRSI ] = surface.filmResistance
360
+ surf[:spandrel ] = spandrel?(surface)
357
361
 
358
362
  surface.subSurfaces.sort_by { |s| s.nameString }.each do |s|
359
- next unless validate(s)
360
-
361
- id = s.nameString
362
- valid = s.vertices.size == 3 || s.vertices.size == 4
363
- log(ERR, "Skipping '#{id}': vertex # 3 or 4 (#{mth})") unless valid
364
- next unless valid
365
- vec = s.vertices
366
- area = s.grossArea
367
- mult = s.multiplier
368
- typ = s.subSurfaceType.downcase
369
- type = :skylight
370
- type = :window if typ.include?("window" )
371
- type = :door if typ.include?("door" )
372
- glazed = type == :door && typ.include?("glass" )
373
- tubular = typ.include?("tubular")
374
- domed = typ.include?("dome" )
375
- unhinged = false
376
-
377
- # 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).
378
444
  if domed
379
- unhinged = true unless s.plane.equal(surface.plane)
380
- 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
381
452
  end
382
453
 
383
- log(ERR, "Skipping '#{id}': gross area ~zero (#{mth})") if area < TOL
384
- next if area < TOL
385
454
  c = s.construction
386
- log(ERR, "Skipping '#{id}': missing construction (#{mth})") if c.empty?
387
- next if c.empty?
455
+
456
+ if c.empty?
457
+ log(ERR, "Skipping '#{id}': missing construction (#{mth})")
458
+ next
459
+ end
460
+
388
461
  c = c.get.to_LayeredConstruction
389
- log(WRN, "Skipping '#{id}': subs limited to #{cl3} (#{mth})") if c.empty?
390
- next if c.empty?
462
+
463
+ if c.empty?
464
+ log(WRN, "Skipping '#{id}': subs limited to #{cl2} (#{mth})")
465
+ next
466
+ end
467
+
391
468
  c = c.get
392
469
 
393
470
  # A subsurface may have an overall U-factor set by the user - a less
@@ -400,7 +477,7 @@ module TBD
400
477
  # window-calculation-module.html#simple-window-model
401
478
  #
402
479
  # TBD will instead rely on Tubular Daylighting Device (TDD) effective
403
- # dome-to-diffuser RSi values (if valid).
480
+ # dome-to-diffuser RSi-factors (if valid).
404
481
  #
405
482
  # https://bigladdersoftware.com/epx/docs/9-6/engineering-reference/
406
483
  # daylighting-devices.html#tubular-daylighting-devices
@@ -415,12 +492,12 @@ module TBD
415
492
  # resistances). This is the least reliable option, especially if
416
493
  # subsurfaces have Frame & Divider objects, or irregular geometry.
417
494
  u = s.uFactor
418
- u = u.get unless u.empty?
495
+ u = u.get unless u.empty?
419
496
 
420
- if tubular & s.respond_to?(:daylightingDeviceTubular) # OSM > v3.3.0
497
+ if tubular & s.respond_to?(:daylightingDeviceTubular) # OSM > v3.3.0
421
498
  unless s.daylightingDeviceTubular.empty?
422
499
  r = s.daylightingDeviceTubular.get.effectiveThermalResistance
423
- u = 1 / r if r > TOL
500
+ u = 1 / r if r > TOL
424
501
  end
425
502
  end
426
503
 
@@ -430,8 +507,12 @@ module TBD
430
507
 
431
508
  unless u.is_a?(Numeric)
432
509
  r = rsi(c, surface.filmResistance)
433
- log(ERR, "Skipping '#{id}': U-factor unavailable (#{mth})") if r < TOL
434
- next if r < TOL
510
+
511
+ if r < TOL
512
+ log(ERR, "Skipping '#{id}': U-factor unavailable (#{mth})")
513
+ next
514
+ end
515
+
435
516
  u = 1 / r
436
517
  end
437
518
 
@@ -443,8 +524,12 @@ module TBD
443
524
  width = s.windowPropertyFrameAndDivider.get.frameWidth
444
525
  vec = offset(vec, width, 300)
445
526
  area = OpenStudio.getArea(vec)
446
- log(ERR, "Skipping '#{id}': invalid offset (#{mth})") if area.empty?
447
- next if area.empty?
527
+
528
+ if area.empty?
529
+ log(ERR, "Skipping '#{id}': invalid offset (#{mth})")
530
+ next
531
+ end
532
+
448
533
  area = area.get
449
534
  end
450
535
 
@@ -459,7 +544,7 @@ module TBD
459
544
  unhinged: unhinged }
460
545
 
461
546
  sub[:glazed] = true if glazed
462
- subs[id] = sub
547
+ subs[id ] = sub
463
548
  end
464
549
 
465
550
  valid = true
@@ -469,24 +554,27 @@ module TBD
469
554
  # also inadvertently catch pre-existing (yet nonetheless invalid)
470
555
  # OpenStudio inputs (without Frame & Dividers).
471
556
  subs.each do |id, sub|
472
- break unless fd
473
- break unless valid
474
- valid = fits?(sub[:points], surface.vertices, id, nom)
475
- 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
476
562
 
477
563
  subs.each do |i, sb|
478
- break unless valid
479
- next if i == id
480
- oops = overlaps?(sb[:points], sub[:points], id, nom)
481
- log(ERR, "Skipping '#{id}': overlaps sibling '#{i}' (#{mth})") if oops
482
- 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
483
571
  end
484
572
  end
485
573
 
486
574
  if fd
487
- subs.values.each { |sub| sub[:gross ] = sub[:area ] } if valid
488
- subs.values.each { |sub| sub[:points] = sub[:v ] } unless valid
489
- 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
490
578
  end
491
579
 
492
580
  subarea = 0
@@ -497,21 +585,22 @@ module TBD
497
585
 
498
586
  # Tranform final Point 3D sets, and store.
499
587
  pts = (t * surface.vertices).map { |v| Topolys::Point3D.new(v.x, v.y, v.z) }
588
+
500
589
  surf[:points] = pts
501
590
  surf[:minz ] = ( pts.map { |pt| pt.z } ).min
502
591
 
503
592
  subs.each do |id, sub|
504
593
  pts = (t * sub[:points]).map { |v| Topolys::Point3D.new(v.x, v.y, v.z) }
594
+
505
595
  sub[:points] = pts
506
596
  sub[:minz ] = ( pts.map { |p| p.z } ).min
507
597
 
508
598
  [:windows, :doors, :skylights].each do |types|
509
599
  type = types.slice(0..-2).to_sym
600
+ next unless sub[:type] == type
510
601
 
511
- if sub[:type] == type
512
- surf[types] = {} unless surf.key?(types)
513
- surf[types][id] = sub
514
- end
602
+ surf[types] = {} unless surf.key?(types)
603
+ surf[types][id] = sub
515
604
  end
516
605
  end
517
606
 
@@ -519,33 +608,37 @@ module TBD
519
608
  end
520
609
 
521
610
  ##
522
- # 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.
523
612
  #
524
- # @param s1 [Surface] first TBD surface
525
- # @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)
526
618
  #
527
619
  # @return [Bool] true if angle between surfaces is concave
528
- # @return [Bool] false if invalid input
620
+ # @return [false] if invalid input (see logs)
529
621
  def concave?(s1 = nil, s2 = nil)
530
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)
531
633
 
532
- return mismatch("s1", s1, Hash, mth, DBG, false) unless s1.is_a?(Hash)
533
- return mismatch("s2", s2, Hash, mth, DBG, false) unless s2.is_a?(Hash)
534
- return hashkey("s1", s1, :angle, mth, DBG, false) unless s1.key?(:angle)
535
- return hashkey("s2", s2, :angle, mth, DBG, false) unless s2.key?(:angle)
536
- return hashkey("s1", s1, :normal, mth, DBG, false) unless s1.key?(:normal)
537
- return hashkey("s2", s2, :normal, mth, DBG, false) unless s2.key?(:normal)
538
- return hashkey("s1", s1, :polar, mth, DBG, false) unless s1.key?(:polar)
539
- return hashkey("s2", s2, :polar, mth, DBG, false) unless s2.key?(:polar)
540
634
  valid1 = s1[:angle].is_a?(Numeric)
541
635
  valid2 = s2[:angle].is_a?(Numeric)
542
- return mismatch("s1 angle", s1[:angle], Numeric, DBG, false) unless valid1
543
- 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
544
638
 
545
639
  angle = 0
546
640
  angle = s2[:angle] - s1[:angle] if s2[:angle] > s1[:angle]
547
641
  angle = s1[:angle] - s2[:angle] if s1[:angle] > s2[:angle]
548
-
549
642
  return false if angle < TOL
550
643
  return false unless (2 * Math::PI - angle).abs > TOL
551
644
  return false if angle > 3 * Math::PI / 4 && angle < 5 * Math::PI / 4
@@ -558,33 +651,37 @@ module TBD
558
651
  end
559
652
 
560
653
  ##
561
- # 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.
562
655
  #
563
- # @param s1 [Surface] first TBD surface
564
- # @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)
565
661
  #
566
662
  # @return [Bool] true if angle between surfaces is convex
567
- # @return [Bool] false if invalid input
663
+ # @return [false] if invalid input (see logs)
568
664
  def convex?(s1 = nil, s2 = nil)
569
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)
570
676
 
571
- return mismatch("s1", s1, Hash, mth, DBG, false) unless s1.is_a?(Hash)
572
- return mismatch("s2", s2, Hash, mth, DBG, false) unless s2.is_a?(Hash)
573
- return hashkey("s1", s1, :angle, mth, DBG, false) unless s1.key?(:angle)
574
- return hashkey("s2", s2, :angle, mth, DBG, false) unless s2.key?(:angle)
575
- return hashkey("s1", s1, :normal, mth, DBG, false) unless s1.key?(:normal)
576
- return hashkey("s2", s2, :normal, mth, DBG, false) unless s2.key?(:normal)
577
- return hashkey("s1", s1, :polar, mth, DBG, false) unless s1.key?(:polar)
578
- return hashkey("s2", s2, :polar, mth, DBG, false) unless s2.key?(:polar)
579
677
  valid1 = s1[:angle].is_a?(Numeric)
580
678
  valid2 = s2[:angle].is_a?(Numeric)
581
- return mismatch("s1 angle", s1[:angle], Numeric, DBG, false) unless valid1
582
- 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
583
681
 
584
682
  angle = 0
585
683
  angle = s2[:angle] - s1[:angle] if s2[:angle] > s1[:angle]
586
684
  angle = s1[:angle] - s2[:angle] if s1[:angle] > s2[:angle]
587
-
588
685
  return false if angle < TOL
589
686
  return false unless (2 * Math::PI - angle).abs > TOL
590
687
  return false if angle > 3 * Math::PI / 4 && angle < 5 * Math::PI / 4
@@ -597,7 +694,64 @@ module TBD
597
694
  end
598
695
 
599
696
  ##
600
- # 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'
601
755
  # boundary conditions.
602
756
  #
603
757
  # @param model [OpenStudio::Model::Model] a model
@@ -606,19 +760,28 @@ module TBD
606
760
  # @param edges [Hash] TBD edges (many linking floors & walls
607
761
  #
608
762
  # @return [Bool] true if Kiva foundations are successfully generated
609
- # @return [Bool] false if invalid input
763
+ # @return [false] if invalid input (see logs)
610
764
  def kiva(model = nil, walls = {}, floors = {}, edges = {})
611
765
  mth = "TBD::#{__callee__}"
612
766
  cl1 = OpenStudio::Model::Model
613
767
  cl2 = Hash
614
768
  a = false
615
-
616
- return mismatch("model", model, cl1, mth, DBG, a) unless model.is_a?(cl1)
617
- 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)
618
771
  return mismatch("floors", floors, cl2, mth, DBG, a) unless floors.is_a?(cl2)
619
- return mismatch("edges", edges, cl2, mth, DBG, a) unless edges.is_a?(cl2)
620
-
621
- 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
622
785
 
623
786
  # Pre-validate foundation-facing constructions.
624
787
  model.getSurfaces.each do |s|
@@ -627,20 +790,20 @@ module TBD
627
790
  next unless s.outsideBoundaryCondition.downcase == "foundation"
628
791
 
629
792
  if construction.empty?
630
- log(ERR, "Invalid construction for KIVA (see #{id})")
631
- kva = false if kva
793
+ log(ERR, "Invalid construction for #{id} (#{mth})")
794
+ kva = false
632
795
  else
633
796
  construction = construction.get.to_LayeredConstruction
634
797
 
635
798
  if construction.empty?
636
- log(ERR, "KIVA requires layered constructions (see #{id})")
637
- kva = false if kva
799
+ log(ERR, "Invalid layered constructions for #{id} (#{mth})")
800
+ kva = false
638
801
  else
639
802
  construction = construction.get
640
803
 
641
804
  unless standardOpaqueLayers?(construction)
642
- log(ERR, "KIVA requires standard materials (see #{id})")
643
- kva = false if kva
805
+ log(ERR, "Non-standard materials for #{id} (#{mth})")
806
+ kva = false
644
807
  end
645
808
  end
646
809
  end
@@ -649,12 +812,12 @@ module TBD
649
812
  return a unless kva
650
813
 
651
814
  # Strictly relying on Kiva's total exposed perimeter approach.
652
- arg = "TotalExposedPerimeter"
815
+ arg = "TotalExposedPerimeter"
653
816
  kiva = true
654
817
  # The following is loosely adapted from:
655
818
  #
656
- # github.com/NREL/OpenStudio-resources/blob/develop/model/simulationtests/
657
- # foundation_kiva.rb ... thanks.
819
+ # github.com/NREL/OpenStudio-resources/blob/develop/model/
820
+ # simulationtests/foundation_kiva.rb ... thanks.
658
821
  #
659
822
  # Access to KIVA settings. This is usually not required (the default KIVA
660
823
  # settings are fine), but its explicit inclusion in the model does offer
@@ -662,6 +825,7 @@ module TBD
662
825
  # required. Initial tests show slight differences in simulation results
663
826
  # w/w/o explcit inclusion of the KIVA settings template in the model.
664
827
  settings = model.getFoundationKivaSettings
828
+
665
829
  k = settings.soilConductivity
666
830
  settings.setSoilConductivity(k)
667
831
 
@@ -671,36 +835,43 @@ module TBD
671
835
  next unless floors.key?(id)
672
836
  next unless floors[id][:boundary].downcase == "foundation"
673
837
  next if floors[id].key?(:kiva)
674
- floors[id][:kiva ] = :slab # initially slabs-on-grade
675
- floors[id][:exposed] = 0.0 # slab-on-grade or basement walkout perimeter
676
838
 
677
- 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|
678
844
  next if i == id
679
845
  next unless walls.key?(i)
680
846
  next unless walls[i][:boundary].downcase == "foundation"
681
847
  next if walls[i].key?(:kiva)
848
+
682
849
  floors[id][:kiva] = :basement
683
850
  walls[i ][:kiva] = id
684
851
  end
685
852
 
686
- edge[:surfaces].keys.each do |i| # loop around current edge
853
+ # Loop around current edge.
854
+ edge[:surfaces].keys.each do |i|
687
855
  next if i == id
688
856
  next unless walls.key?(i)
689
857
  next unless walls[i][:boundary].downcase == "outdoors"
858
+
690
859
  floors[id][:exposed] += edge[:length]
691
860
  end
692
861
 
693
- edges.each do |code2, e| # loop around other floor edges
694
- 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
695
865
 
696
866
  e[:surfaces].keys.each do |i|
697
- next unless i == id # good - same floor
867
+ next unless i == id # good - same floor
698
868
 
699
869
  e[:surfaces].keys.each do |ii|
700
870
  next if i == ii
701
871
  next unless walls.key?(ii)
702
872
  next unless walls[ii][:boundary].downcase == "foundation"
703
873
  next if walls[ii].key?(:kiva)
874
+
704
875
  floors[id][:kiva] = :basement
705
876
  walls[ii ][:kiva] = id
706
877
  end
@@ -709,6 +880,7 @@ module TBD
709
880
  next if i == ii
710
881
  next unless walls.key?(ii)
711
882
  next unless walls[ii][:boundary].downcase == "outdoors"
883
+
712
884
  floors[id][:exposed] += e[:length]
713
885
  end
714
886
  end
@@ -716,32 +888,34 @@ module TBD
716
888
 
717
889
  foundation = OpenStudio::Model::FoundationKiva.new(model)
718
890
  foundation.setName("KIVA Foundation Floor #{id}")
719
-
720
891
  floor = model.getSurfaceByName(id)
721
892
  kiva = false if floor.empty?
722
893
  next if floor.empty?
894
+
723
895
  floor = floor.get
724
896
  construction = floor.construction
725
897
  kiva = false if construction.empty?
726
898
  next if construction.empty?
899
+
727
900
  construction = construction.get
728
901
  floor.setAdjacentFoundation(foundation)
729
902
  floor.setConstruction(construction)
730
-
731
903
  ep = floors[id][:exposed]
732
904
  per = floor.createSurfacePropertyExposedFoundationPerimeter(arg, ep)
733
- kiva = false if per.empty?
734
- next if per.empty?
735
- per = per.get
736
- perimeter = per.totalExposedPerimeter
737
- kiva = false if perimeter.empty?
738
- next if perimeter.empty?
739
- 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
740
914
 
741
915
  if ep < 0.001
742
916
  ok = per.setTotalExposedPerimeter(0.000)
743
917
  ok = per.setTotalExposedPerimeter(0.001) unless ok
744
- kiva = false unless ok
918
+ kiva = false unless ok
745
919
  elsif (perimeter - ep).abs < TOL
746
920
  xps25 = model.getStandardOpaqueMaterialByName("XPS 25mm")
747
921
 
@@ -769,16 +943,20 @@ module TBD
769
943
 
770
944
  walls.each do |i, wall|
771
945
  next unless wall.key?(:kiva)
772
- id = walls[i][:kiva]
946
+
947
+ id = walls[i][:kiva]
773
948
  next unless floors.key?(id)
774
949
  next unless floors[id].key?(:foundation)
775
- mur = model.getSurfaceByName(i) # locate OpenStudio wall
950
+
951
+ mur = model.getSurfaceByName(i) # locate OpenStudio wall
776
952
  kiva = false if mur.empty?
777
953
  next if mur.empty?
954
+
778
955
  mur = mur.get
779
956
  construction = mur.construction
780
957
  kiva = false if construction.empty?
781
958
  next if construction.empty?
959
+
782
960
  construction = construction.get
783
961
  mur.setAdjacentFoundation(floors[id][:foundation])
784
962
  mur.setConstruction(construction)