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.
@@ -1,6 +1,6 @@
1
1
  # BSD 3-Clause License
2
2
  #
3
- # Copyright (c) 2022-2024, Denis Bourgeois
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
- TOL = 0.01 # default distance tolerance (m)
38
- TOL2 = TOL * TOL # default area tolerance (m2)
39
- DBG = OSlg::DEBUG.dup # see github.com/rd2/oslg
40
- INF = OSlg::INFO.dup # see github.com/rd2/oslg
41
- WRN = OSlg::WARN.dup # see github.com/rd2/oslg
42
- ERR = OSlg::ERROR.dup # see github.com/rd2/oslg
43
- FTL = OSlg::FATAL.dup # see github.com/rd2/oslg
44
- NS = "nameString" # OpenStudio object identifier method
45
-
46
- HEAD = 2.032 # standard 80" door
47
- SILL = 0.762 # standard 30" window sill
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|CON|#{specs[:type]}" if id.empty?
605
+ id = "OSut:CON:#{specs[:type]}" if id.empty?
218
606
 
219
- specs[:type] = :wall unless specs.key?(:type)
220
- chk = @@uo.keys.include?(specs[:type])
221
- return invalid("surface type", mth, 2, ERR) unless chk
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
- if u
227
- return mismatch("#{id} Uo", u, Numeric, mth) unless u.is_a?(Numeric)
228
- return invalid("#{id} Uo (> 5.678)", mth, 2, ERR) if u > 5.678
229
- return negative("#{id} Uo" , mth, ERR) if u < 0
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|#{mt}|#{format('%03d', d*1000)[-3..-1]}"
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|#{mt}|#{format('%03d', d*1000)[-3..-1]}"
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|#{mt}|#{format('%03d', d*1000)[-3..-1]}"
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|#{mt}|#{format('%03d', d*1000)[-3..-1]}"
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|#{mt}|#{format('%03d', d*1000)[-3..-1]}"
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|#{mt}|#{format('%03d', d*1000)[-3..-1]}"
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|#{mt}|#{format('%03d', d*1000)[-3..-1]}"
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|#{mt}|#{format('%03d', d*1000)[-3..-1]}"
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|#{mt}|#{format('%03d', d*1000)[-3..-1]}"
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|#{mt}|#{format('%03d', d*1000)[-3..-1]}"
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|#{mt}|#{format('%03d', d*1000)[-3..-1]}"
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|#{mt}|#{format('%03d', d*1000)[-3..-1]}"
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|#{mt}|#{format('%03d', d*1000)[-3..-1]}"
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|#{mt}|#{format('%03d', d*1000)[-3..-1]}"
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|#{mt}|#{format('%03d', d*1000)[-3..-1]}"
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|#{mt}|#{format('%03d', d*1000)[-3..-1]}"
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|#{mt}|#{format('%03d', d*1000)[-3..-1]}"
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|#{mt}|#{format('%03d', d*1000)[-3..-1]}"
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|#{mt}|#{format('%03d', d*1000)[-3..-1]}"
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|#{mt}|#{format('%03d', d*1000)[-3..-1]}"
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|#{mt}|#{format('%03d', d*1000)[-3..-1]}"
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|#{mt}|#{format('%03d', d*1000)[-3..-1]}"
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|#{mt}|#{format('%03d', d*1000)[-3..-1]}"
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|#{mt}|#{format('%03d', d*1000)[-3..-1]}"
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|#{mt}|#{format('%03d', d*1000)[-3..-1]}"
863
+ a[:compo ][:id ] = "OSut:#{mt}:#{format('%03d', d*1000)[-3..-1]}"
468
864
  when :window
469
- a[:glazing][:u ] = specs[:uo ]
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|window"
473
- a[:glazing][:id ] += "|U#{format('%.1f', a[:glazing][:u])}"
474
- a[:glazing][:id ] += "|SHGC#{format('%d', a[:glazing][:shgc]*100)}"
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 ] = specs[:uo ]
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|skylight"
480
- a[:glazing][:id ] += "|U#{format('%.1f', a[:glazing][:u])}"
481
- a[:glazing][:id ] += "|SHGC#{format('%d', a[:glazing][:shgc]*100)}"
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
- glazed = true
486
- glazed = false if a[:glazing].empty?
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 = OpenStudio::Model::Construction.new(layers)
924
+ c = OpenStudio::Model::Construction.new(layers)
530
925
  c.setName(id)
