tbd 3.2.3 → 3.3.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -21,9 +21,9 @@
21
21
  # SOFTWARE.
22
22
 
23
23
  module TBD
24
- # Sources for thermal bridge types and default KHI & PSI values/sets:
24
+ # Sources for thermal bridge types and default KHI- & PSI-factor sets:
25
25
  #
26
- # a) BETBG = Building Envelope Thermal Bridging Guide v1.4 (or higher):
26
+ # a) BETBG = Building Envelope Thermal Bridging Guide v1.4 (or newer):
27
27
  #
28
28
  # www.bchydro.com/content/dam/BCHydro/customer-portal/documents/power-smart/
29
29
  # business/programs/BETB-Building-Envelope-Thermal-Bridging-Guide-v1-4.pdf
@@ -42,47 +42,63 @@ module TBD
42
42
  # Library of point thermal bridges (e.g. columns). Each key:value entry
43
43
  # requires a unique identifier e.g. "poor (BETBG)" and a KHI-value in W/K.
44
44
  class KHI
45
+ extend OSut
46
+
45
47
  # @return [Hash] KHI library
46
48
  attr_reader :point
47
49
 
48
50
  ##
49
- # Construct a new KHI library (with defaults).
51
+ # Constructs a new KHI library (with defaults).
50
52
  def initialize
51
53
  @point = {}
52
54
 
53
- # The following are defaults. Users may edit these defaults,
54
- # append new key:value pairs, or even read-in other pairs on file.
55
- # Units are in W/K.
56
- @point["poor (BETBG)" ] = 0.900 # detail 5.7.2 BETBG
57
- @point["regular (BETBG)" ] = 0.500 # detail 5.7.4 BETBG
58
- @point["efficient (BETBG)" ] = 0.150 # detail 5.7.3 BETBG
59
- @point["code (Quebec)" ] = 0.500 # art. 3.3.1.3. NECB-QC
60
- @point["uncompliant (Quebec)" ] = 1.000 # Guide
61
- @point["(non thermal bridging)"] = 0.000
55
+ # The following are built-in KHI-factors. Users may append new key:value
56
+ # pairs, preferably through a TBD JSON input file. Units are in W/K.
57
+ @point["poor (BETBG)" ] = 0.900 # detail 5.7.2 BETBG
58
+ @point["regular (BETBG)" ] = 0.500 # detail 5.7.4 BETBG
59
+ @point["efficient (BETBG)" ] = 0.150 # detail 5.7.3 BETBG
60
+ @point["code (Quebec)" ] = 0.500 # art. 3.3.1.3. NECB-QC
61
+ @point["uncompliant (Quebec)" ] = 1.000 # NECB-QC Guide
62
+ @point["90.1.22|steel.m|default" ] = 0.480 # steel/metal, compliant
63
+ @point["90.1.22|steel.m|unmitigated"] = 0.920 # steel/metal, non-compliant
64
+ @point["90.1.22|mass.ex|default" ] = 0.330 # ext/integral, compliant
65
+ @point["90.1.22|mass.ex|unmitigated"] = 0.460 # ext/integral, non-compliant
66
+ @point["90.1.22|mass.in|default" ] = 0.330 # interior mass, compliant
67
+ @point["90.1.22|mass.in|unmitigated"] = 0.460 # interior, non-compliant
68
+ @point["90.1.22|wood.fr|default" ] = 0.040 # compliant
69
+ @point["90.1.22|wood.fr|unmitigated"] = 0.330 # non-compliant
70
+ @point["(non thermal bridging)" ] = 0.000 # defaults to 0
62
71
  end
63
72
 
64
73
  ##
65
- # Append a new KHI entry, based on a TBD JSON-formatted KHI object (requires
66
- # a valid, unique :id key and valid :point value).
74
+ # Appends a new KHI entry.
67
75
  #
68
- # @param k [Hash] a new KHI entry
76
+ # @param [Hash] k a new KHI entry
77
+ # @option k [#to_s] :id name
78
+ # @option k [#to_f] :point conductance, in W/K
69
79
  #
70
- # @return [Bool] true if successfully appended
71
- # @return [Bool] false if invalid input
80
+ # @return [Bool] whether KHI entry is successfully appended
81
+ # @return [false] if invalid input (see logs)
72
82
  def append(k = {})
73
83
  mth = "TBD::#{__callee__}"
74
84
  a = false
75
-
76
- return TBD.mismatch("KHI", k, Hash, mth, DBG, a) unless k.is_a?(Hash)
77
- return TBD.hashkey("KHI id", k, :id, mth, DBG, a) unless k.key?(:id)
78
- return TBD.hashkey("KHI pt", k, :point, mth, DBG, a) unless k.key?(:point)
79
-
80
- if @point.key?(k[:id])
81
- TBD.log(ERR, "Skipping '#{k[:id]}': existing KHI entry (#{mth})")
85
+ ck1 = k.respond_to?(:key?)
86
+ return mismatch("KHI" , k, Hash , mth, DBG, a) unless ck1
87
+ return hashkey("KHI id" , k, :id , mth, DBG, a) unless k.key?(:id)
88
+ return hashkey("KHI point", k, :point, mth, DBG, a) unless k.key?(:point)
89
+
90
+ id = trim(k[:id])
91
+ ck1 = id.empty?
92
+ ck2 = k[:point].respond_to?(:to_f)
93
+ return mismatch("KHI id" , k[:id ], String, mth, ERR, a) if ck1
94
+ return mismatch("KHI point", k[:point], Float , mth, ERR, a) unless ck2
95
+
96
+ if @point.key?(id)
97
+ log(ERR, "Skipping '#{id}': existing KHI entry (#{mth})")
82
98
  return false
83
99
  end
84
100
 
85
- @point[k[:id]] = k[:point]
101
+ @point[id] = k[:point].to_f
86
102
 
87
103
  true
88
104
  end
@@ -91,302 +107,753 @@ module TBD
91
107
  ##
92
108
  # Library of linear thermal bridges (e.g. corners, balconies). Each key:value
93
109
  # entry requires a unique identifier e.g. "poor (BETBG)" and a (partial or
94
- # complete) set of PSI-values in W/K per linear meter.
110
+ # complete) set of PSI-factors in W/K per linear meter.
95
111
  class PSI
112
+ extend OSut
113
+
96
114
  # @return [Hash] PSI set
97
115
  attr_reader :set
98
116
 
99
117
  # @return [Hash] shorthand listing of PSI types in a set
100
118
  attr_reader :has
101
119
 
102
- # @return [Hash] shorthand listing of PSI values in a set
120
+ # @return [Hash] shorthand listing of PSI-factors in a set
103
121
  attr_reader :val
104
122
 
105
123
  ##
106
- # Construct a new PSI library (with defaults)
124
+ # Constructs a new PSI library (with defaults)
107
125
  def initialize
108
126
  @set = {}
109
127
  @has = {}
110
128
  @val = {}
111
129
 
112
- # The following are default PSI values (* published, ** calculated). Users
113
- # may edit these sets, add new sets here, or read-in custom sets from a
114
- # TBD JSON input file. PSI units are in W/K per linear meter. The spandrel
115
- # sets are added as practical suggestions in early design stages.
116
-
130
+ # The following are built-in PSI-factor sets, more often predefined sets
131
+ # published in guides or energy codes. Users may append new sets,
132
+ # preferably through a TBD JSON input file. Units are in W/K per meter.
133
+ #
134
+ # The provided "spandrel" sets are suitable for early design.
135
+ #
117
136
  # Convex vs concave PSI adjustments may be warranted if there is a
118
137
  # mismatch between dimensioning conventions (interior vs exterior) used
119
- # for the OpenStudio model (OSM) vs published PSI data. For instance, the
120
- # BETBG data reflects an interior dimensioning convention, while ISO
121
- # 14683 reports PSI values for both conventions. The following may be
122
- # used (with caution) to adjust BETBG PSI values for convex corners when
123
- # using outside dimensions for an OSM.
138
+ # for the OpenStudio model vs published PSI data. For instance, the BETBG
139
+ # data reflects an interior dimensioning convention, while ISO 14683
140
+ # reports PSI-factors for both conventions. The following may be used
141
+ # (with caution) to adjust BETBG PSI-factors for convex corners when
142
+ # using outside dimensions for an OpenStudio model.
124
143
  #
125
144
  # PSIe = PSIi + U * 2(Li-Le), where:
126
- # PSIe = adjusted PSI (W/K per m)
127
- # PSIi = initial published PSI (W/K per m)
128
- # U = average clear field U-factor of adjacent walls (W/m2.K)
129
- # Li = from interior corner to edge of "zone of influence" (m)
130
- # Le = from exterior corner to edge of "zone of influence" (m)
145
+ # PSIe = adjusted PSI W/K per m
146
+ # PSIi = initial published PSI, in W/K per m
147
+ # U = average clear field U-factor of adjacent walls, in W/m2K
148
+ # Li = 'interior corner to edge' length of "zone of influence", in m
149
+ # Le = 'exterior corner to edge' length of "zone of influence", in m
131
150
  #
132
151
  # Li-Le = wall thickness e.g., -0.25m (negative here as Li < Le)
152
+
153
+ # Based on INTERIOR dimensioning (p.15 BETBG).
133
154
  @set["poor (BETBG)"] =
134
155
  {
135
- rimjoist: 1.000, # *
136
- parapet: 0.800, # *
137
- fenestration: 0.500, # *
138
- corner: 0.850, # *
139
- balcony: 1.000, # *
140
- party: 0.850, # *
141
- grade: 0.850, # *
142
- joint: 0.300, # *
143
- transition: 0.000
144
- }.freeze # based on INTERIOR dimensions (p.15 BETBG)
145
- self.gen("poor (BETBG)")
156
+ rimjoist: 1.000000, # re: BETBG
157
+ parapet: 0.800000, # re: BETBG
158
+ roof: 0.800000, # same as parapet
159
+ fenestration: 0.500000, # re: BETBG
160
+ door: 0.500000, # inferred, same as (vertical) fenestration
161
+ skylight: 0.500000, # inferred, same as (vertical) fenestration
162
+ spandrel: 0.155000, # Detail 5.4.4
163
+ corner: 0.850000, # re: BETBG
164
+ balcony: 1.000000, # re: BETBG
165
+ balconysill: 1.000000, # same as balcony
166
+ balconydoorsill: 1.000000, # same as balconysill
167
+ party: 0.850000, # re: BETBG
168
+ grade: 0.850000, # re: BETBG
169
+ joint: 0.300000, # re: BETBG
170
+ transition: 0.000000 # defaults to 0
171
+ }.freeze
146
172
 
173
+ # Based on INTERIOR dimensioning (p.15 BETBG).
147
174
  @set["regular (BETBG)"] =
148
175
  {
149
- rimjoist: 0.500, # *
150
- parapet: 0.450, # *
151
- fenestration: 0.350, # *
152
- corner: 0.450, # *
153
- balcony: 0.500, # *
154
- party: 0.450, # *
155
- grade: 0.450, # *
156
- joint: 0.200, # *
157
- transition: 0.000
158
- }.freeze # based on INTERIOR dimensions (p.15 BETBG)
159
- self.gen("regular (BETBG)")
176
+ rimjoist: 0.500000, # re: BETBG
177
+ parapet: 0.450000, # re: BETBG
178
+ roof: 0.450000, # same as parapet
179
+ fenestration: 0.350000, # re: BETBG
180
+ door: 0.350000, # inferred, same as (vertical) fenestration
181
+ skylight: 0.350000, # inferred, same as (vertical) fenestration
182
+ spandrel: 0.155000, # Detail 5.4.4
183
+ corner: 0.450000, # re: BETBG
184
+ balcony: 0.500000, # re: BETBG
185
+ balconysill: 0.500000, # same as balcony
186
+ balconydoorsill: 0.500000, # same as balconysill
187
+ party: 0.450000, # re: BETBG
188
+ grade: 0.450000, # re: BETBG
189
+ joint: 0.200000, # re: BETBG
190
+ transition: 0.000000 # defaults to 0
191
+ }.freeze
160
192
 
193
+ # Based on INTERIOR dimensioning (p.15 BETBG).
161
194
  @set["efficient (BETBG)"] =
162
195
  {
163
- rimjoist: 0.200, # *
164
- parapet: 0.200, # *
165
- fenestration: 0.200, # *
166
- corner: 0.200, # *
167
- balcony: 0.200, # *
168
- party: 0.200, # *
169
- grade: 0.200, # *
170
- joint: 0.100, # *
171
- transition: 0.000
172
- }.freeze # based on INTERIOR dimensions (p.15 BETBG)
173
- self.gen("efficient (BETBG)")
196
+ rimjoist: 0.200000, # re: BETBG
197
+ parapet: 0.200000, # re: BETBG
198
+ roof: 0.200000, # same as parapet
199
+ fenestration: 0.199999, # re: BETBG
200
+ door: 0.199999, # inferred, same as (vertical) fenestration
201
+ skylight: 0.199999, # inferred, same as (vertical) fenestration
202
+ spandrel: 0.155000, # Detail 5.4.4
203
+ corner: 0.200000, # re: BETBG
204
+ balcony: 0.200000, # re: BETBG
205
+ balconysill: 0.200000, # same as balcony
206
+ balconydoorsill: 0.200000, # same as balconysill
207
+ party: 0.200000, # re: BETBG
208
+ grade: 0.200000, # re: BETBG
209
+ joint: 0.100000, # re: BETBG
210
+ transition: 0.000000 # defaults to 0
211
+ }.freeze
174
212
 
213
+ # "Conventional", closer to window wall spandrels.
175
214
  @set["spandrel (BETBG)"] =
176
215
  {
177
- rimjoist: 0.615, # * Detail 1.2.1
178
- parapet: 1.000, # * Detail 1.3.2
179
- fenestration: 0.000, # * ... generally part of clear-field RSi
180
- corner: 0.425, # * Detail 1.4.1
181
- balcony: 1.110, # * Detail 8.1.9/9.1.6
182
- party: 0.990, # ** ... similar to parapet/balcony
183
- grade: 0.880, # * Detail 2.5.1
184
- joint: 0.500, # * Detail 3.3.2
185
- transition: 0.000
186
- }.freeze # "conventional", closer to window wall spandrels
187
- self.gen("spandrel (BETBG)")
216
+ rimjoist: 0.615000, # Detail 1.2.1
217
+ parapet: 1.000000, # Detail 1.3.2
218
+ roof: 1.000000, # same as parapet
219
+ fenestration: 0.000000, # inferred, generally part of clear-field RSi
220
+ door: 0.000000, # inferred, generally part of clear-field RSi
221
+ skylight: 0.350000, # same as "regular (BETBG)"
222
+ spandrel: 0.155000, # Detail 5.4.4
223
+ corner: 0.425000, # Detail 1.4.1
224
+ balcony: 1.110000, # Detail 8.1.9/9.1.6
225
+ balconysill: 1.110000, # same as balcony
226
+ balconydoorsill: 1.110000, # same as balconysill
227
+ party: 0.990000, # inferred, similar to parapet/balcony
228
+ grade: 0.880000, # Detail 2.5.1
229
+ joint: 0.500000, # Detail 3.3.2
230
+ transition: 0.000000 # defaults to 0
231
+ }.freeze
188
232
 
233
+ # "GoodHigh performance" curtainwall spandrels.
189
234
  @set["spandrel HP (BETBG)"] =
190
235
  {
191
- rimjoist: 0.170, # * Detail 1.2.7
192
- parapet: 0.660, # * Detail 1.3.2
193
- fenestration: 0.000, # * ... generally part of clear-field RSi
194
- corner: 0.200, # * Detail 1.4.2
195
- balcony: 0.400, # * Detail 9.1.15
196
- party: 0.500, # ** ... similar to parapet/balcony
197
- grade: 0.880, # * Detail 2.5.1
198
- joint: 0.140, # * Detail 7.4.2
199
- transition: 0.000
200
- }.freeze # "good/high performance" curtainwall spandrels
201
- self.gen("spandrel HP (BETBG)")
202
-
203
- @set["code (Quebec)"] = # NECB-QC (code-compliant) defaults:
236
+ rimjoist: 0.170000, # Detail 1.2.7
237
+ parapet: 0.660000, # Detail 1.3.2
238
+ roof: 0.660000, # same as parapet
239
+ fenestration: 0.000000, # inferred, generally part of clear-field RSi
240
+ door: 0.000000, # inferred, generally part of clear-field RSi
241
+ skylight: 0.350000, # same as "regular (BETBG)"
242
+ spandrel: 0.155000, # Detail 5.4.4
243
+ corner: 0.200000, # Detail 1.4.2
244
+ balcony: 0.400000, # Detail 9.1.15
245
+ balconysill: 0.400000, # same as balcony
246
+ balconydoorsill: 0.400000, # same as balconysill
247
+ party: 0.500000, # inferred, similar to parapet/balcony
248
+ grade: 0.880000, # Detail 2.5.1
249
+ joint: 0.140000, # Detail 7.4.2
250
+ transition: 0.000000 # defaults to 0
251
+ }.freeze
252
+
253
+ # CCQ, Chapitre I1, code-compliant defaults.
254
+ @set["code (Quebec)"] =
255
+ {
256
+ rimjoist: 0.300000, # re I1
257
+ parapet: 0.325000, # re I1
258
+ roof: 0.325000, # same as parapet
259
+ fenestration: 0.200000, # re I1
260
+ door: 0.200000, # re I1
261
+ skylight: 0.200000, # re I1
262
+ spandrel: 0.155000, # BETBG Detail 5.4.4 (same as uncompliant)
263
+ corner: 0.300000, # inferred from description, not explicitely set
264
+ balcony: 0.500000, # re I1
265
+ balconysill: 0.500000, # same as balcony
266
+ balconydoorsill: 0.500000, # same as balconysill
267
+ party: 0.450000, # re I1
268
+ grade: 0.450000, # re I1
269
+ joint: 0.200000, # re I1
270
+ transition: 0.000000 # defaults to 0
271
+ }.freeze
272
+
273
+ # CCQ, Chapitre I1, non-code-compliant defaults.
274
+ @set["uncompliant (Quebec)"] =
275
+ {
276
+ rimjoist: 0.850000, # re I1
277
+ parapet: 0.800000, # re I1
278
+ roof: 0.800000, # same as parapet
279
+ fenestration: 0.500000, # re I1
280
+ door: 0.500000, # re I1
281
+ skylight: 0.500000, # re I1
282
+ spandrel: 0.155000, # BETBG Detail 5.4.4 (same as compliant)
283
+ corner: 0.850000, # inferred from description, not explicitely set
284
+ balcony: 1.000000, # re I1
285
+ balconysill: 1.000000, # same as balcony
286
+ balconydoorsill: 1.000000, # same as balconysill
287
+ party: 0.850000, # re I1
288
+ grade: 0.850000, # re I1
289
+ joint: 0.500000, # re I1
290
+ transition: 0.000000 # defaults to 0
291
+ }.freeze
292
+
293
+ # ASHRAE 90.1 2022 (A10) "default" steel-framed and metal buildings.
294
+ @set["90.1.22|steel.m|default"] =
204
295
  {
205
- rimjoist: 0.300, # *
206
- parapet: 0.325, # *
207
- fenestration: 0.200, # *
208
- corner: 0.300, # ** not explicitely stated
209
- balcony: 0.500, # *
210
- party: 0.450, # *
211
- grade: 0.450, # *
212
- joint: 0.200, # *
213
- transition: 0.000
296
+ rimjoist: 0.307000, # "intermediate floor to wall intersection"
297
+ parapet: 0.260000, # "parapet" edge
298
+ roof: 0.020000, # (non-parapet) "roof" edge
299
+ fenestration: 0.194000, # "wall to vertical fenestration intersection"
300
+ door: 0.000000, # (unspecified, defaults to 0)
301
+ skylight: 0.000000, # (unspecified, defaults to 0)
302
+ spandrel: 0.000001, # (unspecified, defaults to 0)
303
+ corner: 0.000002, # (unspecified, defaults to 0)
304
+ balcony: 0.307000, # "intermediate floor balcony/overhang" edge
305
+ balconysill: 0.307000, # "intermediate floor balcony" edge (when sill)
306
+ balconydoorsill: 0.307000, # same as balcony
307
+ party: 0.000001, # (unspecified, defaults to 0)
308
+ grade: 0.000001, # (unspecified, defaults to 0)
309
+ joint: 0.376000, # placeholder for "cladding support"
310
+ transition: 0.000000 # defaults to 0
214
311
  }.freeze
215
- self.gen("code (Quebec)")
216
312
 
217
- @set["uncompliant (Quebec)"] = # NECB-QC (non-code-compliant) defaults:
313
+ # ASHRAE 90.1 2022 (A10) "unmitigated" steel-framed and metal buildings.
314
+ @set["90.1.22|steel.m|unmitigated"] =
218
315
  {
219
- rimjoist: 0.850, # *
220
- parapet: 0.800, # *
221
- fenestration: 0.500, # *
222
- corner: 0.850, # ** not explicitely stated
223
- balcony: 1.000, # *
224
- party: 0.850, # *
225
- grade: 0.850, # *
226
- joint: 0.500, # *
227
- transition: 0.000
316
+ rimjoist: 0.842000, # "intermediate floor to wall intersection"
317
+ parapet: 0.500000, # "parapet" edge
318
+ roof: 0.650000, # (non-parapet) "roof" edge
319
+ fenestration: 0.505000, # "wall to vertical fenestration intersection"
320
+ door: 0.000000, # (unspecified, defaults to 0)
321
+ skylight: 0.000000, # (unspecified, defaults to 0)
322
+ spandrel: 0.000001, # (unspecified, defaults to 0)
323
+ corner: 0.000002, # (unspecified, defaults to 0)
324
+ balcony: 0.842000, # "intermediate floor balcony/overhang" edge
325
+ balconysill: 1.686000, # "intermediate floor balcony" edge (when sill)
326
+ balconydoorsill: 0.842000, # same as balcony
327
+ party: 0.000001, # (unspecified, defaults to 0)
328
+ grade: 0.000001, # (unspecified, defaults to 0)
329
+ joint: 0.554000, # placeholder for "cladding support"
330
+ transition: 0.000000 # defaults to 0
228
331
  }.freeze
229
- self.gen("uncompliant (Quebec)")
230
332
 
231
- @set["(non thermal bridging)"] = # ... would not derate surfaces:
333
+ # ASHRAE 90.1 2022 (A10) "default" exterior/integral mass walls.
334
+ @set["90.1.22|mass.ex|default"] =
232
335
  {
233
- rimjoist: 0.000,
234
- parapet: 0.000,
235
- fenestration: 0.000,
236
- corner: 0.000,
237
- balcony: 0.000,
238
- party: 0.000,
239
- grade: 0.000,
240
- joint: 0.000,
241
- transition: 0.000
336
+ rimjoist: 0.205000, # "intermediate floor to wall intersection"
337
+ parapet: 0.217000, # "parapet" edge
338
+ roof: 0.150000, # (non-parapet) "roof" edge
339
+ fenestration: 0.226000, # "wall to vertical fenestration intersection"
340
+ door: 0.000000, # (unspecified, defaults to 0)
341
+ skylight: 0.000000, # (unspecified, defaults to 0)
342
+ spandrel: 0.000001, # (unspecified, defaults to 0)
343
+ corner: 0.000002, # (unspecified, defaults to 0)
344
+ balcony: 0.205000, # "intermediate floor balcony/overhang" edge
345
+ balconysill: 0.307000, # "intermediate floor balcony" edge (when sill)
346
+ balconydoorsill: 0.205000, # same as balcony
347
+ party: 0.000001, # (unspecified, defaults to 0)
348
+ grade: 0.000001, # (unspecified, defaults to 0)
349
+ joint: 0.322000, # placeholder for "cladding support"
350
+ transition: 0.000000 # defaults to 0
242
351
  }.freeze
