"""Base class for Radiance Primitives.
Unless you are a developer you most likely want to use one of the subclasses of
Primitive instead of using this class directly. Look under honeybee_plus.radiance.material
and honeybee_plus.radiance.geometry
http://radsite.lbl.gov/radiance/refer/ray.html
"""
from ..utilcol import check_name
from .radparser import parse_from_string
try:
from .factory import primitive_from_string
except ImportError:
# circular import
pass
try:
from .factory import primitive_from_json
except ImportError:
# circular import
pass
[docs]class Void(object):
"""Void modifier."""
@property
def name(self):
"""Void."""
return 'void'
@property
def can_be_modifier(self):
"""True."""
return True
@property
def is_opaque(self):
"""False for a void."""
return True
[docs] def to_rad_string(self):
"""Return full radiance definition."""
return 'void'
[docs] def to_json(self):
"""Return void."""
return self.to_rad_string()
[docs] def ToString(self):
"""Overwrite .NET ToString."""
return self.to_rad_string()
def __repr__(self):
return self.to_rad_string()
[docs]class Primitive(object):
"""Base class for Radiance Primitives.
Attributes:
name: Primitive name as a string. Do not use white space and special character.
type: One of Radiance standard Primitive types (e.g. glass, plastic, etc)
modifier: Modifier. It can be primitive, mixture, texture or pattern.
(Default: "void").
values: A dictionary of primitive data. key is line number and item is the list
of values {0: [], 1: [], 2: ['0.500', '0.500', '0.500', '0.000', '0.050']}
"""
# list of Radiance material types
MATERIALTYPES = \
set(('plastic', 'glass', 'trans', 'metal', 'mirror', 'illum',
'mixedfunc', 'dielectric', 'transdata', 'light', 'glow', 'BSDF',
'void', 'spotlight', 'prism1', 'prism2', 'mist', 'plastic2',
'metal2', 'trans2', 'ashik2', 'dielectric', 'interface',
'plasfunc', 'metfunc', 'transfunc', 'BRTDfunc',
'plasdata', 'metdata', 'transdata', 'antimatter'))
TEXTURETYPES = set(('texfunc', 'texdata'))
# list of Radiance geometry types
GEOMETRYTYPES = set(('source', 'sphere', 'bubble', 'polygon', 'cone', 'cup',
'cylinder', 'tube', 'ring', 'instance', 'mesh'))
PATTERNTYPES = set(('colorfunc', 'brightfunc', 'colordata', 'brightdata',
'colorpict', 'colortext', 'brighttext'))
MIXTURETYPES = set(('mixfunc', 'mixdata', 'mixpict', 'mixtext'))
TYPES = set().union(MATERIALTYPES, TEXTURETYPES, GEOMETRYTYPES, PATTERNTYPES,
MIXTURETYPES)
# Materials, mixtures, textures or patterns
MODIFIERTYPES = set().union(MATERIALTYPES, MIXTURETYPES, TEXTURETYPES, PATTERNTYPES)
# Materials that are not usually opaque. This will be used to set is_opaque property
# is it's not provided by user and can be overwritten by setting the value for
# is_opaque
NONEOPAQUETYPES = set(('glass', 'trans', 'trans2', 'transdata', 'transfunc',
'dielectric', 'BSDF', 'mixfunc', 'BRTDfunc', 'mist',
'prism1', 'prism2'))
def __init__(self, name, type, modifier=None, values=None, is_opaque=None):
"""Create primitive base."""
self.name = name
self.type = type
self.modifier = modifier
self.values = values or {0: [], 1: [], 2: []}
self._is_opaque = is_opaque
[docs] @classmethod
def from_string(cls, primitive_string, modifier=None):
"""Create a Radiance primitive from a string.
If the primitive has a modifier the modifier primitive should also be part of the
string or should be provided using modifier argument.
"""
primitive_type = cls._get_string_type(primitive_string)
modifier, name, base_data = cls._analyze_string_input(
None, primitive_string, modifier)
count_1 = int(base_data[0])
count_2 = int(base_data[count_1 + 1])
count_3 = int(base_data[count_1 + count_2 + 2])
l1 = [] if count_1 == 0 else base_data[1: count_1 + 1]
l2 = [] if count_2 == 0 \
else base_data[count_1 + 2: count_1 + count_2 + 2]
l3 = [] if count_3 == 0 \
else base_data[count_1 + count_2 + 3: count_1 + count_2 + count_3 + 3]
values = {0: l1, 1: l2, 2: l3}
if cls.__class__.__name__ == 'Primitive':
return cls(name, primitive_type, modifier, values)
else:
# subclass - type will be assigned based on name
return cls(name, modifier, values)
[docs] @classmethod
def from_json(cls, mat_json):
"""Make radiance primitive from json
{
"modifier": "", // primitive modifier (Default: "void")
"type": "custom", // primitive type
"base_type": "type", // primitive type
"name": "", // primitive Name
"values": {} // values
}
"""
modifier = cls._analyze_json_input(cls.__name__.lower(), mat_json)
if cls.__class__.__name__ == 'Primitive':
return cls(name=mat_json["name"],
type=mat_json["type"],
modifier=modifier,
values=mat_json["values"])
else:
# subclass - type will be assigned based on name
return cls(name=mat_json["name"],
modifier=modifier,
values=mat_json["values"])
@property
def values(self):
self._update_values()
return self._values
@values.setter
def values(self, new_values):
"""Modify values for the current primitive.
Args:
new_values: New values as a dictionary. The keys should be between 0 and 2.
Usage:
# This line will assign 9 values to line 0 of the primitive
primitive.values = {0: ["0.5", "0.5", "0.5",
"/usr/local/lib/ray/oakfloor.pic", ".", "frac(U)",
"frac(V)", "-s", "1.1667"]}
"""
self._values = {}
for line_count, value in new_values.items():
assert 0 <= line_count <= 2, ValueError(
'Illegal input: {}. Key values must be between 0-2.'.format(line_count)
)
self._values[line_count] = value
@property
def isRadiancePrimitive(self):
"""Indicate that this object is a Radiance primitive."""
return True
@property
def isRadianceGeometry(self):
"""Indicate if this object is a Radiance geometry."""
return False
@property
def isRadianceTexture(self):
"""Indicate if this object is a Radiance geometry."""
return False
@property
def isRadiancePattern(self):
"""Indicate if this object is a Radiance geometry."""
return False
@property
def isRadianceMixture(self):
"""Indicate if this object is a Radiance geometry."""
return False
@property
def isRadianceMaterial(self):
"""Indicate if this object is a Radiance material."""
return False
@property
def can_be_modifier(self):
"""Indicate if this object can be a modifier.
Materials, mixtures, textures or patterns can be modifiers.
"""
if self.type in self.MODIFIERTYPES:
return True
return False
@property
def name(self):
"""Get/set primitive name."""
return self._name
@name.setter
def name(self, name):
if self.isRadianceMaterial:
assert name not in self.MATERIALTYPES, \
'%s is a radiance primitive type and' \
' should not be used as a material name.' % name
elif self.isRadianceGeometry:
assert name not in self.GEOMETRYTYPES, \
'%s is a radiance geometry type and' \
' should not be used as a geometry name.' % name
self._name = name.rstrip()
check_name(self._name)
@property
def modifier(self):
"""Get/set primitive modifier."""
return self._modifier
@modifier.setter
def modifier(self, modifier):
if not modifier or modifier == 'void':
self._modifier = Void()
else:
if not hasattr(modifier, 'can_be_modifier'):
raise TypeError('Invalid modifier: %s' % modifier)
assert modifier.can_be_modifier, \
'A {} cannot be a modifier. Modifiers can be Materials, mixtures, ' \
'textures or patterns'.format(type(modifier))
self._modifier = modifier
@property
def type(self):
"""Get/set primitive type."""
return self._type
@type.setter
def type(self, type):
_mapper = {'bsdf': 'BSDF', 'brtdfunc': 'BRTDfunc'}
if type not in self.TYPES:
# try base classes for subclasses
for base in self.__class__.__mro__:
if base.__name__.lower() in _mapper:
type = _mapper[base.__name__.lower()]
break
if base.__name__.lower() in self.TYPES:
type = base.__name__.lower()
break
assert type in self.TYPES, \
"%s is not a supported primitive type." % type + \
"Try one of these primitives:\n%s" % str(self.TYPES)
self._type = type
@property
def is_opaque(self):
"""Indicate if the primitive is opaque.
This property is used to separate opaque and non-opaque surfaces.
"""
if self._is_opaque:
return self._is_opaque
elif self.type in self.NONEOPAQUETYPES:
# none opaque material
self._is_opaque = False
return self._is_opaque
else:
# check modifier for surfaces
return self.modifier.is_opaque
@is_opaque.setter
def is_opaque(self, is_opaque):
self._is_opaque = bool(is_opaque)
@staticmethod
def _get_string_type(input_string):
"""This is a helper function for from_string classmethod.
Args:
input_string: Radiance input string
Returns:
primitive type.
"""
input_objects = parse_from_string(input_string)
if not input_objects:
raise ValueError(
'{} includes no radiance primitives.'.format(input_string)
)
bm = input_objects[-1]
bm_data = bm.split()
bm_type = bm_data[1]
return bm_type
@staticmethod
def _analyze_string_input(desired_type, input_string, modifier):
"""This is a helper function for from_string classmethod.
Args:
desired_type: Desired type of base modifier as a string (e.g. plastic).
input_string: Radiance input string
modifier: A radiance modifier for base input string.
Returns:
modifier, name, modifier data as a list
"""
input_objects = parse_from_string(input_string)
if not input_objects:
raise ValueError(
'{} includes no radiance primitives.'.format(input_string)
)
bm = input_objects[-1]
bm_data = bm.split()
bm_modifier = bm_data[0]
bm_type = bm_data[1]
bm_name = bm_data[2]
if desired_type:
# custom primitives won't have desired_type
assert bm_type == desired_type, \
'{} is a {} not a {}.'.format(bm, bm_type, desired_type)
if len(input_objects) == 1:
# There is only one primitive ensure that modifier is void
if bm_modifier != 'void':
assert modifier, \
'{} has a modifier: "{}" which is not provided.'.format(
bm_data[2], bm_modifier
)
assert modifier.can_be_modifier, \
'{} cannot be a modifier!'.format(modifier)
assert modifier.name == bm_modifier, \
'Illegal modifier. Expected {} got {}'.format(bm_modifier,
modifier.name)
else:
modifier == bm_modifier
if modifier != 'void' and len(input_objects) > 1:
# create modifier if any
modifier_primitives = '\n'.join(input_objects[:-1])
try:
modifier = primitive_from_string(modifier_primitives)
except UnboundLocalError:
# circular import
from .factory import primitive_from_string
modifier = primitive_from_string(modifier_primitives)
return modifier, bm_name, bm_data[3:]
@staticmethod
def _analyze_json_input(desired_type, input_json):
"""This is a helper function for from_json classmethod.
Args:
desired_type: Desired type of base modifier as a string (e.g. plastic).
input_json: Radiance input as a dictionary.
Returns:
modifier as a Honeybee Radiance primitive.
"""
if not input_json:
raise ValueError('{} includes no radiance primitives.'.format(input_json))
bm_data = input_json
bm_modifier = bm_data['modifier']
bm_type = bm_data['type']
if desired_type:
# custom primitives won't have desired_type
assert bm_type == desired_type, \
'{} is a {} not a {}.'.format(bm_data, bm_type, desired_type)
if bm_modifier != 'void':
# create modifier if any
try:
modifier = primitive_from_json(bm_modifier)
except UnboundLocalError:
# circular import
from .factory import primitive_from_json
modifier = primitive_from_json(bm_modifier)
else:
modifier = Void()
return modifier
def _update_values(self):
"""update value dictionaries.
_update_values must be implemented under subclasses.
"""
pass
[docs] def head_line(self, minimal=False, include_modifier=True):
"""Return first line of primitive definition.
If primitive has a modifier it returns the modifier definition as well.
"""
if self.modifier.name == 'void':
return "void %s %s\n" % (self.type, self.name)
if include_modifier:
# include modifier primitive in definition
modifier = self.modifier.to_rad_string(minimal)
return "%s\n%s %s %s\n" % (modifier, self.modifier.name, self.type,
self.name)
else:
return "%s %s %s\n" % (self.modifier.name, self.type, self.name)
# add string format for float values
[docs] def to_rad_string(self, minimal=False, include_modifier=True):
"""Return full radiance definition."""
output = [self.head_line(minimal, include_modifier).strip()]
for line_count in range(3):
try:
values = (str(v) for v in self.values[line_count])
except BaseException:
values = [] # line will be printed as 0
else:
count = len(self.values[line_count])
line = '%d %s' % (count, " ".join(values).rstrip())
output.append(' '.join(line.split()))
return " ".join(output) if minimal else "\n".join(output)
[docs] def to_json(self):
"""Translate radiance primitive to json
{
"modifier": "", // primitive modifier (Default: "void")
"type": "custom", // primitive type
"base_type": "type", // primitive type
"name": "", // primitive Name
"values": {} // values
}
"""
return {
"modifier": self.modifier.to_json(),
"type": self.type,
"name": self.name,
"values": self.values
}
[docs] def ToString(self):
"""Overwrite .NET ToString."""
return self.__repr__()
def __repr__(self):
"""Return primitive definition."""
return self.to_rad_string()