tbd 3.2.1 → 3.2.2

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 7a5a9002ff406325bc226c157ed29ffc22ccb60ac46b9c6bf29358f04aa9d6a3
4
- data.tar.gz: 60b0a52d46be3a91a2119cb5d3875826e7b278ca9ce78a230e9575263dbb4c89
3
+ metadata.gz: 858b576df6d2a7c00b547c1bcb86fcae917f441c236f012610309b5df96f2d19
4
+ data.tar.gz: ee47f03ce21e7ef8d95968aa0d159a39875ca546c32b48c8f96d7779b1ef341d
5
5
  SHA512:
6
- metadata.gz: df24c1565b802fd84c8f067d78662adf23d5fb2059145b6b22da98b8c06f70b49283ef7f28613e91c1258e3c7b865d17e81df93c2340aec431483597d8ddb847
7
- data.tar.gz: 34ea2406564f08b36a54987ab08fd5d2cb24e49bfb02640169278f1138f65f36ac2c9279abb7e8be83e1c2aec4cca639b8735ab13f54f0470fe0a8470977be86
6
+ metadata.gz: 1d7d4ad54c0d8d06776b868037a717d8efc11d4f1e1290cbf972202bae04ef4e6e6ea21cf227a8405c508c1d31f1bbb9e5667a21af9186984b28eee19a5e4395
7
+ data.tar.gz: eaae502337964abffe89643e4c1e6070f3a228db3ef6d87983f0cc7c9218f124e72eae6e502bd06ff7119fd594ed784d3f4232a3bd1ddb03c1d23f7bcce1c951
@@ -3,8 +3,8 @@
3
3
  <schema_version>3.0</schema_version>
4
4
  <name>tbd_measure</name>
5
5
  <uid>8890787b-8c25-4dc8-8641-b6be1b6c2357</uid>
6
- <version_id>2fc42e1d-2010-44ae-9837-6390512c41d4</version_id>
7
- <version_modified>20230214T105829Z</version_modified>
6
+ <version_id>ca5db4f4-8624-4699-a544-609690a03d14</version_id>
7
+ <version_modified>20230322T224647Z</version_modified>
8
8
  <xml_checksum>99772807</xml_checksum>
9
9
  <class_name>TBDMeasure</class_name>
10
10
  <display_name>Thermal Bridging and Derating - TBD</display_name>
@@ -429,24 +429,12 @@
429
429
  <usage_type>test</usage_type>
430
430
  <checksum>58ED6635</checksum>
431
431
  </file>
432
- <file>
433
- <filename>geo.rb</filename>
434
- <filetype>rb</filetype>
435
- <usage_type>resource</usage_type>
436
- <checksum>F447D8CE</checksum>
437
- </file>
438
432
  <file>
439
433
  <filename>README.md</filename>
440
434
  <filetype>md</filetype>
441
435
  <usage_type>readme</usage_type>
442
436
  <checksum>B836C43A</checksum>
443
437
  </file>
444
- <file>
445
- <filename>ua.rb</filename>
446
- <filetype>rb</filetype>
447
- <usage_type>resource</usage_type>
448
- <checksum>19193778</checksum>
449
- </file>
450
438
  <file>
451
439
  <filename>psi.rb</filename>
452
440
  <filetype>rb</filetype>
@@ -465,5 +453,17 @@
465
453
  <usage_type>resource</usage_type>
466
454
  <checksum>A3BB982A</checksum>
467
455
  </file>
456
+ <file>
457
+ <filename>geo.rb</filename>
458
+ <filetype>rb</filetype>
459
+ <usage_type>resource</usage_type>
460
+ <checksum>8242FCEF</checksum>
461
+ </file>
462
+ <file>
463
+ <filename>ua.rb</filename>
464
+ <filetype>rb</filetype>
465
+ <usage_type>resource</usage_type>
466
+ <checksum>695F5AC2</checksum>
467
+ </file>
468
468
  </files>
469
469
  </measure>
@@ -292,28 +292,31 @@ module TBD
292
292
 
293
293
  return mismatch("model", model, cl1, mth) unless model.is_a?(cl1)
294
294
  return mismatch("surface", surface, cl2, mth) unless surface.is_a?(cl2)
295
-
296
- return nil unless validate(surface)
295
+ return nil unless validate(surface)
297
296
 
298
297
  nom = surface.nameString
299
298
  surf = {}
300
299
  subs = {}
301
300
  fd = false
302
301
  return empty("'#{nom}' space", mth, ERR) if surface.space.empty?
302
+
303
303
  space = surface.space.get
304
304
  stype = space.spaceType
305
305
  story = space.buildingStory
306
306
  tr = transforms(model, space)
307
307
  return invalid("'#{nom}' transform", mth, 0, FTL) unless tr[:t] && tr[:r]
308
+
308
309
  t = tr[:t]
309
310
  n = trueNormal(surface, tr[:r])
310
311
  return invalid("'#{nom}' normal", mth, 0, FTL) unless n
312
+
311
313
  type = surface.surfaceType.downcase
312
314
  facing = surface.outsideBoundaryCondition
313
315
 
314
316
  if facing.downcase == "surface"
315
317
  empty = surface.adjacentSurface.empty?
316
- return invalid("'#{nom}': adjacent surface", mth, 0, ERR) if empty
318
+ return invalid("'#{nom}': adjacent surface", mth, 0, ERR) if empty
319
+
317
320
  facing = surface.adjacentSurface.get.nameString
318
321
  end
319
322
 
@@ -350,6 +353,7 @@ module TBD
350
353
  surf[:story ] = story.get unless story.empty?
351
354
  surf[:n ] = n
352
355
  surf[:gross ] = surface.grossArea
356
+ surf[:filmRSI ] = surface.filmResistance
353
357
 
354
358
  surface.subSurfaces.sort_by { |s| s.nameString }.each do |s|
355
359
  next unless validate(s)
@@ -39,13 +39,14 @@ module TBD
39
39
  cl1 = OpenStudio::Model::Model
40
40
  cl2 = OpenStudio::Model::LayeredConstruction
