tbd 3.5.2 → 3.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/lib/tbd/ua.rb CHANGED
@@ -25,103 +25,113 @@ module TBD
25
25
  # Calculates construction Uo (including surface film resistances) to meet Ut.
26
26
  #
27
27
  # @param model [OpenStudio::Model::Model] a model
28
- # @param lc [OpenStudio::Model::LayeredConstruction] a layered construction
29
28
  # @param id [#to_s] layered construction identifier
30
- # @param hloss [Numeric] heat loss from major thermal bridging, in W/K
29
+ # @param lc [OpenStudio::Model::LayeredConstruction] a layered construction
30
+ # @param area [Numeric] net surface area covered by layered construction
31
31
  # @param film [Numeric] target surface film resistance, in m2•K/W
32
+ # @param hloss [Numeric] heat loss from major thermal bridging, in W/K
32
33
  # @param ut [Numeric] target overall Ut for lc, in W/m2•K
33
34
  #
34
- # @return [Hash] uo: lc Uo [W/m2•K] to meet Ut, m: uprated lc layer
35
- # @return [Hash] uo: (nil), m: (nil) if invalid input (see logs)
36
- def uo(model = nil, lc = nil, id = "", hloss = 0.0, film = 0.0, ut = 0.0)
35
+ # @return [Float] Uo [W/m2•K] required to meet Ut (see logs if 0 or UMIN)
36
+ def uo(id = "", lc = nil, area = 0, film = 0, hloss = 0, ut = 0)
37
37
  mth = "TBD::#{__callee__}"
38
- res = { uo: nil, m: nil }
39
- cl1 = OpenStudio::Model::Model
40
- cl2 = OpenStudio::Model::LayeredConstruction
41
- cl3 = Numeric
42
- cl4 = String
38
+ cl1 = OpenStudio::Model::LayeredConstruction
39
+ cl2 = Numeric
40
+ cl3 = String
43
41
  id = trim(id)
44
- return mismatch("model", model, cl1, mth, DBG, res) unless model.is_a?(cl1)
45
- return mismatch("id" , id, cl4, mth, DBG, res) if id.empty?
46
- return mismatch("lc" , lc, cl2, mth, DBG, res) unless lc.is_a?(cl2)
47
- return mismatch("hloss", hloss, cl3, mth, DBG, res) unless hloss.is_a?(cl3)
48
- return mismatch("film" , film, cl3, mth, DBG, res) unless film.is_a?(cl3)
49
- return mismatch("Ut" , ut, cl3, mth, DBG, res) unless ut.is_a?(cl3)
50
-
51
- loss = 0.0 # residual heatloss (not assigned) [W/K]
52
- area = lc.getNetArea
53
- lyr = insulatingLayer(lc)
42
+ return mismatch("id" , id, cl3, mth, DBG, 0) if id.empty?
43
+ return mismatch("lc" , lc, cl1, mth, DBG, 0) unless lc.is_a?(cl1)
44
+ return mismatch("area" , area, cl2, mth, DBG, 0) unless area.is_a?(cl2)
45
+ return mismatch("film" , film, cl2, mth, DBG, 0) unless film.is_a?(cl2)
46
+ return mismatch("hloss", hloss, cl2, mth, DBG, 0) unless hloss.is_a?(cl2)
47
+ return mismatch("Ut" , ut, cl2, mth, DBG, 0) unless ut.is_a?(cl2)
48
+
49
+ # Residual heatloss (not assigned) [W/K].
50
+ model = lc.model
51
+ loss = 0
52
+ lyr = insulatingLayer(lc)
53
+
54
+ # Validate insulating layer.
54
55
  lyr[:index] = nil unless lyr[:index].is_a?(Numeric)
55
56
  lyr[:index] = nil unless lyr[:index] >= 0
56
57
  lyr[:index] = nil unless lyr[:index] < lc.layers.size
