Source code for fairyfly_therm.simulation.exposure

# coding=utf-8
"""Model exposure parameters."""
from __future__ import division
import math
import xml.etree.ElementTree as ET

from ladybug_geometry.geometry3d import Vector3D

from fairyfly.typing import float_in_range


[docs] class ModelExposure(object): """Model exposure parameters. Args: model_type: Text to indicate the type of construction detail or the purpose of the model. Chose from the following. (Default: Other). * Window * Opaque Wall * Opaque Roof * Other cross_section_type: Text to indicate the type of cross section that the model represents. The acceptable fields here depend upon the model_type. If None, the default is set to the top of each list below for each model_type. For the Window model_type, choose from the following. * Sill * Jamb * Head * Horizontal Divider * Vertical Divider * Horizontal Meeting Rail * Vertical Meeting Rail * Common Frame * Spacer For the Opaque Wall and Roof model_type, choose from the following. * Sill Plate * Header * End Section * Middle Section * Thermal Bridge * Window Framing - Sill * Rough Opening - Header * Rough Opening - Jamb For the Other model_type, choose from the following. * General Cross Section * Common Thermal Bridge gravity_orientation: Text to indicate the direction of gravity for the model. If None, this will be computed from the orientation of the model plane in the 3D scene. (Default: None). * Down * Up * Left * Right * Into Screen * Out Of Screen wind_orientation: A number between 0 and 360 for the direction the exterior surface of the model faces. This orientation of the exterior surface is used to determine the wind direction for the simulation. For reference, 0 = North, 90 = East, 180 = South, 270 = West. (Default: 0). Properties: * model_type * cross_section_type * gravity_orientation * wind_orientation """ __slots__ = ('_model_type', '_cross_section_type', '_gravity_orientation', '_wind_orientation') MODEL_TYPES = ('Window', 'Opaque Wall', 'Opaque Roof', 'Other') WINDOW_SECTIONS = ( 'Sill', 'Jamb', 'Head', 'Horizontal Divider', 'Vertical Divider', 'Horizontal Meeting Rail', 'Vertical Meeting Rail', 'Common Frame', 'Spacer' ) OPAQUE_SECTIONS = ( 'Sill Plate', 'Header', 'End Section', 'Middle Section', 'Thermal Bridge', 'Window Framing - Sill', 'Rough Opening - Header', 'Rough Opening - Jamb' ) OTHER_SECTIONS = ( 'General Cross Section', 'Common Thermal Bridge', ) ALL_SECTIONS = WINDOW_SECTIONS + OPAQUE_SECTIONS + OTHER_SECTIONS GRAVITY_ORIENTATIONS = ( 'Down', 'Up', 'Left', 'Right', 'Into Screen', 'Out Of Screen' ) SECTION_MAP = { 'Window': WINDOW_SECTIONS[0], 'Opaque Wall': OPAQUE_SECTIONS[0], 'Opaque Roof': OPAQUE_SECTIONS[0], 'Other': OTHER_SECTIONS[0], } def __init__(self, model_type='Other', cross_section_type=None, gravity_orientation=None, wind_orientation=0): """Initialize ModelExposure.""" self._cross_section_type = None # default so that init can proceed self._gravity_orientation = None # default so that init can proceed self.model_type = model_type self.cross_section_type = cross_section_type self.gravity_orientation = gravity_orientation self.wind_orientation = wind_orientation @property def model_type(self): """Get or set text for the type of construction detail.""" return self._model_type @model_type.setter def model_type(self, value): if value is not None: clean_input = str(value).lower() for key in self.MODEL_TYPES: if key.lower() == clean_input: value = key break else: raise ValueError( 'ModelExposure model_type "{}" is not supported.\n' 'Choose from the following:\n{}'.format( value, '\n'.join(self.MODEL_TYPES))) self._model_type = value else: self._model_type = 'Other' self._check_model_and_cross_section() @property def cross_section_type(self): """Get or set text for the type of cross section.""" return self._cross_section_type if self._cross_section_type is not None \ else self.SECTION_MAP[self._model_type] @cross_section_type.setter def cross_section_type(self, value): if value is not None: clean_input = str(value).lower() for key in self.ALL_SECTIONS: if key.lower() == clean_input: value = key break else: raise ValueError( 'ModelExposure cross_section_type "{}" is not supported.\n' 'Choose from the following:\n{}'.format( value, '\n'.join(self.ALL_SECTIONS))) self._cross_section_type = value self._check_model_and_cross_section() @property def gravity_orientation(self): """Get or set text for the direction of gravity for the model.""" return self._gravity_orientation @gravity_orientation.setter def gravity_orientation(self, value): if value is not None: clean_input = str(value).lower() for key in self.GRAVITY_ORIENTATIONS: if key.lower() == clean_input: value = key break else: raise ValueError( 'ModelExposure gravity_orientation "{}" is not supported.\n' 'Choose from the following:\n{}'.format( value, '\n'.join(self.GRAVITY_ORIENTATIONS))) self._gravity_orientation = value @property def wind_orientation(self): """Get or set a number for the direction the exterior surface of the model faces. """ return self._wind_orientation @wind_orientation.setter def wind_orientation(self, value): self._wind_orientation = \ float_in_range(value, 0, 360, 'ModelExposure wind orientation') def _check_model_and_cross_section(self): """Check to be sure that the model and cross section type are compatible.""" # set up the base message templates msg_init = 'Cross section type "{}" is not supported for model ' \ 'type "{}".'.format(self.cross_section_type, self.model_type) msg_template = '{}\nChoose from the following:\n{}' # perform the checks on the inputs if self.cross_section_type is None: return # using None will always be supported if self.model_type == 'Other': assert self.cross_section_type in self.OTHER_SECTIONS, \ msg_template.format(msg_init, '\n'.join(self.OTHER_SECTIONS)) elif self.model_type == 'Window': assert self.cross_section_type in self.WINDOW_SECTIONS, \ msg_template.format(msg_init, '\n'.join(self.WINDOW_SECTIONS)) else: assert self.cross_section_type in self.OPAQUE_SECTIONS, \ msg_template.format(msg_init, '\n'.join(self.OPAQUE_SECTIONS))
[docs] @classmethod def from_therm_xml(cls, xml_element): """Create ModelExposure from an XML element of a THERM ModelExposure. Args: xml_element: An XML element of a THERM ModelExposure. """ xml_exp = xml_element.find('Exposure') purpose = xml_exp.find('ModelPurpose').text if purpose == 'Other': model_type = 'Other' cross_type = xml_exp.find('OtherCrossSection').text elif purpose == 'Window/Transparent Facade': model_type = 'Window' cross_type = xml_exp.find('WindowCrossSection').text else: model_type = 'Opaque {}'.format(xml_exp.find('Assembly').text) cross_type = xml_exp.find('OpaqueCrossSection').text xml_g_orient = xml_element.find('GravityOrientation') xml_w_orient = xml_element.find('ModelOrientation') return ModelExposure( model_type, cross_type, xml_g_orient.text, xml_w_orient.text)
[docs] @classmethod def from_therm_xml_str(cls, xml_str): """Create a ModelExposure from an XML text string of a THERM ModelExposure. Args: xml_str: An XML text string of a THERM ModelExposure. """ root = ET.fromstring(xml_str) return cls.from_therm_xml(root)
[docs] @classmethod def from_dict(cls, data): """Create a ModelExposure from a dictionary. Args: data: A python dictionary in the following format .. code-block:: python { "type": 'ModelExposure', "model_type": 'Window', "cross_section_type": 'Sill', "gravity_orientation": 'Down', "wind_orientation": 0 } """ assert data['type'] == 'ModelExposure', \ 'Expected ModelExposure. Got {}.'.format(data['type']) m_type = data['model_type'] if 'model_type' in data and \ data['model_type'] is not None else 'Other' c_type = data['cross_section_type'] if 'cross_section_type' in data and \ data['cross_section_type'] is not None else None g_orient = data['gravity_orientation'] if 'gravity_orientation' in data and \ data['gravity_orientation'] is not None else None w_orient = data['wind_orientation'] if 'wind_orientation' in data else 0 return cls(m_type, c_type, g_orient, w_orient)
[docs] def to_therm_xml(self, properties_element=None, model_plane=None): """Get an THERM XML element of the ModelExposure. Args: properties_element: An optional XML Element for the ThermModel Properties to which the generated objects will be added. If None, a new XML Element will be generated. (Default: None). model_plane: An optional ladybug-geometry Plane object noting the orientation of the model in 3D space. This will be used to set the default gravity orientation if none has been specified here. (Default: None). .. code-block:: xml <ModelExposure> <ModelOrientation>0</ModelOrientation> <GravityOrientation>Down</GravityOrientation> <Exposure> <ModelPurpose>Window/Transparent Facade</ModelPurpose> <WindowCrossSection>Sill</WindowCrossSection> </Exposure> </ModelExposure> """ # create a new Materials element if one is not specified if properties_element is not None: xml_exp = ET.SubElement(properties_element, 'ModelExposure') else: xml_exp = ET.Element('ModelExposure') # set the wind and gravity orientation xml_w_ornt = ET.SubElement(xml_exp, 'ModelOrientation') xml_w_ornt.text = str(self.wind_orientation) xml_g_ornt = ET.SubElement(xml_exp, 'GravityOrientation') if self.gravity_orientation is None: if model_plane is not None: plane_vec = model_plane.n if model_plane.n.z >= 0 else \ model_plane.n.reverse() up_ang = plane_vec.angle(Vector3D(0, 0, 1)) xml_g_ornt.text = 'Into Screen' if math.degrees(up_ang) < 45 else 'Down' else: xml_g_ornt.text = 'Down' else: xml_g_ornt.text = self.gravity_orientation # set the model and cross-section types xml_e = ET.SubElement(xml_exp, 'Exposure') xml_mp = ET.SubElement(xml_e, 'ModelPurpose') if self.model_type == 'Window': xml_mp.text = 'Window/Transparent Facade' xml_cs = ET.SubElement(xml_e, 'WindowCrossSection') elif self.model_type == 'Opaque Wall': xml_mp.text = 'Opaque Facade' xml_at = ET.SubElement(xml_e, 'Assembly') xml_at.text = 'Walls' xml_cs = ET.SubElement(xml_e, 'OpaqueCrossSection') elif self.model_type == 'Opaque Roof': xml_mp.text = 'Opaque Facade' xml_at = ET.SubElement(xml_e, 'Assembly') xml_at.text = 'Roof' xml_cs = ET.SubElement(xml_e, 'OpaqueCrossSection') else: xml_mp.text = self.model_type xml_cs = ET.SubElement(xml_e, 'OtherCrossSection') xml_cs.text = self.cross_section_type return xml_exp
[docs] def to_therm_xml_str(self): """Get an THERM XML string of the material.""" xml_root = self.to_therm_xml() try: # try to indent the XML to make it read-able ET.indent(xml_root) return ET.tostring(xml_root, encoding='unicode') except AttributeError: # we are in Python 2 and no indent is available return ET.tostring(xml_root)
[docs] def to_dict(self): """ModelExposure dictionary representation.""" base = { 'type': 'ModelExposure', 'model_type': self.model_type, 'wind_orientation': self.wind_orientation } if self._cross_section_type is not None: base['cross_section_type'] = self.cross_section_type if self._gravity_orientation is not None: base['gravity_orientation'] = self.gravity_orientation return base
[docs] def duplicate(self): """Get a copy of this object.""" return self.__copy__()
[docs] def ToString(self): """Overwrite .NET ToString.""" return self.__repr__()
def __key(self): """A tuple based on the object properties, useful for hashing.""" return (self._model_type, self._cross_section_type, self._gravity_orientation, self._wind_orientation) def __hash__(self): return hash(self.__key()) def __eq__(self, other): return isinstance(other, ModelExposure) and self.__key() == other.__key() def __ne__(self, other): return not self.__eq__(other) def __copy__(self): return self.__class__( self._model_type, self._cross_section_type, self._gravity_orientation, self._wind_orientation) def __repr__(self): return 'ModelExposure: {} - {}'.format(self.model_type, self.cross_section_type)