tbd 3.4.5 → 3.5.1

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-2025, Denis Bourgeois
3
+ # Copyright (c) 2022-2026, 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#{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,20 +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
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 zero("#{id} Uo", mth, ERR) if u.round(2) == 0.00
230
- 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
231
626
  end
232
627
 
233
628
  # Optional specs. Log/reset if invalid.
@@ -256,14 +651,14 @@ module OSut
256
651
  d = 0.015
257
652
  a[:compo][:mat] = @@mats[mt]
258
653
  a[:compo][:d ] = d
259
- a[:compo][:id ] = "OSut|#{mt}|#{format('%03d', d*1000)[-3..-1]}"
654
+ a[:compo][:id ] = "OSut:#{mt}:#{format('%03d', d*1000)[-3..-1]}"
260
655
  when :partition
261
656
  unless specs[:clad] == :none
262
657
  d = 0.015
263
658
  mt = :drywall
264
659
  a[:clad][:mat] = @@mats[mt]
265
660
  a[:clad][:d ] = d
266
- a[:clad][:id ] = "OSut|#{mt}|#{format('%03d', d*1000)[-3..-1]}"
661
+ a[:clad][:id ] = "OSut:#{mt}:#{format('%03d', d*1000)[-3..-1]}"
267
662
  end
268
663
 
269
664
  d = 0.015
@@ -275,14 +670,14 @@ module OSut
275
670
  mt = :mineral if u
276
671
  a[:compo][:mat] = @@mats[mt]
277
672
  a[:compo][:d ] = d
278
- a[:compo][:id ] = "OSut|#{mt}|#{format('%03d', d*1000)[-3..-1]}"
673
+ a[:compo][:id ] = "OSut:#{mt}:#{format('%03d', d*1000)[-3..-1]}"
279
674
 
280
675
  unless specs[:finish] == :none
281
676
  d = 0.015
282
677
  mt = :drywall
283
678
  a[:finish][:mat] = @@mats[mt]
284
679
  a[:finish][:d ] = d
285
- a[:finish][:id ] = "OSut|#{mt}|#{format('%03d', d*1000)[-3..-1]}"
680
+ a[:finish][:id ] = "OSut:#{mt}:#{format('%03d', d*1000)[-3..-1]}"
286
681
  end
287
682
  when :wall
288
683
  unless specs[:clad] == :none
@@ -293,7 +688,7 @@ module OSut
293
688
  d = 0.015 if specs[:clad] == :light
294
689
  a[:clad][:mat] = @@mats[mt]
295
690
  a[:clad][:d ] = d
296
- a[:clad][:id ] = "OSut|#{mt}|#{format('%03d', d*1000)[-3..-1]}"
691
+ a[:clad][:id ] = "OSut:#{mt}:#{format('%03d', d*1000)[-3..-1]}"
297
692
  end
298
693
 
299
694
  mt = :drywall
@@ -303,7 +698,7 @@ module OSut
303
698
  d = 0.015 if specs[:frame] == :light
304
699
  a[:sheath][:mat] = @@mats[mt]
305
700
  a[:sheath][:d ] = d
306
- a[:sheath][:id ] = "OSut|#{mt}|#{format('%03d', d*1000)[-3..-1]}"
701
+ a[:sheath][:id ] = "OSut:#{mt}:#{format('%03d', d*1000)[-3..-1]}"
307
702
 
308
703
  mt = :mineral
309
704
  mt = :cellulose if specs[:frame] == :medium
@@ -315,7 +710,7 @@ module OSut
315
710
 
316
711
  a[:compo][:mat] = @@mats[mt]
317
712
  a[:compo][:d ] = d
318
- a[:compo][:id ] = "OSut|#{mt}|#{format('%03d', d*1000)[-3..-1]}"
713
+ a[:compo][:id ] = "OSut:#{mt}:#{format('%03d', d*1000)[-3..-1]}"
319
714
 
320
715
  unless specs[:finish] == :none
321
716
  mt = :concrete
@@ -325,7 +720,7 @@ module OSut
325
720
  d = 0.200 if specs[:finish] == :heavy
326
721
  a[:finish][:mat] = @@mats[mt]
327
722
  a[:finish][:d ] = d
328
- a[:finish][:id ] = "OSut|#{mt}|#{format('%03d', d*1000)[-3..-1]}"
723
+ a[:finish][:id ] = "OSut:#{mt}:#{format('%03d', d*1000)[-3..-1]}"
329
724
  end
330
725
  when :roof
331
726
  unless specs[:clad] == :none
@@ -336,7 +731,7 @@ module OSut
336
731
  d = 0.200 if specs[:clad] == :heavy # e.g. parking garage
337
732
  a[:clad][:mat] = @@mats[mt]
338
733
  a[:clad][:d ] = d
339
- a[:clad][:id ] = "OSut|#{mt}|#{format('%03d', d*1000)[-3..-1]}"
734
+ a[:clad][:id ] = "OSut:#{mt}:#{format('%03d', d*1000)[-3..-1]}"
340
735
  end
341
736
 
342
737
  mt = :mineral
@@ -347,7 +742,7 @@ module OSut
347
742
  d = 0.015 unless u
348
743
  a[:compo][:mat] = @@mats[mt]
349
744
  a[:compo][:d ] = d
350
- a[:compo][:id ] = "OSut|#{mt}|#{format('%03d', d*1000)[-3..-1]}"
745
+ a[:compo][:id ] = "OSut:#{mt}:#{format('%03d', d*1000)[-3..-1]}"
351
746
 
