Source code for ladybug_radiance.skymatrix

"""Get a matrix containing radiation values from each patch of a sky dome.

Creating this matrix is a necessary pre-step before doing incident radiation
analysis or generating a visualizations like a radiation rose.

This class uses Radiance's gendaymtx function to calculate the radiation
for each patch of the sky. Gendaymtx is written by Ian Ashdown and Greg Ward.
More information can be found in Radiance manual at:
http://www.radiance-online.org/learning/documentation/manual-pages/pdfs/gendaymtx.pdf
"""
from __future__ import division
import os
import subprocess
import sys

from ladybug.epw import EPW
from ladybug.wea import Wea
from ladybug.viewsphere import view_sphere
from ladybug.config import folders as lb_folders

from .config import folders

if folders.radbin_path is not None:
    GENDAYMTX_EXE = os.path.join(folders.radbin_path, 'gendaymtx.exe') if \
        os.name == 'nt' else os.path.join(folders.radbin_path, 'gendaymtx')
else:
    GENDAYMTX_EXE = None


[docs] class SkyMatrix(object): """A matrix containing radiation values from each patch of a sky dome. Args: wea: A Ladybug Wea object. 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). high_density: A Boolean to indicate whether the higher-density Reinhart sky matrix should be generated (True), which has roughly 4 times the sky patches as the (default) original Tregenza sky (False). Note that, while the Reinhart sky has a higher resolution and is more accurate, it will result in considerably longer calculation time for incident radiation studies. (Default: False). ground_reflectance: A number between 0 and 1 to note the average ground reflectance that is associated with the sky matrix. (Default: 0.2). Properties: * wea * north * high_density * ground_reflectance * benefit_matrix * folder * wea_duration * direct_values * diffuse_values * metadata * data """ # constants for converting RGB values output by gendaymtx to broadband radiation PATCHES_PER_ROW = { 1: view_sphere.TREGENZA_PATCHES_PER_ROW + (1,), 2: view_sphere.REINHART_PATCHES_PER_ROW + (1,) } PATCH_ROW_COEFF = { 1: view_sphere.TREGENZA_COEFFICIENTS, 2: view_sphere.REINHART_COEFFICIENTS } LINE_BREAK = b'\n' if sys.version_info > (3, 0) else '\n' SEPARATOR = b' ' if sys.version_info > (3, 0) else ' ' __slots__ = ( '_wea', '_north', '_high_density', '_ground_reflectance', '_folder', '_direct_values', '_diffuse_values', '_benefit_matrix', '_metadata') def __init__(self, wea, north=0, high_density=False, ground_reflectance=0.2): """Initialize SkyMatrix.""" self.wea = wea self.north = north self.high_density = high_density self.ground_reflectance = ground_reflectance self.benefit_matrix = None self.folder = None
[docs] @classmethod def from_components( cls, location, direct_normal_irradiance, diffuse_horizontal_irradiance, hoys=None, north=0, high_density=False, ground_reflectance=0.2): """Create a SkyMatrix from individual solar irradiance components. Args: location: Ladybug location object. direct_normal_irradiance: A HourlyContinuousCollection or a HourlyDiscontinuousCollection for direct normal irradiance. The collection must be aligned with the diffuse_horizontal_irradiance. diffuse_horizontal_irradiance: A HourlyContinuousCollection or a HourlyDiscontinuousCollection for diffuse horizontal irradiance, The collection must be aligned with the direct_normal_irradiance. hoys: A list of numbers between 0 and 8760 that represent the hours of the year for which to generate the sky matrix. If None, the matrix will be for the entire year. (Default: None). 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). high_density: A Boolean to indicate whether the higher-density Reinhart sky matrix should be generated (True), which has roughly 4 times the sky patches as the (default) original Tregenza sky (False). Note that, while the Reinhart sky has a higher resolution and is more accurate, it will result in considerably longer calculation time for incident radiation studies. (Default: False). ground_reflectance: A number between 0 and 1 to note the average ground reflectance that is associated with the sky matrix. (Default: 0.2). """ # filter the radiation by hoys if they are input if hoys is not None and len(hoys) != 0: dni = direct_normal_irradiance.filter_by_hoys(hoys) dhi = diffuse_horizontal_irradiance.filter_by_hoys(hoys) else: dni, dhi = direct_normal_irradiance, diffuse_horizontal_irradiance # create the wea and return the SkyMatrix wea = Wea(location, dni, dhi) return cls(wea, north, high_density, ground_reflectance)
[docs] @classmethod def from_components_benefit( cls, location, direct_normal_irradiance, diffuse_horizontal_irradiance, temperature, balance_temperature=15, balance_offset=2, hoys=None, north=0, high_density=False, ground_reflectance=0.2): """Create a SkyMatrix representing benefit/harm based on temperature data. Args: location: Ladybug location object. direct_normal_irradiance: A HourlyContinuousCollection or a HourlyDiscontinuousCollection for direct normal irradiance. The collection must be aligned with the diffuse_horizontal_irradiance. diffuse_horizontal_irradiance: A HourlyContinuousCollection or a HourlyDiscontinuousCollection for diffuse horizontal irradiance. The collection must be aligned with the direct_normal_irradiance. temperature: A HourlyContinuousCollection or a HourlyDiscontinuousCollection for temperature, which will be used to establish whether radiation is desired or not for each time step. The collection must be aligned with the irradiance inputs. balance_temperature: The temperature in Celsius between which radiation switches from being a benefit to a harm. Typical residential buildings have balance temperatures as high as 18C and commercial buildings tend to have lower values around 12C. (Default 15C). balance_offset: The temperature offset from the balance temperature in Celsius where radiation is neither harmful nor helpful. (Default: 2). hoys: A list of numbers between 0 and 8760 that represent the hours of the year for which to generate the sky matrix. If None, the matrix will be for the entire year. (Default: None). 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). high_density: A Boolean to indicate whether the higher-density Reinhart sky matrix should be generated (True), which has roughly 4 times the sky patches as the (default) original Tregenza sky (False). Note that, while the Reinhart sky has a higher resolution and is more accurate, it will result in considerably longer calculation time for incident radiation studies. (Default: False). ground_reflectance: A number between 0 and 1 to note the average ground reflectance that is associated with the sky matrix. (Default: 0.2). """ # filter the radiation by hoys if they are input if hoys is not None and len(hoys) != 0: dni = direct_normal_irradiance.filter_by_hoys(hoys) dhi = diffuse_horizontal_irradiance.filter_by_hoys(hoys) temperature = temperature.filter_by_hoys(hoys) else: dni, dhi = direct_normal_irradiance, diffuse_horizontal_irradiance # create the benefit matrix t_low = balance_temperature - balance_offset t_high = balance_temperature + balance_offset benefit_mtx = [] for t in temperature: if t < t_low: # low temperatures mean radiation is beneficial benefit_mtx.append(True) elif t > t_high: # high temperatures mean radiation is harmful benefit_mtx.append(False) else: # the temperature is neutral, meaning radiation has no effect benefit_mtx.append(None) # create the wea and return the SkyMatrix wea = Wea(location, dni, dhi) sky_mtx = cls(wea, north, high_density, ground_reflectance) sky_mtx.benefit_matrix = benefit_mtx return sky_mtx
[docs] @classmethod def from_epw(cls, epw_file, hoys=None, north=0, high_density=False, ground_reflectance=0.2): """Create a SkyMatrix using the solar irradiance values in an epw file. Args: epw_file: Full path to epw weather file. hoys: A list of numbers between 0 and 8760 that represent the hours of the year for which to generate the sky matrix. If None, the matrix will be for the entire year. (Default: None). 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). high_density: A Boolean to indicate whether the higher-density Reinhart sky matrix should be generated (True), which has roughly 4 times the sky patches as the (default) original Tregenza sky (False). Note that, while the Reinhart sky has a higher resolution and is more accurate, it will result in considerably longer calculation time for incident radiation studies. (Default: False). ground_reflectance: A number between 0 and 1 to note the average ground reflectance that is associated with the sky matrix. (Default: 0.2). """ epw = EPW(epw_file) return cls.from_components( epw.location, epw.direct_normal_radiation, epw.diffuse_horizontal_radiation, hoys, north, high_density, ground_reflectance)
[docs] @classmethod def from_epw_benefit(cls, epw_file, balance_temperature=15, balance_offset=2, hoys=None, north=0, high_density=False, ground_reflectance=0.2): """Create a SkyMatrix using the solar irradiance values in an epw file. Args: epw_file: Full path to epw weather file. balance_temperature: The temperature in Celsius between which radiation switches from being a benefit to a harm. Typical residential buildings have balance temperatures as high as 18C and commercial buildings tend to have lower values around 12C. (Default 15C). balance_offset: The temperature offset from the balance temperature in Celsius where radiation is neither harmful nor helpful. (Default: 2). hoys: A list of numbers between 0 and 8760 that represent the hours of the year for which to generate the sky matrix. If None, the matrix will be for the entire year. (Default: None). 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). high_density: A Boolean to indicate whether the higher-density Reinhart sky matrix should be generated (True), which has roughly 4 times the sky patches as the (default) original Tregenza sky (False). Note that, while the Reinhart sky has a higher resolution and is more accurate, it will result in considerably longer calculation time for incident radiation studies. (Default: False). ground_reflectance: A number between 0 and 1 to note the average ground reflectance that is associated with the sky matrix. (Default: 0.2). """ epw = EPW(epw_file) return cls.from_components_benefit( epw.location, epw.direct_normal_radiation, epw.diffuse_horizontal_radiation, epw.dry_bulb_temperature, balance_temperature, balance_offset, hoys, north, high_density, ground_reflectance)
[docs] @classmethod def from_stat(cls, stat_file, hoys=None, north=0, high_density=False, ground_reflectance=0.2): """Create a ASHRAE Revised Clear SkyMatrix using the data in .stat file. The .stat file must have monthly sky optical depths within it in order to create a Wea this way. Args: stat_file: Full path to a .stat file. hoys: A list of numbers between 0 and 8760 that represent the hours of the year for which to generate the sky matrix. If None, the matrix will be for the entire year. (Default: None). 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). high_density: A Boolean to indicate whether the higher-density Reinhart sky matrix should be generated (True), which has roughly 4 times the sky patches as the (default) original Tregenza sky (False). Note that, while the Reinhart sky has a higher resolution and is more accurate, it will result in considerably longer calculation time for incident radiation studies. (Default: False). ground_reflectance: A number between 0 and 1 to note the average ground reflectance that is associated with the sky matrix. (Default: 0.2). """ wea = Wea.from_stat_file(stat_file) # filter the radiation by hoys if they are input if hoys is not None and len(hoys) != 0: wea = wea.filter_by_hoys(hoys) return cls(wea, north, high_density, ground_reflectance)
[docs] @classmethod def from_ashrae_clear_sky(cls, location, sky_clearness=1, hoys=None, north=0, high_density=False, ground_reflectance=0.2): """Create an original ASHRAE Clear SkyMatrix using a location and sky clearness. The original ASHRAE Clear Sky is intended to determine peak solar load and sizing parameters for HVAC systems. It is not the sky model currently recommended by ASHRAE since it usually overestimates the amount of solar irradiance in comparison to the newer ASHRAE Revised Clear Sky ("Tau Model"). However, the original model here is still useful for cases where monthly optical depth values are not known. For more information on the ASHRAE Clear Sky model, see the EnergyPlus Engineering Reference: https://bigladdersoftware.com/epx/docs/8-9/engineering-reference/climate-calculations.html Args: location: Ladybug location object. sky_clearness: A factor that will be multiplied by the output of the model. This is to help account for locations where clear, dry skies predominate (e.g., at high elevations) or, conversely, where hazy and humid conditions are frequent. See Threlkeld and Jordan (1958) for recommended values. Typical values range from 0.95 to 1.05 and are usually never more than 1.2. Default is set to 1.0. hoys: A list of numbers between 0 and 8760 that represent the hours of the year for which to generate the sky matrix. If None, the matrix will be for the entire year. (Default: None). 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). high_density: A Boolean to indicate whether the higher-density Reinhart sky matrix should be generated (True), which has roughly 4 times the sky patches as the (default) original Tregenza sky (False). Note that, while the Reinhart sky has a higher resolution and is more accurate, it will result in considerably longer calculation time for incident radiation studies. (Default: False). ground_reflectance: A number between 0 and 1 to note the average ground reflectance that is associated with the sky matrix. (Default: 0.2). """ wea = Wea.from_ashrae_clear_sky(location, sky_clearness) # filter the radiation by hoys if they are input if hoys is not None and len(hoys) != 0: wea = wea.filter_by_hoys(hoys) return cls(wea, north, high_density, ground_reflectance)
@property def wea(self): """Get or set a Wea object for the sky matrix.""" return self._wea @wea.setter def wea(self, value): assert isinstance(value, Wea), \ 'wea must be from type Wea not {}'.format(type(value)) self._wea = value self._direct_values = None self._diffuse_values = None self._metadata = None @property def north(self): """Get or set a number north direction. 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. """ return self._north @north.setter def north(self, value): assert isinstance(value, (float, int)), 'Expected number for ' \ 'SkyMatrix north. Got {}.'.format(type(value)) assert -360 <= value <= 360, 'SkyMatrix north must be between 0 and 360. ' \ 'Got {}.'.format(value) self._north = value @property def high_density(self): """Get or set a boolean for whether the sky is a higher-density Reinhart matrix. """ return self._high_density @high_density.setter def high_density(self, value): self._high_density = bool(value) self._direct_values = None self._diffuse_values = None self._metadata = None @property def ground_reflectance(self): """Get or set a number between 0 and 1 to note the average ground reflectance. """ return self._ground_reflectance @ground_reflectance.setter def ground_reflectance(self, value): assert isinstance(value, (float, int)), 'Expected number for ' \ 'SkyMatrix ground_reflectance. Got {}.'.format(type(value)) assert 0 <= value <= 1, 'SkyMatrix ground_reflectance must be between 0 and 1.' \ ' Got {}.'.format(value) self._ground_reflectance = value @property def benefit_matrix(self): """Get or set list of True/False values for whether Wea datetimes are beneficial. This list must have a length that matches the Wea so that each datetime can be matched with a benefit/harm value. True (beneficial) values will contribute positively to the value of each sky patch while False (harmful) values will contribute negatively. A value of None in the list indicates that the hour has neither a benefit or a harmful effect. This is None by default, indicating that all radiation values contribute positively to each sky patch. """ return self._benefit_matrix @benefit_matrix.setter def benefit_matrix(self, value): if value is not None: assert isinstance(value, (list, tuple)), \ 'SkyMatrix.benefit_matrix must be a list. Not {}'.format(type(value)) assert len(value) == len(self.wea), 'Length of SkyMatrix.benefit_matrix ' \ '[{}] must equal the length of the sky matrix Wea [{}].'.format( len(value), len(self.wea)) self._benefit_matrix = value self._direct_values = None self._diffuse_values = None self._metadata = None @property def folder(self): """Get or set the folder in which the Radiance commands are executed. If None, it will be written to Ladybug's default EPW folder. """ if self._folder is None: return os.path.join(lb_folders.default_epw_folder, 'sky_matrices') return self._folder @folder.setter def folder(self, value): if value is not None: assert os.path.isdir(value), 'Path for ' \ 'SkyMatrix folder does not exist: {}'.format(type(value)) self._folder = value @property def wea_duration(self): """Get the duration of the Wea in hours. This is useful for converting the radiation values of the sky patches (kWh/m2) into irradiance (W/m2). """ return len(self.wea) / self.wea.timestep @property def direct_values(self): """Get the direct radiation values for each of the sky patches.""" if self._direct_values is None: self.compute_sky() return self._direct_values @property def diffuse_values(self): """Get the diffuse radiation values for each of the sky patches.""" if self._diffuse_values is None: self.compute_sky() return self._diffuse_values @property def metadata(self): """Get a list of metadata associated with the sky matrix.""" if self._metadata is None: self.compute_sky() return self._metadata @property def data(self): """Get a matrix of all data associated with the sky matrix. The first list contains metadata, followed by direct values and then diffuse values. """ if self._metadata is None: self.compute_sky() return (self._metadata, self._direct_values, self._diffuse_values)
[docs] def compute_sky(self): """Compute the values of the sky matrix.""" # extract metadata needed for all calculations wea_duration = len(self.wea) / self.wea.timestep metd = self.wea.direct_normal_irradiance.header.metadata wea_basename = metd['city'].replace(' ', '_') if 'city' in metd else 'unnamed' # if there's a benefit matrix, split the Wea into two files if self.benefit_matrix is not None: dir_vals1, dif_vals1, dir_vals2, dif_vals2 = [], [], [], [] zip_obj = zip( self.wea.direct_normal_irradiance, self.wea.diffuse_horizontal_irradiance, self.benefit_matrix ) for dir_v, dif_v, ben in zip_obj: if ben: # time contributes positively dir_vals1.append(dir_v) dif_vals1.append(dif_v) dir_vals2.append(0) dif_vals2.append(0) elif ben is None: # time is neither positive nor negative dir_vals1.append(0) dif_vals1.append(0) dir_vals2.append(0) dif_vals2.append(0) else: # time contributes negatively dir_vals2.append(dir_v) dif_vals2.append(dif_v) dir_vals1.append(0) dif_vals1.append(0) # create the first Wea object dir_data1 = self.wea.direct_normal_irradiance.duplicate() dif_data1 = self.wea.diffuse_horizontal_irradiance.duplicate() dir_data1.values = dir_vals1 dif_data1.values = dif_vals1 wea_obj1 = Wea(self.wea.location, dir_data1, dif_data1) # create the second Wea object dir_data2 = self.wea.direct_normal_irradiance.duplicate() dif_data2 = self.wea.diffuse_horizontal_irradiance.duplicate() dir_data2.values = dir_vals2 dif_data2.values = dif_vals2 wea_obj2 = Wea(self.wea.location, dir_data2, dif_data2) # write the Wea files wea_path1 = os.path.join(self.folder, '{}_benefit'.format(wea_basename)) wea_path2 = os.path.join(self.folder, '{}_harm'.format(wea_basename)) wea_file = wea_obj1.write(wea_path1) wea_file2 = wea_obj2.write(wea_path2) else: # otherwise, write the Wea to the folder wea_path = os.path.join(self.folder, wea_basename) wea_file = self.wea.write(wea_path) wea_file2 = None # execute the Radiance gendaymtx command dir_vals, dif_vals = self._run_gendaymtx(wea_file, wea_duration) # if there's a second wea, then use it to compute radiation harm if wea_file2 is not None: dir_vals2, dif_vals2 = self._run_gendaymtx(wea_file2, wea_duration) self._direct_values = tuple(db - dh for db, dh in zip(dir_vals, dir_vals2)) self._diffuse_values = tuple(db - dh for db, dh in zip(dif_vals, dif_vals2)) else: self._direct_values = dir_vals self._diffuse_values = dif_vals # collect sky metadata like the north, which will be used by other operations metadata = [self.north, self.ground_reflectance] dts = self.wea.direct_normal_irradiance.datetimes metadata.extend([dts[0], dts[-1]]) for key, val in self.wea.direct_normal_irradiance.header.metadata.items(): metadata.append('{} : {}'.format(key, val)) self._metadata = tuple(metadata)
def _run_gendaymtx(self, wea_file, wea_duration): """Run a Wea file through gendaymtx and get direct and diffuse radiation. Args: wea_file: Path to a Wea file to be run through gendaymtx. wea_duration: Number for the duration of the Wea in hours. This is used to convert between the average value output by the command and the cumulative value that is needed for all ladybug analyses. """ assert GENDAYMTX_EXE is not None, 'No Radiance installation was found.' density = 2 if self.high_density else 1 use_shell = True if os.name == 'nt' else False # command for direct patches cmds = [GENDAYMTX_EXE, '-m', str(density), '-d', '-O1', '-A', wea_file] process = subprocess.Popen(cmds, stdout=subprocess.PIPE, shell=use_shell) stdout = process.communicate() dir_data_str = stdout[0] # command for diffuse patches cmds = [GENDAYMTX_EXE, '-m', str(density), '-s', '-O1', '-A', wea_file] process = subprocess.Popen(cmds, stdout=subprocess.PIPE, shell=use_shell) stdout = process.communicate() diff_data_str = stdout[0] # parse the data into a single matrix dir_vals = self._parse_mtx_data(dir_data_str, wea_duration, density) diff_vals = self._parse_mtx_data(diff_data_str, wea_duration, density) return dir_vals, diff_vals def _parse_mtx_data(self, data_str, wea_duration, sky_density=1): """Parse a string of Radiance gendaymtx data to a list of radiation-per-patch. This function handles the removing of the header and the conversion of the RGB irradiance=per-steradian values to broadband radiation. It also removes the first patch, which is the ground and is not used by Ladybug. Args: data_str: The string that has been output by gendaymtx to stdout. wea_duration: Number for the duration of the Wea in hours. This is used to convert between the average value output by the command and the cumulative value that is needed for all ladybug analyses. sky_density: Integer (either 1 or 2) for the density. """ # split lines and remove the header, ground patch and last line break data_lines = data_str.split(self.LINE_BREAK) patch_lines = data_lines[9:-1] # loop through the rows and convert the radiation RGB values broadband_irr = [] patch_counter = 0 for i, row_patch_count in enumerate(self.PATCHES_PER_ROW[sky_density]): row_slice = patch_lines[patch_counter:patch_counter + row_patch_count] irr_vals = (self._broadband_radiation(row, i, wea_duration, sky_density) for row in row_slice) broadband_irr.extend(irr_vals) patch_counter += row_patch_count return tuple(broadband_irr) def _broadband_radiation( self, patch_row_str, row_number, wea_duration, sky_density=1): """Parse a row of gendaymtx RGB patch data in W/sr/m2 to radiation in kWh/m2. This includes applying broadband weighting to the RGB bands, multiplication by the steradians of each patch, and multiplying by the duration of time that they sky matrix represents in hours. Args: patch_row_str: Text string for a single row of RGB patch data. row_number: Integer for the row number that the patch corresponds to. sky_density: Integer (either 1 or 2) for the density. wea_duration: Number for the duration of the Wea in hours. This is used to convert between the average value output by the command and the cumulative value that is needed for all ladybug analyses. """ R, G, B = patch_row_str.split(self.SEPARATOR) w_val = 0.265074126 * float(R) + 0.670114631 * float(G) + 0.064811243 * float(B) coeff = self.PATCH_ROW_COEFF[sky_density][row_number] return w_val * coeff * wea_duration / 1000
[docs] def ToString(self): """Overwrite .NET ToString.""" return self.__repr__()
def __len__(self): if self._direct_values is None: self.compute_sky() return len(self._direct_values) def __getitem__(self, key): if self._direct_values is None: self.compute_sky() return self._direct_values[key], self._diffuse_values[key] def __iter__(self): if self._direct_values is None: self.compute_sky() return zip(self._direct_values, self._diffuse_values) def __repr__(self): """Sky Matrix object representation.""" return "SkyMatrix [%s]" % self.wea.location.city