tbd 3.2.3 → 3.3.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.
data/lib/tbd/ua.rb CHANGED
@@ -26,13 +26,13 @@ module TBD
26
26
  #
27
27
  # @param model [OpenStudio::Model::Model] a model
28
28
  # @param lc [OpenStudio::Model::LayeredConstruction] a layered construction
29
- # @param id [String] layered construction identifier
30
- # @param heatloss [Double] heat loss from major thermal bridging [W/K]
31
- # @param film [Double] target surface film resistance [m2.K/W]
32
- # @param ut [Double] target overall Ut for lc [W/m2.K]
29
+ # @param id [#to_s] layered construction identifier
30
+ # @param hloss [Numeric] heat loss from major thermal bridging, in W/K
31
+ # @param film [Numeric] target surface film resistance, in m2K/W
32
+ # @param ut [Numeric] target overall Ut for lc, in W/m2K
33
33
  #
34
- # @return [Hash] uo: lc Uo [W/m2.K] to meet Ut, m: uprated lc layer
35
- # @return [Hash] uo: NilClass, m: NilClass (if invalid input)
34
+ # @return [Hash] uo: lc Uo [W/m2K] to meet Ut, m: uprated lc layer
35
+ # @return [Hash] uo: (nil), m: (nil) if invalid input (see logs)
36
36
  def uo(model = nil, lc = nil, id = "", hloss = 0.0, film = 0.0, ut = 0.0)
37
37
  mth = "TBD::#{__callee__}"
38
38
  res = { uo: nil, m: nil }
@@ -40,53 +40,51 @@ module TBD
40
40
  cl2 = OpenStudio::Model::LayeredConstruction
41
41
  cl3 = Numeric
42
42
  cl4 = String
43
-
43
+ id = trim(id)
44
44
  return mismatch("model", model, cl1, mth, DBG, res) unless model.is_a?(cl1)
45
- return mismatch("id" , id, cl4, mth, DBG, res) unless id.is_a?(cl4)
45
+ return mismatch("id" , id, cl4, mth, DBG, res) if id.empty?
46
46
  return mismatch("lc" , lc, cl2, mth, DBG, res) unless lc.is_a?(cl2)
47
47
  return mismatch("hloss", hloss, cl3, mth, DBG, res) unless hloss.is_a?(cl3)
48
48
  return mismatch("film" , film, cl3, mth, DBG, res) unless film.is_a?(cl3)
49
49
  return mismatch("Ut" , ut, cl3, mth, DBG, res) unless ut.is_a?(cl3)
50
50
 
51
- loss = 0.0 # residual heatloss (not assigned) [W/K]
51
+ loss = 0.0 # residual heatloss (not assigned) [W/K]
52
52
  area = lc.getNetArea
53
53
  lyr = insulatingLayer(lc)
54
54
  lyr[:index] = nil unless lyr[:index].is_a?(Numeric)
55
55
  lyr[:index] = nil unless lyr[:index] >= 0
56
56
  lyr[:index] = nil unless lyr[:index] < lc.layers.size
57
-
58
- return invalid("'#{id}' layer index", mth, 0, ERR, res) unless lyr[:index]
59
- return zero("'#{id}': heatloss" , mth, WRN, res) unless hloss > TOL
60
- return zero("'#{id}': films" , mth, WRN, res) unless film > TOL
61
- return zero("'#{id}': Ut" , mth, WRN, res) unless ut > TOL
62
- return invalid("'#{id}': Ut" , mth, 0, WRN, res) unless ut < 5.678
63
- return zero("'#{id}': net area (m2)", mth, ERR, res) unless area > TOL
57
+ return invalid("#{id} layer index", mth, 3, ERR, res) unless lyr[:index]
58
+ return zero("#{id}: heatloss" , mth, WRN, res) unless hloss > TOL
59
+ return zero("#{id}: films" , mth, WRN, res) unless film > TOL
60
+ return zero("#{id}: Ut" , mth, WRN, res) unless ut > TOL
61
+ return invalid("#{id}: Ut" , mth, 6, WRN, res) unless ut < 5.678
62
+ return zero("#{id}: net area (m2)", mth, ERR, res) unless area > TOL
64
63
 
65
64
  # First, calculate initial layer RSi to initially meet Ut target.
66
- rt = 1 / ut # target construction Rt
67
- ro = rsi(lc, film) # current construction Ro
68
- new_r = lyr[:r] + (rt - ro) # new, un-derated layer RSi
65
+ rt = 1 / ut # target construction Rt
66
+ ro = rsi(lc, film) # current construction Ro
67
+ new_r = lyr[:r] + (rt - ro) # new, un-derated layer RSi
69
68
  new_u = 1 / new_r
70
69
 
71
70
  # Then, uprate (if possible) to counter expected thermal bridging effects.
72
- u_psi = hloss / area # from psi & khi
73
- new_u -= u_psi # uprated layer USi to counter psi & khi
74
- new_r = 1 / new_u # uprated layer RSi to counter psi & khi
75
-
76
- return zero("'#{id}': new Rsi", mth, ERR, res) unless new_r > 0.001
71
+ u_psi = hloss / area # from psi+khi
72
+ new_u -= u_psi # uprated layer USi to counter psi+khi
73
+ new_r = 1 / new_u # uprated layer RSi to counter psi+khi
74
+ return zero("#{id}: new Rsi", mth, ERR, res) unless new_r > 0.001
77
75
 
78
76
  if lyr[:type] == :massless
79
77
  m = lc.getLayer(lyr[:index]).to_MasslessOpaqueMaterial
80
- return invalid("'#{id}' massless layer?", mth, 0, DBG, res) if m.empty?
78
+ return invalid("#{id} massless layer?", mth, 0, DBG, res) if m.empty?
81
79
 
82
80
  m = m.get.clone(model).to_MasslessOpaqueMaterial.get
83
81
  m.setName("#{id} uprated")
84
- new_r = 0.001 unless new_r > 0.001
85
- loss = (new_u - 1 / new_r) * area unless new_r > 0.001
82
+ new_r = 0.001 unless new_r > 0.001
83
+ loss = (new_u - 1 / new_r) * area unless new_r > 0.001
86
84
  m.setThermalResistance(new_r)
87
85
  else # type == :standard
88
86
  m = lc.getLayer(lyr[:index]).to_StandardOpaqueMaterial
89
- return invalid("'#{id}' standard layer?", mth, 0, DBG, res) if m.empty?
87
+ return invalid("#{id} standard layer?", mth, 0, DBG, res) if m.empty?
90
88
 
91
89
  m = m.get.clone(model).to_StandardOpaqueMaterial.get
92
90
  m.setName("#{id} uprated")
@@ -98,30 +96,31 @@ module TBD
98
96
  unless d > 0.003
99
97
  d = 0.003
100
98
  k = d / new_r
101
- k = 3.0 unless k < 3.0
102
- loss = (new_u - k / d) * area unless k < 3.0
99
+ k = 3.0 unless k < 3.0
100
+ loss = (new_u - k / d) * area unless k < 3.0
103
101
  end
104
- else # new_r < 0.001 m2.K/W
102
+ else # new_r < 0.001 m2K/W
105
103
  d = 0.001 * k
106
- d = 0.003 unless d > 0.003
107
- k = d / 0.001 unless d > 0.003
104
+ d = 0.003 unless d > 0.003
105
+ k = d / 0.001 unless d > 0.003
108
106
  loss = (new_u - k / d) * area
109
107
  end
110
108
 
111
- ok = m.setThickness(d)
112
- return invalid("Can't uprate '#{id}': > 3m", mth, 0, ERR, res) unless ok
113
-
114
- m.setThermalConductivity(k) if ok
109
+ if m.setThickness(d)
110
+ m.setThermalConductivity(k)
111
+ else
112
+ return invalid("Can't uprate #{id}: #{d} > 3m", mth, 0, ERR, res)
113
+ end
115
114
  end
116
115
 
117
- return invalid("", mth, 0, ERR, res) unless m
116
+ return invalid("Can't ID insulating layer", mth, 0, ERR, res) unless m
118
117
 
119
118
  lc.setLayer(lyr[:index], m)