352
747
  unless specs[:finish] == :none
353
748
  mt = :concrete
@@ -357,7 +752,7 @@ module OSut
357
752
  d = 0.200 if specs[:finish] == :heavy
358
753
  a[:finish][:mat] = @@mats[mt]
359
754
  a[:finish][:d ] = d
360
- a[:finish][:id ] = "OSut|#{mt}|#{format('%03d', d*1000)[-3..-1]}"
755
+ a[:finish][:id ] = "OSut:#{mt}:#{format('%03d', d*1000)[-3..-1]}"
361
756
  end
362
757
  when :floor
363
758
  unless specs[:clad] == :none
@@ -365,7 +760,7 @@ module OSut
365
760
  d = 0.015
366
761
  a[:clad][:mat] = @@mats[mt]
367
762
  a[:clad][:d ] = d
368
- a[:clad][:id ] = "OSut|#{mt}|#{format('%03d', d*1000)[-3..-1]}"
763
+ a[:clad][:id ] = "OSut:#{mt}:#{format('%03d', d*1000)[-3..-1]}"
369
764
  end
370
765
 
371
766
  mt = :mineral
@@ -376,7 +771,7 @@ module OSut
376
771
  d = 0.015 unless u
377
772
  a[:compo][:mat] = @@mats[mt]
378
773
  a[:compo][:d ] = d
379
- a[:compo][:id ] = "OSut|#{mt}|#{format('%03d', d*1000)[-3..-1]}"
774
+ a[:compo][:id ] = "OSut:#{mt}:#{format('%03d', d*1000)[-3..-1]}"
380
775
 
381
776
  unless specs[:finish] == :none
382
777
  mt = :concrete
@@ -386,21 +781,21 @@ module OSut
386
781
  d = 0.200 if specs[:finish] == :heavy
387
782
  a[:finish][:mat] = @@mats[mt]
388
783
  a[:finish][:d ] = d
389
- a[:finish][:id ] = "OSut|#{mt}|#{format('%03d', d*1000)[-3..-1]}"
784
+ a[:finish][:id ] = "OSut:#{mt}:#{format('%03d', d*1000)[-3..-1]}"
390
785
  end
391
786
  when :slab
392
787
  mt = :sand
393
788
  d = 0.100
394
789
  a[:clad][:mat] = @@mats[mt]
395
790
  a[:clad][:d ] = d
396
- a[:clad][:id ] = "OSut|#{mt}|#{format('%03d', d*1000)[-3..-1]}"
791
+ a[:clad][:id ] = "OSut:#{mt}:#{format('%03d', d*1000)[-3..-1]}"
397
792
 
398
793
  unless specs[:frame] == :none
399
794
  mt = :polyiso
400
795
  d = 0.025
401
796
  a[:sheath][:mat] = @@mats[mt]
402
797
  a[:sheath][:d ] = d
403
- a[:sheath][:id ] = "OSut|#{mt}|#{format('%03d', d*1000)[-3..-1]}"
798
+ a[:sheath][:id ] = "OSut:#{mt}:#{format('%03d', d*1000)[-3..-1]}"
404
799
  end
405
800
 
406
801
  mt = :concrete
@@ -408,14 +803,14 @@ module OSut
408
803
  d = 0.200 if specs[:frame] == :heavy
409
804
  a[:compo][:mat] = @@mats[mt]
410
805
  a[:compo][:d ] = d
411
- a[:compo][:id ] = "OSut|#{mt}|#{format('%03d', d*1000)[-3..-1]}"
806
+ a[:compo][:id ] = "OSut:#{mt}:#{format('%03d', d*1000)[-3..-1]}"
412
807
 
413
808
  unless specs[:finish] == :none
414
809
  mt = :material
415
810
  d = 0.015
416
811
  a[:finish][:mat] = @@mats[mt]
417
812
  a[:finish][:d ] = d
418
- a[:finish][:id ] = "OSut|#{mt}|#{format('%03d', d*1000)[-3..-1]}"
813
+ a[:finish][:id ] = "OSut:#{mt}:#{format('%03d', d*1000)[-3..-1]}"
419
814
  end
420
815
  when :basement
421
816
  unless specs[:clad] == :none
@@ -425,38 +820,38 @@ module OSut
425
820
  d = 0.015 if specs[:clad] == :light
426
821
  a[:clad][:mat] = @@mats[mt]
427
822
  a[:clad][:d ] = d
428
- a[:clad][:id ] = "OSut|#{mt}|#{format('%03d', d*1000)[-3..-1]}"
823
+ a[:clad][:id ] = "OSut:#{mt}:#{format('%03d', d*1000)[-3..-1]}"
429
824
 
430
825
  mt = :polyiso
431
826
  d = 0.025
432
827
  a[:sheath][:mat] = @@mats[mt]
433
828
  a[:sheath][:d ] = d
434
- a[:sheath][:id ] = "OSut|#{mt}|#{format('%03d', d*1000)[-3..-1]}"
829
+ a[:sheath][:id ] = "OSut:#{mt}:#{format('%03d', d*1000)[-3..-1]}"
435
830
 
436
831
  mt = :concrete
437
832
  d = 0.200
438
833
  a[:compo][:mat] = @@mats[mt]
439
834
  a[:compo][:d ] = d
440
- a[:compo][:id ] = "OSut|#{mt}|#{format('%03d', d*1000)[-3..-1]}"
835
+ a[:compo][:id ] = "OSut:#{mt}:#{format('%03d', d*1000)[-3..-1]}"
441
836
  else