243
- self.gen("(non thermal bridging)")
352
+
353
+ # ASHRAE 90.1 2022 (A10) "unmitigated" exterior/integral mass walls.
354
+ @set["90.1.22|mass.ex|unmitigated"] =
355
+ {
356
+ rimjoist: 0.824000, # "intermediate floor to wall intersection"
357
+ parapet: 0.412000, # "parapet" edge
358
+ roof: 0.750000, # (non-parapet) "roof" edge
359
+ fenestration: 0.325000, # "wall to vertical fenestration intersection"
360
+ door: 0.000000, # (unspecified, defaults to 0)
361
+ skylight: 0.000000, # (unspecified, defaults to 0)
362
+ spandrel: 0.000001, # (unspecified, defaults to 0)
363
+ corner: 0.000002, # (unspecified, defaults to 0)
364
+ balcony: 0.824000, # "intermediate floor balcony/overhang" edge
365
+ balconysill: 1.686000, # "intermediate floor balcony" edge (when sill)
366
+ balconydoorsill: 0.824000, # same as balcony
367
+ party: 0.000001, # (unspecified, defaults to 0)
368
+ grade: 0.000001, # (unspecified, defaults to 0)
369
+ joint: 0.476000, # placeholder for "cladding support"
370
+ transition: 0.000000 # defaults to 0
371
+ }.freeze
372
+
373
+ # ASHRAE 90.1 2022 (A10) "default" interior mass walls.
374
+ @set["90.1.22|mass.in|default"] =
375
+ {
376
+ rimjoist: 0.495000, # "intermediate floor to wall intersection"
377
+ parapet: 0.393000, # "parapet" edge
378
+ roof: 0.150000, # (non-parapet) "roof" edge
379
+ fenestration: 0.143000, # "wall to vertical fenestration intersection"
380
+ door: 0.000000, # (unspecified, defaults to 0)
381
+ skylight: 0.000000, # (unspecified, defaults to 0)
382
+ spandrel: 0.000000, # (unspecified, defaults to 0)
383
+ corner: 0.000001, # (unspecified, defaults to 0)
384
+ balcony: 0.495000, # "intermediate floor balcony/overhang" edge
385
+ balconysill: 0.307000, # "intermediate floor balcony" edge (when sill)
386
+ balconydoorsill: 0.495000, # same as balcony
387
+ party: 0.000001, # (unspecified, defaults to 0)
388
+ grade: 0.000001, # (unspecified, defaults to 0)
389
+ joint: 0.322000, # placeholder for "cladding support"
390
+ transition: 0.000000 # defaults to 0
391
+ }.freeze
392
+
393
+ # ASHRAE 90.1 2022 (A10) "unmitigated" interior mass walls.
394
+ @set["90.1.22|mass.in|unmitigated"] =
395
+ {
396
+ rimjoist: 0.824000, # "intermediate floor to wall intersection"
397
+ parapet: 0.884000, # "parapet" edge
398
+ roof: 0.750000, # (non-parapet) "roof" edge
399
+ fenestration: 0.543000, # "wall to vertical fenestration intersection"
400
+ door: 0.000000, # (unspecified, defaults to 0)
401
+ skylight: 0.000000, # (unspecified, defaults to 0)
402
+ spandrel: 0.000000, # (unspecified, defaults to 0)
403
+ corner: 0.000001, # (unspecified, defaults to 0)
404
+ balcony: 0.824000, # "intermediate floor balcony/overhang" edge
405
+ balconysill: 1.686000, # "intermediate floor balcony" edge (when sill)
406
+ balconydoorsill: 0.824000, # same as balcony
407
+ party: 0.000001, # (unspecified, defaults to 0)
408
+ grade: 0.000001, # (unspecified, defaults to 0)
409
+ joint: 0.476000, # placeholder for "cladding support"
410
+ transition: 0.000000 # defaults to 0
411
+ }.freeze
412
+
413
+ # ASHRAE 90.1 2022 (A10) "default" wood-framed (and other) walls.
414
+ @set["90.1.22|wood.fr|default"] =
415
+ {
416
+ rimjoist: 0.084000, # "intermediate floor to wall intersection"
417
+ parapet: 0.056000, # "parapet" edge
418
+ roof: 0.020000, # (non-parapet) "roof" edge
419
+ fenestration: 0.171000, # "wall to vertical fenestration intersection"
420
+ door: 0.000000, # (unspecified, defaults to 0)
421
+ skylight: 0.000000, # (unspecified, defaults to 0)
422
+ spandrel: 0.000000, # (unspecified, defaults to 0)
423
+ corner: 0.000001, # (unspecified, defaults to 0)
424
+ balcony: 0.084000, # "intermediate floor balcony/overhang" edge
425
+ balconysill: 0.171001, # same as :fenestration
426
+ balconydoorsill: 0.084000, # same as balcony
427
+ party: 0.000001, # (unspecified, defaults to 0)
428
+ grade: 0.000001, # (unspecified, defaults to 0)
429
+ joint: 0.074000, # placeholder for "cladding support"
430
+ transition: 0.000000 # defaults to 0
431
+ }.freeze
432
+
433
+ # ASHRAE 90.1 2022 (A10) "unmitigated" wood-framed (and other) walls.
434
+ @set["90.1.22|wood.fr|unmitigated"] =
435
+ {
436
+ rimjoist: 0.582000, # "intermediate floor to wall intersection"
437
+ parapet: 0.056000, # "parapet" edge
438
+ roof: 0.150000, # (non-parapet) "roof" edge
439
+ fenestration: 0.260000, # "wall to vertical fenestration intersection"
440
+ door: 0.000000, # (unspecified, defaults to 0)
441
+ skylight: 0.000000, # (unspecified, defaults to 0)
442
+ spandrel: 0.000000, # (unspecified, defaults to 0)
443
+ corner: 0.000001, # (unspecified, defaults to 0)
444
+ balcony: 0.582000, # same as :rimjoist
445
+ balconysill: 0.582000, # same as :rimjoist
446
+ balconydoorsill: 0.582000, # same as balcony
447
+ party: 0.000001, # (unspecified, defaults to 0)
448
+ grade: 0.000001, # (unspecified, defaults to 0)
449
+ joint: 0.322000, # placeholder for "cladding support"
450
+ transition: 0.000000 # defaults to 0
451
+ }.freeze
452
+
453
+ @set["(non thermal bridging)"] =
454
+ {
455
+ rimjoist: 0.000000, # defaults to 0
456
+ parapet: 0.000000, # defaults to 0
457
+ roof: 0.000000, # defaults to 0
458
+ fenestration: 0.000000, # defaults to 0
459
+ door: 0.000000, # defaults to 0
460
+ skylight: 0.000000, # defaults to 0
461
+ spandrel: 0.000000, # defaults to 0
462
+ corner: 0.000000, # defaults to 0
463
+ balcony: 0.000000, # defaults to 0
464
+ balconysill: 0.000000, # defaults to 0
465
+ party: 0.000000, # defaults to 0
466
+ grade: 0.000000, # defaults to 0
467
+ joint: 0.000000, # defaults to 0
468
+ transition: 0.000000 # defaults to 0
469
+ }.freeze
470
+
471
+ @set.keys.each { |k| self.gen(k) }
244
472
  end
245
473
 
246
474
  ##
247
- # Generate PSI set shorthand listings (requires a valid id).
475
+ # Generates PSI set shorthand listings.
248
476
  #
249
- # @param id [String] a PSI set identifier
477
+ # @param id PSI set identifier
250
478
  #
251
- # @return [Bool] true if successful in generating PSI set shorthands
252
- # @return [Bool] false if invalid input
479
+ # @return [Bool] whether successful in generating PSI set shorthands
480
+ # @return [false] if invalid input (see logs)
253
481
  def gen(id = "")
254
482
  mth = "TBD::#{__callee__}"
255
- a = false
256
-
257
- return TBD.mismatch("id", id, String, mth, DBG, a) unless id.is_a?(String)
258
- return TBD.hashkey(id, @set, id, mth, ERR, a) unless @set.key?(id)
259
-
260
- h = {} # true/false if PSI set has PSI type
261
- h[:joint ] = @set[id].key?(:joint )
262
- h[:transition ] = @set[id].key?(:transition )
263
- h[:fenestration ] = @set[id].key?(:fenestration )
264
- h[:head ] = @set[id].key?(:head )
265
- h[:headconcave ] = @set[id].key?(:headconcave )
266
- h[:headconvex ] = @set[id].key?(:headconvex )
267
- h[:sill ] = @set[id].key?(:sill )
268
- h[:sillconcave ] = @set[id].key?(:sillconcave )
269
- h[:sillconvex ] = @set[id].key?(:sillconvex )
270
- h[:jamb ] = @set[id].key?(:jamb )
271
- h[:jambconcave ] = @set[id].key?(:jambconcave )
272
- h[:jambconvex ] = @set[id].key?(:jambconvex )
273
- h[:corner ] = @set[id].key?(:corner )
274
- h[:cornerconcave ] = @set[id].key?(:cornerconcave )
275
- h[:cornerconvex ] = @set[id].key?(:cornerconvex )
276
- h[:parapet ] = @set[id].key?(:parapet )
277
- h[:partyconcave ] = @set[id].key?(:parapetconcave )
278
- h[:parapetconvex ] = @set[id].key?(:parapetconvex )
279
- h[:party ] = @set[id].key?(:party )
280
- h[:partyconcave ] = @set[id].key?(:partyconcave )
281
- h[:partyconvex ] = @set[id].key?(:partyconvex )
282
- h[:grade ] = @set[id].key?(:grade )
283
- h[:gradeconcave ] = @set[id].key?(:gradeconcave )
284
- h[:gradeconvex ] = @set[id].key?(:gradeconvex )
285
- h[:balcony ] = @set[id].key?(:balcony )
286
- h[:balconyconcave ] = @set[id].key?(:balconyconcave )
287
- h[:balconyconvex ] = @set[id].key?(:balconyconvex )
288
- h[:rimjoist ] = @set[id].key?(:rimjoist )
289
- h[:rimjoistconcave] = @set[id].key?(:rimjoistconcave)
290
- h[:rimjoistconvex ] = @set[id].key?(:rimjoistconvex )
291
- @has[id] = h
292
-
293
- v = {} # PSI-value (W/K per linear meter)
294
- v[:joint ] = 0; v[:transition ] = 0; v[:fenestration ] = 0
295
- v[:head ] = 0; v[:headconcave ] = 0; v[:headconvex ] = 0
296
- v[:sill ] = 0; v[:sillconcave ] = 0; v[:sillconvex ] = 0
297
- v[:jamb ] = 0; v[:jambconcave ] = 0; v[:jambconvex ] = 0
298
- v[:corner ] = 0; v[:cornerconcave ] = 0; v[:cornerconvex ] = 0
299
- v[:parapet ] = 0; v[:parapetconcave ] = 0; v[:parapetconvex ] = 0
300
- v[:party ] = 0; v[:partyconcave ] = 0; v[:partyconvex ] = 0
301
- v[:grade ] = 0; v[:gradeconcave ] = 0; v[:gradeconvex ] = 0
302
- v[:balcony ] = 0; v[:balconyconcave ] = 0; v[:balconyconvex ] = 0
303
- v[:rimjoist] = 0; v[:rimjoistconcave] = 0; v[:rimjoistconvex] = 0
304
-
305
- v[:joint ] = @set[id][:joint ] if h[:joint ]
306
- v[:transition ] = @set[id][:transition ] if h[:transition ]
307
- v[:fenestration ] = @set[id][:fenestration ] if h[:fenestration ]
308
- v[:head ] = @set[id][:fenestration ] if h[:fenestration ]
309
- v[:headconcave ] = @set[id][:fenestration ] if h[:fenestration ]
310
- v[:headconvex ] = @set[id][:fenestration ] if h[:fenestration ]
311
- v[:sill ] = @set[id][:fenestration ] if h[:fenestration ]
312
- v[:sillconcave ] = @set[id][:fenestration ] if h[:fenestration ]
313
- v[:sillconvex ] = @set[id][:fenestration ] if h[:fenestration ]
314
- v[:jamb ] = @set[id][:fenestration ] if h[:fenestration ]
315
- v[:jambconcave ] = @set[id][:fenestration ] if h[:fenestration ]
316
- v[:jambconvex ] = @set[id][:fenestration ] if h[:fenestration ]
317
- v[:head ] = @set[id][:head ] if h[:head ]
318
- v[:headconcave ] = @set[id][:head ] if h[:head ]
319
- v[:headconvex ] = @set[id][:head ] if h[:head ]
320
- v[:sill ] = @set[id][:sill ] if h[:sill ]
321
- v[:sillconcave ] = @set[id][:sill ] if h[:sill ]
322
- v[:sillconvex ] = @set[id][:sill ] if h[:sill ]
323
- v[:jamb ] = @set[id][:jamb ] if h[:jamb ]
324
- v[:jambconcave ] = @set[id][:jamb ] if h[:jamb ]
325
- v[:jambconvex ] = @set[id][:jamb ] if h[:jamb ]
326
- v[:headconcave ] = @set[id][:headconcave ] if h[:headconcave ]
327
- v[:headconvex ] = @set[id][:headconvex ] if h[:headconvex ]
328
- v[:sillconcave ] = @set[id][:sillconcave ] if h[:sillconcave ]
329
- v[:sillconvex ] = @set[id][:sillconvex ] if h[:sillconvex ]
330
- v[:jambconcave ] = @set[id][:jambconcave ] if h[:jambconcave ]
331
- v[:jambconvex ] = @set[id][:jambconvex ] if h[:jambconvex ]
332
- v[:corner ] = @set[id][:corner ] if h[:corner ]
333
- v[:cornerconcave ] = @set[id][:corner ] if h[:corner ]
334
- v[:cornerconvex ] = @set[id][:corner ] if h[:corner ]
335
- v[:cornerconcave ] = @set[id][:cornerconcave ] if h[:cornerconcave ]
336
- v[:cornerconvex ] = @set[id][:cornerconvex ] if h[:cornerconvex ]
337
- v[:parapet ] = @set[id][:parapet ] if h[:parapet ]
338
- v[:parapetconcave ] = @set[id][:parapet ] if h[:parapet ]
339
- v[:parapetconvex ] = @set[id][:parapet ] if h[:parapet ]
340
- v[:parapetconcave ] = @set[id][:parapetconcave ] if h[:parapetconcave ]
341
- v[:parapetconvex ] = @set[id][:parapetconvex ] if h[:parapetconvex ]
342
- v[:party ] = @set[id][:party ] if h[:party ]
343
- v[:partyconcave ] = @set[id][:party ] if h[:party ]
344
- v[:partyconvex ] = @set[id][:party ] if h[:party ]
345
- v[:partyconcave ] = @set[id][:partyconcave ] if h[:partyconcave ]
346
- v[:partyconvex ] = @set[id][:partyconvex ] if h[:partyconvex ]
347
- v[:grade ] = @set[id][:grade ] if h[:grade ]
348
- v[:gradeconcave ] = @set[id][:grade ] if h[:grade ]
349
- v[:gradeconvex ] = @set[id][:grade ] if h[:grade ]
350
- v[:gradeconcave ] = @set[id][:gradeconcave ] if h[:gradeconcave ]
351
- v[:gradeconvex ] = @set[id][:gradeconvex ] if h[:gradeconvex ]
352
- v[:balcony ] = @set[id][:balcony ] if h[:balcony ]
353
- v[:balconyconcave ] = @set[id][:balcony ] if h[:balcony ]
354
- v[:balconyconvex ] = @set[id][:balcony ] if h[:balcony ]
355
- v[:balconyconcave ] = @set[id][:balconyconcave ] if h[:balconyconcave ]
356
- v[:balconyconvex ] = @set[id][:balconyconvex ] if h[:balconyconvex ]
357
- v[:rimjoist ] = @set[id][:rimjoist ] if h[:rimjoist ]
358
- v[:rimjoistconcave] = @set[id][:rimjoist ] if h[:rimjoist ]
359
- v[:rimjoistconvex ] = @set[id][:rimjoist ] if h[:rimjoist ]
360
- v[:rimjoistconcave] = @set[id][:rimjoistconcave] if h[:rimjoistconcave]
361
- v[:rimjoistconvex ] = @set[id][:rimjoistconvex ] if h[:rimjoistconvex ]
483
+ return hashkey(id, @set, id, mth, ERR, false) unless @set.key?(id)
484
+
485
+ h = {} # true/false if PSI set has PSI type
486
+ h[:joint ] = @set[id].key?(:joint)
487
+ h[:transition ] = @set[id].key?(:transition)
488
+ h[:fenestration ] = @set[id].key?(:fenestration)
489
+ h[:head ] = @set[id].key?(:head)
490
+ h[:headconcave ] = @set[id].key?(:headconcave)
491
+ h[:headconvex ] = @set[id].key?(:headconvex)
492
+ h[:sill ] = @set[id].key?(:sill)
493
+ h[:sillconcave ] = @set[id].key?(:sillconcave)
494
+ h[:sillconvex ] = @set[id].key?(:sillconvex)
495
+ h[:jamb ] = @set[id].key?(:jamb)
496
+ h[:jambconcave ] = @set[id].key?(:jambconcave)
497
+ h[:jambconvex ] = @set[id].key?(:jambconvex)
498
+ h[:door ] = @set[id].key?(:door)
499
+ h[:doorhead ] = @set[id].key?(:doorhead)
500
+ h[:doorheadconcave ] = @set[id].key?(:doorheadconcave)
501
+ h[:doorheadconvex ] = @set[id].key?(:doorheadconvex)
502
+ h[:doorsill ] = @set[id].key?(:doorsill)
503
+ h[:doorsillconcave ] = @set[id].key?(:doorsillconcave)
504
+ h[:doorsillconvex ] = @set[id].key?(:doorsillconvex)
505
+ h[:doorjamb ] = @set[id].key?(:doorjamb)
506
+ h[:doorjambconcave ] = @set[id].key?(:doorjambconcave)
507
+ h[:doorjambconvex ] = @set[id].key?(:doorjambconvex)
508
+ h[:skylight ] = @set[id].key?(:skylight)
509
+ h[:skylighthead ] = @set[id].key?(:skylighthead)
510
+ h[:skylightheadconcave ] = @set[id].key?(:skylightheadconcave)
511
+ h[:skylightheadconvex ] = @set[id].key?(:skylightheadconvex)
512
+ h[:skylightsill ] = @set[id].key?(:skylightsill)
513
+ h[:skylightsillconcave ] = @set[id].key?(:skylightsillconcave)
514
+ h[:skylightsillconvex ] = @set[id].key?(:skylightsillconvex)
515
+ h[:skylightjamb ] = @set[id].key?(:skylightjamb)
516
+ h[:skylightjambconcave ] = @set[id].key?(:skylightjambconcave)
517
+ h[:skylightjambconvex ] = @set[id].key?(:skylightjambconvex)
518
+ h[:spandrel ] = @set[id].key?(:spandrel)
519
+ h[:spandrelconcave ] = @set[id].key?(:spandrelconcave)
520
+ h[:spandrelconvex ] = @set[id].key?(:spandrelconvex)
521
+ h[:corner ] = @set[id].key?(:corner)
522
+ h[:cornerconcave ] = @set[id].key?(:cornerconcave)
523
+ h[:cornerconvex ] = @set[id].key?(:cornerconvex)
524
+ h[:party ] = @set[id].key?(:party)
525
+ h[:partyconcave ] = @set[id].key?(:partyconcave)
526
+ h[:partyconvex ] = @set[id].key?(:partyconvex)
527
+ h[:parapet ] = @set[id].key?(:parapet)
528
+ h[:partyconcave ] = @set[id].key?(:parapetconcave)
529
+ h[:parapetconvex ] = @set[id].key?(:parapetconvex)
530
+ h[:roof ] = @set[id].key?(:roof)
531
+ h[:roofconcave ] = @set[id].key?(:roofconcave)
532
+ h[:roofconvex ] = @set[id].key?(:roofconvex)
533
+ h[:grade ] = @set[id].key?(:grade)
534
+ h[:gradeconcave ] = @set[id].key?(:gradeconcave)
535
+ h[:gradeconvex ] = @set[id].key?(:gradeconvex)
536
+ h[:balcony ] = @set[id].key?(:balcony)
537
+ h[:balconyconcave ] = @set[id].key?(:balconyconcave)
538
+ h[:balconyconvex ] = @set[id].key?(:balconyconvex)
539
+ h[:balconysill ] = @set[id].key?(:balconysill)
540
+ h[:balconysillconcave ] = @set[id].key?(:balconysillconvex)
541
+ h[:balconysillconvex ] = @set[id].key?(:balconysillconvex)
542
+ h[:balconydoorsill ] = @set[id].key?(:balconydoorsill)
543
+ h[:balconydoorsillconcave] = @set[id].key?(:balconydoorsillconvex)
544
+ h[:balconydoorsillconvex ] = @set[id].key?(:balconydoorsillconvex)
545
+ h[:rimjoist ] = @set[id].key?(:rimjoist)
546
+ h[:rimjoistconcave ] = @set[id].key?(:rimjoistconcave)
547
+ h[:rimjoistconvex ] = @set[id].key?(:rimjoistconvex)
548
+ @has[id] = h
549
+
550
+ v = {} # PSI-value (W/K per linear meter)
551
+ v[:door ] = 0; v[:fenestration ] = 0; v[:skylight ] = 0
552
+ v[:head ] = 0; v[:headconcave ] = 0; v[:headconvex ] = 0
553
+ v[:sill ] = 0; v[:sillconcave ] = 0; v[:sillconvex ] = 0
554
+ v[:jamb ] = 0; v[:jambconcave ] = 0; v[:jambconvex ] = 0
555
+ v[:doorhead ] = 0; v[:doorheadconcave ] = 0; v[:doorconvex ] = 0
556
+ v[:doorsill ] = 0; v[:doorsillconcave ] = 0; v[:doorsillconvex ] = 0
557
+ v[:doorjamb ] = 0; v[:doorjambconcave ] = 0; v[:doorjambconvex ] = 0
558
+ v[:skylighthead ] = 0; v[:skylightheadconcave ] = 0; v[:skylightconvex ] = 0
559
+ v[:skylightsill ] = 0; v[:skylightsillconcave ] = 0; v[:skylightsillconvex ] = 0
560
+ v[:skylightjamb ] = 0; v[:skylightjambconcave ] = 0; v[:skylightjambconvex ] = 0
561
+ v[:spandrel ] = 0; v[:spandrelconcave ] = 0; v[:spandrelconvex ] = 0
562
+ v[:corner ] = 0; v[:cornerconcave ] = 0; v[:cornerconvex ] = 0
563
+ v[:parapet ] = 0; v[:parapetconcave ] = 0; v[:parapetconvex ] = 0
564
+ v[:roof ] = 0; v[:roofconcave ] = 0; v[:roofconvex ] = 0
565
+ v[:party ] = 0; v[:partyconcave ] = 0; v[:partyconvex ] = 0
566
+ v[:grade ] = 0; v[:gradeconcave ] = 0; v[:gradeconvex ] = 0
567
+ v[:balcony ] = 0; v[:balconyconcave ] = 0; v[:balconyconvex ] = 0
568
+ v[:balconysill ] = 0; v[:balconysillconcave ] = 0; v[:balconysillconvex ] = 0
569
+ v[:balconydoorsill] = 0; v[:balconydoorsillconcave] = 0; v[:balconydoorsillconvex] = 0
570
+ v[:rimjoist ] = 0; v[:rimjoistconcave ] = 0; v[:rimjoistconvex ] = 0
571
+ v[:joint ] = 0; v[:transition ] = 0
572
+
573
+ v[:joint ] = @set[id][:joint ] if h[:joint ]
574
+ v[:transition ] = @set[id][:transition ] if h[:transition ]
575
+ v[:fenestration ] = @set[id][:fenestration ] if h[:fenestration ]
576
+ v[:head ] = @set[id][:fenestration ] if h[:fenestration ]
577
+ v[:headconcave ] = @set[id][:fenestration ] if h[:fenestration ]
578
+ v[:headconvex ] = @set[id][:fenestration ] if h[:fenestration ]
579
+ v[:sill ] = @set[id][:fenestration ] if h[:fenestration ]
580
+ v[:sillconcave ] = @set[id][:fenestration ] if h[:fenestration ]
581
+ v[:sillconvex ] = @set[id][:fenestration ] if h[:fenestration ]
582
+ v[:jamb ] = @set[id][:fenestration ] if h[:fenestration ]
583
+ v[:jambconcave ] = @set[id][:fenestration ] if h[:fenestration ]
584
+ v[:jambconvex ] = @set[id][:fenestration ] if h[:fenestration ]
585
+ v[:door ] = @set[id][:fenestration ] if h[:fenestration ]
586
+ v[:doorhead ] = @set[id][:fenestration ] if h[:fenestration ]
587
+ v[:doorheadconcave ] = @set[id][:fenestration ] if h[:fenestration ]
588
+ v[:doorheadconvex ] = @set[id][:fenestration ] if h[:fenestration ]
589
+ v[:doorsill ] = @set[id][:fenestration ] if h[:fenestration ]
590
+ v[:doorsillconcave ] = @set[id][:fenestration ] if h[:fenestration ]
591
+ v[:doorsillconvex ] = @set[id][:fenestration ] if h[:fenestration ]
592
+ v[:doorjamb ] = @set[id][:fenestration ] if h[:fenestration ]
593
+ v[:doorjambconcave ] = @set[id][:fenestration ] if h[:fenestration ]
594
+ v[:doorjambconvex ] = @set[id][:fenestration ] if h[:fenestration ]
595
+ v[:skylight ] = @set[id][:fenestration ] if h[:fenestration ]
596
+ v[:skylighthead ] = @set[id][:fenestration ] if h[:fenestration ]
597
+ v[:skylightheadconcave ] = @set[id][:fenestration ] if h[:fenestration ]
598
+ v[:skylightheadconvex ] = @set[id][:fenestration ] if h[:fenestration ]
599
+ v[:skylightsill ] = @set[id][:fenestration ] if h[:fenestration ]
600
+ v[:skylightsillconcave ] = @set[id][:fenestration ] if h[:fenestration ]
601
+ v[:skylightsillconvex ] = @set[id][:fenestration ] if h[:fenestration ]
602
+ v[:skylightjamb ] = @set[id][:fenestration ] if h[:fenestration ]
603
+ v[:skylightjambconcave ] = @set[id][:fenestration ] if h[:fenestration ]
604
+ v[:skylightjambconvex ] = @set[id][:fenestration ] if h[:fenestration ]
605
+ v[:door ] = @set[id][:door ] if h[:door ]
606
+ v[:doorhead ] = @set[id][:door ] if h[:door ]
607
+ v[:doorheadconcave ] = @set[id][:door ] if h[:door ]
608
+ v[:doorheadconvex ] = @set[id][:door ] if h[:door ]
609
+ v[:doorsill ] = @set[id][:door ] if h[:door ]
610
+ v[:doorsillconcave ] = @set[id][:door ] if h[:door ]
611
+ v[:doorsillconvex ] = @set[id][:door ] if h[:door ]
612
+ v[:doorjamb ] = @set[id][:door ] if h[:door ]
613
+ v[:doorjambconcave ] = @set[id][:door ] if h[:door ]
614
+ v[:doorjambconvex ] = @set[id][:door ] if h[:door ]
615
+ v[:skylight ] = @set[id][:skylight ] if h[:skylight ]
616
+ v[:skylighthead ] = @set[id][:skylight ] if h[:skylight ]
617
+ v[:skylightheadconcave ] = @set[id][:skylight ] if h[:skylight ]
618
+ v[:skylightheadconvex ] = @set[id][:skylight ] if h[:skylight ]
619
+ v[:skylightsill ] = @set[id][:skylight ] if h[:skylight ]
620
+ v[:skylightsillconcave ] = @set[id][:skylight ] if h[:skylight ]
621
+ v[:skylightsillconvex ] = @set[id][:skylight ] if h[:skylight ]
622
+ v[:skylightjamb ] = @set[id][:skylight ] if h[:skylight ]
623
+ v[:skylightjambconcave ] = @set[id][:skylight ] if h[:skylight ]
624
+ v[:skylightjambconvex ] = @set[id][:skylight ] if h[:skylight ]
625
+ v[:head ] = @set[id][:head ] if h[:head ]
626
+ v[:headconcave ] = @set[id][:head ] if h[:head ]
627
+ v[:headconvex ] = @set[id][:head ] if h[:head ]
628
+ v[:sill ] = @set[id][:sill ] if h[:sill ]
629
+ v[:sillconcave ] = @set[id][:sill ] if h[:sill ]
630
+ v[:sillconvex ] = @set[id][:sill ] if h[:sill ]
631
+ v[:jamb ] = @set[id][:jamb ] if h[:jamb ]
632
+ v[:jambconcave ] = @set[id][:jamb ] if h[:jamb ]
633
+ v[:jambconvex ] = @set[id][:jamb ] if h[:jamb ]
634
+ v[:doorhead ] = @set[id][:doorhead ] if h[:doorhead ]
635
+ v[:doorheadconcave ] = @set[id][:doorhead ] if h[:doorhead ]
636
+ v[:doorheadconvex ] = @set[id][:doorhead ] if h[:doorhead ]
637
+ v[:doorsill ] = @set[id][:doorsill ] if h[:doorsill ]
638
+ v[:doorsillconcave ] = @set[id][:doorsill ] if h[:doorsill ]
639
+ v[:doorsillconvex ] = @set[id][:doorsill ] if h[:doorsill ]
640
+ v[:doorjamb ] = @set[id][:doorjamb ] if h[:doorjamb ]
641
+ v[:doorjambconcave ] = @set[id][:doorjamb ] if h[:doorjamb ]
642
+ v[:doorjambconvex ] = @set[id][:doorjamb ] if h[:doorjamb ]
643
+ v[:skylighthead ] = @set[id][:skylighthead ] if h[:skylighthead ]
644
+ v[:skylightheadconcave ] = @set[id][:skylighthead ] if h[:skylighthead ]
645
+ v[:skylightheadconvex ] = @set[id][:skylighthead ] if h[:skylighthead ]
646
+ v[:skylightsill ] = @set[id][:skylightsill ] if h[:skylightsill ]
647
+ v[:skylightsillconcave ] = @set[id][:skylightsill ] if h[:skylightsill ]
648
+ v[:skylightsillconvex ] = @set[id][:skylightsill ] if h[:skylightsill ]
649
+ v[:skylightjamb ] = @set[id][:skylightjamb ] if h[:skylightjamb ]
650
+ v[:skylightjambconcave ] = @set[id][:skylightjamb ] if h[:skylightjamb ]
651
+ v[:skylightjambconvex ] = @set[id][:skylightjamb ] if h[:skylightjamb ]
652
+ v[:headconcave ] = @set[id][:headconcave ] if h[:headconcave ]
653
+ v[:headconvex ] = @set[id][:headconvex ] if h[:headconvex ]
654
+ v[:sillconcave ] = @set[id][:sillconcave ] if h[:sillconcave ]
655
+ v[:sillconvex ] = @set[id][:sillconvex ] if h[:sillconvex ]
656
+ v[:jambconcave ] = @set[id][:jambconcave ] if h[:jambconcave ]
657
+ v[:jambconvex ] = @set[id][:jambconvex ] if h[:jambconvex ]
658
+ v[:doorheadconcave ] = @set[id][:doorheadconcave ] if h[:doorheadconcave ]
659
+ v[:doorheadconvex ] = @set[id][:doorheadconvex ] if h[:doorheadconvex ]
660
+ v[:doorsillconcave ] = @set[id][:doorsillconcave ] if h[:doorsillconcave ]
661
+ v[:doorsillconvex ] = @set[id][:doorsillconvex ] if h[:doorsillconvex ]
662
+ v[:doorjambconcave ] = @set[id][:doorjambconcave ] if h[:doorjambconcave ]
663
+ v[:doorjambconvex ] = @set[id][:doorjambconvex ] if h[:doorjambconvex ]
664
+ v[:skylightheadconcave ] = @set[id][:skylightheadconcave ] if h[:skylightheadconcave ]
665
+ v[:skylightheadconvex ] = @set[id][:skylightheadconvex ] if h[:skylightheadconvex ]
666
+ v[:skylightsillconcave ] = @set[id][:skylightsillconcave ] if h[:skylightsillconcave ]
667
+ v[:skylightsillconvex ] = @set[id][:skylightsillconvex ] if h[:skylightsillconvex ]
668
+ v[:skylightjambconcave ] = @set[id][:skylightjambconcave ] if h[:skylightjambconcave ]
669
+ v[:skylightjambconvex ] = @set[id][:skylightjambconvex ] if h[:skylightjambconvex ]
670
+ v[:spandrel ] = @set[id][:spandrel ] if h[:spandrel ]
671
+ v[:spandrelconcave ] = @set[id][:spandrel ] if h[:spandrel ]
672
+ v[:spandrelconvex ] = @set[id][:spandrel ] if h[:spandrel ]
673
+ v[:spandrelconcave ] = @set[id][:spandrelconcave ] if h[:spandrelconcave ]
674
+ v[:spandrelconvex ] = @set[id][:spandrelconvex ] if h[:spandrelconvex ]
675
+ v[:corner ] = @set[id][:corner ] if h[:corner ]
676
+ v[:cornerconcave ] = @set[id][:corner ] if h[:corner ]
677
+ v[:cornerconvex ] = @set[id][:corner ] if h[:corner ]
678
+ v[:cornerconcave ] = @set[id][:cornerconcave ] if h[:cornerconcave ]
679
+ v[:cornerconvex ] = @set[id][:cornerconvex ] if h[:cornerconvex ]
680
+ v[:parapet ] = @set[id][:roof ] if h[:roof ]
681
+ v[:parapetconcave ] = @set[id][:roof ] if h[:roof ]
682
+ v[:parapetconvex ] = @set[id][:roof ] if h[:roof ]
683
+ v[:parapetconcave ] = @set[id][:roofconcave ] if h[:roofconcave ]
684
+ v[:parapetconvex ] = @set[id][:roofconvex ] if h[:roofconvex ]
685
+ v[:parapet ] = @set[id][:parapet ] if h[:parapet ]
686
+ v[:parapetconcave ] = @set[id][:parapet ] if h[:parapet ]
687
+ v[:parapetconvex ] = @set[id][:parapet ] if h[:parapet ]
688
+ v[:parapetconcave ] = @set[id][:parapetconcave ] if h[:parapetconcave ]
689
+ v[:parapetconvex ] = @set[id][:parapetconvex ] if h[:parapetconvex ]
690
+ v[:roof ] = @set[id][:parapet ] if h[:parapet ]
691
+ v[:roofconcave ] = @set[id][:parapet ] if h[:parapet ]
692
+ v[:roofconvex ] = @set[id][:parapet ] if h[:parapet ]
693
+ v[:roofconcave ] = @set[id][:parapetconcave ] if h[:parapetconcave ]
694
+ v[:roofconvex ] = @set[id][:parapetxonvex ] if h[:parapetconvex ]
695
+ v[:roof ] = @set[id][:roof ] if h[:roof ]
696
+ v[:roofconcave ] = @set[id][:roof ] if h[:roof ]
697
+ v[:roofconvex ] = @set[id][:roof ] if h[:roof ]
698
+ v[:roofconcave ] = @set[id][:roofconcave ] if h[:roofconcave ]
699
+ v[:roofconvex ] = @set[id][:roofconvex ] if h[:roofconvex ]
700
+ v[:party ] = @set[id][:party ] if h[:party ]
701
+ v[:partyconcave ] = @set[id][:party ] if h[:party ]
702
+ v[:partyconvex ] = @set[id][:party ] if h[:party ]
703
+ v[:partyconcave ] = @set[id][:partyconcave ] if h[:partyconcave ]
704
+ v[:partyconvex ] = @set[id][:partyconvex ] if h[:partyconvex ]
705
+ v[:grade ] = @set[id][:grade ] if h[:grade ]
706
+ v[:gradeconcave ] = @set[id][:grade ] if h[:grade ]
707
+ v[:gradeconvex ] = @set[id][:grade ] if h[:grade ]
708
+ v[:gradeconcave ] = @set[id][:gradeconcave ] if h[:gradeconcave ]
709
+ v[:gradeconvex ] = @set[id][:gradeconvex ] if h[:gradeconvex ]
710
+ v[:balcony ] = @set[id][:balcony ] if h[:balcony ]
711
+ v[:balconyconcave ] = @set[id][:balcony ] if h[:balcony ]
712
+ v[:balconyconvex ] = @set[id][:balcony ] if h[:balcony ]
713
+ v[:balconyconcave ] = @set[id][:balconyconcave ] if h[:balconyconcave ]
714
+ v[:balconyconvex ] = @set[id][:balconyconvex ] if h[:balconyconvex ]
715
+ v[:balconysill ] = @set[id][:fenestration ] if h[:fenestration ]
716
+ v[:balconysillconcave ] = @set[id][:fenestration ] if h[:fenestration ]
717
+ v[:balconysillconvex ] = @set[id][:fenestration ] if h[:fenestration ]
718
+ v[:balconydoorsill ] = @set[id][:fenestration ] if h[:fenestration ]
719
+ v[:balconydoorsillconcave] = @set[id][:fenestration ] if h[:fenestration ]
720
+ v[:balconydoorsillconvex ] = @set[id][:fenestration ] if h[:fenestration ]
721
+ v[:balconysill ] = @set[id][:sill ] if h[:sill ]
722
+ v[:balconysillconcave ] = @set[id][:sill ] if h[:sill ]
723
+ v[:balconysillconvex ] = @set[id][:sill ] if h[:sill ]
724
+ v[:balconysillconcave ] = @set[id][:sillconcave ] if h[:sillconcave ]
725
+ v[:balconysillconvex ] = @set[id][:sillconvex ] if h[:sillconvex ]
726
+ v[:balconydoorsill ] = @set[id][:sill ] if h[:sill ]
727
+ v[:balconydoorsillconcave] = @set[id][:sill ] if h[:sill ]
728
+ v[:balconydoorsillconvex ] = @set[id][:sill ] if h[:sill ]
729
+ v[:balconydoorsillconcave] = @set[id][:sillconcave ] if h[:sillconcave ]
730
+ v[:balconydoorsillconvex ] = @set[id][:sillconvex ] if h[:sillconvex ]
731
+ v[:balconysill ] = @set[id][:balcony ] if h[:balcony ]
732
+ v[:balconysillconcave ] = @set[id][:balcony ] if h[:balcony ]
733
+ v[:balconysillconvex ] = @set[id][:balcony ] if h[:balcony ]
734
+ v[:balconysillconcave ] = @set[id][:balconyconcave ] if h[:balconyconcave ]
735
+ v[:balconysillconvex ] = @set[id][:balconyconvex ] if h[:balconycinvex ]
736
+ v[:balconydoorsill ] = @set[id][:balcony ] if h[:balcony ]
737
+ v[:balconydoorsillconcave] = @set[id][:balcony ] if h[:balcony ]
738
+ v[:balconydoorsillconvex ] = @set[id][:balcony ] if h[:balcony ]
739
+ v[:balconydoorsillconcave] = @set[id][:balconyconcave ] if h[:balconyconcave ]
740
+ v[:balconydoorsillconvex ] = @set[id][:balconyconvex ] if h[:balconycinvex ]
741
+ v[:balconysill ] = @set[id][:balconysill ] if h[:balconysill ]
742
+ v[:balconysillconcave ] = @set[id][:balconysill ] if h[:balconysill ]
743
+ v[:balconysillconvex ] = @set[id][:balconysill ] if h[:balconysill ]
744
+ v[:balconysillconcave ] = @set[id][:balconysillconcave ] if h[:balconysillconcave ]
745
+ v[:balconysillconvex ] = @set[id][:balconysillconvex ] if h[:balconysillconvex ]
746
+ v[:balconydoorsill ] = @set[id][:balconysill ] if h[:balconysill ]
747
+ v[:balconydoorsillconcave] = @set[id][:balconysill ] if h[:balconysill ]
748
+ v[:balconydoorsillconvex ] = @set[id][:balconysill ] if h[:balconysill ]
749
+ v[:balconydoorsillconcave] = @set[id][:balconysillconcave ] if h[:balconysillconcave ]
750
+ v[:balconydoorsillconvex ] = @set[id][:balconysillconvex ] if h[:balconysillconvex ]
751
+ v[:balconydoorsill ] = @set[id][:balconydoorsill ] if h[:balconydoorsill ]
752
+ v[:balconydoorsillconcave] = @set[id][:balconydoorsill ] if h[:balconydoorsill ]
753
+ v[:balconydoorsillconvex ] = @set[id][:balconydoorsill ] if h[:balconydoorsill ]
754
+ v[:balconydoorsillconcave] = @set[id][:balconydoorsillconcave] if h[:balconydoorsillconcave]
755
+ v[:balconydoorsillconvex ] = @set[id][:balconydoorsillconvex ] if h[:balconydoorsillconvex ]
756
+ v[:rimjoist ] = @set[id][:rimjoist ] if h[:rimjoist ]
757
+ v[:rimjoistconcave ] = @set[id][:rimjoist ] if h[:rimjoist ]
758
+ v[:rimjoistconvex ] = @set[id][:rimjoist ] if h[:rimjoist ]
759
+ v[:rimjoistconcave ] = @set[id][:rimjoistconcave ] if h[:rimjoistconcave ]
760
+ v[:rimjoistconvex ] = @set[id][:rimjoistconvex ] if h[:rimjoistconvex ]
362
761
 