57
- return invalid("#{id} layer index", mth, 3, WRN, 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 > UMIN
61
- return invalid("#{id}: Ut" , mth, 6, WRN, res) unless ut < UMAX
62
- return zero("#{id}: net area (m2)", mth, WRN, 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 -= 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, WRN, res) unless new_r > RMIN
58
+ return invalid("#{id} layer index", mth, 3, DBG, 0) unless lyr[:index]
59
+ return zero("#{id}: net area (m2)", mth, DBG, 0) unless area > TOL
60
+ return negative("#{id}: film RSI" , mth, DBG, 0) if film < 0
61
+ return zero("#{id}: heatloss" , mth, DBG, 0) if hloss < TOL
62
+ return zero("#{id}: Ut" , mth, DBG, 0) unless ut > UMIN
63
+ return invalid("#{id}: Ut" , mth, 4, DBG, 0) unless ut < UMAX
64
+
65
+ # Calculate initial layer RSi to initially meet Ut target.
66
+ rt = 1 / ut # target construction Rt
67
+ r0 = rsi(lc, film) # current construction R0
68
+ r = lyr[:r] + rt - r0 # new, un-derated layer RSi
69
+
70
+ # Adjust if below admissible threshold.
71
+ if r < 0
72
+ zero("#{id}: layer RSI", mth, INF)
73
+ r = RMIN
74
+ end
75
+
76
+ # Uprate to counter heat loss from thermal bridging.
77
+ u = 1 / r
78
+ u -= (hloss / area)
79
+
80
+ # Adjust if beyond admissible range.
81
+ if u < UMIN
82
+ negative("#{id}: new Uo", mth, INF)
83
+ u = UMIN
84
+ end
85
+
86
+ r = 1 / u
75
87
 
76
88
  if lyr[:type] == :massless
77
89
  m = lc.getLayer(lyr[:index]).to_MasslessOpaqueMaterial
78
- return invalid("#{id} massless layer?", mth, 0, DBG, res) if m.empty?
90
+ return invalid("#{id} massless layer?", mth, 0, DBG, 0) if m.empty?
79
91
 
80
92
  m = m.get.clone(model).to_MasslessOpaqueMaterial.get
81
93
  m.setName("#{id} uprated")
82
94
 
83
- new_r = RMIN unless new_r > RMIN
84
- loss = (new_u - 1 / new_r) * area unless new_r > RMIN
95
+ if r < RMIN
96
+ r = RMIN
97
+ loss = (u - 1 / r) * area
98
+ end
85
99
 
86
- unless m.setThermalResistance(new_r)
87
- return invalid("Can't uprate #{id}: RSi#{new_r.round(2)}", mth, 0, DBG, res)
100
+ unless m.setThermalResistance(r)
101
+ return invalid("Can't uprate #{id}: RSi#{r.round(2)}", mth, 0, DBG, 0)
88
102
  end
89
103
  else
90
104
  m = lc.getLayer(lyr[:index]).to_StandardOpaqueMaterial
91
- return invalid("#{id} standard layer?", mth, 0, DBG, res) if m.empty?
105
+ return invalid("#{id} standard layer?", mth, 0, DBG, 0) if m.empty?
92
106
 
93
107
  m = m.get.clone(model).to_StandardOpaqueMaterial.get
94
108
  m.setName("#{id} uprated")
95
109
 
96
110
  d = m.thickness
97
- k = (d / new_r).clamp(KMIN, KMAX)
98
- d = (k * new_r).clamp(DMIN, DMAX)
111
+ k = (d / r).clamp(KMIN, KMAX)
112
+ d = (k * r).clamp(DMIN, DMAX)
99
113
 
100
- loss = (new_u - k / d) * area unless d / k > RMIN
114
+ loss = (u - k / d) * area if d / k < RMIN
101
115
 
102
116
  unless m.setThermalConductivity(k)
103
- return invalid("Can't uprate #{id}: K#{k.round(3)}", mth, 0, DBG, res)
117
+ return invalid("Can't uprate #{id}: K#{k.round(3)}", mth, 0, DBG, 0)
104
118
  end
105
119
 
106
120
  unless m.setThickness(d)
107
- return invalid("Can't uprate #{id}: #{(d*1000).to_i}mm", mth, 0, DBG, res)
121
+ return invalid("Can't uprate #{id}: #{(d*1000).to_i}mm", mth, 0, DBG, 0)
108
122
  end
109
123
  end
110
124
 
111
- return invalid("Can't ID insulating layer", mth, 0, DBG, res) unless m
125
+ return invalid("Can't ID insulating layer", mth, 0, DBG, 0) unless m
112
126
 
113
127
  lc.setLayer(lyr[:index], m)
114
- uo = 1 / rsi(lc, film)
115
-
116
- if loss > TOL
117
- h_loss = format "%.3f", loss
118
- return invalid("Can't assign #{h_loss} W/K to #{id}", mth, 0, DBG, res)
119
- end
128
+ ro = rsi(lc, film)
129
+ uo = ro < RMIN ? UMIN : 1 / ro
120
130
 
121
- res[:uo] = uo
122
- res[:m ] = m
131
+ h = format "%.3f", loss
132
+ log(INF, "Can't set #{h} W/K to #{id} #{mth}") if loss > TOL
123
133
 
124
- res
134
+ uo
125
135
  end
126
136
 
127
137
  ##
@@ -185,230 +195,219 @@ module TBD
185
195
  groups[:roof ][:ut] = argh[:roof_ut ]
186
196
  groups[:floor][:ut] = argh[:floor_ut ]
187
197
 
188
- groups[:wall ][:op] = trim(argh[:wall_option ])
189
- groups[:roof ][:op] = trim(argh[:roof_option ])
190
- groups[:floor][:op] = trim(argh[:floor_option ])
198
+ groups[:wall ][:op] = trim(argh[:wall_option ])
199
+ groups[:roof ][:op] = trim(argh[:roof_option ])
200
+ groups[:floor][:op] = trim(argh[:floor_option])
191
201
 
202
+ # Group and process walls, roofs and floors sequentially/independently.
192
203
  groups.each do |type, g|
193
204
  next unless g[:up]
194
205
  next unless g[:ut].is_a?(Numeric)
195
206
  next unless g[:ut] < UMAX
196
- next if g[:ut] < 0
207
+ next unless g[:ut] > UMIN
197
208
 
198
- typ = type
199
- typ = :ceiling if typ == :roof
209
+ typ = type
210
+ typ = :ceiling if typ == :roof
211
+
212
+ # Collection of one or several constructions to uprate.
200
213
  coll = {}
201
- area = 0
202
- film = 100000000000000
203
- lc = nil
204
- id = ""
205
- op = g[:op].downcase
206
- all = tout.include?(op)
207
-
208
- if g[:op].empty?
209
- log(WRN, "Construction (#{type}) to uprate? (#{mth})")
210
- elsif all
214
+ op = g[:op]
215
+
216
+ # Uprate ALL constructions of same type, e.g. walls.
217
+ if tout.include?(op.downcase)
211
218
  s.each do |nom, surface|
212
- next unless surface.key?(:deratable )
213
- next unless surface.key?(:type )
219
+ next unless surface.key?(:deratable)
220
+ next unless surface.key?(:type)
214
221
  next unless surface.key?(:construction)
215
- next unless surface.key?(:filmRSI )
216
- next unless surface.key?(:index )
217
- next unless surface.key?(:ltype )
218
- next unless surface.key?(:r )
222
+ next unless surface.key?(:filmRSI)
223
+ next unless surface.key?(:ltype)
224
+ next unless surface.key?(:r)
225
+ next unless surface.key?(:index)
226
+ next unless surface.key?(:net)
219
227
  next unless surface[:deratable ]
220
228
  next unless surface[:type ] == typ
221
229
  next unless surface[:construction].is_a?(cl3)
222
230
  next if surface[:index ].nil?
223
231
 
224
- # Retain lowest surface film resistance (e.g. tilted surfaces).
225
- c = surface[:construction]
226
- i = c.nameString
227
- aire = c.getNetArea
228
- film = surface[:filmRSI] if surface[:filmRSI] < film
229
-
230
- # Retain construction covering largest area. The following conditional
231
- # is reliable UNLESS linked to other deratable surface types e.g. both
232
- # floors AND walls (see "elsif lc" corrections below).
233
- if aire > area
234
- lc = c
235
- area = aire
236
- id = i
232
+ # Collect constructions to uprate.
233
+ lc = surface[:construction]
234
+ id = lc.nameString
235
+
236
+ # Track construction-specific parameters.
237
+ unless coll.key?(id)
238
+ coll[id] = {}
239
+ coll[id][:lc ] = lc
240
+ coll[id][:s ] = {}
241
+ coll[id][:hloss] = 0
242
+ coll[id][:area ] = 0
243
+ coll[id][:film ] = 0
244
+ coll[id][:fA ] = 0
245
+ coll[id][:uA ] = 0
246
+ coll[id][:u0 ] = 0
237
247
  end
238
248
 
239
- coll[i] = { area: aire, lc: c, s: {} } unless coll.key?(i)
240
- coll[i][:s][nom] = { a: surface[:net] } unless coll[i][:s].key?(nom)
249
+ coll[id][:idx] = surface[:index] unless coll[id].key?(:idx)
250
+ coll[id][:ltp] = surface[:ltype] unless coll[id].key?(:ltp)
251
+
252
+ # Track surface-specific parameters.
253
+ unless coll[id][:s].key?(nom)
254
+ coll[id][:s][nom] = {}
255
+ coll[id][:s][nom][:a] = surface[:net]
256
+ coll[id][:s][nom][:f] = surface[:filmRSI]
257
+ coll[id][:s][nom][:h] = 0
258
+ next unless surface.key?(:heatloss)
259
+ next unless surface[:heatloss].abs > TOL
260
+
261
+ coll[id][:s][nom][:h] = surface[:heatloss]
262
+ end
241
263
  end
242
264
  else
243
- id = g[:op]
265
+ id = op # single, user-selected construction
244
266
  lc = model.getConstructionByName(id)
245
- log(WRN, "Construction '#{id}'? (#{mth})") if lc.empty?
246
- next if lc.empty?
267
+
268
+ if lc.empty?
269
+ log(WRN, "Construction '#{id}'? (#{mth})")
270
+ next
271
+ end
247
272
 
248
273
  lc = lc.get.to_LayeredConstruction
249
- log(WRN, "'#{id}' layered construction? (#{mth})") if lc.empty?
250
- next if lc.empty?
251
274
 
252
- lc = lc.get
253
- area = lc.getNetArea
254
- coll[id] = { area: area, lc: lc, s: {} }
275
+ if lc.empty?
276
+ log(WRN, "'#{id}' layered construction? (#{mth})")
277
+ next
278
+ end
279
+
280
+ lc = lc.get
281
+
282
+ coll[id] = {}
283
+ coll[id][:lc ] = lc
284
+ coll[id][:s ] = {}
285
+ coll[id][:hloss] = 0
286
+ coll[id][:area ] = 0
287
+ coll[id][:film ] = 0
288
+ coll[id][:fA ] = 0
289
+ coll[id][:uA ] = 0
290
+ coll[id][:u0 ] = 0
255
291
 
256
292
  s.each do |nom, surface|
257
- next unless surface.key?(:deratable )
258
- next unless surface.key?(:type )
293
+ next unless surface.key?(:deratable)
294
+ next unless surface.key?(:type)
259
295
  next unless surface.key?(:construction)
260
- next unless surface.key?(:filmRSI )
261
- next unless surface.key?(:index )
262
- next unless surface.key?(:ltype )
263
- next unless surface.key?(:r )
296
+ next unless surface.key?(:filmRSI)
297
+ next unless surface.key?(:ltype)
298
+ next unless surface.key?(:r)
299
+ next unless surface.key?(:index)
300
+ next unless surface.key?(:net)
264
301
  next unless surface[:deratable ]
265
302
  next unless surface[:type ] == typ
266
303
  next unless surface[:construction].is_a?(cl3)
304
+ next unless surface[:construction].nameString == id
267
305
  next if surface[:index ].nil?
268
306
 
269
- i = surface[:construction].nameString
270
- next unless i == id
271
-
272
- # Retain lowest surface film resistance (e.g. tilted surfaces).
273
- film = surface[:filmRSI] if surface[:filmRSI] < film
274
-
275
- coll[i][:s][nom] = { a: surface[:net] } unless coll[i][:s].key?(nom)
307
+ coll[id][:idx] = surface[:index] unless coll[id].key?(:idx)
308
+ coll[id][:ltp] = surface[:ltype] unless coll[id].key?(:ltp)
309
+
310
+ # Track (for surfaces of targeted type):
311
+ # - net area
312
+ # - air film resistances
313
+ unless coll[id][:s].key?(nom)
314
+ coll[id][:s][nom] = {}
315
+ coll[id][:s][nom][:a] = surface[:net]
316
+ coll[id][:s][nom][:f] = surface[:filmRSI]
317
+ coll[id][:s][nom][:h] = 0
318
+ next unless surface.key?(:heatloss)
319
+ next unless surface[:heatloss].abs > TOL
320
+
321
+ coll[id][:s][nom][:h] = surface[:heatloss]
322
+ end
276
323
  end
277
324
  end
278
325
 
279
326
  if coll.empty?
280
- log(WRN, "No #{type} construction to uprate - skipping (#{mth})")
327
+ log(WRN, "Unable to uprate #{type} construction - skipping (#{mth})")
281
328
  next
282
- elsif lc
283
- # Valid layered construction - good to uprate!
284
- lyr = insulatingLayer(lc)
285
- lyr[:index] = nil unless lyr[:index].is_a?(Numeric)
286
- lyr[:index] = nil unless lyr[:index] >= 0
287
- lyr[:index] = nil unless lyr[:index] < lc.layers.size
288
-
289
- log(WRN, "Insulation index for '#{id}'? (#{mth})") unless lyr[:index]
290
- next unless lyr[:index]
291
-
292
- # Ensure lc is exclusively linked to deratable surfaces of right type.
293
- # If not, assign new lc clone to non-targeted surfaces.
294
- s.each do |nom, surface|
295
- next unless surface.key?(:type )
296
- next unless surface.key?(:deratable )
297
- next unless surface.key?(:construction)
298
- next unless surface[:construction].is_a?(cl3)
299
- next unless surface[:construction] == lc
300
- next unless surface[:deratable]
301
-
302
- ok = true
303
- ok = false unless surface[:type] == typ
304
- ok = false unless coll.key?(id)
305
- ok = false unless coll[id][:s].key?(nom)
306
-
307
- unless ok
308
- log(WRN, "Cloning '#{nom}' construction - not '#{id}' (#{mth})")
309
- sss = model.getSurfaceByName(nom)
310
- next if sss.empty?
311
-
312
- sss = sss.get
329
+ else
330
+ coll.each do |id, col|
331
+ lc = col[:lc]
332
+
333
+ # Ensure lc is exclusively linked to deratable surfaces of targeted
334
+ # type. If not, assign new lc clone to non-targeted surfaces.
335
+ s.each do |nom, surface|
336
+ next unless surface.key?(:deratable)
337
+ next unless surface.key?(:type)
338
+ next unless surface.key?(:construction)
339
+ next unless surface.key?(:filmRSI)
340
+ next unless surface.key?(:ltype)
341
+ next unless surface.key?(:r)
342
+ next unless surface.key?(:index)
343
+ next unless surface.key?(:net)
344
+ next unless surface[:deratable]
345
+ next unless surface[:construction].is_a?(cl3)
346
+ next unless surface[:construction] == lc
347
+ next if surface[:index ].nil?
348
+ next if surface[:type ] == typ
349
+ next if coll[id][:s].key?(nom)
350
+
351
+ log(INF, "Cloning '#{nom}' construction - not '#{id}' (#{mth})")
352
+ srf = model.getSurfaceByName(nom)
353
+ next if srf.empty?
354
+
355
+ srf = srf.get
313
356
  cloned = lc.clone(model).to_LayeredConstruction.get
314
357
  cloned.setName("#{nom} - cloned")
315
- sss.setConstruction(cloned)
358
+ srf.setConstruction(cloned)
316
359
  surface[:construction] = cloned
317
- coll[id][:s].delete(nom)
318
360
  end
319
361
  end
320
362
 
321
- hloss = 0 # sum of applicable psi+khi-related losses [W/K]
322
-
323
- # Tally applicable psi+khi losses. Possible construction reassignment.
324
- coll.each do |i, col|
325
- col[:s].keys.each do |nom|
326
- next unless s.key?(nom)
327
- next unless s[nom].key?(:construction)
328
- next unless s[nom].key?(:index)
329
- next unless s[nom].key?(:ltype)
330
- next unless s[nom].key?(:r)
331
-
332
- # Tally applicable psi+khi.
333
- hloss += s[nom][:heatloss ] if s[nom].key?(:heatloss)
334
- next if s[nom][:construction] == lc
335
-
336
- # Reassign construction unless referencing lc.
337
- sss = model.getSurfaceByName(nom)
338
- next if sss.empty?
339
-
340
- sss = sss.get
341
-
342
- if sss.isConstructionDefaulted
343
- set = defaultConstructionSet(sss) # building? story?
344
-
345
- if set.nil?
346
- sss.setConstruction(lc)
347
- else
348
- constructions = set.defaultExteriorSurfaceConstructions
349
-
350
- unless constructions.empty?
351
- constructions = constructions.get
352
- constructions.setWallConstruction(lc) if typ == :wall
353
- constructions.setFloorConstruction(lc) if typ == :floor
354
- constructions.setRoofCeilingConstruction(lc) if typ == :ceiling
355
- end
356
- end
357
- else
358
- sss.setConstruction(lc)
359
- end
363
+ coll.each do |id, col|
364
+ col[:s].values.each do |item|
365
+ col[:hloss] += item[:h]
366
+ col[:area ] += item[:a]
367
+ col[:fA ] += item[:a] / item[:f] unless item[:f] < 0
368
+ end
360
369
 
361
- s[nom][:construction] = lc # reset TBD attributes
362
- s[nom][:index ] = lyr[:index]
363
- s[nom][:ltype ] = lyr[:type ]
364
- s[nom][:r ] = lyr[:r ] # temporary
370
+ if col[:area] < TOL
371
+ empty("#{id} area", mth, WRN)
372
+ next
365
373
  end
366
- end
367
374
 
368
- # Merge to ensure a single entry for coll Hash.
369
- coll.each do |i, col|
370
- next if i == id
375
+ # Area-weighted surface air film resistances.
376
+ col[:film] = 1 / (col[:fA] / col[:area])
371
377
 
372
- col[:s].each do |nom, sss|
373
- coll[id][:s][nom] = sss unless coll[id][:s].key?(nom)
378
+ # Fetch required, uprated Uo.
379
+ u = uo(id, col[:lc], col[:area], col[:film], col[:hloss], g[:ut])
380
+
381
+ unless u > UMIN
382
+ log(WRN, "Unable to completely uprate '#{id}' (#{mth})")
383
+ u = UMIN
374
384
  end
375
- end
376
385
 
377
- coll.delete_if { |i, _| i != id }
386
+ col[:u ] = u
387
+ col[:uA] = u * col[:area]
378
388
 
379
- unless coll.size == 1
380
- log(DBG, "Collection == 1? for '#{id}' (#{mth})")
381
- next
382
- end
389
+ # Recoup uprated construction and insulating layer.
390
+ lc = col[:lc]
391
+ lyr = insulatingLayer(lc)
383
392
 
384
- coll[id][:area] = lc.getNetArea
385
- res = uo(model, lc, id, hloss, film, g[:ut])
393
+ # Reset surface :r (uprated RSi of insulation, before derating).
394
+ col[:s].keys.each do |nom|
395
+ next unless s.key?(nom)
396
+ next unless s[nom].key?(:r)
386
397
 
387
- unless res[:uo] && res[:m]
388
- log(WRN, "Unable to uprate '#{id}' (#{mth})")
389
- next
398
+ s[nom][:r] = lyr[:r]
399
+ end
390
400
  end
391
401
 
392
- lyr = insulatingLayer(lc)
393
-
394
- # Loop through coll :s, and reset :r - likely modified by uo().
395
- coll.values.first[:s].keys.each do |nom|
396
- next unless s.key?(nom)
397
- next unless s[nom].key?(:index)
398
- next unless s[nom].key?(:ltype)
399
- next unless s[nom].key?(:r )
400
- next unless s[nom][:index] == lyr[:index]
401
- next unless s[nom][:ltype] == lyr[:type ]
402
+ # Store UA-averaged, upgraded Uo-factor per type.
403
+ area = coll.values.sum { |col| col[:area] }
404
+ uA = coll.values.sum { |col| col[:uA ] }
402
405
 
403
- s[nom][:r] = lyr[:r] # uprated insulating RSi factor, before derating
406
+ if area > TOL
407
+ argh[:wall_uo ] = uA / area if typ == :wall
408
+ argh[:roof_uo ] = uA / area if typ == :ceiling
409
+ argh[:floor_uo] = uA / area if typ == :floor
404
410
  end
405
-
406
- argh[:wall_uo ] = res[:uo] if typ == :wall
407
- argh[:roof_uo ] = res[:uo] if typ == :ceiling
408
- argh[:floor_uo] = res[:uo] if typ == :floor
409
- else
410
- log(WRN, "Nilled construction to uprate - (#{mth})")
411
- return false
412
411
  end
413
412
  end
414
413
 
@@ -442,54 +441,57 @@ module TBD
442
441
  return mismatch("sets", sets, cl1, mth, DBG, false) unless sets.is_a?(cl2)
443
442
 
444
443
  shorts = sets.shorthands("code (Quebec)")
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
448
444
 
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
445
+ if shorts[:has].empty? || shorts[:val].empty?
446
+ log(DBG, "Missing QC PSI set for 3.3 UA' tradeoff (#{mth})")
447
+ return false
448
+ end
449
+
450
+ unless [true, false].include?(spts)
451
+ log(DBG, "setpoints must be true or false for 3.3 UA' tradeoff")
452
+ return false
453
+ end
452
454
 
453
455
  s.each do |id, surface|
454
456
  next unless surface.key?(:deratable)
455
457
  next unless surface[:deratable]
456
458
  next unless surface.key?(:type)
457
459
 
458
- heating = -50 if spts
459
- cooling = 50 if spts
460
- heating = 21 unless spts
461
- cooling = 24 unless spts
462
- heating = surface[:heating] if surface.key?(:heating)
463
- cooling = surface[:cooling] if surface.key?(:cooling)
460
+ htng = spts ? -24 : 21
461
+ clng = spts ? 50 : 24
462
+ htng = surface[:heating] if surface.key?(:heating)
463
+ clng = surface[:cooling] if surface.key?(:cooling)
464
464
 
465
- # Start with surface U-factors.
466
- ref = 1 / 5.46
467
- ref = 1 / 3.60 if surface[:type] == :wall
465
+ # Avoid 'divide by zero' case.
466
+ htng = -24 if htng < -24
468
467
 
469
- # Adjust for lower heating setpoint (assumes -25C design conditions).
470
- ref *= 43 / (heating + 25) if heating < 18 && cooling > 40
468
+ # Start with surface U-factors. Adjust for lower heating setpoint.
469
+ # Assumes -25C design conditions.
470
+ ref = ( surface[:type] == :wall ) ? (1 / 3.60) : (1 / 5.46)
471
+ ref *= 43 / (htng + 25) if htng > -25 && htng < 18 && clng > 40
471
472
 
472
473
  surface[:ref] = ref
473
474
 
474
- if surface.key?(:skylights) # loop through subsurfaces
475
- ref = 2.85
476
- ref *= 43 / (heating + 25) if heating < 18 && cooling > 40
475
+ # Loop through subsurfaces.
476
+ if surface.key?(:skylights)
477
+ ref = 2.85
478
+ ref *= 43 / (htng + 25) if htng > -25 && htng < 18 && clng > 40
477
479
 
478
480
  surface[:skylights].values.map { |skylight| skylight[:ref] = ref }
479
481
  end
480
482
 
481
483
  if surface.key?(:windows)
482
- ref = 2.0
483
- ref *= 43 / (heating + 25) if heating < 18 && cooling > 40
484
+ ref = 2.0
485
+ ref *= 43 / (htng + 25) if htng > -25 && htng < 18 && clng > 40
484
486
 
485
487
  surface[:windows].values.map { |window| window[:ref] = ref }
486
488
  end
487
489
 
488
490
  if surface.key?(:doors)
489
- surface[:doors].each do |i, door|
490
- ref = 0.9
491
- ref = 2.0 if door.key?(:glazed) && door[:glazed]
492
- ref *= 43 / (heating + 25) if heating < 18 && cooling > 40
491
+ surface[:doors].values.each do |door|
492
+ ref = ( door.key?(:glazed) && door[:glazed] ) ? 2.0 : 0.9
493
+ ref *= 43 / (htng + 25) if htng > -25 && htng < 18 && clng > 40
494
+
493
495
  door[:ref] = ref
494
496
  end
495
497
  end
@@ -1000,7 +1002,7 @@ module TBD
1000
1002
  model = "* modèle : #{ua[:file]}" if ua.key?(:file) && lang == :fr
1001
1003
  model += " (v#{ua[:version]})" if ua.key?(:version)
1002
1004
  report << model unless model.empty?
1003
- report << "* TBD : v3.5.2"
1005
+ report << "* TBD : v3.6.0"
1004
1006
  report << "* date : #{ua[:date]}"
1005
1007
 
1006
1008
  if lang == :en