120
119
  uo = 1 / rsi(lc, film)
121
120
 
122
121
  if loss > TOL
123
122
  h_loss = format "%.3f", loss
124
- return invalid("Can't assign #{h_loss} W/K to '#{id}'", mth, 0, ERR, res)
123
+ return invalid("Can't assign #{h_loss} W/K to #{id}", mth, 0, ERR, res)
125
124
  end
126
125
 
127
126
  res[:uo] = uo
@@ -131,60 +130,85 @@ module TBD
131
130
  end
132
131
 
133
132
  ##
134
- # Uprate insulation layer of construction, based on user-selected Ut (argh).
133
+ # Uprates insulation layer of construction, based on user-selected Ut (argh).
135
134
  #
136
135
  # @param model [OpenStudio::Model::Model] a model
137
- # @param s [Hash] preprocessed collection of TBD surfaces
138
- # @param argh [Hash] TBD arguments
136
+ # @param [Hash] s preprocessed collection of TBD surfaces
137
+ # @option s [:wall, :ceiling, :floor] :type surface type
138
+ # @option s [Bool] :deratable whether surface can be thermally bridged
139
+ # @option s [OpenStudio::LayeredConstruction] :construction construction
140
+ # @option s [#to_i] :index deratable construction layer index
141
+ # @option s [:massless, :standard] :ltype indexed layer type
142
+ # @option s [#to_f] :filmRSI air film resistances (optional)
143
+ # @option s [#to_f] :r thermal resistance (RSI) of indexed layer
144
+ # @param [Hash] argh TBD arguments
145
+ # @option argh [Bool] :uprate_walls (false) whether to uprate walls
146
+ # @option argh [Bool] :uprate_roofs (false) whether to uprate roofs
147
+ # @option argh [Bool] :uprate_floors (false) whether to uprate floors
148
+ # @option argh [#to_f] :wall_ut (5.678) uprated wall Usi-factor target
149
+ # @option argh [#to_f] :roof_ut (5.678) uprated roof Usi-factor target
150
+ # @option argh [#to_f] :floor_ut (5.678) uprated floor Usi-factor target
151
+ # @option argh [#to_s] :wall_option ("") construction to uprate (or "all")
152
+ # @option argh [#to_s] :roof_option ("") construction to uprate (or "all")
153
+ # @option argh [#to_s] :floor_option ("") construction to uprate (or "all")
139
154
  #
140
- # @return [Bool] true if successfully uprated
155
+ # @return [Bool] whether successfully uprated
156
+ # @return [false] if invalid input (see logs)
141
157
  def uprate(model = nil, s = {}, argh = {})
142
- mth = "TBD::#{__callee__}"
143
- cl1 = OpenStudio::Model::Model
144
- cl2 = Hash
145
- cl3 = OpenStudio::Model::LayeredConstruction
146
- a = false
147
-
158
+ mth = "TBD::#{__callee__}"
159
+ cl1 = OpenStudio::Model::Model
160
+ cl2 = Hash
161
+ cl3 = OpenStudio::Model::LayeredConstruction
162
+ tout = []
163
+ tout << "all wall constructions"
164
+ tout << "all roof constructions"
165
+ tout << "all floor constructions"
166
+ a = false
167
+ groups = { wall: {}, roof: {}, floor: {} }
148
168
  return mismatch("model" , model, cl1, mth, DBG, a) unless model.is_a?(cl1)
149
169
  return mismatch("surfaces", s, cl2, mth, DBG, a) unless s.is_a?(cl2)
150
170
  return mismatch("argh" , model, cl1, mth, DBG, a) unless argh.is_a?(cl2)
151
171
 
152
- argh[:uprate_walls ] = false unless argh.key?(:uprate_walls )
153
- argh[:uprate_roofs ] = false unless argh.key?(:uprate_roofs )
172
+ argh[:uprate_walls ] = false unless argh.key?(:uprate_walls)
173
+ argh[:uprate_roofs ] = false unless argh.key?(:uprate_roofs)
154
174
  argh[:uprate_floors] = false unless argh.key?(:uprate_floors)
155
- argh[:wall_ut ] = 5.678 unless argh.key?(:wall_ut )
156
- argh[:roof_ut ] = 5.678 unless argh.key?(:roof_ut )
157
- argh[:floor_ut ] = 5.678 unless argh.key?(:floor_ut )
158
- argh[:wall_option ] = "" unless argh.key?(:wall_option )
159
- argh[:roof_option ] = "" unless argh.key?(:roof_option )
160
- argh[:floor_option ] = "" unless argh.key?(:floor_option )
161
-
162
- groups = { wall: {}, roof: {}, floor: {} }
175
+ argh[:wall_ut ] = 5.678 unless argh.key?(:wall_ut)
176
+ argh[:roof_ut ] = 5.678 unless argh.key?(:roof_ut)
177
+ argh[:floor_ut ] = 5.678 unless argh.key?(:floor_ut)
178
+ argh[:wall_option ] = "" unless argh.key?(:wall_option)
179
+ argh[:roof_option ] = "" unless argh.key?(:roof_option)
180
+ argh[:floor_option ] = "" unless argh.key?(:floor_option)
181
+
182
+ argh[:wall_option ] = trim(argh[:wall_option ])
183
+ argh[:roof_option ] = trim(argh[:roof_option ])
184
+ argh[:floor_option ] = trim(argh[:floor_option])
185
+
163
186
  groups[:wall ][:up] = argh[:uprate_walls ]
164
187
  groups[:roof ][:up] = argh[:uprate_roofs ]
165
188
  groups[:floor][:up] = argh[:uprate_floors]
166
189
  groups[:wall ][:ut] = argh[:wall_ut ]
167
190
  groups[:roof ][:ut] = argh[:roof_ut ]
168
191
  groups[:floor][:ut] = argh[:floor_ut ]
169
- groups[:wall ][:op] = argh[:wall_option ]
170
- groups[:roof ][:op] = argh[:roof_option ]
171
- groups[:floor][:op] = argh[:floor_option ]
192
+
193
+ groups[:wall ][:op] = trim(argh[:wall_option ])
194
+ groups[:roof ][:op] = trim(argh[:roof_option ])
195
+ groups[:floor][:op] = trim(argh[:floor_option ])
172
196
 
173
197
  groups.each do |type, g|
174
198
  next unless g[:up]
175
199
  next unless g[:ut].is_a?(Numeric)
176
200
  next unless g[:ut] < 5.678
201
+ next if g[:ut] < 0
177
202
 
178
203
  typ = type
179
- typ = :ceiling if typ == :roof # fix in future revision. TO-DO.
204
+ typ = :ceiling if typ == :roof
180
205
  coll = {}
181
206
  area = 0
182
207
  film = 100000000000000
183
208
  lc = nil
184
209
  id = ""
185
- all = g[:op].downcase == "all wall constructions" ||
186
- g[:op].downcase == "all roof constructions" ||
187
- g[:op].downcase == "all floor constructions"
210
+ op = g[:op].downcase
211
+ all = tout.include?(op)
188
212
 
189
213
  if g[:op].empty?
190
214
  log(ERR, "Construction (#{type}) to uprate? (#{mth})")
@@ -217,8 +241,8 @@ module TBD
217
241
  id = i
218
242
  end
219
243
 
220
- coll[i] = { area: aire, lc: c, s: {} } unless coll.key?(i)
221
- coll[i][:s][nom] = { a: surface[:net] } unless coll[i][:s].key?(nom)
244
+ coll[i] = { area: aire, lc: c, s: {} } unless coll.key?(i)
245
+ coll[i][:s][nom] = { a: surface[:net] } unless coll[i][:s].key?(nom)
222
246
  end
223
247
  else
224
248
  id = g[:op]
@@ -267,8 +291,8 @@ module TBD
267
291
  lyr[:index] = nil unless lyr[:index] >= 0
268
292
  lyr[:index] = nil unless lyr[:index] < lc.layers.size
269
293
 
270
- log(ERR, "Insulation index for '#{id}'? (#{mth})") unless lyr[:index]
271
- next unless lyr[:index]
294
+ log(ERR, "Insulation index for '#{id}'? (#{mth})") unless lyr[:index]
295
+ next unless lyr[:index]
272
296
 
