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