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