273
297
  # Ensure lc is exclusively linked to deratable surfaces of right type.
274
298
  # If not, assign new lc clone to non-targeted surfaces.
@@ -287,7 +311,7 @@ module TBD
287
311
 
288
312
  unless ok
289
313
  log(WRN, "Cloning '#{nom}' construction - not '#{id}' (#{mth})")
290
- sss = model.getSurfaceByName(nom)
314
+ sss = model.getSurfaceByName(nom)
291
315
  next if sss.empty?
292
316
 
293
317
  sss = sss.get
@@ -299,18 +323,18 @@ module TBD
299
323
  end
300
324
  end
301
325
 
302
- hloss = 0 # sum of applicable psi & khi effects [W/K]
326
+ hloss = 0 # sum of applicable psi+khi-related losses [W/K]
303
327
 
304
- # Tally applicable psi + khi losses. Possible construction reassignment.
328
+ # Tally applicable psi+khi losses. Possible construction reassignment.
305
329
  coll.each do |i, col|
306
330
  col[:s].keys.each do |nom|
307
331
  next unless s.key?(nom)
308
332
  next unless s[nom].key?(:construction)
309
- next unless s[nom].key?(:index )
310
- next unless s[nom].key?(:ltype )
311
- next unless s[nom].key?(:r )
333
+ next unless s[nom].key?(:index)
334
+ next unless s[nom].key?(:ltype)
335
+ next unless s[nom].key?(:r)
312
336
 
313
- # Tally applicable psi + khi.
337
+ # Tally applicable psi+khi.
314
338
  hloss += s[nom][:heatloss ] if s[nom].key?(:heatloss)
315
339
  next if s[nom][:construction] == lc
316
340
 
@@ -321,7 +345,7 @@ module TBD
321
345
  sss = sss.get
322
346
 
323
347
  if sss.isConstructionDefaulted
324
- set = defaultConstructionSet(model, sss) # building? story?
348
+ set = defaultConstructionSet(sss) # building? story?
325
349
  constructions = set.defaultExteriorSurfaceConstructions
326
350
 
327
351
  unless constructions.empty?
@@ -334,10 +358,10 @@ module TBD
334
358
  sss.setConstruction(lc)
335
359
  end
336
360
 
337
- s[nom][:construction] = lc # reset TBD attributes
361
+ s[nom][:construction] = lc # reset TBD attributes
338
362
  s[nom][:index ] = lyr[:index]
339
363
  s[nom][:ltype ] = lyr[:type ]
340
- s[nom][:r ] = lyr[:r ] # temporary
364
+ s[nom][:r ] = lyr[:r ] # temporary
341
365
  end
342
366
  end
343
367
 
@@ -351,14 +375,19 @@ module TBD
351
375
  end
352
376
 
353
377
  coll.delete_if { |i, _| i != id }
354
- log(DBG, "Collection == 1? for '#{id}' (#{mth})") unless coll.size == 1
355
- next unless coll.size == 1
356
378
 
357
- area = lc.getNetArea
358
- coll[id][:area] = area
379
+ unless coll.size == 1
380
+ log(DBG, "Collection == 1? for '#{id}' (#{mth})")
381
+ next
382
+ end
383
+
384
+ coll[id][:area] = lc.getNetArea
359
385
  res = uo(model, lc, id, hloss, film, g[:ut])
360
- log(ERR, "Unable to uprate '#{id}' (#{mth})") unless res[:uo] && res[:m]
361
- next unless res[:uo] && res[:m]
386
+
387
+ unless res[:uo] && res[:m]
388
+ log(ERR, "Unable to uprate '#{id}' (#{mth})")
389
+ next
390
+ end
362
391
 
363
392
  lyr = insulatingLayer(lc)
364
393
 
@@ -371,7 +400,7 @@ module TBD
371
400
  next unless s[nom][:index] == lyr[:index]
372
401
  next unless s[nom][:ltype] == lyr[:type ]
373
402
 
374
- s[nom][:r] = lyr[:r] # uprated insulating RSi factor, before derating
403
+ s[nom][:r] = lyr[:r] # uprated insulating RSi factor, before derating
375
404
  end
376
405
 
377
406
  argh[:wall_uo ] = res[:uo] if typ == :wall
@@ -387,40 +416,49 @@ module TBD
387
416
  end
388
417
 
389
418
  ##
390
- # Set reference values for points, edges & surfaces (& subsurfaces) to
419
+ # Sets reference values for points, edges & surfaces (& subsurfaces) to
391
420
  # compute Quebec energy code (Section 3.3) UA' comparison (2021).
392
421
  #
393
- # @param s [Hash] preprocessed collection of TBD surfaces
422
+ # @param [Hash] s TBD surfaces (keys: Openstudio surface names)
423
+ # @option s [Bool] :deratable whether surface is deratable, s[][:deratable]
424
+ # @option s [:wall, :ceiling, :floor] :type TBD surface type
425
+ # @option s [#to_f] :heating applicable heating setpoint temperature in °C
426
+ # @option s [#to_f] :cooling applicable cooling setpoint temperature in °C
427
+ # @option s [Hash] :windows TBD surface-specific windows e.g. s[][:windows]
428
+ # @option s [Hash] :doors TBD surface-specific doors
429
+ # @option s [Hash] :skylights TBD surface-specific skylights
430
+ # @option s [Hash] :pts point thermal bridges, e.g. s[][:pts] see KHI class
431
+ # @option s [Hash] :edges TBD edges (keys: Topolys edge identifiers)
394
432
  # @param sets [TBD::PSI] a TBD model's PSI sets
395
- # @param spts [Bool] true if OpenStudio model has valid setpoints
433
+ # @param spts [Bool] whether OpenStudio model holds heating/cooling setpoints
396
434
  #
397
- # @return [Bool] true if successful in generating UA' reference values
435
+ # @return [Bool] whether successful in generating UA' reference values
436
+ # @return [false] if invalid inputs (see logs)
398
437
  def qc33(s = {}, sets = nil, spts = true)
399
438
  mth = "TBD::#{__callee__}"
400
439
  cl1 = Hash
401
440
  cl2 = TBD::PSI
402
-
403
- return mismatch("surfaces", s, cl1, mth, DBG, false) unless s.is_a?(cl1)
404
- return mismatch("sets", sets, cl1, mth, DBG, false) unless sets.is_a?(cl2)
441
+ return mismatch("surfaces", s, cl1, mth, DBG, false) unless s.is_a?(cl1)
442
+ return mismatch("sets", sets, cl1, mth, DBG, false) unless sets.is_a?(cl2)
405
443
 
406
444
  shorts = sets.shorthands("code (Quebec)")
407
- empty = shorts[:has].empty? || shorts[:val].empty?
408
- log(DBG, "Missing QC PSI set for 3.3 UA' tradeoff (#{mth})") if empty
409
- return false if empty
445
+ empty = shorts[:has].empty? || shorts[:val].empty?
446
+ log(DBG, "Missing QC PSI set for 3.3 UA' tradeoff (#{mth})") if empty
447
+ return false if empty
410
448
 
411
- ok = spts == true || spts == false
412
- log(DBG, "'setpoints' must be true/false for 3.3 UA' tradeoff") unless ok
413
- return false unless ok
449
+ ok = [true, false].include?(spts)
450
+ log(DBG, "setpoints must be true or false for 3.3 UA' tradeoff") unless ok
451
+ return false unless ok
414
452
 
415
453
  s.each do |id, surface|
416
454
  next unless surface.key?(:deratable)
417
455
  next unless surface[:deratable]
418
456
  next unless surface.key?(:type)
419
457
 
420
- heating = -50 if spts
421
- cooling = 50 if spts
422
- heating = 21 unless spts
423
- cooling = 24 unless spts
458
+ heating = -50 if spts
459
+ cooling = 50 if spts
460
+ heating = 21 unless spts
461
+ cooling = 24 unless spts
424
462
  heating = surface[:heating] if surface.key?(:heating)
425
463
  cooling = surface[:cooling] if surface.key?(:cooling)
