tbd 3.2.3 → 3.4.0

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