osut 0.6.0a1__py3-none-any.whl
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.
- osut/__init__.py +0 -0
- osut/osut.py +2763 -0
- osut-0.6.0a1.dist-info/METADATA +27 -0
- osut-0.6.0a1.dist-info/RECORD +7 -0
- osut-0.6.0a1.dist-info/WHEEL +5 -0
- osut-0.6.0a1.dist-info/licenses/LICENSE +28 -0
- osut-0.6.0a1.dist-info/top_level.txt +1 -0
osut/osut.py
ADDED
|
@@ -0,0 +1,2763 @@
|
|
|
1
|
+
# BSD 3-Clause License
|
|
2
|
+
#
|
|
3
|
+
# Copyright (c) 2022-2025, rd2
|
|
4
|
+
#
|
|
5
|
+
# Redistribution and use in source and binary forms, with or without
|
|
6
|
+
# modification, are permitted provided that the following conditions are met:
|
|
7
|
+
#
|
|
8
|
+
# 1. Redistributions of source code must retain the above copyright notice, this
|
|
9
|
+
# list of conditions and the following disclaimer.
|
|
10
|
+
#
|
|
11
|
+
# 2. Redistributions in binary form must reproduce the above copyright notice,
|
|
12
|
+
# this list of conditions and the following disclaimer in the documentation
|
|
13
|
+
# and/or other materials provided with the distribution.
|
|
14
|
+
#
|
|
15
|
+
# 3. Neither the name of the copyright holder nor the names of its
|
|
16
|
+
# contributors may be used to endorse or promote products derived from
|
|
17
|
+
# this software without specific prior written permission.
|
|
18
|
+
#
|
|
19
|
+
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
|
20
|
+
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
|
21
|
+
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
|
22
|
+
# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
|
23
|
+
# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
|
24
|
+
# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
|
25
|
+
# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
|
26
|
+
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
|
27
|
+
# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
|
28
|
+
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
29
|
+
|
|
30
|
+
import re
|
|
31
|
+
import math
|
|
32
|
+
import collections
|
|
33
|
+
import openstudio
|
|
34
|
+
from oslg import oslg
|
|
35
|
+
from dataclasses import dataclass
|
|
36
|
+
|
|
37
|
+
@dataclass(frozen=True)
|
|
38
|
+
class _CN:
|
|
39
|
+
DBG = oslg.CN.DEBUG
|
|
40
|
+
INF = oslg.CN.INFO
|
|
41
|
+
WRN = oslg.CN.WARN
|
|
42
|
+
ERR = oslg.CN.ERROR
|
|
43
|
+
FTL = oslg.CN.FATAL
|
|
44
|
+
TOL = 0.01 # default distance tolerance (m)
|
|
45
|
+
TOL2 = TOL * TOL # default area tolerance (m2)
|
|
46
|
+
CN = _CN()
|
|
47
|
+
|
|
48
|
+
# General surface orientations (see 'facets' method).
|
|
49
|
+
_sidz = ("bottom", "top", "north", "east", "south", "west")
|
|
50
|
+
|
|
51
|
+
# This first set of utilities support OpenStudio materials, constructions,
|
|
52
|
+
# construction sets, etc. If relying on default StandardOpaqueMaterial:
|
|
53
|
+
# - roughness (rgh) : "Smooth"
|
|
54
|
+
# - thickness : 0.1 m
|
|
55
|
+
# - thermal conductivity (k ) : 0.1 W/m.K
|
|
56
|
+
# - density (rho) : 0.1 kg/m3
|
|
57
|
+
# - specific heat (cp ) : 1400.0 J/kg•K
|
|
58
|
+
#
|
|
59
|
+
# https://s3.amazonaws.com/openstudio-sdk-documentation/cpp/
|
|
60
|
+
# OpenStudio-3.6.1-doc/model/html/
|
|
61
|
+
# classopenstudio_1_1model_1_1_standard_opaque_material.html
|
|
62
|
+
|
|
63
|
+
# ... apart from surface roughness, rarely would these material properties be
|
|
64
|
+
# suitable - and are therefore explicitly set below. On roughness:
|
|
65
|
+
# - "Very Rough" : stucco
|
|
66
|
+
# - "Rough" : brick
|
|
67
|
+
# - "Medium Rough" : concrete
|
|
68
|
+
# - "Medium Smooth" : clear pine
|
|
69
|
+
# - "Smooth" : smooth plaster
|
|
70
|
+
# - "Very Smooth" : glass
|
|
71
|
+
|
|
72
|
+
# Thermal mass categories (e.g. exterior cladding, interior finish, framing).
|
|
73
|
+
# - "none" : token for 'no user selection', resort to defaults
|
|
74
|
+
# - "light" : e.g. 16mm drywall interior
|
|
75
|
+
# - "medium" : e.g. 100mm brick cladding
|
|
76
|
+
# - "heavy" : e.g. 200mm poured concrete
|
|
77
|
+
_mass = ("none", "light", "medium", "heavy")
|
|
78
|
+
|
|
79
|
+
# Basic materials (StandardOpaqueMaterials only).
|
|
80
|
+
_mats = dict(
|
|
81
|
+
material = {}, # generic, e.g. lightweight cladding over furring, fibreboard
|
|
82
|
+
sand = {},
|
|
83
|
+
concrete = {},
|
|
84
|
+
brick = {},
|
|
85
|
+
drywall = {}, # e.g. finished drywall, intermediate sheating
|
|
86
|
+
mineral = {}, # e.g. light, semi-rigid rock wool insulation
|
|
87
|
+
polyiso = {}, # e.g. polyisocyanurate panel (or similar)
|
|
88
|
+
cellulose = {}, # e.g. blown, dry/stabilized fibre
|
|
89
|
+
door = {} # single composite material (45mm insulated steel door)
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
# Default inside + outside air film resistances (m2.K/W).
|
|
93
|
+
_film = dict(
|
|
94
|
+
shading = 0.000, # NA
|
|
95
|
+
partition = 0.150, # uninsulated wood- or steel-framed wall
|
|
96
|
+
wall = 0.150, # un/insulated wall
|
|
97
|
+
roof = 0.140, # un/insulated roof
|
|
98
|
+
floor = 0.190, # un/insulated (exposed) floor
|
|
99
|
+
basement = 0.120, # un/insulated basement wall
|
|
100
|
+
slab = 0.160, # un/insulated basement slab or slab-on-grade
|
|
101
|
+
door = 0.150, # standard, 45mm insulated steel (opaque) door
|
|
102
|
+
window = 0.150, # vertical fenestration, e.g. glazed doors, windows
|
|
103
|
+
skylight = 0.140 # e.g. domed 4' x 4' skylight
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
# Default (~1980s) envelope Uo (W/m2•K), based on surface type.
|
|
107
|
+
_uo = dict(
|
|
108
|
+
shading = None, # N/A
|
|
109
|
+
partition = None, # N/A
|
|
110
|
+
wall = 0.384, # rated R14.8 hr•ft2F/Btu
|
|
111
|
+
roof = 0.327, # rated R17.6 hr•ft2F/Btu
|
|
112
|
+
floor = 0.317, # rated R17.9 hr•ft2F/Btu (exposed floor)
|
|
113
|
+
basement = None,
|
|
114
|
+
slab = None,
|
|
115
|
+
door = 1.800, # insulated, unglazed steel door (single layer)
|
|
116
|
+
window = 2.800, # e.g. patio doors (simple glazing)
|
|
117
|
+
skylight = 3.500 # all skylight technologies
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
# Standard opaque materials, taken from a variety of sources (e.g. energy
|
|
121
|
+
# codes, NREL's BCL).
|
|
122
|
+
# - sand
|
|
123
|
+
# - concrete
|
|
124
|
+
# - brick
|
|
125
|
+
#
|
|
126
|
+
# Material properties remain largely constant between projects. What does
|
|
127
|
+
# tend to vary (between projects) are thicknesses. Actual OpenStudio opaque
|
|
128
|
+
# material objects can be (re)set in more than one way by class methods.
|
|
129
|
+
# In genConstruction, OpenStudio object identifiers are later suffixed with
|
|
130
|
+
# actual material thicknesses, in mm, e.g.:
|
|
131
|
+
# - "concrete200" : 200mm concrete slab
|
|
132
|
+
# - "drywall13" : 1/2" gypsum board
|
|
133
|
+
# - "drywall16" : 5/8" gypsum board
|
|
134
|
+
#
|
|
135
|
+
# Surface absorptances are also defaulted in OpenStudio:
|
|
136
|
+
# - thermal, long-wave (thm) : 90%
|
|
137
|
+
# - solar (sol) : 70%
|
|
138
|
+
# - visible (vis) : 70%
|
|
139
|
+
#
|
|
140
|
+
# These can also be explicitly set (see "sand").
|
|
141
|
+
_mats["material" ]["rgh"] = "MediumSmooth"
|
|
142
|
+
_mats["material" ]["k" ] = 0.115
|
|
143
|
+
_mats["material" ]["rho"] = 540.000
|
|
144
|
+
_mats["material" ]["cp" ] = 1200.000
|
|
145
|
+
|
|
146
|
+
_mats["sand" ]["rgh"] = "Rough"
|
|
147
|
+
_mats["sand" ]["k" ] = 1.290
|
|
148
|
+
_mats["sand" ]["rho"] = 2240.000
|
|
149
|
+
_mats["sand" ]["cp" ] = 830.000
|
|
150
|
+
_mats["sand" ]["thm"] = 0.900
|
|
151
|
+
_mats["sand" ]["sol"] = 0.700
|
|
152
|
+
_mats["sand" ]["vis"] = 0.700
|
|
153
|
+
|
|
154
|
+
_mats["concrete" ]["rgh"] = "MediumRough"
|
|
155
|
+
_mats["concrete" ]["k" ] = 1.730
|
|
156
|
+
_mats["concrete" ]["rho"] = 2240.000
|
|
157
|
+
_mats["concrete" ]["cp" ] = 830.000
|
|
158
|
+
|
|
159
|
+
_mats["brick" ]["rgh"] = "Rough"
|
|
160
|
+
_mats["brick" ]["k" ] = 0.675
|
|
161
|
+
_mats["brick" ]["rho"] = 1600.000
|
|
162
|
+
_mats["brick" ]["cp" ] = 790.000
|
|
163
|
+
|
|
164
|
+
_mats["drywall" ]["k" ] = 0.160
|
|
165
|
+
_mats["drywall" ]["rho"] = 785.000
|
|
166
|
+
_mats["drywall" ]["cp" ] = 1090.000
|
|
167
|
+
|
|
168
|
+
_mats["mineral" ]["k" ] = 0.050
|
|
169
|
+
_mats["mineral" ]["rho"] = 19.000
|
|
170
|
+
_mats["mineral" ]["cp" ] = 960.000
|
|
171
|
+
|
|
172
|
+
_mats["polyiso" ]["k" ] = 0.025
|
|
173
|
+
_mats["polyiso" ]["rho"] = 25.000
|
|
174
|
+
_mats["polyiso" ]["cp" ] = 1590.000
|
|
175
|
+
|
|
176
|
+
_mats["cellulose"]["rgh"] = "VeryRough"
|
|
177
|
+
_mats["cellulose"]["k" ] = 0.050
|
|
178
|
+
_mats["cellulose"]["rho"] = 80.000
|
|
179
|
+
_mats["cellulose"]["cp" ] = 835.000
|
|
180
|
+
|
|
181
|
+
_mats["door" ]["rgh"] = "MediumSmooth"
|
|
182
|
+
_mats["door" ]["k" ] = 0.080
|
|
183
|
+
_mats["door" ]["rho"] = 600.000
|
|
184
|
+
_mats["door" ]["cp" ] = 1000.000
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def sidz() -> tuple:
|
|
188
|
+
"""Returns available 'sidz' keywords."""
|
|
189
|
+
return _sidz
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def mass() -> tuple:
|
|
193
|
+
"""Returns available 'mass' keywords."""
|
|
194
|
+
return _mass
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def mats() -> dict:
|
|
198
|
+
"""Returns stored materials dictionary."""
|
|
199
|
+
return _mats
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def film() -> dict:
|
|
203
|
+
"""Returns inside + outside air film resistance dictionary."""
|
|
204
|
+
return _film
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
def uo() -> dict:
|
|
208
|
+
"""Returns (surface type-specific) Uo dictionary."""
|
|
209
|
+
return _uo
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
def genConstruction(model=None, specs=dict()):
|
|
213
|
+
"""Generates an OpenStudio multilayered construction, + materials if needed.
|
|
214
|
+
|
|
215
|
+
Args:
|
|
216
|
+
specs:
|
|
217
|
+
A dictionary holding multilayered construction parameters:
|
|
218
|
+
- "id" (str): construction identifier
|
|
219
|
+
- "type" (str): surface type - see OSut 'uo()'
|
|
220
|
+
- "uo" (float): assembly clear-field Uo, in W/m2•K - see OSut 'uo()'
|
|
221
|
+
- "clad" (str): exterior cladding - see OSut 'mass()'
|
|
222
|
+
- "frame" (str): assembly framing - see OSut 'mass()'
|
|
223
|
+
- "finish" (str): interior finish - see OSut 'mass()'
|
|
224
|
+
|
|
225
|
+
Returns:
|
|
226
|
+
openstudio.model.Construction: A generated construction.
|
|
227
|
+
None: If invalid inputs (see logs).
|
|
228
|
+
|
|
229
|
+
"""
|
|
230
|
+
mth = "osut.genConstruction"
|
|
231
|
+
cl = openstudio.model.Model
|
|
232
|
+
|
|
233
|
+
if not isinstance(model, cl):
|
|
234
|
+
return oslg.mismatch("model", model, cl, mth, CN.DBG)
|
|
235
|
+
if not isinstance(specs, dict):
|
|
236
|
+
return oslg.mismatch("specs", specs, dict, mth, CN.DBG)
|
|
237
|
+
|
|
238
|
+
if "type" not in specs: specs["type"] = "wall"
|
|
239
|
+
if "id" not in specs: specs["id" ] = ""
|
|
240
|
+
|
|
241
|
+
id = oslg.trim(specs["id"])
|
|
242
|
+
|
|
243
|
+
if not id:
|
|
244
|
+
id = "OSut.CON." + specs["type"]
|
|
245
|
+
if specs["type"] not in uo():
|
|
246
|
+
return oslg.invalid("surface type", mth, 2, CN.ERR)
|
|
247
|
+
if "uo" not in specs:
|
|
248
|
+
specs["uo"] = uo()[ specs["type"] ]
|
|
249
|
+
|
|
250
|
+
u = specs["uo"]
|
|
251
|
+
|
|
252
|
+
if u:
|
|
253
|
+
try:
|
|
254
|
+
u = float(u)
|
|
255
|
+
except:
|
|
256
|
+
return oslg.mismatch(id + " Uo", u, float, mth, CN.ERR)
|
|
257
|
+
|
|
258
|
+
if u < 0:
|
|
259
|
+
return oslg.negative(id + " Uo", mth, CN.ERR)
|
|
260
|
+
if u > 5.678:
|
|
261
|
+
return oslg.invalid(id + " Uo (> 5.678)", mth, 2, CN.ERR)
|
|
262
|
+
|
|
263
|
+
# Optional specs. Log/reset if invalid.
|
|
264
|
+
if "clad" not in specs: specs["clad" ] = "light" # exterior
|
|
265
|
+
if "frame" not in specs: specs["frame" ] = "light"
|
|
266
|
+
if "finish" not in specs: specs["finish"] = "light" # interior
|
|
267
|
+
if specs["clad" ] not in mass(): oslg.log(CN.WRN, "Reset: light cladding")
|
|
268
|
+
if specs["frame" ] not in mass(): oslg.log(CN.WRN, "Reset: light framing")
|
|
269
|
+
if specs["finish"] not in mass(): oslg.log(CN.WRN, "Reset: light finish")
|
|
270
|
+
if specs["clad" ] not in mass(): specs["clad" ] = "light"
|
|
271
|
+
if specs["frame" ] not in mass(): specs["frame" ] = "light"
|
|
272
|
+
if specs["frame" ] not in mass(): specs["finish"] = "light"
|
|
273
|
+
|
|
274
|
+
flm = film()[ specs["type"] ]
|
|
275
|
+
|
|
276
|
+
# Layered assembly (max 4 layers):
|
|
277
|
+
# - cladding
|
|
278
|
+
# - intermediate sheathing
|
|
279
|
+
# - composite insulating/framing
|
|
280
|
+
# - interior finish
|
|
281
|
+
a = dict(clad={}, sheath={}, compo={}, finish={}, glazing={})
|
|
282
|
+
|
|
283
|
+
if specs["type"] == "shading":
|
|
284
|
+
mt = "material"
|
|
285
|
+
d = 0.015
|
|
286
|
+
a["compo"]["mat"] = mats()[mt]
|
|
287
|
+
a["compo"]["d" ] = d
|
|
288
|
+
a["compo"]["id" ] = "OSut." + mt + ".%03d" % int(d * 1000)
|
|
289
|
+
|
|
290
|
+
elif specs["type"] == "partition":
|
|
291
|
+
if not specs["clad"] == "none":
|
|
292
|
+
mt = "drywall"
|
|
293
|
+
d = 0.015
|
|
294
|
+
a["clad"]["mat"] = mats()[mt]
|
|
295
|
+
a["clad"]["d" ] = d
|
|
296
|
+
a["clad"]["id" ] = "OSut." + mt + ".%03d" % int(d * 1000)
|
|
297
|
+
|
|
298
|
+
mt = "concrete"
|
|
299
|
+
d = 0.015
|
|
300
|
+
if specs["frame"] == "light": mt = "material"
|
|
301
|
+
if u: mt = "mineral"
|
|
302
|
+
if specs["frame"] == "medium": d = 0.100
|
|
303
|
+
if specs["frame"] == "heavy": d = 0.200
|
|
304
|
+
if u: d = 0.100
|
|
305
|
+
a["compo"]["mat"] = mats()[mt]
|
|
306
|
+
a["compo"]["d" ] = d
|
|
307
|
+
a["compo"]["id" ] = "OSut." + mt + ".%03d" % int(d * 1000)
|
|
308
|
+
|
|
309
|
+
if not specs["finish"] == "none":
|
|
310
|
+
mt = "drywall"
|
|
311
|
+
d = 0.015
|
|
312
|
+
a["finish"]["mat"] = mats()[mt]
|
|
313
|
+
a["finish"]["d" ] = d
|
|
314
|
+
a["finish"]["id" ] = "OSut." + mt + ".%03d" % int(d * 1000)
|
|
315
|
+
|
|
316
|
+
elif specs["type"] == "wall":
|
|
317
|
+
if not specs["clad"] == "none":
|
|
318
|
+
mt = "material"
|
|
319
|
+
d = 0.100
|
|
320
|
+
if specs["clad"] == "medium": mt = "brick"
|
|
321
|
+
if specs["clad"] == "heavy": mt = "concrete"
|
|
322
|
+
if specs["clad"] == "light": d = 0.015
|
|
323
|
+
a["clad"]["mat"] = mats()[mt]
|
|
324
|
+
a["clad"]["d" ] = d
|
|
325
|
+
a["clad"]["id" ] = "OSut." + mt + ".%03d" % int(d * 1000)
|
|
326
|
+
|
|
327
|
+
mt = "drywall"
|
|
328
|
+
d = 0.100
|
|
329
|
+
if specs["frame"] == "medium": mt = "mineral"
|
|
330
|
+
if specs["frame"] == "heavy": mt = "polyiso"
|
|
331
|
+
if specs["frame"] == "light": d = 0.015
|
|
332
|
+
a["sheath"]["mat"] = mats()[mt]
|
|
333
|
+
a["sheath"]["d" ] = d
|
|
334
|
+
a["sheath"]["id" ] = "OSut." + mt + ".%03d" % int(d * 1000)
|
|
335
|
+
|
|
336
|
+
mt = "mineral"
|
|
337
|
+
d = 0.100
|
|
338
|
+
if specs["frame"] == "medium": mt = "cellulose"
|
|
339
|
+
if specs["frame"] == "heavy": mt = "concrete"
|
|
340
|
+
if not u: mt = "material"
|
|
341
|
+
if specs["frame"] == "heavy": d = 0.200
|
|
342
|
+
if not u: d = 0.015
|
|
343
|
+
a["compo"]["mat"] = mats()[mt]
|
|
344
|
+
a["compo"]["d" ] = d
|
|
345
|
+
a["compo"]["id" ] = "OSut." + mt + ".%03d" % int(d * 1000)
|
|
346
|
+
|
|
347
|
+
if not specs["finish"] == "none":
|
|
348
|
+
mt = "concrete"
|
|
349
|
+
d = 0.015
|
|
350
|
+
if specs["finish"] == "light": mt = "drywall"
|
|
351
|
+
if specs["finish"] == "medium": d = 0.100
|
|
352
|
+
if specs["finish"] == "heavy": d = 0.200
|
|
353
|
+
a["finish"]["mat"] = mats()[mt]
|
|
354
|
+
a["finish"]["d" ] = d
|
|
355
|
+
a["finish"]["id" ] = "OSut." + mt + ".%03d" % int(d * 1000)
|
|
356
|
+
|
|
357
|
+
elif specs["type"] == "roof":
|
|
358
|
+
if not specs["clad"] == "none":
|
|
359
|
+
mt = "concrete"
|
|
360
|
+
d = 0.015
|
|
361
|
+
if specs["clad"] == "light": mt = "material"
|
|
362
|
+
if specs["clad"] == "medium": d = 0.100 # e.g. terrace
|
|
363
|
+
if specs["clad"] == "heavy": d = 0.200 # e.g. parking garage
|
|
364
|
+
a["clad"]["mat"] = mats()[mt]
|
|
365
|
+
a["clad"]["d" ] = d
|
|
366
|
+
a["clad"]["id" ] = "OSut." + mt + ".%03d" % int(d * 1000)
|
|
367
|
+
|
|
368
|
+
mt = "mineral"
|
|
369
|
+
d = 0.100
|
|
370
|
+
if specs["frame"] == "medium": mt = "polyiso"
|
|
371
|
+
if specs["frame"] == "heavy": mt = "cellulose"
|
|
372
|
+
if not u: mt = "material"
|
|
373
|
+
if not u: d = 0.015
|
|
374
|
+
a["compo"]["mat"] = mats()[mt]
|
|
375
|
+
a["compo"]["d" ] = d
|
|
376
|
+
a["compo"]["id" ] = "OSut." + mt + ".%03d" % int(d * 1000)
|
|
377
|
+
|
|
378
|
+
if not specs["finish"] == "none":
|
|
379
|
+
mt = "concrete"
|
|
380
|
+
d = 0.015
|
|
381
|
+
if specs["finish"] == "light": mt = "drywall"
|
|
382
|
+
if specs["finish"] == "medium": d = 0.100 # proxy for steel decking
|
|
383
|
+
if specs["finish"] == "heavy": d = 0.200
|
|
384
|
+
a["finish"]["mat"] = mats()[mt]
|
|
385
|
+
a["finish"]["d" ] = d
|
|
386
|
+
a["finish"]["id" ] = "OSut." + mt + ".%03d" % int(d * 1000)
|
|
387
|
+
|
|
388
|
+
elif specs["type"] == "floor":
|
|
389
|
+
if not specs["clad"] == "none":
|
|
390
|
+
mt = "material"
|
|
391
|
+
d = 0.015
|
|
392
|
+
a["clad"]["mat"] = mats()[mt]
|
|
393
|
+
a["clad"]["d" ] = d
|
|
394
|
+
a["clad"]["id" ] = "OSut." + mt + ".%03d" % int(d * 1000)
|
|
395
|
+
|
|
396
|
+
mt = "mineral"
|
|
397
|
+
d = 0.100
|
|
398
|
+
if specs["frame"] == "medium": mt = "polyiso"
|
|
399
|
+
if specs["frame"] == "heavy": mt = "cellulose"
|
|
400
|
+
if not u: mt = "material"
|
|
401
|
+
if not u: d = 0.015
|
|
402
|
+
a["compo"]["mat"] = mats()[mt]
|
|
403
|
+
a["compo"]["d" ] = d
|
|
404
|
+
a["compo"]["id" ] = "OSut." + mt + ".%03d" % int(d * 1000)
|
|
405
|
+
|
|
406
|
+
if not specs["finish"] == "none":
|
|
407
|
+
mt = "concrete"
|
|
408
|
+
d = 0.015
|
|
409
|
+
if specs["finish"] == "light": mt = "material"
|
|
410
|
+
if specs["finish"] == "medium": d = 0.100
|
|
411
|
+
if specs["finish"] == "heavy": d = 0.200
|
|
412
|
+
a["finish"]["mat"] = mats()[mt]
|
|
413
|
+
a["finish"]["d" ] = d
|
|
414
|
+
a["finish"]["id" ] = "OSut." + mt + ".%03d" % int(d * 1000)
|
|
415
|
+
|
|
416
|
+
elif specs["type"] == "slab":
|
|
417
|
+
mt = "sand"
|
|
418
|
+
d = 0.100
|
|
419
|
+
a["clad"]["mat"] = mats()[mt]
|
|
420
|
+
a["clad"]["d" ] = d
|
|
421
|
+
a["clad"]["id" ] = "OSut." + mt + ".%03d" % int(d * 1000)
|
|
422
|
+
|
|
423
|
+
if not specs["frame"] == "none":
|
|
424
|
+
mt = "polyiso"
|
|
425
|
+
d = 0.025
|
|
426
|
+
a["sheath"]["mat"] = mats()[mt]
|
|
427
|
+
a["sheath"]["d" ] = d
|
|
428
|
+
a["sheath"]["id" ] = "OSut." + mt + ".%03d" % int(d * 1000)
|
|
429
|
+
|
|
430
|
+
mt = "concrete"
|
|
431
|
+
d = 0.100
|
|
432
|
+
if specs["frame"] == "heavy": d = 0.200
|
|
433
|
+
a["compo"]["mat"] = mats()[mt]
|
|
434
|
+
a["compo"]["d" ] = d
|
|
435
|
+
a["compo"]["id" ] = "OSut." + mt + ".%03d" % int(d * 1000)
|
|
436
|
+
|
|
437
|
+
if not specs["finish"] == "none":
|
|
438
|
+
mt = "material"
|
|
439
|
+
d = 0.015
|
|
440
|
+
a["finish"]["mat"] = mats()[mt]
|
|
441
|
+
a["finish"]["d" ] = d
|
|
442
|
+
a["finish"]["id" ] = "OSut." + mt + ".%03d" % int(d * 1000)
|
|
443
|
+
|
|
444
|
+
elif specs["type"] == "basement":
|
|
445
|
+
if not specs["clad"] == "none":
|
|
446
|
+
mt = "concrete"
|
|
447
|
+
d = 0.100
|
|
448
|
+
if specs["clad"] == "light": mt = "material"
|
|
449
|
+
if specs["clad"] == "light": d = 0.015
|
|
450
|
+
a["clad"]["mat"] = mats[mt]
|
|
451
|
+
a["clad"]["d" ] = d
|
|
452
|
+
a["clad"]["id" ] = "OSut." + mt + ".%03d" % int(d * 1000)
|
|
453
|
+
|
|
454
|
+
mt = "polyiso"
|
|
455
|
+
d = 0.025
|
|
456
|
+
a["sheath"]["mat"] = mats()[mt]
|
|
457
|
+
a["sheath"]["d" ] = d
|
|
458
|
+
a["sheath"]["id" ] = "OSut." + mt + ".%03d" % int(d * 1000)
|
|
459
|
+
|
|
460
|
+
mt = "concrete"
|
|
461
|
+
d = 0.200
|
|
462
|
+
a["compo"]["mat"] = mats()[mt]
|
|
463
|
+
a["compo"]["d" ] = d
|
|
464
|
+
a["compo"]["id" ] = "OSut." + mt + ".%03d" % int(d * 1000)
|
|
465
|
+
else:
|
|
466
|
+
mt = "concrete"
|
|
467
|
+
d = 0.200
|
|
468
|
+
a["sheath"]["mat"] = mats()[mt]
|
|
469
|
+
a["sheath"]["d" ] = d
|
|
470
|
+
a["sheath"]["id" ] = "OSut." + mt + ".%03d" % int(d * 1000)
|
|
471
|
+
|
|
472
|
+
if not specs["finish"] == "none":
|
|
473
|
+
mt = "mineral"
|
|
474
|
+
d = 0.075
|
|
475
|
+
a["compo"]["mat"] = mats()[mt]
|
|
476
|
+
a["compo"]["d" ] = d
|
|
477
|
+
a["compo"]["id" ] = "OSut." + mt + ".%03d" % int(d * 1000)
|
|
478
|
+
|
|
479
|
+
mt = "drywall"
|
|
480
|
+
d = 0.015
|
|
481
|
+
a["finish"]["mat"] = mats()[mt]
|
|
482
|
+
a["finish"]["d" ] = d
|
|
483
|
+
a["finish"]["id" ] = "OSut." + mt + ".%03d" % int(d * 1000)
|
|
484
|
+
|
|
485
|
+
elif specs["type"] == "door":
|
|
486
|
+
mt = "door"
|
|
487
|
+
d = 0.045
|
|
488
|
+
a["compo" ]["mat" ] = mats()[mt]
|
|
489
|
+
a["compo" ]["d" ] = d
|
|
490
|
+
a["compo" ]["id" ] = "OSut." + mt + ".%03d" % int(d * 1000)
|
|
491
|
+
|
|
492
|
+
elif specs["type"] == "window":
|
|
493
|
+
a["glazing"]["u" ] = specs["uo"]
|
|
494
|
+
a["glazing"]["shgc"] = 0.450
|
|
495
|
+
if "shgc" in specs: a["glazing"]["shgc"] = specs["shgc"]
|
|
496
|
+
a["glazing"]["id" ] = "OSut.window"
|
|
497
|
+
a["glazing"]["id" ] += ".U%.1f" % a["glazing"]["u"]
|
|
498
|
+
a["glazing"]["id" ] += ".SHGC%d" % (a["glazing"]["shgc"]*100)
|
|
499
|
+
|
|
500
|
+
elif specs["type"] == "skylight":
|
|
501
|
+
a["glazing"]["u" ] = specs["uo"]
|
|
502
|
+
a["glazing"]["shgc"] = 0.450
|
|
503
|
+
if "shgc" in specs: a["glazing"]["shgc"] = specs["shgc"]
|
|
504
|
+
a["glazing"]["id" ] = "OSut.skylight"
|
|
505
|
+
a["glazing"]["id" ] += ".U%.1f" % a["glazing"]["u"]
|
|
506
|
+
a["glazing"]["id" ] += ".SHGC%d" % (a["glazing"]["shgc"]*100)
|
|
507
|
+
|
|
508
|
+
if a["glazing"]:
|
|
509
|
+
layers = openstudio.model.FenestrationMaterialVector()
|
|
510
|
+
|
|
511
|
+
u = a["glazing"]["u" ]
|
|
512
|
+
shgc = a["glazing"]["shgc"]
|
|
513
|
+
lyr = model.getSimpleGlazingByName(a["glazing"]["id"])
|
|
514
|
+
|
|
515
|
+
if lyr:
|
|
516
|
+
lyr = lyr.get()
|
|
517
|
+
else:
|
|
518
|
+
lyr = openstudio.model.SimpleGlazing(model, u, shgc)
|
|
519
|
+
lyr.setName(a["glazing"]["id"])
|
|
520
|
+
|
|
521
|
+
layers.append(lyr)
|
|
522
|
+
else:
|
|
523
|
+
layers = openstudio.model.OpaqueMaterialVector()
|
|
524
|
+
|
|
525
|
+
# Loop through each layer spec, and generate construction.
|
|
526
|
+
for i, l in a.items():
|
|
527
|
+
if not l: continue
|
|
528
|
+
|
|
529
|
+
lyr = model.getStandardOpaqueMaterialByName(l["id"])
|
|
530
|
+
|
|
531
|
+
if lyr:
|
|
532
|
+
lyr = lyr.get()
|
|
533
|
+
else:
|
|
534
|
+
lyr = openstudio.model.StandardOpaqueMaterial(model)
|
|
535
|
+
lyr.setName(l["id"])
|
|
536
|
+
lyr.setThickness(l["d"])
|
|
537
|
+
if "rgh" in l["mat"]: lyr.setRoughness(l["mat"]["rgh"])
|
|
538
|
+
if "k" in l["mat"]: lyr.setConductivity(l["mat"]["k"])
|
|
539
|
+
if "rho" in l["mat"]: lyr.setDensity(l["mat"]["rho"])
|
|
540
|
+
if "cp" in l["mat"]: lyr.setSpecificHeat(l["mat"]["cp" ])
|
|
541
|
+
if "thm" in l["mat"]: lyr.setThermalAbsorptance(l["mat"]["thm"])
|
|
542
|
+
if "sol" in l["mat"]: lyr.setSolarAbsorptance(l["mat"]["sol"])
|
|
543
|
+
if "vis" in l["mat"]: lyr.setVisibleAbsorptance(l["mat"]["vis"])
|
|
544
|
+
|
|
545
|
+
layers.append(lyr)
|
|
546
|
+
|
|
547
|
+
c = openstudio.model.Construction(layers)
|
|
548
|
+
c.setName(id)
|
|
549
|
+
|
|
550
|
+
# Adjust insulating layer thickness or conductivity to match requested Uo.
|
|
551
|
+
if not a["glazing"]:
|
|
552
|
+
ro = 1 / specs["uo"] - film()[specs["type"]] if specs["uo"] else 0
|
|
553
|
+
|
|
554
|
+
if specs["type"] == "door": # 1x layer, adjust conductivity
|
|
555
|
+
layer = c.getLayer(0).to_StandardOpaqueMaterial()
|
|
556
|
+
|
|
557
|
+
if not layer:
|
|
558
|
+
return oslg.invalid(id + " standard material?", mth, 0)
|
|
559
|
+
|
|
560
|
+
layer = layer.get()
|
|
561
|
+
k = layer.thickness() / ro
|
|
562
|
+
layer.setConductivity(k)
|
|
563
|
+
|
|
564
|
+
elif ro > 0: # multiple layers, adjust insulating layer thickness
|
|
565
|
+
lyr = insulatingLayer(c)
|
|
566
|
+
|
|
567
|
+
if not lyr["index"] or not lyr["type"] or not lyr["r"]:
|
|
568
|
+
return oslg.invalid(id + " construction", mth, 0)
|
|
569
|
+
|
|
570
|
+
index = lyr["index"]
|
|
571
|
+
layer = c.getLayer(index).to_StandardOpaqueMaterial()
|
|
572
|
+
|
|
573
|
+
if not layer:
|
|
574
|
+
return oslg.invalid(id + " material %d" % index, mth, 0)
|
|
575
|
+
|
|
576
|
+
layer = layer.get()
|
|
577
|
+
k = layer.conductivity()
|
|
578
|
+
d = (ro - rsi(c) + lyr["r"]) * k
|
|
579
|
+
|
|
580
|
+
if d < 0.03:
|
|
581
|
+
return oslg.invalid(id + " adjusted material thickness", mth, 0)
|
|
582
|
+
|
|
583
|
+
nom = re.sub(r'[^a-zA-Z]', '', layer.nameString())
|
|
584
|
+
nom = re.sub(r'OSut', '', nom)
|
|
585
|
+
nom = "OSut." + nom + ".%03d" % int(d * 1000)
|
|
586
|
+
|
|
587
|
+
if not model.getStandardOpaqueMaterialByName(nom):
|
|
588
|
+
layer.setName(nom)
|
|
589
|
+
layer.setThickness(d)
|
|
590
|
+
|
|
591
|
+
return c
|
|
592
|
+
|
|
593
|
+
|
|
594
|
+
def genShade(subs=None) -> bool:
|
|
595
|
+
"""Generates solar shade(s) (e.g. roller, textile) for glazed OpenStudio
|
|
596
|
+
SubSurfaces (v321+), controlled to minimize overheating in cooling months
|
|
597
|
+
(May to October in Northern Hemisphere), when outdoor dry bulb temperature
|
|
598
|
+
is above 18°C and impinging solar radiation is above 100 W/m2.
|
|
599
|
+
|
|
600
|
+
Args:
|
|
601
|
+
subs:
|
|
602
|
+
A list of sub surfaces.
|
|
603
|
+
|
|
604
|
+
Returns:
|
|
605
|
+
True: If shade successfully generated.
|
|
606
|
+
False: If invalid input (see logs).
|
|
607
|
+
|
|
608
|
+
"""
|
|
609
|
+
# Filter OpenStudio warnings for ShadingControl:
|
|
610
|
+
# ref: https://github.com/NREL/OpenStudio/issues/4911
|
|
611
|
+
# str = ".*(?<!ShadingControl)$"
|
|
612
|
+
# openstudio.Logger().instance().standardOutLogger().setChannelRegex(str)
|
|
613
|
+
|
|
614
|
+
mth = "osut.genShade"
|
|
615
|
+
cl = openstudio.model.SubSurfaceVector
|
|
616
|
+
|
|
617
|
+
if int("".join(openstudio.openStudioVersion().split("."))) < 321:
|
|
618
|
+
return False
|
|
619
|
+
if not isinstance(subs, cl):
|
|
620
|
+
return oslg.mismatch("subs", subs, cl, mth, CN.DBG, False)
|
|
621
|
+
if not subs:
|
|
622
|
+
return oslg.empty("subs", mth, CN.WRN, False)
|
|
623
|
+
|
|
624
|
+
# Shading availability period.
|
|
625
|
+
model = subs[0].model()
|
|
626
|
+
id = "onoff"
|
|
627
|
+
onoff = model.getScheduleTypeLimitsByName(id)
|
|
628
|
+
|
|
629
|
+
if onoff:
|
|
630
|
+
onoff = onoff.get()
|
|
631
|
+
else:
|
|
632
|
+
onoff = openstudio.model.ScheduleTypeLimits(model)
|
|
633
|
+
onoff.setName(id)
|
|
634
|
+
onoff.setLowerLimitValue(0)
|
|
635
|
+
onoff.setUpperLimitValue(1)
|
|
636
|
+
onoff.setNumericType("Discrete")
|
|
637
|
+
onoff.setUnitType("Availability")
|
|
638
|
+
|
|
639
|
+
# Shading schedule.
|
|
640
|
+
id = "OSut.SHADE.Ruleset"
|
|
641
|
+
sch = model.getScheduleRulesetByName(id)
|
|
642
|
+
|
|
643
|
+
if sch:
|
|
644
|
+
sch = sch.get()
|
|
645
|
+
else:
|
|
646
|
+
sch = openstudio.model.ScheduleRuleset(model, 0)
|
|
647
|
+
sch.setName(id)
|
|
648
|
+
sch.setScheduleTypeLimits(onoff)
|
|
649
|
+
sch.defaultDaySchedule.setName("OSut.SHADE.Ruleset.Default")
|
|
650
|
+
|
|
651
|
+
# Summer cooling rule.
|
|
652
|
+
id = "OSut.SHADE.ScheduleRule"
|
|
653
|
+
rule = model.getScheduleRuleByName(id)
|
|
654
|
+
|
|
655
|
+
if rule:
|
|
656
|
+
rule = rule.get()
|
|
657
|
+
else:
|
|
658
|
+
may = openstudio.MonthOfYear("May")
|
|
659
|
+
october = openstudio.MonthOfYear("Oct")
|
|
660
|
+
start = openstudio.Date(may, 1)
|
|
661
|
+
finish = openstudio.Date(october, 31)
|
|
662
|
+
|
|
663
|
+
rule = openstudio.model.ScheduleRule(sch)
|
|
664
|
+
rule.setName(id)
|
|
665
|
+
rule.setStartDate(start)
|
|
666
|
+
rule.setEndDate(finish)
|
|
667
|
+
rule.setApplyAllDays(True)
|
|
668
|
+
rule.daySchedule.setName("OSut.SHADE.Rule.Default")
|
|
669
|
+
rule.daySchedule.addValue(openstudio.Time(0,24,0,0), 1)
|
|
670
|
+
|
|
671
|
+
# Shade object.
|
|
672
|
+
id = "OSut.SHADE"
|
|
673
|
+
shd = mdl.getShadeByName(id)
|
|
674
|
+
|
|
675
|
+
if shd:
|
|
676
|
+
shd = shd.get()
|
|
677
|
+
else:
|
|
678
|
+
shd = openstudio.model.Shade(mdl)
|
|
679
|
+
shd.setName(id)
|
|
680
|
+
|
|
681
|
+
# Shading control (unique to each call).
|
|
682
|
+
id = "OSut.ShadingControl"
|
|
683
|
+
ctl = openstudio.model.ShadingControl(shd)
|
|
684
|
+
ctl.setName(id)
|
|
685
|
+
ctl.setSchedule(sch)
|
|
686
|
+
ctl.setShadingControlType("OnIfHighOutdoorAirTempAndHighSolarOnWindow")
|
|
687
|
+
ctl.setSetpoint(18) # °C
|
|
688
|
+
ctl.setSetpoint2(100) # W/m2
|
|
689
|
+
ctl.setMultipleSurfaceControlType("Group")
|
|
690
|
+
ctl.setSubSurfaces(subs)
|
|
691
|
+
|
|
692
|
+
return True
|
|
693
|
+
|
|
694
|
+
|
|
695
|
+
def genMass(sps=None, ratio=2.0) -> bool:
|
|
696
|
+
""" Generates an internal mass definition and instances for target spaces.
|
|
697
|
+
This is largely adapted from OpenStudio-Standards:
|
|
698
|
+
https://github.com/NREL/openstudio-standards/blob/
|
|
699
|
+
eac3805a65be060b39ecaf7901c908f8ed2c051b/lib/openstudio-standards/
|
|
700
|
+
prototypes/common/objects/Prototype.Model.rb#L572
|
|
701
|
+
|
|
702
|
+
Args:
|
|
703
|
+
sps (OpenStudio::Model::SpaceVector):
|
|
704
|
+
Target spaces.
|
|
705
|
+
ratio (float):
|
|
706
|
+
Ratio of internal mass surface area to floor surface area.
|
|
707
|
+
|
|
708
|
+
Returns:
|
|
709
|
+
bool: Whether successfully generated.
|
|
710
|
+
False: If invalid inputs (see logs).
|
|
711
|
+
|
|
712
|
+
"""
|
|
713
|
+
mth = "osut.genMass"
|
|
714
|
+
cl = openstudio.model.SpaceVector
|
|
715
|
+
|
|
716
|
+
if not isinstance(sps, cl):
|
|
717
|
+
return oslg.mismatch("spaces", sps, cl, mth, CN.DBG, False)
|
|
718
|
+
|
|
719
|
+
try:
|
|
720
|
+
ratio = float(ratio)
|
|
721
|
+
except:
|
|
722
|
+
return oslg.mismatch("ratio", ratio, float, mth, CN.DBG, False)
|
|
723
|
+
|
|
724
|
+
if not sps:
|
|
725
|
+
return oslg.empty("spaces", mth, CN.DBG, False)
|
|
726
|
+
if ratio < 0:
|
|
727
|
+
return oslg.negative("ratio", mth, CN.ERR, False)
|
|
728
|
+
|
|
729
|
+
# A single material.
|
|
730
|
+
mdl = sps[0].model()
|
|
731
|
+
id = "OSut.MASS.Material"
|
|
732
|
+
mat = mdl.getOpaqueMaterialByName(id)
|
|
733
|
+
|
|
734
|
+
if mat:
|
|
735
|
+
mat = mat.get()
|
|
736
|
+
else:
|
|
737
|
+
mat = openstudio.model.StandardOpaqueMaterial(mdl)
|
|
738
|
+
mat.setName(id)
|
|
739
|
+
mat.setRoughness("MediumRough")
|
|
740
|
+
mat.setThickness(0.15)
|
|
741
|
+
mat.setConductivity(1.12)
|
|
742
|
+
mat.setDensity(540)
|
|
743
|
+
mat.setSpecificHeat(1210)
|
|
744
|
+
mat.setThermalAbsorptance(0.9)
|
|
745
|
+
mat.setSolarAbsorptance(0.7)
|
|
746
|
+
mat.setVisibleAbsorptance(0.17)
|
|
747
|
+
|
|
748
|
+
# A single, 1x layered construction.
|
|
749
|
+
id = "OSut.MASS.Construction"
|
|
750
|
+
con = mdl.getConstructionByName(id)
|
|
751
|
+
|
|
752
|
+
if con:
|
|
753
|
+
con = con.get()
|
|
754
|
+
else:
|
|
755
|
+
con = openstudio.model.Construction(mdl)
|
|
756
|
+
con.setName(id)
|
|
757
|
+
layers = openstudio.model.MaterialVector()
|
|
758
|
+
layers.append(mat)
|
|
759
|
+
con.setLayers(layers)
|
|
760
|
+
|
|
761
|
+
id = "OSut.InternalMassDefinition.%.2f" % ratio
|
|
762
|
+
df = mdl.getInternalMassDefinitionByName(id)
|
|
763
|
+
|
|
764
|
+
if df:
|
|
765
|
+
df = df.get
|
|
766
|
+
else:
|
|
767
|
+
df = openstudio.model.InternalMassDefinition(mdl)
|
|
768
|
+
df.setName(id)
|
|
769
|
+
df.setConstruction(con)
|
|
770
|
+
df.setSurfaceAreaperSpaceFloorArea(ratio)
|
|
771
|
+
|
|
772
|
+
for sp in sps:
|
|
773
|
+
mass = openstudio.model.InternalMass(df)
|
|
774
|
+
mass.setName("OSut.InternalMass.%s" % sp.nameString())
|
|
775
|
+
mass.setSpace(sp)
|
|
776
|
+
|
|
777
|
+
return True
|
|
778
|
+
|
|
779
|
+
|
|
780
|
+
def holdsConstruction(set=None, base=None, gr=False, ex=False, type=""):
|
|
781
|
+
"""Validates whether a default construction set holds a base construction.
|
|
782
|
+
|
|
783
|
+
Args:
|
|
784
|
+
set (openstudio.model.DefaultConstructionSet):
|
|
785
|
+
A default construction set.
|
|
786
|
+
base (openstudio.model.ConstructionBase):
|
|
787
|
+
A construction base.
|
|
788
|
+
gr (bool):
|
|
789
|
+
Whether ground-facing surface.
|
|
790
|
+
ex (bool):
|
|
791
|
+
Whether exterior-facing surface.
|
|
792
|
+
type:
|
|
793
|
+
An OpenStudio surface (or sub surface) type (e.g. "Wall").
|
|
794
|
+
|
|
795
|
+
Returns:
|
|
796
|
+
bool: Whether default set holds construction.
|
|
797
|
+
False: If invalid input (see logs).
|
|
798
|
+
|
|
799
|
+
"""
|
|
800
|
+
mth = "osut.holdsConstruction"
|
|
801
|
+
cl1 = openstudio.model.DefaultConstructionSet
|
|
802
|
+
cl2 = openstudio.model.ConstructionBase
|
|
803
|
+
t1 = openstudio.model.Surface.validSurfaceTypeValues()
|
|
804
|
+
t2 = openstudio.model.SubSurface.validSubSurfaceTypeValues()
|
|
805
|
+
t1 = [t.lower() for t in t1]
|
|
806
|
+
t2 = [t.lower() for t in t2]
|
|
807
|
+
c = None
|
|
808
|
+
|
|
809
|
+
if not isinstance(set, cl1):
|
|
810
|
+
return oslg.mismatch("set", set, cl1, mth, CN.DBG, False)
|
|
811
|
+
if not isinstance(base, cl2):
|
|
812
|
+
return oslg.mismatch("base", base, cl2, mth, CN.DBG, False)
|
|
813
|
+
if not isinstance(gr, bool):
|
|
814
|
+
return oslg.mismatch("ground", gr, bool, mth, CN.DBG, False)
|
|
815
|
+
if not isinstance(ex, bool):
|
|
816
|
+
return oslg.mismatch("exterior", ex, bool, mth, CN.DBG, False)
|
|
817
|
+
|
|
818
|
+
try:
|
|
819
|
+
type = str(type)
|
|
820
|
+
except:
|
|
821
|
+
return oslg.mismatch("surface type", type, str, mth, CN.DBG, False)
|
|
822
|
+
|
|
823
|
+
type = type.lower()
|
|
824
|
+
|
|
825
|
+
if type in t1:
|
|
826
|
+
if gr:
|
|
827
|
+
if set.defaultGroundContactSurfaceConstructions():
|
|
828
|
+
c = set.defaultGroundContactSurfaceConstructions().get()
|
|
829
|
+
elif ex:
|
|
830
|
+
if set.defaultExteriorSurfaceConstructions():
|
|
831
|
+
c = set.defaultExteriorSurfaceConstructions().get()
|
|
832
|
+
else:
|
|
833
|
+
if set.defaultInteriorSurfaceConstructions():
|
|
834
|
+
c = set.defaultInteriorSurfaceConstructions().get()
|
|
835
|
+
elif type in t2:
|
|
836
|
+
if gr:
|
|
837
|
+
return False
|
|
838
|
+
if ex:
|
|
839
|
+
if set.defaultExteriorSubSurfaceConstructions():
|
|
840
|
+
c = set.defaultExteriorSubSurfaceConstructions().get()
|
|
841
|
+
else:
|
|
842
|
+
if set.defaultInteriorSubSurfaceConstructions():
|
|
843
|
+
c = set.defaultInteriorSubSurfaceConstructions().get()
|
|
844
|
+
else:
|
|
845
|
+
return oslg.invalid("surface type", mth, 5, CN.DBG, False)
|
|
846
|
+
|
|
847
|
+
if not c: return False
|
|
848
|
+
|
|
849
|
+
if type in t1:
|
|
850
|
+
if type == "roofceiling":
|
|
851
|
+
if c.roofCeilingConstruction():
|
|
852
|
+
if c.roofCeilingConstruction().get() == base: return True
|
|
853
|
+
elif type == "floor":
|
|
854
|
+
if c.floorConstruction():
|
|
855
|
+
if c.floorConstruction().get() == base: return True
|
|
856
|
+
else: # "wall"
|
|
857
|
+
if c.wallConstruction():
|
|
858
|
+
if c.wallConstruction().get() == base: return True
|
|
859
|
+
else: # t2
|
|
860
|
+
if type == "tubulardaylightdiffuser":
|
|
861
|
+
if c.tubularDaylightDiffuserConstruction():
|
|
862
|
+
if c.tubularDaylightDiffuserConstruction() == base: return True
|
|
863
|
+
elif type == "tubulardaylightdome":
|
|
864
|
+
if c.tubularDaylightDomeConstruction():
|
|
865
|
+
if c.tubularDaylightDomeConstruction().get() == base: return True
|
|
866
|
+
elif type == "skylight":
|
|
867
|
+
if c.overheadDoorConstruction():
|
|
868
|
+
if c.overheadDoorConstruction().get() == base: return True
|
|
869
|
+
elif type == "glassdoor":
|
|
870
|
+
if c.glassDoorConstruction():
|
|
871
|
+
if c.glassDoorConstruction().get() == base: return True
|
|
872
|
+
elif type == "door":
|
|
873
|
+
if c.doorConstruction():
|
|
874
|
+
if c.doorConstruction().get() == base: return True
|
|
875
|
+
elif type == "operablewindow":
|
|
876
|
+
if c.operableWindowConstruction():
|
|
877
|
+
if c.operableWindowConstruction().get() == base: return True
|
|
878
|
+
else: # "fixedwindow"
|
|
879
|
+
if c.fixedWindowConstruction():
|
|
880
|
+
if c.fixedWindowConstruction().get() == base: return True
|
|
881
|
+
|
|
882
|
+
return False
|
|
883
|
+
|
|
884
|
+
|
|
885
|
+
def defaultConstructionSet(s=None):
|
|
886
|
+
"""Returns a surface's default construction set.
|
|
887
|
+
|
|
888
|
+
Args:
|
|
889
|
+
s (openstudio.model.Surface):
|
|
890
|
+
A surface.
|
|
891
|
+
|
|
892
|
+
Returns:
|
|
893
|
+
openstudio.model.DefaultConstructionSet: A default construction set.
|
|
894
|
+
None: If invalid inputs (see logs).
|
|
895
|
+
|
|
896
|
+
"""
|
|
897
|
+
mth = "osut.defaultConstructionSet"
|
|
898
|
+
cl = openstudio.model.Surface
|
|
899
|
+
|
|
900
|
+
if not isinstance(s, cl):
|
|
901
|
+
return oslg.mismatch("surface", s, cl, mth)
|
|
902
|
+
if not s.isConstructionDefaulted():
|
|
903
|
+
oslg.log(CN.WRN, "construction not defaulted (%s)" % mth)
|
|
904
|
+
return None
|
|
905
|
+
if not s.construction():
|
|
906
|
+
return oslg.empty("construction", mth, CN.WRN)
|
|
907
|
+
if not s.space():
|
|
908
|
+
return oslg.empty("space", mth, CN.WRN)
|
|
909
|
+
|
|
910
|
+
mdl = s.model()
|
|
911
|
+
base = s.construction().get()
|
|
912
|
+
space = s.space().get()
|
|
913
|
+
type = s.surfaceType()
|
|
914
|
+
bnd = s.outsideBoundaryCondition().lower()
|
|
915
|
+
|
|
916
|
+
ground = True if s.isGroundSurface() else False
|
|
917
|
+
exterior = True if bnd == "outdoors" else False
|
|
918
|
+
|
|
919
|
+
if space.defaultConstructionSet():
|
|
920
|
+
set = space.defaultConstructionSet().get()
|
|
921
|
+
|
|
922
|
+
if holdsConstruction(set, base, ground, exterior, type): return set
|
|
923
|
+
|
|
924
|
+
if space.spaceType():
|
|
925
|
+
spacetype = space.spaceType().get()
|
|
926
|
+
|
|
927
|
+
if spacetype.defaultConstructionSet():
|
|
928
|
+
set = spacetype.defaultConstructionSet().get()
|
|
929
|
+
|
|
930
|
+
if holdsConstruction(set, base, ground, exterior, type):
|
|
931
|
+
return set
|
|
932
|
+
|
|
933
|
+
if space.buildingStory():
|
|
934
|
+
story = space.buildingStory().get()
|
|
935
|
+
|
|
936
|
+
if story.defaultConstructionSet():
|
|
937
|
+
set = story.defaultConstructionSet().get()
|
|
938
|
+
|
|
939
|
+
if holdsConstruction(set, base, ground, exterior, type):
|
|
940
|
+
return set
|
|
941
|
+
|
|
942
|
+
|
|
943
|
+
building = mdl.getBuilding()
|
|
944
|
+
|
|
945
|
+
if building.defaultConstructionSet():
|
|
946
|
+
set = building.defaultConstructionSet().get()
|
|
947
|
+
|
|
948
|
+
if holdsConstruction(set, base, ground, exterior, type):
|
|
949
|
+
return set
|
|
950
|
+
|
|
951
|
+
return None
|
|
952
|
+
|
|
953
|
+
|
|
954
|
+
def are_standardOpaqueLayers(lc=None) -> bool:
|
|
955
|
+
"""Validates if every material in a layered construction is standard/opaque.
|
|
956
|
+
|
|
957
|
+
Args:
|
|
958
|
+
lc (openstudio.model.LayeredConstruction):
|
|
959
|
+
an OpenStudio layered construction
|
|
960
|
+
|
|
961
|
+
Returns:
|
|
962
|
+
True: If all layers are valid (standard & opaque).
|
|
963
|
+
False: If invalid inputs (see logs).
|
|
964
|
+
|
|
965
|
+
"""
|
|
966
|
+
mth = "osut.are_standardOpaqueLayers"
|
|
967
|
+
cl = openstudio.model.LayeredConstruction
|
|
968
|
+
|
|
969
|
+
if not isinstance(lc, cl):
|
|
970
|
+
return oslg.mismatch("lc", lc, cl, mth, CN.DBG, 0.0)
|
|
971
|
+
|
|
972
|
+
for m in lc.layers():
|
|
973
|
+
if not m.to_StandardOpaqueMaterial(): return False
|
|
974
|
+
|
|
975
|
+
return True
|
|
976
|
+
|
|
977
|
+
|
|
978
|
+
def thickness(lc=None) -> float:
|
|
979
|
+
"""Returns total (standard opaque) layered construction thickness (m).
|
|
980
|
+
|
|
981
|
+
Args:
|
|
982
|
+
lc (openstudio.model.LayeredConstruction):
|
|
983
|
+
an OpenStudio layered construction
|
|
984
|
+
|
|
985
|
+
Returns:
|
|
986
|
+
float: A standard opaque construction thickness.
|
|
987
|
+
0.0: If invalid inputs (see logs).
|
|
988
|
+
|
|
989
|
+
"""
|
|
990
|
+
mth = "osut.thickness"
|
|
991
|
+
cl = openstudio.model.LayeredConstruction
|
|
992
|
+
d = 0.0
|
|
993
|
+
|
|
994
|
+
if not isinstance(lc, cl):
|
|
995
|
+
return oslg.mismatch("lc", lc, cl, mth, CN.DBG, 0.0)
|
|
996
|
+
if not are_standardOpaqueLayers(lc):
|
|
997
|
+
oslg.log(CN.ERR, "holding non-StandardOpaqueMaterial(s) %s" % mth)
|
|
998
|
+
return d
|
|
999
|
+
|
|
1000
|
+
for m in lc.layers(): d += m.thickness()
|
|
1001
|
+
|
|
1002
|
+
return d
|
|
1003
|
+
|
|
1004
|
+
|
|
1005
|
+
def glazingAirFilmRSi(usi=5.85) -> float:
|
|
1006
|
+
"""Returns total air film resistance of a fenestrated construction (m2•K/W).
|
|
1007
|
+
|
|
1008
|
+
Args:
|
|
1009
|
+
usi (float):
|
|
1010
|
+
A fenestrated construction's U-factor (W/m2•K).
|
|
1011
|
+
|
|
1012
|
+
Returns:
|
|
1013
|
+
float: Total air film resistances.
|
|
1014
|
+
0.1216: If invalid input (see logs).
|
|
1015
|
+
|
|
1016
|
+
"""
|
|
1017
|
+
# The sum of thermal resistances of calculated exterior and interior film
|
|
1018
|
+
# coefficients under standard winter conditions are taken from:
|
|
1019
|
+
#
|
|
1020
|
+
# https://bigladdersoftware.com/epx/docs/9-6/engineering-reference/
|
|
1021
|
+
# window-calculation-module.html#simple-window-model
|
|
1022
|
+
#
|
|
1023
|
+
# These remain acceptable approximations for flat windows, yet likely
|
|
1024
|
+
# unsuitable for subsurfaces with curved or projecting shapes like domed
|
|
1025
|
+
# skylights. The solution here is considered an adequate fix for reporting,
|
|
1026
|
+
# awaiting eventual OpenStudio (and EnergyPlus) upgrades to report NFRC 100
|
|
1027
|
+
# (or ISO) air film resistances under standard winter conditions.
|
|
1028
|
+
#
|
|
1029
|
+
# For U-factors above 8.0 W/m2•K (or invalid input), the function returns
|
|
1030
|
+
# 0.1216 m2•K/W, which corresponds to a construction with a single glass
|
|
1031
|
+
# layer thickness of 2mm & k = ~0.6 W/m.K.
|
|
1032
|
+
#
|
|
1033
|
+
# The EnergyPlus Engineering calculations were designed for vertical
|
|
1034
|
+
# windows, not for horizontal, slanted or domed surfaces - use with caution.
|
|
1035
|
+
mth = "osut.glazingAirFilmRSi"
|
|
1036
|
+
val = 0.1216
|
|
1037
|
+
|
|
1038
|
+
try:
|
|
1039
|
+
usi = float(usi)
|
|
1040
|
+
except:
|
|
1041
|
+
return oslg.mismatch("usi", usi, float, mth, CN.DBG, val)
|
|
1042
|
+
|
|
1043
|
+
if usi > 8.0:
|
|
1044
|
+
return oslg.invalid("usi", mth, 1, CN.WRN, val)
|
|
1045
|
+
elif usi < 0:
|
|
1046
|
+
return oslg.negative("usi", mth, CN.WRN, val)
|
|
1047
|
+
elif abs(usi) < CN.TOL:
|
|
1048
|
+
return oslg.zero("usi", mth, CN.WRN, val)
|
|
1049
|
+
|
|
1050
|
+
rsi = 1 / (0.025342 * usi + 29.163853) # exterior film, next interior film
|
|
1051
|
+
|
|
1052
|
+
if usi < 5.85:
|
|
1053
|
+
return rsi + 1 / (0.359073 * math.log(usi) + 6.949915)
|
|
1054
|
+
|
|
1055
|
+
return rsi + 1 / (1.788041 * usi - 2.886625)
|
|
1056
|
+
|
|
1057
|
+
|
|
1058
|
+
def rsi(lc=None, film=0.0, t=0.0) -> float:
|
|
1059
|
+
"""Returns a construction's 'standard calc' thermal resistance (m2•K/W),
|
|
1060
|
+
which includes air film resistances. It excludes insulating effects of
|
|
1061
|
+
shades, screens, etc. in the case of fenestrated constructions. Adapted
|
|
1062
|
+
from BTAP's 'Material' Module "get_conductance" (P. Lopez).
|
|
1063
|
+
|
|
1064
|
+
Args:
|
|
1065
|
+
lc (openstudio.model.LayeredConstruction):
|
|
1066
|
+
an OpenStudio layered construction
|
|
1067
|
+
film (float):
|
|
1068
|
+
thermal resistance of surface air films (m2•K/W)
|
|
1069
|
+
t (float):
|
|
1070
|
+
gas temperature (°C) (optional)
|
|
1071
|
+
|
|
1072
|
+
Returns:
|
|
1073
|
+
float: A layered construction's thermal resistance.
|
|
1074
|
+
0.0: If invalid input (see logs).
|
|
1075
|
+
|
|
1076
|
+
"""
|
|
1077
|
+
mth = "osut.rsi"
|
|
1078
|
+
cl = openstudio.model.LayeredConstruction
|
|
1079
|
+
|
|
1080
|
+
if not isinstance(lc, cl):
|
|
1081
|
+
return oslg.mismatch("lc", lc, cl, mth, CN.DBG, 0.0)
|
|
1082
|
+
|
|
1083
|
+
try:
|
|
1084
|
+
film = float(film)
|
|
1085
|
+
except:
|
|
1086
|
+
return oslg.mismatch("film", film, float, mth, CN.DBG, 0.0)
|
|
1087
|
+
|
|
1088
|
+
try:
|
|
1089
|
+
t = float(t)
|
|
1090
|
+
except:
|
|
1091
|
+
return oslg.mismatch("temp K", t, float, mth, CN.DBG, 0.0)
|
|
1092
|
+
|
|
1093
|
+
t += 273.0 # °C to K
|
|
1094
|
+
|
|
1095
|
+
if t < 0:
|
|
1096
|
+
return oslg.negative("temp K", mth, CN.ERR, 0.0)
|
|
1097
|
+
if film < 0:
|
|
1098
|
+
return oslg.negative("film", mth, CN.ERR, 0.0)
|
|
1099
|
+
|
|
1100
|
+
rsi = film
|
|
1101
|
+
|
|
1102
|
+
for m in lc.layers():
|
|
1103
|
+
if m.to_SimpleGlazing():
|
|
1104
|
+
return 1 / m.to_SimpleGlazing().get().uFactor()
|
|
1105
|
+
elif m.to_StandardGlazing():
|
|
1106
|
+
rsi += m.to_StandardGlazing().get().thermalResistance()
|
|
1107
|
+
elif m.to_RefractionExtinctionGlazing():
|
|
1108
|
+
rsi += m.to_RefractionExtinctionGlazing().get().thermalResistance()
|
|
1109
|
+
elif m.to_Gas():
|
|
1110
|
+
rsi += m.to_Gas().get().getThermalResistance(t)
|
|
1111
|
+
elif m.to_GasMixture():
|
|
1112
|
+
rsi += m.to_GasMixture().get().getThermalResistance(t)
|
|
1113
|
+
|
|
1114
|
+
# Opaque materials next.
|
|
1115
|
+
if m.to_StandardOpaqueMaterial():
|
|
1116
|
+
rsi += m.to_StandardOpaqueMaterial().get().thermalResistance()
|
|
1117
|
+
elif m.to_MasslessOpaqueMaterial():
|
|
1118
|
+
rsi += m.to_MasslessOpaqueMaterial()
|
|
1119
|
+
elif m.to_RoofVegetation():
|
|
1120
|
+
rsi += m.to_RoofVegetation().get().thermalResistance()
|
|
1121
|
+
elif m.to_AirGap():
|
|
1122
|
+
rsi += m.to_AirGap().get().thermalResistance()
|
|
1123
|
+
|
|
1124
|
+
return rsi
|
|
1125
|
+
|
|
1126
|
+
|
|
1127
|
+
def insulatingLayer(lc=None) -> dict:
|
|
1128
|
+
"""Identifies a layered construction's (opaque) insulating layer.
|
|
1129
|
+
|
|
1130
|
+
Args:
|
|
1131
|
+
lc (openStudio.model.LayeredConstruction):
|
|
1132
|
+
an OpenStudio layered construction
|
|
1133
|
+
|
|
1134
|
+
Returns:
|
|
1135
|
+
An insulating-layer dictionary:
|
|
1136
|
+
- "index" (int): construction's insulating layer index [0, n layers)
|
|
1137
|
+
- "type" (str): layer material type ("standard" or "massless")
|
|
1138
|
+
- "r" (float): material thermal resistance in m2•K/W.
|
|
1139
|
+
If unsuccessful, dictionary is voided as follows (see logs):
|
|
1140
|
+
"index": None
|
|
1141
|
+
"type": None
|
|
1142
|
+
"r": 0.0
|
|
1143
|
+
|
|
1144
|
+
"""
|
|
1145
|
+
mth = "osut.insulatingLayer"
|
|
1146
|
+
cl = openstudio.model.LayeredConstruction
|
|
1147
|
+
res = dict(index=None, type=None, r=0.0)
|
|
1148
|
+
i = 0 # iterator
|
|
1149
|
+
|
|
1150
|
+
if not isinstance(lc, cl):
|
|
1151
|
+
return oslg.mismatch("lc", lc, cl, mth, CN.DBG, res)
|
|
1152
|
+
|
|
1153
|
+
for m in lc.layers():
|
|
1154
|
+
if m.to_MasslessOpaqueMaterial():
|
|
1155
|
+
m = m.to_MasslessOpaqueMaterial().get()
|
|
1156
|
+
|
|
1157
|
+
if m.thermalResistance() < 0.001 or m.thermalResistance() < res["r"]:
|
|
1158
|
+
i += 1
|
|
1159
|
+
continue
|
|
1160
|
+
else:
|
|
1161
|
+
res["r" ] = m.thermalResistance()
|
|
1162
|
+
res["index"] = i
|
|
1163
|
+
res["type" ] = "massless"
|
|
1164
|
+
|
|
1165
|
+
if m.to_StandardOpaqueMaterial():
|
|
1166
|
+
m = m.to_StandardOpaqueMaterial().get()
|
|
1167
|
+
k = m.thermalConductivity()
|
|
1168
|
+
d = m.thickness()
|
|
1169
|
+
|
|
1170
|
+
if (d < 0.003) or (k > 3.0) or (d / k < res["r"]):
|
|
1171
|
+
i += 1
|
|
1172
|
+
continue
|
|
1173
|
+
else:
|
|
1174
|
+
res["r" ] = d / k
|
|
1175
|
+
res["index"] = i
|
|
1176
|
+
res["type" ] = "standard"
|
|
1177
|
+
|
|
1178
|
+
i += 1
|
|
1179
|
+
|
|
1180
|
+
return res
|
|
1181
|
+
|
|
1182
|
+
|
|
1183
|
+
def is_spandrel(s=None) -> bool:
|
|
1184
|
+
"""Validates whether opaque surface can be considered as a curtain wall
|
|
1185
|
+
(or similar technology) spandrel, regardless of construction layers, by
|
|
1186
|
+
looking up AdditionalProperties or its identifier.
|
|
1187
|
+
|
|
1188
|
+
Args:
|
|
1189
|
+
s (openstudio.model.Surface):
|
|
1190
|
+
An opaque surface.
|
|
1191
|
+
|
|
1192
|
+
Returns:
|
|
1193
|
+
bool: Whether surface can be considered 'spandrel'.
|
|
1194
|
+
False: If invalid input (see logs).
|
|
1195
|
+
"""
|
|
1196
|
+
mth = "osut.is_spandrel"
|
|
1197
|
+
cl = openstudio.model.Surface
|
|
1198
|
+
|
|
1199
|
+
if not isinstance(s, cl):
|
|
1200
|
+
return oslg.mismatch("surface", s, cl, mth, CN.DBG, False)
|
|
1201
|
+
|
|
1202
|
+
# Prioritize AdditionalProperties route.
|
|
1203
|
+
if s.additionalProperties().hasFeature("spandrel"):
|
|
1204
|
+
val = s.additionalProperties().getFeatureAsBoolean("spandrel")
|
|
1205
|
+
|
|
1206
|
+
if not val:
|
|
1207
|
+
return oslg.invalid("spandrel", mth, 1, CN.ERR, False)
|
|
1208
|
+
|
|
1209
|
+
val = val.get()
|
|
1210
|
+
|
|
1211
|
+
if not isinstance(val, bool):
|
|
1212
|
+
return invalid("spandrel bool", mth, 1, CN.ERR, False)
|
|
1213
|
+
|
|
1214
|
+
return val
|
|
1215
|
+
|
|
1216
|
+
# Fallback: check for 'spandrel' in surface name.
|
|
1217
|
+
return "spandrel" in s.nameString().lower()
|
|
1218
|
+
|
|
1219
|
+
|
|
1220
|
+
def is_fenestration(s=None) -> bool:
|
|
1221
|
+
"""Validates whether a sub surface is fenestrated.
|
|
1222
|
+
|
|
1223
|
+
Args:
|
|
1224
|
+
s (openstudio.model.SubSurface):
|
|
1225
|
+
An OpenStudio sub surface.
|
|
1226
|
+
|
|
1227
|
+
Returns:
|
|
1228
|
+
bool: Whether subsurface can be considered 'fenestrated'.
|
|
1229
|
+
False: If invalid input (see logs).
|
|
1230
|
+
|
|
1231
|
+
"""
|
|
1232
|
+
mth = "osut.is_fenestration"
|
|
1233
|
+
cl = openstudio.model.SubSurface
|
|
1234
|
+
|
|
1235
|
+
if not isinstance(s, cl):
|
|
1236
|
+
return oslg.mismatch("subsurface", s, cl, mth, CN.DBG, False)
|
|
1237
|
+
|
|
1238
|
+
# OpenStudio::Model::SubSurface.validSubSurfaceTypeValues
|
|
1239
|
+
# "FixedWindow" : fenestration
|
|
1240
|
+
# "OperableWindow" : fenestration
|
|
1241
|
+
# "Door"
|
|
1242
|
+
# "GlassDoor" : fenestration
|
|
1243
|
+
# "OverheadDoor"
|
|
1244
|
+
# "Skylight" : fenestration
|
|
1245
|
+
# "TubularDaylightDome" : fenestration
|
|
1246
|
+
# "TubularDaylightDiffuser" : fenestration
|
|
1247
|
+
if s.subSurfaceType().lower() in ["door", "overheaddoor"]: return False
|
|
1248
|
+
|
|
1249
|
+
return True
|
|
1250
|
+
|
|
1251
|
+
|
|
1252
|
+
def has_airLoopsHVAC(model=None) -> bool:
|
|
1253
|
+
"""Validates if model has zones with HVAC air loops.
|
|
1254
|
+
|
|
1255
|
+
Args:
|
|
1256
|
+
model (openstudio.model.Model):
|
|
1257
|
+
An OpenStudio model.
|
|
1258
|
+
|
|
1259
|
+
Returns:
|
|
1260
|
+
bool: Whether model has HVAC air loops.
|
|
1261
|
+
False: If invalid input (see logs).
|
|
1262
|
+
"""
|
|
1263
|
+
mth = "osut.has_airLoopsHVAC"
|
|
1264
|
+
cl = openstudio.model.Model
|
|
1265
|
+
|
|
1266
|
+
if not isinstance(model, cl):
|
|
1267
|
+
return oslg.mismatch("model", model, cl, mth, CN.DBG, False)
|
|
1268
|
+
|
|
1269
|
+
for zone in model.getThermalZones():
|
|
1270
|
+
if zone.canBePlenum(): continue
|
|
1271
|
+
if zone.airLoopHVACs() or zone.isPlenum(): return True
|
|
1272
|
+
|
|
1273
|
+
return False
|
|
1274
|
+
|
|
1275
|
+
|
|
1276
|
+
def scheduleRulesetMinMax(sched=None) -> dict:
|
|
1277
|
+
"""Returns MIN/MAX values of a schedule (ruleset).
|
|
1278
|
+
|
|
1279
|
+
Args:
|
|
1280
|
+
sched (openstudio.model.ScheduleRuleset):
|
|
1281
|
+
A schedule.
|
|
1282
|
+
|
|
1283
|
+
Returns:
|
|
1284
|
+
dict:
|
|
1285
|
+
- "min" (float): min temperature. (None if invalid inputs - see logs).
|
|
1286
|
+
- "max" (float): max temperature. (None if invalid inputs - see logs).
|
|
1287
|
+
"""
|
|
1288
|
+
# Largely inspired from David Goldwasser's
|
|
1289
|
+
# "schedule_ruleset_annual_min_max_value":
|
|
1290
|
+
#
|
|
1291
|
+
# github.com/NREL/openstudio-standards/blob/
|
|
1292
|
+
# 99cf713750661fe7d2082739f251269c2dfd9140/lib/openstudio-standards/
|
|
1293
|
+
# standards/Standards.ScheduleRuleset.rb#L124
|
|
1294
|
+
mth = "osut.scheduleRulesetMinMax"
|
|
1295
|
+
cl = openstudio.model.ScheduleRuleset
|
|
1296
|
+
res = dict(min=None, max=None)
|
|
1297
|
+
|
|
1298
|
+
if not isinstance(sched, cl):
|
|
1299
|
+
return oslg.mismatch("sched", sched, cl, mth, CN.DBG, res)
|
|
1300
|
+
|
|
1301
|
+
values = list(sched.defaultDaySchedule().values())
|
|
1302
|
+
|
|
1303
|
+
for rule in sched.scheduleRules(): values += rule.daySchedule().values()
|
|
1304
|
+
|
|
1305
|
+
res["min"] = min(values)
|
|
1306
|
+
res["max"] = max(values)
|
|
1307
|
+
|
|
1308
|
+
try:
|
|
1309
|
+
res["min"] = float(res["min"])
|
|
1310
|
+
except:
|
|
1311
|
+
res["min"] = None
|
|
1312
|
+
|
|
1313
|
+
try:
|
|
1314
|
+
res["max"] = float(res["max"])
|
|
1315
|
+
except:
|
|
1316
|
+
res["max"] = None
|
|
1317
|
+
|
|
1318
|
+
return res
|
|
1319
|
+
|
|
1320
|
+
|
|
1321
|
+
def scheduleConstantMinMax(sched=None) -> dict:
|
|
1322
|
+
"""Returns MIN/MAX values of a schedule (constant).
|
|
1323
|
+
|
|
1324
|
+
Args:
|
|
1325
|
+
sched (openstudio.model.ScheduleConstant):
|
|
1326
|
+
A schedule.
|
|
1327
|
+
|
|
1328
|
+
Returns:
|
|
1329
|
+
dict:
|
|
1330
|
+
- "min" (float): min temperature. (None if invalid inputs - see logs).
|
|
1331
|
+
- "max" (float): max temperature. (None if invalid inputs - see logs).
|
|
1332
|
+
"""
|
|
1333
|
+
# Largely inspired from David Goldwasser's
|
|
1334
|
+
# "schedule_constant_annual_min_max_value":
|
|
1335
|
+
#
|
|
1336
|
+
# github.com/NREL/openstudio-standards/blob/
|
|
1337
|
+
# 99cf713750661fe7d2082739f251269c2dfd9140/lib/openstudio-standards/
|
|
1338
|
+
# standards/Standards.ScheduleConstant.rb#L21
|
|
1339
|
+
mth = "osut.scheduleConstantMinMax"
|
|
1340
|
+
cl = openstudio.model.ScheduleConstant
|
|
1341
|
+
res = dict(min=None, max=None)
|
|
1342
|
+
|
|
1343
|
+
if not isinstance(sched, cl):
|
|
1344
|
+
return oslg.mismatch("sched", sched, cl, mth, CN.DBG, res)
|
|
1345
|
+
|
|
1346
|
+
try:
|
|
1347
|
+
value = float(sched.value())
|
|
1348
|
+
except:
|
|
1349
|
+
return None
|
|
1350
|
+
|
|
1351
|
+
res["min"] = value
|
|
1352
|
+
res["max"] = value
|
|
1353
|
+
|
|
1354
|
+
return res
|
|
1355
|
+
|
|
1356
|
+
|
|
1357
|
+
def scheduleCompactMinMax(sched=None) -> dict:
|
|
1358
|
+
"""Returns MIN/MAX values of a schedule (compact).
|
|
1359
|
+
|
|
1360
|
+
Args:
|
|
1361
|
+
sched (openstudio.model.ScheduleCompact):
|
|
1362
|
+
A schedule.
|
|
1363
|
+
|
|
1364
|
+
Returns:
|
|
1365
|
+
dict:
|
|
1366
|
+
- "min" (float): min temperature. (None if invalid inputs - see logs).
|
|
1367
|
+
- "max" (float): max temperature. (None if invalid inputs - see logs).
|
|
1368
|
+
"""
|
|
1369
|
+
# Largely inspired from Andrew Parker's
|
|
1370
|
+
# "schedule_compact_annual_min_max_value":
|
|
1371
|
+
#
|
|
1372
|
+
# github.com/NREL/openstudio-standards/blob/
|
|
1373
|
+
# 99cf713750661fe7d2082739f251269c2dfd9140/lib/openstudio-standards/
|
|
1374
|
+
# standards/Standards.ScheduleCompact.rb#L8
|
|
1375
|
+
mth = "osut.scheduleCompactMinMax"
|
|
1376
|
+
cl = openstudio.model.ScheduleCompact
|
|
1377
|
+
vals = []
|
|
1378
|
+
prev = ""
|
|
1379
|
+
res = dict(min=None, max=None)
|
|
1380
|
+
|
|
1381
|
+
if not isinstance(sched, cl):
|
|
1382
|
+
return oslg.mismatch("sched", sched, cl, mth, CN.DBG, res)
|
|
1383
|
+
|
|
1384
|
+
for eg in sched.extensibleGroups():
|
|
1385
|
+
if "until" in prev:
|
|
1386
|
+
if eg.getDouble(0): vals.append(eg.getDouble(0).get())
|
|
1387
|
+
|
|
1388
|
+
str = eg.getString(0)
|
|
1389
|
+
|
|
1390
|
+
if str: prev = str.get().lower()
|
|
1391
|
+
|
|
1392
|
+
if not vals:
|
|
1393
|
+
return oslg.empty("compact sched values", mth, CN.WRN, res)
|
|
1394
|
+
|
|
1395
|
+
res["min"] = min(vals)
|
|
1396
|
+
res["max"] = max(vals)
|
|
1397
|
+
|
|
1398
|
+
try:
|
|
1399
|
+
res["min"] = float(res["min"])
|
|
1400
|
+
except:
|
|
1401
|
+
res["min"] = None
|
|
1402
|
+
|
|
1403
|
+
try:
|
|
1404
|
+
res["max"] = float(res["max"])
|
|
1405
|
+
except:
|
|
1406
|
+
res["max"] = None
|
|
1407
|
+
|
|
1408
|
+
return res
|
|
1409
|
+
|
|
1410
|
+
|
|
1411
|
+
def scheduleIntervalMinMax(sched=None) -> dict:
|
|
1412
|
+
"""Returns MIN/MAX values of a schedule (interval).
|
|
1413
|
+
|
|
1414
|
+
Args:
|
|
1415
|
+
sched (openstudio.model.ScheduleInterval):
|
|
1416
|
+
A schedule.
|
|
1417
|
+
|
|
1418
|
+
Returns:
|
|
1419
|
+
dict:
|
|
1420
|
+
- "min" (float): min temperature. (None if invalid inputs - see logs).
|
|
1421
|
+
- "max" (float): max temperature. (None if invalid inputs - see logs).
|
|
1422
|
+
"""
|
|
1423
|
+
mth = "osut.scheduleCompactMinMax"
|
|
1424
|
+
cl = openstudio.model.ScheduleInterval
|
|
1425
|
+
vals = []
|
|
1426
|
+
res = dict(min=None, max=None)
|
|
1427
|
+
|
|
1428
|
+
if not isinstance(sched, cl):
|
|
1429
|
+
return oslg.mismatch("sched", sched, cl, mth, CN.DBG, res)
|
|
1430
|
+
|
|
1431
|
+
vals = sched.timeSeries().values()
|
|
1432
|
+
|
|
1433
|
+
res["min"] = min(values)
|
|
1434
|
+
res["max"] = max(values)
|
|
1435
|
+
|
|
1436
|
+
try:
|
|
1437
|
+
res["min"] = float(res["min"])
|
|
1438
|
+
except:
|
|
1439
|
+
res["min"] = None
|
|
1440
|
+
|
|
1441
|
+
try:
|
|
1442
|
+
res["max"] = float(res["max"])
|
|
1443
|
+
except:
|
|
1444
|
+
res["max"] = None
|
|
1445
|
+
|
|
1446
|
+
return res
|
|
1447
|
+
|
|
1448
|
+
|
|
1449
|
+
def maxHeatScheduledSetpoint(zone=None) -> dict:
|
|
1450
|
+
"""Returns MAX zone heating temperature schedule setpoint [°C] and
|
|
1451
|
+
whether zone has an active dual setpoint thermostat.
|
|
1452
|
+
|
|
1453
|
+
Args:
|
|
1454
|
+
zone (openstudio.model.ThermalZone):
|
|
1455
|
+
An OpenStudio thermal zone.
|
|
1456
|
+
|
|
1457
|
+
Returns:
|
|
1458
|
+
dict:
|
|
1459
|
+
- spt (float): MAX heating setpoint (None if invalid inputs - see logs).
|
|
1460
|
+
- dual (bool): dual setpoint? (False if invalid inputs - see logs).
|
|
1461
|
+
"""
|
|
1462
|
+
# Largely inspired from Parker & Marrec's "thermal_zone_heated?" procedure.
|
|
1463
|
+
# The solution here is a tad more relaxed to encompass SEMIHEATED zones as
|
|
1464
|
+
# per Canadian NECB criteria (basically any space with at least 10 W/m2 of
|
|
1465
|
+
# installed heating equipement, i.e. below freezing in Canada).
|
|
1466
|
+
#
|
|
1467
|
+
# github.com/NREL/openstudio-standards/blob/
|
|
1468
|
+
# 58964222d25783e9da4ae292e375fb0d5c902aa5/lib/openstudio-standards/
|
|
1469
|
+
# standards/Standards.ThermalZone.rb#L910
|
|
1470
|
+
mth = "osut.maxHeatScheduledSetpoint"
|
|
1471
|
+
cl = openstudio.model.ThermalZone
|
|
1472
|
+
res = dict(spt=None, dual=False)
|
|
1473
|
+
|
|
1474
|
+
if not isinstance(zone, cl):
|
|
1475
|
+
return oslg.mismatch("zone", zone, cl, mth, CN.DBG, res)
|
|
1476
|
+
|
|
1477
|
+
# Zone radiant heating? Get schedule from radiant system.
|
|
1478
|
+
for equip in zone.equipment():
|
|
1479
|
+
sched = None
|
|
1480
|
+
|
|
1481
|
+
if equip.to_ZoneHVACHighTemperatureRadiant():
|
|
1482
|
+
equip = equip.to_ZoneHVACHighTemperatureRadiant().get()
|
|
1483
|
+
|
|
1484
|
+
if equip.heatingSetpointTemperatureSchedule():
|
|
1485
|
+
sched = equip.heatingSetpointTemperatureSchedule().get()
|
|
1486
|
+
|
|
1487
|
+
if equip.to_ZoneHVACLowTemperatureRadiantElectric():
|
|
1488
|
+
equip = equip.to_ZoneHVACLowTemperatureRadiantElectric().get()
|
|
1489
|
+
|
|
1490
|
+
sched = equip.heatingSetpointTemperatureSchedule()
|
|
1491
|
+
|
|
1492
|
+
if equip.to_ZoneHVACLowTempRadiantConstFlow():
|
|
1493
|
+
equip = equip.to_ZoneHVACLowTempRadiantConstFlow().get()
|
|
1494
|
+
coil = equip.heatingCoil()
|
|
1495
|
+
|
|
1496
|
+
if coil.to_CoilHeatingLowTempRadiantConstFlow():
|
|
1497
|
+
coil = coil.to_CoilHeatingLowTempRadiantConstFlow().get()
|
|
1498
|
+
|
|
1499
|
+
if coil.heatingHighControlTemperatureSchedule():
|
|
1500
|
+
sched = c.heatingHighControlTemperatureSchedule().get()
|
|
1501
|
+
|
|
1502
|
+
if equip.to_ZoneHVACLowTempRadiantVarFlow():
|
|
1503
|
+
equip = equip.to_ZoneHVACLowTempRadiantVarFlow().get()
|
|
1504
|
+
coil = equip.heatingCoil()
|
|
1505
|
+
|
|
1506
|
+
if coil.to_CoilHeatingLowTempRadiantVarFlow():
|
|
1507
|
+
coil = coil.to_CoilHeatingLowTempRadiantVarFlow().get()
|
|
1508
|
+
|
|
1509
|
+
if coil.heatingControlTemperatureSchedule():
|
|
1510
|
+
sched = coil.heatingControlTemperatureSchedule().get()
|
|
1511
|
+
|
|
1512
|
+
if not sched: continue
|
|
1513
|
+
|
|
1514
|
+
if sched.to_ScheduleRuleset():
|
|
1515
|
+
sched = sched.to_ScheduleRuleset().get()
|
|
1516
|
+
maximum = scheduleRulesetMinMax(sched)["max"]
|
|
1517
|
+
|
|
1518
|
+
if maximum:
|
|
1519
|
+
if not res["spt"] or res["spt"] < maximum:
|
|
1520
|
+
res["spt"] = maximum
|
|
1521
|
+
|
|
1522
|
+
dd = sched.winterDesignDaySchedule()
|
|
1523
|
+
|
|
1524
|
+
if dd.values():
|
|
1525
|
+
if not res["spt"] or res["spt"] < max(dd.values()):
|
|
1526
|
+
res["spt"] = max(dd.values())
|
|
1527
|
+
|
|
1528
|
+
if sched.to_ScheduleConstant():
|
|
1529
|
+
sched = sched.to_ScheduleConstant().get()
|
|
1530
|
+
maximum = scheduleConstantMinMax(sched)["max"]
|
|
1531
|
+
|
|
1532
|
+
if maximum:
|
|
1533
|
+
if not res["spt"] or res["spt"] < maximum:
|
|
1534
|
+
res["spt"] = maximum
|
|
1535
|
+
|
|
1536
|
+
if sched.to_ScheduleCompact():
|
|
1537
|
+
sched = sched.to_ScheduleCompact().get()
|
|
1538
|
+
maximum = scheduleCompactMinMax(sched)["max"]
|
|
1539
|
+
|
|
1540
|
+
if maximum:
|
|
1541
|
+
if not res["spt"] or res["spt"] < maximum:
|
|
1542
|
+
res["spt"] = maximum
|
|
1543
|
+
|
|
1544
|
+
if sched.to_ScheduleInterval():
|
|
1545
|
+
sched = sched.to_ScheduleInterval().get()
|
|
1546
|
+
maximum = scheduleIntervalMinMax(sched)["max"]
|
|
1547
|
+
|
|
1548
|
+
if maximum:
|
|
1549
|
+
if not res["spt"] or res["spt"] < maximum:
|
|
1550
|
+
res["spt"] = maximum
|
|
1551
|
+
|
|
1552
|
+
if not zone.thermostat(): return res
|
|
1553
|
+
|
|
1554
|
+
tstat = zone.thermostat().get()
|
|
1555
|
+
res["spt"] = None
|
|
1556
|
+
|
|
1557
|
+
if (tstat.to_ThermostatSetpointDualSetpoint() or
|
|
1558
|
+
tstat.to_ZoneControlThermostatStagedDualSetpoint()):
|
|
1559
|
+
|
|
1560
|
+
if tstat.to_ThermostatSetpointDualSetpoint():
|
|
1561
|
+
tstat = tstat.to_ThermostatSetpointDualSetpoint().get()
|
|
1562
|
+
else:
|
|
1563
|
+
tstat = tstat.to_ZoneControlThermostatStagedDualSetpoint().get()
|
|
1564
|
+
|
|
1565
|
+
if tstat.heatingSetpointTemperatureSchedule():
|
|
1566
|
+
res["dual"] = True
|
|
1567
|
+
sched = tstat.heatingSetpointTemperatureSchedule().get()
|
|
1568
|
+
|
|
1569
|
+
if sched.to_ScheduleRuleset():
|
|
1570
|
+
sched = sched.to_ScheduleRuleset().get()
|
|
1571
|
+
maximum = scheduleRulesetMinMax(sched)["max"]
|
|
1572
|
+
|
|
1573
|
+
if maximum:
|
|
1574
|
+
if not res["spt"] or res["spt"] < maximum:
|
|
1575
|
+
res["spt"] = maximum
|
|
1576
|
+
|
|
1577
|
+
dd = sched.winterDesignDaySchedule()
|
|
1578
|
+
|
|
1579
|
+
if dd.values():
|
|
1580
|
+
if not res["spt"] or res["spt"] < max(dd.values()):
|
|
1581
|
+
res["spt"] = max(dd.values())
|
|
1582
|
+
|
|
1583
|
+
if sched.to_ScheduleConstant():
|
|
1584
|
+
sched = sched.to_ScheduleConstant().get()
|
|
1585
|
+
maximum = scheduleConstantMinMax(sched)["max"]
|
|
1586
|
+
|
|
1587
|
+
if maximum:
|
|
1588
|
+
if not res["spt"] or res["spt"] < maximum:
|
|
1589
|
+
res["spt"] = maximum
|
|
1590
|
+
|
|
1591
|
+
if sched.to_ScheduleCompact():
|
|
1592
|
+
sched = sched.to_ScheduleCompact().get()
|
|
1593
|
+
maximum = scheduleCompactMinMax(sched)["max"]
|
|
1594
|
+
|
|
1595
|
+
if maximum:
|
|
1596
|
+
if not res["spt"] or res["spt"] < maximum:
|
|
1597
|
+
res["spt"] = maximum
|
|
1598
|
+
|
|
1599
|
+
if sched.to_ScheduleInterval():
|
|
1600
|
+
sched = sched.to_ScheduleInterval().get()
|
|
1601
|
+
maximum = scheduleIntervalMinMax(sched)["max"]
|
|
1602
|
+
|
|
1603
|
+
if maximum:
|
|
1604
|
+
if not res["spt"] or res["spt"] < maximum:
|
|
1605
|
+
res["spt"] = maximum
|
|
1606
|
+
|
|
1607
|
+
if sched.to_ScheduleYear():
|
|
1608
|
+
sched = sched.to_ScheduleYear().get()
|
|
1609
|
+
|
|
1610
|
+
for week in sched.getScheduleWeeks():
|
|
1611
|
+
if not week.winterDesignDaySchedule():
|
|
1612
|
+
dd = week.winterDesignDaySchedule().get()
|
|
1613
|
+
|
|
1614
|
+
if dd.values():
|
|
1615
|
+
if not res["spt"] or res["spt"] < max(dd.values()):
|
|
1616
|
+
res["spt"] = max(dd.values())
|
|
1617
|
+
return res
|
|
1618
|
+
|
|
1619
|
+
|
|
1620
|
+
def has_heatingTemperatureSetpoints(model=None):
|
|
1621
|
+
"""Confirms if model has zones with valid heating temperature setpoints.
|
|
1622
|
+
|
|
1623
|
+
Args:
|
|
1624
|
+
model (openstudio.model.Model):
|
|
1625
|
+
An OpenStudio model.
|
|
1626
|
+
|
|
1627
|
+
Returns:
|
|
1628
|
+
bool: Whether model holds valid heating temperature setpoints.
|
|
1629
|
+
False: If invalid inputs (see logs).
|
|
1630
|
+
"""
|
|
1631
|
+
mth = "osut.has_heatingTemperatureSetpoints"
|
|
1632
|
+
cl = openstudio.model.Model
|
|
1633
|
+
|
|
1634
|
+
if not isinstance(model, cl):
|
|
1635
|
+
return oslg.mismatch("model", model, cl, mth, CN.DBG, False)
|
|
1636
|
+
|
|
1637
|
+
for zone in model.getThermalZones():
|
|
1638
|
+
if maxHeatScheduledSetpoint(zone)["spt"]: return True
|
|
1639
|
+
|
|
1640
|
+
return False
|
|
1641
|
+
|
|
1642
|
+
|
|
1643
|
+
def minCoolScheduledSetpoint(zone=None):
|
|
1644
|
+
"""Returns MIN zone cooling temperature schedule setpoint [°C] and
|
|
1645
|
+
whether zone has an active dual setpoint thermostat.
|
|
1646
|
+
|
|
1647
|
+
Args:
|
|
1648
|
+
zone (openstudio.model.ThermalZone):
|
|
1649
|
+
An OpenStudio thermal zone.
|
|
1650
|
+
|
|
1651
|
+
Returns:
|
|
1652
|
+
dict:
|
|
1653
|
+
- spt (float): MIN cooling setpoint (None if invalid inputs - see logs).
|
|
1654
|
+
- dual (bool): dual setpoint? (False if invalid inputs - see logs).
|
|
1655
|
+
"""
|
|
1656
|
+
# Largely inspired from Parker & Marrec's "thermal_zone_cooled?" procedure.
|
|
1657
|
+
#
|
|
1658
|
+
# github.com/NREL/openstudio-standards/blob/
|
|
1659
|
+
# 99cf713750661fe7d2082739f251269c2dfd9140/lib/openstudio-standards/
|
|
1660
|
+
# standards/Standards.ThermalZone.rb#L1058
|
|
1661
|
+
mth = "osut.minCoolScheduledSetpoint"
|
|
1662
|
+
cl = openstudio.model.ThermalZone
|
|
1663
|
+
res = dict(spt=None, dual=False)
|
|
1664
|
+
|
|
1665
|
+
if not isinstance(zone, cl):
|
|
1666
|
+
return oslg.mismatch("zone", zone, cl, mth, CN.DBG, res)
|
|
1667
|
+
|
|
1668
|
+
# Zone radiant cooling? Get schedule from radiant system.
|
|
1669
|
+
for equip in zone.equipment():
|
|
1670
|
+
sched = None
|
|
1671
|
+
|
|
1672
|
+
if equip.to_ZoneHVACLowTempRadiantConstFlow():
|
|
1673
|
+
equip = equip.to_ZoneHVACLowTempRadiantConstFlow().get()
|
|
1674
|
+
coil = equip.coolingCoil()
|
|
1675
|
+
|
|
1676
|
+
if coil.to_CoilCoolingLowTempRadiantConstFlow():
|
|
1677
|
+
coil = coil.to_CoilCoolingLowTempRadiantConstFlow().get()
|
|
1678
|
+
|
|
1679
|
+
if coil.coolingLowControlTemperatureSchedule():
|
|
1680
|
+
sched = coil.coolingLowControlTemperatureSchedule().get()
|
|
1681
|
+
|
|
1682
|
+
if equip.to_ZoneHVACLowTempRadiantVarFlow():
|
|
1683
|
+
equip = equip.to_ZoneHVACLowTempRadiantVarFlow().get()
|
|
1684
|
+
coil = equip.coolingCoil()
|
|
1685
|
+
|
|
1686
|
+
if coil.to_CoilCoolingLowTempRadiantVarFlow():
|
|
1687
|
+
coil = coil.to_CoilCoolingLowTempRadiantVarFlow().get()
|
|
1688
|
+
|
|
1689
|
+
if coil.coolingControlTemperatureSchedule():
|
|
1690
|
+
sched = coil.coolingControlTemperatureSchedule().get()
|
|
1691
|
+
|
|
1692
|
+
if not sched: continue
|
|
1693
|
+
|
|
1694
|
+
if sched.to_ScheduleRuleset():
|
|
1695
|
+
sched = sched.to_ScheduleRuleset().get()
|
|
1696
|
+
minimum = scheduleRulesetMinMax(sched)["min"]
|
|
1697
|
+
|
|
1698
|
+
if minimum:
|
|
1699
|
+
if not res["spt"] or res["spt"] > minimum:
|
|
1700
|
+
res["spt"] = minimum
|
|
1701
|
+
|
|
1702
|
+
dd = sched.summerDesignDaySchedule()
|
|
1703
|
+
|
|
1704
|
+
if dd.values():
|
|
1705
|
+
if not res["spt"] or res["spt"] > min(dd.values()):
|
|
1706
|
+
res["spt"] = min(dd.values())
|
|
1707
|
+
|
|
1708
|
+
if sched.to_ScheduleConstant():
|
|
1709
|
+
sched = sched.to_ScheduleConstant().get()
|
|
1710
|
+
minimum = scheduleConstantMinMax(sched)["min"]
|
|
1711
|
+
|
|
1712
|
+
if minimum:
|
|
1713
|
+
if not res["spt"] or res["spt"] > minimum:
|
|
1714
|
+
res["spt"] = minimum
|
|
1715
|
+
|
|
1716
|
+
if sched.to_ScheduleCompact():
|
|
1717
|
+
sched = sched.to_ScheduleCompact().get()
|
|
1718
|
+
minimum = scheduleCompactMinMax(sched)["min"]
|
|
1719
|
+
|
|
1720
|
+
if minimum:
|
|
1721
|
+
if not res["spt"] or res["spt"] > minimum:
|
|
1722
|
+
res["spt"] = minimum
|
|
1723
|
+
|
|
1724
|
+
if sched.to_ScheduleInterval():
|
|
1725
|
+
sched = sched.to_ScheduleInterval().get()
|
|
1726
|
+
minimum = scheduleIntervalMinMax(sched)["min"]
|
|
1727
|
+
|
|
1728
|
+
if minimum:
|
|
1729
|
+
if not res["spt"] or res["spt"] > minimum:
|
|
1730
|
+
res["spt"] = minimum
|
|
1731
|
+
|
|
1732
|
+
if not zone.thermostat(): return res
|
|
1733
|
+
|
|
1734
|
+
tstat = zone.thermostat().get()
|
|
1735
|
+
res["spt"] = None
|
|
1736
|
+
|
|
1737
|
+
if (tstat.to_ThermostatSetpointDualSetpoint() or
|
|
1738
|
+
tstat.to_ZoneControlThermostatStagedDualSetpoint()):
|
|
1739
|
+
|
|
1740
|
+
if tstat.to_ThermostatSetpointDualSetpoint():
|
|
1741
|
+
tstat = tstat.to_ThermostatSetpointDualSetpoint().get()
|
|
1742
|
+
else:
|
|
1743
|
+
tstat = tstat.to_ZoneControlThermostatStagedDualSetpoint().get()
|
|
1744
|
+
|
|
1745
|
+
if tstat.coolingSetpointTemperatureSchedule():
|
|
1746
|
+
res["dual"] = True
|
|
1747
|
+
sched = tstat.coolingSetpointTemperatureSchedule().get()
|
|
1748
|
+
|
|
1749
|
+
if sched.to_ScheduleRuleset():
|
|
1750
|
+
sched = sched.to_ScheduleRuleset().get()
|
|
1751
|
+
|
|
1752
|
+
minimum = scheduleRulesetMinMax(sched)["min"]
|
|
1753
|
+
|
|
1754
|
+
if minimum:
|
|
1755
|
+
if not res["spt"] or res["spt"] > minimum:
|
|
1756
|
+
res["spt"] = minimum
|
|
1757
|
+
|
|
1758
|
+
dd = sched.summerDesignDaySchedule()
|
|
1759
|
+
|
|
1760
|
+
if dd.values():
|
|
1761
|
+
if not res["spt"] or res["spt"] > min(dd.values()):
|
|
1762
|
+
res["spt"] = min(dd.values())
|
|
1763
|
+
|
|
1764
|
+
if sched.to_ScheduleConstant():
|
|
1765
|
+
sched = sched.to_ScheduleConstant().get()
|
|
1766
|
+
minimum = scheduleConstantMinMax(sched)[:min]
|
|
1767
|
+
|
|
1768
|
+
if minimum:
|
|
1769
|
+
if not res["spt"] or res["spt"] > minimum:
|
|
1770
|
+
res["spt"] = minimum
|
|
1771
|
+
|
|
1772
|
+
if sched.to_ScheduleCompact():
|
|
1773
|
+
sched = sched.to_ScheduleCompact().get()
|
|
1774
|
+
minimum = scheduleCompactMinMax(sched)["min"]
|
|
1775
|
+
|
|
1776
|
+
if minimum:
|
|
1777
|
+
if not res["spt"] or res["spt"] > minimum:
|
|
1778
|
+
res["spt"] = minimum
|
|
1779
|
+
|
|
1780
|
+
if sched.to_ScheduleInterval():
|
|
1781
|
+
sched = sched.to_ScheduleInterval().get()
|
|
1782
|
+
minimum = scheduleIntervalMinMax(sched)["min"]
|
|
1783
|
+
|
|
1784
|
+
if minimum:
|
|
1785
|
+
if not res["spt"] or res["spt"] > minimum:
|
|
1786
|
+
res["spt"] = minimum
|
|
1787
|
+
|
|
1788
|
+
if sched.to_ScheduleYear():
|
|
1789
|
+
sched = sched.to_ScheduleYear().get()
|
|
1790
|
+
|
|
1791
|
+
for week in sched.getScheduleWeeks():
|
|
1792
|
+
if not week.summerDesignDaySchedule():
|
|
1793
|
+
dd = week.summerDesignDaySchedule().get()
|
|
1794
|
+
|
|
1795
|
+
if dd.values():
|
|
1796
|
+
if not res["spt"] or res["spt"] < min(dd.values()):
|
|
1797
|
+
res["spt"] = min(dd.values())
|
|
1798
|
+
|
|
1799
|
+
return res
|
|
1800
|
+
|
|
1801
|
+
|
|
1802
|
+
def has_coolingTemperatureSetpoints(model=None):
|
|
1803
|
+
"""Confirms if model has zones with valid cooling temperature setpoints.
|
|
1804
|
+
|
|
1805
|
+
Args:
|
|
1806
|
+
model (openstudio.model.Model):
|
|
1807
|
+
An OpenStudio model.
|
|
1808
|
+
|
|
1809
|
+
Returns:
|
|
1810
|
+
bool: Whether model holds valid cooling temperature setpoints.
|
|
1811
|
+
False: If invalid inputs (see logs).
|
|
1812
|
+
"""
|
|
1813
|
+
mth = "osut.has_coolingTemperatureSetpoints"
|
|
1814
|
+
cl = openstudio.model.Model
|
|
1815
|
+
|
|
1816
|
+
if not isinstance(model, cl):
|
|
1817
|
+
return oslg.mismatch("model", model, cl, mth, CN.DBG, False)
|
|
1818
|
+
|
|
1819
|
+
for zone in model.getThermalZones():
|
|
1820
|
+
if minCoolScheduledSetpoint(zone)["spt"]: return True
|
|
1821
|
+
|
|
1822
|
+
return False
|
|
1823
|
+
|
|
1824
|
+
|
|
1825
|
+
def is_vestibule(space=None):
|
|
1826
|
+
"""Validates whether space is a vestibule.
|
|
1827
|
+
|
|
1828
|
+
Args:
|
|
1829
|
+
space ():
|
|
1830
|
+
An OpenStudio space.
|
|
1831
|
+
|
|
1832
|
+
Returns:
|
|
1833
|
+
bool: Whether space is considered a vestibule.
|
|
1834
|
+
False: If invalid input (see logs).
|
|
1835
|
+
"""
|
|
1836
|
+
# INFO: OpenStudio-Standards' "thermal_zone_vestibule" criteria:
|
|
1837
|
+
# - zones less than 200ft2; AND
|
|
1838
|
+
# - having infiltration using Design Flow Rate
|
|
1839
|
+
#
|
|
1840
|
+
# github.com/NREL/openstudio-standards/blob/
|
|
1841
|
+
# 86bcd026a20001d903cc613bed6d63e94b14b142/lib/openstudio-standards/
|
|
1842
|
+
# standards/Standards.ThermalZone.rb#L1264
|
|
1843
|
+
#
|
|
1844
|
+
# This (unused) OpenStudio-Standards method likely needs revision; it
|
|
1845
|
+
# returns "false" if the thermal zone area were less than 200ft2. Not sure
|
|
1846
|
+
# which edition of 90.1 relies on a 200ft2 threshold (2010?); 90.1 2016
|
|
1847
|
+
# doesn't. Yet even fixed, the method would nonetheless misidentify as
|
|
1848
|
+
# "vestibule" a small space along an exterior wall, such as a semiheated
|
|
1849
|
+
# storage space.
|
|
1850
|
+
#
|
|
1851
|
+
# The code below is intended as a simple short-term solution, basically
|
|
1852
|
+
# relying on AdditionalProperties, or (if missing) a "vestibule" substring
|
|
1853
|
+
# within a space's spaceType name (or the latter's standardsSpaceType).
|
|
1854
|
+
#
|
|
1855
|
+
# Alternatively, some future method could infer its status as a vestibule
|
|
1856
|
+
# based on a few basic features (common to all vintages):
|
|
1857
|
+
# - 1x+ outdoor-facing wall(s) holding 1x+ door(s)
|
|
1858
|
+
# - adjacent to 1x+ 'occupied' conditioned space(s)
|
|
1859
|
+
# - ideally, 1x+ door(s) between vestibule and 1x+ such adjacent space(s)
|
|
1860
|
+
#
|
|
1861
|
+
# An additional method parameter (i.e. std = "necb") could be added to
|
|
1862
|
+
# ensure supplementary Standard-specific checks, e.g. maximum floor area,
|
|
1863
|
+
# minimum distance between doors.
|
|
1864
|
+
#
|
|
1865
|
+
# Finally, an entirely separate method could be developed to first identify
|
|
1866
|
+
# whether "building entrances" (a defined term in 90.1) actually require
|
|
1867
|
+
# vestibules as per specific code requirements. Food for thought.
|
|
1868
|
+
mth = "osut.is_vestibule"
|
|
1869
|
+
cl = openstudio.model.Space
|
|
1870
|
+
|
|
1871
|
+
if not isinstance(space, cl):
|
|
1872
|
+
return oslg.mismatch("space", space, cl, mth, CN.DBG, False)
|
|
1873
|
+
|
|
1874
|
+
id = space.nameString()
|
|
1875
|
+
m1 = "%s:vestibule" % id
|
|
1876
|
+
m2 = "%s:boolean" % m1
|
|
1877
|
+
|
|
1878
|
+
if space.additionalProperties().hasFeature("vestibule"):
|
|
1879
|
+
val = space.additionalProperties().getFeatureAsBoolean("vestibule")
|
|
1880
|
+
|
|
1881
|
+
if val:
|
|
1882
|
+
val = val.get()
|
|
1883
|
+
|
|
1884
|
+
if isinstance(val, bool):
|
|
1885
|
+
return val
|
|
1886
|
+
else:
|
|
1887
|
+
return oslg.invalid(m2, mth, 1, CN.ERR, False)
|
|
1888
|
+
else:
|
|
1889
|
+
return oslg.invalid(m1, mth, 1, CN.ERR, False)
|
|
1890
|
+
|
|
1891
|
+
if space.spaceType():
|
|
1892
|
+
type = space.spaceType().get()
|
|
1893
|
+
if "plenum" in type.nameString().lower(): return False
|
|
1894
|
+
if "vestibule" in type.nameString().lower(): return True
|
|
1895
|
+
|
|
1896
|
+
if type.standardsSpaceType():
|
|
1897
|
+
type = type.standardsSpaceType().get().lower()
|
|
1898
|
+
if "plenum" in type: return False
|
|
1899
|
+
if "vestibule" in type: return True
|
|
1900
|
+
|
|
1901
|
+
return False
|
|
1902
|
+
|
|
1903
|
+
|
|
1904
|
+
def is_plenum(space=None):
|
|
1905
|
+
"""Validates whether a space is an indirectly-conditioned plenum.
|
|
1906
|
+
|
|
1907
|
+
Args:
|
|
1908
|
+
space (openstudio.model.Space):
|
|
1909
|
+
An OpenStudio space.
|
|
1910
|
+
|
|
1911
|
+
Returns:
|
|
1912
|
+
bool: Whether space is considered a plenum.
|
|
1913
|
+
False: If invalid input (see logs).
|
|
1914
|
+
"""
|
|
1915
|
+
# Largely inspired from NREL's "space_plenum?":
|
|
1916
|
+
#
|
|
1917
|
+
# github.com/NREL/openstudio-standards/blob/
|
|
1918
|
+
# 58964222d25783e9da4ae292e375fb0d5c902aa5/lib/openstudio-standards/
|
|
1919
|
+
# standards/Standards.Space.rb#L1384
|
|
1920
|
+
#
|
|
1921
|
+
# Ideally, OSut's "is_plenum" should be in sync with OpenStudio SDK's
|
|
1922
|
+
# "isPlenum" method, which solely looks for either HVAC air mixer objects:
|
|
1923
|
+
# - AirLoopHVACReturnPlenum
|
|
1924
|
+
# - AirLoopHVACSupplyPlenum
|
|
1925
|
+
#
|
|
1926
|
+
# Of the OpenStudio-Standards Prototype models, only the LargeOffice
|
|
1927
|
+
# holds AirLoopHVACReturnPlenum objects. OpenStudio-Standards' method
|
|
1928
|
+
# "space_plenum?" indeed catches them by checking if the space is
|
|
1929
|
+
# "partofTotalFloorArea" (which internally has an "isPlenum" check). So
|
|
1930
|
+
# "isPlenum" closely follows ASHRAE 90.1 2016's definition of "plenum":
|
|
1931
|
+
#
|
|
1932
|
+
# "plenum": a compartment or chamber ...
|
|
1933
|
+
# - to which one or more ducts are connected
|
|
1934
|
+
# - that forms a part of the air distribution system, and
|
|
1935
|
+
# - that is NOT USED for occupancy or storage.
|
|
1936
|
+
#
|
|
1937
|
+
# Canadian NECB 2020 has the following (not as well) defined term:
|
|
1938
|
+
# "plenum": a chamber forming part of an air duct system.
|
|
1939
|
+
# ... we'll assume that a space shall also be considered
|
|
1940
|
+
# UNOCCUPIED if it's "part of an air duct system".
|
|
1941
|
+
#
|
|
1942
|
+
# As intended, "isPlenum" would NOT identify as a "plenum" any vented
|
|
1943
|
+
# UNCONDITIONED or UNENCLOSED attic or crawlspace - good. Yet "isPlenum"
|
|
1944
|
+
# would also ignore dead air spaces integrating ducted return air. The
|
|
1945
|
+
# SDK's "partofTotalFloorArea" would be more suitable in such cases, as
|
|
1946
|
+
# long as modellers have, a priori, set this parameter to FALSE.
|
|
1947
|
+
#
|
|
1948
|
+
# By initially relying on the SDK's "partofTotalFloorArea", "space_plenum?"
|
|
1949
|
+
# ends up catching a MUCH WIDER range of spaces, which aren't caught by
|
|
1950
|
+
# "isPlenum". This includes attics, crawlspaces, non-plenum air spaces above
|
|
1951
|
+
# ceiling tiles, and any other UNOCCUPIED space in a model. The term
|
|
1952
|
+
# "plenum" in this context is more of a catch-all shorthand - to be used
|
|
1953
|
+
# with caution. For instance, "space_plenum?" shouldn't be used (in
|
|
1954
|
+
# isolation) to determine whether an UNOCCUPIED space should have its
|
|
1955
|
+
# envelope insulated ("plenum") or not ("attic").
|
|
1956
|
+
#
|
|
1957
|
+
# In contrast to OpenStudio-Standards' "space_plenum?", OSut's "is_plenum"
|
|
1958
|
+
# strictly returns FALSE if a space is indeed "partofTotalFloorArea". It
|
|
1959
|
+
# also returns FALSE if the space is a vestibule. Otherwise, it needs more
|
|
1960
|
+
# information to determine if such an UNOCCUPIED space is indeed a
|
|
1961
|
+
# plenum. Beyond these 2x criteria, a space is considered a plenum if:
|
|
1962
|
+
#
|
|
1963
|
+
# CASE A: it includes the substring "plenum" (case insensitive) in its
|
|
1964
|
+
# spaceType's name, or in the latter's standardsSpaceType string;
|
|
1965
|
+
#
|
|
1966
|
+
# CASE B: "isPlenum" == TRUE in an OpenStudio model WITH HVAC airloops; OR
|
|
1967
|
+
#
|
|
1968
|
+
# CASE C: its zone holds an 'inactive' thermostat (i.e. can't extract valid
|
|
1969
|
+
# setpoints) in an OpenStudio model with setpoint temperatures.
|
|
1970
|
+
#
|
|
1971
|
+
# If a modeller is instead simply interested in identifying UNOCCUPIED
|
|
1972
|
+
# spaces that are INDIRECTLYCONDITIONED (not necessarily plenums), then the
|
|
1973
|
+
# following combination is likely more reliable and less confusing:
|
|
1974
|
+
# - SDK's partofTotalFloorArea == FALSE
|
|
1975
|
+
# - OSut's is_unconditioned == FALSE
|
|
1976
|
+
mth = "osut.is_plenum"
|
|
1977
|
+
cl = openstudio.model.Space
|
|
1978
|
+
|
|
1979
|
+
if not isinstance(space, cl):
|
|
1980
|
+
return oslg.mismatch("space", space, cl, mth, CN.DBG, False)
|
|
1981
|
+
|
|
1982
|
+
if space.partofTotalFloorArea(): return False
|
|
1983
|
+
if is_vestibule(space): return False
|
|
1984
|
+
|
|
1985
|
+
# CASE A: "plenum" spaceType.
|
|
1986
|
+
if space.spaceType():
|
|
1987
|
+
type = space.spaceType().get()
|
|
1988
|
+
|
|
1989
|
+
if "plenum" in type.nameString().lower():
|
|
1990
|
+
return True
|
|
1991
|
+
|
|
1992
|
+
if type.standardsSpaceType():
|
|
1993
|
+
type = type.standardsSpaceType().get().lower()
|
|
1994
|
+
|
|
1995
|
+
if "plenum" in type: return True
|
|
1996
|
+
|
|
1997
|
+
# CASE B: "isPlenum" == TRUE if airloops.
|
|
1998
|
+
if has_airLoopsHVAC(space.model()): return space.isPlenum()
|
|
1999
|
+
|
|
2000
|
+
# CASE C: zone holds an 'inactive' thermostat.
|
|
2001
|
+
zone = space.thermalZone()
|
|
2002
|
+
heated = has_heatingTemperatureSetpoints(space.model())
|
|
2003
|
+
cooled = has_coolingTemperatureSetpoints(space.model())
|
|
2004
|
+
|
|
2005
|
+
if heated or cooled:
|
|
2006
|
+
if not zone: return False
|
|
2007
|
+
|
|
2008
|
+
zone = zone.get()
|
|
2009
|
+
heat = maxHeatScheduledSetpoint(zone)
|
|
2010
|
+
cool = minCoolScheduledSetpoint(zone)
|
|
2011
|
+
if heat["spt"] or cool["spt"]: return False # directly CONDITIONED
|
|
2012
|
+
return heat["dual"] or cool["dual"] # FALSE if both are None
|
|
2013
|
+
|
|
2014
|
+
return False
|
|
2015
|
+
|
|
2016
|
+
|
|
2017
|
+
def setpoints(space=None):
|
|
2018
|
+
"""Retrieves a space's (implicit or explicit) heating/cooling setpoints.
|
|
2019
|
+
|
|
2020
|
+
Args:
|
|
2021
|
+
space (OpenStudio::Model::Space):
|
|
2022
|
+
An OpenStudio space.
|
|
2023
|
+
Returns:
|
|
2024
|
+
dict:
|
|
2025
|
+
- heating (float): heating setpoint (None if invalid inputs - see logs).
|
|
2026
|
+
- cooling (float): cooling setpoint (None if invalid inputs - see logs).
|
|
2027
|
+
"""
|
|
2028
|
+
mth = "osut.setpoints"
|
|
2029
|
+
cl1 = openstudio.model.Space
|
|
2030
|
+
cl2 = str
|
|
2031
|
+
res = dict(heating=None, cooling=None)
|
|
2032
|
+
tg1 = "space_conditioning_category"
|
|
2033
|
+
tg2 = "indirectlyconditioned"
|
|
2034
|
+
cts = ["nonresconditioned", "resconditioned", "semiheated", "unconditioned"]
|
|
2035
|
+
cnd = None
|
|
2036
|
+
|
|
2037
|
+
if not isinstance(space, cl1):
|
|
2038
|
+
return oslg.mismatch("space", space, cl1, mth, CN.DBG, res)
|
|
2039
|
+
|
|
2040
|
+
# 1. Check for OpenStudio-Standards' space conditioning categories.
|
|
2041
|
+
if space.additionalProperties().hasFeature(tg1):
|
|
2042
|
+
cnd = space.additionalProperties().getFeatureAsString(tg1)
|
|
2043
|
+
|
|
2044
|
+
if cnd:
|
|
2045
|
+
cnd = cnd.get()
|
|
2046
|
+
|
|
2047
|
+
if cnd.lower() in cts:
|
|
2048
|
+
if cnd.lower() == "unconditioned": return res
|
|
2049
|
+
else:
|
|
2050
|
+
oslg.invalid("%s:%s" % (tg1, cnd), mth, 0, CN.ERR)
|
|
2051
|
+
cnd = None
|
|
2052
|
+
else:
|
|
2053
|
+
cnd = None
|
|
2054
|
+
|
|
2055
|
+
# 2. Check instead OSut's INDIRECTLYCONDITIONED (parent space) link.
|
|
2056
|
+
if not cnd:
|
|
2057
|
+
id = space.additionalProperties().getFeatureAsString(tg2)
|
|
2058
|
+
|
|
2059
|
+
if id:
|
|
2060
|
+
id = id.get()
|
|
2061
|
+
dad = space.model().getSpaceByName(id)
|
|
2062
|
+
|
|
2063
|
+
if dad:
|
|
2064
|
+
# Now focus on 'parent' space of INDIRECTLYCONDITIONED space.
|
|
2065
|
+
space = dad.get()
|
|
2066
|
+
cnd = tg2
|
|
2067
|
+
else:
|
|
2068
|
+
log(ERR, "Unknown space %s (%s)" % (id, mth))
|
|
2069
|
+
|
|
2070
|
+
# 3. Fetch space setpoints (if model indeed holds valid setpoints).
|
|
2071
|
+
heated = has_heatingTemperatureSetpoints(space.model())
|
|
2072
|
+
cooled = has_coolingTemperatureSetpoints(space.model())
|
|
2073
|
+
zone = space.thermalZone()
|
|
2074
|
+
|
|
2075
|
+
if heated or cooled:
|
|
2076
|
+
if not zone: return res # UNCONDITIONED
|
|
2077
|
+
|
|
2078
|
+
zone = zone.get()
|
|
2079
|
+
res["heating"] = maxHeatScheduledSetpoint(zone)["spt"]
|
|
2080
|
+
res["cooling"] = minCoolScheduledSetpoint(zone)["spt"]
|
|
2081
|
+
|
|
2082
|
+
# 4. Reset if AdditionalProperties were found & valid.
|
|
2083
|
+
if cnd:
|
|
2084
|
+
if cnd.lower() == "unconditioned":
|
|
2085
|
+
res["heating"] = None
|
|
2086
|
+
res["cooling"] = None
|
|
2087
|
+
elif cnd.lower() == "semiheated":
|
|
2088
|
+
if not res["heating"]: res["heating"] = 14.0
|
|
2089
|
+
res["cooling"] = None
|
|
2090
|
+
elif "conditioned" in cnd.lower():
|
|
2091
|
+
# "nonresconditioned", "resconditioned" or "indirectlyconditioned"
|
|
2092
|
+
if not res["heating"]: res["heating"] = 21.0 # default
|
|
2093
|
+
if not res["cooling"]: res["cooling"] = 24.0 # default
|
|
2094
|
+
|
|
2095
|
+
# 5. Reset if plenum.
|
|
2096
|
+
if is_plenum(space):
|
|
2097
|
+
if not res["heating"]: res["heating"] = 21.0 # default
|
|
2098
|
+
if not res["cooling"]: res["cooling"] = 24.0 # default
|
|
2099
|
+
|
|
2100
|
+
return res
|
|
2101
|
+
|
|
2102
|
+
|
|
2103
|
+
def is_unconditioned(space=None):
|
|
2104
|
+
"""Validates if a space is UNCONDITIONED.
|
|
2105
|
+
|
|
2106
|
+
Args:
|
|
2107
|
+
space (openstudio.model.Space):
|
|
2108
|
+
An OpenStudio space.
|
|
2109
|
+
Returns:
|
|
2110
|
+
bool: Whether space is considered UNCONDITIONED.
|
|
2111
|
+
False: If invalid input (see logs).
|
|
2112
|
+
"""
|
|
2113
|
+
mth = "osut.is_unconditioned"
|
|
2114
|
+
cl = openstudio.model.Space
|
|
2115
|
+
|
|
2116
|
+
if not isinstance(space, cl):
|
|
2117
|
+
return oslg.mismatch("space", space, cl, mth, CN.DBG, False)
|
|
2118
|
+
|
|
2119
|
+
if setpoints(space)["heating"]: return False
|
|
2120
|
+
if setpoints(space)["cooling"]: return False
|
|
2121
|
+
|
|
2122
|
+
return True
|
|
2123
|
+
|
|
2124
|
+
|
|
2125
|
+
def is_refrigerated(space=None):
|
|
2126
|
+
"""Confirms if a space can be considered as REFRIGERATED.
|
|
2127
|
+
|
|
2128
|
+
Args:
|
|
2129
|
+
space (openstudio.model.Space):
|
|
2130
|
+
An OpenStudio space.
|
|
2131
|
+
|
|
2132
|
+
Returns:
|
|
2133
|
+
bool: Whether space is considered REFRIGERATED.
|
|
2134
|
+
False: If invalid inputs (see logs).
|
|
2135
|
+
"""
|
|
2136
|
+
mth = "osut.is_refrigerated"
|
|
2137
|
+
cl = openstudio.model.Space
|
|
2138
|
+
tg0 = "refrigerated"
|
|
2139
|
+
|
|
2140
|
+
if not isinstance(space, cl):
|
|
2141
|
+
return oslg.mismatch("space", space, cl, mth, CN.DBG, False)
|
|
2142
|
+
|
|
2143
|
+
id = space.nameString()
|
|
2144
|
+
|
|
2145
|
+
# 1. First check OSut's REFRIGERATED status.
|
|
2146
|
+
status = space.additionalProperties().getFeatureAsString(tg0)
|
|
2147
|
+
|
|
2148
|
+
if status:
|
|
2149
|
+
status = status.get()
|
|
2150
|
+
if isinstance(status, bool): return status
|
|
2151
|
+
log(ERR, "Unknown %s REFRIGERATED %s (%s)" % (id, status, mth))
|
|
2152
|
+
|
|
2153
|
+
# 2. Else, compare design heating/cooling setpoints.
|
|
2154
|
+
stps = setpoints(space)
|
|
2155
|
+
if stps["heating"]: return False
|
|
2156
|
+
if not stps["cooling"]: return False
|
|
2157
|
+
if stps["cooling"] < 15: return True
|
|
2158
|
+
|
|
2159
|
+
return False
|
|
2160
|
+
|
|
2161
|
+
|
|
2162
|
+
def is_semiheated(space=None):
|
|
2163
|
+
"""Confirms if a space can be considered as SEMIHEATED as per NECB 2020
|
|
2164
|
+
1.2.1.2. 2): Design heating setpoint < 15°C (and non-REFRIGERATED).
|
|
2165
|
+
|
|
2166
|
+
Args:
|
|
2167
|
+
space (openstudio.model.space):
|
|
2168
|
+
An OpenStudio space.
|
|
2169
|
+
|
|
2170
|
+
Returns:
|
|
2171
|
+
bool: Whether space is considered SEMIHEATED.
|
|
2172
|
+
False: If invalid inputs (see logs).
|
|
2173
|
+
"""
|
|
2174
|
+
mth = "osut.is_semiheated"
|
|
2175
|
+
cl = openstudio.model.Space
|
|
2176
|
+
|
|
2177
|
+
if not isinstance(space, cl):
|
|
2178
|
+
return oslg.mismatch("space", space, cl, mth, CN.DBG, False)
|
|
2179
|
+
if is_refrigerated(space):
|
|
2180
|
+
return False
|
|
2181
|
+
|
|
2182
|
+
stps = setpoints(space)
|
|
2183
|
+
if stps["cooling"]: return False
|
|
2184
|
+
if not stps["heating"]: return False
|
|
2185
|
+
if stps["heating"] < 15: return True
|
|
2186
|
+
|
|
2187
|
+
return False
|
|
2188
|
+
|
|
2189
|
+
|
|
2190
|
+
def availabilitySchedule(model=None, avl=""):
|
|
2191
|
+
"""Generates an HVAC availability schedule (if missing from model).
|
|
2192
|
+
|
|
2193
|
+
Args:
|
|
2194
|
+
model (openstudio.model.Model):
|
|
2195
|
+
An OpenStudio model.
|
|
2196
|
+
avl (str):
|
|
2197
|
+
Seasonal availability choice (optional, default "ON").
|
|
2198
|
+
|
|
2199
|
+
Returns:
|
|
2200
|
+
OpenStudio::Model::Schedule: An OpenStudio HVAC availability schedule.
|
|
2201
|
+
None: If invalid input (see logs).
|
|
2202
|
+
"""
|
|
2203
|
+
mth = "osut.availabilitySchedule"
|
|
2204
|
+
cl = openstudio.model.Model
|
|
2205
|
+
limits = None
|
|
2206
|
+
|
|
2207
|
+
if not isinstance(model, cl):
|
|
2208
|
+
return oslg.mismatch("model", model, cl, mth)
|
|
2209
|
+
|
|
2210
|
+
try:
|
|
2211
|
+
avl = str(avl)
|
|
2212
|
+
except:
|
|
2213
|
+
return oslg.mismatch("availability", avl, str, mth, CN.ERR)
|
|
2214
|
+
|
|
2215
|
+
# Either fetch availability ScheduleTypeLimits object, or create one.
|
|
2216
|
+
for l in model.getScheduleTypeLimitss():
|
|
2217
|
+
id = l.nameString().lower()
|
|
2218
|
+
|
|
2219
|
+
if limits: break
|
|
2220
|
+
if not l.lowerLimitValue(): continue
|
|
2221
|
+
if not l.upperLimitValue(): continue
|
|
2222
|
+
if not l.numericType(): continue
|
|
2223
|
+
if not int(l.lowerLimitValue().get()) == 0: continue
|
|
2224
|
+
if not int(l.upperLimitValue().get()) == 1: continue
|
|
2225
|
+
if not l.numericType().get().lower() == "discrete": continue
|
|
2226
|
+
if not l.unitType().lower() == "availability": continue
|
|
2227
|
+
if id != "hvac operation scheduletypelimits": continue
|
|
2228
|
+
|
|
2229
|
+
limits = l
|
|
2230
|
+
|
|
2231
|
+
if not limits:
|
|
2232
|
+
limits = openstudio.model.ScheduleTypeLimits(model)
|
|
2233
|
+
limits.setName("HVAC Operation ScheduleTypeLimits")
|
|
2234
|
+
limits.setLowerLimitValue(0)
|
|
2235
|
+
limits.setUpperLimitValue(1)
|
|
2236
|
+
limits.setNumericType("Discrete")
|
|
2237
|
+
limits.setUnitType("Availability")
|
|
2238
|
+
|
|
2239
|
+
time = openstudio.Time(0,24)
|
|
2240
|
+
secs = time.totalSeconds()
|
|
2241
|
+
on = openstudio.model.ScheduleDay(model, 1)
|
|
2242
|
+
off = openstudio.model.ScheduleDay(model, 0)
|
|
2243
|
+
|
|
2244
|
+
# Seasonal availability start/end dates.
|
|
2245
|
+
year = model.yearDescription()
|
|
2246
|
+
|
|
2247
|
+
if not year:
|
|
2248
|
+
return oslg.empty("yearDescription", mth, CN.ERR)
|
|
2249
|
+
|
|
2250
|
+
year = year.get()
|
|
2251
|
+
may01 = year.makeDate(openstudio.MonthOfYear("May"), 1)
|
|
2252
|
+
oct31 = year.makeDate(openstudio.MonthOfYear("Oct"), 31)
|
|
2253
|
+
|
|
2254
|
+
if oslg.trim(avl).lower() == "winter":
|
|
2255
|
+
# available from November 1 to April 30 (6 months)
|
|
2256
|
+
val = 1
|
|
2257
|
+
sch = off
|
|
2258
|
+
nom = "WINTER Availability SchedRuleset"
|
|
2259
|
+
dft = "WINTER Availability dftDaySched"
|
|
2260
|
+
tag = "May-Oct WINTER Availability SchedRule"
|
|
2261
|
+
day = "May-Oct WINTER SchedRule Day"
|
|
2262
|
+
elif oslg.trim(avl).lower() == "summer":
|
|
2263
|
+
# available from May 1 to October 31 (6 months)
|
|
2264
|
+
val = 0
|
|
2265
|
+
sch = on
|
|
2266
|
+
nom = "SUMMER Availability SchedRuleset"
|
|
2267
|
+
dft = "SUMMER Availability dftDaySched"
|
|
2268
|
+
tag = "May-Oct SUMMER Availability SchedRule"
|
|
2269
|
+
day = "May-Oct SUMMER SchedRule Day"
|
|
2270
|
+
elif oslg.trim(avl).lower() == "off":
|
|
2271
|
+
# never available
|
|
2272
|
+
val = 0
|
|
2273
|
+
sch = on
|
|
2274
|
+
nom = "OFF Availability SchedRuleset"
|
|
2275
|
+
dft = "OFF Availability dftDaySched"
|
|
2276
|
+
tag = ""
|
|
2277
|
+
day = ""
|
|
2278
|
+
else:
|
|
2279
|
+
# always available
|
|
2280
|
+
val = 1
|
|
2281
|
+
sch = on
|
|
2282
|
+
nom = "ON Availability SchedRuleset"
|
|
2283
|
+
dft = "ON Availability dftDaySched"
|
|
2284
|
+
tag = ""
|
|
2285
|
+
day = ""
|
|
2286
|
+
|
|
2287
|
+
# Fetch existing schedule.
|
|
2288
|
+
ok = True
|
|
2289
|
+
schedule = model.getScheduleByName(nom)
|
|
2290
|
+
|
|
2291
|
+
if schedule:
|
|
2292
|
+
schedule = schedule.get().to_ScheduleRuleset()
|
|
2293
|
+
|
|
2294
|
+
if schedule:
|
|
2295
|
+
schedule = schedule.get()
|
|
2296
|
+
default = schedule.defaultDaySchedule()
|
|
2297
|
+
ok = ok and default.nameString() == dft
|
|
2298
|
+
ok = ok and len(default.times()) == 1
|
|
2299
|
+
ok = ok and len(default.values()) == 1
|
|
2300
|
+
ok = ok and default.times()[0] == time
|
|
2301
|
+
ok = ok and default.values()[0] == val
|
|
2302
|
+
rules = schedule.scheduleRules()
|
|
2303
|
+
ok = ok and len(rules) < 2
|
|
2304
|
+
|
|
2305
|
+
if len(rules) == 1:
|
|
2306
|
+
rule = rules[0]
|
|
2307
|
+
ok = ok and rule.nameString() == tag
|
|
2308
|
+
ok = ok and rule.startDate()
|
|
2309
|
+
ok = ok and rule.endDate()
|
|
2310
|
+
ok = ok and rule.startDate().get() == may01
|
|
2311
|
+
ok = ok and rule.endDate().get() == oct31
|
|
2312
|
+
ok = ok and rule.applyAllDays()
|
|
2313
|
+
|
|
2314
|
+
d = rule.daySchedule()
|
|
2315
|
+
ok = ok and d.nameString() == day
|
|
2316
|
+
ok = ok and len(d.times()) == 1
|
|
2317
|
+
ok = ok and len(d.values()) == 1
|
|
2318
|
+
ok = ok and d.times()[0].totalSeconds() == secs
|
|
2319
|
+
ok = ok and int(d.values()[0]) != val
|
|
2320
|
+
|
|
2321
|
+
if ok: return schedule
|
|
2322
|
+
|
|
2323
|
+
schedule = openstudio.model.ScheduleRuleset(model)
|
|
2324
|
+
schedule.setName(nom)
|
|
2325
|
+
|
|
2326
|
+
if not schedule.setScheduleTypeLimits(limits):
|
|
2327
|
+
oslg.log(ERR, "'%s': Can't set schedule type limits (%s)" % (nom, mth))
|
|
2328
|
+
return nil
|
|
2329
|
+
|
|
2330
|
+
if not schedule.defaultDaySchedule().addValue(time, val):
|
|
2331
|
+
oslg.log(ERR, "'%s': Can't set default day schedule (%s)" % (nom, mth))
|
|
2332
|
+
return None
|
|
2333
|
+
|
|
2334
|
+
schedule.defaultDaySchedule().setName(dft)
|
|
2335
|
+
|
|
2336
|
+
if tag:
|
|
2337
|
+
rule = openstudio.model.ScheduleRule(schedule, sch)
|
|
2338
|
+
rule.setName(tag)
|
|
2339
|
+
|
|
2340
|
+
if not rule.setStartDate(may01):
|
|
2341
|
+
oslg.log(ERR, "'%s': Can't set start date (%s)" % (tag, mth))
|
|
2342
|
+
return None
|
|
2343
|
+
|
|
2344
|
+
if not rule.setEndDate(oct31):
|
|
2345
|
+
oslg.log(ERR, "'%s': Can't set end date (%s)" % (tag, mth))
|
|
2346
|
+
return None
|
|
2347
|
+
|
|
2348
|
+
if not rule.setApplyAllDays(True):
|
|
2349
|
+
oslg.log(ERR, "'%s': Can't apply to all days (%s)" % (tag, mth))
|
|
2350
|
+
return None
|
|
2351
|
+
|
|
2352
|
+
rule.daySchedule().setName(day)
|
|
2353
|
+
|
|
2354
|
+
return schedule
|
|
2355
|
+
|
|
2356
|
+
|
|
2357
|
+
def transforms(group=None) -> dict:
|
|
2358
|
+
""""Returns OpenStudio site/space transformation & rotation angle.
|
|
2359
|
+
|
|
2360
|
+
Args:
|
|
2361
|
+
group:
|
|
2362
|
+
A site or space PlanarSurfaceGroup object.
|
|
2363
|
+
|
|
2364
|
+
Returns:
|
|
2365
|
+
A transformation + rotation dictionary:
|
|
2366
|
+
- t (openstudio.Transformation): site/space transformation.
|
|
2367
|
+
None: if invalid inputs (see logs).
|
|
2368
|
+
- r (float): Site/space rotation angle [0,2PI) radians.
|
|
2369
|
+
None: if invalid inputs (see logs).
|
|
2370
|
+
|
|
2371
|
+
"""
|
|
2372
|
+
mth = "osut.transforms"
|
|
2373
|
+
res = dict(t=None, r=None)
|
|
2374
|
+
cl = openstudio.model.PlanarSurfaceGroup
|
|
2375
|
+
|
|
2376
|
+
if isinstance(group, cl):
|
|
2377
|
+
return oslg.mismatch("group", group, cl, mth, CN.DBG, res)
|
|
2378
|
+
|
|
2379
|
+
mdl = group.model()
|
|
2380
|
+
|
|
2381
|
+
res["t"] = group.siteTransformation()
|
|
2382
|
+
res["r"] = group.directionofRelativeNorth() + mdl.getBuilding().northAxis()
|
|
2383
|
+
|
|
2384
|
+
return res
|
|
2385
|
+
|
|
2386
|
+
|
|
2387
|
+
def trueNormal(s=None, r=0):
|
|
2388
|
+
"""Returns the site/true outward normal vector of a surface.
|
|
2389
|
+
|
|
2390
|
+
Args:
|
|
2391
|
+
s (OpenStudio::Model::PlanarSurface):
|
|
2392
|
+
An OpenStudio Planar Surface.
|
|
2393
|
+
r (float):
|
|
2394
|
+
a group/site rotation angle [0,2PI) radians
|
|
2395
|
+
|
|
2396
|
+
Returns:
|
|
2397
|
+
openstudio.Vector3d: A surface's true normal vector.
|
|
2398
|
+
None : If invalid input (see logs).
|
|
2399
|
+
|
|
2400
|
+
"""
|
|
2401
|
+
mth = "osut.trueNormal"
|
|
2402
|
+
cl = openstudio.model.PlanarSurface
|
|
2403
|
+
|
|
2404
|
+
if not isinstance(s, cl):
|
|
2405
|
+
return oslg.mismatch("surface", s, cl, mth)
|
|
2406
|
+
|
|
2407
|
+
try:
|
|
2408
|
+
r = float(r)
|
|
2409
|
+
except:
|
|
2410
|
+
return oslg.mismatch("rotation", r, float, mth)
|
|
2411
|
+
|
|
2412
|
+
r = float(-r) * math.pi / 180.0
|
|
2413
|
+
|
|
2414
|
+
vx = s.outwardNormal().x * math.cos(r) - s.outwardNormal().y * math.sin(r)
|
|
2415
|
+
vy = s.outwardNormal().x * math.sin(r) + s.outwardNormal().y * math.cos(r)
|
|
2416
|
+
vz = s.outwardNormal().z
|
|
2417
|
+
|
|
2418
|
+
return openstudio.Point3d(vx, vy, vz) - openstudio.Point3d(0, 0, 0)
|
|
2419
|
+
|
|
2420
|
+
|
|
2421
|
+
def scalar(v=None, m=0) -> openstudio.Vector3d:
|
|
2422
|
+
"""Returns scalar product of an OpenStudio Vector3d.
|
|
2423
|
+
|
|
2424
|
+
Args:
|
|
2425
|
+
v (OpenStudio::Vector3d):
|
|
2426
|
+
An OpenStudio vector.
|
|
2427
|
+
m (float):
|
|
2428
|
+
A scalar.
|
|
2429
|
+
|
|
2430
|
+
Returns:
|
|
2431
|
+
(openstudio.Vector3d) scaled points (see logs if (0,0,0)).
|
|
2432
|
+
|
|
2433
|
+
"""
|
|
2434
|
+
mth = "osut.scalar"
|
|
2435
|
+
cl = openstudio.Vector3d
|
|
2436
|
+
v0 = openstudio.Vector3d()
|
|
2437
|
+
|
|
2438
|
+
if not isinstance(v, cl):
|
|
2439
|
+
return oslg.mismatch("vector", v, cl, mth, CN.DBG, v0)
|
|
2440
|
+
|
|
2441
|
+
try:
|
|
2442
|
+
m = float(m)
|
|
2443
|
+
except:
|
|
2444
|
+
return oslg.mismatch("scalar", m, float, mth, CN.DBG, v0)
|
|
2445
|
+
|
|
2446
|
+
v0 = openstudio.Vector3d(m * v.x(), m * v.y(), m * v.z())
|
|
2447
|
+
|
|
2448
|
+
return v0
|
|
2449
|
+
|
|
2450
|
+
|
|
2451
|
+
def to_p3Dv(pts=None) -> openstudio.Point3dVector:
|
|
2452
|
+
"""Returns OpenStudio 3D points as an OpenStudio point vector, validating
|
|
2453
|
+
points in the process.
|
|
2454
|
+
|
|
2455
|
+
Args:
|
|
2456
|
+
pts (list): OpenStudio 3D points.
|
|
2457
|
+
|
|
2458
|
+
Returns:
|
|
2459
|
+
openstudio.Point3dVector: Vector of 3D points (see logs if empty).
|
|
2460
|
+
|
|
2461
|
+
"""
|
|
2462
|
+
mth = "osut.to_p3Dv"
|
|
2463
|
+
cl = openstudio.Point3d
|
|
2464
|
+
v = openstudio.Point3dVector()
|
|
2465
|
+
|
|
2466
|
+
if isinstance(pts, cl):
|
|
2467
|
+
v.append(pts)
|
|
2468
|
+
return v
|
|
2469
|
+
elif isinstance(pts, openstudio.Point3dVector):
|
|
2470
|
+
return pts
|
|
2471
|
+
elif isinstance(pts, openstudio.model.PlanarSurface):
|
|
2472
|
+
return pts.vertices()
|
|
2473
|
+
|
|
2474
|
+
try:
|
|
2475
|
+
pts = list(pts)
|
|
2476
|
+
except:
|
|
2477
|
+
return oslg.mismatch("points", pts, list, mth, CN.DBG, v)
|
|
2478
|
+
|
|
2479
|
+
for pt in pts:
|
|
2480
|
+
if not isinstance(pt, cl):
|
|
2481
|
+
return oslg.mismatch("point", pt, cl, mth, CN.DBG, v)
|
|
2482
|
+
|
|
2483
|
+
for pt in pts:
|
|
2484
|
+
v.append(openstudio.Point3d(pt.x(), pt.y(), pt.z()))
|
|
2485
|
+
|
|
2486
|
+
return v
|
|
2487
|
+
|
|
2488
|
+
|
|
2489
|
+
def is_same_vtx(s1=None, s2=None, indexed=True) -> bool:
|
|
2490
|
+
"""Returns True if 2 sets of OpenStudio 3D points are nearly equal.
|
|
2491
|
+
|
|
2492
|
+
Args:
|
|
2493
|
+
s1:
|
|
2494
|
+
1st set of OpenStudio 3D points
|
|
2495
|
+
s2:
|
|
2496
|
+
2nd set of OpenStudio 3D points
|
|
2497
|
+
indexed (bool):
|
|
2498
|
+
whether to attempt to harmonize vertex sequence
|
|
2499
|
+
|
|
2500
|
+
Returns:
|
|
2501
|
+
bool: Whether sets are nearly equal (within TOL).
|
|
2502
|
+
False: If invalid input (see logs).
|
|
2503
|
+
|
|
2504
|
+
"""
|
|
2505
|
+
s1 = list(to_p3Dv(s1))
|
|
2506
|
+
s2 = list(to_p3Dv(s2))
|
|
2507
|
+
if not s1: return False
|
|
2508
|
+
if not s2: return False
|
|
2509
|
+
if len(s1) != len(s2): return False
|
|
2510
|
+
if not isinstance(indexed, bool): indexed = True
|
|
2511
|
+
|
|
2512
|
+
if indexed:
|
|
2513
|
+
xOK = abs(s1[0].x() - s2[0].x()) < CN.TOL
|
|
2514
|
+
yOK = abs(s1[0].y() - s2[0].y()) < CN.TOL
|
|
2515
|
+
zOK = abs(s1[0].z() - s2[0].z()) < CN.TOL
|
|
2516
|
+
|
|
2517
|
+
if xOK and yOK and zOK and len(s1) == 1:
|
|
2518
|
+
return True
|
|
2519
|
+
else:
|
|
2520
|
+
indx = None
|
|
2521
|
+
|
|
2522
|
+
for i, pt in enumerate(s2):
|
|
2523
|
+
if indx: continue
|
|
2524
|
+
|
|
2525
|
+
xOK = abs(s1[0].x() - s2[i].x()) < CN.TOL
|
|
2526
|
+
yOK = abs(s1[0].y() - s2[i].y()) < CN.TOL
|
|
2527
|
+
zOK = abs(s1[0].z() - s2[i].z()) < CN.TOL
|
|
2528
|
+
|
|
2529
|
+
if xOK and yOK and zOK: indx = i
|
|
2530
|
+
|
|
2531
|
+
if not indx: return False
|
|
2532
|
+
|
|
2533
|
+
s2 = collections.deque(s2)
|
|
2534
|
+
s2.rotate(indx)
|
|
2535
|
+
s2 = list(s2)
|
|
2536
|
+
|
|
2537
|
+
# openstudio.isAlmostEqual3dPt(p1, p2, TOL) # ... from v350 onwards.
|
|
2538
|
+
for i in range(len(s1)):
|
|
2539
|
+
xOK = abs(s1[i].x() - s2[i].x()) < CN.TOL
|
|
2540
|
+
yOK = abs(s1[i].y() - s2[i].y()) < CN.TOL
|
|
2541
|
+
zOK = abs(s1[i].z() - s2[i].z()) < CN.TOL
|
|
2542
|
+
|
|
2543
|
+
if not xOK or not yOK or not zOK: return False
|
|
2544
|
+
|
|
2545
|
+
return True
|
|
2546
|
+
|
|
2547
|
+
|
|
2548
|
+
def facets(spaces=[], boundary="all", type="all", sides=[]) -> list:
|
|
2549
|
+
"""Returns an array of OpenStudio space surfaces or subsurfaces that match
|
|
2550
|
+
criteria, e.g. exterior, north-east facing walls in hotel "lobby". Note
|
|
2551
|
+
that the 'sides' list relies on space coordinates (not building or site
|
|
2552
|
+
coordinates). Also, the 'sides' list is exclusive (not inclusive), e.g.
|
|
2553
|
+
walls strictly facing north or east would not be returned if 'sides' holds
|
|
2554
|
+
["north", "east"]. No outside boundary condition filters if 'boundary'
|
|
2555
|
+
argument == "all". No surface type filters if 'type' argument == "all".
|
|
2556
|
+
|
|
2557
|
+
Args:
|
|
2558
|
+
spaces (list of openstudio.model.Space):
|
|
2559
|
+
Target spaces.
|
|
2560
|
+
boundary (str):
|
|
2561
|
+
OpenStudio outside boundary condition.
|
|
2562
|
+
type (str):
|
|
2563
|
+
OpenStudio surface (or subsurface) type.
|
|
2564
|
+
sides (list):
|
|
2565
|
+
Direction keys, e.g. "north" (see osut.sidz())
|
|
2566
|
+
|
|
2567
|
+
Returns:
|
|
2568
|
+
list of openstudio.model.Surface: Surfaces (may be empty, no logs).
|
|
2569
|
+
list of openstudio.model.SubSurface: SubSurfaces (may be empty, no logs).
|
|
2570
|
+
"""
|
|
2571
|
+
mth = "osut.facets"
|
|
2572
|
+
|
|
2573
|
+
spaces = [spaces] if isinstance(spaces, openstudio.model.Space) else spaces
|
|
2574
|
+
|
|
2575
|
+
try:
|
|
2576
|
+
spaces = list(spaces)
|
|
2577
|
+
except:
|
|
2578
|
+
return []
|
|
2579
|
+
|
|
2580
|
+
sides = [sides] if isinstance(sides, str) else sides
|
|
2581
|
+
|
|
2582
|
+
try:
|
|
2583
|
+
sides = list(sides)
|
|
2584
|
+
except:
|
|
2585
|
+
return []
|
|
2586
|
+
|
|
2587
|
+
faces = []
|
|
2588
|
+
boundary = oslg.trim(boundary).lower()
|
|
2589
|
+
type = oslg.trim(type).lower()
|
|
2590
|
+
if not boundary: return []
|
|
2591
|
+
if not type: return []
|
|
2592
|
+
|
|
2593
|
+
# Filter sides. If 'sides' is initially empty, return all surfaces of
|
|
2594
|
+
# matching type and outside boundary condition.
|
|
2595
|
+
if sides:
|
|
2596
|
+
sides = [side for side in sides if side in sidz()]
|
|
2597
|
+
|
|
2598
|
+
if not sides: return []
|
|
2599
|
+
|
|
2600
|
+
for space in spaces:
|
|
2601
|
+
if not isinstance(space, openstudio.model.Space): return []
|
|
2602
|
+
|
|
2603
|
+
for s in space.surfaces():
|
|
2604
|
+
if boundary != "all":
|
|
2605
|
+
if s.outsideBoundaryCondition().lower() != boundary: continue
|
|
2606
|
+
|
|
2607
|
+
if type != "all":
|
|
2608
|
+
if s.surfaceType().lower() != type: continue
|
|
2609
|
+
|
|
2610
|
+
if sides:
|
|
2611
|
+
aims = []
|
|
2612
|
+
|
|
2613
|
+
if s.outwardNormal().z() > CN.TOL: aims.append("top")
|
|
2614
|
+
if s.outwardNormal().z() < -CN.TOL: aims.append("bottom")
|
|
2615
|
+
if s.outwardNormal().y() > CN.TOL: aims.append("north")
|
|
2616
|
+
if s.outwardNormal().x() > CN.TOL: aims.append("east")
|
|
2617
|
+
if s.outwardNormal().y() < -CN.TOL: aims.append("south")
|
|
2618
|
+
if s.outwardNormal().x() < -CN.TOL: aims.append("west")
|
|
2619
|
+
|
|
2620
|
+
if all([side in aims for side in sides]):
|
|
2621
|
+
faces.append(s)
|
|
2622
|
+
else:
|
|
2623
|
+
faces.append(s)
|
|
2624
|
+
|
|
2625
|
+
for space in spaces:
|
|
2626
|
+
for s in space.surfaces():
|
|
2627
|
+
if boundary != "all":
|
|
2628
|
+
if s.outsideBoundaryCondition().lower() != boundary: continue
|
|
2629
|
+
|
|
2630
|
+
for sub in s.subSurfaces():
|
|
2631
|
+
if type != "all":
|
|
2632
|
+
if sub.subSurfaceType().lower() != type: continue
|
|
2633
|
+
|
|
2634
|
+
if sides:
|
|
2635
|
+
aims = []
|
|
2636
|
+
|
|
2637
|
+
if sub.outwardNormal().z() > CN.TOL: aims.append("top")
|
|
2638
|
+
if sub.outwardNormal().z() < -CN.TOL: aims.append("bottom")
|
|
2639
|
+
if sub.outwardNormal().y() > CN.TOL: aims.append("north")
|
|
2640
|
+
if sub.outwardNormal().x() > CN.TOL: aims.append("east")
|
|
2641
|
+
if sub.outwardNormal().y() < -CN.TOL: aims.append("south")
|
|
2642
|
+
if sub.outwardNormal().x() < -CN.TOL: aims.append("west")
|
|
2643
|
+
|
|
2644
|
+
if all([side in aims for side in sides]):
|
|
2645
|
+
faces.append(sub)
|
|
2646
|
+
else:
|
|
2647
|
+
faces.append(sub)
|
|
2648
|
+
|
|
2649
|
+
return faces
|
|
2650
|
+
|
|
2651
|
+
|
|
2652
|
+
def genSlab(pltz=[], z=0):
|
|
2653
|
+
"""Generates an OpenStudio 3D point vector of a composite floor "slab", a
|
|
2654
|
+
'union' of multiple rectangular, horizontal floor "plates". Each plate
|
|
2655
|
+
must either share an edge with (or encompass or overlap) any of the
|
|
2656
|
+
preceding plates in the array. The generated slab may not be convex.
|
|
2657
|
+
|
|
2658
|
+
Args:
|
|
2659
|
+
pltz (list):
|
|
2660
|
+
Collection of individual floor plates (dicts), each holding:
|
|
2661
|
+
- "x" (float): Left corner of plate origin (bird's eye view).
|
|
2662
|
+
- "y" (float): Bottom corner of plate origin (bird's eye view).
|
|
2663
|
+
- "dx" (float): Plate width (bird's eye view).
|
|
2664
|
+
- "dy" (float): Plate depth (bird's eye view)
|
|
2665
|
+
- "z" (float): Z-axis coordinate.
|
|
2666
|
+
|
|
2667
|
+
Returns:
|
|
2668
|
+
openstudio.point3dVector: Slab vertices (see logs if empty).
|
|
2669
|
+
"""
|
|
2670
|
+
mth = "osut.genSlab"
|
|
2671
|
+
slb = openstudio.Point3dVector()
|
|
2672
|
+
bkp = openstudio.Point3dVector()
|
|
2673
|
+
|
|
2674
|
+
# Input validation.
|
|
2675
|
+
if not isinstance(pltz, list):
|
|
2676
|
+
return oslg.mismatch("plates", pltz, list, mth, CN.DBG, slb)
|
|
2677
|
+
|
|
2678
|
+
try:
|
|
2679
|
+
z = float(z)
|
|
2680
|
+
except:
|
|
2681
|
+
return oslg.mismatch("Z", z, float, mth, CN.DBG, slb)
|
|
2682
|
+
|
|
2683
|
+
for i, plt in enumerate(pltz):
|
|
2684
|
+
id = "plate # %d (index %d)" % (i+1, i)
|
|
2685
|
+
|
|
2686
|
+
if not isinstance(plt, dict):
|
|
2687
|
+
return oslg.mismatch(id, plt, dict, mth, CN.DBG, slb)
|
|
2688
|
+
|
|
2689
|
+
if "x" not in plt: return oslg.hashkey(id, plt, "x", mth, CN.DBG, slb)
|
|
2690
|
+
if "y" not in plt: return oslg.hashkey(id, plt, "y", mth, CN.DBG, slb)
|
|
2691
|
+
if "dx" not in plt: return oslg.hashkey(id, plt, "dx", mth, CN.DBG, slb)
|
|
2692
|
+
if "dy" not in plt: return oslg.hashkey(id, plt, "dy", mth, CN.DBG, slb)
|
|
2693
|
+
|
|
2694
|
+
x = plt["x" ]
|
|
2695
|
+
y = plt["y" ]
|
|
2696
|
+
dx = plt["dx"]
|
|
2697
|
+
dy = plt["dy"]
|
|
2698
|
+
|
|
2699
|
+
try:
|
|
2700
|
+
x = float(x)
|
|
2701
|
+
except:
|
|
2702
|
+
oslg.mismatch("%s X" % id, x, float, mth, CN.DBG, slb)
|
|
2703
|
+
|
|
2704
|
+
try:
|
|
2705
|
+
y = float(y)
|
|
2706
|
+
except:
|
|
2707
|
+
oslg.mismatch("%s Y" % id, y, float, mth, CN.DBG, slb)
|
|
2708
|
+
|
|
2709
|
+
try:
|
|
2710
|
+
dx = float(dx)
|
|
2711
|
+
except:
|
|
2712
|
+
oslg.mismatch("%s dX" % id, dx, float, mth, CN.DBG, slb)
|
|
2713
|
+
|
|
2714
|
+
try:
|
|
2715
|
+
dy = float(dy)
|
|
2716
|
+
except:
|
|
2717
|
+
oslg.mismatch("%s dY" % id, dy, float, mth, CN.DBG, slb)
|
|
2718
|
+
|
|
2719
|
+
if abs(dx) < CN.TOL: return oslg.zero("%s dX" % id, mth, CN.ERR, slb)
|
|
2720
|
+
if abs(dy) < CN.TOL: return oslg.zero("%s dY" % id, mth, CN.ERR, slb)
|
|
2721
|
+
|
|
2722
|
+
# Join plates.
|
|
2723
|
+
for i, plt in enumerate(pltz):
|
|
2724
|
+
id = "plate # %d (index %d)" % (i+1, i)
|
|
2725
|
+
|
|
2726
|
+
x = plt["x" ]
|
|
2727
|
+
y = plt["y" ]
|
|
2728
|
+
dx = plt["dx"]
|
|
2729
|
+
dy = plt["dy"]
|
|
2730
|
+
|
|
2731
|
+
# Adjust X if dX < 0.
|
|
2732
|
+
if dx < 0: x -= -dx
|
|
2733
|
+
if dx < 0: dx = -dx
|
|
2734
|
+
|
|
2735
|
+
# Adjust Y if dY < 0.
|
|
2736
|
+
if dy < 0: y -= -dy
|
|
2737
|
+
if dy < 0: dy = -dy
|
|
2738
|
+
|
|
2739
|
+
vtx = []
|
|
2740
|
+
vtx.append(openstudio.Point3d(x + dx, y + dy, 0))
|
|
2741
|
+
vtx.append(openstudio.Point3d(x + dx, y, 0))
|
|
2742
|
+
vtx.append(openstudio.Point3d(x, y, 0))
|
|
2743
|
+
vtx.append(openstudio.Point3d(x, y + dy, 0))
|
|
2744
|
+
|
|
2745
|
+
if slb:
|
|
2746
|
+
slab = openstudio.join(slb, vtx, CN.TOL2)
|
|
2747
|
+
|
|
2748
|
+
if slab:
|
|
2749
|
+
slb = slab.get()
|
|
2750
|
+
else:
|
|
2751
|
+
return oslg.invalid(id, mth, 0, CN.ERR, bkp)
|
|
2752
|
+
else:
|
|
2753
|
+
slb = vtx
|
|
2754
|
+
|
|
2755
|
+
# Once joined, re-adjust Z-axis coordinates.
|
|
2756
|
+
if abs(z) > CN.TOL:
|
|
2757
|
+
vtx = openstudio.Point3dVector()
|
|
2758
|
+
|
|
2759
|
+
for pt in slb: vtx.append(openstudio.Point3d(pt.x(), pt.y(), z))
|
|
2760
|
+
|
|
2761
|
+
slb = vtx
|
|
2762
|
+
|
|
2763
|
+
return slb
|