426
464
 
@@ -429,18 +467,21 @@ module TBD
429
467
  ref = 1 / 3.60 if surface[:type] == :wall
430
468
 
431
469
  # Adjust for lower heating setpoint (assumes -25°C design conditions).
432
- ref *= 43 / (heating + 25) if heating < 18 && cooling > 40
433
- surface[:ref] = ref # ... and store
470
+ ref *= 43 / (heating + 25) if heating < 18 && cooling > 40
471
+
472
+ surface[:ref] = ref
434
473
 
435
- if surface.key?(:skylights) # loop through subsurfaces
474
+ if surface.key?(:skylights) # loop through subsurfaces
436
475
  ref = 2.85
437
- ref *= 43 / (heating + 25) if heating < 18 && cooling > 40
476
+ ref *= 43 / (heating + 25) if heating < 18 && cooling > 40
477
+
438
478
  surface[:skylights].values.map { |skylight| skylight[:ref] = ref }
439
479
  end
440
480
 
441
481
  if surface.key?(:windows)
442
482
  ref = 2.0
443
- ref *= 43 / (heating + 25) if heating < 18 && cooling > 40
483
+ ref *= 43 / (heating + 25) if heating < 18 && cooling > 40
484
+
444
485
  surface[:windows].values.map { |window| window[:ref] = ref }
445
486
  end
446
487
 
@@ -448,21 +489,22 @@ module TBD
448
489
  surface[:doors].each do |i, door|
449
490
  ref = 0.9
450
491
  ref = 2.0 if door.key?(:glazed) && door[:glazed]
451
- ref *= 43 / (heating + 25) if heating < 18 && cooling > 40
492
+ ref *= 43 / (heating + 25) if heating < 18 && cooling > 40
452
493
  door[:ref] = ref
453
494
  end
454
495
  end
455
496
 
456
497
  # Loop through point thermal bridges.
457
- surface[:pts].map { |i, pt| pt[:ref] = 0.5 } if surface.key?(:pts)
498
+ surface[:pts].map { |i, pt| pt[:ref] = 0.5 } if surface.key?(:pts)
458
499
 
459
500
  # Loop through linear thermal bridges.
460
501
  if surface.key?(:edges)
461
502
  surface[:edges].values.each do |edge|
462
503
  next unless edge.key?(:type)
463
504
  next unless edge.key?(:ratio)
505
+
464
506
  safe = sets.safe("code (Quebec)", edge[:type])
465
- edge[:ref] = shorts[:val][safe] * edge[:ratio] if safe
507
+ edge[:ref] = shorts[:val][safe] * edge[:ratio] if safe
466
508
  end
467
509
  end
468
510
  end
@@ -471,52 +513,67 @@ module TBD
471
513
  end
472
514
 
473
515
  ##
474
- # Generate UA' summary.
516
+ # Generates multilingual UA' summary.
475
517
  #
476
518
  # @param date [Time] Time stamp
477
- # @param argh [Hash] TBD arguments
519
+ # @param [Hash] argh TBD arguments
520
+ # @option argh [#to_s] :seed OpenStudio file, e.g. "school23.osm"
521
+ # @option argh [#to_s] :ua_ref reference ruleset e.g. "code (Quebec)"
522
+ # @option argh [Hash] :surfaces set of TBD surfaces (see )
523
+ # @option argh [#to_s] :version OpenStudio SDK, e.g. "3.6.1"
524
+ # @option argh [Hash] :io TBD input/output variables (see TBD JSON schema)
478
525
  #
479
- # @return [Hash] multilingual binned values for UA' summary
526
+ # @return [Hash] binned values for UA' (see logs if empty)
480
527
  def ua_summary(date = Time.now, argh = {})
481
528
  mth = "TBD::#{__callee__}"
529
+ cl1 = Time
530
+ cl2 = String
531
+ cl3 = Hash
532
+ ua = {}
533
+ return mismatch("date", date, cl1, mth, DBG, ua) unless date.is_a?(cl1)
534
+ return mismatch("argh", argh, cl3, mth, DBG, ua) unless argh.is_a?(cl3)
535
+
536
+ argh[:seed ] = "" unless argh.key?(:seed)
537
+ argh[:ua_ref ] = "" unless argh.key?(:ua_ref)
538
+ argh[:surfaces] = nil unless argh.key?(:surfaces)
539
+ argh[:version ] = "" unless argh.key?(:version)
540
+ argh[:io ] = {} unless argh.key?(:io)
541
+
542
+ file = argh[:seed ]
543
+ ref = argh[:ua_ref ]
544
+ s = argh[:surfaces]
545
+ v = argh[:version ]
546
+ io = argh[:io ]
547
+ return mismatch( "seed", file, cl2, mth, DBG, ua) unless file.is_a?(cl2)
548
+ return mismatch( "UA' ref", ref, cl2, mth, DBG, ua) unless ref.is_a?(cl2)
549
+ return mismatch( "version", v, cl2, mth, DBG, ua) unless v.is_a?(cl2)
550
+ return mismatch("surfaces", s, cl3, mth, DBG, ua) unless s.is_a?(cl3)
551
+ return mismatch( "io", io, cl3, mth, DBG, ua) unless io.is_a?(cl3)
552
+ return empty( "surfaces", mth, WRN, ua) if s.empty?
482
553
 
483
- ua = {}
484
- argh = {} unless argh.is_a?(Hash )
485
- argh[:seed ] = "" unless argh.key?(:seed )
486
- argh[:ua_ref ] = "" unless argh.key?(:ua_ref )
487
- argh[:surfaces ] = nil unless argh.key?(:surfaces )
488
- argh[:version ] = "" unless argh.key?(:version )
489
- argh[:io ] = {} unless argh.key?(:io )
490
554
  argh[:io][:description] = "" unless argh[:io].key?(:description)
491
-
492
- descr = argh[:io][:description]
493
- file = argh[:seed ]
494
- version = argh[:version ]
495
- s = argh[:surfaces ]
496
-
497
- return mismatch("TBD surfaces", s, Hash, mth, DBG, ua) unless s.is_a?(Hash)
498
- return empty("TBD Surfaces", mth, WRN, ua) if s.empty?
555
+ descr = argh[:io][:description]
499
556
 
500
557
  ua[:descr ] = ""
501
558
  ua[:file ] = ""
502
559
  ua[:version] = ""
503
560
  ua[:model ] = "∑U•A + ∑PSI•L + ∑KHI•n"
504
561
  ua[:date ] = date
505
- ua[:descr ] = descr unless descr.nil? || descr.empty?
506
- ua[:file ] = file unless file.nil? || file.empty?
507
- ua[:version] = version unless version.nil? || version.empty?
562
+ ua[:descr ] = descr unless descr.nil? || descr.empty?
563
+ ua[:file ] = file unless file.nil? || file.empty?
564
+ ua[:version] = v unless v.nil? || v.empty?
508
565
 
509
566
  [:en, :fr].each { |lang| ua[lang] = {} }
510
567
 
511
- ua[:en][:notes] = "Automated assessment from the OpenStudio Measure, " \
512
- "Thermal Bridging and Derating (TBD). Open source and MIT-licensed, " \
513
- "TBD is provided as is (without warranty). Procedures are documented " \
568
+ ua[:en][:notes] = "Automated assessment from the OpenStudio Measure, "\
569
+ "Thermal Bridging and Derating (TBD). Open source and MIT-licensed, "\
570
+ "TBD is provided as is (without warranty). Procedures are documented "\
514
571
  "in the source code: https://github.com/rd2/tbd. "
515
572
 
516
- ua[:fr][:notes] = "Analyse automatisée à partir de la measure OpenStudio, "\
517
- "'Thermal Bridging and Derating' (ou TBD). Distribuée librement " \
518
- "(licence MIT), TBD est offerte telle quelle (sans garantie). " \
519
- "L'approche est documentée au sein du code source : " \
573
+ ua[:fr][:notes] = "Analyse automatisée à partir de la measure "\
574
+ "OpenStudio, 'Thermal Bridging and Derating' (ou TBD). Distribuée "\
575
+ "librement (licence MIT), TBD est offerte telle quelle (sans "\
576
+ "garantie). L'approche est documentée au sein du code source : "\
520
577
  "https://github.com/rd2/tbd."