442
837
  mt = :concrete
443
838
  d = 0.200
444
839
  a[:sheath][:mat] = @@mats[mt]
445
840
  a[:sheath][:d ] = d
446
- a[:sheath][:id ] = "OSut|#{mt}|#{format('%03d', d*1000)[-3..-1]}"
841
+ a[:sheath][:id ] = "OSut:#{mt}:#{format('%03d', d*1000)[-3..-1]}"
447
842
 
448
843
  unless specs[:finish] == :none
449
844
  mt = :mineral
450
845
  d = 0.075
451
846
  a[:compo][:mat] = @@mats[mt]
452
847
  a[:compo][:d ] = d
453
- a[:compo][:id ] = "OSut|#{mt}|#{format('%03d', d*1000)[-3..-1]}"
848
+ a[:compo][:id ] = "OSut:#{mt}:#{format('%03d', d*1000)[-3..-1]}"
454
849
 
455
850
  mt = :drywall
456
851
  d = 0.015
457
852
  a[:finish][:mat] = @@mats[mt]
458
853
  a[:finish][:d ] = d
459
- a[:finish][:id ] = "OSut|#{mt}|#{format('%03d', d*1000)[-3..-1]}"
854
+ a[:finish][:id ] = "OSut:#{mt}:#{format('%03d', d*1000)[-3..-1]}"
460
855
  end
461
856
  end
462
857
  when :door
@@ -465,27 +860,25 @@ module OSut
465
860
 
466
861
  a[:compo ][:mat ] = @@mats[mt]
467
862
  a[:compo ][:d ] = d
468
- a[:compo ][:id ] = "OSut|#{mt}|#{format('%03d', d*1000)[-3..-1]}"
863
+ a[:compo ][:id ] = "OSut:#{mt}:#{format('%03d', d*1000)[-3..-1]}"
469
864
  when :window
470
865
  a[:glazing][:u ] = u ? u : @@uo[:window]
471
866
  a[:glazing][:shgc] = 0.450
472
867
  a[:glazing][:shgc] = specs[:shgc] if specs.key?(:shgc)
473
- a[:glazing][:id ] = "OSut|window"
474
- a[:glazing][:id ] += "|U#{format('%.1f', a[:glazing][:u])}"
475
- 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)}"
476
871
  when :skylight
477
872
  a[:glazing][:u ] = u ? u : @@uo[:skylight]
478
873
  a[:glazing][:shgc] = 0.450
479
874
  a[:glazing][:shgc] = specs[:shgc] if specs.key?(:shgc)
480
- a[:glazing][:id ] = "OSut|skylight"
481
- a[:glazing][:id ] += "|U#{format('%.1f', a[:glazing][:u])}"
482
- 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)}"
483
878
  end
484
879
 
485
880
  # Initiate layers.
486
- unglazed = a[:glazing].empty? ? true : false
487
-
488
- if unglazed
881
+ if a[:glazing].empty?
489
882
  layers = OpenStudio::Model::OpaqueMaterialVector.new
490
883
 
491
884
  # Loop through each layer spec, and generate construction.
@@ -528,14 +921,14 @@ module OSut
528
921
  layers << lyr
529
922
  end
530
923
 
531
- c = OpenStudio::Model::Construction.new(layers)
924
+ c = OpenStudio::Model::Construction.new(layers)
532
925
  c.setName(id)
533
926
 
534
- # Adjust insulating layer thickness or conductivity to match requested Uo.
535
- if u and unglazed
927
+ # Adjust insulating layer conductivity (maybe thickness) to match Uo.
928
+ if u and a[:glazing].empty?
536
929
  ro = 1 / u - film
537
930
 
538
- if ro > 0
931
+ if ro > RMIN
539
932
  if specs[:type] == :door # 1x layer, adjust conductivity
540
933
  layer = c.getLayer(0).to_StandardOpaqueMaterial
541
934
  return invalid("#{id} standard material?", mth, 0) if layer.empty?
@@ -543,35 +936,14 @@ module OSut
543
936
  layer = layer.get
544
937
  k = layer.thickness / ro
545
938
  layer.setConductivity(k)
546
- else # multiple layers, adjust insulating layer thickness
939
+ else # multiple layers, adjust layer conductivity, then thickness
547
940
  lyr = insulatingLayer(c)
548
941
  return invalid("#{id} construction", mth, 0) if lyr[:index].nil?
549
942
  return invalid("#{id} construction", mth, 0) if lyr[:type ].nil?
550
- return invalid("#{id} construction", mth, 0) if lyr[:r ].zero?
943
+ return invalid("#{id} construction", mth, 0) if lyr[:r ].to_i.zero?
551
944
 
552
945
  index = lyr[:index]
553
- layer = c.getLayer(index).to_StandardOpaqueMaterial
554
- return invalid("#{id} material @#{index}", mth, 0) if layer.empty?
555
-
556
- layer = layer.get
557
- k = layer.conductivity
558
- d = (ro - rsi(c) + lyr[:r]) * k
559
- return invalid("#{id} adjusted m", mth, 0) if d < 0.03
560
-
561
- nom = "OSut|"
562
- nom += layer.nameString.gsub(/[^a-z]/i, "").gsub("OSut", "")
563
- nom += "|"
564
- nom += format("%03d", d*1000)[-3..-1]
565
-
566
- lyr = model.getStandardOpaqueMaterialByName(nom)
567
-
568
- if lyr.empty?
569
- layer.setName(nom)
570
- layer.setThickness(d)
571
- else
572
- omat = lyr.get
573
- c.setLayer(index, omat)
574
- end
946
+ resetUo(c, film, index, u)
575
947
  end