41
41
  cl3 = Numeric
42
+ cl4 = String
42
43
 
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)
44
+ return mismatch("model", model, cl1, mth, DBG, res) unless model.is_a?(cl1)
45
+ return mismatch("id" , id, cl4, mth, DBG, res) unless id.is_a?(cl4)
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)
49
50
 
50
51
  loss = 0.0 # residual heatloss (not assigned) [W/K]
51
52
  area = lc.getNetArea
@@ -54,12 +55,12 @@ module TBD
54
55
  lyr[:index] = nil unless lyr[:index] >= 0
55
56
  lyr[:index] = nil unless lyr[:index] < lc.layers.size
56
57
 
57
- return invalid("'#{id}' layer index", mth, 0, ERR, res) unless lyr[:index]
58
- return zero("'#{id}': heatloss", mth, WRN, res) unless hloss > TOL
59
- return zero("'#{id}': films", mth, WRN, res) unless film > TOL
60
- return zero("'#{id}': Ut", mth, WRN, res) unless ut > TOL
61
- return invalid("'#{id}': Ut", mth, 0, WRN, res) unless ut < 5.678
62
- return zero("'#{id}': net area (m2)", mth, ERR, res) unless area > TOL
58
+ return invalid("'#{id}' layer index", mth, 0, ERR, res) unless lyr[:index]
59
+ return zero("'#{id}': heatloss" , mth, WRN, res) unless hloss > TOL
60
+ return zero("'#{id}': films" , mth, WRN, res) unless film > TOL
61
+ return zero("'#{id}': Ut" , mth, WRN, res) unless ut > TOL
62
+ return invalid("'#{id}': Ut" , mth, 0, WRN, res) unless ut < 5.678
63
+ return zero("'#{id}': net area (m2)", mth, ERR, res) unless area > TOL
63
64
 
64
65
  # First, calculate initial layer RSi to initially meet Ut target.
65
66
  rt = 1 / ut # target construction Rt
@@ -76,7 +77,7 @@ module TBD
76
77
 
77
78
  if lyr[:type] == :massless
78
79
  m = lc.getLayer(lyr[:index]).to_MasslessOpaqueMaterial
79
- return invalid("'#{id}' massless layer?", mth, 0) if m.empty?
80
+ return invalid("'#{id}' massless layer?", mth, 0, DBG, res) if m.empty?
80
81
 
81
82
  m = m.get.clone(model).to_MasslessOpaqueMaterial.get
82
83
  m.setName("#{id} uprated")
@@ -85,7 +86,7 @@ module TBD
85
86
  m.setThermalResistance(new_r)
86
87
  else # type == :standard
87
88
  m = lc.getLayer(lyr[:index]).to_StandardOpaqueMaterial
88
- return invalid("'#{id}' standard layer?", mth, 0) if m.empty?
89
+ return invalid("'#{id}' standard layer?", mth, 0, DBG, res) if m.empty?
89
90
 
90
91
  m = m.get.clone(model).to_StandardOpaqueMaterial.get
91
92
  m.setName("#{id} uprated")
@@ -141,11 +142,12 @@ module TBD
141
142
  mth = "TBD::#{__callee__}"
142
143
  cl1 = OpenStudio::Model::Model
143
144
  cl2 = Hash
145
+ cl3 = OpenStudio::Model::LayeredConstruction
144
146
  a = false
145
147
 
146
- return mismatch("model", model, cl1, mth, DBG, a) unless model.is_a?(cl1)
147
- return mismatch("surfaces", s, cl2, mth, DBG, a) unless s.is_a?(cl2)
148
- return mismatch("argh", model, cl1, mth, DBG, a) unless argh.is_a?(cl2)
148
+ return mismatch("model" , model, cl1, mth, DBG, a) unless model.is_a?(cl1)
149
+ return mismatch("surfaces", s, cl2, mth, DBG, a) unless s.is_a?(cl2)
150
+ return mismatch("argh" , model, cl1, mth, DBG, a) unless argh.is_a?(cl2)
149
151
 
150
152
  argh[:uprate_walls ] = false unless argh.key?(:uprate_walls )
151
153
  argh[:uprate_roofs ] = false unless argh.key?(:uprate_roofs )
@@ -168,11 +170,13 @@ module TBD
168
170
  groups[:roof ][:op] = argh[:roof_option ]
169
171
  groups[:floor][:op] = argh[:floor_option ]
170
172
 
171
- groups.each do |label, g|
173
+ groups.each do |type, g|
172
174
  next unless g[:up]
173
175
  next unless g[:ut].is_a?(Numeric)
174
176
  next unless g[:ut] < 5.678
175
177
 
178
+ typ = type
179
+ typ = :ceiling if typ == :roof # fix in future revision. TO-DO.
176
180
  coll = {}
177
181
  area = 0
178
182
  film = 100000000000000
@@ -183,86 +187,81 @@ module TBD
183
187
  g[:op].downcase == "all floor constructions"
184
188
 
185
189
  if g[:op].empty?
186
- log(ERR, "Construction to uprate? (#{mth})")
190
+ log(ERR, "Construction (#{type}) to uprate? (#{mth})")
187
191
  elsif all
188
- model.getSurfaces.each do |sss|
189
- next unless sss.surfaceType.downcase.include?(label.to_s)
190
- next unless sss.outsideBoundaryCondition.downcase == "outdoors"
191
- next if sss.construction.empty?
192
- next if sss.construction.get.to_LayeredConstruction.empty?
193
-
194
- c = sss.construction.get.to_LayeredConstruction.get
195
- i = c.nameString
196
-
197
- # Reliable unless referenced by other surface types e.g. floor vs wall.
198
- if c.getNetArea > area
199
- area = c.getNetArea
192
+ s.each do |nom, surface|
193
+ next unless surface.key?(:deratable )
194
+ next unless surface.key?(:type )
195
+ next unless surface.key?(:construction)
196
+ next unless surface.key?(:filmRSI )
197
+ next unless surface.key?(:index )
198
+ next unless surface.key?(:ltype )
199
+ next unless surface.key?(:r )
200
+ next unless surface[:deratable ]
201
+ next unless surface[:type ] == typ
202
+ next unless surface[:construction].is_a?(cl3)
203
+ next if surface[:index ].nil?
204
+
205
+ # Retain lowest surface film resistance (e.g. tilted surfaces).
206
+ c = surface[:construction]
207
+ i = c.nameString
208
+ aire = c.getNetArea
209
+ film = surface[:filmRSI] if surface[:filmRSI] < film
210
+
211
+ # Retain construction covering largest area. The following conditional
212
+ # is reliable UNLESS linked to other deratable surface types e.g. both
213
+ # floors AND walls (see "elsif lc" corrections below).
214
+ if aire > area
200
215
  lc = c
