tbd 3.2.3 → 3.4.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.
@@ -1,6 +1,6 @@
1
1
  # MIT License
2
2
  #
3
- # Copyright (c) 2020-2023 Denis Bourgeois & Dan Macumber
3
+ # Copyright (c) 2020-2024 Denis Bourgeois & Dan Macumber
4
4
  #
5
5
  # Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  # of this software and associated documentation files (the "Software"), to deal
@@ -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,141 @@ 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
350
+ surf[:occupied ] = space.partofTotalFloorArea
311
351
  surf[:boundary ] = facing
312
352
  surf[:ground ] = surface.isGroundSurface
313
353
  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?
354
+ surf[:type ] = :ceiling if type.include?("ceiling")
355
+ surf[:type ] = :wall if type.include?("wall" )
356
+ surf[:stype ] = stype.get unless stype.empty?
357
+ surf[:story ] = story.get unless story.empty?
318
358
  surf[:n ] = n
319
359
  surf[:gross ] = surface.grossArea
320
360
  surf[:filmRSI ] = surface.filmResistance
361
+ surf[:spandrel ] = spandrel?(surface)
321
362
 
322
363
  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.
364
+ next if poly(s).empty?
365
+
366
+ id = s.nameString
367
+ typ = surface.surfaceType.downcase
368
+
369
+ unless (3..4).cover?(s.vertices.size)
370
+ log(ERR, "Skipping '#{id}': vertex # 3 or 4 (#{mth})")
371
+ next
372
+ end
373
+
374
+ vec = s.vertices
375
+ area = s.grossArea
376
+ mult = s.multiplier
377
+
378
+ # An OpenStudio subsurface has a "type" (string), either defaulted during
379
+ # initialization or explicitely set by the user (from a built-in list):
380
+ #
381
+ # OpenStudio::Model::SubSurface.validSubSurfaceTypeValues
382
+ # - "FixedWindow"
383
+ # - "OperableWindow"
384
+ # - "Door"
385
+ # - "GlassDoor"
386
+ # - "OverheadDoor"
387
+ # - "Skylight"
388
+ # - "TubularDaylightDome"
389
+ # - "TubularDaylightDiffuser"
390
+ typ = s.subSurfaceType.downcase
391
+
392
+ # An OpenStudio default subsurface construction set can hold unique
393
+ # constructions assigned for each of these admissible types. In addition,
394
+ # type assignment determines whether frame/divider attributes can be
395
+ # linked to a subsurface (this shortlist has evolved between OpenStudio
396
+ # releases). Type assignment is relied upon when calculating (admissible)
397
+ # fenestration areas. TBD also relies on OpenStudio subsurface type
398
+ # assignment, with resulting TBD tags being a bit more concise, e.g.:
399
+ #
400
+ # - :window includes "FixedWindow" and "OperableWindow"
401
+ # - :door includes "Door", "OverheadWindow" and "GlassDoor"
402
+ # ... a (roof) access roof hatch should be assigned as a "Door"
403
+ # - :skylight includes "Skylight", "TubularDaylightDome", etc.
404
+ #
405
+ type = :skylight
406
+ type = :window if typ.include?("window") # operable or not
407
+ type = :door if typ.include?("door") # fenestrated or not
408
+
409
+ # In fact, ANY subsurface other than :window or :door is tagged as
410
+ # :skylight, e.g. a glazed floor opening (CN, Calgary, Tokyo towers). This
411
+ # happens to reflect OpenStudio default initialization behaviour. For
412
+ # instance, a subsurface added to an exposed (horizontal) floor in
413
+ # OpenStudio is automatically assigned a "Skylight" type. This is similar
414
+ # to the auto-assignment of (opaque) walls, roof/ceilings and floors
415
+ # (based on surface tilt) in OpenStudio.
416
+ #
417
+ # When it comes to major thermal bridging, ASHRAE 90.1 (2022) makes a
418
+ # clear distinction between "vertical fenestration" (a defined term) and
419
+ # all other subsurfaces. "Vertical fenestration" would include both
420
+ # instances of "Window", as well as "GlassDoor". It would exclude however
421
+ # a non-fenestrated "door" (another defined term), like "Door" &
422
+ # "OverheadDoor", as well as skylights. TBD tracks relevant subsurface
423
+ # attributes via a handful of boolean variables:
424
+ glazed = type == :door && typ.include?("glass") # fenestrated door
425
+ tubular = typ.include?("tubular") # dome or diffuser
426
+ domed = typ.include?("dome") # (tubular) dome
427
+ unhinged = false # (tubular) dome
428
+
429
+ # It would be tempting (and simple) to have TBD further validate whether a
430
+ # "GlassDoor" is actually integrated within a (vertical) wall. The
431
+ # automated type assignment in OpenStudio is very simple and reliable (as
432
+ # discussed in the preceding paragraphs), yet users can nonetheless reset
433
+ # this explicitly. For instance, while a vertical surface may indeed be
434
+ # auto-assigned "Wall", a modeller can just as easily reset its type as
435
+ # "Floor". Although OpenStudio supports 90.1 rules by default, it's not
436
+ # enforced. TBD retains the same approach: for whatever osbcur reason a
437
+ # modeller may decide (and hopefully the "authority having jurisdiction"
438
+ # may authorize) to reset a wall as a "Floor" or a roof skylight as a
439
+ # "GlassDoor", TBD maintains the same OpenStudio policy. Either OpenStudio
440
+ # (and consequently EnergyPlus) sub/surface type assignment is reliable,
441
+ # or it is not.
442
+
443
+ # Determine if TDD dome subsurface is 'unhinged', i.e. unconnected to its
444
+ # base surface (not same 3D plane).
342
445
  if domed
