# coding=utf-8
"""Object representing a group of sub-faces or shades that change states in unison."""
from __future__ import division
from .state import RadianceShadeState, RadianceSubFaceState
from ..geometry import Polygon
from ..modifier.material import BSDF
from ..lib.modifiers import white_glow
from honeybee.typing import valid_rad_string
from honeybee.shade import Shade
from honeybee.aperture import Aperture
from honeybee.door import Door
from honeybee.boundarycondition import Surface
from honeybee_radiance_command.options.rfluxmtx import RfluxmtxControlParameters
import os
[docs]
class DynamicShadeGroup(object):
"""Object representing a group of dynamic shades that change states in unison.
Note that this object is not intended to set any Model properties. It is only
used by the Model to collect objects with the same dynamic_group_identifier
and export a common set of radiance files from them.
Args:
identifier: Text string for a unique Group ID. Must not contain spaces
or special characters. This will be used to identify the object across
a model and in the exported Radiance files.
dynamic_objects: An array of Shades that are contained within the group.
Properties:
* identifier
* dynamic_objects
* state_count
* is_opaque
* is_indoor
* states_json_list
"""
__slots__ = ('_identifier', '_dynamic_objects', '_state_count')
def __init__(self, identifier, dynamic_objects):
"""Initialize DynamicShadeGroup."""
# set identifier
self._identifier = valid_rad_string(identifier)
# set the dynamic_objects
if not isinstance(dynamic_objects, tuple):
dynamic_objects = tuple(dynamic_objects)
self._check_dynamic_objects(dynamic_objects)
self._dynamic_objects = dynamic_objects
# compute the state_count
count = 1
for obj in self.dynamic_objects:
s_cnt = len(obj.properties.radiance._states)
if s_cnt > count:
count = s_cnt
self._state_count = count
@property
def identifier(self):
"""Get a text string for the unique group identifier."""
return self._identifier
@property
def dynamic_objects(self):
"""Get a tuple of objects contained within the group."""
return self._dynamic_objects
@property
def state_count(self):
"""Gat an integer for the total number of states of the dynamic group.
This is equal to the number of states in the dynamic_object with the
highest number of states. After a dynamic_object with fewer states than that
of it's dynamic group has hit its highest state, it just remains in that
state as the other dynamic_objects continue to change.
"""
return self._state_count
@property
def is_opaque(self):
"""Get a boolean for whether all states of all shades in the group are opaque.
"""
for obj in self._dynamic_objects:
for state in obj.properties.radiance._states:
if not state.modifier.is_opaque:
return False
return True
@property
def is_indoor(self):
"""Get a boolean for whether all shades in the group are indoor."""
for obj in self._dynamic_objects:
if not obj.is_indoor:
return False
return True
@property
def states_json_list(self):
"""A list for the states.json file that notes the files for each state.
The files will follow a standard naming convention as follows.
<dynamic group identifier>..<field name>..<state count>.rad.
For instance skylight..direct..000.rad
"""
ident = self.identifier
states_list = []
for st_i in range(self.state_count):
states_list.append({
'identifier': '{}_{}'.format(st_i, ident),
'default': './{}..default..{}.rad'.format(ident, str(st_i)),
'direct': './{}..direct..{}.rad'.format(ident, str(st_i))
})
return states_list
[docs]
def states_by_index(self, state_index):
"""Get an array of state objects representing a single state for this group.
The resulting array will parallel the dynamic_objects of this group, with
one state object per dynamic object.
Args:
state_index: An integer from 0 up to the state_count - 1 , which notes
the state of the group for which a rad string will be produced.
"""
# make sure that the state_index is valid
assert 0 <= state_index < self.state_count, '"{}" is not a valid state index ' \
'for dynamic group "{}".'.format(state_index, self.identifier)
# gather the state objects that correspond to the state_index
states = []
for obj in self._dynamic_objects:
try: # try to get the state at the correct index
states.append(obj.properties.radiance._states[state_index])
except IndexError: # use the last state that is available
try:
states.append(obj.properties.radiance._states[-1])
except IndexError: # no states assigned; create default a dummy state
st = RadianceShadeState()
st._parent = obj
states.append(st)
return states
[docs]
def to_radiance(self, state_index, direct=False, minimal=False):
"""Generate a RAD string representation of a state for this group.
The resulting string includes everything going into a single .rad file
to simulate the state, including all geometry and modifiers.
Args:
state_index: An integer from 0 up to the state_count - 1 , which notes
the state of the group for which a rad string will be produced.
direct: Boolean to note whether to write the "direct" version of the
state. (Default: False)
minimal: Boolean to note whether the radiance string should be written
in a minimal format (with spaces instead of line breaks). Default: False.
"""
states = self.states_by_index(state_index)
# gather all unique modifiers across the geometry of the state
modifiers = []
if not direct:
for state in states:
mod = state.modifier
if not self._instance_in_array(mod, modifiers):
modifiers.append(mod)
for shd in state._shades:
mod = shd.modifier
if not self._instance_in_array(mod, modifiers):
modifiers.append(mod)
else: # use modifier_direct
for state in states:
mod = state.modifier_direct
if not self._instance_in_array(mod, modifiers):
modifiers.append(mod)
for shd in state._shades:
mod = shd.modifier_direct
if not self._instance_in_array(mod, modifiers):
modifiers.append(mod)
modifiers = list(set(modifiers))
# get rad strings for all modifier and geometry.
state_str = ['# STATE {} for "{}"'.format(state_index, self.identifier)]
for mod in modifiers:
if isinstance(mod, BSDF):
self._process_bsdf_modifier(mod, state_str, minimal)
else:
state_str.append(mod.to_radiance(minimal))
for state in states:
state_str.append(state.to_radiance(direct, minimal))
return '\n\n'.join(state_str)
@staticmethod
def _check_dynamic_objects(dynamic_objects):
for obj in dynamic_objects:
assert isinstance(obj, Shade), \
'Expected Shade for DynamicShadeGroup. Got {}.'.format(type(obj))
@staticmethod
def _process_bsdf_modifier(modifier, mod_strs, minimal):
"""Process a BSDF modifier for a radiance model folder."""
bsdf_name = os.path.split(modifier.bsdf_file)[-1]
mod_dup = modifier.duplicate() # duplicate to avoid editing the original
# the hidden _bsdf_file property is edited since the file has not yet been copied
mod_dup._bsdf_file = os.path.join('model', 'bsdf', bsdf_name)
mod_strs.insert(1, mod_dup.to_radiance(minimal))
@staticmethod
def _instance_in_array(object_instance, object_array):
"""Check if a specific object instance is already in an array.
This can be much faster than `if object_instance in object_array`
when you expect to be testing a lot of the same instance of an object for
inclusion in an array since the builtin method uses an == operator to
test inclusion.
"""
for val in object_array:
if val is object_instance:
return True
return False
def __repr__(self):
return 'Dynamic Shade Group: {}'.format(self.identifier)
[docs]
class DynamicSubFaceGroup(DynamicShadeGroup):
"""A group of dynamic Apertures and Doors that change states in unison.
Note that this object is not intended to set any Model properties. It is only
used by the Model to collect objects with the same dynamic_group_identifier
and export a common set of radiance files from them.
Args:
identifier: Text string for a unique Group ID. Must not contain spaces
or special characters. This will be used to identify the object across
a model and in the exported Radiance files.
dynamic_objects: An array of Shades that are contained within the group.
Properties:
* identifier
* dynamic_objects
* state_count
* is_opaque
* is_indoor
* states_json_list
"""
__slots__ = ()
def __init__(self, modifier, dynamic_objects):
"""Initialize DynamicSubFaceGroup."""
DynamicShadeGroup.__init__(self, modifier, dynamic_objects)
@property
def is_opaque(self):
"""Always False for dynamic sub-faces."""
return False
@property
def is_indoor(self):
"""Get a boolean for whether all sub-faces in the group have a Surface BC."""
for obj in self._dynamic_objects:
if not isinstance(obj.boundary_condition, Surface):
return False
return True
@property
def states_json_list(self):
"""A list for the states.json file that notes the files for each state.
The files will follow a standard naming convention as follows.
<dynamic group identifier>..<field name>..<state count>.rad.
For instance skylight..direct..000.rad
Note that this list does not contain the tmtx, vmtx, or dmtx keys
and these should be added separately if this states.json is to be used for
3-phase studies.
"""
ident = self.identifier
states_list = []
for st_i in range(self.state_count):
states_list.append({
'identifier': '{}_{}'.format(st_i, ident),
'default': './{}..default..{}.rad'.format(ident, str(st_i)),
'direct': './{}..direct..{}.rad'.format(ident, str(st_i)),
'black': './{}..black.rad'.format(ident)
})
return states_list
[docs]
def rfluxmtx_control_params(self, state_index=0, sampling=None, up_direction=None):
"""Create a formatted Rfluxmtx control parameters string.
The optional values are sampling and up_direction. If these values are not
provided they will be assigned based on the first State of the dynamic object.
The control params are only meaningful for BSDF modifiers with one of the
sampling types listed below.
Args:
sampling: Set hemisphere sampling type. Acceptable inputs for hemisphere
sampling type are:
* u for uniform.(Usually applicable for ground).
* kf for klems full.
* kh for klems half.
* kq for klems quarter.
* rN for Reinhart - Tregenza type skies. N stands for subdivisions
and defaults to 1.
* scN for shirley-chiu subdivisions.
Add a ``-`` in front of the input for left-handed coordinates. For more
information see rfluxmtx docs.
https://www.radiance-online.org/learning/documentation/manual-pages/pdfs/rfluxmtx.pdf/at_download/file
up_direction: Orient the "up" direction for the hemisphere using the
indicated axis or a direction vector. Valid inputs include the
following. [-]{X|Y|Z|ux,uy,uz}. (Default: Y).
Returns:
RfluxmtxControlParameters
"""
state = self.states_by_index(state_index)[0]
if up_direction is None:
up_direction = state.vmtx_geometry.plane.y
if sampling is None:
sampling = state.modifier.sampling_type
if sampling is None:
raise ValueError(
'Rfluxmtx control parameters can only be generated for states '
'with BSDF modifier. Current modifier: %s is a %s.' % (
state.modifier, self.modifier.__class__.__name__)
)
up_direction = tuple(up_direction)
return RfluxmtxControlParameters(sampling, up_direction)
[docs]
def states_by_index(self, state_index):
"""Get an array of state objects representing a single state for this group.
The resulting array will parallel the dynamic_objects of this group, with
one state object per dynamic object.
Args:
state_index: An integer from 0 up to the state_count - 1 , which notes
the state of the group for which a rad string will be produced.
"""
# make sure that the state_index is valid
assert 0 <= state_index < self.state_count, '"{}" is not a valid state index ' \
'for dynamic group "{}".'.format(state_index, self.identifier)
# gather the state objects that correspond to the state_index
states = []
for obj in self._dynamic_objects:
try: # try to get the state at the correct index
states.append(obj.properties.radiance._states[state_index])
except IndexError: # use the last state that is available
try:
states.append(obj.properties.radiance._states[-1])
except IndexError: # no states assigned; create default a dummy state
st = RadianceSubFaceState()
st._parent = obj
states.append(st)
return states
[docs]
def blk_to_radiance(self, minimal=False):
"""Generate a RAD string for the black representation of this group.
The resulting string includes everything going into the black .rad file,
including all geometry and modifiers.
Args:
minimal: Boolean to note whether the radiance string should be written
in a minimal format (with spaces instead of line breaks). Default: False.
"""
# gather all unique modifier_blk and write geometry rad strings
blk_str = ['# BLACK representation for "{}"'.format(self.identifier)]
modifiers = []
for obj in self._dynamic_objects:
mod = obj.properties.radiance.modifier_blk
base_poly = Polygon(obj.identifier, obj.vertices, mod)
blk_str.append(base_poly.to_radiance(minimal, False, False))
if not self._instance_in_array(mod, modifiers):
modifiers.append(mod)
modifiers = list(set(modifiers))
# get rad strings for all modifiers.
for mod in modifiers:
if isinstance(mod, BSDF):
self._process_bsdf_modifier(mod, blk_str, minimal)
else:
blk_str.insert(1, mod.to_radiance(minimal))
return '\n\n'.join(blk_str)
[docs]
def tmxt_bsdf(self, state_index):
"""A BSDF modifier representing the tranmission matrix of a state if it exists.
This will be None unless all of the objects in the group have the same
BSDF modifier for the state. Note that having a single tmxt_bsdf
is a requirement in order to be compatible with 3-phase and 5-phase
simulation.
Args:
state_index: An integer from 0 up to the state_count - 1 , which notes
the state of the group for which a rad string will be produced.
"""
bsdf_mods = []
for state in self.states_by_index(state_index):
mod = state.modifier
if not isinstance(mod, BSDF):
return None # not a BSDF; not valid for 3-phase
elif not self._instance_in_array(mod, bsdf_mods):
bsdf_mods.append(mod)
if len(set(bsdf_mods)) != 1:
return None # more than one type of BSDF; not valid for 3-phase
return bsdf_mods[0] # just one BSDF for the t_mtx; it's valid!
[docs]
def vmtx_to_radiance(self, state_index, minimal=False):
"""Generate a vmtx RAD string representation of a state.
The resulting string has all geometry geometry and the white_glow modifier.
Args:
state_index: An integer from 0 up to the state_count - 1 , which notes
the state of the group for which a rad string will be produced.
minimal: Boolean to note whether the radiance string should be written
in a minimal format (with spaces instead of line breaks). Default: False.
"""
states = self.states_by_index(state_index)
# create unique glow modifier for aperture group
unique_glow = white_glow.duplicate()
unique_glow.identifier = 'white_glow_{}'.format(self.identifier)
# get rad strings for the white_glow modifier and geometry.
state_str = ['# VMTX representation for "{}"'.format(self.identifier),
unique_glow.to_radiance(minimal)]
# use first state to add the header
header = self.rfluxmtx_control_params(state_index=state_index)
state_str.append(header.to_radiance())
for state in states:
state_str.append(state.vmtx_to_radiance(unique_glow, minimal))
return '\n\n'.join(state_str)
[docs]
def dmtx_to_radiance(self, state_index, minimal=False):
"""Generate a dmtx RAD string representation of a state.
The resulting string has all geometry geometry and the white_glow modifier.
Args:
state_index: An integer from 0 up to the state_count - 1 , which notes
the state of the group for which a rad string will be produced.
minimal: Boolean to note whether the radiance string should be written
in a minimal format (with spaces instead of line breaks). Default: False.
"""
states = self.states_by_index(state_index)
# get rad strings for the white_glow modifier and geometry.
state_str = ['# DMTX representation for "{}"'.format(self.identifier),
white_glow.to_radiance(minimal)]
# use first state to add the header
header = self.rfluxmtx_control_params()
state_str.append(header.to_radiance())
for state in states:
state_str.append(state.dmtx_to_radiance(minimal))
return '\n\n'.join(state_str)
@staticmethod
def _check_dynamic_objects(dynamic_objects):
for obj in dynamic_objects:
assert isinstance(obj, (Aperture, Door)), 'Expected Aperture or Door ' \
'for DynamicSubFaceGroup. Got {}.'.format(type(obj))
def __repr__(self):
return 'Dynamic SubFace Group: {}'.format(self.identifier)