576
948
  end
577
949
  end
@@ -619,20 +991,20 @@ module OSut
619
991
  end
620
992
 
621
993
  # Shading schedule.
622
- id = "OSut|SHADE|Ruleset"
994
+ id = "OSut:SHADE:Ruleset"
623
995
  sch = mdl.getScheduleRulesetByName(id)
624
996
 
625
997
  if sch.empty?
626
998
  sch = OpenStudio::Model::ScheduleRuleset.new(mdl, 0)
627
999
  sch.setName(id)
628
1000
  sch.setScheduleTypeLimits(onoff)
629
- sch.defaultDaySchedule.setName("OSut|Shade|Ruleset|Default")
1001
+ sch.defaultDaySchedule.setName("OSut:Shade:Ruleset:Default")
630
1002
  else
631
1003
  sch = sch.get
632
1004
  end
633
1005
 
634
1006
  # Summer cooling rule.
635
- id = "OSut|SHADE|ScheduleRule"
1007
+ id = "OSut:SHADE:ScheduleRule"
636
1008
  rule = mdl.getScheduleRuleByName(id)
637
1009
 
638
1010
  if rule.empty?
@@ -646,14 +1018,14 @@ module OSut
646
1018
  rule.setStartDate(start)
647
1019
  rule.setEndDate(finish)
648
1020
  rule.setApplyAllDays(true)
649
- rule.daySchedule.setName("OSut|Shade|Rule|Default")
1021
+ rule.daySchedule.setName("OSut:Shade:Rule:Default")
650
1022
  rule.daySchedule.addValue(OpenStudio::Time.new(0,24,0,0), 1)
651
1023
  else
652
1024
  rule = rule.get
653
1025
  end
654
1026
 
655
1027
  # Shade object.
656
- id = "OSut|Shade"
1028
+ id = "OSut:Shade"
657
1029
  shd = mdl.getShadeByName(id)
658
1030
 
659
1031
  if shd.empty?
@@ -664,7 +1036,7 @@ module OSut
664
1036
  end
665
1037
 
666
1038
  # Shading control (unique to each call).
667
- id = "OSut|ShadingControl"
1039
+ id = "OSut:ShadingControl"
668
1040
  ctl = OpenStudio::Model::ShadingControl.new(shd)
669
1041
  ctl.setName(id)
670
1042
  ctl.setSchedule(sch)
@@ -700,7 +1072,7 @@ module OSut
700
1072
 
701
1073
  # A single material.
702
1074
  mdl = sps.first.model
703
- id = "OSut|MASS|Material"
1075
+ id = "OSut:MASS:Material"
704
1076
  mat = mdl.getOpaqueMaterialByName(id)
705
1077
 
706
1078
  if mat.empty?
@@ -719,7 +1091,7 @@ module OSut
719
1091
  end
720
1092
 
721
1093
  # A single, 1x layered construction.
722
- id = "OSut|MASS|Construction"
1094
+ id = "OSut:MASS:Construction"
723
1095
  con = mdl.getConstructionByName(id)
724
1096
 
725
1097
  if con.empty?
@@ -732,7 +1104,7 @@ module OSut
732
1104
  con = con.get
733
1105
  end
734
1106
 
735
- id = "OSut|InternalMassDefinition|" + (format "%.2f", ratio)
1107
+ id = "OSut:InternalMassDefinition:" + (format "%.2f", ratio)
736
1108
  df = mdl.getInternalMassDefinitionByName(id)
737
1109
 
738
1110
  if df.empty?
@@ -746,7 +1118,7 @@ module OSut
746
1118
 
747
1119
  sps.each do |sp|
748
1120
  mass = OpenStudio::Model::InternalMass.new(df)
749
- mass.setName("OSut|InternalMass|#{sp.nameString}")
1121
+ mass.setName("OSut:InternalMass:#{sp.nameString}")
750
1122
  mass.setSpace(sp)
751
1123
  end
752
1124
 
@@ -754,13 +1126,13 @@ module OSut
754
1126
  end
755
1127
 
756
1128
  ##
757
- # Validates if a default construction set holds a base construction.
1129
+ # Validates if a default construction set holds an opaque base construction.
758
1130
  #
759
1131
  # @param set [OpenStudio::Model::DefaultConstructionSet] a default set
760
- # @param bse [OpenStudio::Model::ConstructionBase] a construction base
1132
+ # @param bse [OpenStudio::Model::ConstructionBase] an opaque construction base
761
1133
  # @param gr [Bool] if ground-facing surface
762
1134
  # @param ex [Bool] if exterior-facing surface
763
- # @param tp [#to_s] a surface type
1135
+ # @param tp [#to_sym] surface type: "floor", "wall" or "roofceiling"
764
1136
  #
765
1137
  # @return [Bool] whether default set holds construction
766
1138
  # @return [false] if invalid input (see logs)
@@ -779,7 +1151,7 @@ module OSut
779
1151
  ck2 = bse.is_a?(cl2)
780
1152
  ck3 = [true, false].include?(gr)
781
1153
  ck4 = [true, false].include?(ex)
782
- ck5 = tp.respond_to?(:to_s)
1154
+ ck5 = tp.respond_to?(:to_sym)
783
1155
  return mismatch(id1, set, cl1, mth, DBG, false) unless ck1
784
1156
  return mismatch(id2, bse, cl2, mth, DBG, false) unless ck2