343
- unhinged = true unless s.plane.equal(surface.plane)
344
- n = s.outwardNormal if unhinged
446
+ unhinged = true unless s.plane.equal(surface.plane)
447
+ n = s.outwardNormal if unhinged
448
+ end
449
+
450
+ if area < TOL
451
+ log(ERR, "Skipping '#{id}': gross area ~zero (#{mth})")
452
+ next
345
453
  end
346
454
 
347
- log(ERR, "Skipping '#{id}': gross area ~zero (#{mth})") if area < TOL
348
- next if area < TOL
349
455
  c = s.construction
350
- log(ERR, "Skipping '#{id}': missing construction (#{mth})") if c.empty?
351
- next if c.empty?
456
+
457
+ if c.empty?
458
+ log(ERR, "Skipping '#{id}': missing construction (#{mth})")
459
+ next
460
+ end
461
+
352
462
  c = c.get.to_LayeredConstruction
353
- log(WRN, "Skipping '#{id}': subs limited to #{cl3} (#{mth})") if c.empty?
354
- next if c.empty?
463
+
464
+ if c.empty?
465
+ log(WRN, "Skipping '#{id}': subs limited to #{cl2} (#{mth})")
466
+ next
467
+ end
468
+
355
469
  c = c.get
356
470
 
357
471
  # A subsurface may have an overall U-factor set by the user - a less
@@ -364,7 +478,7 @@ module TBD
364
478
  # window-calculation-module.html#simple-window-model
365
479
  #
366
480
  # TBD will instead rely on Tubular Daylighting Device (TDD) effective
367
- # dome-to-diffuser RSi values (if valid).
481
+ # dome-to-diffuser RSi-factors (if valid).
368
482
  #
369
483
  # https://bigladdersoftware.com/epx/docs/9-6/engineering-reference/
370
484
  # daylighting-devices.html#tubular-daylighting-devices
@@ -379,12 +493,12 @@ module TBD
379
493
  # resistances). This is the least reliable option, especially if
380
494
  # subsurfaces have Frame & Divider objects, or irregular geometry.
381
495
  u = s.uFactor
382
- u = u.get unless u.empty?
496
+ u = u.get unless u.empty?
383
497
 
384
- if tubular & s.respond_to?(:daylightingDeviceTubular) # OSM > v3.3.0
498
+ if tubular & s.respond_to?(:daylightingDeviceTubular) # OSM > v3.3.0
385
499
  unless s.daylightingDeviceTubular.empty?
386
500
  r = s.daylightingDeviceTubular.get.effectiveThermalResistance
