tbd 3.4.5 → 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.
@@ -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,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,15 @@ 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
+
932
+ if ro > RMIN
539
933
  if specs[:type] == :door # 1x layer, adjust conductivity
540
934
  layer = c.getLayer(0).to_StandardOpaqueMaterial
541
935
  return invalid("#{id} standard material?", mth, 0) if layer.empty?
@@ -543,34 +937,33 @@ module OSut
543
937
  layer = layer.get
544
938
  k = layer.thickness / ro
545
939
  layer.setConductivity(k)
546
- else # multiple layers, adjust insulating layer thickness
940
+ else # multiple layers, adjust layer conductivity, then thickness
547
941
  lyr = insulatingLayer(c)
548
942
  return invalid("#{id} construction", mth, 0) if lyr[:index].nil?
549
943
  return invalid("#{id} construction", mth, 0) if lyr[:type ].nil?
550
- return invalid("#{id} construction", mth, 0) if lyr[:r ].zero?
944
+ return invalid("#{id} construction", mth, 0) if lyr[:r ].to_i.zero?
551
945
 
552
946
  index = lyr[:index]
553
947
  layer = c.getLayer(index).to_StandardOpaqueMaterial
554
948
  return invalid("#{id} material @#{index}", mth, 0) if layer.empty?
555
949
 
556
950
  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
951
 
561
- nom = "OSut|"
562
- nom += layer.nameString.gsub(/[^a-z]/i, "").gsub("OSut", "")
563
- nom += "|"
564
- nom += format("%03d", d*1000)[-3..-1]
952
+ k = (layer.thickness / (ro - rsi(c) + lyr[:r])).clamp(KMIN, KMAX)
953
+ d = (k * (ro - rsi(c) + lyr[:r])).clamp(DMIN, DMAX)
954
+
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]}"
565
958
 
566
959
  lyr = model.getStandardOpaqueMaterialByName(nom)
567
960
 
568
961
  if lyr.empty?
569
962
  layer.setName(nom)
963
+ layer.setConductivity(k)
570
964
  layer.setThickness(d)
571
965
  else
572
- omat = lyr.get
573
- c.setLayer(index, omat)
966
+ c.setLayer(index, lyr.get)
574
967
  end
575
968
  end
576
969
  end
@@ -619,20 +1012,20 @@ module OSut
619
1012
  end
620
1013
 
621
1014
  # Shading schedule.
622
- id = "OSut|SHADE|Ruleset"
1015
+ id = "OSut:SHADE:Ruleset"
623
1016
  sch = mdl.getScheduleRulesetByName(id)
624
1017
 
625
1018
  if sch.empty?
626
1019
  sch = OpenStudio::Model::ScheduleRuleset.new(mdl, 0)
627
1020
  sch.setName(id)
628
1021
  sch.setScheduleTypeLimits(onoff)
629
- sch.defaultDaySchedule.setName("OSut|Shade|Ruleset|Default")
1022
+ sch.defaultDaySchedule.setName("OSut:Shade:Ruleset:Default")
630
1023
  else
631
1024
  sch = sch.get
632
1025
  end
633
1026
 
634
1027
  # Summer cooling rule.
635
- id = "OSut|SHADE|ScheduleRule"
1028
+ id = "OSut:SHADE:ScheduleRule"
636
1029
  rule = mdl.getScheduleRuleByName(id)
637
1030
 
638
1031
  if rule.empty?
@@ -646,14 +1039,14 @@ module OSut
646
1039
  rule.setStartDate(start)
647
1040
  rule.setEndDate(finish)
648
1041
  rule.setApplyAllDays(true)
649
- rule.daySchedule.setName("OSut|Shade|Rule|Default")
1042
+ rule.daySchedule.setName("OSut:Shade:Rule:Default")
650
1043
  rule.daySchedule.addValue(OpenStudio::Time.new(0,24,0,0), 1)
651
1044
  else
652
1045
  rule = rule.get
653
1046
  end
654
1047
 
655
1048
  # Shade object.
656
- id = "OSut|Shade"
1049
+ id = "OSut:Shade"
657
1050
  shd = mdl.getShadeByName(id)
658
1051
 
659
1052
  if shd.empty?