363
762
  max = [v[:parapetconcave], v[:parapetconvex]].max
364
763
  v[:parapet] = max unless @has[:parapet]
764
+
765
+ max = [v[:roofconcave], v[:roofconvex]].max
766
+ v[:roof] = max unless @has[:roof]
365
767
  @val[id] = v
366
768
 
367
769
  true
368
770
  end
369
771
 
370
772
  ##
371
- # Append a new PSI set, based on a TBD JSON-formatted PSI set object -
372
- # requires a valid, unique :id.
773
+ # Appends a new PSI set.
373
774
  #
374
- # @param set [Hash] a new PSI set
775
+ # @param [Hash] set a new PSI set
776
+ # @option set [#to_s] :id PSI set identifier
777
+ # @option set [#to_f] :rimjoist intermediate floor-to-wall intersection
778
+ # @option set [#to_f] :rimjoistconcave basilaire variant
779
+ # @option set [#to_f] :rimjoistconvex cantilever variant
780
+ # @option set [#to_f] :parapet roof-to-wall intersection
781
+ # @option set [#to_f] :parapetconcave basilaire variant
782
+ # @option set [#to_f] :parapetconvex typical
783
+ # @option set [#to_f] :roof roof-to-wall intersection
784
+ # @option set [#to_f] :roofconcave basilaire variant
785
+ # @option set [#to_f] :roofconvex typical
786
+ # @option set [#to_f] :fenestration head/sill/jamb interface
787
+ # @option set [#to_f] :head (fenestrated) header interface
788
+ # @option set [#to_f] :headconcave (fenestrated) basilaire variant
789
+ # @option set [#to_f] :headconvex (fenestrated) parapet variant
790
+ # @option set [#to_f] :sill (fenestrated) threshold/sill interface
791
+ # @option set [#to_f] :sillconcave (fenestrated) basilaire variant
792
+ # @option set [#to_f] :sillconvex (fenestrated) cantilever variant
793
+ # @option set [#to_f] :jamb (fenestrated) side jamb interface
794
+ # @option set [#to_f] :jambconcave (fenestrated) interior corner variant
795
+ # @option set [#to_f] :jambconvex (fenestrated) exterior corner variant
796
+ # @option set [#to_f] :door (opaque) head/sill/jamb interface
797
+ # @option set [#to_f] :doorhead (opaque) header interface
798
+ # @option set [#to_f] :doorheadconcave (opaque) basilaire variant
799
+ # @option set [#to_f] :doorheadconvex (opaque) parapet variant
800
+ # @option set [#to_f] :doorsill (opaque) threshold interface
801
+ # @option set [#to_f] :doorsillconcave (opaque) basilaire variant
802
+ # @option set [#to_f] :doorsillconvex (opaque) cantilever variant
803
+ # @option set [#to_f] :doorjamb (opaque) side jamb interface
804
+ # @option set [#to_f] :doorjambconcave (opaque) interior corner variant
805
+ # @option set [#to_f] :doorjambconvex (opaque) exterior corner variant
806
+ # @option set [#to_f] :skylight to roof interface
807
+ # @option set [#to_f] :skylighthead header interface
808
+ # @option set [#to_f] :skylightheadconcave basilaire variant
809
+ # @option set [#to_f] :skylightheadconvex parapet variant
810
+ # @option set [#to_f] :skylightsill sill interface
811
+ # @option set [#to_f] :skylightsillconcave basilaire variant
812
+ # @option set [#to_f] :skylightsillconvex cantilever variant
813
+ # @option set [#to_f] :skylightjamb side jamb interface
814
+ # @option set [#to_f] :skylightjambconcave (opaque) interior corner variant
815
+ # @option set [#to_f] :skylightjambconvex (opaque) parapet variant
816
+ # @option set [#to_f] :spandrel spandrel/other interface
817
+ # @option set [#to_f] :spandrelconcave interior corner variant
818
+ # @option set [#to_f] :spandrelconvex exterior corner variant
819
+ # @option set [#to_f] :corner corner intersection
820
+ # @option set [#to_f] :cornerconcave interior corner variant
821
+ # @option set [#to_f] :cornerconvex exterior corner variant
822
+ # @option set [#to_f] :balcony intermediate floor-balcony intersection
823
+ # @option set [#to_f] :balconyconcave basilaire variant
824
+ # @option set [#to_f] :balconyconvex cantilever variant
825
+ # @option set [#to_f] :balconysill intermediate floor-balcony-fenestration intersection
826
+ # @option set [#to_f] :balconysilloncave basilaire variant
827
+ # @option set [#to_f] :balconysillconvex cantilever variant
828
+ # @option set [#to_f] :balconydoorsill intermediate floor-balcony-door intersection
829
+ # @option set [#to_f] :balconydoorsilloncave basilaire variant
830
+ # @option set [#to_f] :balconydoorsillconvex cantilever variant
831
+ # @option set [#to_f] :party demising surface intersection
832
+ # @option set [#to_f] :partyconcave interior corner or basilaire variant
833
+ # @option set [#to_f] :partyconvex exterior corner or cantilever variant
834
+ # @option set [#to_f] :grade foundation wall or slab-on-grade intersection
835
+ # @option set [#to_f] :gradeconcave cantilever variant
836
+ # @option set [#to_f] :gradeconvex basilaire variant
837
+ # @option set [#to_f] :joint strong ~coplanar joint
838
+ # @option set [#to_f] :transition mild ~coplanar transition
375
839
  #
376
- # @return [Bool] true if successfully appended
377
- # @return [Bool] false if invalid input
840
+ # @return [Bool] whether PSI set is successfully appended
841
+ # @return [false] if invalid input (see logs)
378
842
  def append(set = {})
379
843
  mth = "TBD::#{__callee__}"
380
844
  a = false
845
+ s = {}
846
+ return mismatch("set" , set, Hash, mth, DBG, a) unless set.is_a?(Hash)
847
+ return hashkey("set id", set, :id , mth, DBG, a) unless set.key?(:id)
381
848
 
382
- return TBD.mismatch("set", set, Hash, mth, DBG, a) unless set.is_a?(Hash)
383
- return TBD.hashkey("set id", set, :id, mth, DBG, a) unless set.key?(:id)
849
+ id = trim(set[:id])
850
+ return mismatch("set ID", set[:id], String, mth, ERR, a) if id.empty?
384
851
 
385
- exists = @set.key?(set[:id])
386
- TBD.log(ERR, "'#{set[:id]}': existing PSI set (#{mth})") if exists
387
- return false if exists
852
+ if @set.key?(id)
853
+ log(ERR, "'#{id}': existing PSI set (#{mth})")
854
+ return a
855
+ end
388
856
 
389
- s = {}
390
857
  # Most PSI types have concave and convex variants, depending on the polar
391
858
  # position of deratable surfaces about an edge-as-thermal-bridge. One
392
859
  # exception is :fenestration, which TBD later breaks down into :head,
@@ -394,64 +861,97 @@ module TBD
394
861
  # type that is not autoassigned to an edge (i.e., only via a TBD JSON
395
862
  # input file). Finally, transitions are autoassigned by TBD when an edge
396
863
  # is "flat", i.e, no noticeable polar angle difference between surfaces.
397
- s[:rimjoist ] = set[:rimjoist ] if set.key?(:rimjoist )
398
- s[:rimjoistconcave] = set[:rimjoistconcave] if set.key?(:rimjoistconcave)
399
- s[:rimjoistconvex ] = set[:rimjoistconvex ] if set.key?(:rimjoistconvex )
400
- s[:parapet ] = set[:parapet ] if set.key?(:parapet )
401
- s[:parapetconcave ] = set[:parapetconcave ] if set.key?(:parapetconcave )
402
- s[:parapetconvex ] = set[:parapetconvex ] if set.key?(:parapetconvex )
403
- s[:head ] = set[:head ] if set.key?(:head )
404
- s[:headconcave ] = set[:headconcave ] if set.key?(:headconcave )
405
- s[:headconvex ] = set[:headconvex ] if set.key?(:headconvex )
406
- s[:sill ] = set[:sill ] if set.key?(:sill )
407
- s[:sillconcave ] = set[:sillconcave ] if set.key?(:sillconcave )
408
- s[:sillconvex ] = set[:sillconvex ] if set.key?(:sillconvex )
409
- s[:jamb ] = set[:jamb ] if set.key?(:jamb )
410
- s[:jambconcave ] = set[:jambconcave ] if set.key?(:jambconcave )
411
- s[:jambconvex ] = set[:jambconvex ] if set.key?(:jambconcave )
412
- s[:corner ] = set[:corner ] if set.key?(:corner )
413
- s[:cornerconcave ] = set[:cornerconcave ] if set.key?(:cornerconcave )
414
- s[:cornerconvex ] = set[:cornerconvex ] if set.key?(:cornerconvex )
415
- s[:balcony ] = set[:balcony ] if set.key?(:balcony )
416
- s[:balconyconcave ] = set[:balconyconcave ] if set.key?(:balconyconcave )
417
- s[:balconyconvex ] = set[:balconyconvex ] if set.key?(:balconyconvex )
418
- s[:party ] = set[:party ] if set.key?(:party )
419
- s[:partyconcave ] = set[:partyconcave ] if set.key?(:partyconcave )
420
- s[:partyconvex ] = set[:partyconvex ] if set.key?(:partyconvex )
421
- s[:grade ] = set[:grade ] if set.key?(:grade )
422
- s[:gradeconcave ] = set[:gradeconcave ] if set.key?(:gradeconcave )
423
- s[:gradeconvex ] = set[:gradeconvex ] if set.key?(:gradeconvex )
424
- s[:fenestration ] = set[:fenestration ] if set.key?(:fenestration )
425
- s[:joint ] = set[:joint ] if set.key?(:joint )
426
- s[:transition ] = set[:transition ] if set.key?(:transition )
427
-
428
- s[:joint ] = 0.000 unless set.key?(:joint )
429
- s[:transition ] = 0.000 unless set.key?(:transition )
430
-
431
- @set[set[:id]] = s
432
- self.gen(set[:id])
864
+ s[:rimjoist ] = set[:rimjoist ] if set.key?(:rimjoist)
865
+ s[:rimjoistconcave ] = set[:rimjoistconcave ] if set.key?(:rimjoistconcave)
866
+ s[:rimjoistconvex ] = set[:rimjoistconvex ] if set.key?(:rimjoistconvex)
867
+ s[:parapet ] = set[:parapet ] if set.key?(:parapet)
868
+ s[:parapetconcave ] = set[:parapetconcave ] if set.key?(:parapetconcave)
869
+ s[:parapetconvex ] = set[:parapetconvex ] if set.key?(:parapetconvex)
870
+ s[:roof ] = set[:roof ] if set.key?(:roof)
871
+ s[:roofconcave ] = set[:roofconcave ] if set.key?(:roofconcave)
872
+ s[:roofconvex ] = set[:roofconvex ] if set.key?(:roofconvex)
873
+ s[:fenestration ] = set[:fenestration ] if set.key?(:fenestration)
874
+ s[:head ] = set[:head ] if set.key?(:head)
875
+ s[:headconcave ] = set[:headconcave ] if set.key?(:headconcave)
876
+ s[:headconvex ] = set[:headconvex ] if set.key?(:headconvex)
877
+ s[:sill ] = set[:sill ] if set.key?(:sill)
878
+ s[:sillconcave ] = set[:sillconcave ] if set.key?(:sillconcave)
879
+ s[:sillconvex ] = set[:sillconvex ] if set.key?(:sillconvex)
880
+ s[:jamb ] = set[:jamb ] if set.key?(:jamb)
881
+ s[:jambconcave ] = set[:jambconcave ] if set.key?(:jambconcave)
882
+ s[:jambconvex ] = set[:jambconvex ] if set.key?(:jambconvex)
883
+ s[:door ] = set[:door ] if set.key?(:door)
884
+ s[:doorhead ] = set[:doorhead ] if set.key?(:doorhead)
885
+ s[:doorheadconcave ] = set[:doorheadconcave ] if set.key?(:doorheadconcave)
886
+ s[:doorheadconvex ] = set[:doorheadconvex ] if set.key?(:doorheadconvex)
887
+ s[:doorsill ] = set[:doorsill ] if set.key?(:doorsill)
888
+ s[:doorsillconcave ] = set[:doorsillconcave ] if set.key?(:doorsillconcave)
889
+ s[:doorsillconvex ] = set[:doorsillconvex ] if set.key?(:doorsillconvex)
890
+ s[:doorjamb ] = set[:doorjamb ] if set.key?(:doorjamb)
891
+ s[:doorjambconcave ] = set[:doorjambconcave ] if set.key?(:doorjambconcave)
892
+ s[:doorjambconvex ] = set[:doorjambconvex ] if set.key?(:doorjambconvex)
893
+ s[:skylight ] = set[:skylight ] if set.key?(:skylight)
894
+ s[:skylighthead ] = set[:skylighthead ] if set.key?(:skylighthead)
895
+ s[:skylightheadconcave ] = set[:skylightheadconcave ] if set.key?(:skylightheadconcave)
896
+ s[:skylightheadconvex ] = set[:skylightheadconvex ] if set.key?(:skylightheadconvex)
897
+ s[:skylightsill ] = set[:skylightsill ] if set.key?(:skylightsill)
898
+ s[:skylightsillconcave ] = set[:skylightsillconcave ] if set.key?(:skylightsillconcave)
899
+ s[:skylightsillconvex ] = set[:skylightsillconvex ] if set.key?(:skylightsillconvex)
900
+ s[:skylightjamb ] = set[:skylightjamb ] if set.key?(:skylightjamb)
901
+ s[:skylightjambconcave ] = set[:skylightjambconcave ] if set.key?(:skylightjambconcave)
902
+ s[:skylightjambconvex ] = set[:skylightjambconvex ] if set.key?(:skylightjambconvex)
903
+ s[:spandrel ] = set[:spandrel ] if set.key?(:spandrel)
904
+ s[:spandrelconcave ] = set[:spandrelconcave ] if set.key?(:spandrelconcave)
905
+ s[:spandrelconvex ] = set[:spandrelconvex ] if set.key?(:spandrelconvex)
906
+ s[:corner ] = set[:corner ] if set.key?(:corner)
907
+ s[:cornerconcave ] = set[:cornerconcave ] if set.key?(:cornerconcave)
908
+ s[:cornerconvex ] = set[:cornerconvex ] if set.key?(:cornerconvex)
909
+ s[:balcony ] = set[:balcony ] if set.key?(:balcony)
910
+ s[:balconyconcave ] = set[:balconyconcave ] if set.key?(:balconyconcave)
911
+ s[:balconyconvex ] = set[:balconyconvex ] if set.key?(:balconyconvex)
912
+ s[:balconysill ] = set[:balconysill ] if set.key?(:balconysill)
913
+ s[:balconysillconcave ] = set[:balconysillconcave ] if set.key?(:balconysillconcave)
914
+ s[:balconysillconvex ] = set[:balconysillconvex ] if set.key?(:balconysillconvex)
915
+ s[:balconydoorsill ] = set[:balconydoorsill ] if set.key?(:balconydoorsill)
916
+ s[:balconydoorsillconcave] = set[:balconydoorsillconcave] if set.key?(:balconydoorsillconcave)
917
+ s[:balconydoorsillconvex ] = set[:balconydoorsillconvex ] if set.key?(:balconydoorsillconvex)
918
+ s[:party ] = set[:party ] if set.key?(:party)
919
+ s[:partyconcave ] = set[:partyconcave ] if set.key?(:partyconcave)
920
+ s[:partyconvex ] = set[:partyconvex ] if set.key?(:partyconvex)
921
+ s[:grade ] = set[:grade ] if set.key?(:grade)
922
+ s[:gradeconcave ] = set[:gradeconcave ] if set.key?(:gradeconcave)
923
+ s[:gradeconvex ] = set[:gradeconvex ] if set.key?(:gradeconvex)
924
+ s[:joint ] = set[:joint ] if set.key?(:joint)
925
+ s[:transition ] = set[:transition ] if set.key?(:transition)
926
+
927
+ s[:joint ] = 0.000 unless set.key?(:joint)
928
+ s[:transition ] = 0.000 unless set.key?(:transition)
929
+
930
+ @set[id] = s
931
+ self.gen(id)
433
932
 
