/
HB Load Shade Benefit.py
452 lines (415 loc) · 22.7 KB
/
HB Load Shade Benefit.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
# Ladybug: A Plugin for Environmental Analysis (GPL)
# This file is part of Ladybug.
#
# Copyright (c) 2024, Ladybug Tools.
# You should have received a copy of the GNU Affero General Public License
# along with Ladybug; If not, see <http://www.gnu.org/licenses/>.
#
# @license AGPL-3.0-or-later <https://spdx.org/licenses/AGPL-3.0-or-later>
"""
Visualize the desirability of shade in terms of its impact on the annual heating
and cooling loads of Honeybee Rooms.
_
The calculation runs by performing a simple fast energy simulation of the
connected Honeybee Rooms without any shade other than context. The resulting
heating/cooling loads are extracted at each timestep of the simulation along with
the direct (beam) solar gain through each of the Room's windows. Solar vectors
are generated for each step of the simulation and projected from the Room's
Aperture geometries through the shades assigned to those Apertures.
_
Solar vectors for timesteps when the Room is cooling mode contribute positively
to shade desirability (shade help) while solar vectors for hours when the
Room is heating mode contribute negatively (shade harm). This contribution
is weighted by how much cooling or heating energy the Room requires at the
timestep along with the direct solar gain through each Aperture at the timestep.
_
The component outputs a colored mesh of the shades assigned to the Room Apertures
illustrating the net effect of shading each part of the geometry. A higher
saturation of blue indicates that shading the cell is desirable. A higher
saturation of red indicates that shading the cell is harmful (blocking more sun
when the Room is in heating mode than cooling mode). Desaturated cells indicate
that shading the cell will have relatively little effect on the heating or
cooling loads of the Room.
_
The units for shade desirability are kWh of Room cooling load avoided per unit
area of shade if the test cell of the shade is helpful (blue). If the test cell
is harmful (red), the units are kWh of Room heating load increased per unit
area of shade. So, if a given square meter of input _shade_geo has a shad
desirability of 10 kWh/m2, this means that a shade in this location provides
roughly 10 kWh of avoided cooling load to the parent Room over the year.
_
The method used by this component is based off of the Shaderade method developed
by Jon Sargent, Jeffrey Niemasz and Christoph Reinhart. More information can be
found in the following publication document:
Sargent, Jon; Niemasz, Jeffrey; Reinhart, Christoph. SHADERADE: Combining
Rhinoceros and EnergyPlus for the Design of Static Exterior Shading Devices.
Building Simulation, 2011, Sydney, Australia.
http://www.ibpsa.org/proceedings/bs2011/p_1209.pdf
-
Args:
_rooms: A list of Honeybee Rooms for which cooling/heating shade benefit/harm will
be evaluated. At least some of these Rooms should have Apertures
with Shades assigned to them in order for this component to produce
meaningful results. Note that all Shades generated with the "HB Louver
Shades" component or the "HB Extruded Border" component are
automatically assigned to a parent Aperture. For more complex
Shade geometries, the "HB Add Shade" component can be used to
assign the Shade to a parent Aperture.
context_: Honeybee Shades representing context geometry that can block sun
to the _rooms, therefore discounting any benefit or harm that could
come to the Room's Shades.
_epw_file: Path to an .epw file on your system as a text string. This will be
used in the energy simulation to determine heating/cooling loads
and to generate solar vectors for the shade benefit calculation.
_north_: A number between -360 and 360 for the counterclockwise difference
between the North and the positive Y-axis in degrees.
90 is West and 270 is East. (Default: 0).
_grid_size: A positive number in Rhino model units for the size of grid cells at
which the Shade geometries of the input _rooms will be subdivided
for shade benefit analysis. The smaller the grid size, the higher
the resolution of the analysis and the longer the calculation will
take. So it is recommended that one start with a large value here
and decrease the value as needed. However, the grid size should
usually be smaller than the dimensions of the smallest piece of
Shade geometry in order to yield meaningful results.
_timestep_: An integer for the number of timesteps per hour at which the energy
simulation will run and sun vectors will be generated for the analysis.
Higher values will result in the generation of more vectors, which
will make the resulting shade meshes smoother and produce a better
representation of real benefit/harm. However, the calculation will take
longer as there are more intersection operations to perform. The
default is 1 timestep per hour, which is the coarsest resolution
avalable and the fastest calculation.
lag_time_: A number for the amount of time in hours between when solar gain
eneters the room and the gain results in an increased cooling load.
Typically, it takes an hour or so for solar gains falling on the
room floors to heat up the floor surface and then convect to the
room air where the gain can be absorbed by a cooling system. This
means that the cooling value associated with each sun vector should
be a step or two after the time of the sun vector. Lag time can
be longer than an hour if the room has a particularly high thermal
mass or it may be shorter if the room has less mass or uses a radiant
cooling system integrated into the floor where the sun is absorbed.
Note that the value input here can be a decimal value to indicate
that the lag time is a fraction of an hour. (Default: 1.0 hour).
legend_par_: Optional legend parameters from the "LB Legend Parameters"
that will be used to customize the display of the results.
_cpu_count_: An integer to set the number of CPUs used in the execution of the
intersection calculation. If unspecified, it will automatically default
to one less than the number of CPUs currently available on the
machine or 1 if only one processor is available.
_run: Set to "True" to run the component and perform shade benefit analysis.
Returns:
report: ...
vectors: The sun vectors that were used to evaluate the shade (note that
these will increase as the _timestep_ increases).
points: Points across the room Aperture geometry from which sun vectors
are projected. Note that only Apertures with assigned Shades are
evaluated in order to avoid unnessarily increasing the calculation
time by evaluating windows for which there is not shade.
mesh: A colored mesh of the Shades assigned to the room's apertures showing
where shading is helpful (blue), harmful (red), or does not make
much of a difference (white or desaturated colors). Note that
the colors can change depending upon the input legend_par_.
legend: Legend showing the numeric values of kWh per unit shade area of
decreased/increased cooling/heating load that correspond to the
colors in the shade mesh.
title: A text object for the study title.
shade_help: The cumulative kWh of avoided cooling load per square area unit
obtained by shading the given cell. If a given square meter of
shade geometry has a helpfulness of 10 kWh/m2, this means that
a shade in this location decreases the cooling load of the Room
by roughly 10 kWh over the year.
shade_harm: The cumulative kWh of increased heating load per square area unit
obtained by shading the given cell. If a given square meter of
shade geometry has a harmfulness of -10 kWh/m2, this means that
a shade in this location increases the heating load of the Room
by roughly 10 kWh over the year.
shade_net: The sum of the helpfulness and harmfulness for each cell. This will be
negative if shading the cell has a net harmful effect and positive
if the shade has a net helpful effect.
"""
ghenv.Component.Name = 'HB Load Shade Benefit'
ghenv.Component.NickName = 'LoadShadeBenefit'
ghenv.Component.Message = '1.8.0'
ghenv.Component.Category = 'HB-Energy'
ghenv.Component.SubCategory = '5 :: Simulate'
ghenv.Component.AdditionalHelpFromDocStrings = '2'
import os
import subprocess
import json
import math
try:
from ladybug_geometry.geometry3d import Mesh3D
except ImportError as e:
raise ImportError('\nFailed to import ladybug_geometry:\n\t{}'.format(e))
try:
from ladybug.futil import write_to_file_by_name, nukedir
from ladybug.sunpath import Sunpath
from ladybug.color import Colorset
from ladybug.graphic import GraphicContainer
from ladybug.epw import EPW
from ladybug.sql import SQLiteResult
from ladybug.datacollection import HourlyContinuousCollection
except ImportError as e:
raise ImportError('\nFailed to import ladybug:\n\t{}'.format(e))
try:
from honeybee.config import folders
from honeybee.boundarycondition import Outdoors
from honeybee.model import Model
except ImportError as e:
raise ImportError('\nFailed to import honeybee:\n\t{}'.format(e))
try:
from honeybee_energy.simulation.parameter import SimulationParameter
from honeybee_energy.run import run_idf
from honeybee_energy.result.err import Err
from honeybee_energy.writer import energyplus_idf_version
from honeybee_energy.config import folders as energy_folders
except ImportError as e:
raise ImportError('\nFailed to import honeybee_energy:\n\t{}'.format(e))
try:
from lbt_recipes.version import check_energyplus_version
except ImportError as e:
raise ImportError('\nFailed to import lbt_recipes:\n\t{}'.format(e))
try:
from ladybug_rhino.togeometry import to_vector2d, to_joined_gridded_mesh3d
from ladybug_rhino.fromgeometry import from_face3d, from_mesh3d, \
from_point3d, from_vector3d
from ladybug_rhino.config import conversion_to_meters, units_system, \
tolerance, angle_tolerance, units_abbreviation
from ladybug_rhino.fromobjects import legend_objects
from ladybug_rhino.text import text_objects
from ladybug_rhino.intersect import join_geometry_to_mesh, generate_intersection_rays, \
intersect_rays_with_mesh_faces
from ladybug_rhino.grasshopper import all_required_inputs, give_warning, hide_output, \
recommended_processor_count
except ImportError as e:
raise ImportError('\nFailed to import ladybug_rhino:\n\t{}'.format(e))
def serialize_data(data_dicts):
"""Reserialize a list of HourlyContinuousCollection dictionaries."""
return [HourlyContinuousCollection.from_dict(data) for data in data_dicts]
# List of all the output strings that will be requested
cool_out = 'Zone Ideal Loads Supply Air Total Cooling Energy'
heat_out = 'Zone Ideal Loads Supply Air Total Heating Energy'
solar_out = 'Surface Window Transmitted Beam Solar Radiation Energy'
all_output = (cool_out, heat_out, solar_out)
if all_required_inputs(ghenv.Component) and _run:
# check the presence of energyplus and check that the version is compatible
check_energyplus_version()
# set the defaults and process all of the inputs
workers = _cpu_count_ if _cpu_count_ is not None else recommended_processor_count()
timestep = _timestep_ if _timestep_ is not None else 1
lag_time = 1 if lag_time_ is None else lag_time_
lag_steps = int(timestep * lag_time)
if _north_ is not None: # process the north_
try:
_north_ = math.degrees(
to_vector2d(_north_).angle_clockwise(Vector2D(0, 1)))
except AttributeError: # north angle instead of vector
_north_ = float(north_)
else:
_north_ = 0
# gather all assigned shades and remove them from the rooms
rooms = [r.duplicate() for r in _rooms] # duplicate to avoid editing input
ap_count, shd_count = 0, 0
shade_dict = {}
for room in rooms:
if room.properties.energy.is_conditioned and \
room.properties.energy.setpoint is not None:
r_dict = {}
for face in room.faces:
if isinstance(face.boundary_condition, Outdoors):
aps = face.apertures
if len(aps) != 0:
fap_ids, fap_geos, fshd_geos = [], [], []
for ap in aps:
fap_ids.append(ap.identifier.upper())
fap_geos.append(from_face3d(ap.geometry))
fshd_geos.extend(from_face3d(shd.geometry)
for shd in ap.outdoor_shades)
ap.remove_shades() # remove shades for the energy simulation
if len(fshd_geos) != 0:
r_dict[face.identifier] = {
'ap_ids': fap_ids,
'ap_geo': fap_geos,
'shd_geo': fshd_geos,
'normal': from_vector3d(face.normal)
}
ap_count += len(fap_geos)
shd_count += len(fshd_geos)
if len(r_dict) != 0:
shade_dict[room.identifier.upper()] = r_dict
# make sure that there are shades to evaluate
if shd_count == 0:
msg = 'There were no shades to evaluate across all of the input _rooms.\n' \
'Make sure that shades are assigned to the apertures of conditioned\n' \
'rooms or use components like "HB Louver Shades" to generate shades\n' \
'that are assigned to the apertures.'
print(msg)
raise ValueError(msg)
else:
msg = 'Evaluating {} shade geometries across {} apertures.'.format(
shd_count, ap_count)
print(msg)
# create the Model from the _rooms and context_
_model = Model('Load_Shade_Benefit', rooms, orphaned_shades=context_,
units=units_system(),
tolerance=tolerance, angle_tolerance=angle_tolerance)
# process the simulation folder name and the directory
directory = os.path.join(folders.default_simulation_folder, _model.identifier)
sch_directory = os.path.join(directory, 'schedules')
nukedir(directory) # delete any existing files in the directory
# create simulation parameters for the coarsest/fastest E+ sim possible
_sim_par_ = SimulationParameter()
_sim_par_.timestep = timestep
_sim_par_.north_angle = _north_
_sim_par_.shadow_calculation.solar_distribution = 'FullExterior'
_sim_par_.output.reporting_frequency = 'Timestep'
_sim_par_.output.include_html = False
for out_p in all_output:
_sim_par_.output.add_output(out_p)
# assign design days from the EPW
msg = None
folder, epw_file_name = os.path.split(_epw_file)
ddy_file = os.path.join(folder, epw_file_name.replace('.epw', '.ddy'))
if os.path.isfile(ddy_file):
try:
_sim_par_.sizing_parameter.add_from_ddy_996_004(ddy_file)
except AssertionError:
msg = 'No design days were found in the .ddy file next to the _epw_file.'
else:
msg = 'No .ddy file was found next to the _epw_file.'
if msg is not None:
epw_obj = EPW(_epw_file)
des_days = [epw_obj.approximate_design_day('WinterDesignDay'),
epw_obj.approximate_design_day('SummerDesignDay')]
_sim_par_.sizing_parameter.design_days = des_days
msg = msg + '\nDesign days were generated from the input _epw_file but this ' \
'\nis not as accurate as design days from DDYs distributed with the EPW.'
give_warning(ghenv.Component, msg)
print(msg)
# create the strings for simulation paramters and model
ver_str = energyplus_idf_version() if energy_folders.energyplus_version \
is not None else energyplus_idf_version(compatibe_ep_version)
sim_par_str = _sim_par_.to_idf()
model_str = _model.to.idf(
_model, schedule_directory=sch_directory, patch_missing_adjacencies=True)
idf_str = '\n\n'.join([ver_str, sim_par_str, model_str])
# write the final string into an IDF
idf = os.path.join(directory, 'in.idf')
write_to_file_by_name(directory, 'in.idf', idf_str, True)
# run the IDF through EnergyPlus
silent = True if _run == 1 else False
sql, zsz, rdd, html, err = run_idf(idf, _epw_file, silent=silent)
if sql is None and err is not None: # something went wrong; parse the errors
err_obj = Err(err)
print(err_obj.file_contents)
for error in err_obj.fatal_errors:
raise Exception(error)
# parse the result sql and get the timestep data collections
if os.name == 'nt': # we are on windows; use IronPython like usual
sql_obj = SQLiteResult(sql)
cooling = sql_obj.data_collections_by_output_name(cool_out)
heating = sql_obj.data_collections_by_output_name(heat_out)
solar = sql_obj.data_collections_by_output_name(solar_out)
else: # we are on Mac; sqlite3 module doesn't work in Mac IronPython
# Execute the honybee CLI to obtain the results via CPython
cmds = [folders.python_exe_path, '-m', 'honeybee_energy', 'result',
'data-by-outputs', sql]
for outp in all_output:
cmds.append('["{}"]'.format(outp))
custom_env = os.environ.copy()
custom_env['PYTHONHOME'] = ''
process = subprocess.Popen(cmds, stdout=subprocess.PIPE, env=custom_env)
stdout = process.communicate()
data_coll_dicts = json.loads(stdout[0])
cooling = serialize_data(data_coll_dicts[0])
heating = serialize_data(data_coll_dicts[1])
solar = serialize_data(data_coll_dicts[2])
# convert the results to a dictionary for quick access
cool_dict, heat_dict, solar_dict = {}, {}, {}
for cool in cooling:
cool_dict[cool.header.metadata['System'].split(' ')[0]] = cool
for heat in heating:
heat_dict[heat.header.metadata['System'].split(' ')[0]] = heat
for sol in solar:
solar_dict[sol.header.metadata['Surface']] = sol
# initialize sunpath based on the EPW and get all of the vectors
epw_obj = EPW(_epw_file)
location = epw_obj.location
sp = Sunpath.from_location(location, _north_)
lb_vecs, relevant_i = [], []
for i, dt in enumerate(solar[0].datetimes):
sun = sp.calculate_sun_from_date_time(dt)
if sun.is_during_day:
lb_vecs.append(sun.sun_vector_reversed)
relevant_i.append(i)
vectors = [from_vector3d(lb_vec) for lb_vec in lb_vecs]
# if there is context, remove any rays that are blocked by the context
context_mesh = None
if len(context_) != 0 and context_[0] is not None:
context_mesh = join_geometry_to_mesh([from_face3d(c.geometry) for c in context_])
# loop through the relevant rooms and compute shade benefit
points, mesh = [], []
shade_help, shade_harm, shade_net = [], [], []
hide_output(ghenv.Component, 2)
for room_id, room_data in shade_dict.items():
cool_vals = cool_dict[room_id].values
heat_vals = heat_dict[room_id].values
# shif the values by the lag
cool_vals = cool_vals[-lag_steps:] + cool_vals[:-lag_steps]
heat_vals = heat_vals[-lag_steps:] + heat_vals[:-lag_steps]
for ap_data in room_data.values():
solar_vals = solar_dict[ap_data['ap_ids'][0]]
for ap_id in ap_data['ap_ids'][1:]:
solar_vals += solar_dict[ap_id]
# create the gridded mesh from the geometry
analysis_mesh = to_joined_gridded_mesh3d(ap_data['shd_geo'], _grid_size)
ap_mesh = from_mesh3d(analysis_mesh)
study_mesh = to_joined_gridded_mesh3d(ap_data['ap_geo'], _grid_size / 1.75)
ap_points = [from_point3d(pt) for pt in study_mesh.face_centroids]
points.extend(ap_points)
mesh.append(analysis_mesh)
# create a series of rays that represent the sun projected through the shade
int_rays = generate_intersection_rays(ap_points, vectors)
normals = [ap_data['normal']] * len(int_rays)
# intersect the sun rays with the shade mesh
face_int = intersect_rays_with_mesh_faces(
ap_mesh, int_rays, context_mesh, normals, cpu_count=workers)
# loop through the face intersection result and evaluate the benefit
pt_div = 1 / float(len(ap_points))
for face_res, face_area in zip(face_int, analysis_mesh.face_areas):
f_help, f_harm = 0, 0
for t_ind in face_res:
ri = relevant_i[t_ind]
cl, ht, sl = cool_vals[ri], heat_vals[ri], solar_vals[ri]
if cl > 0: # a step where shade helps
f_help += min(cl, sl)
elif ht > 0:
f_harm -= min(ht, sl)
# Normalize by the area of the cell so there's is a consistent metric
# between cells of different areas.
shd_help = ((f_help / face_area)) * pt_div
shd_harm = ((f_harm / face_area)) * pt_div
shade_help.append(shd_help)
shade_harm.append(shd_harm)
shade_net.append(shd_help + shd_harm)
# create the mesh and legend outputs
mesh = Mesh3D.join_meshes(mesh)
graphic = GraphicContainer(shade_net, mesh.min, mesh.max, legend_par_)
graphic.legend_parameters.title = 'kWh/{}2'.format(units_abbreviation())
if legend_par_ is None or legend_par_.are_colors_default:
graphic.legend_parameters.colors = reversed(Colorset.shade_benefit_harm())
if legend_par_ is None or legend_par_.min is None or legend_par_.max is None:
bnd_val = max(max(shade_net), abs(min(shade_net)))
if legend_par_ is None or legend_par_.min is None:
graphic.legend_parameters.min = -bnd_val
if legend_par_ is None or legend_par_.max is None:
graphic.legend_parameters.max = bnd_val
title = text_objects('Cooling/Heating Load Shade Benefit', graphic.lower_title_location,
graphic.legend_parameters.text_height * 1.5,
graphic.legend_parameters.font)
# create all of the visual outputs
mesh.colors = graphic.value_colors
mesh = from_mesh3d(mesh)
legend = legend_objects(graphic.legend)