216
+ area = aire
201
217
  id = i
202
218
  end
203
219
 
204
- film = sss.filmResistance if sss.filmResistance < film
205
- nom = sss.nameString
206
- coll[i] = { area: c.getNetArea, lc: c, s: {} } unless coll.key?(i)
207
- coll[i][:s][nom] = { a: sss.netArea } unless coll[i][:s].key?(nom)
220
+ coll[i] = { area: aire, lc: c, s: {} } unless coll.key?(i)
221
+ coll[i][:s][nom] = { a: surface[:net] } unless coll[i][:s].key?(nom)
208
222
  end
209
223
  else
210
224
  id = g[:op]
211
- c = model.getConstructionByName(id)
212
-
213
- if c.empty?
214
- log(ERR, "Construction '#{id}'? (#{mth})")
215
- else
216
- c = c.get.to_LayeredConstruction
217
-
218
- if c.empty?
219
- log(ERR, "'#{id}' layered construction? (#{mth})")
220
- else
221
- lc = c.get
222
- area = lc.getNetArea
223
- coll[id] = { area: area, lc: lc, s: {} }
224
-
225
- model.getSurfaces.each do |sss|
226
- next unless sss.surfaceType.downcase.include?(label.to_s)
227
- next unless sss.outsideBoundaryCondition.downcase == "outdoors"
228
- next if sss.construction.empty?
229
- next if sss.construction.get.to_LayeredConstruction.empty?
230
- lc = sss.construction.get.to_LayeredConstruction.get
231
- next unless id == lc.nameString
232
- nom = sss.nameString
233
- film = sss.filmResistance if sss.filmResistance < film
234
- ok = coll[id][:s].key?(nom)
235
- coll[id][:s][nom] = { a: sss.netArea } unless ok
236
- end
237
- end
225
+ lc = model.getConstructionByName(id)
226
+ log(ERR, "Construction '#{id}'? (#{mth})") if lc.empty?
227
+ next if lc.empty?
228
+
229
+ lc = lc.get.to_LayeredConstruction
230
+ log(ERR, "'#{id}' layered construction? (#{mth})") if lc.empty?
231
+ next if lc.empty?
232
+
233
+ lc = lc.get
234
+ area = lc.getNetArea
235
+ coll[id] = { area: area, lc: lc, s: {} }
236
+
237
+ s.each do |nom, surface|
238
+ next unless surface.key?(:deratable )
239
+ next unless surface.key?(:type )
240
+ next unless surface.key?(:construction)
241
+ next unless surface.key?(:filmRSI )
242
+ next unless surface.key?(:index )
243
+ next unless surface.key?(:ltype )
244
+ next unless surface.key?(:r )
245
+ next unless surface[:deratable ]
246
+ next unless surface[:type ] == typ
247
+ next unless surface[:construction].is_a?(cl3)
248
+ next if surface[:index ].nil?
249
+
250
+ i = surface[:construction].nameString
251
+ next unless i == id
252
+
253
+ # Retain lowest surface film resistance (e.g. tilted surfaces).
254
+ film = surface[:filmRSI] if surface[:filmRSI] < film
255
+
256
+ coll[i][:s][nom] = { a: surface[:net] } unless coll[i][:s].key?(nom)
238
257
  end
239
258
  end
240
259
 
241
260
  if coll.empty?
242
- log(ERR, "No #{label} construction to uprate - skipping (#{mth})")
261
+ log(ERR, "No #{type} construction to uprate - skipping (#{mth})")
243
262
  next
244
- elsif lc # valid layered construction - good to uprate!
245
- # Ensure lc is referenced by surface types == label.
246
- model.getSurfaces.each do |sss|
247
- next if sss.construction.empty?
248
- next if sss.construction.get.to_LayeredConstruction.empty?
249
- c = sss.construction.get.to_LayeredConstruction.get
250
- i = c.nameString
251
- next unless coll.key?(i)
252
-
253
- unless sss.surfaceType.downcase.include?(label.to_s)
254
- log(ERR, "Uprating #{label.to_s}, not '#{sss.nameString}' (#{mth})")
255
- cloned = c.clone(model).to_LayeredConstruction.get
256
- cloned.setName("'#{i}' cloned")
257
- sss.setConstruction(cloned)
258
- ok = s.key?(sss.nameString)
259
- s[sss.nameString][:construction] = cloned if ok
260
- coll[i][:s].delete(sss.nameString)
261
- coll[i][:area] = c.getNetArea
262
- next
263
- end
264
- end
265
-
263
+ elsif lc
264
+ # Valid layered construction - good to uprate!
266
265
  lyr = insulatingLayer(lc)
267
266
  lyr[:index] = nil unless lyr[:index].is_a?(Numeric)
268
267
  lyr[:index] = nil unless lyr[:index] >= 0
@@ -270,64 +269,81 @@ module TBD
270
269
 
271
270
  log(ERR, "Insulation index for '#{id}'? (#{mth})") unless lyr[:index]
272
271
  next unless lyr[:index]
273
- hloss = 0 # sum of applicable psi & khi effects [W/K]
274
272
 
