tbd 3.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitattributes +3 -0
- data/.github/workflows/pull_request.yml +72 -0
- data/.gitignore +23 -0
- data/.rspec +3 -0
- data/Gemfile +3 -0
- data/LICENSE.md +21 -0
- data/README.md +154 -0
- data/Rakefile +60 -0
- data/json/midrise.json +64 -0
- data/json/tbd_5ZoneNoHVAC.json +19 -0
- data/json/tbd_5ZoneNoHVAC_btap.json +91 -0
- data/json/tbd_seb_n2.json +41 -0
- data/json/tbd_seb_n4.json +57 -0
- data/json/tbd_warehouse10.json +24 -0
- data/json/tbd_warehouse5.json +37 -0
- data/lib/measures/tbd/LICENSE.md +21 -0
- data/lib/measures/tbd/README.md +136 -0
- data/lib/measures/tbd/README.md.erb +42 -0
- data/lib/measures/tbd/docs/.gitkeep +1 -0
- data/lib/measures/tbd/measure.rb +327 -0
- data/lib/measures/tbd/measure.xml +460 -0
- data/lib/measures/tbd/resources/geo.rb +714 -0
- data/lib/measures/tbd/resources/geometry.rb +351 -0
- data/lib/measures/tbd/resources/model.rb +1431 -0
- data/lib/measures/tbd/resources/oslog.rb +381 -0
- data/lib/measures/tbd/resources/psi.rb +2229 -0
- data/lib/measures/tbd/resources/tbd.rb +55 -0
- data/lib/measures/tbd/resources/transformation.rb +121 -0
- data/lib/measures/tbd/resources/ua.rb +986 -0
- data/lib/measures/tbd/resources/utils.rb +1636 -0
- data/lib/measures/tbd/resources/version.rb +3 -0
- data/lib/measures/tbd/tests/tbd_full_PSI.json +17 -0
- data/lib/measures/tbd/tests/tbd_tests.rb +222 -0
- data/lib/tbd/geo.rb +714 -0
- data/lib/tbd/psi.rb +2229 -0
- data/lib/tbd/ua.rb +986 -0
- data/lib/tbd/version.rb +25 -0
- data/lib/tbd.rb +93 -0
- data/sponsors/canada.png +0 -0
- data/sponsors/quebec.png +0 -0
- data/tbd.gemspec +43 -0
- data/tbd.schema.json +571 -0
- data/v291_MacOS.md +110 -0
- metadata +191 -0
@@ -0,0 +1,2229 @@
|
|
1
|
+
# MIT License
|
2
|
+
#
|
3
|
+
# Copyright (c) 2020-2022 Denis Bourgeois & Dan Macumber
|
4
|
+
#
|
5
|
+
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
# of this software and associated documentation files (the "Software"), to deal
|
7
|
+
# in the Software without restriction, including without limitation the rights
|
8
|
+
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
# copies of the Software, and to permit persons to whom the Software is
|
10
|
+
# furnished to do so, subject to the following conditions:
|
11
|
+
#
|
12
|
+
# The above copyright notice and this permission notice shall be included in all
|
13
|
+
# copies or substantial portions of the Software.
|
14
|
+
#
|
15
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
21
|
+
# SOFTWARE.
|
22
|
+
|
23
|
+
module TBD
|
24
|
+
# Sources for thermal bridge types and default KHI & PSI values/sets:
|
25
|
+
#
|
26
|
+
# a) BETBG = Building Envelope Thermal Bridging Guide v1.4 (or higher):
|
27
|
+
#
|
28
|
+
# www.bchydro.com/content/dam/BCHydro/customer-portal/documents/power-smart/
|
29
|
+
# business/programs/BETB-Building-Envelope-Thermal-Bridging-Guide-v1-4.pdf
|
30
|
+
#
|
31
|
+
# b) ISO 14683 (Appendix C): www.iso.org/standard/65706.html
|
32
|
+
#
|
33
|
+
# c) NECB-QC = Québec's energy code for new commercial buildings:
|
34
|
+
#
|
35
|
+
# www2.publicationsduquebec.gouv.qc.ca/dynamicSearch/
|
36
|
+
# telecharge.php?type=1&file=72541.pdf
|
37
|
+
#
|
38
|
+
# www.rbq.gouv.qc.ca/domaines-dintervention/efficacite-energetique/
|
39
|
+
# la-formation/autres-batiments-outils-educatifs.html
|
40
|
+
|
41
|
+
##
|
42
|
+
# Library of point thermal bridges (e.g. columns). Each key:value entry
|
43
|
+
# requires a unique identifier e.g. "poor (BETBG)" and a KHI-value in W/K.
|
44
|
+
class KHI
|
45
|
+
# @return [Hash] KHI library
|
46
|
+
attr_reader :point
|
47
|
+
|
48
|
+
##
|
49
|
+
# Construct a new KHI library (with defaults).
|
50
|
+
def initialize
|
51
|
+
@point = {}
|
52
|
+
|
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
|
62
|
+
end
|
63
|
+
|
64
|
+
##
|
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).
|
67
|
+
#
|
68
|
+
# @param k [Hash] a new KHI entry
|
69
|
+
#
|
70
|
+
# @return [Bool] true if successfully appended
|
71
|
+
# @return [Bool] false if invalid input
|
72
|
+
def append(k = {})
|
73
|
+
mth = "TBD::#{__callee__}"
|
74
|
+
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})")
|
82
|
+
return false
|
83
|
+
end
|
84
|
+
|
85
|
+
@point[k[:id]] = k[:point]
|
86
|
+
|
87
|
+
true
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
##
|
92
|
+
# Library of linear thermal bridges (e.g. corners, balconies). Each key:value
|
93
|
+
# 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.
|
95
|
+
class PSI
|
96
|
+
# @return [Hash] PSI set
|
97
|
+
attr_reader :set
|
98
|
+
|
99
|
+
# @return [Hash] shorthand listing of PSI types in a set
|
100
|
+
attr_reader :has
|
101
|
+
|
102
|
+
# @return [Hash] shorthand listing of PSI values in a set
|
103
|
+
attr_reader :val
|
104
|
+
|
105
|
+
##
|
106
|
+
# Construct a new PSI library (with defaults)
|
107
|
+
def initialize
|
108
|
+
@set = {}
|
109
|
+
@has = {}
|
110
|
+
@val = {}
|
111
|
+
|
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
|
+
|
117
|
+
# Convex vs concave PSI adjustments may be warranted if there is a
|
118
|
+
# 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.
|
124
|
+
#
|
125
|
+
# 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)
|
131
|
+
#
|
132
|
+
# Li-Le = wall thickness e.g., -0.25m (negative here as Li < Le)
|
133
|
+
@set["poor (BETBG)"] =
|
134
|
+
{
|
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)")
|
146
|
+
|
147
|
+
@set["regular (BETBG)"] =
|
148
|
+
{
|
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)")
|
160
|
+
|
161
|
+
@set["efficient (BETBG)"] =
|
162
|
+
{
|
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)")
|
174
|
+
|
175
|
+
@set["spandrel (BETBG)"] =
|
176
|
+
{
|
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)")
|
188
|
+
|
189
|
+
@set["spandrel HP (BETBG)"] =
|
190
|
+
{
|
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:
|
204
|
+
{
|
205
|
+
rimjoist: 0.300, # *
|
206
|
+
parapet: 0.325, # *
|
207
|
+
fenestration: 0.200, # *
|
208
|
+
corner: 0.300, # ** "regular (BETBG)", adj. for ext. dimensions
|
209
|
+
balcony: 0.500, # *
|
210
|
+
party: 0.450, # *
|
211
|
+
grade: 0.450, # *
|
212
|
+
joint: 0.200, # *
|
213
|
+
transition: 0.000
|
214
|
+
}.freeze # based on EXTERIOR dimensions (art. 3.1.1.6)
|
215
|
+
self.gen("code (Quebec)")
|
216
|
+
|
217
|
+
@set["uncompliant (Quebec)"] = # NECB-QC (non-code-compliant) defaults:
|
218
|
+
{
|
219
|
+
rimjoist: 0.850, # *
|
220
|
+
parapet: 0.800, # *
|
221
|
+
fenestration: 0.500, # *
|
222
|
+
corner: 0.850, # ** ... not stated
|
223
|
+
balcony: 1.000, # *
|
224
|
+
party: 0.850, # *
|
225
|
+
grade: 0.850, # *
|
226
|
+
joint: 0.500, # *
|
227
|
+
transition: 0.000
|
228
|
+
}.freeze # based on EXTERIOR dimensions (art. 3.1.1.6)
|
229
|
+
self.gen("uncompliant (Quebec)")
|
230
|
+
|
231
|
+
@set["(non thermal bridging)"] = # ... would not derate surfaces:
|
232
|
+
{
|
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
|
242
|
+
}.freeze
|
243
|
+
self.gen("(non thermal bridging)")
|
244
|
+
end
|
245
|
+
|
246
|
+
##
|
247
|
+
# Generate PSI set shorthand listings (requires a valid id).
|
248
|
+
#
|
249
|
+
# @param id [String] a PSI set identifier
|
250
|
+
#
|
251
|
+
# @return [Bool] true if successful in generating PSI set shorthands
|
252
|
+
# @return [Bool] false if invalid input
|
253
|
+
def gen(id = "")
|
254
|
+
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 ]
|
362
|
+
|
363
|
+
max = [v[:parapetconcave], v[:parapetconvex]].max
|
364
|
+
v[:parapet] = max unless @has[:parapet]
|
365
|
+
@val[id] = v
|
366
|
+
|
367
|
+
true
|
368
|
+
end
|
369
|
+
|
370
|
+
##
|
371
|
+
# Append a new PSI set, based on a TBD JSON-formatted PSI set object -
|
372
|
+
# requires a valid, unique :id.
|
373
|
+
#
|
374
|
+
# @param set [Hash] a new PSI set
|
375
|
+
#
|
376
|
+
# @return [Bool] true if successfully appended
|
377
|
+
# @return [Bool] false if invalid input
|
378
|
+
def append(set = {})
|
379
|
+
mth = "TBD::#{__callee__}"
|
380
|
+
a = false
|
381
|
+
|
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)
|
384
|
+
|
385
|
+
exists = @set.key?(set[:id])
|
386
|
+
TBD.log(ERR, "'#{set[:id]}': existing PSI set (#{mth})") if exists
|
387
|
+
return false if exists
|
388
|
+
|
389
|
+
s = {}
|
390
|
+
# Most PSI types have concave and convex variants, depending on the polar
|
391
|
+
# position of deratable surfaces about an edge-as-thermal-bridge. One
|
392
|
+
# exception is :fenestration, which TBD later breaks down into :head,
|
393
|
+
# :sill or :jamb edge types. Another exception is a :joint edge: a PSI
|
394
|
+
# type that is not autoassigned to an edge (i.e., only via a TBD JSON
|
395
|
+
# input file). Finally, transitions are autoassigned by TBD when an edge
|
396
|
+
# 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])
|
433
|
+
|
434
|
+
true
|
435
|
+
end
|
436
|
+
|
437
|
+
##
|
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).
|
441
|
+
#
|
442
|
+
# @param id [String] a PSI set identifier
|
443
|
+
#
|
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)
|
446
|
+
def shorthands(id = "")
|
447
|
+
mth = "TBD::#{__callee__}"
|
448
|
+
cl = String
|
449
|
+
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)
|
455
|
+
|
456
|
+
sh[:has] = @has[id]
|
457
|
+
sh[:val] = @val[id]
|
458
|
+
|
459
|
+
sh
|
460
|
+
end
|
461
|
+
|
462
|
+
##
|
463
|
+
# Validate whether a given PSI set has a complete list of PSI type:values.
|
464
|
+
#
|
465
|
+
# @param id [String] a PSI set identifier
|
466
|
+
#
|
467
|
+
# @return [Bool] true if found and is complete
|
468
|
+
# @return [Bool] false if invalid input
|
469
|
+
def complete?(id = "")
|
470
|
+
mth = "TBD::#{__callee__}"
|
471
|
+
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)
|
477
|
+
|
478
|
+
holes = []
|
479
|
+
holes << :head if @has[id][:head ]
|
480
|
+
holes << :sill if @has[id][:sill ]
|
481
|
+
holes << :jamb if @has[id][:jamb ]
|
482
|
+
ok = holes.size == 3
|
483
|
+
ok = true if @has[id][:fenestration ]
|
484
|
+
return false unless ok
|
485
|
+
|
486
|
+
corners = []
|
487
|
+
corners << :concave if @has[id][:cornerconcave ]
|
488
|
+
corners << :convex if @has[id][:cornerconvex ]
|
489
|
+
ok = corners.size == 2
|
490
|
+
ok = true if @has[id][:corner ]
|
491
|
+
return false unless ok
|
492
|
+
|
493
|
+
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 ]
|
503
|
+
|
504
|
+
ok
|
505
|
+
end
|
506
|
+
|
507
|
+
##
|
508
|
+
# Return safe PSI type if missing input from PSI set (based on inheritance).
|
509
|
+
#
|
510
|
+
# @param id [String] a PSI set identifier
|
511
|
+
# @param type [Symbol] a PSI type, e.g. :rimjoistconcave
|
512
|
+
#
|
513
|
+
# @return [Symbol] safe PSI type
|
514
|
+
# @return [Nil] if invalid input or no safe PSI type found
|
515
|
+
def safe(id = "", type = nil)
|
516
|
+
mth = "TBD::#{__callee__}"
|
517
|
+
cl1 = String
|
518
|
+
cl2 = Symbol
|
519
|
+
|
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)
|
524
|
+
|
525
|
+
safer = type
|
526
|
+
|
527
|
+
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
|
538
|
+
end
|
539
|
+
|
540
|
+
return safer if @has[id][safer]
|
541
|
+
|
542
|
+
nil
|
543
|
+
end
|
544
|
+
end
|
545
|
+
|
546
|
+
##
|
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).
|
554
|
+
#
|
555
|
+
# @param s [Hash] preprocessed TBD surfaces
|
556
|
+
# @param e [Hash] preprocessed TBD edges
|
557
|
+
# @param argh [Hash] arguments
|
558
|
+
#
|
559
|
+
# @return [Hash] io: JSON inputs (Hash), psi:/khi: new (enriched) sets (Hash)
|
560
|
+
# @return [Hash] io: empty Hash if invalid input
|
561
|
+
def inputs(s = {}, e = {}, argh = {})
|
562
|
+
mth = "TBD::#{__callee__}"
|
563
|
+
opt = :option
|
564
|
+
ipt = { io: {}, psi: PSI.new, khi: KHI.new }
|
565
|
+
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)
|
571
|
+
|
572
|
+
argh[:io_path] = nil unless argh.key?(:io_path)
|
573
|
+
argh[:schema_path] = nil unless argh.key?(:schema_path)
|
574
|
+
pth = argh[:io_path]
|
575
|
+
sch = argh[:schema_path]
|
576
|
+
|
577
|
+
if pth
|
578
|
+
return empty("JSON file", mth, FTL, ipt) unless File.size?(pth)
|
579
|
+
io = File.read(pth)
|
580
|
+
io = JSON.parse(io, symbolize_names: true)
|
581
|
+
return mismatch("io", io, Hash, mth, FTL, ipt) unless io.is_a?(Hash)
|
582
|
+
|
583
|
+
# Schema validation is not yet supported in the OpenStudio Application.
|
584
|
+
# We nonetheless recommend that users rely on the json-schema gem, or an
|
585
|
+
# online linter, prior to using TBD. The following checks focus on content
|
586
|
+
# - ignoring bad JSON input otherwise caught via JSON validation.
|
587
|
+
#
|
588
|
+
# A side note: JSON validation relies on case-senitive string comparisons
|
589
|
+
# (e.g. OpenStudio space or surface names, vs corresponding TBD JSON
|
590
|
+
# identifiers). So "Space-1" doesn't match "SPACE-1" - head's up.
|
591
|
+
if sch
|
592
|
+
require "json-schema"
|
593
|
+
|
594
|
+
return invalid("JSON schema", mth, 0, FTL, ipt) unless File.exist?(sch)
|
595
|
+
return empty("JSON schema", mth, FTL, ipt) if File.zero?(sch)
|
596
|
+
schema = File.read(sch)
|
597
|
+
schema = JSON.parse(schema, symbolize_names: true)
|
598
|
+
valid = JSON::Validator.validate!(schema, io)
|
599
|
+
return invalid("JSON schema validation", mth, 0, FTL, ipt) unless valid
|
600
|
+
end
|
601
|
+
|
602
|
+
# Append JSON entries to library of linear & point thermal bridges.
|
603
|
+
io[:psis].each { |psi| ipt[:psi].append(psi) } if io.key?(:psis)
|
604
|
+
io[:khis].each { |khi| ipt[:khi].append(khi) } if io.key?(:khis)
|
605
|
+
|
606
|
+
# JSON-defined or user-selected, building PSI set must be complete/valid.
|
607
|
+
io[:building] = { psi: argh[opt] } unless io.key?(:building)
|
608
|
+
bdg = io[:building]
|
609
|
+
ok = bdg.key?(:psi)
|
610
|
+
return hashkey("Building PSI", bdg, :psi, mth, FTL, ipt) unless ok
|
611
|
+
ok = ipt[:psi].complete?(bdg[:psi])
|
612
|
+
return invalid("Complete building PSI", mth, 0, FTL, ipt) unless ok
|
613
|
+
|
614
|
+
# Validate remaining (optional) JSON entries.
|
615
|
+
[:stories, :spacetypes, :spaces].each do |types|
|
616
|
+
key = :story
|
617
|
+
key = :stype if types == :spacetypes
|
618
|
+
key = :space if types == :spaces
|
619
|
+
|
620
|
+
if io.key?(types)
|
621
|
+
io[types].each do |type|
|
622
|
+
next unless type.key?(:psi)
|
623
|
+
next unless type.key?(:id)
|
624
|
+
s1 = "JSON/OSM '#{type[:id]}' (#{mth})"
|
625
|
+
s2 = "JSON/PSI '#{type[:id]}' set (#{mth})"
|
626
|
+
match = false
|
627
|
+
|
628
|
+
s.values.each do |props| # TBD model surface linked to type?
|
629
|
+
break if match
|
630
|
+
next unless props.key?(key)
|
631
|
+
match = type[:id] == props[key].nameString
|
632
|
+
end
|
633
|
+
|
634
|
+
log(ERR, s1) unless match
|
635
|
+
log(ERR, s2) unless ipt[:psi].set.key?(type[:psi])
|
636
|
+
end
|
637
|
+
end
|
638
|
+
end
|
639
|
+
|
640
|
+
if io.key?(:surfaces)
|
641
|
+
io[:surfaces].each do |surface|
|
642
|
+
next unless surface.key?(:id)
|
643
|
+
s1 = "JSON/OSM surface '#{surface[:id]}' (#{mth})"
|
644
|
+
log(ERR, s1) unless s.key?(surface[:id])
|
645
|
+
|
646
|
+
# surfaces can OPTIONALLY hold custom PSI sets and/or KHI data
|
647
|
+
if surface.key?(:psi)
|
648
|
+
s2 = "JSON/OSM surface/set '#{surface[:id]}' (#{mth})"
|
649
|
+
log(ERR, s2) unless ipt[:psi].set.key?(surface[:psi])
|
650
|
+
end
|
651
|
+
|
652
|
+
if surface.key?(:khis)
|
653
|
+
surface[:khis].each do |khi|
|
654
|
+
next unless khi.key?(:id)
|
655
|
+
s3 = "JSON/KHI surface '#{surface[:id]}' '#{khi[:id]}' (#{mth})"
|
656
|
+
log(ERR, s3) unless ipt[:khi].point.key?(khi[:id])
|
657
|
+
end
|
658
|
+
end
|
659
|
+
end
|
660
|
+
end
|
661
|
+
|
662
|
+
if io.key?(:subsurfaces)
|
663
|
+
io[:subsurfaces].each do |sub|
|
664
|
+
next unless sub.key?(:id)
|
665
|
+
next unless sub.key?(:usi)
|
666
|
+
match = false
|
667
|
+
|
668
|
+
s.each do |id, surface|
|
669
|
+
break if match
|
670
|
+
|
671
|
+
[:windows, :doors, :skylights].each do |holes|
|
672
|
+
if surface.key?(holes)
|
673
|
+
surface[holes].keys.each do |id|
|
674
|
+
break if match
|
675
|
+
match = sub[:id] == id
|
676
|
+
end
|
677
|
+
end
|
678
|
+
end
|
679
|
+
end
|
680
|
+
|
681
|
+
log(ERR, "JSON/OSM subsurface '#{sub[:id]}' (#{mth})") unless match
|
682
|
+
end
|
683
|
+
end
|
684
|
+
|
685
|
+
if io.key?(:edges)
|
686
|
+
io[:edges].each do |edge|
|
687
|
+
next unless edge.key?(:type)
|
688
|
+
next unless edge.key?(:surfaces)
|
689
|
+
surfaces = edge[:surfaces]
|
690
|
+
type = edge[:type].to_sym
|
691
|
+
safer = ipt[:psi].safe(bdg[:psi], type) # fallback
|
692
|
+
log(ERR, "Skipping invalid edge PSI '#{type}' (#{mth})") unless safer
|
693
|
+
next unless safer
|
694
|
+
valid = true
|
695
|
+
|
696
|
+
surfaces.each do |surface| # TBD edge's surfaces on file
|
697
|
+
e.values.each do |ee| # TBD edges in memory
|
698
|
+
break unless valid # if previous anomaly detected
|
699
|
+
next if ee.key?(:io_type) # validated from previous loop
|
700
|
+
next unless ee.key?(:surfaces)
|
701
|
+
surfs = ee[:surfaces]
|
702
|
+
next unless surfs.key?(surface)
|
703
|
+
|
704
|
+
# An edge on file is valid if ALL of its listed surfaces together
|
705
|
+
# connect at least one or more TBD/Topolys model edges in memory.
|
706
|
+
# Each of the latter may connect e.g. 3x TBD/Topolys surfaces,
|
707
|
+
# but the list of surfaces on file may be shorter, e.g. only 2x.
|
708
|
+
match = true
|
709
|
+
surfaces.each { |id| match = false unless surfs.key?(id) }
|
710
|
+
next unless match
|
711
|
+
|
712
|
+
if edge.key?(:length) # optional
|
713
|
+
next unless (ee[:length] - edge[:length]).abs < TOL
|
714
|
+
end
|
715
|
+
|
716
|
+
# Optionally, edge coordinates may narrow down potential matches.
|
717
|
+
if edge.key?(:v0x) || edge.key?(:v0y) || edge.key?(:v0z) ||
|
718
|
+
edge.key?(:v1x) || edge.key?(:v1y) || edge.key?(:v1z)
|
719
|
+
|
720
|
+
unless edge.key?(:v0x) && edge.key?(:v0y) && edge.key?(:v0z) &&
|
721
|
+
edge.key?(:v1x) && edge.key?(:v1y) && edge.key?(:v1z)
|
722
|
+
|
723
|
+
log(ERR, "Mismatch '#{surface}' edge vertices (#{mth})")
|
724
|
+
valid = false
|
725
|
+
next
|
726
|
+
end
|
727
|
+
|
728
|
+
e1 = {}
|
729
|
+
e2 = {}
|
730
|
+
e1[:v0] = Topolys::Point3D.new(edge[:v0x].to_f,
|
731
|
+
edge[:v0y].to_f,
|
732
|
+
edge[:v0z].to_f)
|
733
|
+
e1[:v1] = Topolys::Point3D.new(edge[:v1x].to_f,
|
734
|
+
edge[:v1y].to_f,
|
735
|
+
edge[:v1z].to_f)
|
736
|
+
e2[:v0] = ee[:v0].point
|
737
|
+
e2[:v1] = ee[:v1].point
|
738
|
+
next unless matches?(e1, e2)
|
739
|
+
end
|
740
|
+
|
741
|
+
if edge.key?(:psi) # optional
|
742
|
+
set = edge[:psi]
|
743
|
+
|
744
|
+
if ipt[:psi].set.key?(set)
|
745
|
+
saferr = ipt[:psi].safe(set, type)
|
746
|
+
ee[:io_set ] = set if saferr
|
747
|
+
ee[:io_type] = type if saferr
|
748
|
+
log(ERR, "Invalid '#{set}': '#{type}' (#{mth})") unless saferr
|
749
|
+
valid = false unless saferr
|
750
|
+
else
|
751
|
+
log(ERR, "Missing edge PSI '#{set}' (#{mth})")
|
752
|
+
valid = false
|
753
|
+
end
|
754
|
+
else
|
755
|
+
ee[:io_type] = type # success: matching edge - setting edge type
|
756
|
+
end
|
757
|
+
end
|
758
|
+
end
|
759
|
+
end
|
760
|
+
end
|
761
|
+
else
|
762
|
+
# No (optional) user-defined TBD JSON input file. In such cases, provided
|
763
|
+
# argh[:option] must refer to a valid PSI set. If valid, all edges inherit
|
764
|
+
# a default PSI set (without KHI entries).
|
765
|
+
ok = ipt[:psi].complete?(argh[opt])
|
766
|
+
io[:building] = { psi: argh[opt] } if ok
|
767
|
+
log(FTL, "Incomplete building PSI set '#{argh[opt]}' (#{mth})") unless ok
|
768
|
+
return ipt unless ok
|
769
|
+
end
|
770
|
+
|
771
|
+
ipt[:io] = io
|
772
|
+
|
773
|
+
ipt
|
774
|
+
end
|
775
|
+
|
776
|
+
##
|
777
|
+
# Thermally derate insulating material within construction.
|
778
|
+
#
|
779
|
+
# @param model [OpenStudio::Model::Model] a model
|
780
|
+
# @param id [String] surface identifier
|
781
|
+
# @param surface [Hash] a TBD surface
|
782
|
+
# @param lc [OpenStudio::Model::LayeredConstruction] a layered construction
|
783
|
+
#
|
784
|
+
# @return [OpenStudio::Model::Material] derated (cloned) material
|
785
|
+
# @return [NilClass] if invalid input
|
786
|
+
def derate(model = nil, id = "", s = {}, lc = nil)
|
787
|
+
mth = "TBD::#{__callee__}"
|
788
|
+
m = nil
|
789
|
+
k1 = :heatloss
|
790
|
+
k2 = :ltype
|
791
|
+
k3 = :construction
|
792
|
+
k4 = :index
|
793
|
+
cl1 = OpenStudio::Model::Model
|
794
|
+
cl2 = OpenStudio::Model::LayeredConstruction
|
795
|
+
cl3 = Numeric
|
796
|
+
cl4 = Symbol
|
797
|
+
cl5 = Integer
|
798
|
+
|
799
|
+
return mismatch("model", model, cl, mth) unless model.is_a?(cl1)
|
800
|
+
return mismatch("id", id, String, mth) unless id.is_a?(String)
|
801
|
+
return mismatch(id, s, Hash, mth) unless s.is_a?(Hash)
|
802
|
+
return mismatch("lc", lc, Hash, mth) unless lc.is_a?(cl2)
|
803
|
+
return hashkey("'#{id}' W/K", s, k1, mth) unless s.key?(k1)
|
804
|
+
return invalid("'#{id}' W/K", mth, 3) unless s[k1]
|
805
|
+
return mismatch("'#{id}' W/K", s[k1], cl3, mth) unless s[k1].is_a?(cl3)
|
806
|
+
return zero("'#{id}' W/K", mth, WRN) if s[k1].abs < TOL
|
807
|
+
return hashkey("'#{id}' m2", s, :net, mth) unless s.key?(:net)
|
808
|
+
return invalid("'#{id}' m2", mth, 3) unless s[:net]
|
809
|
+
return mismatch("'#{id}' m2", s[:net], cl3, mth) unless s[:net].is_a?(cl3)
|
810
|
+
return zero("'#{id}' m2", mth, WRN) if s[:net].abs < TOL
|
811
|
+
return hashkey("'#{id}' type", s, k2, mth) unless s.key?(k2)
|
812
|
+
return invalid("'#{id}' type", mth, 3) unless s[k2]
|
813
|
+
return mismatch("'#{id}' type", s[k2], cl4, mth) unless s[k2].is_a?(cl4)
|
814
|
+
|
815
|
+
ok = s[k2] == :massless || s[k2] == :standard
|
816
|
+
|
817
|
+
return invalid("'#{id}' type", mth, 3) unless ok
|
818
|
+
return hashkey("'#{id}' construction", s, k3, mth) unless s.key?(k3)
|
819
|
+
return hashkey("'#{id}' index", s, k4, mth) unless s.key?(k4)
|
820
|
+
return invalid("'#{id}' index", mth, 3) unless s[k4]
|
821
|
+
return mismatch("'#{id}' index", s[k4], cl5, mth) unless s[k4].is_a?(cl5)
|
822
|
+
return negative("'#{id}' index", mth) if s[k4] < 0
|
823
|
+
return hashkey("'#{id}' Rsi", s, :r, mth) unless s.key?(:r)
|
824
|
+
return invalid("'#{id}' Rsi", mth, 3) unless s[:r]
|
825
|
+
return mismatch("'#{id}' Rsi", s[:r], cl3, mth) unless s[:r].is_a?(cl3)
|
826
|
+
return zero("'#{id}' Rsi", mth, WRN) if s[:r].abs < 0.001
|
827
|
+
|
828
|
+
derated = lc.nameString.include?(" tbd")
|
829
|
+
log(WRN, "Won't derate '#{id}': already derated (#{mth})") if derated
|
830
|
+
return m if derated
|
831
|
+
|
832
|
+
index = s[:index]
|
833
|
+
ltype = s[:ltype]
|
834
|
+
r = s[:r]
|
835
|
+
u = s[:heatloss] / s[:net]
|
836
|
+
loss = 0
|
837
|
+
de_u = 1 / r + u # derated U
|
838
|
+
de_r = 1 / de_u # derated R
|
839
|
+
|
840
|
+
if ltype == :massless
|
841
|
+
m = lc.getLayer(index).to_MasslessOpaqueMaterial
|
842
|
+
return invalid("'#{id}' massless layer?", mth, 0) if m.empty?
|
843
|
+
m = m.get
|
844
|
+
up = ""
|
845
|
+
up = "uprated " if m.nameString.include?(" uprated")
|
846
|
+
m = m.clone(model).to_MasslessOpaqueMaterial.get
|
847
|
+
m.setName("'#{id}' #{up}m tbd")
|
848
|
+
de_r = 0.001 unless de_r > 0.001
|
849
|
+
loss = (de_u - 1 / de_r) * s[:net] unless de_r > 0.001
|
850
|
+
m.setThermalResistance(de_r)
|
851
|
+
else
|
852
|
+
m = lc.getLayer(index).to_StandardOpaqueMaterial
|
853
|
+
return invalid("'#{id}' standard layer?", mth, 0) if m.empty?
|
854
|
+
m = m.get
|
855
|
+
up = ""
|
856
|
+
up = "uprated " if m.nameString.include?(" uprated")
|
857
|
+
m = m.clone(model).to_StandardOpaqueMaterial.get
|
858
|
+
m.setName("'#{id}' #{up}m tbd")
|
859
|
+
k = m.thermalConductivity
|
860
|
+
|
861
|
+
if de_r > 0.001
|
862
|
+
d = de_r * k
|
863
|
+
|
864
|
+
unless d > 0.003
|
865
|
+
d = 0.003
|
866
|
+
k = d / de_r
|
867
|
+
k = 3 unless k < 3
|
868
|
+
loss = (de_u - k / d) * s[:net] unless k < 3
|
869
|
+
end
|
870
|
+
else # de_r < 0.001 m2.K/W
|
871
|
+
d = 0.001 * k
|
872
|
+
d = 0.003 unless d > 0.003
|
873
|
+
k = d / 0.001 unless d > 0.003
|
874
|
+
loss = (de_u - k / d) * s[:net]
|
875
|
+
end
|
876
|
+
|
877
|
+
m.setThickness(d)
|
878
|
+
m.setThermalConductivity(k)
|
879
|
+
end
|
880
|
+
|
881
|
+
if m && loss > TOL
|
882
|
+
s[:r_heatloss] = loss
|
883
|
+
h_loss = format "%.3f", s[:r_heatloss]
|
884
|
+
log(WRN, "Won't assign #{h_loss} W/K to '#{id}': too conductive (#{mth})")
|
885
|
+
end
|
886
|
+
|
887
|
+
m
|
888
|
+
end
|
889
|
+
|
890
|
+
##
|
891
|
+
# Process TBD objects, based on OpenStudio model (OSM) and Topolys model,
|
892
|
+
# and derate admissible envelope surfaces by substituting insulating material
|
893
|
+
# within surface multilayered constructions with derated clones. Returns a
|
894
|
+
# hash holding 2x key:value pairs ... io: objects for JSON serialization and
|
895
|
+
# surfaces: derated TBD surfaces.
|
896
|
+
#
|
897
|
+
# @param model [OpenStudio::Model::Model] a model
|
898
|
+
# @param argh [Hash] TBD arguments
|
899
|
+
#
|
900
|
+
# @return [Hash] io: (Hash), surfaces: (Hash)
|
901
|
+
# @return [Hash] io: nil, surfaces: nil (if invalid input)
|
902
|
+
def process(model = nil, argh = {})
|
903
|
+
mth = "TBD::#{__callee__}"
|
904
|
+
cl = OpenStudio::Model::Model
|
905
|
+
tbd = { io: nil, surfaces: {} }
|
906
|
+
|
907
|
+
return mismatch("model", model, cl, mth, DBG, tbd) unless model.is_a?(cl)
|
908
|
+
return mismatch("argh", argh, Hash, mth, DBG, tbd) unless argh.is_a?(Hash)
|
909
|
+
|
910
|
+
argh = {} if argh.empty?
|
911
|
+
argh[:option ] = "" unless argh.key?(:option )
|
912
|
+
argh[:io_path ] = nil unless argh.key?(:io_path )
|
913
|
+
argh[:schema_path ] = nil unless argh.key?(:schema_path )
|
914
|
+
argh[:uprate_walls ] = false unless argh.key?(:uprate_walls )
|
915
|
+
argh[:uprate_roofs ] = false unless argh.key?(:uprate_roofs )
|
916
|
+
argh[:uprate_floors] = false unless argh.key?(:uprate_floors)
|
917
|
+
argh[:wall_ut ] = 0 unless argh.key?(:wall_ut )
|
918
|
+
argh[:roof_ut ] = 0 unless argh.key?(:roof_ut )
|
919
|
+
argh[:floor_ut ] = 0 unless argh.key?(:floor_ut )
|
920
|
+
argh[:wall_option ] = "" unless argh.key?(:wall_option )
|
921
|
+
argh[:roof_option ] = "" unless argh.key?(:roof_option )
|
922
|
+
argh[:floor_option ] = "" unless argh.key?(:floor_option )
|
923
|
+
argh[:gen_ua ] = false unless argh.key?(:gen_ua )
|
924
|
+
argh[:ua_ref ] = "" unless argh.key?(:ua_ref )
|
925
|
+
argh[:gen_kiva ] = false unless argh.key?(:gen_kiva )
|
926
|
+
|
927
|
+
# Create the Topolys Model.
|
928
|
+
t_model = Topolys::Model.new
|
929
|
+
|
930
|
+
# "true" if any space/zone holds valid setpoint temperatures. With invalid
|
931
|
+
# inputs, these 2x methods return "false", ignoring any
|
932
|
+
# setpoint-based logic, e.g. semi-heated spaces (DEBUG errors are logged).
|
933
|
+
setpoints = heatingTemperatureSetpoints?(model)
|
934
|
+
setpoints = coolingTemperatureSetpoints?(model) || setpoints
|
935
|
+
|
936
|
+
# "true" if any space/zone is part of an HVAC air loop. With invalid inputs,
|
937
|
+
# the method returns "false", ignoring any air-loop related logic, e.g.
|
938
|
+
# plenum zones as HVAC objects (DEBUG errors are logged).
|
939
|
+
airloops = airLoopsHVAC?(model)
|
940
|
+
|
941
|
+
model.getSurfaces.sort_by { |s| s.nameString }.each do |s|
|
942
|
+
# Fetch key attributes of opaque surfaces. Method returns nil with invalid
|
943
|
+
# input (DEBUG and ERROR messages may be logged). TBD ignores them.
|
944
|
+
surface = properties(model, s)
|
945
|
+
next if surface.nil?
|
946
|
+
|
947
|
+
# Similar to "setpoints?" methods above, the boolean methods below also
|
948
|
+
# return "false" with invalid inputs, ignoring any space/zone
|
949
|
+
# conditioning-based logic (e.g. semi-heated spaces, mislabelling a
|
950
|
+
# plenum as an unconditioned zone).
|
951
|
+
if setpoints
|
952
|
+
if surface[:space].thermalZone.empty?
|
953
|
+
plenum = plenum?(surface[:space], airloops, setpoints)
|
954
|
+
surface[:conditioned] = false unless plenum
|
955
|
+
else
|
956
|
+
zone = surface[:space].thermalZone.get
|
957
|
+
heat = maxHeatScheduledSetpoint(zone)
|
958
|
+
cool = minCoolScheduledSetpoint(zone)
|
959
|
+
|
960
|
+
unless heat[:spt] || cool[:spt]
|
961
|
+
plenum = plenum?(surface[:space], airloops, setpoints)
|
962
|
+
heat[:spt] = 21 if plenum
|
963
|
+
cool[:spt] = 24 if plenum
|
964
|
+
surface[:conditioned] = false unless plenum
|
965
|
+
end
|
966
|
+
|
967
|
+
free = heat[:spt] && heat[:spt] < -40 && cool[:spt] && cool[:spt] > 40
|
968
|
+
surface[:conditioned] = false if free
|
969
|
+
end
|
970
|
+
end
|
971
|
+
|
972
|
+
surface[:heating] = heat[:spt] if heat[:spt] # if valid heating setpoints
|
973
|
+
surface[:cooling] = cool[:spt] if cool[:spt] # if valid cooling setpoints
|
974
|
+
|
975
|
+
tbd[:surfaces][s.nameString] = surface
|
976
|
+
end # (opaque) surfaces populated
|
977
|
+
|
978
|
+
return empty("TBD surfaces", mth, ERR, tbd) if tbd[:surfaces].empty?
|
979
|
+
|
980
|
+
# TBD only derates constructions of opaque surfaces in CONDITIONED spaces,
|
981
|
+
# ... if facing outdoors or facing UNENCLOSED/UNCONDITIONED spaces.
|
982
|
+
tbd[:surfaces].each do |id, surface|
|
983
|
+
surface[:deratable] = false
|
984
|
+
|
985
|
+
next unless surface[:conditioned]
|
986
|
+
next if surface[:ground]
|
987
|
+
|
988
|
+
unless surface[:boundary].downcase == "outdoors"
|
989
|
+
next unless tbd[:surfaces].key?(surface[:boundary])
|
990
|
+
next if tbd[:surfaces][surface[:boundary]][:conditioned]
|
991
|
+
end
|
992
|
+
|
993
|
+
ok = surface.key?(:index)
|
994
|
+
surface[:deratable] = true if ok
|
995
|
+
log(ERR, "Skipping '#{id}': insulating layer? (#{mth})") unless ok
|
996
|
+
end
|
997
|
+
|
998
|
+
[:windows, :doors, :skylights].each do |holes| # sort kids
|
999
|
+
tbd[:surfaces].values.each do |surface|
|
1000
|
+
ok = surface.key?(holes)
|
1001
|
+
surface[holes] = surface[holes].sort_by { |_, s| s[:minz] }.to_h if ok
|
1002
|
+
end
|
1003
|
+
end
|
1004
|
+
|
1005
|
+
# Split "surfaces" hash into "floors", "ceilings" and "walls" hashes.
|
1006
|
+
floors = tbd[:surfaces].select { |_, s| s[:type] == :floor }
|
1007
|
+
ceilings = tbd[:surfaces].select { |_, s| s[:type] == :ceiling }
|
1008
|
+
walls = tbd[:surfaces].select { |_, s| s[:type] == :wall }
|
1009
|
+
floors = floors.sort_by { |_, s| [s[:minz], s[:space]] }.to_h
|
1010
|
+
ceilings = ceilings.sort_by { |_, s| [s[:minz], s[:space]] }.to_h
|
1011
|
+
walls = walls.sort_by { |_, s| [s[:minz], s[:space]] }.to_h
|
1012
|
+
|
1013
|
+
# Fetch OpenStudio shading surfaces & key attributes.
|
1014
|
+
shades = {}
|
1015
|
+
|
1016
|
+
model.getShadingSurfaces.each do |s|
|
1017
|
+
id = s.nameString
|
1018
|
+
empty = s.shadingSurfaceGroup.empty?
|
1019
|
+
log(ERR, "Can't process '#{id}' transformation (#{mth})") if empty
|
1020
|
+
next if empty
|
1021
|
+
group = s.shadingSurfaceGroup.get
|
1022
|
+
shading = group.to_ShadingSurfaceGroup
|
1023
|
+
tr = transforms(model, group)
|
1024
|
+
ok = tr[:t] && tr[:r]
|
1025
|
+
t = tr[:t]
|
1026
|
+
log(FTL, "Can't process '#{id}' transformation (#{mth})") unless ok
|
1027
|
+
return tbd unless ok
|
1028
|
+
|
1029
|
+
unless shading.empty?
|
1030
|
+
empty = shading.get.space.empty?
|
1031
|
+
tr[:r] += shading.get.space.get.directionofRelativeNorth unless empty
|
1032
|
+
end
|
1033
|
+
|
1034
|
+
n = trueNormal(s, tr[:r])
|
1035
|
+
log(FTL, "Can't process '#{id}' true normal (#{mth})") unless n
|
1036
|
+
return tbd unless n
|
1037
|
+
|
1038
|
+
points = (t * s.vertices).map { |v| Topolys::Point3D.new(v.x, v.y, v.z) }
|
1039
|
+
minz = ( points.map { |p| p.z } ).min
|
1040
|
+
shades[id] = { group: group, points: points, minz: minz, n: n }
|
1041
|
+
end # shading surfaces populated
|
1042
|
+
|
1043
|
+
# Mutually populate TBD & Topolys surfaces. Keep track of created "holes".
|
1044
|
+
holes = {}
|
1045
|
+
floor_holes = dads(t_model, floors )
|
1046
|
+
ceiling_holes = dads(t_model, ceilings)
|
1047
|
+
wall_holes = dads(t_model, walls )
|
1048
|
+
|
1049
|
+
holes.merge!(floor_holes )
|
1050
|
+
holes.merge!(ceiling_holes)
|
1051
|
+
holes.merge!(wall_holes )
|
1052
|
+
dads(t_model, shades)
|
1053
|
+
|
1054
|
+
# Loop through Topolys edges and populate TBD edge hash. Initially, there
|
1055
|
+
# should be a one-to-one correspondence between Topolys and TBD edge
|
1056
|
+
# objects. Use Topolys-generated identifiers as unique edge hash keys.
|
1057
|
+
edges = {}
|
1058
|
+
|
1059
|
+
holes.each do |id, wire| # start with hole edges
|
1060
|
+
wire.edges.each do |e|
|
1061
|
+
i = e.id
|
1062
|
+
l = e.length
|
1063
|
+
ok = edges.key?(i)
|
1064
|
+
edges[i] = { length: l, v0: e.v0, v1: e.v1, surfaces: {} } unless ok
|
1065
|
+
ok = edges[i][:surfaces].key?(wire.attributes[:id])
|
1066
|
+
edges[i][:surfaces][wire.attributes[:id]] = { wire: wire.id } unless ok
|
1067
|
+
end
|
1068
|
+
end
|
1069
|
+
|
1070
|
+
# Next, floors, ceilings & walls; then shades.
|
1071
|
+
faces(floors, edges )
|
1072
|
+
faces(ceilings, edges)
|
1073
|
+
faces(walls, edges )
|
1074
|
+
faces(shades, edges )
|
1075
|
+
|
1076
|
+
# Generate OSM Kiva settings and objects if foundation-facing floors.
|
1077
|
+
# returns false if partial failure (log failure eventually).
|
1078
|
+
kiva(model, walls, floors, edges) if argh[:gen_kiva]
|
1079
|
+
|
1080
|
+
# Thermal bridging characteristics of edges are determined - in part - by
|
1081
|
+
# relative polar position of linked surfaces (or wires) around each edge.
|
1082
|
+
# This attribute is key in distinguishing concave from convex edges.
|
1083
|
+
#
|
1084
|
+
# For each linked surface (or rather surface wires), set polar position
|
1085
|
+
# around edge with respect to a reference vector (perpendicular to the
|
1086
|
+
# edge), +clockwise as one is looking in the opposite position of the edge
|
1087
|
+
# vector. For instance, a vertical edge has a reference vector pointing
|
1088
|
+
# North - surfaces eastward of the edge are (0°,180°], while surfaces
|
1089
|
+
# westward of the edge are (180°,360°].
|
1090
|
+
#
|
1091
|
+
# Much of the following code is of a topological nature, and should ideally
|
1092
|
+
# (or eventually) become available functionality offered by Topolys. Topolys
|
1093
|
+
# "wrappers" like TBD are good, short-term test beds to identify desired
|
1094
|
+
# features for future Topolys enhancements.
|
1095
|
+
zenith = Topolys::Vector3D.new(0, 0, 1).freeze
|
1096
|
+
north = Topolys::Vector3D.new(0, 1, 0).freeze
|
1097
|
+
east = Topolys::Vector3D.new(1, 0, 0).freeze
|
1098
|
+
|
1099
|
+
edges.values.each do |edge|
|
1100
|
+
origin = edge[:v0].point
|
1101
|
+
terminal = edge[:v1].point
|
1102
|
+
dx = (origin.x - terminal.x).abs
|
1103
|
+
dy = (origin.y - terminal.y).abs
|
1104
|
+
dz = (origin.z - terminal.z).abs
|
1105
|
+
horizontal = dz.abs < TOL
|
1106
|
+
vertical = dx < TOL && dy < TOL
|
1107
|
+
edge_V = terminal - origin
|
1108
|
+
edge_plane = Topolys::Plane3D.new(origin, edge_V)
|
1109
|
+
|
1110
|
+
if vertical
|
1111
|
+
reference_V = north.dup
|
1112
|
+
elsif horizontal
|
1113
|
+
reference_V = zenith.dup
|
1114
|
+
else # project zenith vector unto edge plane
|
1115
|
+
reference = edge_plane.project(origin + zenith)
|
1116
|
+
reference_V = reference - origin
|
1117
|
+
end
|
1118
|
+
|
1119
|
+
edge[:surfaces].each do |id, surface|
|
1120
|
+
# Loop through each linked wire and determine farthest point from
|
1121
|
+
# edge while ensuring candidate point is not aligned with edge.
|
1122
|
+
t_model.wires.each do |wire|
|
1123
|
+
if surface[:wire] == wire.id # there should be a unique match
|
1124
|
+
normal = tbd[:surfaces][id][:n] if tbd[:surfaces].key?(id)
|
1125
|
+
normal = holes[id].attributes[:n] if holes.key?(id)
|
1126
|
+
normal = shades[id][:n] if shades.key?(id)
|
1127
|
+
farthest = Topolys::Point3D.new(origin.x, origin.y, origin.z)
|
1128
|
+
farthest_V = farthest - origin # zero magnitude, initially
|
1129
|
+
inverted = false
|
1130
|
+
i_origin = wire.points.index(origin)
|
1131
|
+
i_terminal = wire.points.index(terminal)
|
1132
|
+
i_last = wire.points.size - 1
|
1133
|
+
|
1134
|
+
if i_terminal == 0
|
1135
|
+
inverted = true unless i_origin == i_last
|
1136
|
+
elsif i_origin == i_last
|
1137
|
+
inverted = true unless i_terminal == 0
|
1138
|
+
else
|
1139
|
+
inverted = true unless i_terminal - i_origin == 1
|
1140
|
+
end
|
1141
|
+
|
1142
|
+
wire.points.each do |point|
|
1143
|
+
next if point == origin
|
1144
|
+
next if point == terminal
|
1145
|
+
point_on_plane = edge_plane.project(point)
|
1146
|
+
origin_point_V = point_on_plane - origin
|
1147
|
+
point_V_magnitude = origin_point_V.magnitude
|
1148
|
+
next unless point_V_magnitude > TOL
|
1149
|
+
|
1150
|
+
# Generate a plane between origin, terminal & point. Only consider
|
1151
|
+
# planes that share the same normal as wire.
|
1152
|
+
if inverted
|
1153
|
+
plane = Topolys::Plane3D.from_points(terminal, origin, point)
|
1154
|
+
else
|
1155
|
+
plane = Topolys::Plane3D.from_points(origin, terminal, point)
|
1156
|
+
end
|
1157
|
+
|
1158
|
+
next unless (normal.x - plane.normal.x).abs < TOL &&
|
1159
|
+
(normal.y - plane.normal.y).abs < TOL &&
|
1160
|
+
(normal.z - plane.normal.z).abs < TOL
|
1161
|
+
|
1162
|
+
farther = point_V_magnitude > farthest_V.magnitude
|
1163
|
+
farthest = point if farther
|
1164
|
+
farthest_V = origin_point_V if farther
|
1165
|
+
end
|
1166
|
+
|
1167
|
+
angle = reference_V.angle(farthest_V)
|
1168
|
+
adjust = false # adjust angle [180°, 360°] if necessary
|
1169
|
+
|
1170
|
+
if vertical
|
1171
|
+
adjust = true if east.dot(farthest_V) < -TOL
|
1172
|
+
else
|
1173
|
+
if north.dot(farthest_V).abs < TOL ||
|
1174
|
+
(north.dot(farthest_V).abs - 1).abs < TOL
|
1175
|
+
adjust = true if east.dot(farthest_V) < -TOL
|
1176
|
+
else
|
1177
|
+
adjust = true if north.dot(farthest_V) < -TOL
|
1178
|
+
end
|
1179
|
+
end
|
1180
|
+
|
1181
|
+
angle = 2 * Math::PI - angle if adjust
|
1182
|
+
angle -= 2 * Math::PI if (angle - 2 * Math::PI).abs < TOL
|
1183
|
+
surface[:angle] = angle
|
1184
|
+
farthest_V.normalize!
|
1185
|
+
surface[:polar] = farthest_V
|
1186
|
+
surface[:normal] = normal
|
1187
|
+
end
|
1188
|
+
end # end of edge-linked, surface-to-wire loop
|
1189
|
+
end # end of edge-linked surface loop
|
1190
|
+
|
1191
|
+
edge[:horizontal] = horizontal
|
1192
|
+
edge[:vertical ] = vertical
|
1193
|
+
edge[:surfaces ] = edge[:surfaces].sort_by{ |i, p| p[:angle] }.to_h
|
1194
|
+
end # end of edge loop
|
1195
|
+
|
1196
|
+
# Topolys edges may constitute thermal bridges (and therefore thermally
|
1197
|
+
# derate linked OpenStudio opaque surfaces), depending on a number of
|
1198
|
+
# factors such as surface type, space conditioning and boundary conditions.
|
1199
|
+
# Thermal bridging attributes (type & PSI-value pairs) are grouped into PSI
|
1200
|
+
# sets, normally accessed through the :option user argument (in the
|
1201
|
+
# OpenStudio Measure interface).
|
1202
|
+
#
|
1203
|
+
# Process user-defined TBD JSON file inputs if file exists & valid:
|
1204
|
+
# :io holds valid TBD JSON file entries
|
1205
|
+
# :psi holds TBD PSI sets (built-in defaults + those on file)
|
1206
|
+
# :khi holds TBD KHI points (built-in defaults + those on file)
|
1207
|
+
#
|
1208
|
+
# Without an input JSON file, a valid 'json' Hash simply holds:
|
1209
|
+
# :io[:building][:psi] ... a single valid, default PSI set for all edges
|
1210
|
+
# :psi ... built-in TBD PSI sets
|
1211
|
+
# :khi ... built-in TBD KHI points
|
1212
|
+
json = inputs(tbd[:surfaces], edges, argh)
|
1213
|
+
|
1214
|
+
# A user-defined TBD JSON input file can hold a number of anomalies that
|
1215
|
+
# won't affect results, such as custom PSI sets that aren't referenced
|
1216
|
+
# elsewhere (similar to OpenStudio materials on file that aren't referenced
|
1217
|
+
# by any OpenStudio construction). This may trigger 'warnings' in the log
|
1218
|
+
# file, but they're in principle benign.
|
1219
|
+
#
|
1220
|
+
# A user-defined JSON input file can instead hold a number of more serious
|
1221
|
+
# anomalies that risk generating erroneous or unintended results. They're
|
1222
|
+
# logged as well, yet it remains up to the user to decide how serious a risk
|
1223
|
+
# this may be. If a custom edge is defined on file (e.g., "expansion joint"
|
1224
|
+
# thermal bridge instead of a "transition") yet TBD is unable to match
|
1225
|
+
# it against OpenStudio and/or Topolys edges (or surfaces), then TBD
|
1226
|
+
# will log this as an error while simply 'skipping' the anomaly (TBD will
|
1227
|
+
# otherwise ignore the requested change and pursue its processes).
|
1228
|
+
#
|
1229
|
+
# There are 2 types of errors that are considered FATAL when processing
|
1230
|
+
# user-defined TBD JSON input files:
|
1231
|
+
# - incorrect JSON formatting of the input file (can't parse)
|
1232
|
+
# - TBD is unable to identify a 'complete' building-level PSI set
|
1233
|
+
# (either a bad argument from the Measure, or bad input on file).
|
1234
|
+
#
|
1235
|
+
# ... in such circumstances, TBD will halt all processes and exit while
|
1236
|
+
# signaling to OpenStudio to halt its own processes (e.g., not launch an
|
1237
|
+
# EnergyPlus simulation). This is similar to accessing an invalid .osm file.
|
1238
|
+
return tbd if fatal?
|
1239
|
+
|
1240
|
+
psi = json[:io][:building][:psi] # default building PSI on file
|
1241
|
+
shorts = json[:psi].shorthands(psi)
|
1242
|
+
empty = shorts[:has].empty? || shorts[:val].empty?
|
1243
|
+
log(FTL, "Invalid or incomplete building PSI set (#{mth})") if empty
|
1244
|
+
return tbd if empty
|
1245
|
+
|
1246
|
+
edges.values.each do |edge|
|
1247
|
+
next unless edge.key?(:surfaces)
|
1248
|
+
deratables = []
|
1249
|
+
|
1250
|
+
edge[:surfaces].keys.each do |id|
|
1251
|
+
next unless tbd[:surfaces].key?(id)
|
1252
|
+
deratables << id if tbd[:surfaces][id][:deratable]
|
1253
|
+
end
|
1254
|
+
|
1255
|
+
next if deratables.empty?
|
1256
|
+
set = {}
|
1257
|
+
|
1258
|
+
if edge.key?(:io_type)
|
1259
|
+
bdg = json[:psi].safe(psi, edge[:io_type]) # building safe type fallback
|
1260
|
+
edge[:sets] = {} unless edge.key?(:sets)
|
1261
|
+
edge[:sets][edge[:io_type]] = shorts[:val][bdg] # building safe fallback
|
1262
|
+
set[edge[:io_type]] = shorts[:val][bdg]
|
1263
|
+
edge[:psi] = set
|
1264
|
+
|
1265
|
+
if edge.key?(:io_set) && json[:psi].set.key?(edge[:io_set])
|
1266
|
+
type = json[:psi].safe(edge[:io_set], edge[:io_type])
|
1267
|
+
edge[:set] = edge[:io_set] if type
|
1268
|
+
end
|
1269
|
+
|
1270
|
+
match = true
|
1271
|
+
end
|
1272
|
+
|
1273
|
+
edge[:surfaces].keys.each do |id|
|
1274
|
+
break if match
|
1275
|
+
next unless tbd[:surfaces].key?(id)
|
1276
|
+
next unless deratables.include?(id)
|
1277
|
+
|
1278
|
+
# Evaluate current set content before processing a new linked surface.
|
1279
|
+
is = {}
|
1280
|
+
is[:head ] = set.keys.to_s.include?("head" )
|
1281
|
+
is[:sill ] = set.keys.to_s.include?("sill" )
|
1282
|
+
is[:jamb ] = set.keys.to_s.include?("jamb" )
|
1283
|
+
is[:corner ] = set.keys.to_s.include?("corner" )
|
1284
|
+
is[:parapet ] = set.keys.to_s.include?("parapet" )
|
1285
|
+
is[:party ] = set.keys.to_s.include?("party" )
|
1286
|
+
is[:grade ] = set.keys.to_s.include?("grade" )
|
1287
|
+
is[:balcony ] = set.keys.to_s.include?("balcony" )
|
1288
|
+
is[:rimjoist] = set.keys.to_s.include?("rimjoist")
|
1289
|
+
|
1290
|
+
# Label edge as :head, :sill or :jamb if linked to:
|
1291
|
+
# 1x subsurface
|
1292
|
+
edge[:surfaces].keys.each do |i|
|
1293
|
+
break if is[:head] || is[:sill] || is[:jamb]
|
1294
|
+
next if deratables.include?(i)
|
1295
|
+
next unless holes.key?(i)
|
1296
|
+
|
1297
|
+
gardian = ""
|
1298
|
+
gardian = id if deratables.size == 1 # just dad
|
1299
|
+
|
1300
|
+
if gardian.empty? # seek uncle
|
1301
|
+
pops = {} # kids?
|
1302
|
+
uncles = {} # nieces?
|
1303
|
+
boys = [] # kids
|
1304
|
+
nieces = [] # nieces
|
1305
|
+
uncle = deratables.first unless deratables.first == id # uncle 1st?
|
1306
|
+
uncle = deratables.last unless deratables.last == id # uncle 2nd?
|
1307
|
+
|
1308
|
+
pops[:w ] = tbd[:surfaces][id ].key?(:windows )
|
1309
|
+
pops[:d ] = tbd[:surfaces][id ].key?(:doors )
|
1310
|
+
pops[:s ] = tbd[:surfaces][id ].key?(:skylights)
|
1311
|
+
uncles[:w] = tbd[:surfaces][uncle].key?(:windows )
|
1312
|
+
uncles[:d] = tbd[:surfaces][uncle].key?(:doors )
|
1313
|
+
uncles[:s] = tbd[:surfaces][uncle].key?(:skylights)
|
1314
|
+
|
1315
|
+
boys += tbd[:surfaces][id ][:windows ].keys if pops[:w]
|
1316
|
+
boys += tbd[:surfaces][id ][:doors ].keys if pops[:d]
|
1317
|
+
boys += tbd[:surfaces][id ][:skylights].keys if pops[:s]
|
1318
|
+
nieces += tbd[:surfaces][uncle][:windows ].keys if uncles[:w]
|
1319
|
+
nieces += tbd[:surfaces][uncle][:doors ].keys if uncles[:d]
|
1320
|
+
nieces += tbd[:surfaces][uncle][:skylights].keys if uncles[:s]
|
1321
|
+
|
1322
|
+
gardian = uncle if boys.include?(i)
|
1323
|
+
gardian = id if nieces.include?(i)
|
1324
|
+
end
|
1325
|
+
|
1326
|
+
next if gardian.empty?
|
1327
|
+
s1 = edge[:surfaces][gardian]
|
1328
|
+
s2 = edge[:surfaces][i]
|
1329
|
+
concave = concave?(s1, s2)
|
1330
|
+
convex = convex?(s1, s2)
|
1331
|
+
flat = !concave && !convex
|
1332
|
+
|
1333
|
+
# Subsurface edges are tagged as :head, :sill or :jamb, regardless
|
1334
|
+
# of building PSI set subsurface tags. If the latter is simply
|
1335
|
+
# :fenestration, then its (single) PSI value is systematically
|
1336
|
+
# attributed to subsurface :head, :sill & :jamb edges. If absent,
|
1337
|
+
# concave or convex variants also inherit from base type.
|
1338
|
+
#
|
1339
|
+
# TBD tags a subsurface edge as :jamb if the subsurface is "flat".
|
1340
|
+
# If not flat, TBD tags a horizontal edge as either :head or :sill
|
1341
|
+
# based on the polar angle of the subsurface around the edge vs sky
|
1342
|
+
# zenith. Otherwise, all other subsurface edges are tagged as :jamb.
|
1343
|
+
if ((s2[:normal].dot(zenith)).abs - 1).abs < TOL
|
1344
|
+
set[:jamb ] = shorts[:val][:jamb ] if flat
|
1345
|
+
set[:jambconcave] = shorts[:val][:jambconcave] if concave
|
1346
|
+
set[:jambconvex ] = shorts[:val][:jambconvex ] if convex
|
1347
|
+
is[:jamb ] = true
|
1348
|
+
else
|
1349
|
+
if edge[:horizontal]
|
1350
|
+
if s2[:polar].dot(zenith) < 0
|
1351
|
+
set[:head ] = shorts[:val][:head ] if flat
|
1352
|
+
set[:headconcave] = shorts[:val][:headconcave] if concave
|
1353
|
+
set[:headconvex ] = shorts[:val][:headconvex ] if convex
|
1354
|
+
is[:head ] = true
|
1355
|
+
else
|
1356
|
+
set[:sill ] = shorts[:val][:sill ] if flat
|
1357
|
+
set[:sillconcave] = shorts[:val][:sillconcave] if concave
|
1358
|
+
set[:sillconvex ] = shorts[:val][:sillconvex ] if convex
|
1359
|
+
is[:sill ] = true
|
1360
|
+
end
|
1361
|
+
else
|
1362
|
+
set[:jamb ] = shorts[:val][:jamb ] if flat
|
1363
|
+
set[:jambconcave] = shorts[:val][:jambconcave] if concave
|
1364
|
+
set[:jambconvex ] = shorts[:val][:jambconvex ] if convex
|
1365
|
+
is[:jamb ] = true
|
1366
|
+
end
|
1367
|
+
end
|
1368
|
+
end
|
1369
|
+
|
1370
|
+
# Label edge as :cornerconcave or :cornerconvex if linked to:
|
1371
|
+
# 2x deratable walls & f(relative polar wall vectors around edge)
|
1372
|
+
edge[:surfaces].keys.each do |i|
|
1373
|
+
break if is[:corner]
|
1374
|
+
break unless deratables.size == 2
|
1375
|
+
break unless walls.key?(id)
|
1376
|
+
next if i == id
|
1377
|
+
next unless deratables.include?(i)
|
1378
|
+
next unless walls.key?(i)
|
1379
|
+
|
1380
|
+
s1 = edge[:surfaces][id]
|
1381
|
+
s2 = edge[:surfaces][i]
|
1382
|
+
concave = concave?(s1, s2)
|
1383
|
+
convex = convex?(s1, s2)
|
1384
|
+
|
1385
|
+
set[:cornerconcave] = shorts[:val][:cornerconcave] if concave
|
1386
|
+
set[:cornerconvex ] = shorts[:val][:cornerconvex ] if convex
|
1387
|
+
is[:corner ] = true
|
1388
|
+
end
|
1389
|
+
|
1390
|
+
# Label edge as :parapet if linked to:
|
1391
|
+
# 1x deratable wall
|
1392
|
+
# 1x deratable ceiling
|
1393
|
+
edge[:surfaces].keys.each do |i|
|
1394
|
+
break if is[:parapet]
|
1395
|
+
break unless deratables.size == 2
|
1396
|
+
break unless ceilings.key?(id)
|
1397
|
+
next if i == id
|
1398
|
+
next unless deratables.include?(i)
|
1399
|
+
next unless walls.key?(i)
|
1400
|
+
|
1401
|
+
s1 = edge[:surfaces][id]
|
1402
|
+
s2 = edge[:surfaces][i]
|
1403
|
+
concave = concave?(s1, s2)
|
1404
|
+
convex = convex?(s1, s2)
|
1405
|
+
flat = !concave && !convex
|
1406
|
+
|
1407
|
+
set[:parapet ] = shorts[:val][:parapet ] if flat
|
1408
|
+
set[:parapetconcave] = shorts[:val][:parapetconcave] if concave
|
1409
|
+
set[:parapetconvex ] = shorts[:val][:parapetconvex ] if convex
|
1410
|
+
is[:parapet ] = true
|
1411
|
+
end
|
1412
|
+
|
1413
|
+
# Label edge as :party if linked to:
|
1414
|
+
# 1x adiabatic surface
|
1415
|
+
# 1x (only) deratable surface
|
1416
|
+
edge[:surfaces].keys.each do |i|
|
1417
|
+
break if is[:party]
|
1418
|
+
break unless deratables.size == 1
|
1419
|
+
next if i == id
|
1420
|
+
next unless tbd[:surfaces].key?(i)
|
1421
|
+
next if holes.key?(i)
|
1422
|
+
next if shades.key?(i)
|
1423
|
+
next unless tbd[:surfaces][i][:boundary].downcase == "adiabatic"
|
1424
|
+
|
1425
|
+
s1 = edge[:surfaces][id]
|
1426
|
+
s2 = edge[:surfaces][i]
|
1427
|
+
concave = concave?(s1, s2)
|
1428
|
+
convex = convex?(s1, s2)
|
1429
|
+
flat = !concave && !convex
|
1430
|
+
|
1431
|
+
set[:party ] = shorts[:val][:party ] if flat
|
1432
|
+
set[:partyconcave] = shorts[:val][:partyconcave] if concave
|
1433
|
+
set[:partyconvex ] = shorts[:val][:partyconvex ] if convex
|
1434
|
+
is[:party ] = true
|
1435
|
+
end
|
1436
|
+
|
1437
|
+
# Label edge as :grade if linked to:
|
1438
|
+
# 1x surface (e.g. slab or wall) facing ground
|
1439
|
+
# 1x surface (i.e. wall) facing outdoors
|
1440
|
+
edge[:surfaces].keys.each do |i|
|
1441
|
+
break if is[:grade]
|
1442
|
+
break unless deratables.size == 1
|
1443
|
+
next if i == id
|
1444
|
+
next unless tbd[:surfaces].key?(i)
|
1445
|
+
next unless tbd[:surfaces][i].key?(:ground)
|
1446
|
+
next unless tbd[:surfaces][i][:ground]
|
1447
|
+
|
1448
|
+
s1 = edge[:surfaces][id]
|
1449
|
+
s2 = edge[:surfaces][i]
|
1450
|
+
concave = concave?(s1, s2)
|
1451
|
+
convex = convex?(s1, s2)
|
1452
|
+
flat = !concave && !convex
|
1453
|
+
|
1454
|
+
set[:grade ] = shorts[:val][:grade ] if flat
|
1455
|
+
set[:gradeconcave] = shorts[:val][:gradeconcave] if concave
|
1456
|
+
set[:gradeconvex ] = shorts[:val][:gradeconvex ] if convex
|
1457
|
+
is[:grade ] = true
|
1458
|
+
end
|
1459
|
+
|
1460
|
+
# Label edge as :rimjoist (or :balcony) if linked to:
|
1461
|
+
# 1x deratable surface
|
1462
|
+
# 1x CONDITIONED floor
|
1463
|
+
# 1x shade (optional)
|
1464
|
+
balcony = false
|
1465
|
+
|
1466
|
+
edge[:surfaces].keys.each do |i|
|
1467
|
+
break if balcony
|
1468
|
+
next if i == id
|
1469
|
+
balcony = true if shades.key?(i)
|
1470
|
+
end
|
1471
|
+
|
1472
|
+
edge[:surfaces].keys.each do |i|
|
1473
|
+
break if is[:rimjoist] || is[:balcony]
|
1474
|
+
break unless deratables.size == 2
|
1475
|
+
break if floors.key?(id)
|
1476
|
+
next if i == id
|
1477
|
+
next unless floors.key?(i)
|
1478
|
+
next unless floors[i].key?(:conditioned)
|
1479
|
+
next unless floors[i][:conditioned]
|
1480
|
+
next if floors[i][:ground]
|
1481
|
+
|
1482
|
+
other = deratables.first unless deratables.first == id
|
1483
|
+
other = deratables.last unless deratables.last == id
|
1484
|
+
|
1485
|
+
s1 = edge[:surfaces][id]
|
1486
|
+
s2 = edge[:surfaces][other]
|
1487
|
+
concave = concave?(s1, s2)
|
1488
|
+
convex = convex?(s1, s2)
|
1489
|
+
flat = !concave && !convex
|
1490
|
+
|
1491
|
+
if balcony
|
1492
|
+
set[:balcony ] = shorts[:val][:balcony ] if flat
|
1493
|
+
set[:balconyconcave ] = shorts[:val][:balconyconcave ] if concave
|
1494
|
+
set[:balconyconvex ] = shorts[:val][:balconyconvex ] if convex
|
1495
|
+
is[:balcony ] = true
|
1496
|
+
else
|
1497
|
+
set[:rimjoist ] = shorts[:val][:rimjoist ] if flat
|
1498
|
+
set[:rimjoistconcave] = shorts[:val][:rimjoistconcave] if concave
|
1499
|
+
set[:rimjoistconvex ] = shorts[:val][:rimjoistconvex ] if convex
|
1500
|
+
is[:rimjoist ] = true
|
1501
|
+
end
|
1502
|
+
end
|
1503
|
+
end # edge's surfaces loop
|
1504
|
+
|
1505
|
+
edge[:psi] = set unless set.empty?
|
1506
|
+
edge[:set] = psi unless set.empty?
|
1507
|
+
end # edge loop
|
1508
|
+
|
1509
|
+
# Tracking (mild) transitions between deratable surfaces around edges that
|
1510
|
+
# have not been previously tagged.
|
1511
|
+
edges.values.each do |edge|
|
1512
|
+
next if edge.key?(:psi)
|
1513
|
+
next unless edge.key?(:surfaces)
|
1514
|
+
deratable = false
|
1515
|
+
|
1516
|
+
edge[:surfaces].keys.each do |id|
|
1517
|
+
next unless tbd[:surfaces].key?(id)
|
1518
|
+
next unless tbd[:surfaces][id][:deratable]
|
1519
|
+
deratable = tbd[:surfaces][id][:deratable]
|
1520
|
+
end
|
1521
|
+
|
1522
|
+
next unless deratable
|
1523
|
+
edge[:psi] = { transition: 0.000 }
|
1524
|
+
edge[:set] = json[:io][:building][:psi]
|
1525
|
+
end
|
1526
|
+
|
1527
|
+
# 'Unhinged' subsurfaces, like Tubular Daylight Device (TDD) domes,
|
1528
|
+
# usually don't share edges with parent surfaces, e.g. floating 300mm above
|
1529
|
+
# parent roof surface. Add parent surface ID to unhinged edges.
|
1530
|
+
edges.values.each do |edge|
|
1531
|
+
next if edge.key?(:psi)
|
1532
|
+
next unless edge.key?(:surfaces)
|
1533
|
+
next unless edge[:surfaces].size == 1
|
1534
|
+
id = edge[:surfaces].first.first
|
1535
|
+
next unless holes.key?(id)
|
1536
|
+
next unless holes[id].attributes.key?(:unhinged)
|
1537
|
+
next unless holes[id].attributes[:unhinged]
|
1538
|
+
|
1539
|
+
subsurface = model.getSubSurfaceByName(id)
|
1540
|
+
next if subsurface.empty?
|
1541
|
+
subsurface = subsurface.get
|
1542
|
+
surface = subsurface.surface
|
1543
|
+
next if surface.empty?
|
1544
|
+
nom = surface.get.nameString
|
1545
|
+
next unless tbd[:surfaces].key?(nom)
|
1546
|
+
next unless tbd[:surfaces][nom].key?(:conditioned)
|
1547
|
+
next unless tbd[:surfaces][nom][:conditioned]
|
1548
|
+
|
1549
|
+
edge[:surfaces][nom] = {}
|
1550
|
+
|
1551
|
+
set = {}
|
1552
|
+
set[:jamb] = shorts[:val][:jamb]
|
1553
|
+
edge[:psi] = set
|
1554
|
+
edge[:set] = json[:io][:building][:psi]
|
1555
|
+
end
|
1556
|
+
|
1557
|
+
# A priori, TBD applies (default) :building PSI types and values to
|
1558
|
+
# individual edges. If a TBD JSON input file holds custom PSI sets for:
|
1559
|
+
# :stories
|
1560
|
+
# :spacetypes
|
1561
|
+
# :surfaces
|
1562
|
+
# :edges
|
1563
|
+
# ... that may apply to individual edges, then the default :building PSI
|
1564
|
+
# types and/or values are overridden, as follows:
|
1565
|
+
# custom :stories PSI sets trump :building PSI sets
|
1566
|
+
# custom :spacetypes PSI sets trump aforementioned PSI sets
|
1567
|
+
# custom :spaces PSI sets trump aforementioned PSI sets
|
1568
|
+
# custom :surfaces PSI sets trump aforementioned PSI sets
|
1569
|
+
# custom :edges PSI sets trump aforementioned PSI sets
|
1570
|
+
if json[:io]
|
1571
|
+
if json[:io].key?(:subsurfaces) # reset subsurface U-factors (if on file)
|
1572
|
+
json[:io][:subsurfaces].each do |sub|
|
1573
|
+
next unless sub.key?(:id)
|
1574
|
+
next unless sub.key?(:usi)
|
1575
|
+
match = false
|
1576
|
+
|
1577
|
+
tbd[:surfaces].values.each do |surface|
|
1578
|
+
break if match
|
1579
|
+
|
1580
|
+
[:windows, :doors, :skylights].each do |types|
|
1581
|
+
if surface.key?(types)
|
1582
|
+
surface[types].each do |id, opening|
|
1583
|
+
break if match
|
1584
|
+
next unless opening.key?(:u)
|
1585
|
+
match = true if sub[:id] == id
|
1586
|
+
opening[:u] = sub[:usi] if sub[:id] == id
|
1587
|
+
end
|
1588
|
+
end
|
1589
|
+
end
|
1590
|
+
end
|
1591
|
+
end
|
1592
|
+
end
|
1593
|
+
|
1594
|
+
[:stories, :spacetypes, :spaces].each do |groups|
|
1595
|
+
key = :story
|
1596
|
+
key = :stype if groups == :spacetypes
|
1597
|
+
key = :space if groups == :spaces
|
1598
|
+
next unless json[:io].key?(groups)
|
1599
|
+
|
1600
|
+
json[:io][groups].each do |group|
|
1601
|
+
next unless group.key?(:id)
|
1602
|
+
next unless group.key?(:psi)
|
1603
|
+
next unless json[:psi].set.key?(group[:psi])
|
1604
|
+
sh = json[:psi].shorthands(group[:psi])
|
1605
|
+
next if sh[:val].empty?
|
1606
|
+
|
1607
|
+
edges.values.each do |edge|
|
1608
|
+
next if edge.key?(:io_set)
|
1609
|
+
next unless edge.key?(:psi)
|
1610
|
+
next unless edge.key?(:surfaces)
|
1611
|
+
|
1612
|
+
edge[:surfaces].keys.each do |id|
|
1613
|
+
next unless tbd[:surfaces].key?(id)
|
1614
|
+
next unless tbd[:surfaces][id].key?(key)
|
1615
|
+
next unless group[:id] == tbd[:surfaces][id][key].nameString
|
1616
|
+
|
1617
|
+
edge[groups] = {} unless edge.key?(groups)
|
1618
|
+
edge[groups][group[:psi]] = {}
|
1619
|
+
set = {}
|
1620
|
+
|
1621
|
+
if edge.key?(:io_type)
|
1622
|
+
safer = json[:psi].safe(group[:psi], edge[:io_type])
|
1623
|
+
set[edge[:io_type]] = sh[:val][safer] if safer
|
1624
|
+
else
|
1625
|
+
edge[:psi].keys.each do |type|
|
1626
|
+
safer = json[:psi].safe(group[:psi], type)
|
1627
|
+
set[type] = sh[:val][safer] if safer
|
1628
|
+
end
|
1629
|
+
end
|
1630
|
+
|
1631
|
+
edge[groups][group[:psi]] = set unless set.empty?
|
1632
|
+
end
|
1633
|
+
end
|
1634
|
+
end
|
1635
|
+
|
1636
|
+
# TBD/Topolys edges will generally be linked to more than one surface
|
1637
|
+
# and hence to more than one story. It is possible for a TBD JSON file
|
1638
|
+
# to hold 2x story PSI sets that end up targetting one or more edges
|
1639
|
+
# common to both stories. In such cases, TBD retains the most conductive
|
1640
|
+
# PSI type/value from either story PSI set.
|
1641
|
+
edges.values.each do |edge|
|
1642
|
+
next unless edge.key?(:psi)
|
1643
|
+
next unless edge.key?(groups)
|
1644
|
+
|
1645
|
+
edge[:psi].keys.each do |type|
|
1646
|
+
vals = {}
|
1647
|
+
|
1648
|
+
edge[groups].keys.each do |set|
|
1649
|
+
sh = json[:psi].shorthands(set)
|
1650
|
+
next if sh[:val].empty?
|
1651
|
+
safer = json[:psi].safe(set, type)
|
1652
|
+
vals[set] = sh[:val][safer] if safer
|
1653
|
+
end
|
1654
|
+
|
1655
|
+
next if vals.empty?
|
1656
|
+
edge[:psi ][type] = vals.values.max
|
1657
|
+
edge[:sets] = {} unless edge.key?(:sets)
|
1658
|
+
edge[:sets][type] = vals.key(vals.values.max)
|
1659
|
+
end
|
1660
|
+
end
|
1661
|
+
end
|
1662
|
+
|
1663
|
+
if json[:io].key?(:surfaces)
|
1664
|
+
json[:io][:surfaces].each do |surface|
|
1665
|
+
next unless surface.key?(:id)
|
1666
|
+
next unless surface.key?(:psi)
|
1667
|
+
next unless json[:psi].set.key?(surface[:psi])
|
1668
|
+
sh = json[:psi].shorthands(surface[:psi])
|
1669
|
+
next if sh[:val].empty?
|
1670
|
+
|
1671
|
+
edges.values.each do |edge|
|
1672
|
+
next if edge.key?(:io_set)
|
1673
|
+
next unless edge.key?(:psi)
|
1674
|
+
next unless edge.key?(:surfaces)
|
1675
|
+
|
1676
|
+
edge[:surfaces].each do |id, s|
|
1677
|
+
next unless tbd[:surfaces].key?(id)
|
1678
|
+
next unless surface[:id] == id
|
1679
|
+
set = {}
|
1680
|
+
|
1681
|
+
if edge.key?(:io_type)
|
1682
|
+
safer = json[:psi].safe(surface[:psi], edge[:io_type])
|
1683
|
+
set[:io_type] = sh[:val][safer] if safer
|
1684
|
+
else
|
1685
|
+
edge[:psi].keys.each do |type|
|
1686
|
+
safer = json[:psi].safe(surface[:psi], type)
|
1687
|
+
set[type] = sh[:val][safer] if safer
|
1688
|
+
end
|
1689
|
+
end
|
1690
|
+
|
1691
|
+
s[:psi] = set unless set.empty?
|
1692
|
+
s[:set] = surface[:psi] unless set.empty?
|
1693
|
+
end
|
1694
|
+
end
|
1695
|
+
end
|
1696
|
+
|
1697
|
+
# TBD/Topolys edges will generally be linked to more than one surface. A
|
1698
|
+
# TBD JSON file may hold 2x surface PSI sets that target a shared edge.
|
1699
|
+
# TBD retains the most conductive PSI type/value from either set.
|
1700
|
+
edges.values.each do |edge|
|
1701
|
+
next unless edge.key?(:psi)
|
1702
|
+
next unless edge.key?(:surfaces)
|
1703
|
+
|
1704
|
+
edge[:psi].keys.each do |type|
|
1705
|
+
vals = {}
|
1706
|
+
|
1707
|
+
edge[:surfaces].each do |id, s|
|
1708
|
+
next unless s.key?(:psi)
|
1709
|
+
next unless s.key?(:set)
|
1710
|
+
next if s[:set].empty?
|
1711
|
+
sh = json[:psi].shorthands(s[:set])
|
1712
|
+
next if sh[:val].empty?
|
1713
|
+
safer = json[:psi].safe(s[:set], type)
|
1714
|
+
vals[s[:set]] = sh[:val][safer] if safer
|
1715
|
+
end
|
1716
|
+
|
1717
|
+
next if vals.empty?
|
1718
|
+
edge[:psi][type] = vals.values.max
|
1719
|
+
edge[:sets] = {} unless edge.key?(:sets)
|
1720
|
+
edge[:sets][type] = vals.key(vals.values.max)
|
1721
|
+
end
|
1722
|
+
end
|
1723
|
+
end
|
1724
|
+
|
1725
|
+
# Loop through all customized edges on file w/w/o a custom PSI set.
|
1726
|
+
edges.values.each do |edge|
|
1727
|
+
next unless edge.key?(:psi)
|
1728
|
+
next unless edge.key?(:io_type)
|
1729
|
+
next unless edge.key?(:surfaces)
|
1730
|
+
|
1731
|
+
if edge.key?(:io_set)
|
1732
|
+
next unless json[:psi].set.key?(edge[:io_set])
|
1733
|
+
set = edge[:io_set]
|
1734
|
+
else
|
1735
|
+
next unless edge[:sets].key?(edge[:io_type])
|
1736
|
+
next unless json[:psi].set.key?(edge[:sets][edge[:io_type]])
|
1737
|
+
set = edge[:sets][edge[:io_type]]
|
1738
|
+
end
|
1739
|
+
|
1740
|
+
sh = json[:psi].shorthands(set)
|
1741
|
+
next if sh[:val].empty?
|
1742
|
+
safer = json[:psi].safe(set, edge[:io_type])
|
1743
|
+
next unless safer
|
1744
|
+
|
1745
|
+
if edge.key?(:io_set)
|
1746
|
+
edge[:psi] = {}
|
1747
|
+
edge[:set] = edge[:io_set]
|
1748
|
+
else
|
1749
|
+
edge[:sets] = {} unless edge.key?(:sets)
|
1750
|
+
edge[:sets][edge[:io_type]] = sh[:val][safer]
|
1751
|
+
end
|
1752
|
+
|
1753
|
+
edge[:psi][edge[:io_type]] = sh[:val][safer]
|
1754
|
+
end
|
1755
|
+
end
|
1756
|
+
|
1757
|
+
# Loop through each edge and assign heat loss to linked surfaces.
|
1758
|
+
edges.each do |identifier, edge|
|
1759
|
+
next unless edge.key?(:psi)
|
1760
|
+
rsi = 0
|
1761
|
+
max = edge[:psi].values.max
|
1762
|
+
type = edge[:psi].key(max)
|
1763
|
+
length = edge[:length]
|
1764
|
+
bridge = { psi: max, type: type, length: length }
|
1765
|
+
deratables = {}
|
1766
|
+
apertures = {}
|
1767
|
+
|
1768
|
+
if edge.key?(:sets) && edge[:sets].key?(type)
|
1769
|
+
edge[:set] = edge[:sets][type] unless edge.key?(:io_set)
|
1770
|
+
end
|
1771
|
+
|
1772
|
+
# Retrieve valid linked surfaces as deratables.
|
1773
|
+
edge[:surfaces].each do |id, s|
|
1774
|
+
next unless tbd[:surfaces].key?(id)
|
1775
|
+
next unless tbd[:surfaces][id][:deratable]
|
1776
|
+
deratables[id] = s
|
1777
|
+
end
|
1778
|
+
|
1779
|
+
edge[:surfaces].each { |id, s| apertures[id] = s if holes.key?(id) }
|
1780
|
+
next if apertures.size > 1 # edge links 2x openings
|
1781
|
+
|
1782
|
+
# Prune dad if edge links an opening, its dad and an uncle.
|
1783
|
+
if deratables.size > 1 && apertures.size > 0
|
1784
|
+
deratables.each do |id, deratable|
|
1785
|
+
[:windows, :doors, :skylights].each do |types|
|
1786
|
+
next unless tbd[:surfaces][id].key?(types)
|
1787
|
+
|
1788
|
+
tbd[:surfaces][id][types].keys.each do |sub|
|
1789
|
+
deratables.delete(id) if apertures.key?(sub)
|
1790
|
+
end
|
1791
|
+
end
|
1792
|
+
end
|
1793
|
+
end
|
1794
|
+
|
1795
|
+
next if deratables.empty?
|
1796
|
+
|
1797
|
+
# Sum RSI of targeted insulating layer from each deratable surface.
|
1798
|
+
deratables.each do |id, deratable|
|
1799
|
+
next unless tbd[:surfaces][id].key?(:r)
|
1800
|
+
rsi += tbd[:surfaces][id][:r]
|
1801
|
+
end
|
1802
|
+
|
1803
|
+
# Assign heat loss from thermal bridges to surfaces, in proportion to
|
1804
|
+
# insulating layer thermal resistance.
|
1805
|
+
deratables.each do |id, deratable|
|
1806
|
+
ratio = 0
|
1807
|
+
ratio = tbd[:surfaces][id][:r] / rsi if rsi > 0.001
|
1808
|
+
loss = bridge[:psi] * ratio
|
1809
|
+
b = { psi: loss, type: bridge[:type], length: length, ratio: ratio }
|
1810
|
+
tbd[:surfaces][id][:edges] = {} unless tbd[:surfaces][id].key?(:edges)
|
1811
|
+
tbd[:surfaces][id][:edges][identifier] = b
|
1812
|
+
end
|
1813
|
+
end
|
1814
|
+
|
1815
|
+
# Assign thermal bridging heat loss [in W/K] to each deratable surface.
|
1816
|
+
tbd[:surfaces].each do |id, surface|
|
1817
|
+
next unless surface.key?(:edges)
|
1818
|
+
surface[:heatloss] = 0
|
1819
|
+
e = surface[:edges].values
|
1820
|
+
e.each { |edge| surface[:heatloss] += edge[:psi] * edge[:length] }
|
1821
|
+
end
|
1822
|
+
|
1823
|
+
# Add point conductances (W/K x count), in TBD JSON file (under surfaces).
|
1824
|
+
tbd[:surfaces].each do |id, s|
|
1825
|
+
next unless s[:deratable]
|
1826
|
+
next unless json[:io]
|
1827
|
+
next unless json[:io].key?(:surfaces)
|
1828
|
+
|
1829
|
+
json[:io][:surfaces].each do |surface|
|
1830
|
+
next unless surface.key?(:khis)
|
1831
|
+
next unless surface.key?(:id)
|
1832
|
+
next unless surface[:id] == id
|
1833
|
+
|
1834
|
+
surface[:khis].each do |k|
|
1835
|
+
next unless k.key?(:id)
|
1836
|
+
next unless k.key?(:count)
|
1837
|
+
next unless json[:khi].point.key?(k[:id])
|
1838
|
+
next unless json[:khi].point[k[:id]] > 0.001
|
1839
|
+
s[:heatloss] = 0 unless s.key?(:heatloss)
|
1840
|
+
s[:heatloss] += json[:khi].point[k[:id]] * k[:count]
|
1841
|
+
s[:pts] = {} unless s.key?(:pts)
|
1842
|
+
s[:pts][k[:id]] = { val: json[:khi].point[k[:id]], n: k[:count] }
|
1843
|
+
end
|
1844
|
+
end
|
1845
|
+
end
|
1846
|
+
|
1847
|
+
# If user has selected a Ut to meet, e.g. argh'ments:
|
1848
|
+
# :uprate_walls
|
1849
|
+
# :wall_ut
|
1850
|
+
# :wall_option
|
1851
|
+
# (same triple arguments for roofs and exposed floors)
|
1852
|
+
# ... first 'uprate' targeted insulation layers (see ua.rb) before derating.
|
1853
|
+
# Check for new argh keys [:wall_uo], [:roof_uo] and/or [:floor_uo].
|
1854
|
+
up = argh[:uprate_walls] || argh[:uprate_roofs] || argh[:uprate_floors]
|
1855
|
+
uprate(model, tbd[:surfaces], argh) if up
|
1856
|
+
|
1857
|
+
# Derated (cloned) constructions are unique to each deratable surface.
|
1858
|
+
# Unique construction names are prefixed with the surface name,
|
1859
|
+
# and suffixed with " tbd", indicating that the construction is
|
1860
|
+
# henceforth thermally derated. The " tbd" expression is also key in
|
1861
|
+
# avoiding inadvertent derating - TBD will not derate constructions
|
1862
|
+
# (or rather layered materials) having " tbd" in their OpenStudio name.
|
1863
|
+
tbd[:surfaces].each do |id, surface|
|
1864
|
+
next unless surface.key?(:construction)
|
1865
|
+
next unless surface.key?(:index )
|
1866
|
+
next unless surface.key?(:ltype )
|
1867
|
+
next unless surface.key?(:r )
|
1868
|
+
next unless surface.key?(:edges )
|
1869
|
+
next unless surface.key?(:heatloss )
|
1870
|
+
next unless surface[:heatloss].abs > TOL
|
1871
|
+
|
1872
|
+
model.getSurfaces.each do |s|
|
1873
|
+
next unless id == s.nameString
|
1874
|
+
index = surface[:index ]
|
1875
|
+
current_c = surface[:construction]
|
1876
|
+
c = current_c.clone(model).to_LayeredConstruction.get
|
1877
|
+
m = nil
|
1878
|
+
m = derate(model, id, surface, c) if index
|
1879
|
+
# m may be nilled simply because the targeted construction has already
|
1880
|
+
# been derated, i.e. holds " tbd" in its name. Names of cloned/derated
|
1881
|
+
# constructions (due to TBD) include the surface name (since derated
|
1882
|
+
# constructions are now unique to each surface) and the suffix " c tbd".
|
1883
|
+
if m
|
1884
|
+
c.setLayer(index, m)
|
1885
|
+
c.setName("#{id} c tbd")
|
1886
|
+
current_R = rsi(current_c, s.filmResistance)
|
1887
|
+
|
1888
|
+
# In principle, the derated "ratio" could be calculated simply by
|
1889
|
+
# accessing a surface's uFactor. Yet air layers within constructions
|
1890
|
+
# (not air films) are ignored in OpenStudio's uFactor calculation.
|
1891
|
+
# An example would be 25mm-50mm pressure-equalized air gaps behind
|
1892
|
+
# brick veneer. This is not always compliant to some energy codes.
|
1893
|
+
# TBD currently factors-in air gap (and exterior cladding) R-values.
|
1894
|
+
#
|
1895
|
+
# If one comments out the following loop (3 lines), tested surfaces
|
1896
|
+
# with air layers will generate discrepencies between the calculed RSi
|
1897
|
+
# value above and the inverse of the uFactor. All other surface
|
1898
|
+
# constructions pass the test.
|
1899
|
+
#
|
1900
|
+
# if ((1/current_R) - s.uFactor.to_f).abs > 0.005
|
1901
|
+
# puts "#{s.nameString} - Usi:#{1/current_R} UFactor: #{s.uFactor}"
|
1902
|
+
# end
|
1903
|
+
s.setConstruction(c)
|
1904
|
+
|
1905
|
+
# If the derated surface construction separates CONDITIONED space from
|
1906
|
+
# UNCONDITIONED or UNENCLOSED space, then derate the adjacent surface
|
1907
|
+
# construction as well (unless defaulted).
|
1908
|
+
if s.outsideBoundaryCondition.downcase == "surface"
|
1909
|
+
unless s.adjacentSurface.empty?
|
1910
|
+
adjacent = s.adjacentSurface.get
|
1911
|
+
nom = adjacent.nameString
|
1912
|
+
default = adjacent.isConstructionDefaulted == false
|
1913
|
+
|
1914
|
+
if default && tbd[:surfaces].key?(nom)
|
1915
|
+
current_cc = tbd[:surfaces][nom][:construction]
|
1916
|
+
cc = current_cc.clone(model).to_LayeredConstruction.get
|
1917
|
+
|
1918
|
+
cc.setLayer(tbd[:surfaces][nom][:index], m)
|
1919
|
+
cc.setName("#{nom} c tbd")
|
1920
|
+
adjacent.setConstruction(cc)
|
1921
|
+
end
|
1922
|
+
end
|
1923
|
+
end
|
1924
|
+
|
1925
|
+
# Compute updated RSi value from layers.
|
1926
|
+
updated_c = s.construction.get.to_LayeredConstruction.get
|
1927
|
+
updated_R = rsi(updated_c, s.filmResistance)
|
1928
|
+
ratio = -(current_R - updated_R) * 100 / current_R
|
1929
|
+
|
1930
|
+
surface[:ratio] = ratio if ratio.abs > TOL
|
1931
|
+
surface[:u ] = 1 / current_R # un-derated U-factors (for UA')
|
1932
|
+
end
|
1933
|
+
end
|
1934
|
+
end
|
1935
|
+
|
1936
|
+
# Ensure deratable surfaces have U-factors (even if NOT derated).
|
1937
|
+
tbd[:surfaces].each do |id, surface|
|
1938
|
+
next unless surface[:deratable]
|
1939
|
+
next unless surface.key?(:construction)
|
1940
|
+
next if surface.key?(:u)
|
1941
|
+
s = model.getSurfaceByName(id)
|
1942
|
+
log(ERR, "Skipping missing surface '#{id}' (#{mth})") if s.empty?
|
1943
|
+
next if s.empty?
|
1944
|
+
surface[:u] = 1.0 / rsi(surface[:construction], s.get.filmResistance)
|
1945
|
+
end
|
1946
|
+
|
1947
|
+
json[:io][:edges] = []
|
1948
|
+
# Enrich io with TBD/Topolys edge info before returning:
|
1949
|
+
# 1. edge custom PSI set, if on file
|
1950
|
+
# 2. edge PSI type
|
1951
|
+
# 3. edge length (m)
|
1952
|
+
# 4. edge origin & end vertices
|
1953
|
+
# 5. array of linked outside- or ground-facing surfaces
|
1954
|
+
edges.values.each do |e|
|
1955
|
+
next unless e.key?(:psi)
|
1956
|
+
next unless e.key?(:set)
|
1957
|
+
v = e[:psi].values.max
|
1958
|
+
set = e[:set]
|
1959
|
+
t = e[:psi].key(v)
|
1960
|
+
l = e[:length]
|
1961
|
+
edge = { psi: set, type: t, length: l, surfaces: e[:surfaces].keys }
|
1962
|
+
edge[:v0x] = e[:v0].point.x
|
1963
|
+
edge[:v0y] = e[:v0].point.y
|
1964
|
+
edge[:v0z] = e[:v0].point.z
|
1965
|
+
edge[:v1x] = e[:v1].point.x
|
1966
|
+
edge[:v1y] = e[:v1].point.y
|
1967
|
+
edge[:v1z] = e[:v1].point.z
|
1968
|
+
|
1969
|
+
json[:io][:edges] << edge
|
1970
|
+
end
|
1971
|
+
|
1972
|
+
empty = json[:io][:edges].empty?
|
1973
|
+
json[:io][:edges].sort_by { |e| [ e[:v0x], e[:v0y], e[:v0z],
|
1974
|
+
e[:v1x], e[:v1y], e[:v1z] ] } unless empty
|
1975
|
+
json[:io].delete(:edges) if empty
|
1976
|
+
|
1977
|
+
# Populate UA' trade-off reference values (optional).
|
1978
|
+
ua = argh[:gen_ua] && argh[:ua_ref] && argh[:ua_ref] == "code (Quebec)"
|
1979
|
+
qc33(tbd[:surfaces], json[:psi], setpoints) if ua
|
1980
|
+
|
1981
|
+
tbd[:io] = json[:io]
|
1982
|
+
|
1983
|
+
tbd
|
1984
|
+
end
|
1985
|
+
|
1986
|
+
##
|
1987
|
+
# TBD exit strategy for OpenStudio Measures. May write out TBD model
|
1988
|
+
# content/results if requested (see argh). Always writes out minimal logs,
|
1989
|
+
# (see tbd.out.json).
|
1990
|
+
#
|
1991
|
+
# @param runner [Runner] OpenStudio Measure runner
|
1992
|
+
# @param argh [Hash] TBD arguments
|
1993
|
+
#
|
1994
|
+
# @return [Bool] true if TBD Measure is successful
|
1995
|
+
def exit(runner = nil, argh = {})
|
1996
|
+
mth = "TBD::#{__callee__}"
|
1997
|
+
|
1998
|
+
# Generated files target a design context ( >= WARN ) ... change TBD log
|
1999
|
+
# level for debugging purposes. By default, log status is set < DBG
|
2000
|
+
# while log level is set @INF.
|
2001
|
+
state = msg(status)
|
2002
|
+
state = msg(INF) if status.zero?
|
2003
|
+
argh = {} unless argh.is_a?(Hash)
|
2004
|
+
argh[:io ] = nil unless argh.key?(:io)
|
2005
|
+
argh[:surfaces] = nil unless argh.key?(:surfaces)
|
2006
|
+
|
2007
|
+
unless argh[:io] && argh[:surfaces]
|
2008
|
+
state = "Halting all TBD processes, yet running OpenStudio"
|
2009
|
+
state = "Halting all TBD processes, and halting OpenStudio" if fatal?
|
2010
|
+
end
|
2011
|
+
|
2012
|
+
argh[:io ] = {} unless argh[:io]
|
2013
|
+
argh[:seed ] = "" unless argh.key?(:seed )
|
2014
|
+
argh[:version ] = "" unless argh.key?(:version )
|
2015
|
+
argh[:gen_ua ] = false unless argh.key?(:gen_ua )
|
2016
|
+
argh[:ua_ref ] = "" unless argh.key?(:ua_ref )
|
2017
|
+
argh[:setpoints ] = false unless argh.key?(:setpoints )
|
2018
|
+
argh[:write_tbd ] = false unless argh.key?(:write_tbd )
|
2019
|
+
argh[:uprate_walls ] = false unless argh.key?(:uprate_walls )
|
2020
|
+
argh[:uprate_roofs ] = false unless argh.key?(:uprate_roofs )
|
2021
|
+
argh[:uprate_floors] = false unless argh.key?(:uprate_floors)
|
2022
|
+
argh[:wall_ut ] = 5.678 unless argh.key?(:wall_ut )
|
2023
|
+
argh[:roof_ut ] = 5.678 unless argh.key?(:roof_ut )
|
2024
|
+
argh[:floor_ut ] = 5.678 unless argh.key?(:floor_ut )
|
2025
|
+
argh[:wall_option ] = "" unless argh.key?(:wall_option )
|
2026
|
+
argh[:roof_option ] = "" unless argh.key?(:roof_option )
|
2027
|
+
argh[:floor_option ] = "" unless argh.key?(:floor_option )
|
2028
|
+
argh[:wall_uo ] = nil unless argh.key?(:wall_ut )
|
2029
|
+
argh[:roof_uo ] = nil unless argh.key?(:roof_ut )
|
2030
|
+
argh[:floor_uo ] = nil unless argh.key?(:floor_ut )
|
2031
|
+
|
2032
|
+
groups = { wall: {}, roof: {}, floor: {} }
|
2033
|
+
groups[:wall ][:up] = argh[:uprate_walls ]
|
2034
|
+
groups[:roof ][:up] = argh[:uprate_roofs ]
|
2035
|
+
groups[:floor][:up] = argh[:uprate_floors]
|
2036
|
+
groups[:wall ][:ut] = argh[:wall_ut ]
|
2037
|
+
groups[:roof ][:ut] = argh[:roof_ut ]
|
2038
|
+
groups[:floor][:ut] = argh[:floor_ut ]
|
2039
|
+
groups[:wall ][:op] = argh[:wall_option ]
|
2040
|
+
groups[:roof ][:op] = argh[:roof_option ]
|
2041
|
+
groups[:floor][:op] = argh[:floor_option ]
|
2042
|
+
groups[:wall ][:uo] = argh[:wall_uo ]
|
2043
|
+
groups[:roof ][:uo] = argh[:roof_uo ]
|
2044
|
+
groups[:floor][:uo] = argh[:floor_uo ]
|
2045
|
+
|
2046
|
+
io = argh[:io ]
|
2047
|
+
out = argh[:write_tbd]
|
2048
|
+
descr = ""
|
2049
|
+
descr = argh[:seed] unless argh[:seed].empty?
|
2050
|
+
io[:description] = descr unless io.key?(:description)
|
2051
|
+
descr = io[:description]
|
2052
|
+
|
2053
|
+
schema_pth = "https://github.com/rd2/tbd/blob/master/tbd.schema.json"
|
2054
|
+
io[:schema] = schema_pth unless io.key?(:schema)
|
2055
|
+
tbd_log = { date: Time.now, status: state }
|
2056
|
+
u_t = []
|
2057
|
+
|
2058
|
+
groups.each do |label, g|
|
2059
|
+
next if fatal?
|
2060
|
+
next unless g[:uo]
|
2061
|
+
next unless g[:uo].is_a?(Numeric)
|
2062
|
+
|
2063
|
+
uo = format("%.3f", g[:uo])
|
2064
|
+
ut = format("%.3f", g[:ut])
|
2065
|
+
output = "An initial #{label.to_s} Uo of #{uo} W/m2•K is required to " \
|
2066
|
+
"achieve an overall Ut of #{ut} W/m2•K for #{g[:op]}"
|
2067
|
+
u_t << output
|
2068
|
+
runner.registerInfo(output)
|
2069
|
+
end
|
2070
|
+
|
2071
|
+
tbd_log[:ut] = u_t unless u_t.empty?
|
2072
|
+
ua_md_en = nil
|
2073
|
+
ua_md_fr = nil
|
2074
|
+
ua = nil
|
2075
|
+
ok = argh[:surfaces] && argh[:gen_ua]
|
2076
|
+
ua = ua_summary(tbd_log[:date], argh) if ok
|
2077
|
+
|
2078
|
+
unless fatal? || ua.nil? || ua.empty?
|
2079
|
+
if ua.key?(:en)
|
2080
|
+
if ua[:en].key?(:b1) || ua[:en].key?(:b2)
|
2081
|
+
runner.registerInfo("-")
|
2082
|
+
runner.registerInfo(ua[:model])
|
2083
|
+
tbd_log[:ua] = {}
|
2084
|
+
ua_md_en = ua_md(ua, :en)
|
2085
|
+
ua_md_fr = ua_md(ua, :fr)
|
2086
|
+
end
|
2087
|
+
|
2088
|
+
if ua[:en].key?(:b1) && ua[:en][:b1].key?(:summary)
|
2089
|
+
runner.registerInfo(" - #{ua[:en][:b1][:summary]}")
|
2090
|
+
|
2091
|
+
ua[:en][:b1].each do |k, v|
|
2092
|
+
runner.registerInfo(" --- #{v}") unless k == :summary
|
2093
|
+
end
|
2094
|
+
|
2095
|
+
tbd_log[:ua][:bloc1] = ua[:en][:b1]
|
2096
|
+
end
|
2097
|
+
|
2098
|
+
if ua[:en].key?(:b2) && ua[:en][:b2].key?(:summary)
|
2099
|
+
runner.registerInfo(" - #{ua[:en][:b2][:summary]}")
|
2100
|
+
|
2101
|
+
ua[:en][:b2].each do |k, v|
|
2102
|
+
runner.registerInfo(" --- #{v}") unless k == :summary
|
2103
|
+
end
|
2104
|
+
|
2105
|
+
tbd_log[:ua][:bloc2] = ua[:en][:b2]
|
2106
|
+
end
|
2107
|
+
end
|
2108
|
+
|
2109
|
+
runner.registerInfo(" -")
|
2110
|
+
end
|
2111
|
+
|
2112
|
+
results = []
|
2113
|
+
|
2114
|
+
if argh[:surfaces]
|
2115
|
+
argh[:surfaces].each do |id, surface|
|
2116
|
+
next if fatal?
|
2117
|
+
next unless surface.key?(:ratio)
|
2118
|
+
ratio = format("%4.1f", surface[:ratio])
|
2119
|
+
output = "RSi derated by #{ratio}% : #{id}"
|
2120
|
+
|
2121
|
+
results << output
|
2122
|
+
runner.registerInfo(output)
|
2123
|
+
end
|
2124
|
+
end
|
2125
|
+
|
2126
|
+
tbd_log[:results] = results unless results.empty?
|
2127
|
+
tbd_msgs = []
|
2128
|
+
|
2129
|
+
logs.each do |l|
|
2130
|
+
tbd_msgs << { level: tag(l[:level]), message: l[:message] }
|
2131
|
+
|
2132
|
+
runner.registerWarning(l[:message]) if l[:level] > INF
|
2133
|
+
runner.registerInfo(l[:message]) if l[:level] <= INF
|
2134
|
+
end
|
2135
|
+
|
2136
|
+
tbd_log[:messages] = tbd_msgs unless tbd_msgs.empty?
|
2137
|
+
io[:log] = tbd_log
|
2138
|
+
|
2139
|
+
# User's may not be requesting detailed output - delete non-essential items.
|
2140
|
+
io.delete(:psis ) unless out
|
2141
|
+
io.delete(:khis ) unless out
|
2142
|
+
io.delete(:building ) unless out
|
2143
|
+
io.delete(:stories ) unless out
|
2144
|
+
io.delete(:spacetypes) unless out
|
2145
|
+
io.delete(:spaces ) unless out
|
2146
|
+
io.delete(:surfaces ) unless out
|
2147
|
+
io.delete(:edges ) unless out
|
2148
|
+
|
2149
|
+
# Deterministic sorting
|
2150
|
+
io[:schema ] = io.delete(:schema ) if io.key?(:schema )
|
2151
|
+
io[:description] = io.delete(:description) if io.key?(:description)
|
2152
|
+
io[:log ] = io.delete(:log ) if io.key?(:log )
|
2153
|
+
io[:psis ] = io.delete(:psis ) if io.key?(:psis )
|
2154
|
+
io[:khis ] = io.delete(:khis ) if io.key?(:khis )
|
2155
|
+
io[:building ] = io.delete(:building ) if io.key?(:building )
|
2156
|
+
io[:stories ] = io.delete(:stories ) if io.key?(:stories )
|
2157
|
+
io[:spacetypes ] = io.delete(:spacetypes ) if io.key?(:spacetypes )
|
2158
|
+
io[:spaces ] = io.delete(:spaces ) if io.key?(:spaces )
|
2159
|
+
io[:surfaces ] = io.delete(:surfaces ) if io.key?(:surfaces )
|
2160
|
+
io[:edges ] = io.delete(:edges ) if io.key?(:edges )
|
2161
|
+
|
2162
|
+
out_dir = '.'
|
2163
|
+
file_paths = runner.workflow.absoluteFilePaths
|
2164
|
+
|
2165
|
+
# 'Apply Measure Now' won't cp files from 1st path back to generated_files.
|
2166
|
+
match1 = /WorkingFiles/.match(file_paths[1].to_s)
|
2167
|
+
match2 = /files/.match(file_paths[1].to_s)
|
2168
|
+
match = match1 || match2
|
2169
|
+
|
2170
|
+
if file_paths.size >= 2 && File.exists?(file_paths[1].to_s) && match
|
2171
|
+
out_dir = file_paths[1].to_s
|
2172
|
+
elsif !file_paths.empty? && File.exists?(file_paths.first.to_s)
|
2173
|
+
out_dir = file_paths.first.to_s
|
2174
|
+
end
|
2175
|
+
|
2176
|
+
out_path = File.join(out_dir, "tbd.out.json")
|
2177
|
+
|
2178
|
+
File.open(out_path, 'w') do |file|
|
2179
|
+
file.puts JSON::pretty_generate(io)
|
2180
|
+
# Make sure data is written to the disk one way or the other.
|
2181
|
+
begin
|
2182
|
+
file.fsync
|
2183
|
+
rescue StandardError
|
2184
|
+
file.flush
|
2185
|
+
end
|
2186
|
+
end
|
2187
|
+
|
2188
|
+
unless TBD.fatal? || ua.nil? || ua.empty?
|
2189
|
+
unless ua_md_en.nil? || ua_md_en.empty?
|
2190
|
+
ua_path = File.join(out_dir, "ua_en.md")
|
2191
|
+
|
2192
|
+
File.open(ua_path, 'w') do |file|
|
2193
|
+
file.puts ua_md_en
|
2194
|
+
|
2195
|
+
begin
|
2196
|
+
file.fsync
|
2197
|
+
rescue StandardError
|
2198
|
+
file.flush
|
2199
|
+
end
|
2200
|
+
end
|
2201
|
+
end
|
2202
|
+
|
2203
|
+
unless ua_md_fr.nil? || ua_md_fr.empty?
|
2204
|
+
ua_path = File.join(out_dir, "ua_fr.md")
|
2205
|
+
|
2206
|
+
File.open(ua_path, 'w') do |file|
|
2207
|
+
file.puts ua_md_fr
|
2208
|
+
|
2209
|
+
begin
|
2210
|
+
file.fsync
|
2211
|
+
rescue StandardError
|
2212
|
+
file.flush
|
2213
|
+
end
|
2214
|
+
end
|
2215
|
+
end
|
2216
|
+
end
|
2217
|
+
|
2218
|
+
if fatal?
|
2219
|
+
runner.registerError("#{state} - see 'tbd.out.json'")
|
2220
|
+
return false
|
2221
|
+
elsif error? || warn?
|
2222
|
+
runner.registerWarning("#{state} - see 'tbd.out.json'")
|
2223
|
+
return true
|
2224
|
+
else
|
2225
|
+
runner.registerInfo("#{state} - see 'tbd.out.json'")
|
2226
|
+
return true
|
2227
|
+
end
|
2228
|
+
end
|
2229
|
+
end
|