tbd 3.2.1 → 3.2.2

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.
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: []