387
- u = 1 / r if r > TOL
501
+ u = 1 / r if r > TOL
388
502
  end
389
503
  end
390
504
 
@@ -394,8 +508,12 @@ module TBD
394
508
 
395
509
  unless u.is_a?(Numeric)
396
510
  r = rsi(c, surface.filmResistance)
397
- log(ERR, "Skipping '#{id}': U-factor unavailable (#{mth})") if r < TOL
398
- next if r < TOL
511
+
512
+ if r < TOL
513
+ log(ERR, "Skipping '#{id}': U-factor unavailable (#{mth})")
514
+ next
515
+ end
516
+
399
517
  u = 1 / r
400
518
  end
401
519
 
@@ -407,8 +525,12 @@ module TBD
407
525
  width = s.windowPropertyFrameAndDivider.get.frameWidth
408
526
  vec = offset(vec, width, 300)
409
527
  area = OpenStudio.getArea(vec)
410
- log(ERR, "Skipping '#{id}': invalid offset (#{mth})") if area.empty?
411
- next if area.empty?
528
+
529
+ if area.empty?
530
+ log(ERR, "Skipping '#{id}': invalid offset (#{mth})")
531
+ next
532
+ end
533
+
412
534
  area = area.get
413
535
  end
414
536
 
@@ -423,7 +545,7 @@ module TBD
423
545
  unhinged: unhinged }
424
546
 
425
547
  sub[:glazed] = true if glazed
426
- subs[id] = sub
548
+ subs[id ] = sub
427
549
  end
428
550
 
429
551
  valid = true
@@ -433,24 +555,27 @@ module TBD
433
555
  # also inadvertently catch pre-existing (yet nonetheless invalid)
434
556
  # OpenStudio inputs (without Frame & Dividers).
435
557
  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
558
+ break unless fd
559
+ break unless valid
560
+
561
+ valid = fits?(sub[:points], surface.vertices)
562
+ log(ERR, "Skipping '#{id}': can't fit in '#{nom}' (#{mth})") unless valid
440
563
 
441
564
  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
565
+ break unless valid
566
+ next if i == id
567
+
568
+ if overlaps?(sb[:points], sub[:points])
569
+ log(ERR, "Skipping '#{id}': overlaps sibling '#{i}' (#{mth})")
570
+ valid = false
571
+ end
447
572
  end
448
573
  end
449
574
 
450
575
  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
576
+ subs.values.each { |sub| sub[:gross ] = sub[:area ] } if valid
577
+ subs.values.each { |sub| sub[:points] = sub[:v ] } unless valid
578
+ subs.values.each { |sub| sub[:area ] = sub[:gross] } unless valid
454
579
  end
455
580
 
456
581
  subarea = 0
@@ -461,21 +586,22 @@ module TBD
461
586
 
462
587
  # Tranform final Point 3D sets, and store.
463
588
  pts = (t * surface.vertices).map { |v| Topolys::Point3D.new(v.x, v.y, v.z) }
589
+
464
590
  surf[:points] = pts
465
591
  surf[:minz ] = ( pts.map { |pt| pt.z } ).min
466
592
 
467
593
  subs.each do |id, sub|
468
594
  pts = (t * sub[:points]).map { |v| Topolys::Point3D.new(v.x, v.y, v.z) }
595
+
469
596
  sub[:points] = pts
470
597
  sub[:minz ] = ( pts.map { |p| p.z } ).min
471
598
 
472
599
  [:windows, :doors, :skylights].each do |types|
473
600
  type = types.slice(0..-2).to_sym
601
+ next unless sub[:type] == type
474
602
 
475
- if sub[:type] == type
476
- surf[types] = {} unless surf.key?(types)
477
- surf[types][id] = sub
478
- end
603
+ surf[types] = {} unless surf.key?(types)
604
+ surf[types][id] = sub
479
605
  end
480
606
  end
481
607
 
@@ -483,33 +609,37 @@ module TBD
483
609
  end
484
610
 
485
611
  ##
486
- # Validate whether edge surfaces form a concave angle, as seen from outside.
612
+ # Validates whether edge surfaces form a concave angle, as seen from outside.
487
613
  #
