tbd 3.2.3 → 3.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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