531
926
 
532
- # Adjust insulating layer thickness or conductivity to match requested Uo.
533
- unless glazed
534
- ro = 0
535
- ro = 1 / specs[:uo] - @@film[ specs[:type] ] if specs[:uo]
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
- if specs[:type] == :door # 1x layer, adjust conductivity
538
- layer = c.getLayer(0).to_StandardOpaqueMaterial
539
- return invalid("#{id} standard material?", mth, 0) if layer.empty?
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
- layer = layer.get
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
- index = lyr[:index]
551
- layer = c.getLayer(index).to_StandardOpaqueMaterial
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
- layer = layer.get
555
- k = layer.conductivity
556
- d = (ro - rsi(c) + lyr[:r]) * k
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
- nom = "OSut|"
560
- nom += layer.nameString.gsub(/[^a-z]/i, "").gsub("OSut", "")
561
- nom += "|"
562
- nom += format("%03d", d*1000)[-3..-1]
563
- layer.setName(nom) if model.getStandardOpaqueMaterialByName(nom).empty?
564
- layer.setThickness(d)
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|SHADE|Ruleset"
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|Shade|Ruleset|Default")
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|SHADE|ScheduleRule"
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|Shade|Rule|Default")
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|Shade"
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|ShadingControl"
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|MASS|Material"
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|MASS|Construction"
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|InternalMassDefinition|" + (format "%.2f", ratio)
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|InternalMass|#{sp.nameString}")
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 a base construction.
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 [OpensStudio::Model::ConstructionBase] a construction base
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 [#to_s] a surface type
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?(:to_s)
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
- t += 273.0 # °C to K
1000
- return negative("temp K", mth, ERR, 0.0) if t < 0
1001
- return negative("film", mth, ERR, 0.0) if film < 0
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
- rsi = film
1186
+ constructions = nil
1004
1187
 
1005
- lc.layers.each do |m|
1006
- # Fenestration materials first.
1007
- empty = m.to_SimpleGlazing.empty?
1008
- return 1 / m.to_SimpleGlazing.get.uFactor unless empty
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
- empty = m.to_StandardGlazing.empty?
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
- # Opaque materials next.
1020
- empty = m.to_StandardOpaqueMaterial.empty?
1021
- rsi += m.to_StandardOpaqueMaterial.get.thermalResistance unless empty
1022
- empty = m.to_MasslessOpaqueMaterial.empty?
1023
- rsi += m.to_MasslessOpaqueMaterial.get.thermalResistance unless empty
1024
- empty = m.to_RoofVegetation.empty?
1025
- rsi += m.to_RoofVegetation.get.thermalResistance unless empty
1026
- empty = m.to_AirGap.empty?
1027
- rsi += m.to_AirGap.get.thermalResistance unless empty
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
- rsi
1222
+ false
1031
1223
  end
1032
1224
 
1033
1225
  ##
1034
- # Identifies a layered construction's (opaque) insulating layer. The method
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 lc [OpenStudio::Model::LayeredConstruction] a layered construction
1228
+ # @param s [OpenStudio::Model::Surface] a surface
1040
1229
  #
1041
- # @return [Hash] index: (Integer), type: (Symbol), r: (Float)
1042
- # @return [Hash] index: nil, type: nil, r: 0 if invalid input (see logs)
1043
- def insulatingLayer(lc = nil)
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::LayeredConstruction
1046
- res = { index: nil, type: nil, r: 0.0 }
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 = lc.nameString
1051
- return mismatch(id, lc, cl1, mth, DBG, res) unless lc.is_a?(cl)
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
- lc.layers.each do |m|
1054
- unless m.to_MasslessOpaqueMaterial.empty?
1055
- m = m.to_MasslessOpaqueMaterial.get
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
- if m.thermalResistance < 0.001 || m.thermalResistance < res[:r]
1058
- i += 1
1059
- next
1060
- else
1061
- res[:r ] = m.thermalResistance
1062
- res[:index] = i
1063
- res[:type ] = :massless
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
- unless m.to_StandardOpaqueMaterial.empty?
1068
- m = m.to_StandardOpaqueMaterial.get
1069
- k = m.thermalConductivity
1070
- d = m.thickness
1277
+ unless space.spaceType.empty?
1278
+ spacetype = space.spaceType.get
1071
1279
 
1072
- if d < 0.003 || k > 3.0 || d / k < res[:r]
1073
- i += 1
1074
- next
1075
- else
1076
- res[:r ] = d / k
1077
- res[:index] = i
1078
- res[:type ] = :standard
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
- i += 1
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
- res
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
- vals = sched.timeSeries.values
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?(:to_s)
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 = "TBD::#{__callee__}"
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?(cl1)
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 if pts.is_a?(cl2)
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
- return mismatch("point", pt, cl4, mth, DBG, v) unless pt.is_a?(cl1)
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.first : pair.last
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 if empty)
2779
- def getUniques(pts = nil, n = 0)
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
- return mismatch("n unique points", n, Integer, mth, DBG, v) unless ok
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.to_i
2790
- n = 0 unless n.abs < v.size
2791
- v = v[0..n] if n > 0
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 getSegments(pts = nil)
3095
+ def segments(pts = nil)
2807
3096
  mth = "OSut::#{__callee__}"
