tbd 3.2.3 → 3.4.0

Sign up to get free protection for your applications and to get access to all the features.
data/lib/tbd/geo.rb CHANGED
@@ -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)