785
1157
  return invalid("ground" , mth, 3, DBG, false) unless ck3
@@ -859,6 +1231,9 @@ module OSut
859
1231
  type = s.surfaceType
860
1232
  ground = false
861
1233
  exterior = false
1234
+ adjacent = s.adjacentSurface.empty? ? nil : s.adjacentSurface.get
1235
+ aspace = adjacent.nil? || adjacent.space.empty? ? nil : adjacent.space.get
1236
+ typ = adjacent.nil? ? nil : adjacent.surfaceType
862
1237
 
863
1238
  if s.isGroundSurface
864
1239
  ground = true
@@ -866,7 +1241,14 @@ module OSut
866
1241
  exterior = true
867
1242
  end
868
1243
 
869
- unless space.defaultConstructionSet.empty?
1244
+ if space.defaultConstructionSet.empty?
1245
+ unless aspace.nil?
1246
+ unless aspace.defaultConstructionSet.empty?
1247
+ set = aspace.defaultConstructionSet.get
1248
+ return set if holdsConstruction?(set, base, ground, exterior, typ)
1249
+ end
1250
+ end
1251
+ else
870
1252
  set = space.defaultConstructionSet.get
871
1253
  return set if holdsConstruction?(set, base, ground, exterior, type)
872
1254
  end
@@ -880,6 +1262,17 @@ module OSut
880
1262
  end
881
1263
  end
882
1264
 
1265
+ unless aspace.nil? || aspace.spaceType.empty?
1266
+ unless aspace.spaceType.empty?
1267
+ spacetype = aspace.spaceType.get
1268
+
1269
+ unless spacetype.defaultConstructionSet.empty?
1270
+ set = spacetype.defaultConstructionSet.get
1271
+ return set if holdsConstruction?(set, base, ground, exterior, typ)
1272
+ end
1273
+ end
1274
+ end
1275
+
883
1276
  unless space.buildingStory.empty?
884
1277
  story = space.buildingStory.get
885
1278
 
@@ -889,6 +1282,15 @@ module OSut
889
1282
  end
890
1283
  end
891
1284
 
1285
+ unless aspace.nil? || aspace.buildingStory.empty?
1286
+ story = aspace.buildingStory.get
1287
+
1288
+ unless spacetype.defaultConstructionSet.empty?
1289
+ set = spacetype.defaultConstructionSet.get
1290
+ return set if holdsConstruction?(set, base, ground, exterior, typ)
1291
+ end
1292
+ end
1293
+
892
1294
  building = mdl.getBuilding
893
1295
 
894
1296
  unless building.defaultConstructionSet.empty?
@@ -899,203 +1301,6 @@ module OSut
899
1301
  nil
900
1302
  end
901
1303
 
