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