osut 0.2.8 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.github/workflows/pull_request.yml +17 -1
- data/.gitignore +2 -0
- data/README.md +11 -12
- data/lib/osut/utils.rb +2995 -680
- data/lib/osut/version.rb +1 -1
- data/osut.gemspec +3 -4
- metadata +8 -8
data/lib/osut/utils.rb
CHANGED
@@ -31,21 +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
|
-
|
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
|
47
1119
|
# relies as much as possible on space conditioning categories found in
|
48
|
-
# 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
|
+
#
|
49
1122
|
# Both documents share many similarities, regardless of nomenclature. There
|
50
1123
|
# are however noticeable differences between approaches on how a space is
|
51
1124
|
# tagged as falling into one of the aforementioned categories. First, an
|
@@ -67,11 +1140,11 @@ module OSut
|
|
67
1140
|
#
|
68
1141
|
# ... includes plenums, atria, etc.
|
69
1142
|
#
|
70
|
-
# -
|
1143
|
+
# - SEMIHEATED space: an ENCLOSED space that has a heating system
|
71
1144
|
# >= 10 W/m2, yet NOT a CONDITIONED space (see above).
|
72
1145
|
#
|
73
1146
|
# - UNCONDITIONED space: an ENCLOSED space that is NOT a conditioned
|
74
|
-
# space or a
|
1147
|
+
# space or a SEMIHEATED space (see above).
|
75
1148
|
#
|
76
1149
|
# NOTE: Crawlspaces, attics, and parking garages with natural or
|
77
1150
|
# mechanical ventilation are considered UNENCLOSED spaces.
|
@@ -92,41 +1165,116 @@ module OSut
|
|
92
1165
|
# to ASHRAE 90.1 (e.g. heating and/or cooling based, no distinction for
|
93
1166
|
# INDIRECTLY conditioned spaces like plenums).
|
94
1167
|
#
|
95
|
-
#
|
96
|
-
# is based on desired/intended design space setpoint
|
97
|
-
# system sizing criteria. No further treatment
|
98
|
-
# 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).
|
99
1175
|
#
|
100
1176
|
# The single NECB criterion distinguishing UNCONDITIONED ENCLOSED spaces
|
101
1177
|
# (such as vestibules) from UNENCLOSED spaces (such as attics) remains the
|
102
1178
|
# intention to ventilate - or rather to what degree. Regardless, the methods
|
103
|
-
# here are designed to process both classifications in the same way, namely
|
104
|
-
# focusing on adjacent surfaces to CONDITIONED (or
|
105
|
-
# of the building envelope.
|
106
|
-
|
107
|
-
# In light of the above, methods here are designed without a priori
|
108
|
-
# of explicit system sizing choices or access to iterative
|
109
|
-
# processes. As discussed in greater detail
|
110
|
-
# 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.
|
111
1190
|
#
|
112
1191
|
# For an OpenStudio model in an incomplete or preliminary state, e.g. holding
|
113
|
-
# fully-formed ENCLOSED spaces
|
114
|
-
# temperatures (early design stage assessments of form, porosity or
|
115
|
-
#
|
116
|
-
#
|
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
|
117
1254
|
#
|
118
|
-
#
|
119
|
-
#
|
120
|
-
|
121
|
-
|
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
|
122
1270
|
|
123
1271
|
##
|
124
|
-
#
|
1272
|
+
# Returns MIN/MAX values of a schedule (ruleset).
|
125
1273
|
#
|
126
|
-
# @param sched [OpenStudio::Model::ScheduleRuleset] schedule
|
1274
|
+
# @param sched [OpenStudio::Model::ScheduleRuleset] a schedule
|
127
1275
|
#
|
128
1276
|
# @return [Hash] min: (Float), max: (Float)
|
129
|
-
# @return [Hash] min: nil, max: nil
|
1277
|
+
# @return [Hash] min: nil, max: nil if invalid inputs (see logs)
|
130
1278
|
def scheduleRulesetMinMax(sched = nil)
|
131
1279
|
# Largely inspired from David Goldwasser's
|
132
1280
|
# "schedule_ruleset_annual_min_max_value":
|
@@ -137,44 +1285,28 @@ module OSut
|
|
137
1285
|
mth = "OSut::#{__callee__}"
|
138
1286
|
cl = OpenStudio::Model::ScheduleRuleset
|
139
1287
|
res = { min: nil, max: nil }
|
140
|
-
|
141
|
-
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)
|
142
1289
|
|
143
1290
|
id = sched.nameString
|
144
1291
|
return mismatch(id, sched, cl, mth, DBG, res) unless sched.is_a?(cl)
|
145
1292
|
|
146
|
-
|
147
|
-
profiles << sched.defaultDaySchedule
|
148
|
-
sched.scheduleRules.each { |rule| profiles << rule.daySchedule }
|
1293
|
+
values = sched.defaultDaySchedule.values.to_a
|
149
1294
|
|
150
|
-
|
151
|
-
id = profile.nameString
|
1295
|
+
sched.scheduleRules.each { |rule| values += rule.daySchedule.values }
|
152
1296
|
|
153
|
-
|
154
|
-
|
155
|
-
log(WRN, "Skipping non-numeric value in '#{id}' (#{mth})") unless ok
|
156
|
-
next unless ok
|
157
|
-
|
158
|
-
res[:min] = val unless res[:min]
|
159
|
-
res[:min] = val if res[:min] > val
|
160
|
-
res[:max] = val unless res[:max]
|
161
|
-
res[:max] = val if res[:max] < val
|
162
|
-
end
|
163
|
-
end
|
164
|
-
|
165
|
-
valid = res[:min] && res[:max]
|
166
|
-
log(ERR, "Invalid MIN/MAX in '#{id}' (#{mth})") unless valid
|
1297
|
+
res[:min] = values.min.is_a?(Numeric) ? values.min : nil
|
1298
|
+
res[:max] = values.max.is_a?(Numeric) ? values.max : nil
|
167
1299
|
|
168
1300
|
res
|
169
1301
|
end
|
170
1302
|
|
171
1303
|
##
|
172
|
-
#
|
1304
|
+
# Returns MIN/MAX values of a schedule (constant).
|
173
1305
|
#
|
174
|
-
# @param sched [OpenStudio::Model::ScheduleConstant] schedule
|
1306
|
+
# @param sched [OpenStudio::Model::ScheduleConstant] a schedule
|
175
1307
|
#
|
176
1308
|
# @return [Hash] min: (Float), max: (Float)
|
177
|
-
# @return [Hash] min: nil, max: nil
|
1309
|
+
# @return [Hash] min: nil, max: nil if invalid inputs (see logs)
|
178
1310
|
def scheduleConstantMinMax(sched = nil)
|
179
1311
|
# Largely inspired from David Goldwasser's
|
180
1312
|
# "schedule_constant_annual_min_max_value":
|
@@ -185,14 +1317,13 @@ module OSut
|
|
185
1317
|
mth = "OSut::#{__callee__}"
|
186
1318
|
cl = OpenStudio::Model::ScheduleConstant
|
187
1319
|
res = { min: nil, max: nil }
|
188
|
-
|
189
|
-
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)
|
190
1321
|
|
191
1322
|
id = sched.nameString
|
192
1323
|
return mismatch(id, sched, cl, mth, DBG, res) unless sched.is_a?(cl)
|
193
1324
|
|
194
|
-
|
195
|
-
mismatch("
|
1325
|
+
ok = sched.value.is_a?(Numeric)
|
1326
|
+
mismatch("#{id} value", sched.value, Numeric, mth, ERR, res) unless ok
|
196
1327
|
res[:min] = sched.value
|
197
1328
|
res[:max] = sched.value
|
198
1329
|
|
@@ -200,12 +1331,12 @@ module OSut
|
|
200
1331
|
end
|
201
1332
|
|
202
1333
|
##
|
203
|
-
#
|
1334
|
+
# Returns MIN/MAX values of a schedule (compact).
|
204
1335
|
#
|
205
1336
|
# @param sched [OpenStudio::Model::ScheduleCompact] schedule
|
206
1337
|
#
|
207
1338
|
# @return [Hash] min: (Float), max: (Float)
|
208
|
-
# @return [Hash] min: nil, max: nil
|
1339
|
+
# @return [Hash] min: nil, max: nil if invalid input (see logs)
|
209
1340
|
def scheduleCompactMinMax(sched = nil)
|
210
1341
|
# Largely inspired from Andrew Parker's
|
211
1342
|
# "schedule_compact_annual_min_max_value":
|
@@ -213,78 +1344,69 @@ module OSut
|
|
213
1344
|
# github.com/NREL/openstudio-standards/blob/
|
214
1345
|
# 99cf713750661fe7d2082739f251269c2dfd9140/lib/openstudio-standards/
|
215
1346
|
# standards/Standards.ScheduleCompact.rb#L8
|
216
|
-
mth
|
217
|
-
cl
|
218
|
-
vals
|
219
|
-
|
220
|
-
res
|
221
|
-
|
222
|
-
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)
|
223
1353
|
|
224
1354
|
id = sched.nameString
|
225
1355
|
return mismatch(id, sched, cl, mth, DBG, res) unless sched.is_a?(cl)
|
226
1356
|
|
227
1357
|
sched.extensibleGroups.each do |eg|
|
228
|
-
if
|
1358
|
+
if prev.include?("until")
|
229
1359
|
vals << eg.getDouble(0).get unless eg.getDouble(0).empty?
|
230
1360
|
end
|
231
1361
|
|
232
|
-
str
|
233
|
-
|
1362
|
+
str = eg.getString(0)
|
1363
|
+
prev = str.get.downcase unless str.empty?
|
234
1364
|
end
|
235
1365
|
|
236
|
-
return empty("
|
1366
|
+
return empty("#{id} values", mth, ERR, res) if vals.empty?
|
237
1367
|
|
238
|
-
|
239
|
-
|
240
|
-
return res unless ok
|
241
|
-
|
242
|
-
res[:min] = vals.min
|
243
|
-
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
|
244
1370
|
|
245
1371
|
res
|
246
1372
|
end
|
247
1373
|
|
248
1374
|
##
|
249
|
-
#
|
1375
|
+
# Returns MIN/MAX values for schedule (interval).
|
250
1376
|
#
|
251
1377
|
# @param sched [OpenStudio::Model::ScheduleInterval] schedule
|
252
1378
|
#
|
253
1379
|
# @return [Hash] min: (Float), max: (Float)
|
254
|
-
# @return [Hash] min: nil, max: nil
|
1380
|
+
# @return [Hash] min: nil, max: nil if invalid input (see logs)
|
255
1381
|
def scheduleIntervalMinMax(sched = nil)
|
256
1382
|
mth = "OSut::#{__callee__}"
|
257
1383
|
cl = OpenStudio::Model::ScheduleInterval
|
258
1384
|
vals = []
|
259
1385
|
res = { min: nil, max: nil }
|
260
|
-
|
261
|
-
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)
|
262
1387
|
|
263
1388
|
id = sched.nameString
|
264
1389
|
return mismatch(id, sched, cl, mth, DBG, res) unless sched.is_a?(cl)
|
265
1390
|
|
266
1391
|
vals = sched.timeSeries.values
|
267
|
-
ok = vals.min.is_a?(Numeric) && vals.max.is_a?(Numeric)
|
268
|
-
log(ERR, "Non-numeric values in '#{id}' (#{mth})") unless ok
|
269
|
-
return res unless ok
|
270
1392
|
|
271
|
-
res[:min] = vals.min
|
272
|
-
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
|
273
1395
|
|
274
1396
|
res
|
275
1397
|
end
|
276
1398
|
|
277
1399
|
##
|
278
|
-
#
|
279
|
-
# 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.
|
280
1402
|
#
|
281
1403
|
# @param zone [OpenStudio::Model::ThermalZone] a thermal zone
|
282
1404
|
#
|
283
1405
|
# @return [Hash] spt: (Float), dual: (Bool)
|
284
|
-
# @return [Hash] spt: nil, dual: false
|
1406
|
+
# @return [Hash] spt: nil, dual: false if invalid input (see logs)
|
285
1407
|
def maxHeatScheduledSetpoint(zone = nil)
|
286
1408
|
# Largely inspired from Parker & Marrec's "thermal_zone_heated?" procedure.
|
287
|
-
# The solution here is a tad more relaxed to encompass
|
1409
|
+
# The solution here is a tad more relaxed to encompass SEMIHEATED zones as
|
288
1410
|
# per Canadian NECB criteria (basically any space with at least 10 W/m2 of
|
289
1411
|
# installed heating equipement, i.e. below freezing in Canada).
|
290
1412
|
#
|
@@ -294,8 +1416,7 @@ module OSut
|
|
294
1416
|
mth = "OSut::#{__callee__}"
|
295
1417
|
cl = OpenStudio::Model::ThermalZone
|
296
1418
|
res = { spt: nil, dual: false }
|
297
|
-
|
298
|
-
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)
|
299
1420
|
|
300
1421
|
id = zone.nameString
|
301
1422
|
return mismatch(id, zone, cl, mth, DBG, res) unless zone.is_a?(cl)
|
@@ -378,8 +1499,8 @@ module OSut
|
|
378
1499
|
|
379
1500
|
return res if zone.thermostat.empty?
|
380
1501
|
|
381
|
-
tstat
|
382
|
-
res[:spt]
|
1502
|
+
tstat = zone.thermostat.get
|
1503
|
+
res[:spt] = nil
|
383
1504
|
|
384
1505
|
unless tstat.to_ThermostatSetpointDualSetpoint.empty? &&
|
385
1506
|
tstat.to_ZoneControlThermostatStagedDualSetpoint.empty?
|
@@ -392,7 +1513,7 @@ module OSut
|
|
392
1513
|
|
393
1514
|
unless tstat.heatingSetpointTemperatureSchedule.empty?
|
394
1515
|
res[:dual] = true
|
395
|
-
sched
|
1516
|
+
sched = tstat.heatingSetpointTemperatureSchedule.get
|
396
1517
|
|
397
1518
|
unless sched.to_ScheduleRuleset.empty?
|
398
1519
|
sched = sched.to_ScheduleRuleset.get
|
@@ -451,16 +1572,15 @@ module OSut
|
|
451
1572
|
end
|
452
1573
|
|
453
1574
|
##
|
454
|
-
#
|
1575
|
+
# Validates if model has zones with valid heating temperature setpoints.
|
455
1576
|
#
|
456
1577
|
# @param model [OpenStudio::Model::Model] a model
|
457
1578
|
#
|
458
|
-
# @return [Bool]
|
459
|
-
# @return [
|
1579
|
+
# @return [Bool] whether model holds valid heating temperature setpoints
|
1580
|
+
# @return [false] if invalid input (see logs)
|
460
1581
|
def heatingTemperatureSetpoints?(model = nil)
|
461
1582
|
mth = "OSut::#{__callee__}"
|
462
1583
|
cl = OpenStudio::Model::Model
|
463
|
-
|
464
1584
|
return mismatch("model", model, cl, mth, DBG, false) unless model.is_a?(cl)
|
465
1585
|
|
466
1586
|
model.getThermalZones.each do |zone|
|
@@ -471,13 +1591,13 @@ module OSut
|
|
471
1591
|
end
|
472
1592
|
|
473
1593
|
##
|
474
|
-
#
|
475
|
-
# 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.
|
476
1596
|
#
|
477
1597
|
# @param zone [OpenStudio::Model::ThermalZone] a thermal zone
|
478
1598
|
#
|
479
1599
|
# @return [Hash] spt: (Float), dual: (Bool)
|
480
|
-
# @return [Hash] spt: nil, dual: false
|
1600
|
+
# @return [Hash] spt: nil, dual: false if invalid input (see logs)
|
481
1601
|
def minCoolScheduledSetpoint(zone = nil)
|
482
1602
|
# Largely inspired from Parker & Marrec's "thermal_zone_cooled?" procedure.
|
483
1603
|
#
|
@@ -487,8 +1607,7 @@ module OSut
|
|
487
1607
|
mth = "OSut::#{__callee__}"
|
488
1608
|
cl = OpenStudio::Model::ThermalZone
|
489
1609
|
res = { spt: nil, dual: false }
|
490
|
-
|
491
|
-
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)
|
492
1611
|
|
493
1612
|
id = zone.nameString
|
494
1613
|
return mismatch(id, zone, cl, mth, DBG, res) unless zone.is_a?(cl)
|
@@ -558,8 +1677,8 @@ module OSut
|
|
558
1677
|
|
559
1678
|
return res if zone.thermostat.empty?
|
560
1679
|
|
561
|
-
tstat
|
562
|
-
res[:spt]
|
1680
|
+
tstat = zone.thermostat.get
|
1681
|
+
res[:spt] = nil
|
563
1682
|
|
564
1683
|
unless tstat.to_ThermostatSetpointDualSetpoint.empty? &&
|
565
1684
|
tstat.to_ZoneControlThermostatStagedDualSetpoint.empty?
|
@@ -572,7 +1691,7 @@ module OSut
|
|
572
1691
|
|
573
1692
|
unless tstat.coolingSetpointTemperatureSchedule.empty?
|
574
1693
|
res[:dual] = true
|
575
|
-
sched
|
1694
|
+
sched = tstat.coolingSetpointTemperatureSchedule.get
|
576
1695
|
|
577
1696
|
unless sched.to_ScheduleRuleset.empty?
|
578
1697
|
sched = sched.to_ScheduleRuleset.get
|
@@ -631,17 +1750,16 @@ module OSut
|
|
631
1750
|
end
|
632
1751
|
|
633
1752
|
##
|
634
|
-
#
|
1753
|
+
# Validates if model has zones with valid cooling temperature setpoints.
|
635
1754
|
#
|
636
1755
|
# @param model [OpenStudio::Model::Model] a model
|
637
1756
|
#
|
638
|
-
# @return [Bool]
|
639
|
-
# @return [
|
1757
|
+
# @return [Bool] whether model holds valid cooling temperature setpoints
|
1758
|
+
# @return [false] if invalid input (see logs)
|
640
1759
|
def coolingTemperatureSetpoints?(model = nil)
|
641
1760
|
mth = "OSut::#{__callee__}"
|
642
1761
|
cl = OpenStudio::Model::Model
|
643
|
-
|
644
|
-
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)
|
645
1763
|
|
646
1764
|
model.getThermalZones.each do |zone|
|
647
1765
|
return true if minCoolScheduledSetpoint(zone)[:spt]
|
@@ -651,117 +1769,318 @@ module OSut
|
|
651
1769
|
end
|
652
1770
|
|
653
1771
|
##
|
654
|
-
#
|
1772
|
+
# Validates whether space is a vestibule.
|
655
1773
|
#
|
656
|
-
# @param
|
1774
|
+
# @param space [OpenStudio::Model::Space] a space
|
657
1775
|
#
|
658
|
-
# @return [Bool]
|
659
|
-
# @return [
|
660
|
-
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.
|
661
1811
|
mth = "OSut::#{__callee__}"
|
662
|
-
cl = OpenStudio::Model::
|
1812
|
+
cl = OpenStudio::Model::Space
|
1813
|
+
return mismatch("space", space, cl, mth, DBG, false) unless space.is_a?(cl)
|
663
1814
|
|
664
|
-
|
1815
|
+
id = space.nameString
|
1816
|
+
m1 = "#{id}:vestibule"
|
1817
|
+
m2 = "#{id}:vestibule:boolean"
|
665
1818
|
|
666
|
-
|
667
|
-
|
668
|
-
return
|
669
|
-
|
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
|
670
1838
|
end
|
671
1839
|
|
672
1840
|
false
|
673
1841
|
end
|
674
1842
|
|
675
1843
|
##
|
676
|
-
#
|
1844
|
+
# Validates whether a space is an indirectly-conditioned plenum.
|
677
1845
|
#
|
678
1846
|
# @param space [OpenStudio::Model::Space] a space
|
679
|
-
# @param loops [Bool] true if model has airLoopHVAC object(s)
|
680
|
-
# @param setpoints [Bool] true if model has valid temperature setpoints
|
681
1847
|
#
|
682
|
-
# @return [Bool]
|
683
|
-
# @return [
|
684
|
-
def plenum?(space = nil
|
685
|
-
# 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?":
|
686
1852
|
#
|
687
|
-
#
|
688
|
-
#
|
689
|
-
#
|
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").
|
690
1892
|
#
|
691
|
-
#
|
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
|
-
# CASE A:
|
694
|
-
#
|
1899
|
+
# CASE A: it includes the substring "plenum" (case insensitive) in its
|
1900
|
+
# spaceType's name, or in the latter's standardsSpaceType string;
|
695
1901
|
#
|
696
|
-
# CASE B:
|
697
|
-
# total floor area yet linked to a zone holding an 'inactive'
|
698
|
-
# thermostat, i.e. can't extract valid setpoints; OR
|
1902
|
+
# CASE B: "isPlenum" == TRUE in an OpenStudio model WITH HVAC airloops: OR
|
699
1903
|
#
|
700
|
-
# CASE C:
|
701
|
-
#
|
702
|
-
#
|
1904
|
+
# CASE C: its zone holds an 'inactive' thermostat (i.e. can't extract valid
|
1905
|
+
# setpoints) in an OpenStudio model with setpoint temperatures.
|
1906
|
+
#
|
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
|
703
1912
|
mth = "OSut::#{__callee__}"
|
704
1913
|
cl = OpenStudio::Model::Space
|
705
|
-
|
706
|
-
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)
|
707
1917
|
|
708
1918
|
id = space.nameString
|
709
|
-
|
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
|
710
1950
|
|
711
|
-
|
712
|
-
|
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
|
1980
|
+
|
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
|
713
1989
|
|
714
|
-
|
715
|
-
|
1990
|
+
# 2. Check instead OSut's INDIRECTLYCONDITIONED (parent space) link.
|
1991
|
+
if cnd.nil?
|
1992
|
+
id = space.additionalProperties.getFeatureAsString(tg2)
|
716
1993
|
|
717
|
-
|
718
|
-
|
719
|
-
|
1994
|
+
unless id.empty?
|
1995
|
+
id = id.get
|
1996
|
+
dad = space.model.getSpaceByName(id)
|
720
1997
|
|
721
|
-
|
722
|
-
|
723
|
-
|
724
|
-
|
725
|
-
|
726
|
-
|
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
|
727
2005
|
end
|
728
2006
|
end
|
729
2007
|
|
730
|
-
|
731
|
-
|
732
|
-
|
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]
|
733
2019
|
end
|
734
2020
|
|
735
|
-
|
736
|
-
|
737
|
-
|
738
|
-
|
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
|
2034
|
+
end
|
2035
|
+
|
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)
|
739
2056
|
|
740
|
-
false
|
2057
|
+
ok = false
|
2058
|
+
ok = setpoints(space)[:heating].nil? && setpoints(space)[:cooling].nil?
|
2059
|
+
|
2060
|
+
ok
|
741
2061
|
end
|
742
2062
|
|
743
2063
|
##
|
744
|
-
#
|
2064
|
+
# Generates an HVAC availability schedule.
|
745
2065
|
#
|
746
2066
|
# @param model [OpenStudio::Model::Model] a model
|
747
2067
|
# @param avl [String] seasonal availability choice (optional, default "ON")
|
748
2068
|
#
|
749
2069
|
# @return [OpenStudio::Model::Schedule] HVAC availability sched
|
750
|
-
# @return [
|
2070
|
+
# @return [nil] if invalid input (see logs)
|
751
2071
|
def availabilitySchedule(model = nil, avl = "")
|
752
2072
|
mth = "OSut::#{__callee__}"
|
753
2073
|
cl = OpenStudio::Model::Model
|
754
2074
|
limits = nil
|
755
|
-
|
756
|
-
return
|
757
|
-
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)
|
758
2077
|
|
759
2078
|
# Either fetch availability ScheduleTypeLimits object, or create one.
|
760
2079
|
model.getScheduleTypeLimitss.each do |l|
|
761
|
-
break
|
762
|
-
next
|
763
|
-
next
|
764
|
-
next
|
2080
|
+
break if limits
|
2081
|
+
next if l.lowerLimitValue.empty?
|
2082
|
+
next if l.upperLimitValue.empty?
|
2083
|
+
next if l.numericType.empty?
|
765
2084
|
next unless l.lowerLimitValue.get.to_i == 0
|
766
2085
|
next unless l.upperLimitValue.get.to_i == 1
|
767
2086
|
next unless l.numericType.get.downcase == "discrete"
|
@@ -787,35 +2106,35 @@ module OSut
|
|
787
2106
|
|
788
2107
|
# Seasonal availability start/end dates.
|
789
2108
|
year = model.yearDescription
|
790
|
-
return
|
2109
|
+
return empty("yearDescription", mth, ERR) if year.empty?
|
791
2110
|
|
792
2111
|
year = year.get
|
793
2112
|
may01 = year.makeDate(OpenStudio::MonthOfYear.new("May"), 1)
|
794
2113
|
oct31 = year.makeDate(OpenStudio::MonthOfYear.new("Oct"), 31)
|
795
2114
|
|
796
|
-
case avl.
|
797
|
-
when "winter"
|
2115
|
+
case trim(avl).downcase
|
2116
|
+
when "winter" # available from November 1 to April 30 (6 months)
|
798
2117
|
val = 1
|
799
2118
|
sch = off
|
800
2119
|
nom = "WINTER Availability SchedRuleset"
|
801
2120
|
dft = "WINTER Availability dftDaySched"
|
802
2121
|
tag = "May-Oct WINTER Availability SchedRule"
|
803
2122
|
day = "May-Oct WINTER SchedRule Day"
|
804
|
-
when "summer"
|
2123
|
+
when "summer" # available from May 1 to October 31 (6 months)
|
805
2124
|
val = 0
|
806
2125
|
sch = on
|
807
2126
|
nom = "SUMMER Availability SchedRuleset"
|
808
2127
|
dft = "SUMMER Availability dftDaySched"
|
809
2128
|
tag = "May-Oct SUMMER Availability SchedRule"
|
810
2129
|
day = "May-Oct SUMMER SchedRule Day"
|
811
|
-
when "off"
|
2130
|
+
when "off" # never available
|
812
2131
|
val = 0
|
813
2132
|
sch = on
|
814
2133
|
nom = "OFF Availability SchedRuleset"
|
815
2134
|
dft = "OFF Availability dftDaySched"
|
816
2135
|
tag = ""
|
817
2136
|
day = ""
|
818
|
-
else
|
2137
|
+
else # always available
|
819
2138
|
val = 1
|
820
2139
|
sch = on
|
821
2140
|
nom = "ON Availability SchedRuleset"
|
@@ -833,14 +2152,14 @@ module OSut
|
|
833
2152
|
|
834
2153
|
unless schedule.empty?
|
835
2154
|
schedule = schedule.get
|
836
|
-
default
|
2155
|
+
default = schedule.defaultDaySchedule
|
837
2156
|
ok = ok && default.nameString == dft
|
838
2157
|
ok = ok && default.times.size == 1
|
839
2158
|
ok = ok && default.values.size == 1
|
840
2159
|
ok = ok && default.times.first == time
|
841
2160
|
ok = ok && default.values.first == val
|
842
2161
|
rules = schedule.scheduleRules
|
843
|
-
ok = ok &&
|
2162
|
+
ok = ok && rules.size < 2
|
844
2163
|
|
845
2164
|
if rules.size == 1
|
846
2165
|
rule = rules.first
|
@@ -865,30 +2184,37 @@ module OSut
|
|
865
2184
|
|
866
2185
|
schedule = OpenStudio::Model::ScheduleRuleset.new(model)
|
867
2186
|
schedule.setName(nom)
|
868
|
-
ok = schedule.setScheduleTypeLimits(limits)
|
869
|
-
log(ERR, "'#{nom}': Can't set schedule type limits (#{mth})") unless ok
|
870
|
-
return nil unless ok
|
871
2187
|
|
872
|
-
|
873
|
-
|
874
|
-
|
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
|
875
2197
|
|
876
2198
|
schedule.defaultDaySchedule.setName(dft)
|
877
2199
|
|
878
2200
|
unless tag.empty?
|
879
2201
|
rule = OpenStudio::Model::ScheduleRule.new(schedule, sch)
|
880
2202
|
rule.setName(tag)
|
881
|
-
ok = rule.setStartDate(may01)
|
882
|
-
log(ERR, "'#{tag}': Can't set start date (#{mth})") unless ok
|
883
|
-
return nil unless ok
|
884
2203
|
|
885
|
-
|
886
|
-
|
887
|
-
|
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
|
888
2213
|
|
889
|
-
|
890
|
-
|
891
|
-
|
2214
|
+
unless rule.setApplyAllDays(true)
|
2215
|
+
log(ERR, "'#{tag}': Can't apply to all days (#{mth})")
|
2216
|
+
return nil
|
2217
|
+
end
|
892
2218
|
|
893
2219
|
rule.daySchedule.setName(day)
|
894
2220
|
end
|
@@ -896,612 +2222,756 @@ module OSut
|
|
896
2222
|
schedule
|
897
2223
|
end
|
898
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
|
+
|
899
2238
|
##
|
900
|
-
#
|
2239
|
+
# Returns OpenStudio site/space transformation & rotation angle [0,2PI) rads.
|
901
2240
|
#
|
902
|
-
# @param
|
903
|
-
# @param bse [OpensStudio::Model::ConstructionBase] a construction base
|
904
|
-
# @param gr [Bool] true if ground-facing surface
|
905
|
-
# @param ex [Bool] true if exterior-facing surface
|
906
|
-
# @param typ [String] a surface type
|
2241
|
+
# @param group [OpenStudio::Model::PlanarSurfaceGroup] a site or space object
|
907
2242
|
#
|
908
|
-
# @return [
|
909
|
-
# @return [
|
910
|
-
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)
|
911
2246
|
mth = "OSut::#{__callee__}"
|
912
|
-
|
913
|
-
|
914
|
-
|
915
|
-
return invalid("set", mth, 1, DBG, false) unless set.respond_to?(NS)
|
916
|
-
|
917
|
-
id = set.nameString
|
918
|
-
return mismatch(id, set, cl1, mth, DBG, false) unless set.is_a?(cl1)
|
919
|
-
return invalid("base", mth, 2, DBG, false) unless bse.respond_to?(NS)
|
920
|
-
|
921
|
-
id = bse.nameString
|
922
|
-
return mismatch(id, bse, cl2, mth, DBG, false) unless bse.is_a?(cl2)
|
923
|
-
|
924
|
-
valid = gr == true || gr == false
|
925
|
-
return invalid("ground", mth, 3, DBG, false) unless valid
|
926
|
-
|
927
|
-
valid = ex == true || ex == false
|
928
|
-
return invalid("exterior", mth, 4, DBG, false) unless valid
|
929
|
-
|
930
|
-
valid = typ.respond_to?(:to_s)
|
931
|
-
return invalid("surface typ", mth, 4, DBG, false) unless valid
|
932
|
-
|
933
|
-
type = typ.to_s.downcase
|
934
|
-
valid = type == "floor" || type == "wall" || type == "roofceiling"
|
935
|
-
return invalid("surface type", mth, 5, DBG, false) unless valid
|
936
|
-
|
937
|
-
constructions = nil
|
938
|
-
|
939
|
-
if gr
|
940
|
-
unless set.defaultGroundContactSurfaceConstructions.empty?
|
941
|
-
constructions = set.defaultGroundContactSurfaceConstructions.get
|
942
|
-
end
|
943
|
-
elsif ex
|
944
|
-
unless set.defaultExteriorSurfaceConstructions.empty?
|
945
|
-
constructions = set.defaultExteriorSurfaceConstructions.get
|
946
|
-
end
|
947
|
-
else
|
948
|
-
unless set.defaultInteriorSurfaceConstructions.empty?
|
949
|
-
constructions = set.defaultInteriorSurfaceConstructions.get
|
950
|
-
end
|
951
|
-
end
|
2247
|
+
cl2 = OpenStudio::Model::PlanarSurfaceGroup
|
2248
|
+
res = { t: nil, r: nil }
|
2249
|
+
return invalid("group", mth, 2, DBG, res) unless group.respond_to?(NS)
|
952
2250
|
|
953
|
-
|
2251
|
+
id = group.nameString
|
2252
|
+
mdl = group.model
|
2253
|
+
return mismatch(id, group, cl2, mth, DBG, res) unless group.is_a?(cl2)
|
954
2254
|
|
955
|
-
|
956
|
-
|
957
|
-
unless constructions.roofCeilingConstruction.empty?
|
958
|
-
construction = constructions.roofCeilingConstruction.get
|
959
|
-
return true if construction == bse
|
960
|
-
end
|
961
|
-
when "floor"
|
962
|
-
unless constructions.floorConstruction.empty?
|
963
|
-
construction = constructions.floorConstruction.get
|
964
|
-
return true if construction == bse
|
965
|
-
end
|
966
|
-
else
|
967
|
-
unless constructions.wallConstruction.empty?
|
968
|
-
construction = constructions.wallConstruction.get
|
969
|
-
return true if construction == bse
|
970
|
-
end
|
971
|
-
end
|
2255
|
+
res[:t] = group.siteTransformation
|
2256
|
+
res[:r] = group.directionofRelativeNorth + mdl.getBuilding.northAxis
|
972
2257
|
|
973
|
-
|
2258
|
+
res
|
974
2259
|
end
|
975
2260
|
|
976
2261
|
##
|
977
|
-
#
|
2262
|
+
# Returns true if 2 OpenStudio 3D points are nearly equal
|
978
2263
|
#
|
979
|
-
# @param
|
980
|
-
# @param
|
2264
|
+
# @param p1 [OpenStudio::Point3d] 1st 3D point
|
2265
|
+
# @param p2 [OpenStudio::Point3d] 2nd 3D point
|
981
2266
|
#
|
982
|
-
# @return [
|
983
|
-
# @return [
|
984
|
-
def
|
2267
|
+
# @return [Bool] whether equal points (within TOL)
|
2268
|
+
# @return [false] if invalid input (see logs)
|
2269
|
+
def same?(p1 = nil, p2 = nil)
|
985
2270
|
mth = "OSut::#{__callee__}"
|
986
|
-
|
987
|
-
|
988
|
-
|
989
|
-
return mismatch("model", model, cl1, mth) unless model.is_a?(cl1)
|
990
|
-
return invalid("s", mth, 2) unless s.respond_to?(NS)
|
991
|
-
|
992
|
-
id = s.nameString
|
993
|
-
return mismatch(id, s, cl2, mth) unless s.is_a?(cl2)
|
994
|
-
|
995
|
-
ok = s.isConstructionDefaulted
|
996
|
-
log(ERR, "'#{id}' construction not defaulted (#{mth})") unless ok
|
997
|
-
return nil unless ok
|
998
|
-
return empty("'#{id}' construction", mth, ERR) if s.construction.empty?
|
999
|
-
|
1000
|
-
base = s.construction.get
|
1001
|
-
return empty("'#{id}' space", mth, ERR) if s.space.empty?
|
1002
|
-
|
1003
|
-
space = s.space.get
|
1004
|
-
type = s.surfaceType
|
1005
|
-
ground = false
|
1006
|
-
exterior = false
|
1007
|
-
|
1008
|
-
if s.isGroundSurface
|
1009
|
-
ground = true
|
1010
|
-
elsif s.outsideBoundaryCondition.downcase == "outdoors"
|
1011
|
-
exterior = true
|
1012
|
-
end
|
1013
|
-
|
1014
|
-
unless space.defaultConstructionSet.empty?
|
1015
|
-
set = space.defaultConstructionSet.get
|
1016
|
-
return set if holdsConstruction?(set, base, ground, exterior, type)
|
1017
|
-
end
|
1018
|
-
|
1019
|
-
unless space.spaceType.empty?
|
1020
|
-
spacetype = space.spaceType.get
|
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)
|
1021
2274
|
|
1022
|
-
|
1023
|
-
|
1024
|
-
return set if holdsConstruction?(set, base, ground, exterior, type)
|
1025
|
-
end
|
1026
|
-
end
|
1027
|
-
|
1028
|
-
unless space.buildingStory.empty?
|
1029
|
-
story = space.buildingStory.get
|
1030
|
-
|
1031
|
-
unless story.defaultConstructionSet.empty?
|
1032
|
-
set = story.defaultConstructionSet.get
|
1033
|
-
return set if holdsConstruction?(set, base, ground, exterior, type)
|
1034
|
-
end
|
1035
|
-
end
|
1036
|
-
|
1037
|
-
building = model.getBuilding
|
1038
|
-
|
1039
|
-
unless building.defaultConstructionSet.empty?
|
1040
|
-
set = building.defaultConstructionSet.get
|
1041
|
-
return set if holdsConstruction?(set, base, ground, exterior, type)
|
1042
|
-
end
|
1043
|
-
|
1044
|
-
nil
|
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
|
1045
2277
|
end
|
1046
2278
|
|
1047
2279
|
##
|
1048
|
-
#
|
2280
|
+
# Returns true if a line segment is along the X-axis.
|
1049
2281
|
#
|
1050
|
-
# @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
|
1051
2285
|
#
|
1052
|
-
# @return [Bool]
|
1053
|
-
# @return [
|
1054
|
-
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)
|
1055
2289
|
mth = "OSut::#{__callee__}"
|
1056
|
-
cl = OpenStudio::
|
1057
|
-
|
1058
|
-
return
|
1059
|
-
return mismatch(
|
1060
|
-
|
1061
|
-
|
1062
|
-
|
1063
|
-
|
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
|
1064
2298
|
end
|
1065
2299
|
|
1066
2300
|
##
|
1067
|
-
#
|
2301
|
+
# Returns true if a line segment is along the Y-axis.
|
1068
2302
|
#
|
1069
|
-
# @param
|
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
|
1070
2306
|
#
|
1071
|
-
# @return [
|
1072
|
-
# @return [
|
1073
|
-
def
|
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)
|
1074
2310
|
mth = "OSut::#{__callee__}"
|
1075
|
-
cl = OpenStudio::
|
1076
|
-
|
1077
|
-
return
|
1078
|
-
|
1079
|
-
|
1080
|
-
return
|
1081
|
-
|
1082
|
-
|
1083
|
-
log(ERR, "'#{id}' holds non-StandardOpaqueMaterial(s) (#{mth})") unless ok
|
1084
|
-
return 0.0 unless ok
|
1085
|
-
|
1086
|
-
thickness = 0.0
|
1087
|
-
lc.layers.each { |m| thickness += m.thickness }
|
1088
|
-
|
1089
|
-
thickness
|
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
|
1090
2319
|
end
|
1091
2320
|
|
1092
2321
|
##
|
1093
|
-
#
|
2322
|
+
# Returns true if a line segment is along the Z-axis.
|
1094
2323
|
#
|
1095
|
-
# @param
|
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
|
1096
2327
|
#
|
1097
|
-
# @return [
|
1098
|
-
|
1099
|
-
|
1100
|
-
# coefficients under standard winter conditions are taken from:
|
1101
|
-
#
|
1102
|
-
# https://bigladdersoftware.com/epx/docs/9-6/engineering-reference/
|
1103
|
-
# window-calculation-module.html#simple-window-model
|
1104
|
-
#
|
1105
|
-
# These remain acceptable approximations for flat windows, yet likely
|
1106
|
-
# unsuitable for subsurfaces with curved or projecting shapes like domed
|
1107
|
-
# skylights. The solution here is considered an adequate fix for reporting,
|
1108
|
-
# awaiting eventual OpenStudio (and EnergyPlus) upgrades to report NFRC 100
|
1109
|
-
# (or ISO) air film resistances under standard winter conditions.
|
1110
|
-
#
|
1111
|
-
# For U-factors above 8.0 W/m2•K (or invalid input), the function returns
|
1112
|
-
# 0.1216 m2•K/W, which corresponds to a construction with a single glass
|
1113
|
-
# layer thickness of 2mm & k = ~0.6 W/m.K.
|
1114
|
-
#
|
1115
|
-
# The EnergyPlus Engineering calculations were designed for vertical windows
|
1116
|
-
# - not horizontal, slanted or domed surfaces - use with caution.
|
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)
|
1117
2331
|
mth = "OSut::#{__callee__}"
|
1118
|
-
cl =
|
1119
|
-
|
1120
|
-
return mismatch("
|
1121
|
-
return
|
1122
|
-
return
|
1123
|
-
return
|
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
|
1124
2341
|
|
1125
|
-
|
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
|
1126
2355
|
|
1127
|
-
|
1128
|
-
|
2356
|
+
m = m.to_f
|
2357
|
+
OpenStudio::Vector3d.new(m * v.x, m * v.y, m * v.z)
|
1129
2358
|
end
|
1130
2359
|
|
1131
2360
|
##
|
1132
|
-
#
|
2361
|
+
# Returns OpenStudio 3D points as an OpenStudio point vector, validating
|
2362
|
+
# points in the process (if Array).
|
1133
2363
|
#
|
1134
|
-
# @param
|
1135
|
-
# @param film [Float] thermal resistance of surface air films (m2•K/W)
|
1136
|
-
# @param t [Float] gas temperature (°C) (optional)
|
2364
|
+
# @param pts [Set<OpenStudio::Point3d>] OpenStudio 3D points
|
1137
2365
|
#
|
1138
|
-
# @return [
|
1139
|
-
def
|
1140
|
-
# This is adapted from BTAP's Material Module's "get_conductance" (P. Lopez)
|
1141
|
-
#
|
1142
|
-
# https://github.com/NREL/OpenStudio-Prototype-Buildings/blob/
|
1143
|
-
# c3d5021d8b7aef43e560544699fb5c559e6b721d/lib/btap/measures/
|
1144
|
-
# btap_equest_converter/envelope.rb#L122
|
2366
|
+
# @return [OpenStudio::Point3dVector] 3D vector (see logs if empty)
|
2367
|
+
def to_p3Dv(pts = nil)
|
1145
2368
|
mth = "OSut::#{__callee__}"
|
1146
|
-
cl1 =
|
1147
|
-
cl2 =
|
1148
|
-
|
1149
|
-
|
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)
|
1150
2377
|
|
1151
|
-
|
2378
|
+
pts.each do |pt|
|
2379
|
+
return mismatch("point", pt, cl4, mth, DBG, v) unless pt.is_a?(cl4)
|
2380
|
+
end
|
1152
2381
|
|
1153
|
-
|
1154
|
-
return mismatch("film", film, cl2, mth, DBG, 0.0) unless film.is_a?(cl2)
|
1155
|
-
return mismatch("temp K", t, cl2, mth, DBG, 0.0) unless t.is_a?(cl2)
|
2382
|
+
pts.each { |pt| v << OpenStudio::Point3d.new(pt.x, pt.y, pt.z) }
|
1156
2383
|
|
1157
|
-
|
1158
|
-
|
1159
|
-
return negative("film", mth, DBG, 0.0) if film < 0
|
2384
|
+
v
|
2385
|
+
end
|
1160
2386
|
|
1161
|
-
|
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)
|
1162
2400
|
|
1163
|
-
|
1164
|
-
# Fenestration materials first (ignoring shades, screens, etc.)
|
1165
|
-
empty = m.to_SimpleGlazing.empty?
|
1166
|
-
return 1 / m.to_SimpleGlazing.get.uFactor unless empty
|
2401
|
+
pts.each { |pt| return true if same?(p1, pt) }
|
1167
2402
|
|
1168
|
-
|
1169
|
-
|
1170
|
-
empty = m.to_RefractionExtinctionGlazing.empty?
|
1171
|
-
rsi += m.to_RefractionExtinctionGlazing.get.thermalResistance unless empty
|
1172
|
-
empty = m.to_Gas.empty?
|
1173
|
-
rsi += m.to_Gas.get.getThermalResistance(t) unless empty
|
1174
|
-
empty = m.to_GasMixture.empty?
|
1175
|
-
rsi += m.to_GasMixture.get.getThermalResistance(t) unless empty
|
2403
|
+
false
|
2404
|
+
end
|
1176
2405
|
|
1177
|
-
|
1178
|
-
|
1179
|
-
|
1180
|
-
|
1181
|
-
|
1182
|
-
|
1183
|
-
|
1184
|
-
|
1185
|
-
|
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) }
|
1186
2432
|
end
|
1187
2433
|
|
1188
|
-
|
2434
|
+
v
|
1189
2435
|
end
|
1190
2436
|
|
1191
2437
|
##
|
1192
|
-
#
|
1193
|
-
# returns a 3-keyed hash ... :index (insulating layer index within layered
|
1194
|
-
# construction), :type (standard: or massless: material type), and
|
1195
|
-
# :r (material thermal resistance in m2•K/W).
|
2438
|
+
# Returns true if OpenStudio 3D points share X, Y or Z coordinates.
|
1196
2439
|
#
|
1197
|
-
# @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
|
1198
2443
|
#
|
1199
|
-
# @return [
|
1200
|
-
# @return [
|
1201
|
-
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)
|
1202
2447
|
mth = "OSut::#{__callee__}"
|
1203
|
-
|
1204
|
-
|
1205
|
-
|
1206
|
-
|
1207
|
-
return
|
1208
|
-
|
1209
|
-
|
1210
|
-
|
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
|
1211
2465
|
|
1212
|
-
|
1213
|
-
|
1214
|
-
m = m.to_MasslessOpaqueMaterial.get
|
2466
|
+
true
|
2467
|
+
end
|
1215
2468
|
|
1216
|
-
|
1217
|
-
|
1218
|
-
|
1219
|
-
|
1220
|
-
|
1221
|
-
|
1222
|
-
|
1223
|
-
|
1224
|
-
|
2469
|
+
##
|
2470
|
+
# Returns next sequential point in an OpenStudio 3D point vector.
|
2471
|
+
#
|
2472
|
+
# @param pts [OpenStudio::Point3dVector] 3D points
|
2473
|
+
# @param pt [OpenStudio::Point3d] a given 3D point
|
2474
|
+
#
|
2475
|
+
# @return [OpenStudio::Point3d] the next sequential point
|
2476
|
+
# @return [nil] if invalid input (see logs)
|
2477
|
+
def next(pts = nil, pt = nil)
|
2478
|
+
mth = "OSut::#{__callee__}"
|
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
|
1225
2483
|
|
1226
|
-
|
1227
|
-
m = m.to_StandardOpaqueMaterial.get
|
1228
|
-
k = m.thermalConductivity
|
1229
|
-
d = m.thickness
|
2484
|
+
pair = pts.each_cons(2).find { |p1, _| same?(p1, pt) }
|
1230
2485
|
|
1231
|
-
|
1232
|
-
|
1233
|
-
next
|
1234
|
-
else
|
1235
|
-
res[:r ] = d / k
|
1236
|
-
res[:index] = i
|
1237
|
-
res[:type ] = :standard
|
1238
|
-
end
|
1239
|
-
end
|
2486
|
+
pair.nil? ? pts.first : pair.last
|
2487
|
+
end
|
1240
2488
|
|
1241
|
-
|
1242
|
-
|
2489
|
+
##
|
2490
|
+
# Returns unique OpenStudio 3D points from an OpenStudio 3D point vector.
|
2491
|
+
#
|
2492
|
+
# @param pts [Set<OpenStudio::Point3d] 3D points
|
2493
|
+
# @param n [#to_i] requested number of unique points (0 returns all)
|
2494
|
+
#
|
2495
|
+
# @return [OpenStudio::Point3dVector] unique points (see logs if empty)
|
2496
|
+
def getUniques(pts = nil, n = 0)
|
2497
|
+
mth = "OSut::#{__callee__}"
|
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
|
1243
2503
|
|
1244
|
-
|
2504
|
+
pts.each { |pt| v << pt unless holds?(v, pt) }
|
2505
|
+
|
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
|
2510
|
+
|
2511
|
+
v
|
1245
2512
|
end
|
1246
2513
|
|
1247
2514
|
##
|
1248
|
-
#
|
2515
|
+
# Returns sequential non-collinear points in an OpenStudio 3D point vector.
|
1249
2516
|
#
|
1250
|
-
# @param
|
1251
|
-
# @param
|
2517
|
+
# @param pts [Set<OpenStudio::Point3d] 3D points
|
2518
|
+
# @param n [#to_i] requested number of non-collinears (0 returns all)
|
1252
2519
|
#
|
1253
|
-
# @return [
|
1254
|
-
|
1255
|
-
def transforms(model = nil, group = nil)
|
2520
|
+
# @return [OpenStudio::Point3dVector] non-collinears (see logs if empty)
|
2521
|
+
def getNonCollinears(pts = nil, n = 0)
|
1256
2522
|
mth = "OSut::#{__callee__}"
|
1257
|
-
|
1258
|
-
|
1259
|
-
|
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
|
1260
2543
|
|
1261
|
-
|
1262
|
-
|
2544
|
+
if holds?(a, pts[0])
|
2545
|
+
a = a.rotate(-1) unless same?(a[0], pts[0])
|
2546
|
+
end
|
1263
2547
|
|
1264
|
-
|
1265
|
-
|
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
|
1266
2552
|
|
1267
|
-
|
1268
|
-
|
2553
|
+
to_p3Dv(a)
|
2554
|
+
end
|
1269
2555
|
|
1270
|
-
|
2556
|
+
##
|
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.
|
2562
|
+
#
|
2563
|
+
# @param pts [Set<OpenStudio::Point3d>] 3D points
|
2564
|
+
# @param co [Bool] whether to keep collinear points
|
2565
|
+
#
|
2566
|
+
# @return [OpenStudio::Point3dVectorVector] line segments (see logs if empty)
|
2567
|
+
def getSegments(pts = nil, co = false)
|
2568
|
+
mth = "OSut::#{__callee__}"
|
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
|
2585
|
+
end
|
2586
|
+
|
2587
|
+
vv
|
1271
2588
|
end
|
1272
2589
|
|
1273
2590
|
##
|
1274
|
-
#
|
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.
|
1275
2596
|
#
|
1276
|
-
# @param
|
1277
|
-
# @param
|
2597
|
+
# @param pts [OpenStudio::Point3dVector] 3D points
|
2598
|
+
# @param co [Bool] whether to keep collinear points
|
1278
2599
|
#
|
1279
|
-
# @return [OpenStudio::
|
1280
|
-
|
1281
|
-
def scalar(v = OpenStudio::Vector3d.new(0,0,0), m = 0)
|
2600
|
+
# @return [OpenStudio::Point3dVectorVector] triads (see logs if empty)
|
2601
|
+
def getTriads(pts = nil, co = false)
|
1282
2602
|
mth = "OSut::#{__callee__}"
|
1283
|
-
|
1284
|
-
|
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
|
1285
2623
|
|
1286
|
-
|
1287
|
-
|
1288
|
-
return mismatch("y", v.y, cl2, mth, DBG, v) unless v.y.respond_to?(:to_f)
|
1289
|
-
return mismatch("z", v.z, cl2, mth, DBG, v) unless v.z.respond_to?(:to_f)
|
1290
|
-
return mismatch("m", m, cl2, mth, DBG, v) unless m.respond_to?(:to_f)
|
2624
|
+
vv
|
2625
|
+
end
|
1291
2626
|
|
1292
|
-
|
2627
|
+
##
|
2628
|
+
# Determines if pre-'aligned' OpenStudio 3D points are listed clockwise.
|
2629
|
+
#
|
2630
|
+
# @param pts [OpenStudio::Point3dVector] 3D points
|
2631
|
+
#
|
2632
|
+
# @return [Bool] whether sequence is clockwise
|
2633
|
+
# @return [false] if invalid input (see logs)
|
2634
|
+
def clockwise?(pts = nil)
|
2635
|
+
mth = "OSut::#{__callee__}"
|
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)
|
2640
|
+
|
2641
|
+
OpenStudio.pointInPolygon(pts.first, pts, TOL)
|
1293
2642
|
end
|
1294
2643
|
|
1295
2644
|
##
|
1296
|
-
#
|
2645
|
+
# Returns 'aligned' OpenStudio 3D points conforming to Openstudio's
|
2646
|
+
# counterclockwise UpperLeftCorner (ULC) convention.
|
1297
2647
|
#
|
1298
|
-
# @param pts [
|
2648
|
+
# @param pts [Set<OpenStudio::Point3d>] aligned 3D points
|
1299
2649
|
#
|
1300
|
-
# @return [
|
1301
|
-
def
|
2650
|
+
# @return [OpenStudio::Point3dVector] ULC points (see logs if empty)
|
2651
|
+
def ulc(pts = nil)
|
1302
2652
|
mth = "OSut::#{__callee__}"
|
1303
|
-
|
1304
|
-
cl2 = OpenStudio::Point3d
|
2653
|
+
pts = to_p3Dv(pts)
|
1305
2654
|
v = OpenStudio::Point3dVector.new
|
2655
|
+
p0 = OpenStudio::Point3d.new(0,0,0)
|
2656
|
+
i0 = nil
|
1306
2657
|
|
1307
|
-
|
1308
|
-
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)
|
1309
2660
|
|
1310
|
-
|
1311
|
-
pts
|
2661
|
+
# Ensure counterclockwise sequence.
|
2662
|
+
pts = pts.to_a
|
2663
|
+
pts = pts.reverse if clockwise?(pts)
|
1312
2664
|
|
1313
|
-
|
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)
|
1314
2677
|
end
|
1315
2678
|
|
1316
2679
|
##
|
1317
|
-
#
|
1318
|
-
#
|
1319
|
-
#
|
1320
|
-
#
|
1321
|
-
#
|
1322
|
-
#
|
1323
|
-
#
|
1324
|
-
# @
|
1325
|
-
# @
|
1326
|
-
|
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)
|
1327
2696
|
mth = "OSut::#{__callee__}"
|
1328
|
-
|
1329
|
-
|
1330
|
-
|
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)
|
2703
|
+
|
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
|
2710
|
+
|
2711
|
+
# --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- #
|
2712
|
+
# Basic tests:
|
2713
|
+
p3 = getNonCollinears(pts, 3)
|
2714
|
+
return empty("polygon", mth, ERR, v) if p3.size < 3
|
2715
|
+
|
2716
|
+
pln = OpenStudio::Plane.new(p3)
|
2717
|
+
|
2718
|
+
pts.each do |pt|
|
2719
|
+
return empty("plane", mth, ERR, v) unless pln.pointOnPlane(pt)
|
2720
|
+
end
|
2721
|
+
|
2722
|
+
t = tt
|
2723
|
+
t = OpenStudio::Transformation.alignFace(pts) unless tt.is_a?(cl)
|
2724
|
+
a = (t.inverse * pts).reverse
|
1331
2725
|
|
1332
|
-
|
1333
|
-
|
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
|
2736
|
+
|
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
|
2742
|
+
|
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?
|
2750
|
+
|
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
|
1334
2784
|
|
1335
|
-
|
1336
|
-
|
1337
|
-
|
1338
|
-
|
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
|
1339
2808
|
|
1340
|
-
|
1341
|
-
|
2809
|
+
a
|
2810
|
+
end
|
1342
2811
|
|
1343
|
-
|
1344
|
-
|
1345
|
-
|
1346
|
-
|
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__}"
|
1347
2821
|
|
1348
|
-
|
1349
|
-
|
2822
|
+
poly(pts, false, true, false, true).max_by(&:x).x
|
2823
|
+
end
|
1350
2824
|
|
1351
|
-
|
1352
|
-
|
1353
|
-
|
1354
|
-
|
2825
|
+
##
|
2826
|
+
# Returns 'height' of a set of OpenStudio 3D points (perpendicular view).
|
2827
|
+
#
|
2828
|
+
# @param pts [Set<OpenStudio::Point3d>] 3D points
|
2829
|
+
#
|
2830
|
+
# @return [Float] top-to-bottom height
|
2831
|
+
# @return [0.0] if invalid inputs (see logs)
|
2832
|
+
def height(pts = nil)
|
2833
|
+
mth = "OSut::#{__callee__}"
|
1355
2834
|
|
1356
|
-
|
1357
|
-
|
1358
|
-
ft_p2 = flatZ( (ft.inverse * p2).reverse ) unless cw
|
1359
|
-
ft_p2 = flatZ( (ft.inverse * p2) ) if cw
|
1360
|
-
return false if ft_p2.empty?
|
2835
|
+
poly(pts, false, true, false, true).max_by(&:y).y
|
2836
|
+
end
|
1361
2837
|
|
1362
|
-
|
1363
|
-
|
1364
|
-
|
1365
|
-
|
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?
|
1366
2868
|
|
1367
2869
|
area1 = area1.get
|
1368
2870
|
area2 = area2.get
|
1369
|
-
union = OpenStudio.join(
|
1370
|
-
return
|
2871
|
+
union = OpenStudio.join(p1, p2, TOL2)
|
2872
|
+
return false if union.empty?
|
1371
2873
|
|
1372
2874
|
union = union.get
|
1373
2875
|
area = OpenStudio.getArea(union)
|
1374
|
-
return
|
2876
|
+
return false if area.empty?
|
1375
2877
|
|
1376
2878
|
area = area.get
|
1377
2879
|
|
1378
|
-
|
1379
|
-
|
1380
|
-
|
2880
|
+
if area > TOL
|
2881
|
+
return true if (area - area2).abs < TOL
|
2882
|
+
end
|
1381
2883
|
|
1382
|
-
|
2884
|
+
false
|
1383
2885
|
end
|
1384
2886
|
|
1385
2887
|
##
|
1386
|
-
#
|
2888
|
+
# Determines whether OpenStudio polygons overlap.
|
1387
2889
|
#
|
1388
|
-
# @param p1 [OpenStudio::
|
1389
|
-
# @param p2 [OpenStudio::
|
1390
|
-
# @param
|
1391
|
-
# @param id2 [String] polygon #2 identifier (optional)
|
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)
|
1392
2893
|
#
|
1393
|
-
# @return
|
1394
|
-
# @return [
|
1395
|
-
def overlaps?(p1 = nil, p2 = nil,
|
1396
|
-
mth
|
1397
|
-
|
1398
|
-
|
1399
|
-
|
1400
|
-
|
1401
|
-
return
|
1402
|
-
|
1403
|
-
|
1404
|
-
|
1405
|
-
|
1406
|
-
|
1407
|
-
|
1408
|
-
|
1409
|
-
|
1410
|
-
|
1411
|
-
|
1412
|
-
return
|
1413
|
-
return
|
1414
|
-
|
1415
|
-
|
1416
|
-
|
1417
|
-
|
1418
|
-
|
1419
|
-
|
1420
|
-
# XY-plane transformation matrix ... needs to be clockwise for boost.
|
1421
|
-
ft = OpenStudio::Transformation.alignFace(p1)
|
1422
|
-
ft_p1 = flatZ( (ft.inverse * p1) )
|
1423
|
-
ft_p2 = flatZ( (ft.inverse * p2) )
|
1424
|
-
return false if ft_p1.empty?
|
1425
|
-
return false if ft_p2.empty?
|
1426
|
-
|
1427
|
-
cw = OpenStudio.pointInPolygon(ft_p1.first, ft_p1, TOL)
|
1428
|
-
ft_p1 = flatZ( (ft.inverse * p1).reverse ) unless cw
|
1429
|
-
ft_p2 = flatZ( (ft.inverse * p2).reverse ) unless cw
|
1430
|
-
return false if ft_p1.empty?
|
1431
|
-
return false if ft_p2.empty?
|
1432
|
-
|
1433
|
-
area1 = OpenStudio.getArea(ft_p1)
|
1434
|
-
area2 = OpenStudio.getArea(ft_p2)
|
1435
|
-
return empty("#{i1} area", mth, ERR, a) if area1.empty?
|
1436
|
-
return empty("#{i2} area", mth, ERR, a) if area2.empty?
|
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?
|
1437
2920
|
|
1438
2921
|
area1 = area1.get
|
1439
2922
|
area2 = area2.get
|
1440
|
-
union = OpenStudio.join(
|
1441
|
-
return
|
2923
|
+
union = OpenStudio.join(p1, p2, TOL2)
|
2924
|
+
return false if union.empty?
|
1442
2925
|
|
1443
2926
|
union = union.get
|
1444
2927
|
area = OpenStudio.getArea(union)
|
1445
|
-
return
|
2928
|
+
return false if area.empty?
|
1446
2929
|
|
1447
|
-
area
|
1448
|
-
|
2930
|
+
area = area.get
|
2931
|
+
delta = area1 + area2 - area
|
1449
2932
|
|
1450
|
-
|
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
|
2939
|
+
|
2940
|
+
false
|
1451
2941
|
end
|
1452
2942
|
|
1453
2943
|
##
|
1454
|
-
#
|
2944
|
+
# Generates offset vertices (by width) for a 3- or 4-sided, convex polygon.
|
1455
2945
|
#
|
1456
|
-
# @param p1 [OpenStudio::
|
1457
|
-
# @param w [
|
1458
|
-
# @param v [
|
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)
|
1459
2949
|
#
|
1460
|
-
# @return [OpenStudio::Point3dVector] offset points if
|
1461
|
-
|
1462
|
-
|
1463
|
-
|
1464
|
-
|
1465
|
-
vrsn = OpenStudio.openStudioVersion.split(".").map(&:to_i).join.to_i
|
1466
|
-
|
1467
|
-
valid = p1.is_a?(OpenStudio::Point3dVector) || p1.is_a?(Array)
|
1468
|
-
return mismatch("pts", p1, cl1, mth, DBG, p1) unless valid
|
1469
|
-
return empty("pts", mth, ERR, p1) if p1.empty?
|
1470
|
-
|
1471
|
-
valid = p1.size == 3 || p1.size == 4
|
1472
|
-
iv = true if p1.size == 4
|
1473
|
-
return invalid("pts", mth, 1, DBG, p1) unless valid
|
1474
|
-
return invalid("width", mth, 2, DBG, p1) unless w.respond_to?(:to_f)
|
1475
|
-
|
1476
|
-
w = w.to_f
|
1477
|
-
return p1 if w < 0.0254
|
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)
|
1478
2955
|
|
1479
|
-
|
1480
|
-
v
|
1481
|
-
v = vrsn if v.zero?
|
2956
|
+
mismatch("width", w, Numeric, mth) unless w.respond_to?(:to_f)
|
2957
|
+
mismatch("version", v, Integer, mth) unless v.respond_to?(:to_i)
|
1482
2958
|
|
1483
|
-
|
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
|
1484
2967
|
|
1485
2968
|
unless v < 340
|
1486
|
-
|
1487
|
-
|
1488
|
-
|
1489
|
-
|
1490
|
-
|
1491
|
-
|
1492
|
-
ft_pts = flatZ( (ft.inverse * p1).reverse ) unless cw
|
1493
|
-
offset = OpenStudio.buffer(ft_pts, w, TOL)
|
1494
|
-
return p1 if offset.empty?
|
1495
|
-
|
1496
|
-
offset = offset.get
|
1497
|
-
offset = ft * offset if cw
|
1498
|
-
offset = (ft * offset).reverse unless cw
|
1499
|
-
|
1500
|
-
pz = OpenStudio::Point3dVector.new
|
1501
|
-
offset.each { |o| pz << OpenStudio::Point3d.new(o.x, o.y, o.z ) }
|
1502
|
-
|
1503
|
-
return pz
|
1504
|
-
else # brute force approach
|
2969
|
+
t = OpenStudio::Transformation.alignFace(p1)
|
2970
|
+
offset = OpenStudio.buffer(pts, w, TOL)
|
2971
|
+
return p1 if offset.empty?
|
2972
|
+
|
2973
|
+
return to_p3Dv(t * offset.get.reverse)
|
2974
|
+
else # brute force approach
|
1505
2975
|
pz = {}
|
1506
2976
|
pz[:A] = {}
|
1507
2977
|
pz[:B] = {}
|
@@ -1684,6 +3154,851 @@ module OSut
|
|
1684
3154
|
end
|
1685
3155
|
end
|
1686
3156
|
|
3157
|
+
##
|
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.
|
3391
|
+
#
|
3392
|
+
# @param space [OpenStudio::Model::Space] a space
|
3393
|
+
#
|
3394
|
+
# @return [Array<OpenStudio::Model::Surface>] surfaces (see logs if empty)
|
3395
|
+
def getRoofs(space = nil)
|
3396
|
+
mth = "OSut::#{__callee__}"
|
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"}
|
3405
|
+
|
3406
|
+
clngs = clngs.select {|s| s.surfaceType.downcase == "roofceiling"}
|
3407
|
+
clngs = clngs.select {|s| s.outsideBoundaryCondition.downcase == "surface"}
|
3408
|
+
|
3409
|
+
clngs.each do |ceiling|
|
3410
|
+
floor = ceiling.adjacentSurface
|
3411
|
+
next if floor.empty?
|
3412
|
+
|
3413
|
+
other = floor.get.space
|
3414
|
+
next if other.empty?
|
3415
|
+
|
3416
|
+
rufs = other.get.surfaces
|
3417
|
+
|
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
|
3428
|
+
end
|
3429
|
+
|
3430
|
+
roofs
|
3431
|
+
end
|
3432
|
+
|
3433
|
+
##
|
3434
|
+
# Adds sub surfaces (e.g. windows, doors, skylights) to surface.
|
3435
|
+
#
|
3436
|
+
# @param s [OpenStudio::Model::Surface] a model surface
|
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)
|
3459
|
+
mth = "OSut::#{__callee__}"
|
3460
|
+
v = OpenStudio.openStudioVersion.split(".").join.to_i
|
3461
|
+
cl1 = OpenStudio::Model::Surface
|
3462
|
+
cl2 = Array
|
3463
|
+
cl3 = Hash
|
3464
|
+
min = 0.050 # minimum ratio value ( 5%)
|
3465
|
+
max = 0.950 # maximum ratio value (95%)
|
3466
|
+
no = false
|
3467
|
+
|
3468
|
+
# --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- #
|
3469
|
+
# Exit if mismatched or invalid argument classes.
|
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?
|
3473
|
+
|
3474
|
+
# --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- #
|
3475
|
+
# Clear existing sub surfaces if requested.
|
3476
|
+
nom = s.nameString
|
3477
|
+
mdl = s.model
|
3478
|
+
|
3479
|
+
unless [true, false].include?(clear)
|
3480
|
+
log(WRN, "#{nom}: Keeping existing sub surfaces (#{mth})")
|
3481
|
+
clear = false
|
3482
|
+
end
|
3483
|
+
|
3484
|
+
s.subSurfaces.map(&:remove) if clear
|
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
|
+
|
3499
|
+
# --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- #
|
3500
|
+
# Allowable sub surface types ... & Frame&Divider enabled
|
3501
|
+
# - "FixedWindow" | true
|
3502
|
+
# - "OperableWindow" | true
|
3503
|
+
# - "Door" | false
|
3504
|
+
# - "GlassDoor" | true
|
3505
|
+
# - "OverheadDoor" | false
|
3506
|
+
# - "Skylight" | false if v < 321
|
3507
|
+
# - "TubularDaylightDome" | false
|
3508
|
+
# - "TubularDaylightDiffuser" | false
|
3509
|
+
type = "FixedWindow"
|
3510
|
+
types = OpenStudio::Model::SubSurface.validSubSurfaceTypeValues
|
3511
|
+
stype = s.surfaceType # Wall, RoofCeiling or Floor
|
3512
|
+
|
3513
|
+
# --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- #
|
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
|
3519
|
+
|
3520
|
+
# --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- #
|
3521
|
+
# Assign default values to certain sub keys (if missing), +more validation.
|
3522
|
+
subs.each_with_index do |sub, index|
|
3523
|
+
return mismatch("sub", sub, cl4, mth, DBG, no) unless sub.is_a?(cl3)
|
3524
|
+
|
3525
|
+
# Required key:value pairs (either set by the user or defaulted).
|
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]
|
3544
|
+
|
3545
|
+
# If sub surface type is invalid, log/reset. Additional corrections may
|
3546
|
+
# be enabled once a sub surface is actually instantiated.
|
3547
|
+
unless types.include?(sub[:type])
|
3548
|
+
log(WRN, "Reset invalid '#{id}' type to '#{type}' (#{mth})")
|
3549
|
+
sub[:type] = type
|
3550
|
+
end
|
3551
|
+
|
3552
|
+
# Log/ignore (optional) frame & divider object.
|
3553
|
+
unless sub[:frame].nil?
|
3554
|
+
if sub[:frame].respond_to?(:frameWidth)
|
3555
|
+
sub[:frame] = nil if sub[:type] == "Skylight" && v < 321
|
3556
|
+
sub[:frame] = nil if sub[:type] == "Door"
|
3557
|
+
sub[:frame] = nil if sub[:type] == "OverheadDoor"
|
3558
|
+
sub[:frame] = nil if sub[:type] == "TubularDaylightDome"
|
3559
|
+
sub[:frame] = nil if sub[:type] == "TubularDaylightDiffuser"
|
3560
|
+
log(WRN, "Skip '#{id}' FrameDivider (#{mth})") if sub[:frame].nil?
|
3561
|
+
else
|
3562
|
+
sub[:frame] = nil
|
3563
|
+
log(WRN, "Skip '#{id}' invalid FrameDivider object (#{mth})")
|
3564
|
+
end
|
3565
|
+
end
|
3566
|
+
|
3567
|
+
# The (optional) "assembly" must reference a valid OpenStudio
|
3568
|
+
# construction base, to explicitly assign to each instantiated sub
|
3569
|
+
# surface. If invalid, log/reset/ignore. Additional checks are later
|
3570
|
+
# activated once a sub surface is actually instantiated.
|
3571
|
+
unless sub[:assembly].nil?
|
3572
|
+
unless sub[:assembly].respond_to?(:isFenestration)
|
3573
|
+
log(WRN, "Skip invalid '#{id}' construction (#{mth})")
|
3574
|
+
sub[:assembly] = nil
|
3575
|
+
end
|
3576
|
+
end
|
3577
|
+
|
3578
|
+
# Log/reset negative float values. Set ~0.0 values to 0.0.
|
3579
|
+
sub.each do |key, value|
|
3580
|
+
next if key == :count
|
3581
|
+
next if key == :multiplier
|
3582
|
+
next if key == :type
|
3583
|
+
next if key == :id
|
3584
|
+
next if key == :frame
|
3585
|
+
next if key == :assembly
|
3586
|
+
|
3587
|
+
ok = value.respond_to?(:to_f)
|
3588
|
+
return mismatch(key, value, Float, mth, DBG, no) unless ok
|
3589
|
+
next if key == :centreline
|
3590
|
+
|
3591
|
+
negative(key, mth, WRN) if value < 0
|
3592
|
+
value = 0.0 if value.abs < TOL
|
3593
|
+
end
|
3594
|
+
end
|
3595
|
+
|
3596
|
+
# --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- #
|
3597
|
+
# Log/reset (or abandon) conflicting user-set geometry key:value pairs:
|
3598
|
+
# :head e.g. std 80" door + frame/buffers (+ m)
|
3599
|
+
# :sill e.g. std 30" sill + frame/buffers (+ m)
|
3600
|
+
# :height any sub surface height, below "head" (+ m)
|
3601
|
+
# :width e.g. 1.200 m
|
3602
|
+
# :offset if array (+ m)
|
3603
|
+
# :centreline left or right of base surface centreline (+/- m)
|
3604
|
+
# :r_buffer buffer between sub/array and right-side corner (+ m)
|
3605
|
+
# :l_buffer buffer between sub/array and left-side corner (+ m)
|
3606
|
+
#
|
3607
|
+
# If successful, this will generate sub surfaces and add them to the model.
|
3608
|
+
subs.each do |sub|
|
3609
|
+
# Set-up unique sub parameters:
|
3610
|
+
# - Frame & Divider "width"
|
3611
|
+
# - minimum "clear glazing" limits
|
3612
|
+
# - buffers, etc.
|
3613
|
+
id = sub[:id]
|
3614
|
+
frame = 0
|
3615
|
+
frame = sub[:frame].frameWidth unless sub[:frame].nil?
|
3616
|
+
frames = 2 * frame
|
3617
|
+
buffer = frame + bfr
|
3618
|
+
buffers = 2 * buffer
|
3619
|
+
dim = 0.200 unless (3 * frame) > 0.200
|
3620
|
+
dim = 3 * frame if (3 * frame) > 0.200
|
3621
|
+
glass = dim - frames
|
3622
|
+
min_sill = buffer
|
3623
|
+
min_head = buffers + glass
|
3624
|
+
max_head = max_y - buffer
|
3625
|
+
max_sill = max_head - (buffers + glass)
|
3626
|
+
min_ljamb = buffer
|
3627
|
+
max_ljamb = max_x - (buffers + glass)
|
3628
|
+
min_rjamb = buffers + glass
|
3629
|
+
max_rjamb = max_x - buffer
|
3630
|
+
max_height = max_y - buffers
|
3631
|
+
max_width = max_x - buffers
|
3632
|
+
|
3633
|
+
# Default sub surface "head" & "sill" height, unless user-specified.
|
3634
|
+
typ_head = HEAD
|
3635
|
+
typ_sill = SILL
|
3636
|
+
|
3637
|
+
if sub.key?(:ratio)
|
3638
|
+
typ_head = mid_y * (1 + sub[:ratio]) if sub[:ratio] > 0.75
|
3639
|
+
typ_head = mid_y * (1 + sub[:ratio]) unless stype.downcase == "wall"
|
3640
|
+
typ_sill = mid_y * (1 - sub[:ratio]) if sub[:ratio] > 0.75
|
3641
|
+
typ_sill = mid_y * (1 - sub[:ratio]) unless stype.downcase == "wall"
|
3642
|
+
end
|
3643
|
+
|
3644
|
+
# Log/reset "height" if beyond min/max.
|
3645
|
+
if sub.key?(:height)
|
3646
|
+
unless sub[:height].between?(glass, max_height)
|
3647
|
+
sub[:height] = glass if sub[:height] < glass
|
3648
|
+
sub[:height] = max_height if sub[:height] > max_height
|
3649
|
+
log(WRN, "Reset '#{id}' height to #{sub[:height]} m (#{mth})")
|
3650
|
+
end
|
3651
|
+
end
|
3652
|
+
|
3653
|
+
# Log/reset "head" height if beyond min/max.
|
3654
|
+
if sub.key?(:head)
|
3655
|
+
unless sub[:head].between?(min_head, max_head)
|
3656
|
+
sub[:head] = max_head if sub[:head] > max_head
|
3657
|
+
sub[:head] = min_head if sub[:head] < min_head
|
3658
|
+
log(WRN, "Reset '#{id}' head height to #{sub[:head]} m (#{mth})")
|
3659
|
+
end
|
3660
|
+
end
|
3661
|
+
|
3662
|
+
# Log/reset "sill" height if beyond min/max.
|
3663
|
+
if sub.key?(:sill)
|
3664
|
+
unless sub[:sill].between?(min_sill, max_sill)
|
3665
|
+
sub[:sill] = max_sill if sub[:sill] > max_sill
|
3666
|
+
sub[:sill] = min_sill if sub[:sill] < min_sill
|
3667
|
+
log(WRN, "Reset '#{id}' sill height to #{sub[:sill]} m (#{mth})")
|
3668
|
+
end
|
3669
|
+
end
|
3670
|
+
|
3671
|
+
# At this point, "head", "sill" and/or "height" have been tentatively
|
3672
|
+
# validated (and/or have been corrected) independently from one another.
|
3673
|
+
# Log/reset "head" & "sill" heights if conflicting.
|
3674
|
+
if sub.key?(:head) && sub.key?(:sill) && sub[:head] < sub[:sill] + glass
|
3675
|
+
sill = sub[:head] - glass
|
3676
|
+
|
3677
|
+
if sill < min_sill
|
3678
|
+
sub[:ratio ] = 0 if sub.key?(:ratio)
|
3679
|
+
sub[:count ] = 0
|
3680
|
+
sub[:multiplier] = 0
|
3681
|
+
sub[:height ] = 0 if sub.key?(:height)
|
3682
|
+
sub[:width ] = 0 if sub.key?(:width)
|
3683
|
+
log(ERR, "Skip: invalid '#{id}' head/sill combo (#{mth})")
|
3684
|
+
next
|
3685
|
+
else
|
3686
|
+
sub[:sill] = sill
|
3687
|
+
log(WRN, "(Re)set '#{id}' sill height to #{sub[:sill]} m (#{mth})")
|
3688
|
+
end
|
3689
|
+
end
|
3690
|
+
|
3691
|
+
# Attempt to reconcile "head", "sill" and/or "height". If successful,
|
3692
|
+
# all 3x parameters are set (if missing), or reset if invalid.
|
3693
|
+
if sub.key?(:head) && sub.key?(:sill)
|
3694
|
+
height = sub[:head] - sub[:sill]
|
3695
|
+
|
3696
|
+
if sub.key?(:height) && (sub[:height] - height).abs > TOL
|
3697
|
+
log(WRN, "(Re)set '#{id}' height to #{height} m (#{mth})")
|
3698
|
+
end
|
3699
|
+
|
3700
|
+
sub[:height] = height
|
3701
|
+
elsif sub.key?(:head) # no "sill"
|
3702
|
+
if sub.key?(:height)
|
3703
|
+
sill = sub[:head] - sub[:height]
|
3704
|
+
|
3705
|
+
if sill < min_sill
|
3706
|
+
sill = min_sill
|
3707
|
+
height = sub[:head] - sill
|
3708
|
+
|
3709
|
+
if height < glass
|
3710
|
+
sub[:ratio ] = 0 if sub.key?(:ratio)
|
3711
|
+
sub[:count ] = 0
|
3712
|
+
sub[:multiplier] = 0
|
3713
|
+
sub[:height ] = 0 if sub.key?(:height)
|
3714
|
+
sub[:width ] = 0 if sub.key?(:width)
|
3715
|
+
log(ERR, "Skip: invalid '#{id}' head/height combo (#{mth})")
|
3716
|
+
next
|
3717
|
+
else
|
3718
|
+
sub[:sill ] = sill
|
3719
|
+
sub[:height] = height
|
3720
|
+
log(WRN, "(Re)set '#{id}' height to #{sub[:height]} m (#{mth})")
|
3721
|
+
end
|
3722
|
+
else
|
3723
|
+
sub[:sill] = sill
|
3724
|
+
end
|
3725
|
+
else
|
3726
|
+
sub[:sill ] = typ_sill
|
3727
|
+
sub[:height] = sub[:head] - sub[:sill]
|
3728
|
+
end
|
3729
|
+
elsif sub.key?(:sill) # no "head"
|
3730
|
+
if sub.key?(:height)
|
3731
|
+
head = sub[:sill] + sub[:height]
|
3732
|
+
|
3733
|
+
if head > max_head
|
3734
|
+
head = max_head
|
3735
|
+
height = head - sub[:sill]
|
3736
|
+
|
3737
|
+
if height < glass
|
3738
|
+
sub[:ratio ] = 0 if sub.key?(:ratio)
|
3739
|
+
sub[:count ] = 0
|
3740
|
+
sub[:multiplier] = 0
|
3741
|
+
sub[:height ] = 0 if sub.key?(:height)
|
3742
|
+
sub[:width ] = 0 if sub.key?(:width)
|
3743
|
+
log(ERR, "Skip: invalid '#{id}' sill/height combo (#{mth})")
|
3744
|
+
next
|
3745
|
+
else
|
3746
|
+
sub[:head ] = head
|
3747
|
+
sub[:height] = height
|
3748
|
+
log(WRN, "(Re)set '#{id}' height to #{sub[:height]} m (#{mth})")
|
3749
|
+
end
|
3750
|
+
else
|
3751
|
+
sub[:head] = head
|
3752
|
+
end
|
3753
|
+
else
|
3754
|
+
sub[:head ] = typ_head
|
3755
|
+
sub[:height] = sub[:head] - sub[:sill]
|
3756
|
+
end
|
3757
|
+
elsif sub.key?(:height) # neither "head" nor "sill"
|
3758
|
+
head = typ_head
|
3759
|
+
sill = head - sub[:height]
|
3760
|
+
|
3761
|
+
if sill < min_sill
|
3762
|
+
sill = min_sill
|
3763
|
+
head = sill + sub[:height]
|
3764
|
+
end
|
3765
|
+
|
3766
|
+
sub[:head] = head
|
3767
|
+
sub[:sill] = sill
|
3768
|
+
else
|
3769
|
+
sub[:head ] = typ_head
|
3770
|
+
sub[:sill ] = typ_sill
|
3771
|
+
sub[:height] = sub[:head] - sub[:sill]
|
3772
|
+
end
|
3773
|
+
|
3774
|
+
# Log/reset "width" if beyond min/max.
|
3775
|
+
if sub.key?(:width)
|
3776
|
+
unless sub[:width].between?(glass, max_width)
|
3777
|
+
sub[:width] = glass if sub[:width] < glass
|
3778
|
+
sub[:width] = max_width if sub[:width] > max_width
|
3779
|
+
log(WRN, "Reset '#{id}' width to #{sub[:width]} m (#{mth})")
|
3780
|
+
end
|
3781
|
+
end
|
3782
|
+
|
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
|
+
|
3787
|
+
if sub[:count] < 1
|
3788
|
+
sub[:count] = 1
|
3789
|
+
log(WRN, "Reset '#{id}' count to #{sub[:count]} (#{mth})")
|
3790
|
+
end
|
3791
|
+
else
|
3792
|
+
sub[:count] = 1
|
3793
|
+
end
|
3794
|
+
|
3795
|
+
sub[:count] = 1 unless sub.key?(:count)
|
3796
|
+
|
3797
|
+
# Log/reset if left-sided buffer under min jamb position.
|
3798
|
+
if sub.key?(:l_buffer)
|
3799
|
+
if sub[:l_buffer] < min_ljamb
|
3800
|
+
sub[:l_buffer] = min_ljamb
|
3801
|
+
log(WRN, "Reset '#{id}' left buffer to #{sub[:l_buffer]} m (#{mth})")
|
3802
|
+
end
|
3803
|
+
end
|
3804
|
+
|
3805
|
+
# Log/reset if right-sided buffer beyond max jamb position.
|
3806
|
+
if sub.key?(:r_buffer)
|
3807
|
+
if sub[:r_buffer] > max_rjamb
|
3808
|
+
sub[:r_buffer] = min_rjamb
|
3809
|
+
log(WRN, "Reset '#{id}' right buffer to #{sub[:r_buffer]} m (#{mth})")
|
3810
|
+
end
|
3811
|
+
end
|
3812
|
+
|
3813
|
+
centre = mid_x
|
3814
|
+
centre += sub[:centreline] if sub.key?(:centreline)
|
3815
|
+
n = sub[:count ]
|
3816
|
+
h = sub[:height ] + frames
|
3817
|
+
w = 0 # overall width of sub(s) bounding box (to calculate)
|
3818
|
+
x0 = 0 # left-side X-axis coordinate of sub(s) bounding box
|
3819
|
+
xf = 0 # right-side X-axis coordinate of sub(s) bounding box
|
3820
|
+
|
3821
|
+
# Log/reset "offset", if conflicting vs "width".
|
3822
|
+
if sub.key?(:ratio)
|
3823
|
+
if sub[:ratio] < TOL
|
3824
|
+
sub[:ratio ] = 0
|
3825
|
+
sub[:count ] = 0
|
3826
|
+
sub[:multiplier] = 0
|
3827
|
+
sub[:height ] = 0 if sub.key?(:height)
|
3828
|
+
sub[:width ] = 0 if sub.key?(:width)
|
3829
|
+
log(ERR, "Skip: '#{id}' ratio ~0 (#{mth})")
|
3830
|
+
next
|
3831
|
+
end
|
3832
|
+
|
3833
|
+
# Log/reset if "ratio" beyond min/max?
|
3834
|
+
unless sub[:ratio].between?(min, max)
|
3835
|
+
sub[:ratio] = min if sub[:ratio] < min
|
3836
|
+
sub[:ratio] = max if sub[:ratio] > max
|
3837
|
+
log(WRN, "Reset ratio (min/max) to #{sub[:ratio]} (#{mth})")
|
3838
|
+
end
|
3839
|
+
|
3840
|
+
# Log/reset "count" unless 1.
|
3841
|
+
unless sub[:count] == 1
|
3842
|
+
sub[:count] = 1
|
3843
|
+
log(WRN, "Reset count (ratio) to 1 (#{mth})")
|
3844
|
+
end
|
3845
|
+
|
3846
|
+
area = s.grossArea * sub[:ratio] # sub m2, including (optional) frames
|
3847
|
+
w = area / h
|
3848
|
+
width = w - frames
|
3849
|
+
x0 = centre - w/2
|
3850
|
+
xf = centre + w/2
|
3851
|
+
|
3852
|
+
if sub.key?(:l_buffer)
|
3853
|
+
if sub.key?(:centreline)
|
3854
|
+
log(WRN, "Skip #{id} left buffer (vs centreline) (#{mth})")
|
3855
|
+
else
|
3856
|
+
x0 = sub[:l_buffer] - frame
|
3857
|
+
xf = x0 + w
|
3858
|
+
centre = x0 + w/2
|
3859
|
+
end
|
3860
|
+
elsif sub.key?(:r_buffer)
|
3861
|
+
if sub.key?(:centreline)
|
3862
|
+
log(WRN, "Skip #{id} right buffer (vs centreline) (#{mth})")
|
3863
|
+
else
|
3864
|
+
xf = max_x - sub[:r_buffer] + frame
|
3865
|
+
x0 = xf - w
|
3866
|
+
centre = x0 + w/2
|
3867
|
+
end
|
3868
|
+
end
|
3869
|
+
|
3870
|
+
# Too wide?
|
3871
|
+
if x0 < min_ljamb || xf > max_rjamb
|
3872
|
+
sub[:ratio ] = 0 if sub.key?(:ratio)
|
3873
|
+
sub[:count ] = 0
|
3874
|
+
sub[:multiplier] = 0
|
3875
|
+
sub[:height ] = 0 if sub.key?(:height)
|
3876
|
+
sub[:width ] = 0 if sub.key?(:width)
|
3877
|
+
log(ERR, "Skip: invalid (ratio) width/centreline (#{mth})")
|
3878
|
+
next
|
3879
|
+
end
|
3880
|
+
|
3881
|
+
if sub.key?(:width) && (sub[:width] - width).abs > TOL
|
3882
|
+
sub[:width] = width
|
3883
|
+
log(WRN, "Reset width (ratio) to #{sub[:width]} (#{mth})")
|
3884
|
+
end
|
3885
|
+
|
3886
|
+
sub[:width] = width unless sub.key?(:width)
|
3887
|
+
else
|
3888
|
+
unless sub.key?(:width)
|
3889
|
+
sub[:ratio ] = 0 if sub.key?(:ratio)
|
3890
|
+
sub[:count ] = 0
|
3891
|
+
sub[:multiplier] = 0
|
3892
|
+
sub[:height ] = 0 if sub.key?(:height)
|
3893
|
+
sub[:width ] = 0 if sub.key?(:width)
|
3894
|
+
log(ERR, "Skip: missing '#{id}' width (#{mth})")
|
3895
|
+
next
|
3896
|
+
end
|
3897
|
+
|
3898
|
+
width = sub[:width] + frames
|
3899
|
+
gap = (max_x - n * width) / (n + 1)
|
3900
|
+
gap = sub[:offset] - width if sub.key?(:offset)
|
3901
|
+
gap = 0 if gap < bfr
|
3902
|
+
offset = gap + width
|
3903
|
+
|
3904
|
+
if sub.key?(:offset) && (offset - sub[:offset]).abs > TOL
|
3905
|
+
sub[:offset] = offset
|
3906
|
+
log(WRN, "Reset sub offset to #{sub[:offset]} m (#{mth})")
|
3907
|
+
end
|
3908
|
+
|
3909
|
+
sub[:offset] = offset unless sub.key?(:offset)
|
3910
|
+
|
3911
|
+
# Overall width (including frames) of bounding box around array.
|
3912
|
+
w = n * width + (n - 1) * gap
|
3913
|
+
x0 = centre - w/2
|
3914
|
+
xf = centre + w/2
|
3915
|
+
|
3916
|
+
if sub.key?(:l_buffer)
|
3917
|
+
if sub.key?(:centreline)
|
3918
|
+
log(WRN, "Skip #{id} left buffer (vs centreline) (#{mth})")
|
3919
|
+
else
|
3920
|
+
x0 = sub[:l_buffer] - frame
|
3921
|
+
xf = x0 + w
|
3922
|
+
centre = x0 + w/2
|
3923
|
+
end
|
3924
|
+
elsif sub.key?(:r_buffer)
|
3925
|
+
if sub.key?(:centreline)
|
3926
|
+
log(WRN, "Skip #{id} right buffer (vs centreline) (#{mth})")
|
3927
|
+
else
|
3928
|
+
xf = max_x - sub[:r_buffer] + frame
|
3929
|
+
x0 = xf - w
|
3930
|
+
centre = x0 + w/2
|
3931
|
+
end
|
3932
|
+
end
|
3933
|
+
|
3934
|
+
# Too wide?
|
3935
|
+
if x0 < bfr || xf > max_x - bfr
|
3936
|
+
sub[:ratio ] = 0 if sub.key?(:ratio)
|
3937
|
+
sub[:count ] = 0
|
3938
|
+
sub[:multiplier] = 0
|
3939
|
+
sub[:height ] = 0 if sub.key?(:height)
|
3940
|
+
sub[:width ] = 0 if sub.key?(:width)
|
3941
|
+
log(ERR, "Skip: invalid array width/centreline (#{mth})")
|
3942
|
+
next
|
3943
|
+
end
|
3944
|
+
end
|
3945
|
+
|
3946
|
+
# Initialize left-side X-axis coordinate of only/first sub.
|
3947
|
+
pos = x0 + frame
|
3948
|
+
|
3949
|
+
# Generate sub(s).
|
3950
|
+
sub[:count].times do |i|
|
3951
|
+
name = "#{id}|#{i}"
|
3952
|
+
fr = 0
|
3953
|
+
fr = sub[:frame].frameWidth if sub[:frame]
|
3954
|
+
|
3955
|
+
vec = OpenStudio::Point3dVector.new
|
3956
|
+
vec << OpenStudio::Point3d.new(pos, sub[:head], 0)
|
3957
|
+
vec << OpenStudio::Point3d.new(pos, sub[:sill], 0)
|
3958
|
+
vec << OpenStudio::Point3d.new(pos + sub[:width], sub[:sill], 0)
|
3959
|
+
vec << OpenStudio::Point3d.new(pos + sub[:width], sub[:head], 0)
|
3960
|
+
vec = t * vec
|
3961
|
+
|
3962
|
+
# Log/skip if conflict between individual sub and base surface.
|
3963
|
+
vc = vec
|
3964
|
+
vc = offset(vc, fr, 300) if fr > 0
|
3965
|
+
ok = fits?(vc, s)
|
3966
|
+
log(ERR, "Skip '#{name}': won't fit in '#{nom}' (#{mth})") unless ok
|
3967
|
+
break unless ok
|
3968
|
+
|
3969
|
+
# Log/skip if conflicts with existing subs (even if same array).
|
3970
|
+
s.subSurfaces.each do |sb|
|
3971
|
+
nome = sb.nameString
|
3972
|
+
fd = sb.windowPropertyFrameAndDivider
|
3973
|
+
fr = 0 if fd.empty?
|
3974
|
+
fr = fd.get.frameWidth unless fd.empty?
|
3975
|
+
vk = sb.vertices
|
3976
|
+
vk = offset(vk, fr, 300) if fr > 0
|
3977
|
+
oops = overlaps?(vc, vk)
|
3978
|
+
log(ERR, "Skip '#{name}': overlaps '#{nome}' (#{mth})") if oops
|
3979
|
+
ok = false if oops
|
3980
|
+
break if oops
|
3981
|
+
end
|
3982
|
+
|
3983
|
+
break unless ok
|
3984
|
+
|
3985
|
+
sb = OpenStudio::Model::SubSurface.new(vec, mdl)
|
3986
|
+
sb.setName(name)
|
3987
|
+
sb.setSubSurfaceType(sub[:type])
|
3988
|
+
sb.setConstruction(sub[:assembly]) if sub[:assembly]
|
3989
|
+
ok = sb.allowWindowPropertyFrameAndDivider
|
3990
|
+
sb.setWindowPropertyFrameAndDivider(sub[:frame]) if sub[:frame] && ok
|
3991
|
+
sb.setMultiplier(sub[:multiplier]) if sub[:multiplier] > 1
|
3992
|
+
sb.setSurface(s)
|
3993
|
+
|
3994
|
+
# Reset "pos" if array.
|
3995
|
+
pos += sub[:offset] if sub.key?(:offset)
|
3996
|
+
end
|
3997
|
+
end
|
3998
|
+
|
3999
|
+
true
|
4000
|
+
end
|
4001
|
+
|
1687
4002
|
##
|
1688
4003
|
# Callback when other modules extend OSlg
|
1689
4004
|
#
|