488
- # @param s1 [Surface] first TBD surface
489
- # @param s2 [Surface] second TBD surface
614
+ # @param [Hash] s1 first TBD surface
615
+ # @param [Hash] s2 second TBD surface
616
+ # @option s1 [Topolys::Vector3D] :normal surface normal vector
617
+ # @option s1 [Topolys::Vector3D] :polar vector around edge
618
+ # @option s1 [Numeric] :angle polar angle vs reference (e.g. North, Zenith)
490
619
  #
491
620
  # @return [Bool] true if angle between surfaces is concave
492
- # @return [Bool] false if invalid input
621
+ # @return [false] if invalid input (see logs)
493
622
  def concave?(s1 = nil, s2 = nil)
494
623
  mth = "TBD::#{__callee__}"
624
+ return mismatch("s1", s1, Hash, mth, DBG, false) unless s1.is_a?(Hash)
625
+ return mismatch("s2", s2, Hash, mth, DBG, false) unless s2.is_a?(Hash)
626
+ return false if s1 == s2
627
+
628
+ return hashkey("s1", s1, :angle, mth, DBG, false) unless s1.key?(:angle)
629
+ return hashkey("s2", s2, :angle, mth, DBG, false) unless s2.key?(:angle)
630
+ return hashkey("s1", s1, :normal, mth, DBG, false) unless s1.key?(:normal)
631
+ return hashkey("s2", s2, :normal, mth, DBG, false) unless s2.key?(:normal)
632
+ return hashkey("s1", s1, :polar, mth, DBG, false) unless s1.key?(:polar)
633
+ return hashkey("s2", s2, :polar, mth, DBG, false) unless s2.key?(:polar)
495
634
 
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
635
  valid1 = s1[:angle].is_a?(Numeric)
505
636
  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
637
+ return mismatch("s1 angle", s1[:angle], Numeric, DBG, false) unless valid1
638
+ return mismatch("s1 angle", s1[:angle], Numeric, DBG, false) unless valid2
508
639
 
509
640
  angle = 0
510
641
  angle = s2[:angle] - s1[:angle] if s2[:angle] > s1[:angle]
511
642
  angle = s1[:angle] - s2[:angle] if s1[:angle] > s2[:angle]
512
-
513
643
  return false if angle < TOL
514
644
  return false unless (2 * Math::PI - angle).abs > TOL
515
645
  return false if angle > 3 * Math::PI / 4 && angle < 5 * Math::PI / 4
@@ -522,33 +652,37 @@ module TBD
522
652
  end
523
653
 
524
654
  ##
525
- # Validate whether edge surfaces form a convex angle, as seen from outside.
655
+ # Validates whether edge surfaces form a convex angle, as seen from outside.
526
656
  #
527
- # @param s1 [Surface] first TBD surface
528
- # @param s2 [Surface] second TBD surface
657
+ # @param [Hash] s1 first TBD surface
658
+ # @param [Hash] s2 second TBD surface
659
+ # @option s1 [Topolys::Vector3D] :normal surface normal vector
660
+ # @option s1 [Topolys::Vector3D] :polar vector around edge
661
+ # @option s1 [Numeric] :angle polar angle vs reference (e.g. North, Zenith)
529
662
  #
530
663
  # @return [Bool] true if angle between surfaces is convex
531
- # @return [Bool] false if invalid input
664
+ # @return [false] if invalid input (see logs)
532
665
  def convex?(s1 = nil, s2 = nil)
533
666
  mth = "TBD::#{__callee__}"
667
+ return mismatch("s1", s1, Hash, mth, DBG, false) unless s1.is_a?(Hash)
668
+ return mismatch("s2", s2, Hash, mth, DBG, false) unless s2.is_a?(Hash)
669
+ return false if s1 == s2
670
+
671
+ return hashkey("s1", s1, :angle, mth, DBG, false) unless s1.key?(:angle)
672
+ return hashkey("s2", s2, :angle, mth, DBG, false) unless s2.key?(:angle)
673
+ return hashkey("s1", s1, :normal, mth, DBG, false) unless s1.key?(:normal)
674
+ return hashkey("s2", s2, :normal, mth, DBG, false) unless s2.key?(:normal)
675
+ return hashkey("s1", s1, :polar, mth, DBG, false) unless s1.key?(:polar)
676
+ return hashkey("s2", s2, :polar, mth, DBG, false) unless s2.key?(:polar)
534
677
 
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
678
  valid1 = s1[:angle].is_a?(Numeric)