521
578
 
522
579
  walls = { net: 0, gross: 0, subs: 0 }
@@ -527,17 +584,17 @@ module TBD
527
584
  val = {}
528
585
  psi = PSI.new
529
586
 
530
- unless argh[:ua_ref].empty?
531
- shorts = psi.shorthands(argh[:ua_ref])
587
+ unless ref.empty?
588
+ shorts = psi.shorthands(ref)
532
589
  empty = shorts[:has].empty? && shorts[:val].empty?
533
- has = shorts[:has] unless empty
534
- val = shorts[:val] unless empty
535
- log(ERR, "Invalid UA' reference set (#{mth})") if empty
590
+ has = shorts[:has] unless empty
591
+ val = shorts[:val] unless empty
592
+ log(ERR, "Invalid UA' reference set (#{mth})") if empty
536
593
 
537
594
  unless empty
538
- ua[:model] += " : Design vs '#{argh[:ua_ref]}'"
595
+ ua[:model] += " : Design vs '#{ref}'"
539
596
 
540
- case argh[:ua_ref]
597
+ case ref
541
598
  when "code (Quebec)"
542
599
  ua[:en][:objective] = "COMPLIANCE ASSESSMENT"
543
600
  ua[:en][:details ] = []
@@ -546,9 +603,9 @@ module TBD
546
603
  ua[:en][:details ] << "Division B, Section 3.3"
547
604
  ua[:en][:details ] << "Building Envelope Trade-off Path"
548
605
 
549
- ua[:en][:notes] << " Calculations comply with Section 3.3 " \
550
- "requirements. Results are based on user input not subject to " \
551
- "prior validation (see DESCRIPTION), and as such the assessment " \
606
+ ua[:en][:notes] << " Calculations comply with Section 3.3 "\
607
+ "requirements. Results are based on user input not subject to "\
608
+ "prior validation (see DESCRIPTION), and as such the assessment "\
552
609
  "shall not be considered as a certification of compliance."
553
610
 
554
611
  ua[:fr][:objective] = "ANALYSE DE CONFORMITÉ"
@@ -558,10 +615,10 @@ module TBD
558
615
  ua[:fr][:details ] << "Division B, Section 3.3"
559
616
  ua[:fr][:details ] << "Méthode des solutions de remplacement"
560
617
 
561
- ua[:fr][:notes] << " Les calculs sont conformes aux dispositions de "\
562
- "la Section 3.3. Les résultats sont tributaires d'intrants " \
563
- "fournis par l'utilisateur, sans validation préalable (voir " \
564
- "DESCRIPTION). Ce document ne peut constituer une attestation de " \
618
+ ua[:fr][:notes] << " Les calculs sont conformes aux dispositions "\
619
+ "de la Section 3.3. Les résultats sont tributaires d'intrants "\
620
+ "fournis par l'utilisateur, sans validation préalable (voir "\
621
+ "DESCRIPTION). Ce document ne peut constituer une attestation de "\
565
622
  "conformité."
566
623
  else
567
624
  ua[:en][:objective] = "UA'"
@@ -582,26 +639,28 @@ module TBD
582
639
  blc = { walls: 0, roofs: 0, floors: 0, doors: 0,
583
640
  windows: 0, skylights: 0, rimjoists: 0, parapets: 0,
584
641
  trim: 0, corners: 0, balconies: 0, grade: 0,
585
- other: 0 } # includes party wall edges, expansion joints, etc.
642
+ other: 0 } # party edges, expansion joints, spandrel edges, etc.
586
643
 
587
644
  b1 = {}
588
645
  b2 = {}
589
- b1[:pro] = blc # proposed design
590
- b1[:ref] = blc.clone # reference
591
- b2[:pro] = blc.clone # proposed design
592
- b2[:ref] = blc.clone # reference
646
+ b1[:pro] = blc # proposed design
647
+ b1[:ref] = blc.clone # reference
648
+ b2[:pro] = blc.clone # proposed design
649
+ b2[:ref] = blc.clone # reference
593
650
 
594
651
  # Loop through surfaces, subsurfaces and edges and populate bloc1 & bloc2.
595
- argh[:surfaces].each do |id, surface|
652
+ s.each do |id, surface|
596
653
  next unless surface.key?(:deratable)
597
654
  next unless surface[:deratable]
598
655
  next unless surface.key?(:type)
656
+
599
657
  type = surface[:type]
600
- next unless type == :wall || type == :ceiling || type == :floor
658
+ next unless [:wall, :ceiling, :floor].include?(type)
601
659
  next unless surface.key?(:net)
602
660
  next unless surface[:net] > TOL
603
661
  next unless surface.key?(:u)
604
662
  next unless surface[:u] > TOL
663
+
605
664
  heating = 21.0
606
665
  heating = surface[:heating] if surface.key?(:heating)
607
666
  bloc = b1
@@ -611,18 +670,18 @@ module TBD
611
670
  if type == :wall
612
671
  areas[:walls][:net ] += surface[:net]
613
672
  bloc[:pro][:walls ] += surface[:net] * surface[:u ]
614
- bloc[:ref][:walls ] += surface[:net] * surface[:ref] if reference
615
- bloc[:ref][:walls ] += surface[:net] * surface[:u ] unless reference
673
+ bloc[:ref][:walls ] += surface[:net] * surface[:ref] if reference
674
+ bloc[:ref][:walls ] += surface[:net] * surface[:u ] unless reference
616
675
  elsif type == :ceiling
617
676
  areas[:roofs][:net ] += surface[:net]
618
677
  bloc[:pro][:roofs ] += surface[:net] * surface[:u ]
619
- bloc[:ref][:roofs ] += surface[:net] * surface[:ref] if reference
620
- bloc[:ref][:roofs ] += surface[:net] * surface[:u ] unless reference
678
+ bloc[:ref][:roofs ] += surface[:net] * surface[:ref] if reference
679
+ bloc[:ref][:roofs ] += surface[:net] * surface[:u ] unless reference
621
680
  else
622
681
  areas[:floors][:net] += surface[:net]
623
682
  bloc[:pro][:floors ] += surface[:net] * surface[:u ]
624
- bloc[:ref][:floors ] += surface[:net] * surface[:ref] if reference
625
- bloc[:ref][:floors ] += surface[:net] * surface[:u ] unless reference
683
+ bloc[:ref][:floors ] += surface[:net] * surface[:ref] if reference
684
+ bloc[:ref][:floors ] += surface[:net] * surface[:u ] unless reference
626
685
  end
627
686
 
628
687
  [:doors, :windows, :skylights].each do |subs|
@@ -635,13 +694,13 @@ module TBD
635
694
  next unless sub[:u ] > TOL
636
695
 
637
696
  gross = sub[:gross]
638
- gross *= sub[:mult ] if sub.key?(:mult)
639
- areas[:walls ][:subs] += gross if type == :wall
640
- areas[:roofs ][:subs] += gross if type == :ceiling
641
- areas[:floors][:subs] += gross if type == :floor
697
+ gross *= sub[:mult ] if sub.key?(:mult)
698
+ areas[:walls ][:subs] += gross if type == :wall
699
+ areas[:roofs ][:subs] += gross if type == :ceiling
700
+ areas[:floors][:subs] += gross if type == :floor
642
701
  bloc[:pro ][subs ] += gross * sub[:u ]
643
- bloc[:ref ][subs ] += gross * sub[:ref] if sub.key?(:ref)
644
- bloc[:ref ][subs ] += gross * sub[:u ] unless sub.key?(:ref)
702
+ bloc[:ref ][subs ] += gross * sub[:ref] if sub.key?(:ref)
703
+ bloc[:ref ][subs ] += gross * sub[:u ] unless sub.key?(:ref)
645
704
  end
646
705
  end
647
706
 
@@ -653,52 +712,67 @@ module TBD
653
712
  next unless edge.key?(:psi)
654
713
 
655
714
  loss = edge[:length] * edge[:psi]
656
- type = edge[:type].to_s
715
+ type = edge[:type].to_s.downcase
657
716
 