434
933
  true
435
934
  end
436
935
 
437
936
  ##
438
- # Return PSI set shorthands. The return Hash holds 2x keys ... has: a Hash
439
- # of true/false (values) for any admissible PSI type (keys), and val: a Hash
440
- # of PSI-values for any admissible PSI type (default: 0.0 W/K per meter).
937
+ # Returns PSI set shorthands. The return Hash holds 2 keys, has: a Hash
938
+ # of true/false (values) for any admissible PSI type (keys), and val: a
939
+ # Hash of PSI-factors (values) for any admissible PSI type (keys).
940
+ # PSI-factors default to 0 W/K per linear meter if missing from set.
441
941
  #
442
- # @param id [String] a PSI set identifier
942
+ # @param id [#to_s] PSI set identifier
943
+ # @example intermediate floor slab intersection
944
+ # shorthands("A901")
443
945
  #
444
- # @return [Hash] has: Hash of true/false, val: Hash of PSI values
445
- # @return [Hash] has: empty Hash, val: empty Hash (if invalid/missing set)
946
+ # @return [Hash] has: Hash (Bool), val: Hash (PSI factors) see logs if empty
446
947
  def shorthands(id = "")
447
948
  mth = "TBD::#{__callee__}"
448
- cl = String
449
949
  sh = { has: {}, val: {} }
450
-
451
- return TBD.mismatch("id", id, String, mth, DBG, sh) unless id.is_a?(cl)
452
- return TBD.hashkey(id, @set, id, mth, ERR, sh) unless @set.key?(id)
453
- return TBD.hashkey(id, @has, id, mth, ERR, sh) unless @has.key?(id)
454
- return TBD.hashkey(id, @val, id, mth, ERR, sh) unless @val.key?(id)
950
+ id = trim(id)
951
+ return mismatch("set ID", id, String, mth, ERR, a) if id.empty?
952
+ return hashkey(id, @set , id, mth, ERR, sh) unless @set.key?(id)
953
+ return hashkey(id, @has , id, mth, ERR, sh) unless @has.key?(id)
954
+ return hashkey(id, @val , id, mth, ERR, sh) unless @val.key?(id)
455
955
 
456
956
  sh[:has] = @has[id]
457
957
  sh[:val] = @val[id]
@@ -460,81 +960,98 @@ module TBD
460
960
  end
461
961
 
462
962
  ##
463
- # Validate whether a given PSI set has a complete list of PSI type:values.
963
+ # Validates whether a given PSI set has a complete list of PSI type:values.
464
964
  #
465
- # @param id [String] a PSI set identifier
965
+ # @param id [#to_s] PSI set identifier
466
966
  #
467
- # @return [Bool] true if found and is complete
468
- # @return [Bool] false if invalid input
967
+ # @return [Bool] whether provided PSI set is held in memory and is complete
968
+ # @return [false] if invalid input (see logs)
469
969
  def complete?(id = "")
470
970
  mth = "TBD::#{__callee__}"
471
971
  a = false
472
-
473
- return TBD.mismatch("id", id, String, mth, DBG, a) unless id.is_a?(String)
474
- return TBD.hashkey(id, @set, id, mth, ERR, a) unless @set.key?(id)
475
- return TBD.hashkey(id, @has, id, mth, ERR, a) unless @has.key?(id)
476
- return TBD.hashkey(id, @val, id, mth, ERR, a) unless @val.key?(id)
972
+ id = trim(id)
973
+ return mismatch("set ID", id, String, mth, ERR, a) if id.empty?
974
+ return hashkey(id, @set , id, mth, ERR, a) unless @set.key?(id)
975
+ return hashkey(id, @has , id, mth, ERR, a) unless @has.key?(id)
976
+ return hashkey(id, @val , id, mth, ERR, a) unless @val.key?(id)
477
977
 
478
978
  holes = []
479
- holes << :head if @has[id][:head ]
480
- holes << :sill if @has[id][:sill ]
481
- holes << :jamb if @has[id][:jamb ]
979
+ holes << :head if @has[id][:head ]
980
+ holes << :sill if @has[id][:sill ]
981
+ holes << :jamb if @has[id][:jamb ]
482
982
  ok = holes.size == 3
483
- ok = true if @has[id][:fenestration ]
484
- return false unless ok
983
+ ok = true if @has[id][:fenestration ]
984
+ return false unless ok
485
985
 
486
986
  corners = []
487
- corners << :concave if @has[id][:cornerconcave ]
488
- corners << :convex if @has[id][:cornerconvex ]
987
+ corners << :concave if @has[id][:cornerconcave ]
988
+ corners << :convex if @has[id][:cornerconvex ]
489
989
  ok = corners.size == 2
490
- ok = true if @has[id][:corner ]
491
- return false unless ok
990
+ ok = true if @has[id][:corner ]
991
+ return false unless ok
492
992
 
493
993
  parapets = []
494
- parapets << :concave if @has[id][:parapetconcave]
495
- parapets << :convex if @has[id][:parapetconvex ]
496
- ok = parapets.size == 2
497
- ok = true if @has[id][:parapet ]
498
- return false unless ok
499
- return false unless @has[id][:party ]
500
- return false unless @has[id][:grade ]
501
- return false unless @has[id][:balcony ]
502
- return false unless @has[id][:rimjoist ]
994
+ roofs = []
995
+ parapets << :concave if @has[id][:parapetconcave]
996
+ parapets << :convex if @has[id][:parapetconvex ]
997
+ roofs << :concave if @has[id][:roofconcave ]
998
+ parapets << :convex if @has[id][:roofconvex ]
999
+ ok = parapets.size == 2 || roofs.size == 2
1000
+ ok = true if @has[id][:parapet ]
1001
+ ok = true if @has[id][:roof ]
1002
+ return false unless ok
1003
+ return false unless @has[id][:party ]
1004
+ return false unless @has[id][:grade ]
1005
+ return false unless @has[id][:balcony ]
1006
+ return false unless @has[id][:rimjoist ]
503
1007
 
504
1008
  ok
505
1009
  end
506
1010
 
507
1011
  ##
508
- # Return safe PSI type if missing input from PSI set (based on inheritance).
1012
+ # Returns safe PSI type if missing from PSI set (based on inheritance).
509
1013
  #
510
- # @param id [String] a PSI set identifier
511
- # @param type [Symbol] a PSI type, e.g. :rimjoistconcave
1014
+ # @param id [#to_s] PSI set identifier
1015
+ # @param type [#to_sym] PSI type
1016
+ # @example intermediate floor slab intersection
1017
+ # safe("90.1.22|wood.fr|unmitigated", :rimjoistconcave)
512
1018
  #
513
1019
  # @return [Symbol] safe PSI type
514
- # @return [Nil] if invalid input or no safe PSI type found
1020
+ # @return [nil] if invalid inputs (see logs)
515
1021
  def safe(id = "", type = nil)
516
1022
  mth = "TBD::#{__callee__}"
517
- cl1 = String
518
- cl2 = Symbol
1023
+ id = trim(id)
1024
+ ck1 = id.empty?
1025
+ ck2 = type.respond_to?(:to_sym)
1026
+ return mismatch("set ID", id, String, mth) if ck1
1027
+ return mismatch("type", type, Symbol, mth) unless ck2
1028
+ return hashkey(id, @set, id, mth, ERR) unless @set.key?(id)
1029
+ return hashkey(id, @has, id, mth, ERR) unless @has.key?(id)
519
1030
 
520
- return TBD.mismatch("id", id, cl1, mth) unless id.is_a?(cl1)
521
- return TBD.mismatch("type", type, cl2, mth, ERR) unless type.is_a?(cl2)
522
- return TBD.hashkey(id, @set, id, mth, ERR) unless @set.key?(id)
523
- return TBD.hashkey(id, @has, id, mth, ERR) unless @has.key?(id)
1031
+ safer = type.to_sym
524
1032
 
525
- safer = type
1033
+ unless @has[id][safer]
1034
+ concave = safer.to_s.include?("concave")
1035
+ convex = safer.to_s.include?("convex")
1036
+ safer = safer.to_s.chomp("concave").to_sym if concave
1037
+ safer = safer.to_s.chomp("convex").to_sym if convex
1038
+ end
526
1039
 
527
1040
  unless @has[id][safer]
528
- concave = type.to_s.include?("concave")
529
- convex = type.to_s.include?("convex")
530
- safer = type.to_s.chomp("concave").to_sym if concave
531
- safer = type.to_s.chomp("convex").to_sym if convex
532
-
533
- unless @has[id][safer]
534
- safer = :fenestration if safer == :head
535
- safer = :fenestration if safer == :sill
536
- safer = :fenestration if safer == :jamb
537
- end
1041
+ safer = :fenestration if safer == :head
1042
+ safer = :fenestration if safer == :sill
1043
+ safer = :fenestration if safer == :jamb
1044
+ safer = :door if safer == :doorhead
1045
+ safer = :door if safer == :doorsill
1046
+ safer = :door if safer == :doorjamb
1047
+ safer = :skylight if safer == :skylighthead
1048
+ safer = :skylight if safer == :skylightsill
1049
+ safer = :skylight if safer == :skylightjamb
1050
+ end
1051
+
1052
+ unless @has[id][safer]
1053
+ safer = :fenestration if safer == :skylight
1054
+ safer = :fenestration if safer == :door
538
1055
  end
539
1056
 
540
1057
  return safer if @has[id][safer]
@@ -544,30 +1061,41 @@ module TBD
544
1061
  end
545
1062
 
546
1063
  ##
547
- # Process TBD JSON inputs, after TBD has processed OpenStudio model variables
548
- # and retrieved corresponding Topolys model surface/edge properties. TBD user
549
- # inputs allow customization of default assumptions and inferred values.
550
- # If successful, "edges" (input) may inherit additional properties, e.g.:
551
- # edge-specific PSI set (defined in TBD JSON file), edge-specific PSI type
552
- # (e.g. "corner", defined in TBD JSON file), project-wide PSI set (if absent
553
- # from TBD JSON file).
1064
+ # Processes TBD JSON inputs, after TBD has preprocessed OpenStudio model
1065
+ # variables and retrieved corresponding Topolys model surface/edge
1066
+ # properties. TBD user inputs allow customization of default assumptions and
1067
+ # inferred values. If successful, "edges" (input) may inherit additional
1068
+ # properties, e.g.: edge-specific PSI set (defined in TBD JSON file),
1069
+ # edge-specific PSI type (e.g. "corner", defined in TBD JSON file),
1070
+ # project-wide PSI set (if absent from TBD JSON file).
554
1071
  #
555
- # @param s [Hash] preprocessed TBD surfaces
556
- # @param e [Hash] preprocessed TBD edges
557
- # @param argh [Hash] arguments
1072
+ # @param [Hash] s TBD surfaces (keys: Openstudio surface names)
1073
+ # @option s [Hash] :windows TBD surface-specific windows e.g. s[][:windows]
1074
+ # @option s [Hash] :doors TBD surface-specific doors
1075
+ # @option s [Hash] :skylights TBD surface-specific skylights
1076
+ # @option s [OpenStudio::Model::BuildingStory] :story OpenStudio story
1077
+ # @option s ["Wall", "RoofCeiling", "Floor"] :stype OpenStudio surface type
1078
+ # @option s [OpenStudio::Model::Space] :space OpenStudio space
1079
+ # @param [Hash] e TBD edges (keys: Topolys edge identifiers)
1080
+ # @option e [Hash] :surfaces linked TBD surfaces e.g. e[][:surfaces]
1081
+ # @option e [#to_f] :length edge length in m
1082
+ # @option e [Topolys::Point3D] :v0 origin vertex
1083
+ # @option e [Topolys::Point3D] :v1 terminal vertex
1084
+ # @param [Hash] argh TBD arguments
1085
+ # @option argh [#to_s] :option selected PSI set
1086
+ # @option argh [#to_s] :io_path tbd.json input file path
1087
+ # @option argh [#to_s] :schema_path TBD JSON schema file path
558
1088
  #
559
- # @return [Hash] io: JSON inputs (Hash), psi:/khi: new (enriched) sets (Hash)
560
- # @return [Hash] io: empty Hash if invalid input
1089
+ # @return [Hash] io: (Hash), psi:/khi: enriched sets (see logs if empty)
561
1090
  def inputs(s = {}, e = {}, argh = {})
562
1091
  mth = "TBD::#{__callee__}"
563
1092
  opt = :option
564
1093
  ipt = { io: {}, psi: PSI.new, khi: KHI.new }
565
1094
  io = {}
566
-
567
- return mismatch("s", s, Hash, mth, DBG, ipt) unless s.is_a?(Hash)
568
- return mismatch("e", s, Hash, mth, DBG, ipt) unless e.is_a?(Hash)
569
- return mismatch("argh", s, Hash, mth, DBG, ipt) unless argh.is_a?(Hash)
570
- return hashkey("argh", argh, opt, mth, DBG, ipt) unless argh.key?(opt)
1095
+ return mismatch("s" , s , Hash, mth, DBG, ipt) unless s.is_a?(Hash)
1096
+ return mismatch("e" , e , Hash, mth, DBG, ipt) unless e.is_a?(Hash)
1097
+ return mismatch("argh", argh, Hash, mth, DBG, ipt) unless argh.is_a?(Hash)
1098
+ return hashkey("argh" , argh, opt , mth, DBG, ipt) unless argh.key?(opt)
571
1099
 
572
1100
  argh[:io_path ] = nil unless argh.key?(:io_path)
573
1101
  argh[:schema_path] = nil unless argh.key?(:schema_path)
@@ -580,41 +1108,43 @@ module TBD
580
1108
  io = pth
581
1109
  else
582
1110
  return empty("JSON file", mth, FTL, ipt) unless File.size?(pth)
1111
+
583
1112
  io = File.read(pth)
584
1113
  io = JSON.parse(io, symbolize_names: true)
585
1114
  return mismatch("io", io, Hash, mth, FTL, ipt) unless io.is_a?(Hash)
586
1115
  end
587
1116
 
588
1117
  # Schema validation is not yet supported in the OpenStudio Application.
589
- # We nonetheless recommend that users rely on the json-schema gem, or an
590
- # online linter, prior to using TBD. The following checks focus on content
591
- # - ignoring bad JSON input otherwise caught via JSON validation.
1118
+ # It is nonetheless recommended that users rely on the json-schema gem,
1119
+ # or an online linter, prior to using TBD. The following checks focus on
1120
+ # content - ignoring bad JSON input otherwise caught via JSON validation.
592
1121
  #
593
1122
  # A side note: JSON validation relies on case-senitive string comparisons
594
1123
  # (e.g. OpenStudio space or surface names, vs corresponding TBD JSON
595
- # identifiers). So "Space-1" doesn't match "SPACE-1" - head's up.
1124
+ # identifiers). So "Space-1" doesn't match "SPACE-1" ... head's up!
596
1125
  if sch
597
1126
  require "json-schema"
1127
+ return invalid("JSON schema", mth, 3, FTL, ipt) unless File.exist?(sch)
1128
+ return empty("JSON schema" , mth, FTL, ipt) if File.zero?(sch)
598
1129
 
599
- return invalid("JSON schema", mth, 0, FTL, ipt) unless File.exist?(sch)
600
- return empty("JSON schema", mth, FTL, ipt) if File.zero?(sch)
601
1130
  schema = File.read(sch)
602
1131
  schema = JSON.parse(schema, symbolize_names: true)
603
1132
  valid = JSON::Validator.validate!(schema, io)
604
- return invalid("JSON schema validation", mth, 0, FTL, ipt) unless valid
1133
+ return invalid("JSON schema validation", mth, 3, FTL, ipt) unless valid
605
1134
  end
606
1135
 
607
1136
  # Append JSON entries to library of linear & point thermal bridges.
608
- io[:psis].each { |psi| ipt[:psi].append(psi) } if io.key?(:psis)
609
- io[:khis].each { |khi| ipt[:khi].append(khi) } if io.key?(:khis)
1137
+ io[:psis].each { |psi| ipt[:psi].append(psi) } if io.key?(:psis)
1138
+ io[:khis].each { |khi| ipt[:khi].append(khi) } if io.key?(:khis)
610
1139
 
611
1140
  # JSON-defined or user-selected, building PSI set must be complete/valid.
612
1141
  io[:building] = { psi: argh[opt] } unless io.key?(:building)
613
1142
  bdg = io[:building]
614
- ok = bdg.key?(:psi)
615
- return hashkey("Building PSI", bdg, :psi, mth, FTL, ipt) unless ok
1143
+ ok = bdg.key?(:psi)
1144
+ return hashkey("Building PSI", bdg, :psi, mth, FTL, ipt) unless ok
1145
+
616
1146
  ok = ipt[:psi].complete?(bdg[:psi])
617
- return invalid("Complete building PSI", mth, 0, FTL, ipt) unless ok
1147
+ return invalid("Complete building PSI", mth, 3, FTL, ipt) unless ok
618
1148
 
619
1149
  # Validate remaining (optional) JSON entries.
620
1150
  [:stories, :spacetypes, :spaces].each do |types|
@@ -625,14 +1155,16 @@ module TBD
625
1155
  if io.key?(types)
626
1156
  io[types].each do |type|
627
1157
  next unless type.key?(:psi)
628
- next unless type.key?(:id)
1158
+ next unless type.key?(:id )
1159
+
629
1160
  s1 = "JSON/OSM '#{type[:id]}' (#{mth})"
630
1161
  s2 = "JSON/PSI '#{type[:id]}' set (#{mth})"
631
1162
  match = false
632
1163
 
633
- s.values.each do |props| # TBD model surface linked to type?
634
- break if match
1164
+ s.values.each do |props| # TBD surface linked to type?
1165
+ break if match
635
1166
  next unless props.key?(key)
1167
+
636
1168
  match = type[:id] == props[key].nameString
637
1169
  end
638
1170
 
@@ -645,6 +1177,7 @@ module TBD
645
1177
  if io.key?(:surfaces)
646
1178
  io[:surfaces].each do |surface|
647
1179
  next unless surface.key?(:id)
1180
+
648
1181
  s1 = "JSON/OSM surface '#{surface[:id]}' (#{mth})"
649
1182
  log(ERR, s1) unless s.key?(surface[:id])
650
1183
 
@@ -657,6 +1190,7 @@ module TBD
657
1190
  if surface.key?(:khis)
658
1191
  surface[:khis].each do |khi|
659
1192
  next unless khi.key?(:id)
1193
+
660
1194
  s3 = "JSON/KHI surface '#{surface[:id]}' '#{khi[:id]}' (#{mth})"
661
1195
  log(ERR, s3) unless ipt[:khi].point.key?(khi[:id])
662
1196
  end
@@ -668,6 +1202,7 @@ module TBD
668
1202
  io[:subsurfaces].each do |sub|
669
1203
  next unless sub.key?(:id)
670
1204
  next unless sub.key?(:usi)
1205
+
671
1206
  match = false
672
1207
 
673
1208
  s.each do |id, surface|
@@ -677,6 +1212,7 @@ module TBD
677
1212
  if surface.key?(holes)
678
1213
  surface[holes].keys.each do |id|
679
1214
  break if match
1215
+
680
1216
  match = sub[:id] == id
681
1217
  end
682
1218
  end
@@ -691,30 +1227,33 @@ module TBD
691
1227
  io[:edges].each do |edge|
692
1228
  next unless edge.key?(:type)
693
1229
  next unless edge.key?(:surfaces)
694
- surfaces = edge[:surfaces]
695
- type = edge[:type].to_sym
696
- safer = ipt[:psi].safe(bdg[:psi], type) # fallback
1230
+
1231
+ surfaces = edge[:surfaces]
1232
+ type = edge[:type].to_sym
1233
+ safer = ipt[:psi].safe(bdg[:psi], type) # fallback
697
1234
  log(ERR, "Skipping invalid edge PSI '#{type}' (#{mth})") unless safer
698
1235
  next unless safer
1236
+
699
1237
  valid = true
700
1238
 
701
- surfaces.each do |surface| # TBD edge's surfaces on file
702
- e.values.each do |ee| # TBD edges in memory
703
- break unless valid # if previous anomaly detected
704
- next if ee.key?(:io_type) # validated from previous loop
1239
+ surfaces.each do |surface| # TBD edge's surfaces on file
1240
+ e.values.each do |ee| # TBD edges in memory
1241
+ break unless valid # if previous anomaly detected
1242
+ next if ee.key?(:io_type) # validated from previous loop
705
1243
  next unless ee.key?(:surfaces)
1244
+
706
1245
  surfs = ee[:surfaces]
707
1246
  next unless surfs.key?(surface)
708
1247
 
709
1248
  # An edge on file is valid if ALL of its listed surfaces together
710
- # connect at least one or more TBD/Topolys model edges in memory.
711
- # Each of the latter may connect e.g. 3x TBD/Topolys surfaces,
712
- # but the list of surfaces on file may be shorter, e.g. only 2x.
1249
+ # connect at least 1 or more TBD/Topolys model edges in memory.
1250
+ # Each of the latter may connect e.g. 3 TBD/Topolys surfaces,
1251
+ # but the list of surfaces on file may be shorter, e.g. only 2.
713
1252
  match = true
714
1253
  surfaces.each { |id| match = false unless surfs.key?(id) }
715
1254
  next unless match
716
1255
 
717
- if edge.key?(:length) # optional
1256
+ if edge.key?(:length) # optional
718
1257
  next unless (ee[:length] - edge[:length]).abs < TOL
719
1258
  end
720
1259
 
@@ -724,7 +1263,6 @@ module TBD
724
1263
 
725
1264
  unless edge.key?(:v0x) && edge.key?(:v0y) && edge.key?(:v0z) &&
726
1265
  edge.key?(:v1x) && edge.key?(:v1y) && edge.key?(:v1z)
727
-
728
1266
  log(ERR, "Mismatch '#{surface}' edge vertices (#{mth})")
729
1267
  valid = false
730
1268
  next
@@ -743,17 +1281,17 @@ module TBD
743
1281
  next unless matches?(e1, e2)
744
1282
  end
745
1283
 
746
- if edge.key?(:psi) # optional
1284
+ if edge.key?(:psi) # optional
747
1285
  set = edge[:psi]
748
1286
 
749
1287
  if ipt[:psi].set.key?(set)
750
1288
  saferr = ipt[:psi].safe(set, type)
751
- ee[:io_set ] = set if saferr
752
- ee[:io_type] = type if saferr
753
- log(ERR, "Invalid '#{set}': '#{type}' (#{mth})") unless saferr
754
- valid = false unless saferr
1289
+ ee[:io_set ] = set if saferr
1290
+ ee[:io_type] = type if saferr
1291
+ log(ERR, "Invalid #{set}: #{type} (#{mth})") unless saferr
1292
+ valid = false unless saferr
755
1293
  else
756
- log(ERR, "Missing edge PSI '#{set}' (#{mth})")
1294
+ log(ERR, "Missing edge PSI #{set} (#{mth})")
757
1295
  valid = false
758
1296
  end
759
1297
  else
@@ -767,10 +1305,12 @@ module TBD
767
1305
  # No (optional) user-defined TBD JSON input file. In such cases, provided
768
1306
  # argh[:option] must refer to a valid PSI set. If valid, all edges inherit