@@ -664,7 +1057,7 @@ module OSut
664
1057
  end
665
1058
 
666
1059
  # Shading control (unique to each call).
667
- id = "OSut|ShadingControl"
1060
+ id = "OSut:ShadingControl"
668
1061
  ctl = OpenStudio::Model::ShadingControl.new(shd)
669
1062
  ctl.setName(id)
670
1063
  ctl.setSchedule(sch)
@@ -700,7 +1093,7 @@ module OSut
700
1093
 
701
1094
  # A single material.
702
1095
  mdl = sps.first.model
703
- id = "OSut|MASS|Material"
1096
+ id = "OSut:MASS:Material"
704
1097
  mat = mdl.getOpaqueMaterialByName(id)
705
1098
 
706
1099
  if mat.empty?
@@ -719,7 +1112,7 @@ module OSut
719
1112
  end
720
1113
 
721
1114
  # A single, 1x layered construction.
722
- id = "OSut|MASS|Construction"
1115
+ id = "OSut:MASS:Construction"
723
1116
  con = mdl.getConstructionByName(id)
724
1117
 
725
1118
  if con.empty?
@@ -732,7 +1125,7 @@ module OSut
732
1125
  con = con.get
733
1126
  end
734
1127
 
735
- id = "OSut|InternalMassDefinition|" + (format "%.2f", ratio)
1128
+ id = "OSut:InternalMassDefinition:" + (format "%.2f", ratio)
736
1129
  df = mdl.getInternalMassDefinitionByName(id)
737
1130
 
738
1131
  if df.empty?
@@ -746,7 +1139,7 @@ module OSut
746
1139
 
747
1140
  sps.each do |sp|
748
1141
  mass = OpenStudio::Model::InternalMass.new(df)
749
- mass.setName("OSut|InternalMass|#{sp.nameString}")
1142
+ mass.setName("OSut:InternalMass:#{sp.nameString}")
750
1143
  mass.setSpace(sp)
751
1144
  end
752
1145
 
@@ -754,13 +1147,13 @@ module OSut
754
1147
  end
755
1148
 
756
1149
  ##
757
- # Validates if a default construction set holds a base construction.
1150
+ # Validates if a default construction set holds an opaque base construction.
758
1151
  #
759
1152
  # @param set [OpenStudio::Model::DefaultConstructionSet] a default set
760
- # @param bse [OpenStudio::Model::ConstructionBase] a construction base
1153
+ # @param bse [OpenStudio::Model::ConstructionBase] an opaque construction base
761
1154
  # @param gr [Bool] if ground-facing surface
762
1155
  # @param ex [Bool] if exterior-facing surface
763
- # @param tp [#to_s] a surface type
1156
+ # @param tp [#to_sym] surface type: "floor", "wall" or "roofceiling"
764
1157
  #
765
1158
  # @return [Bool] whether default set holds construction
766
1159
  # @return [false] if invalid input (see logs)
@@ -779,7 +1172,7 @@ module OSut
779
1172
  ck2 = bse.is_a?(cl2)
780
1173
  ck3 = [true, false].include?(gr)
781
1174
  ck4 = [true, false].include?(ex)
782
- ck5 = tp.respond_to?(:to_s)
1175
+ ck5 = tp.respond_to?(:to_sym)
783
1176
  return mismatch(id1, set, cl1, mth, DBG, false) unless ck1
784
1177
  return mismatch(id2, bse, cl2, mth, DBG, false) unless ck2
785
1178
  return invalid("ground" , mth, 3, DBG, false) unless ck3
@@ -859,6 +1252,9 @@ module OSut
859
1252
  type = s.surfaceType
860
1253
  ground = false
861
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
862
1258
 
863
1259
  if s.isGroundSurface
864
1260
  ground = true
@@ -866,7 +1262,14 @@ module OSut
866
1262
  exterior = true
867
1263
  end
868
1264
 
869
- unless space.defaultConstructionSet.empty?
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)
1270
+ end
1271
+ end
1272
+ else
870
1273
  set = space.defaultConstructionSet.get
871
1274
  return set if holdsConstruction?(set, base, ground, exterior, type)
872
1275
  end
@@ -880,6 +1283,17 @@ module OSut
880
1283
  end
881
1284
  end
