tbd 3.4.4 → 3.5.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 +16 -48
- data/LICENSE.md +1 -1
- data/README.md +29 -35
- data/lib/measures/tbd/LICENSE.md +1 -1
- data/lib/measures/tbd/measure.rb +1 -1
- data/lib/measures/tbd/measure.xml +11 -11
- data/lib/measures/tbd/resources/geo.rb +11 -10
- data/lib/measures/tbd/resources/geometry.rb +351 -351
- data/lib/measures/tbd/resources/model.rb +1425 -1425
- data/lib/measures/tbd/resources/oslog.rb +43 -25
- data/lib/measures/tbd/resources/psi.rb +109 -85
- data/lib/measures/tbd/resources/tbd.rb +1 -1
- data/lib/measures/tbd/resources/transformation.rb +120 -120
- data/lib/measures/tbd/resources/ua.rb +66 -66
- data/lib/measures/tbd/resources/utils.rb +989 -613
- data/lib/measures/tbd/resources/version.rb +3 -3
- data/lib/measures/tbd/tests/tbd_tests.rb +1 -1
- data/lib/tbd/geo.rb +11 -10
- data/lib/tbd/psi.rb +109 -85
- data/lib/tbd/ua.rb +66 -66
- data/lib/tbd/version.rb +2 -2
- data/lib/tbd.rb +18 -12
- data/tbd.gemspec +1 -1
- data/v291_MacOS.md +19 -28
- metadata +5 -5
@@ -1,6 +1,6 @@
|
|
1
1
|
# BSD 3-Clause License
|
2
2
|
#
|
3
|
-
# Copyright (c) 2022-
|
3
|
+
# Copyright (c) 2022-2025, Denis Bourgeois
|
4
4
|
# All rights reserved.
|
5
5
|
#
|
6
6
|
# Redistribution and use in source and binary forms, with or without
|
@@ -31,20 +31,26 @@
|
|
31
31
|
require "openstudio"
|
32
32
|
|
33
33
|
module OSut
|
34
|
-
# DEBUG for devs; WARN/ERROR for users (bad OS input), see OSlg
|
35
34
|
extend OSlg
|
36
35
|
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
36
|
+
DBG = OSlg::DEBUG.dup # see github.com/rd2/oslg
|
37
|
+
INF = OSlg::INFO.dup # see github.com/rd2/oslg
|
38
|
+
WRN = OSlg::WARN.dup # see github.com/rd2/oslg
|
39
|
+
ERR = OSlg::ERROR.dup # see github.com/rd2/oslg
|
40
|
+
FTL = OSlg::FATAL.dup # see github.com/rd2/oslg
|
41
|
+
NS = "nameString" # OpenStudio object identifier method
|
42
|
+
TOL = 0.01 # default distance tolerance (m)
|
43
|
+
TOL2 = TOL * TOL # default area tolerance (m2)
|
44
|
+
HEAD = 2.032 # standard 80" door
|
45
|
+
SILL = 0.762 # standard 30" window sill
|
46
|
+
DMIN = 0.010 # min. insulating material thickness
|
47
|
+
DMAX = 1.000 # max. insulating material thickness
|
48
|
+
KMIN = 0.010 # min. insulating material thermal conductivity
|
49
|
+
KMAX = 2.000 # max. insulating material thermal conductivity
|
50
|
+
UMAX = KMAX / DMIN # material USi upper limit, 200.000
|
51
|
+
UMIN = KMIN / DMAX # material USi lower limit, 0.010
|
52
|
+
RMIN = 1.0 / UMAX # material RSi lower limit, 0.005 (or R-IP 0.03)
|
53
|
+
RMAX = 1.0 / UMIN # material RSi upper limit, 100.000 (or R-IP 567.80)
|
48
54
|
|
49
55
|
# General surface orientations (see facets method)
|
50
56
|
SIDZ = [:bottom, # e.g. ground-facing, exposed floors
|
@@ -191,6 +197,388 @@ module OSut
|
|
191
197
|
@@mats[:door ][:rho] = 600.000
|
192
198
|
@@mats[:door ][:cp ] = 1000.000
|
193
199
|
|
200
|
+
##
|
201
|
+
# Validates if every material in a layered construction is standard & opaque.
|
202
|
+
#
|
203
|
+
# @param lc [OpenStudio::LayeredConstruction] a layered construction
|
204
|
+
#
|
205
|
+
# @return [Bool] whether all layers are valid
|
206
|
+
# @return [false] if invalid input (see logs)
|
207
|
+
def standardOpaqueLayers?(lc = nil)
|
208
|
+
mth = "OSut::#{__callee__}"
|
209
|
+
cl = OpenStudio::Model::LayeredConstruction
|
210
|
+
return invalid("lc", mth, 1, DBG, false) unless lc.respond_to?(NS)
|
211
|
+
return mismatch(lc.nameString, lc, cl, mth, DBG, false) unless lc.is_a?(cl)
|
212
|
+
|
213
|
+
lc.layers.each { |m| return false if m.to_StandardOpaqueMaterial.empty? }
|
214
|
+
|
215
|
+
true
|
216
|
+
end
|
217
|
+
|
218
|
+
##
|
219
|
+
# Returns total (standard opaque) layered construction thickness (m).
|
220
|
+
#
|
221
|
+
# @param lc [OpenStudio::LayeredConstruction] a layered construction
|
222
|
+
#
|
223
|
+
# @return [Float] construction thickness
|
224
|
+
# @return [0.0] if invalid input (see logs)
|
225
|
+
def thickness(lc = nil)
|
226
|
+
mth = "OSut::#{__callee__}"
|
227
|
+
cl = OpenStudio::Model::LayeredConstruction
|
228
|
+
return invalid("lc", mth, 1, DBG, 0.0) unless lc.respond_to?(NS)
|
229
|
+
return mismatch(lc.nameString, lc, cl, mth, DBG, 0.0) unless lc.is_a?(cl)
|
230
|
+
|
231
|
+
unless standardOpaqueLayers?(lc)
|
232
|
+
log(ERR, "#{lc.nameString} holds non-StandardOpaqueMaterial(s) (#{mth})")
|
233
|
+
return 0.0
|
234
|
+
end
|
235
|
+
|
236
|
+
thickness = 0.0
|
237
|
+
|
238
|
+
lc.layers.each { |m| thickness += m.thickness }
|
239
|
+
|
240
|
+
thickness
|
241
|
+
end
|
242
|
+
|
243
|
+
##
|
244
|
+
# Returns total air film resistance of a fenestrated construction (m2•K/W)
|
245
|
+
#
|
246
|
+
# @param usi [Numeric] a fenestrated construction's U-factor (W/m2•K)
|
247
|
+
#
|
248
|
+
# @return [Float] total air film resistances
|
249
|
+
# @return [0.1216] if invalid input (see logs)
|
250
|
+
def glazingAirFilmRSi(usi = 5.85)
|
251
|
+
# The sum of thermal resistances of calculated exterior and interior film
|
252
|
+
# coefficients under standard winter conditions are taken from:
|
253
|
+
#
|
254
|
+
# https://bigladdersoftware.com/epx/docs/9-6/engineering-reference/
|
255
|
+
# window-calculation-module.html#simple-window-model
|
256
|
+
#
|
257
|
+
# These remain acceptable approximations for flat windows, yet likely
|
258
|
+
# unsuitable for subsurfaces with curved or projecting shapes like domed
|
259
|
+
# skylights. The solution here is considered an adequate fix for reporting.
|
260
|
+
#
|
261
|
+
# For U-factors above 8.0 W/m2•K (or invalid input), the function returns
|
262
|
+
# 0.1216 m2•K/W, which corresponds to a construction with a single glass
|
263
|
+
# layer thickness of 2mm & k = ~0.6 W/m.K.
|
264
|
+
#
|
265
|
+
# The EnergyPlus Engineering calculations were designed for vertical
|
266
|
+
# windows - not horizontal, slanted or domed surfaces - use with caution.
|
267
|
+
mth = "OSut::#{__callee__}"
|
268
|
+
cl = Numeric
|
269
|
+
return mismatch("usi", usi, cl, mth, DBG, 0.1216) unless usi.is_a?(cl)
|
270
|
+
return invalid("usi", mth, 1, WRN, 0.1216) if usi > 8.0
|
271
|
+
return negative("usi", mth, WRN, 0.1216) if usi < 0
|
272
|
+
return zero("usi", mth, WRN, 0.1216) if usi.abs < TOL
|
273
|
+
|
274
|
+
rsi = 1 / (0.025342 * usi + 29.163853) # exterior film, next interior film
|
275
|
+
return rsi + 1 / (0.359073 * Math.log(usi) + 6.949915) if usi < 5.85
|
276
|
+
return rsi + 1 / (1.788041 * usi - 2.886625)
|
277
|
+
end
|
278
|
+
|
279
|
+
##
|
280
|
+
# Returns a construction's 'standard calc' thermal resistance (m2•K/W), which
|
281
|
+
# includes air film resistances. It excludes insulating effects of shades,
|
282
|
+
# screens, etc. in the case of fenestrated constructions.
|
283
|
+
#
|
284
|
+
# @param lc [OpenStudio::Model::LayeredConstruction] a layered construction
|
285
|
+
# @param film [Numeric] thermal resistance of surface air films (m2•K/W)
|
286
|
+
# @param t [Numeric] gas temperature (°C) (optional)
|
287
|
+
#
|
288
|
+
# @return [Float] layered construction's thermal resistance
|
289
|
+
# @return [0.0] if invalid input (see logs)
|
290
|
+
def rsi(lc = nil, film = 0.0, t = 0.0)
|
291
|
+
# This is adapted from BTAP's Material Module "get_conductance" (P. Lopez)
|
292
|
+
#
|
293
|
+
# https://github.com/NREL/OpenStudio-Prototype-Buildings/blob/
|
294
|
+
# c3d5021d8b7aef43e560544699fb5c559e6b721d/lib/btap/measures/
|
295
|
+
# btap_equest_converter/envelope.rb#L122
|
296
|
+
mth = "OSut::#{__callee__}"
|
297
|
+
cl1 = OpenStudio::Model::LayeredConstruction
|
298
|
+
cl2 = Numeric
|
299
|
+
return invalid("lc", mth, 1, DBG, 0.0) unless lc.respond_to?(NS)
|
300
|
+
return mismatch(lc.nameString, lc, cl1, mth, DBG, 0.0) unless lc.is_a?(cl1)
|
301
|
+
return mismatch("film", film, cl2, mth, DBG, 0.0) unless film.is_a?(cl2)
|
302
|
+
return mismatch("temp K", t, cl2, mth, DBG, 0.0) unless t.is_a?(cl2)
|
303
|
+
|
304
|
+
t += 273.0 # °C to K
|
305
|
+
return negative("temp K", mth, ERR, 0.0) if t < 0
|
306
|
+
return negative("film", mth, ERR, 0.0) if film < 0
|
307
|
+
|
308
|
+
rsi = film
|
309
|
+
|
310
|
+
lc.layers.each do |m|
|
311
|
+
# Fenestration materials first.
|
312
|
+
empty = m.to_SimpleGlazing.empty?
|
313
|
+
return 1 / m.to_SimpleGlazing.get.uFactor unless empty
|
314
|
+
|
315
|
+
empty = m.to_StandardGlazing.empty?
|
316
|
+
rsi += m.to_StandardGlazing.get.thermalResistance unless empty
|
317
|
+
empty = m.to_RefractionExtinctionGlazing.empty?
|
318
|
+
rsi += m.to_RefractionExtinctionGlazing.get.thermalResistance unless empty
|
319
|
+
empty = m.to_Gas.empty?
|
320
|
+
rsi += m.to_Gas.get.getThermalResistance(t) unless empty
|
321
|
+
empty = m.to_GasMixture.empty?
|
322
|
+
rsi += m.to_GasMixture.get.getThermalResistance(t) unless empty
|
323
|
+
|
324
|
+
# Opaque materials next.
|
325
|
+
empty = m.to_StandardOpaqueMaterial.empty?
|
326
|
+
rsi += m.to_StandardOpaqueMaterial.get.thermalResistance unless empty
|
327
|
+
empty = m.to_MasslessOpaqueMaterial.empty?
|
328
|
+
rsi += m.to_MasslessOpaqueMaterial.get.thermalResistance unless empty
|
329
|
+
empty = m.to_RoofVegetation.empty?
|
330
|
+
rsi += m.to_RoofVegetation.get.thermalResistance unless empty
|
331
|
+
empty = m.to_AirGap.empty?
|
332
|
+
rsi += m.to_AirGap.get.thermalResistance unless empty
|
333
|
+
end
|
334
|
+
|
335
|
+
rsi
|
336
|
+
end
|
337
|
+
|
338
|
+
##
|
339
|
+
# Identifies a layered construction's (opaque) insulating layer. The method
|
340
|
+
# returns a 3-keyed hash :index, the insulating layer index [0, n layers)
|
341
|
+
# within the layered construction; :type, either :standard or :massless; and
|
342
|
+
# :r, material thermal resistance in m2•K/W.
|
343
|
+
#
|
344
|
+
# @param lc [OpenStudio::Model::LayeredConstruction] a layered construction
|
345
|
+
#
|
346
|
+
# @return [Hash] index: (Integer), type: (Symbol), r: (Float)
|
347
|
+
# @return [Hash] index: nil, type: nil, r: 0.0 if invalid input (see logs)
|
348
|
+
def insulatingLayer(lc = nil)
|
349
|
+
mth = "OSut::#{__callee__}"
|
350
|
+
cl = OpenStudio::Model::LayeredConstruction
|
351
|
+
res = { index: nil, type: nil, r: 0.0 }
|
352
|
+
i = 0 # iterator
|
353
|
+
return invalid("lc", mth, 1, DBG, res) unless lc.respond_to?(NS)
|
354
|
+
return mismatch(lc.nameString, lc, cl, mth, DBG, res) unless lc.is_a?(cl)
|
355
|
+
|
356
|
+
lc.layers.each do |m|
|
357
|
+
unless m.to_MasslessOpaqueMaterial.empty?
|
358
|
+
m = m.to_MasslessOpaqueMaterial.get
|
359
|
+
|
360
|
+
if m.thermalResistance < RMIN || m.thermalResistance < res[:r]
|
361
|
+
i += 1
|
362
|
+
next
|
363
|
+
else
|
364
|
+
res[:r ] = m.thermalResistance
|
365
|
+
res[:index] = i
|
366
|
+
res[:type ] = :massless
|
367
|
+
end
|
368
|
+
end
|
369
|
+
|
370
|
+
unless m.to_StandardOpaqueMaterial.empty?
|
371
|
+
m = m.to_StandardOpaqueMaterial.get
|
372
|
+
k = m.thermalConductivity
|
373
|
+
d = m.thickness
|
374
|
+
|
375
|
+
if d < DMIN || k > KMAX || d / k < res[:r]
|
376
|
+
i += 1
|
377
|
+
next
|
378
|
+
else
|
379
|
+
res[:r ] = d / k
|
380
|
+
res[:index] = i
|
381
|
+
res[:type ] = :standard
|
382
|
+
end
|
383
|
+
end
|
384
|
+
|
385
|
+
i += 1
|
386
|
+
end
|
387
|
+
|
388
|
+
res
|
389
|
+
end
|
390
|
+
|
391
|
+
##
|
392
|
+
# Validates whether a material is both uniquely reserved to a single layered
|
393
|
+
# construction in a model, and referenced only once in the construction.
|
394
|
+
# Limited to 'standard' or 'massless' materials.
|
395
|
+
#
|
396
|
+
# @param m [OpenStudio::Model::OpaqueMaterial] a material
|
397
|
+
#
|
398
|
+
# @return [Boolean] whether material is unique
|
399
|
+
# @return [false] if missing)
|
400
|
+
def uniqueMaterial?(m = nil)
|
401
|
+
mth = "OSut::#{__callee__}"
|
402
|
+
cl1 = OpenStudio::Model::OpaqueMaterial
|
403
|
+
return invalid("mat", mth, 1, DBG, false) unless m.respond_to?(NS)
|
404
|
+
return mismatch(m.nameString, m, cl1, mth, DBG, false) unless m.is_a?(cl1)
|
405
|
+
|
406
|
+
num = 0
|
407
|
+
lcs = m.model.getLayeredConstructions
|
408
|
+
|
409
|
+
unless m.to_MasslessOpaqueMaterial.empty?
|
410
|
+
m = m.to_MasslessOpaqueMaterial.get
|
411
|
+
|
412
|
+
lcs.each { |lc| num += lc.getLayerIndices(m).size }
|
413
|
+
|
414
|
+
return true if num == 1
|
415
|
+
end
|
416
|
+
|
417
|
+
unless m.to_StandardOpaqueMaterial.empty?
|
418
|
+
m = m.to_StandardOpaqueMaterial.get
|
419
|
+
|
420
|
+
lcs.each { |lc| num += lc.getLayerIndices(m).size }
|
421
|
+
|
422
|
+
return true if num == 1
|
423
|
+
end
|
424
|
+
|
425
|
+
false
|
426
|
+
end
|
427
|
+
|
428
|
+
##
|
429
|
+
# Sets a layered construction material as unique. Solution similar to
|
430
|
+
# OpenStudio::Model::LayeredConstruction's 'ensureUniqueLayers', yet limited
|
431
|
+
# here to a single indexed OpenStudio material, typically the principal
|
432
|
+
# insulating material. Returns true if the indexed material is already unique.
|
433
|
+
# Limited to 'standard' or 'massless' materials.
|
434
|
+
#
|
435
|
+
# @param lc [OpenStudio::Model::LayeredConstruction] a construction
|
436
|
+
# @param index [Integer] the construction layer index of the material
|
437
|
+
#
|
438
|
+
# @return [Boolean] if assigned as unique
|
439
|
+
# @return [false] if invalid inputs
|
440
|
+
def assignUniqueMaterial(lc = nil, index = nil)
|
441
|
+
mth = "OSut::#{__callee__}"
|
442
|
+
cl1 = OpenStudio::Model::LayeredConstruction
|
443
|
+
cl2 = Integer
|
444
|
+
return invalid("lc", mth, 1, DBG, false) unless lc.respond_to?(NS)
|
445
|
+
return mismatch(lc.nameString, lc, cl1, mth, DBG, false) unless lc.is_a?(cl1)
|
446
|
+
return mismatch("index", index, cl2, mth, DBG, false) unless index.is_a?(cl2)
|
447
|
+
return invalid("index", mth, 0, DBG, false) unless index.between?(0, lc.numLayers - 1)
|
448
|
+
|
449
|
+
m = lc.getLayer(index)
|
450
|
+
|
451
|
+
unless m.to_MasslessOpaqueMaterial.empty?
|
452
|
+
m = m.to_MasslessOpaqueMaterial.get
|
453
|
+
return true if uniqueMaterial?(m)
|
454
|
+
|
455
|
+
mat = m.clone(m.model).to_MasslessOpaqueMaterial.get
|
456
|
+
return lc.setLayer(index, mat)
|
457
|
+
end
|
458
|
+
|
459
|
+
unless m.to_StandardOpaqueMaterial.empty?
|
460
|
+
m = m.to_StandardOpaqueMaterial.get
|
461
|
+
return true if uniqueMaterial?(m)
|
462
|
+
|
463
|
+
mat = m.clone(m.model).to_StandardOpaqueMaterial.get
|
464
|
+
return lc.setLayer(index, mat)
|
465
|
+
end
|
466
|
+
|
467
|
+
false
|
468
|
+
end
|
469
|
+
|
470
|
+
##
|
471
|
+
# Resets a construction's Uo factor by adjusting its insulating layer
|
472
|
+
# thermal conductivity, then if needed its thickness (or its RSi value if
|
473
|
+
# massless). Unless material uniquness is requested, a matching material is
|
474
|
+
# recovered instead of instantiating a new one. The latter is renamed
|
475
|
+
# according to its adjusted conductivity/thickness (or RSi value).
|
476
|
+
#
|
477
|
+
# @param lc [OpenStudio::Model::LayeredConstruction] a construction
|
478
|
+
# @param film [Float] construction air film resistance
|
479
|
+
# @param index [Integer] the insulating layer's array index
|
480
|
+
# @param uo [Float] desired Uo factor (with air film resistance)
|
481
|
+
# @param uniq [Boolean] whether to enforce material uniqueness
|
482
|
+
#
|
483
|
+
# @return [Float] new layer RSi [RMIN, RMAX]
|
484
|
+
# @return [0.0] if invalid input
|
485
|
+
def resetUo(lc = nil, film = nil, index = nil, uo = nil, uniq = false)
|
486
|
+
mth = "OSut::#{__callee__}"
|
487
|
+
r = 0.0 # thermal resistance of new material
|
488
|
+
cl1 = OpenStudio::Model::LayeredConstruction
|
489
|
+
cl2 = Numeric
|
490
|
+
cl3 = Integer
|
491
|
+
return invalid("lc", mth, 1, DBG, r) unless lc.respond_to?(NS)
|
492
|
+
return mismatch(lc.nameString, lc, cl1, mth, DBG, r) unless lc.is_a?(cl1)
|
493
|
+
return mismatch("film", film, cl2, mth, DBG, r) unless film.is_a?(cl2)
|
494
|
+
return negative("film", mth, DBG, r) if film.negative?
|
495
|
+
return mismatch("index", index, cl3, mth, DBG, r) unless index.is_a?(cl3)
|
496
|
+
return invalid("index", mth, 3, DBG, r) unless index.between?(0, lc.numLayers - 1)
|
497
|
+
return mismatch("uo", uo, cl2, mth, DBG, r) unless uo.is_a?(cl2)
|
498
|
+
|
499
|
+
unless uo.between?(UMIN, UMAX)
|
500
|
+
uo = clamp(UMIN, UMAX)
|
501
|
+
log(WRN, "Resetting Uo (#{lc.nameString}) to #{uo.round(3)} (#{mth})")
|
502
|
+
end
|
503
|
+
|
504
|
+
uniq = false unless [true, false].include?(uniq)
|
505
|
+
r0 = rsi(lc, film) # current construction RSi value
|
506
|
+
ro = 1 / uo # desired construction RSi value
|
507
|
+
dR = ro - r0 # desired increase in construction RSi
|
508
|
+
m = lc.getLayer(index)
|
509
|
+
|
510
|
+
unless m.to_MasslessOpaqueMaterial.empty?
|
511
|
+
m = m.to_MasslessOpaqueMaterial.get
|
512
|
+
r = m.thermalResistance
|
513
|
+
return r if dR.abs.round(2) == 0.00
|
514
|
+
|
515
|
+
r = (r + dR).clamp(RMIN, RMAX)
|
516
|
+
id = "OSut:RSi#{r.round(2)}"
|
517
|
+
mt = lc.model.getMasslessOpaqueMaterialByName(id)
|
518
|
+
|
519
|
+
# Existing material?
|
520
|
+
unless mt.empty?
|
521
|
+
mt = mt.get
|
522
|
+
|
523
|
+
if r.round(2) == mt.thermalResistance.round(2) && uniq == false
|
524
|
+
lc.setLayer(index, mt)
|
525
|
+
return r
|
526
|
+
end
|
527
|
+
end
|
528
|
+
|
529
|
+
mt = m.clone(m.model).to_MasslessOpaqueMaterial.get
|
530
|
+
mt.setName(id)
|
531
|
+
|
532
|
+
unless mt.setThermalResistance(r)
|
533
|
+
return invalid("Failed #{id}: RSi#{de_r.round(2)}", mth)
|
534
|
+
end
|
535
|
+
|
536
|
+
lc.setLayer(index, mt)
|
537
|
+
|
538
|
+
return r
|
539
|
+
end
|
540
|
+
|
541
|
+
unless m.to_StandardOpaqueMaterial.empty?
|
542
|
+
m = m.to_StandardOpaqueMaterial.get
|
543
|
+
r = m.thickness / m.conductivity
|
544
|
+
return r if dR.abs.round(2) == 0.00
|
545
|
+
|
546
|
+
k = (m.thickness / (r + dR)).clamp(KMIN, KMAX)
|
547
|
+
d = (k * (r + dR)).clamp(DMIN, DMAX)
|
548
|
+
r = d / k
|
549
|
+
id = "OSUT:K#{format('%4.3f', k)}:#{format('%03d', d*1000)[-3..-1]}"
|
550
|
+
mt = lc.model.getStandardOpaqueMaterialByName(id)
|
551
|
+
|
552
|
+
# Existing material?
|
553
|
+
unless mt.empty?
|
554
|
+
mt = mt.get
|
555
|
+
rt = mt.thickness / mt.conductivity
|
556
|
+
|
557
|
+
if r.round(2) == rt.round(2) && uniq == false
|
558
|
+
lc.setLayer(index, mt)
|
559
|
+
return r
|
560
|
+
end
|
561
|
+
end
|
562
|
+
|
563
|
+
mt = m.clone(m.model).to_StandardOpaqueMaterial.get
|
564
|
+
mt.setName(id)
|
565
|
+
|
566
|
+
unless mt.setThermalConductivity(k)
|
567
|
+
return invalid("Failed #{id}: K#{k.round(3)}", mth)
|
568
|
+
end
|
569
|
+
|
570
|
+
unless mt.setThickness(d)
|
571
|
+
return invalid("Failed #{id}: #{(d*1000).to_i}mm", mth)
|
572
|
+
end
|
573
|
+
|
574
|
+
lc.setLayer(index, mt)
|
575
|
+
|
576
|
+
return r
|
577
|
+
end
|
578
|
+
|
579
|
+
0
|
580
|
+
end
|
581
|
+
|
194
582
|
##
|
195
583
|
# Generates an OpenStudio multilayered construction, + materials if needed.
|
196
584
|
#
|
@@ -214,19 +602,27 @@ module OSut
|
|
214
602
|
|
215
603
|
specs[:id] = "" unless specs.key?(:id)
|
216
604
|
id = trim(specs[:id])
|
217
|
-
id = "OSut
|
605
|
+
id = "OSut:CON:#{specs[:type]}" if id.empty?
|
218
606
|
|
219
|
-
|
220
|
-
|
221
|
-
|
607
|
+
if specs.key?(:type)
|
608
|
+
unless @@uo.keys.include?(specs[:type])
|
609
|
+
return invalid("surface type", mth, 2, ERR)
|
610
|
+
end
|
611
|
+
else
|
612
|
+
specs[:type] = :wall
|
613
|
+
end
|
222
614
|
|
223
|
-
specs[:uo] = @@uo[ specs[:type] ] unless specs.key?(:uo)
|
615
|
+
specs[:uo] = @@uo[ specs[:type] ] unless specs.key?(:uo) # can be nil
|
224
616
|
u = specs[:uo]
|
225
617
|
|
226
|
-
|
227
|
-
return mismatch("#{id} Uo", u, Numeric, mth)
|
228
|
-
|
229
|
-
|
618
|
+
unless u.nil?
|
619
|
+
return mismatch("#{id} Uo", u, Numeric, mth) unless u.is_a?(Numeric)
|
620
|
+
|
621
|
+
unless u.between?(UMIN, 5.678)
|
622
|
+
uO = u
|
623
|
+
u = uO.clamp(UMIN, 5.678)
|
624
|
+
log(ERR, "Resetting Uo #{uO.round(3)} to #{u.round(3)} (#{mth})")
|
625
|
+
end
|
230
626
|
end
|
231
627
|
|
232
628
|
# Optional specs. Log/reset if invalid.
|
@@ -255,14 +651,14 @@ module OSut
|
|
255
651
|
d = 0.015
|
256
652
|
a[:compo][:mat] = @@mats[mt]
|
257
653
|
a[:compo][:d ] = d
|
258
|
-
a[:compo][:id ] = "OSut
|
654
|
+
a[:compo][:id ] = "OSut:#{mt}:#{format('%03d', d*1000)[-3..-1]}"
|
259
655
|
when :partition
|
260
656
|
unless specs[:clad] == :none
|
261
657
|
d = 0.015
|
262
658
|
mt = :drywall
|
263
659
|
a[:clad][:mat] = @@mats[mt]
|
264
660
|
a[:clad][:d ] = d
|
265
|
-
a[:clad][:id ] = "OSut
|
661
|
+
a[:clad][:id ] = "OSut:#{mt}:#{format('%03d', d*1000)[-3..-1]}"
|
266
662
|
end
|
267
663
|
|
268
664
|
d = 0.015
|
@@ -274,14 +670,14 @@ module OSut
|
|
274
670
|
mt = :mineral if u
|
275
671
|
a[:compo][:mat] = @@mats[mt]
|
276
672
|
a[:compo][:d ] = d
|
277
|
-
a[:compo][:id ] = "OSut
|
673
|
+
a[:compo][:id ] = "OSut:#{mt}:#{format('%03d', d*1000)[-3..-1]}"
|
278
674
|
|
279
675
|
unless specs[:finish] == :none
|
280
676
|
d = 0.015
|
281
677
|
mt = :drywall
|
282
678
|
a[:finish][:mat] = @@mats[mt]
|
283
679
|
a[:finish][:d ] = d
|
284
|
-
a[:finish][:id ] = "OSut
|
680
|
+
a[:finish][:id ] = "OSut:#{mt}:#{format('%03d', d*1000)[-3..-1]}"
|
285
681
|
end
|
286
682
|
when :wall
|
287
683
|
unless specs[:clad] == :none
|
@@ -292,7 +688,7 @@ module OSut
|
|
292
688
|
d = 0.015 if specs[:clad] == :light
|
293
689
|
a[:clad][:mat] = @@mats[mt]
|
294
690
|
a[:clad][:d ] = d
|
295
|
-
a[:clad][:id ] = "OSut
|
691
|
+
a[:clad][:id ] = "OSut:#{mt}:#{format('%03d', d*1000)[-3..-1]}"
|
296
692
|
end
|
297
693
|
|
298
694
|
mt = :drywall
|
@@ -302,7 +698,7 @@ module OSut
|
|
302
698
|
d = 0.015 if specs[:frame] == :light
|
303
699
|
a[:sheath][:mat] = @@mats[mt]
|
304
700
|
a[:sheath][:d ] = d
|
305
|
-
a[:sheath][:id ] = "OSut
|
701
|
+
a[:sheath][:id ] = "OSut:#{mt}:#{format('%03d', d*1000)[-3..-1]}"
|
306
702
|
|
307
703
|
mt = :mineral
|
308
704
|
mt = :cellulose if specs[:frame] == :medium
|
@@ -314,7 +710,7 @@ module OSut
|
|
314
710
|
|
315
711
|
a[:compo][:mat] = @@mats[mt]
|
316
712
|
a[:compo][:d ] = d
|
317
|
-
a[:compo][:id ] = "OSut
|
713
|
+
a[:compo][:id ] = "OSut:#{mt}:#{format('%03d', d*1000)[-3..-1]}"
|
318
714
|
|
319
715
|
unless specs[:finish] == :none
|
320
716
|
mt = :concrete
|
@@ -324,7 +720,7 @@ module OSut
|
|
324
720
|
d = 0.200 if specs[:finish] == :heavy
|
325
721
|
a[:finish][:mat] = @@mats[mt]
|
326
722
|
a[:finish][:d ] = d
|
327
|
-
a[:finish][:id ] = "OSut
|
723
|
+
a[:finish][:id ] = "OSut:#{mt}:#{format('%03d', d*1000)[-3..-1]}"
|
328
724
|
end
|
329
725
|
when :roof
|
330
726
|
unless specs[:clad] == :none
|
@@ -335,7 +731,7 @@ module OSut
|
|
335
731
|
d = 0.200 if specs[:clad] == :heavy # e.g. parking garage
|
336
732
|
a[:clad][:mat] = @@mats[mt]
|
337
733
|
a[:clad][:d ] = d
|
338
|
-
a[:clad][:id ] = "OSut
|
734
|
+
a[:clad][:id ] = "OSut:#{mt}:#{format('%03d', d*1000)[-3..-1]}"
|
339
735
|
end
|
340
736
|
|
341
737
|
mt = :mineral
|
@@ -346,7 +742,7 @@ module OSut
|
|
346
742
|
d = 0.015 unless u
|
347
743
|
a[:compo][:mat] = @@mats[mt]
|
348
744
|
a[:compo][:d ] = d
|
349
|
-
a[:compo][:id ] = "OSut
|
745
|
+
a[:compo][:id ] = "OSut:#{mt}:#{format('%03d', d*1000)[-3..-1]}"
|
350
746
|
|
351
747
|
unless specs[:finish] == :none
|
352
748
|
mt = :concrete
|
@@ -356,7 +752,7 @@ module OSut
|
|
356
752
|
d = 0.200 if specs[:finish] == :heavy
|
357
753
|
a[:finish][:mat] = @@mats[mt]
|
358
754
|
a[:finish][:d ] = d
|
359
|
-
a[:finish][:id ] = "OSut
|
755
|
+
a[:finish][:id ] = "OSut:#{mt}:#{format('%03d', d*1000)[-3..-1]}"
|
360
756
|
end
|
361
757
|
when :floor
|
362
758
|
unless specs[:clad] == :none
|
@@ -364,7 +760,7 @@ module OSut
|
|
364
760
|
d = 0.015
|
365
761
|
a[:clad][:mat] = @@mats[mt]
|
366
762
|
a[:clad][:d ] = d
|
367
|
-
a[:clad][:id ] = "OSut
|
763
|
+
a[:clad][:id ] = "OSut:#{mt}:#{format('%03d', d*1000)[-3..-1]}"
|
368
764
|
end
|
369
765
|
|
370
766
|
mt = :mineral
|
@@ -375,7 +771,7 @@ module OSut
|
|
375
771
|
d = 0.015 unless u
|
376
772
|
a[:compo][:mat] = @@mats[mt]
|
377
773
|
a[:compo][:d ] = d
|
378
|
-
a[:compo][:id ] = "OSut
|
774
|
+
a[:compo][:id ] = "OSut:#{mt}:#{format('%03d', d*1000)[-3..-1]}"
|
379
775
|
|
380
776
|
unless specs[:finish] == :none
|
381
777
|
mt = :concrete
|
@@ -385,21 +781,21 @@ module OSut
|
|
385
781
|
d = 0.200 if specs[:finish] == :heavy
|
386
782
|
a[:finish][:mat] = @@mats[mt]
|
387
783
|
a[:finish][:d ] = d
|
388
|
-
a[:finish][:id ] = "OSut
|
784
|
+
a[:finish][:id ] = "OSut:#{mt}:#{format('%03d', d*1000)[-3..-1]}"
|
389
785
|
end
|
390
786
|
when :slab
|
391
787
|
mt = :sand
|
392
788
|
d = 0.100
|
393
789
|
a[:clad][:mat] = @@mats[mt]
|
394
790
|
a[:clad][:d ] = d
|
395
|
-
a[:clad][:id ] = "OSut
|
791
|
+
a[:clad][:id ] = "OSut:#{mt}:#{format('%03d', d*1000)[-3..-1]}"
|
396
792
|
|
397
793
|
unless specs[:frame] == :none
|
398
794
|
mt = :polyiso
|
399
795
|
d = 0.025
|
400
796
|
a[:sheath][:mat] = @@mats[mt]
|
401
797
|
a[:sheath][:d ] = d
|
402
|
-
a[:sheath][:id ] = "OSut
|
798
|
+
a[:sheath][:id ] = "OSut:#{mt}:#{format('%03d', d*1000)[-3..-1]}"
|
403
799
|
end
|
404
800
|
|
405
801
|
mt = :concrete
|
@@ -407,14 +803,14 @@ module OSut
|
|
407
803
|
d = 0.200 if specs[:frame] == :heavy
|
408
804
|
a[:compo][:mat] = @@mats[mt]
|
409
805
|
a[:compo][:d ] = d
|
410
|
-
a[:compo][:id ] = "OSut
|
806
|
+
a[:compo][:id ] = "OSut:#{mt}:#{format('%03d', d*1000)[-3..-1]}"
|
411
807
|
|
412
808
|
unless specs[:finish] == :none
|
413
809
|
mt = :material
|
414
810
|
d = 0.015
|
415
811
|
a[:finish][:mat] = @@mats[mt]
|
416
812
|
a[:finish][:d ] = d
|
417
|
-
a[:finish][:id ] = "OSut
|
813
|
+
a[:finish][:id ] = "OSut:#{mt}:#{format('%03d', d*1000)[-3..-1]}"
|
418
814
|
end
|
419
815
|
when :basement
|
420
816
|
unless specs[:clad] == :none
|
@@ -424,38 +820,38 @@ module OSut
|
|
424
820
|
d = 0.015 if specs[:clad] == :light
|
425
821
|
a[:clad][:mat] = @@mats[mt]
|
426
822
|
a[:clad][:d ] = d
|
427
|
-
a[:clad][:id ] = "OSut
|
823
|
+
a[:clad][:id ] = "OSut:#{mt}:#{format('%03d', d*1000)[-3..-1]}"
|
428
824
|
|
429
825
|
mt = :polyiso
|
430
826
|
d = 0.025
|
431
827
|
a[:sheath][:mat] = @@mats[mt]
|
432
828
|
a[:sheath][:d ] = d
|
433
|
-
a[:sheath][:id ] = "OSut
|
829
|
+
a[:sheath][:id ] = "OSut:#{mt}:#{format('%03d', d*1000)[-3..-1]}"
|
434
830
|
|
435
831
|
mt = :concrete
|
436
832
|
d = 0.200
|
437
833
|
a[:compo][:mat] = @@mats[mt]
|
438
834
|
a[:compo][:d ] = d
|
439
|
-
a[:compo][:id ] = "OSut
|
835
|
+
a[:compo][:id ] = "OSut:#{mt}:#{format('%03d', d*1000)[-3..-1]}"
|
440
836
|
else
|
441
837
|
mt = :concrete
|
442
838
|
d = 0.200
|
443
839
|
a[:sheath][:mat] = @@mats[mt]
|
444
840
|
a[:sheath][:d ] = d
|
445
|
-
a[:sheath][:id ] = "OSut
|
841
|
+
a[:sheath][:id ] = "OSut:#{mt}:#{format('%03d', d*1000)[-3..-1]}"
|
446
842
|
|
447
843
|
unless specs[:finish] == :none
|
448
844
|
mt = :mineral
|
449
845
|
d = 0.075
|
450
846
|
a[:compo][:mat] = @@mats[mt]
|
451
847
|
a[:compo][:d ] = d
|
452
|
-
a[:compo][:id ] = "OSut
|
848
|
+
a[:compo][:id ] = "OSut:#{mt}:#{format('%03d', d*1000)[-3..-1]}"
|
453
849
|
|
454
850
|
mt = :drywall
|
455
851
|
d = 0.015
|
456
852
|
a[:finish][:mat] = @@mats[mt]
|
457
853
|
a[:finish][:d ] = d
|
458
|
-
a[:finish][:id ] = "OSut
|
854
|
+
a[:finish][:id ] = "OSut:#{mt}:#{format('%03d', d*1000)[-3..-1]}"
|
459
855
|
end
|
460
856
|
end
|
461
857
|
when :door
|
@@ -464,43 +860,27 @@ module OSut
|
|
464
860
|
|
465
861
|
a[:compo ][:mat ] = @@mats[mt]
|
466
862
|
a[:compo ][:d ] = d
|
467
|
-
a[:compo ][:id ] = "OSut
|
863
|
+
a[:compo ][:id ] = "OSut:#{mt}:#{format('%03d', d*1000)[-3..-1]}"
|
468
864
|
when :window
|
469
|
-
a[:glazing][:u ] =
|
865
|
+
a[:glazing][:u ] = u ? u : @@uo[:window]
|
470
866
|
a[:glazing][:shgc] = 0.450
|
471
867
|
a[:glazing][:shgc] = specs[:shgc] if specs.key?(:shgc)
|
472
|
-
a[:glazing][:id ] = "OSut
|
473
|
-
a[:glazing][:id ] += "
|
474
|
-
a[:glazing][:id ] += "
|
868
|
+
a[:glazing][:id ] = "OSut:window"
|
869
|
+
a[:glazing][:id ] += ":U#{format('%.1f', a[:glazing][:u])}"
|
870
|
+
a[:glazing][:id ] += ":SHGC#{format('%d', a[:glazing][:shgc]*100)}"
|
475
871
|
when :skylight
|
476
|
-
a[:glazing][:u ] =
|
872
|
+
a[:glazing][:u ] = u ? u : @@uo[:skylight]
|
477
873
|
a[:glazing][:shgc] = 0.450
|
478
874
|
a[:glazing][:shgc] = specs[:shgc] if specs.key?(:shgc)
|
479
|
-
a[:glazing][:id ] = "OSut
|
480
|
-
a[:glazing][:id ] += "
|
481
|
-
a[:glazing][:id ] += "
|
875
|
+
a[:glazing][:id ] = "OSut:skylight"
|
876
|
+
a[:glazing][:id ] += ":U#{format('%.1f', a[:glazing][:u])}"
|
877
|
+
a[:glazing][:id ] += ":SHGC#{format('%d', a[:glazing][:shgc]*100)}"
|
482
878
|
end
|
483
879
|
|
484
880
|
# Initiate layers.
|
485
|
-
|
486
|
-
|
487
|
-
layers = OpenStudio::Model::OpaqueMaterialVector.new unless glazed
|
488
|
-
layers = OpenStudio::Model::FenestrationMaterialVector.new if glazed
|
489
|
-
|
490
|
-
if glazed
|
491
|
-
u = a[:glazing][:u ]
|
492
|
-
shgc = a[:glazing][:shgc]
|
493
|
-
lyr = model.getSimpleGlazingByName(a[:glazing][:id])
|
494
|
-
|
495
|
-
if lyr.empty?
|
496
|
-
lyr = OpenStudio::Model::SimpleGlazing.new(model, u, shgc)
|
497
|
-
lyr.setName(a[:glazing][:id])
|
498
|
-
else
|
499
|
-
lyr = lyr.get
|
500
|
-
end
|
881
|
+
if a[:glazing].empty?
|
882
|
+
layers = OpenStudio::Model::OpaqueMaterialVector.new
|
501
883
|
|
502
|
-
layers << lyr
|
503
|
-
else
|
504
884
|
# Loop through each layer spec, and generate construction.
|
505
885
|
a.each do |i, l|
|
506
886
|
next if l.empty?
|
@@ -524,44 +904,68 @@ module OSut
|
|
524
904
|
|
525
905
|
layers << lyr
|
526
906
|
end
|
907
|
+
else
|
908
|
+
layers = OpenStudio::Model::FenestrationMaterialVector.new
|
909
|
+
|
910
|
+
u0 = a[:glazing][:u ]
|
911
|
+
shgc = a[:glazing][:shgc]
|
912
|
+
lyr = model.getSimpleGlazingByName(a[:glazing][:id])
|
913
|
+
|
914
|
+
if lyr.empty?
|
915
|
+
lyr = OpenStudio::Model::SimpleGlazing.new(model, u0, shgc)
|
916
|
+
lyr.setName(a[:glazing][:id])
|
917
|
+
else
|
918
|
+
lyr = lyr.get
|
919
|
+
end
|
920
|
+
|
921
|
+
layers << lyr
|
527
922
|
end
|
528
923
|
|
529
|
-
c
|
924
|
+
c = OpenStudio::Model::Construction.new(layers)
|
530
925
|
c.setName(id)
|
531
926
|
|
532
|
-
# Adjust insulating layer
|
533
|
-
|
534
|
-
ro =
|
535
|
-
|
927
|
+
# Adjust insulating layer conductivity (maybe thickness) to match Uo.
|
928
|
+
if u and a[:glazing].empty?
|
929
|
+
ro = 1 / u - film
|
930
|
+
|
931
|
+
|
932
|
+
if ro > RMIN
|
933
|
+
if specs[:type] == :door # 1x layer, adjust conductivity
|
934
|
+
layer = c.getLayer(0).to_StandardOpaqueMaterial
|
935
|
+
return invalid("#{id} standard material?", mth, 0) if layer.empty?
|
936
|
+
|
937
|
+
layer = layer.get
|
938
|
+
k = layer.thickness / ro
|
939
|
+
layer.setConductivity(k)
|
940
|
+
else # multiple layers, adjust layer conductivity, then thickness
|
941
|
+
lyr = insulatingLayer(c)
|
942
|
+
return invalid("#{id} construction", mth, 0) if lyr[:index].nil?
|
943
|
+
return invalid("#{id} construction", mth, 0) if lyr[:type ].nil?
|
944
|
+
return invalid("#{id} construction", mth, 0) if lyr[:r ].to_i.zero?
|
536
945
|
|
537
|
-
|
538
|
-
|
539
|
-
|
946
|
+
index = lyr[:index]
|
947
|
+
layer = c.getLayer(index).to_StandardOpaqueMaterial
|
948
|
+
return invalid("#{id} material @#{index}", mth, 0) if layer.empty?
|
540
949
|
|
541
|
-
|
542
|
-
k = layer.thickness / ro
|
543
|
-
layer.setConductivity(k)
|
544
|
-
elsif ro > 0 # multiple layers, adjust insulating layer thickness
|
545
|
-
lyr = insulatingLayer(c)
|
546
|
-
return invalid("#{id} construction", mth, 0) if lyr[:index].nil?
|
547
|
-
return invalid("#{id} construction", mth, 0) if lyr[:type ].nil?
|
548
|
-
return invalid("#{id} construction", mth, 0) if lyr[:r ].zero?
|
950
|
+
layer = layer.get
|
549
951
|
|
550
|
-
|
551
|
-
|
552
|
-
return invalid("#{id} material @#{index}", mth, 0) if layer.empty?
|
952
|
+
k = (layer.thickness / (ro - rsi(c) + lyr[:r])).clamp(KMIN, KMAX)
|
953
|
+
d = (k * (ro - rsi(c) + lyr[:r])).clamp(DMIN, DMAX)
|
553
954
|
|
554
|
-
|
555
|
-
|
556
|
-
|
557
|
-
return invalid("#{id} adjusted m", mth, 0) if d < 0.03
|
955
|
+
nom = "OSut:"
|
956
|
+
nom += layer.nameString.gsub(/[^a-z]/i, "").gsub("OSut", "")
|
957
|
+
nom += ":K#{format('%4.3f', k)}:#{format('%03d', d*1000)[-3..-1]}"
|
558
958
|
|
559
|
-
|
560
|
-
|
561
|
-
|
562
|
-
|
563
|
-
|
564
|
-
|
959
|
+
lyr = model.getStandardOpaqueMaterialByName(nom)
|
960
|
+
|
961
|
+
if lyr.empty?
|
962
|
+
layer.setName(nom)
|
963
|
+
layer.setConductivity(k)
|
964
|
+
layer.setThickness(d)
|
965
|
+
else
|
966
|
+
c.setLayer(index, lyr.get)
|
967
|
+
end
|
968
|
+
end
|
565
969
|
end
|
566
970
|
end
|
567
971
|
|
@@ -608,20 +1012,20 @@ module OSut
|
|
608
1012
|
end
|
609
1013
|
|
610
1014
|
# Shading schedule.
|
611
|
-
id = "OSut
|
1015
|
+
id = "OSut:SHADE:Ruleset"
|
612
1016
|
sch = mdl.getScheduleRulesetByName(id)
|
613
1017
|
|
614
1018
|
if sch.empty?
|
615
1019
|
sch = OpenStudio::Model::ScheduleRuleset.new(mdl, 0)
|
616
1020
|
sch.setName(id)
|
617
1021
|
sch.setScheduleTypeLimits(onoff)
|
618
|
-
sch.defaultDaySchedule.setName("OSut
|
1022
|
+
sch.defaultDaySchedule.setName("OSut:Shade:Ruleset:Default")
|
619
1023
|
else
|
620
1024
|
sch = sch.get
|
621
1025
|
end
|
622
1026
|
|
623
1027
|
# Summer cooling rule.
|
624
|
-
id = "OSut
|
1028
|
+
id = "OSut:SHADE:ScheduleRule"
|
625
1029
|
rule = mdl.getScheduleRuleByName(id)
|
626
1030
|
|
627
1031
|
if rule.empty?
|
@@ -635,14 +1039,14 @@ module OSut
|
|
635
1039
|
rule.setStartDate(start)
|
636
1040
|
rule.setEndDate(finish)
|
637
1041
|
rule.setApplyAllDays(true)
|
638
|
-
rule.daySchedule.setName("OSut
|
1042
|
+
rule.daySchedule.setName("OSut:Shade:Rule:Default")
|
639
1043
|
rule.daySchedule.addValue(OpenStudio::Time.new(0,24,0,0), 1)
|
640
1044
|
else
|
641
1045
|
rule = rule.get
|
642
1046
|
end
|
643
1047
|
|
644
1048
|
# Shade object.
|
645
|
-
id = "OSut
|
1049
|
+
id = "OSut:Shade"
|
646
1050
|
shd = mdl.getShadeByName(id)
|
647
1051
|
|
648
1052
|
if shd.empty?
|
@@ -653,7 +1057,7 @@ module OSut
|
|
653
1057
|
end
|
654
1058
|
|
655
1059
|
# Shading control (unique to each call).
|
656
|
-
id = "OSut
|
1060
|
+
id = "OSut:ShadingControl"
|
657
1061
|
ctl = OpenStudio::Model::ShadingControl.new(shd)
|
658
1062
|
ctl.setName(id)
|
659
1063
|
ctl.setSchedule(sch)
|
@@ -689,7 +1093,7 @@ module OSut
|
|
689
1093
|
|
690
1094
|
# A single material.
|
691
1095
|
mdl = sps.first.model
|
692
|
-
id = "OSut
|
1096
|
+
id = "OSut:MASS:Material"
|
693
1097
|
mat = mdl.getOpaqueMaterialByName(id)
|
694
1098
|
|
695
1099
|
if mat.empty?
|
@@ -708,7 +1112,7 @@ module OSut
|
|
708
1112
|
end
|
709
1113
|
|
710
1114
|
# A single, 1x layered construction.
|
711
|
-
id = "OSut
|
1115
|
+
id = "OSut:MASS:Construction"
|
712
1116
|
con = mdl.getConstructionByName(id)
|
713
1117
|
|
714
1118
|
if con.empty?
|
@@ -721,7 +1125,7 @@ module OSut
|
|
721
1125
|
con = con.get
|
722
1126
|
end
|
723
1127
|
|
724
|
-
id = "OSut
|
1128
|
+
id = "OSut:InternalMassDefinition:" + (format "%.2f", ratio)
|
725
1129
|
df = mdl.getInternalMassDefinitionByName(id)
|
726
1130
|
|
727
1131
|
if df.empty?
|
@@ -735,7 +1139,7 @@ module OSut
|
|
735
1139
|
|
736
1140
|
sps.each do |sp|
|
737
1141
|
mass = OpenStudio::Model::InternalMass.new(df)
|
738
|
-
mass.setName("OSut
|
1142
|
+
mass.setName("OSut:InternalMass:#{sp.nameString}")
|
739
1143
|
mass.setSpace(sp)
|
740
1144
|
end
|
741
1145
|
|
@@ -743,13 +1147,13 @@ module OSut
|
|
743
1147
|
end
|
744
1148
|
|
745
1149
|
##
|
746
|
-
# Validates if a default construction set holds
|
1150
|
+
# Validates if a default construction set holds an opaque base construction.
|
747
1151
|
#
|
748
1152
|
# @param set [OpenStudio::Model::DefaultConstructionSet] a default set
|
749
|
-
# @param bse [
|
1153
|
+
# @param bse [OpenStudio::Model::ConstructionBase] an opaque construction base
|
750
1154
|
# @param gr [Bool] if ground-facing surface
|
751
1155
|
# @param ex [Bool] if exterior-facing surface
|
752
|
-
# @param tp [#
|
1156
|
+
# @param tp [#to_sym] surface type: "floor", "wall" or "roofceiling"
|
753
1157
|
#
|
754
1158
|
# @return [Bool] whether default set holds construction
|
755
1159
|
# @return [false] if invalid input (see logs)
|
@@ -766,323 +1170,156 @@ module OSut
|
|
766
1170
|
id2 = bse.nameString
|
767
1171
|
ck1 = set.is_a?(cl1)
|
768
1172
|
ck2 = bse.is_a?(cl2)
|
769
|
-
ck3 = [true, false].include?(gr)
|
770
|
-
ck4 = [true, false].include?(ex)
|
771
|
-
ck5 = tp.respond_to?(:
|
772
|
-
return mismatch(id1, set, cl1, mth, DBG, false) unless ck1
|
773
|
-
return mismatch(id2, bse, cl2, mth, DBG, false) unless ck2
|
774
|
-
return invalid("ground" , mth, 3, DBG, false) unless ck3
|
775
|
-
return invalid("exterior" , mth, 4, DBG, false) unless ck4
|
776
|
-
return invalid("surface type", mth, 5, DBG, false) unless ck5
|
777
|
-
|
778
|
-
type = trim(tp).downcase
|
779
|
-
ck1 = ["floor", "wall", "roofceiling"].include?(type)
|
780
|
-
return invalid("surface type", mth, 5, DBG, false) unless ck1
|
781
|
-
|
782
|
-
constructions = nil
|
783
|
-
|
784
|
-
if gr
|
785
|
-
unless set.defaultGroundContactSurfaceConstructions.empty?
|
786
|
-
constructions = set.defaultGroundContactSurfaceConstructions.get
|
787
|
-
end
|
788
|
-
elsif ex
|
789
|
-
unless set.defaultExteriorSurfaceConstructions.empty?
|
790
|
-
constructions = set.defaultExteriorSurfaceConstructions.get
|
791
|
-
end
|
792
|
-
else
|
793
|
-
unless set.defaultInteriorSurfaceConstructions.empty?
|
794
|
-
constructions = set.defaultInteriorSurfaceConstructions.get
|
795
|
-
end
|
796
|
-
end
|
797
|
-
|
798
|
-
return false unless constructions
|
799
|
-
|
800
|
-
case type
|
801
|
-
when "roofceiling"
|
802
|
-
unless constructions.roofCeilingConstruction.empty?
|
803
|
-
construction = constructions.roofCeilingConstruction.get
|
804
|
-
return true if construction == bse
|
805
|
-
end
|
806
|
-
when "floor"
|
807
|
-
unless constructions.floorConstruction.empty?
|
808
|
-
construction = constructions.floorConstruction.get
|
809
|
-
return true if construction == bse
|
810
|
-
end
|
811
|
-
else
|
812
|
-
unless constructions.wallConstruction.empty?
|
813
|
-
construction = constructions.wallConstruction.get
|
814
|
-
return true if construction == bse
|
815
|
-
end
|
816
|
-
end
|
817
|
-
|
818
|
-
false
|
819
|
-
end
|
820
|
-
|
821
|
-
##
|
822
|
-
# Returns a surface's default construction set.
|
823
|
-
#
|
824
|
-
# @param s [OpenStudio::Model::Surface] a surface
|
825
|
-
#
|
826
|
-
# @return [OpenStudio::Model::DefaultConstructionSet] default set
|
827
|
-
# @return [nil] if invalid input (see logs)
|
828
|
-
def defaultConstructionSet(s = nil)
|
829
|
-
mth = "OSut::#{__callee__}"
|
830
|
-
cl = OpenStudio::Model::Surface
|
831
|
-
return invalid("surface", mth, 1) unless s.respond_to?(NS)
|
832
|
-
|
833
|
-
id = s.nameString
|
834
|
-
ok = s.isConstructionDefaulted
|
835
|
-
m1 = "#{id} construction not defaulted (#{mth})"
|
836
|
-
m2 = "#{id} construction"
|
837
|
-
m3 = "#{id} space"
|
838
|
-
return mismatch(id, s, cl, mth) unless s.is_a?(cl)
|
839
|
-
|
840
|
-
log(ERR, m1) unless ok
|
841
|
-
return nil unless ok
|
842
|
-
return empty(m2, mth, ERR) if s.construction.empty?
|
843
|
-
return empty(m3, mth, ERR) if s.space.empty?
|
844
|
-
|
845
|
-
mdl = s.model
|
846
|
-
base = s.construction.get
|
847
|
-
space = s.space.get
|
848
|
-
type = s.surfaceType
|
849
|
-
ground = false
|
850
|
-
exterior = false
|
851
|
-
|
852
|
-
if s.isGroundSurface
|
853
|
-
ground = true
|
854
|
-
elsif s.outsideBoundaryCondition.downcase == "outdoors"
|
855
|
-
exterior = true
|
856
|
-
end
|
857
|
-
|
858
|
-
unless space.defaultConstructionSet.empty?
|
859
|
-
set = space.defaultConstructionSet.get
|
860
|
-
return set if holdsConstruction?(set, base, ground, exterior, type)
|
861
|
-
end
|
862
|
-
|
863
|
-
unless space.spaceType.empty?
|
864
|
-
spacetype = space.spaceType.get
|
865
|
-
|
866
|
-
unless spacetype.defaultConstructionSet.empty?
|
867
|
-
set = spacetype.defaultConstructionSet.get
|
868
|
-
return set if holdsConstruction?(set, base, ground, exterior, type)
|
869
|
-
end
|
870
|
-
end
|
871
|
-
|
872
|
-
unless space.buildingStory.empty?
|
873
|
-
story = space.buildingStory.get
|
874
|
-
|
875
|
-
unless story.defaultConstructionSet.empty?
|
876
|
-
set = story.defaultConstructionSet.get
|
877
|
-
return set if holdsConstruction?(set, base, ground, exterior, type)
|
878
|
-
end
|
879
|
-
end
|
880
|
-
|
881
|
-
building = mdl.getBuilding
|
882
|
-
|
883
|
-
unless building.defaultConstructionSet.empty?
|
884
|
-
set = building.defaultConstructionSet.get
|
885
|
-
return set if holdsConstruction?(set, base, ground, exterior, type)
|
886
|
-
end
|
887
|
-
|
888
|
-
nil
|
889
|
-
end
|
890
|
-
|
891
|
-
##
|
892
|
-
# Validates if every material in a layered construction is standard & opaque.
|
893
|
-
#
|
894
|
-
# @param lc [OpenStudio::LayeredConstruction] a layered construction
|
895
|
-
#
|
896
|
-
# @return [Bool] whether all layers are valid
|
897
|
-
# @return [false] if invalid input (see logs)
|
898
|
-
def standardOpaqueLayers?(lc = nil)
|
899
|
-
mth = "OSut::#{__callee__}"
|
900
|
-
cl = OpenStudio::Model::LayeredConstruction
|
901
|
-
return invalid("lc", mth, 1, DBG, false) unless lc.respond_to?(NS)
|
902
|
-
return mismatch(lc.nameString, lc, cl, mth, DBG, false) unless lc.is_a?(cl)
|
903
|
-
|
904
|
-
lc.layers.each { |m| return false if m.to_StandardOpaqueMaterial.empty? }
|
905
|
-
|
906
|
-
true
|
907
|
-
end
|
908
|
-
|
909
|
-
##
|
910
|
-
# Returns total (standard opaque) layered construction thickness (m).
|
911
|
-
#
|
912
|
-
# @param lc [OpenStudio::LayeredConstruction] a layered construction
|
913
|
-
#
|
914
|
-
# @return [Float] construction thickness
|
915
|
-
# @return [0.0] if invalid input (see logs)
|
916
|
-
def thickness(lc = nil)
|
917
|
-
mth = "OSut::#{__callee__}"
|
918
|
-
cl = OpenStudio::Model::LayeredConstruction
|
919
|
-
return invalid("lc", mth, 1, DBG, 0.0) unless lc.respond_to?(NS)
|
920
|
-
|
921
|
-
id = lc.nameString
|
922
|
-
return mismatch(id, lc, cl, mth, DBG, 0.0) unless lc.is_a?(cl)
|
923
|
-
|
924
|
-
ok = standardOpaqueLayers?(lc)
|
925
|
-
log(ERR, "'#{id}' holds non-StandardOpaqueMaterial(s) (#{mth})") unless ok
|
926
|
-
return 0.0 unless ok
|
927
|
-
|
928
|
-
thickness = 0.0
|
929
|
-
lc.layers.each { |m| thickness += m.thickness }
|
930
|
-
|
931
|
-
thickness
|
932
|
-
end
|
933
|
-
|
934
|
-
##
|
935
|
-
# Returns total air film resistance of a fenestrated construction (m2•K/W)
|
936
|
-
#
|
937
|
-
# @param usi [Numeric] a fenestrated construction's U-factor (W/m2•K)
|
938
|
-
#
|
939
|
-
# @return [Float] total air film resistances
|
940
|
-
# @return [0.1216] if invalid input (see logs)
|
941
|
-
def glazingAirFilmRSi(usi = 5.85)
|
942
|
-
# The sum of thermal resistances of calculated exterior and interior film
|
943
|
-
# coefficients under standard winter conditions are taken from:
|
944
|
-
#
|
945
|
-
# https://bigladdersoftware.com/epx/docs/9-6/engineering-reference/
|
946
|
-
# window-calculation-module.html#simple-window-model
|
947
|
-
#
|
948
|
-
# These remain acceptable approximations for flat windows, yet likely
|
949
|
-
# unsuitable for subsurfaces with curved or projecting shapes like domed
|
950
|
-
# skylights. The solution here is considered an adequate fix for reporting,
|
951
|
-
# awaiting eventual OpenStudio (and EnergyPlus) upgrades to report NFRC 100
|
952
|
-
# (or ISO) air film resistances under standard winter conditions.
|
953
|
-
#
|
954
|
-
# For U-factors above 8.0 W/m2•K (or invalid input), the function returns
|
955
|
-
# 0.1216 m2•K/W, which corresponds to a construction with a single glass
|
956
|
-
# layer thickness of 2mm & k = ~0.6 W/m.K.
|
957
|
-
#
|
958
|
-
# The EnergyPlus Engineering calculations were designed for vertical
|
959
|
-
# windows - not horizontal, slanted or domed surfaces - use with caution.
|
960
|
-
mth = "OSut::#{__callee__}"
|
961
|
-
cl = Numeric
|
962
|
-
return mismatch("usi", usi, cl, mth, DBG, 0.1216) unless usi.is_a?(cl)
|
963
|
-
return invalid("usi", mth, 1, WRN, 0.1216) if usi > 8.0
|
964
|
-
return negative("usi", mth, WRN, 0.1216) if usi < 0
|
965
|
-
return zero("usi", mth, WRN, 0.1216) if usi.abs < TOL
|
966
|
-
|
967
|
-
rsi = 1 / (0.025342 * usi + 29.163853) # exterior film, next interior film
|
968
|
-
return rsi + 1 / (0.359073 * Math.log(usi) + 6.949915) if usi < 5.85
|
969
|
-
return rsi + 1 / (1.788041 * usi - 2.886625)
|
970
|
-
end
|
971
|
-
|
972
|
-
##
|
973
|
-
# Returns a construction's 'standard calc' thermal resistance (m2•K/W), which
|
974
|
-
# includes air film resistances. It excludes insulating effects of shades,
|
975
|
-
# screens, etc. in the case of fenestrated constructions.
|
976
|
-
#
|
977
|
-
# @param lc [OpenStudio::Model::LayeredConstruction] a layered construction
|
978
|
-
# @param film [Numeric] thermal resistance of surface air films (m2•K/W)
|
979
|
-
# @param t [Numeric] gas temperature (°C) (optional)
|
980
|
-
#
|
981
|
-
# @return [Float] layered construction's thermal resistance
|
982
|
-
# @return [0.0] if invalid input (see logs)
|
983
|
-
def rsi(lc = nil, film = 0.0, t = 0.0)
|
984
|
-
# This is adapted from BTAP's Material Module "get_conductance" (P. Lopez)
|
985
|
-
#
|
986
|
-
# https://github.com/NREL/OpenStudio-Prototype-Buildings/blob/
|
987
|
-
# c3d5021d8b7aef43e560544699fb5c559e6b721d/lib/btap/measures/
|
988
|
-
# btap_equest_converter/envelope.rb#L122
|
989
|
-
mth = "OSut::#{__callee__}"
|
990
|
-
cl1 = OpenStudio::Model::LayeredConstruction
|
991
|
-
cl2 = Numeric
|
992
|
-
return invalid("lc", mth, 1, DBG, 0.0) unless lc.respond_to?(NS)
|
993
|
-
|
994
|
-
id = lc.nameString
|
995
|
-
return mismatch(id, lc, cl1, mth, DBG, 0.0) unless lc.is_a?(cl1)
|
996
|
-
return mismatch("film", film, cl2, mth, DBG, 0.0) unless film.is_a?(cl2)
|
997
|
-
return mismatch("temp K", t, cl2, mth, DBG, 0.0) unless t.is_a?(cl2)
|
1173
|
+
ck3 = [true, false].include?(gr)
|
1174
|
+
ck4 = [true, false].include?(ex)
|
1175
|
+
ck5 = tp.respond_to?(:to_sym)
|
1176
|
+
return mismatch(id1, set, cl1, mth, DBG, false) unless ck1
|
1177
|
+
return mismatch(id2, bse, cl2, mth, DBG, false) unless ck2
|
1178
|
+
return invalid("ground" , mth, 3, DBG, false) unless ck3
|
1179
|
+
return invalid("exterior" , mth, 4, DBG, false) unless ck4
|
1180
|
+
return invalid("surface type", mth, 5, DBG, false) unless ck5
|
998
1181
|
|
999
|
-
|
1000
|
-
|
1001
|
-
return
|
1182
|
+
type = trim(tp).downcase
|
1183
|
+
ck1 = ["floor", "wall", "roofceiling"].include?(type)
|
1184
|
+
return invalid("surface type", mth, 5, DBG, false) unless ck1
|
1002
1185
|
|
1003
|
-
|
1186
|
+
constructions = nil
|
1004
1187
|
|
1005
|
-
|
1006
|
-
|
1007
|
-
|
1008
|
-
|
1188
|
+
if gr
|
1189
|
+
unless set.defaultGroundContactSurfaceConstructions.empty?
|
1190
|
+
constructions = set.defaultGroundContactSurfaceConstructions.get
|
1191
|
+
end
|
1192
|
+
elsif ex
|
1193
|
+
unless set.defaultExteriorSurfaceConstructions.empty?
|
1194
|
+
constructions = set.defaultExteriorSurfaceConstructions.get
|
1195
|
+
end
|
1196
|
+
else
|
1197
|
+
unless set.defaultInteriorSurfaceConstructions.empty?
|
1198
|
+
constructions = set.defaultInteriorSurfaceConstructions.get
|
1199
|
+
end
|
1200
|
+
end
|
1009
1201
|
|
1010
|
-
|
1011
|
-
rsi += m.to_StandardGlazing.get.thermalResistance unless empty
|
1012
|
-
empty = m.to_RefractionExtinctionGlazing.empty?
|
1013
|
-
rsi += m.to_RefractionExtinctionGlazing.get.thermalResistance unless empty
|
1014
|
-
empty = m.to_Gas.empty?
|
1015
|
-
rsi += m.to_Gas.get.getThermalResistance(t) unless empty
|
1016
|
-
empty = m.to_GasMixture.empty?
|
1017
|
-
rsi += m.to_GasMixture.get.getThermalResistance(t) unless empty
|
1202
|
+
return false unless constructions
|
1018
1203
|
|
1019
|
-
|
1020
|
-
|
1021
|
-
|
1022
|
-
|
1023
|
-
|
1024
|
-
|
1025
|
-
|
1026
|
-
|
1027
|
-
|
1204
|
+
case type
|
1205
|
+
when "roofceiling"
|
1206
|
+
unless constructions.roofCeilingConstruction.empty?
|
1207
|
+
construction = constructions.roofCeilingConstruction.get
|
1208
|
+
return true if construction == bse
|
1209
|
+
end
|
1210
|
+
when "floor"
|
1211
|
+
unless constructions.floorConstruction.empty?
|
1212
|
+
construction = constructions.floorConstruction.get
|
1213
|
+
return true if construction == bse
|
1214
|
+
end
|
1215
|
+
else
|
1216
|
+
unless constructions.wallConstruction.empty?
|
1217
|
+
construction = constructions.wallConstruction.get
|
1218
|
+
return true if construction == bse
|
1219
|
+
end
|
1028
1220
|
end
|
1029
1221
|
|
1030
|
-
|
1222
|
+
false
|
1031
1223
|
end
|
1032
1224
|
|
1033
1225
|
##
|
1034
|
-
#
|
1035
|
-
# returns a 3-keyed hash :index, the insulating layer index [0, n layers)
|
1036
|
-
# within the layered construction; :type, either :standard or :massless; and
|
1037
|
-
# :r, material thermal resistance in m2•K/W.
|
1226
|
+
# Returns a surface's default construction set.
|
1038
1227
|
#
|
1039
|
-
# @param
|
1228
|
+
# @param s [OpenStudio::Model::Surface] a surface
|
1040
1229
|
#
|
1041
|
-
# @return [
|
1042
|
-
# @return [
|
1043
|
-
def
|
1230
|
+
# @return [OpenStudio::Model::DefaultConstructionSet] default set
|
1231
|
+
# @return [nil] if invalid input (see logs)
|
1232
|
+
def defaultConstructionSet(s = nil)
|
1044
1233
|
mth = "OSut::#{__callee__}"
|
1045
|
-
cl = OpenStudio::Model::
|
1046
|
-
|
1047
|
-
i = 0 # iterator
|
1048
|
-
return invalid("lc", mth, 1, DBG, res) unless lc.respond_to?(NS)
|
1234
|
+
cl = OpenStudio::Model::Surface
|
1235
|
+
return invalid("surface", mth, 1) unless s.respond_to?(NS)
|
1049
1236
|
|
1050
|
-
id
|
1051
|
-
|
1237
|
+
id = s.nameString
|
1238
|
+
ok = s.isConstructionDefaulted
|
1239
|
+
m1 = "#{id} construction not defaulted (#{mth})"
|
1240
|
+
m2 = "#{id} construction"
|
1241
|
+
m3 = "#{id} space"
|
1242
|
+
return mismatch(id, s, cl, mth) unless s.is_a?(cl)
|
1052
1243
|
|
1053
|
-
|
1054
|
-
|
1055
|
-
|
1244
|
+
log(ERR, m1) unless ok
|
1245
|
+
return nil unless ok
|
1246
|
+
return empty(m2, mth, ERR) if s.construction.empty?
|
1247
|
+
return empty(m3, mth, ERR) if s.space.empty?
|
1056
1248
|
|
1057
|
-
|
1058
|
-
|
1059
|
-
|
1060
|
-
|
1061
|
-
|
1062
|
-
|
1063
|
-
|
1249
|
+
mdl = s.model
|
1250
|
+
base = s.construction.get
|
1251
|
+
space = s.space.get
|
1252
|
+
type = s.surfaceType
|
1253
|
+
ground = false
|
1254
|
+
exterior = false
|
1255
|
+
adjacent = s.adjacentSurface.empty? ? nil : s.adjacentSurface.get
|
1256
|
+
aspace = adjacent.nil? || adjacent.space.empty? ? nil : adjacent.space.get
|
1257
|
+
typ = adjacent.nil? ? nil : adjacent.surfaceType
|
1258
|
+
|
1259
|
+
if s.isGroundSurface
|
1260
|
+
ground = true
|
1261
|
+
elsif s.outsideBoundaryCondition.downcase == "outdoors"
|
1262
|
+
exterior = true
|
1263
|
+
end
|
1264
|
+
|
1265
|
+
if space.defaultConstructionSet.empty?
|
1266
|
+
unless aspace.nil?
|
1267
|
+
unless aspace.defaultConstructionSet.empty?
|
1268
|
+
set = aspace.defaultConstructionSet.get
|
1269
|
+
return set if holdsConstruction?(set, base, ground, exterior, typ)
|
1064
1270
|
end
|
1065
1271
|
end
|
1272
|
+
else
|
1273
|
+
set = space.defaultConstructionSet.get
|
1274
|
+
return set if holdsConstruction?(set, base, ground, exterior, type)
|
1275
|
+
end
|
1066
1276
|
|
1067
|
-
|
1068
|
-
|
1069
|
-
k = m.thermalConductivity
|
1070
|
-
d = m.thickness
|
1277
|
+
unless space.spaceType.empty?
|
1278
|
+
spacetype = space.spaceType.get
|
1071
1279
|
|
1072
|
-
|
1073
|
-
|
1074
|
-
|
1075
|
-
|
1076
|
-
|
1077
|
-
|
1078
|
-
|
1280
|
+
unless spacetype.defaultConstructionSet.empty?
|
1281
|
+
set = spacetype.defaultConstructionSet.get
|
1282
|
+
return set if holdsConstruction?(set, base, ground, exterior, type)
|
1283
|
+
end
|
1284
|
+
end
|
1285
|
+
|
1286
|
+
unless aspace.nil? || aspace.spaceType.empty?
|
1287
|
+
unless aspace.spaceType.empty?
|
1288
|
+
spacetype = aspace.spaceType.get
|
1289
|
+
|
1290
|
+
unless spacetype.defaultConstructionSet.empty?
|
1291
|
+
set = spacetype.defaultConstructionSet.get
|
1292
|
+
return set if holdsConstruction?(set, base, ground, exterior, typ)
|
1079
1293
|
end
|
1080
1294
|
end
|
1295
|
+
end
|
1081
1296
|
|
1082
|
-
|
1297
|
+
unless space.buildingStory.empty?
|
1298
|
+
story = space.buildingStory.get
|
1299
|
+
|
1300
|
+
unless story.defaultConstructionSet.empty?
|
1301
|
+
set = story.defaultConstructionSet.get
|
1302
|
+
return set if holdsConstruction?(set, base, ground, exterior, type)
|
1303
|
+
end
|
1083
1304
|
end
|
1084
1305
|
|
1085
|
-
|
1306
|
+
unless aspace.nil? || aspace.buildingStory.empty?
|
1307
|
+
story = aspace.buildingStory.get
|
1308
|
+
|
1309
|
+
unless spacetype.defaultConstructionSet.empty?
|
1310
|
+
set = spacetype.defaultConstructionSet.get
|
1311
|
+
return set if holdsConstruction?(set, base, ground, exterior, typ)
|
1312
|
+
end
|
1313
|
+
end
|
1314
|
+
|
1315
|
+
building = mdl.getBuilding
|
1316
|
+
|
1317
|
+
unless building.defaultConstructionSet.empty?
|
1318
|
+
set = building.defaultConstructionSet.get
|
1319
|
+
return set if holdsConstruction?(set, base, ground, exterior, type)
|
1320
|
+
end
|
1321
|
+
|
1322
|
+
nil
|
1086
1323
|
end
|
1087
1324
|
|
1088
1325
|
##
|
@@ -1102,7 +1339,7 @@ module OSut
|
|
1102
1339
|
id = s.nameString
|
1103
1340
|
m1 = "#{id}:spandrel"
|
1104
1341
|
m2 = "#{id}:spandrel:boolean"
|
1105
|
-
return mismatch(id, s, cl, mth) unless s.is_a?(cl)
|
1342
|
+
return mismatch(id, s, cl, mth, false) unless s.is_a?(cl)
|
1106
1343
|
|
1107
1344
|
if s.additionalProperties.hasFeature("spandrel")
|
1108
1345
|
val = s.additionalProperties.getFeatureAsBoolean("spandrel")
|
@@ -1129,7 +1366,7 @@ module OSut
|
|
1129
1366
|
return invalid("subsurface", mth, 1, DBG, false) unless s.respond_to?(NS)
|
1130
1367
|
|
1131
1368
|
id = s.nameString
|
1132
|
-
return mismatch(id, s, cl, mth, false) unless s.is_a?(cl)
|
1369
|
+
return mismatch(id, s, cl, mth, DBG, false) unless s.is_a?(cl)
|
1133
1370
|
|
1134
1371
|
# OpenStudio::Model::SubSurface.validSubSurfaceTypeValues
|
1135
1372
|
# "FixedWindow" : fenestration
|
@@ -1422,7 +1659,15 @@ module OSut
|
|
1422
1659
|
id = sched.nameString
|
1423
1660
|
return mismatch(id, sched, cl, mth, DBG, res) unless sched.is_a?(cl)
|
1424
1661
|
|
1425
|
-
|
1662
|
+
values = sched.timeSeries.values
|
1663
|
+
|
1664
|
+
values.each do |value|
|
1665
|
+
if value.respond_to?(:to_f)
|
1666
|
+
vals << value.to_f
|
1667
|
+
else
|
1668
|
+
invalid("numerical at #{i}", mth, 1, ERR)
|
1669
|
+
end
|
1670
|
+
end
|
1426
1671
|
|
1427
1672
|
res[:min] = vals.min.is_a?(Numeric) ? vals.min : nil
|
1428
1673
|
res[:max] = vals.max.is_a?(Numeric) ? vals.min : nil
|
@@ -1529,6 +1774,16 @@ module OSut
|
|
1529
1774
|
res[:spt] = max if res[:spt] < max
|
1530
1775
|
end
|
1531
1776
|
end
|
1777
|
+
|
1778
|
+
unless sched.to_ScheduleInterval.empty?
|
1779
|
+
sched = sched.to_ScheduleInterval.get
|
1780
|
+
max = scheduleIntervalMinMax(sched)[:max]
|
1781
|
+
|
1782
|
+
if max
|
1783
|
+
res[:spt] = max unless res[:spt]
|
1784
|
+
res[:spt] = max if res[:spt] < max
|
1785
|
+
end
|
1786
|
+
end
|
1532
1787
|
end
|
1533
1788
|
|
1534
1789
|
return res if zone.thermostat.empty?
|
@@ -1586,6 +1841,16 @@ module OSut
|
|
1586
1841
|
end
|
1587
1842
|
end
|
1588
1843
|
|
1844
|
+
unless sched.to_ScheduleInterval.empty?
|
1845
|
+
sched = sched.to_ScheduleInterval.get
|
1846
|
+
max = scheduleIntervalMinMax(sched)[:max]
|
1847
|
+
|
1848
|
+
if max
|
1849
|
+
res[:spt] = max unless res[:spt]
|
1850
|
+
res[:spt] = max if res[:spt] < max
|
1851
|
+
end
|
1852
|
+
end
|
1853
|
+
|
1589
1854
|
unless sched.to_ScheduleYear.empty?
|
1590
1855
|
sched = sched.to_ScheduleYear.get
|
1591
1856
|
|
@@ -1707,6 +1972,16 @@ module OSut
|
|
1707
1972
|
res[:spt] = min if res[:spt] > min
|
1708
1973
|
end
|
1709
1974
|
end
|
1975
|
+
|
1976
|
+
unless sched.to_ScheduleInterval.empty?
|
1977
|
+
sched = sched.to_ScheduleInterval.get
|
1978
|
+
min = scheduleIntervalMinMax(sched)[:min]
|
1979
|
+
|
1980
|
+
if min
|
1981
|
+
res[:spt] = min unless res[:spt]
|
1982
|
+
res[:spt] = min if res[:spt] > min
|
1983
|
+
end
|
1984
|
+
end
|
1710
1985
|
end
|
1711
1986
|
|
1712
1987
|
return res if zone.thermostat.empty?
|
@@ -1764,6 +2039,16 @@ module OSut
|
|
1764
2039
|
end
|
1765
2040
|
end
|
1766
2041
|
|
2042
|
+
unless sched.to_ScheduleInterval.empty?
|
2043
|
+
sched = sched.to_ScheduleInerval.get
|
2044
|
+
min = scheduleIntervalMinMax(sched)[:min]
|
2045
|
+
|
2046
|
+
if min
|
2047
|
+
res[:spt] = min unless res[:spt]
|
2048
|
+
res[:spt] = min if res[:spt] > min
|
2049
|
+
end
|
2050
|
+
end
|
2051
|
+
|
1767
2052
|
unless sched.to_ScheduleYear.empty?
|
1768
2053
|
sched = sched.to_ScheduleYear.get
|
1769
2054
|
|
@@ -2157,7 +2442,7 @@ module OSut
|
|
2157
2442
|
cl = OpenStudio::Model::Model
|
2158
2443
|
limits = nil
|
2159
2444
|
return mismatch("model", model, cl, mth) unless model.is_a?(cl)
|
2160
|
-
return invalid("availability", avl, 2, mth) unless avl.respond_to?(:
|
2445
|
+
return invalid("availability", avl, 2, mth) unless avl.respond_to?(:to_sym)
|
2161
2446
|
|
2162
2447
|
# Either fetch availability ScheduleTypeLimits object, or create one.
|
2163
2448
|
model.getScheduleTypeLimitss.each do |l|
|
@@ -2351,7 +2636,7 @@ module OSut
|
|
2351
2636
|
# @return [OpenStudio::Vector3d] true normal vector
|
2352
2637
|
# @return [nil] if invalid input (see logs)
|
2353
2638
|
def trueNormal(s = nil, r = 0)
|
2354
|
-
mth = "
|
2639
|
+
mth = "OSut::#{__callee__}"
|
2355
2640
|
cl = OpenStudio::Model::PlanarSurface
|
2356
2641
|
return mismatch("surface", s, cl, mth) unless s.is_a?(cl)
|
2357
2642
|
return invalid("rotation angle", mth, 2) unless r.respond_to?(:to_f)
|
@@ -2384,31 +2669,31 @@ module OSut
|
|
2384
2669
|
|
2385
2670
|
##
|
2386
2671
|
# Returns OpenStudio 3D points as an OpenStudio point vector, validating
|
2387
|
-
# points in the process (if Array).
|
2672
|
+
# points in the process (e.g. if Array).
|
2388
2673
|
#
|
2389
2674
|
# @param pts [Set<OpenStudio::Point3d>] OpenStudio 3D points
|
2390
2675
|
#
|
2391
2676
|
# @return [OpenStudio::Point3dVector] 3D vector (see logs if empty)
|
2392
2677
|
def to_p3Dv(pts = nil)
|
2393
2678
|
mth = "OSut::#{__callee__}"
|
2394
|
-
cl1 = OpenStudio::Point3d
|
2395
|
-
cl2 = OpenStudio::Point3dVector
|
2396
|
-
cl3 = OpenStudio::Model::PlanarSurface
|
2397
|
-
cl4 = Array
|
2398
2679
|
v = OpenStudio::Point3dVector.new
|
2399
2680
|
|
2400
|
-
if pts.is_a?(
|
2681
|
+
if pts.is_a?(OpenStudio::Point3d)
|
2401
2682
|
v << pts
|
2402
2683
|
return v
|
2684
|
+
elsif pts.is_a?(OpenStudio::Point3dVector)
|
2685
|
+
return pts
|
2686
|
+
elsif pts.is_a?(OpenStudio::Model::PlanarSurface)
|
2687
|
+
pts.vertices.each { |pt| v << OpenStudio::Point3d.new(pt.x, pt.y, pt.z) }
|
2688
|
+
return v
|
2403
2689
|
end
|
2404
2690
|
|
2405
|
-
return pts
|
2406
|
-
return pts.vertices if pts.is_a?(cl3)
|
2407
|
-
|
2408
|
-
return mismatch("points", pts, cl1, mth, DBG, v) unless pts.is_a?(cl4)
|
2691
|
+
return mismatch("points", pts, Array, mth, DBG, v) unless pts.is_a?(Array)
|
2409
2692
|
|
2410
2693
|
pts.each do |pt|
|
2411
|
-
|
2694
|
+
unless pt.is_a?(OpenStudio::Point3d)
|
2695
|
+
return mismatch("point", pt, OpenStudio::Point3d, mth, DBG, v)
|
2696
|
+
end
|
2412
2697
|
end
|
2413
2698
|
|
2414
2699
|
pts.each { |pt| v << OpenStudio::Point3d.new(pt.x, pt.y, pt.z) }
|
@@ -2684,7 +2969,7 @@ module OSut
|
|
2684
2969
|
|
2685
2970
|
pair = pts.each_cons(2).find { |p1, _| same?(p1, pt) }
|
2686
2971
|
|
2687
|
-
pair.nil? ? pts
|
2972
|
+
pair.nil? ? pts[0] : pair[-1]
|
2688
2973
|
end
|
2689
2974
|
|
2690
2975
|
##
|
@@ -2775,21 +3060,25 @@ module OSut
|
|
2775
3060
|
# @param pts [Set<OpenStudio::Point3d] 3D points
|
2776
3061
|
# @param n [#to_i] requested number of unique points (0 returns all)
|
2777
3062
|
#
|
2778
|
-
# @return [OpenStudio::Point3dVector] unique points (see logs
|
2779
|
-
def
|
3063
|
+
# @return [OpenStudio::Point3dVector] unique points (see logs)
|
3064
|
+
def uniques(pts = nil, n = 0)
|
2780
3065
|
mth = "OSut::#{__callee__}"
|
2781
3066
|
pts = to_p3Dv(pts)
|
2782
|
-
ok = n.respond_to?(:to_i)
|
2783
3067
|
v = OpenStudio::Point3dVector.new
|
2784
3068
|
return v if pts.empty?
|
2785
|
-
|
3069
|
+
|
3070
|
+
if n.is_a?(Numeric)
|
3071
|
+
n = n.to_i
|
3072
|
+
else
|
3073
|
+
mismatch("n points", n, Integer, mth, DBG)
|
3074
|
+
n = 0
|
3075
|
+
end
|
2786
3076
|
|
2787
3077
|
pts.each { |pt| v << pt unless holds?(v, pt) }
|
2788
3078
|
|
2789
|
-
n = n.
|
2790
|
-
|
2791
|
-
v = v[
|
2792
|
-
v = v[n..-1] if n < 0
|
3079
|
+
n = 0 if n.abs > v.size
|
3080
|
+
v = v[0..n-1] if n > 0
|
3081
|
+
v = v[n..-1] if n < 0
|
2793
3082
|
|
2794
3083
|
v
|
2795
3084
|
end
|
@@ -2803,10 +3092,10 @@ module OSut
|
|
2803
3092
|
# @param pts [Set<OpenStudio::Point3d>] 3D points
|
2804
3093
|
#
|
2805
3094
|
# @return [OpenStudio::Point3dVectorVector] line segments (see logs if empty)
|
2806
|
-
def
|
3095
|
+
def segments(pts = nil)
|
2807
3096
|
mth = "OSut::#{__callee__}"
|
2808
3097
|
vv = OpenStudio::Point3dVectorVector.new
|
2809
|
-
pts =
|
3098
|
+
pts = uniques(pts)
|
2810
3099
|
return vv if pts.size < 2
|
2811
3100
|
|
2812
3101
|
pts.each_with_index do |p1, i1|
|
@@ -2833,7 +3122,6 @@ module OSut
|
|
2833
3122
|
# @return [false] if invalid input (see logs)
|
2834
3123
|
def segment?(pts = nil)
|
2835
3124
|
pts = to_p3Dv(pts)
|
2836
|
-
return false if pts.empty?
|
2837
3125
|
return false unless pts.size == 2
|
2838
3126
|
return false if same?(pts[0], pts[1])
|
2839
3127
|
|
@@ -2850,10 +3138,10 @@ module OSut
|
|
2850
3138
|
# @param pts [OpenStudio::Point3dVector] 3D points
|
2851
3139
|
#
|
2852
3140
|
# @return [OpenStudio::Point3dVectorVector] triads (see logs if empty)
|
2853
|
-
def
|
3141
|
+
def triads(pts = nil, co = false)
|
2854
3142
|
mth = "OSut::#{__callee__}"
|
2855
3143
|
vv = OpenStudio::Point3dVectorVector.new
|
2856
|
-
pts =
|
3144
|
+
pts = uniques(pts)
|
2857
3145
|
return vv if pts.size < 2
|
2858
3146
|
|
2859
3147
|
pts.each_with_index do |p1, i1|
|
@@ -2880,7 +3168,7 @@ module OSut
|
|
2880
3168
|
# @param pts [Set<OpenStudio::Point3d>] 3D points
|
2881
3169
|
#
|
2882
3170
|
# @return [Bool] whether set is a valid triad (i.e. a trio of 3D points)
|
2883
|
-
# @return [false] if invalid input (see logs)
|
3171
|
+
# @return [false] if invalid input (see 'to_p3Dv' logs)
|
2884
3172
|
def triad?(pts = nil)
|
2885
3173
|
pts = to_p3Dv(pts)
|
2886
3174
|
return false if pts.empty?
|
@@ -2905,18 +3193,17 @@ module OSut
|
|
2905
3193
|
mth = "OSut::#{__callee__}"
|
2906
3194
|
cl1 = OpenStudio::Point3d
|
2907
3195
|
cl2 = OpenStudio::Point3dVector
|
2908
|
-
return mismatch(
|
2909
|
-
return
|
2910
|
-
|
3196
|
+
return mismatch("point", p0, cl1, mth, DBG, false) unless p0.is_a?(cl1)
|
3197
|
+
return false unless segment?(sg)
|
2911
3198
|
return true if holds?(sg, p0)
|
2912
3199
|
|
2913
|
-
a = sg
|
2914
|
-
b = sg
|
3200
|
+
a = sg[ 0]
|
3201
|
+
b = sg[-1]
|
2915
3202
|
ab = b - a
|
2916
3203
|
abn = b - a
|
2917
3204
|
abn.normalize
|
2918
3205
|
ap = p0 - a
|
2919
|
-
sp
|
3206
|
+
sp = ap.dot(abn)
|
2920
3207
|
return false if sp < 0
|
2921
3208
|
|
2922
3209
|
apd = scalar(abn, sp)
|
@@ -2941,9 +3228,9 @@ module OSut
|
|
2941
3228
|
mth = "OSut::#{__callee__}"
|
2942
3229
|
cl1 = OpenStudio::Point3d
|
2943
3230
|
cl2 = OpenStudio::Point3dVectorVector
|
2944
|
-
sgs = sgs.is_a?(cl2) ? sgs :
|
2945
|
-
return empty("segments",
|
2946
|
-
return mismatch("point", p0,
|
3231
|
+
sgs = sgs.is_a?(cl2) ? sgs : segments(sgs)
|
3232
|
+
return empty("segments", mth, DBG, false) if sgs.empty?
|
3233
|
+
return mismatch("point", p0, cl1, mth, DBG, false) unless p0.is_a?(cl1)
|
2947
3234
|
|
2948
3235
|
sgs.each { |sg| return true if pointAlongSegment?(p0, sg) }
|
2949
3236
|
|
@@ -2958,9 +3245,9 @@ module OSut
|
|
2958
3245
|
#
|
2959
3246
|
# @return [OpenStudio::Point3d] point of intersection of both lines
|
2960
3247
|
# @return [nil] if no intersection, equal, or invalid input (see logs)
|
2961
|
-
def
|
2962
|
-
s1
|
2963
|
-
s2
|
3248
|
+
def lineIntersection(s1 = [], s2 = [])
|
3249
|
+
s1 = segments(s1)
|
3250
|
+
s2 = segments(s2)
|
2964
3251
|
return nil if s1.empty?
|
2965
3252
|
return nil if s2.empty?
|
2966
3253
|
|
@@ -2971,10 +3258,10 @@ module OSut
|
|
2971
3258
|
return nil if same?(s1, s2)
|
2972
3259
|
return nil if same?(s1, s2.to_a.reverse)
|
2973
3260
|
|
2974
|
-
a1 = s1
|
2975
|
-
|
2976
|
-
|
2977
|
-
b2 = s2
|
3261
|
+
a1 = s1.first
|
3262
|
+
b1 = s2.first
|
3263
|
+
a2 = s1.last
|
3264
|
+
b2 = s2.last
|
2978
3265
|
|
2979
3266
|
# Matching segment endpoints?
|
2980
3267
|
return a1 if same?(a1, b1)
|
@@ -2983,18 +3270,18 @@ module OSut
|
|
2983
3270
|
return a2 if same?(a2, b2)
|
2984
3271
|
|
2985
3272
|
# Segment endpoint along opposite segment?
|
2986
|
-
return a1 if
|
2987
|
-
return a2 if
|
2988
|
-
return b1 if
|
2989
|
-
return b2 if
|
3273
|
+
return a1 if pointAlongSegment?(a1, s2)
|
3274
|
+
return a2 if pointAlongSegment?(a2, s2)
|
3275
|
+
return b1 if pointAlongSegment?(b1, s1)
|
3276
|
+
return b2 if pointAlongSegment?(b2, s1)
|
2990
3277
|
|
2991
|
-
# Line segments as vectors. Skip if
|
3278
|
+
# Line segments as vectors. Skip if collinear or parallel.
|
2992
3279
|
a = a2 - a1
|
2993
3280
|
b = b2 - b1
|
2994
3281
|
xab = a.cross(b)
|
2995
3282
|
return nil if xab.length.round(4) < TOL2
|
2996
3283
|
|
2997
|
-
# Link 1st point to other segment endpoints as vectors. Must be coplanar.
|
3284
|
+
# Link 1st point to other segment endpoints, as vectors. Must be coplanar.
|
2998
3285
|
a1b1 = b1 - a1
|
2999
3286
|
a1b2 = b2 - a1
|
3000
3287
|
xa1b1 = a.cross(a1b1)
|
@@ -3035,7 +3322,7 @@ module OSut
|
|
3035
3322
|
return nil if a.dot(p0 - a1) < 0
|
3036
3323
|
|
3037
3324
|
# Ensure intersection is sandwiched between endpoints.
|
3038
|
-
return nil unless
|
3325
|
+
return nil unless pointAlongSegment?(p0, s2) && pointAlongSegment?(p0, s1)
|
3039
3326
|
|
3040
3327
|
p0
|
3041
3328
|
end
|
@@ -3049,14 +3336,14 @@ module OSut
|
|
3049
3336
|
# @return [Bool] whether 3D line intersects 3D segments
|
3050
3337
|
# @return [false] if invalid input (see logs)
|
3051
3338
|
def lineIntersects?(l = [], s = [])
|
3052
|
-
l
|
3053
|
-
s
|
3339
|
+
l = segments(l)
|
3340
|
+
s = segments(s)
|
3054
3341
|
return nil if l.empty?
|
3055
3342
|
return nil if s.empty?
|
3056
3343
|
|
3057
3344
|
l = l.first
|
3058
3345
|
|
3059
|
-
s.each { |segment| return true if
|
3346
|
+
s.each { |segment| return true if lineIntersection(l, segment) }
|
3060
3347
|
|
3061
3348
|
false
|
3062
3349
|
end
|
@@ -3142,28 +3429,33 @@ module OSut
|
|
3142
3429
|
end
|
3143
3430
|
|
3144
3431
|
##
|
3145
|
-
# Returns
|
3432
|
+
# Returns non-collinear points in an OpenStudio 3D point vector.
|
3146
3433
|
#
|
3147
3434
|
# @param pts [Set<OpenStudio::Point3d] 3D points
|
3148
3435
|
# @param n [#to_i] requested number of non-collinears (0 returns all)
|
3149
3436
|
#
|
3150
|
-
# @return [OpenStudio::Point3dVector] non-collinears (see logs
|
3151
|
-
def
|
3437
|
+
# @return [OpenStudio::Point3dVector] non-collinears (see logs)
|
3438
|
+
def nonCollinears(pts = nil, n = 0)
|
3152
3439
|
mth = "OSut::#{__callee__}"
|
3153
|
-
pts = getUniques(pts)
|
3154
|
-
ok = n.respond_to?(:to_i)
|
3155
|
-
v = OpenStudio::Point3dVector.new
|
3156
3440
|
a = []
|
3441
|
+
pts = uniques(pts)
|
3157
3442
|
return pts if pts.size < 3
|
3158
|
-
|
3443
|
+
|
3444
|
+
if n.is_a?(Numeric)
|
3445
|
+
n = n.to_i
|
3446
|
+
else
|
3447
|
+
mismatch("n points", n, Integer, mth, DBG)
|
3448
|
+
n = 0
|
3449
|
+
end
|
3159
3450
|
|
3160
3451
|
# Evaluate cross product of vectors of 3x sequential points.
|
3161
3452
|
pts.each_with_index do |p2, i2|
|
3162
|
-
i1
|
3163
|
-
i3
|
3164
|
-
i3
|
3165
|
-
p1
|
3166
|
-
p3
|
3453
|
+
i1 = i2 - 1
|
3454
|
+
i3 = i2 + 1
|
3455
|
+
i3 = 0 if i3 == pts.size
|
3456
|
+
p1 = pts[i1]
|
3457
|
+
p3 = pts[i3]
|
3458
|
+
|
3167
3459
|
v13 = p3 - p1
|
3168
3460
|
v12 = p2 - p1
|
3169
3461
|
next if v12.cross(v13).length < TOL2
|
@@ -3171,36 +3463,47 @@ module OSut
|
|
3171
3463
|
a << p2
|
3172
3464
|
end
|
3173
3465
|
|
3174
|
-
if
|
3466
|
+
if a.include?(pts[0])
|
3175
3467
|
a = a.rotate(-1) unless same?(a[0], pts[0])
|
3176
3468
|
end
|
3177
3469
|
|
3178
|
-
n = n.
|
3179
|
-
a = a[0..n-1]
|
3180
|
-
a = a[n
|
3470
|
+
n = 0 if n.abs > a.size
|
3471
|
+
a = a[0..n-1] if n > 0
|
3472
|
+
a = a[n..-1] if n < 0
|
3181
3473
|
|
3182
3474
|
to_p3Dv(a)
|
3183
3475
|
end
|
3184
3476
|
|
3185
3477
|
##
|
3186
|
-
# Returns
|
3478
|
+
# Returns collinear points in an OpenStudio 3D point vector.
|
3187
3479
|
#
|
3188
3480
|
# @param pts [Set<OpenStudio::Point3d] 3D points
|
3189
3481
|
# @param n [#to_i] requested number of collinears (0 returns all)
|
3190
3482
|
#
|
3191
|
-
# @return [OpenStudio::Point3dVector] collinears (see logs
|
3192
|
-
def
|
3483
|
+
# @return [OpenStudio::Point3dVector] collinears (see logs)
|
3484
|
+
def collinears(pts = nil, n = 0)
|
3193
3485
|
mth = "OSut::#{__callee__}"
|
3194
|
-
|
3195
|
-
|
3196
|
-
v = OpenStudio::Point3dVector.new
|
3486
|
+
a = OpenStudio::Point3dVector.new
|
3487
|
+
pts = uniques(pts)
|
3197
3488
|
return pts if pts.size < 3
|
3198
|
-
return mismatch("n collinears", n, Integer, mth, DBG, v) unless ok
|
3199
3489
|
|
3200
|
-
|
3201
|
-
|
3490
|
+
if n.is_a?(Numeric)
|
3491
|
+
n = n.to_i
|
3492
|
+
else
|
3493
|
+
mismatch("n points", n, Integer, mth, DBG, pts)
|
3494
|
+
n = 0
|
3495
|
+
end
|
3496
|
+
|
3497
|
+
ncolls = nonCollinears(pts)
|
3498
|
+
return a if ncolls.empty?
|
3202
3499
|
|
3203
|
-
|
3500
|
+
a = pts.delete_if { |pt| holds?(ncolls, pt) }
|
3501
|
+
|
3502
|
+
n = 0 if n.abs > a.size
|
3503
|
+
a = a[0..n-1] if n > 0
|
3504
|
+
a = a[n..-1] if n < 0
|
3505
|
+
|
3506
|
+
a
|
3204
3507
|
end
|
3205
3508
|
|
3206
3509
|
##
|
@@ -3237,7 +3540,7 @@ module OSut
|
|
3237
3540
|
|
3238
3541
|
# --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- #
|
3239
3542
|
# Minimum 3 points?
|
3240
|
-
p3 =
|
3543
|
+
p3 = nonCollinears(pts, 3)
|
3241
3544
|
return empty("polygon (non-collinears < 3)", mth, ERR, v) if p3.size < 3
|
3242
3545
|
|
3243
3546
|
# Coplanar?
|
@@ -3268,8 +3571,8 @@ module OSut
|
|
3268
3571
|
# --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- #
|
3269
3572
|
# Ensure uniqueness and/or non-collinearity. Preserve original sequence.
|
3270
3573
|
p0 = a.first
|
3271
|
-
a =
|
3272
|
-
a =
|
3574
|
+
a = uniques(a).to_a if uq
|
3575
|
+
a = nonCollinears(a).to_a if co
|
3273
3576
|
i0 = a.index { |pt| same?(pt, p0) }
|
3274
3577
|
a = a.rotate(i0) unless i0.nil?
|
3275
3578
|
|
@@ -3278,7 +3581,7 @@ module OSut
|
|
3278
3581
|
if vx && a.size > 3
|
3279
3582
|
zen = OpenStudio::Point3d.new(0, 0, 1000)
|
3280
3583
|
|
3281
|
-
|
3584
|
+
triads(a).each do |trio|
|
3282
3585
|
p1 = trio[0]
|
3283
3586
|
p2 = trio[1]
|
3284
3587
|
p3 = trio[2]
|
@@ -3344,31 +3647,31 @@ module OSut
|
|
3344
3647
|
return false unless pl.pointOnPlane(p0)
|
3345
3648
|
|
3346
3649
|
entirely = false unless [true, false].include?(entirely)
|
3347
|
-
|
3650
|
+
sgments = segments(s)
|
3348
3651
|
|
3349
3652
|
# Along polygon edges, or near vertices?
|
3350
|
-
if pointAlongSegments?(p0,
|
3653
|
+
if pointAlongSegments?(p0, sgments)
|
3351
3654
|
return false if entirely
|
3352
3655
|
return true unless entirely
|
3353
3656
|
end
|
3354
3657
|
|
3355
|
-
|
3658
|
+
sgments.each do |sgment|
|
3356
3659
|
# - draw vector from segment midpoint to point
|
3357
3660
|
# - scale 1000x (assuming no building surface would be 1km wide)
|
3358
3661
|
# - convert vector to an independent line segment
|
3359
3662
|
# - loop through polygon segments, tally the number of intersections
|
3360
3663
|
# - avoid double-counting polygon vertices as intersections
|
3361
3664
|
# - return false if number of intersections is even
|
3362
|
-
mid = midpoint(
|
3665
|
+
mid = midpoint(sgment.first, sgment.last)
|
3363
3666
|
mpV = scalar(mid - p0, 1000)
|
3364
3667
|
p1 = p0 + mpV
|
3365
3668
|
ctr = 0
|
3366
3669
|
|
3367
3670
|
# Skip if ~collinear.
|
3368
|
-
next if mpV.cross(
|
3671
|
+
next if mpV.cross(sgment.last - sgment.first).length.round(4) < TOL2
|
3369
3672
|
|
3370
|
-
|
3371
|
-
intersect =
|
3673
|
+
sgments.each do |sg|
|
3674
|
+
intersect = lineIntersection([p0, p1], sg)
|
3372
3675
|
next unless intersect
|
3373
3676
|
|
3374
3677
|
# Skip test altogether if one of the polygon vertices.
|
@@ -3518,7 +3821,7 @@ module OSut
|
|
3518
3821
|
return false if pts.empty?
|
3519
3822
|
return false unless rectangular?(pts)
|
3520
3823
|
|
3521
|
-
|
3824
|
+
segments(pts).each do |pt|
|
3522
3825
|
l = (pt[1] - pt[0]).length
|
3523
3826
|
d = l unless d
|
3524
3827
|
return false unless l.round(2) == d.round(2)
|
@@ -3552,7 +3855,7 @@ module OSut
|
|
3552
3855
|
p2.each { |p0| return false if pointWithinPolygon?(p0, p1, true) }
|
3553
3856
|
|
3554
3857
|
# p1 segment mid-points must not lie OUTSIDE of p2.
|
3555
|
-
|
3858
|
+
segments(p1).each do |sg|
|
3556
3859
|
mp = midpoint(sg.first, sg.last)
|
3557
3860
|
return false unless pointWithinPolygon?(mp, p2)
|
3558
3861
|
end
|
@@ -3595,22 +3898,17 @@ module OSut
|
|
3595
3898
|
cw1 = clockwise?(p01)
|
3596
3899
|
a1 = cw1 ? p01.to_a.reverse : p01.to_a
|
3597
3900
|
a2 = p02.to_a
|
3598
|
-
a2 = flatten(a2).to_a if flat
|
3599
|
-
return invalid("points 2", mth, 2, DBG, face) unless xyz?(a2, :z)
|
3600
|
-
|
3601
|
-
cw2 = clockwise?(a2)
|
3602
|
-
a2 = a2.reverse if cw2
|
3603
3901
|
else
|
3604
3902
|
t = OpenStudio::Transformation.alignFace(p01)
|
3605
3903
|
a1 = t.inverse * p01
|
3606
3904
|
a2 = t.inverse * p02
|
3607
|
-
a2 = flatten(a2).to_a if flat
|
3608
|
-
return invalid("points 2", mth, 2, DBG, face) unless xyz?(a2, :z)
|
3609
|
-
|
3610
|
-
cw2 = clockwise?(a2)
|
3611
|
-
a2 = a2.reverse if cw2
|
3612
3905
|
end
|
3613
3906
|
|
3907
|
+
a2 = flatten(a2).to_a if flat
|
3908
|
+
cw2 = clockwise?(a2)
|
3909
|
+
a2 = a2.reverse if cw2
|
3910
|
+
return invalid("points 2", mth, 2, DBG, face) unless xyz?(a2, :z)
|
3911
|
+
|
3614
3912
|
# Return either (transformed) polygon if one fits into the other.
|
3615
3913
|
p1t = p01
|
3616
3914
|
|
@@ -3690,7 +3988,7 @@ module OSut
|
|
3690
3988
|
p2 = poly(p2)
|
3691
3989
|
return face if p1.empty?
|
3692
3990
|
return face if p2.empty?
|
3693
|
-
return mismatch("ray", ray, cl, mth) unless ray.is_a?(cl)
|
3991
|
+
return mismatch("ray", ray, cl, mth, face) unless ray.is_a?(cl)
|
3694
3992
|
|
3695
3993
|
# From OpenStudio SDK v3.7.0 onwards, one could/should rely on:
|
3696
3994
|
#
|
@@ -4023,12 +4321,12 @@ module OSut
|
|
4023
4321
|
#
|
4024
4322
|
# @param [Set<OpenStudio::Point3d>] a triad (3D points)
|
4025
4323
|
#
|
4026
|
-
# @return [Set<OpenStudio::Point3D>] a rectangular
|
4324
|
+
# @return [Set<OpenStudio::Point3D>] a rectangular BLC box (see logs if empty)
|
4027
4325
|
def triadBox(pts = nil)
|
4028
4326
|
mth = "OSut::#{__callee__}"
|
4029
4327
|
bkp = OpenStudio::Point3dVector.new
|
4030
4328
|
box = []
|
4031
|
-
pts =
|
4329
|
+
pts = nonCollinears(pts)
|
4032
4330
|
return bkp if pts.empty?
|
4033
4331
|
|
4034
4332
|
t = xyz?(pts, :z) ? nil : OpenStudio::Transformation.alignFace(pts)
|
@@ -4065,7 +4363,7 @@ module OSut
|
|
4065
4363
|
box << OpenStudio::Point3d.new(p1.x, p1.y, p1.z)
|
4066
4364
|
box << OpenStudio::Point3d.new(p2.x, p2.y, p2.z)
|
4067
4365
|
box << OpenStudio::Point3d.new(p3.x, p3.y, p3.z)
|
4068
|
-
box =
|
4366
|
+
box = nonCollinears(box, 4)
|
4069
4367
|
return bkp unless box.size == 4
|
4070
4368
|
|
4071
4369
|
box = blc(box)
|
@@ -4098,7 +4396,7 @@ module OSut
|
|
4098
4396
|
|
4099
4397
|
# Generate vertical plane along longest segment.
|
4100
4398
|
mpoints = []
|
4101
|
-
sgs =
|
4399
|
+
sgs = segments(pts)
|
4102
4400
|
longest = sgs.max_by { |s| OpenStudio.getDistanceSquared(s.first, s.last) }
|
4103
4401
|
plane = verticalPlane(longest.first, longest.last)
|
4104
4402
|
|
@@ -4112,7 +4410,7 @@ module OSut
|
|
4112
4410
|
box << mpoints.first
|
4113
4411
|
box << mpoints.last
|
4114
4412
|
box << plane.project(mpoints.last)
|
4115
|
-
box =
|
4413
|
+
box = nonCollinears(box).to_a
|
4116
4414
|
return bkp unless box.size == 4
|
4117
4415
|
|
4118
4416
|
box = clockwise?(box) ? blc(box.reverse) : blc(box)
|
@@ -4165,16 +4463,16 @@ module OSut
|
|
4165
4463
|
aire = 0
|
4166
4464
|
|
4167
4465
|
# PATH C : Right-angle, midpoint triad approach.
|
4168
|
-
|
4466
|
+
segments(pts).each do |sg|
|
4169
4467
|
m0 = midpoint(sg.first, sg.last)
|
4170
4468
|
|
4171
|
-
|
4469
|
+
segments(pts).each do |seg|
|
4172
4470
|
p1 = seg.first
|
4173
4471
|
p2 = seg.last
|
4174
4472
|
next if same?(p1, sg.first)
|
4175
4473
|
next if same?(p1, sg.last)
|
4176
4474
|
next if same?(p2, sg.first)
|
4177
|
-
next if same?(p2, sg.
|
4475
|
+
next if same?(p2, sg.last)
|
4178
4476
|
|
4179
4477
|
out = triadBox(OpenStudio::Point3dVector.new([m0, p1, p2]))
|
4180
4478
|
next if out.empty?
|
@@ -4194,7 +4492,7 @@ module OSut
|
|
4194
4492
|
end
|
4195
4493
|
|
4196
4494
|
# PATH D : Right-angle triad approach, may override PATH C boxes.
|
4197
|
-
|
4495
|
+
segments(pts).each do |sg|
|
4198
4496
|
p0 = sg.first
|
4199
4497
|
p1 = sg.last
|
4200
4498
|
|
@@ -4227,7 +4525,7 @@ module OSut
|
|
4227
4525
|
# PATH E : Medial box, segment approach.
|
4228
4526
|
aire = 0
|
4229
4527
|
|
4230
|
-
|
4528
|
+
segments(pts).each do |sg|
|
4231
4529
|
p0 = sg.first
|
4232
4530
|
p1 = sg.last
|
4233
4531
|
|
@@ -4260,7 +4558,7 @@ module OSut
|
|
4260
4558
|
# PATH F : Medial box, triad approach.
|
4261
4559
|
aire = 0
|
4262
4560
|
|
4263
|
-
|
4561
|
+
triads(pts).each do |sg|
|
4264
4562
|
p0 = sg[0]
|
4265
4563
|
p1 = sg[1]
|
4266
4564
|
p2 = sg[2]
|
@@ -4292,7 +4590,7 @@ module OSut
|
|
4292
4590
|
holes = OpenStudio::Point3dVectorVector.new
|
4293
4591
|
|
4294
4592
|
OpenStudio.computeTriangulation(outer, holes).each do |triangle|
|
4295
|
-
|
4593
|
+
segments(triangle).each do |sg|
|
4296
4594
|
p0 = sg.first
|
4297
4595
|
p1 = sg.last
|
4298
4596
|
|
@@ -4349,7 +4647,7 @@ module OSut
|
|
4349
4647
|
#
|
4350
4648
|
# @return [Hash] :set, :box, :bbox, :t, :r & :o
|
4351
4649
|
# @return [Hash] :set, :box, :bbox, :t, :r & :o (nil) if invalid (see logs)
|
4352
|
-
def
|
4650
|
+
def realignedFace(pts = nil, force = false)
|
4353
4651
|
mth = "OSut::#{__callee__}"
|
4354
4652
|
out = { set: nil, box: nil, bbox: nil, t: nil, r: nil, o: nil }
|
4355
4653
|
pts = poly(pts, false, true)
|
@@ -4372,11 +4670,11 @@ module OSut
|
|
4372
4670
|
box = boundedBox(pts)
|
4373
4671
|
return invalid("bounded box", mth, 0, DBG, out) if box.empty?
|
4374
4672
|
|
4375
|
-
|
4376
|
-
return invalid("bounded box segments", mth, 0, DBG, out) if
|
4673
|
+
sgments = segments(box)
|
4674
|
+
return invalid("bounded box segments", mth, 0, DBG, out) if sgments.empty?
|
4377
4675
|
|
4378
4676
|
# Deterministic ID of box rotation/translation 'origin'.
|
4379
|
-
|
4677
|
+
sgments.each_with_index do |sg, idx|
|
4380
4678
|
sgs[sg] = {}
|
4381
4679
|
sgs[sg][:idx] = idx
|
4382
4680
|
sgs[sg][:mid] = midpoint(sg[0], sg[1])
|
@@ -4396,10 +4694,10 @@ module OSut
|
|
4396
4694
|
i = sg0[:idx]
|
4397
4695
|
end
|
4398
4696
|
|
4399
|
-
k = i + 2 <
|
4697
|
+
k = i + 2 < sgments.size ? i + 2 : i - 2
|
4400
4698
|
|
4401
|
-
origin = midpoint(
|
4402
|
-
terminal = midpoint(
|
4699
|
+
origin = midpoint(sgments[i][0], sgments[i][1])
|
4700
|
+
terminal = midpoint(sgments[k][0], sgments[k][1])
|
4403
4701
|
seg = terminal - origin
|
4404
4702
|
right = OpenStudio::Point3d.new(origin.x + d, origin.y , 0) - origin
|
4405
4703
|
north = OpenStudio::Point3d.new(origin.x, origin.y + d, 0) - origin
|
@@ -4441,7 +4739,7 @@ module OSut
|
|
4441
4739
|
# @param pts [Set<OpenStudio::Point3d>] 3D points, once re/aligned
|
4442
4740
|
# @param force [Bool] whether to force rotation of (narrow) bounded box
|
4443
4741
|
#
|
4444
|
-
# @return [Float] width
|
4742
|
+
# @return [Float] width along X-axis, once re/aligned
|
4445
4743
|
# @return [0.0] if invalid inputs
|
4446
4744
|
def alignedWidth(pts = nil, force = false)
|
4447
4745
|
mth = "OSut::#{__callee__}"
|
@@ -4453,7 +4751,7 @@ module OSut
|
|
4453
4751
|
force = false
|
4454
4752
|
end
|
4455
4753
|
|
4456
|
-
pts =
|
4754
|
+
pts = realignedFace(pts, force)[:set]
|
4457
4755
|
return 0 if pts.size < 2
|
4458
4756
|
|
4459
4757
|
pts.max_by(&:x).x - pts.min_by(&:x).x
|
@@ -4477,12 +4775,84 @@ module OSut
|
|
4477
4775
|
force = false
|
4478
4776
|
end
|
4479
4777
|
|
4480
|
-
pts =
|
4778
|
+
pts = realignedFace(pts, force)[:set]
|
4481
4779
|
return 0 if pts.size < 2
|
4482
4780
|
|
4483
4781
|
pts.max_by(&:y).y - pts.min_by(&:y).y
|
4484
4782
|
end
|
4485
4783
|
|
4784
|
+
##
|
4785
|
+
# Fetch a space's full height (in space coordinates). The solution considers
|
4786
|
+
# all surface types ("Floor" vs "Wall" vs "RoofCeiling").
|
4787
|
+
#
|
4788
|
+
# @param space [OpenStudio::Model::Space] a space
|
4789
|
+
#
|
4790
|
+
# @return [Float] full height of space (0 if invalid input)
|
4791
|
+
def spaceHeight(space = nil)
|
4792
|
+
return 0 unless space.is_a?(OpenStudio::Model::Space)
|
4793
|
+
|
4794
|
+
minZ = 10000
|
4795
|
+
maxZ = -10000
|
4796
|
+
|
4797
|
+
space.surfaces.each do |surface|
|
4798
|
+
minZ = [surface.vertices.min_by(&:z).z, minZ].min
|
4799
|
+
maxZ = [surface.vertices.max_by(&:z).z, maxZ].max
|
4800
|
+
end
|
4801
|
+
|
4802
|
+
maxZ < minZ ? 0 : maxZ - minZ
|
4803
|
+
end
|
4804
|
+
|
4805
|
+
##
|
4806
|
+
# Fetch a space's width, based on the geometry of space floors.
|
4807
|
+
#
|
4808
|
+
# @param space [OpenStudio::Model::Space] a space
|
4809
|
+
#
|
4810
|
+
# @return [Float] width of a space (0 if invalid input)
|
4811
|
+
def spaceWidth(space = nil)
|
4812
|
+
return 0 unless space.is_a?(OpenStudio::Model::Space)
|
4813
|
+
|
4814
|
+
floors = facets(space, "all", "Floor")
|
4815
|
+
return 0 if floors.empty?
|
4816
|
+
|
4817
|
+
# Automatically determining a space's "width" is not straightforward:
|
4818
|
+
# - a space may hold multiple floor surfaces at various Z-axis levels
|
4819
|
+
# - a space may hold multiple floor surfaces, with unique "widths"
|
4820
|
+
# - a floor surface may expand/contract (in "width") along its length.
|
4821
|
+
#
|
4822
|
+
# First, attempt to merge all floor surfaces together as 1x polygon:
|
4823
|
+
# - select largest floor surface (in area)
|
4824
|
+
# - determine its 3D plane
|
4825
|
+
# - retain only other floor surfaces sharing same 3D plane
|
4826
|
+
# - recover potential union between floor surfaces
|
4827
|
+
# - fall back to largest floor surface if invalid union
|
4828
|
+
# - return width of largest bounded box
|
4829
|
+
floors = floors.sort_by(&:grossArea).reverse
|
4830
|
+
floor = floors.first
|
4831
|
+
plane = floor.plane
|
4832
|
+
t = OpenStudio::Transformation.alignFace(floor.vertices)
|
4833
|
+
polyg = poly(floor, false, true, true, t, :ulc).to_a.reverse
|
4834
|
+
return 0 if polyg.empty?
|
4835
|
+
|
4836
|
+
if floors.size > 1
|
4837
|
+
floors = floors.select { |flr| plane.equal(flr.plane, 0.001) }
|
4838
|
+
|
4839
|
+
if floors.size > 1
|
4840
|
+
polygs = floors.map { |flr| poly(flr, false, true, true, t, :ulc) }
|
4841
|
+
polygs = polygs.reject { |plg| plg.empty? }
|
4842
|
+
polygs = polygs.map { |plg| plg.to_a.reverse }
|
4843
|
+
union = OpenStudio.joinAll(polygs, 0.01).first
|
4844
|
+
polyg = poly(union, false, true, true)
|
4845
|
+
return 0 if polyg.empty?
|
4846
|
+
end
|
4847
|
+
end
|
4848
|
+
|
4849
|
+
res = realignedFace(polyg.to_a.reverse)
|
4850
|
+
return 0 if res[:box].nil?
|
4851
|
+
|
4852
|
+
# A bounded box's 'height', at its narrowest, is its 'width'.
|
4853
|
+
height(res[:box])
|
4854
|
+
end
|
4855
|
+
|
4486
4856
|
##
|
4487
4857
|
# Identifies 'leader line anchors', i.e. specific 3D points of a (larger) set
|
4488
4858
|
# (e.g. delineating a larger, parent polygon), each anchor linking the BLC
|
@@ -4504,7 +4874,7 @@ module OSut
|
|
4504
4874
|
#
|
4505
4875
|
# @param s [Set<OpenStudio::Point3d>] a (larger) parent set of points
|
4506
4876
|
# @param [Array<Hash>] set a collection of (smaller) sequenced points
|
4507
|
-
# @option [
|
4877
|
+
# @option [#to_sym] tag sequence of subset vertices to target
|
4508
4878
|
#
|
4509
4879
|
# @return [Integer] number of successfully anchored subsets (see logs)
|
4510
4880
|
def genAnchors(s = nil, set = [], tag = :box)
|
@@ -4512,18 +4882,20 @@ module OSut
|
|
4512
4882
|
n = 0
|
4513
4883
|
id = s.respond_to?(:nameString) ? "#{s.nameString}: " : ""
|
4514
4884
|
pts = poly(s)
|
4515
|
-
return invalid("#{id} polygon", mth, 1, DBG, n)
|
4516
|
-
return mismatch("set", set,
|
4885
|
+
return invalid("#{id} polygon", mth, 1, DBG, n) if pts.empty?
|
4886
|
+
return mismatch("set", set, Array, mth, DBG, n) unless set.respond_to?(:to_a)
|
4887
|
+
return mismatch("tag", tag, Symbol, mth, DBG, n) unless tag.respond_to?(:to_sym)
|
4517
4888
|
|
4518
4889
|
origin = OpenStudio::Point3d.new(0,0,0)
|
4519
4890
|
zenith = OpenStudio::Point3d.new(0,0,1)
|
4520
4891
|
ray = zenith - origin
|
4521
4892
|
set = set.to_a
|
4893
|
+
tag = tag.to_sym
|
4522
4894
|
|
4523
4895
|
# Validate individual subsets. Purge surface-specific leader line anchors.
|
4524
4896
|
set.each_with_index do |st, i|
|
4525
4897
|
str1 = id + "subset ##{i+1}"
|
4526
|
-
str2 = str1 + " #{tag
|
4898
|
+
str2 = str1 + " #{trim(tag)}"
|
4527
4899
|
return mismatch(str1, st, Hash, mth, DBG, n) unless st.respond_to?(:key?)
|
4528
4900
|
return hashkey( str1, st, tag, mth, DBG, n) unless st.key?(tag)
|
4529
4901
|
return empty("#{str2} vertices", mth, DBG, n) if st[tag].empty?
|
@@ -4576,7 +4948,7 @@ module OSut
|
|
4576
4948
|
else
|
4577
4949
|
st[:t] = OpenStudio::Transformation.alignFace(pts) unless st.key?(:t)
|
4578
4950
|
tpts = st[:t].inverse * st[tag]
|
4579
|
-
o =
|
4951
|
+
o = realignedFace(tpts, true)
|
4580
4952
|
tpts = st[:t] * (o[:r] * (o[:t] * o[:set]))
|
4581
4953
|
|
4582
4954
|
st[:out] = o
|
@@ -4594,7 +4966,7 @@ module OSut
|
|
4594
4966
|
nb = 0
|
4595
4967
|
|
4596
4968
|
# Check for intersections between leader line and larger polygon edges.
|
4597
|
-
|
4969
|
+
segments(pts).each do |sg|
|
4598
4970
|
break unless nb.zero?
|
4599
4971
|
next if holds?(sg, pt)
|
4600
4972
|
|
@@ -4608,7 +4980,7 @@ module OSut
|
|
4608
4980
|
|
4609
4981
|
ost = other[tag]
|
4610
4982
|
|
4611
|
-
|
4983
|
+
segments(ost).each { |sg| nb += 1 if lineIntersects?(ld, sg) }
|
4612
4984
|
end
|
4613
4985
|
|
4614
4986
|
# ... and previous leader lines (first come, first serve basis).
|
@@ -4626,7 +4998,7 @@ module OSut
|
|
4626
4998
|
end
|
4627
4999
|
|
4628
5000
|
# Finally, check for self-intersections.
|
4629
|
-
|
5001
|
+
segments(tpts).each do |sg|
|
4630
5002
|
break unless nb.zero?
|
4631
5003
|
next if holds?(sg, tpts.first)
|
4632
5004
|
|
@@ -4666,7 +5038,7 @@ module OSut
|
|
4666
5038
|
# @param s [Set<OpenStudio::Point3d>] a larger (parent) set of points
|
4667
5039
|
# @param [Array<Hash>] set a collection of (smaller) sequenced vertices
|
4668
5040
|
# @option set [Hash] :ld a polygon-specific leader line anchors
|
4669
|
-
# @option [
|
5041
|
+
# @option [#to_sym] tag sequence of set vertices to target
|
4670
5042
|
#
|
4671
5043
|
# @return [OpenStudio::Point3dVector] extended vertices (see logs if empty)
|
4672
5044
|
def genExtendedVertices(s = nil, set = [], tag = :vtx)
|
@@ -4678,16 +5050,19 @@ module OSut
|
|
4678
5050
|
a = OpenStudio::Point3dVector.new
|
4679
5051
|
v = []
|
4680
5052
|
return a if pts.empty?
|
4681
|
-
return mismatch("set", set,
|
5053
|
+
return mismatch("set", set, Array, mth, DBG, a) unless set.respond_to?(:to_a)
|
5054
|
+
return mismatch("tag", tag, Symbol, mth, DBG, n) unless tag.respond_to?(:to_sym)
|
4682
5055
|
|
4683
5056
|
set = set.to_a
|
5057
|
+
tag = tag.to_sym
|
4684
5058
|
|
4685
5059
|
# Validate individual sets.
|
4686
5060
|
set.each_with_index do |st, i|
|
4687
5061
|
str1 = id + "subset ##{i+1}"
|
4688
|
-
str2 = str1 + " #{tag
|
4689
|
-
next if st.key?(:void) && st[:void]
|
5062
|
+
str2 = str1 + " #{trim(tag)}"
|
4690
5063
|
return mismatch(str1, st, Hash, mth, DBG, a) unless st.respond_to?(:key?)
|
5064
|
+
next if st.key?(:void) && st[:void]
|
5065
|
+
|
4691
5066
|
return hashkey( str1, st, tag, mth, DBG, a) unless st.key?(tag)
|
4692
5067
|
return empty("#{str2} vertices", mth, DBG, a) if st[tag].empty?
|
4693
5068
|
return hashkey( str1, st, :ld, mth, DBG, a) unless st.key?(:ld)
|
@@ -4696,9 +5071,9 @@ module OSut
|
|
4696
5071
|
return invalid("#{str2} polygon", mth, 0, DBG, a) if stt.empty?
|
4697
5072
|
|
4698
5073
|
ld = st[:ld]
|
4699
|
-
return mismatch(
|
4700
|
-
return hashkey(
|
4701
|
-
return mismatch(
|
5074
|
+
return mismatch(str2, ld, Hash, mth, DBG, a) unless ld.is_a?(Hash)
|
5075
|
+
return hashkey( str2, ld, s, mth, DBG, a) unless ld.key?(s)
|
5076
|
+
return mismatch(str2, ld[s], cl, mth, DBG, a) unless ld[s].is_a?(cl)
|
4702
5077
|
end
|
4703
5078
|
|
4704
5079
|
# Re-sequence polygon vertices.
|
@@ -4976,8 +5351,8 @@ module OSut
|
|
4976
5351
|
# surface type filters if 'type' argument == "all".
|
4977
5352
|
#
|
4978
5353
|
# @param spaces [Set<OpenStudio::Model::Space>] target spaces
|
4979
|
-
# @param boundary [#
|
4980
|
-
# @param type [#
|
5354
|
+
# @param boundary [#to_sym] OpenStudio outside boundary condition
|
5355
|
+
# @param type [#to_sym] OpenStudio surface (or subsurface) type
|
4981
5356
|
# @param sides [Set<Symbols>] direction keys, e.g. :north (see OSut::SIDZ)
|
4982
5357
|
#
|
4983
5358
|
# @return [Array<OpenStudio::Model::Surface>] surfaces (may be empty, no logs)
|
@@ -4986,7 +5361,7 @@ module OSut
|
|
4986
5361
|
spaces = spaces.respond_to?(:to_a) ? spaces.to_a : []
|
4987
5362
|
return [] if spaces.empty?
|
4988
5363
|
|
4989
|
-
sides = sides.respond_to?(:to_sym) ? [sides] : sides
|
5364
|
+
sides = sides.respond_to?(:to_sym) ? [trim(sides).to_sym] : sides
|
4990
5365
|
sides = sides.respond_to?(:to_a) ? sides.to_a : []
|
4991
5366
|
|
4992
5367
|
faces = []
|
@@ -5091,7 +5466,7 @@ module OSut
|
|
5091
5466
|
pltz.each_with_index do |plt, i|
|
5092
5467
|
id = "plate # #{i+1} (index #{i})"
|
5093
5468
|
|
5094
|
-
return mismatch(id, plt,
|
5469
|
+
return mismatch(id, plt, cl2, mth, DBG, slb) unless plt.is_a?(cl2)
|
5095
5470
|
return hashkey( id, plt, :x, mth, DBG, slb) unless plt.key?(:x )
|
5096
5471
|
return hashkey( id, plt, :y, mth, DBG, slb) unless plt.key?(:y )
|
5097
5472
|
return hashkey( id, plt, :dx, mth, DBG, slb) unless plt.key?(:dx)
|
@@ -5142,7 +5517,7 @@ module OSut
|
|
5142
5517
|
end
|
5143
5518
|
|
5144
5519
|
# Once joined, re-adjust Z-axis coordinates.
|
5145
|
-
unless z.
|
5520
|
+
unless z.round(2) == 0.00
|
5146
5521
|
vtx = OpenStudio::Point3dVector.new
|
5147
5522
|
slb.each { |pt| vtx << OpenStudio::Point3d.new(pt.x, pt.y, z) }
|
5148
5523
|
slb = vtx
|
@@ -5162,18 +5537,18 @@ module OSut
|
|
5162
5537
|
# @param spaces [Set<OpenStudio::Model::Space>] target spaces
|
5163
5538
|
#
|
5164
5539
|
# @return [Array<OpenStudio::Model::Surface>] roofs (may be empty)
|
5165
|
-
def
|
5540
|
+
def roofs(spaces = [])
|
5166
5541
|
mth = "OSut::#{__callee__}"
|
5167
5542
|
up = OpenStudio::Point3d.new(0,0,1) - OpenStudio::Point3d.new(0,0,0)
|
5168
|
-
|
5543
|
+
rfs = []
|
5169
5544
|
spaces = spaces.is_a?(OpenStudio::Model::Space) ? [spaces] : spaces
|
5170
5545
|
spaces = spaces.respond_to?(:to_a) ? spaces.to_a : []
|
5171
5546
|
|
5172
5547
|
spaces = spaces.select { |space| space.is_a?(OpenStudio::Model::Space) }
|
5173
5548
|
|
5174
5549
|
# Space-specific outdoor-facing roof surfaces.
|
5175
|
-
|
5176
|
-
|
5550
|
+
rfs = facets(spaces, "Outdoors", "RoofCeiling")
|
5551
|
+
rfs = rfs.select { |rf| roof?(rf) }
|
5177
5552
|
|
5178
5553
|
spaces.each do |space|
|
5179
5554
|
# When unoccupied spaces are involved (e.g. plenums, attics), the target
|
@@ -5209,12 +5584,12 @@ module OSut
|
|
5209
5584
|
cst = cast(cv0, rvi, up)
|
5210
5585
|
next unless overlaps?(cst, rvi, false)
|
5211
5586
|
|
5212
|
-
|
5587
|
+
rfs << ruf unless rfs.include?(ruf)
|
5213
5588
|
end
|
5214
5589
|
end
|
5215
5590
|
end
|
5216
5591
|
|
5217
|
-
|
5592
|
+
rfs
|
5218
5593
|
end
|
5219
5594
|
|
5220
5595
|
##
|
@@ -5240,10 +5615,10 @@ module OSut
|
|
5240
5615
|
return invalid("baselit" , mth, 4, DBG, false) unless ck4
|
5241
5616
|
|
5242
5617
|
walls = sidelit ? facets(space, "Outdoors", "Wall") : []
|
5243
|
-
|
5618
|
+
rufs = toplit ? facets(space, "Outdoors", "RoofCeiling") : []
|
5244
5619
|
floors = baselit ? facets(space, "Outdoors", "Floor") : []
|
5245
5620
|
|
5246
|
-
(walls +
|
5621
|
+
(walls + rufs + floors).each do |surface|
|
5247
5622
|
surface.subSurfaces.each do |sub|
|
5248
5623
|
# All fenestrated subsurface types are considered, as user can set these
|
5249
5624
|
# explicitly (e.g. skylight in a wall) in OpenStudio.
|
@@ -5260,7 +5635,7 @@ module OSut
|
|
5260
5635
|
# @param s [OpenStudio::Model::Surface] a model surface
|
5261
5636
|
# @param [Array<Hash>] subs requested attributes
|
5262
5637
|
# @option subs [#to_s] :id identifier e.g. "Window 007"
|
5263
|
-
# @option subs [#
|
5638
|
+
# @option subs [#to_sym] :type ("FixedWindow") OpenStudio subsurface type
|
5264
5639
|
# @option subs [#to_i] :count (1) number of individual subs per array
|
5265
5640
|
# @option subs [#to_i] :multiplier (1) OpenStudio subsurface multiplier
|
5266
5641
|
# @option subs [#frameWidth] :frame (nil) OpenStudio frame & divider object
|
@@ -5370,11 +5745,11 @@ module OSut
|
|
5370
5745
|
box = boundedBox(s0)
|
5371
5746
|
|
5372
5747
|
if realign
|
5373
|
-
s00 =
|
5748
|
+
s00 = realignedFace(box, true)
|
5374
5749
|
return invalid("bound realignment", mth, 0, DBG, false) unless s00[:set]
|
5375
5750
|
end
|
5376
5751
|
elsif realign
|
5377
|
-
s00 =
|
5752
|
+
s00 = realignedFace(s0, false)
|
5378
5753
|
return invalid("unbound realignment", mth, 0, DBG, false) unless s00[:set]
|
5379
5754
|
end
|
5380
5755
|
|
@@ -5389,12 +5764,13 @@ module OSut
|
|
5389
5764
|
return mismatch("sub", sub, cl4, mth, DBG, no) unless sub.is_a?(cl3)
|
5390
5765
|
|
5391
5766
|
# Required key:value pairs (either set by the user or defaulted).
|
5392
|
-
sub[:frame ] = nil unless sub.key?(:frame
|
5393
|
-
sub[:assembly ] = nil unless sub.key?(:assembly
|
5394
|
-
sub[:count ] = 1 unless sub.key?(:count
|
5767
|
+
sub[:frame ] = nil unless sub.key?(:frame)
|
5768
|
+
sub[:assembly ] = nil unless sub.key?(:assembly)
|
5769
|
+
sub[:count ] = 1 unless sub.key?(:count)
|
5395
5770
|
sub[:multiplier] = 1 unless sub.key?(:multiplier)
|
5396
|
-
sub[:id ] = "" unless sub.key?(:id
|
5397
|
-
sub[:type ] = type unless sub.key?(:type
|
5771
|
+
sub[:id ] = "" unless sub.key?(:id)
|
5772
|
+
sub[:type ] = type unless sub.key?(:type)
|
5773
|
+
sub[:type ] = type unless sub[:type].respond_to?(:to_sym)
|
5398
5774
|
sub[:type ] = trim(sub[:type])
|
5399
5775
|
sub[:id ] = trim(sub[:id])
|
5400
5776
|
sub[:type ] = type if sub[:type].empty?
|
@@ -5983,7 +6359,6 @@ module OSut
|
|
5983
6359
|
# previously-added leader lines.
|
5984
6360
|
#
|
5985
6361
|
# @todo: revise approach for attics ONCE skylight wells have been added.
|
5986
|
-
olap = nil
|
5987
6362
|
olap = overlap(cst, rvi, false)
|
5988
6363
|
next if olap.empty?
|
5989
6364
|
|
@@ -6013,24 +6388,24 @@ module OSut
|
|
6013
6388
|
# (Array of 2x linked surfaces). Each surface may be linked to more than one
|
6014
6389
|
# horizontal ridge.
|
6015
6390
|
#
|
6016
|
-
# @param
|
6391
|
+
# @param rfs [Array<OpenStudio::Model::Surface>] target surfaces
|
6017
6392
|
#
|
6018
6393
|
# @return [Array] horizontal ridges (see logs if empty)
|
6019
|
-
def
|
6394
|
+
def horizontalRidges(rfs = [])
|
6020
6395
|
mth = "OSut::#{__callee__}"
|
6021
6396
|
ridges = []
|
6022
|
-
return ridges unless
|
6397
|
+
return ridges unless rfs.is_a?(Array)
|
6023
6398
|
|
6024
|
-
|
6025
|
-
|
6399
|
+
rfs = rfs.select { |s| s.is_a?(OpenStudio::Model::Surface) }
|
6400
|
+
rfs = rfs.select { |s| sloped?(s) }
|
6026
6401
|
|
6027
|
-
|
6028
|
-
maxZ =
|
6029
|
-
next if
|
6402
|
+
rfs.each do |rf|
|
6403
|
+
maxZ = rf.vertices.max_by(&:z).z
|
6404
|
+
next if rf.space.empty?
|
6030
6405
|
|
6031
|
-
space =
|
6406
|
+
space = rf.space.get
|
6032
6407
|
|
6033
|
-
|
6408
|
+
segments(rf).each do |edge|
|
6034
6409
|
next unless xyz?(edge, :z, maxZ)
|
6035
6410
|
|
6036
6411
|
# Skip if already tracked.
|
@@ -6045,18 +6420,18 @@ module OSut
|
|
6045
6420
|
|
6046
6421
|
next if match
|
6047
6422
|
|
6048
|
-
ridge = { edge: edge, length: (edge[1] - edge[0]).length, roofs: [
|
6423
|
+
ridge = { edge: edge, length: (edge[1] - edge[0]).length, roofs: [rf] }
|
6049
6424
|
|
6050
6425
|
# Links another roof (same space)?
|
6051
6426
|
match = false
|
6052
6427
|
|
6053
|
-
|
6428
|
+
rfs.each do |ruf|
|
6054
6429
|
break if match
|
6055
|
-
next if ruf ==
|
6430
|
+
next if ruf == rf
|
6056
6431
|
next if ruf.space.empty?
|
6057
6432
|
next unless ruf.space.get == space
|
6058
6433
|
|
6059
|
-
|
6434
|
+
segments(ruf).each do |edg|
|
6060
6435
|
break if match
|
6061
6436
|
next unless same?(edge, edg) || same?(edge, edg.reverse)
|
6062
6437
|
|
@@ -6100,7 +6475,7 @@ module OSut
|
|
6100
6475
|
if opts[:size].respond_to?(:to_f)
|
6101
6476
|
w = opts[:size].to_f
|
6102
6477
|
w2 = w * w
|
6103
|
-
return invalid(size, mth, 0, ERR, []) if w.round(2) < gap4
|
6478
|
+
return invalid("size", mth, 0, ERR, []) if w.round(2) < gap4
|
6104
6479
|
else
|
6105
6480
|
return mismatch("size", opts[:size], Numeric, mth, DBG, [])
|
6106
6481
|
end
|
@@ -6123,12 +6498,12 @@ module OSut
|
|
6123
6498
|
spaces = spaces.select { |sp| sp.partofTotalFloorArea }
|
6124
6499
|
spaces = spaces.reject { |sp| unconditioned?(sp) }
|
6125
6500
|
spaces = spaces.reject { |sp| vestibule?(sp) }
|
6126
|
-
spaces = spaces.reject { |sp|
|
6501
|
+
spaces = spaces.reject { |sp| roofs(sp).empty? }
|
6127
6502
|
spaces = spaces.reject { |sp| sp.floorArea < 4 * w2 }
|
6128
6503
|
spaces = spaces.sort_by(&:floorArea).reverse
|
6129
|
-
return empty("spaces", mth, WRN,
|
6504
|
+
return empty("spaces", mth, WRN, []) if spaces.empty?
|
6130
6505
|
else
|
6131
|
-
return mismatch("spaces", spaces, Array, mth, DBG,
|
6506
|
+
return mismatch("spaces", spaces, Array, mth, DBG, [])
|
6132
6507
|
end
|
6133
6508
|
|
6134
6509
|
# Unfenestrated spaces have no windows, glazed doors or skylights. By
|
@@ -6169,7 +6544,7 @@ module OSut
|
|
6169
6544
|
|
6170
6545
|
# Gather roof surfaces - possibly those of attics or plenums above.
|
6171
6546
|
spaces.each do |sp|
|
6172
|
-
|
6547
|
+
roofs(sp).each do |rf|
|
6173
6548
|
espaces[sp] = {roofs: []} unless espaces.key?(sp)
|
6174
6549
|
espaces[sp][:roofs] << rf unless espaces[sp][:roofs].include?(rf)
|
6175
6550
|
end
|
@@ -6224,7 +6599,7 @@ module OSut
|
|
6224
6599
|
# @option opts [Bool] :sloped (true) whether to consider sloped roof surfaces
|
6225
6600
|
# @option opts [Bool] :plenum (true) whether to consider plenum wells
|
6226
6601
|
# @option opts [Bool] :attic (true) whether to consider attic wells
|
6227
|
-
# @option opts [Array<#
|
6602
|
+
# @option opts [Array<#to_sym>] :patterns requested skylight allocation (3x)
|
6228
6603
|
# @example (a) consider 2D array of individual skylights, e.g. n(1.22m x 1.22m)
|
6229
6604
|
# opts[:patterns] = ["array"]
|
6230
6605
|
# @example (b) consider 'a', then array of 1x(size) x n(size) skylight strips
|
@@ -6244,6 +6619,7 @@ module OSut
|
|
6244
6619
|
bfr = 0.005 # minimum array perimeter buffer (no wells)
|
6245
6620
|
w = 1.22 # default 48" x 48" skylight base
|
6246
6621
|
w2 = w * w # m2
|
6622
|
+
v = OpenStudio.openStudioVersion.split(".").join.to_i
|
6247
6623
|
|
6248
6624
|
# --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- #
|
6249
6625
|
# Excerpts of ASHRAE 90.1 2022 definitions:
|
@@ -6360,10 +6736,10 @@ module OSut
|
|
6360
6736
|
|
6361
6737
|
if frame.respond_to?(:frameWidth)
|
6362
6738
|
frame = nil if v < 321
|
6363
|
-
frame = nil if
|
6364
|
-
frame = nil if
|
6739
|
+
frame = nil if frame.frameWidth.round(2) < 0
|
6740
|
+
frame = nil if frame.frameWidth.round(2) > gap
|
6365
6741
|
|
6366
|
-
f =
|
6742
|
+
f = frame.frameWidth if frame
|
6367
6743
|
log(WRN, "Skip Frame&Divider (#{mth})") unless frame
|
6368
6744
|
else
|
6369
6745
|
frame = nil
|
@@ -6420,7 +6796,7 @@ module OSut
|
|
6420
6796
|
end
|
6421
6797
|
|
6422
6798
|
# Purge if requested.
|
6423
|
-
|
6799
|
+
roofs(spaces).each { |s| s.subSurfaces.map(&:remove) } if clear
|
6424
6800
|
|
6425
6801
|
# Safely exit, e.g. if strictly called to purge existing roof subsurfaces.
|
6426
6802
|
return 0 if area && area.round(2) == 0
|
@@ -6540,7 +6916,7 @@ module OSut
|
|
6540
6916
|
if opts.key?(:patterns)
|
6541
6917
|
if opts[:patterns].is_a?(Array)
|
6542
6918
|
opts[:patterns].each_with_index do |pattern, i|
|
6543
|
-
pattern = trim(pattern).downcase
|
6919
|
+
pattern = pattern.respond_to?(:to_sym) ? trim(pattern).downcase : ""
|
6544
6920
|
|
6545
6921
|
if pattern.empty?
|
6546
6922
|
invalid("pattern #{i+1}", mth, 0, ERR)
|
@@ -6588,14 +6964,14 @@ module OSut
|
|
6588
6964
|
next unless opts[opt] == false
|
6589
6965
|
|
6590
6966
|
case opt
|
6591
|
-
when :sidelit then filters.map! { |
|
6592
|
-
when :sloped then filters.map! { |
|
6593
|
-
when :plenum then filters.map! { |
|
6594
|
-
when :attic then filters.map! { |
|
6967
|
+
when :sidelit then filters.map! { |fl| fl.include?("b") ? fl.delete("b") : fl }
|
6968
|
+
when :sloped then filters.map! { |fl| fl.include?("c") ? fl.delete("c") : fl }
|
6969
|
+
when :plenum then filters.map! { |fl| fl.include?("d") ? fl.delete("d") : fl }
|
6970
|
+
when :attic then filters.map! { |fl| fl.include?("e") ? fl.delete("e") : fl }
|
6595
6971
|
end
|
6596
6972
|
end
|
6597
6973
|
|
6598
|
-
filters.reject! { |
|
6974
|
+
filters.reject! { |fl| fl.empty? }
|
6599
6975
|
filters.uniq!
|
6600
6976
|
|
6601
6977
|
# Remaining filters may be further pruned automatically after space/roof
|
@@ -6698,7 +7074,7 @@ module OSut
|
|
6698
7074
|
# Process outdoor-facing roof surfaces of plenums and attics above.
|
6699
7075
|
rooms.each do |space, room|
|
6700
7076
|
t0 = room[:t0]
|
6701
|
-
rufs =
|
7077
|
+
rufs = roofs(space) - room[:roofs]
|
6702
7078
|
|
6703
7079
|
rufs.each do |ruf|
|
6704
7080
|
next unless roof?(ruf)
|
@@ -6860,12 +7236,12 @@ module OSut
|
|
6860
7236
|
# Ensure uniqueness of plenum roofs.
|
6861
7237
|
attics.values.each do |attic|
|
6862
7238
|
attic[:roofs ].uniq!
|
6863
|
-
attic[:ridges] =
|
7239
|
+
attic[:ridges] = horizontalRidges(attic[:roofs]) # @todo
|
6864
7240
|
end
|
6865
7241
|
|
6866
7242
|
plenums.values.each do |plenum|
|
6867
7243
|
plenum[:roofs ].uniq!
|
6868
|
-
plenum[:ridges] =
|
7244
|
+
plenum[:ridges] = horizontalRidges(plenum[:roofs]) # @todo
|
6869
7245
|
end
|
6870
7246
|
|
6871
7247
|
# Regardless of the selected skylight arrangement pattern, the solution only
|
@@ -7388,7 +7764,7 @@ module OSut
|
|
7388
7764
|
pattern = "array"
|
7389
7765
|
elsif fpm2.keys.include?("strips")
|
7390
7766
|
pattern = "strips"
|
7391
|
-
else fpm2.keys.include?("strip")
|
7767
|
+
else # fpm2.keys.include?("strip")
|
7392
7768
|
pattern = "strip"
|
7393
7769
|
end
|
7394
7770
|
else
|
@@ -7399,7 +7775,7 @@ module OSut
|
|
7399
7775
|
pattern = "strip"
|
7400
7776
|
elsif fpm2.keys.include?("strips")
|
7401
7777
|
pattern = "strips"
|
7402
|
-
else fpm2.keys.include?("array")
|
7778
|
+
else # fpm2.keys.include?("array")
|
7403
7779
|
pattern = "array"
|
7404
7780
|
end
|
7405
7781
|
end
|
@@ -7502,7 +7878,7 @@ module OSut
|
|
7502
7878
|
# Size contraction: round 2: prioritize larger sets.
|
7503
7879
|
adm2 = 0
|
7504
7880
|
|
7505
|
-
sets.each_with_index do |set
|
7881
|
+
sets.each_with_index do |set|
|
7506
7882
|
next if set[:w].round(2) <= w0
|
7507
7883
|
next if set[:d].round(2) <= w0
|
7508
7884
|
|
@@ -7674,12 +8050,12 @@ module OSut
|
|
7674
8050
|
|
7675
8051
|
# Generate well walls.
|
7676
8052
|
vX = cast(roof, tile, ray)
|
7677
|
-
s0 =
|
7678
|
-
sX =
|
8053
|
+
s0 = segments(t0 * roof.vertices)
|
8054
|
+
sX = segments(t0 * vX)
|
7679
8055
|
|
7680
8056
|
s0.each_with_index do |sg, j|
|
7681
|
-
sg0 = sg
|
7682
|
-
sgX = sX[j]
|
8057
|
+
sg0 = sg
|
8058
|
+
sgX = sX[j]
|
7683
8059
|
vec = OpenStudio::Point3dVector.new
|
7684
8060
|
vec << sg0.first
|
7685
8061
|
vec << sg0.last
|