769
1307
  # a default PSI set (without KHI entries).
770
- ok = ipt[:psi].complete?(argh[opt])
771
- io[:building] = { psi: argh[opt] } if ok
772
- log(FTL, "Incomplete building PSI set '#{argh[opt]}' (#{mth})") unless ok
773
- return ipt unless ok
1308
+ msg = "Incomplete building PSI set '#{argh[opt]}' (#{mth})"
1309
+ ok = ipt[:psi].complete?(argh[opt])
1310
+
1311
+ io[:building] = { psi: argh[opt] } if ok
1312
+ log(FTL, msg) unless ok
1313
+ return ipt unless ok
774
1314
  end
775
1315
 
776
1316
  ipt[:io] = io
@@ -779,86 +1319,83 @@ module TBD
779
1319
  end
780
1320
 
781
1321
  ##
782
- # Thermally derate insulating material within construction.
1322
+ # Thermally derates insulating material within construction.
783
1323
  #
784
- # @param model [OpenStudio::Model::Model] a model
785
- # @param id [String] surface identifier
786
- # @param surface [Hash] a TBD surface
1324
+ # @param id [#to_s] surface identifier
1325
+ # @param [Hash] s TBD surface parameters
1326
+ # @option s [#to_f] :heatloss heat loss from major thermal bridging, in W/K
1327
+ # @option s [#to_f] :net surface net area, in m2
1328
+ # @option s [:massless, :standard] :ltype indexed layer type
1329
+ # @option s [#to_i] :index deratable construction layer index
1330
+ # @option s [#to_f] :r deratable layer Rsi-factor, in m2•K/W
787
1331
  # @param lc [OpenStudio::Model::LayeredConstruction] a layered construction
788
1332
  #
789
1333
  # @return [OpenStudio::Model::Material] derated (cloned) material
790
- # @return [NilClass] if invalid input
791
- def derate(model = nil, id = "", s = {}, lc = nil)
1334
+ # @return [nil] if invalid input (see logs)
1335
+ def derate(id = "", s = {}, lc = nil)
792
1336
  mth = "TBD::#{__callee__}"
793
1337
  m = nil
794
- k1 = :heatloss
795
- k2 = :ltype
796
- k3 = :construction
797
- k4 = :index
798
- cl1 = OpenStudio::Model::Model
799
- cl2 = OpenStudio::Model::LayeredConstruction
800
- cl3 = Numeric
801
- cl4 = Symbol
802
- cl5 = Integer
803
-
804
- return mismatch("model", model, cl, mth) unless model.is_a?(cl1)
805
- return mismatch("id", id, String, mth) unless id.is_a?(String)
806
- return mismatch(id, s, Hash, mth) unless s.is_a?(Hash)
807
- return mismatch("lc", lc, Hash, mth) unless lc.is_a?(cl2)
808
- return hashkey("'#{id}' W/K", s, k1, mth) unless s.key?(k1)
809
- return invalid("'#{id}' W/K", mth, 3) unless s[k1]
810
- return mismatch("'#{id}' W/K", s[k1], cl3, mth) unless s[k1].is_a?(cl3)
811
- return zero("'#{id}' W/K", mth, WRN) if s[k1].abs < TOL
812
- return hashkey("'#{id}' m2", s, :net, mth) unless s.key?(:net)
813
- return invalid("'#{id}' m2", mth, 3) unless s[:net]
814
- return mismatch("'#{id}' m2", s[:net], cl3, mth) unless s[:net].is_a?(cl3)
815
- return zero("'#{id}' m2", mth, WRN) if s[:net].abs < TOL
816
- return hashkey("'#{id}' type", s, k2, mth) unless s.key?(k2)
817
- return invalid("'#{id}' type", mth, 3) unless s[k2]
818
- return mismatch("'#{id}' type", s[k2], cl4, mth) unless s[k2].is_a?(cl4)
819
-
820
- ok = s[k2] == :massless || s[k2] == :standard
821
-
822
- return invalid("'#{id}' type", mth, 3) unless ok
823
- return hashkey("'#{id}' construction", s, k3, mth) unless s.key?(k3)
824
- return hashkey("'#{id}' index", s, k4, mth) unless s.key?(k4)
825
- return invalid("'#{id}' index", mth, 3) unless s[k4]
826
- return mismatch("'#{id}' index", s[k4], cl5, mth) unless s[k4].is_a?(cl5)
827
- return negative("'#{id}' index", mth) if s[k4] < 0
828
- return hashkey("'#{id}' Rsi", s, :r, mth) unless s.key?(:r)
829
- return invalid("'#{id}' Rsi", mth, 3) unless s[:r]
830
- return mismatch("'#{id}' Rsi", s[:r], cl3, mth) unless s[:r].is_a?(cl3)
831
- return zero("'#{id}' Rsi", mth, WRN) if s[:r].abs < 0.001
832
-
833
- derated = lc.nameString.include?(" tbd")
834
- log(WRN, "Won't derate '#{id}': already derated (#{mth})") if derated
835
- return m if derated
836
-
837
- index = s[:index]
838
- ltype = s[:ltype]
839
- r = s[:r]
840
- u = s[:heatloss] / s[:net]
1338
+ id = trim(id)
1339
+ kys = [:heatloss, :net, :ltype, :index, :r]
1340
+ ck1 = s.is_a?(Hash)
1341
+ ck2 = lc.is_a?(OpenStudio::Model::LayeredConstruction)
1342
+ return mismatch("id" , id, cl6, mth) if id.empty?
1343
+ return mismatch("#{id} surface" , s , cl1, mth) unless ck1
1344
+ return mismatch("#{id} construction", lc, cl2, mth) unless ck2
1345
+
1346
+ kys.each do |k|
1347
+ tag = "#{id} #{k}"
1348
+ return hashkey(tag, s, k, mth, ERR) unless s.key?(k)
1349
+
1350
+ case k
1351
+ when :heatloss
1352
+ return mismatch(tag, s[k], Numeric, mth) unless s[k].respond_to?(:to_f)
1353
+ return zero(tag, mth, WRN) if s[k].to_f.abs < 0.001
1354
+ when :net, :r
1355
+ return mismatch(tag, s[k], Numeric, mth) unless s[k].respond_to?(:to_f)
1356
+ return negative(tag, mth, 2, ERR) if s[k].to_f < 0
1357
+ return zero(tag, mth, WRN) if s[k].to_f.abs < 0.001
1358
+ when :index
1359
+ return mismatch(tag, s[k], Numeric, mth) unless s[k].respond_to?(:to_i)
1360
+ return negative(tag, mth, 2, ERR) if s[k].to_f < 0
1361
+ else # :ltype
1362
+ next if [:massless, :standard].include?(s[k])
1363
+ return invalid(tag, mth, 2, ERR)
1364
+ end
1365
+ end
1366
+
1367
+ if lc.nameString.downcase.include?(" tbd")
1368
+ log(WRN, "Won't derate '#{id}': tagged as derated (#{mth})")
1369
+ return m
1370
+ end
1371
+
1372
+ model = lc.model
1373
+ ltype = s[:ltype ]
1374
+ index = s[:index ].to_i
1375
+ net = s[:net ].to_f
1376
+ r = s[:r ].to_f
1377
+ u = s[:heatloss].to_f / net
841
1378
  loss = 0
842
- de_u = 1 / r + u # derated U
843
- de_r = 1 / de_u # derated R
1379
+ de_u = 1 / r + u # derated U
1380
+ de_r = 1 / de_u # derated R
844
1381
 
845
1382
  if ltype == :massless
846
1383
  m = lc.getLayer(index).to_MasslessOpaqueMaterial
847
- return invalid("'#{id}' massless layer?", mth, 0) if m.empty?
1384
+ return invalid("#{id} massless layer?", mth, 0) if m.empty?
848
1385
  m = m.get
849
1386
  up = ""
850
- up = "uprated " if m.nameString.include?(" uprated")
1387
+ up = "uprated " if m.nameString.downcase.include?(" uprated")
851
1388
  m = m.clone(model).to_MasslessOpaqueMaterial.get
852
1389
  m.setName("#{id} #{up}m tbd")
853
- de_r = 0.001 unless de_r > 0.001
854
- loss = (de_u - 1 / de_r) * s[:net] unless de_r > 0.001
1390
+ de_r = 0.001 unless de_r > 0.001
1391
+ loss = (de_u - 1 / de_r) * net unless de_r > 0.001
855
1392
  m.setThermalResistance(de_r)
856
1393
  else
857
1394
  m = lc.getLayer(index).to_StandardOpaqueMaterial
858
- return invalid("'#{id}' standard layer?", mth, 0) if m.empty?
1395
+ return invalid("#{id} standard layer?", mth, 0) if m.empty?
859
1396
  m = m.get
860
1397
  up = ""
861
- up = "uprated " if m.nameString.include?(" uprated")
1398
+ up = "uprated " if m.nameString.downcase.include?(" uprated")
862
1399
  m = m.clone(model).to_StandardOpaqueMaterial.get
863
1400
  m.setName("#{id} #{up}m tbd")
864
1401
  k = m.thermalConductivity
@@ -869,14 +1406,14 @@ module TBD
869
1406
  unless d > 0.003
870
1407
  d = 0.003
871
1408
  k = d / de_r
872
- k = 3 unless k < 3
873
- loss = (de_u - k / d) * s[:net] unless k < 3
1409
+ k = 3 unless k < 3
1410
+ loss = (de_u - k / d) * net unless k < 3
874
1411
  end
875
- else # de_r < 0.001 m2.K/W
1412
+ else # de_r < 0.001 m2K/W
876
1413
  d = 0.001 * k
877
- d = 0.003 unless d > 0.003
878
- k = d / 0.001 unless d > 0.003
879
- loss = (de_u - k / d) * s[:net]
1414
+ d = 0.003 unless d > 0.003
1415
+ k = d / 0.001 unless d > 0.003
1416
+ loss = (de_u - k / d) * net
880
1417
  end
881
1418
 
882
1419
  m.setThickness(d)
@@ -885,50 +1422,78 @@ module TBD
885
1422
 
886
1423
  if m && loss > TOL
887
1424
  s[:r_heatloss] = loss
888
- h_loss = format "%.3f", s[:r_heatloss]
889
- log(WRN, "Won't assign #{h_loss} W/K to '#{id}': too conductive (#{mth})")
1425
+ hl = format "%.3f", s[:r_heatloss]
1426
+ log(WRN, "Won't assign #{hl} W/K to '#{id}': too conductive (#{mth})")
890
1427
  end
891
1428
 
892
1429
  m
893
1430
  end
894
1431
 
895
1432
  ##
896
- # Process TBD objects, based on OpenStudio model (OSM) and Topolys model,
897
- # and derate admissible envelope surfaces by substituting insulating material
898
- # within surface multilayered constructions with derated clones. Returns a
899
- # hash holding 2x key:value pairs ... io: objects for JSON serialization and
900
- # surfaces: derated TBD surfaces.
1433
+ # Processes TBD objects, based on an OpenStudio and generated Topolys model,
1434
+ # and derates admissible envelope surfaces by substituting insulating
1435
+ # materials with derated clones, within surface multilayered constructions.
1436
+ # Returns a Hash holding 2 key:value pairs; io: objects for JSON
1437
+ # serialization, and surfaces: derated TBD surfaces (see exit method).
901
1438
  #
902
1439
  # @param model [OpenStudio::Model::Model] a model
903
- # @param argh [Hash] TBD arguments
1440
+ # @param [Hash] argh TBD arguments
1441
+ # @option argh [#to_s] :option selected PSI set
1442
+ # @option argh [#to_s] :io_path tbd.json input file path
1443
+ # @option argh [#to_s] :schema_path TBD JSON schema file path
1444
+ # @option argh [Bool] :parapet (true) wall-roof edge as parapet
1445
+ # @option argh [Bool] :uprate_walls whether to uprate walls
1446
+ # @option argh [Bool] :uprate_roofs whether to uprate roofs
1447
+ # @option argh [Bool] :uprate_floors whether to uprate floors
1448
+ # @option argh [Bool] :wall_ut uprated wall Ut target in W/m2•K
1449
+ # @option argh [Bool] :roof_ut uprated roof Ut target in W/m2•K
1450
+ # @option argh [Bool] :floor_ut uprated floor Ut target in W/m2•K
1451
+ # @option argh [#to_s] :wall_option wall construction to uprate (or "all")
1452
+ # @option argh [#to_s] :roof_option roof construction to uprate (or "all")
1453
+ # @option argh [#to_s] :floor_option floor construction to uprate (or "all")
1454
+ # @option argh [Bool] :gen_ua whether to generate a UA' report
1455
+ # @option argh [#to_s] :ua_ref selected UA' ruleset
1456
+ # @option argh [Bool] :gen_kiva whether to generate KIVA inputs
1457
+ # @option argh [#to_f] :sub_tol proximity tolerance between edges in m
904
1458
  #
905
1459
  # @return [Hash] io: (Hash), surfaces: (Hash)
906
- # @return [Hash] io: nil, surfaces: nil (if invalid input)
1460
+ # @return [Hash] io: nil, surfaces: nil if invalid input (see logs)
907
1461
  def process(model = nil, argh = {})
908
1462
  mth = "TBD::#{__callee__}"
909
1463
  cl = OpenStudio::Model::Model
910
1464
  tbd = { io: nil, surfaces: {} }
911
-
912
1465
  return mismatch("model", model, cl, mth, DBG, tbd) unless model.is_a?(cl)
913
1466
  return mismatch("argh", argh, Hash, mth, DBG, tbd) unless argh.is_a?(Hash)
914
1467
 
915
- argh = {} if argh.empty?
916
- argh[:sub_tol ] = TBD::TOL unless argh.key?(:sub_tol )
917
- argh[:option ] = "" unless argh.key?(:option )
918
- argh[:io_path ] = nil unless argh.key?(:io_path )
919
- argh[:schema_path ] = nil unless argh.key?(:schema_path )
920
- argh[:uprate_walls ] = false unless argh.key?(:uprate_walls )
921
- argh[:uprate_roofs ] = false unless argh.key?(:uprate_roofs )
922
- argh[:uprate_floors] = false unless argh.key?(:uprate_floors)
923
- argh[:wall_ut ] = 0 unless argh.key?(:wall_ut )
924
- argh[:roof_ut ] = 0 unless argh.key?(:roof_ut )
925
- argh[:floor_ut ] = 0 unless argh.key?(:floor_ut )
926
- argh[:wall_option ] = "" unless argh.key?(:wall_option )
927
- argh[:roof_option ] = "" unless argh.key?(:roof_option )
928
- argh[:floor_option ] = "" unless argh.key?(:floor_option )
929
- argh[:gen_ua ] = false unless argh.key?(:gen_ua )
930
- argh[:ua_ref ] = "" unless argh.key?(:ua_ref )
931
- argh[:gen_kiva ] = false unless argh.key?(:gen_kiva )
1468
+ argh = {} if argh.empty?
1469
+ argh[:option ] = "" unless argh.key?(:option)
1470
+ argh[:io_path ] = nil unless argh.key?(:io_path)
1471
+ argh[:schema_path ] = nil unless argh.key?(:schema_path)
1472
+ argh[:parapet ] = true unless argh.key?(:parapet)
1473
+ argh[:uprate_walls ] = false unless argh.key?(:uprate_walls)
1474
+ argh[:uprate_roofs ] = false unless argh.key?(:uprate_roofs)
1475
+ argh[:uprate_floors] = false unless argh.key?(:uprate_floors)
1476
+ argh[:wall_ut ] = 0 unless argh.key?(:wall_ut)
1477
+ argh[:roof_ut ] = 0 unless argh.key?(:roof_ut)
1478
+ argh[:floor_ut ] = 0 unless argh.key?(:floor_ut)
1479
+ argh[:wall_option ] = "" unless argh.key?(:wall_option)
1480
+ argh[:roof_option ] = "" unless argh.key?(:roof_option)
1481
+ argh[:floor_option ] = "" unless argh.key?(:floor_option)
1482
+ argh[:gen_ua ] = false unless argh.key?(:gen_ua)
1483
+ argh[:ua_ref ] = "" unless argh.key?(:ua_ref)
1484
+ argh[:gen_kiva ] = false unless argh.key?(:gen_kiva)
1485
+ argh[:reset_kiva ] = false unless argh.key?(:reset_kiva)
1486
+ argh[:sub_tol ] = TBD::TOL unless argh.key?(:sub_tol)
1487
+
1488
+ # Ensure true or false: whether to generate KIVA inputs.
1489
+ unless [true, false].include?(argh[:gen_kiva])
1490
+ return invalid("generate KIVA option", mth, 0, DBG, tbd)
1491
+ end
1492
+
1493
+ # Ensure true or false: whether to first purge (existing) KIVA inputs.
1494
+ unless [true, false].include?(argh[:reset_kiva])
1495
+ return invalid("reset KIVA option", mth, 0, DBG, tbd)
1496
+ end
932
1497
 
933
1498
  # Create the Topolys Model.
934
1499
  t_model = Topolys::Model.new
@@ -936,51 +1501,16 @@ module TBD
936
1501
  # "true" if any space/zone holds valid setpoint temperatures. With invalid
937
1502
  # inputs, these 2x methods return "false", ignoring any
938
1503
  # setpoint-based logic, e.g. semi-heated spaces (DEBUG errors are logged).
939
- setpoints = heatingTemperatureSetpoints?(model)
940
- setpoints = coolingTemperatureSetpoints?(model) || setpoints
941
-
942
- # "true" if any space/zone is part of an HVAC air loop. With invalid inputs,
943
- # the method returns "false", ignoring any air-loop related logic, e.g.
944
- # plenum zones as HVAC objects (DEBUG errors are logged).
945
- airloops = airLoopsHVAC?(model)
1504
+ heated = heatingTemperatureSetpoints?(model)
1505
+ cooled = coolingTemperatureSetpoints?(model)
1506
+ argh[:setpoints] = heated || cooled
946
1507
 
947
1508
  model.getSurfaces.sort_by { |s| s.nameString }.each do |s|
948
- # Fetch key attributes of opaque surfaces. Method returns nil with invalid
949
- # input (DEBUG and ERROR messages may be logged). TBD ignores them.
950
- surface = properties(model, s)
951
- next if surface.nil?
952
-
953
- # Similar to "setpoints?" methods above, the boolean methods below also
954
- # return "false" with invalid inputs, ignoring any space/zone
955
- # conditioning-based logic (e.g. semi-heated spaces, mislabelling a
956
- # plenum as an unconditioned zone).
957
- if setpoints
958
- if surface[:space].thermalZone.empty?
959
- plenum = plenum?(surface[:space], airloops, setpoints)
960
- surface[:conditioned] = false unless plenum
961
- else
962
- zone = surface[:space].thermalZone.get
963
- heat = maxHeatScheduledSetpoint(zone)
964
- cool = minCoolScheduledSetpoint(zone)
965
-
966
- unless heat[:spt] || cool[:spt]
967
- plenum = plenum?(surface[:space], airloops, setpoints)
968
- heat[:spt] = 21 if plenum
969
- cool[:spt] = 24 if plenum
970
- surface[:conditioned] = false unless plenum
971
- end
972
-
973
- free = heat[:spt] && heat[:spt] < -40 && cool[:spt] && cool[:spt] > 40
974
- surface[:conditioned] = false if free
975
- end
976
- end
977
-
978
- # Recover if valid setpoints.
979
- surface[:heating] = heat[:spt] if heat && heat[:spt]
980
- surface[:cooling] = cool[:spt] if cool && cool[:spt]
981
-
982
- tbd[:surfaces][s.nameString] = surface
983
- end # (opaque) surfaces populated
1509
+ # Fetch key attributes of opaque surfaces (and any linked sub surfaces).
1510
+ # Method returns nil with invalid input (see logs); TBD ignores them.
1511
+ surface = properties(s, argh)
1512
+ tbd[:surfaces][s.nameString] = surface unless surface.nil?
1513
+ end
984
1514
 
985
1515
  return empty("TBD surfaces", mth, ERR, tbd) if tbd[:surfaces].empty?
986
1516
 
@@ -988,74 +1518,77 @@ module TBD
988
1518
  # ... if facing outdoors or facing UNENCLOSED/UNCONDITIONED spaces.
989
1519
  tbd[:surfaces].each do |id, surface|
990
1520
  surface[:deratable] = false
991
-
992
1521
  next unless surface[:conditioned]
993
- next if surface[:ground]
1522
+ next if surface[:ground ]
994
1523
 
995
1524
  unless surface[:boundary].downcase == "outdoors"
996
1525
  next unless tbd[:surfaces].key?(surface[:boundary])
997
- next if tbd[:surfaces][surface[:boundary]][:conditioned]
1526
+ next if tbd[:surfaces][surface[:boundary]][:conditioned]
998
1527
  end
999
1528
 
1000
- ok = surface.key?(:index)
1001
- surface[:deratable] = true if ok
1002
- log(ERR, "Skipping '#{id}': insulating layer? (#{mth})") unless ok
1529
+ if surface.key?(:index)
1530
+ surface[:deratable] = true
1531
+ else
1532
+ log(ERR, "Skipping '#{id}': insulating layer? (#{mth})")
1533
+ end
1003
1534
  end
1004
1535
 
1005
- [:windows, :doors, :skylights].each do |holes| # sort kids
1536
+ # Sort subsurfaces before processing.
1537
+ [:windows, :doors, :skylights].each do |holes|
1006
1538
  tbd[:surfaces].values.each do |surface|
1007
- ok = surface.key?(holes)
1008
- surface[holes] = surface[holes].sort_by { |_, s| s[:minz] }.to_h if ok
1539
+ next unless surface.key?(holes)
1540
+
1541
+ surface[holes] = surface[holes].sort_by { |_, s| s[:minz] }.to_h
1009
1542
  end
1010
1543
  end
1011
1544
 
1012
1545
  # Split "surfaces" hash into "floors", "ceilings" and "walls" hashes.
1013
- floors = tbd[:surfaces].select { |_, s| s[:type] == :floor }
1014
- ceilings = tbd[:surfaces].select { |_, s| s[:type] == :ceiling }
1015
- walls = tbd[:surfaces].select { |_, s| s[:type] == :wall }
1016
- floors = floors.sort_by { |_, s| [s[:minz], s[:space]] }.to_h
1017
- ceilings = ceilings.sort_by { |_, s| [s[:minz], s[:space]] }.to_h
1018
- walls = walls.sort_by { |_, s| [s[:minz], s[:space]] }.to_h
1546
+ floors = tbd[:surfaces].select { |_, s| s[:type] == :floor }
1547
+ ceilings = tbd[:surfaces].select { |_, s| s[:type] == :ceiling }
1548
+ walls = tbd[:surfaces].select { |_, s| s[:type] == :wall }
1549
+
1550
+ floors = floors.sort_by { |_, s| [s[:minz], s[:space]] }.to_h
1551
+ ceilings = ceilings.sort_by { |_, s| [s[:minz], s[:space]] }.to_h
1552
+ walls = walls.sort_by { |_, s| [s[:minz], s[:space]] }.to_h
1019
1553
 
1020
1554
  # Fetch OpenStudio shading surfaces & key attributes.
1021
1555
  shades = {}
1022
1556
 
1023
1557
  model.getShadingSurfaces.each do |s|
1024
- id = s.nameString
1025
- empty = s.shadingSurfaceGroup.empty?
1026
- log(ERR, "Can't process '#{id}' transformation (#{mth})") if empty
1027
- next if empty
1028
- group = s.shadingSurfaceGroup.get
1029
- shading = group.to_ShadingSurfaceGroup
1030
- tr = transforms(model, group)
1031
- ok = tr[:t] && tr[:r]
1032
- t = tr[:t]
1033
- log(FTL, "Can't process '#{id}' transformation (#{mth})") unless ok
1034
- return tbd unless ok
1035
-
1036
- unless shading.empty?
1037
- empty = shading.get.space.empty?
1038
- tr[:r] += shading.get.space.get.directionofRelativeNorth unless empty
1039
- end
1558
+ id = s.nameString
1559
+ group = s.shadingSurfaceGroup
1560
+ log(ERR, "Can't process '#{id}' transformation (#{mth})") if group.empty?
1561
+ next if group.empty?
1562
+
1563
+ group = group.get
1564
+ tr = transforms(group)
1565
+ t = tr[:t] if tr[:t] && tr[:r]
1040
1566
 
1041
- n = trueNormal(s, tr[:r])
1042
- log(FTL, "Can't process '#{id}' true normal (#{mth})") unless n
1043
- return tbd unless n
1567
+ log(FTL, "Can't process '#{id}' transformation (#{mth})") unless t
1568
+ return tbd unless t
1569
+
1570
+ space = group.space
1571
+ tr[:r] += space.get.directionofRelativeNorth unless space.empty?
1572
+ n = trueNormal(s, tr[:r])
1573
+ log(FTL, "Can't process '#{id}' true normal (#{mth})") unless n
1574
+ return tbd unless n
1044
1575
 
1045
1576
  points = (t * s.vertices).map { |v| Topolys::Point3D.new(v.x, v.y, v.z) }
1577
+
1046
1578
  minz = ( points.map { |p| p.z } ).min
1047
- shades[id] = { group: group, points: points, minz: minz, n: n }
1048
- end # shading surfaces populated
1579
+
1580
+ shades[id] = { group: group, points: points, minz: minz, n: n }
1581
+ end
1049
1582
 
1050
1583
  # Mutually populate TBD & Topolys surfaces. Keep track of created "holes".
1051
1584
  holes = {}
1052
- floor_holes = dads(t_model, floors )
1585
+ floor_holes = dads(t_model, floors)
1053
1586
  ceiling_holes = dads(t_model, ceilings)
1054
- wall_holes = dads(t_model, walls )
1587
+ wall_holes = dads(t_model, walls)
1055
1588
 
1056
- holes.merge!(floor_holes )
1589
+ holes.merge!(floor_holes)
1057
1590
  holes.merge!(ceiling_holes)
1058
- holes.merge!(wall_holes )
1591
+ holes.merge!(wall_holes)
1059
1592
  dads(t_model, shades)
1060
1593
 
1061
1594
  # Loop through Topolys edges and populate TBD edge hash. Initially, there
