tbd 3.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitattributes +3 -0
- data/.github/workflows/pull_request.yml +72 -0
- data/.gitignore +23 -0
- data/.rspec +3 -0
- data/Gemfile +3 -0
- data/LICENSE.md +21 -0
- data/README.md +154 -0
- data/Rakefile +60 -0
- data/json/midrise.json +64 -0
- data/json/tbd_5ZoneNoHVAC.json +19 -0
- data/json/tbd_5ZoneNoHVAC_btap.json +91 -0
- data/json/tbd_seb_n2.json +41 -0
- data/json/tbd_seb_n4.json +57 -0
- data/json/tbd_warehouse10.json +24 -0
- data/json/tbd_warehouse5.json +37 -0
- data/lib/measures/tbd/LICENSE.md +21 -0
- data/lib/measures/tbd/README.md +136 -0
- data/lib/measures/tbd/README.md.erb +42 -0
- data/lib/measures/tbd/docs/.gitkeep +1 -0
- data/lib/measures/tbd/measure.rb +327 -0
- data/lib/measures/tbd/measure.xml +460 -0
- data/lib/measures/tbd/resources/geo.rb +714 -0
- data/lib/measures/tbd/resources/geometry.rb +351 -0
- data/lib/measures/tbd/resources/model.rb +1431 -0
- data/lib/measures/tbd/resources/oslog.rb +381 -0
- data/lib/measures/tbd/resources/psi.rb +2229 -0
- data/lib/measures/tbd/resources/tbd.rb +55 -0
- data/lib/measures/tbd/resources/transformation.rb +121 -0
- data/lib/measures/tbd/resources/ua.rb +986 -0
- data/lib/measures/tbd/resources/utils.rb +1636 -0
- data/lib/measures/tbd/resources/version.rb +3 -0
- data/lib/measures/tbd/tests/tbd_full_PSI.json +17 -0
- data/lib/measures/tbd/tests/tbd_tests.rb +222 -0
- data/lib/tbd/geo.rb +714 -0
- data/lib/tbd/psi.rb +2229 -0
- data/lib/tbd/ua.rb +986 -0
- data/lib/tbd/version.rb +25 -0
- data/lib/tbd.rb +93 -0
- data/sponsors/canada.png +0 -0
- data/sponsors/quebec.png +0 -0
- data/tbd.gemspec +43 -0
- data/tbd.schema.json +571 -0
- data/v291_MacOS.md +110 -0
- 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
|