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.
- checksums.yaml +4 -4
- data/.github/workflows/pull_request.yml +32 -0
- data/.yardopts +14 -0
- data/LICENSE.md +1 -1
- 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/json/tbd_z5.json +12 -0
- data/lib/measures/tbd/LICENSE.md +1 -1
- data/lib/measures/tbd/README.md +27 -11
- data/lib/measures/tbd/measure.rb +156 -73
- data/lib/measures/tbd/measure.xml +168 -66
- data/lib/measures/tbd/resources/geo.rb +437 -222
- data/lib/measures/tbd/resources/oslog.rb +213 -161
- data/lib/measures/tbd/resources/psi.rb +1927 -902
- data/lib/measures/tbd/resources/tbd.rb +1 -1
- data/lib/measures/tbd/resources/ua.rb +381 -310
- 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 +2 -2
- data/lib/tbd/geo.rb +437 -222
- data/lib/tbd/psi.rb +1927 -902
- data/lib/tbd/ua.rb +381 -310
- data/lib/tbd/version.rb +2 -2
- data/lib/tbd.rb +15 -35
- data/tbd.gemspec +2 -2
- data/tbd.schema.json +204 -20
- data/v291_MacOS.md +2 -4
- metadata +11 -6
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# MIT License
|
|
2
2
|
#
|
|
3
|
-
# Copyright (c) 2020-
|
|
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
|
-
#
|
|
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,141 @@ 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
|
|
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
|
|
315
|
-
surf[:type ] = :wall
|
|
316
|
-
surf[:stype ] = stype.get
|
|
317
|
-
surf[:story ] = story.get
|
|
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
|
|
324
|
-
|
|
325
|
-
id
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
#
|
|
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
|
|
344
|
-
n
|
|
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
|
-
|
|
351
|
-
|
|
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
|
-
|
|
354
|
-
|
|
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
|
|
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
|
|
496
|
+
u = u.get unless u.empty?
|
|
383
497
|
|
|
384
|
-
if tubular & s.respond_to?(:daylightingDeviceTubular)
|
|
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
|
|
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
|
-
|
|
398
|
-
|
|
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
|
-
|
|
411
|
-
|
|
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]
|
|
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
|
|
437
|
-
break
|
|
438
|
-
|
|
439
|
-
|
|
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
|
|
443
|
-
next
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
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 ] }
|
|
452
|
-
subs.values.each { |sub| sub[:points] = sub[:v ] }
|
|
453
|
-
subs.values.each { |sub| sub[:area ] = sub[:gross] }
|
|
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
|
-
|
|
476
|
-
|
|
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
|
-
#
|
|
612
|
+
# Validates whether edge surfaces form a concave angle, as seen from outside.
|
|
487
613
|
#
|
|
488
|
-
# @param
|
|
489
|
-
# @param
|
|
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 [
|
|
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)
|
|
507
|
-
return mismatch("s1 angle", s1[:angle], Numeric, DBG, false)
|
|
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
|
-
#
|
|
655
|
+
# Validates whether edge surfaces form a convex angle, as seen from outside.
|
|
526
656
|
#
|
|
527
|
-
# @param
|
|
528
|
-
# @param
|
|
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 [
|
|
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)
|
|
546
|
-
return mismatch("s1 angle", s1[:angle], Numeric, DBG, false)
|
|
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
|
-
#
|
|
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 [
|
|
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("
|
|
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",
|
|
584
|
-
|
|
585
|
-
|
|
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
|
|
595
|
-
kva = false
|
|
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, "
|
|
601
|
-
kva = false
|
|
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, "
|
|
607
|
-
kva = false
|
|
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
|
|
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/
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
658
|
-
|
|
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
|
|
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
|
|
698
|
-
next
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
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
|
|
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
|
-
|
|
947
|
+
|
|
948
|
+
id = walls[i][:kiva]
|
|
737
949
|
next unless floors.key?(id)
|
|
738
950
|
next unless floors[id].key?(:foundation)
|
|
739
|
-
|
|
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)
|