2808
3097
  vv = OpenStudio::Point3dVectorVector.new
2809
- pts = getUniques(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 getTriads(pts = nil, co = false)
3141
+ def triads(pts = nil, co = false)
2854
3142
  mth = "OSut::#{__callee__}"
2855
3143
  vv = OpenStudio::Point3dVectorVector.new
2856
- pts = getUniques(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( "point", p0, cl1, mth, DBG, false) unless p0.is_a?(cl1)
2909
- return mismatch("segment", sg, cl2, mth, DBG, false) unless segment?(sg)
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.first
2914
- b = sg.last
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 = ap.dot(abn)
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 : getSegments(sgs)
2945
- return empty("segments", mth, DBG, false) if sgs.empty?
2946
- return mismatch("point", p0, cl, mth, DBG, false) unless p0.is_a?(cl1)
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 getLineIntersection(s1 = [], s2 = [])
2962
- s1 = getSegments(s1)
2963
- s2 = getSegments(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[0]
2975
- a2 = s1[1]
2976
- b1 = s2[0]
2977
- b2 = s2[1]
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 pointAlongSegments?(a1, s2)
2987
- return a2 if pointAlongSegments?(a2, s2)
2988
- return b1 if pointAlongSegments?(b1, s1)
2989
- return b2 if pointAlongSegments?(b2, s1)
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 colinear.
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 pointAlongSegments?(p0, s2) && pointAlongSegments?(p0, s1)
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 = getSegments(l)
3053
- s = getSegments(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 getLineIntersection(l, segment) }
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 sequential non-collinear points in an OpenStudio 3D point vector.
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 if empty)
3151
- def getNonCollinears(pts = nil, n = 0)
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
- return mismatch("n non-collinears", n, Integer, mth, DBG, v) unless ok
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 = i2 - 1
3163
- i3 = i2 + 1
3164
- i3 = 0 if i3 == pts.size
3165
- p1 = pts[i1]
3166
- p3 = pts[i3]
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 holds?(a, pts[0])
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.to_i
3179
- a = a[0..n-1] if n > 0
3180
- a = a[n-1..-1] if n < 0
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 sequential collinear points in an OpenStudio 3D point vector.
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 if empty)
3192
- def getCollinears(pts = nil, n = 0)
3483
+ # @return [OpenStudio::Point3dVector] collinears (see logs)
3484
+ def collinears(pts = nil, n = 0)
3193
3485
  mth = "OSut::#{__callee__}"
3194
- pts = getUniques(pts)
3195
- ok = n.respond_to?(:to_i)
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
- ncolls = getNonCollinears(pts)
3201
- return pts if ncolls.empty?
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
- to_p3Dv( pts.delete_if { |pt| holds?(ncolls, pt) } )
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 = getNonCollinears(pts, 3)
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 = getUniques(a).to_a if uq
3272
- a = getNonCollinears(a).to_a if co
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
- getTriads(a).each do |trio|
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
- segments = getSegments(s)
3650
+ sgments = segments(s)
3348
3651
 
3349
3652
  # Along polygon edges, or near vertices?
3350
- if pointAlongSegments?(p0, segments)
3653
+ if pointAlongSegments?(p0, sgments)
3351
3654
  return false if entirely
3352
3655
  return true unless entirely
3353
3656
  end
3354
3657
 
3355
- segments.each do |segment|
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(segment.first, segment.last)
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(segment.last - segment.first).length.round(4) < TOL2
3671
+ next if mpV.cross(sgment.last - sgment.first).length.round(4) < TOL2
3369
3672
 
3370
- segments.each do |sg|
3371
- intersect = getLineIntersection([p0, p1], sg)
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
- getSegments(pts).each do |pt|
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
- getSegments(p1).each do |sg|
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 ULC box (see logs if empty)
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 = getNonCollinears(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 = getNonCollinears(box, 4)
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 = getSegments(pts)
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 = getNonCollinears(box).to_a
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
- getSegments(pts).each do |sg|
4466
+ segments(pts).each do |sg|
4169
4467
  m0 = midpoint(sg.first, sg.last)
4170
4468
 
4171
- getSegments(pts).each do |seg|
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.first)
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
- getSegments(pts).each do |sg|
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
- getSegments(pts).each do |sg|
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
- getTriads(pts).each do |sg|
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
- getSegments(triangle).each do |sg|
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 getRealignedFace(pts = nil, force = false)
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
- segments = getSegments(box)
4376
- return invalid("bounded box segments", mth, 0, DBG, out) if segments.empty?
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
- segments.each_with_index do |sg, idx|
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 < segments.size ? i + 2 : i - 2
4697
+ k = i + 2 < sgments.size ? i + 2 : i - 2
4400
4698
 
4401
- origin = midpoint(segments[i][0], segments[i][1])
4402
- terminal = midpoint(segments[k][0], segments[k][1])
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 al©ong X-axis, once re/aligned
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 = getRealignedFace(pts, force)[:set]
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 = getRealignedFace(pts, force)[:set]
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 [Symbol] tag sequence of subset vertices to target
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) if pts.empty?
4516
- return mismatch("set", set, Array, mth, DBG, n) unless set.respond_to?(:to_a)
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.to_s}"
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 = getRealignedFace(tpts, true)
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
- getSegments(pts).each do |sg|
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
- getSegments(ost).each { |sg| nb += 1 if lineIntersects?(ld, sg) }
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
- getSegments(tpts).each do |sg|
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 [Symbol] tag sequence of set vertices to target
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, Array, mth, DBG, a) unless set.respond_to?(:to_a)
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.to_s}"
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(str, ld, Hash, mth, DBG, a) unless ld.is_a?(Hash)
4700
- return hashkey( str, ld, s, mth, DBG, a) unless ld.key?(s)
4701
- return mismatch(str, ld[s], cl, mth, DBG, a) unless ld[s].is_a?(cl)
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 [#to_s] OpenStudio outside boundary condition
4980
- # @param type [#to_s] OpenStudio surface (or subsurface) 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, cl1, mth, DBG, slb) unless plt.is_a?(cl2)
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.zero?
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 getRoofs(spaces = [])
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
- roofs = []
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
- roofs = facets(spaces, "Outdoors", "RoofCeiling")
5176
- roofs = roofs.select { |roof| roof?(roof) }
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
- roofs << ruf unless roofs.include?(ruf)
5587
+ rfs << ruf unless rfs.include?(ruf)
5213
5588
  end
5214
5589
  end
5215
5590
  end
5216
5591
 
5217
- roofs
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
- roofs = toplit ? facets(space, "Outdoors", "RoofCeiling") : []
5618
+ rufs = toplit ? facets(space, "Outdoors", "RoofCeiling") : []
5244
5619
  floors = baselit ? facets(space, "Outdoors", "Floor") : []
5245
5620
 
5246
- (walls + roofs + floors).each do |surface|
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 [#to_s] :type ("FixedWindow") OpenStudio subsurface type
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 = getRealignedFace(box, true)
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 = getRealignedFace(s0, false)
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 roofs [Array<OpenStudio::Model::Surface>] target surfaces
6391
+ # @param rfs [Array<OpenStudio::Model::Surface>] target surfaces
6017
6392
  #
6018
6393
  # @return [Array] horizontal ridges (see logs if empty)
6019
- def getHorizontalRidges(roofs = [])
6394
+ def horizontalRidges(rfs = [])
6020
6395
  mth = "OSut::#{__callee__}"
6021
6396
  ridges = []
6022
- return ridges unless roofs.is_a?(Array)
6397
+ return ridges unless rfs.is_a?(Array)
6023
6398
 
6024
- roofs = roofs.select { |s| s.is_a?(OpenStudio::Model::Surface) }
6025
- roofs = roofs.select { |s| sloped?(s) }
6399
+ rfs = rfs.select { |s| s.is_a?(OpenStudio::Model::Surface) }
6400
+ rfs = rfs.select { |s| sloped?(s) }
6026
6401
 
6027
- roofs.each do |roof|
6028
- maxZ = roof.vertices.max_by(&:z).z
6029
- next if roof.space.empty?
6402
+ rfs.each do |rf|
6403
+ maxZ = rf.vertices.max_by(&:z).z
6404
+ next if rf.space.empty?
6030
6405
 
6031
- space = roof.space.get
6406
+ space = rf.space.get
6032
6407
 
6033
- getSegments(roof).each do |edge|
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: [roof] }
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
- roofs.each do |ruf|
6428
+ rfs.each do |ruf|
6054
6429
  break if match
6055
- next if ruf == roof
6430
+ next if ruf == rf
6056
6431
  next if ruf.space.empty?
6057
6432
  next unless ruf.space.get == space
6058
6433
 
6059
- getSegments(ruf).each do |edg|
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| getRoofs(sp).empty? }
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, 0) if spaces.empty?
6504
+ return empty("spaces", mth, WRN, []) if spaces.empty?
6130
6505
  else