882
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)
1293
+ end
1294
+ end
1295
+ end
1296
+
883
1297
  unless space.buildingStory.empty?
884
1298
  story = space.buildingStory.get
885
1299
 
@@ -889,6 +1303,15 @@ module OSut
889
1303
  end
890
1304
  end
891
1305
 
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
+
892
1315
  building = mdl.getBuilding
893
1316
 
894
1317
  unless building.defaultConstructionSet.empty?
@@ -899,203 +1322,6 @@ module OSut
899
1322
  nil
900
1323
  end
901
1324
 
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
1325
  ##
1100
1326
  # Validates whether opaque surface can be considered as a curtain wall (or
1101
1327
  # similar technology) spandrel, regardless of construction layers, by looking
@@ -2216,7 +2442,7 @@ module OSut
2216
2442
  cl = OpenStudio::Model::Model
2217
2443
  limits = nil
2218
2444
  return mismatch("model", model, cl, mth) unless model.is_a?(cl)
2219
- 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)
2220
2446
 
2221
2447
  # Either fetch availability ScheduleTypeLimits object, or create one.
2222
2448
  model.getScheduleTypeLimitss.each do |l|
@@ -4648,7 +4874,7 @@ module OSut
4648
4874
  #
4649
4875
  # @param s [Set<OpenStudio::Point3d>] a (larger) parent set of points
4650
4876
  # @param [Array<Hash>] set a collection of (smaller) sequenced points
4651
- # @option [Symbol] tag sequence of subset vertices to target
4877
+ # @option [#to_sym] tag sequence of subset vertices to target
4652
4878
  #
4653
4879
  # @return [Integer] number of successfully anchored subsets (see logs)
4654
4880
  def genAnchors(s = nil, set = [], tag = :box)
@@ -4656,18 +4882,20 @@ module OSut
4656
4882
  n = 0
4657
4883
  id = s.respond_to?(:nameString) ? "#{s.nameString}: " : ""
4658
4884
  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)
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)
4661
4888
 
4662
4889
  origin = OpenStudio::Point3d.new(0,0,0)
4663
4890
  zenith = OpenStudio::Point3d.new(0,0,1)
4664
4891
  ray = zenith - origin
4665
4892
  set = set.to_a
4893
+ tag = tag.to_sym
4666
4894
 
4667
4895
  # Validate individual subsets. Purge surface-specific leader line anchors.
4668
4896
  set.each_with_index do |st, i|
4669
4897
  str1 = id + "subset ##{i+1}"
4670
- str2 = str1 + " #{tag.to_s}"
4898
+ str2 = str1 + " #{trim(tag)}"
4671
4899
  return mismatch(str1, st, Hash, mth, DBG, n) unless st.respond_to?(:key?)
4672
4900
  return hashkey( str1, st, tag, mth, DBG, n) unless st.key?(tag)
4673
4901
  return empty("#{str2} vertices", mth, DBG, n) if st[tag].empty?
@@ -4810,7 +5038,7 @@ module OSut
4810
5038
  # @param s [Set<OpenStudio::Point3d>] a larger (parent) set of points
4811
5039
  # @param [Array<Hash>] set a collection of (smaller) sequenced vertices
4812
5040
  # @option set [Hash] :ld a polygon-specific leader line anchors
4813
- # @option [Symbol] tag sequence of set vertices to target
5041
+ # @option [#to_sym] tag sequence of set vertices to target
4814
5042
  #
4815
5043
  # @return [OpenStudio::Point3dVector] extended vertices (see logs if empty)
4816
5044
  def genExtendedVertices(s = nil, set = [], tag = :vtx)
@@ -4822,14 +5050,16 @@ module OSut
4822
5050
  a = OpenStudio::Point3dVector.new
4823
5051
  v = []
4824
5052
  return a if pts.empty?
4825
- 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)
4826
5055
 
4827
5056
  set = set.to_a
5057
+ tag = tag.to_sym
4828
5058
 
4829
5059
  # Validate individual sets.
4830
5060
  set.each_with_index do |st, i|
4831
5061
  str1 = id + "subset ##{i+1}"
4832
- str2 = str1 + " #{tag.to_s}"
5062
+ str2 = str1 + " #{trim(tag)}"
4833
5063
  return mismatch(str1, st, Hash, mth, DBG, a) unless st.respond_to?(:key?)