275
- coll.each do |i, col|
276
- next unless col.key?(:s)
277
- next unless col.is_a?(Hash)
273
+ # Ensure lc is exclusively linked to deratable surfaces of right type.
274
+ # If not, assign new lc clone to non-targeted surfaces.
275
+ s.each do |nom, surface|
276
+ next unless surface.key?(:type )
277
+ next unless surface.key?(:deratable )
278
+ next unless surface.key?(:construction)
279
+ next unless surface[:construction].is_a?(cl3)
280
+ next unless surface[:construction] == lc
281
+
282
+ ok = true
283
+ ok = false unless surface[:type ] == typ
284
+ ok = false unless surface[:deratable]
285
+ ok = false unless coll.key?(id)
286
+ ok = false unless coll[id][:s].key?(nom)
287
+
288
+ unless ok
289
+ log(WRN, "Cloning '#{nom}' construction - not '#{id}' (#{mth})")
290
+ sss = model.getSurfaceByName(nom)
291
+ next if sss.empty?
292
+
293
+ sss = sss.get
294
+ cloned = lc.clone(model).to_LayeredConstruction.get
295
+ cloned.setName("#{nom} - cloned")
296
+ sss.setConstruction(cloned)
297
+ surface[:construction] = cloned
298
+ coll[id][:s].delete(nom)
299
+ end
300
+ end
301
+
302
+ hloss = 0 # sum of applicable psi & khi effects [W/K]
278
303
 
304
+ # Tally applicable psi + khi losses. Possible construction reassignment.
305
+ coll.each do |i, col|
279
306
  col[:s].keys.each do |nom|
280
307
  next unless s.key?(nom)
281
- next unless s[nom].key?(:deratable )
282
308
  next unless s[nom].key?(:construction)
283
309
  next unless s[nom].key?(:index )
284
310
  next unless s[nom].key?(:ltype )
285
311
  next unless s[nom].key?(:r )
286
- next unless s[nom].key?(:type )
287
-
288
- next unless s[nom][:deratable]
289
- type = s[nom][:type].to_s.downcase
290
- type = "roof" if type == "ceiling"
291
- next unless type.include?(label.to_s)
292
312
 
293
313
  # Tally applicable psi + khi.
294
- hloss += s[nom][:heatloss] if s[nom].key?(:heatloss)
295
-
296
- # Skip construction reassignment if already referencing right one.
297
- unless s[nom][:construction] == lc
298
- sss = model.getSurfaceByName(nom)
299
- next if sss.empty?
300
- sss = sss.get
301
-
302
- if sss.isConstructionDefaulted
303
- set = defaultConstructionSet(model, sss)
304
- constructions = set.defaultExteriorSurfaceConstructions.get
305
-
306
- case sss.surfaceType.downcase
307
- when "roofceiling"
308
- constructions.setRoofCeilingConstruction(lc)
309
- when "floor"
310
- constructions.setFloorConstruction(lc)
311
- else
312
- constructions.setWallConstruction(lc)
313
- end
314
- else
315
- sss.setConstruction(lc)
316
- end
314
+ hloss += s[nom][:heatloss ] if s[nom].key?(:heatloss)
315
+ next if s[nom][:construction] == lc
316
+
317
+ # Reassign construction unless referencing lc.
318
+ sss = model.getSurfaceByName(nom)
319
+ next if sss.empty?
317
320
 
318
- s[nom][:construction] = lc # reset TBD attributes
319
- s[nom][:index ] = lyr[:index]
320
- s[nom][:ltype ] = lyr[:type ]
321
- s[nom][:r ] = lyr[:r ] # temporary
321
+ sss = sss.get
322
+
323
+ if sss.isConstructionDefaulted
324
+ set = defaultConstructionSet(model, sss) # building? story?
325
+ constructions = set.defaultExteriorSurfaceConstructions
326
+
327
+ unless constructions.empty?
328
+ constructions = constructions.get
329
+ constructions.setWallConstruction(lc) if typ == :wall
330
+ constructions.setFloorConstruction(lc) if typ == :floor
331
+ constructions.setRoofCeilingConstruction(lc) if typ == :ceiling
332
+ end
333
+ else
334
+ sss.setConstruction(lc)
322
335
  end
336
+
337
+ s[nom][:construction] = lc # reset TBD attributes
338
+ s[nom][:index ] = lyr[:index]
339
+ s[nom][:ltype ] = lyr[:type ]
340
+ s[nom][:r ] = lyr[:r ] # temporary
323
341
  end
324
342
  end
325
343
 
326
344
  # Merge to ensure a single entry for coll Hash.
327
345
  coll.each do |i, col|
328
346
  next if i == id
329
- next unless coll.key?(id)
330
- coll[id][:area] += col[:area]
331
347
 
332
348
  col[:s].each do |nom, sss|
333
349
  coll[id][:s][nom] = sss unless coll[id][:s].key?(nom)
@@ -338,6 +354,8 @@ module TBD
338
354
  log(DBG, "Collection == 1? for '#{id}' (#{mth})") unless coll.size == 1
339
355
  next unless coll.size == 1
340
356
 
357
+ area = lc.getNetArea
358
+ coll[id][:area] = area
341
359
  res = uo(model, lc, id, hloss, film, g[:ut])
342
360
  log(ERR, "Unable to uprate '#{id}' (#{mth})") unless res[:uo] && res[:m]
343
361
  next unless res[:uo] && res[:m]
@@ -347,27 +365,18 @@ module TBD
347
365
  # Loop through coll :s, and reset :r - likely modified by uo().
348
366
  coll.values.first[:s].keys.each do |nom|
349
367
  next unless s.key?(nom)
350
- next unless s[nom].key?(:deratable )
351
- next unless s[nom].key?(:construction)
352
- next unless s[nom].key?(:index )
353
- next unless s[nom].key?(:ltype )
354
- next unless s[nom].key?(:type )
355
-
356
- next unless s[nom][:deratable ]
357
- next unless s[nom][:construction] == lc
358
- next unless s[nom][:index ] == lyr[:index]
359
- next unless s[nom][:ltype ] == lyr[:type]
360
-
361
- type = s[nom][:type].to_s.downcase
362
- type = "roof" if type == "ceiling"
363
- next unless type.include?(label.to_s)
364
- next unless s[nom].key?(:r)
365
- s[nom][:r] = lyr[:r] # final
368
+ next unless s[nom].key?(:index)
369
+ next unless s[nom].key?(:ltype)
370
+ next unless s[nom].key?(:r )
371
+ next unless s[nom][:index] == lyr[:index]
372
+ next unless s[nom][:ltype] == lyr[:type ]
373
+
374
+ s[nom][:r] = lyr[:r] # uprated insulating RSi factor, before derating
366
375
  end