658
- case type
659
- when /rimjoist/i
660
- bloc[:pro][:rimjoists] += loss
661
- when /parapet/i
662
- bloc[:pro][:parapets ] += loss
663
- when /fenestration/i
717
+ if edge[:type].to_s.downcase.include?("balcony")
718
+ bloc[:pro][:balconies] += loss
719
+ elsif edge[:type].to_s.downcase.include?("door")
720
+ bloc[:pro][:trim ] += loss
721
+ elsif edge[:type].to_s.downcase.include?("skylight")
664
722
  bloc[:pro][:trim ] += loss
665
- when /head/i
723
+ elsif edge[:type].to_s.downcase.include?("fenestration")
666
724
  bloc[:pro][:trim ] += loss
667
- when /sill/i
725
+ elsif edge[:type].to_s.downcase.include?("head")
668
726
  bloc[:pro][:trim ] += loss
669
- when /jamb/i
727
+ elsif edge[:type].to_s.downcase.include?("sill")
670
728
  bloc[:pro][:trim ] += loss
671
- when /corner/i
729
+ elsif edge[:type].to_s.downcase.include?("jamb")
730
+ bloc[:pro][:trim ] += loss
731
+ elsif edge[:type].to_s.downcase.include?("rimjoist")
732
+ bloc[:pro][:rimjoists] += loss
733
+ elsif edge[:type].to_s.downcase.include?("parapet")
734
+ bloc[:pro][:parapets ] += loss
735
+ elsif edge[:type].to_s.downcase.include?("roof")
736
+ bloc[:pro][:parapets ] += loss
737
+ elsif edge[:type].to_s.downcase.include?("corner")
672
738
  bloc[:pro][:corners ] += loss
673
- when /grade/i
739
+ elsif edge[:type].to_s.downcase.include?("grade")
674
740
  bloc[:pro][:grade ] += loss
675
741
  else
676
742
  bloc[:pro][:other ] += loss
677
743
  end
678
744
 
679
745
  next if val.empty?
680
- next if argh[:ua_ref].empty?
681
- safer = psi.safe(argh[:ua_ref], edge[:type])
746
+ next if ref.empty?
747
+
748
+ safer = psi.safe(ref, edge[:type])
682
749
  ok = edge.key?(:ref)
683
- loss = edge[:length] * edge[:ref] if ok
684
- loss = edge[:length] * val[safer] * edge[:ratio] unless ok
750
+ loss = edge[:length] * edge[:ref] if ok
751
+ loss = edge[:length] * val[safer] * edge[:ratio] unless ok
685
752
 
686
- case safer.to_s
687
- when /rimjoist/i
688
- bloc[:ref][:rimjoists] += loss
689
- when /parapet/i
690
- bloc[:ref][:parapets ] += loss
691
- when /fenestration/i
753
+ if edge[:type].to_s.downcase.include?("balcony")
754
+ bloc[:ref][:balconies] += loss
755
+ elsif edge[:type].to_s.downcase.include?("door")
692
756
  bloc[:ref][:trim ] += loss
693
- when /head/i
757
+ elsif edge[:type].to_s.downcase.include?("skylight")
694
758
  bloc[:ref][:trim ] += loss
695
- when /sill/i
759
+ elsif edge[:type].to_s.downcase.include?("fenestration")
696
760
  bloc[:ref][:trim ] += loss
697
- when /jamb/i
761
+ elsif edge[:type].to_s.downcase.include?("head")
698
762
  bloc[:ref][:trim ] += loss
699
- when /corner/i
763
+ elsif edge[:type].to_s.downcase.include?("sill")
764
+ bloc[:ref][:trim ] += loss
765
+ elsif edge[:type].to_s.downcase.include?("jamb")
766
+ bloc[:ref][:trim ] += loss
767
+ elsif edge[:type].to_s.downcase.include?("rimjoist")
768
+ bloc[:ref][:rimjoists] += loss
769
+ elsif edge[:type].to_s.downcase.include?("parapet")
770
+ bloc[:ref][:parapets ] += loss
771
+ elsif edge[:type].to_s.downcase.include?("roof")
772
+ bloc[:ref][:parapets ] += loss
773
+ elsif edge[:type].to_s.downcase.include?("corner")
700
774
  bloc[:ref][:corners ] += loss
701
- when /grade/i
775
+ elsif edge[:type].to_s.downcase.include?("grade")
702
776
  bloc[:ref][:grade ] += loss
703
777
  else
704
778
  bloc[:ref][:other ] += loss
@@ -710,8 +784,10 @@ module TBD
710
784
  surface[:pts].values.each do |pts|
711
785
  next unless pts.key?(:val)
712
786
  next unless pts.key?(:n)
787
+
713
788
  bloc[:pro][:other] += pts[:val] * pts[:n]
714
789
  next unless pts.key?(:ref)
790
+
715
791
  bloc[:ref][:other] += pts[:ref] * pts[:n]
716
792
  end
717
793
  end
@@ -735,97 +811,69 @@ module TBD
735
811
  ua[lang][b] = {}
736
812
 
737
813
  if b == :b1
738
- ua[:en][b][:summary] = "heated : #{str}" if lang == :en
739
- ua[:fr][b][:summary] = "chauffé : #{str}" if lang == :fr
814
+ ua[:en][b][:summary] = "heated : #{str}" if lang == :en
815
+ ua[:fr][b][:summary] = "chauffé : #{str}" if lang == :fr
740
816
  else
741
- ua[:en][b][:summary] = "semi-heated : #{str}" if lang == :en
742
- ua[:fr][b][:summary] = "semi-chauffé : #{str}" if lang == :fr
817
+ ua[:en][b][:summary] = "semi-heated : #{str}" if lang == :en
818
+ ua[:fr][b][:summary] = "semi-chauffé : #{str}" if lang == :fr
743
819
  end
744
820
 
745
- # ** https://bugs.ruby-lang.org/issues/13761 (Ruby > 2.2.5)
746
- # str += format(" +%.1f%", ratio) if ratio && pro_sum > ref_sum ... now:
747
- # str += format(" +%.1f%%", ratio) if ratio && pro_sum > ref_sum
748
-
749
821
  bloc[:pro].each do |k, v|
750
822
  rf = bloc[:ref][k]
751
823
  next if v < TOL && rf < TOL
752
824
  ratio = nil
753
- ratio = (100.0 * (v - rf) / rf).abs if rf > TOL
825
+ ratio = (100.0 * (v - rf) / rf).abs if rf > TOL
754
826
  str = format("%.1f W/K (vs %.1f W/K)", v, rf)
755
- str += format(" +%.1f%%", ratio) if ratio && v > rf
756
- str += format(" -%.1f%%", ratio) if ratio && v < rf
827
+ str += format(" +%.1f%%", ratio) if ratio && v > rf
828
+ str += format(" -%.1f%%", ratio) if ratio && v < rf
757
829
 
758
830
  case k
759
831
  when :walls
760
- ua[:en][b][k] = "walls : #{str}" if lang == :en
761
- ua[:fr][b][k] = "murs : #{str}" if lang == :fr
832
+ ua[:en][b][k] = "walls : #{str}" if lang == :en
833
+ ua[:fr][b][k] = "murs : #{str}" if lang == :fr
762
834
  when :roofs
763
- ua[:en][b][k] = "roofs : #{str}" if lang == :en
764
- ua[:fr][b][k] = "toits : #{str}" if lang == :fr
835
+ ua[:en][b][k] = "roofs : #{str}" if lang == :en
836
+ ua[:fr][b][k] = "toits : #{str}" if lang == :fr
765
837
  when :floors
766
- ua[:en][b][k] = "floors : #{str}" if lang == :en
767
- ua[:fr][b][k] = "planchers : #{str}" if lang == :fr
838
+ ua[:en][b][k] = "floors : #{str}" if lang == :en
839
+ ua[:fr][b][k] = "planchers : #{str}" if lang == :fr
768
840
  when :doors