902
- ##
903
- # Validates if every material in a layered construction is standard & opaque.
904
- #
905
- # @param lc [OpenStudio::LayeredConstruction] a layered construction
906
- #
907
- # @return [Bool] whether all layers are valid
908
- # @return [false] if invalid input (see logs)
909
- def standardOpaqueLayers?(lc = nil)
910
- mth = "OSut::#{__callee__}"
911
- cl = OpenStudio::Model::LayeredConstruction
912
- return invalid("lc", mth, 1, DBG, false) unless lc.respond_to?(NS)
913
- return mismatch(lc.nameString, lc, cl, mth, DBG, false) unless lc.is_a?(cl)
914
-
915
- lc.layers.each { |m| return false if m.to_StandardOpaqueMaterial.empty? }
916
-
917
- true
918
- end
919
-
920
- ##
921
- # Returns total (standard opaque) layered construction thickness (m).
922
- #
923
- # @param lc [OpenStudio::LayeredConstruction] a layered construction
924
- #
925
- # @return [Float] construction thickness
926
- # @return [0.0] if invalid input (see logs)
927
- def thickness(lc = nil)
928
- mth = "OSut::#{__callee__}"
929
- cl = OpenStudio::Model::LayeredConstruction
930
- return invalid("lc", mth, 1, DBG, 0.0) unless lc.respond_to?(NS)
931
-
932
- id = lc.nameString
933
- return mismatch(id, lc, cl, mth, DBG, 0.0) unless lc.is_a?(cl)
934
-
935
- ok = standardOpaqueLayers?(lc)
936
- log(ERR, "'#{id}' holds non-StandardOpaqueMaterial(s) (#{mth})") unless ok
937
- return 0.0 unless ok
938
-
939
- thickness = 0.0
940
- lc.layers.each { |m| thickness += m.thickness }
941
-
942
- thickness
943
- end
944
-
945
- ##
946
- # Returns total air film resistance of a fenestrated construction (m2•K/W)
947
- #
948
- # @param usi [Numeric] a fenestrated construction's U-factor (W/m2•K)
949
- #
950
- # @return [Float] total air film resistances
951
- # @return [0.1216] if invalid input (see logs)
952
- def glazingAirFilmRSi(usi = 5.85)
953
- # The sum of thermal resistances of calculated exterior and interior film
954
- # coefficients under standard winter conditions are taken from:
955
- #
956
- # https://bigladdersoftware.com/epx/docs/9-6/engineering-reference/
957
- # window-calculation-module.html#simple-window-model
958
- #
959
- # These remain acceptable approximations for flat windows, yet likely
960
- # unsuitable for subsurfaces with curved or projecting shapes like domed
961
- # skylights. The solution here is considered an adequate fix for reporting,
962
- # awaiting eventual OpenStudio (and EnergyPlus) upgrades to report NFRC 100
963
- # (or ISO) air film resistances under standard winter conditions.
964
- #
965
- # For U-factors above 8.0 W/m2•K (or invalid input), the function returns
966
- # 0.1216 m2•K/W, which corresponds to a construction with a single glass
967
- # layer thickness of 2mm & k = ~0.6 W/m.K.
968
- #
969
- # The EnergyPlus Engineering calculations were designed for vertical
970
- # windows - not horizontal, slanted or domed surfaces - use with caution.
971
- mth = "OSut::#{__callee__}"
972
- cl = Numeric
973
- return mismatch("usi", usi, cl, mth, DBG, 0.1216) unless usi.is_a?(cl)
974
- return invalid("usi", mth, 1, WRN, 0.1216) if usi > 8.0
975
- return negative("usi", mth, WRN, 0.1216) if usi < 0
976
- return zero("usi", mth, WRN, 0.1216) if usi.abs < TOL
977
-
978
- rsi = 1 / (0.025342 * usi + 29.163853) # exterior film, next interior film
979
- return rsi + 1 / (0.359073 * Math.log(usi) + 6.949915) if usi < 5.85
980
- return rsi + 1 / (1.788041 * usi - 2.886625)
981
- end
982
-
983
- ##
984
- # Returns a construction's 'standard calc' thermal resistance (m2•K/W), which
985
- # includes air film resistances. It excludes insulating effects of shades,
986
- # screens, etc. in the case of fenestrated constructions.
987
- #
988
- # @param lc [OpenStudio::Model::LayeredConstruction] a layered construction
989
- # @param film [Numeric] thermal resistance of surface air films (m2•K/W)
990
- # @param t [Numeric] gas temperature (°C) (optional)
991
- #
992
- # @return [Float] layered construction's thermal resistance
993
- # @return [0.0] if invalid input (see logs)
994
- def rsi(lc = nil, film = 0.0, t = 0.0)
995
- # This is adapted from BTAP's Material Module "get_conductance" (P. Lopez)
996
- #
997
- # https://github.com/NREL/OpenStudio-Prototype-Buildings/blob/
998
- # c3d5021d8b7aef43e560544699fb5c559e6b721d/lib/btap/measures/
999
- # btap_equest_converter/envelope.rb#L122
1000
- mth = "OSut::#{__callee__}"
1001
- cl1 = OpenStudio::Model::LayeredConstruction
1002
- cl2 = Numeric
1003
- return invalid("lc", mth, 1, DBG, 0.0) unless lc.respond_to?(NS)
1004
-
1005
- id = lc.nameString
1006
- return mismatch(id, lc, cl1, mth, DBG, 0.0) unless lc.is_a?(cl1)
1007
- return mismatch("film", film, cl2, mth, DBG, 0.0) unless film.is_a?(cl2)
1008
- return mismatch("temp K", t, cl2, mth, DBG, 0.0) unless t.is_a?(cl2)
1009
-
1010
- t += 273.0 # °C to K
1011
- return negative("temp K", mth, ERR, 0.0) if t < 0
1012
- return negative("film", mth, ERR, 0.0) if film < 0
1013
-
1014
- rsi = film
1015
-
1016
- lc.layers.each do |m|
1017
- # Fenestration materials first.
1018
- empty = m.to_SimpleGlazing.empty?
1019
- return 1 / m.to_SimpleGlazing.get.uFactor unless empty
1020
-
1021
- empty = m.to_StandardGlazing.empty?
1022
- rsi += m.to_StandardGlazing.get.thermalResistance unless empty
1023
- empty = m.to_RefractionExtinctionGlazing.empty?
1024
- rsi += m.to_RefractionExtinctionGlazing.get.thermalResistance unless empty
1025
- empty = m.to_Gas.empty?
1026
- rsi += m.to_Gas.get.getThermalResistance(t) unless empty
1027
- empty = m.to_GasMixture.empty?
1028
- rsi += m.to_GasMixture.get.getThermalResistance(t) unless empty
1029
-
1030
- # Opaque materials next.
1031
- empty = m.to_StandardOpaqueMaterial.empty?
1032
- rsi += m.to_StandardOpaqueMaterial.get.thermalResistance unless empty
1033
- empty = m.to_MasslessOpaqueMaterial.empty?
1034
- rsi += m.to_MasslessOpaqueMaterial.get.thermalResistance unless empty
1035
- empty = m.to_RoofVegetation.empty?
1036
- rsi += m.to_RoofVegetation.get.thermalResistance unless empty
1037
- empty = m.to_AirGap.empty?
1038
- rsi += m.to_AirGap.get.thermalResistance unless empty
1039
- end
1040
-
1041
- rsi
1042
- end
1043
-
1044
- ##
1045
- # Identifies a layered construction's (opaque) insulating layer. The method
1046
- # returns a 3-keyed hash :index, the insulating layer index [0, n layers)
1047
- # within the layered construction; :type, either :standard or :massless; and
1048
- # :r, material thermal resistance in m2•K/W.
1049
- #
1050
- # @param lc [OpenStudio::Model::LayeredConstruction] a layered construction
1051
- #
1052
- # @return [Hash] index: (Integer), type: (Symbol), r: (Float)
1053
- # @return [Hash] index: nil, type: nil, r: 0 if invalid input (see logs)
1054
- def insulatingLayer(lc = nil)
1055
- mth = "OSut::#{__callee__}"
1056
- cl = OpenStudio::Model::LayeredConstruction
1057
- res = { index: nil, type: nil, r: 0.0 }
1058
- i = 0 # iterator
1059
- return invalid("lc", mth, 1, DBG, res) unless lc.respond_to?(NS)
1060
-
1061
- id = lc.nameString
1062
- return mismatch(id, lc, cl, mth, DBG, res) unless lc.is_a?(cl)
1063
-
1064
- lc.layers.each do |m|
1065
- unless m.to_MasslessOpaqueMaterial.empty?
1066
- m = m.to_MasslessOpaqueMaterial.get
1067
-
1068
- if m.thermalResistance < 0.001 || m.thermalResistance < res[:r]
1069
- i += 1
1070
- next
1071
- else
1072
- res[:r ] = m.thermalResistance
1073
- res[:index] = i
1074
- res[:type ] = :massless
1075
- end
1076
- end
1077
-
1078
- unless m.to_StandardOpaqueMaterial.empty?
1079
- m = m.to_StandardOpaqueMaterial.get
1080
- k = m.thermalConductivity
1081
- d = m.thickness
1082
-
1083
- if d < 0.003 || k > 3.0 || d / k < res[:r]
1084
- i += 1
1085
- next
1086
- else
1087
- res[:r ] = d / k
1088
- res[:index] = i
1089
- res[:type ] = :standard
1090
- end
1091
- end
1092
-
1093
- i += 1
1094
- end
1095
-
1096
- res
1097
- end
1098
-
1099
1304
  ##