@@ -1063,25 +1596,44 @@ module TBD
1063
1596
  # objects. Use Topolys-generated identifiers as unique edge hash keys.
1064
1597
  edges = {}
1065
1598
 
1066
- holes.each do |id, wire| # start with hole edges
1599
+ # Start with hole edges.
1600
+ holes.each do |id, wire|
1067
1601
  wire.edges.each do |e|
1068
- i = e.id
1069
- l = e.length
1070
- ok = edges.key?(i)
1071
- edges[i] = { length: l, v0: e.v0, v1: e.v1, surfaces: {} } unless ok
1072
- ok = edges[i][:surfaces].key?(wire.attributes[:id])
1073
- edges[i][:surfaces][wire.attributes[:id]] = { wire: wire.id } unless ok
1602
+ i = e.id
1603
+ l = e.length
1604
+ ex = edges.key?(i)
1605
+
1606
+ edges[i] = { length: l, v0: e.v0, v1: e.v1, surfaces: {} } unless ex
1607
+
1608
+ next if edges[i][:surfaces].key?(wire.attributes[:id])
1609
+
1610
+ edges[i][:surfaces][wire.attributes[:id]] = { wire: wire.id }
1074
1611
  end
1075
1612
  end
1076
1613
 
1077
1614
  # Next, floors, ceilings & walls; then shades.
1078
- faces(floors, edges )
1615
+ faces(floors , edges)
1079
1616
  faces(ceilings, edges)
1080
- faces(walls, edges )
1081
- faces(shades, edges )
1617
+ faces(walls , edges)
1618
+ faces(shades , edges)
1619
+
1620
+ # Purge existing KIVA objects from model.
1621
+ if argh[:reset_kiva]
1622
+ kva = false
1623
+ kva = true unless model.getSurfacePropertyExposedFoundationPerimeters.empty?
1624
+ kva = true unless model.getFoundationKivas.empty?
1625
+
1626
+ if kva
1627
+ if argh[:gen_kiva]
1628
+ resetKIVA(model, "Foundation")
1629
+ else
1630
+ resetKIVA(model, "Ground")
1631
+ end
1632
+ end
1633
+ end
1082
1634
 
1083
1635
  # Generate OSM Kiva settings and objects if foundation-facing floors.
1084
- # returns false if partial failure (log failure eventually).
1636
+ # Returns false if partial failure (log failure eventually).
1085
1637
  kiva(model, walls, floors, edges) if argh[:gen_kiva]
1086
1638
 
1087
1639
  # Thermal bridging characteristics of edges are determined - in part - by
@@ -1113,8 +1665,10 @@ module TBD
1113
1665
  vertical = dx < TOL && dy < TOL
1114
1666
  edge_V = terminal - origin
1115
1667
 
1116
- invalid("1x edge length < TOL", mth, 0, ERROR) if edge_V.magnitude < TOL
1117
- next if edge_V.magnitude < TOL
1668
+ if edge_V.magnitude < TOL
1669
+ invalid("1x edge length < TOL", mth, 0, ERROR)
1670
+ next
1671
+ end
1118
1672
 
1119
1673
  edge_plane = Topolys::Plane3D.new(origin, edge_V)
1120
1674
 
@@ -1122,7 +1676,7 @@ module TBD
1122
1676
  reference_V = north.dup
1123
1677
  elsif horizontal
1124
1678
  reference_V = zenith.dup
1125
- else # project zenith vector unto edge plane
1679
+ else # project zenith vector unto edge plane
1126
1680
  reference = edge_plane.project(origin + zenith)
1127
1681
  reference_V = reference - origin
1128
1682
  end
@@ -1131,12 +1685,13 @@ module TBD
1131
1685
  # Loop through each linked wire and determine farthest point from
1132
1686
  # edge while ensuring candidate point is not aligned with edge.
1133
1687
  t_model.wires.each do |wire|
1134
- next unless surface[:wire] == wire.id # should be a unique match
1688
+ next unless surface[:wire] == wire.id # should be a unique match
1689
+
1135
1690
  normal = tbd[:surfaces][id][:n] if tbd[:surfaces].key?(id)
1136
1691
  normal = holes[id].attributes[:n] if holes.key?(id)
1137
1692
  normal = shades[id][:n] if shades.key?(id)
1138
1693
  farthest = Topolys::Point3D.new(origin.x, origin.y, origin.z)
1139
- farthest_V = farthest - origin # zero magnitude, initially
1694
+ farthest_V = farthest - origin # zero magnitude, initially
1140
1695
  inverted = false
1141
1696
  i_origin = wire.points.index(origin)
1142
1697
  i_terminal = wire.points.index(terminal)
@@ -1167,9 +1722,10 @@ module TBD
1167
1722
  plane = Topolys::Plane3D.from_points(origin, terminal, point)
1168
1723
  end
1169
1724
 
1170
- next unless (normal.x - plane.normal.x).abs < TOL &&
1171
- (normal.y - plane.normal.y).abs < TOL &&
1172
- (normal.z - plane.normal.z).abs < TOL
1725
+ dnx = (normal.x - plane.normal.x).abs
1726
+ dny = (normal.y - plane.normal.y).abs
1727
+ dnz = (normal.z - plane.normal.z).abs
1728
+ next unless dnx < TOL && dny < TOL && dnz < TOL
1173
1729
 
1174
1730
  farther = point_V_magnitude > farthest_V.magnitude
1175
1731
  farthest = point if farther
@@ -1180,16 +1736,18 @@ module TBD
1180
1736
  invalid("#{id} polar angle", mth, 0, ERROR, 0) if angle.nil?
1181
1737
  angle = 0 if angle.nil?
1182
1738
 
1183
- adjust = false # adjust angle [180°, 360°] if necessary
1739
+ adjust = false # adjust angle [180°, 360°] if necessary
1184
1740
 
1185
1741
  if vertical
1186
1742
  adjust = true if east.dot(farthest_V) < -TOL
1187
1743
  else
1188
- if north.dot(farthest_V).abs < TOL ||
1189
- (north.dot(farthest_V).abs - 1).abs < TOL
1744
+ dN = north.dot(farthest_V)
1745
+ dN1 = north.dot(farthest_V).abs - 1
1746
+
1747
+ if dN.abs < TOL || dN1.abs < TOL
1190
1748
  adjust = true if east.dot(farthest_V) < -TOL
1191
1749
  else
1192
- adjust = true if north.dot(farthest_V) < -TOL
1750
+ adjust = true if dN < -TOL
1193
1751
  end
1194
1752
  end
1195
1753
 
@@ -1199,13 +1757,13 @@ module TBD
1199
1757
  farthest_V.normalize!
1200
1758
  surface[:polar ] = farthest_V
1201
1759
  surface[:normal] = normal
1202
- end # end of edge-linked, surface-to-wire loop
1203
- end # end of edge-linked surface loop
1760
+ end # end of edge-linked, surface-to-wire loop
1761
+ end # end of edge-linked surface loop
1204
1762
 
1205
1763
  edge[:horizontal] = horizontal
1206
1764
  edge[:vertical ] = vertical
1207
- edge[:surfaces ] = edge[:surfaces].sort_by{ |i, p| p[:angle] }.to_h
1208
- end # end of edge loop
1765
+ edge[:surfaces ] = edge[:surfaces].sort_by{ |_, p| p[:angle] }.to_h
1766
+ end # end of edge loop
1209
1767
 
1210
1768
  # Topolys edges may constitute thermal bridges (and therefore thermally
1211
1769
  # derate linked OpenStudio opaque surfaces), depending on a number of
@@ -1251,23 +1809,27 @@ module TBD
1251
1809
  # EnergyPlus simulation). This is similar to accessing an invalid .osm file.
1252
1810
  return tbd if fatal?
1253
1811
 
1254
- psi = json[:io][:building][:psi] # default building PSI on file
1812
+ psi = json[:io][:building][:psi] # default building PSI on file
1255
1813
  shorts = json[:psi].shorthands(psi)
1256
- empty = shorts[:has].empty? || shorts[:val].empty?
1257
- log(FTL, "Invalid or incomplete building PSI set (#{mth})") if empty
1258
- return tbd if empty
1814
+
1815
+ if shorts[:has].empty? || shorts[:val].empty?
1816
+ log(FTL, "Invalid or incomplete building PSI set (#{mth})")
1817
+ return tbd
1818
+ end
1259
1819
 
1260
1820
  edges.values.each do |edge|
1261
1821
  next unless edge.key?(:surfaces)
1822
+
1262
1823
  deratables = []
1824
+ set = {}
1263
1825
 
1264
1826
  edge[:surfaces].keys.each do |id|
1265
1827
  next unless tbd[:surfaces].key?(id)
1828
+
1266
1829
  deratables << id if tbd[:surfaces][id][:deratable]
1267
1830
  end
1268
1831
 
1269
1832
  next if deratables.empty?
1270
- set = {}
1271
1833
 
1272
1834
  if edge.key?(:io_type)
1273
1835
  bdg = json[:psi].safe(psi, edge[:io_type]) # building safe type fallback
@@ -1290,97 +1852,226 @@ module TBD
1290
1852
  next unless deratables.include?(id)
1291
1853
 
1292
1854
  # Evaluate current set content before processing a new linked surface.
1293
- is = {}
1294
- is[:head ] = set.keys.to_s.include?("head" )
1295
- is[:sill ] = set.keys.to_s.include?("sill" )
1296
- is[:jamb ] = set.keys.to_s.include?("jamb" )
1297
- is[:corner ] = set.keys.to_s.include?("corner" )
1298
- is[:parapet ] = set.keys.to_s.include?("parapet" )
1299
- is[:party ] = set.keys.to_s.include?("party" )
1300
- is[:grade ] = set.keys.to_s.include?("grade" )
1301
- is[:balcony ] = set.keys.to_s.include?("balcony" )
1302
- is[:rimjoist] = set.keys.to_s.include?("rimjoist")
1303
-
1304
- # Label edge as :head, :sill or :jamb if linked to:
1305
- # 1x subsurface
1855
+ is = {}
1856
+ is[:head ] = set.keys.to_s.include?("head")
1857
+ is[:sill ] = set.keys.to_s.include?("sill")
1858
+ is[:jamb ] = set.keys.to_s.include?("jamb")
1859
+ is[:doorhead ] = set.keys.to_s.include?("doorhead")
1860
+ is[:doorsill ] = set.keys.to_s.include?("doorsill")
1861
+ is[:doorjamb ] = set.keys.to_s.include?("doorjamb")
1862
+ is[:skylighthead ] = set.keys.to_s.include?("skylighthead")
1863
+ is[:skylightsill ] = set.keys.to_s.include?("skylightsill")
1864
+ is[:skylightjamb ] = set.keys.to_s.include?("skylightjamb")
1865
+ is[:spandrel ] = set.keys.to_s.include?("spandrel")
1866
+ is[:corner ] = set.keys.to_s.include?("corner")
1867
+ is[:parapet ] = set.keys.to_s.include?("parapet")
1868
+ is[:roof ] = set.keys.to_s.include?("roof")
1869
+ is[:party ] = set.keys.to_s.include?("party")
1870
+ is[:grade ] = set.keys.to_s.include?("grade")
1871
+ is[:balcony ] = set.keys.to_s.include?("balcony")
1872
+ is[:balconysill ] = set.keys.to_s.include?("balconysill")
1873
+ is[:balconydoorsill ] = set.keys.to_s.include?("balconydoorsill")
1874
+ is[:rimjoist ] = set.keys.to_s.include?("rimjoist")
1875
+
1876
+ # Label edge as ...
1877
+ # :head, :sill, :jamb (vertical fenestration)
1878
+ # :doorhead, :doorsill, :doorjamb (opaque door)
1879
+ # :skylighthead, :skylightsill, :skylightjamb (all other cases)
1880
+ #
1881
+ # ... if linked to:
1882
+ # 1x subsurface (vertical or non-vertical)
1306
1883
  edge[:surfaces].keys.each do |i|
1307
- break if is[:head] || is[:sill] || is[:jamb]
1884
+ break if is[:head ]
1885
+ break if is[:sill ]
1886
+ break if is[:jamb ]
1887
+ break if is[:doorhead ]
1888
+ break if is[:doorsill ]
1889
+ break if is[:doorjamb ]
1890
+ break if is[:skylighthead]
1891
+ break if is[:skylightsill]
1892
+ break if is[:skylightjamb]
1308
1893
  next if deratables.include?(i)
1309
1894
  next unless holes.key?(i)
1310
1895
 
1311
- gardian = ""
1312
- gardian = id if deratables.size == 1 # just dad
1313
-
1314
- if gardian.empty? # seek uncle
1315
- pops = {} # kids?
1316
- uncles = {} # nieces?
1317
- boys = [] # kids
1318
- nieces = [] # nieces
1319
- uncle = deratables.first unless deratables.first == id # uncle 1st?
1320
- uncle = deratables.last unless deratables.last == id # uncle 2nd?
1321
-
1322
- pops[:w ] = tbd[:surfaces][id ].key?(:windows )
1323
- pops[:d ] = tbd[:surfaces][id ].key?(:doors )
1324
- pops[:s ] = tbd[:surfaces][id ].key?(:skylights)
1325
- uncles[:w] = tbd[:surfaces][uncle].key?(:windows )
1326
- uncles[:d] = tbd[:surfaces][uncle].key?(:doors )
1327
- uncles[:s] = tbd[:surfaces][uncle].key?(:skylights)
1328
-
1329
- boys += tbd[:surfaces][id ][:windows ].keys if pops[:w]
1330
- boys += tbd[:surfaces][id ][:doors ].keys if pops[:d]
1331
- boys += tbd[:surfaces][id ][:skylights].keys if pops[:s]
1332
- nieces += tbd[:surfaces][uncle][:windows ].keys if uncles[:w]
1333
- nieces += tbd[:surfaces][uncle][:doors ].keys if uncles[:d]
1334
- nieces += tbd[:surfaces][uncle][:skylights].keys if uncles[:s]
1335
-
1336
- gardian = uncle if boys.include?(i)
1337
- gardian = id if nieces.include?(i)
1896
+ # In most cases, subsurface edges simply delineate the rough opening
1897
+ # of its base surface (here, a "gardian"). Door sills, corner windows,
1898
+ # as well as a subsurface header aligned with a plenum "floor"
1899
+ # (ceiling tiles), are common instances where a subsurface edge links
1900
+ # 2x (opaque) surfaces. Deratable surface "id" may not be the gardian
1901
+ # of subsurface "i" - the latter may be a neighbour. The single
1902
+ # surface to derate is not the gardian in such cases.
1903
+ gardian = deratables.size == 1 ? id : ""
1904
+ target = gardian
1905
+
1906
+ # Retrieve base surface's subsurfaces.
1907
+ windows = tbd[:surfaces][id].key?(:windows)
1908
+ doors = tbd[:surfaces][id].key?(:doors)
1909
+ skylights = tbd[:surfaces][id].key?(:skylights)
1910
+
1911
+ windows = windows ? tbd[:surfaces][id][:windows ] : {}
1912
+ doors = doors ? tbd[:surfaces][id][:doors ] : {}
1913
+ skylights = skylights ? tbd[:surfaces][id][:skylights] : {}
1914
+
1915
+ # The gardian is "id" if subsurface "ids" holds "i".
1916
+ ids = windows.keys + doors.keys + skylights.keys
1917
+
1918
+ if gardian.empty?
1919
+ other = deratables.first == id ? deratables.last : deratables.first
1920
+
1921
+ gardian = ids.include?(i) ? id : other
1922
+ target = ids.include?(i) ? other : id
1923
+
1924
+ windows = tbd[:surfaces][gardian].key?(:windows)
1925
+ doors = tbd[:surfaces][gardian].key?(:doors)
1926
+ skylights = tbd[:surfaces][gardian].key?(:skylights)
1927
+
1928
+ windows = windows ? tbd[:surfaces][gardian][:windows ] : {}
1929
+ doors = doors ? tbd[:surfaces][gardian][:doors ] : {}
1930
+ skylights = skylights ? tbd[:surfaces][gardian][:skylights] : {}
1931
+
1932
+ ids = windows.keys + doors.keys + skylights.keys
1338
1933
  end
1339
1934
 
1340
- next if gardian.empty?
1341
- s1 = edge[:surfaces][gardian]
1342
- s2 = edge[:surfaces][i]
1935
+ unless ids.include?(i)
1936
+ log(ERR, "Orphaned subsurface #{i} (mth)")
1937
+ next
1938
+ end
1939
+
1940
+ window = windows.key?(i) ? windows[i] : {}
1941
+ door = doors.key?(i) ? doors[i] : {}
1942
+ skylight = skylights.key?(i) ? skylights[i] : {}
1943
+
1944
+ sub = window unless window.empty?
1945
+ sub = door unless door.empty?
1946
+ sub = skylight unless skylight.empty?
1947
+
1948
+ window = sub[:type] == :window
1949
+ door = sub[:type] == :door
1950
+ glazed = door && sub.key?(:glazed) && sub[:glazed]
1951
+
1952
+ s1 = edge[:surfaces][target]
1953
+ s2 = edge[:surfaces][i ]
1343
1954
  concave = concave?(s1, s2)
1344
1955
  convex = convex?(s1, s2)
1345
1956
  flat = !concave && !convex
1346
1957
 
1347
- # Subsurface edges are tagged as :head, :sill or :jamb, regardless
1348
- # of building PSI set subsurface tags. If the latter is simply
1349
- # :fenestration, then its (single) PSI value is systematically
1350
- # attributed to subsurface :head, :sill & :jamb edges. If absent,
1351
- # concave or convex variants also inherit from base type.
1958
+ # Subsurface edges are tagged as head, sill or jamb, regardless of
1959
+ # building PSI set subsurface-related tags. If the latter is simply
1960
+ # :fenestration, then its single PSI factor is systematically
1961
+ # assigned to e.g. a window's :head, :sill & :jamb edges.
1352
1962
  #
1353
- # TBD tags a subsurface edge as :jamb if the subsurface is "flat".
1354
- # If not flat, TBD tags a horizontal edge as either :head or :sill
1355
- # based on the polar angle of the subsurface around the edge vs sky
1356
- # zenith. Otherwise, all other subsurface edges are tagged as :jamb.
1357
- if ((s2[:normal].dot(zenith)).abs - 1).abs < TOL
1358
- set[:jamb ] = shorts[:val][:jamb ] if flat
1359
- set[:jambconcave] = shorts[:val][:jambconcave] if concave
1360
- set[:jambconvex ] = shorts[:val][:jambconvex ] if convex
1361
- is[:jamb ] = true
1362
- else
1363
- if edge[:horizontal]
1364
- if s2[:polar].dot(zenith) < 0
1365
- set[:head ] = shorts[:val][:head ] if flat
1366
- set[:headconcave] = shorts[:val][:headconcave] if concave
1367
- set[:headconvex ] = shorts[:val][:headconvex ] if convex
1368
- is[:head ] = true
1369
- else
1370
- set[:sill ] = shorts[:val][:sill ] if flat
1371
- set[:sillconcave] = shorts[:val][:sillconcave] if concave
1372
- set[:sillconvex ] = shorts[:val][:sillconvex ] if convex
1373
- is[:sill ] = true
1374
- end
1375
- else
1963
+ # Additionally, concave or convex variants also inherit from the base
1964
+ # type if undefined in the PSI set.
1965
+ #
1966
+ # If a subsurface is not horizontal, TBD tags any horizontal edge as
1967
+ # either :head or :sill based on the polar angle of the subsurface
1968
+ # around the edge vs sky zenith. Otherwise, all other subsurface edges
1969
+ # are tagged as :jamb.
1970
+ if ((s2[:normal].dot(zenith)).abs - 1).abs < TOL # horizontal surface
1971
+ if glazed || window
1376
1972
  set[:jamb ] = shorts[:val][:jamb ] if flat
1377
1973
  set[:jambconcave] = shorts[:val][:jambconcave] if concave
1378
1974
  set[:jambconvex ] = shorts[:val][:jambconvex ] if convex
1379
1975
  is[:jamb ] = true
1976
+ elsif door
1977
+ set[:doorjamb ] = shorts[:val][:doorjamb ] if flat
1978
+ set[:doorjambconcave] = shorts[:val][:doorjambconcave] if concave
1979
+ set[:doorjambconvex ] = shorts[:val][:doorjambconvex ] if convex
1980
+ is[:doorjamb ] = true
1981
+ else
1982
+ set[:skylightjamb ] = shorts[:val][:skylightjamb ] if flat
1983
+ set[:skylightjambconcave] = shorts[:val][:skylightjambconcave] if concave
1984
+ set[:skylightjambconvex ] = shorts[:val][:skylightjambconvex ] if convex
1985
+ is[:skylightjamb ] = true
1986
+ end
1987
+ else
1988
+ if glazed || window
1989
+ if edge[:horizontal]
1990
+ if s2[:polar].dot(zenith) < 0
1991
+ set[:head ] = shorts[:val][:head ] if flat
1992
+ set[:headconcave] = shorts[:val][:headconcave] if concave
1993
+ set[:headconvex ] = shorts[:val][:headconvex ] if convex
1994
+ is[:head ] = true
1995
+ else
1996
+ set[:sill ] = shorts[:val][:sill ] if flat
1997
+ set[:sillconcave] = shorts[:val][:sillconcave] if concave
1998
+ set[:sillconvex ] = shorts[:val][:sillconvex ] if convex
1999
+ is[:sill ] = true
2000
+ end
2001
+ else
2002
+ set[:jamb ] = shorts[:val][:jamb ] if flat
2003
+ set[:jambconcave] = shorts[:val][:jambconcave] if concave
2004
+ set[:jambconvex ] = shorts[:val][:jambconvex ] if convex
2005
+ is[:jamb ] = true
2006
+ end
2007
+ elsif door
2008
+ if edge[:horizontal]
2009
+ if s2[:polar].dot(zenith) < 0
2010
+
2011
+ set[:doorhead ] = shorts[:val][:doorhead ] if flat
2012
+ set[:doorheadconcave] = shorts[:val][:doorheadconcave] if concave
2013
+ set[:doorheadconvex ] = shorts[:val][:doorheadconvex ] if convex
2014
+ is[:doorhead ] = true
2015
+ else
2016
+ set[:doorsill ] = shorts[:val][:doorsill ] if flat
2017
+ set[:doorsillconcave] = shorts[:val][:doorsillconcave] if concave
2018
+ set[:doorsillconvex ] = shorts[:val][:doorsillconvex ] if convex
2019
+ is[:doorsill ] = true
2020
+ end
2021
+ else
2022
+ set[:doorjamb ] = shorts[:val][:doorjamb ] if flat
2023
+ set[:doorjambconcave] = shorts[:val][:doorjambconcave] if concave
2024
+ set[:doorjambconvex ] = shorts[:val][:doorjambconvex ] if convex
2025
+ is[:doorjamb ] = true
2026
+ end
2027
+ else
2028
+ if edge[:horizontal]
2029
+ if s2[:polar].dot(zenith) < 0
2030
+ set[:skylighthead ] = shorts[:val][:skylighthead ] if flat
2031
+ set[:skylightheadconcave] = shorts[:val][:skylightheadconcave] if concave
2032
+ set[:skylightheadconvex ] = shorts[:val][:skylightheadconvex ] if convex
2033
+ is[:skylighthead ] = true
2034
+ else
2035
+ set[:skylightsill ] = shorts[:val][:skylightsill ] if flat
2036
+ set[:skylightsillconcave] = shorts[:val][:skylightsillconcave] if concave
2037
+ set[:skylightsillconvex ] = shorts[:val][:skylightsillconvex ] if convex
2038
+ is[:skylightsill ] = true
2039
+ end
2040
+ else
2041
+ set[:skylightjamb ] = shorts[:val][:skylightjamb ] if flat
2042
+ set[:skylightjambconcave] = shorts[:val][:skylightjambconcave] if concave
2043
+ set[:skylightjambconvex ] = shorts[:val][:skylightjambconvex ] if convex
2044
+ is[:skylightjamb ] = true
2045
+ end
1380
2046
  end
1381
2047
  end
1382
2048
  end
1383
2049
 
2050
+ # Label edge as :spandrel if linked to:
2051
+ # 1x deratable, non-spandrel wall
2052
+ # 1x deratable, spandrel wall
2053
+ edge[:surfaces].keys.each do |i|
2054
+ break if is[:spandrel]
2055
+ break unless deratables.size == 2
2056
+ break unless walls.key?(id)
2057
+ break unless walls[id][:spandrel]
2058
+ next if i == id
2059
+ next unless deratables.include?(i)
2060
+ next unless walls.key?(i)
2061
+ next if walls[i][:spandrel]
2062
+
2063
+ s1 = edge[:surfaces][id]
2064
+ s2 = edge[:surfaces][i ]
2065
+ concave = concave?(s1, s2)
2066
+ convex = convex?(s1, s2)
2067
+ flat = !concave && !convex
2068
+
2069
+ set[:spandrel ] = shorts[:val][:spandrel ] if flat
2070
+ set[:spandrelconcave] = shorts[:val][:spandrelconcave] if concave
2071
+ set[:spandrelconvex ] = shorts[:val][:spandrelconvex ] if convex
2072
+ is[:spandrel ] = true
2073
+ end
2074
+
1384
2075
  # Label edge as :cornerconcave or :cornerconvex if linked to:
1385
2076
  # 2x deratable walls & f(relative polar wall vectors around edge)
1386
2077
  edge[:surfaces].keys.each do |i|
@@ -1388,8 +2079,8 @@ module TBD
1388
2079
  break unless deratables.size == 2
1389
2080
  break unless walls.key?(id)
1390
2081
  next if i == id
1391
- next unless deratables.include?(i)
1392
- next unless walls.key?(i)
2082
+ next unless deratables.include?(i)
2083
+ next unless walls.key?(i)
1393
2084
 
1394
2085
  s1 = edge[:surfaces][id]
1395
2086
  s2 = edge[:surfaces][i]
@@ -1401,11 +2092,12 @@ module TBD
1401
2092
  is[:corner ] = true
1402
2093
  end
1403
2094
 
1404
- # Label edge as :parapet if linked to:
2095
+ # Label edge as :parapet/:roof if linked to:
1405
2096
  # 1x deratable wall