367
376
 
368
- argh[:wall_uo ] = res[:uo] if label == :wall
369
- argh[:roof_uo ] = res[:uo] if label == :roof
370
- argh[:floor_uo] = res[:uo] if label == :floor
377
+ argh[:wall_uo ] = res[:uo] if typ == :wall
378
+ argh[:roof_uo ] = res[:uo] if typ == :ceiling
379
+ argh[:floor_uo] = res[:uo] if typ == :floor
371
380
  else
372
381
  log(ERR, "Nilled construction to uprate - (#{mth})")
373
382
  return false
@@ -920,7 +929,7 @@ module TBD
920
929
  model = "* modèle : #{ua[:file]}" if ua.key?(:file) && lang == :fr
921
930
  model += " (v#{ua[:version]})" if ua.key?(:version)
922
931
  report << model unless model.empty?
923
- report << "* TBD : v3.2.1"
932
+ report << "* TBD : v3.2.2"
924
933
  report << "* date : #{ua[:date]}"
925
934
 
926
935
  if lang == :en
data/lib/tbd/geo.rb CHANGED
@@ -292,28 +292,31 @@ module TBD
292
292
 
293
293
  return mismatch("model", model, cl1, mth) unless model.is_a?(cl1)
294
294
  return mismatch("surface", surface, cl2, mth) unless surface.is_a?(cl2)
295
-
296
- return nil unless validate(surface)
295
+ return nil unless validate(surface)
297
296
 
298
297
  nom = surface.nameString
299
298
  surf = {}
300
299
  subs = {}
301
300
  fd = false
302
301
  return empty("'#{nom}' space", mth, ERR) if surface.space.empty?
302
+
303
303
  space = surface.space.get
304
304
  stype = space.spaceType
305
305
  story = space.buildingStory
306
306
  tr = transforms(model, space)
307
307
  return invalid("'#{nom}' transform", mth, 0, FTL) unless tr[:t] && tr[:r]
308
+
308
309
  t = tr[:t]
309
310
  n = trueNormal(surface, tr[:r])
310
311
  return invalid("'#{nom}' normal", mth, 0, FTL) unless n
312
+
311
313
  type = surface.surfaceType.downcase
312
314
  facing = surface.outsideBoundaryCondition
313
315
 
314
316
  if facing.downcase == "surface"
315
317
  empty = surface.adjacentSurface.empty?
316
- return invalid("'#{nom}': adjacent surface", mth, 0, ERR) if empty
318
+ return invalid("'#{nom}': adjacent surface", mth, 0, ERR) if empty
319
+
317
320
  facing = surface.adjacentSurface.get.nameString
318
321
  end
319
322
 
@@ -350,6 +353,7 @@ module TBD
350
353
  surf[:story ] = story.get unless story.empty?
351
354
  surf[:n ] = n
352
355
  surf[:gross ] = surface.grossArea
356
+ surf[:filmRSI ] = surface.filmResistance
353
357
 
354
358
  surface.subSurfaces.sort_by { |s| s.nameString }.each do |s|
355
359
  next unless validate(s)
data/lib/tbd/ua.rb CHANGED
@@ -39,13 +39,14 @@ module TBD
39
39
  cl1 = OpenStudio::Model::Model
40
40
  cl2 = OpenStudio::Model::LayeredConstruction
41
41
  cl3 = Numeric
42
+ cl4 = String
42
43
 
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)
44
+ return mismatch("model", model, cl1, mth, DBG, res) unless model.is_a?(cl1)
45
+ return mismatch("id" , id, cl4, mth, DBG, res) unless id.is_a?(cl4)
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)
49
50
 
50
51
  loss = 0.0 # residual heatloss (not assigned) [W/K]
51
52
  area = lc.getNetArea
@@ -54,12 +55,12 @@ module TBD
54
55
  lyr[:index] = nil unless lyr[:index] >= 0
55
56
  lyr[:index] = nil unless lyr[:index] < lc.layers.size
56
57
 
57
- return invalid("'#{id}' layer index", mth, 0, ERR, res) unless lyr[:index]
58
- return zero("'#{id}': heatloss", mth, WRN, res) unless hloss > TOL
59
- return zero("'#{id}': films", mth, WRN, res) unless film > TOL
60
- return zero("'#{id}': Ut", mth, WRN, res) unless ut > TOL
61
- return invalid("'#{id}': Ut", mth, 0, WRN, res) unless ut < 5.678
62
- return zero("'#{id}': net area (m2)", mth, ERR, res) unless area > TOL
58
+ return invalid("'#{id}' layer index", mth, 0, ERR, res) unless lyr[:index]
59
+ return zero("'#{id}': heatloss" , mth, WRN, res) unless hloss > TOL
60
+ return zero("'#{id}': films" , mth, WRN, res) unless film > TOL
61
+ return zero("'#{id}': Ut" , mth, WRN, res) unless ut > TOL
62
+ return invalid("'#{id}': Ut" , mth, 0, WRN, res) unless ut < 5.678
63
+ return zero("'#{id}': net area (m2)", mth, ERR, res) unless area > TOL
63
64
 
64
65
  # First, calculate initial layer RSi to initially meet Ut target.
65
66
  rt = 1 / ut # target construction Rt
@@ -76,7 +77,7 @@ module TBD
76
77
 
77
78
  if lyr[:type] == :massless
78
79
  m = lc.getLayer(lyr[:index]).to_MasslessOpaqueMaterial
79
- return invalid("'#{id}' massless layer?", mth, 0) if m.empty?
80
+ return invalid("'#{id}' massless layer?", mth, 0, DBG, res) if m.empty?
80
81
 
81
82
  m = m.get.clone(model).to_MasslessOpaqueMaterial.get