6131
- return mismatch("spaces", spaces, Array, mth, DBG, 0)
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
- getRoofs(sp).each do |rf|
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<#to_s>] :patterns requested skylight allocation (3x)
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 f.frameWidth.round(2) < 0
6364
- frame = nil if f.frameWidth.round(2) > gap
6739
+ frame = nil if frame.frameWidth.round(2) < 0
6740
+ frame = nil if frame.frameWidth.round(2) > gap
6365
6741
 
6366
- f = f.frameWidth if frame
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
- getRoofs(spaces).each { |s| s.subSurfaces.map(&:remove) } if clear
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! { |f| f.include?("b") ? f.delete("b") : f }
6592
- when :sloped then filters.map! { |f| f.include?("c") ? f.delete("c") : f }
6593
- when :plenum then filters.map! { |f| f.include?("d") ? f.delete("d") : f }
6594
- when :attic then filters.map! { |f| f.include?("e") ? f.delete("e") : f }
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! { |f| f.empty? }
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 = getRoofs(space) - room[:roofs]
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] = getHorizontalRidges(attic[:roofs]) # @todo
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] = getHorizontalRidges(plenum[:roofs]) # @todo
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, i|
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 = getSegments(t0 * roof.vertices)
7678
- sX = getSegments(t0 * vX)
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.to_a
7682
- sgX = sX[j].to_a
8057
+ sg0 = sg
8058
+ sgX = sX[j]
7683
8059
  vec = OpenStudio::Point3dVector.new
7684
8060
  vec << sg0.first
7685
8061
  vec << sg0.last