tbd 3.0.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.
Files changed (45) hide show
  1. checksums.yaml +7 -0
  2. data/.gitattributes +3 -0
  3. data/.github/workflows/pull_request.yml +72 -0
  4. data/.gitignore +23 -0
  5. data/.rspec +3 -0
  6. data/Gemfile +3 -0
  7. data/LICENSE.md +21 -0
  8. data/README.md +154 -0
  9. data/Rakefile +60 -0
  10. data/json/midrise.json +64 -0
  11. data/json/tbd_5ZoneNoHVAC.json +19 -0
  12. data/json/tbd_5ZoneNoHVAC_btap.json +91 -0
  13. data/json/tbd_seb_n2.json +41 -0
  14. data/json/tbd_seb_n4.json +57 -0
  15. data/json/tbd_warehouse10.json +24 -0
  16. data/json/tbd_warehouse5.json +37 -0
  17. data/lib/measures/tbd/LICENSE.md +21 -0
  18. data/lib/measures/tbd/README.md +136 -0
  19. data/lib/measures/tbd/README.md.erb +42 -0
  20. data/lib/measures/tbd/docs/.gitkeep +1 -0
  21. data/lib/measures/tbd/measure.rb +327 -0
  22. data/lib/measures/tbd/measure.xml +460 -0
  23. data/lib/measures/tbd/resources/geo.rb +714 -0
  24. data/lib/measures/tbd/resources/geometry.rb +351 -0
  25. data/lib/measures/tbd/resources/model.rb +1431 -0
  26. data/lib/measures/tbd/resources/oslog.rb +381 -0
  27. data/lib/measures/tbd/resources/psi.rb +2229 -0
  28. data/lib/measures/tbd/resources/tbd.rb +55 -0
  29. data/lib/measures/tbd/resources/transformation.rb +121 -0
  30. data/lib/measures/tbd/resources/ua.rb +986 -0
  31. data/lib/measures/tbd/resources/utils.rb +1636 -0
  32. data/lib/measures/tbd/resources/version.rb +3 -0
  33. data/lib/measures/tbd/tests/tbd_full_PSI.json +17 -0
  34. data/lib/measures/tbd/tests/tbd_tests.rb +222 -0
  35. data/lib/tbd/geo.rb +714 -0
  36. data/lib/tbd/psi.rb +2229 -0
  37. data/lib/tbd/ua.rb +986 -0
  38. data/lib/tbd/version.rb +25 -0
  39. data/lib/tbd.rb +93 -0
  40. data/sponsors/canada.png +0 -0
  41. data/sponsors/quebec.png +0 -0
  42. data/tbd.gemspec +43 -0
  43. data/tbd.schema.json +571 -0
  44. data/v291_MacOS.md +110 -0
  45. metadata +191 -0
