# coding=utf-8
"""Object representing a single state for a dynamic object."""
from __future__ import division
from .stategeo import StateGeometry
from ..geometry import Polygon
from ..modifier import Modifier
from ..mutil import dict_to_modifier
from ..lib.modifiers import white_glow
from ladybug_geometry.geometry3d.face import Face3D
import math
class _RadianceState(object):
"""Object representing a single state for a dynamic object.
Args:
modifier: A Honeybee Radiance Modifier object to be applied to this state's
parent in this state. This is used to swap out the modifier in
multi-phase studies. If None, it will be the parent's default modifier.
shades: An optional array of StateGeometry objects to be included
with this state. The StateGeometry objects cannot already have
another parent state.
Properties:
* modifier
* shades
* modifier_direct
* parent
* has_parent
"""
__slots__ = ('_modifier', '_shades', '_modifier_direct', '_parent')
def __init__(self, modifier=None, shades=None):
"""Initialize RadianceState."""
self._parent = None # will be set when the state is assigned
self.modifier = modifier
self.shades = shades
self._modifier_direct = None
@property
def modifier(self):
"""Get or set the modifier to be applied to the parent in this state.
"""
if not self._modifier and self.has_parent: # use the parent's default
return self.parent.properties.radiance.modifier
return self._modifier
@modifier.setter
def modifier(self, value):
if value is not None:
assert isinstance(value, Modifier), \
'Expected Modifier for RadianceState. Got {}'.format(type(value))
value.lock() # lock editing in case modifier has multiple references
self._modifier = value
@property
def shades(self):
"""Get or set an array of StateGeometry objects to be included with this state.
The StateGeometry objects cannot already have another parent state.
"""
return tuple(self._shades)
@shades.setter
def shades(self, value):
if value is not None:
try:
self._shades = [self._check_shade(sh) for sh in value]
except (ValueError, TypeError):
raise TypeError('RadianceState shades must be an iterable. '
'Got {}.'.format(type(value)))
else:
self._shades = []
@property
def modifier_direct(self):
"""Get or set a modifier for the parent to be used in direct studies.
If None, it will be the same as the modifier of this state if that modifier
is nonopaque. Otherwise, it will be the modifier_blk of the parent.
"""
if not self._modifier_direct: # use the state's default modifier
mod = self.modifier
if mod is None or not mod.is_opaque:
return mod
return self._parent.properties.radiance.modifier_blk
return self._modifier_direct
@modifier_direct.setter
def modifier_direct(self, value):
if value is not None:
assert isinstance(value, Modifier), \
'Expected Modifier for RadianceState. Got {}'.format(type(value))
value.lock() # lock editing in case modifier has multiple references
self._modifier_direct = value
@property
def parent(self):
"""Get the parent of this State if assigned. None if not assigned."""
return self._parent
@property
def has_parent(self):
"""Get a boolean noting whether this State has a parent."""
return self._parent is not None
def remove_shades(self):
"""Remove all shades assigned to this object."""
for shade in self._shades:
shade._parent = None
self._shades = []
def add_shades(self, shades):
"""Add an array of Shade objects to this state.
Args:
shades: An array of Shade objects to add to the this state.
"""
for shade in shades:
self._shades.append(self._check_shade(shade))
def add_shade(self, shade):
"""Add a Shade object to this state.
Args:
shade: A Shade object to add to the this state.
"""
self._shades.append(self._check_shade(shade))
def move(self, moving_vec):
"""Move all shades assigned to this state along a vector.
Args:
moving_vec: A ladybug_geometry Vector3D with the direction and distance
to move the shades.
"""
for shd in self._shades:
shd.move(moving_vec)
def rotate(self, axis, angle, origin):
"""Rotate all shades assigned to this state.
Args:
axis: A ladybug_geometry Vector3D axis representing the axis of rotation.
angle: An angle for rotation in degrees.
origin: A ladybug_geometry Point3D for the origin around which the
object will be rotated.
"""
for shd in self._shades:
shd.rotate(axis, angle, origin)
def rotate_xy(self, angle, origin):
"""Rotate all shades counterclockwise in the world XY plane.
Args:
angle: An angle in degrees.
origin: A ladybug_geometry Point3D for the origin around which the
object will be rotated.
"""
for shd in self._shades:
shd.rotate_xy(angle, origin)
def reflect(self, plane):
"""Reflect all shades assigned to this state across a plane.
Args:
plane: A ladybug_geometry Plane across which the object will
be reflected.
"""
for shd in self._shades:
shd.reflect(plane)
def scale(self, factor, origin=None):
"""Scale all shades assigned to this state by a factor.
Args:
factor: A number representing how much the object should be scaled.
origin: A ladybug_geometry Point3D representing the origin from which
to scale. If None, it will be scaled from the World origin (0, 0, 0).
"""
for shd in self._shades:
shd.scale(factor, origin)
def to_radiance(self, direct=False, minimal=False):
"""Generate a RAD string representation of this state.
Note that the resulting string lacks modifiers but includes both the
parent geometry and the geometry of any shades.
Args:
direct: Boolean to note whether to write the "direct" version of the
state, which will have the modifier_direct applied to the parent
of the state and use the modifier_blk assigned to each of the
shades. (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.
"""
assert self.has_parent, 'State must have a parent to use to_radiance.'
modifier = self.modifier_direct if direct else self.modifier
base_poly = Polygon(self.parent.identifier, self.parent.vertices, modifier)
rad_strs = [base_poly.to_radiance(minimal, False, False)]
for shd in self._shades:
rad_strs.append(shd.to_radiance(direct, minimal))
return '\n\n'.join(rad_strs)
def duplicate(self):
"""Get a copy of this object."""
return self.__copy__()
def _check_shade(self, shade):
assert isinstance(shade, StateGeometry), \
'Expected StateGeometry for RadianceState. Got {}.'.format(type(shade))
assert shade.parent is None, \
'StateGeometry for a RadianceState cannot already have a parent object.'
shade._parent = self
return shade
def _duplicate_shades(self, new_state):
"""Add duplicated child shades to a duplicated new_state."""
new_state._shades = [shd.duplicate() for shd in self._shades]
for shd in new_state._shades:
shd._parent = new_state
def __copy__(self):
new_obj = _RadianceState(self._modifier)
self._duplicate_shades(new_obj)
new_obj._modifier_direct = self._modifier_direct
return new_obj
def ToString(self):
return self.__repr__()
def __repr__(self):
return 'State: ({})'.format(self.modifier.display_name) \
if self.modifier is not None else 'State: ()'
[docs]
class RadianceShadeState(_RadianceState):
"""Object representing a single state for a dynamic Shade.
Args:
modifier: A Honeybee Radiance Modifier object to be applied to this state's
parent in this state. This can be used to change the transmittance of
deciduous trees, change the modifier of a ground surface to account
for snow reflectance, etc. If None, it will be the parent's default
modifier.
shades: An optional array of StateGeometry objects to be included
with this state. The StateGeometry objects cannot already have
another parent state.
Properties:
* modifier
* shades
* modifier_direct
* parent
* has_parent
"""
__slots__ = ()
def __init__(self, modifier=None, shades=None):
"""Initialize RadianceShadeState."""
_RadianceState.__init__(self, modifier, shades)
[docs]
@classmethod
def from_dict(cls, data):
"""Create RadianceShadeState from a dictionary.
Note that the dictionary must be a non-abridged version for this
classmethod to work.
Args:
data: A dictionary representation of RadianceShadeState with the
format below.
.. code-block:: python
{
'type': 'RadianceShadeState',
'modifier': {}, # A Honeybee Radiance Modifier dictionary
'shades': [], # A list of StateGeometry dictionaries
'modifier_direct': {} # A Honeybee Radiance Modifier dictionary
}
"""
assert data['type'] == 'RadianceShadeState', \
'Expected RadianceShadeState. Got {}.'.format(data['type'])
new_state = cls()
if 'modifier' in data and data['modifier'] is not None:
new_state.modifier = dict_to_modifier(data['modifier'])
if 'shades' in data and data['shades'] is not None:
new_state.shades = [StateGeometry.from_dict(shd) for shd in data['shades']]
if 'modifier_direct' in data and data['modifier_direct'] is not None:
new_state.modifier_direct = dict_to_modifier(data['modifier_direct'])
return new_state
[docs]
@classmethod
def from_dict_abridged(cls, data, modifiers):
"""Create RadianceShadeState from an abridged dictionary.
Args:
data: A dictionary representation of RadianceShadeStateAbridged with
the format below.
modifiers: A dictionary of modifiers with modifier identifiers as keys,
which will be used to re-assign modifiers.
.. code-block:: python
{
'type': 'RadianceShadeStateAbridged',
'modifier': str, # An identifier of a honeybee-radiance modifier
'shades': [], # A list of abridged StateGeometry dictionaries
'modifier_direct': str # An identifier of a honeybee-radiance modifier
}
"""
assert data['type'] == 'RadianceShadeStateAbridged', \
'Expected RadianceShadeStateAbridged. Got {}.'.format(data['type'])
new_state = cls()
if 'modifier' in data and data['modifier'] is not None:
new_state.modifier = modifiers[data['modifier']]
if 'shades' in data and data['shades'] is not None:
new_state.shades = [StateGeometry.from_dict_abridged(shd, modifiers)
for shd in data['shades']]
if 'modifier_direct' in data and data['modifier_direct'] is not None:
new_state.modifier_direct = modifiers[data['modifier_direct']]
return new_state
[docs]
def to_dict(self, abridged=False):
"""Convert RadianceShadeState to a dictionary.
Args:
abridged: Boolean to note whether the full dictionary describing the
object should be returned (False) or just an abridged version (True).
Default: False.
"""
base = {'type': 'RadianceShadeStateAbridged'} if abridged else \
{'type': 'RadianceShadeState'}
if self._modifier:
base['modifier'] = self._modifier.identifier if abridged else \
self._modifier.to_dict()
if len(self._shades) != 0:
base['shades'] = [shd.to_dict(abridged=abridged) for shd in self.shades]
if self._modifier_direct is not None:
base['modifier_direct'] = self._modifier_direct.identifier if abridged \
else self._modifier.to_dict()
return base
def __copy__(self):
new_obj = RadianceShadeState(self._modifier)
self._duplicate_shades(new_obj)
new_obj._modifier_direct = self._modifier_direct
return new_obj
[docs]
class RadianceSubFaceState(_RadianceState):
"""Object representing a single state for a dynamic Aperture or Door.
Args:
modifier: A Honeybee Radiance Modifier object to be applied to this state's
parent in this state. This is used to swap out the modifier in
multi-phase studies. If None, it will be the parent's default modifier.
shades: An optional array of StateGeometry objects to be included
with this state. The StateGeometry objects cannot already have
another parent state.
Properties:
* modifier
* shades
* modifier_direct
* vmtx_geometry
* dmtx_geometry
* mtxs_default
* parent
* has_parent
"""
__slots__ = ('_vmtx_geometry', '_dmtx_geometry')
def __init__(self, modifier=None, shades=None):
"""Initialize RadianceSubFaceState."""
_RadianceState.__init__(self, modifier, shades)
self._vmtx_geometry = None
self._dmtx_geometry = None
@property
def modifier_direct(self):
"""Get or set a modifier for the parent to be used in direct studies.
If None, it will be the same as the modifier of this state. This property
is only used in 2-phase and 5-phase studies and should usually be left
as None in 2-phase studies. In 5-phase studies, this will be used for
the 5th phase.
"""
if not self._modifier_direct: # use the state's default modifier
return self.modifier
return self._modifier_direct
@modifier_direct.setter
def modifier_direct(self, value):
if value is not None:
assert isinstance(value, Modifier), \
'Expected Modifier for RadianceState. Got {}'.format(type(value))
value.lock() # lock editing in case modifier has multiple references
self._modifier_direct = value
@property
def vmtx_geometry(self):
"""Get or set a Face3D to be used for the inward-facing vmtx file.
If None, it will be a flipped (inward-facing) version of this state's parent.
Note that this property is only used in 3-phase and 5-phase studies and
its usual purpose is to account for thickness of the tmtx (BSDF) layer.
Also note that the gen_geo_from_vmtx_offset or gen_geos_from_tmtx_thickness
methods can be used to automatically generate this geometry without the
need to set it here.
"""
if not self._vmtx_geometry: # use the inward-version of the parent geometry
return self.parent.geometry.flip() if self.has_parent else None
return self._vmtx_geometry
@vmtx_geometry.setter
def vmtx_geometry(self, value):
if value is not None:
assert isinstance(value, Face3D), \
'Expected Face3D for RadianceSubFaceState vmtx_geometry. ' \
'Got {}'.format(type(value))
self._vmtx_geometry = value
@property
def dmtx_geometry(self):
"""Get or set a Face3D to be used for the outward-facing dmtx file.
If None, it will be a flipped (inward-facing) version of this state's parent.
Note that this property is only used in 3-phase and 5-phase studies and
its usual purpose is to account for thickness of the tmtx (BSDF) layer.
Also note that the gen_geo_from_dmtx_offset or gen_geos_from_tmtx_thickness
methods can be used to automatically generate this geometry without the
need to set it here.
"""
if not self._dmtx_geometry: # use the inward-version of the parent geometry
return self.parent.geometry.flip() if self.has_parent else None
return self._dmtx_geometry
@dmtx_geometry.setter
def dmtx_geometry(self, value):
if value is not None:
assert isinstance(value, Face3D), \
'Expected Face3D for RadianceSubFaceState dmtx_geometry. ' \
'Got {}'.format(type(value))
self._dmtx_geometry = value
@property
def mtxs_default(self):
"""Get a boolean noting whether the vmtx_geometry and dmtx_geometry are None.
This indicates that the vmtx_geometry and dmtx_geometry are both just a
flipped version of the parent geometry.
"""
return self._vmtx_geometry is None and self._dmtx_geometry is None
[docs]
def gen_geos_from_tmtx_thickness(self, thickness):
"""Auto-generate the vmtx_geometry and dmtx_geometry using a tmtx thickness.
Args:
thickness: A number for the thickness of the tmtx layer. The state's
vmtx_geometry will be set to the the parent geometry moved half
of this thickness inward. The dmtx_geometry will be set to the
parent geometry moved half of this thickness outward.
"""
assert self.has_parent, \
'State must have a parent to use gen_geos_from_tmtx_thickness.'
dist = thickness / 2
out_vec = self.parent.normal * dist
in_vec = out_vec.reverse()
base_geo = self.parent.geometry.flip()
self.dmtx_geometry = base_geo.move(out_vec)
self.vmtx_geometry = base_geo.move(in_vec)
[docs]
def gen_geo_from_vmtx_offset(self, offset):
"""Auto-generate the vmtx_geometry using an offset from the parent geometry.
Args:
offset: A number for the offset of the vmtx layer from the parent geometry.
"""
assert self.has_parent, \
'State must have a parent to use gen_geo_from_vmtx_offset.'
in_vec = self.parent.normal.reverse() * offset
base_geo = self.parent.geometry.flip()
self.vmtx_geometry = base_geo.move(in_vec)
[docs]
def gen_geo_from_dmtx_offset(self, offset):
"""Auto-generate the dmtx_geometry using an offset from the parent geometry.
Args:
offset: A number for the offset of the dmtx layer from the parent geometry.
"""
assert self.has_parent, \
'State must have a parent to use gen_geo_from_dmtx_offset.'
out_vec = self.parent.normal * offset
base_geo = self.parent.geometry.flip()
self.vmtx_geometry = base_geo.move(out_vec)
[docs]
def move(self, moving_vec):
"""Move all shades and mtx geometry assigned to this state along a vector.
Args:
moving_vec: A ladybug_geometry Vector3D with the direction and distance
to move the shades.
"""
for shd in self._shades:
shd.move(moving_vec)
if self._vmtx_geometry:
self._vmtx_geometry = self._vmtx_geometry.move(moving_vec)
if self._dmtx_geometry:
self._dmtx_geometry = self._dmtx_geometry.move(moving_vec)
[docs]
def rotate(self, axis, angle, origin):
"""Rotate all shades and mtx geometry assigned to this state.
Args:
axis: A ladybug_geometry Vector3D axis representing the axis of rotation.
angle: An angle for rotation in degrees.
origin: A ladybug_geometry Point3D for the origin around which the
object will be rotated.
"""
for shd in self._shades:
shd.rotate(axis, angle, origin)
if self._vmtx_geometry:
self._vmtx_geometry = self._vmtx_geometry.rotate(
axis, math.radians(angle), origin)
if self._dmtx_geometry:
self._dmtx_geometry = self._dmtx_geometry.rotate(
axis, math.radians(angle), origin)
[docs]
def rotate_xy(self, angle, origin):
"""Rotate all shades and mtx geometry counterclockwise in the world XY plane.
Args:
angle: An angle in degrees.
origin: A ladybug_geometry Point3D for the origin around which the
object will be rotated.
"""
for shd in self._shades:
shd.rotate_xy(angle, origin)
if self._vmtx_geometry:
self._vmtx_geometry = self._vmtx_geometry.rotate_xy(
math.radians(angle), origin)
if self._dmtx_geometry:
self._dmtx_geometry = self._dmtx_geometry.rotate_xy(
math.radians(angle), origin)
[docs]
def reflect(self, plane):
"""Reflect all shades and mtx geometry assigned to this state across a plane.
Args:
plane: A ladybug_geometry Plane across which the object will
be reflected.
"""
for shd in self._shades:
shd.reflect(plane)
if self._vmtx_geometry:
self._vmtx_geometry = self._vmtx_geometry.reflect(plane.n, plane.o)
if self._dmtx_geometry:
self._dmtx_geometry = self._dmtx_geometry.reflect(plane.n, plane.o)
[docs]
def scale(self, factor, origin=None):
"""Scale all shades and mtx geometry assigned to this state by a factor.
Args:
factor: A number representing how much the object should be scaled.
origin: A ladybug_geometry Point3D representing the origin from which
to scale. If None, it will be scaled from the World origin (0, 0, 0).
"""
for shd in self._shades:
shd.scale(factor, origin)
if self._vmtx_geometry:
self._vmtx_geometry = self._vmtx_geometry.scale(factor, origin)
if self._dmtx_geometry:
self._dmtx_geometry = self._dmtx_geometry.scale(factor, origin)
[docs]
def vmtx_to_radiance(self, modifier=white_glow, minimal=False):
"""Generate a RAD string representation of this state's vmtx.
The resulting string lacks modifiers and only includes the vmtx_geometry.
Args:
modifier: A modifier assigned to the vmtx_geometry. (Default: white_glow)
minimal: Boolean to note whether the radiance string should be written
in a minimal format (spaces instead of line breaks). (Default: False)
"""
assert self.has_parent, 'State must have a parent to use vmtx_to_radiance.'
vmtx_poly = Polygon(self.parent.identifier,
self.vmtx_geometry.vertices, modifier)
return vmtx_poly.to_radiance(minimal, False, False)
[docs]
def dmtx_to_radiance(self, minimal=False):
"""Generate a RAD string representation of this state's dmtx.
The resulting string lacks modifiers and only includes the dmtx_geometry.
Args:
minimal: Boolean to note whether the radiance string should be written
in a minimal format (with spaces instead of line breaks). Default: False.
"""
assert self.has_parent, 'State must have a parent to use dmtx_to_radiance.'
dmtx_poly = Polygon(self.parent.identifier,
self.dmtx_geometry.vertices, white_glow)
return dmtx_poly.to_radiance(minimal, False, False)
[docs]
@classmethod
def from_dict(cls, data):
"""Create RadianceSubFaceState from a dictionary.
Note that the dictionary must be a non-abridged version for this
classmethod to work.
Args:
data: A dictionary representation of RadianceSubFaceState with the
format below.
.. code-block:: python
{
'type': 'RadianceSubFaceState',
'modifier': {}, # A Honeybee Radiance Modifier dictionary
'shades': [], # A list of abridged StateGeometry dictionaries
'modifier_direct': {}, # A Honeybee Radiance Modifier dictionary
'vmtx_geometry': {}, # A Face3D for the view matrix geometry
'dmtx_geometry': {} # A Face3D for the daylight matrix geometry
}
"""
assert data['type'] == 'RadianceSubFaceState', \
'Expected RadianceSubFaceState. Got {}.'.format(data['type'])
new_state = cls()
if 'modifier' in data and data['modifier'] is not None:
new_state.modifier = dict_to_modifier(data['modifier'])
if 'shades' in data and data['shades'] is not None:
new_state.shades = [StateGeometry.from_dict(shd) for shd in data['shades']]
if 'modifier_direct' in data and data['modifier_direct'] is not None:
new_state.modifier_direct = dict_to_modifier(data['modifier_direct'])
if 'vmtx_geometry' in data and data['vmtx_geometry'] is not None:
new_state.vmtx_geometry = Face3D.from_dict(data['vmtx_geometry'])
if 'dmtx_geometry' in data and data['dmtx_geometry'] is not None:
new_state.dmtx_geometry = Face3D.from_dict(data['dmtx_geometry'])
return new_state
[docs]
@classmethod
def from_dict_abridged(cls, data, modifiers):
"""Create RadianceSubFaceState from an abridged dictionary.
Note that the dictionary must be a non-abridged version for this
classmethod to work.
Args:
data: A dictionary representation of RadianceSubFaceStateAbridged
with the format below.
modifiers: A dictionary of modifiers with modifier identifiers as keys,
which will be used to re-assign modifiers.
.. code-block:: python
{
'type': 'RadianceSubFaceStateAbridged',
'modifier': str, # An identifier of a honeybee-radiance modifier
'shades': [], # A list of abridged StateGeometry dictionaries
'modifier_direct': str, # An identifier of a honeybee-radiance modifier
'vmtx_geometry': {}, # A Face3D for the view matrix geometry
'dmtx_geometry': {} # A Face3D for the daylight matrix geometry
}
"""
assert data['type'] == 'RadianceSubFaceStateAbridged', \
'Expected RadianceSubFaceStateAbridged. Got {}.'.format(data['type'])
new_state = cls()
if 'modifier' in data and data['modifier'] is not None:
new_state.modifier = modifiers[data['modifier']]
if 'shades' in data and data['shades'] is not None:
new_state.shades = [StateGeometry.from_dict_abridged(shd, modifiers)
for shd in data['shades']]
if 'modifier_direct' in data and data['modifier_direct'] is not None:
new_state.modifier_direct = modifiers[data['modifier_direct']]
if 'vmtx_geometry' in data and data['vmtx_geometry'] is not None:
new_state.vmtx_geometry = Face3D.from_dict(data['vmtx_geometry'])
if 'dmtx_geometry' in data and data['dmtx_geometry'] is not None:
new_state.dmtx_geometry = Face3D.from_dict(data['dmtx_geometry'])
return new_state
[docs]
def to_dict(self, abridged=False):
"""Convert RadianceSubFaceState to a dictionary.
Args:
abridged: Boolean to note whether the full dictionary describing the
object should be returned (False) or just an abridged version (True).
Default: False.
"""
base = {'type': 'RadianceSubFaceStateAbridged'} if abridged else \
{'type': 'RadianceSubFaceState'}
if self._modifier:
base['modifier'] = self._modifier.identifier if abridged else \
self._modifier.to_dict()
if len(self._shades) != 0:
base['shades'] = [shd.to_dict(abridged=abridged) for shd in self.shades]
if self._modifier_direct is not None:
base['modifier_direct'] = self._modifier_direct.identifier if abridged \
else self._modifier.to_dict()
if self._vmtx_geometry is not None:
base['vmtx_geometry'] = self._vmtx_geometry.to_dict()
if self._dmtx_geometry is not None:
base['dmtx_geometry'] = self._dmtx_geometry.to_dict()
return base
def __copy__(self):
new_obj = RadianceSubFaceState(self._modifier)
self._duplicate_shades(new_obj)
new_obj._modifier_direct = self._modifier_direct
new_obj._vmtx_geometry = self._vmtx_geometry
new_obj._dmtx_geometry = self._dmtx_geometry
return new_obj