1100
1305
  # Validates whether opaque surface can be considered as a curtain wall (or
1101
1306
  # similar technology) spandrel, regardless of construction layers, by looking
@@ -2216,7 +2421,7 @@ module OSut
2216
2421
  cl = OpenStudio::Model::Model
2217
2422
  limits = nil
2218
2423
  return mismatch("model", model, cl, mth) unless model.is_a?(cl)
2219
- return invalid("availability", avl, 2, mth) unless avl.respond_to?(:to_s)
2424
+ return invalid("availability", avl, 2, mth) unless avl.respond_to?(:to_sym)
2220
2425
 
2221
2426
  # Either fetch availability ScheduleTypeLimits object, or create one.
2222
2427
  model.getScheduleTypeLimitss.each do |l|
@@ -4648,7 +4853,7 @@ module OSut
4648
4853
  #
4649
4854
  # @param s [Set<OpenStudio::Point3d>] a (larger) parent set of points
4650
4855
  # @param [Array<Hash>] set a collection of (smaller) sequenced points
4651
- # @option [Symbol] tag sequence of subset vertices to target
4856
+ # @option [#to_sym] tag sequence of subset vertices to target
4652
4857
  #
4653
4858
  # @return [Integer] number of successfully anchored subsets (see logs)
4654
4859
  def genAnchors(s = nil, set = [], tag = :box)
@@ -4656,18 +4861,20 @@ module OSut
4656
4861
  n = 0
4657
4862
  id = s.respond_to?(:nameString) ? "#{s.nameString}: " : ""
4658
4863
  pts = poly(s)
4659
- return invalid("#{id} polygon", mth, 1, DBG, n) if pts.empty?
4660
- return mismatch("set", set, Array, mth, DBG, n) unless set.respond_to?(:to_a)
4864
+ return invalid("#{id} polygon", mth, 1, DBG, n) if pts.empty?
4865
+ return mismatch("set", set, Array, mth, DBG, n) unless set.respond_to?(:to_a)
4866
+ return mismatch("tag", tag, Symbol, mth, DBG, n) unless tag.respond_to?(:to_sym)
4661
4867
 
4662
4868
  origin = OpenStudio::Point3d.new(0,0,0)
4663
4869
  zenith = OpenStudio::Point3d.new(0,0,1)
4664
4870
  ray = zenith - origin
4665
4871
  set = set.to_a
4872
+ tag = tag.to_sym
4666
4873
 
4667
4874
  # Validate individual subsets. Purge surface-specific leader line anchors.
4668
4875
  set.each_with_index do |st, i|
4669
4876
  str1 = id + "subset ##{i+1}"
4670
- str2 = str1 + " #{tag.to_s}"
4877
+ str2 = str1 + " #{trim(tag)}"
4671
4878
  return mismatch(str1, st, Hash, mth, DBG, n) unless st.respond_to?(:key?)
4672
4879
  return hashkey( str1, st, tag, mth, DBG, n) unless st.key?(tag)
4673
4880
  return empty("#{str2} vertices", mth, DBG, n) if st[tag].empty?
@@ -4810,7 +5017,7 @@ module OSut
4810
5017
  # @param s [Set<OpenStudio::Point3d>] a larger (parent) set of points
4811
5018
  # @param [Array<Hash>] set a collection of (smaller) sequenced vertices
4812
5019
  # @option set [Hash] :ld a polygon-specific leader line anchors
4813
- # @option [Symbol] tag sequence of set vertices to target
5020
+ # @option [#to_sym] tag sequence of set vertices to target
4814
5021
  #
4815
5022
  # @return [OpenStudio::Point3dVector] extended vertices (see logs if empty)
4816
5023
  def genExtendedVertices(s = nil, set = [], tag = :vtx)
@@ -4822,14 +5029,16 @@ module OSut
4822
5029
  a = OpenStudio::Point3dVector.new
4823
5030
  v = []
4824
5031
  return a if pts.empty?