82
83
  m.setName("#{id} uprated")
@@ -85,7 +86,7 @@ module TBD
85
86
  m.setThermalResistance(new_r)
86
87
  else # type == :standard
87
88
  m = lc.getLayer(lyr[:index]).to_StandardOpaqueMaterial
88
- return invalid("'#{id}' standard layer?", mth, 0) if m.empty?
89
+ return invalid("'#{id}' standard layer?", mth, 0, DBG, res) if m.empty?
89
90
 
90
91
  m = m.get.clone(model).to_StandardOpaqueMaterial.get
91
92
  m.setName("#{id} uprated")
@@ -141,11 +142,12 @@ module TBD
141
142
  mth = "TBD::#{__callee__}"
142
143
  cl1 = OpenStudio::Model::Model
143
144
  cl2 = Hash
145
+ cl3 = OpenStudio::Model::LayeredConstruction
144
146
  a = false
145
147
 
146
- return mismatch("model", model, cl1, mth, DBG, a) unless model.is_a?(cl1)
147
- return mismatch("surfaces", s, cl2, mth, DBG, a) unless s.is_a?(cl2)
148
- return mismatch("argh", model, cl1, mth, DBG, a) unless argh.is_a?(cl2)
148
+ return mismatch("model" , model, cl1, mth, DBG, a) unless model.is_a?(cl1)
149
+ return mismatch("surfaces", s, cl2, mth, DBG, a) unless s.is_a?(cl2)
150
+ return mismatch("argh" , model, cl1, mth, DBG, a) unless argh.is_a?(cl2)
149
151
 
150
152
  argh[:uprate_walls ] = false unless argh.key?(:uprate_walls )
151
153
  argh[:uprate_roofs ] = false unless argh.key?(:uprate_roofs )
@@ -168,11 +170,13 @@ module TBD
168
170
  groups[:roof ][:op] = argh[:roof_option ]
169
171
  groups[:floor][:op] = argh[:floor_option ]
170
172
 
171
- groups.each do |label, g|
173
+ groups.each do |type, g|
172
174
  next unless g[:up]
173
175
  next unless g[:ut].is_a?(Numeric)
174
176
  next unless g[:ut] < 5.678
175
177
 
178
+ typ = type
179
+ typ = :ceiling if typ == :roof # fix in future revision. TO-DO.
176
180
  coll = {}
177
181
  area = 0
178
182
  film = 100000000000000
@@ -183,86 +187,81 @@ module TBD
183
187
  g[:op].downcase == "all floor constructions"
184
188
 
185
189
  if g[:op].empty?
186
- log(ERR, "Construction to uprate? (#{mth})")
190
+ log(ERR, "Construction (#{type}) to uprate? (#{mth})")
187
191
  elsif all
188
- model.getSurfaces.each do |sss|
189
- next unless sss.surfaceType.downcase.include?(label.to_s)
190
- next unless sss.outsideBoundaryCondition.downcase == "outdoors"
191
- next if sss.construction.empty?
192
- next if sss.construction.get.to_LayeredConstruction.empty?
193
-
194
- c = sss.construction.get.to_LayeredConstruction.get
195
- i = c.nameString
196
-
197
- # Reliable unless referenced by other surface types e.g. floor vs wall.
198
- if c.getNetArea > area
199
- area = c.getNetArea
192
+ s.each do |nom, surface|
193
+ next unless surface.key?(:deratable )
194
+ next unless surface.key?(:type )
195
+ next unless surface.key?(:construction)
196
+ next unless surface.key?(:filmRSI )
197
+ next unless surface.key?(:index )
198
+ next unless surface.key?(:ltype )
199
+ next unless surface.key?(:r )
200
+ next unless surface[:deratable ]
201
+ next unless surface[:type ] == typ
202
+ next unless surface[:construction].is_a?(cl3)
203
+ next if surface[:index ].nil?
204
+
205
+ # Retain lowest surface film resistance (e.g. tilted surfaces).
206
+ c = surface[:construction]
207
+ i = c.nameString
208
+ aire = c.getNetArea
209
+ film = surface[:filmRSI] if surface[:filmRSI] < film
210
+
211
+ # Retain construction covering largest area. The following conditional
212
+ # is reliable UNLESS linked to other deratable surface types e.g. both
213
+ # floors AND walls (see "elsif lc" corrections below).
214
+ if aire > area
200
215
  lc = c
216
+ area = aire
201
217
  id = i
202
218
  end
203
219
 
204
- film = sss.filmResistance if sss.filmResistance < film
205
- nom = sss.nameString
206
- coll[i] = { area: c.getNetArea, lc: c, s: {} } unless coll.key?(i)
207
- coll[i][:s][nom] = { a: sss.netArea } unless coll[i][:s].key?(nom)
220
+ coll[i] = { area: aire, lc: c, s: {} } unless coll.key?(i)
221
+ coll[i][:s][nom] = { a: surface[:net] } unless coll[i][:s].key?(nom)
208
222
  end
209
223
  else
210
224
  id = g[:op]