4834
5064
  next if st.key?(:void) && st[:void]
4835
5065
 
@@ -5121,8 +5351,8 @@ module OSut
5121
5351
  # surface type filters if 'type' argument == "all".
5122
5352
  #
5123
5353
  # @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
5354
+ # @param boundary [#to_sym] OpenStudio outside boundary condition
5355
+ # @param type [#to_sym] OpenStudio surface (or subsurface) type
5126
5356
  # @param sides [Set<Symbols>] direction keys, e.g. :north (see OSut::SIDZ)
5127
5357
  #
5128
5358
  # @return [Array<OpenStudio::Model::Surface>] surfaces (may be empty, no logs)
@@ -5131,7 +5361,7 @@ module OSut
5131
5361
  spaces = spaces.respond_to?(:to_a) ? spaces.to_a : []
5132
5362
  return [] if spaces.empty?
5133
5363
 
5134
- sides = sides.respond_to?(:to_sym) ? [sides] : sides
5364
+ sides = sides.respond_to?(:to_sym) ? [trim(sides).to_sym] : sides
5135
5365
  sides = sides.respond_to?(:to_a) ? sides.to_a : []
5136
5366
 
5137
5367
  faces = []
@@ -5405,7 +5635,7 @@ module OSut
5405
5635
  # @param s [OpenStudio::Model::Surface] a model surface
5406
5636
  # @param [Array<Hash>] subs requested attributes
5407
5637
  # @option subs [#to_s] :id identifier e.g. "Window 007"
5408
- # @option subs [#to_s] :type ("FixedWindow") OpenStudio subsurface type
5638
+ # @option subs [#to_sym] :type ("FixedWindow") OpenStudio subsurface type
5409
5639
  # @option subs [#to_i] :count (1) number of individual subs per array
5410
5640
  # @option subs [#to_i] :multiplier (1) OpenStudio subsurface multiplier
5411
5641
  # @option subs [#frameWidth] :frame (nil) OpenStudio frame & divider object
@@ -5534,12 +5764,13 @@ module OSut
5534
5764
  return mismatch("sub", sub, cl4, mth, DBG, no) unless sub.is_a?(cl3)
5535
5765
 
5536
5766
  # 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 )
5767
+ sub[:frame ] = nil unless sub.key?(:frame)
5768
+ sub[:assembly ] = nil unless sub.key?(:assembly)
5769
+ sub[:count ] = 1 unless sub.key?(:count)
5540
5770
  sub[:multiplier] = 1 unless sub.key?(:multiplier)
5541
- sub[:id ] = "" unless sub.key?(:id )
5542
- 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)
5543
5774
  sub[:type ] = trim(sub[:type])
5544
5775
  sub[:id ] = trim(sub[:id])
5545
5776
  sub[:type ] = type if sub[:type].empty?
@@ -6368,7 +6599,7 @@ module OSut
6368
6599
  # @option opts [Bool] :sloped (true) whether to consider sloped roof surfaces
6369
6600
  # @option opts [Bool] :plenum (true) whether to consider plenum wells
6370
6601
  # @option opts [Bool] :attic (true) whether to consider attic wells
6371
- # @option opts [Array<#to_s>] :patterns requested skylight allocation (3x)
6602
+ # @option opts [Array<#to_sym>] :patterns requested skylight allocation (3x)
6372
6603
  # @example (a) consider 2D array of individual skylights, e.g. n(1.22m x 1.22m)
6373
6604
  # opts[:patterns] = ["array"]
6374
6605
  # @example (b) consider 'a', then array of 1x(size) x n(size) skylight strips
@@ -6685,7 +6916,7 @@ module OSut
6685
6916
  if opts.key?(:patterns)
6686
6917
  if opts[:patterns].is_a?(Array)
6687
6918
  opts[:patterns].each_with_index do |pattern, i|
6688
- pattern = trim(pattern).downcase
6919
+ pattern = pattern.respond_to?(:to_sym) ? trim(pattern).downcase : ""
6689
6920
 
6690
6921
  if pattern.empty?
6691
6922
  invalid("pattern #{i+1}", mth, 0, ERR)