"""Method to translate a Model to a VisualizationSet."""
import os
import json
import io
from ladybug_geometry.geometry3d import Point3D, Face3D
from ladybug.datatype.generic import GenericType
from ladybug.color import Color
from ladybug_display.geometry3d import DisplayPoint3D, DisplayFace3D, DisplayMesh3D
from ladybug_display.visualization import VisualizationSet, ContextGeometry, \
AnalysisGeometry, VisualizationData, VisualizationMetaData
from honeybee.boundarycondition import Outdoors, Ground, Surface
from honeybee.facetype import Wall, RoofCeiling, Floor, AirBoundary
from honeybee.colorobj import ColorRoom, ColorFace
from honeybee.shade import Shade
from honeybee.typing import clean_string
from ._util import _process_wireframe
from .colorobj import color_room_to_vis_set, color_face_to_vis_set
TYPE_COLORS = {
'Wall': Color(230, 180, 60),
'Roof': Color(128, 20, 20),
'Floor': Color(128, 128, 128),
'Air Boundary': Color(255, 255, 200, 100),
'Interior Wall': Color(230, 215, 150),
'Ceiling': Color(255, 128, 128),
'Interior Floor': Color(255, 128, 128),
'Aperture': Color(64, 180, 255, 100),
'Door': Color(160, 150, 100),
'Glass Door': Color(128, 204, 255, 100),
'Outdoor Shade': Color(120, 75, 190),
'Context Shade': Color(80, 50, 128),
'Indoor Shade': Color(159, 99, 255)
}
BC_COLORS = {
'Outdoors': Color(64, 180, 255),
'Surface': Color(0, 128, 0),
'Ground': Color(165, 82, 0),
'Adiabatic': Color(255, 128, 128),
'Other': Color(255, 255, 200)
}
[docs]
def model_to_vis_set(
model, color_by='type', include_wireframe=True, use_mesh=True,
hide_color_by=False, room_attrs=None, face_attrs=None,
grid_display_mode='Default', hide_grid=True,
grid_data_path=None, grid_data_display_mode='Surface', active_grid_data=None):
"""Translate a Honeybee Model to a VisualizationSet.
Args:
model: A Honeybee Model object to be converted to a VisualizationSet.
color_by: Text that dictates the colors of the Model geometry.
If none, only a wireframe of the Model will be generated, assuming
include_wireframe is True. This is useful when the primary purpose of
the visualization is to display results in relation to the Model
geometry or display some room_attrs or face_attrs as an AnalysisGeometry
or Text labels. (Default: type). Choose from the following:
* type
* boundary_condition
* None
include_wireframe: Boolean to note whether a ContextGeometry dedicated to
the Model Wireframe (in DisplayLineSegment3D) should be included
in the output VisualizationSet. (Default: True).
use_mesh: Boolean to note whether the colored model geometries should
be represented with DisplayMesh3D objects (True) instead of DisplayFace3D
objects (False). Meshes can usually be rendered faster and they scale
well for large models but all geometry is triangulated (meaning that
the wireframe in certain platforms might not appear ideal). (Default: True).
hide_color_by: Boolean to note whether the color_by geometry should be
hidden or shown by default. Hiding the color-by geometry is useful
when the primary purpose of the visualization is to display grid_data
or room/face attributes but it is still desirable to have the option
to turn on the geometry.
room_attrs: An optional list of room attribute objects.
face_attrs: An optional list of face attribute objects.
grid_display_mode: Text that dictates how the ContextGeometry for Model
SensorGrids should display in the resulting visualization. The Default
option will draw sensor points whenever there is no grid_data_path and
won't draw them at all when grid data is provided, assuming the
AnalysisGeometry of the grids is sufficient. Choose from the following:
* Default
* Points
* Wireframe
* Surface
* SurfaceWithEdges
* None
hide_grid: Boolean to note whether the SensorGrid ContextGeometry should be
hidden or shown by default. (Default: True).
grid_data_path: An optional path to a folder containing data that aligns
with the SensorGrids in the model. Any sub folder within this path
that contains a grids_into.json (and associated CSV files) will be
converted to an AnalysisGeometry in the resulting VisualizationSet.
If a vis_metadata.json file is found within this sub-folder, the
information contained within it will be used to customize the
AnalysisGeometry. Note that it is acceptable if data and
grids_info.json exist in the root of this grid_data_path. Also
note that this argument has no impact if honeybee-radiance is not
installed and SensorGrids cannot be decoded. (Default: None).
grid_data_display_mode: Optional text to set the display_mode of the
AnalysisGeometry that is is generated from the grid_data_path above. Note
that this has no effect if there are no meshes associated with the model
SensorGrids. (Default: Surface). Choose from the following:
* Surface
* SurfaceWithEdges
* Wireframe
* Points
active_grid_data: Optional text to specify the active data in the
AnalysisGeometry. This should match the name of the sub-folder
within the grid_data_path that should be active. If None, the
first data set in the grid_data_path with be active. (Default: None).
Returns:
A VisualizationSet object that represents the model.
"""
# group the geometries according to typical ContextGeometry layers
color_by = str(color_by).lower()
if color_by == 'type':
# set up a dictionary to hold all geometries
type_dict = {
'Wall': [], 'Roof': [], 'Floor': [], 'Air Boundary': [],
'Interior Wall': [], 'Ceiling': [], 'Interior Floor': [],
'Aperture': [], 'Door': [], 'Glass Door': [],
'Outdoor Shade': [], 'Context Shade': [], 'Indoor Shade': []
}
# add all faces to the dictionary
for face in model.faces:
for ap in face._apertures:
type_dict['Aperture'].append(ap.geometry)
for dr in face._doors:
if dr.is_glass:
type_dict['Glass Door'].append(dr.geometry)
else:
type_dict['Door'].append(dr.geometry)
if isinstance(face.type, AirBoundary):
type_dict['Air Boundary'].append(face.punched_geometry)
elif isinstance(face.boundary_condition, (Outdoors, Ground)):
if isinstance(face.type, Wall):
type_dict['Wall'].append(face.punched_geometry)
elif isinstance(face.type, RoofCeiling):
type_dict['Roof'].append(face.punched_geometry)
elif isinstance(face.type, Floor):
type_dict['Floor'].append(face.punched_geometry)
else:
if isinstance(face.type, Wall):
type_dict['Interior Wall'].append(face.punched_geometry)
elif isinstance(face.type, RoofCeiling):
type_dict['Ceiling'].append(face.punched_geometry)
elif isinstance(face.type, Floor):
type_dict['Interior Floor'].append(face.punched_geometry)
# add orphaned apertures to the dictionary
for ap in model._orphaned_apertures:
type_dict['Aperture'].append(ap.geometry)
# add all doors to the dictionary
for dr in model._orphaned_doors:
if dr.is_glass:
type_dict['Glass Door'].append(dr.geometry)
else:
type_dict['Door'].append(dr.geometry)
# add all shades to the dictionary
for shd in model.outdoor_shades:
if shd.is_detached:
type_dict['Context Shade'].append(shd.geometry)
else:
type_dict['Outdoor Shade'].append(shd.geometry)
for shd in model.indoor_shades:
type_dict['Indoor Shade'].append(shd.geometry)
# add all of the shade meshes to the dictionary
for shd in model.shade_meshes:
if shd.is_detached:
type_dict['Context Shade'].append(shd.geometry)
else:
type_dict['Outdoor Shade'].append(shd.geometry)
elif color_by == 'boundary_condition':
type_dict = {
'Outdoors': [], 'Surface': [], 'Ground': [], 'Adiabatic': [], 'Other': []}
# add all faces to the dictionary
for face in model.faces:
if isinstance(face.boundary_condition, Outdoors):
type_dict['Outdoors'].append(face.punched_geometry)
for ap in face._apertures:
type_dict['Outdoors'].append(ap.geometry)
for dr in face._doors:
type_dict['Outdoors'].append(dr.geometry)
elif isinstance(face.boundary_condition, Surface):
type_dict['Surface'].append(face.punched_geometry)
for ap in face._apertures:
type_dict['Surface'].append(ap.geometry)
for dr in face._doors:
type_dict['Surface'].append(dr.geometry)
elif isinstance(face.boundary_condition, Ground):
type_dict['Ground'].append(face.geometry)
elif face.boundary_condition.name == 'Adiabatic':
type_dict['Adiabatic'].append(face.geometry)
else:
type_dict['Other'].append(face.geometry)
# add orphaned apertures to the dictionary
for ap in model._orphaned_apertures:
if isinstance(ap.boundary_condition, Outdoors):
type_dict['Outdoors'].append(ap.geometry)
else:
type_dict['Surface'].append(ap.geometry)
# add all doors to the dictionary
for dr in model._orphaned_doors:
if isinstance(dr.boundary_condition, Outdoors):
type_dict['Outdoors'].append(dr.geometry)
else:
type_dict['Surface'].append(dr.geometry)
# add all shades to the dictionary
for shd in model.shades:
type_dict['Other'].append(shd.geometry)
# add all of the shade meshes to the dictionary
for shd in model.shade_meshes:
type_dict['Other'].append(shd.geometry)
elif color_by == 'none':
type_dict = {}
else: # unrecognized property for coloring
msg = 'Unrecognized color_by input "{}" for model_to_vis_set.'.format(color_by)
raise ValueError(msg)
# loop through the dictionary and add a ContextGeometry for each group
geo_objs = []
for geo_id, geometries in type_dict.items():
if len(geometries) != 0:
col = TYPE_COLORS[geo_id] if color_by == 'type' else BC_COLORS[geo_id]
if use_mesh:
dis_geos = []
for f in geometries:
c_geo = f.triangulated_mesh3d if isinstance(f, Face3D) else f
dis_geos.append(DisplayMesh3D(c_geo, color=col))
else:
dis_geos = []
for geo in geometries:
c_geo = DisplayFace3D(geo, color=col) if isinstance(geo, Face3D) \
else DisplayMesh3D(geo, color=col)
dis_geos.append(c_geo)
con_geo = ContextGeometry(geo_id.replace(' ', '_'), dis_geos)
if hide_color_by:
con_geo.hidden = True
con_geo.display_name = geo_id
geo_objs.append(con_geo)
# add room attributes to the VisualizationSet if requested
if room_attrs and len(model.rooms) != 0:
for rm_attr in room_attrs:
attrs = rm_attr.attrs
if rm_attr.text:
units, tol = model.units, model.tolerance
for r_attr in attrs:
ra_col_obj = ColorRoom(model.rooms, r_attr, rm_attr.legend_par)
geo_objs.append(
color_room_to_vis_set(ra_col_obj, False, True, units, tol)[0])
if rm_attr.color:
ra_col_obj = ColorRoom(model.rooms, attrs[0], rm_attr.legend_par)
geo_obj = color_room_to_vis_set(ra_col_obj, False, False)[0]
geo_obj.display_name = rm_attr.name
geo_obj.identifier = clean_string(rm_attr.name)
for r_attr in attrs[1:]:
ra_col_obj = ColorRoom(model.rooms, r_attr, rm_attr.legend_par)
ra_a_geo = color_room_to_vis_set(ra_col_obj, False, False)[0]
geo_obj.add_data_set(ra_a_geo[0])
geo_objs.append(geo_obj)
# add face attributes to the VisualizationSet if requested
if face_attrs is not None and len(face_attrs) != 0:
faces = []
for room in model.rooms:
faces.extend(room.faces)
faces.extend(room.apertures)
faces.extend(room.shades)
faces.extend(model.orphaned_faces)
faces.extend(model.orphaned_apertures)
faces.extend(model.orphaned_doors)
faces.extend(model.orphaned_shades)
if len(faces) != 0:
for ff_attr in face_attrs:
if ff_attr.face_types:
face_attr_types = tuple(ff_attr.face_types)
f_faces = [
face for face in faces
if isinstance(face, face_attr_types) or
(hasattr(face, 'type') and isinstance(face.type, face_attr_types))
]
else:
f_faces = faces
if ff_attr.boundary_conditions:
bcs = tuple(ff_attr.boundary_conditions)
f_faces = [
face for face in f_faces if
isinstance(face, Shade) or
(hasattr(face, 'boundary_condition') and
isinstance(face.boundary_condition, bcs))
]
if not f_faces:
continue
if ff_attr.text:
units, tol = model.units, model.tolerance
for f_attr in ff_attr.attrs:
fa_col_obj = ColorFace(f_faces, f_attr, ff_attr.legend_par)
geo_objs.append(
color_face_to_vis_set(
fa_col_obj, False, True, units, tol)[0]
)
if ff_attr.color:
fa_col_obj = ColorFace(f_faces, ff_attr.attrs[0], ff_attr.legend_par)
geo_obj = color_face_to_vis_set(fa_col_obj, False, False)[0]
geo_obj.identifier = clean_string(ff_attr.name)
geo_obj.display_name = ff_attr.name
for r_attr in ff_attr.attrs[1:]:
fa_col_obj = ColorFace(f_faces, r_attr, ff_attr.legend_par)
ra_a_geo = color_face_to_vis_set(fa_col_obj, False, False)[0]
geo_obj.add_data_set(ra_a_geo[0])
geo_objs.append(geo_obj)
# add the sensor grid geometry if requested
gdm = grid_display_mode.lower()
default_exclude = gdm == 'default' and grid_data_path is not None and \
os.path.isdir(grid_data_path)
if gdm != 'none' and not default_exclude:
# get the sensor grids and evaluate whether they have meshes
try:
grid_objs = model.properties.radiance.sensor_grids
except AttributeError: # honeybee-radiance is not installed
grid_objs = []
if len(grid_objs) != 0:
grid_meshes = [g.mesh for g in grid_objs]
g_meshes_avail = all(m is not None for m in grid_meshes)
# create the context geometry for the sensor grids
dis_geos = []
if gdm in ('default', 'points') or not g_meshes_avail:
for grid in grid_objs:
for p in grid.positions:
dis_geos.append(DisplayPoint3D(Point3D(*p)))
elif gdm == 'wireframe':
for mesh in grid_meshes:
dis_geos.append(DisplayMesh3D(mesh, display_mode='Wireframe'))
elif gdm in ('surface', 'surfacewithedges'):
for mesh in grid_meshes:
grey = Color(100, 100, 100)
d_mesh = DisplayMesh3D(
mesh, color=grey, display_mode=grid_display_mode)
dis_geos.append(d_mesh)
con_geo = ContextGeometry('Sensor_Grids', dis_geos)
if hide_grid:
con_geo.hidden = True
con_geo.display_name = 'Sensor Grids'
geo_objs.append(con_geo)
# add grid data if requested
if grid_data_path is not None and os.path.isdir(grid_data_path):
# first try to get all of the Model sensor grids
try:
grids = {g.full_identifier: g for g in
model.properties.radiance.sensor_grids}
except AttributeError: # honeybee-radiance is not installed
grids = {}
if len(grids) != 0:
# gather all of the directories with results
gi_dirs, gi_file, act_data, cur_data = [], 'grids_info.json', 0, 0
root_gi_file = os.path.join(grid_data_path, gi_file)
if os.path.isfile(root_gi_file):
gi_dirs.append(grid_data_path)
for sub_f in os.listdir(grid_data_path):
sub_dir = os.path.join(grid_data_path, sub_f)
if os.path.isdir(sub_dir):
sub_gi_file = os.path.join(sub_dir, gi_file)
if os.path.isfile(sub_gi_file):
gi_dirs.append(sub_dir)
if active_grid_data is not None and \
sub_f.lower() == active_grid_data.lower():
act_data = cur_data
cur_data += 1
# loop through the result directories and load the results
data_sets = []
for g_dir in gi_dirs:
g_values = _read_sensor_grid_result(g_dir)
meta_file = os.path.join(g_dir, 'vis_metadata.json')
if os.path.isfile(meta_file):
with io.open(meta_file, 'r', encoding='utf-8') as mf:
m_data = json.load(mf)
gm_data = VisualizationMetaData.from_dict(m_data)
v_data = VisualizationData(
g_values, gm_data.legend_parameters,
gm_data.data_type, gm_data.unit)
data_sets.append(v_data)
else:
generic_type = GenericType(os.path.split(g_dir)[-1], '')
v_data = VisualizationData(g_values, data_type=generic_type)
data_sets.append(v_data)
# create the analysis geometry
if len(data_sets) != 0:
ex_gi_file = os.path.join(gi_dirs[0], gi_file)
with io.open(ex_gi_file, encoding='utf-8') as json_file:
grid_list = json.load(json_file)
grid_objs = [grids[g['full_id']] for g in grid_list]
grid_meshes = [g.mesh for g in grid_objs]
if all(m is not None for m in grid_meshes):
a_geo = AnalysisGeometry('Grid_Data', grid_meshes, data_sets)
else:
gr_pts = [Point3D(*pos) for gr in grid_objs for pos in gr.positions]
a_geo = AnalysisGeometry('Grid_Data', gr_pts, data_sets)
a_geo.display_name = 'Grid Data'
a_geo.display_mode = grid_data_display_mode
a_geo.active_data = act_data
geo_objs.append(a_geo)
# add the wireframe if requested
if include_wireframe:
wf_geo = model_to_vis_set_wireframe(model)
if wf_geo is not None:
geo_objs.append(wf_geo[0])
# build the VisualizationSet and return it
vis_set = VisualizationSet(model.identifier, geo_objs, model.units)
vis_set.display_name = model.display_name
return vis_set
[docs]
def model_to_vis_set_wireframe(model, color=None):
"""Get a VisualizationSet with a single ContextGeometry for the model wireframe.
Args:
model: A Honeybee Model object to be translated to a wireframe.
color: An optional Color object to set the color of the wireframe.
If None, the color will be black.
Returns:
A VisualizationSet with a single ContextGeometry and a list of
DisplayLineSegment3D for the wireframe of the Model.
"""
# loop through all of the objects and add their wire frames
wireframe = []
for face in model.faces:
_process_wireframe(face.geometry, wireframe, color, 2)
for ap in face._apertures:
_process_wireframe(ap.geometry, wireframe, color)
for dr in face._doors:
_process_wireframe(dr.geometry, wireframe, color)
for ap in model._orphaned_apertures:
_process_wireframe(ap.geometry, wireframe, color)
for dr in model._orphaned_doors:
_process_wireframe(dr.geometry, wireframe, color)
for shd in model.indoor_shades:
_process_wireframe(shd.geometry, wireframe, color)
for shd in model.outdoor_shades:
lw = 2 if shd.is_detached else 1
_process_wireframe(shd.geometry, wireframe, color, lw)
# build the VisualizationSet and return it
if len(wireframe) == 0:
return None
vis_set = VisualizationSet(
model.identifier, [ContextGeometry('Wireframe', wireframe)])
vis_set.display_name = model.display_name
return vis_set
[docs]
def model_comparison_to_vis_set(
base_model, incoming_model, base_color=None, incoming_color=None):
"""Translate two Honeybee Models to be compared to a VisualizationSet.
Args:
base_model: A Honeybee Model object for the base model used in the
comparison. Typically, this is the model with more data to be kept.
incoming_model: A Honeybee Model object for the incoming model used in the
comparison. Typically, this is the model with new data to be
evaluated against the base model.
base_color: An optional ladybug Color to set the color of the base model.
If None, a default blue color will be used. (Default: None).
incoming_color: An optional ladybug Color to set the color of the incoming model.
If None, a default red color will be used. (Default: None).
"""
# make sure that both models have the same units system
original_units = None
if base_model.units != incoming_model.units:
original_units = incoming_model.units
incoming_model.convert_to_units(base_model.units)
# set the default colors if not provided
if base_color is None:
base_color = Color(98, 190, 190, 128)
if incoming_color is None:
incoming_color = Color(190, 98, 98, 128)
# initialize the VisualizationSet to hold everything
vs_id = 'Compare_{}_{}'.format(base_model.identifier[:30],
incoming_model.identifier[:30])
vis_set = VisualizationSet(vs_id, [])
vis_set.display_name = 'Compare "{}" to "{}"'.format(
base_model.display_name, incoming_model.display_name)
# get the wireframe of the base model
base_geos = []
for room in base_model.rooms:
room_wire = room.to_vis_set_wireframe(False, False, base_color)
base_geos.extend(room_wire[0].geometry)
base_id = 'Base_Wireframe_{}'.format(base_model.identifier)
base_wireframe = ContextGeometry(base_id, base_geos)
base_wireframe.display_name = 'Base Wireframe'
vis_set.add_geometry(base_wireframe)
# get the wireframe of the incoming model
incoming_geos = []
for room in incoming_model.rooms:
room_wire = room.to_vis_set_wireframe(False, False, incoming_color)
incoming_geos.extend(room_wire[0].geometry)
incoming_id = 'Incoming_Wireframe_{}'.format(incoming_model.identifier)
incoming_wireframe = ContextGeometry(incoming_id, incoming_geos)
incoming_wireframe.display_name = 'Incoming Wireframe'
for vis_geo in incoming_wireframe.geometry:
vis_geo.color = incoming_color
vis_set.add_geometry(incoming_wireframe)
# get the apertures of the base model
base_geos = []
for aperture in base_model.apertures + base_model.doors:
dis_geo = DisplayFace3D(aperture.geometry, base_color, 'SurfaceWithEdges')
base_geos.append(dis_geo)
base_id = 'Base_Windows_{}'.format(base_model.identifier)
base_windows = ContextGeometry(base_id, base_geos)
base_windows.display_name = 'Base Windows'
vis_set.add_geometry(base_windows)
# get the apertures of the incoming model
incoming_geos = []
for aperture in incoming_model.apertures + incoming_model.doors:
dis_geo = DisplayFace3D(aperture.geometry, incoming_color, 'SurfaceWithEdges')
incoming_geos.append(dis_geo)
incoming_id = 'Incoming_Windows_{}'.format(incoming_model.identifier)
incoming_windows = ContextGeometry(incoming_id, incoming_geos)
incoming_windows.display_name = 'Incoming Windows'
vis_set.add_geometry(incoming_windows)
# put back the original units if different
if original_units is not None:
incoming_model.convert_to_units(original_units)
return vis_set
def _read_sensor_grid_result(result_folder):
"""Read results from files that align with sensor grids.
Args:
result_folder: Path to the folder containing the results.
Returns:
A matrix with each sub-list containing the values for each of the sensor grids.
"""
# check that the required files are present
if not os.path.isdir(result_folder):
raise ValueError('Invalid result folder: %s' % result_folder)
grid_json = os.path.join(result_folder, 'grids_info.json')
if not os.path.isfile(grid_json):
raise ValueError('Result folder contains no grids_info.json.')
# load the list of grids and gather all of the results
with io.open(grid_json, encoding='utf-8') as json_file:
grid_list = json.load(json_file)
results = []
for grid in grid_list:
grid_id = grid['full_id']
sensor_count = grid['count']
try:
st_ln = grid['start_ln']
except KeyError:
# older version of sensor info
st_ln = 0
# find the result file and append the results
result_file = None
for f in os.listdir(result_folder):
if f.startswith(grid_id) and not f.endswith('.json'):
result_file = os.path.join(result_folder, f)
break
if result_file is not None:
print(
'Loading results for {} with {} sensors. '
'Starting from line {}.'.format(grid_id, sensor_count, st_ln)
)
with io.open(result_file, encoding='utf-8') as inf:
for _ in range(st_ln):
next(inf)
for count in range(sensor_count):
try:
value = float(next(inf))
except (StopIteration, ValueError):
with io.open(result_file, 'r', encoding='utf-8') as rf:
content = rf.read()
ln_count = len(content.split())
raise ValueError(
'Failed to load the results for {} '
'with {} sensors. Sensor id: {}\n'
'Here is the content of the file with {} values:\n'
'## Start of the file\n{}\n## End of the file.'.format(
grid_id, sensor_count, count, ln_count, content)
)
else:
results.append(value)
return results