1406
2097
  # 1x deratable ceiling
1407
2098
  edge[:surfaces].keys.each do |i|
1408
2099
  break if is[:parapet]
2100
+ break if is[:roof ]
1409
2101
  break unless deratables.size == 2
1410
2102
  break unless ceilings.key?(id)
1411
2103
  next if i == id
@@ -1413,15 +2105,22 @@ module TBD
1413
2105
  next unless walls.key?(i)
1414
2106
 
1415
2107
  s1 = edge[:surfaces][id]
1416
- s2 = edge[:surfaces][i]
2108
+ s2 = edge[:surfaces][i ]
1417
2109
  concave = concave?(s1, s2)
1418
2110
  convex = convex?(s1, s2)
1419
2111
  flat = !concave && !convex
1420
2112
 
1421
- set[:parapet ] = shorts[:val][:parapet ] if flat
1422
- set[:parapetconcave] = shorts[:val][:parapetconcave] if concave
1423
- set[:parapetconvex ] = shorts[:val][:parapetconvex ] if convex
1424
- is[:parapet ] = true
2113
+ if argh[:parapet]
2114
+ set[:parapet ] = shorts[:val][:parapet ] if flat
2115
+ set[:parapetconcave] = shorts[:val][:parapetconcave] if concave
2116
+ set[:parapetconvex ] = shorts[:val][:parapetconvex ] if convex
2117
+ is[:parapet ] = true
2118
+ else
2119
+ set[:roof ] = shorts[:val][:roof ] if flat
2120
+ set[:roofconcave] = shorts[:val][:roofconcave] if concave
2121
+ set[:roofconvex ] = shorts[:val][:roofconvex ] if convex
2122
+ is[:roof ] = true
2123
+ end
1425
2124
  end
1426
2125
 
1427
2126
  # Label edge as :party if linked to:
@@ -1439,7 +2138,7 @@ module TBD
1439
2138
  next unless facing == "othersidecoefficients"
1440
2139
 
1441
2140
  s1 = edge[:surfaces][id]
1442
- s2 = edge[:surfaces][i]
2141
+ s2 = edge[:surfaces][i ]
1443
2142
  concave = concave?(s1, s2)
1444
2143
  convex = convex?(s1, s2)
1445
2144
  flat = !concave && !convex
@@ -1473,30 +2172,106 @@ module TBD
1473
2172
  is[:grade ] = true
1474
2173
  end
1475
2174
 
1476
- # Label edge as :rimjoist (or :balcony) if linked to:
2175
+ # Label edge as :rimjoist, :balcony, :balconysill or :balconydoorsill if linked to:
1477
2176
  # 1x deratable surface
1478
2177
  # 1x CONDITIONED floor
1479
2178
  # 1x shade (optional)
1480
- balcony = false
2179
+ # 1x subsurface (optional)
2180
+ #
2181
+ # Despite referring to 'sill' or 'doorsill', a 'balconysill' or
2182
+ # 'balconydoorsill' edge may instead link (rarer) cases of balcony and a
2183
+ # fenestratio/door head. ASHRAE 90.1 2022 does not make the distinction
2184
+ # between sill vs head when intermediatre floor, balcony and vertical
2185
+ # fenestration meet. 'Sills' are simply the most common occurrence.
2186
+ balcony = false
2187
+ balconysill = false # vertical fenestration
2188
+ balconydoorsill = false # opaque door
1481
2189
 
1482
2190
  edge[:surfaces].keys.each do |i|
1483
- break if balcony
1484
- next if i == id
1485
- balcony = true if shades.key?(i)
2191
+ break if balcony
2192
+ next if i == id
2193
+
2194
+ balcony = shades.key?(i)
1486
2195
  end
1487
2196
 
1488
2197
  edge[:surfaces].keys.each do |i|
1489
- break if is[:rimjoist] || is[:balcony]
2198
+ break unless balcony
2199
+ break if balconysill
2200
+ break if balconydoorsill
2201
+ next if i == id
2202
+ next unless holes.key?(i)
2203
+
2204
+ # Deratable surface "id" may not be the gardian of "i" (see sills).
2205
+ gardian = deratables.size == 1 ? id : ""
2206
+ target = gardian
2207
+
2208
+ # Retrieve base surface's subsurfaces.
2209
+ windows = tbd[:surfaces][id].key?(:windows)
2210
+ doors = tbd[:surfaces][id].key?(:doors)
2211
+ skylights = tbd[:surfaces][id].key?(:skylights)
2212
+
2213
+ windows = windows ? tbd[:surfaces][id][:windows ] : {}
2214
+ doors = doors ? tbd[:surfaces][id][:doors ] : {}
2215
+ skylights = skylights ? tbd[:surfaces][id][:skylights] : {}
2216
+
2217
+ # The gardian is "id" if subsurface "ids" holds "i".
2218
+ ids = windows.keys + doors.keys + skylights.keys
2219
+
2220
+ if gardian.empty?
2221
+ other = deratables.first == id ? deratables.last : deratables.first
2222
+
2223
+ gardian = ids.include?(i) ? id : other
2224
+ target = ids.include?(i) ? other : id
2225
+
2226
+ windows = tbd[:surfaces][gardian].key?(:windows)
2227
+ doors = tbd[:surfaces][gardian].key?(:doors)
2228
+ skylights = tbd[:surfaces][gardian].key?(:skylights)
2229
+
2230
+ windows = windows ? tbd[:surfaces][gardian][:windows ] : {}
2231
+ doors = doors ? tbd[:surfaces][gardian][:doors ] : {}
2232
+ skylights = skylights ? tbd[:surfaces][gardian][:skylights] : {}
2233
+
2234
+ ids = windows.keys + doors.keys + skylights.keys
2235
+ end
2236
+
2237
+ unless ids.include?(i)
2238
+ log(ERR, "Balcony sill: orphaned subsurface #{i} (mth)")
2239
+ next
2240
+ end
2241
+
2242
+ window = windows.key?(i) ? windows[i] : {}
2243
+ door = doors.key?(i) ? doors[i] : {}
2244
+ skylight = skylights.key?(i) ? skylights[i] : {}
2245
+
2246
+ sub = window unless window.empty?
2247
+ sub = door unless door.empty?
2248
+ sub = skylight unless skylight.empty?
2249
+
2250
+ window = sub[:type] == :window
2251
+ door = sub[:type] == :door
2252
+ glazed = door && sub.key?(:glazed) && sub[:glazed]
2253
+
2254
+ if window || glazed
2255
+ balconysill = true
2256
+ elsif door
2257
+ balconydoorsill = true
2258
+ end
2259
+ end
2260
+
2261
+ edge[:surfaces].keys.each do |i|
2262
+ break if is[:rimjoist ] || is[:balcony ] ||
2263
+ is[:balconysill] || is[:balconydoorsill]
1490
2264
  break unless deratables.size > 0
1491
2265
  break if floors.key?(id)
1492
2266
  next if i == id
1493
2267
  next unless floors.key?(i)
1494
2268
  next unless floors[i].key?(:conditioned)
1495
2269
  next unless floors[i][:conditioned]
1496
- next if floors[i][:ground]
2270
+ next if floors[i][:ground ]
1497
2271
 
1498
2272
  other = deratables.first unless deratables.first == id
1499
2273
  other = deratables.last unless deratables.last == id
2274
+ other = id if deratables.size == 1
1500
2275
 
1501
2276
  s1 = edge[:surfaces][id]
1502
2277
  s2 = edge[:surfaces][other]
@@ -1504,38 +2279,50 @@ module TBD
1504
2279
  convex = convex?(s1, s2)
1505
2280
  flat = !concave && !convex
1506
2281
 
1507
- if balcony
1508
- set[:balcony ] = shorts[:val][:balcony ] if flat
1509
- set[:balconyconcave ] = shorts[:val][:balconyconcave ] if concave
1510
- set[:balconyconvex ] = shorts[:val][:balconyconvex ] if convex
1511
- is[:balcony ] = true
2282
+ if balconydoorsill
2283
+ set[:balconydoorsill ] = shorts[:val][:balconydoorsill ] if flat
2284
+ set[:balconydoorsillconcave] = shorts[:val][:balconydoorsillconcave] if concave
2285
+ set[:balconydoorsillconvex ] = shorts[:val][:balconydoorsillconvex ] if convex
2286
+ is[:balconydoorsill ] = true
2287
+ elsif balconysill
2288
+ set[:balconysill ] = shorts[:val][:balconysill ] if flat
2289
+ set[:balconysillconcave ] = shorts[:val][:balconysillconcave ] if concave
2290
+ set[:balconysillconvex ] = shorts[:val][:balconysillconvex ] if convex
2291
+ is[:balconysill ] = true
2292
+ elsif balcony
2293
+ set[:balcony ] = shorts[:val][:balcony ] if flat
2294
+ set[:balconyconcave ] = shorts[:val][:balconyconcave ] if concave
2295
+ set[:balconyconvex ] = shorts[:val][:balconyconvex ] if convex
2296
+ is[:balcony ] = true
1512
2297
  else
1513
- set[:rimjoist ] = shorts[:val][:rimjoist ] if flat
1514
- set[:rimjoistconcave] = shorts[:val][:rimjoistconcave] if concave
1515
- set[:rimjoistconvex ] = shorts[:val][:rimjoistconvex ] if convex
1516
- is[:rimjoist ] = true
2298
+ set[:rimjoist ] = shorts[:val][:rimjoist ] if flat
2299
+ set[:rimjoistconcave ] = shorts[:val][:rimjoistconcave ] if concave
2300
+ set[:rimjoistconvex ] = shorts[:val][:rimjoistconvex ] if convex
2301
+ is[:rimjoist ] = true
1517
2302
  end
1518
2303
  end
1519
- end # edge's surfaces loop
2304
+ end # edge's surfaces loop
1520
2305
 
1521
2306
  edge[:psi] = set unless set.empty?
1522
2307
  edge[:set] = psi unless set.empty?
1523
- end # edge loop
2308
+ end # edge loop
1524
2309
 
1525
2310
  # Tracking (mild) transitions between deratable surfaces around edges that
1526
2311
  # have not been previously tagged.
1527
2312
  edges.values.each do |edge|
2313
+ deratable = false
1528
2314
  next if edge.key?(:psi)
1529
2315
  next unless edge.key?(:surfaces)
1530
- deratable = false
1531
2316
 
1532
2317
  edge[:surfaces].keys.each do |id|
1533
2318
  next unless tbd[:surfaces].key?(id)
1534
2319
  next unless tbd[:surfaces][id][:deratable]
2320
+
1535
2321
  deratable = tbd[:surfaces][id][:deratable]
1536
2322
  end
1537
2323
 
1538
2324
  next unless deratable
2325
+
1539
2326
  edge[:psi] = { transition: 0.000 }
1540
2327
  edge[:set] = json[:io][:building][:psi]
1541
2328
  end
@@ -1547,6 +2334,7 @@ module TBD
1547
2334
  next if edge.key?(:psi)
1548
2335
  next unless edge.key?(:surfaces)
1549
2336
  next unless edge[:surfaces].size == 1
2337
+
1550
2338
  id = edge[:surfaces].first.first
1551
2339
  next unless holes.key?(id)
1552
2340
  next unless holes[id].attributes.key?(:unhinged)
@@ -1554,9 +2342,11 @@ module TBD
1554
2342
 
1555
2343
  subsurface = model.getSubSurfaceByName(id)
1556
2344
  next if subsurface.empty?
2345
+
1557
2346
  subsurface = subsurface.get
1558
2347
  surface = subsurface.surface
1559
2348
  next if surface.empty?
2349
+
1560
2350
  nom = surface.get.nameString
1561
2351
  next unless tbd[:surfaces].key?(nom)
1562
2352
  next unless tbd[:surfaces][nom].key?(:conditioned)
@@ -1570,90 +2360,197 @@ module TBD
1570
2360
  edge[:set] = json[:io][:building][:psi]
1571
2361
  end
1572
2362
 
1573
- # A priori, TBD applies (default) :building PSI types and values to
1574
- # individual edges. If a TBD JSON input file holds custom PSI sets for:
1575
- # :stories
1576
- # :spacetypes
1577
- # :surfaces
1578
- # :edges
1579
- # ... that may apply to individual edges, then the default :building PSI
1580
- # types and/or values are overridden, as follows:
1581
- # custom :stories PSI sets trump :building PSI sets
1582
- # custom :spacetypes PSI sets trump aforementioned PSI sets
1583
- # custom :spaces PSI sets trump aforementioned PSI sets
1584
- # custom :surfaces PSI sets trump aforementioned PSI sets
1585
- # custom :edges PSI sets trump aforementioned PSI sets
1586
2363
  if json[:io]
1587
- if json[:io].key?(:subsurfaces) # reset subsurface U-factors (if on file)
2364
+ # Reset subsurface U-factors (if on file).
2365
+ if json[:io].key?(:subsurfaces)
1588
2366
  json[:io][:subsurfaces].each do |sub|
2367
+ match = false
1589
2368
  next unless sub.key?(:id)
1590
2369
  next unless sub.key?(:usi)
1591
- match = false
1592
2370
 
1593
2371
  tbd[:surfaces].values.each do |surface|
1594
2372
  break if match
1595
2373
 
1596
2374
  [:windows, :doors, :skylights].each do |types|
1597
- if surface.key?(types)
1598
- surface[types].each do |id, opening|
1599
- break if match
1600
- next unless opening.key?(:u)
1601
- match = true if sub[:id] == id
1602
- opening[:u] = sub[:usi] if sub[:id] == id
1603
- end
2375
+ break if match
2376
+ next unless surface.key?(types)
2377
+
2378
+ surface[types].each do |id, opening|
2379
+ break if match
2380
+ next unless opening.key?(:u)
2381
+ next unless sub[:id] == id
2382
+
2383
+ opening[:u] = sub[:usi]
2384
+ match = true
1604
2385
  end
1605
2386
  end
1606
2387
  end
1607
2388
  end
1608
2389
  end
1609
2390
 
2391
+ # Reset wall-to-roof intersection type (if on file) ... per group.
2392
+ [:stories, :spacetypes, :spaces].each do |groups|
2393
+ key = :story
2394
+ key = :stype if groups == :spacetypes
2395
+ key = :space if groups == :spaces
2396
+ next unless json[:io].key?(groups)
2397
+
2398
+ json[:io][groups].each do |group|
2399
+ next unless group.key?(:id)
2400
+ next unless group.key?(:parapet)
2401
+
2402
+ edges.values.each do |edge|
2403
+ match = false
2404
+ next unless edge.key?(:psi)
2405
+ next unless edge.key?(:surfaces)
2406
+ next if edge.key?(:io_type)
2407
+
2408
+ edge[:surfaces].keys.each do |id|
2409
+ break if match
2410
+ next unless tbd[:surfaces].key?(id)
2411
+ next unless tbd[:surfaces][id].key?(key)
2412
+
2413
+ match = group[:id] == tbd[:surfaces][id][key].nameString
2414
+ end
2415
+
2416
+ next unless match
2417
+
2418
+ parapets = edge[:psi].keys.select {|ty| ty.to_s.include?("parapet")}
2419
+ roofs = edge[:psi].keys.select {|ty| ty.to_s.include?("roof")}
2420
+
2421
+ if group[:parapet]
2422
+ next unless parapets.empty?
2423
+ next if roofs.empty?
2424
+
2425
+ type = :parapet
2426
+ type = :parapetconcave if roofs.first.to_s.include?("concave")
2427
+ type = :parapetconvex if roofs.first.to_s.include?("convex")
2428
+
2429
+ edge[:psi][type] = shorts[:val][type]
2430
+ roofs.each {|ty| edge[:psi].delete(ty)}
2431
+ else
2432
+ next unless roofs.empty?
2433
+ next if parapets.empty?
2434
+
2435
+ type = :roof
2436
+ type = :roofconcave if parapets.first.to_s.include?("concave")
2437
+ type = :roofconvex if parapets.first.to_s.include?("convex")
2438
+
2439
+ edge[:psi][type] = shorts[:val][type]
2440
+
2441
+ parapets.each { |ty| edge[:psi].delete(ty) }
2442
+ end
2443
+ end
2444
+ end
2445
+ end
2446
+
2447
+ # Reset wall-to-roof intersection type (if on file) - individual surfaces.
2448
+ if json[:io].key?(:surfaces)
2449
+ json[:io][:surfaces].each do |surface|
2450
+ next unless surface.key?(:parapet)
2451
+ next unless surface.key?(:id)
2452
+
2453
+ edges.values.each do |edge|
2454
+ next if edge.key?(:io_type)
2455
+ next unless edge.key?(:psi)
2456
+ next unless edge.key?(:surfaces)
2457
+ next unless edge[:surfaces].keys.include?(surface[:id])
2458
+
2459
+ parapets = edge[:psi].keys.select {|ty| ty.to_s.include?("parapet")}
2460
+ roofs = edge[:psi].keys.select {|ty| ty.to_s.include?("roof")}
2461
+
2462
+
2463
+ if surface[:parapet]
2464
+ next unless parapets.empty?
2465
+ next if roofs.empty?
2466
+
2467
+ type = :parapet
2468
+ type = :parapetconcave if roofs.first.to_s.include?("concave")
2469
+ type = :parapetconvex if roofs.first.to_s.include?("convex")
2470
+
2471
+ edge[:psi][type] = shorts[:val][type]
2472
+ roofs.each {|ty| edge[:psi].delete(ty)}
2473
+ else
2474
+ next unless roofs.empty?
2475
+ next if parapets.empty?
2476
+
2477
+ type = :roof
2478
+ type = :roofconcave if parapets.first.to_s.include?("concave")
2479
+ type = :roofconvex if parapets.first.to_s.include?("convex")
2480
+
2481
+ edge[:psi][type] = shorts[:val][type]
2482
+ parapets.each {|ty| edge[:psi].delete(ty)}
2483
+ end
2484
+ end
2485
+ end
2486
+ end
2487
+
2488
+ # A priori, TBD applies (default) :building PSI types and values to
2489
+ # individual edges. If a TBD JSON input file holds custom PSI sets for:
2490
+ # :stories
2491
+ # :spacetypes
2492
+ # :surfaces
2493
+ # :edges
2494
+ # ... that may apply to individual edges, then the default :building PSI
2495
+ # types and/or values are overridden, as follows:
2496
+ # custom :stories PSI sets trump :building PSI sets
2497
+ # custom :spacetypes PSI sets trump aforementioned PSI sets
2498
+ # custom :spaces PSI sets trump aforementioned PSI sets
2499
+ # custom :surfaces PSI sets trump aforementioned PSI sets
2500
+ # custom :edges PSI sets trump aforementioned PSI sets
1610
2501
  [:stories, :spacetypes, :spaces].each do |groups|
1611
2502
  key = :story
1612
2503
  key = :stype if groups == :spacetypes
1613
2504
  key = :space if groups == :spaces
1614
- next unless json[:io].key?(groups)
2505
+ next unless json[:io].key?(groups)
1615
2506
 
1616
2507
  json[:io][groups].each do |group|
1617
2508
  next unless group.key?(:id)
1618
2509
  next unless group.key?(:psi)
1619
2510
  next unless json[:psi].set.key?(group[:psi])
1620
- sh = json[:psi].shorthands(group[:psi])
1621
- next if sh[:val].empty?
2511
+
2512
+ sh = json[:psi].shorthands(group[:psi])
2513
+ next if sh[:val].empty?
1622
2514
 
1623
2515
  edges.values.each do |edge|
1624
- next if edge.key?(:io_set)
2516
+ match = false
1625
2517
  next unless edge.key?(:psi)
1626
2518
  next unless edge.key?(:surfaces)
2519
+ next if edge.key?(:io_set)
1627
2520
 
1628
2521
  edge[:surfaces].keys.each do |id|
2522
+ break if match
1629
2523
  next unless tbd[:surfaces].key?(id)
1630
2524
  next unless tbd[:surfaces][id].key?(key)
1631
- next unless group[:id] == tbd[:surfaces][id][key].nameString
1632
2525
 
1633
- edge[groups] = {} unless edge.key?(groups)
1634
- edge[groups][group[:psi]] = {}
1635
- set = {}
2526
+ match = group[:id] == tbd[:surfaces][id][key].nameString
2527
+ end
1636
2528
 
1637
- if edge.key?(:io_type)
1638
- safer = json[:psi].safe(group[:psi], edge[:io_type])
1639
- set[edge[:io_type]] = sh[:val][safer] if safer
1640
- else
1641
- edge[:psi].keys.each do |type|
1642
- safer = json[:psi].safe(group[:psi], type)
1643
- set[type] = sh[:val][safer] if safer
1644
- end
1645
- end
2529
+ next unless match
2530
+
2531
+ set = {}
2532
+ edge[groups] = {} unless edge.key?(groups)
2533
+ edge[groups][group[:psi]] = {}
1646
2534
 
1647
- edge[groups][group[:psi]] = set unless set.empty?
2535
+ if edge.key?(:io_type)
2536
+ safer = json[:psi].safe(group[:psi], edge[:io_type])
2537
+ set[edge[:io_type]] = sh[:val][safer] if safer
2538
+ else
2539
+ edge[:psi].keys.each do |type|
2540
+ safer = json[:psi].safe(group[:psi], type)
2541
+ set[type] = sh[:val][safer] if safer
2542
+ end
1648
2543
  end
2544
+
2545
+ edge[groups][group[:psi]] = set unless set.empty?
1649
2546
  end
1650
2547
  end
1651
2548
 
1652
2549
  # TBD/Topolys edges will generally be linked to more than one surface
1653
- # and hence to more than one story. It is possible for a TBD JSON file
1654
- # to hold 2x story PSI sets that end up targetting one or more edges
1655
- # common to both stories. In such cases, TBD retains the most conductive
1656
- # PSI type/value from either story PSI set.
2550
+ # and hence to more than one group. It is possible for a TBD JSON file
2551
+ # to hold 2x group PSI sets that end up targetting one or more edges
2552
+ # common to both groups. In such cases, TBD retains the most conductive
2553
+ # PSI type/value from either group PSI set.
1657
2554
  edges.values.each do |edge|
1658
2555
  next unless edge.key?(:psi)
1659
2556
  next unless edge.key?(groups)
@@ -1662,13 +2559,15 @@ module TBD
1662
2559
  vals = {}
1663
2560
 
1664
2561
  edge[groups].keys.each do |set|
1665
- sh = json[:psi].shorthands(set)
1666
- next if sh[:val].empty?
2562
+ sh = json[:psi].shorthands(set)
2563
+ next if sh[:val].empty?
2564
+
1667
2565
  safer = json[:psi].safe(set, type)
1668
2566
  vals[set] = sh[:val][safer] if safer
1669
2567
  end
1670
2568
 
1671
- next if vals.empty?
2569
+ next if vals.empty?
2570
+
1672
2571
  edge[:psi ][type] = vals.values.max
1673
2572
  edge[:sets] = {} unless edge.key?(:sets)
1674
2573
  edge[:sets][type] = vals.key(vals.values.max)
@@ -1678,35 +2577,37 @@ module TBD
1678
2577
 
1679
2578
  if json[:io].key?(:surfaces)
1680
2579
  json[:io][:surfaces].each do |surface|
1681
- next unless surface.key?(:id)
1682
2580
  next unless surface.key?(:psi)
2581
+ next unless surface.key?(:id)
2582
+ next unless tbd[:surfaces].key?(surface[:id ])
1683
2583
  next unless json[:psi].set.key?(surface[:psi])
1684
- sh = json[:psi].shorthands(surface[:psi])
1685
- next if sh[:val].empty?
2584
+
2585
+ sh = json[:psi].shorthands(surface[:psi])
2586
+ next if sh[:val].empty?
1686
2587
 
1687
2588
  edges.values.each do |edge|
1688
2589
  next if edge.key?(:io_set)
1689
2590
  next unless edge.key?(:psi)
1690
2591
  next unless edge.key?(:surfaces)
2592
+ next unless edge[:surfaces].keys.include?(surface[:id])
1691
2593
 
1692
- edge[:surfaces].each do |id, s|
1693
- next unless tbd[:surfaces].key?(id)
1694
- next unless surface[:id] == id
1695
- set = {}
2594
+ s = edge[:surfaces][surface[:id]]
2595
+ set = {}
1696
2596
 
1697
- if edge.key?(:io_type)
1698
- safer = json[:psi].safe(surface[:psi], edge[:io_type])
1699
- set[:io_type] = sh[:val][safer] if safer
1700
- else
1701
- edge[:psi].keys.each do |type|
1702
- safer = json[:psi].safe(surface[:psi], type)
1703
- set[type] = sh[:val][safer] if safer
1704
- end
2597
+ if edge.key?(:io_type)
2598
+ safer = json[:psi].safe(surface[:psi], edge[:io_type])
2599
+ set[:io_type] = sh[:val][safer] if safer
2600
+ else
2601
+ edge[:psi].keys.each do |type|
2602
+ safer = json[:psi].safe(surface[:psi], type)
2603
+ set[type] = sh[:val][safer] if safer
1705
2604
  end
1706
-
1707
- s[:psi] = set unless set.empty?
1708
- s[:set] = surface[:psi] unless set.empty?
1709
2605
  end
2606
+
2607
+ next if set.empty?
2608
+
2609
+ s[:psi] = set
2610
+ s[:set] = surface[:psi]
1710
2611
  end
1711
2612
  end
1712
2613
 
@@ -1721,17 +2622,20 @@ module TBD
1721
2622
  vals = {}
1722
2623
 
1723
2624
  edge[:surfaces].each do |id, s|
1724
- next unless s.key?(:psi)
1725
- next unless s.key?(:set)
1726
- next if s[:set].empty?
1727
- sh = json[:psi].shorthands(s[:set])
1728
- next if sh[:val].empty?
2625
+ next unless s.key?(:psi)
2626
+ next unless s.key?(:set)
2627
+ next if s[:set].empty?
2628
+
2629
+ sh = json[:psi].shorthands(s[:set])
2630
+ next if sh[:val].empty?
2631
+
1729
2632
  safer = json[:psi].safe(s[:set], type)
1730
2633
  vals[s[:set]] = sh[:val][safer] if safer
1731
2634
  end
1732
2635
 