769
- ua[:en][b][k] = "doors : #{str}" if lang == :en
770
- ua[:fr][b][k] = "portes : #{str}" if lang == :fr
841
+ ua[:en][b][k] = "doors : #{str}" if lang == :en
842
+ ua[:fr][b][k] = "portes : #{str}" if lang == :fr
771
843
  when :windows
772
- ua[:en][b][k] = "windows : #{str}" if lang == :en
773
- ua[:fr][b][k] = "fenêtres : #{str}" if lang == :fr
844
+ ua[:en][b][k] = "windows : #{str}" if lang == :en
845
+ ua[:fr][b][k] = "fenêtres : #{str}" if lang == :fr
774
846
  when :skylights
775
- ua[:en][b][k] = "skylights : #{str}" if lang == :en
776
- ua[:fr][b][k] = "lanterneaux : #{str}" if lang == :fr
847
+ ua[:en][b][k] = "skylights : #{str}" if lang == :en
848
+ ua[:fr][b][k] = "lanterneaux : #{str}" if lang == :fr
777
849
  when :rimjoists
778
- ua[:en][b][k] = "rimjoists : #{str}" if lang == :en
779
- ua[:fr][b][k] = "rives : #{str}" if lang == :fr
850
+ ua[:en][b][k] = "rimjoists : #{str}" if lang == :en
851
+ ua[:fr][b][k] = "rives : #{str}" if lang == :fr
780
852
  when :parapets
781
- ua[:en][b][k] = "parapets : #{str}" if lang == :en
782
- ua[:fr][b][k] = "parapets : #{str}" if lang == :fr
853
+ ua[:en][b][k] = "parapets : #{str}" if lang == :en
854
+ ua[:fr][b][k] = "parapets : #{str}" if lang == :fr
783
855
  when :trim
784
- ua[:en][b][k] = "trim : #{str}" if lang == :en
785
- ua[:fr][b][k] = "chassis : #{str}" if lang == :fr
856
+ ua[:en][b][k] = "trim : #{str}" if lang == :en
857
+ ua[:fr][b][k] = "chassis : #{str}" if lang == :fr
786
858
  when :corners
787
- ua[:en][b][k] = "corners : #{str}" if lang == :en
788
- ua[:fr][b][k] = "coins : #{str}" if lang == :fr
859
+ ua[:en][b][k] = "corners : #{str}" if lang == :en
860
+ ua[:fr][b][k] = "coins : #{str}" if lang == :fr
789
861
  when :balconies
790
- ua[:en][b][k] = "balconies : #{str}" if lang == :en
791
- ua[:fr][b][k] = "balcons : #{str}" if lang == :fr
862
+ ua[:en][b][k] = "balconies : #{str}" if lang == :en
863
+ ua[:fr][b][k] = "balcons : #{str}" if lang == :fr
792
864
  when :grade
793
- ua[:en][b][k] = "grade : #{str}" if lang == :en
794
- ua[:fr][b][k] = "tracé : #{str}" if lang == :fr
865
+ ua[:en][b][k] = "grade : #{str}" if lang == :en
866
+ ua[:fr][b][k] = "tracé : #{str}" if lang == :fr
795
867
  else
796
- ua[:en][b][k] = "other : #{str}" if lang == :en
797
- ua[:fr][b][k] = "autres : #{str}" if lang == :fr
868
+ ua[:en][b][k] = "other : #{str}" if lang == :en
869
+ ua[:fr][b][k] = "autres : #{str}" if lang == :fr
798
870
  end
799
871
  end
800
872
 
801
- # Deterministic sorting
873
+ # Deterministic sorting.
802
874
  ua[lang][b][:summary] = ua[lang][b].delete(:summary)
803
- ok = ua[lang][b].key?(:walls)
804
- ua[lang][b][:walls] = ua[lang][b].delete(:walls) if ok
805
- ok = ua[lang][b].key?(:roofs)
806
- ua[lang][b][:roofs] = ua[lang][b].delete(:roofs) if ok
807
- ok = ua[lang][b].key?(:floors)
808
- ua[lang][b][:floors] = ua[lang][b].delete(:floors) if ok
809
- ok = ua[lang][b].key?(:doors)
810
- ua[lang][b][:doors] = ua[lang][b].delete(:doors) if ok
811
- ok = ua[lang][b].key?(:windows)
812
- ua[lang][b][:windows] = ua[lang][b].delete(:windows) if ok
813
- ok = ua[lang][b].key?(:skylights)
814
- ua[lang][b][:skylights] = ua[lang][b].delete(:skylights) if ok
815
- ok = ua[lang][b].key?(:rimjoists)
816
- ua[lang][b][:rimjoists] = ua[lang][b].delete(:rimjoists) if ok
817
- ok = ua[lang][b].key?(:parapets)
818
- ua[lang][b][:parapets] = ua[lang][b].delete(:parapets) if ok
819
- ok = ua[lang][b].key?(:trim)
820
- ua[lang][b][:trim] = ua[lang][b].delete(:trim) if ok
821
- ok = ua[lang][b].key?(:corners)
822
- ua[lang][b][:corners] = ua[lang][b].delete(:corners) if ok
823
- ok = ua[lang][b].key?(:balconies)
824
- ua[lang][b][:balconies] = ua[lang][b].delete(:balconies) if ok
825
- ok = ua[lang][b].key?(:grade)
826
- ua[lang][b][:grade] = ua[lang][b].delete(:grade) if ok
827
- ok = ua[lang][b].key?(:other)
828
- ua[lang][b][:other] = ua[lang][b].delete(:other) if ok
875
+
876
+ ua[lang][b].keys.each { |k| ua[lang][b][k] = ua[lang][b].delete(k) }
829
877
  end
830
878
  end
831
879
  end
@@ -834,62 +882,85 @@ module TBD
834
882
  areas[:walls ][:gross] = areas[:walls ][:net] + areas[:walls ][:subs]
835
883
  areas[:roofs ][:gross] = areas[:roofs ][:net] + areas[:roofs ][:subs]
836
884
  areas[:floors][:gross] = areas[:floors][:net] + areas[:floors][:subs]
885
+
837
886
  ua[:en][:areas] = {}
838
887
  ua[:fr][:areas] = {}
839
888
 
840
889
  str = format("walls : %.1f m2 (net)", areas[:walls][:net])
841
890
  str += format(", %.1f m2 (gross)", areas[:walls][:gross])
842
- ua[:en][:areas][:walls] = str unless areas[:walls][:gross] < TOL
891
+ ua[:en][:areas][:walls] = str unless areas[:walls ][:gross] < TOL
892
+
843
893
  str = format("roofs : %.1f m2 (net)", areas[:roofs][:net])
844
894
  str += format(", %.1f m2 (gross)", areas[:roofs][:gross])
845
- ua[:en][:areas][:roofs] = str unless areas[:roofs][:gross] < TOL
895
+ ua[:en][:areas][:roofs] = str unless areas[:roofs ][:gross] < TOL
896
+
846
897
  str = format("floors : %.1f m2 (net)", areas[:floors][:net])
847
898
  str += format(", %.1f m2 (gross)", areas[:floors][:gross])
848
- ua[:en][:areas][:floors] = str unless areas[:floors][:gross] < TOL
899
+ ua[:en][:areas][:floors] = str unless areas[:floors][:gross] < TOL
849
900
 
850
901
  str = format("murs : %.1f m2 (net)", areas[:walls][:net])
851
902
  str += format(", %.1f m2 (brut)", areas[:walls][:gross])
852
- ua[:fr][:areas][:walls] = str unless areas[:walls][:gross] < TOL
903
+ ua[:fr][:areas][:walls] = str unless areas[:walls ][:gross] < TOL
904
+
853
905
  str = format("toits : %.1f m2 (net)", areas[:roofs][:net])
854
906
  str += format(", %.1f m2 (brut)", areas[:roofs][:gross])
855
- ua[:fr][:areas][:roofs] = str unless areas[:roofs][:gross] < TOL
907
+ ua[:fr][:areas][:roofs] = str unless areas[:roofs ][:gross] < TOL
908
+
856
909
  str = format("planchers : %.1f m2 (net)", areas[:floors][:net])
857
910
  str += format(", %.1f m2 (brut)", areas[:floors][:gross])