544
679
  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
680
+ return mismatch("s1 angle", s1[:angle], Numeric, DBG, false) unless valid1
681
+ return mismatch("s1 angle", s1[:angle], Numeric, DBG, false) unless valid2
547
682
 
548
683
  angle = 0
549
684
  angle = s2[:angle] - s1[:angle] if s2[:angle] > s1[:angle]
550
685
  angle = s1[:angle] - s2[:angle] if s1[:angle] > s2[:angle]
551
-
552
686
  return false if angle < TOL
553
687
  return false unless (2 * Math::PI - angle).abs > TOL
554
688
  return false if angle > 3 * Math::PI / 4 && angle < 5 * Math::PI / 4
@@ -561,7 +695,64 @@ module TBD
561
695
  end
562
696
 
563
697
  ##
564
- # Generate Kiva settings and objects if model surfaces have 'foundation'
698
+ # Purge existing KIVA-related objects in an OpenStudio model. Resets ground-
699
+ # facing surface outside boundary condition to "Ground" or "Foundation".
700
+ #
701
+ # @param model [OpenStudio::Model::Model] a model
702
+ # @param boundary ["Ground", "Foundation"] new outside boundary condition
703
+ #
704
+ # @return [Bool] true if model is free of KIVA-related objects
705
+ # @return [false] if invalid input (see logs)
706
+ def resetKIVA(model = nil, boundary = "Foundation")
707
+ mth = "TBD::#{__callee__}"
708
+ cl = OpenStudio::Model::Model
709
+ ck1 = model.is_a?(cl)
710
+ ck2 = boundary.respond_to?(:to_s)
711
+ kva = false
712
+ b = ["Ground", "Foundation"]
713
+ return mismatch("model" , model , cl , mth, DBG, kva) unless ck1
714
+ return mismatch("boundary", boundary, String, mth, DBG, kva) unless ck2
715
+
716
+ boundary.capitalize!
717
+ return invalid("boundary", mth, 2, DBG, kva) unless b.include?(boundary)
718
+
719
+ # Reset surface KIVA-related objects.
720
+ model.getSurfaces.each do |surface|
721
+ kva = true unless surface.adjacentFoundation.empty?
722
+ kva = true unless surface.surfacePropertyExposedFoundationPerimeter.empty?
723
+ surface.resetAdjacentFoundation
724
+ surface.resetSurfacePropertyExposedFoundationPerimeter
725
+ next unless surface.isGroundSurface
726
+ next if surface.outsideBoundaryCondition.capitalize == boundary
727
+
728
+ lc = surface.construction.empty? ? nil : surface.construction.get
729
+ surface.setOutsideBoundaryCondition(boundary)
730
+ next if boundary == "Ground"
731
+ next if lc.nil?
732
+
733
+ surface.setConstruction(lc) if surface.construction.empty?
734
+ end
735
+
736
+ perimeters = model.getSurfacePropertyExposedFoundationPerimeters
737
+
738
+ kva = true unless perimeters.empty?
739
+
740
+ # Remove KIVA exposed perimeters.
741
+ perimeters.each { |perimeter| perimeter.remove }
742
+
743
+ # Remove KIVA custom blocks, & foundations.
744
+ model.getFoundationKivas.each do |kiva|
745
+ kiva.removeAllCustomBlocks
746
+ kiva.remove
747
+ end
748
+
749
+ log(INF, "Purged KIVA objects from model (#{mth})") if kva
750
+
751
+ true
752
+ end
753
+
754
+ ##
755
+ # Generates Kiva settings and objects if model surfaces have 'foundation'
565
756
  # boundary conditions.
566
757
  #
567
758
  # @param model [OpenStudio::Model::Model] a model
@@ -570,19 +761,28 @@ module TBD
570
761
  # @param edges [Hash] TBD edges (many linking floors & walls
571
762
  #
572
763
  # @return [Bool] true if Kiva foundations are successfully generated