1733
- next if vals.empty?
1734
- edge[:psi][type] = vals.values.max
2636
+ next if vals.empty?
2637
+
2638
+ edge[:psi ][type] = vals.values.max
1735
2639
  edge[:sets] = {} unless edge.key?(:sets)
1736
2640
  edge[:sets][type] = vals.key(vals.values.max)
1737
2641
  end
@@ -1746,15 +2650,18 @@ module TBD
1746
2650
 
1747
2651
  if edge.key?(:io_set)
1748
2652
  next unless json[:psi].set.key?(edge[:io_set])
2653
+
1749
2654
  set = edge[:io_set]
1750
2655
  else
1751
2656
  next unless edge[:sets].key?(edge[:io_type])
1752
2657
  next unless json[:psi].set.key?(edge[:sets][edge[:io_type]])
2658
+
1753
2659
  set = edge[:sets][edge[:io_type]]
1754
2660
  end
1755
2661
 
1756
2662
  sh = json[:psi].shorthands(set)
1757
2663
  next if sh[:val].empty?
2664
+
1758
2665
  safer = json[:psi].safe(set, edge[:io_type])
1759
2666
  next unless safer
1760
2667
 
@@ -1772,9 +2679,10 @@ module TBD
1772
2679
 
1773
2680
  # Fetch edge multipliers for subsurfaces, if applicable.
1774
2681
  edges.values.each do |edge|
1775
- next if edge.key?(:mult) # skip if already assigned
2682
+ next if edge.key?(:mult) # skip if already assigned
1776
2683
  next unless edge.key?(:surfaces)
1777
2684
  next unless edge.key?(:psi)
2685
+
1778
2686
  ok = false
1779
2687
 
1780
2688
  edge[:psi].keys.each do |k|
@@ -1786,10 +2694,10 @@ module TBD
1786
2694
  ok = jamb || sill || head
1787
2695
  end
1788
2696
 
1789
- next unless ok # if OK, edge links subsurface(s) ... yet which one(s)?
2697
+ next unless ok # if OK, edge links subsurface(s) ... yet which one(s)?
1790
2698
 
1791
2699
  edge[:surfaces].each do |id, surface|
1792
- next unless tbd[:surfaces].key?(id) # look up parent (opaque) surface
2700
+ next unless tbd[:surfaces].key?(id) # look up parent (opaque) surface
1793
2701
 
1794
2702
  [:windows, :doors, :skylights].each do |subtypes|
1795
2703
  next unless tbd[:surfaces][id].key?(subtypes)
@@ -1801,7 +2709,7 @@ module TBD
1801
2709
  # An edge may be tagged with (potentially conflicting) multipliers.
1802
2710
  # This is only possible if the edge links 2 subsurfaces, e.g. a
1803
2711
  # shared jamb between window & door. By default, TBD tags common
1804
- # subsurface edges as (mild) "transitions" (i.e. PSI 0 W/K.m), so
2712
+ # subsurface edges as (mild) "transitions" (i.e. PSI 0 W/Km), so
1805
2713
  # there would be no point in assigning an edge multiplier. Users
1806
2714
  # can however reset an edge type via a TBD JSON input file (e.g.
1807
2715
  # "joint" instead of "transition"). It would be a very odd choice,
@@ -1820,9 +2728,9 @@ module TBD
1820
2728
  # edges' origin and terminal vertices must be in close proximity. Edges
1821
2729
  # of unhinged subsurfaces are ignored.
1822
2730
  edges.each do |id, edge|
1823
- nb = 0 # linked subsurfaces (i.e. "holes")
2731
+ nb = 0 # linked subsurfaces (i.e. "holes")
1824
2732
  match = false
1825
- next if edge.key?(:io_type) # skip if set in JSON
2733
+ next if edge.key?(:io_type) # skip if set in JSON
1826
2734
  next unless edge.key?(:v0)
1827
2735
  next unless edge.key?(:v1)
1828
2736
  next unless edge.key?(:psi)
@@ -1879,6 +2787,7 @@ module TBD
1879
2787
  # Loop through each edge and assign heat loss to linked surfaces.
1880
2788
  edges.each do |identifier, edge|
1881
2789
  next unless edge.key?(:psi)
2790
+
1882
2791
  rsi = 0
1883
2792
  max = edge[:psi ].values.max
1884
2793
  type = edge[:psi ].key(max)
@@ -1896,11 +2805,12 @@ module TBD
1896
2805
  edge[:surfaces].each do |id, s|
1897
2806
  next unless tbd[:surfaces].key?(id)
1898
2807
  next unless tbd[:surfaces][id][:deratable]
2808
+
1899
2809
  deratables[id] = s
1900
2810
  end
1901
2811
 
1902
2812
  edge[:surfaces].each { |id, s| apertures[id] = s if holes.key?(id) }
1903
- next if apertures.size > 1 # edge links 2x openings
2813
+ next if apertures.size > 1 # edge links 2x openings
1904
2814
 
1905
2815
  # Prune dad if edge links an opening, its dad and an uncle.
1906
2816
  if deratables.size > 1 && apertures.size > 0
@@ -1920,6 +2830,7 @@ module TBD
1920
2830
  # Sum RSI of targeted insulating layer from each deratable surface.
1921
2831
  deratables.each do |id, deratable|
1922
2832
  next unless tbd[:surfaces][id].key?(:r)
2833
+
1923
2834
  rsi += tbd[:surfaces][id][:r]
1924
2835
  end
1925
2836
 
@@ -1938,8 +2849,10 @@ module TBD
1938
2849
  # Assign thermal bridging heat loss [in W/K] to each deratable surface.
1939
2850
  tbd[:surfaces].each do |id, surface|
1940
2851
  next unless surface.key?(:edges)
2852
+
1941
2853
  surface[:heatloss] = 0
1942
2854
  e = surface[:edges].values
2855
+
1943
2856
  e.each { |edge| surface[:heatloss] += edge[:psi] * edge[:length] }
1944
2857
  end
1945
2858
 
@@ -1959,9 +2872,11 @@ module TBD
1959
2872
  next unless k.key?(:count)
1960
2873
  next unless json[:khi].point.key?(k[:id])
1961
2874
  next unless json[:khi].point[k[:id]] > 0.001
1962
- s[:heatloss] = 0 unless s.key?(:heatloss)
1963
- s[:heatloss] += json[:khi].point[k[:id]] * k[:count]
1964
- s[:pts] = {} unless s.key?(:pts)
2875
+
2876
+ s[:heatloss] = 0 unless s.key?(:heatloss)
2877
+ s[:heatloss] += json[:khi].point[k[:id]] * k[:count]
2878
+ s[:pts ] = {} unless s.key?(:pts)
2879
+
1965
2880
  s[:pts][k[:id]] = { val: json[:khi].point[k[:id]], n: k[:count] }
1966
2881
  end
1967
2882
  end
@@ -1970,8 +2885,8 @@ module TBD
1970
2885
  # If user has selected a Ut to meet, e.g. argh'ments:
1971
2886
  # :uprate_walls
1972
2887
  # :wall_ut
1973
- # :wall_option
1974
- # (same triple arguments for roofs and exposed floors)
2888
+ # :wall_option ... (same triple arguments for roofs and exposed floors)
2889
+ #
1975
2890
  # ... first 'uprate' targeted insulation layers (see ua.rb) before derating.
1976
2891
  # Check for new argh keys [:wall_uo], [:roof_uo] and/or [:floor_uo].
1977
2892
  up = argh[:uprate_walls] || argh[:uprate_roofs] || argh[:uprate_floors]
@@ -1985,74 +2900,75 @@ module TBD
1985
2900
  # (or rather layered materials) having " tbd" in their OpenStudio name.
1986
2901
  tbd[:surfaces].each do |id, surface|
1987
2902
  next unless surface.key?(:construction)
1988
- next unless surface.key?(:index )
1989
- next unless surface.key?(:ltype )
1990
- next unless surface.key?(:r )
1991
- next unless surface.key?(:edges )
1992
- next unless surface.key?(:heatloss )
2903
+ next unless surface.key?(:index)
2904
+ next unless surface.key?(:ltype)
2905
+ next unless surface.key?(:r)
2906
+ next unless surface.key?(:edges)
2907
+ next unless surface.key?(:heatloss)
1993
2908
  next unless surface[:heatloss].abs > TOL
1994
2909
 
1995
- model.getSurfaces.each do |s|
1996
- next unless id == s.nameString
1997
- index = surface[:index ]
1998
- current_c = surface[:construction]
1999
- c = current_c.clone(model).to_LayeredConstruction.get
2000
- m = nil
2001
- m = derate(model, id, surface, c) if index
2002
- # m may be nilled simply because the targeted construction has already
2003
- # been derated, i.e. holds " tbd" in its name. Names of cloned/derated
2004
- # constructions (due to TBD) include the surface name (since derated
2005
- # constructions are now unique to each surface) and the suffix " c tbd".
2006
- if m
2007
- c.setLayer(index, m)
2008
- c.setName("#{id} c tbd")
2009
- current_R = rsi(current_c, s.filmResistance)
2010
-
2011
- # In principle, the derated "ratio" could be calculated simply by
2012
- # accessing a surface's uFactor. Yet air layers within constructions
2013
- # (not air films) are ignored in OpenStudio's uFactor calculation.
2014
- # An example would be 25mm-50mm pressure-equalized air gaps behind
2015
- # brick veneer. This is not always compliant to some energy codes.
2016
- # TBD currently factors-in air gap (and exterior cladding) R-values.
2017
- #
2018
- # If one comments out the following loop (3 lines), tested surfaces
2019
- # with air layers will generate discrepencies between the calculed RSi
2020
- # value above and the inverse of the uFactor. All other surface
2021
- # constructions pass the test.
2022
- #
2023
- # if ((1/current_R) - s.uFactor.to_f).abs > 0.005
2024
- # puts "#{s.nameString} - Usi:#{1/current_R} UFactor: #{s.uFactor}"
2025
- # end
2026
- s.setConstruction(c)
2027
-
2028
- # If the derated surface construction separates CONDITIONED space from
2029
- # UNCONDITIONED or UNENCLOSED space, then derate the adjacent surface
2030
- # construction as well (unless defaulted).
2031
- if s.outsideBoundaryCondition.downcase == "surface"
2032
- unless s.adjacentSurface.empty?
2033
- adjacent = s.adjacentSurface.get
2034
- nom = adjacent.nameString
2035
- default = adjacent.isConstructionDefaulted == false
2036
-
2037
- if default && tbd[:surfaces].key?(nom)
2038
- current_cc = tbd[:surfaces][nom][:construction]
2039
- cc = current_cc.clone(model).to_LayeredConstruction.get
2040
-
2041
- cc.setLayer(tbd[:surfaces][nom][:index], m)
2042
- cc.setName("#{nom} c tbd")
2043
- adjacent.setConstruction(cc)
2044
- end
2910
+ s = model.getSurfaceByName(id)
2911
+ next if s.empty?
2912
+
2913
+ s = s.get
2914
+
2915
+ index = surface[:index ]
2916
+ current_c = surface[:construction]
2917
+ c = current_c.clone(model).to_LayeredConstruction.get
2918
+ m = nil
2919
+ m = derate(id, surface, c) if index
2920
+ # m may be nilled simply because the targeted construction has already
2921
+ # been derated, i.e. holds " tbd" in its name. Names of cloned/derated
2922
+ # constructions (due to TBD) include the surface name (since derated
2923
+ # constructions are now unique to each surface) and the suffix " c tbd".
2924
+ if m
2925
+ c.setLayer(index, m)
2926
+ c.setName("#{id} c tbd")
2927
+ current_R = rsi(current_c, s.filmResistance)
2928
+
2929
+ # In principle, the derated "ratio" could be calculated simply by
2930
+ # accessing a surface's uFactor. Yet air layers within constructions
2931
+ # (not air films) are ignored in OpenStudio's uFactor calculation.
2932
+ # An example would be 25mm-50mm pressure-equalized air gaps behind
2933
+ # brick veneer. This is not always compliant to some energy codes.
2934
+ # TBD currently factors-in air gap (and exterior cladding) R-values.
2935
+ #
2936
+ # If one comments out the following loop (3 lines), tested surfaces
2937
+ # with air layers will generate discrepencies between the calculed RSi
2938
+ # value above and the inverse of the uFactor. All other surface
2939
+ # constructions pass the test.
2940
+ #
2941
+ # if ((1/current_R) - s.uFactor.to_f).abs > 0.005
2942
+ # puts "#{s.nameString} - Usi:#{1/current_R} UFactor: #{s.uFactor}"
2943
+ # end
2944
+ s.setConstruction(c)
2945
+
2946
+ # If the derated surface construction separates CONDITIONED space from
2947
+ # UNCONDITIONED or UNENCLOSED space, then derate the adjacent surface
2948
+ # construction as well (unless defaulted).
2949
+ if s.outsideBoundaryCondition.downcase == "surface"
2950
+ unless s.adjacentSurface.empty?
2951
+ adjacent = s.adjacentSurface.get
2952
+ nom = adjacent.nameString
2953
+ default = adjacent.isConstructionDefaulted == false
2954
+
2955
+ if default && tbd[:surfaces].key?(nom)
2956
+ current_cc = tbd[:surfaces][nom][:construction]
2957
+ cc = current_cc.clone(model).to_LayeredConstruction.get
2958
+ cc.setLayer(tbd[:surfaces][nom][:index], m)
2959
+ cc.setName("#{nom} c tbd")
2960
+ adjacent.setConstruction(cc)
2045
2961
  end
2046
2962
  end
2963
+ end
2047
2964
 
2048
- # Compute updated RSi value from layers.
2049
- updated_c = s.construction.get.to_LayeredConstruction.get
2050
- updated_R = rsi(updated_c, s.filmResistance)
2051
- ratio = -(current_R - updated_R) * 100 / current_R
2965
+ # Compute updated RSi value from layers.
2966
+ updated_c = s.construction.get.to_LayeredConstruction.get
2967
+ updated_R = rsi(updated_c, s.filmResistance)
2968
+ ratio = -(current_R - updated_R) * 100 / current_R
2052
2969
 
2053
- surface[:ratio] = ratio if ratio.abs > TOL
2054
- surface[:u ] = 1 / current_R # un-derated U-factors (for UA')
2055
- end
2970
+ surface[:ratio] = ratio if ratio.abs > TOL
2971
+ surface[:u ] = 1 / current_R # un-derated U-factors (for UA')
2056
2972
  end
2057
2973
  end
2058
2974
 
@@ -2061,9 +2977,12 @@ module TBD
2061
2977
  next unless surface[:deratable]
2062
2978
  next unless surface.key?(:construction)
2063
2979
  next if surface.key?(:u)
2064
- s = model.getSurfaceByName(id)
2065
- log(ERR, "Skipping missing surface '#{id}' (#{mth})") if s.empty?
2066
- next if s.empty?
2980
+
2981
+ s = model.getSurfaceByName(id)
2982
+ msg = "Skipping missing surface '#{id}' (#{mth})"
2983
+ log(ERR, msg) if s.empty?
2984
+ next if s.empty?
2985
+
2067
2986
  surface[:u] = 1.0 / rsi(surface[:construction], s.get.filmResistance)
2068
2987
  end
2069
2988
 
@@ -2075,14 +2994,16 @@ module TBD
2075
2994
  # 4. edge origin & end vertices
2076
2995
  # 5. array of linked outside- or ground-facing surfaces
2077
2996
  edges.values.each do |e|
2078
- next unless e.key?(:psi)
2079
- next unless e.key?(:set)
2080
- v = e[:psi].values.max
2081
- set = e[:set]
2082
- t = e[:psi].key(v)
2083
- l = e[:length]
2084
- l *= e[:mult] if e.key?(:mult)
2085
- edge = { psi: set, type: t, length: l, surfaces: e[:surfaces].keys }
2997
+ next unless e.key?(:psi)
2998
+ next unless e.key?(:set)
2999
+
3000
+ v = e[:psi].values.max
3001
+ set = e[:set]
3002
+ t = e[:psi].key(v)
3003
+ l = e[:length]
3004
+ l *= e[:mult] if e.key?(:mult)
3005
+ edge = { psi: set, type: t, length: l, surfaces: e[:surfaces].keys }
3006
+
2086
3007
  edge[:v0x] = e[:v0].point.x
2087
3008
  edge[:v0y] = e[:v0].point.y
2088
3009
  edge[:v0z] = e[:v0].point.z
@@ -2093,42 +3014,71 @@ module TBD
2093
3014
  json[:io][:edges] << edge
2094
3015
  end
2095
3016
 
2096
- empty = json[:io][:edges].empty?
2097
- json[:io][:edges].sort_by { |e| [ e[:v0x], e[:v0y], e[:v0z],
2098
- e[:v1x], e[:v1y], e[:v1z] ] } unless empty
2099
- json[:io].delete(:edges) if empty
3017
+ if json[:io][:edges].empty?
3018
+ json[:io].delete(:edges)
3019
+ else
3020
+ json[:io][:edges].sort_by { |e| [ e[:v0x], e[:v0y], e[:v0z],
3021
+ e[:v1x], e[:v1y], e[:v1z] ] }
3022
+ end
2100
3023
 
2101
3024
  # Populate UA' trade-off reference values (optional).
2102
- ua = argh[:gen_ua] && argh[:ua_ref] && argh[:ua_ref] == "code (Quebec)"
2103
- qc33(tbd[:surfaces], json[:psi], setpoints) if ua
3025
+ if argh[:gen_ua] && argh[:ua_ref]
3026
+ case argh[:ua_ref]
3027
+ when "code (Quebec)"
3028
+ qc33(tbd[:surfaces], json[:psi], argh[:setpoints])
3029
+ end
3030
+ end
2104
3031
 
2105
- tbd[:io] = json[:io]
3032
+ tbd[:io ] = json[:io ]
3033
+ argh[:io ] = tbd[:io ]
3034
+ argh[:surfaces] = tbd[:surfaces]
3035
+ argh[:version ] = model.getVersion.versionIdentifier
2106
3036
 
2107
3037
  tbd
2108
3038
  end
2109
3039
 
2110
3040
  ##
2111
- # TBD exit strategy for OpenStudio Measures. May write out TBD model
2112
- # content/results if requested (see argh). Always writes out minimal logs,
2113
- # (see tbd.out.json).
3041
+ # Exits TBD Measures. Writes out TBD model content and results if requested.
3042
+ # Always writes out minimal logs (see "tbd.out.json" file).
2114
3043
  #
2115
3044
  # @param runner [Runner] OpenStudio Measure runner
2116
- # @param argh [Hash] TBD arguments
3045
+ # @param [Hash] argh TBD arguments
3046
+ # @option argh [Hash] :io TBD input/output variables (see TBD JSON schema)
3047
+ # @option argh [Hash] :surfaces TBD surfaces (keys: Openstudio surface names)
3048
+ # @option argh [#to_s] :seed OpenStudio file, e.g. "school23.osm"
3049
+ # @option argh [#to_s] :version :version OpenStudio SDK, e.g. "3.6.1"
3050
+ # @option argh [Bool] :gen_ua whether to generate a UA' report
3051
+ # @option argh [#to_s] :ua_ref selected UA' ruleset
3052
+ # @option argh [Bool] :setpoints whether OpenStudio model holds setpoints
3053
+ # @option argh [Bool] :write_tbd whether to output a JSON file
3054
+ # @option argh [Bool] :uprate_walls whether to uprate walls
3055
+ # @option argh [Bool] :uprate_roofs whether to uprate roofs
3056
+ # @option argh [Bool] :uprate_floors whether to uprate floors
3057
+ # @option argh [#to_f] :wall_ut uprated wall Ut target in W/m2•K
3058
+ # @option argh [#to_f] :roof_ut uprated roof Ut target in W/m2•K
3059
+ # @option argh [#to_f] :floor_ut uprated floor Ut target in W/m2•K
3060
+ # @option argh [#to_s] :wall_option wall construction to uprate (or "all")
3061
+ # @option argh [#to_s] :roof_option roof construction to uprate (or "all")
3062
+ # @option argh [#to_s] :floor_option floor construction to uprate (or "all")
3063
+ # @option argh [#to_f] :wall_uo required wall Uo to achieve Ut in W/m2•K
3064
+ # @option argh [#to_f] :roof_uo required roof Uo to achieve Ut in W/m2•K
3065
+ # @option argh [#to_f] :floor_uo required floor Uo to achieve Ut in W/m2•K
2117
3066
  #
2118
- # @return [Bool] true if TBD Measure is successful
3067
+ # @return [Bool] whether TBD Measure is successful (see logs)
2119
3068
  def exit(runner = nil, argh = {})
2120
3069
  # Generated files target a design context ( >= WARN ) ... change TBD log
2121
3070
  # level for debugging purposes. By default, log status is set < DBG
2122
3071
  # while log level is set @INF.
2123
- state = msg(status)
2124
- state = msg(INF) if status.zero?
3072
+ groups = { wall: {}, roof: {}, floor: {} }
3073
+ state = msg(status)
3074
+ state = msg(INF) if status.zero?
2125
3075
  argh = {} unless argh.is_a?(Hash)
2126
3076
  argh[:io ] = nil unless argh.key?(:io)
2127
3077
  argh[:surfaces] = nil unless argh.key?(:surfaces)
2128
3078
 
2129
3079
  unless argh[:io] && argh[:surfaces]
2130
3080
  state = "Halting all TBD processes, yet running OpenStudio"
2131
- state = "Halting all TBD processes, and halting OpenStudio" if fatal?
3081
+ state = "Halting all TBD processes, and halting OpenStudio" if fatal?
2132
3082
  end
2133
3083
 
2134
3084
  argh[:io ] = {} unless argh[:io]
@@ -2151,7 +3101,6 @@ module TBD
2151
3101
  argh[:roof_uo ] = nil unless argh.key?(:roof_ut )
2152
3102
  argh[:floor_uo ] = nil unless argh.key?(:floor_ut )
2153
3103
 
2154
- groups = { wall: {}, roof: {}, floor: {} }
2155
3104
  groups[:wall ][:up] = argh[:uprate_walls ]
2156
3105
  groups[:roof ][:up] = argh[:uprate_roofs ]
2157
3106
  groups[:floor][:up] = argh[:uprate_floors]
@@ -2184,7 +3133,7 @@ module TBD
2184
3133
 
2185
3134
  uo = format("%.3f", g[:uo])
2186
3135
  ut = format("%.3f", g[:ut])
2187
- output = "An initial #{label.to_s} Uo of #{uo} W/m2•K is required to " \
3136
+ output = "An initial #{label.to_s} Uo of #{uo} W/m2•K is required to " \
2188
3137
  "achieve an overall Ut of #{ut} W/m2•K for #{g[:op]}"
2189
3138
  u_t << output
2190
3139
  runner.registerInfo(output)
@@ -2195,16 +3144,16 @@ module TBD
2195
3144
  ua_md_fr = nil
2196
3145
  ua = nil
2197
3146
  ok = argh[:surfaces] && argh[:gen_ua]
2198
- ua = ua_summary(tbd_log[:date], argh) if ok
3147
+ ua = ua_summary(tbd_log[:date], argh) if ok
2199
3148
 
2200
3149
  unless fatal? || ua.nil? || ua.empty?
2201
3150
  if ua.key?(:en)
2202
3151
  if ua[:en].key?(:b1) || ua[:en].key?(:b2)
3152
+ tbd_log[:ua] = {}
2203
3153
  runner.registerInfo("-")
2204
3154
  runner.registerInfo(ua[:model])
2205
- tbd_log[:ua] = {}
2206
- ua_md_en = ua_md(ua, :en)
2207
- ua_md_fr = ua_md(ua, :fr)
3155
+ ua_md_en = ua_md(ua, :en)
3156
+ ua_md_fr = ua_md(ua, :fr)
2208
3157
  end
2209
3158
 
2210
3159
  if ua[:en].key?(:b1) && ua[:en][:b1].key?(:summary)
@@ -2237,9 +3186,9 @@ module TBD
2237
3186
  argh[:surfaces].each do |id, surface|
2238
3187
  next if fatal?
2239
3188
  next unless surface.key?(:ratio)
2240
- ratio = format("%4.1f", surface[:ratio])
2241
- output = "RSi derated by #{ratio}% : #{id}"
2242
3189
 
3190
+ ratio = format("%4.1f", surface[:ratio])
3191
+ output = "RSi derated by #{ratio}% : #{id}"
2243
3192
  results << output
2244
3193
  runner.registerInfo(output)
2245
3194
  end
@@ -2285,14 +3234,14 @@ module TBD
2285
3234
  file_paths = runner.workflow.absoluteFilePaths
2286
3235
 
2287
3236
  # 'Apply Measure Now' won't cp files from 1st path back to generated_files.
2288
- match1 = /WorkingFiles/.match(file_paths[1].to_s)
2289
- match2 = /files/.match(file_paths[1].to_s)
3237
+ match1 = /WorkingFiles/.match(file_paths[1].to_s.strip)
3238
+ match2 = /files/.match(file_paths[1].to_s.strip)
2290
3239
  match = match1 || match2
2291
3240
 
2292
- if file_paths.size >= 2 && File.exists?(file_paths[1].to_s) && match
2293
- out_dir = file_paths[1].to_s
2294
- elsif !file_paths.empty? && File.exists?(file_paths.first.to_s)
2295
- out_dir = file_paths.first.to_s
3241
+ if file_paths.size >= 2 && File.exists?(file_paths[1].to_s.strip) && match
3242
+ out_dir = file_paths[1].to_s.strip
3243
+ elsif !file_paths.empty? && File.exists?(file_paths.first.to_s.strip)
3244
+ out_dir = file_paths.first.to_s.strip
2296
3245
  end
2297
3246
 
2298
3247
  out_path = File.join(out_dir, "tbd.out.json")
@@ -2307,7 +3256,7 @@ module TBD
2307
3256
  end
2308
3257
  end
2309
3258
 
2310
- unless TBD.fatal? || ua.nil? || ua.empty?
3259
+ unless fatal? || ua.nil? || ua.empty?
2311
3260
  unless ua_md_en.nil? || ua_md_en.empty?
2312
3261
  ua_path = File.join(out_dir, "ua_en.md")
2313
3262