211
- c = model.getConstructionByName(id)
212
-
213
- if c.empty?
214
- log(ERR, "Construction '#{id}'? (#{mth})")
215
- else
216
- c = c.get.to_LayeredConstruction
217
-
218
- if c.empty?
219
- log(ERR, "'#{id}' layered construction? (#{mth})")
220
- else
221
- lc = c.get
222
- area = lc.getNetArea
223
- coll[id] = { area: area, lc: lc, s: {} }
224
-
225
- model.getSurfaces.each do |sss|
226
- next unless sss.surfaceType.downcase.include?(label.to_s)
227
- next unless sss.outsideBoundaryCondition.downcase == "outdoors"
228
- next if sss.construction.empty?
229
- next if sss.construction.get.to_LayeredConstruction.empty?
230
- lc = sss.construction.get.to_LayeredConstruction.get
231
- next unless id == lc.nameString
232
- nom = sss.nameString
233
- film = sss.filmResistance if sss.filmResistance < film
234
- ok = coll[id][:s].key?(nom)
235
- coll[id][:s][nom] = { a: sss.netArea } unless ok
236
- end
237
- end
225
+ lc = model.getConstructionByName(id)
226
+ log(ERR, "Construction '#{id}'? (#{mth})") if lc.empty?
227
+ next if lc.empty?
228
+
229
+ lc = lc.get.to_LayeredConstruction
230
+ log(ERR, "'#{id}' layered construction? (#{mth})") if lc.empty?
231
+ next if lc.empty?
232
+
233
+ lc = lc.get
234
+ area = lc.getNetArea
235
+ coll[id] = { area: area, lc: lc, s: {} }
236
+
237
+ s.each do |nom, surface|
238
+ next unless surface.key?(:deratable )
239
+ next unless surface.key?(:type )
240
+ next unless surface.key?(:construction)
241
+ next unless surface.key?(:filmRSI )
242
+ next unless surface.key?(:index )
243
+ next unless surface.key?(:ltype )
244
+ next unless surface.key?(:r )
245
+ next unless surface[:deratable ]
246
+ next unless surface[:type ] == typ
247
+ next unless surface[:construction].is_a?(cl3)
248
+ next if surface[:index ].nil?
249
+
250
+ i = surface[:construction].nameString
251
+ next unless i == id
252
+
253
+ # Retain lowest surface film resistance (e.g. tilted surfaces).
254
+ film = surface[:filmRSI] if surface[:filmRSI] < film
255
+
256
+ coll[i][:s][nom] = { a: surface[:net] } unless coll[i][:s].key?(nom)
238
257
  end
239
258
  end
240
259
 
241
260
  if coll.empty?
242
- log(ERR, "No #{label} construction to uprate - skipping (#{mth})")
261
+ log(ERR, "No #{type} construction to uprate - skipping (#{mth})")
243
262
  next
244
- elsif lc # valid layered construction - good to uprate!
245
- # Ensure lc is referenced by surface types == label.
246
- model.getSurfaces.each do |sss|
247
- next if sss.construction.empty?
248
- next if sss.construction.get.to_LayeredConstruction.empty?
249
- c = sss.construction.get.to_LayeredConstruction.get
250
- i = c.nameString
251
- next unless coll.key?(i)
252
-
253
- unless sss.surfaceType.downcase.include?(label.to_s)
254
- log(ERR, "Uprating #{label.to_s}, not '#{sss.nameString}' (#{mth})")
255
- cloned = c.clone(model).to_LayeredConstruction.get
256
- cloned.setName("'#{i}' cloned")
257
- sss.setConstruction(cloned)
258
- ok = s.key?(sss.nameString)
259
- s[sss.nameString][:construction] = cloned if ok
260
- coll[i][:s].delete(sss.nameString)
261
- coll[i][:area] = c.getNetArea
262
- next
263
- end
264
- end
265
-
263
+ elsif lc
264
+ # Valid layered construction - good to uprate!
266
265
  lyr = insulatingLayer(lc)
267
266
  lyr[:index] = nil unless lyr[:index].is_a?(Numeric)
268
267
  lyr[:index] = nil unless lyr[:index] >= 0
@@ -270,64 +269,81 @@ module TBD
270
269
 
271
270
  log(ERR, "Insulation index for '#{id}'? (#{mth})") unless lyr[:index]
272
271
  next unless lyr[:index]
273
- hloss = 0 # sum of applicable psi & khi effects [W/K]
274
272
 
275
- coll.each do |i, col|
276
- next unless col.key?(:s)
277
- next unless col.is_a?(Hash)
273
+ # Ensure lc is exclusively linked to deratable surfaces of right type.
274
+ # If not, assign new lc clone to non-targeted surfaces.
275
+ s.each do |nom, surface|
276
+ next unless surface.key?(:type )
277
+ next unless surface.key?(:deratable )
278
+ next unless surface.key?(:construction)
279
+ next unless surface[:construction].is_a?(cl3)
280
+ next unless surface[:construction] == lc
281
+
282
+ ok = true
283
+ ok = false unless surface[:type ] == typ
284
+ ok = false unless surface[:deratable]
285
+ ok = false unless coll.key?(id)
286
+ ok = false unless coll[id][:s].key?(nom)
287
+
288
+ unless ok
289
+ log(WRN, "Cloning '#{nom}' construction - not '#{id}' (#{mth})")
290
+ sss = model.getSurfaceByName(nom)
291
+ next if sss.empty?
292
+
293
+ sss = sss.get
294
+ cloned = lc.clone(model).to_LayeredConstruction.get
295
+ cloned.setName("#{nom} - cloned")
296
+ sss.setConstruction(cloned)
297
+ surface[:construction] = cloned
298
+ coll[id][:s].delete(nom)
299
+ end
300
+ end
301
+
302
+ hloss = 0 # sum of applicable psi & khi effects [W/K]
278
303
 
304
+ # Tally applicable psi + khi losses. Possible construction reassignment.
305
+ coll.each do |i, col|
279
306
  col[:s].keys.each do |nom|
280
307
  next unless s.key?(nom)
281
- next unless s[nom].key?(:deratable )
282
308
  next unless s[nom].key?(:construction)
283
309
  next unless s[nom].key?(:index )
284
310
  next unless s[nom].key?(:ltype )
285
311
  next unless s[nom].key?(:r )
286
- next unless s[nom].key?(:type )
287
-
288
- next unless s[nom][:deratable]
289
- type = s[nom][:type].to_s.downcase
290
- type = "roof" if type == "ceiling"
291
- next unless type.include?(label.to_s)
292
312
 
293
313
  # Tally applicable psi + khi.
