tbd 3.2.3 → 3.3.0

Sign up to get free protection for your applications and to get access to all the features.
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