Source code for fairyfly_therm.result

# coding=utf-8
"""Module for parsing THERM results."""
from __future__ import division

import os
import zipfile
import json
import re
import xml.etree.ElementTree as ET

from ladybug_geometry.geometry2d import Vector2D, Point2D, Polygon2D
from ladybug_geometry.geometry3d import Plane, Mesh3D, Face3D


[docs] class THMZResult(object): """Object for parsing results out of simulated THMZ files. Args: file_path: Full path to a THMZ file that was simulated using THERM. Properties: * file_path * u_factors * plane * shape_polygons * shape_faces * mesh * temperatures * heat_fluxes * heat_flux_magnitudes """ def __init__(self, file_path): """Initialize THMZResult.""" assert os.path.isfile(file_path), 'No file was found at {}'.format(file_path) assert file_path.lower().endswith('.thmz'), \ '"{}" is not a THMZ file ending in .thmz.'.format(file_path) self._file_path = file_path # set up variables to track what has been loaded from the file self._plane_loaded = False self._mesh_loaded = False self._faces_loaded = False self._u_factors_loaded = False self._ss_mesh_results_loaded = False # values to be computed as soon as they are requested self._plane = None self._translation_vec = None self._shape_polygons = None self._shape_faces = None self._u_factors = None self._mesh = None self._disconnected_i = None self._temperatures = None self._heat_fluxes = None self._heat_flux_magnitudes = None @property def file_path(self): """Get the path to the .thmz file.""" return self._file_path @property def u_factors(self): """Get a tuple of UFactor objects for the . This will be None if there is no U-Factor information in the THMZ file and this will be an empty tuple if the model had no U-Factor tags assigned to it. """ if not self._u_factors_loaded: self._extract_u_factors() return self._u_factors @property def plane(self): """Get a ladybug-geometry Plane for the 3D plane in which the mesh exists. This will be the World XY plane if there is no plane information in the THMZ file. """ if not self._plane_loaded: self._extract_plane() return self._plane @property def shape_polygons(self): """Get a ladybug-geometry Polygon2Ds for the Shape geometries in the THMZ file. """ if not self._plane_loaded: self._extract_plane() return self._shape_polygons @property def shape_faces(self): """Get a ladybug-geometry Face3Ds for the Shape geometries in the THMZ file. """ if not self._faces_loaded: self._extract_faces() return self._shape_faces @property def mesh(self): """Get a ladybug-geometry Mesh3D for the finite element mesh of the model. Will be None if the THMZ file has not been simulated and there is no Mesh in the file. """ if not self._mesh_loaded: self._extract_mesh() return self._mesh @property def temperatures(self): """Get a tuple of temperatures in Celsius that correspond to the mesh vertices. Will be None if the THMZ file has not been simulated and there are no steady state results in the file. """ if not self._ss_mesh_results_loaded: self._extract_steady_state_results() return self._temperatures @property def heat_fluxes(self): """Get a tuple of Vector2Ds that correspond to the mesh vertices for heat fluxes. Will be None if the THMZ file has not been simulated and there are no steady state results in the file. """ if not self._ss_mesh_results_loaded: self._extract_steady_state_results() return self._heat_fluxes @property def heat_flux_magnitudes(self): """Get a tuple of heat flux values in W/m2 in that correspond to mesh vertices. Will be None if the THMZ file has not been simulated and there are no steady state results in the file. """ if not self._ss_mesh_results_loaded: self._extract_steady_state_results() return self._heat_flux_magnitudes def _extract_u_factors(self): """Extract U-factor results from the THMZ file.""" self._u_factors_loaded = True try: # load the root of the SteadyStateResults.xml with zipfile.ZipFile(self.file_path, 'r') as archive: with archive.open('SteadyStateResults.xml') as f: # Read content as bytes and decode to a string content = f.read().decode('utf-8') except KeyError: # no results in the file; it has not been simulated return # extract the temperatures and heat fluxes from the model xml_root = ET.fromstring(content) xml_case = xml_root.find('Case') u_factors = [] for xml_u_fac in xml_case: if xml_u_fac.tag == 'U-factors': u_factors.append(UFactor(xml_u_fac)) self._u_factors = tuple(u_factors) def _extract_plane(self): """Extract a Plane object and Polygons from the THMZ file.""" self._plane_loaded = True # load the root of the Model.xml with zipfile.ZipFile(self.file_path, 'r') as archive: with archive.open('Model.xml') as f: # Read content as bytes and decode to a string content = f.read().decode('utf-8') xml_root = ET.fromstring(content) # extract the Plane specification form the model xml_props = xml_root.find('Properties') xml_gen = xml_props.find('General') xml_notes = xml_gen.find('Notes') all_notes = xml_notes.text if all_notes is not None: _plane_pattern = re.compile(r"Plane:\s(.*)") matches = _plane_pattern.findall(all_notes) if len(matches) > 0: self._plane = Plane.from_dict(json.loads(matches[0])) else: self._plane = Plane() else: self._plane = Plane() # extract the shape geometries as Polygon2Ds shape_geos = [] xml_shapes = xml_root.find('Polygons') for xml_shape in xml_shapes: vertices = [] for xpt in xml_shape.find('Points'): vertices.append(Point2D(xpt.find('x').text, xpt.find('y').text)) shape_geos.append(Polygon2D(vertices)) self._shape_polygons = tuple(shape_geos) # extract the translation vector from the glazing origin xml_origin = xml_root.find('GlazingOrigin') xml_o_x = xml_origin.find('x') xml_o_y = xml_origin.find('y') self._translation_vec = Vector2D(float(xml_o_x.text), float(xml_o_y.text)) def _extract_mesh(self): """Extract a Mesh3D object from the THMZ file.""" self._mesh_loaded = True try: # load the root of the Mesh.xml with zipfile.ZipFile(self.file_path, 'r') as archive: with archive.open('Mesh.xml') as f: # Read content as bytes and decode to a string content = f.read().decode('utf-8') except KeyError: # no mesh in the file; it has not been simulated return # get the mesh information from the root xml_root = ET.fromstring(content) xml_case = xml_root.find('Case') # extract the vertices (aka. nodes) from the model plane = self.plane vertices_2d = [] for xml_node in xml_case.find('Nodes'): pt_2d = Point2D( float(xml_node.find('x').text) * 1000, float(xml_node.find('y').text) * 1000 ) # convert from meters back to mm vertices_2d.append(pt_2d) # move the vertices to be in 3D Fairyfly space instead of 2D THERM space vertices = [] for pt2 in vertices_2d: vertices.append(plane.xy_to_xyz(pt2.move(self._translation_vec))) # extract the faces (aka. elements) from the model faces, pt_set = [], set() for xml_face in xml_case.find('Elements'): face_i = [] for e_prop in xml_face: if e_prop.tag.startswith('node'): fi = int(e_prop.text) face_i.append(fi) pt_set.add(fi) faces.append(tuple(face_i)) # remove any vertices that do not connect to a face self._disconnected_i = [] for i in range(len(vertices)): if i not in pt_set: self._disconnected_i.append(i) self._disconnected_i = list(reversed(self._disconnected_i)) for ri in self._disconnected_i: vertices.pop(ri) self._mesh = Mesh3D(vertices, faces) def _extract_faces(self): """Extract Face3D object from the THMZ file.""" self._faces_loaded = True polygons, plane = self.shape_polygons, self.plane faces = [] for polygon in polygons: vertices = [plane.xy_to_xyz(pt2) for pt2 in polygon] faces.append(Face3D(vertices, plane)) self._shape_faces = tuple(faces) def _extract_steady_state_results(self): """Extract steady state results from the THMZ file.""" self.mesh # extract the mesh to coordinate results self._ss_mesh_results_loaded = True try: # load the root of the SteadyStateMeshResults.xml with zipfile.ZipFile(self.file_path, 'r') as archive: with archive.open('SteadyStateMeshResults.xml') as f: # Read content as bytes and decode to a string content = f.read().decode('utf-8') except KeyError: # no results in the file; it has not been simulated return # extract the temperatures and heat fluxes from the model xml_root = ET.fromstring(content) xml_case = xml_root.find('Case') temperatures, heat_fluxes, flux_magnitudes = [], [], [] for xml_node in xml_case.find('Nodes'): temperatures.append(float(xml_node.find('Temperature').text)) vec_2d = Vector2D(xml_node.find('X-flux').text, xml_node.find('Y-flux').text) heat_fluxes.append(vec_2d) flux_magnitudes.append(vec_2d.magnitude) # remove the last two vertices (I don't know where they come from) for res_list in (temperatures, heat_fluxes, flux_magnitudes): for ri in self._disconnected_i: res_list.pop(ri) self._temperatures = tuple(temperatures) self._heat_fluxes = tuple(heat_fluxes) self._heat_flux_magnitudes = tuple(flux_magnitudes)
[docs] def ToString(self): """Overwrite .NET ToString.""" return self.__repr__()
def __repr__(self): return 'THMZ Result: {}'.format(self.file_path)
[docs] class UFactor(object): """Object for holding the results of an individual U-factor tag. Args: xml_element: An XML element for a U-factor result in the SteadyStateResults.xml file. Properties: * name * delta_temperature * heat_flux * total_u_factor * total_length * projected_x_u_factor * projected_x_length * projected_y_u_factor * projected_y_length * projected_in_glass_plane_u_factor * projected_in_glass_plane_length * custom_rotation_u_factor * custom_rotation_length """ __slots__ = ( '_name', '_delta_temperature', '_heat_flux', '_total_u_factor', '_total_length', '_projected_x_u_factor', '_projected_x_length', '_projected_y_u_factor', '_projected_y_length', '_projected_in_glass_plane_u_factor', '_projected_in_glass_plane_length', '_custom_rotation_u_factor', '_custom_rotation_length') def __init__(self, xml_element): """Initialize UFactor.""" # get the basic properties like the name and heat flux self._name = xml_element.find('Tag').text self._delta_temperature = float(xml_element.find('DeltaT').text) self._heat_flux = float(xml_element.find('HeatFlux').text) # set defaults for the different U-factors in case they are not found self._total_u_factor = None self._total_length = 0 self._projected_x_u_factor = None self._projected_x_length = 0 self._projected_y_u_factor = None self._projected_y_length = 0 self._projected_in_glass_plane_u_factor = None self._projected_in_glass_plane_length = 0 self._custom_rotation_u_factor = None self._custom_rotation_length = 0 # extract the different types of U-factors for xml_project in xml_element: if xml_project.tag == 'Projection': len_type = xml_project.find('Length-type').text if len_type == 'Total Length': self._set_u_factors( xml_project, '_total_u_factor', '_total_length') elif len_type == 'Projected X': self._set_u_factors( xml_project, '_projected_x_u_factor', '_projected_x_length') elif len_type == 'Projected Y': self._set_u_factors( xml_project, '_projected_y_u_factor', '_projected_y_length') elif len_type == 'Projected in glass plane': self._set_u_factors( xml_project, '_projected_in_glass_plane_u_factor', '_projected_in_glass_plane_length') elif len_type == 'Custom Rotation': self._set_u_factors( xml_project, '_custom_rotation_u_factor', '_custom_rotation_length') @property def name(self): """Get the name of the U-factor tag.""" return self._name @property def delta_temperature(self): """Get a number for the temperature delta across the boundaries in Celsius.""" return self._delta_temperature @property def heat_flux(self): """Get a number for the heat flux across the boundaries in W/m2.""" return self._heat_flux @property def total_u_factor(self): """Get a number for the total U-Factor across the boundaries in W/m2-K.""" return self._total_u_factor @property def total_length(self): """Get a number for the total length the boundaries in mm.""" return self._total_length @property def projected_x_u_factor(self): """Get a number for the X-projected U-Factor across the boundaries in W/m2-K.""" return self._projected_x_u_factor @property def projected_x_length(self): """Get a number for the X-projected length the boundaries in mm.""" return self._projected_x_length @property def projected_y_u_factor(self): """Get a number for the X-projected U-Factor across the boundaries in W/m2-K.""" return self._projected_y_u_factor @property def projected_y_length(self): """Get a number for the Y-projected length the boundaries in mm.""" return self._projected_y_length @property def projected_in_glass_plane_u_factor(self): """Get a number for the glass plane-projected U-Factor in W/m2-K.""" return self._projected_in_glass_plane_u_factor @property def projected_in_glass_plane_length(self): """Get a number for the glass plane-projected length in mm.""" return self._projected_in_glass_plane_length @property def custom_rotation_u_factor(self): """Get a number for the custom rotation-projected U-Factor in W/m2-K.""" return self._custom_rotation_u_factor @property def custom_rotation_length(self): """Get a number for the custom rotation-projected length in mm.""" return self._custom_rotation_length def _set_u_factors(self, xml_project, u_fac_attr, len_attr): """Set the U-Factor properties given an XML Projection element.""" xml_u_fac = xml_project.find('U-factor') if xml_u_fac is not None: setattr(self, u_fac_attr, float(xml_u_fac.text)) xml_len = xml_project.find('Length') if xml_len is not None: setattr(self, len_attr, float(xml_len.text))
[docs] def ToString(self): """Overwrite .NET ToString.""" return self.__repr__()
def __repr__(self): return 'UFactor: {}'.format(self.name)