data/lib/tbd/ua.rb ADDED
@@ -0,0 +1,986 @@
1
+ # MIT License
2
+ #
3
+ # Copyright (c) 2020-2022 Denis Bourgeois & Dan Macumber
4
+ #
5
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ # of this software and associated documentation files (the "Software"), to deal
7
+ # in the Software without restriction, including without limitation the rights
8
+ # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ # copies of the Software, and to permit persons to whom the Software is
10
+ # furnished to do so, subject to the following conditions:
11
+ #
12
+ # The above copyright notice and this permission notice shall be included in all
13
+ # copies or substantial portions of the Software.
14
+ #
15
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ # SOFTWARE.
22
+
23
+ module TBD
24
+ ##
25
+ # Calculates construction Uo (including surface film resistances) to meet Ut.
26
+ #
27
+ # @param model [OpenStudio::Model::Model] a model
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]
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)
36
+ def uo(model = nil, lc = nil, id = "", hloss = 0.0, film = 0.0, ut = 0.0)
37
+ mth = "TBD::#{__callee__}"
38
+ res = { uo: nil, m: nil }
39
+ cl1 = OpenStudio::Model::Model
40
+ cl2 = OpenStudio::Model::LayeredConstruction
41
+ cl3 = Numeric
42
+
43
+ return mismatch("model", model, cl1, mth, DBG, res) unless model.is_a?(cl1)
44
+ return mismatch("id", id, String, mth, DBG, res) unless id.is_a?(String)
45
+ return mismatch("lc", lc, cl2, mth, DBG, res) unless lc.is_a?(cl2)
46
+ return mismatch("hloss", hloss, cl3, mth, DBG, res) unless hloss.is_a?(cl3)
47
+ return mismatch("film", film, cl3, mth, DBG, res) unless film.is_a?(cl3)
48
+ return mismatch("Ut", ut, cl3, mth, DBG, res) unless ut.is_a?(cl3)
49
+
50
+ lyr = insulatingLayer(lc)
51
+ lyr[:index] = nil unless lyr[:index].is_a?(Numeric)
52
+ lyr[:index] = nil unless lyr[:index] >= 0
53
+ lyr[:index] = nil unless lyr[:index] < lc.layers.size
54
+
55
+ return invalid("'#{id}' layer index", mth, 0, ERR, res) unless lyr[:index]
56
+ return zero("'#{id}': heatloss", mth, WRN, res) unless hloss > TOL
57
+ return zero("'#{id}': films", mth, WRN, res) unless film > TOL
58
+ return zero("'#{id}': Ut", mth, WRN, res) unless ut > TOL
59
+ return invalid("'#{id}': Ut", mth, 0, WRN, res) unless ut < 5.678
60
+
61
+ area = lc.getNetArea
62
+ return zero("'#{id}': net area (m2)", mth, ERR, res) unless area > TOL
63
+
64
+ # First, calculate initial layer RSi to initially meet Ut target.
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
68
+ new_u = 1 / new_r
69
+
70
+ # Then, uprate (if possible) to counter expected thermal bridging effects.
71
+ u_psi = hloss / area # from psi & khi
72
+ new_u = 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
+
75
+ return zero("'#{id}': new Rsi", mth, ERR, res) unless new_r > 0.001
76
+ loss = 0.0 # residual heatloss (not assigned) [W/K]
77
+
78
+ if lyr[:type] == :massless
79
+ m = lc.getLayer(lyr[:index]).to_MasslessOpaqueMaterial
80
+ return invalid("'#{id}' massless layer?", mth, 0) if m.empty?
81
+ m = m.get.clone(model).to_MasslessOpaqueMaterial.get
82
+ m.setName("#{id} uprated")
83
+ new_r = 0.001 unless new_r > 0.001
84
+ loss = (new_u - 1 / new_r) * area unless new_r > 0.001
85
+ m.setThermalResistance(new_r)
86
+ else # type == :standard
87
+ m = lc.getLayer(lyr[:index]).to_StandardOpaqueMaterial
88
+ return invalid("'#{id}' standard layer?", mth, 0) if m.empty?
89
+ m = m.get.clone(model).to_StandardOpaqueMaterial.get
90
+ m.setName("#{id} uprated")
91
+ k = m.thermalConductivity
92
+
93
+ if new_r > 0.001
94
+ d = new_r * k
95
+
96
+ unless d > 0.003
97
+ d = 0.003
98
+ k = d / new_r
99
+ k = 3.0 unless k < 3.0
100
+ loss = (new_u - k / d) * area unless k < 3.0
101
+ end
102
+ else # new_r < 0.001 m2.K/W
103
+ d = 0.001 * k
104
+ d = 0.003 unless d > 0.003
105
+ k = d / 0.001 unless d > 0.003
106
+ loss = (new_u - k / d) * area
107
+ end
108
+
109
+ ok = m.setThickness(d)
110
+ return invalid("Can't uprate '#{id}': > 3m", mth, 0, ERR, res) unless ok
111
+ m.setThermalConductivity(k) if ok
112
+ end
113
+
114
+ return invalid("", mth, 0, ERR, res) unless m
115
+ lc.setLayer(lyr[:index], m)
116
+ uo = 1 / rsi(lc, film)
117
+
118
+ if loss > TOL
119
+ h_loss = format "%.3f", loss
120
+ return invalid("Can't assign #{h_loss} W/K to '#{id}'", mth, 0, ERR, res)
121
+ end
122
+
123
+ res[:uo] = uo
124
+ res[:m ] = m
125
+
126
+ res
127
+ end
128
+
129
+ ##
130
+ # Uprate insulation layer of construction, based on user-selected Ut (argh).
131
+ #
132
+ # @param model [OpenStudio::Model::Model] a model
133
+ # @param s [Hash] preprocessed collection of TBD surfaces
134
+ # @param argh [Hash] TBD arguments
135
+ #
136
+ # @return [Bool] true if successfully uprated
137
+ def uprate(model = nil, s = {}, argh = {})
138
+ mth = "TBD::#{__callee__}"
139
+ cl1 = OpenStudio::Model::Model
140
+ cl2 = Hash
141
+ a = false
142
+
143
+ return mismatch("model", model, cl1, mth, DBG, a) unless model.is_a?(cl1)
144
+ return mismatch("surfaces", s, cl2, mth, DBG, a) unless s.is_a?(cl2)
145
+ return mismatch("argh", model, cl1, mth, DBG, a) unless argh.is_a?(cl2)
146
+
147
+ argh[:uprate_walls ] = false unless argh.key?(:uprate_walls )
148
+ argh[:uprate_roofs ] = false unless argh.key?(:uprate_roofs )
149
+ argh[:uprate_floors] = false unless argh.key?(:uprate_floors)
150
+ argh[:wall_ut ] = 5.678 unless argh.key?(:wall_ut )
151
+ argh[:roof_ut ] = 5.678 unless argh.key?(:roof_ut )
152
+ argh[:floor_ut ] = 5.678 unless argh.key?(:floor_ut )
153
+ argh[:wall_option ] = "" unless argh.key?(:wall_option )
154
+ argh[:roof_option ] = "" unless argh.key?(:roof_option )
155
+ argh[:floor_option ] = "" unless argh.key?(:floor_option )
156
+
157
+ groups = { wall: {}, roof: {}, floor: {} }
158
+ groups[:wall ][:up] = argh[:uprate_walls ]
159
+ groups[:roof ][:up] = argh[:uprate_roofs ]
160
+ groups[:floor][:up] = argh[:uprate_floors]
161
+ groups[:wall ][:ut] = argh[:wall_ut ]
162
+ groups[:roof ][:ut] = argh[:roof_ut ]
163
+ groups[:floor][:ut] = argh[:floor_ut ]
164
+ groups[:wall ][:op] = argh[:wall_option ]
165
+ groups[:roof ][:op] = argh[:roof_option ]
166
+ groups[:floor][:op] = argh[:floor_option ]
167
+
168
+ groups.each do |label, g|
169
+ next unless g[:up]
170
+ next unless g[:ut].is_a?(Numeric)
171
+ next unless g[:ut] < 5.678
172
+
173
+ coll = {}
174
+ area = 0
175
+ film = 100000000000000
176
+ lc = nil
177
+ uo = nil
178
+ id = ""
179
+ all = g[:op].downcase == "all wall constructions" ||
180
+ g[:op].downcase == "all roof constructions" ||
181
+ g[:op].downcase == "all floor constructions"
182
+
183
+ if g[:op].empty?
184
+ log(ERR, "Construction to uprate? (#{mth})")
185
+ elsif all
186
+ model.getSurfaces.each do |sss|
187
+ next unless sss.surfaceType.downcase.include?(label.to_s)
188
+ next unless sss.outsideBoundaryCondition.downcase == "outdoors"
189
+ next if sss.construction.empty?
190
+ next if sss.construction.get.to_LayeredConstruction.empty?
191
+ c = sss.construction.get.to_LayeredConstruction.get
192
+ i = c.nameString
193
+
194
+ # Reliable unless referenced by other surface types e.g. floor vs wall.
195
+ if c.getNetArea > area
196
+ area = c.getNetArea
197
+ lc = c
198
+ id = i
199
+ end
200
+
201
+ film = sss.filmResistance if sss.filmResistance < film
202
+ nom = sss.nameString
203
+ coll[i] = { area: c.getNetArea, lc: c, s: {} } unless coll.key?(i)
204
+ coll[i][:s][nom] = { a: sss.netArea } unless coll[i][:s].key?(nom)
205
+ end
206
+ else
207
+ id = g[:op]
208
+ c = model.getConstructionByName(id)
209
+
210
+ if c.empty?
211
+ log(ERR, "Construction '#{id}'? (#{mth})")
212
+ else
213
+ c = c.get.to_LayeredConstruction
214
+
215
+ if c.empty?
216
+ log(ERR, "'#{id}' layered construction? (#{mth})")
217
+ else
218
+ lc = c.get
219
+ area = lc.getNetArea
220
+ coll[id] = { area: area, lc: lc, s: {} }
221
+
222
+ model.getSurfaces.each do |sss|
223
+ next unless sss.surfaceType.downcase.include?(label.to_s)
224
+ next unless sss.outsideBoundaryCondition.downcase == "outdoors"
225
+ next if sss.construction.empty?
226
+ next if sss.construction.get.to_LayeredConstruction.empty?
227
+ lc = sss.construction.get.to_LayeredConstruction.get
228
+ next unless id == lc.nameString
229
+ nom = sss.nameString
230
+ film = sss.filmResistance if sss.filmResistance < film
231
+ ok = coll[id][:s].key?(nom)
232
+ coll[id][:s][nom] = { a: sss.netArea } unless ok
233
+ end
234
+ end
235
+ end
236
+ end
237
+
238
+ if coll.empty?
239
+ log(ERR, "No construction to uprate - skipping (#{mth})")
240
+ next
241
+ elsif lc # valid layered construction - good to uprate!
242
+ # Ensure lc is referenced by surface types == label.
243
+ model.getSurfaces.each do |sss|
244
+ next if sss.construction.empty?
245
+ next if sss.construction.get.to_LayeredConstruction.empty?
246
+ c = sss.construction.get.to_LayeredConstruction.get
247
+ i = c.nameString
248
+ next unless coll.key?(i)
249
+
250
+ unless sss.surfaceType.downcase.include?(label.to_s)
251
+ log(ERR, "Uprating #{label.to_s}, not '#{sss.nameString}' (#{mth})")
252
+ cloned = c.clone(model).to_LayeredConstruction.get
253
+ cloned.setName("'#{i}' cloned")
254
+ sss.setConstruction(cloned)
255
+ ok = s.key?(sss.nameString)
256
+ s[sss.nameString][:construction] = cloned if ok
257
+ coll[i][:s].delete(sss.nameString)
258
+ coll[i][:area] = c.getNetArea
259
+ next
260
+ end
261
+ end
262
+
263
+ lyr = insulatingLayer(lc)
264
+ lyr[:index] = nil unless lyr[:index].is_a?(Numeric)
265
+ lyr[:index] = nil unless lyr[:index] >= 0
266
+ lyr[:index] = nil unless lyr[:index] < lc.layers.size
267
+
268
+ log(ERR, "Insulation index for '#{id}'? (#{mth})") unless lyr[:index]
269
+ next unless lyr[:index]
270
+ hloss = 0 # sum of applicable psi & khi effects [W/K]
271
+
272
+ coll.each do |i, col|
273
+ next unless col.key?(:s)
274
+ next unless col.is_a?(Hash)
275
+
276
+ col[:s].keys.each do |nom|
277
+ next unless s.key?(nom)
278
+ next unless s[nom].key?(:deratable )
279
+ next unless s[nom].key?(:construction)
280
+ next unless s[nom].key?(:index )
281
+ next unless s[nom].key?(:ltype )
282
+ next unless s[nom].key?(:r )
283
+ next unless s[nom].key?(:type )
284
+
285
+ next unless s[nom][:deratable]
286
+ type = s[nom][:type].to_s.downcase
287
+ type = "roof" if type == "ceiling"
288
+ next unless type.include?(label.to_s)
289
+
290
+ # Tally applicable psi + khi.
291
+ hloss += s[nom][:heatloss] if s[nom].key?(:heatloss)
292
+
293
+ # Skip construction reassignment if already referencing right one.
294
+ unless s[nom][:construction] == lc
295
+ sss = model.getSurfaceByName(nom)
296
+ next if sss.empty?
297
+ sss = sss.get
298
+
299
+ if sss.isConstructionDefaulted
300
+ set = defaultConstructionSet(model, sss)
301
+ constructions = set.defaultExteriorSurfaceConstructions.get
302
+
303
+ case sss.surfaceType.downcase
304
+ when "roofceiling"
305
+ constructions.setRoofCeilingConstruction(lc)
306
+ when "floor"
307
+ constructions.setFloorConstruction(lc)
308
+ else
309
+ constructions.setWallConstruction(lc)
310
+ end
311
+ else
312
+ sss.setConstruction(lc)
313
+ end
314
+
315
+ s[nom][:construction] = lc # reset TBD attributes
316
+ s[nom][:index ] = lyr[:index]
317
+ s[nom][:ltype ] = lyr[:type ]
318
+ s[nom][:r ] = lyr[:r ] # temporary
319
+ end
320
+ end
321
+ end
322
+
323
+ # Merge to ensure a single entry for coll Hash.
324
+ coll.each do |i, col|
325
+ next if i == id
326
+ next unless coll.key?(id)
327
+ coll[id][:area] += col[:area]
328
+
329
+ col[:s].each do |nom, sss|
330
+ coll[id][:s][nom] = sss unless coll[id][:s].key?(nom)
331
+ end
332
+ end
333
+
334
+ coll.delete_if { |i, _| i != id }
335
+ log(DBG, "Collection == 1? for '#{id}' (#{mth})") unless coll.size == 1
336
+ next unless coll.size == 1
337
+
338
+ res = uo(model, lc, id, hloss, film, g[:ut])
339
+ log(ERR, "Unable to uprate '#{id}' (#{mth})") unless res[:uo] && res[:m]
340
+ next unless res[:uo] && res[:m]
341
+
342
+ lyr = insulatingLayer(lc)
343
+
344
+ # Loop through coll :s, and reset :r - likely modified by uo().
345
+ coll.values.first[:s].keys.each do |nom|
346
+ next unless s.key?(nom)
347
+ next unless s[nom].key?(:deratable )
348
+ next unless s[nom].key?(:construction)
349
+ next unless s[nom].key?(:index )
350
+ next unless s[nom].key?(:ltype )
351
+ next unless s[nom].key?(:type )
352
+
353
+ next unless s[nom][:deratable ]
354
+ next unless s[nom][:construction] == lc
355
+ next unless s[nom][:index ] == lyr[:index]
356
+ next unless s[nom][:ltype ] == lyr[:type]
357
+
358
+ type = s[nom][:type].to_s.downcase
359
+ type = "roof" if type == "ceiling"
360
+ next unless type.include?(label.to_s)
361
+ next unless s[nom].key?(:r)
362
+ s[nom][:r] = lyr[:r] # final
363
+ end
364
+
365
+ argh[:wall_uo ] = res[:uo] if label == :wall
366
+ argh[:roof_uo ] = res[:uo] if label == :roof
367
+ argh[:floor_uo] = res[:uo] if label == :floor
368
+ else
369
+ log(ERR, "Nilled construction to uprate - (#{mth})")
370
+ return false
371
+ end
372
+ end
373
+
374
+ true
375
+ end
376
+
377
+ ##
378
+ # Set reference values for points, edges & surfaces (& subsurfaces) to
379
+ # compute Quebec energy code (Section 3.3) UA' comparison (2021).
380
+ #
381
+ # @param s [Hash] preprocessed collection of TBD surfaces
382
+ # @param sets [TBD::PSI] a TBD model's PSI sets
383
+ # @param spts [Bool] true if OpenStudio model has valid setpoints
384
+ #
385
+ # @return [Bool] true if successful in generating UA' reference values
386
+ def qc33(s = {}, sets = nil, spts = true)
387
+ mth = "TBD::#{__callee__}"
388
+ cl1 = Hash
389
+ cl2 = TBD::PSI
390
+
391
+ return mismatch("surfaces", s, cl1, mth, DBG, false) unless s.is_a?(cl1)
392
+ return mismatch("sets", sets, cl1, mth, DBG, false) unless sets.is_a?(cl2)
393
+
394
+ shorts = sets.shorthands("code (Quebec)")
395
+ empty = shorts[:has].empty? || shorts[:val].empty?
396
+ log(DBG, "Missing QC PSI set for 3.3 UA' tradeoff (#{mth})") if empty
397
+ return false if empty
398
+
399
+ ok = spts == true || spts == false
400
+ log(DBG, "'setpoints' must be true/false for 3.3 UA' tradeoff") unless ok
401
+ return false unless ok
402
+
403
+ s.each do |id, surface|
404
+ next unless surface.key?(:deratable)
405
+ next unless surface[:deratable]
406
+ next unless surface.key?(:type)
407
+
408
+ heating = -50 if spts
409
+ cooling = 50 if spts
410
+ heating = 21 unless spts
411
+ cooling = 24 unless spts
412
+ heating = surface[:heating] if surface.key?(:heating)
413
+ cooling = surface[:cooling] if surface.key?(:cooling)
414
+
415
+ # Start with surface U-factors.
416
+ ref = 1 / 5.46
417
+ ref = 1 / 3.60 if surface[:type] == :wall
418
+
419
+ # Adjust for lower heating setpoint (assumes -25°C design conditions).
420
+ ref *= 43 / (heating + 25) if heating < 18 && cooling > 40
421
+ surface[:ref] = ref # ... and store
422
+
423
+ if surface.key?(:skylights) # loop through subsurfaces
424
+ ref = 2.85
425
+ ref *= 43 / (heating + 25) if heating < 18 && cooling > 40
426
+ surface[:skylights].values.map { |skylight| skylight[:ref] = ref }
427
+ end
428
+
429
+ if surface.key?(:windows)
430
+ ref = 2.0
431
+ ref *= 43 / (heating + 25) if heating < 18 && cooling > 40
432
+ surface[:windows].values.map { |window| window[:ref] = ref }
433
+ end
434
+
435
+ if surface.key?(:doors)
436
+ surface[:doors].each do |i, door|
437
+ ref = 0.9
438
+ ref = 2.0 if door.key?(:glazed) && door[:glazed]
439
+ ref *= 43 / (heating + 25) if heating < 18 && cooling > 40
440
+ door[:ref] = ref
441
+ end
442
+ end
443
+
444
+ # Loop through point thermal bridges.
445
+ surface[:pts].map { |i, pt| pt[:ref] = 0.5 } if surface.key?(:pts)
446
+
447
+ # Loop through linear thermal bridges.
448
+ if surface.key?(:edges)
449
+ surface[:edges].values.each do |edge|
450
+ next unless edge.key?(:type)
451
+ next unless edge.key?(:ratio)
452
+ safe = sets.safe("code (Quebec)", edge[:type])
453
+ edge[:ref] = shorts[:val][safe] * edge[:ratio] if safe
454
+ end
455
+ end
456
+ end
457
+
458
+ true
459
+ end
460
+
461
+ ##
462
+ # Generate UA' summary.
463
+ #
464
+ # @param date [Time] Time stamp
465
+ # @param argh [Hash] TBD arguments
466
+ #
467
+ # @return [Hash] multilingual binned values for UA' summary
468
+ def ua_summary(date = Time.now, argh = {})
469
+ mth = "TBD::#{__callee__}"
470
+
471
+ ua = {}
472
+ argh = {} unless argh.is_a?(Hash )
473
+ argh[:seed ] = "" unless argh.key?(:seed )
474
+ argh[:ua_ref ] = "" unless argh.key?(:ua_ref )
475
+ argh[:surfaces ] = nil unless argh.key?(:surfaces )
476
+ argh[:version ] = "" unless argh.key?(:version )
477
+ argh[:io ] = {} unless argh.key?(:io )
478
+ argh[:io][:description] = "" unless argh[:io].key?(:description)
479
+
480
+ descr = argh[:io][:description]
481
+ file = argh[:seed ]
482
+ version = argh[:version ]
483
+ s = argh[:surfaces ]
484
+
485
+ return mismatch("TBD surfaces", s, Hash, mth, DBG, ua) unless s.is_a?(Hash)
486
+ return empty("TBD Surfaces", mth, WRN, ua) if s.empty?
487
+
488
+ ua[:descr ] = ""
489
+ ua[:file ] = ""
490
+ ua[:version] = ""
491
+ ua[:model ] = "∑U•A + ∑PSI•L + ∑KHI•n"
492
+ ua[:date ] = date
493
+ ua[:descr ] = descr unless descr.nil? || descr.empty?
494
+ ua[:file ] = file unless file.nil? || file.empty?
495
+ ua[:version] = version unless version.nil? || version.empty?
496
+
497
+ [:en, :fr].each { |lang| ua[lang] = {} }
498
+
499
+ ua[:en][:notes] = "Automated assessment from the OpenStudio Measure, " \
500
+ "Thermal Bridging and Derating (TBD). Open source and MIT-licensed, " \
501
+ "TBD is provided as is (without warranty). Procedures are documented " \
502
+ "in the source code: https://github.com/rd2/tbd. "
503
+
504
+ ua[:fr][:notes] = "Analyse automatisée à partir de la measure OpenStudio, "\
505
+ "'Thermal Bridging and Derating' (ou TBD). Distribuée librement " \
506
+ "(licence MIT), TBD est offerte telle quelle (sans garantie). " \
507
+ "L'approche est documentée au sein du code source : " \
508
+ "https://github.com/rd2/tbd."
509
+
510
+ walls = { net: 0, gross: 0, subs: 0 }
511
+ roofs = { net: 0, gross: 0, subs: 0 }
512
+ floors = { net: 0, gross: 0, subs: 0 }
513
+ areas = { walls: walls, roofs: roofs, floors: floors }
514
+ has = {}
515
+ val = {}
516
+ psi = PSI.new
517
+
518
+ unless argh[:ua_ref].empty?
519
+ shorts = psi.shorthands(argh[:ua_ref])
520
+ empty = shorts[:has].empty? && shorts[:val].empty?
521
+ has = shorts[:has] unless empty
522
+ val = shorts[:val] unless empty
523
+ log(ERR, "Invalid UA' reference set (#{mth})") if empty
524
+
525
+ unless empty
526
+ ua[:model] += " : Design vs '#{argh[:ua_ref]}'"
527
+
528
+ case argh[:ua_ref]
529
+ when "code (Quebec)"
530
+ ua[:en][:objective] = "COMPLIANCE ASSESSMENT"
531
+ ua[:en][:details ] = []
532
+ ua[:en][:details ] << "Quebec Construction Code, Chapter I.1"
533
+ ua[:en][:details ] << "NECB 2015, modified version (2020)"
534
+ ua[:en][:details ] << "Division B, Section 3.3"
535
+ ua[:en][:details ] << "Building Envelope Trade-off Path"
536
+
537
+ ua[:en][:notes] << " Calculations comply with Section 3.3 " \
538
+ "requirements. Results are based on user input not subject to " \
539
+ "prior validation (see DESCRIPTION), and as such the assessment " \
540
+ "shall not be considered as a certification of compliance."
541
+
542
+ ua[:fr][:objective] = "ANALYSE DE CONFORMITÉ"
543
+ ua[:fr][:details ] = []
544
+ ua[:fr][:details ] << "Code de construction du Québec, Chapitre I.1"
545
+ ua[:fr][:details ] << "CNÉB 2015, version modifiée (2020)"
546
+ ua[:fr][:details ] << "Division B, Section 3.3"
547
+ ua[:fr][:details ] << "Méthode des solutions de remplacement"
548
+
549
+ ua[:fr][:notes] << " Les calculs sont conformes aux dispositions de "\
550
+ "la Section 3.3. Les résultats sont tributaires d'intrants " \
551
+ "fournis par l'utilisateur, sans validation préalable (voir " \
552
+ "DESCRIPTION). Ce document ne peut constituer une attestation de " \
553
+ "conformité."
554
+ else
555
+ ua[:en][:objective] = "UA'"
556
+ ua[:fr][:objective] = "UA'"
557
+ end
558
+ end
559
+ end
560
+
561
+ # Set up 2x heating setpoint (HSTP) "blocks" (or bins):
562
+ # bloc1: spaces/zones with HSTP >= 18°C
563
+ # bloc2: spaces/zones with HSTP < 18°C
564
+ # (ref: 2021 Quebec energy code 3.3. UA' trade-off methodology)
565
+ # (... can be extended in the future to cover other standards)
566
+ #
567
+ # Determine UA' compliance separately for (i) bloc1 & (ii) bloc2.
568
+ #
569
+ # Each block's UA' = ∑ U•area + ∑ PSI•length + ∑ KHI•count
570
+ blc = { walls: 0, roofs: 0, floors: 0, doors: 0,
571
+ windows: 0, skylights: 0, rimjoists: 0, parapets: 0,
572
+ trim: 0, corners: 0, balconies: 0, grade: 0,
573
+ other: 0 } # includes party wall edges, expansion joints, etc.
574
+
575
+ b1 = {}
576
+ b2 = {}
577
+ b1[:pro] = blc # proposed design
578
+ b1[:ref] = blc.clone # reference
579
+ b2[:pro] = blc.clone # proposed design
580
+ b2[:ref] = blc.clone # reference
581
+
582
+ # Loop through surfaces, subsurfaces and edges and populate bloc1 & bloc2.
583
+ argh[:surfaces].each do |id, surface|
584
+ next unless surface.key?(:deratable)
585
+ next unless surface[:deratable]
586
+ next unless surface.key?(:type)
587
+ type = surface[:type]
588
+ next unless type == :wall || type == :ceiling || type == :floor
589
+ next unless surface.key?(:net)
590
+ next unless surface[:net] > TOL
591
+ next unless surface.key?(:u)
592
+ next unless surface[:u] > TOL
593
+ heating = 21.0
594
+ heating = surface[:heating] if surface.key?(:heating)
595
+ bloc = b1
596
+ bloc = b2 if heating < 18
597
+
598
+ reference = surface.key?(:ref)
599
+ if type == :wall
600
+ areas[:walls][:net ] += surface[:net]
601
+ bloc[:pro][:walls ] += surface[:net] * surface[:u ]
602
+ bloc[:ref][:walls ] += surface[:net] * surface[:ref] if reference
603
+ bloc[:ref][:walls ] += surface[:net] * surface[:u ] unless reference
604
+ elsif type == :ceiling
605
+ areas[:roofs][:net ] += surface[:net]
606
+ bloc[:pro][:roofs ] += surface[:net] * surface[:u ]
607
+ bloc[:ref][:roofs ] += surface[:net] * surface[:ref] if reference
608
+ bloc[:ref][:roofs ] += surface[:net] * surface[:u ] unless reference
609
+ else
610
+ areas[:floors][:net] += surface[:net]
611
+ bloc[:pro][:floors] += surface[:net] * surface[:u ]
612
+ bloc[:ref][:floors] += surface[:net] * surface[:ref] if reference
613
+ bloc[:ref][:floors] += surface[:net] * surface[:u ] unless reference
614
+ end
615
+
616
+ if surface.key?(:doors)
617
+ surface[:doors].values.each do |door|
618
+ next unless door.key?(:gross)
619
+ next unless door[:gross] > TOL
620
+ next unless door.key?(:u)
621
+ next unless door[:u] > TOL
622
+ areas[:walls][:subs ] += door[:gross] if type == :wall
623
+ areas[:roofs][:subs ] += door[:gross] if type == :ceiling
624
+ areas[:floors][:subs] += door[:gross] if type == :floor
625
+ bloc[:pro][:doors ] += door[:gross] * door[:u]
626
+
627
+ ok = door.key?(:ref)
628
+ bloc[:ref][:doors ] += door[:gross] * door[:ref] if ok
629
+ bloc[:ref][:doors ] += door[:gross] * door[:u ] unless ok
630
+ end
631
+ end
632
+
633
+ if surface.key?(:windows)
634
+ surface[:windows].values.each do |window|
635
+ next unless window.key?(:gross)
636
+ next unless window[:gross] > TOL
637
+ next unless window.key?(:u)
638
+ next unless window[:u] > TOL
639
+ areas[:walls][:subs ] += window[:gross] if type == :wall
640
+ areas[:roofs][:subs ] += window[:gross] if type == :ceiling
641
+ areas[:floors][:subs] += window[:gross] if type == :floor
642
+ bloc[:pro][:windows] += window[:gross] * window[:u]
643
+
644
+ ok = window.key?(:ref)
645
+ bloc[:ref][:windows ] += window[:gross] * window[:ref] if ok
646
+ bloc[:ref][:windows ] += window[:gross] * window[:u ] unless ok
647
+ end
648
+ end
649
+
650
+ if surface.key?(:skylights)
651
+ surface[:skylights].values.each do |sky|
652
+ next unless sky.key?(:gross)
653
+ next unless sky[:gross] > TOL
654
+ next unless sky.key?(:u)
655
+ next unless sky[:u] > TOL
656
+ areas[:walls][:subs ] += sky[:gross] if type == :wall
657
+ areas[:roofs][:subs ] += sky[:gross] if type == :ceiling
658
+ areas[:floors][:subs ] += sky[:gross] if type == :floor
659
+ bloc[:pro][:skylights] += sky[:gross] * sky[:u]
660
+
661
+ ok = sky.key?(:ref)
662
+ bloc[:ref][:skylights] += sky[:gross] * sky[:ref] if ok
663
+ bloc[:ref][:skylights] += sky[:gross] * sky[:u ] unless ok
664
+ end
665
+ end
666
+
667
+ if surface.key?(:edges)
668
+ surface[:edges].values.each do |edge|
669
+ next unless edge.key?(:type)
670
+ next unless edge.key?(:length)
671
+ next unless edge[:length] > TOL
672
+ next unless edge.key?(:psi)
673
+
674
+ loss = edge[:length] * edge[:psi]
675
+ type = edge[:type].to_s
676
+
677
+ case type
678
+ when /rimjoist/i
679
+ bloc[:pro][:rimjoists] += loss
680
+ when /parapet/i
681
+ bloc[:pro][:parapets ] += loss
682
+ when /fenestration/i
683
+ bloc[:pro][:trim ] += loss
684
+ when /head/i
685
+ bloc[:pro][:trim ] += loss
686
+ when /sill/i
687
+ bloc[:pro][:trim ] += loss
688
+ when /jamb/i
689
+ bloc[:pro][:trim ] += loss
690
+ when /corner/i
691
+ bloc[:pro][:corners ] += loss
692
+ when /grade/i
693
+ bloc[:pro][:grade ] += loss
694
+ else
695
+ bloc[:pro][:other ] += loss
696
+ end
697
+
698
+ next if val.empty?
699
+ next if argh[:ua_ref].empty?
700
+ safer = psi.safe(argh[:ua_ref], edge[:type])
701
+ ok = edge.key?(:ref)
702
+ loss = edge[:length] * edge[:ref] if ok
703
+ loss = edge[:length] * val[safer] * edge[:ratio] unless ok
704
+
705
+ case safer.to_s
706
+ when /rimjoist/i
707
+ bloc[:ref][:rimjoists] += loss
708
+ when /parapet/i
709
+ bloc[:ref][:parapets ] += loss
710
+ when /fenestration/i
711
+ bloc[:ref][:trim ] += loss
712
+ when /head/i
713
+ bloc[:ref][:trim ] += loss
714
+ when /sill/i
715
+ bloc[:ref][:trim ] += loss
716
+ when /jamb/i
717
+ bloc[:ref][:trim ] += loss
718
+ when /corner/i
719
+ bloc[:ref][:corners ] += loss
720
+ when /grade/i
721
+ bloc[:ref][:grade ] += loss
722
+ else
723
+ bloc[:ref][:other ] += loss
724
+ end
725
+ end
726
+ end
727
+
728
+ if surface.key?(:pts)
729
+ surface[:pts].values.each do |pts|
730
+ next unless pts.key?(:val)
731
+ next unless pts.key?(:n)
732
+ bloc[:pro][:other] += pts[:val] * pts[:n]
733
+ next unless pts.key?(:ref)
734
+ bloc[:ref][:other] += pts[:ref] * pts[:n]
735
+ end
736
+ end
737
+ end
738
+
739
+ [:en, :fr].each do |lang|
740
+ blc = [:b1, :b2]
741
+
742
+ blc.each do |b|
743
+ bloc = b1
744
+ bloc = b2 if b == :b2
745
+ pro_sum = bloc[:pro].values.reduce(:+)
746
+ ref_sum = bloc[:ref].values.reduce(:+)
747
+
748
+ if pro_sum > TOL || ref_sum > TOL
749
+ ratio = nil
750
+ ratio = (100.0 * (pro_sum - ref_sum) / ref_sum).abs if ref_sum > TOL
751
+ str = format("%.1f W/K (vs %.1f W/K)", pro_sum, ref_sum)
752
+ str += format(" +%.1f%%", ratio) if ratio && pro_sum > ref_sum # **
753
+ str += format(" -%.1f%%", ratio) if ratio && pro_sum < ref_sum
754
+ ua[lang][b] = {}
755
+
756
+ if b == :b1
757
+ ua[:en][b][:summary] = "heated : #{str}" if lang == :en
758
+ ua[:fr][b][:summary] = "chauffé : #{str}" if lang == :fr
759
+ else
760
+ ua[:en][b][:summary] = "semi-heated : #{str}" if lang == :en
761
+ ua[:fr][b][:summary] = "semi-chauffé : #{str}" if lang == :fr
762
+ end
763
+
764
+ # ** https://bugs.ruby-lang.org/issues/13761 (Ruby > 2.2.5)
765
+ # str += format(" +%.1f%", ratio) if ratio && pro_sum > ref_sum ... now:
766
+ # str += format(" +%.1f%%", ratio) if ratio && pro_sum > ref_sum
767
+
768
+ bloc[:pro].each do |k, v|
769
+ rf = bloc[:ref][k]
770
+ next if v < TOL && rf < TOL
771
+ ratio = nil
772
+ ratio = (100.0 * (v - rf) / rf).abs if rf > TOL
773
+ str = format("%.1f W/K (vs %.1f W/K)", v, rf)
774
+ str += format(" +%.1f%%", ratio) if ratio && v > rf
775
+ str += format(" -%.1f%%", ratio) if ratio && v < rf
776
+
777
+ case k
778
+ when :walls
779
+ ua[:en][b][k] = "walls : #{str}" if lang == :en
780
+ ua[:fr][b][k] = "murs : #{str}" if lang == :fr
781
+ when :roofs
782
+ ua[:en][b][k] = "roofs : #{str}" if lang == :en
783
+ ua[:fr][b][k] = "toits : #{str}" if lang == :fr
784
+ when :floors
785
+ ua[:en][b][k] = "floors : #{str}" if lang == :en
786
+ ua[:fr][b][k] = "planchers : #{str}" if lang == :fr
787
+ when :doors
788
+ ua[:en][b][k] = "doors : #{str}" if lang == :en
789
+ ua[:fr][b][k] = "portes : #{str}" if lang == :fr
790
+ when :windows
791
+ ua[:en][b][k] = "windows : #{str}" if lang == :en
792
+ ua[:fr][b][k] = "fenêtres : #{str}" if lang == :fr
793
+ when :skylights
794
+ ua[:en][b][k] = "skylights : #{str}" if lang == :en
795
+ ua[:fr][b][k] = "lanterneaux : #{str}" if lang == :fr
796
+ when :rimjoists
797
+ ua[:en][b][k] = "rimjoists : #{str}" if lang == :en
798
+ ua[:fr][b][k] = "rives : #{str}" if lang == :fr
799
+ when :parapets
800
+ ua[:en][b][k] = "parapets : #{str}" if lang == :en
801
+ ua[:fr][b][k] = "parapets : #{str}" if lang == :fr
802
+ when :trim
803
+ ua[:en][b][k] = "trim : #{str}" if lang == :en
804
+ ua[:fr][b][k] = "chassis : #{str}" if lang == :fr
805
+ when :corners
806
+ ua[:en][b][k] = "corners : #{str}" if lang == :en
807
+ ua[:fr][b][k] = "coins : #{str}" if lang == :fr
808
+ when :balconies
809
+ ua[:en][b][k] = "balconies : #{str}" if lang == :en
810
+ ua[:fr][b][k] = "balcons : #{str}" if lang == :fr
811
+ when :grade
812
+ ua[:en][b][k] = "grade : #{str}" if lang == :en
813
+ ua[:fr][b][k] = "tracé : #{str}" if lang == :fr
814
+ else
815
+ ua[:en][b][k] = "other : #{str}" if lang == :en
816
+ ua[:fr][b][k] = "autres : #{str}" if lang == :fr
817
+ end
818
+ end
819
+
820
+ # Deterministic sorting
821
+ ua[lang][b][:summary] = ua[lang][b].delete(:summary)
822
+ ok = ua[lang][b].key?(:walls)
823
+ ua[lang][b][:walls] = ua[lang][b].delete(:walls) if ok
824
+ ok = ua[lang][b].key?(:roofs)
825
+ ua[lang][b][:roofs] = ua[lang][b].delete(:roofs) if ok
826
+ ok = ua[lang][b].key?(:floors)
827
+ ua[lang][b][:floors] = ua[lang][b].delete(:floors) if ok
828
+ ok = ua[lang][b].key?(:doors)
829
+ ua[lang][b][:doors] = ua[lang][b].delete(:doors) if ok
830
+ ok = ua[lang][b].key?(:windows)
831
+ ua[lang][b][:windows] = ua[lang][b].delete(:windows) if ok
832
+ ok = ua[lang][b].key?(:skylights)
833
+ ua[lang][b][:skylights] = ua[lang][b].delete(:skylights) if ok
834
+ ok = ua[lang][b].key?(:rimjoists)
835
+ ua[lang][b][:rimjoists] = ua[lang][b].delete(:rimjoists) if ok
836
+ ok = ua[lang][b].key?(:parapets)
837
+ ua[lang][b][:parapets] = ua[lang][b].delete(:parapets) if ok
838
+ ok = ua[lang][b].key?(:trim)
839
+ ua[lang][b][:trim] = ua[lang][b].delete(:trim) if ok
840
+ ok = ua[lang][b].key?(:corners)
841
+ ua[lang][b][:corners] = ua[lang][b].delete(:corners) if ok
842
+ ok = ua[lang][b].key?(:balconies)
843
+ ua[lang][b][:balconies] = ua[lang][b].delete(:balconies) if ok
844
+ ok = ua[lang][b].key?(:grade)
845
+ ua[lang][b][:grade] = ua[lang][b].delete(:grade) if ok
846
+ ok = ua[lang][b].key?(:other)
847
+ ua[lang][b][:other] = ua[lang][b].delete(:other) if ok
848
+ end
849
+ end
850
+ end
851
+
852
+ # Areas (m2).
853
+ areas[:walls ][:gross] = areas[:walls ][:net] + areas[:walls ][:subs]
854
+ areas[:roofs ][:gross] = areas[:roofs ][:net] + areas[:roofs ][:subs]
855
+ areas[:floors][:gross] = areas[:floors][:net] + areas[:floors][:subs]
856
+ ua[:en][:areas] = {}
857
+ ua[:fr][:areas] = {}
858
+
859
+ str = format("walls : %.1f m2 (net)", areas[:walls][:net])
860
+ str += format(", %.1f m2 (gross)", areas[:walls][:gross])
861
+ ua[:en][:areas][:walls] = str unless areas[:walls][:gross] < TOL
862
+ str = format("roofs : %.1f m2 (net)", areas[:roofs][:net])
863
+ str += format(", %.1f m2 (gross)", areas[:roofs][:gross])
864
+ ua[:en][:areas][:roofs] = str unless areas[:roofs][:gross] < TOL
865
+ str = format("floors : %.1f m2 (net)", areas[:floors][:net])
866
+ str += format(", %.1f m2 (gross)", areas[:floors][:gross])
867
+ ua[:en][:areas][:floors] = str unless areas[:floors][:gross] < TOL
868
+
869
+ str = format("murs : %.1f m2 (net)", areas[:walls][:net])
870
+ str += format(", %.1f m2 (brut)", areas[:walls][:gross])
871
+ ua[:fr][:areas][:walls] = str unless areas[:walls][:gross] < TOL
872
+ str = format("toits : %.1f m2 (net)", areas[:roofs][:net])
873
+ str += format(", %.1f m2 (brut)", areas[:roofs][:gross])
874
+ ua[:fr][:areas][:roofs] = str unless areas[:roofs][:gross] < TOL
875
+ str = format("planchers : %.1f m2 (net)", areas[:floors][:net])
876
+ str += format(", %.1f m2 (brut)", areas[:floors][:gross])
877
+ ua[:fr][:areas][:floors] = str unless areas[:floors][:gross] < TOL
878
+
879
+ ua
880
+ end
881
+
882
+ ##
883
+ # Generate MD-formatted file.
884
+ #
885
+ # @param ua [Hash] preprocessed collection of UA-related strings
886
+ # @param lang [String] preferred language ("en" vs "fr")
887
+ #
888
+ # @return [Array] MD-formatted strings (empty if invalid inputs)
889
+ def ua_md(ua = {}, lang = :en)
890
+ mth = "TBD::#{__callee__}"
891
+ report = []
892
+
893
+ return mismatch("ua", ua, Hash, mth, DBG, report) unless ua.is_a?(Hash)
894
+ return empty("ua", mth, DBG, report) if ua.empty?
895
+ return hashkey("", ua, lang, mth, DBG, report) unless ua.key?(lang)
896
+
897
+ if ua[lang].key?(:objective)
898
+ report << "# #{ua[lang][:objective]} "
899
+ report << " "
900
+ end
901
+
902
+ if ua[lang].key?(:details)
903
+ ua[lang][:details].each { |d| report << "#{d} " }
904
+ report << " "
905
+ end
906
+
907
+ if ua.key?(:model)
908
+ report << "##### SUMMARY " if lang == :en
909
+ report << "##### SOMMAIRE " if lang == :fr
910
+ report << " "
911
+ report << "#{ua[:model]} "
912
+ report << " "
913
+ end
914
+
915
+ if ua[lang].key?(:b1) && ua[lang][:b1].key?(:summary)
916
+ last = ua[lang][:b1].keys.to_a.last
917
+ report << "* #{ua[lang][:b1][:summary]}"
918
+
919
+ ua[lang][:b1].each do |k, v|
920
+ next if k == :summary
921
+ report << " * #{v}" unless k == last
922
+ report << " * #{v} " if k == last
923
+ report << " " if k == last
924
+ end
925
+ report << " "
926
+ end
927
+
928
+ if ua[lang].key?(:b2) && ua[lang][:b2].key?(:summary)
929
+ last = ua[lang][:b2].keys.to_a.last
930
+ report << "* #{ua[lang][:b2][:summary]}"
931
+
932
+ ua[lang][:b2].each do |k, v|
933
+ next if k == :summary
934
+ report << " * #{v}" unless k == last
935
+ report << " * #{v} " if k == last
936
+ report << " " if k == last
937
+ end
938
+ report << " "
939
+ end
940
+
941
+ if ua.key?(:date)
942
+ report << "##### DESCRIPTION "
943
+ report << " "
944
+ report << "* project : #{ua[:descr]}" if ua.key?(:descr) && lang == :en
945
+ report << "* projet : #{ua[:descr]}" if ua.key?(:descr) && lang == :fr
946
+ model = ""
947
+ model = "* model : #{ua[:file]}" if ua.key?(:file) && lang == :en
948
+ model = "* modèle : #{ua[:file]}" if ua.key?(:file) && lang == :fr
949
+ model += " (v#{ua[:version]})" if ua.key?(:version)
950
+ report << model unless model.empty?
951
+ report << "* TBD : v3.0.0"
952
+ report << "* date : #{ua[:date]}"
953
+
954
+ if lang == :en
955
+ report << "* status : #{msg(status)}" unless status.zero?
956
+ report << "* status : success !" if status.zero?
957
+ elsif lang == :fr
958
+ report << "* statut : #{msg(status)}" unless status.zero?
959
+ report << "* statut : succès !" if status.zero?
960
+ end
961
+ report << " "
962
+ end
963
+
964
+ if ua[lang].key?(:areas)
965
+ report << "##### AREAS " if lang == :en
966
+ report << "##### AIRES " if lang == :fr
967
+ report << " "
968
+ ok = ua[lang][:areas].key?(:walls)
969
+ report << "* #{ua[lang][:areas][:walls]}" if ok
970
+ ok = ua[lang][:areas].key?(:roofs)
971
+ report << "* #{ua[lang][:areas][:roofs]}" if ok
972
+ ok = ua[lang][:areas].key?(:floors)
973
+ report << "* #{ua[lang][:areas][:floors]}" if ok
974
+ report << " "
975
+ end
976
+
977
+ if ua[lang].key?(:notes)
978
+ report << "##### NOTES "
979
+ report << " "
980
+ report << "#{ua[lang][:notes]} "
981
+ report << " "
982
+ end
983
+
984
+ report
985
+ end
986
+ end