573
- # @return [Bool] false if invalid input
764
+ # @return [false] if invalid input (see logs)
574
765
  def kiva(model = nil, walls = {}, floors = {}, edges = {})
575
766
  mth = "TBD::#{__callee__}"
576
767
  cl1 = OpenStudio::Model::Model
577
768
  cl2 = Hash
578
769
  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)
770
+ return mismatch("model" , model, cl1, mth, DBG, a) unless model.is_a?(cl1)
771
+ return mismatch("walls" , walls, cl2, mth, DBG, a) unless walls.is_a?(cl2)
582
772
  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
773
+ return mismatch("edges" , edges, cl2, mth, DBG, a) unless edges.is_a?(cl2)
774
+
775
+ # Check for existing KIVA objects.
776
+ kva = false
777
+ kva = true unless model.getSurfacePropertyExposedFoundationPerimeters.empty?
778
+ kva = true unless model.getFoundationKivas.empty?
779
+
780
+ if kva
781
+ log(ERR, "Exiting - KIVA objects in model (#{mth})")
782
+ return a
783
+ else
784
+ kva = true
785
+ end
586
786
 
587
787
  # Pre-validate foundation-facing constructions.
588
788
  model.getSurfaces.each do |s|
@@ -591,20 +791,20 @@ module TBD
591
791
  next unless s.outsideBoundaryCondition.downcase == "foundation"
592
792
 
593
793
  if construction.empty?
594
- log(ERR, "Invalid construction for KIVA (see #{id})")
595
- kva = false if kva
794
+ log(ERR, "Invalid construction for #{id} (#{mth})")
795
+ kva = false
596
796
  else
597
797
  construction = construction.get.to_LayeredConstruction
598
798
 
599
799
  if construction.empty?
600
- log(ERR, "KIVA requires layered constructions (see #{id})")
601
- kva = false if kva
800
+ log(ERR, "Invalid layered constructions for #{id} (#{mth})")
801
+ kva = false
602
802
  else
603
803
  construction = construction.get
604
804
 
605
805
  unless standardOpaqueLayers?(construction)
606
- log(ERR, "KIVA requires standard materials (see #{id})")
607
- kva = false if kva
806
+ log(ERR, "Non-standard materials for #{id} (#{mth})")
807
+ kva = false
608
808
  end
609
809
  end
610
810
  end
@@ -613,12 +813,12 @@ module TBD
613
813
  return a unless kva
614
814
 
615
815
  # Strictly relying on Kiva's total exposed perimeter approach.
616
- arg = "TotalExposedPerimeter"
816
+ arg = "TotalExposedPerimeter"
617
817
  kiva = true
618
818
  # The following is loosely adapted from:
619
819
  #
620
- # github.com/NREL/OpenStudio-resources/blob/develop/model/simulationtests/
621
- # foundation_kiva.rb ... thanks.
820
+ # github.com/NREL/OpenStudio-resources/blob/develop/model/
821
+ # simulationtests/foundation_kiva.rb ... thanks.
622
822
  #
623
823
  # Access to KIVA settings. This is usually not required (the default KIVA
624
824
  # settings are fine), but its explicit inclusion in the model does offer
@@ -626,6 +826,7 @@ module TBD
626
826
  # required. Initial tests show slight differences in simulation results
627
827
  # w/w/o explcit inclusion of the KIVA settings template in the model.
628
828
  settings = model.getFoundationKivaSettings
829
+
629
830
  k = settings.soilConductivity
630
831
  settings.setSoilConductivity(k)
631
832
 
@@ -635,36 +836,43 @@ module TBD
635
836
  next unless floors.key?(id)
636
837
  next unless floors[id][:boundary].downcase == "foundation"
637
838
  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
839
 
641
- edge[:surfaces].keys.each do |i| # loop around current edge
840
+ floors[id][:kiva ] = :slab # initially slabs-on-grade
841
+ floors[id][:exposed] = 0.0 # slab-on-grade or walkout perimeter
842
+
843
+ # Loop around current edge.
844
+ edge[:surfaces].keys.each do |i|
642
845
  next if i == id
643
846
  next unless walls.key?(i)