858
- ua[:fr][:areas][:floors] = str unless areas[:floors][:gross] < TOL
911
+ ua[:fr][:areas][:floors] = str unless areas[:floors][:gross] < TOL
859
912
 
860
913
  ua
861
914
  end
862
915
 
863
916
  ##
864
- # Generate MD-formatted file.
917
+ # Generates MD-formatted, UA' summary file.
865
918
  #
866
- # @param ua [Hash] preprocessed collection of UA-related strings
867
- # @param lang [String] preferred language ("en" vs "fr")
919
+ # @param [#key?] ua preprocessed collection of UA-related strings
920
+ # option ua [#to_s] :objective ua[lang][:objective] = "COMPLIANCE [...]"
921
+ # option ua [#&] :details ua[lang][:details] = "QC Energy Code [...]"
922
+ # option ua [#to_s] :model "∑U•A + ∑PSI•L + ∑KHI•n [...]"
923
+ # option ua [#key?] :b1 TB block of CONDITIONED spaces, ua[lang][:b1]
924
+ # option ua [#key?] :b2 TB block of SEMIHEATED spaces, ua[lang][:b2]
925
+ # option ua [#to_s] :descr user-provided project/summary description
926
+ # option ua [#to_s] :file OpenStudio file, e.g. "school23.osm"
927
+ # option ua [#to_s] :version OpenStudio SDK, e.g. "3.6.1"
928
+ # option ua [Time] :date time signature
929
+ # option ua [#to_s] :notes advisory info, ua[lang][:notes]
930
+ # option ua [#key?] :areas binned areas (String), ua[lang][:areas][:walls]
931
+ # @param lang [#to_sym] selected language, :en or :fr
868
932
  #
869
- # @return [Array] MD-formatted strings (empty if invalid inputs)
933
+ # @return [Array<String>] MD-formatted strings (see logs if empty)
870
934
  def ua_md(ua = {}, lang = :en)
871
935
  mth = "TBD::#{__callee__}"
872
936
  report = []
937
+ ck1 = ua.respond_to?(:key?)
938
+ ck2 = lang.respond_to?(:to_sym)
939
+ return mismatch( "ua", ua, Hash, mth, DBG, report) unless ck1
940
+ return mismatch("lang", lang, Symbol, mth, DBG, report) unless ck2
873
941
 
874
- return mismatch("ua", ua, Hash, mth, DBG, report) unless ua.is_a?(Hash)
875
- return empty("ua", mth, DBG, report) if ua.empty?
876
- return hashkey("", ua, lang, mth, DBG, report) unless ua.key?(lang)
942
+ lang = lang.to_sym
943
+ return hashkey("language", ua, lang, mth, DBG, report) unless ua.key?(lang)
944
+ return empty("ua" , mth, DBG, report) if ua.empty?
877
945
 
878
- if ua[lang].key?(:objective)
879
- report << "# #{ua[lang][:objective]} "
946
+ if ua[lang].key?(:objective) && ua[lang][:objective].respond_to?(:to_s)
947
+ report << "# #{ua[lang][:objective].to_s} "
880
948
  report << " "
881
949
  end
882
950
 
883
- if ua[lang].key?(:details)
884
- ua[lang][:details].each { |d| report << "#{d} " }
951
+ if ua[lang].key?(:details) && ua[lang][:details].respond_to?(:&)
952
+ ua[lang][:details].each do |d|
953
+ report << "#{d.to_s} " if d.respond_to?(:to_s)
954
+ end
955
+
885
956
  report << " "
886
957
  end
887
958
 
888
- if ua.key?(:model)
889
- report << "##### SUMMARY " if lang == :en
890
- report << "##### SOMMAIRE " if lang == :fr
959
+ if ua.key?(:model) && ua[:model].respond_to?(:to_s)
960
+ report << "##### SUMMARY " if lang == :en
961
+ report << "##### SOMMAIRE " if lang == :fr
891
962
  report << " "
892
- report << "#{ua[:model]} "
963
+ report << "#{ua[:model].to_s} "
893
964
  report << " "
894
965
  end
895
966
 
@@ -898,10 +969,10 @@ module TBD
898
969
  report << "* #{ua[lang][:b1][:summary]}"
899
970
 
900
971
  ua[lang][:b1].each do |k, v|
901
- next if k == :summary
902
- report << " * #{v}" unless k == last
903
- report << " * #{v} " if k == last
904
- report << " " if k == last
972
+ next if k == :summary
973
+ report << " * #{v}" unless k == last
974
+ report << " * #{v} " if k == last
975
+ report << " " if k == last
905
976
  end
906
977
  report << " "
907
978
  end
@@ -911,10 +982,10 @@ module TBD
911
982
  report << "* #{ua[lang][:b2][:summary]}"
912
983
 
913
984
  ua[lang][:b2].each do |k, v|
914
- next if k == :summary
915
- report << " * #{v}" unless k == last
916
- report << " * #{v} " if k == last
917
- report << " " if k == last
985
+ next if k == :summary
986
+ report << " * #{v}" unless k == last
987
+ report << " * #{v} " if k == last
988
+ report << " " if k == last
918
989
  end
919
990
  report << " "
920
991
  end
@@ -922,36 +993,36 @@ module TBD
922
993
  if ua.key?(:date)
923
994
  report << "##### DESCRIPTION "
924
995
  report << " "
925
- report << "* project : #{ua[:descr]}" if ua.key?(:descr) && lang == :en
926
- report << "* projet : #{ua[:descr]}" if ua.key?(:descr) && lang == :fr
996
+ report << "* project : #{ua[:descr]}" if ua.key?(:descr) && lang == :en
997
+ report << "* projet : #{ua[:descr]}" if ua.key?(:descr) && lang == :fr
927
998
  model = ""
928
- model = "* model : #{ua[:file]}" if ua.key?(:file) && lang == :en
929
- model = "* modèle : #{ua[:file]}" if ua.key?(:file) && lang == :fr
930
- model += " (v#{ua[:version]})" if ua.key?(:version)
931
- report << model unless model.empty?
932
- report << "* TBD : v3.2.3"
999
+ model = "* model : #{ua[:file]}" if ua.key?(:file) && lang == :en
1000
+ model = "* modèle : #{ua[:file]}" if ua.key?(:file) && lang == :fr
1001
+ model += " (v#{ua[:version]})" if ua.key?(:version)
1002
+ report << model unless model.empty?
1003
+ report << "* TBD : v3.3.0"
933
1004
  report << "* date : #{ua[:date]}"
934
1005
 
935
1006
  if lang == :en
936
- report << "* status : #{msg(status)}" unless status.zero?
937
- report << "* status : success !" if status.zero?
1007
+ report << "* status : #{msg(status)}" unless status.zero?
1008
+ report << "* status : success !" if status.zero?
938
1009
  elsif lang == :fr
939
- report << "* statut : #{msg(status)}" unless status.zero?
940
- report << "* statut : succès !" if status.zero?
1010
+ report << "* statut : #{msg(status)}" unless status.zero?
1011
+ report << "* statut : succès !" if status.zero?
941
1012
  end
942
1013
  report << " "
943
1014
  end
944
1015
 
945
1016
  if ua[lang].key?(:areas)
946
- report << "##### AREAS " if lang == :en
947
- report << "##### AIRES " if lang == :fr
1017
+ report << "##### AREAS " if lang == :en
1018
+ report << "##### AIRES " if lang == :fr
948
1019
  report << " "
949
1020
  ok = ua[lang][:areas].key?(:walls)
950
- report << "* #{ua[lang][:areas][:walls]}" if ok
1021
+ report << "* #{ua[lang][:areas][:walls]}" if ok
951
1022
  ok = ua[lang][:areas].key?(:roofs)
952
- report << "* #{ua[lang][:areas][:roofs]}" if ok
1023
+ report << "* #{ua[lang][:areas][:roofs]}" if ok
953
1024
  ok = ua[lang][:areas].key?(:floors)
954
- report << "* #{ua[lang][:areas][:floors]}" if ok
1025
+ report << "* #{ua[lang][:areas][:floors]}" if ok
955
1026
  report << " "
956
1027
  end
957
1028