tbd 3.2.3 → 3.4.0

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