644
847
  next unless walls[i][:boundary].downcase == "foundation"
645
848
  next if walls[i].key?(:kiva)
849
+
646
850
  floors[id][:kiva] = :basement
647
851
  walls[i ][:kiva] = id
648
852
  end
649
853
 
650
- edge[:surfaces].keys.each do |i| # loop around current edge
854
+ # Loop around current edge.
855
+ edge[:surfaces].keys.each do |i|
651
856
  next if i == id
652
857
  next unless walls.key?(i)
653
858
  next unless walls[i][:boundary].downcase == "outdoors"
859
+
654
860
  floors[id][:exposed] += edge[:length]
655
861
  end
656
862
 
657
- edges.each do |code2, e| # loop around other floor edges
658
- next if code1 == code2 # skip - same edge
863
+ # Loop around other floor edges.
864
+ edges.each do |code2, e|
865
+ next if code1 == code2 # skip - same edge
659
866
 
660
867
  e[:surfaces].keys.each do |i|
661
- next unless i == id # good - same floor
868
+ next unless i == id # good - same floor
662
869
 
663
870
  e[:surfaces].keys.each do |ii|
664
871
  next if i == ii
665
872
  next unless walls.key?(ii)
666
873
  next unless walls[ii][:boundary].downcase == "foundation"
667
874
  next if walls[ii].key?(:kiva)
875
+
668
876
  floors[id][:kiva] = :basement
669
877
  walls[ii ][:kiva] = id
670
878
  end
@@ -673,6 +881,7 @@ module TBD
673
881
  next if i == ii
674
882
  next unless walls.key?(ii)
675
883
  next unless walls[ii][:boundary].downcase == "outdoors"
884
+
676
885
  floors[id][:exposed] += e[:length]
677
886
  end
678
887
  end
@@ -680,32 +889,34 @@ module TBD
680
889
 
681
890
  foundation = OpenStudio::Model::FoundationKiva.new(model)
682
891
  foundation.setName("KIVA Foundation Floor #{id}")
683
-
684
892
  floor = model.getSurfaceByName(id)
685
893
  kiva = false if floor.empty?
686
894
  next if floor.empty?
895
+
687
896
  floor = floor.get
688
897
  construction = floor.construction
689
898
  kiva = false if construction.empty?
690
899
  next if construction.empty?
900
+
691
901
  construction = construction.get
692
902
  floor.setAdjacentFoundation(foundation)
693
903
  floor.setConstruction(construction)
694
-
695
904
  ep = floors[id][:exposed]
696
905
  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
906
+ kiva = false if per.empty?
907
+ next if per.empty?
908
+
909
+ per = per.get
910
+ perimeter = per.totalExposedPerimeter
911
+ kiva = false if perimeter.empty?
912
+ next if perimeter.empty?
913
+
914
+ perimeter = perimeter.get
704
915
 
705
916
  if ep < 0.001
706
917
  ok = per.setTotalExposedPerimeter(0.000)
707
918
  ok = per.setTotalExposedPerimeter(0.001) unless ok
708
- kiva = false unless ok
919
+ kiva = false unless ok
709
920
  elsif (perimeter - ep).abs < TOL
710
921
  xps25 = model.getStandardOpaqueMaterialByName("XPS 25mm")
711
922
 
@@ -733,16 +944,20 @@ module TBD
733
944
 
734
945
  walls.each do |i, wall|
735
946
  next unless wall.key?(:kiva)
736
- id = walls[i][:kiva]
947
+
948
+ id = walls[i][:kiva]
737
949
  next unless floors.key?(id)
738
950
  next unless floors[id].key?(:foundation)
739
- mur = model.getSurfaceByName(i) # locate OpenStudio wall
951
+
952
+ mur = model.getSurfaceByName(i) # locate OpenStudio wall
740
953
  kiva = false if mur.empty?
741
954
  next if mur.empty?
955
+
742
956
  mur = mur.get
743
957
  construction = mur.construction
744
958
  kiva = false if construction.empty?
745
959
  next if construction.empty?
960
+
746
961
  construction = construction.get
747
962
  mur.setAdjacentFoundation(floors[id][:foundation])
748
963
  mur.setConstruction(construction)