4825
- return mismatch("set", set, Array, mth, DBG, a) unless set.respond_to?(:to_a)
5032
+ return mismatch("set", set, Array, mth, DBG, a) unless set.respond_to?(:to_a)
5033
+ return mismatch("tag", tag, Symbol, mth, DBG, n) unless tag.respond_to?(:to_sym)
4826
5034
 
4827
5035
  set = set.to_a
5036
+ tag = tag.to_sym
4828
5037
 
4829
5038
  # Validate individual sets.
4830
5039
  set.each_with_index do |st, i|
4831
5040
  str1 = id + "subset ##{i+1}"
4832
- str2 = str1 + " #{tag.to_s}"
5041
+ str2 = str1 + " #{trim(tag)}"
4833
5042
  return mismatch(str1, st, Hash, mth, DBG, a) unless st.respond_to?(:key?)
4834
5043
  next if st.key?(:void) && st[:void]
4835
5044
 
@@ -5121,8 +5330,8 @@ module OSut
5121
5330
  # surface type filters if 'type' argument == "all".
5122
5331
  #
5123
5332
  # @param spaces [Set<OpenStudio::Model::Space>] target spaces
5124
- # @param boundary [#to_s] OpenStudio outside boundary condition
5125
- # @param type [#to_s] OpenStudio surface (or subsurface) type
5333
+ # @param boundary [#to_sym] OpenStudio outside boundary condition
5334
+ # @param type [#to_sym] OpenStudio surface (or subsurface) type
5126
5335
  # @param sides [Set<Symbols>] direction keys, e.g. :north (see OSut::SIDZ)
5127
5336
  #
5128
5337
  # @return [Array<OpenStudio::Model::Surface>] surfaces (may be empty, no logs)
@@ -5131,7 +5340,7 @@ module OSut
5131
5340
  spaces = spaces.respond_to?(:to_a) ? spaces.to_a : []
5132
5341
  return [] if spaces.empty?
5133
5342
 
5134
- sides = sides.respond_to?(:to_sym) ? [sides] : sides
5343
+ sides = sides.respond_to?(:to_sym) ? [trim(sides).to_sym] : sides
5135
5344
  sides = sides.respond_to?(:to_a) ? sides.to_a : []
5136
5345
 
5137
5346
  faces = []
@@ -5405,7 +5614,7 @@ module OSut
5405
5614
  # @param s [OpenStudio::Model::Surface] a model surface
5406
5615
  # @param [Array<Hash>] subs requested attributes
5407
5616
  # @option subs [#to_s] :id identifier e.g. "Window 007"
5408
- # @option subs [#to_s] :type ("FixedWindow") OpenStudio subsurface type
5617
+ # @option subs [#to_sym] :type ("FixedWindow") OpenStudio subsurface type
5409
5618
  # @option subs [#to_i] :count (1) number of individual subs per array
5410
5619
  # @option subs [#to_i] :multiplier (1) OpenStudio subsurface multiplier
5411
5620
  # @option subs [#frameWidth] :frame (nil) OpenStudio frame & divider object
@@ -5534,12 +5743,13 @@ module OSut
5534
5743
  return mismatch("sub", sub, cl4, mth, DBG, no) unless sub.is_a?(cl3)
5535
5744
 
5536
5745
  # Required key:value pairs (either set by the user or defaulted).
5537
- sub[:frame ] = nil unless sub.key?(:frame )
5538
- sub[:assembly ] = nil unless sub.key?(:assembly )
5539
- sub[:count ] = 1 unless sub.key?(:count )
5746
+ sub[:frame ] = nil unless sub.key?(:frame)
5747
+ sub[:assembly ] = nil unless sub.key?(:assembly)
5748
+ sub[:count ] = 1 unless sub.key?(:count)
5540
5749
  sub[:multiplier] = 1 unless sub.key?(:multiplier)
5541
- sub[:id ] = "" unless sub.key?(:id )
5542
- sub[:type ] = type unless sub.key?(:type )
5750
+ sub[:id ] = "" unless sub.key?(:id)
5751
+ sub[:type ] = type unless sub.key?(:type)
5752
+ sub[:type ] = type unless sub[:type].respond_to?(:to_sym)
5543
5753
  sub[:type ] = trim(sub[:type])
5544
5754
  sub[:id ] = trim(sub[:id])
5545
5755
  sub[:type ] = type if sub[:type].empty?
@@ -6368,7 +6578,7 @@ module OSut
6368
6578
  # @option opts [Bool] :sloped (true) whether to consider sloped roof surfaces
6369
6579
  # @option opts [Bool] :plenum (true) whether to consider plenum wells
6370
6580
  # @option opts [Bool] :attic (true) whether to consider attic wells
6371
- # @option opts [Array<#to_s>] :patterns requested skylight allocation (3x)
6581
+ # @option opts [Array<#to_sym>] :patterns requested skylight allocation (3x)
6372
6582
  # @example (a) consider 2D array of individual skylights, e.g. n(1.22m x 1.22m)
6373
6583
  # opts[:patterns] = ["array"]
6374
6584
  # @example (b) consider 'a', then array of 1x(size) x n(size) skylight strips
@@ -6685,7 +6895,7 @@ module OSut
6685
6895
  if opts.key?(:patterns)
6686
6896
  if opts[:patterns].is_a?(Array)
6687
6897
  opts[:patterns].each_with_index do |pattern, i|
6688
- pattern = trim(pattern).downcase
6898
+ pattern = pattern.respond_to?(:to_sym) ? trim(pattern).downcase : ""
6689
6899
 
6690
6900
  if pattern.empty?
6691
6901
  invalid("pattern #{i+1}", mth, 0, ERR)