osut 0.3.0 → 0.4.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 +17 -1
- data/README.md +11 -12
- data/lib/osut/utils.rb +2491 -764
- data/lib/osut/version.rb +1 -1
- data/osut.gemspec +3 -4
- metadata +8 -8
data/lib/osut/utils.rb
CHANGED
@@ -31,23 +31,1094 @@
|
|
31
31
|
require "openstudio"
|
32
32
|
|
33
33
|
module OSut
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
34
|
+
# DEBUG for devs; WARN/ERROR for users (bad OS input), see OSlg
|
35
|
+
extend OSlg
|
36
|
+
|
37
|
+
TOL = 0.01 # default distance tolerance (m)
|
38
|
+
TOL2 = TOL * TOL # default area tolerance (m2)
|
39
|
+
DBG = OSlg::DEBUG # see github.com/rd2/oslg
|
40
|
+
INF = OSlg::INFO # see github.com/rd2/oslg
|
41
|
+
WRN = OSlg::WARN # see github.com/rd2/oslg
|
42
|
+
ERR = OSlg::ERROR # see github.com/rd2/oslg
|
43
|
+
FTL = OSlg::FATAL # see github.com/rd2/oslg
|
44
|
+
NS = "nameString" # OpenStudio object identifier method
|
45
|
+
|
46
|
+
HEAD = 2.032 # standard 80" door
|
47
|
+
SILL = 0.762 # standard 30" window sill
|
48
|
+
|
49
|
+
# General surface orientations (see facets method)
|
50
|
+
SIDZ = [:bottom, # e.g. ground-facing, exposed floros
|
51
|
+
:top, # e.g. roof/ceiling
|
52
|
+
:north, # NORTH
|
53
|
+
:east, # EAST
|
54
|
+
:south, # SOUTH
|
55
|
+
:west # WEST
|
56
|
+
].freeze
|
57
|
+
|
58
|
+
# This first set of utilities support OpenStudio materials, constructions,
|
59
|
+
# construction sets, etc. If relying on default StandardOpaqueMaterial:
|
60
|
+
# - roughness (rgh) : "Smooth"
|
61
|
+
# - thickness : 0.1 m
|
62
|
+
# - thermal conductivity (k ) : 0.1 W/m.K
|
63
|
+
# - density (rho) : 0.1 kg/m3
|
64
|
+
# - specific heat (cp ) : 1400.0 J/kg.K
|
65
|
+
#
|
66
|
+
# https://s3.amazonaws.com/openstudio-sdk-documentation/cpp/
|
67
|
+
# OpenStudio-3.6.1-doc/model/html/
|
68
|
+
# classopenstudio_1_1model_1_1_standard_opaque_material.html
|
69
|
+
#
|
70
|
+
# ... apart from surface roughness, rarely would these material properties be
|
71
|
+
# suitable - and are therefore explicitely set below. On roughness:
|
72
|
+
# - "Very Rough" : stucco
|
73
|
+
# - "Rough" : brick
|
74
|
+
# - "Medium Rough" : concrete
|
75
|
+
# - "Medium Smooth" : clear pine
|
76
|
+
# - "Smooth" : smooth plaster
|
77
|
+
# - "Very Smooth" : glass
|
78
|
+
|
79
|
+
# thermal mass categories (e.g. exterior cladding, interior finish, framing)
|
80
|
+
@@mass = [
|
81
|
+
:none, # token for 'no user selection', resort to defaults
|
82
|
+
:light, # e.g. 16mm drywall interior
|
83
|
+
:medium, # e.g. 100mm brick cladding
|
84
|
+
:heavy # e.g. 200mm poured concrete
|
85
|
+
].freeze
|
86
|
+
|
87
|
+
# basic materials (StandardOpaqueMaterials only)
|
88
|
+
@@mats = {
|
89
|
+
sand: {},
|
90
|
+
concrete: {},
|
91
|
+
brick: {},
|
92
|
+
cladding: {}, # e.g. lightweight cladding over furring
|
93
|
+
sheathing: {}, # e.g. plywood
|
94
|
+
polyiso: {}, # e.g. polyisocyanurate panel (or similar)
|
95
|
+
cellulose: {}, # e.g. blown, dry/stabilized fiber
|
96
|
+
mineral: {}, # e.g. semi-rigid rock wool insulation
|
97
|
+
drywall: {},
|
98
|
+
door: {} # single composite material (45mm insulated steel door)
|
99
|
+
}.freeze
|
100
|
+
|
101
|
+
# default inside+outside air film resistances (m2.K/W)
|
102
|
+
@@film = {
|
103
|
+
shading: 0.000, # NA
|
104
|
+
partition: 0.000,
|
105
|
+
wall: 0.150,
|
106
|
+
roof: 0.140,
|
107
|
+
floor: 0.190,
|
108
|
+
basement: 0.120,
|
109
|
+
slab: 0.160,
|
110
|
+
door: 0.150,
|
111
|
+
window: 0.150, # ignored if SimpleGlazingMaterial
|
112
|
+
skylight: 0.140 # ignored if SimpleGlazingMaterial
|
113
|
+
}.freeze
|
114
|
+
|
115
|
+
# default (~1980s) envelope Uo (W/m2•K), based on surface type
|
116
|
+
@@uo = {
|
117
|
+
shading: 0.000, # N/A
|
118
|
+
partition: 0.000, # N/A
|
119
|
+
wall: 0.384, # rated Ro ~14.8 hr•ft2F/Btu
|
120
|
+
roof: 0.327, # rated Ro ~17.6 hr•ft2F/Btu
|
121
|
+
floor: 0.317, # rated Ro ~17.9 hr•ft2F/Btu (exposed floor)
|
122
|
+
basement: 0.000, # uninsulated
|
123
|
+
slab: 0.000, # uninsulated
|
124
|
+
door: 1.800, # insulated, unglazed steel door (single layer)
|
125
|
+
window: 2.800, # e.g. patio doors (simple glazing)
|
126
|
+
skylight: 3.500 # all skylight technologies
|
127
|
+
}.freeze
|
128
|
+
|
129
|
+
# Standard opaque materials, taken from a variety of sources (e.g. energy
|
130
|
+
# codes, NREL's BCL). Material identifiers are symbols, e.g.:
|
131
|
+
# - :brick
|
132
|
+
# - :sand
|
133
|
+
# - :concrete
|
134
|
+
#
|
135
|
+
# Material properties remain largely constant between projects. What does
|
136
|
+
# tend to vary (between projects) are thicknesses. Actual OpenStudio opaque
|
137
|
+
# material objects can be (re)set in more than one way by class methods.
|
138
|
+
# In genConstruction, OpenStudio object identifiers are later suffixed with
|
139
|
+
# actual material thicknesses, in mm, e.g.:
|
140
|
+
# - "concrete200" : 200mm concrete slab
|
141
|
+
# - "drywall13" : 1/2" gypsum board
|
142
|
+
# - "drywall16" : 5/8" gypsum board
|
143
|
+
#
|
144
|
+
# Surface absorptances are also defaulted in OpenStudio:
|
145
|
+
# - thermal, long-wave (thm) : 90%
|
146
|
+
# - solar (sol) : 70%
|
147
|
+
# - visible (vis) : 70%
|
148
|
+
#
|
149
|
+
# These can also be explicitly set, here (e.g. a redundant 'sand' example):
|
150
|
+
@@mats[:sand ][:rgh] = "Rough"
|
151
|
+
@@mats[:sand ][:k ] = 1.290
|
152
|
+
@@mats[:sand ][:rho] = 2240.000
|
153
|
+
@@mats[:sand ][:cp ] = 830.000
|
154
|
+
@@mats[:sand ][:thm] = 0.900
|
155
|
+
@@mats[:sand ][:sol] = 0.700
|
156
|
+
@@mats[:sand ][:vis] = 0.700
|
157
|
+
|
158
|
+
@@mats[:concrete ][:rgh] = "MediumRough"
|
159
|
+
@@mats[:concrete ][:k ] = 1.730
|
160
|
+
@@mats[:concrete ][:rho] = 2240.000
|
161
|
+
@@mats[:concrete ][:cp ] = 830.000
|
162
|
+
|
163
|
+
@@mats[:brick ][:rgh] = "Rough"
|
164
|
+
@@mats[:brick ][:k ] = 0.675
|
165
|
+
@@mats[:brick ][:rho] = 1600.000
|
166
|
+
@@mats[:brick ][:cp ] = 790.000
|
167
|
+
|
168
|
+
@@mats[:cladding ][:rgh] = "MediumSmooth"
|
169
|
+
@@mats[:cladding ][:k ] = 0.115
|
170
|
+
@@mats[:cladding ][:rho] = 540.000
|
171
|
+
@@mats[:cladding ][:cp ] = 1200.000
|
172
|
+
|
173
|
+
@@mats[:sheathing][:k ] = 0.160
|
174
|
+
@@mats[:sheathing][:rho] = 545.000
|
175
|
+
@@mats[:sheathing][:cp ] = 1210.000
|
176
|
+
|
177
|
+
@@mats[:polyiso ][:k ] = 0.025
|
178
|
+
@@mats[:polyiso ][:rho] = 25.000
|
179
|
+
@@mats[:polyiso ][:cp ] = 1590.000
|
180
|
+
|
181
|
+
@@mats[:cellulose][:rgh] = "VeryRough"
|
182
|
+
@@mats[:cellulose][:k ] = 0.050
|
183
|
+
@@mats[:cellulose][:rho] = 80.000
|
184
|
+
@@mats[:cellulose][:cp ] = 835.000
|
185
|
+
|
186
|
+
@@mats[:mineral ][:k ] = 0.050
|
187
|
+
@@mats[:mineral ][:rho] = 19.000
|
188
|
+
@@mats[:mineral ][:cp ] = 960.000
|
189
|
+
|
190
|
+
@@mats[:drywall ][:k ] = 0.160
|
191
|
+
@@mats[:drywall ][:rho] = 785.000
|
192
|
+
@@mats[:drywall ][:cp ] = 1090.000
|
193
|
+
|
194
|
+
@@mats[:door ][:rgh] = "MediumSmooth"
|
195
|
+
@@mats[:door ][:k ] = 0.080
|
196
|
+
@@mats[:door ][:rho] = 600.000
|
197
|
+
@@mats[:door ][:cp ] = 1000.000
|
198
|
+
|
199
|
+
##
|
200
|
+
# Generates an OpenStudio multilayered construction; materials if needed.
|
201
|
+
#
|
202
|
+
# @param model [OpenStudio::Model::Model] a model
|
203
|
+
# @param [Hash] specs OpenStudio construction specifications
|
204
|
+
# @option specs [#to_s] :id ("") construction identifier
|
205
|
+
# @option specs [Symbol] :type (:wall), see @@uo
|
206
|
+
# @option specs [Numeric] :uo clear-field Uo, in W/m2.K, see @@uo
|
207
|
+
# @option specs [Symbol] :clad (:light) exterior cladding, see @@mass
|
208
|
+
# @option specs [Symbol] :frame (:light) assembly framing, see @@mass
|
209
|
+
# @option specs [Symbol] :finish (:light) interior finishing, see @@mass
|
210
|
+
#
|
211
|
+
# @return [OpenStudio::Model::Construction] generated construction
|
212
|
+
# @return [nil] if invalid inputs (see logs)
|
213
|
+
def genConstruction(model = nil, specs = {})
|
214
|
+
mth = "OSut::#{__callee__}"
|
215
|
+
cl1 = OpenStudio::Model::Model
|
216
|
+
cl2 = Hash
|
217
|
+
return mismatch("model", model, cl1, mth) unless model.is_a?(cl1)
|
218
|
+
return mismatch("specs", specs, cl2, mth) unless specs.is_a?(cl2)
|
219
|
+
|
220
|
+
specs[:id ] = "" unless specs.key?(:id )
|
221
|
+
specs[:type] = :wall unless specs.key?(:type)
|
222
|
+
chk = @@uo.keys.include?(specs[:type])
|
223
|
+
return invalid("surface type", mth, 2, ERR) unless chk
|
224
|
+
|
225
|
+
id = trim(specs[:id])
|
226
|
+
id = "OSut|CON|#{specs[:type]}" if id.empty?
|
227
|
+
specs[:uo] = @@uo[ specs[:type] ] unless specs.key?(:uo)
|
228
|
+
u = specs[:uo]
|
229
|
+
return mismatch("#{id} Uo", u, Numeric, mth) unless u.is_a?(Numeric)
|
230
|
+
return invalid("#{id} Uo (> 5.678)", mth, 2, ERR) if u > 5.678
|
231
|
+
return negative("#{id} Uo" , mth, ERR) if u < 0
|
232
|
+
|
233
|
+
# Optional specs. Log/reset if invalid.
|
234
|
+
specs[:clad ] = :light unless specs.key?(:clad ) # exterior
|
235
|
+
specs[:frame ] = :light unless specs.key?(:frame )
|
236
|
+
specs[:finish] = :light unless specs.key?(:finish) # interior
|
237
|
+
log(WRN, "Reset to light cladding") unless @@mass.include?(specs[:clad ])
|
238
|
+
log(WRN, "Reset to light framing" ) unless @@mass.include?(specs[:frame ])
|
239
|
+
log(WRN, "Reset to light finish" ) unless @@mass.include?(specs[:finish])
|
240
|
+
specs[:clad ] = :light unless @@mass.include?(specs[:clad ])
|
241
|
+
specs[:frame ] = :light unless @@mass.include?(specs[:frame ])
|
242
|
+
specs[:finish] = :light unless @@mass.include?(specs[:finish])
|
243
|
+
|
244
|
+
film = @@film[ specs[:type] ]
|
245
|
+
|
246
|
+
# Layered assembly (max 4 layers):
|
247
|
+
# - cladding
|
248
|
+
# - intermediate sheathing
|
249
|
+
# - composite insulating/framing
|
250
|
+
# - interior finish
|
251
|
+
a = {clad: {}, sheath: {}, compo: {}, finish: {}, glazing: {}}
|
252
|
+
|
253
|
+
case specs[:type]
|
254
|
+
when :shading
|
255
|
+
mt = :sheathing
|
256
|
+
d = 0.015
|
257
|
+
a[:compo][:mat] = @@mats[mt]
|
258
|
+
a[:compo][:d ] = d
|
259
|
+
a[:compo][:id ] = "OSut|#{mt}|#{format('%03d', d*1000)[-3..-1]}"
|
260
|
+
when :partition
|
261
|
+
d = 0.015
|
262
|
+
mt = :drywall
|
263
|
+
a[:clad][:mat] = @@mats[mt]
|
264
|
+
a[:clad][:d ] = d
|
265
|
+
a[:clad][:id ] = "OSut|#{mt}|#{format('%03d', d*1000)[-3..-1]}"
|
266
|
+
|
267
|
+
mt = :sheathing
|
268
|
+
a[:compo][:mat] = @@mats[mt]
|
269
|
+
a[:compo][:d ] = d
|
270
|
+
a[:compo][:id ] = "OSut|#{mt}|#{format('%03d', d*1000)[-3..-1]}"
|
271
|
+
|
272
|
+
mt = :drywall
|
273
|
+
a[:finish][:mat] = @@mats[mt]
|
274
|
+
a[:finish][:d ] = d
|
275
|
+
a[:finish][:id ] = "OSut|#{mt}|#{format('%03d', d*1000)[-3..-1]}"
|
276
|
+
when :wall
|
277
|
+
unless specs[:clad] == :none
|
278
|
+
mt = :cladding
|
279
|
+
mt = :brick if specs[:clad] == :medium
|
280
|
+
mt = :concrete if specs[:clad] == :heavy
|
281
|
+
d = 0.100
|
282
|
+
d = 0.015 if specs[:clad] == :light
|
283
|
+
a[:clad][:mat] = @@mats[mt]
|
284
|
+
a[:clad][:d ] = d
|
285
|
+
a[:clad][:id ] = "OSut|#{mt}|#{format('%03d', d*1000)[-3..-1]}"
|
286
|
+
end
|
287
|
+
|
288
|
+
mt = :drywall
|
289
|
+
mt = :polyiso if specs[:frame] == :medium
|
290
|
+
mt = :mineral if specs[:frame] == :heavy
|
291
|
+
d = 0.100
|
292
|
+
d = 0.015 if specs[:frame] == :light
|
293
|
+
a[:sheath][:mat] = @@mats[mt]
|
294
|
+
a[:sheath][:d ] = d
|
295
|
+
a[:sheath][:id ] = "OSut|#{mt}|#{format('%03d', d*1000)[-3..-1]}"
|
296
|
+
|
297
|
+
mt = :concrete
|
298
|
+
mt = :mineral if specs[:frame] == :light
|
299
|
+
d = 0.100
|
300
|
+
d = 0.200 if specs[:frame] == :heavy
|
301
|
+
a[:compo][:mat] = @@mats[mt]
|
302
|
+
a[:compo][:d ] = d
|
303
|
+
a[:compo][:id ] = "OSut|#{mt}|#{format('%03d', d*1000)[-3..-1]}"
|
304
|
+
|
305
|
+
unless specs[:finish] == :none
|
306
|
+
mt = :concrete
|
307
|
+
mt = :drywall if specs[:finish] == :light
|
308
|
+
d = 0.015
|
309
|
+
d = 0.100 if specs[:finish] == :medium
|
310
|
+
d = 0.200 if specs[:finish] == :heavy
|
311
|
+
a[:finish][:mat] = @@mats[mt]
|
312
|
+
a[:finish][:d ] = d
|
313
|
+
a[:finish][:id ] = "OSut|#{mt}|#{format('%03d', d*1000)[-3..-1]}"
|
314
|
+
end
|
315
|
+
when :roof
|
316
|
+
unless specs[:clad] == :none
|
317
|
+
mt = :concrete
|
318
|
+
mt = :cladding if specs[:clad] == :light
|
319
|
+
d = 0.015
|
320
|
+
d = 0.100 if specs[:clad] == :medium # e.g. terrace
|
321
|
+
d = 0.200 if specs[:clad] == :heavy # e.g. parking garage
|
322
|
+
a[:clad][:mat] = @@mats[mt]
|
323
|
+
a[:clad][:d ] = d
|
324
|
+
a[:clad][:id ] = "OSut|#{mt}|#{format('%03d', d*1000)[-3..-1]}"
|
325
|
+
|
326
|
+
mt = :sheathing
|
327
|
+
d = 0.015
|
328
|
+
a[:sheath][:mat] = @@mats[mt]
|
329
|
+
a[:sheath][:d ] = d
|
330
|
+
a[:sheath][:id ] = "OSut|#{mt}|#{format('%03d', d*1000)[-3..-1]}"
|
331
|
+
end
|
332
|
+
|
333
|
+
mt = :cellulose
|
334
|
+
mt = :polyiso if specs[:frame] == :medium
|
335
|
+
mt = :mineral if specs[:frame] == :heavy
|
336
|
+
d = 0.100
|
337
|
+
a[:compo][:mat] = @@mats[mt]
|
338
|
+
a[:compo][:d ] = d
|
339
|
+
a[:compo][:id ] = "OSut|#{mt}|#{format('%03d', d*1000)[-3..-1]}"
|
340
|
+
|
341
|
+
unless specs[:finish] == :none
|
342
|
+
mt = :concrete
|
343
|
+
mt = :drywall if specs[:finish] == :light
|
344
|
+
d = 0.015
|
345
|
+
d = 0.100 if specs[:finish] == :medium # proxy for steel decking
|
346
|
+
d = 0.200 if specs[:finish] == :heavy
|
347
|
+
a[:finish][:mat] = @@mats[mt]
|
348
|
+
a[:finish][:d ] = d
|
349
|
+
a[:finish][:id ] = "OSut|#{mt}|#{format('%03d', d*1000)[-3..-1]}"
|
350
|
+
end
|
351
|
+
when :floor # exposed
|
352
|
+
unless specs[:clad] == :none
|
353
|
+
mt = :cladding
|
354
|
+
d = 0.015
|
355
|
+
a[:clad][:mat] = @@mats[mt]
|
356
|
+
a[:clad][:d ] = d
|
357
|
+
a[:clad][:id ] = "OSut|#{mt}|#{format('%03d', d*1000)[-3..-1]}"
|
358
|
+
|
359
|
+
mt = :sheathing
|
360
|
+
d = 0.015
|
361
|
+
a[:sheath][:mat] = @@mats[mt]
|
362
|
+
a[:sheath][:d ] = d
|
363
|
+
a[:sheath][:id ] = "OSut|#{mt}|#{format('%03d', d*1000)[-3..-1]}"
|
364
|
+
end
|
365
|
+
|
366
|
+
mt = :cellulose
|
367
|
+
mt = :polyiso if specs[:frame] == :medium
|
368
|
+
mt = :mineral if specs[:frame] == :heavy
|
369
|
+
d = 0.100 # possibly an insulating layer to reset
|
370
|
+
a[:compo][:mat] = @@mats[mt]
|
371
|
+
a[:compo][:d ] = d
|
372
|
+
a[:compo][:id ] = "OSut|#{mt}|#{format('%03d', d*1000)[-3..-1]}"
|
373
|
+
|
374
|
+
unless specs[:finish] == :none
|
375
|
+
mt = :concrete
|
376
|
+
mt = :sheathing if specs[:finish] == :light
|
377
|
+
d = 0.015
|
378
|
+
d = 0.100 if specs[:finish] == :medium
|
379
|
+
d = 0.200 if specs[:finish] == :heavy
|
380
|
+
a[:finish][:mat] = @@mats[mt]
|
381
|
+
a[:finish][:d ] = d
|
382
|
+
a[:finish][:id ] = "OSut|#{mt}|#{format('%03d', d*1000)[-3..-1]}"
|
383
|
+
end
|
384
|
+
when :slab # basement slab or slab-on-grade
|
385
|
+
mt = :sand
|
386
|
+
d = 0.100
|
387
|
+
a[:clad][:mat] = @@mats[mt]
|
388
|
+
a[:clad][:d ] = d
|
389
|
+
a[:clad][:id ] = "OSut|#{mt}|#{format('%03d', d*1000)[-3..-1]}"
|
390
|
+
|
391
|
+
unless specs[:frame] == :none
|
392
|
+
mt = :polyiso
|
393
|
+
d = 0.025
|
394
|
+
a[:sheath][:mat] = @@mats[mt]
|
395
|
+
a[:sheath][:d ] = d
|
396
|
+
a[:sheath][:id ] = "OSut|#{mt}|#{format('%03d', d*1000)[-3..-1]}"
|
397
|
+
end
|
398
|
+
|
399
|
+
mt = :concrete
|
400
|
+
d = 0.100
|
401
|
+
d = 0.200 if specs[:frame] == :heavy
|
402
|
+
a[:compo][:mat] = @@mats[mt]
|
403
|
+
a[:compo][:d ] = d
|
404
|
+
a[:compo][:id ] = "OSut|#{mt}|#{format('%03d', d*1000)[-3..-1]}"
|
405
|
+
|
406
|
+
unless specs[:finish] == :none
|
407
|
+
mt = :sheathing
|
408
|
+
d = 0.015
|
409
|
+
a[:finish][:mat] = @@mats[mt]
|
410
|
+
a[:finish][:d ] = d
|
411
|
+
a[:finish][:id ] = "OSut|#{mt}|#{format('%03d', d*1000)[-3..-1]}"
|
412
|
+
end
|
413
|
+
when :basement # wall
|
414
|
+
unless specs[:clad] == :none
|
415
|
+
mt = :concrete
|
416
|
+
mt = :sheathing if specs[:clad] == :light
|
417
|
+
d = 0.100
|
418
|
+
d = 0.015 if specs[:clad] == :light
|
419
|
+
a[:clad][:mat] = @@mats[mt]
|
420
|
+
a[:clad][:d ] = d
|
421
|
+
a[:clad][:id ] = "OSut|#{mt}|#{format('%03d', d*1000)[-3..-1]}"
|
422
|
+
|
423
|
+
mt = :polyiso
|
424
|
+
d = 0.025
|
425
|
+
a[:sheath][:mat] = @@mats[mt]
|
426
|
+
a[:sheath][:d ] = d
|
427
|
+
a[:sheath][:id ] = "OSut|#{mt}|#{format('%03d', d*1000)[-3..-1]}"
|
428
|
+
|
429
|
+
mt = :concrete
|
430
|
+
d = 0.200
|
431
|
+
a[:compo][:mat] = @@mats[mt]
|
432
|
+
a[:compo][:d ] = d
|
433
|
+
a[:compo][:id ] = "OSut|#{mt}|#{format('%03d', d*1000)[-3..-1]}"
|
434
|
+
else
|
435
|
+
mt = :concrete
|
436
|
+
d = 0.200
|
437
|
+
a[:sheath][:mat] = @@mats[mt]
|
438
|
+
a[:sheath][:d ] = d
|
439
|
+
a[:sheath][:id ] = "OSut|#{mt}|#{format('%03d', d*1000)[-3..-1]}"
|
440
|
+
|
441
|
+
unless specs[:finish] == :none
|
442
|
+
mt = :mineral
|
443
|
+
d = 0.075
|
444
|
+
a[:compo][:mat] = @@mats[mt]
|
445
|
+
a[:compo][:d ] = d
|
446
|
+
a[:compo][:id ] = "OSut|#{mt}|#{format('%03d', d*1000)[-3..-1]}"
|
447
|
+
|
448
|
+
mt = :drywall
|
449
|
+
d = 0.015
|
450
|
+
a[:finish][:mat] = @@mats[mt]
|
451
|
+
a[:finish][:d ] = d
|
452
|
+
a[:finish][:id ] = "OSut|#{mt}|#{format('%03d', d*1000)[-3..-1]}"
|
453
|
+
end
|
454
|
+
end
|
455
|
+
when :door # opaque
|
456
|
+
# 45mm insulated (composite) steel door.
|
457
|
+
mt = :door
|
458
|
+
d = 0.045
|
459
|
+
|
460
|
+
a[:compo ][:mat ] = @@mats[mt]
|
461
|
+
a[:compo ][:d ] = d
|
462
|
+
a[:compo ][:id ] = "OSut|#{mt}|#{format('%03d', d*1000)[-3..-1]}"
|
463
|
+
when :window # e.g. patio doors (simple glazing)
|
464
|
+
# SimpleGlazingMaterial.
|
465
|
+
a[:glazing][:u ] = specs[:uo ]
|
466
|
+
a[:glazing][:shgc] = 0.450
|
467
|
+
a[:glazing][:shgc] = specs[:shgc] if specs.key?(:shgc)
|
468
|
+
a[:glazing][:id ] = "OSut|window"
|
469
|
+
a[:glazing][:id ] += "|U#{format('%.1f', a[:glazing][:u])}"
|
470
|
+
a[:glazing][:id ] += "|SHGC#{format('%d', a[:glazing][:shgc]*100)}"
|
471
|
+
when :skylight
|
472
|
+
# SimpleGlazingMaterial.
|
473
|
+
a[:glazing][:u ] = specs[:uo ]
|
474
|
+
a[:glazing][:shgc] = 0.450
|
475
|
+
a[:glazing][:shgc] = specs[:shgc] if specs.key?(:shgc)
|
476
|
+
a[:glazing][:id ] = "OSut|skylight"
|
477
|
+
a[:glazing][:id ] += "|U#{format('%.1f', a[:glazing][:u])}"
|
478
|
+
a[:glazing][:id ] += "|SHGC#{format('%d', a[:glazing][:shgc]*100)}"
|
479
|
+
end
|
480
|
+
|
481
|
+
# Initiate layers.
|
482
|
+
glazed = true
|
483
|
+
glazed = false if a[:glazing].empty?
|
484
|
+
layers = OpenStudio::Model::OpaqueMaterialVector.new unless glazed
|
485
|
+
layers = OpenStudio::Model::FenestrationMaterialVector.new if glazed
|
486
|
+
|
487
|
+
if glazed
|
488
|
+
u = a[:glazing][:u ]
|
489
|
+
shgc = a[:glazing][:shgc]
|
490
|
+
lyr = model.getSimpleGlazingByName(a[:glazing][:id])
|
491
|
+
|
492
|
+
if lyr.empty?
|
493
|
+
lyr = OpenStudio::Model::SimpleGlazing.new(model, u, shgc)
|
494
|
+
lyr.setName(a[:glazing][:id])
|
495
|
+
else
|
496
|
+
lyr = lyr.get
|
497
|
+
end
|
498
|
+
|
499
|
+
layers << lyr
|
500
|
+
else
|
501
|
+
# Loop through each layer spec, and generate construction.
|
502
|
+
a.each do |i, l|
|
503
|
+
next if l.empty?
|
504
|
+
|
505
|
+
lyr = model.getStandardOpaqueMaterialByName(l[:id])
|
506
|
+
|
507
|
+
if lyr.empty?
|
508
|
+
lyr = OpenStudio::Model::StandardOpaqueMaterial.new(model)
|
509
|
+
lyr.setName(l[:id])
|
510
|
+
lyr.setThickness(l[:d])
|
511
|
+
lyr.setRoughness( l[:mat][:rgh]) if l[:mat].key?(:rgh)
|
512
|
+
lyr.setConductivity( l[:mat][:k ]) if l[:mat].key?(:k )
|
513
|
+
lyr.setDensity( l[:mat][:rho]) if l[:mat].key?(:rho)
|
514
|
+
lyr.setSpecificHeat( l[:mat][:cp ]) if l[:mat].key?(:cp )
|
515
|
+
lyr.setThermalAbsorptance(l[:mat][:thm]) if l[:mat].key?(:thm)
|
516
|
+
lyr.setSolarAbsorptance( l[:mat][:sol]) if l[:mat].key?(:sol)
|
517
|
+
lyr.setVisibleAbsorptance(l[:mat][:vis]) if l[:mat].key?(:vis)
|
518
|
+
else
|
519
|
+
lyr = lyr.get
|
520
|
+
end
|
521
|
+
|
522
|
+
layers << lyr
|
523
|
+
end
|
524
|
+
end
|
525
|
+
|
526
|
+
c = OpenStudio::Model::Construction.new(layers)
|
527
|
+
c.setName(id)
|
528
|
+
|
529
|
+
# Adjust insulating layer thickness or conductivity to match requested Uo.
|
530
|
+
unless glazed
|
531
|
+
ro = 0
|
532
|
+
ro = 1 / specs[:uo] - @@film[ specs[:type] ] if specs[:uo] > 0
|
533
|
+
|
534
|
+
if specs[:type] == :door # 1x layer, adjust conductivity
|
535
|
+
layer = c.getLayer(0).to_StandardOpaqueMaterial
|
536
|
+
return invalid("#{id} standard material?", mth, 0) if layer.empty?
|
537
|
+
|
538
|
+
layer = layer.get
|
539
|
+
k = layer.thickness / ro
|
540
|
+
layer.setConductivity(k)
|
541
|
+
elsif ro > 0 # multiple layers, adjust insulating layer thickness
|
542
|
+
lyr = insulatingLayer(c)
|
543
|
+
return invalid("#{id} construction", mth, 0) if lyr[:index].nil?
|
544
|
+
return invalid("#{id} construction", mth, 0) if lyr[:type ].nil?
|
545
|
+
return invalid("#{id} construction", mth, 0) if lyr[:r ].zero?
|
546
|
+
|
547
|
+
index = lyr[:index]
|
548
|
+
layer = c.getLayer(index).to_StandardOpaqueMaterial
|
549
|
+
return invalid("#{id} material @#{index}", mth, 0) if layer.empty?
|
550
|
+
|
551
|
+
layer = layer.get
|
552
|
+
k = layer.conductivity
|
553
|
+
d = (ro - rsi(c) + lyr[:r]) * k
|
554
|
+
return invalid("#{id} adjusted m", mth, 0) if d < 0.03
|
555
|
+
|
556
|
+
nom = "OSut|"
|
557
|
+
nom += layer.nameString.gsub(/[^a-z]/i, "").gsub("OSut", "")
|
558
|
+
nom += "|"
|
559
|
+
nom += format("%03d", d*1000)[-3..-1]
|
560
|
+
layer.setName(nom) if model.getStandardOpaqueMaterialByName(nom).empty?
|
561
|
+
layer.setThickness(d)
|
562
|
+
end
|
563
|
+
end
|
564
|
+
|
565
|
+
c
|
566
|
+
end
|
567
|
+
|
568
|
+
##
|
569
|
+
# Generates a solar shade (e.g. roller, textile) for glazed OpenStudio
|
570
|
+
# SubSurfaces (v351+), controlled to minimize overheating in cooling months
|
571
|
+
# (May to October in Northern Hemisphere), when outdoor dry bulb temperature
|
572
|
+
# is above 18°C and impinging solar radiation is above 100 W/m2.
|
573
|
+
#
|
574
|
+
# @param subs [OpenStudio::Model::SubSurfaceVector] sub surfaces
|
575
|
+
#
|
576
|
+
# @return [Bool] whether successfully generated
|
577
|
+
# @return [false] if invalid input (see logs)
|
578
|
+
def genShade(subs = OpenStudio::Model::SubSurfaceVector.new)
|
579
|
+
# Filter OpenStudio warnings for ShadingControl:
|
580
|
+
# ref: https://github.com/NREL/OpenStudio/issues/4911
|
581
|
+
str = ".*(?<!ShadingControl)$"
|
582
|
+
OpenStudio::Logger.instance.standardOutLogger.setChannelRegex(str)
|
583
|
+
|
584
|
+
mth = "OSut::#{__callee__}"
|
585
|
+
v = OpenStudio.openStudioVersion.split(".").join.to_i
|
586
|
+
cl = OpenStudio::Model::SubSurfaceVector
|
587
|
+
return mismatch("subs ", subs, cl2, mth, DBG, false) unless subs.is_a?(cl)
|
588
|
+
return empty( "subs", mth, WRN, false) if subs.empty?
|
589
|
+
return false if v < 321
|
590
|
+
|
591
|
+
# Shading availability period.
|
592
|
+
mdl = subs.first.model
|
593
|
+
id = "onoff"
|
594
|
+
onoff = mdl.getScheduleTypeLimitsByName(id)
|
595
|
+
|
596
|
+
if onoff.empty?
|
597
|
+
onoff = OpenStudio::Model::ScheduleTypeLimits.new(mdl)
|
598
|
+
onoff.setName(id)
|
599
|
+
onoff.setLowerLimitValue(0)
|
600
|
+
onoff.setUpperLimitValue(1)
|
601
|
+
onoff.setNumericType("Discrete")
|
602
|
+
onoff.setUnitType("Availability")
|
603
|
+
else
|
604
|
+
onoff = onoff.get
|
605
|
+
end
|
606
|
+
|
607
|
+
# Shading schedule.
|
608
|
+
id = "OSut|SHADE|Ruleset"
|
609
|
+
sch = mdl.getScheduleRulesetByName(id)
|
610
|
+
|
611
|
+
if sch.empty?
|
612
|
+
sch = OpenStudio::Model::ScheduleRuleset.new(mdl, 0)
|
613
|
+
sch.setName(id)
|
614
|
+
sch.setScheduleTypeLimits(onoff)
|
615
|
+
sch.defaultDaySchedule.setName("OSut|Shade|Ruleset|Default")
|
616
|
+
else
|
617
|
+
sch = sch.get
|
618
|
+
end
|
619
|
+
|
620
|
+
# Summer cooling rule.
|
621
|
+
id = "OSut|SHADE|ScheduleRule"
|
622
|
+
rule = mdl.getScheduleRuleByName(id)
|
623
|
+
|
624
|
+
if rule.empty?
|
625
|
+
may = OpenStudio::MonthOfYear.new("May")
|
626
|
+
october = OpenStudio::MonthOfYear.new("Oct")
|
627
|
+
start = OpenStudio::Date.new(may, 1)
|
628
|
+
finish = OpenStudio::Date.new(october, 31)
|
629
|
+
|
630
|
+
rule = OpenStudio::Model::ScheduleRule.new(sch)
|
631
|
+
rule.setName(id)
|
632
|
+
rule.setStartDate(start)
|
633
|
+
rule.setEndDate(finish)
|
634
|
+
rule.setApplyAllDays(true)
|
635
|
+
rule.daySchedule.setName("OSut|Shade|Rule|Default")
|
636
|
+
rule.daySchedule.addValue(OpenStudio::Time.new(0,24,0,0), 1)
|
637
|
+
else
|
638
|
+
rule = rule.get
|
639
|
+
end
|
640
|
+
|
641
|
+
# Shade object.
|
642
|
+
id = "OSut|Shade"
|
643
|
+
shd = mdl.getShadeByName(id)
|
644
|
+
|
645
|
+
if shd.empty?
|
646
|
+
shd = OpenStudio::Model::Shade.new(mdl)
|
647
|
+
shd.setName(id)
|
648
|
+
else
|
649
|
+
shd = shd.get
|
650
|
+
end
|
651
|
+
|
652
|
+
# Shading control (unique to each call).
|
653
|
+
id = "OSut|ShadingControl"
|
654
|
+
ctl = OpenStudio::Model::ShadingControl.new(shd)
|
655
|
+
ctl.setName(id)
|
656
|
+
ctl.setSchedule(sch)
|
657
|
+
ctl.setShadingControlType("OnIfHighOutdoorAirTempAndHighSolarOnWindow")
|
658
|
+
ctl.setSetpoint(18) # °C
|
659
|
+
ctl.setSetpoint2(100) # W/m2
|
660
|
+
ctl.setMultipleSurfaceControlType("Group")
|
661
|
+
ctl.setSubSurfaces(subs)
|
662
|
+
end
|
663
|
+
|
664
|
+
##
|
665
|
+
# Generates an internal mass definition and instances for target spaces.
|
666
|
+
#
|
667
|
+
# @param sps [OpenStudio::Model::SpaceVector] target spaces
|
668
|
+
# @param ratio [Numeric] internal mass surface / floor areas
|
669
|
+
#
|
670
|
+
# @return [Bool] whether successfully generated
|
671
|
+
# @return [false] if invalid input (see logs)
|
672
|
+
def genMass(sps = OpenStudio::Model::SpaceVector.new, ratio = 2.0)
|
673
|
+
# This is largely adapted from OpenStudio-Standards:
|
674
|
+
#
|
675
|
+
# https://github.com/NREL/openstudio-standards/blob/
|
676
|
+
# d332605c2f7a35039bf658bf55cad40a7bcac317/lib/openstudio-standards/
|
677
|
+
# prototypes/common/objects/Prototype.Model.rb#L786
|
678
|
+
mth = "OSut::#{__callee__}"
|
679
|
+
cl1 = OpenStudio::Model::SpaceVector
|
680
|
+
cl2 = Numeric
|
681
|
+
no = false
|
682
|
+
return mismatch("spaces", sps, cl1, mth, DBG, no) unless sps.is_a?(cl1)
|
683
|
+
return mismatch( "ratio", ratio, cl2, mth, DBG, no) unless ratio.is_a?(cl2)
|
684
|
+
return empty( "spaces", mth, WRN, no) if sps.empty?
|
685
|
+
return negative( "ratio", mth, ERR, no) if ratio < 0
|
686
|
+
|
687
|
+
# A single material.
|
688
|
+
mdl = sps.first.model
|
689
|
+
id = "OSut|MASS|Material"
|
690
|
+
mat = mdl.getOpaqueMaterialByName(id)
|
691
|
+
|
692
|
+
if mat.empty?
|
693
|
+
mat = OpenStudio::Model::StandardOpaqueMaterial.new(mdl)
|
694
|
+
mat.setName(id)
|
695
|
+
mat.setRoughness("MediumRough")
|
696
|
+
mat.setThickness(0.15)
|
697
|
+
mat.setConductivity(1.12)
|
698
|
+
mat.setDensity(540)
|
699
|
+
mat.setSpecificHeat(1210)
|
700
|
+
mat.setThermalAbsorptance(0.9)
|
701
|
+
mat.setSolarAbsorptance(0.7)
|
702
|
+
mat.setVisibleAbsorptance(0.17)
|
703
|
+
else
|
704
|
+
mat = mat.get
|
705
|
+
end
|
706
|
+
|
707
|
+
# A single, 1x layered construction.
|
708
|
+
id = "OSut|MASS|Construction"
|
709
|
+
con = mdl.getConstructionByName(id)
|
710
|
+
|
711
|
+
if con.empty?
|
712
|
+
con = OpenStudio::Model::Construction.new(mdl)
|
713
|
+
con.setName(id)
|
714
|
+
layers = OpenStudio::Model::MaterialVector.new
|
715
|
+
layers << mat
|
716
|
+
con.setLayers(layers)
|
717
|
+
else
|
718
|
+
con = con.get
|
719
|
+
end
|
720
|
+
|
721
|
+
id = "OSut|InternalMassDefinition|" + (format "%.2f", ratio)
|
722
|
+
df = mdl.getInternalMassDefinitionByName(id)
|
723
|
+
|
724
|
+
if df.empty?
|
725
|
+
df = OpenStudio::Model::InternalMassDefinition.new(mdl)
|
726
|
+
df.setName(id)
|
727
|
+
df.setConstruction(con)
|
728
|
+
df.setSurfaceAreaperSpaceFloorArea(ratio)
|
729
|
+
else
|
730
|
+
df = df.get
|
731
|
+
end
|
732
|
+
|
733
|
+
sps.each do |sp|
|
734
|
+
mass = OpenStudio::Model::InternalMass.new(df)
|
735
|
+
mass.setName("OSut|InternalMass|#{sp.nameString}")
|
736
|
+
mass.setSpace(sp)
|
737
|
+
end
|
738
|
+
|
739
|
+
true
|
740
|
+
end
|
741
|
+
|
742
|
+
##
|
743
|
+
# Validates if a default construction set holds a base construction.
|
744
|
+
#
|
745
|
+
# @param set [OpenStudio::Model::DefaultConstructionSet] a default set
|
746
|
+
# @param bse [OpensStudio::Model::ConstructionBase] a construction base
|
747
|
+
# @param gr [Bool] if ground-facing surface
|
748
|
+
# @param ex [Bool] if exterior-facing surface
|
749
|
+
# @param tp [#to_s] a surface type
|
750
|
+
#
|
751
|
+
# @return [Bool] whether default set holds construction
|
752
|
+
# @return [false] if invalid input (see logs)
|
753
|
+
def holdsConstruction?(set = nil, bse = nil, gr = false, ex = false, tp = "")
|
754
|
+
mth = "OSut::#{__callee__}"
|
755
|
+
cl1 = OpenStudio::Model::DefaultConstructionSet
|
756
|
+
cl2 = OpenStudio::Model::ConstructionBase
|
757
|
+
ck1 = set.respond_to?(NS)
|
758
|
+
ck2 = bse.respond_to?(NS)
|
759
|
+
return invalid("set" , mth, 1, DBG, false) unless ck1
|
760
|
+
return invalid("base", mth, 2, DBG, false) unless ck2
|
761
|
+
|
762
|
+
id1 = set.nameString
|
763
|
+
id2 = bse.nameString
|
764
|
+
ck1 = set.is_a?(cl1)
|
765
|
+
ck2 = bse.is_a?(cl2)
|
766
|
+
ck3 = [true, false].include?(gr)
|
767
|
+
ck4 = [true, false].include?(ex)
|
768
|
+
ck5 = tp.respond_to?(:to_s)
|
769
|
+
return mismatch(id1, set, cl1, mth, DBG, false) unless ck1
|
770
|
+
return mismatch(id2, bse, cl2, mth, DBG, false) unless ck2
|
771
|
+
return invalid("ground" , mth, 3, DBG, false) unless ck3
|
772
|
+
return invalid("exterior" , mth, 4, DBG, false) unless ck4
|
773
|
+
return invalid("surface type", mth, 5, DBG, false) unless ck5
|
774
|
+
|
775
|
+
type = trim(tp).downcase
|
776
|
+
ck1 = ["floor", "wall", "roofceiling"].include?(type)
|
777
|
+
return invalid("surface type", mth, 5, DBG, false) unless ck1
|
778
|
+
|
779
|
+
constructions = nil
|
780
|
+
|
781
|
+
if gr
|
782
|
+
unless set.defaultGroundContactSurfaceConstructions.empty?
|
783
|
+
constructions = set.defaultGroundContactSurfaceConstructions.get
|
784
|
+
end
|
785
|
+
elsif ex
|
786
|
+
unless set.defaultExteriorSurfaceConstructions.empty?
|
787
|
+
constructions = set.defaultExteriorSurfaceConstructions.get
|
788
|
+
end
|
789
|
+
else
|
790
|
+
unless set.defaultInteriorSurfaceConstructions.empty?
|
791
|
+
constructions = set.defaultInteriorSurfaceConstructions.get
|
792
|
+
end
|
793
|
+
end
|
794
|
+
|
795
|
+
return false unless constructions
|
796
|
+
|
797
|
+
case type
|
798
|
+
when "roofceiling"
|
799
|
+
unless constructions.roofCeilingConstruction.empty?
|
800
|
+
construction = constructions.roofCeilingConstruction.get
|
801
|
+
return true if construction == bse
|
802
|
+
end
|
803
|
+
when "floor"
|
804
|
+
unless constructions.floorConstruction.empty?
|
805
|
+
construction = constructions.floorConstruction.get
|
806
|
+
return true if construction == bse
|
807
|
+
end
|
808
|
+
else
|
809
|
+
unless constructions.wallConstruction.empty?
|
810
|
+
construction = constructions.wallConstruction.get
|
811
|
+
return true if construction == bse
|
812
|
+
end
|
813
|
+
end
|
814
|
+
|
815
|
+
false
|
816
|
+
end
|
817
|
+
|
818
|
+
##
|
819
|
+
# Returns a surface's default construction set.
|
820
|
+
#
|
821
|
+
# @param s [OpenStudio::Model::Surface] a surface
|
822
|
+
#
|
823
|
+
# @return [OpenStudio::Model::DefaultConstructionSet] default set
|
824
|
+
# @return [nil] if invalid input (see logs)
|
825
|
+
def defaultConstructionSet(s = nil)
|
826
|
+
mth = "OSut::#{__callee__}"
|
827
|
+
cl = OpenStudio::Model::Surface
|
828
|
+
return invalid("surface", mth, 1) unless s.respond_to?(NS)
|
829
|
+
|
830
|
+
id = s.nameString
|
831
|
+
ok = s.isConstructionDefaulted
|
832
|
+
m1 = "#{id} construction not defaulted (#{mth})"
|
833
|
+
m2 = "#{id} construction"
|
834
|
+
m3 = "#{id} space"
|
835
|
+
return mismatch(id, s, cl, mth) unless s.is_a?(cl)
|
836
|
+
|
837
|
+
log(ERR, m1) unless ok
|
838
|
+
return nil unless ok
|
839
|
+
return empty(m2, mth, ERR) if s.construction.empty?
|
840
|
+
return empty(m3, mth, ERR) if s.space.empty?
|
841
|
+
|
842
|
+
mdl = s.model
|
843
|
+
base = s.construction.get
|
844
|
+
space = s.space.get
|
845
|
+
type = s.surfaceType
|
846
|
+
ground = false
|
847
|
+
exterior = false
|
848
|
+
|
849
|
+
if s.isGroundSurface
|
850
|
+
ground = true
|
851
|
+
elsif s.outsideBoundaryCondition.downcase == "outdoors"
|
852
|
+
exterior = true
|
853
|
+
end
|
854
|
+
|
855
|
+
unless space.defaultConstructionSet.empty?
|
856
|
+
set = space.defaultConstructionSet.get
|
857
|
+
return set if holdsConstruction?(set, base, ground, exterior, type)
|
858
|
+
end
|
859
|
+
|
860
|
+
unless space.spaceType.empty?
|
861
|
+
spacetype = space.spaceType.get
|
862
|
+
|
863
|
+
unless spacetype.defaultConstructionSet.empty?
|
864
|
+
set = spacetype.defaultConstructionSet.get
|
865
|
+
return set if holdsConstruction?(set, base, ground, exterior, type)
|
866
|
+
end
|
867
|
+
end
|
868
|
+
|
869
|
+
unless space.buildingStory.empty?
|
870
|
+
story = space.buildingStory.get
|
871
|
+
|
872
|
+
unless story.defaultConstructionSet.empty?
|
873
|
+
set = story.defaultConstructionSet.get
|
874
|
+
return set if holdsConstruction?(set, base, ground, exterior, type)
|
875
|
+
end
|
876
|
+
end
|
877
|
+
|
878
|
+
building = mdl.getBuilding
|
879
|
+
|
880
|
+
unless building.defaultConstructionSet.empty?
|
881
|
+
set = building.defaultConstructionSet.get
|
882
|
+
return set if holdsConstruction?(set, base, ground, exterior, type)
|
883
|
+
end
|
884
|
+
|
885
|
+
nil
|
886
|
+
end
|
887
|
+
|
888
|
+
##
|
889
|
+
# Validates if every material in a layered construction is standard & opaque.
|
890
|
+
#
|
891
|
+
# @param lc [OpenStudio::LayeredConstruction] a layered construction
|
892
|
+
#
|
893
|
+
# @return [Bool] whether all layers are valid
|
894
|
+
# @return [false] if invalid input (see logs)
|
895
|
+
def standardOpaqueLayers?(lc = nil)
|
896
|
+
mth = "OSut::#{__callee__}"
|
897
|
+
cl = OpenStudio::Model::LayeredConstruction
|
898
|
+
return invalid("lc", mth, 1, DBG, false) unless lc.respond_to?(NS)
|
899
|
+
return mismatch(lc.nameString, lc, cl, mth, DBG, false) unless lc.is_a?(cl)
|
900
|
+
|
901
|
+
lc.layers.each { |m| return false if m.to_StandardOpaqueMaterial.empty? }
|
902
|
+
|
903
|
+
true
|
904
|
+
end
|
905
|
+
|
906
|
+
##
|
907
|
+
# Returns total (standard opaque) layered construction thickness (m).
|
908
|
+
#
|
909
|
+
# @param lc [OpenStudio::LayeredConstruction] a layered construction
|
910
|
+
#
|
911
|
+
# @return [Float] construction thickness
|
912
|
+
# @return [0.0] if invalid input (see logs)
|
913
|
+
def thickness(lc = nil)
|
914
|
+
mth = "OSut::#{__callee__}"
|
915
|
+
cl = OpenStudio::Model::LayeredConstruction
|
916
|
+
return invalid("lc", mth, 1, DBG, 0.0) unless lc.respond_to?(NS)
|
917
|
+
|
918
|
+
id = lc.nameString
|
919
|
+
return mismatch(id, lc, cl, mth, DBG, 0.0) unless lc.is_a?(cl)
|
920
|
+
|
921
|
+
ok = standardOpaqueLayers?(lc)
|
922
|
+
log(ERR, "'#{id}' holds non-StandardOpaqueMaterial(s) (#{mth})") unless ok
|
923
|
+
return 0.0 unless ok
|
924
|
+
|
925
|
+
thickness = 0.0
|
926
|
+
lc.layers.each { |m| thickness += m.thickness }
|
927
|
+
|
928
|
+
thickness
|
929
|
+
end
|
930
|
+
|
931
|
+
##
|
932
|
+
# Returns total air film resistance of a fenestrated construction (m2•K/W)
|
933
|
+
#
|
934
|
+
# @param usi [Numeric] a fenestrated construction's U-factor (W/m2•K)
|
935
|
+
#
|
936
|
+
# @return [Float] total air film resistances
|
937
|
+
# @return [0.1216] if invalid input (see logs)
|
938
|
+
def glazingAirFilmRSi(usi = 5.85)
|
939
|
+
# The sum of thermal resistances of calculated exterior and interior film
|
940
|
+
# coefficients under standard winter conditions are taken from:
|
941
|
+
#
|
942
|
+
# https://bigladdersoftware.com/epx/docs/9-6/engineering-reference/
|
943
|
+
# window-calculation-module.html#simple-window-model
|
944
|
+
#
|
945
|
+
# These remain acceptable approximations for flat windows, yet likely
|
946
|
+
# unsuitable for subsurfaces with curved or projecting shapes like domed
|
947
|
+
# skylights. The solution here is considered an adequate fix for reporting,
|
948
|
+
# awaiting eventual OpenStudio (and EnergyPlus) upgrades to report NFRC 100
|
949
|
+
# (or ISO) air film resistances under standard winter conditions.
|
950
|
+
#
|
951
|
+
# For U-factors above 8.0 W/m2•K (or invalid input), the function returns
|
952
|
+
# 0.1216 m2•K/W, which corresponds to a construction with a single glass
|
953
|
+
# layer thickness of 2mm & k = ~0.6 W/m.K.
|
954
|
+
#
|
955
|
+
# The EnergyPlus Engineering calculations were designed for vertical
|
956
|
+
# windows - not horizontal, slanted or domed surfaces - use with caution.
|
957
|
+
mth = "OSut::#{__callee__}"
|
958
|
+
cl = Numeric
|
959
|
+
return mismatch("usi", usi, cl, mth, DBG, 0.1216) unless usi.is_a?(cl)
|
960
|
+
return invalid("usi", mth, 1, WRN, 0.1216) if usi > 8.0
|
961
|
+
return negative("usi", mth, WRN, 0.1216) if usi < 0
|
962
|
+
return zero("usi", mth, WRN, 0.1216) if usi.abs < TOL
|
963
|
+
|
964
|
+
rsi = 1 / (0.025342 * usi + 29.163853) # exterior film, next interior film
|
965
|
+
return rsi + 1 / (0.359073 * Math.log(usi) + 6.949915) if usi < 5.85
|
966
|
+
return rsi + 1 / (1.788041 * usi - 2.886625)
|
967
|
+
end
|
968
|
+
|
969
|
+
##
|
970
|
+
# Returns a construction's 'standard calc' thermal resistance (m2•K/W), which
|
971
|
+
# includes air film resistances. It excludes insulating effects of shades,
|
972
|
+
# screens, etc. in the case of fenestrated constructions.
|
973
|
+
#
|
974
|
+
# @param lc [OpenStudio::Model::LayeredConstruction] a layered construction
|
975
|
+
# @param film [Numeric] thermal resistance of surface air films (m2•K/W)
|
976
|
+
# @param t [Numeric] gas temperature (°C) (optional)
|
977
|
+
#
|
978
|
+
# @return [Float] layered construction's thermal resistance
|
979
|
+
# @return [0.0] if invalid input (see logs)
|
980
|
+
def rsi(lc = nil, film = 0.0, t = 0.0)
|
981
|
+
# This is adapted from BTAP's Material Module "get_conductance" (P. Lopez)
|
982
|
+
#
|
983
|
+
# https://github.com/NREL/OpenStudio-Prototype-Buildings/blob/
|
984
|
+
# c3d5021d8b7aef43e560544699fb5c559e6b721d/lib/btap/measures/
|
985
|
+
# btap_equest_converter/envelope.rb#L122
|
986
|
+
mth = "OSut::#{__callee__}"
|
987
|
+
cl1 = OpenStudio::Model::LayeredConstruction
|
988
|
+
cl2 = Numeric
|
989
|
+
return invalid("lc", mth, 1, DBG, 0.0) unless lc.respond_to?(NS)
|
990
|
+
|
991
|
+
id = lc.nameString
|
992
|
+
return mismatch(id, lc, cl1, mth, DBG, 0.0) unless lc.is_a?(cl1)
|
993
|
+
return mismatch("film", film, cl2, mth, DBG, 0.0) unless film.is_a?(cl2)
|
994
|
+
return mismatch("temp K", t, cl2, mth, DBG, 0.0) unless t.is_a?(cl2)
|
995
|
+
|
996
|
+
t += 273.0 # °C to K
|
997
|
+
return negative("temp K", mth, ERR, 0.0) if t < 0
|
998
|
+
return negative("film", mth, ERR, 0.0) if film < 0
|
999
|
+
|
1000
|
+
rsi = film
|
1001
|
+
|
1002
|
+
lc.layers.each do |m|
|
1003
|
+
# Fenestration materials first.
|
1004
|
+
empty = m.to_SimpleGlazing.empty?
|
1005
|
+
return 1 / m.to_SimpleGlazing.get.uFactor unless empty
|
1006
|
+
|
1007
|
+
empty = m.to_StandardGlazing.empty?
|
1008
|
+
rsi += m.to_StandardGlazing.get.thermalResistance unless empty
|
1009
|
+
empty = m.to_RefractionExtinctionGlazing.empty?
|
1010
|
+
rsi += m.to_RefractionExtinctionGlazing.get.thermalResistance unless empty
|
1011
|
+
empty = m.to_Gas.empty?
|
1012
|
+
rsi += m.to_Gas.get.getThermalResistance(t) unless empty
|
1013
|
+
empty = m.to_GasMixture.empty?
|
1014
|
+
rsi += m.to_GasMixture.get.getThermalResistance(t) unless empty
|
1015
|
+
|
1016
|
+
# Opaque materials next.
|
1017
|
+
empty = m.to_StandardOpaqueMaterial.empty?
|
1018
|
+
rsi += m.to_StandardOpaqueMaterial.get.thermalResistance unless empty
|
1019
|
+
empty = m.to_MasslessOpaqueMaterial.empty?
|
1020
|
+
rsi += m.to_MasslessOpaqueMaterial.get.thermalResistance unless empty
|
1021
|
+
empty = m.to_RoofVegetation.empty?
|
1022
|
+
rsi += m.to_RoofVegetation.get.thermalResistance unless empty
|
1023
|
+
empty = m.to_AirGap.empty?
|
1024
|
+
rsi += m.to_AirGap.get.thermalResistance unless empty
|
1025
|
+
end
|
1026
|
+
|
1027
|
+
rsi
|
1028
|
+
end
|
1029
|
+
|
1030
|
+
##
|
1031
|
+
# Identifies a layered construction's (opaque) insulating layer. The method
|
1032
|
+
# returns a 3-keyed hash :index, the insulating layer index [0, n layers)
|
1033
|
+
# within the layered construction; :type, either :standard or :massless; and
|
1034
|
+
# :r, material thermal resistance in m2•K/W.
|
1035
|
+
#
|
1036
|
+
# @param lc [OpenStudio::Model::LayeredConstruction] a layered construction
|
1037
|
+
#
|
1038
|
+
# @return [Hash] index: (Integer), type: (Symbol), r: (Float)
|
1039
|
+
# @return [Hash] index: nil, type: nil, r: 0 if invalid input (see logs)
|
1040
|
+
def insulatingLayer(lc = nil)
|
1041
|
+
mth = "OSut::#{__callee__}"
|
1042
|
+
cl = OpenStudio::Model::LayeredConstruction
|
1043
|
+
res = { index: nil, type: nil, r: 0.0 }
|
1044
|
+
i = 0 # iterator
|
1045
|
+
return invalid("lc", mth, 1, DBG, res) unless lc.respond_to?(NS)
|
1046
|
+
|
1047
|
+
id = lc.nameString
|
1048
|
+
return mismatch(id, lc, cl1, mth, DBG, res) unless lc.is_a?(cl)
|
1049
|
+
|
1050
|
+
lc.layers.each do |m|
|
1051
|
+
unless m.to_MasslessOpaqueMaterial.empty?
|
1052
|
+
m = m.to_MasslessOpaqueMaterial.get
|
1053
|
+
|
1054
|
+
if m.thermalResistance < 0.001 || m.thermalResistance < res[:r]
|
1055
|
+
i += 1
|
1056
|
+
next
|
1057
|
+
else
|
1058
|
+
res[:r ] = m.thermalResistance
|
1059
|
+
res[:index] = i
|
1060
|
+
res[:type ] = :massless
|
1061
|
+
end
|
1062
|
+
end
|
1063
|
+
|
1064
|
+
unless m.to_StandardOpaqueMaterial.empty?
|
1065
|
+
m = m.to_StandardOpaqueMaterial.get
|
1066
|
+
k = m.thermalConductivity
|
1067
|
+
d = m.thickness
|
1068
|
+
|
1069
|
+
if d < 0.003 || k > 3.0 || d / k < res[:r]
|
1070
|
+
i += 1
|
1071
|
+
next
|
1072
|
+
else
|
1073
|
+
res[:r ] = d / k
|
1074
|
+
res[:index] = i
|
1075
|
+
res[:type ] = :standard
|
1076
|
+
end
|
1077
|
+
end
|
1078
|
+
|
1079
|
+
i += 1
|
1080
|
+
end
|
1081
|
+
|
1082
|
+
res
|
1083
|
+
end
|
1084
|
+
|
1085
|
+
##
|
1086
|
+
# Validates whether opaque surface can be considered as a curtain wall (or
|
1087
|
+
# similar technology) spandrel, regardless of construction layers, by looking
|
1088
|
+
# up AdditionalProperties or its identifier.
|
1089
|
+
#
|
1090
|
+
# @param s [OpenStudio::Model::Surface] an opaque surface
|
1091
|
+
#
|
1092
|
+
# @return [Bool] whether surface can be considered 'spandrel'
|
1093
|
+
# @return [false] if invalid input (see logs)
|
1094
|
+
def spandrel?(s = nil)
|
1095
|
+
mth = "OSut::#{__callee__}"
|
1096
|
+
cl = OpenStudio::Model::Surface
|
1097
|
+
return invalid("surface", mth, 1, DBG, false) unless s.respond_to?(NS)
|
1098
|
+
|
1099
|
+
id = s.nameString
|
1100
|
+
m1 = "#{id}:spandrel"
|
1101
|
+
m2 = "#{id}:spandrel:boolean"
|
1102
|
+
|
1103
|
+
if s.additionalProperties.hasFeature("spandrel")
|
1104
|
+
val = s.additionalProperties.getFeatureAsBoolean("spandrel")
|
1105
|
+
return invalid(m1, mth, 1, ERR, false) if val.empty?
|
1106
|
+
|
1107
|
+
val = val.get
|
1108
|
+
return invalid(m2, mth, 1, ERR, false) unless [true, false].include?(val)
|
1109
|
+
return val
|
1110
|
+
end
|
1111
|
+
|
1112
|
+
id.downcase.include?("spandrel")
|
1113
|
+
end
|
1114
|
+
|
1115
|
+
# ---- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- ---- #
|
1116
|
+
# ---- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- ---- #
|
1117
|
+
# This next set of utilities (~850 lines) help distinguish spaces that are
|
1118
|
+
# directly vs indirectly CONDITIONED, vs SEMIHEATED. The solution here
|
49
1119
|
# relies as much as possible on space conditioning categories found in
|
50
|
-
# standards like ASHRAE 90.1 and energy codes like the Canadian
|
1120
|
+
# standards like ASHRAE 90.1 and energy codes like the Canadian NECBs.
|
1121
|
+
#
|
51
1122
|
# Both documents share many similarities, regardless of nomenclature. There
|
52
1123
|
# are however noticeable differences between approaches on how a space is
|
53
1124
|
# tagged as falling into one of the aforementioned categories. First, an
|
@@ -69,11 +1140,11 @@ module OSut
|
|
69
1140
|
#
|
70
1141
|
# ... includes plenums, atria, etc.
|
71
1142
|
#
|
72
|
-
# -
|
1143
|
+
# - SEMIHEATED space: an ENCLOSED space that has a heating system
|
73
1144
|
# >= 10 W/m2, yet NOT a CONDITIONED space (see above).
|
74
1145
|
#
|
75
1146
|
# - UNCONDITIONED space: an ENCLOSED space that is NOT a conditioned
|
76
|
-
# space or a
|
1147
|
+
# space or a SEMIHEATED space (see above).
|
77
1148
|
#
|
78
1149
|
# NOTE: Crawlspaces, attics, and parking garages with natural or
|
79
1150
|
# mechanical ventilation are considered UNENCLOSED spaces.
|
@@ -94,41 +1165,116 @@ module OSut
|
|
94
1165
|
# to ASHRAE 90.1 (e.g. heating and/or cooling based, no distinction for
|
95
1166
|
# INDIRECTLY conditioned spaces like plenums).
|
96
1167
|
#
|
97
|
-
#
|
98
|
-
# is based on desired/intended design space setpoint
|
99
|
-
# system sizing criteria. No further treatment
|
100
|
-
# distinguish
|
1168
|
+
# SEMIHEATED spaces are described in the NECB (yet not a defined term). The
|
1169
|
+
# distinction is also based on desired/intended design space setpoint
|
1170
|
+
# temperatures (here 15°C) - not system sizing criteria. No further treatment
|
1171
|
+
# is implemented here to distinguish SEMIHEATED from CONDITIONED spaces;
|
1172
|
+
# notwithstanding the AdditionalProperties tag (described further in this
|
1173
|
+
# section), it is up to users to determine if a CONDITIONED space is
|
1174
|
+
# indeed SEMIHEATED or not (e.g. based on MIN/MAX setpoints).
|
101
1175
|
#
|
102
1176
|
# The single NECB criterion distinguishing UNCONDITIONED ENCLOSED spaces
|
103
1177
|
# (such as vestibules) from UNENCLOSED spaces (such as attics) remains the
|
104
1178
|
# intention to ventilate - or rather to what degree. Regardless, the methods
|
105
|
-
# here are designed to process both classifications in the same way, namely
|
106
|
-
# focusing on adjacent surfaces to CONDITIONED (or
|
107
|
-
# of the building envelope.
|
108
|
-
|
109
|
-
# In light of the above, methods here are designed without a priori
|
110
|
-
# of explicit system sizing choices or access to iterative
|
111
|
-
# processes. As discussed in greater detail
|
112
|
-
# rely on zoning
|
1179
|
+
# here are designed to process both classifications in the same way, namely
|
1180
|
+
# by focusing on adjacent surfaces to CONDITIONED (or SEMIHEATED) spaces as
|
1181
|
+
# part of the building envelope.
|
1182
|
+
|
1183
|
+
# In light of the above, OSut methods here are designed without a priori
|
1184
|
+
# knowledge of explicit system sizing choices or access to iterative
|
1185
|
+
# autosizing processes. As discussed in greater detail below, methods here
|
1186
|
+
# are developed to rely on zoning and/or "intended" temperature setpoints.
|
1187
|
+
# In addition, OSut methods here cannot distinguish between UNCONDITIONED vs
|
1188
|
+
# UNENCLOSED spaces from OpenStudio geometry alone. They are henceforth
|
1189
|
+
# considered synonymous.
|
113
1190
|
#
|
114
1191
|
# For an OpenStudio model in an incomplete or preliminary state, e.g. holding
|
115
|
-
# fully-formed ENCLOSED spaces
|
116
|
-
# temperatures (early design stage assessments of form, porosity or
|
117
|
-
#
|
118
|
-
#
|
1192
|
+
# fully-formed ENCLOSED spaces WITHOUT thermal zoning information or setpoint
|
1193
|
+
# temperatures (early design stage assessments of form, porosity or
|
1194
|
+
# envelope), OpenStudio spaces are considered CONDITIONED by default. This
|
1195
|
+
# default behaviour may be reset based on the (Space) AdditionalProperties
|
1196
|
+
# "space_conditioning_category" key (4x possible values), which is relied
|
1197
|
+
# upon by OpenStudio-Standards:
|
1198
|
+
#
|
1199
|
+
# github.com/NREL/openstudio-standards/blob/
|
1200
|
+
# d2b5e28928e712cb3f137ab5c1ad6d8889ca02b7/lib/openstudio-standards/
|
1201
|
+
# standards/Standards.Space.rb#L1604C5-L1605C1
|
1202
|
+
#
|
1203
|
+
# OpenStudio-Standards recognizes 4x possible value strings:
|
1204
|
+
# - "NonResConditioned"
|
1205
|
+
# - "ResConditioned"
|
1206
|
+
# - "Semiheated"
|
1207
|
+
# - "Unconditioned"
|
1208
|
+
#
|
1209
|
+
# OSut maintains existing "space_conditioning_category" key/value pairs
|
1210
|
+
# intact. Based on these, OSut methods may return related outputs:
|
1211
|
+
#
|
1212
|
+
# "space_conditioning_category" | OSut status | heating °C | cooling °C
|
1213
|
+
# ------------------------------- ------------- ---------- ----------
|
1214
|
+
# - "NonResConditioned" CONDITIONED 21.0 24.0
|
1215
|
+
# - "ResConditioned" CONDITIONED 21.0 24.0
|
1216
|
+
# - "Semiheated" SEMIHEATED 15.0 NA
|
1217
|
+
# - "Unconditioned" UNCONDITIONED NA NA
|
1218
|
+
#
|
1219
|
+
# OSut also looks up another (Space) AdditionalProperties 'key',
|
1220
|
+
# "indirectlyconditioned" to flag plenum or occupied spaces indirectly
|
1221
|
+
# conditioned with transfer air only. The only accepted 'value' for an
|
1222
|
+
# "indirectlyconditioned" 'key' is the name (string) of another (linked)
|
1223
|
+
# space, e.g.:
|
1224
|
+
#
|
1225
|
+
# "indirectlyconditioned" space | linked space, e.g. "core_space"
|
1226
|
+
# ------------------------------- ---------------------------------------
|
1227
|
+
# return air plenum occupied space below
|
1228
|
+
# supply air plenum occupied space above
|
1229
|
+
# dead air space (not a plenum) nearby occupied space
|
1230
|
+
#
|
1231
|
+
# OSut doesn't validate whether the "indirectlyconditioned" space is actually
|
1232
|
+
# adjacent to its linked space. It nonetheless relies on the latter's
|
1233
|
+
# conditioning category (e.g. CONDITIONED, SEMIHEATED) to determine
|
1234
|
+
# anticipated ambient temperatures in the former. For instance, an
|
1235
|
+
# "indirectlyconditioned"-tagged return air plenum linked to a SEMIHEATED
|
1236
|
+
# space is considered as free-floating in terms of cooling, and unlikely to
|
1237
|
+
# have ambient conditions below 15°C under heating (winter) design
|
1238
|
+
# conditions. OSut will associate this plenum to a 15°C heating setpoint
|
1239
|
+
# temperature. If the SEMIHEATED space instead has a heating setpoint
|
1240
|
+
# temperature of 7°C, then OSut will associate a 7°C heating setpoint to this
|
1241
|
+
# plenum.
|
1242
|
+
#
|
1243
|
+
# Even when a (more developed) OpenStudio model holds valid space/zone
|
1244
|
+
# temperature setpoints, OSut gives priority to these AdditionalProperties.
|
1245
|
+
# For instance, a CONDITIONED space can be considered INDIRECTLYCONDITIONED,
|
1246
|
+
# even if its zone thermostat has a valid heating and/or cooling setpoint.
|
1247
|
+
# This is in sync with OpenStudio-Standards' method
|
1248
|
+
# "space_conditioning_category()".
|
1249
|
+
|
1250
|
+
##
|
1251
|
+
# Validates if model has zones with HVAC air loops.
|
1252
|
+
#
|
1253
|
+
# @param model [OpenStudio::Model::Model] a model
|
119
1254
|
#
|
120
|
-
#
|
121
|
-
#
|
122
|
-
|
123
|
-
|
1255
|
+
# @return [Bool] whether model has HVAC air loops
|
1256
|
+
# @return [false] if invalid input (see logs)
|
1257
|
+
def airLoopsHVAC?(model = nil)
|
1258
|
+
mth = "OSut::#{__callee__}"
|
1259
|
+
cl = OpenStudio::Model::Model
|
1260
|
+
return mismatch("model", model, cl, mth, DBG, false) unless model.is_a?(cl)
|
1261
|
+
|
1262
|
+
model.getThermalZones.each do |zone|
|
1263
|
+
next if zone.canBePlenum
|
1264
|
+
return true unless zone.airLoopHVACs.empty?
|
1265
|
+
return true if zone.isPlenum
|
1266
|
+
end
|
1267
|
+
|
1268
|
+
false
|
1269
|
+
end
|
124
1270
|
|
125
1271
|
##
|
126
|
-
#
|
1272
|
+
# Returns MIN/MAX values of a schedule (ruleset).
|
127
1273
|
#
|
128
|
-
# @param sched [OpenStudio::Model::ScheduleRuleset] schedule
|
1274
|
+
# @param sched [OpenStudio::Model::ScheduleRuleset] a schedule
|
129
1275
|
#
|
130
1276
|
# @return [Hash] min: (Float), max: (Float)
|
131
|
-
# @return [Hash] min: nil, max: nil
|
1277
|
+
# @return [Hash] min: nil, max: nil if invalid inputs (see logs)
|
132
1278
|
def scheduleRulesetMinMax(sched = nil)
|
133
1279
|
# Largely inspired from David Goldwasser's
|
134
1280
|
# "schedule_ruleset_annual_min_max_value":
|
@@ -139,44 +1285,28 @@ module OSut
|
|
139
1285
|
mth = "OSut::#{__callee__}"
|
140
1286
|
cl = OpenStudio::Model::ScheduleRuleset
|
141
1287
|
res = { min: nil, max: nil }
|
142
|
-
|
143
|
-
return invalid("sched", mth, 1, DBG, res) unless sched.respond_to?(NS)
|
1288
|
+
return invalid("sched", mth, 1, DBG, res) unless sched.respond_to?(NS)
|
144
1289
|
|
145
1290
|
id = sched.nameString
|
146
1291
|
return mismatch(id, sched, cl, mth, DBG, res) unless sched.is_a?(cl)
|
147
1292
|
|
148
|
-
|
149
|
-
profiles << sched.defaultDaySchedule
|
150
|
-
sched.scheduleRules.each { |rule| profiles << rule.daySchedule }
|
1293
|
+
values = sched.defaultDaySchedule.values.to_a
|
151
1294
|
|
152
|
-
|
153
|
-
id = profile.nameString
|
154
|
-
|
155
|
-
profile.values.each do |val|
|
156
|
-
ok = val.is_a?(Numeric)
|
157
|
-
log(WRN, "Skipping non-numeric value in '#{id}' (#{mth})") unless ok
|
158
|
-
next unless ok
|
159
|
-
|
160
|
-
res[:min] = val unless res[:min]
|
161
|
-
res[:min] = val if res[:min] > val
|
162
|
-
res[:max] = val unless res[:max]
|
163
|
-
res[:max] = val if res[:max] < val
|
164
|
-
end
|
165
|
-
end
|
1295
|
+
sched.scheduleRules.each { |rule| values += rule.daySchedule.values }
|
166
1296
|
|
167
|
-
|
168
|
-
|
1297
|
+
res[:min] = values.min.is_a?(Numeric) ? values.min : nil
|
1298
|
+
res[:max] = values.max.is_a?(Numeric) ? values.max : nil
|
169
1299
|
|
170
1300
|
res
|
171
1301
|
end
|
172
1302
|
|
173
1303
|
##
|
174
|
-
#
|
1304
|
+
# Returns MIN/MAX values of a schedule (constant).
|
175
1305
|
#
|
176
|
-
# @param sched [OpenStudio::Model::ScheduleConstant] schedule
|
1306
|
+
# @param sched [OpenStudio::Model::ScheduleConstant] a schedule
|
177
1307
|
#
|
178
1308
|
# @return [Hash] min: (Float), max: (Float)
|
179
|
-
# @return [Hash] min: nil, max: nil
|
1309
|
+
# @return [Hash] min: nil, max: nil if invalid inputs (see logs)
|
180
1310
|
def scheduleConstantMinMax(sched = nil)
|
181
1311
|
# Largely inspired from David Goldwasser's
|
182
1312
|
# "schedule_constant_annual_min_max_value":
|
@@ -187,14 +1317,13 @@ module OSut
|
|
187
1317
|
mth = "OSut::#{__callee__}"
|
188
1318
|
cl = OpenStudio::Model::ScheduleConstant
|
189
1319
|
res = { min: nil, max: nil }
|
190
|
-
|
191
|
-
return invalid("sched", mth, 1, DBG, res) unless sched.respond_to?(NS)
|
1320
|
+
return invalid("sched", mth, 1, DBG, res) unless sched.respond_to?(NS)
|
192
1321
|
|
193
1322
|
id = sched.nameString
|
194
1323
|
return mismatch(id, sched, cl, mth, DBG, res) unless sched.is_a?(cl)
|
195
1324
|
|
196
|
-
|
197
|
-
mismatch("
|
1325
|
+
ok = sched.value.is_a?(Numeric)
|
1326
|
+
mismatch("#{id} value", sched.value, Numeric, mth, ERR, res) unless ok
|
198
1327
|
res[:min] = sched.value
|
199
1328
|
res[:max] = sched.value
|
200
1329
|
|
@@ -202,12 +1331,12 @@ module OSut
|
|
202
1331
|
end
|
203
1332
|
|
204
1333
|
##
|
205
|
-
#
|
1334
|
+
# Returns MIN/MAX values of a schedule (compact).
|
206
1335
|
#
|
207
1336
|
# @param sched [OpenStudio::Model::ScheduleCompact] schedule
|
208
1337
|
#
|
209
1338
|
# @return [Hash] min: (Float), max: (Float)
|
210
|
-
# @return [Hash] min: nil, max: nil
|
1339
|
+
# @return [Hash] min: nil, max: nil if invalid input (see logs)
|
211
1340
|
def scheduleCompactMinMax(sched = nil)
|
212
1341
|
# Largely inspired from Andrew Parker's
|
213
1342
|
# "schedule_compact_annual_min_max_value":
|
@@ -215,78 +1344,69 @@ module OSut
|
|
215
1344
|
# github.com/NREL/openstudio-standards/blob/
|
216
1345
|
# 99cf713750661fe7d2082739f251269c2dfd9140/lib/openstudio-standards/
|
217
1346
|
# standards/Standards.ScheduleCompact.rb#L8
|
218
|
-
mth
|
219
|
-
cl
|
220
|
-
vals
|
221
|
-
|
222
|
-
res
|
223
|
-
|
224
|
-
return invalid("sched", mth, 1, DBG, res) unless sched.respond_to?(NS)
|
1347
|
+
mth = "OSut::#{__callee__}"
|
1348
|
+
cl = OpenStudio::Model::ScheduleCompact
|
1349
|
+
vals = []
|
1350
|
+
prev = ""
|
1351
|
+
res = { min: nil, max: nil }
|
1352
|
+
return invalid("sched", mth, 1, DBG, res) unless sched.respond_to?(NS)
|
225
1353
|
|
226
1354
|
id = sched.nameString
|
227
1355
|
return mismatch(id, sched, cl, mth, DBG, res) unless sched.is_a?(cl)
|
228
1356
|
|
229
1357
|
sched.extensibleGroups.each do |eg|
|
230
|
-
if
|
1358
|
+
if prev.include?("until")
|
231
1359
|
vals << eg.getDouble(0).get unless eg.getDouble(0).empty?
|
232
1360
|
end
|
233
1361
|
|
234
|
-
str
|
235
|
-
|
1362
|
+
str = eg.getString(0)
|
1363
|
+
prev = str.get.downcase unless str.empty?
|
236
1364
|
end
|
237
1365
|
|
238
|
-
return empty("
|
239
|
-
|
240
|
-
ok = vals.min.is_a?(Numeric) && vals.max.is_a?(Numeric)
|
241
|
-
log(ERR, "Non-numeric values in '#{id}' (#{mth})") unless ok
|
242
|
-
return res unless ok
|
1366
|
+
return empty("#{id} values", mth, ERR, res) if vals.empty?
|
243
1367
|
|
244
|
-
res[:min] = vals.min
|
245
|
-
res[:max] = vals.max
|
1368
|
+
res[:min] = vals.min.is_a?(Numeric) ? vals.min : nil
|
1369
|
+
res[:max] = vals.min.is_a?(Numeric) ? vals.max : nil
|
246
1370
|
|
247
1371
|
res
|
248
1372
|
end
|
249
1373
|
|
250
1374
|
##
|
251
|
-
#
|
1375
|
+
# Returns MIN/MAX values for schedule (interval).
|
252
1376
|
#
|
253
1377
|
# @param sched [OpenStudio::Model::ScheduleInterval] schedule
|
254
1378
|
#
|
255
1379
|
# @return [Hash] min: (Float), max: (Float)
|
256
|
-
# @return [Hash] min: nil, max: nil
|
1380
|
+
# @return [Hash] min: nil, max: nil if invalid input (see logs)
|
257
1381
|
def scheduleIntervalMinMax(sched = nil)
|
258
1382
|
mth = "OSut::#{__callee__}"
|
259
1383
|
cl = OpenStudio::Model::ScheduleInterval
|
260
1384
|
vals = []
|
261
1385
|
res = { min: nil, max: nil }
|
262
|
-
|
263
|
-
return invalid("sched", mth, 1, DBG, res) unless sched.respond_to?(NS)
|
1386
|
+
return invalid("sched", mth, 1, DBG, res) unless sched.respond_to?(NS)
|
264
1387
|
|
265
1388
|
id = sched.nameString
|
266
1389
|
return mismatch(id, sched, cl, mth, DBG, res) unless sched.is_a?(cl)
|
267
1390
|
|
268
1391
|
vals = sched.timeSeries.values
|
269
|
-
ok = vals.min.is_a?(Numeric) && vals.max.is_a?(Numeric)
|
270
|
-
log(ERR, "Non-numeric values in '#{id}' (#{mth})") unless ok
|
271
|
-
return res unless ok
|
272
1392
|
|
273
|
-
res[:min] = vals.min
|
274
|
-
res[:max] = vals.max
|
1393
|
+
res[:min] = vals.min.is_a?(Numeric) ? vals.min : nil
|
1394
|
+
res[:max] = vals.max.is_a?(Numeric) ? vals.min : nil
|
275
1395
|
|
276
1396
|
res
|
277
1397
|
end
|
278
1398
|
|
279
1399
|
##
|
280
|
-
#
|
281
|
-
# zone has active dual setpoint thermostat.
|
1400
|
+
# Returns MAX zone heating temperature schedule setpoint [°C] and whether
|
1401
|
+
# zone has an active dual setpoint thermostat.
|
282
1402
|
#
|
283
1403
|
# @param zone [OpenStudio::Model::ThermalZone] a thermal zone
|
284
1404
|
#
|
285
1405
|
# @return [Hash] spt: (Float), dual: (Bool)
|
286
|
-
# @return [Hash] spt: nil, dual: false
|
1406
|
+
# @return [Hash] spt: nil, dual: false if invalid input (see logs)
|
287
1407
|
def maxHeatScheduledSetpoint(zone = nil)
|
288
1408
|
# Largely inspired from Parker & Marrec's "thermal_zone_heated?" procedure.
|
289
|
-
# The solution here is a tad more relaxed to encompass
|
1409
|
+
# The solution here is a tad more relaxed to encompass SEMIHEATED zones as
|
290
1410
|
# per Canadian NECB criteria (basically any space with at least 10 W/m2 of
|
291
1411
|
# installed heating equipement, i.e. below freezing in Canada).
|
292
1412
|
#
|
@@ -296,8 +1416,7 @@ module OSut
|
|
296
1416
|
mth = "OSut::#{__callee__}"
|
297
1417
|
cl = OpenStudio::Model::ThermalZone
|
298
1418
|
res = { spt: nil, dual: false }
|
299
|
-
|
300
|
-
return invalid("zone", mth, 1, DBG, res) unless zone.respond_to?(NS)
|
1419
|
+
return invalid("zone", mth, 1, DBG, res) unless zone.respond_to?(NS)
|
301
1420
|
|
302
1421
|
id = zone.nameString
|
303
1422
|
return mismatch(id, zone, cl, mth, DBG, res) unless zone.is_a?(cl)
|
@@ -380,8 +1499,8 @@ module OSut
|
|
380
1499
|
|
381
1500
|
return res if zone.thermostat.empty?
|
382
1501
|
|
383
|
-
tstat
|
384
|
-
res[:spt]
|
1502
|
+
tstat = zone.thermostat.get
|
1503
|
+
res[:spt] = nil
|
385
1504
|
|
386
1505
|
unless tstat.to_ThermostatSetpointDualSetpoint.empty? &&
|
387
1506
|
tstat.to_ZoneControlThermostatStagedDualSetpoint.empty?
|
@@ -394,7 +1513,7 @@ module OSut
|
|
394
1513
|
|
395
1514
|
unless tstat.heatingSetpointTemperatureSchedule.empty?
|
396
1515
|
res[:dual] = true
|
397
|
-
sched
|
1516
|
+
sched = tstat.heatingSetpointTemperatureSchedule.get
|
398
1517
|
|
399
1518
|
unless sched.to_ScheduleRuleset.empty?
|
400
1519
|
sched = sched.to_ScheduleRuleset.get
|
@@ -453,16 +1572,15 @@ module OSut
|
|
453
1572
|
end
|
454
1573
|
|
455
1574
|
##
|
456
|
-
#
|
1575
|
+
# Validates if model has zones with valid heating temperature setpoints.
|
457
1576
|
#
|
458
1577
|
# @param model [OpenStudio::Model::Model] a model
|
459
1578
|
#
|
460
|
-
# @return [Bool]
|
461
|
-
# @return [
|
1579
|
+
# @return [Bool] whether model holds valid heating temperature setpoints
|
1580
|
+
# @return [false] if invalid input (see logs)
|
462
1581
|
def heatingTemperatureSetpoints?(model = nil)
|
463
1582
|
mth = "OSut::#{__callee__}"
|
464
1583
|
cl = OpenStudio::Model::Model
|
465
|
-
|
466
1584
|
return mismatch("model", model, cl, mth, DBG, false) unless model.is_a?(cl)
|
467
1585
|
|
468
1586
|
model.getThermalZones.each do |zone|
|
@@ -473,13 +1591,13 @@ module OSut
|
|
473
1591
|
end
|
474
1592
|
|
475
1593
|
##
|
476
|
-
#
|
477
|
-
# zone has active dual setpoint thermostat.
|
1594
|
+
# Returns MIN zone cooling temperature schedule setpoint [°C] and whether
|
1595
|
+
# zone has an active dual setpoint thermostat.
|
478
1596
|
#
|
479
1597
|
# @param zone [OpenStudio::Model::ThermalZone] a thermal zone
|
480
1598
|
#
|
481
1599
|
# @return [Hash] spt: (Float), dual: (Bool)
|
482
|
-
# @return [Hash] spt: nil, dual: false
|
1600
|
+
# @return [Hash] spt: nil, dual: false if invalid input (see logs)
|
483
1601
|
def minCoolScheduledSetpoint(zone = nil)
|
484
1602
|
# Largely inspired from Parker & Marrec's "thermal_zone_cooled?" procedure.
|
485
1603
|
#
|
@@ -489,8 +1607,7 @@ module OSut
|
|
489
1607
|
mth = "OSut::#{__callee__}"
|
490
1608
|
cl = OpenStudio::Model::ThermalZone
|
491
1609
|
res = { spt: nil, dual: false }
|
492
|
-
|
493
|
-
return invalid("zone", mth, 1, DBG, res) unless zone.respond_to?(NS)
|
1610
|
+
return invalid("zone", mth, 1, DBG, res) unless zone.respond_to?(NS)
|
494
1611
|
|
495
1612
|
id = zone.nameString
|
496
1613
|
return mismatch(id, zone, cl, mth, DBG, res) unless zone.is_a?(cl)
|
@@ -560,8 +1677,8 @@ module OSut
|
|
560
1677
|
|
561
1678
|
return res if zone.thermostat.empty?
|
562
1679
|
|
563
|
-
tstat
|
564
|
-
res[:spt]
|
1680
|
+
tstat = zone.thermostat.get
|
1681
|
+
res[:spt] = nil
|
565
1682
|
|
566
1683
|
unless tstat.to_ThermostatSetpointDualSetpoint.empty? &&
|
567
1684
|
tstat.to_ZoneControlThermostatStagedDualSetpoint.empty?
|
@@ -574,7 +1691,7 @@ module OSut
|
|
574
1691
|
|
575
1692
|
unless tstat.coolingSetpointTemperatureSchedule.empty?
|
576
1693
|
res[:dual] = true
|
577
|
-
sched
|
1694
|
+
sched = tstat.coolingSetpointTemperatureSchedule.get
|
578
1695
|
|
579
1696
|
unless sched.to_ScheduleRuleset.empty?
|
580
1697
|
sched = sched.to_ScheduleRuleset.get
|
@@ -633,17 +1750,16 @@ module OSut
|
|
633
1750
|
end
|
634
1751
|
|
635
1752
|
##
|
636
|
-
#
|
1753
|
+
# Validates if model has zones with valid cooling temperature setpoints.
|
637
1754
|
#
|
638
1755
|
# @param model [OpenStudio::Model::Model] a model
|
639
1756
|
#
|
640
|
-
# @return [Bool]
|
641
|
-
# @return [
|
1757
|
+
# @return [Bool] whether model holds valid cooling temperature setpoints
|
1758
|
+
# @return [false] if invalid input (see logs)
|
642
1759
|
def coolingTemperatureSetpoints?(model = nil)
|
643
1760
|
mth = "OSut::#{__callee__}"
|
644
1761
|
cl = OpenStudio::Model::Model
|
645
|
-
|
646
|
-
return mismatch("model", model, cl, mth, DBG, false) unless model.is_a?(cl)
|
1762
|
+
return mismatch("model", model, cl, mth, DBG, false) unless model.is_a?(cl)
|
647
1763
|
|
648
1764
|
model.getThermalZones.each do |zone|
|
649
1765
|
return true if minCoolScheduledSetpoint(zone)[:spt]
|
@@ -653,117 +1769,318 @@ module OSut
|
|
653
1769
|
end
|
654
1770
|
|
655
1771
|
##
|
656
|
-
#
|
1772
|
+
# Validates whether space is a vestibule.
|
657
1773
|
#
|
658
|
-
# @param
|
1774
|
+
# @param space [OpenStudio::Model::Space] a space
|
659
1775
|
#
|
660
|
-
# @return [Bool]
|
661
|
-
# @return [
|
662
|
-
def
|
1776
|
+
# @return [Bool] whether space is considered a vestibule
|
1777
|
+
# @return [false] if invalid input (see logs)
|
1778
|
+
def vestibule?(space = nil)
|
1779
|
+
# INFO: OpenStudio-Standards' "thermal_zone_vestibule" criteria:
|
1780
|
+
# - zones less than 200ft2; AND
|
1781
|
+
# - having infiltration using Design Flow Rate
|
1782
|
+
#
|
1783
|
+
# github.com/NREL/openstudio-standards/blob/
|
1784
|
+
# 86bcd026a20001d903cc613bed6d63e94b14b142/lib/openstudio-standards/
|
1785
|
+
# standards/Standards.ThermalZone.rb#L1264
|
1786
|
+
#
|
1787
|
+
# This (unused) OpenStudio-Standards method likely needs revision; it would
|
1788
|
+
# return "false" if the thermal zone area were less than 200ft2. Not sure
|
1789
|
+
# which edition of 90.1 relies on a 200ft2 threshold (2010?); 90.1 2016
|
1790
|
+
# doesn't. Yet even fixed, the method would nonetheless misidentify as
|
1791
|
+
# "vestibule" a small space along an exterior wall, such as a semiheated
|
1792
|
+
# storage space.
|
1793
|
+
#
|
1794
|
+
# The code below is intended as a simple short-term solution, basically
|
1795
|
+
# relying on AdditionalProperties, or (if missing) a "vestibule" substring
|
1796
|
+
# within a space's spaceType name (or the latter's standardsSpaceType).
|
1797
|
+
#
|
1798
|
+
# Alternatively, some future method could infer its status as a vestibule
|
1799
|
+
# based on a few basic features (common to all vintages):
|
1800
|
+
# - 1x+ outdoor-facing wall(s) holding 1x+ door(s)
|
1801
|
+
# - adjacent to 1x+ 'occupied' conditioned space(s)
|
1802
|
+
# - ideally, 1x+ door(s) between vestibule and 1x+ such adjacent space(s)
|
1803
|
+
#
|
1804
|
+
# An additional method parameter (i.e. std = :necb) could be added to
|
1805
|
+
# ensure supplementary Standard-specific checks, e.g. maximum floor area,
|
1806
|
+
# minimum distance between doors.
|
1807
|
+
#
|
1808
|
+
# Finally, an entirely separate method could be developed to first identify
|
1809
|
+
# whether "building entrances" (a defined term in 90.1) actually require
|
1810
|
+
# vestibules as per specific code requirements. Food for thought.
|
663
1811
|
mth = "OSut::#{__callee__}"
|
664
|
-
cl = OpenStudio::Model::
|
1812
|
+
cl = OpenStudio::Model::Space
|
1813
|
+
return mismatch("space", space, cl, mth, DBG, false) unless space.is_a?(cl)
|
665
1814
|
|
666
|
-
|
1815
|
+
id = space.nameString
|
1816
|
+
m1 = "#{id}:vestibule"
|
1817
|
+
m2 = "#{id}:vestibule:boolean"
|
667
1818
|
|
668
|
-
|
669
|
-
|
670
|
-
return
|
671
|
-
|
1819
|
+
if space.additionalProperties.hasFeature("vestibule")
|
1820
|
+
val = space.additionalProperties.getFeatureAsBoolean("vestibule")
|
1821
|
+
return invalid(m1, mth, 1, ERR, false) if val.empty?
|
1822
|
+
|
1823
|
+
val = val.get
|
1824
|
+
return invalid(m2, mth, 1, ERR, false) unless [true, false].include?(val)
|
1825
|
+
return val
|
1826
|
+
end
|
1827
|
+
|
1828
|
+
unless space.spaceType.empty?
|
1829
|
+
type = space.spaceType.get
|
1830
|
+
return false if type.nameString.downcase.include?("plenum")
|
1831
|
+
return true if type.nameString.downcase.include?("vestibule")
|
1832
|
+
|
1833
|
+
unless type.standardsSpaceType.empty?
|
1834
|
+
type = type.standardsSpaceType.get.downcase
|
1835
|
+
return false if type.include?("plenum")
|
1836
|
+
return true if type.include?("vestibule")
|
1837
|
+
end
|
672
1838
|
end
|
673
1839
|
|
674
1840
|
false
|
675
1841
|
end
|
676
1842
|
|
677
1843
|
##
|
678
|
-
#
|
1844
|
+
# Validates whether a space is an indirectly-conditioned plenum.
|
679
1845
|
#
|
680
1846
|
# @param space [OpenStudio::Model::Space] a space
|
681
|
-
# @param loops [Bool] true if model has airLoopHVAC object(s)
|
682
|
-
# @param setpoints [Bool] true if model has valid temperature setpoints
|
683
1847
|
#
|
684
|
-
# @return [Bool]
|
685
|
-
# @return [
|
686
|
-
def plenum?(space = nil
|
687
|
-
# Largely inspired from NREL's "space_plenum?"
|
1848
|
+
# @return [Bool] whether space is considered a plenum
|
1849
|
+
# @return [false] if invalid input (see logs)
|
1850
|
+
def plenum?(space = nil)
|
1851
|
+
# Largely inspired from NREL's "space_plenum?":
|
688
1852
|
#
|
689
|
-
#
|
690
|
-
#
|
691
|
-
#
|
1853
|
+
# github.com/NREL/openstudio-standards/blob/
|
1854
|
+
# 58964222d25783e9da4ae292e375fb0d5c902aa5/lib/openstudio-standards/
|
1855
|
+
# standards/Standards.Space.rb#L1384
|
1856
|
+
#
|
1857
|
+
# Ideally, "plenum?" should be in sync with OpenStudio SDK's "isPlenum"
|
1858
|
+
# method, which solely looks for either HVAC air mixer objects:
|
1859
|
+
# - AirLoopHVACReturnPlenum
|
1860
|
+
# - AirLoopHVACSupplyPlenum
|
1861
|
+
#
|
1862
|
+
# Of the OpenStudio-Standards Prototype models, only the LargeOffice
|
1863
|
+
# holds AirLoopHVACReturnPlenum objects. OpenStudio-Standards' method
|
1864
|
+
# "space_plenum?" indeed catches them by checking if the space is
|
1865
|
+
# "partofTotalFloorArea" (which internally has an "isPlenum" check). So
|
1866
|
+
# "isPlenum" closely follows ASHRAE 90.1 2016's definition of "plenum":
|
1867
|
+
#
|
1868
|
+
# "plenum": a compartment or chamber ...
|
1869
|
+
# - to which one or more ducts are connected
|
1870
|
+
# - that forms a part of the air distribution system, and
|
1871
|
+
# - that is NOT USED for occupancy or storage.
|
1872
|
+
#
|
1873
|
+
# Canadian NECB 2020 has the following (not as well) defined term:
|
1874
|
+
# "plenum": a chamber forming part of an air duct system.
|
1875
|
+
# ... we'll assume that a space shall also be considered
|
1876
|
+
# UNOCCUPIED if it's "part of an air duct system".
|
1877
|
+
#
|
1878
|
+
# As intended, "isPlenum" would NOT identify as a "plenum" any vented
|
1879
|
+
# UNCONDITIONED or UNENCLOSED attic or crawlspace - good. Yet "isPlenum"
|
1880
|
+
# would also ignore dead air spaces integrating ducted return air. The
|
1881
|
+
# SDK's "partofTotalFloorArea" would be more suitable in such cases, as
|
1882
|
+
# long as modellers have, a priori, set this parameter to FALSE.
|
1883
|
+
#
|
1884
|
+
# OpenStudio-Standards' "space_plenum?" catches a MUCH WIDER range of
|
1885
|
+
# spaces, which aren't caught by "isPlenum". This includes attics,
|
1886
|
+
# crawlspaces, non-plenum air spaces above ceiling tiles, and any other
|
1887
|
+
# UNOCCUPIED space in a model. The term "plenum" in this context is more
|
1888
|
+
# of a catch-all shorthand - to be used with caution. For instance,
|
1889
|
+
# "space_plenum?" shouldn't be used (in isolation) to determine whether an
|
1890
|
+
# UNOCCUPIED space should have its envelope insulated ("plenum") or not
|
1891
|
+
# ("attic").
|
1892
|
+
#
|
1893
|
+
# In contrast to OpenStudio-Standards' "space_plenum?", this method
|
1894
|
+
# strictly returns FALSE if a space is indeed "partofTotalFloorArea". It
|
1895
|
+
# also returns FALSE if the space is a vestibule. Otherwise, it needs more
|
1896
|
+
# information to determine if such an UNOCCUPIED space is indeed a
|
1897
|
+
# plenum. Beyond these 2x criteria, a space is considered a plenum if:
|
692
1898
|
#
|
693
|
-
# A
|
1899
|
+
# CASE A: it includes the substring "plenum" (case insensitive) in its
|
1900
|
+
# spaceType's name, or in the latter's standardsSpaceType string;
|
694
1901
|
#
|
695
|
-
# CASE
|
696
|
-
# OpenStudio model (complete with HVAC air loops); OR
|
1902
|
+
# CASE B: "isPlenum" == TRUE in an OpenStudio model WITH HVAC airloops: OR
|
697
1903
|
#
|
698
|
-
# CASE
|
699
|
-
#
|
700
|
-
# thermostat, i.e. can't extract valid setpoints; OR
|
1904
|
+
# CASE C: its zone holds an 'inactive' thermostat (i.e. can't extract valid
|
1905
|
+
# setpoints) in an OpenStudio model with setpoint temperatures.
|
701
1906
|
#
|
702
|
-
#
|
703
|
-
#
|
704
|
-
#
|
1907
|
+
# If a modeller is instead simply interested in identifying UNOCCUPIED
|
1908
|
+
# spaces that are INDIRECTLYCONDITIONED (not necessarily plenums), then the
|
1909
|
+
# following combination is likely more reliable and less confusing:
|
1910
|
+
# - SDK's partofTotalFloorArea == FALSE
|
1911
|
+
# - OSut's unconditioned? == FALSE
|
705
1912
|
mth = "OSut::#{__callee__}"
|
706
1913
|
cl = OpenStudio::Model::Space
|
707
|
-
|
708
|
-
return
|
1914
|
+
return mismatch("space", space, cl, mth, DBG, false) unless space.is_a?(cl)
|
1915
|
+
return false if space.partofTotalFloorArea
|
1916
|
+
return false if vestibule?(space)
|
709
1917
|
|
710
1918
|
id = space.nameString
|
711
|
-
|
1919
|
+
m1 = "#{id}:plenum"
|
1920
|
+
m1 = "#{id}:plenum boolean"
|
1921
|
+
|
1922
|
+
# CASE A: "plenum" spaceType.
|
1923
|
+
unless space.spaceType.empty?
|
1924
|
+
type = space.spaceType.get
|
1925
|
+
return true if type.nameString.downcase.include?("plenum")
|
1926
|
+
|
1927
|
+
unless type.standardsSpaceType.empty?
|
1928
|
+
type = type.standardsSpaceType.get.downcase
|
1929
|
+
return true if type.include?("plenum")
|
1930
|
+
end
|
1931
|
+
end
|
1932
|
+
|
1933
|
+
# CASE B: "isPlenum" == TRUE if airloops.
|
1934
|
+
return space.isPlenum if airLoopsHVAC?(space.model)
|
1935
|
+
|
1936
|
+
# CASE C: zone holds an 'inactive' thermostat.
|
1937
|
+
zone = space.thermalZone
|
1938
|
+
heated = heatingTemperatureSetpoints?(space.model)
|
1939
|
+
cooled = coolingTemperatureSetpoints?(space.model)
|
1940
|
+
|
1941
|
+
if heated || cooled
|
1942
|
+
return false if zone.empty?
|
1943
|
+
|
1944
|
+
zone = zone.get
|
1945
|
+
heat = maxHeatScheduledSetpoint(zone)
|
1946
|
+
cool = minCoolScheduledSetpoint(zone)
|
1947
|
+
return false if heat[:spt] || cool[:spt] # directly CONDITIONED
|
1948
|
+
return heat[:dual] || cool[:dual] # FALSE if both are nilled
|
1949
|
+
end
|
1950
|
+
|
1951
|
+
false
|
1952
|
+
end
|
1953
|
+
|
1954
|
+
##
|
1955
|
+
# Retrieves a space's (implicit or explicit) heating/cooling setpoints.
|
1956
|
+
#
|
1957
|
+
# @param space [OpenStudio::Model::Space] a space
|
1958
|
+
#
|
1959
|
+
# @return [Hash] heating: (Float), cooling: (Float)
|
1960
|
+
# @return [Hash] heating: nil, cooling: nil if invalid input (see logs)
|
1961
|
+
def setpoints(space = nil)
|
1962
|
+
mth = "OSut::#{__callee__}"
|
1963
|
+
cl1 = OpenStudio::Model::Space
|
1964
|
+
cl2 = String
|
1965
|
+
res = {heating: nil, cooling: nil}
|
1966
|
+
tg1 = "space_conditioning_category"
|
1967
|
+
tg2 = "indirectlyconditioned"
|
1968
|
+
cts = ["nonresconditioned", "resconditioned", "semiheated", "unconditioned"]
|
1969
|
+
cnd = nil
|
1970
|
+
return mismatch("space", space, cl1, mth, DBG, res) unless space.is_a?(cl1)
|
1971
|
+
|
1972
|
+
# 1. Check for OpenStudio-Standards' space conditioning categories.
|
1973
|
+
if space.additionalProperties.hasFeature(tg1)
|
1974
|
+
cnd = space.additionalProperties.getFeatureAsString(tg1)
|
1975
|
+
|
1976
|
+
if cnd.empty?
|
1977
|
+
cnd = nil
|
1978
|
+
else
|
1979
|
+
cnd = cnd.get
|
712
1980
|
|
713
|
-
|
714
|
-
|
1981
|
+
if cts.include?(cnd.downcase)
|
1982
|
+
return res if cnd.downcase == "unconditioned"
|
1983
|
+
else
|
1984
|
+
invalid("#{tg1}:#{cnd}", mth, 0, ERR)
|
1985
|
+
cnd = nil
|
1986
|
+
end
|
1987
|
+
end
|
1988
|
+
end
|
715
1989
|
|
716
|
-
|
717
|
-
|
1990
|
+
# 2. Check instead OSut's INDIRECTLYCONDITIONED (parent space) link.
|
1991
|
+
if cnd.nil?
|
1992
|
+
id = space.additionalProperties.getFeatureAsString(tg2)
|
718
1993
|
|
719
|
-
|
720
|
-
|
721
|
-
|
1994
|
+
unless id.empty?
|
1995
|
+
id = id.get
|
1996
|
+
dad = space.model.getSpaceByName(id)
|
722
1997
|
|
723
|
-
|
724
|
-
|
725
|
-
|
726
|
-
|
727
|
-
|
728
|
-
|
1998
|
+
if dad.empty?
|
1999
|
+
log(ERR, "Unknown space #{id} (#{mth})")
|
2000
|
+
else
|
2001
|
+
# Now focus on 'parent' space linked to INDIRECTLYCONDITIONED space.
|
2002
|
+
space = dad.get
|
2003
|
+
cnd = tg2
|
2004
|
+
end
|
729
2005
|
end
|
730
2006
|
end
|
731
2007
|
|
732
|
-
|
733
|
-
|
734
|
-
|
2008
|
+
# 3. Fetch space setpoints (if model indeed holds valid setpoints).
|
2009
|
+
heated = heatingTemperatureSetpoints?(space.model)
|
2010
|
+
cooled = coolingTemperatureSetpoints?(space.model)
|
2011
|
+
zone = space.thermalZone
|
2012
|
+
|
2013
|
+
if heated || cooled
|
2014
|
+
return res if zone.empty? # UNCONDITIONED
|
2015
|
+
|
2016
|
+
zone = zone.get
|
2017
|
+
res[:heating] = maxHeatScheduledSetpoint(zone)[:spt]
|
2018
|
+
res[:cooling] = minCoolScheduledSetpoint(zone)[:spt]
|
735
2019
|
end
|
736
2020
|
|
737
|
-
|
738
|
-
|
739
|
-
|
2021
|
+
# 4. Reset if AdditionalProperties were found & valid.
|
2022
|
+
unless cnd.nil?
|
2023
|
+
if cnd.downcase == "unconditioned"
|
2024
|
+
res[:heating] = nil
|
2025
|
+
res[:cooling] = nil
|
2026
|
+
elsif cnd.downcase == "semiheated"
|
2027
|
+
res[:heating] = 15.0 if res[:heating].nil?
|
2028
|
+
res[:cooling] = nil
|
2029
|
+
elsif cnd.downcase.include?("conditioned")
|
2030
|
+
# "nonresconditioned", "resconditioned" or "indirectlyconditioned"
|
2031
|
+
res[:heating] = 21.0 if res[:heating].nil? # default
|
2032
|
+
res[:cooling] = 24.0 if res[:cooling].nil? # default
|
2033
|
+
end
|
740
2034
|
end
|
741
2035
|
|
742
|
-
|
2036
|
+
# 5. Reset if plenum?
|
2037
|
+
if plenum?(space)
|
2038
|
+
res[:heating] = 21.0 if res[:heating].nil? # default
|
2039
|
+
res[:cooling] = 24.0 if res[:cooling].nil? # default
|
2040
|
+
end
|
2041
|
+
|
2042
|
+
res
|
2043
|
+
end
|
2044
|
+
|
2045
|
+
##
|
2046
|
+
# Validates if a space is UNCONDITIONED.
|
2047
|
+
#
|
2048
|
+
# @param space [OpenStudio::Model::Space] a space
|
2049
|
+
#
|
2050
|
+
# @return [Bool] whether space is considered UNCONDITIONED
|
2051
|
+
# @return [false] if invalid input (see logs)
|
2052
|
+
def unconditioned?(space = nil)
|
2053
|
+
mth = "OSut::#{__callee__}"
|
2054
|
+
cl = OpenStudio::Model::Space
|
2055
|
+
return mismatch("space", space, cl, mth, DBG, false) unless space.is_a?(cl)
|
2056
|
+
|
2057
|
+
ok = false
|
2058
|
+
ok = setpoints(space)[:heating].nil? && setpoints(space)[:cooling].nil?
|
2059
|
+
|
2060
|
+
ok
|
743
2061
|
end
|
744
2062
|
|
745
2063
|
##
|
746
|
-
#
|
2064
|
+
# Generates an HVAC availability schedule.
|
747
2065
|
#
|
748
2066
|
# @param model [OpenStudio::Model::Model] a model
|
749
2067
|
# @param avl [String] seasonal availability choice (optional, default "ON")
|
750
2068
|
#
|
751
2069
|
# @return [OpenStudio::Model::Schedule] HVAC availability sched
|
752
|
-
# @return [
|
2070
|
+
# @return [nil] if invalid input (see logs)
|
753
2071
|
def availabilitySchedule(model = nil, avl = "")
|
754
2072
|
mth = "OSut::#{__callee__}"
|
755
2073
|
cl = OpenStudio::Model::Model
|
756
2074
|
limits = nil
|
757
|
-
|
758
|
-
return
|
759
|
-
return invalid("availability", avl, 2, mth) unless avl.respond_to?(:to_s)
|
2075
|
+
return mismatch("model", model, cl, mth) unless model.is_a?(cl)
|
2076
|
+
return invalid("availability", avl, 2, mth) unless avl.respond_to?(:to_s)
|
760
2077
|
|
761
2078
|
# Either fetch availability ScheduleTypeLimits object, or create one.
|
762
2079
|
model.getScheduleTypeLimitss.each do |l|
|
763
|
-
break
|
764
|
-
next
|
765
|
-
next
|
766
|
-
next
|
2080
|
+
break if limits
|
2081
|
+
next if l.lowerLimitValue.empty?
|
2082
|
+
next if l.upperLimitValue.empty?
|
2083
|
+
next if l.numericType.empty?
|
767
2084
|
next unless l.lowerLimitValue.get.to_i == 0
|
768
2085
|
next unless l.upperLimitValue.get.to_i == 1
|
769
2086
|
next unless l.numericType.get.downcase == "discrete"
|
@@ -789,35 +2106,35 @@ module OSut
|
|
789
2106
|
|
790
2107
|
# Seasonal availability start/end dates.
|
791
2108
|
year = model.yearDescription
|
792
|
-
return
|
2109
|
+
return empty("yearDescription", mth, ERR) if year.empty?
|
793
2110
|
|
794
2111
|
year = year.get
|
795
2112
|
may01 = year.makeDate(OpenStudio::MonthOfYear.new("May"), 1)
|
796
2113
|
oct31 = year.makeDate(OpenStudio::MonthOfYear.new("Oct"), 31)
|
797
2114
|
|
798
|
-
case avl.
|
799
|
-
when "winter"
|
2115
|
+
case trim(avl).downcase
|
2116
|
+
when "winter" # available from November 1 to April 30 (6 months)
|
800
2117
|
val = 1
|
801
2118
|
sch = off
|
802
2119
|
nom = "WINTER Availability SchedRuleset"
|
803
2120
|
dft = "WINTER Availability dftDaySched"
|
804
2121
|
tag = "May-Oct WINTER Availability SchedRule"
|
805
2122
|
day = "May-Oct WINTER SchedRule Day"
|
806
|
-
when "summer"
|
2123
|
+
when "summer" # available from May 1 to October 31 (6 months)
|
807
2124
|
val = 0
|
808
2125
|
sch = on
|
809
2126
|
nom = "SUMMER Availability SchedRuleset"
|
810
2127
|
dft = "SUMMER Availability dftDaySched"
|
811
2128
|
tag = "May-Oct SUMMER Availability SchedRule"
|
812
2129
|
day = "May-Oct SUMMER SchedRule Day"
|
813
|
-
when "off"
|
2130
|
+
when "off" # never available
|
814
2131
|
val = 0
|
815
2132
|
sch = on
|
816
2133
|
nom = "OFF Availability SchedRuleset"
|
817
2134
|
dft = "OFF Availability dftDaySched"
|
818
2135
|
tag = ""
|
819
2136
|
day = ""
|
820
|
-
else
|
2137
|
+
else # always available
|
821
2138
|
val = 1
|
822
2139
|
sch = on
|
823
2140
|
nom = "ON Availability SchedRuleset"
|
@@ -835,14 +2152,14 @@ module OSut
|
|
835
2152
|
|
836
2153
|
unless schedule.empty?
|
837
2154
|
schedule = schedule.get
|
838
|
-
default
|
2155
|
+
default = schedule.defaultDaySchedule
|
839
2156
|
ok = ok && default.nameString == dft
|
840
2157
|
ok = ok && default.times.size == 1
|
841
2158
|
ok = ok && default.values.size == 1
|
842
2159
|
ok = ok && default.times.first == time
|
843
2160
|
ok = ok && default.values.first == val
|
844
2161
|
rules = schedule.scheduleRules
|
845
|
-
ok = ok &&
|
2162
|
+
ok = ok && rules.size < 2
|
846
2163
|
|
847
2164
|
if rules.size == 1
|
848
2165
|
rule = rules.first
|
@@ -867,30 +2184,37 @@ module OSut
|
|
867
2184
|
|
868
2185
|
schedule = OpenStudio::Model::ScheduleRuleset.new(model)
|
869
2186
|
schedule.setName(nom)
|
870
|
-
ok = schedule.setScheduleTypeLimits(limits)
|
871
|
-
log(ERR, "'#{nom}': Can't set schedule type limits (#{mth})") unless ok
|
872
|
-
return nil unless ok
|
873
2187
|
|
874
|
-
|
875
|
-
|
876
|
-
|
2188
|
+
unless schedule.setScheduleTypeLimits(limits)
|
2189
|
+
log(ERR, "'#{nom}': Can't set schedule type limits (#{mth})")
|
2190
|
+
return nil
|
2191
|
+
end
|
2192
|
+
|
2193
|
+
unless schedule.defaultDaySchedule.addValue(time, val)
|
2194
|
+
log(ERR, "'#{nom}': Can't set default day schedule (#{mth})")
|
2195
|
+
return nil
|
2196
|
+
end
|
877
2197
|
|
878
2198
|
schedule.defaultDaySchedule.setName(dft)
|
879
2199
|
|
880
2200
|
unless tag.empty?
|
881
2201
|
rule = OpenStudio::Model::ScheduleRule.new(schedule, sch)
|
882
2202
|
rule.setName(tag)
|
883
|
-
ok = rule.setStartDate(may01)
|
884
|
-
log(ERR, "'#{tag}': Can't set start date (#{mth})") unless ok
|
885
|
-
return nil unless ok
|
886
2203
|
|
887
|
-
|
888
|
-
|
889
|
-
|
2204
|
+
unless rule.setStartDate(may01)
|
2205
|
+
log(ERR, "'#{tag}': Can't set start date (#{mth})")
|
2206
|
+
return nil
|
2207
|
+
end
|
2208
|
+
|
2209
|
+
unless rule.setEndDate(oct31)
|
2210
|
+
log(ERR, "'#{tag}': Can't set end date (#{mth})")
|
2211
|
+
return nil
|
2212
|
+
end
|
890
2213
|
|
891
|
-
|
892
|
-
|
893
|
-
|
2214
|
+
unless rule.setApplyAllDays(true)
|
2215
|
+
log(ERR, "'#{tag}': Can't apply to all days (#{mth})")
|
2216
|
+
return nil
|
2217
|
+
end
|
894
2218
|
|
895
2219
|
rule.daySchedule.setName(day)
|
896
2220
|
end
|
@@ -898,615 +2222,756 @@ module OSut
|
|
898
2222
|
schedule
|
899
2223
|
end
|
900
2224
|
|
2225
|
+
# ---- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- ---- #
|
2226
|
+
# ---- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- ---- #
|
2227
|
+
# This final set of utilities targets OpenStudio geometry. Many of the
|
2228
|
+
# following geometry methods rely on Boost as an OpenStudio dependency.
|
2229
|
+
# As per Boost requirements, points (e.g. polygons) must first be 'aligned':
|
2230
|
+
# - first rotated/tilted as to lay flat along XY plane (Z-axis ~= 0)
|
2231
|
+
# - initial Z-axis values are represented as Y-axis values
|
2232
|
+
# - points with the lowest X-axis values are 'aligned' along X-axis (0)
|
2233
|
+
# - points with the lowest Z-axis values are 'aligned' along Y-axis (0)
|
2234
|
+
# - for several Boost methods, points must be clockwise in sequence
|
2235
|
+
#
|
2236
|
+
# Check OSut's poly() method, which offers such Boost-related options.
|
2237
|
+
|
901
2238
|
##
|
902
|
-
#
|
2239
|
+
# Returns OpenStudio site/space transformation & rotation angle [0,2PI) rads.
|
903
2240
|
#
|
904
|
-
# @param
|
905
|
-
# @param bse [OpensStudio::Model::ConstructionBase] a construction base
|
906
|
-
# @param gr [Bool] true if ground-facing surface
|
907
|
-
# @param ex [Bool] true if exterior-facing surface
|
908
|
-
# @param typ [String] a surface type
|
2241
|
+
# @param group [OpenStudio::Model::PlanarSurfaceGroup] a site or space object
|
909
2242
|
#
|
910
|
-
# @return [
|
911
|
-
# @return [
|
912
|
-
def
|
2243
|
+
# @return [Hash] t: (OpenStudio::Transformation), r: (Float)
|
2244
|
+
# @return [Hash] t: nil, r: nil if invalid input (see logs)
|
2245
|
+
def transforms(group = nil)
|
913
2246
|
mth = "OSut::#{__callee__}"
|
914
|
-
|
915
|
-
|
916
|
-
|
917
|
-
return invalid("set", mth, 1, DBG, false) unless set.respond_to?(NS)
|
918
|
-
|
919
|
-
id = set.nameString
|
920
|
-
return mismatch(id, set, cl1, mth, DBG, false) unless set.is_a?(cl1)
|
921
|
-
return invalid("base", mth, 2, DBG, false) unless bse.respond_to?(NS)
|
922
|
-
|
923
|
-
id = bse.nameString
|
924
|
-
return mismatch(id, bse, cl2, mth, DBG, false) unless bse.is_a?(cl2)
|
925
|
-
|
926
|
-
valid = gr == true || gr == false
|
927
|
-
return invalid("ground", mth, 3, DBG, false) unless valid
|
928
|
-
|
929
|
-
valid = ex == true || ex == false
|
930
|
-
return invalid("exterior", mth, 4, DBG, false) unless valid
|
931
|
-
|
932
|
-
valid = typ.respond_to?(:to_s)
|
933
|
-
return invalid("surface typ", mth, 4, DBG, false) unless valid
|
934
|
-
|
935
|
-
type = typ.to_s.downcase
|
936
|
-
valid = type == "floor" || type == "wall" || type == "roofceiling"
|
937
|
-
return invalid("surface type", mth, 5, DBG, false) unless valid
|
2247
|
+
cl2 = OpenStudio::Model::PlanarSurfaceGroup
|
2248
|
+
res = { t: nil, r: nil }
|
2249
|
+
return invalid("group", mth, 2, DBG, res) unless group.respond_to?(NS)
|
938
2250
|
|
939
|
-
|
2251
|
+
id = group.nameString
|
2252
|
+
mdl = group.model
|
2253
|
+
return mismatch(id, group, cl2, mth, DBG, res) unless group.is_a?(cl2)
|
940
2254
|
|
941
|
-
|
942
|
-
|
943
|
-
constructions = set.defaultGroundContactSurfaceConstructions.get
|
944
|
-
end
|
945
|
-
elsif ex
|
946
|
-
unless set.defaultExteriorSurfaceConstructions.empty?
|
947
|
-
constructions = set.defaultExteriorSurfaceConstructions.get
|
948
|
-
end
|
949
|
-
else
|
950
|
-
unless set.defaultInteriorSurfaceConstructions.empty?
|
951
|
-
constructions = set.defaultInteriorSurfaceConstructions.get
|
952
|
-
end
|
953
|
-
end
|
2255
|
+
res[:t] = group.siteTransformation
|
2256
|
+
res[:r] = group.directionofRelativeNorth + mdl.getBuilding.northAxis
|
954
2257
|
|
955
|
-
|
2258
|
+
res
|
2259
|
+
end
|
956
2260
|
|
957
|
-
|
958
|
-
|
959
|
-
|
960
|
-
|
961
|
-
|
962
|
-
|
963
|
-
|
964
|
-
|
965
|
-
|
966
|
-
|
967
|
-
|
968
|
-
|
969
|
-
|
970
|
-
construction = constructions.wallConstruction.get
|
971
|
-
return true if construction == bse
|
972
|
-
end
|
973
|
-
end
|
2261
|
+
##
|
2262
|
+
# Returns true if 2 OpenStudio 3D points are nearly equal
|
2263
|
+
#
|
2264
|
+
# @param p1 [OpenStudio::Point3d] 1st 3D point
|
2265
|
+
# @param p2 [OpenStudio::Point3d] 2nd 3D point
|
2266
|
+
#
|
2267
|
+
# @return [Bool] whether equal points (within TOL)
|
2268
|
+
# @return [false] if invalid input (see logs)
|
2269
|
+
def same?(p1 = nil, p2 = nil)
|
2270
|
+
mth = "OSut::#{__callee__}"
|
2271
|
+
cl = OpenStudio::Point3d
|
2272
|
+
return mismatch("point 1", p1, cl, mth, DBG, false) unless p1.is_a?(cl)
|
2273
|
+
return mismatch("point 2", p2, cl, mth, DBG, false) unless p2.is_a?(cl)
|
974
2274
|
|
975
|
-
|
2275
|
+
# OpenStudio.isAlmostEqual3dPt(p1, p2, TOL) # ... from v350 onwards.
|
2276
|
+
(p1.x-p2.x).abs < TOL && (p1.y-p2.y).abs < TOL && (p1.z-p2.z).abs < TOL
|
976
2277
|
end
|
977
2278
|
|
978
2279
|
##
|
979
|
-
#
|
2280
|
+
# Returns true if a line segment is along the X-axis.
|
980
2281
|
#
|
981
|
-
# @param
|
982
|
-
# @param
|
2282
|
+
# @param p1 [OpenStudio::Point3d] 1st 3D point of a line segment
|
2283
|
+
# @param p2 [OpenStudio::Point3d] 2nd 3D point of a line segment
|
2284
|
+
# @param strict [Bool] whether segment shouldn't hold Y- or Z-axis components
|
983
2285
|
#
|
984
|
-
# @return [
|
985
|
-
# @return [
|
986
|
-
def
|
2286
|
+
# @return [Bool] whether along the X-axis
|
2287
|
+
# @return [false] if invalid input (see logs)
|
2288
|
+
def xx?(p1 = nil, p2 = nil, strict = true)
|
987
2289
|
mth = "OSut::#{__callee__}"
|
988
|
-
|
989
|
-
|
990
|
-
|
991
|
-
return mismatch("
|
992
|
-
return
|
2290
|
+
cl = OpenStudio::Point3d
|
2291
|
+
strict = true unless [true, false].include?(strict)
|
2292
|
+
return mismatch("point 1", p1, cl, mth, DBG, false) unless p1.is_a?(cl)
|
2293
|
+
return mismatch("point 2", p2, cl, mth, DBG, false) unless p2.is_a?(cl)
|
2294
|
+
return false if (p1.y - p2.y).abs > TOL && strict
|
2295
|
+
return false if (p1.z - p2.z).abs > TOL && strict
|
2296
|
+
|
2297
|
+
(p1.x - p2.x).abs > TOL
|
2298
|
+
end
|
993
2299
|
|
994
|
-
|
995
|
-
|
2300
|
+
##
|
2301
|
+
# Returns true if a line segment is along the Y-axis.
|
2302
|
+
#
|
2303
|
+
# @param p1 [OpenStudio::Point3d] 1st 3D point of a line segment
|
2304
|
+
# @param p2 [OpenStudio::Point3d] 2nd 3D point of a line segment
|
2305
|
+
# @param strict [Bool] whether segment shouldn't hold X- or Z-axis components
|
2306
|
+
#
|
2307
|
+
# @return [Bool] whether along the Y-axis
|
2308
|
+
# @return [false] if invalid input (see logs)
|
2309
|
+
def yy?(p1 = nil, p2 = nil, strict = true)
|
2310
|
+
mth = "OSut::#{__callee__}"
|
2311
|
+
cl = OpenStudio::Point3d
|
2312
|
+
strict = true unless [true, false].include?(strict)
|
2313
|
+
return mismatch("point 1", p1, cl, mth, DBG, false) unless p1.is_a?(cl)
|
2314
|
+
return mismatch("point 2", p2, cl, mth, DBG, false) unless p2.is_a?(cl)
|
2315
|
+
return false if (p1.x - p2.x).abs > TOL && strict
|
2316
|
+
return false if (p1.z - p2.z).abs > TOL && strict
|
2317
|
+
|
2318
|
+
(p1.y - p2.y).abs > TOL
|
2319
|
+
end
|
996
2320
|
|
997
|
-
|
998
|
-
|
999
|
-
|
1000
|
-
|
2321
|
+
##
|
2322
|
+
# Returns true if a line segment is along the Z-axis.
|
2323
|
+
#
|
2324
|
+
# @param p1 [OpenStudio::Point3d] 1st 3D point of a line segment
|
2325
|
+
# @param p2 [OpenStudio::Point3d] 2nd 3D point of a line segment
|
2326
|
+
# @param strict [Bool] whether segment shouldn't hold X- or Y-axis components
|
2327
|
+
#
|
2328
|
+
# @return [Bool] whether along the Z-axis
|
2329
|
+
# @return [false] if invalid input (see logs)
|
2330
|
+
def zz?(p1 = nil, p2 = nil, strict = true)
|
2331
|
+
mth = "OSut::#{__callee__}"
|
2332
|
+
cl = OpenStudio::Point3d
|
2333
|
+
strict = true unless [true, false].include?(strict)
|
2334
|
+
return mismatch("point 1", p1, cl, mth, DBG, false) unless p1.is_a?(cl)
|
2335
|
+
return mismatch("point 2", p2, cl, mth, DBG, false) unless p2.is_a?(cl)
|
2336
|
+
return false if (p1.x - p2.x).abs > TOL && strict
|
2337
|
+
return false if (p1.y - p2.y).abs > TOL && strict
|
2338
|
+
|
2339
|
+
(p1.z - p2.z).abs > TOL
|
2340
|
+
end
|
1001
2341
|
|
1002
|
-
|
1003
|
-
|
2342
|
+
##
|
2343
|
+
# Returns a scalar product of an OpenStudio Vector3d.
|
2344
|
+
#
|
2345
|
+
# @param v [OpenStudio::Vector3d] a vector
|
2346
|
+
# @param m [#to_f] a scalar
|
2347
|
+
#
|
2348
|
+
# @return [OpenStudio::Vector3d] scaled points (see logs if empty)
|
2349
|
+
def scalar(v = OpenStudio::Vector3d.new, m = 0)
|
2350
|
+
mth = "OSut::#{__callee__}"
|
2351
|
+
cl = OpenStudio::Vector3d
|
2352
|
+
ok = m.respond_to?(:to_f)
|
2353
|
+
return mismatch("vector", v, cl, mth, DBG, v) unless v.is_a?(cl)
|
2354
|
+
return mismatch("m", m, Numeric, mth, DBG, v) unless ok
|
1004
2355
|
|
1005
|
-
|
1006
|
-
|
1007
|
-
|
1008
|
-
exterior = false
|
2356
|
+
m = m.to_f
|
2357
|
+
OpenStudio::Vector3d.new(m * v.x, m * v.y, m * v.z)
|
2358
|
+
end
|
1009
2359
|
|
1010
|
-
|
1011
|
-
|
1012
|
-
|
1013
|
-
|
1014
|
-
|
2360
|
+
##
|
2361
|
+
# Returns OpenStudio 3D points as an OpenStudio point vector, validating
|
2362
|
+
# points in the process (if Array).
|
2363
|
+
#
|
2364
|
+
# @param pts [Set<OpenStudio::Point3d>] OpenStudio 3D points
|
2365
|
+
#
|
2366
|
+
# @return [OpenStudio::Point3dVector] 3D vector (see logs if empty)
|
2367
|
+
def to_p3Dv(pts = nil)
|
2368
|
+
mth = "OSut::#{__callee__}"
|
2369
|
+
cl1 = Array
|
2370
|
+
cl2 = OpenStudio::Point3dVector
|
2371
|
+
cl3 = OpenStudio::Model::PlanarSurface
|
2372
|
+
cl4 = OpenStudio::Point3d
|
2373
|
+
v = OpenStudio::Point3dVector.new
|
2374
|
+
return pts if pts.is_a?(cl2)
|
2375
|
+
return pts.vertices if pts.is_a?(cl3)
|
2376
|
+
return mismatch("points", pts, cl1, mth, DBG, v) unless pts.is_a?(cl1)
|
1015
2377
|
|
1016
|
-
|
1017
|
-
|
1018
|
-
return set if holdsConstruction?(set, base, ground, exterior, type)
|
2378
|
+
pts.each do |pt|
|
2379
|
+
return mismatch("point", pt, cl4, mth, DBG, v) unless pt.is_a?(cl4)
|
1019
2380
|
end
|
1020
2381
|
|
1021
|
-
|
1022
|
-
spacetype = space.spaceType.get
|
2382
|
+
pts.each { |pt| v << OpenStudio::Point3d.new(pt.x, pt.y, pt.z) }
|
1023
2383
|
|
1024
|
-
|
1025
|
-
|
1026
|
-
return set if holdsConstruction?(set, base, ground, exterior, type)
|
1027
|
-
end
|
1028
|
-
end
|
2384
|
+
v
|
2385
|
+
end
|
1029
2386
|
|
1030
|
-
|
1031
|
-
|
2387
|
+
##
|
2388
|
+
# Returns true if an OpenStudio 3D point is part of a set of 3D points.
|
2389
|
+
#
|
2390
|
+
# @param pts [Set<OpenStudio::Point3dVector>] 3d points
|
2391
|
+
# @param p1 [OpenStudio::Point3d] a 3D point
|
2392
|
+
#
|
2393
|
+
# @return [Bool] whether part of a set of 3D points
|
2394
|
+
# @return [false] if invalid input (see logs)
|
2395
|
+
def holds?(pts = nil, p1 = nil)
|
2396
|
+
mth = "OSut::#{__callee__}"
|
2397
|
+
pts = to_p3Dv(pts)
|
2398
|
+
cl = OpenStudio::Point3d
|
2399
|
+
return mismatch("point", p1, cl, mth, DBG, false) unless p1.is_a?(cl)
|
1032
2400
|
|
1033
|
-
|
1034
|
-
set = story.defaultConstructionSet.get
|
1035
|
-
return set if holdsConstruction?(set, base, ground, exterior, type)
|
1036
|
-
end
|
1037
|
-
end
|
2401
|
+
pts.each { |pt| return true if same?(p1, pt) }
|
1038
2402
|
|
1039
|
-
|
2403
|
+
false
|
2404
|
+
end
|
1040
2405
|
|
1041
|
-
|
1042
|
-
|
1043
|
-
|
2406
|
+
##
|
2407
|
+
# Flattens OpenStudio 3D points vs X, Y or Z axes.
|
2408
|
+
#
|
2409
|
+
# @param pts [Set<OpenStudio::Point3d>] 3D points
|
2410
|
+
# @param axs [Symbol] :x, :y or :z axis
|
2411
|
+
# @param val [#to_f] axis value
|
2412
|
+
#
|
2413
|
+
# @return [OpenStudio::Point3dVector] flattened points (see logs if empty)
|
2414
|
+
def flatten(pts = nil, axs = :z, val = 0)
|
2415
|
+
mth = "OSut::#{__callee__}"
|
2416
|
+
pts = to_p3Dv(pts)
|
2417
|
+
v = OpenStudio::Point3dVector.new
|
2418
|
+
ok1 = val.respond_to?(:to_f)
|
2419
|
+
ok2 = [:x, :y, :z].include?(axs)
|
2420
|
+
return mismatch("val", val, Numeric, mth, DBG, v) unless ok1
|
2421
|
+
return invalid("axis (XYZ?)", mth, 2, DBG, v) unless ok2
|
2422
|
+
|
2423
|
+
val = val.to_f
|
2424
|
+
|
2425
|
+
case axs
|
2426
|
+
when :x
|
2427
|
+
pts.each { |pt| v << OpenStudio::Point3d.new(val, pt.y, pt.z) }
|
2428
|
+
when :y
|
2429
|
+
pts.each { |pt| v << OpenStudio::Point3d.new(pt.x, val, pt.z) }
|
2430
|
+
else
|
2431
|
+
pts.each { |pt| v << OpenStudio::Point3d.new(pt.x, pt.y, val) }
|
1044
2432
|
end
|
1045
2433
|
|
1046
|
-
|
2434
|
+
v
|
1047
2435
|
end
|
1048
2436
|
|
1049
2437
|
##
|
1050
|
-
#
|
2438
|
+
# Returns true if OpenStudio 3D points share X, Y or Z coordinates.
|
1051
2439
|
#
|
1052
|
-
# @param
|
2440
|
+
# @param pts [Set<OpenStudio::Point3d>] OpenStudio 3D points
|
2441
|
+
# @param axs [Symbol] if potentially along :x, :y or :z axis
|
2442
|
+
# @param val [Numeric] axis value
|
1053
2443
|
#
|
1054
|
-
# @return [Bool]
|
1055
|
-
# @return [
|
1056
|
-
def
|
2444
|
+
# @return [Bool] if points share X, Y or Z coordinates
|
2445
|
+
# @return [false] if invalid input (see logs)
|
2446
|
+
def xyz?(pts = nil, axs = :z, val = 0)
|
1057
2447
|
mth = "OSut::#{__callee__}"
|
1058
|
-
|
1059
|
-
|
1060
|
-
|
1061
|
-
return
|
1062
|
-
|
1063
|
-
|
2448
|
+
pts = to_p3Dv(pts)
|
2449
|
+
ok1 = val.respond_to?(:to_f)
|
2450
|
+
ok2 = [:x, :y, :z].include?(axs)
|
2451
|
+
return false if pts.empty?
|
2452
|
+
return mismatch("val", val, Numeric, mth, DBG, false) unless ok1
|
2453
|
+
return invalid("axis (XYZ?)", mth, 2, DBG, false) unless ok2
|
2454
|
+
|
2455
|
+
val = val.to_f
|
2456
|
+
|
2457
|
+
case axs
|
2458
|
+
when :x
|
2459
|
+
pts.each { |pt| return false if (pt.x - val).abs > TOL }
|
2460
|
+
when :y
|
2461
|
+
pts.each { |pt| return false if (pt.y - val).abs > TOL }
|
2462
|
+
else
|
2463
|
+
pts.each { |pt| return false if (pt.z - val).abs > TOL }
|
2464
|
+
end
|
1064
2465
|
|
1065
2466
|
true
|
1066
2467
|
end
|
1067
2468
|
|
1068
2469
|
##
|
1069
|
-
#
|
2470
|
+
# Returns next sequential point in an OpenStudio 3D point vector.
|
1070
2471
|
#
|
1071
|
-
# @param
|
2472
|
+
# @param pts [OpenStudio::Point3dVector] 3D points
|
2473
|
+
# @param pt [OpenStudio::Point3d] a given 3D point
|
1072
2474
|
#
|
1073
|
-
# @return [
|
1074
|
-
# @return [
|
1075
|
-
def
|
2475
|
+
# @return [OpenStudio::Point3d] the next sequential point
|
2476
|
+
# @return [nil] if invalid input (see logs)
|
2477
|
+
def next(pts = nil, pt = nil)
|
1076
2478
|
mth = "OSut::#{__callee__}"
|
1077
|
-
|
2479
|
+
pts = to_p3Dv(pts)
|
2480
|
+
cl = OpenStudio::Point3d
|
2481
|
+
return mismatch("point", pt, cl, mth) unless pt.is_a?(cl)
|
2482
|
+
return invalid("points (2+)", mth, 1, WRN) if pts.size < 2
|
1078
2483
|
|
1079
|
-
|
2484
|
+
pair = pts.each_cons(2).find { |p1, _| same?(p1, pt) }
|
1080
2485
|
|
1081
|
-
|
1082
|
-
return mismatch(id, lc, cl, mth, DBG, 0.0) unless lc.is_a?(cl)
|
1083
|
-
|
1084
|
-
ok = standardOpaqueLayers?(lc)
|
1085
|
-
log(ERR, "'#{id}' holds non-StandardOpaqueMaterial(s) (#{mth})") unless ok
|
1086
|
-
return 0.0 unless ok
|
1087
|
-
|
1088
|
-
thickness = 0.0
|
1089
|
-
lc.layers.each { |m| thickness += m.thickness }
|
1090
|
-
|
1091
|
-
thickness
|
2486
|
+
pair.nil? ? pts.first : pair.last
|
1092
2487
|
end
|
1093
2488
|
|
1094
2489
|
##
|
1095
|
-
#
|
2490
|
+
# Returns unique OpenStudio 3D points from an OpenStudio 3D point vector.
|
1096
2491
|
#
|
1097
|
-
# @param
|
2492
|
+
# @param pts [Set<OpenStudio::Point3d] 3D points
|
2493
|
+
# @param n [#to_i] requested number of unique points (0 returns all)
|
1098
2494
|
#
|
1099
|
-
# @return [
|
1100
|
-
def
|
1101
|
-
# The sum of thermal resistances of calculated exterior and interior film
|
1102
|
-
# coefficients under standard winter conditions are taken from:
|
1103
|
-
#
|
1104
|
-
# https://bigladdersoftware.com/epx/docs/9-6/engineering-reference/
|
1105
|
-
# window-calculation-module.html#simple-window-model
|
1106
|
-
#
|
1107
|
-
# These remain acceptable approximations for flat windows, yet likely
|
1108
|
-
# unsuitable for subsurfaces with curved or projecting shapes like domed
|
1109
|
-
# skylights. The solution here is considered an adequate fix for reporting,
|
1110
|
-
# awaiting eventual OpenStudio (and EnergyPlus) upgrades to report NFRC 100
|
1111
|
-
# (or ISO) air film resistances under standard winter conditions.
|
1112
|
-
#
|
1113
|
-
# For U-factors above 8.0 W/m2•K (or invalid input), the function returns
|
1114
|
-
# 0.1216 m2•K/W, which corresponds to a construction with a single glass
|
1115
|
-
# layer thickness of 2mm & k = ~0.6 W/m.K.
|
1116
|
-
#
|
1117
|
-
# The EnergyPlus Engineering calculations were designed for vertical windows
|
1118
|
-
# - not horizontal, slanted or domed surfaces - use with caution.
|
2495
|
+
# @return [OpenStudio::Point3dVector] unique points (see logs if empty)
|
2496
|
+
def getUniques(pts = nil, n = 0)
|
1119
2497
|
mth = "OSut::#{__callee__}"
|
1120
|
-
|
2498
|
+
pts = to_p3Dv(pts)
|
2499
|
+
ok = n.respond_to?(:to_i)
|
2500
|
+
v = OpenStudio::Point3dVector.new
|
2501
|
+
return v if pts.empty?
|
2502
|
+
return mismatch("n unique points", n, Integer, mth, DBG, v) unless ok
|
1121
2503
|
|
1122
|
-
|
1123
|
-
return invalid("usi", mth, 1, WRN, 0.1216) if usi > 8.0
|
1124
|
-
return negative("usi", mth, WRN, 0.1216) if usi < 0
|
1125
|
-
return zero("usi", mth, WRN, 0.1216) if usi.abs < TOL
|
2504
|
+
pts.each { |pt| v << pt unless holds?(v, pt) }
|
1126
2505
|
|
1127
|
-
|
2506
|
+
n = n.to_i
|
2507
|
+
n = 0 unless n.abs < v.size
|
2508
|
+
v = v[0..n] if n > 0
|
2509
|
+
v = v[n..-1] if n < 0
|
1128
2510
|
|
1129
|
-
|
1130
|
-
return rsi + 1 / (1.788041 * usi - 2.886625)
|
2511
|
+
v
|
1131
2512
|
end
|
1132
2513
|
|
1133
2514
|
##
|
1134
|
-
#
|
2515
|
+
# Returns sequential non-collinear points in an OpenStudio 3D point vector.
|
1135
2516
|
#
|
1136
|
-
# @param
|
1137
|
-
# @param
|
1138
|
-
# @param t [Float] gas temperature (°C) (optional)
|
2517
|
+
# @param pts [Set<OpenStudio::Point3d] 3D points
|
2518
|
+
# @param n [#to_i] requested number of non-collinears (0 returns all)
|
1139
2519
|
#
|
1140
|
-
# @return [
|
1141
|
-
def
|
1142
|
-
# This is adapted from BTAP's Material Module's "get_conductance" (P. Lopez)
|
1143
|
-
#
|
1144
|
-
# https://github.com/NREL/OpenStudio-Prototype-Buildings/blob/
|
1145
|
-
# c3d5021d8b7aef43e560544699fb5c559e6b721d/lib/btap/measures/
|
1146
|
-
# btap_equest_converter/envelope.rb#L122
|
2520
|
+
# @return [OpenStudio::Point3dVector] non-collinears (see logs if empty)
|
2521
|
+
def getNonCollinears(pts = nil, n = 0)
|
1147
2522
|
mth = "OSut::#{__callee__}"
|
1148
|
-
|
1149
|
-
|
1150
|
-
|
1151
|
-
|
1152
|
-
|
1153
|
-
|
1154
|
-
|
1155
|
-
|
1156
|
-
|
1157
|
-
|
1158
|
-
|
1159
|
-
|
1160
|
-
|
1161
|
-
|
1162
|
-
|
1163
|
-
|
1164
|
-
|
1165
|
-
|
1166
|
-
|
1167
|
-
|
1168
|
-
return 1 / m.to_SimpleGlazing.get.uFactor unless empty
|
1169
|
-
|
1170
|
-
empty = m.to_StandardGlazing.empty?
|
1171
|
-
rsi += m.to_StandardGlazing.get.thermalResistance unless empty
|
1172
|
-
empty = m.to_RefractionExtinctionGlazing.empty?
|
1173
|
-
rsi += m.to_RefractionExtinctionGlazing.get.thermalResistance unless empty
|
1174
|
-
empty = m.to_Gas.empty?
|
1175
|
-
rsi += m.to_Gas.get.getThermalResistance(t) unless empty
|
1176
|
-
empty = m.to_GasMixture.empty?
|
1177
|
-
rsi += m.to_GasMixture.get.getThermalResistance(t) unless empty
|
2523
|
+
pts = getUniques(pts)
|
2524
|
+
ok = n.respond_to?(:to_i)
|
2525
|
+
v = OpenStudio::Point3dVector.new
|
2526
|
+
a = []
|
2527
|
+
return pts if pts.size < 2
|
2528
|
+
return mismatch("n non-collinears", n, Integer, mth, DBG, v) unless ok
|
2529
|
+
|
2530
|
+
# Evaluate cross product of vectors of 3x sequential points.
|
2531
|
+
pts.each_with_index do |p2, i2|
|
2532
|
+
i1 = i2 - 1
|
2533
|
+
i3 = i2 + 1
|
2534
|
+
i3 = 0 if i3 == pts.size
|
2535
|
+
p1 = pts[i1]
|
2536
|
+
p3 = pts[i3]
|
2537
|
+
v13 = p3 - p1
|
2538
|
+
v12 = p2 - p1
|
2539
|
+
next if v12.cross(v13).length < TOL
|
2540
|
+
|
2541
|
+
a << p2
|
2542
|
+
end
|
1178
2543
|
|
1179
|
-
|
1180
|
-
|
1181
|
-
rsi += m.to_StandardOpaqueMaterial.get.thermalResistance unless empty
|
1182
|
-
empty = m.to_MasslessOpaqueMaterial.empty?
|
1183
|
-
rsi += m.to_MasslessOpaqueMaterial.get.thermalResistance unless empty
|
1184
|
-
empty = m.to_RoofVegetation.empty?
|
1185
|
-
rsi += m.to_RoofVegetation.get.thermalResistance unless empty
|
1186
|
-
empty = m.to_AirGap.empty?
|
1187
|
-
rsi += m.to_AirGap.get.thermalResistance unless empty
|
2544
|
+
if holds?(a, pts[0])
|
2545
|
+
a = a.rotate(-1) unless same?(a[0], pts[0])
|
1188
2546
|
end
|
1189
2547
|
|
1190
|
-
|
2548
|
+
n = n.to_i
|
2549
|
+
n = 0 unless n.abs < pts.size
|
2550
|
+
a = a[0..n] if n > 0
|
2551
|
+
a = a[n..-1] if n < 0
|
2552
|
+
|
2553
|
+
to_p3Dv(a)
|
1191
2554
|
end
|
1192
2555
|
|
1193
2556
|
##
|
1194
|
-
#
|
1195
|
-
#
|
1196
|
-
#
|
1197
|
-
#
|
2557
|
+
# Returns paired sequential points as (non-zero length) line segments. If the
|
2558
|
+
# set strictly holds 2x unique points, a single segment is returned.
|
2559
|
+
# Otherwise, the returned number of segments equals the number of unique
|
2560
|
+
# points. If non-collinearity is requested, then the number of returned
|
2561
|
+
# segments equals the number of non-colliear points.
|
1198
2562
|
#
|
1199
|
-
# @param
|
2563
|
+
# @param pts [Set<OpenStudio::Point3d>] 3D points
|
2564
|
+
# @param co [Bool] whether to keep collinear points
|
1200
2565
|
#
|
1201
|
-
# @return [
|
1202
|
-
|
1203
|
-
def insulatingLayer(lc = nil)
|
2566
|
+
# @return [OpenStudio::Point3dVectorVector] line segments (see logs if empty)
|
2567
|
+
def getSegments(pts = nil, co = false)
|
1204
2568
|
mth = "OSut::#{__callee__}"
|
1205
|
-
|
1206
|
-
|
1207
|
-
|
1208
|
-
|
1209
|
-
return
|
1210
|
-
|
1211
|
-
|
1212
|
-
|
1213
|
-
|
1214
|
-
|
1215
|
-
|
1216
|
-
|
1217
|
-
|
1218
|
-
|
1219
|
-
|
1220
|
-
|
1221
|
-
else
|
1222
|
-
res[:r ] = m.thermalResistance
|
1223
|
-
res[:index] = i
|
1224
|
-
res[:type ] = :massless
|
1225
|
-
end
|
1226
|
-
end
|
1227
|
-
|
1228
|
-
unless m.to_StandardOpaqueMaterial.empty?
|
1229
|
-
m = m.to_StandardOpaqueMaterial.get
|
1230
|
-
k = m.thermalConductivity
|
1231
|
-
d = m.thickness
|
1232
|
-
|
1233
|
-
if d < 0.003 || k > 3.0 || d / k < res[:r]
|
1234
|
-
i += 1
|
1235
|
-
next
|
1236
|
-
else
|
1237
|
-
res[:r ] = d / k
|
1238
|
-
res[:index] = i
|
1239
|
-
res[:type ] = :standard
|
1240
|
-
end
|
1241
|
-
end
|
1242
|
-
|
1243
|
-
i += 1
|
2569
|
+
vv = OpenStudio::Point3dVectorVector.new
|
2570
|
+
co = false unless [true, false].include?(co)
|
2571
|
+
pts = getNonCollinears(pts) unless co
|
2572
|
+
pts = getUniques(pts) if co
|
2573
|
+
return vv if pts.size < 2
|
2574
|
+
|
2575
|
+
pts.each_with_index do |p1, i1|
|
2576
|
+
i2 = i1 + 1
|
2577
|
+
i2 = 0 if i2 == pts.size
|
2578
|
+
p2 = pts[i2]
|
2579
|
+
|
2580
|
+
line = OpenStudio::Point3dVector.new
|
2581
|
+
line << p1
|
2582
|
+
line << p2
|
2583
|
+
vv << line
|
2584
|
+
break if pts.size == 2
|
1244
2585
|
end
|
1245
2586
|
|
1246
|
-
|
2587
|
+
vv
|
1247
2588
|
end
|
1248
2589
|
|
1249
2590
|
##
|
1250
|
-
#
|
2591
|
+
# Returns points as (non-zero length) 'triads', i.e. 3x sequential points.
|
2592
|
+
# If the set holds less than 3x unique points, an empty triad is
|
2593
|
+
# returned. Otherwise, the returned number of triads equals the number of
|
2594
|
+
# unique points. If non-collinearity is requested, then the number of
|
2595
|
+
# returned triads equals the number of non-collinear points.
|
1251
2596
|
#
|
1252
|
-
# @param
|
1253
|
-
# @param
|
2597
|
+
# @param pts [OpenStudio::Point3dVector] 3D points
|
2598
|
+
# @param co [Bool] whether to keep collinear points
|
1254
2599
|
#
|
1255
|
-
# @return [
|
1256
|
-
|
1257
|
-
def transforms(model = nil, group = nil)
|
2600
|
+
# @return [OpenStudio::Point3dVectorVector] triads (see logs if empty)
|
2601
|
+
def getTriads(pts = nil, co = false)
|
1258
2602
|
mth = "OSut::#{__callee__}"
|
1259
|
-
|
1260
|
-
|
1261
|
-
|
1262
|
-
|
1263
|
-
return
|
1264
|
-
|
1265
|
-
|
1266
|
-
|
1267
|
-
|
1268
|
-
|
1269
|
-
|
1270
|
-
|
2603
|
+
vv = OpenStudio::Point3dVectorVector.new
|
2604
|
+
co = false unless [true, false].include?(co)
|
2605
|
+
pts = getNonCollinears(pts) unless co
|
2606
|
+
pts = getUniques(pts) if co
|
2607
|
+
return vv if pts.size < 2
|
2608
|
+
|
2609
|
+
pts.each_with_index do |p1, i1|
|
2610
|
+
i2 = i1 + 1
|
2611
|
+
i2 = 0 if i2 == pts.size
|
2612
|
+
i3 = i2 + 1
|
2613
|
+
i3 = 0 if i3 == pts.size
|
2614
|
+
p2 = pts[i2]
|
2615
|
+
p3 = pts[i3]
|
2616
|
+
|
2617
|
+
tri = OpenStudio::Point3dVector.new
|
2618
|
+
tri << p1
|
2619
|
+
tri << p2
|
2620
|
+
tri << p3
|
2621
|
+
vv << tri
|
2622
|
+
end
|
1271
2623
|
|
1272
|
-
|
2624
|
+
vv
|
1273
2625
|
end
|
1274
2626
|
|
1275
2627
|
##
|
1276
|
-
#
|
2628
|
+
# Determines if pre-'aligned' OpenStudio 3D points are listed clockwise.
|
1277
2629
|
#
|
1278
|
-
# @param
|
1279
|
-
# @param m [Float] a scalar
|
2630
|
+
# @param pts [OpenStudio::Point3dVector] 3D points
|
1280
2631
|
#
|
1281
|
-
# @return [
|
1282
|
-
# @return [
|
1283
|
-
def
|
2632
|
+
# @return [Bool] whether sequence is clockwise
|
2633
|
+
# @return [false] if invalid input (see logs)
|
2634
|
+
def clockwise?(pts = nil)
|
1284
2635
|
mth = "OSut::#{__callee__}"
|
1285
|
-
|
1286
|
-
|
2636
|
+
pts = to_p3Dv(pts)
|
2637
|
+
n = false
|
2638
|
+
return invalid("points (3+)", mth, 1, DBG, n) if pts.size < 3
|
2639
|
+
return invalid("points (aligned)", mth, 1, DBG, n) unless xyz?(pts, :z, 0)
|
1287
2640
|
|
1288
|
-
|
1289
|
-
return mismatch("x", v.x, cl2, mth, DBG, v) unless v.x.respond_to?(:to_f)
|
1290
|
-
return mismatch("y", v.y, cl2, mth, DBG, v) unless v.y.respond_to?(:to_f)
|
1291
|
-
return mismatch("z", v.z, cl2, mth, DBG, v) unless v.z.respond_to?(:to_f)
|
1292
|
-
return mismatch("m", m, cl2, mth, DBG, v) unless m.respond_to?(:to_f)
|
1293
|
-
|
1294
|
-
OpenStudio::Vector3d.new(m * v.x, m * v.y, m * v.z)
|
2641
|
+
OpenStudio.pointInPolygon(pts.first, pts, TOL)
|
1295
2642
|
end
|
1296
2643
|
|
1297
2644
|
##
|
1298
|
-
#
|
2645
|
+
# Returns 'aligned' OpenStudio 3D points conforming to Openstudio's
|
2646
|
+
# counterclockwise UpperLeftCorner (ULC) convention.
|
1299
2647
|
#
|
1300
|
-
# @param pts [
|
2648
|
+
# @param pts [Set<OpenStudio::Point3d>] aligned 3D points
|
1301
2649
|
#
|
1302
|
-
# @return [
|
1303
|
-
def
|
2650
|
+
# @return [OpenStudio::Point3dVector] ULC points (see logs if empty)
|
2651
|
+
def ulc(pts = nil)
|
1304
2652
|
mth = "OSut::#{__callee__}"
|
1305
|
-
|
1306
|
-
cl2 = OpenStudio::Point3d
|
2653
|
+
pts = to_p3Dv(pts)
|
1307
2654
|
v = OpenStudio::Point3dVector.new
|
2655
|
+
p0 = OpenStudio::Point3d.new(0,0,0)
|
2656
|
+
i0 = nil
|
1308
2657
|
|
1309
|
-
|
1310
|
-
return
|
2658
|
+
return invalid("points (3+)", mth, 1, DBG, v) if pts.size < 3
|
2659
|
+
return invalid("points (aligned)", mth, 1, DBG, v) unless xyz?(pts, :z, 0)
|
1311
2660
|
|
1312
|
-
|
1313
|
-
pts
|
2661
|
+
# Ensure counterclockwise sequence.
|
2662
|
+
pts = pts.to_a
|
2663
|
+
pts = pts.reverse if clockwise?(pts)
|
1314
2664
|
|
1315
|
-
|
2665
|
+
# Fetch index of candidate (0,0,0) point (i == 1, in most cases). Resort
|
2666
|
+
# to last X == 0 point. Leave as is if failed attempts.
|
2667
|
+
i0 = pts.index { |pt| same?(pt, p0) }
|
2668
|
+
i0 = pts.rindex { |pt| pt.x.abs < TOL } if i0.nil?
|
2669
|
+
|
2670
|
+
unless i0.nil?
|
2671
|
+
i = pts.size - 1
|
2672
|
+
i = i0 - 1 unless i0 == 0
|
2673
|
+
pts = pts.rotate(i)
|
2674
|
+
end
|
2675
|
+
|
2676
|
+
to_p3Dv(pts)
|
1316
2677
|
end
|
1317
2678
|
|
1318
2679
|
##
|
1319
|
-
#
|
1320
|
-
#
|
1321
|
-
#
|
1322
|
-
#
|
1323
|
-
#
|
1324
|
-
#
|
1325
|
-
#
|
1326
|
-
# @
|
1327
|
-
# @
|
1328
|
-
|
2680
|
+
# Returns an OpenStudio 3D point vector as basis for a valid OpenStudio 3D
|
2681
|
+
# polygon. In addition to basic OpenStudio polygon tests (e.g. all points
|
2682
|
+
# sharing the same 3D plane, non-self-intersecting), the method can
|
2683
|
+
# optionally check for convexity, or ensure uniqueness and/or collinearity.
|
2684
|
+
# Returned vector can also be 'aligned', as well as in UpperLeftCorner (ULC)
|
2685
|
+
# counterclockwise sequence, or in clockwise sequence.
|
2686
|
+
#
|
2687
|
+
# @param pts [Set<OpenStudio::Point3d>] 3D points
|
2688
|
+
# @param vx [Bool] whether to check for convexity
|
2689
|
+
# @param uq [Bool] whether to ensure uniqueness
|
2690
|
+
# @param co [Bool] whether to ensure non-collinearity
|
2691
|
+
# @param tt [Bool, OpenStudio::Transformation] whether to 'align'
|
2692
|
+
# @param sq [:no, :ulc, :cw] unaltered, ULC or clockwise sequence
|
2693
|
+
#
|
2694
|
+
# @return [OpenStudio::Point3dVector] 3D points (see logs if empty)
|
2695
|
+
def poly(pts = nil, vx = false, uq = false, co = true, tt = false, sq = :no)
|
1329
2696
|
mth = "OSut::#{__callee__}"
|
1330
|
-
|
1331
|
-
|
1332
|
-
|
2697
|
+
pts = to_p3Dv(pts)
|
2698
|
+
cl = OpenStudio::Transformation
|
2699
|
+
v = OpenStudio::Point3dVector.new
|
2700
|
+
vx = false unless [true, false].include?(vx)
|
2701
|
+
uq = false unless [true, false].include?(uq)
|
2702
|
+
co = true unless [true, false].include?(co)
|
1333
2703
|
|
1334
|
-
|
1335
|
-
|
2704
|
+
# --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- #
|
2705
|
+
# Exit if mismatched/invalid arguments.
|
2706
|
+
ok1 = tt == true || tt == false || tt.is_a?(cl)
|
2707
|
+
ok2 = sq == :no || sq == :ulc || sq == :cw
|
2708
|
+
return invalid("transformation", mth, 5, DBG, v) unless ok1
|
2709
|
+
return invalid("sequence", mth, 6, DBG, v) unless ok2
|
1336
2710
|
|
1337
|
-
|
1338
|
-
|
1339
|
-
|
1340
|
-
|
2711
|
+
# --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- #
|
2712
|
+
# Basic tests:
|
2713
|
+
p3 = getNonCollinears(pts, 3)
|
2714
|
+
return empty("polygon", mth, ERR, v) if p3.size < 3
|
1341
2715
|
|
1342
|
-
|
1343
|
-
valid2 = p2.is_a?(cl1) || p2.is_a?(Array)
|
2716
|
+
pln = OpenStudio::Plane.new(p3)
|
1344
2717
|
|
1345
|
-
|
1346
|
-
|
1347
|
-
|
1348
|
-
return empty(i2, mth, ERR, a) if p2.empty?
|
2718
|
+
pts.each do |pt|
|
2719
|
+
return empty("plane", mth, ERR, v) unless pln.pointOnPlane(pt)
|
2720
|
+
end
|
1349
2721
|
|
1350
|
-
|
1351
|
-
|
2722
|
+
t = tt
|
2723
|
+
t = OpenStudio::Transformation.alignFace(pts) unless tt.is_a?(cl)
|
2724
|
+
a = (t.inverse * pts).reverse
|
1352
2725
|
|
1353
|
-
|
1354
|
-
|
1355
|
-
|
1356
|
-
|
2726
|
+
if tt.is_a?(cl)
|
2727
|
+
# Using a transformation that is most likely not specific to pts. The
|
2728
|
+
# most probable reason to retain this option is when testing for polygon
|
2729
|
+
# intersections, unions, etc., operations that typically require that
|
2730
|
+
# points remain nonetheless 'aligned'. If re-activated, this logs a
|
2731
|
+
# warning if aligned points aren't @Z =0, before 'flattening'.
|
2732
|
+
#
|
2733
|
+
# invalid("points (non-aligned)", mth, 1, WRN) unless xyz?(a, :z, 0)
|
2734
|
+
a = flatten(a).to_a unless xyz?(a, :z, 0)
|
2735
|
+
end
|
1357
2736
|
|
1358
|
-
|
1359
|
-
|
1360
|
-
|
1361
|
-
|
1362
|
-
return
|
2737
|
+
# The following 2x lines are commented out. This is a very commnon and very
|
2738
|
+
# useful test, yet tested cases are first caught by the 'pointOnPlane'
|
2739
|
+
# test above. Keeping it for possible further testing.
|
2740
|
+
# bad = OpenStudio.selfIntersects(a, TOL)
|
2741
|
+
# return invalid("points (intersecting)", mth, 1, ERR, v) if bad
|
1363
2742
|
|
1364
|
-
|
1365
|
-
|
1366
|
-
|
1367
|
-
|
2743
|
+
# --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- #
|
2744
|
+
# Ensure uniqueness and/or non-collinearity. Preserve original sequence.
|
2745
|
+
p0 = a.first
|
2746
|
+
a = OpenStudio.simplify(a, false, TOL) if uq
|
2747
|
+
a = OpenStudio.simplify(a, true, TOL) unless co
|
2748
|
+
i0 = a.index { |pt| same?(pt, p0) }
|
2749
|
+
a = a.rotate(i0) unless i0.nil?
|
1368
2750
|
|
1369
|
-
|
1370
|
-
|
1371
|
-
|
1372
|
-
|
2751
|
+
# --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- #
|
2752
|
+
# Check for convexity (optional).
|
2753
|
+
if vx
|
2754
|
+
a1 = OpenStudio.simplify(a, true, TOL).reverse
|
2755
|
+
dX = a1.max_by(&:x).x.abs
|
2756
|
+
dY = a1.max_by(&:y).y.abs
|
2757
|
+
d = [dX, dY].max
|
2758
|
+
return false if d < TOL
|
2759
|
+
|
2760
|
+
u = OpenStudio::Vector3d.new(0, 0, d)
|
2761
|
+
|
2762
|
+
a1.each_with_index do |p1, i1|
|
2763
|
+
i2 = i1 + 1
|
2764
|
+
i2 = 0 if i2 == a1.size
|
2765
|
+
p2 = a1[i2]
|
2766
|
+
pi = p1 + u
|
2767
|
+
vi = OpenStudio::Point3dVector.new
|
2768
|
+
vi << pi
|
2769
|
+
vi << p1
|
2770
|
+
vi << p2
|
2771
|
+
plane = OpenStudio::Plane.new(vi)
|
2772
|
+
normal = plane.outwardNormal
|
2773
|
+
|
2774
|
+
a1.each do |p3|
|
2775
|
+
next if same?(p1, p3)
|
2776
|
+
next if same?(p2, p3)
|
2777
|
+
next if plane.pointOnPlane(p3)
|
2778
|
+
next if normal.dot(p3 - p1) < 0
|
2779
|
+
|
2780
|
+
return invalid("points (non-convex)", mth, 1, ERR, v)
|
2781
|
+
end
|
2782
|
+
end
|
2783
|
+
end
|
1373
2784
|
|
1374
|
-
|
1375
|
-
|
1376
|
-
|
2785
|
+
# --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- #
|
2786
|
+
# Alter sequence (optional).
|
2787
|
+
unless tt
|
2788
|
+
case sq
|
2789
|
+
when :ulc
|
2790
|
+
a = to_p3Dv(t * ulc(a.reverse))
|
2791
|
+
when :cw
|
2792
|
+
a = to_p3Dv(t * a)
|
2793
|
+
a = OpenStudio.reverse(a) unless clockwise?(a)
|
2794
|
+
else
|
2795
|
+
a = to_p3Dv(t * a.reverse)
|
2796
|
+
end
|
2797
|
+
else
|
2798
|
+
case sq
|
2799
|
+
when :ulc
|
2800
|
+
a = ulc(a.reverse)
|
2801
|
+
when :cw
|
2802
|
+
a = to_p3Dv(a)
|
2803
|
+
a = OpenStudio.reverse(a) unless clockwise?(a)
|
2804
|
+
else
|
2805
|
+
a = to_p3Dv(a.reverse)
|
2806
|
+
end
|
2807
|
+
end
|
1377
2808
|
|
1378
|
-
|
2809
|
+
a
|
2810
|
+
end
|
1379
2811
|
|
1380
|
-
|
1381
|
-
|
1382
|
-
|
2812
|
+
##
|
2813
|
+
# Returns 'width' of a set of OpenStudio 3D points (perpendicular view).
|
2814
|
+
#
|
2815
|
+
# @param pts [Set<OpenStudio::Point3d>] 3D points
|
2816
|
+
#
|
2817
|
+
# @return [Float] left-to-right width
|
2818
|
+
# @return [0.0] if invalid inputs (see logs)
|
2819
|
+
def width(pts = nil)
|
2820
|
+
mth = "OSut::#{__callee__}"
|
1383
2821
|
|
1384
|
-
true
|
2822
|
+
poly(pts, false, true, false, true).max_by(&:x).x
|
1385
2823
|
end
|
1386
2824
|
|
1387
2825
|
##
|
1388
|
-
#
|
2826
|
+
# Returns 'height' of a set of OpenStudio 3D points (perpendicular view).
|
1389
2827
|
#
|
1390
|
-
# @param
|
1391
|
-
# @param p2 [OpenStudio::Point3dVector] or Point3D array of polygon #2
|
1392
|
-
# @param id1 [String] polygon #1 identifier (optional)
|
1393
|
-
# @param id2 [String] polygon #2 identifier (optional)
|
2828
|
+
# @param pts [Set<OpenStudio::Point3d>] 3D points
|
1394
2829
|
#
|
1395
|
-
# @return
|
1396
|
-
# @return [
|
1397
|
-
def
|
2830
|
+
# @return [Float] top-to-bottom height
|
2831
|
+
# @return [0.0] if invalid inputs (see logs)
|
2832
|
+
def height(pts = nil)
|
1398
2833
|
mth = "OSut::#{__callee__}"
|
1399
|
-
|
1400
|
-
|
1401
|
-
|
1402
|
-
|
1403
|
-
|
1404
|
-
|
1405
|
-
|
1406
|
-
|
1407
|
-
|
1408
|
-
|
1409
|
-
|
1410
|
-
|
1411
|
-
|
1412
|
-
|
1413
|
-
|
1414
|
-
|
1415
|
-
|
1416
|
-
|
1417
|
-
return
|
1418
|
-
|
1419
|
-
|
1420
|
-
|
1421
|
-
|
1422
|
-
|
1423
|
-
|
1424
|
-
|
1425
|
-
|
1426
|
-
return
|
1427
|
-
return
|
1428
|
-
|
1429
|
-
|
1430
|
-
|
1431
|
-
|
1432
|
-
return
|
1433
|
-
return false if ft_p2.empty?
|
1434
|
-
|
1435
|
-
area1 = OpenStudio.getArea(ft_p1)
|
1436
|
-
area2 = OpenStudio.getArea(ft_p2)
|
1437
|
-
return empty("#{i1} area", mth, ERR, a) if area1.empty?
|
1438
|
-
return empty("#{i2} area", mth, ERR, a) if area2.empty?
|
2834
|
+
|
2835
|
+
poly(pts, false, true, false, true).max_by(&:y).y
|
2836
|
+
end
|
2837
|
+
|
2838
|
+
##
|
2839
|
+
# Determines whether a 1st OpenStudio polygon fits in a 2nd polygon.
|
2840
|
+
#
|
2841
|
+
# @param p1 [Set<OpenStudio::Point3d>] 1st set of 3D points
|
2842
|
+
# @param p2 [Set<OpenStudio::Point3d>] 2nd set of 3D points
|
2843
|
+
# @param flat [Bool] whether points are to be pre-flattened (Z=0)
|
2844
|
+
#
|
2845
|
+
# @return [Bool] whether 1st polygon fits within the 2nd polygon
|
2846
|
+
# @return [false] if invalid input (see logs)
|
2847
|
+
def fits?(p1 = nil, p2 = nil, flat = true)
|
2848
|
+
mth = "OSut::#{__callee__}"
|
2849
|
+
p1 = poly(p1, false, true, false)
|
2850
|
+
p2 = poly(p2, false, true, false)
|
2851
|
+
flat = true unless [true, false].include?(flat)
|
2852
|
+
return false if p1.empty?
|
2853
|
+
return false if p2.empty?
|
2854
|
+
|
2855
|
+
# Aligned, clockwise points using transformation from 2nd polygon.
|
2856
|
+
t = OpenStudio::Transformation.alignFace(p2)
|
2857
|
+
p1 = poly(p1, false, false, true, t, :cw)
|
2858
|
+
p2 = poly(p2, false, false, true, t, :cw)
|
2859
|
+
p1 = flatten(p1) if flat
|
2860
|
+
p2 = flatten(p2) if flat
|
2861
|
+
return false if p1.empty?
|
2862
|
+
return false if p2.empty?
|
2863
|
+
|
2864
|
+
area1 = OpenStudio.getArea(p1)
|
2865
|
+
area2 = OpenStudio.getArea(p2)
|
2866
|
+
return empty("points 1 area", mth, ERR, false) if area1.empty?
|
2867
|
+
return empty("points 2 area", mth, ERR, false) if area2.empty?
|
1439
2868
|
|
1440
2869
|
area1 = area1.get
|
1441
2870
|
area2 = area2.get
|
1442
|
-
union = OpenStudio.join(
|
1443
|
-
return
|
2871
|
+
union = OpenStudio.join(p1, p2, TOL2)
|
2872
|
+
return false if union.empty?
|
1444
2873
|
|
1445
2874
|
union = union.get
|
1446
2875
|
area = OpenStudio.getArea(union)
|
1447
|
-
return
|
2876
|
+
return false if area.empty?
|
1448
2877
|
|
1449
2878
|
area = area.get
|
1450
|
-
return false if area < TOL
|
1451
2879
|
|
1452
|
-
|
1453
|
-
|
2880
|
+
if area > TOL
|
2881
|
+
return true if (area - area2).abs < TOL
|
2882
|
+
end
|
1454
2883
|
|
1455
|
-
|
2884
|
+
false
|
1456
2885
|
end
|
1457
2886
|
|
1458
2887
|
##
|
1459
|
-
#
|
2888
|
+
# Determines whether OpenStudio polygons overlap.
|
1460
2889
|
#
|
1461
|
-
# @param p1 [OpenStudio::
|
1462
|
-
# @param
|
1463
|
-
# @param
|
2890
|
+
# @param p1 [Set<OpenStudio::Point3d>] 1st set of 3D points
|
2891
|
+
# @param p2 [Set<OpenStudio::Point3d>] 2nd set of 3D points
|
2892
|
+
# @param flat [Bool] whether points are to be pre-flattened (Z=0)
|
1464
2893
|
#
|
1465
|
-
# @return [
|
1466
|
-
# @return [
|
1467
|
-
def
|
1468
|
-
mth
|
1469
|
-
|
1470
|
-
|
2894
|
+
# @return [Bool] whether polygons overlap (or fit)
|
2895
|
+
# @return [false] if invalid input (see logs)
|
2896
|
+
def overlaps?(p1 = nil, p2 = nil, flat = true)
|
2897
|
+
mth = "OSut::#{__callee__}"
|
2898
|
+
p1 = poly(p1, false, true, false)
|
2899
|
+
p2 = poly(p2, false, true, false)
|
2900
|
+
flat = true unless [true, false].include?(flat)
|
2901
|
+
return false if p1.empty?
|
2902
|
+
return false if p2.empty?
|
2903
|
+
|
2904
|
+
# Aligned, clockwise & convex points using transformation from 1st polygon.
|
2905
|
+
t = OpenStudio::Transformation.alignFace(p1)
|
2906
|
+
p1 = poly(p1, false, false, true, t, :cw)
|
2907
|
+
p2 = poly(p2, false, false, true, t, :cw)
|
2908
|
+
p1 = flatten(p1) if flat
|
2909
|
+
p2 = flatten(p2) if flat
|
2910
|
+
return false if p1.empty?
|
2911
|
+
return false if p2.empty?
|
2912
|
+
|
2913
|
+
return true if fits?(p1, p2)
|
2914
|
+
return true if fits?(p2, p1)
|
2915
|
+
|
2916
|
+
area1 = OpenStudio.getArea(p1)
|
2917
|
+
area2 = OpenStudio.getArea(p2)
|
2918
|
+
return empty("points 1 area", mth, ERR, false) if area1.empty?
|
2919
|
+
return empty("points 2 area", mth, ERR, false) if area2.empty?
|
1471
2920
|
|
1472
|
-
|
1473
|
-
|
1474
|
-
|
2921
|
+
area1 = area1.get
|
2922
|
+
area2 = area2.get
|
2923
|
+
union = OpenStudio.join(p1, p2, TOL2)
|
2924
|
+
return false if union.empty?
|
1475
2925
|
|
1476
|
-
|
1477
|
-
|
1478
|
-
return
|
1479
|
-
return invalid("width", mth, 2, DBG, p1) unless w.respond_to?(:to_f)
|
2926
|
+
union = union.get
|
2927
|
+
area = OpenStudio.getArea(union)
|
2928
|
+
return false if area.empty?
|
1480
2929
|
|
1481
|
-
|
1482
|
-
|
2930
|
+
area = area.get
|
2931
|
+
delta = area1 + area2 - area
|
1483
2932
|
|
1484
|
-
|
1485
|
-
|
1486
|
-
|
2933
|
+
if area > TOL
|
2934
|
+
return false if (area - area1).abs < TOL
|
2935
|
+
return false if (area - area2).abs < TOL
|
2936
|
+
return false if delta.abs < TOL
|
2937
|
+
return true if delta > TOL
|
2938
|
+
end
|
1487
2939
|
|
1488
|
-
|
2940
|
+
false
|
2941
|
+
end
|
1489
2942
|
|
1490
|
-
|
1491
|
-
|
1492
|
-
|
1493
|
-
|
1494
|
-
|
2943
|
+
##
|
2944
|
+
# Generates offset vertices (by width) for a 3- or 4-sided, convex polygon.
|
2945
|
+
#
|
2946
|
+
# @param p1 [Set<OpenStudio::Point3d>] OpenStudio 3D points
|
2947
|
+
# @param w [#to_f] offset width (min: 0.0254m)
|
2948
|
+
# @param v [#to_i] OpenStudio SDK version, eg '321' for "v3.2.1" (optional)
|
2949
|
+
#
|
2950
|
+
# @return [OpenStudio::Point3dVector] offset points (see logs if unaltered)
|
2951
|
+
def offset(p1 = nil, w = 0, v = 0)
|
2952
|
+
mth = "OSut::#{__callee__}"
|
2953
|
+
pts = poly(p1, true, true, false, true, :cw)
|
2954
|
+
return invalid("points", mth, 1, DBG, p1) unless [3, 4].include?(pts.size)
|
1495
2955
|
|
1496
|
-
|
1497
|
-
|
1498
|
-
offset = OpenStudio.buffer(ft_pts, w, TOL)
|
1499
|
-
return p1 if offset.empty?
|
2956
|
+
mismatch("width", w, Numeric, mth) unless w.respond_to?(:to_f)
|
2957
|
+
mismatch("version", v, Integer, mth) unless v.respond_to?(:to_i)
|
1500
2958
|
|
1501
|
-
|
1502
|
-
|
1503
|
-
|
2959
|
+
vs = OpenStudio.openStudioVersion.split(".").join.to_i
|
2960
|
+
iv = true if pts.size == 4
|
2961
|
+
v = v.to_i if v.respond_to?(:to_i)
|
2962
|
+
v = -1 unless v.respond_to?(:to_i)
|
2963
|
+
v = vs if v < 0
|
2964
|
+
w = w.to_f if w.respond_to?(:to_f)
|
2965
|
+
w = 0 unless w.respond_to?(:to_f)
|
2966
|
+
w = 0 if w < 0.0254
|
1504
2967
|
|
1505
|
-
|
1506
|
-
|
2968
|
+
unless v < 340
|
2969
|
+
t = OpenStudio::Transformation.alignFace(p1)
|
2970
|
+
offset = OpenStudio.buffer(pts, w, TOL)
|
2971
|
+
return p1 if offset.empty?
|
1507
2972
|
|
1508
|
-
return
|
1509
|
-
else
|
2973
|
+
return to_p3Dv(t * offset.get.reverse)
|
2974
|
+
else # brute force approach
|
1510
2975
|
pz = {}
|
1511
2976
|
pz[:A] = {}
|
1512
2977
|
pz[:B] = {}
|
@@ -1690,81 +3155,347 @@ module OSut
|
|
1690
3155
|
end
|
1691
3156
|
|
1692
3157
|
##
|
1693
|
-
#
|
3158
|
+
# Generates a ULC OpenStudio 3D point vector (a bounding box) that surrounds
|
3159
|
+
# multiple (smaller) OpenStudio 3D point vectors. The generated, 4-point
|
3160
|
+
# outline is optionally buffered (or offset). Frame and Divider frame widths
|
3161
|
+
# are taken into account.
|
3162
|
+
#
|
3163
|
+
# @param a [Array] sets of OpenStudio 3D points
|
3164
|
+
# @param bfr [Numeric] an optional buffer size (min: 0.0254m)
|
3165
|
+
# @param flat [Bool] if points are to be pre-flattened (Z=0)
|
3166
|
+
#
|
3167
|
+
# @return [OpenStudio::Point3dVector] ULC outline (see logs if empty)
|
3168
|
+
def outline(a = [], bfr = 0, flat = true)
|
3169
|
+
mth = "OSut::#{__callee__}"
|
3170
|
+
flat = true unless [true, false].include?(flat)
|
3171
|
+
xMIN = nil
|
3172
|
+
xMAX = nil
|
3173
|
+
yMIN = nil
|
3174
|
+
yMAX = nil
|
3175
|
+
a2 = []
|
3176
|
+
out = OpenStudio::Point3dVector.new
|
3177
|
+
cl = Array
|
3178
|
+
return mismatch("array", a, cl, mth, DBG, out) unless a.is_a?(cl)
|
3179
|
+
return empty("array", mth, DBG, out) if a.empty?
|
3180
|
+
|
3181
|
+
mismatch("buffer", bfr, Numeric, mth) unless bfr.respond_to?(:to_f)
|
3182
|
+
|
3183
|
+
bfr = bfr.to_f if bfr.respond_to?(:to_f)
|
3184
|
+
bfr = 0 unless bfr.respond_to?(:to_f)
|
3185
|
+
bfr = 0 if bfr < 0.0254
|
3186
|
+
vtx = poly(a.first)
|
3187
|
+
t = OpenStudio::Transformation.alignFace(vtx) unless vtx.empty?
|
3188
|
+
return out if vtx.empty?
|
3189
|
+
|
3190
|
+
a.each do |pts|
|
3191
|
+
points = poly(pts, false, true, false, t)
|
3192
|
+
points = flatten(points) if flat
|
3193
|
+
next if points.empty?
|
3194
|
+
|
3195
|
+
a2 << points
|
3196
|
+
end
|
3197
|
+
|
3198
|
+
a2.each do |pts|
|
3199
|
+
minX = pts.min_by(&:x).x
|
3200
|
+
maxX = pts.max_by(&:x).x
|
3201
|
+
minY = pts.min_by(&:y).y
|
3202
|
+
maxY = pts.max_by(&:y).y
|
3203
|
+
|
3204
|
+
# Consider frame width, if frame-and-divider-enabled sub surface.
|
3205
|
+
if pts.respond_to?(:allowWindowPropertyFrameAndDivider)
|
3206
|
+
fd = pts.windowPropertyFrameAndDivider
|
3207
|
+
w = 0
|
3208
|
+
w = fd.get.frameWidth unless fd.empty?
|
3209
|
+
|
3210
|
+
if w > TOL
|
3211
|
+
minX -= w
|
3212
|
+
maxX += w
|
3213
|
+
minY -= w
|
3214
|
+
maxY += w
|
3215
|
+
end
|
3216
|
+
end
|
3217
|
+
|
3218
|
+
xMIN = minX if xMIN.nil?
|
3219
|
+
xMAX = maxX if xMAX.nil?
|
3220
|
+
yMIN = minY if yMIN.nil?
|
3221
|
+
yMAX = maxY if yMAX.nil?
|
3222
|
+
|
3223
|
+
xMIN = [xMIN, minX].min
|
3224
|
+
xMAX = [xMAX, maxX].max
|
3225
|
+
yMIN = [yMIN, minY].min
|
3226
|
+
yMAX = [yMAX, maxY].max
|
3227
|
+
end
|
3228
|
+
|
3229
|
+
return negative("outline width", mth, DBG, out) if xMAX < xMIN
|
3230
|
+
return negative("outline height", mth, DBG, out) if yMAX < yMIN
|
3231
|
+
return zero("outline width", mth, DBG, out) if (xMIN - xMAX).abs < TOL
|
3232
|
+
return zero("outline height", mth, DBG, out) if (yMIN - yMAX).abs < TOL
|
3233
|
+
|
3234
|
+
# Generate ULC point 3D vector.
|
3235
|
+
out << OpenStudio::Point3d.new(xMIN, yMAX, 0)
|
3236
|
+
out << OpenStudio::Point3d.new(xMIN, yMIN, 0)
|
3237
|
+
out << OpenStudio::Point3d.new(xMAX, yMIN, 0)
|
3238
|
+
out << OpenStudio::Point3d.new(xMAX, yMAX, 0)
|
3239
|
+
|
3240
|
+
# Apply buffer, apply ULC (options).
|
3241
|
+
out = offset(out, bfr, 300) if bfr > 0.0254
|
3242
|
+
|
3243
|
+
to_p3Dv(t * out)
|
3244
|
+
end
|
3245
|
+
|
3246
|
+
##
|
3247
|
+
# Returns an array of OpenStudio space-specific surfaces that match criteria,
|
3248
|
+
# e.g. exterior, north-east facing walls in hotel "lobby". Note 'sides' rely
|
3249
|
+
# on space coordinates (not absolute model coordinates). And 'sides' are
|
3250
|
+
# exclusive, not inclusive (e.g. walls strictly north-facing or strictly
|
3251
|
+
# east-facing would not be returned if 'sides' holds [:north, :east]).
|
3252
|
+
#
|
3253
|
+
# @param spaces [Array<OpenStudio::Model::Space>] target spaces
|
3254
|
+
# @param boundary [#to_s] OpenStudio outside boundary condition
|
3255
|
+
# @param type [#to_s] OpenStudio surface type
|
3256
|
+
# @param sides [Array<Symbols>] direction keys, e.g. :north (see OSut::SIDZ)
|
3257
|
+
#
|
3258
|
+
# @return [Array<OpenStudio::Model::Surface>] surfaces (may be empty)
|
3259
|
+
def facets(spaces = [], boundary = "Outdoors", type = "Wall", sides = [])
|
3260
|
+
return [] unless spaces.respond_to?(:&)
|
3261
|
+
return [] unless sides.respond_to?(:&)
|
3262
|
+
return [] if sides.empty?
|
3263
|
+
|
3264
|
+
faces = []
|
3265
|
+
boundary = trim(boundary).downcase
|
3266
|
+
type = trim(type).downcase
|
3267
|
+
return [] if boundary.empty?
|
3268
|
+
return [] if type.empty?
|
3269
|
+
|
3270
|
+
# Keep valid sides.
|
3271
|
+
sides = sides.select { |side| SIDZ.include?(side) }
|
3272
|
+
return [] if sides.empty?
|
3273
|
+
|
3274
|
+
spaces.each do |space|
|
3275
|
+
return [] unless space.respond_to?(:setSpaceType)
|
3276
|
+
|
3277
|
+
space.surfaces.each do |s|
|
3278
|
+
next unless s.outsideBoundaryCondition.downcase == boundary
|
3279
|
+
next unless s.surfaceType.downcase == type
|
3280
|
+
|
3281
|
+
orientations = []
|
3282
|
+
orientations << :top if s.outwardNormal.z > TOL
|
3283
|
+
orientations << :bottom if s.outwardNormal.z < -TOL
|
3284
|
+
orientations << :north if s.outwardNormal.y > TOL
|
3285
|
+
orientations << :east if s.outwardNormal.x > TOL
|
3286
|
+
orientations << :south if s.outwardNormal.y < -TOL
|
3287
|
+
orientations << :west if s.outwardNormal.x < -TOL
|
3288
|
+
|
3289
|
+
faces << s if sides.all? { |o| orientations.include?(o) }
|
3290
|
+
end
|
3291
|
+
end
|
3292
|
+
|
3293
|
+
faces
|
3294
|
+
end
|
3295
|
+
|
3296
|
+
##
|
3297
|
+
# Generates an OpenStudio 3D point vector of a composite floor "slab", a
|
3298
|
+
# 'union' of multiple rectangular, horizontal floor "plates". Each plate
|
3299
|
+
# must either share an edge with (or encompass or overlap) any of the
|
3300
|
+
# preceding plates in the array. The generated slab may not be convex.
|
3301
|
+
#
|
3302
|
+
# @param [Array<Hash>] pltz individual floor plates, each holding:
|
3303
|
+
# @option pltz [Numeric] :x left corner of plate origin (bird's eye view)
|
3304
|
+
# @option pltz [Numeric] :y bottom corner of plate origin (bird's eye view)
|
3305
|
+
# @option pltz [Numeric] :dx plate width (bird's eye view)
|
3306
|
+
# @option pltz [Numeric] :dy plate depth (bird's eye view)
|
3307
|
+
# @param z [Numeric] Z-axis coordinate
|
3308
|
+
#
|
3309
|
+
# @return [OpenStudio::Point3dVector] slab vertices (see logs if empty)
|
3310
|
+
def genSlab(pltz = [], z = 0)
|
3311
|
+
mth = "OSut::#{__callee__}"
|
3312
|
+
slb = OpenStudio::Point3dVector.new
|
3313
|
+
bkp = OpenStudio::Point3dVector.new
|
3314
|
+
cl1 = Array
|
3315
|
+
cl2 = Hash
|
3316
|
+
cl3 = Numeric
|
3317
|
+
|
3318
|
+
# Input validation.
|
3319
|
+
return mismatch("plates", pltz, cl1, mth, DBG, slb) unless pltz.is_a?(cl1)
|
3320
|
+
return mismatch( "Z", z, cl3, mth, DBG, slb) unless z.is_a?(cl3)
|
3321
|
+
|
3322
|
+
pltz.each_with_index do |plt, i|
|
3323
|
+
id = "plate # #{i+1} (index #{i})"
|
3324
|
+
|
3325
|
+
return mismatch(id, plt, cl1, mth, DBG, slb) unless plt.is_a?(cl2)
|
3326
|
+
return hashkey( id, plt, :x, mth, DBG, slb) unless plt.key?(:x )
|
3327
|
+
return hashkey( id, plt, :y, mth, DBG, slb) unless plt.key?(:y )
|
3328
|
+
return hashkey( id, plt, :dx, mth, DBG, slb) unless plt.key?(:dx)
|
3329
|
+
return hashkey( id, plt, :dy, mth, DBG, slb) unless plt.key?(:dy)
|
3330
|
+
|
3331
|
+
x = plt[:x ]
|
3332
|
+
y = plt[:y ]
|
3333
|
+
dx = plt[:dx]
|
3334
|
+
dy = plt[:dy]
|
3335
|
+
|
3336
|
+
return mismatch("#{id} X", x, cl3, mth, DBG, slb) unless x.is_a?(cl3)
|
3337
|
+
return mismatch("#{id} Y", y, cl3, mth, DBG, slb) unless y.is_a?(cl3)
|
3338
|
+
return mismatch("#{id} dX", dx, cl3, mth, DBG, slb) unless dx.is_a?(cl3)
|
3339
|
+
return mismatch("#{id} dY", dy, cl3, mth, DBG, slb) unless dy.is_a?(cl3)
|
3340
|
+
return zero( "#{id} dX", mth, ERR, slb) if dx.abs < TOL
|
3341
|
+
return zero( "#{id} dY", mth, ERR, slb) if dy.abs < TOL
|
3342
|
+
end
|
3343
|
+
|
3344
|
+
# Join plates.
|
3345
|
+
pltz.each_with_index do |plt, i|
|
3346
|
+
id = "plate # #{i+1} (index #{i})"
|
3347
|
+
x = plt[:x ]
|
3348
|
+
y = plt[:y ]
|
3349
|
+
dx = plt[:dx]
|
3350
|
+
dy = plt[:dy]
|
3351
|
+
|
3352
|
+
# Adjust X if dX < 0.
|
3353
|
+
x -= -dx if dx < 0
|
3354
|
+
dx = -dx if dx < 0
|
3355
|
+
|
3356
|
+
# Adjust Y if dY < 0.
|
3357
|
+
y -= -dy if dy < 0
|
3358
|
+
dy = -dy if dy < 0
|
3359
|
+
|
3360
|
+
vtx = []
|
3361
|
+
vtx << OpenStudio::Point3d.new(x + dx, y + dy, 0)
|
3362
|
+
vtx << OpenStudio::Point3d.new(x + dx, y, 0)
|
3363
|
+
vtx << OpenStudio::Point3d.new(x, y, 0)
|
3364
|
+
vtx << OpenStudio::Point3d.new(x, y + dy, 0)
|
3365
|
+
|
3366
|
+
if slb.empty?
|
3367
|
+
slb = vtx
|
3368
|
+
else
|
3369
|
+
slab = OpenStudio.join(slb, vtx, TOL2)
|
3370
|
+
slb = slab.get unless slab.empty?
|
3371
|
+
return invalid(id, mth, 0, ERR, bkp) if slab.empty?
|
3372
|
+
end
|
3373
|
+
end
|
3374
|
+
|
3375
|
+
# Once joined, re-adjust Z-axis coordinates.
|
3376
|
+
unless z.zero?
|
3377
|
+
vtx = OpenStudio::Point3dVector.new
|
3378
|
+
slb.each { |pt| vtx << OpenStudio::Point3d.new(pt.x, pt.y, z) }
|
3379
|
+
slb = vtx
|
3380
|
+
end
|
3381
|
+
|
3382
|
+
slb
|
3383
|
+
end
|
3384
|
+
|
3385
|
+
##
|
3386
|
+
# Returns outdoor-facing, space-(related) roof/ceiling surfaces. These
|
3387
|
+
# include outdoor-facing roof/ceilings of the space per se, as well as
|
3388
|
+
# any outside-facing roof/ceiling surface of an unoccupied space
|
3389
|
+
# immediately above (e.g. a plenum) overlapping any of the roof/ceilings
|
3390
|
+
# of the space itself.
|
1694
3391
|
#
|
1695
|
-
# @param
|
3392
|
+
# @param space [OpenStudio::Model::Space] a space
|
1696
3393
|
#
|
1697
|
-
# @return [
|
1698
|
-
def
|
3394
|
+
# @return [Array<OpenStudio::Model::Surface>] surfaces (see logs if empty)
|
3395
|
+
def getRoofs(space = nil)
|
1699
3396
|
mth = "OSut::#{__callee__}"
|
1700
|
-
cl
|
3397
|
+
cl = OpenStudio::Model::Space
|
3398
|
+
return mismatch("space", space, cl, mth, DBG, []) unless space.is_a?(cl)
|
3399
|
+
|
3400
|
+
roofs = space.surfaces # outdoor-facing roofs of the space
|
3401
|
+
clngs = space.surfaces # surface-facing ceilings of the space
|
3402
|
+
|
3403
|
+
roofs = roofs.select {|s| s.surfaceType.downcase == "roofceiling"}
|
3404
|
+
roofs = roofs.select {|s| s.outsideBoundaryCondition.downcase == "outdoors"}
|
1701
3405
|
|
1702
|
-
|
3406
|
+
clngs = clngs.select {|s| s.surfaceType.downcase == "roofceiling"}
|
3407
|
+
clngs = clngs.select {|s| s.outsideBoundaryCondition.downcase == "surface"}
|
1703
3408
|
|
1704
|
-
|
1705
|
-
|
1706
|
-
|
3409
|
+
clngs.each do |ceiling|
|
3410
|
+
floor = ceiling.adjacentSurface
|
3411
|
+
next if floor.empty?
|
1707
3412
|
|
1708
|
-
|
1709
|
-
|
3413
|
+
other = floor.get.space
|
3414
|
+
next if other.empty?
|
1710
3415
|
|
1711
|
-
|
1712
|
-
v1 = s.vertices[i]
|
1713
|
-
v2 = s.vertices[i + 1] unless i == last
|
1714
|
-
v2 = s.vertices.first if i == last
|
1715
|
-
vec = v2 - v1
|
1716
|
-
bad = vec.length < TOL
|
3416
|
+
rufs = other.get.surfaces
|
1717
3417
|
|
1718
|
-
|
1719
|
-
|
1720
|
-
|
1721
|
-
|
3418
|
+
rufs = rufs.select {|s| s.surfaceType.downcase == "roofceiling"}
|
3419
|
+
rufs = rufs.select {|s| s.outsideBoundaryCondition.downcase == "outdoors"}
|
3420
|
+
next if rufs.empty?
|
3421
|
+
|
3422
|
+
# Only keep track of "other" roof(s) that "overlap" ceiling below.
|
3423
|
+
rufs.each do |ruf|
|
3424
|
+
next unless overlaps?(ceiling, ruf)
|
3425
|
+
|
3426
|
+
roofs << ruf unless roofs.include?(ruf)
|
3427
|
+
end
|
1722
3428
|
end
|
1723
3429
|
|
1724
|
-
|
1725
|
-
true
|
3430
|
+
roofs
|
1726
3431
|
end
|
1727
3432
|
|
1728
3433
|
##
|
1729
|
-
#
|
3434
|
+
# Adds sub surfaces (e.g. windows, doors, skylights) to surface.
|
1730
3435
|
#
|
1731
|
-
# @param model [OpenStudio::Model::Model] a model
|
1732
3436
|
# @param s [OpenStudio::Model::Surface] a model surface
|
1733
|
-
# @param
|
1734
|
-
# @
|
1735
|
-
# @
|
1736
|
-
#
|
1737
|
-
# @
|
1738
|
-
|
3437
|
+
# @param [Array<Hash>] subs requested attributes
|
3438
|
+
# @option subs [#to_s] :id identifier e.g. "Window 007"
|
3439
|
+
# @option subs [#to_s] :type ("FixedWindow") OpenStudio subsurface type
|
3440
|
+
# @option subs [#to_i] :count (1) number of individual subs per array
|
3441
|
+
# @option subs [#to_i] :multiplier (1) OpenStudio subsurface multiplier
|
3442
|
+
# @option subs [#frameWidth] :frame (nil) OpenStudio frame & divider object
|
3443
|
+
# @option subs [#isFenestration] :assembly (nil) OpenStudio construction
|
3444
|
+
# @option subs [#to_f] :ratio e.g. %FWR [0.0, 1.0]
|
3445
|
+
# @option subs [#to_f] :head (OSut::HEAD) e.g. door height (incl frame)
|
3446
|
+
# @option subs [#to_f] :sill (OSut::SILL) e.g. window sill (incl frame)
|
3447
|
+
# @option subs [#to_f] :height sill-to-head height
|
3448
|
+
# @option subs [#to_f] :width e.g. door width
|
3449
|
+
# @option subs [#to_f] :offset left-right centreline dX e.g. between doors
|
3450
|
+
# @option subs [#to_f] :centreline left-right dX (sub/array vs base)
|
3451
|
+
# @option subs [#to_f] :r_buffer gap between sub/array and right corner
|
3452
|
+
# @option subs [#to_f] :l_buffer gap between sub/array and left corner
|
3453
|
+
# @param clear [Bool] whether to remove current sub surfaces
|
3454
|
+
# @param bfr [#to_f] safety buffer, to maintain near other edges
|
3455
|
+
#
|
3456
|
+
# @return [Bool] whether addition is successful
|
3457
|
+
# @return [false] if invalid input (see logs)
|
3458
|
+
def addSubs(s = nil, subs = [], clear = false, bfr = 0.005)
|
1739
3459
|
mth = "OSut::#{__callee__}"
|
1740
3460
|
v = OpenStudio.openStudioVersion.split(".").join.to_i
|
1741
|
-
cl1 = OpenStudio::Model::
|
1742
|
-
cl2 =
|
1743
|
-
cl3 =
|
1744
|
-
cl4 = Hash
|
1745
|
-
cl5 = Numeric
|
3461
|
+
cl1 = OpenStudio::Model::Surface
|
3462
|
+
cl2 = Array
|
3463
|
+
cl3 = Hash
|
1746
3464
|
min = 0.050 # minimum ratio value ( 5%)
|
1747
3465
|
max = 0.950 # maximum ratio value (95%)
|
1748
3466
|
no = false
|
1749
3467
|
|
1750
3468
|
# --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- #
|
1751
3469
|
# Exit if mismatched or invalid argument classes.
|
1752
|
-
return mismatch("
|
1753
|
-
return mismatch("
|
1754
|
-
return
|
1755
|
-
return no unless surface_valid?(s)
|
3470
|
+
return mismatch("surface", s, cl2, mth, DBG, no) unless s.is_a?(cl1)
|
3471
|
+
return mismatch("subs", subs, cl3, mth, DBG, no) unless subs.is_a?(cl2)
|
3472
|
+
return empty("surface points", mth, DBG, no) if poly(s).empty?
|
1756
3473
|
|
1757
3474
|
# --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- #
|
1758
3475
|
# Clear existing sub surfaces if requested.
|
1759
3476
|
nom = s.nameString
|
3477
|
+
mdl = s.model
|
1760
3478
|
|
1761
|
-
unless
|
3479
|
+
unless [true, false].include?(clear)
|
1762
3480
|
log(WRN, "#{nom}: Keeping existing sub surfaces (#{mth})")
|
1763
3481
|
clear = false
|
1764
3482
|
end
|
1765
3483
|
|
1766
3484
|
s.subSurfaces.map(&:remove) if clear
|
1767
3485
|
|
3486
|
+
# --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- #
|
3487
|
+
# Ensure minimum safety buffer.
|
3488
|
+
if bfr.respond_to?(:to_f)
|
3489
|
+
bfr = bfr.to_f
|
3490
|
+
return negative("safety buffer", mth, ERR, no) if bfr < 0
|
3491
|
+
|
3492
|
+
msg = "Safety buffer < 5mm may generate invalid geometry (#{mth})"
|
3493
|
+
log(WRN, msg) if bfr < 0.005
|
3494
|
+
else
|
3495
|
+
log(ERR, "Setting safety buffer to 5mm (#{mth})")
|
3496
|
+
bfr = 0.005
|
3497
|
+
end
|
3498
|
+
|
1768
3499
|
# --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- #
|
1769
3500
|
# Allowable sub surface types ... & Frame&Divider enabled
|
1770
3501
|
# - "FixedWindow" | true
|
@@ -1780,47 +3511,36 @@ module OSut
|
|
1780
3511
|
stype = s.surfaceType # Wall, RoofCeiling or Floor
|
1781
3512
|
|
1782
3513
|
# --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- #
|
1783
|
-
|
1784
|
-
|
1785
|
-
|
1786
|
-
|
1787
|
-
|
1788
|
-
# - Z-axis values are represented as Y-axis values
|
1789
|
-
tr = OpenStudio::Transformation.alignFace(s.vertices)
|
1790
|
-
|
1791
|
-
# Aligned vertices of host surface, and fetch attributes.
|
1792
|
-
aligned = tr.inverse * s.vertices
|
1793
|
-
max_x = aligned.max_by(&:x).x
|
1794
|
-
max_y = aligned.max_by(&:y).y
|
1795
|
-
mid_x = max_x / 2
|
1796
|
-
mid_y = max_y / 2
|
3514
|
+
t = OpenStudio::Transformation.alignFace(s.vertices)
|
3515
|
+
max_x = width(s)
|
3516
|
+
max_y = height(s)
|
3517
|
+
mid_x = max_x / 2
|
3518
|
+
mid_y = max_y / 2
|
1797
3519
|
|
1798
3520
|
# --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- #
|
1799
3521
|
# Assign default values to certain sub keys (if missing), +more validation.
|
1800
3522
|
subs.each_with_index do |sub, index|
|
1801
|
-
return mismatch("sub", sub, cl4, mth, DBG, no) unless sub.is_a?(
|
3523
|
+
return mismatch("sub", sub, cl4, mth, DBG, no) unless sub.is_a?(cl3)
|
1802
3524
|
|
1803
3525
|
# Required key:value pairs (either set by the user or defaulted).
|
1804
|
-
sub[:
|
1805
|
-
sub[:
|
1806
|
-
sub[:count ] = 1
|
1807
|
-
sub[:multiplier] = 1
|
1808
|
-
sub[:
|
1809
|
-
sub[:
|
1810
|
-
|
1811
|
-
|
1812
|
-
|
1813
|
-
|
1814
|
-
|
1815
|
-
|
1816
|
-
|
1817
|
-
|
1818
|
-
|
1819
|
-
|
1820
|
-
|
1821
|
-
|
1822
|
-
sub[:id] = "#{nom}|#{index}" if sub[:id].empty?
|
1823
|
-
id = sub[:id]
|
3526
|
+
sub[:frame ] = nil unless sub.key?(:frame )
|
3527
|
+
sub[:assembly ] = nil unless sub.key?(:assembly )
|
3528
|
+
sub[:count ] = 1 unless sub.key?(:count )
|
3529
|
+
sub[:multiplier] = 1 unless sub.key?(:multiplier)
|
3530
|
+
sub[:id ] = "" unless sub.key?(:id )
|
3531
|
+
sub[:type ] = type unless sub.key?(:type )
|
3532
|
+
sub[:type ] = trim(sub[:type])
|
3533
|
+
sub[:id ] = trim(sub[:id])
|
3534
|
+
sub[:type ] = type if sub[:type].empty?
|
3535
|
+
sub[:id ] = "OSut|#{nom}|#{index}" if sub[:id ].empty?
|
3536
|
+
sub[:count ] = 1 unless sub[:count ].respond_to?(:to_i)
|
3537
|
+
sub[:multiplier] = 1 unless sub[:multiplier].respond_to?(:to_i)
|
3538
|
+
sub[:count ] = sub[:count ].to_i
|
3539
|
+
sub[:multiplier] = sub[:multiplier].to_i
|
3540
|
+
sub[:count ] = 1 if sub[:count ] < 1
|
3541
|
+
sub[:multiplier] = 1 if sub[:multiplier] < 1
|
3542
|
+
|
3543
|
+
id = sub[:id]
|
1824
3544
|
|
1825
3545
|
# If sub surface type is invalid, log/reset. Additional corrections may
|
1826
3546
|
# be enabled once a sub surface is actually instantiated.
|
@@ -1855,14 +3575,17 @@ module OSut
|
|
1855
3575
|
end
|
1856
3576
|
end
|
1857
3577
|
|
1858
|
-
# Log/reset negative
|
3578
|
+
# Log/reset negative float values. Set ~0.0 values to 0.0.
|
1859
3579
|
sub.each do |key, value|
|
1860
|
-
next if key == :
|
3580
|
+
next if key == :count
|
3581
|
+
next if key == :multiplier
|
1861
3582
|
next if key == :type
|
3583
|
+
next if key == :id
|
1862
3584
|
next if key == :frame
|
1863
3585
|
next if key == :assembly
|
1864
3586
|
|
1865
|
-
|
3587
|
+
ok = value.respond_to?(:to_f)
|
3588
|
+
return mismatch(key, value, Float, mth, DBG, no) unless ok
|
1866
3589
|
next if key == :centreline
|
1867
3590
|
|
1868
3591
|
negative(key, mth, WRN) if value < 0
|
@@ -1907,9 +3630,9 @@ module OSut
|
|
1907
3630
|
max_height = max_y - buffers
|
1908
3631
|
max_width = max_x - buffers
|
1909
3632
|
|
1910
|
-
# Default sub surface "head" & "sill" height
|
1911
|
-
typ_head = HEAD
|
1912
|
-
typ_sill = SILL
|
3633
|
+
# Default sub surface "head" & "sill" height, unless user-specified.
|
3634
|
+
typ_head = HEAD
|
3635
|
+
typ_sill = SILL
|
1913
3636
|
|
1914
3637
|
if sub.key?(:ratio)
|
1915
3638
|
typ_head = mid_y * (1 + sub[:ratio]) if sub[:ratio] > 0.75
|
@@ -2051,18 +3774,22 @@ module OSut
|
|
2051
3774
|
# Log/reset "width" if beyond min/max.
|
2052
3775
|
if sub.key?(:width)
|
2053
3776
|
unless sub[:width].between?(glass, max_width)
|
2054
|
-
sub[:width] = glass
|
2055
|
-
sub[:width] = max_width
|
3777
|
+
sub[:width] = glass if sub[:width] < glass
|
3778
|
+
sub[:width] = max_width if sub[:width] > max_width
|
2056
3779
|
log(WRN, "Reset '#{id}' width to #{sub[:width]} m (#{mth})")
|
2057
3780
|
end
|
2058
3781
|
end
|
2059
3782
|
|
2060
|
-
# Log/reset "count" if < 1
|
2061
|
-
if sub.
|
3783
|
+
# Log/reset "count" if < 1 (or not an Integer)
|
3784
|
+
if sub[:count].respond_to?(:to_i)
|
3785
|
+
sub[:count] = sub[:count].to_i
|
3786
|
+
|
2062
3787
|
if sub[:count] < 1
|
2063
3788
|
sub[:count] = 1
|
2064
3789
|
log(WRN, "Reset '#{id}' count to #{sub[:count]} (#{mth})")
|
2065
3790
|
end
|
3791
|
+
else
|
3792
|
+
sub[:count] = 1
|
2066
3793
|
end
|
2067
3794
|
|
2068
3795
|
sub[:count] = 1 unless sub.key?(:count)
|
@@ -2221,7 +3948,7 @@ module OSut
|
|
2221
3948
|
|
2222
3949
|
# Generate sub(s).
|
2223
3950
|
sub[:count].times do |i|
|
2224
|
-
name = "#{id}
|
3951
|
+
name = "#{id}|#{i}"
|
2225
3952
|
fr = 0
|
2226
3953
|
fr = sub[:frame].frameWidth if sub[:frame]
|
2227
3954
|
|
@@ -2230,12 +3957,12 @@ module OSut
|
|
2230
3957
|
vec << OpenStudio::Point3d.new(pos, sub[:sill], 0)
|
2231
3958
|
vec << OpenStudio::Point3d.new(pos + sub[:width], sub[:sill], 0)
|
2232
3959
|
vec << OpenStudio::Point3d.new(pos + sub[:width], sub[:head], 0)
|
2233
|
-
vec =
|
3960
|
+
vec = t * vec
|
2234
3961
|
|
2235
3962
|
# Log/skip if conflict between individual sub and base surface.
|
2236
3963
|
vc = vec
|
2237
3964
|
vc = offset(vc, fr, 300) if fr > 0
|
2238
|
-
ok = fits?(vc, s
|
3965
|
+
ok = fits?(vc, s)
|
2239
3966
|
log(ERR, "Skip '#{name}': won't fit in '#{nom}' (#{mth})") unless ok
|
2240
3967
|
break unless ok
|
2241
3968
|
|
@@ -2247,7 +3974,7 @@ module OSut
|
|
2247
3974
|
fr = fd.get.frameWidth unless fd.empty?
|
2248
3975
|
vk = sb.vertices
|
2249
3976
|
vk = offset(vk, fr, 300) if fr > 0
|
2250
|
-
oops = overlaps?(vc, vk
|
3977
|
+
oops = overlaps?(vc, vk)
|
2251
3978
|
log(ERR, "Skip '#{name}': overlaps '#{nome}' (#{mth})") if oops
|
2252
3979
|
ok = false if oops
|
2253
3980
|
break if oops
|
@@ -2255,7 +3982,7 @@ module OSut
|
|
2255
3982
|
|
2256
3983
|
break unless ok
|
2257
3984
|
|
2258
|
-
sb = OpenStudio::Model::SubSurface.new(vec,
|
3985
|
+
sb = OpenStudio::Model::SubSurface.new(vec, mdl)
|
2259
3986
|
sb.setName(name)
|
2260
3987
|
sb.setSubSurfaceType(sub[:type])
|
2261
3988
|
sb.setConstruction(sub[:assembly]) if sub[:assembly]
|