Source code for ladybug_radiance.visualize.radrose

"""Class for visualizing the impact of radiation from different direction as a rose."""
from __future__ import division
import math

from ladybug_geometry.geometry2d.pointvector import Point2D
from ladybug_geometry.geometry3d.pointvector import Point3D, Vector3D
from ladybug_geometry.geometry3d.line import LineSegment3D
from ladybug_geometry.geometry3d.mesh import Mesh3D

from ladybug.datatype.energyintensity import Radiation
from ladybug.datatype.energyflux import Irradiance
from ladybug.viewsphere import view_sphere
from ladybug.compass import Compass
from ladybug.graphic import GraphicContainer
from ladybug.legend import LegendParameters
from ladybug.color import Colorset


[docs] class RadiationRose(object): """Visualize the impact of radiation from different direction as a rose. By default, the Radiation Rose depicts the amount of solar energy received by a vertical wall facing each of the directions of the compass rose. This is useful for understanding the radiation harm/benefit experienced by different building orientations or the orientations with the highest peak cooling load (for sky matrices of clear skies). The tilt_angle can be used to assess the solar energy falling on geometries that are not perfectly vertical, such as a tilted photovoltaic panel. Args: sky_matrix: A SkyMatrix object, which describes the radiation coming from the various patches of the sky. intersection_matrix: An optional lists of lists, which can be used to account for context shade surrounding the radiation rose. The matrix should have a length equal to the direction_count and begin from north moving clockwise. Each sub-list should consist of booleans and have a length equal to the number of sky patches times 2 (indicating sky patches and ground patches). True indicates that a certain patch is seen and False indicates that the match is blocked. If None, the radiation rose will be computed assuming no obstructions. (Default: None). direction_count: An integer greater than or equal to 3, which notes the number of arrows to be generated for the radiation rose. (Default: 36). tilt_angle: A number between 0 and 90 that sets the vertical tilt angle (aka. the altitude) for all of the directions. By default, the Radiation Rose depicts the amount of solar energy received by a vertical wall (tilt_angle=0). The tilt_angle can be changed to a specific value to assess the solar energy falling on geometries that are not perfectly vertical, such as a tilted photovoltaic panel. (Default: 0). legend_parameters: An optional LegendParameter object to change the display of the radiation rose. If None, default legend parameters will be used. (Default: None). plot_irradiance: Boolean to note whether the radiation rose should be plotted with units of total Radiation (kWh/m2) [False] or with units of average Irradiance (W/m2) [True]. (Default: False). center_point: A point for the center of the rose. (Default: (0, 0, 0)). radius: A number to set the radius of the radiation rose. (Default: 100). arrow_scale: A fractional number to note the scale of the radiation rose arrows in relation to the entire graphic. (Default: 1). Properties: * direction_count * tilt_angle * legend_parameters * plot_irradiance * center_point * radius * arrow_scale * north * direction_vectors * total_values * direct_values * diffuse_values * metadata * is_benefit """ __slots__ = ( '_north', '_direction_vectors', '_total_values', '_direct_values', '_diffuse_values', '_metadata', '_is_benefit', '_direction_count', '_tilt_angle', '_legend_parameters', '_plot_irradiance', '_center_point', '_radius', '_arrow_scale') def __init__(self, sky_matrix, intersection_matrix=None, direction_count=36, tilt_angle=0, legend_parameters=None, plot_irradiance=False, center_point=Point3D(0, 0, 0), radius=100, arrow_scale=1): """Initialize RadiationRose.""" # deconstruct the sky matrix and derive key data from it metadata, direct, diffuse = sky_matrix.data self._metadata = metadata self._north = metadata[0] # first item is the north angle self._plot_irradiance = bool(plot_irradiance) if plot_irradiance: factor = 1000 / sky_matrix.wea_duration \ if hasattr(sky_matrix, 'wea_duration') else \ 1000 / (((metadata[3] - metadata[2]).total_seconds() / 3600) + 1) direct = tuple(v * factor for v in direct) diffuse = tuple(v * factor for v in diffuse) elif not isinstance(direct, tuple): direct, diffuse = tuple(direct), tuple(diffuse) # get the radiation coming from the ground dir_ground = ((sum(direct) / len(direct)) * metadata[1],) * len(direct) dif_ground = ((sum(diffuse) / len(diffuse)) * metadata[1],) * len(diffuse) all_dir = direct + dir_ground all_dif = diffuse + dif_ground # get the vectors and angles for each direction self._direction_count = int(direction_count) assert self._direction_count >= 3, 'RadiationRose direction_count must be ' \ 'greater than or equal to 3. Got {}.'.format(direction_count) assert isinstance(tilt_angle, (float, int)), 'Expected number for tilt_angle. ' \ 'Got {}.'.format(type(tilt_angle)) assert 0 <= tilt_angle < 90, 'Rose tilt_angle must be between 0 and 90. ' \ 'Got {}.'.format(tilt_angle) self._tilt_angle = tilt_angle base_dir_vecs = view_sphere.horizontal_radial_vectors(self._direction_count) if self._tilt_angle != 0: x_axis = Vector3D(1, 0, 0) base_vec = Vector3D(0, 1, 0) horiz_angle = -2 * math.pi / self._direction_count vert_angle = math.radians(self._tilt_angle) dir_vecs = tuple( base_vec.rotate(x_axis, vert_angle).rotate_xy(horiz_angle * i) for i in range(self._direction_count)) else: dir_vecs = base_dir_vecs patch_vecs = view_sphere.tregenza_sphere_vectors if len(direct) == 145 else \ view_sphere.reinhart_sphere_vectors cos_angles = [[math.cos(v1.angle(v2)) for v2 in patch_vecs] for v1 in dir_vecs] if self._north != 0: na = math.radians(self._north) self._direction_vectors = tuple(vec.rotate_xy(na) for vec in base_dir_vecs) else: self._direction_vectors = base_dir_vecs # compute the radiation values for each direction point_relation = [] if intersection_matrix is None: for cos_a in cos_angles: pt_rel = [] for a in cos_a: w = 0 if a < 0 else a pt_rel.append(w) point_relation.append(pt_rel) else: for int_vals, cos_a in zip(intersection_matrix, cos_angles): pt_rel = [] for iv, a in zip(int_vals, cos_a): w = 0 if a < 0 or not iv else a pt_rel.append(w) point_relation.append(pt_rel) total_res, direct_res, diff_res = [], [], [] for pt_rel in point_relation: dir_v = sum(r * w for r, w in zip(pt_rel, all_dir)) dif_v = sum(r * w for r, w in zip(pt_rel, all_dif)) direct_res.append(dir_v) diff_res.append(dif_v) total_res.append(dir_v + dif_v) self._direct_values = tuple(direct_res) self._diffuse_values = tuple(diff_res) self._total_values = tuple(total_res) # override the legend default min and max to make sense for the radiation rose if legend_parameters is not None: assert isinstance(legend_parameters, LegendParameters), \ 'Expected LegendParameters. Got {}.'.format(type(legend_parameters)) l_par = legend_parameters.duplicate() else: l_par = LegendParameters() if hasattr(sky_matrix, 'benefit_matrix') and \ sky_matrix.benefit_matrix is not None: if l_par.min is None: l_par.min = min((min(self._total_values), -max(self._total_values))) if l_par.max is None: l_par.max = max((-min(self._total_values), max(self._total_values))) if l_par.are_colors_default: l_par.colors = reversed(Colorset.benefit_harm()) self._is_benefit = True else: if l_par.min is None: l_par.min = 0 if l_par.max is None: l_par.max = max(self._total_values) self._is_benefit = False self._legend_parameters = l_par # process the geometry parameters of the rose assert isinstance(center_point, Point3D), 'Expected Point3D for rose center. ' \ 'Got {}.'.format(type(center_point)) self._center_point = center_point assert isinstance(radius, (float, int)), 'Expected number for radius. ' \ 'Got {}.'.format(type(radius)) assert radius > 0, 'Rose radius must be greater than zero. ' \ 'Got {}.'.format(radius) self._radius = radius assert isinstance(arrow_scale, (float, int)), \ 'Expected number for arrow_scale. Got {}.'.format(type(arrow_scale)) assert arrow_scale > 0, \ 'Rose arrow_scale must be greater than zero. Got {}.'.format(arrow_scale) self._arrow_scale = arrow_scale @property def direction_count(self): """Get the number of directions for the radiation rose.""" return self._direction_count @property def tilt_angle(self): """Get the angle of the radiation rose vertical tilt in degrees.""" return self._tilt_angle @property def legend_parameters(self): """Get the legend parameters assigned to this radiation rose object.""" return self._legend_parameters @property def plot_irradiance(self): """Get a boolean for whether the rose values are for irradiance in (W/m2).""" return self._plot_irradiance @property def center_point(self): """Get a Point3D for the center of the radiation rose.""" return self._center_point @property def radius(self): """Get a number for the radius of the radiation rose.""" return self._radius @property def arrow_scale(self): """Get a number for the scale of arrows on the radiation rose.""" return self._arrow_scale @property def north(self): """Get a number north direction.""" return self._north @property def direction_vectors(self): """Get a list of vectors for each of the directions the rose is facing. All vectors are unit vectors. """ return self._direction_vectors @property def total_values(self): """Get a tuple of values for the total radiation/irradiance of each direction.""" return self._total_values @property def direct_values(self): """Get a tuple of values for the direct radiation/irradiance of each direction. """ return self._direct_values @property def diffuse_values(self): """Get a tuple of values for the diffuse radiation/irradiance of each direction. """ return self._diffuse_values @property def metadata(self): """Get a tuple of information about the metadata assigned to the radiation rose. """ return self._metadata @property def is_benefit(self): """Boolean to note whether the sky matrix includes benefit information.""" return self._is_benefit
[docs] def draw(self, rad_type='total', center=None, max_rad=None): """Draw an arrow mesh, orientation lines, compass, graphic/legend, and title. Args: rad_type: Text for the type of radiation to use. Choose from total, direct, diffuse. (Default: total). center: A Point3D to override the center of the rose. This is useful when rendering all of the sky components together and one rose should not be on top of another. If None, the center point assigned to the object instance is used. (Default: None). max_rad: An optional number to set the level of radiation or irradiance associated with the full radius of the rose. If None, this is determined by the maximum level of radiation in the input data but a number can be specified here to fix this at a specific value. This is particularly useful when comparing different roses to one another. (Default: None). Returns: A tuple with five values. - arrow_mesh -- A colored Mesh3D for the rose arrows. - orientation_lines -- A list of LineSegment3D for each direction. - compass -- A ladybug Compass object for the rose. - graphic -- A GraphicContainer for the colored arrow mesh, indicating the legend and title location for the rose. - rose_title -- Text for the title of the rose. """ # get the dome data to be plotted and the center point if rad_type.lower() == 'total': rad_data = self.total_values elif rad_type.lower() == 'direct': rad_data = self.direct_values elif rad_type.lower() == 'diffuse': rad_data = self.diffuse_values else: raise ValueError('Radiation type "{}" not recognized.'.format(rad_type)) center = self.center_point if center is None else center # generate the mesh for the arrows of the rose ar_angle = math.pi / self.direction_count if max_rad is None: len_factor = self.radius / max(self.total_values) if not self.is_benefit \ else self.radius / max((-min(self.total_values), max(self.total_values))) else: len_factor = self.radius / max_rad verts, faces, f_count = [center], [], 0 for r_val, dir_vec in zip(rad_data, self.direction_vectors): # get key dimensions of the arrow arrow_length = abs(r_val) * len_factor head_dist = arrow_length * 0.8 arrow_width = math.tan(ar_angle) * head_dist * 0.9 * self.arrow_scale h_vec = dir_vec * head_dist w_vec = dir_vec.rotate_xy(math.pi / 2) * arrow_width # create the mesh vertices right_pt = center.move(h_vec + w_vec) end_pt = center.move(dir_vec * arrow_length) left_pt = center.move(h_vec - w_vec) verts.extend((right_pt, end_pt, left_pt)) faces.append((0, f_count + 1, f_count + 2, f_count + 3)) f_count += 3 arrow_mesh = Mesh3D(verts, faces) # generate the frequency lines low_cent = Point3D(center.x, center.y, center.z - 0.01) orientation_lines = [ LineSegment3D(low_cent, vec * self.radius) for vec in self.direction_vectors] # output the rose visualization, including graphic and compass move_fac = self.radius * 1.15 min_pt = center.move(Vector3D(-move_fac, -move_fac, 0)) max_pt = center.move(Vector3D(move_fac, move_fac, 0)) if self.plot_irradiance: d_type, unit, typ_str = Irradiance(), 'W/m2', 'Irradiance' else: d_type, unit, typ_str = Radiation(), 'kWh/m2', 'Radiation' graphic = GraphicContainer( rad_data, min_pt, max_pt, self.legend_parameters, d_type, unit) arrow_mesh.colors = graphic.value_colors compass = Compass(self.radius, Point2D(center.x, center.y), self.north) # construct a title from the metadata st, end = self.metadata[2], self.metadata[3] time_str = '{} - {}'.format(st, end) if st != end else st if self.is_benefit: typ_str = '{} Benefit/Harm'.format(typ_str) rose_title = '{} {}\n{}\n{}'.format( rad_type.title(), typ_str, time_str, '\n'.join([dat for dat in self.metadata[4:]])) return arrow_mesh, orientation_lines, compass, graphic, rose_title
[docs] @staticmethod def radial_vectors(direction_count, tilt_angle=0): """Generate a list of radial vectors.""" if tilt_angle != 0: x_axis = Vector3D(1, 0, 0) base_vec = Vector3D(0, 1, 0) horiz_angle = -2 * math.pi / direction_count vert_angle = math.radians(tilt_angle) return tuple( base_vec.rotate(x_axis, vert_angle).rotate_xy(horiz_angle * i) for i in range(direction_count)) else: return view_sphere.horizontal_radial_vectors(direction_count)
[docs] def ToString(self): """Overwrite .NET ToString.""" return self.__repr__()
def __len__(self): return self.direction_count def __getitem__(self, key): return self._total_values[key], self._direct_values[key], \ self._diffuse_values[key] def __iter__(self): return zip(self._total_values, self._direct_values, self._diffuse_values) def __repr__(self): """Rose object representation.""" return 'RadiationRose [{} directions]'.format(self.direction_count)