"""Radiance Solar Access Grid-based Analysis Recipe."""
from .._gridbasedbase import GenericGridBased
from ..recipeutil import write_rad_files, write_extra_files
from ..recipedcutil import rgb_matrix_file_to_ill
from ...parameters.rcontrib import RcontribParameters
from ...command.oconv import Oconv
from ...command.rcontrib import Rcontrib
from ...analysisgrid import AnalysisGrid
from ...sky.analemma import Analemma
from ....futil import write_to_file
from ....vectormath.euclid import Vector3
from ....hbsurface import HBSurface
from ladybug.sunpath import Sunpath
from ladybug.location import Location
from ladybug.legend import LegendParameters
from ladybug.color import Colorset
import os
[docs]class SolarAccessGridBased(GenericGridBased):
"""Solar access recipe.
This class calculates number of sunlight hours for a group of test points.
Attributes:
sun_vectors: A list of ladybug sun vectors as (x, y, z) values. Z value
for sun vectors should be negative (coming from sun toward earth)
hoys: A list of hours of the year for each sun vector.
analysis_grids: List of analysis grids.
timestep: The number of timesteps per hour for sun vectors. This number
should be smaller than 60 and divisible by 60. The default is set to
1 such that one sun vector is generated for each hour (Default: 1).
hb_objects: An optional list of Honeybee surfaces or zones (Default: None).
sub_folder: Analysis subfolder for this recipe. (Default: "solaraccess")
Usage:
# initiate analysis_recipe
analysis_recipe = SolarAccess(sun_vectors, analysis_grids)
# add honeybee object
analysis_recipe.hb_objects = HBObjs
# write analysis files to local drive
analysis_recipe.write_to_file(_folder_, _name_)
# run the analysis
analysis_recipe.run(debug=False)
# get the results
print(analysis_recipe.results())
"""
def __init__(self, sun_vectors, hoys, analysis_grids, timestep=1, hb_objects=None,
sub_folder='solaraccess'):
"""Create sunlighthours recipe."""
GenericGridBased.__init__(
self, analysis_grids, hb_objects, sub_folder
)
assert len(hoys) == len(sun_vectors), \
ValueError(
'Length of sun_vectors {} must be equall to '
'the length of hoys {}'.format(len(sun_vectors), len(hoys))
)
self.sun_vectors = sun_vectors
self._hoys = hoys
self.timestep = timestep
# this is a bug! should be set under a setter method
self._radiance_parameters = RcontribParameters()
self._radiance_parameters.irradiance_calc = True
self._radiance_parameters.ambient_bounces = 0
self._radiance_parameters.direct_certainty = 1
self._radiance_parameters.direct_threshold = 0
self._radiance_parameters.direct_jitter = 0
[docs] @classmethod
def from_json(cls, rec_json):
"""Create the solar access recipe from json.
{
"id": "solar_access",
"type": "gridbased",
"location": null, // a honeybee location - see below
"hoys": [], // list of hours of the year
"surfaces": [], // list of honeybee surfaces
"analysis_grids": [] // list of analysis grids
"sun_vectors": [] // list of sun vectors if location is not provided
}
"""
hoys = rec_json["hoys"]
if 'sun_vectors' not in rec_json or not rec_json['sun_vectors']:
# create sun vectors from location inputs
loc = Location.from_json(rec_json['location'])
sp = Sunpath.from_location(loc)
suns = (sp.calculate_sun_from_hoy(hoy) for hoy in hoys)
sun_vectors = tuple(s.sun_vector for s in suns if s.is_during_day)
else:
sun_vectors = rec_json['sun_vectors']
analysis_grids = \
tuple(AnalysisGrid.from_json(ag) for ag in rec_json["analysis_grids"])
hb_objects = tuple(HBSurface.from_json(srf) for srf in rec_json["surfaces"])
return cls(sun_vectors, hoys, analysis_grids, 1, hb_objects)
[docs] @classmethod
def from_points_and_vectors(cls, sun_vectors, hoys, point_groups, vector_groups=[],
timestep=1, hb_objects=None, sub_folder='sunlighthour'):
"""Create sunlighthours recipe from points and vectors.
Args:
sun_vectors: A list of ladybug sun vectors as (x, y, z) values. Z value
for sun vectors should be negative (coming from sun toward earth)
hoys: A list of hours of the year for each sun vector.
point_groups: A list of (x, y, z) test points or lists of (x, y, z) test
points. Each list of test points will be converted to a
TestPointGroup. If testPts is a single flattened list only one
TestPointGroup will be created.
vector_groups: An optional list of (x, y, z) vectors. Each vector
represents direction of corresponding point in testPts. If the
vector is not provided (0, 0, 1) will be assigned.
timestep: The number of timesteps per hour for sun vectors. This number
should be smaller than 60 and divisible by 60. The default is set to
1 such that one sun vector is generated for each hour (Default: 1).
hb_objects: An optional list of Honeybee surfaces or zones (Default: None).
sub_folder: Analysis subfolder for this recipe. (Default: "sunlighthours")
"""
analysis_grids = cls.analysis_grids_from_points_and_vectors(point_groups,
vector_groups)
return cls(sun_vectors, hoys, analysis_grids, timestep, hb_objects, sub_folder)
[docs] @classmethod
def from_suns(cls, suns, point_groups, vector_groups=[], timestep=1,
hb_objects=None, sub_folder='sunlighthour'):
"""Create sunlighthours recipe from LB sun objects.
Attributes:
suns: A list of ladybug suns.
point_groups: A list of (x, y, z) test points or lists of (x, y, z) test
points. Each list of test points will be converted to a
TestPointGroup. If testPts is a single flattened list only one
TestPointGroup will be created.
vector_groups: An optional list of (x, y, z) vectors. Each vector
represents direction of corresponding point in testPts. If the
vector is not provided (0, 0, 1) will be assigned.
timestep: The number of timesteps per hour for sun vectors. This number
should be smaller than 60 and divisible by 60. The default is set to
1 such that one sun vector is generated for each hour (Default: 1).
hb_objects: An optional list of Honeybee surfaces or zones (Default: None).
sub_folder: Analysis subfolder for this recipe. (Default: "sunlighthours")
"""
try:
sun_vectors = tuple(s.sun_vector for s in suns if s.is_during_day)
hoys = tuple(s.hoy for s in suns if s.is_during_day)
except AttributeError:
raise TypeError('The input is not a valid LBSun.')
analysis_grids = cls.analysis_grids_from_points_and_vectors(point_groups,
vector_groups)
return cls(sun_vectors, hoys, analysis_grids, timestep, hb_objects, sub_folder)
[docs] @classmethod
def from_location_and_hoys(cls, location, hoys, point_groups, vector_groups=[],
timestep=1, hb_objects=None, sub_folder='sunlighthour'):
"""Create sunlighthours recipe from Location and hours of year."""
sp = Sunpath.from_location(location)
suns = tuple(sp.calculate_sun_from_hoy(hoy) for hoy in hoys)
sun_vectors = tuple(s.sun_vector for s in suns if s.is_during_day)
sun_up_hoys = tuple(s.hoy for s in suns if s.is_during_day)
analysis_grids = cls.analysis_grids_from_points_and_vectors(point_groups,
vector_groups)
return cls(sun_vectors, sun_up_hoys, analysis_grids, timestep, hb_objects,
sub_folder)
[docs] @classmethod
def from_location_and_analysis_period(
cls, location, analysis_period, point_groups, vector_groups=None,
hb_objects=None, sub_folder='sunlighthour'):
"""Create sunlighthours recipe from Location and analysis period."""
vector_groups = vector_groups or ()
sp = Sunpath.from_location(location)
suns = tuple(sp.calculate_sun_from_hoy(hoy) for hoy in analysis_period.hoys)
sun_vectors = tuple(s.sun_vector for s in suns if s.is_during_day)
hoys = tuple(s.hoy for s in suns if s.is_during_day)
analysis_grids = cls.analysis_grids_from_points_and_vectors(point_groups,
vector_groups)
return cls(sun_vectors, hoys, analysis_grids, analysis_period.timestep,
hb_objects, sub_folder)
@property
def hoys(self):
"""Return list of hours of the year."""
return self._hoys
@property
def sun_vectors(self):
"""A list of ladybug sun vectors as (x, y, z) values."""
return self._sun_vectors
@sun_vectors.setter
def sun_vectors(self, vectors):
try:
self._sun_vectors = tuple(Vector3(*v).flipped() for v in vectors
if v[2] < 0)
except TypeError:
self._sun_vectors = tuple(Vector3(v.X, v.Y, v.Z).flipped()
for v in vectors if v.Z < 0)
except IndexError:
raise ValueError("Failed to create the sun vectors!")
if len(self.sun_vectors) != len(vectors):
print('%d vectors with positive z value are found and removed '
'from sun vectors' % (len(vectors) - len(self.sun_vectors)))
@property
def timestep(self):
"""An intger for the number of timesteps per hour for sun vectors.
This number should be smaller than 60 and divisible by 60.
"""
return self._timestep
@timestep.setter
def timestep(self, ts):
try:
self._timestep = int(ts)
except TypeError:
self._timestep = 1
assert self._timestep != 0, 'ValueError: TimeStep cannot be 0.'
@property
def legend_parameters(self):
"""Legend parameters for solar access analysis."""
col = Colorset.ecotect()
return LegendParameters(0, None, colors=col)
[docs] def write(self, target_folder, project_name='untitled', header=True,
transpose=False):
"""Write analysis files to target folder.
Files for sunlight hours analysis are:
test points <project_name.pts>: List of analysis points.
suns file <*.sun>: list of sun sources .
suns material file <*_suns.mat>: Radiance materials for sun sources.
suns geometry file <*_suns.rad>: Radiance geometries for sun sources.
material file <*.mat>: Radiance materials. Will be empty if hb_objects is
None.
geometry file <*.rad>: Radiance geometries. Will be empty if hb_objects is
None.
batch file <*.bat>: An executable batch file which has the list of commands.
oconv [material file] [geometry file] [sun materials file] [sun
geometries file] > [octree file]
rcontrib -ab 0 -ad 10000 -I -M [sunlist.txt] -dc 1 [octree file]< [pts
file] > [rcontrib results file]
Args:
target_folder: Path to parent folder. Files will be created under
target_folder/gridbased. use self.sub_folder to change subfolder name.
project_name: Name of this project as a string.
Returns:
True in case of success.
"""
# 0.prepare target folder
self._commands = []
# create main folder target_folder/project_name
project_folder = \
super(GenericGridBased, self).write_content(target_folder, project_name)
# write geometry and material files
opqfiles, glzfiles, wgsfiles = write_rad_files(
project_folder + '/scene', project_name, self.opaque_rad_file,
self.glazing_rad_file, self.window_groups_rad_files
)
# additional radiance files added to the recipe as scene
extrafiles = write_extra_files(self.scene, project_folder + '/scene', True)
# 1.write points
points_file = self.write_analysis_grids(project_folder, project_name)
# 2.write sun files
ann = Analemma(self.sun_vectors, self.hoys)
ann.execute(project_folder + '/sky')
sun_modifiers = os.path.join('.', 'sky', ann.sunlist_file)
suns_geo = os.path.join(project_folder + '/sky', ann.analemma_file)
# 2.1.add sun list to modifiers
self._radiance_parameters.mod_file = sun_modifiers
self._radiance_parameters.y_dimension = self.total_point_count
# 3.write batch file
if header:
self._commands.append(self.header(project_folder))
# TODO(Mostapha): add window_groups here if any!
# # 4.1.prepare oconv
oct_scene_files = opqfiles + glzfiles + wgsfiles + [suns_geo] + \
extrafiles.fp
oct_scene_files_items = []
for f in oct_scene_files:
if isinstance(f, (list, tuple)):
print('Point-in-time recipes cannot currently handle dynamic window'
' groups. The first state will be used for simulation.')
oct_scene_files_items.append(f[0])
else:
oct_scene_files_items.append(f)
oc = Oconv(project_name)
oc.scene_files = tuple(self.relpath(f, project_folder)
for f in oct_scene_files_items)
# # 4.2.prepare Rcontrib
rct = Rcontrib('result/' + project_name,
rcontrib_parameters=self._radiance_parameters)
rct.octree_file = str(oc.output_file)
rct.points_file = self.relpath(points_file, project_folder)
batch_file = os.path.join(project_folder, "commands.bat")
rmtx = rgb_matrix_file_to_ill((str(rct.output_file),),
'result/{}.ill'.format(project_name),
transpose)
# # 4.3 write batch file
self._commands.append(oc.to_rad_string())
self._commands.append(rct.to_rad_string())
self._commands.append(rmtx.to_rad_string())
self._result_files = os.path.join(project_folder, str(rmtx.output_file))
batch_file = os.path.join(project_folder, "commands.bat")
return write_to_file(batch_file, '\n'.join(self.commands))
[docs] def results(self):
"""Return results for this analysis."""
assert self._isCalculated, \
"You haven't run the Recipe yet. Use self.run " + \
"to run the analysis before loading the results."
print('Unloading the current values from the analysis grids.')
for ag in self.analysis_grids:
ag.unload()
hours = self.hoys
rf = self._result_files
start_line = 0
for count, analysisGrid in enumerate(self.analysis_grids):
if count:
start_line += len(self.analysis_grids[count - 1])
# TODO(): Add timestep
analysisGrid.set_values_from_file(
rf, hours, start_line=start_line, header=True, check_point_count=False,
mode=1
)
return self.analysis_grids
[docs] def to_json(self):
"""Create the solar access recipe from json.
{
"id": "solar_access",
"type": "gridbased",
"location": null, // a honeybee location - see below
"hoys": [], // list of hours of the year
"surfaces": [], // list of honeybee surfaces
"analysis_grids": [], // list of analysis grids
"sun_vectors": []
}
"""
return {
"id": "solar_access",
"type": "gridbased",
"location": None,
"hoys": self.hoys,
"surfaces": [srf.to_json() for srf in self.hb_objects],
"analysis_grids": [ag.to_json() for ag in self.analysis_grids],
"sun_vectors": [tuple(-1 * c for c in v) for v in self.sun_vectors]
}