294
- hloss += s[nom][:heatloss] if s[nom].key?(:heatloss)
295
-
296
- # Skip construction reassignment if already referencing right one.
297
- unless s[nom][:construction] == lc
298
- sss = model.getSurfaceByName(nom)
299
- next if sss.empty?
300
- sss = sss.get
301
-
302
- if sss.isConstructionDefaulted
303
- set = defaultConstructionSet(model, sss)
304
- constructions = set.defaultExteriorSurfaceConstructions.get
305
-
306
- case sss.surfaceType.downcase
307
- when "roofceiling"
308
- constructions.setRoofCeilingConstruction(lc)
309
- when "floor"
310
- constructions.setFloorConstruction(lc)
311
- else
312
- constructions.setWallConstruction(lc)
313
- end
314
- else
315
- sss.setConstruction(lc)
316
- end
314
+ hloss += s[nom][:heatloss ] if s[nom].key?(:heatloss)
315
+ next if s[nom][:construction] == lc
316
+
317
+ # Reassign construction unless referencing lc.
318
+ sss = model.getSurfaceByName(nom)
319
+ next if sss.empty?
317
320
 
318
- s[nom][:construction] = lc # reset TBD attributes
319
- s[nom][:index ] = lyr[:index]
320
- s[nom][:ltype ] = lyr[:type ]
321
- s[nom][:r ] = lyr[:r ] # temporary
321
+ sss = sss.get
322
+
323
+ if sss.isConstructionDefaulted
324
+ set = defaultConstructionSet(model, sss) # building? story?
325
+ constructions = set.defaultExteriorSurfaceConstructions
326
+
327
+ unless constructions.empty?
328
+ constructions = constructions.get
329
+ constructions.setWallConstruction(lc) if typ == :wall
330
+ constructions.setFloorConstruction(lc) if typ == :floor
331
+ constructions.setRoofCeilingConstruction(lc) if typ == :ceiling
332
+ end
333
+ else
334
+ sss.setConstruction(lc)
322
335
  end
336
+
337
+ s[nom][:construction] = lc # reset TBD attributes
338
+ s[nom][:index ] = lyr[:index]
339
+ s[nom][:ltype ] = lyr[:type ]
340
+ s[nom][:r ] = lyr[:r ] # temporary
323
341
  end
324
342
  end
325
343
 
326
344
  # Merge to ensure a single entry for coll Hash.
327
345
  coll.each do |i, col|
328
346
  next if i == id
329
- next unless coll.key?(id)
330
- coll[id][:area] += col[:area]
331
347
 
332
348
  col[:s].each do |nom, sss|
333
349
  coll[id][:s][nom] = sss unless coll[id][:s].key?(nom)
@@ -338,6 +354,8 @@ module TBD
338
354
  log(DBG, "Collection == 1? for '#{id}' (#{mth})") unless coll.size == 1
339
355
  next unless coll.size == 1
340
356
 
357
+ area = lc.getNetArea
358
+ coll[id][:area] = area
341
359
  res = uo(model, lc, id, hloss, film, g[:ut])
342
360
  log(ERR, "Unable to uprate '#{id}' (#{mth})") unless res[:uo] && res[:m]
343
361
  next unless res[:uo] && res[:m]
@@ -347,27 +365,18 @@ module TBD
347
365
  # Loop through coll :s, and reset :r - likely modified by uo().
348
366
  coll.values.first[:s].keys.each do |nom|
349
367
  next unless s.key?(nom)
350
- next unless s[nom].key?(:deratable )
351
- next unless s[nom].key?(:construction)
352
- next unless s[nom].key?(:index )
353
- next unless s[nom].key?(:ltype )
354
- next unless s[nom].key?(:type )
355
-
356
- next unless s[nom][:deratable ]
357
- next unless s[nom][:construction] == lc
358
- next unless s[nom][:index ] == lyr[:index]
359
- next unless s[nom][:ltype ] == lyr[:type]
360
-
361
- type = s[nom][:type].to_s.downcase
362
- type = "roof" if type == "ceiling"
363
- next unless type.include?(label.to_s)
364
- next unless s[nom].key?(:r)
365
- s[nom][:r] = lyr[:r] # final
368
+ next unless s[nom].key?(:index)
369
+ next unless s[nom].key?(:ltype)
370
+ next unless s[nom].key?(:r )
371
+ next unless s[nom][:index] == lyr[:index]
372
+ next unless s[nom][:ltype] == lyr[:type ]
373
+
374
+ s[nom][:r] = lyr[:r] # uprated insulating RSi factor, before derating
366
375
  end
367
376
 
368
- argh[:wall_uo ] = res[:uo] if label == :wall
369
- argh[:roof_uo ] = res[:uo] if label == :roof
370
- argh[:floor_uo] = res[:uo] if label == :floor
377
+ argh[:wall_uo ] = res[:uo] if typ == :wall
378
+ argh[:roof_uo ] = res[:uo] if typ == :ceiling
379
+ argh[:floor_uo] = res[:uo] if typ == :floor
371
380
  else
372
381
  log(ERR, "Nilled construction to uprate - (#{mth})")
373
382
  return false
@@ -920,7 +929,7 @@ module TBD
920
929
  model = "* modèle : #{ua[:file]}" if ua.key?(:file) && lang == :fr
921
930
  model += " (v#{ua[:version]})" if ua.key?(:version)
922
931
  report << model unless model.empty?
923
- report << "* TBD : v3.2.1"
932
+ report << "* TBD : v3.2.2"
924
933
  report << "* date : #{ua[:date]}"
925
934
 
926
935
  if lang == :en
data/lib/tbd/version.rb CHANGED
@@ -21,5 +21,5 @@
21
21
  # SOFTWARE.
22
22
 
23
23
  module TBD
24
- VERSION = "3.2.1".freeze
24
+ VERSION = "3.2.2".freeze
25
25
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: tbd
3
3
  version: !ruby/object:Gem::Version
4
- version: 3.2.1
4
+ version: 3.2.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Denis Bourgeois & Dan Macumber
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2023-02-16 00:00:00.000000000 Z
11
+ date: 2023-03-23 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: topolys
@@ -161,7 +161,7 @@ licenses:
161
161
  - MIT
162
162
  metadata:
163
163
  homepage_uri: https://github.com/rd2/tbd
164
- source_code_uri: https://github.com/rd2/tbd/tree/v3.2.1
164
+ source_code_uri: https://github.com/rd2/tbd/tree/v3.2.2
165
165
  bug_tracker_uri: https://github.com/rd2/tbd/issues
166
166
  post_install_message:
167
167
  rdoc_options: []