Source code for fairyfly_therm.writer

# coding=utf-8
"""Methods to write Fairyfly core objects to THERM XML and THMZ."""
import os
import uuid
import random
import datetime
import zipfile
import json
import tempfile
import xml.etree.ElementTree as ET

from ladybug_geometry.geometry2d import Polygon2D
from ladybug_geometry.geometry3d import LineSegment3D, Polyline3D, Face3D, Polyface3D
from ladybug_geometry.bounding import bounding_box
from fairyfly.typing import clean_string, therm_id_from_uuid
from fairyfly.shape import Shape
from fairyfly.boundary import Boundary

from fairyfly_therm.config import folders
from fairyfly_therm.material import CavityMaterial
from fairyfly_therm.simulation.parameter import SimulationParameter
from fairyfly_therm.lib.conditions import adiabatic, frame_cavity

HANDLE_COUNTER = 1  # counter used to generate unique handles when necessary


[docs] def shape_to_therm_xml(shape, plane=None, polygons_element=None, reset_counter=True): """Generate an THERM XML Polygon Element object from a fairyfly Shape. Args: shape: A fairyfly Shape for which an THERM XML Polygon Element object will be returned. plane: An optional ladybug-geometry Plane to set the 2D coordinate system into which the 3D Shape will be projected to THERM space. If None the Face3D.plane of the Shape's geometry will be used. (Default: None). polygons_element: An optional XML Element for the Polygons to which the generated Element will be added. If None, a new XML Element will be generated. (Default: None). reset_counter: A boolean to note whether the global counter for unique handles should be reset after the method is run. (Default: True). .. code-block:: xml <Polygon> <UUID>9320589a-2ee0-bab0-72c3f49441f3</UUID> <ID>1</ID> <MaterialUUID>8dd145d0-5f30-11ea-bc55-0242ac130003</MaterialUUID> <MaterialName>Laminated panel</MaterialName> <Origin> <x>0</x> <y>0</y> </Origin> <Points> <Point> <x>181</x> <y>-219</y> </Point> <Point> <x>181</x> <y>-371.4</y> </Point> <Point> <x>200</x> <y>-371.4</y> </Point> <Point> <x>200</x> <y>-219</y> </Point> </Points> <Type>Material</Type> </Polygon> """ global HANDLE_COUNTER # declare that we will edit the global variable # create a new Polygon element if one is not specified if polygons_element is not None: xml_poly = ET.SubElement(polygons_element, 'Polygon') else: xml_poly = ET.Element('Polygon') # add all of the required basic attributes xml_uuid = ET.SubElement(xml_poly, 'UUID') xml_uuid.text = shape.therm_uuid xml_id = ET.SubElement(xml_poly, 'ID') xml_id.text = str(HANDLE_COUNTER) HANDLE_COUNTER += 1 xml_mat_id = ET.SubElement(xml_poly, 'MaterialUUID') shape_mat = shape.properties.therm.material xml_mat_id.text = shape_mat.therm_uuid \ if isinstance(shape_mat, CavityMaterial) else shape_mat.identifier xml_mat_name = ET.SubElement(xml_poly, 'MaterialName') xml_mat_name.text = shape.properties.therm.material.display_name # add an origin xml_origin = ET.SubElement(xml_poly, 'Origin') for coord in ('x', 'y'): xml_oc = ET.SubElement(xml_origin, coord) xml_oc.text = '0' # add all of the geometry xml_points = ET.SubElement(xml_poly, 'Points') polygon = shape.geometry.polygon2d.vertices if plane is None else \ [plane.xyz_to_xy(pt3) for pt3 in shape.geometry.vertices] for pt_2d in polygon: xml_point = ET.SubElement(xml_points, 'Point') xml_x = ET.SubElement(xml_point, 'x') xml_x.text = str(round(pt_2d.x, 1)) xml_y = ET.SubElement(xml_point, 'y') xml_y.text = str(round(pt_2d.y, 1)) # add the cavity ID if it exists if shape.user_data is not None and 'cavity_id' in shape.user_data: xml_cav_id = ET.SubElement(xml_poly, 'CavityUUID') xml_cav_id.text = str(shape.user_data['cavity_id']) # add the type of polygon xml_type = ET.SubElement(xml_poly, 'Type') xml_type.text = 'Material' if reset_counter: # reset the counter back to 1 if requested HANDLE_COUNTER = 1 return xml_poly
[docs] def boundary_to_therm_xml(boundary, plane=None, boundaries_element=None, reset_counter=True): """Generate an THERM XML Boundary Element object from a fairyfly Boundary. Args: boundary: A fairyfly Boundary for which an THERM XML Boundary Element object will be returned. plane: An optional ladybug-geometry Plane to set the 2D coordinate system into which the 3D Boundary will be projected to THERM space. If None, it will be assumed that the Boundary lies in the World XY plane. (Default: None). boundaries_element: An optional XML Element for the Boundaries to which the generated objects will be added. If None, a new XML Element will be generated. (Default: None). reset_counter: A boolean to note whether the global counter for unique handles should be reset after the method is run. (Default: True). .. code-block:: xml <Boundary> <ID>45</ID> <UUID>14264c7e-1801-a3c1-0e115d8227ac</UUID> <Name>NFRC 100-2010 Exterior</Name> <FluxTag></FluxTag> <IsBlocking>true</IsBlocking> <NeighborPolygonUUID>5b9e5933-1080-4e9e-5c3b537d8230</NeighborPolygonUUID> <Origin> <x>0</x> <y>0</y> </Origin> <StartPoint> <x>235.670456</x> <y>-147.081726</y> </StartPoint> <EndPoint> <x>235.670456</x> <y>-297.081238</y> </EndPoint> <Side>0</Side> <ThermalEmissionProperties> <Emissivity>0.84</Emissivity> <Temperature>0</Temperature> <UseGlobalEmissivity>true</UseGlobalEmissivity> </ThermalEmissionProperties> <IsIlluminated>false</IsIlluminated> <EdgeID>0</EdgeID> <Type>Boundary Condition</Type> <Color>0x000000</Color> <Status>0</Status> </Boundary> """ global HANDLE_COUNTER # declare that we will edit the global variable # create a new Boundaries element if one is not specified if boundaries_element is None: boundaries_element = ET.Element('Boundaries') # determine an edge ID and color to be used for all segments in the boundary edge_id = str(random.randint(10000000, 99999999)) color = boundary.properties.therm.condition.color.to_hex().replace('#', '0x') # loop through each of the line segments and add a Boundary element for i, seg in enumerate(boundary.geometry): # add all of the required basic attributes xml_bound = ET.SubElement(boundaries_element, 'Boundary') xml_id = ET.SubElement(xml_bound, 'ID') xml_id.text = str(HANDLE_COUNTER) HANDLE_COUNTER += 1 xml_uuid = ET.SubElement(xml_bound, 'UUID') xml_uuid.text = boundary.therm_uuid[:-12] + str(uuid.uuid4())[-12:] xml_name = ET.SubElement(xml_bound, 'Name') xml_name.text = boundary.properties.therm.condition.display_name xml_flux = ET.SubElement(xml_bound, 'FluxTag') if boundary.properties.therm.u_factor_tag is not None: xml_flux.text = boundary.properties.therm.u_factor_tag xml_blocks = ET.SubElement(xml_bound, 'IsBlocking') xml_blocks.text = 'true' # add the UUIDs of the neighboring shapes if boundary.user_data is not None and 'adj_polys' in boundary.user_data: adj_ids = boundary.user_data['adj_polys'][i] for j, adj_id in enumerate(adj_ids): if j == 0: xml_ajd_p = ET.SubElement(xml_bound, 'NeighborPolygonUUID') else: xml_ajd_p = ET.SubElement( xml_bound, 'NeighborPolygonUUID{}'.format(j + 1)) xml_ajd_p.text = adj_id # add an origin xml_origin = ET.SubElement(xml_bound, 'Origin') for coord in ('x', 'y'): xml_oc = ET.SubElement(xml_origin, coord) xml_oc.text = '0' # add the boundary geometry pts_2d = seg.vertices if plane is None else \ [plane.xyz_to_xy(pt3) for pt3 in seg.vertices] for k, pt_2d in enumerate(pts_2d): xml_point = ET.SubElement(xml_bound, 'StartPoint') if k == 0 else \ ET.SubElement(xml_bound, 'EndPoint') xml_x = ET.SubElement(xml_point, 'x') xml_x.text = str(round(pt_2d.x, 1)) xml_y = ET.SubElement(xml_point, 'y') xml_y.text = str(round(pt_2d.y, 1)) # add the various thermal properties xml_side = ET.SubElement(xml_bound, 'Side') xml_side.text = '0' xml_e_prop = ET.SubElement(xml_bound, 'ThermalEmissionProperties') xml_emiss = ET.SubElement(xml_e_prop, 'Emissivity') if boundary.user_data is not None and 'emissivities' in boundary.user_data: xml_emiss.text = str(boundary.user_data['emissivities'][i]) else: xml_emiss.text = '0.9' xml_temp = ET.SubElement(xml_e_prop, 'Temperature') xml_temp.text = '0' xml_g_emiss = ET.SubElement(xml_e_prop, 'UseGlobalEmissivity') xml_g_emiss.text = 'true' xml_is_ill = ET.SubElement(xml_bound, 'IsIlluminated') xml_is_ill.text = 'false' # add the final identifying properties xml_edge_id = ET.SubElement(xml_bound, 'EdgeID') xml_edge_id.text = edge_id if boundary.user_data is not None and 'enclosure_numbers' in boundary.user_data: xml_enclosure = ET.SubElement(xml_bound, 'EnclosureNumber') xml_enclosure.text = boundary.user_data['enclosure_numbers'][i] xml_type = ET.SubElement(xml_bound, 'Type') xml_type.text = 'Frame Cavity' else: xml_type = ET.SubElement(xml_bound, 'Type') xml_type.text = 'Boundary Condition' xml_color = ET.SubElement(xml_bound, 'Color') xml_color.text = color xml_status = ET.SubElement(xml_bound, 'Status') if boundary.user_data is not None and 'enclosure_numbers' in boundary.user_data: xml_status.text = '64' else: xml_status.text = '0' if reset_counter: # reset the counter back to 1 if requested HANDLE_COUNTER = 1 return boundaries_element
def _cavity_to_therm_xml(properties, cavities_element=None): """Generate an THERM XML Cavity Element object from a list of properties. Args: properties: A list of cavity properties, including emissivities and cavity area. cavities_element: An optional XML Element for the Cavities to which the generated objects will be added. If None, a new XML Element will be generated. (Default: None). .. code-block:: xml <Cavity> <UUID>adfca13b-af61-a0d7-d29f0fbefbcc</UUID> <HeatFlowDirection>Unknown</HeatFlowDirection> <Emissivity1>0.9</Emissivity1> <Emissivity2>0.9</Emissivity2> <Temperature1>7</Temperature1> <Temperature2>-4</Temperature2> <MaxXDimension>-1</MaxXDimension> <MaxYDimension>-1</MaxYDimension> <ActualHeight>1000</ActualHeight> <Area>3.260304e-12</Area> <LocalEmissivities>false</LocalEmissivities> <Pressure>1.013e+05</Pressure> <WarmLocator> <x>0</x> <y>0</y> </WarmLocator> <ColdLocator> <x>0</x> <y>0</y> </ColdLocator> </Cavity> """ # create a new Cavity element if one is not specified if cavities_element is not None: xml_cav = ET.SubElement(cavities_element, 'Cavity') else: xml_cav = ET.Element('Cavity') # add all of the required basic attributes xml_uuid = ET.SubElement(xml_cav, 'UUID') xml_uuid.text = str(properties[0]) xml_hfd = ET.SubElement(xml_cav, 'HeatFlowDirection') xml_hfd.text = 'Unknown' xml_e1 = ET.SubElement(xml_cav, 'Emissivity1') xml_e1.text = str(properties[1]) xml_e2 = ET.SubElement(xml_cav, 'Emissivity2') xml_e2.text = str(properties[2]) xml_t1 = ET.SubElement(xml_cav, 'Temperature1') xml_t1.text = '7' xml_t2 = ET.SubElement(xml_cav, 'Temperature2') xml_t2.text = '-4' xml_mx = ET.SubElement(xml_cav, 'MaxXDimension') xml_mx.text = '-1' xml_my = ET.SubElement(xml_cav, 'MaxYDimension') xml_my.text = '-1' xml_ah = ET.SubElement(xml_cav, 'ActualHeight') xml_ah.text = '1000' xml_ar = ET.SubElement(xml_cav, 'Area') xml_ar.text = str(properties[3]) xml_le = ET.SubElement(xml_cav, 'LocalEmissivities') xml_le.text = 'true' xml_pressure = ET.SubElement(xml_cav, 'Pressure') xml_pressure.text = '1.01e+05' # add a warm locator xml_warm = ET.SubElement(xml_cav, 'WarmLocator') for coord in ('x', 'y'): xml_oc = ET.SubElement(xml_warm, coord) xml_oc.text = '0' # add a cold locator xml_cold = ET.SubElement(xml_cav, 'ColdLocator') for coord in ('x', 'y'): xml_oc = ET.SubElement(xml_cold, coord) xml_oc.text = '0' return xml_cav
[docs] def model_to_therm_xml(model, simulation_par=None): """Generate an THERM XML Element object for a fairyfly Model. The resulting Element has all geometry (Shapes and Boundaries). Args: model: A fairyfly Model for which a THERM XML ElementTree object will be returned. simulation_par: A fairyfly-therm SimulationParameter object to specify how the THERM simulation should be run. If None, default simulation parameters will be generated. (Default: None). """ global HANDLE_COUNTER # declare that we will edit the global variable # check that we have at least one shape to translate assert len(model.shapes) > 0, \ 'Model must have at least one Shape to translate to THERM.' # duplicate model to avoid mutating it as we edit it for THERM export original_model = model model = model.duplicate() # scale the model if the units are not millimeters if model.units != 'Millimeters': model.convert_to_units('Millimeters') # remove degenerate geometry within THERM native tolerance try: model.remove_duplicate_vertices(0.1) except ValueError: error = 'Failed to remove degenerate Shapes.\nYour Model units system is: {}. ' \ 'Is this correct?'.format(original_model.units) raise ValueError(error) # determine the plane and the scale to be used for all geometry translation plane = model.properties.therm.therm_plane min_pt, max_pt = bounding_box([s.geometry for s in model.shapes]) max_dim = max((max_pt.x - min_pt.x, max_pt.y - min_pt.y, max_pt.z - min_pt.z)) scale = 1.0 if max_dim < 100 else 100 / max_dim # check that all geometries lie within the tolerance of the plane for shape in model.shapes: for pt in shape.vertices: if plane.distance_to_point(pt) > 0.1: msg = 'Not all of the model shapes lie in the same plane as ' \ 'each other. Shape "{}" is out of plane by {} ' \ 'millimeters.'.format(shape.full_id, plane.distance_to_point(pt)) raise ValueError(msg) for bound in model.boundaries: for pt in bound.vertices: if plane.distance_to_point(pt) > 0.1: msg = 'Not all of the model boundaries lie in the same plane as ' \ 'the shapes. Boundary "{}" is out of plane by {} ' \ 'millimeters.'.format(bound.full_id, plane.distance_to_point(pt)) raise ValueError(msg) # split any shapes that have holes in them split_shapes = [] for shape in model.shapes: if shape.geometry.has_holes: shape_geo = shape.geometry split_geo = shape_geo.split_through_holes() if any(g.is_self_intersecting for g in split_geo): split_geo = shape_geo.split_through_hole_center_lines(0.1) for geo in split_geo: try: geo = geo.remove_colinear_vertices(0.1) new_shp = shape.duplicate() new_shp._geometry = geo new_shp.identifier = str(uuid.uuid4()) split_shapes.append(new_shp) except AssertionError: # degenerate geometry to ignore pass else: split_shapes.append(shape) if len(model.shapes) != split_shapes: model.shapes = split_shapes # ensure that all shapes are counterclockwise for shape in model.shapes: poly = Polygon2D(plane.xyz_to_xy(pt) for pt in shape.geometry) if poly.is_clockwise: shape._geometry = shape.geometry.flip() # intersect the shape geometries with one another Shape.intersect_adjacency(model.shapes, 0.1, plane) # determine if there are any Boundary points that do not share a Shape vertex boundary_pts = [] for bound in model.boundaries: for seg in bound.geometry: for pt in seg.vertices: for o_pt in boundary_pts: if pt.is_equivalent(o_pt, tolerance=0.1): break else: # the point is unique boundary_pts.append(pt) orphaned_points = [] for bpt in boundary_pts: matched = False for shape in model.shapes: if matched: break for spt in shape.vertices: if bpt.is_equivalent(spt, tolerance=0.1): matched = True break if not matched: # a boundary point with no Shape orphaned_points.append(bpt) # insert extra vertices to the shapes if they do not align with boundary end points for or_pt in orphaned_points: for shape in model.shapes: shape.insert_vertex(or_pt, tolerance=0.1) # ensure that there is only one contiguous shape without holes shape_geos = [shape.geometry for shape in model.shapes] polyface = Polyface3D.from_faces(shape_geos, tolerance=0.1) outer_edges = polyface.naked_edges joined_boundary = Polyline3D.join_segments(outer_edges, tolerance=0.1) if len(joined_boundary) != 1: b_msg = 'The Shapes of the input model do not form a contiguous region ' \ 'without any holes.' join_faces = [Face3D(poly.vertices) for poly in joined_boundary] merged_faces = Face3D.merge_faces_to_holes(join_faces, 0.1) region_count = len(merged_faces) plural = 's' if region_count != 1 else '' hole_count, hole_areas = 0, [] for mf in merged_faces: if mf.has_holes: hole_count += len(mf.holes) for h in mf.holes: hole_areas.append(Face3D(h).area) if region_count == 1 and sum(hole_areas) < 0.1: pass # artifact of splitting holes else: d_msg = '{} distinct region{} with {} total holes were found.'.format( region_count, plural, hole_count) if hole_count != 0: d_msg = '{}\nHole Areas [mm2]: {}'.format( d_msg, ', '.join(str(h) for h in hole_areas)) raise ValueError('{}\n{}'.format(b_msg, d_msg)) # gather all of the extra edges to be written as adiabatic adiabatic_geo, adiabatic_adj = [], [] for edge in outer_edges: bnd_matched = False for bound in model.boundaries: for si, seg in enumerate(bound.geometry): if seg.distance_to_point(edge.p1) < 0.1 and \ seg.distance_to_point(edge.p2) < 0.1: if seg.length - edge.length > 0.1: # split the boundary geo cpt_1 = seg.closest_point(edge.p1) cpt_2 = seg.closest_point(edge.p2) all_pts = [seg.p1, cpt_1, cpt_2, seg.p2] pt_dists = [seg.p1.distance_to_point(pt) for pt in all_pts] s_pts = [p for _, p in sorted(zip(pt_dists, all_pts), key=lambda pair: pair[0])] new_geo = list(bound.geometry) new_geo.pop(si) for p in range(3): if s_pts[p].distance_to_point(s_pts[p + 1]) > 0.1: nl = LineSegment3D.from_end_points(s_pts[p], s_pts[p + 1]) new_geo.append(nl) bound._geometry = tuple(new_geo) bnd_matched = True break if bnd_matched: break else: # adiabatic segment to be added at the end shape_matched = False for shape in model.shapes: for seg in shape.geometry.segments: if edge.p1.is_equivalent(seg.p1, 0.1) or \ edge.p1.is_equivalent(seg.p2, 0.1): if edge.p2.is_equivalent(seg.p1, 0.1) or \ edge.p2.is_equivalent(seg.p2, 0.1): adiabatic_adj.append([shape.therm_uuid]) if edge.p1.is_equivalent(seg.p2, 0.1): edge = edge.flip() shape_matched = True break if shape_matched: break adiabatic_geo.append(edge) if not shape_matched: adiabatic_adj.append([]) # gather any edges to be written with a frame cavity boundary frame_cavity_geo, cavity_props, enclosure_numbers = [], [], [] enclosure_count = 1 solid_shapes, cavity_shapes = [], [] for shape in model.shapes: c_mat = shape.properties.therm.material if isinstance(c_mat, CavityMaterial) and c_mat.cavity_model != 'CEN': cavity_shapes.append(shape) cav_id = therm_id_from_uuid(str(uuid.uuid4())) cav_number = str(enclosure_count) enclosure_count += 1 if shape.user_data is None: shape.user_data = {'cavity_id': cav_id} else: shape.user_data['cavity_id'] = cav_id cavity_prop = [cav_id, c_mat.emissivity, c_mat.emissivity_back, shape.area * 1e-6] cavity_props.append(cavity_prop) for edge in shape.geometry.segments: frame_cavity_geo.append(edge) enclosure_numbers.append(cav_number) else: solid_shapes.append(shape) if len(frame_cavity_geo) == 0: cavity_boundary = None all_boundaries = model.boundaries else: cavity_boundary = Boundary(frame_cavity_geo) cavity_boundary.properties.therm.condition = frame_cavity cavity_boundary.user_data = {'enclosure_numbers': enclosure_numbers} all_boundaries = model.boundaries + (cavity_boundary,) # add the UUIDs of the polygons next to the edges to the Boundary.user_data ordered_shapes = cavity_shapes + solid_shapes for bound in all_boundaries: oriented_geo, bound_adj_shapes, bound_emissivity = [], [], [] for edge in bound.geometry: adj_shapes, bnd_e = [], 0.9 for shape in ordered_shapes: for seg in shape.geometry.segments: if edge.p1.is_equivalent(seg.p1, 0.1) or \ edge.p1.is_equivalent(seg.p2, 0.1): if edge.p2.is_equivalent(seg.p1, 0.1) or \ edge.p2.is_equivalent(seg.p2, 0.1): adj_shapes.append(shape.therm_uuid) if edge.p1.is_equivalent(seg.p2, 0.1): edge = edge.flip() shape_mat = shape.properties.therm.material if not isinstance(shape_mat, CavityMaterial): bnd_e = shape_mat.emissivity break bound_adj_shapes.append(adj_shapes) bound_emissivity.append(bnd_e) oriented_geo.append(edge) bound._geometry = tuple(oriented_geo) if bound.user_data is None: bound.user_data = { 'adj_polys': bound_adj_shapes, 'emissivities': bound_emissivity } else: bound.user_data['adj_polys'] = bound_adj_shapes bound.user_data['emissivities'] = bound_emissivity # load up the template XML file for the model package_dir = os.path.dirname(os.path.abspath(__file__)) template_file = os.path.join(package_dir, '_templates', 'Default.xml') xml_tree = ET.parse(template_file) xml_root = xml_tree.getroot() model_name = clean_string(model.display_name) # assign the property for the scale so it looks good in THERM xml_preferences = xml_root.find('Preferences') xml_settings = xml_preferences.find('Settings') xml_scale = xml_settings.find('Scale') xml_scale.text = str(scale) # set the properties for the document xml_props = xml_root.find('Properties') xml_gen = xml_props.find('General') therm_ver = '.'.join(str(i) for i in folders.THERM_VERSION) therm_ver = 'Version {}'.format(therm_ver) xml_calc_ver = xml_gen.find('CalculationVersion') xml_calc_ver.text = therm_ver xml_cre_ver = xml_gen.find('CreationVersion') xml_cre_ver.text = therm_ver xml_mod_ver = xml_gen.find('LastModifiedVersion') xml_mod_ver.text = therm_ver xml_cre_date = xml_gen.find('CreationDate') xml_cre_date.text = str(datetime.datetime.now().replace(microsecond=0)) xml_cre_date = xml_gen.find('LastModified') xml_cre_date.text = str(datetime.datetime.now().replace(microsecond=0)) xml_model_name = xml_gen.find('Title') xml_model_name.text = model_name # write the 3D plane into the notes section xml_notes = xml_gen.find('Notes') xml_notes.text = 'Plane: {}'.format(json.dumps(plane.to_dict())) # add the calculation options sim_par = simulation_par if simulation_par is not None else SimulationParameter() xml_calc_opt = xml_props.find('CalculationOptions') sim_par.mesh.to_therm_xml(xml_calc_opt) sim_par.exposure.to_therm_xml(xml_props, plane) # write all of the cavity definitions into the model if len(cavity_props) != 0: xml_cavities = ET.SubElement(xml_root, 'Cavities') for cp in cavity_props: _cavity_to_therm_xml(cp, xml_cavities) # translate all Shapes to polygons xml_polygons = ET.SubElement(xml_root, 'Polygons') for shape in model.shapes: shape_to_therm_xml(shape, plane, xml_polygons, reset_counter=False) # translate all Boundaries xml_boundaries = ET.SubElement(xml_root, 'Boundaries') for bound in model.boundaries: # remove any boundary geometries that are not assigned to shapes adj_polys, seg_es = bound.user_data['adj_polys'], bound.user_data['emissivities'] new_segs, new_adj_polys, new_seg_es = [], [], [] for seg, adj_poly, seg_e in zip(bound.geometry, adj_polys, seg_es): if len(adj_poly) != 0: new_segs.append(seg) new_adj_polys.append(adj_poly) new_seg_es.append(seg_e) # write the boundary into the XML if len(new_segs) != 0: bound._geometry = tuple(new_segs) bound.user_data['adj_polys'] = new_adj_polys bound.user_data['emissivities'] = new_seg_es boundary_to_therm_xml(bound, plane, xml_boundaries, reset_counter=False) # add the extra adiabatic boundaries ad_bnd = Boundary(adiabatic_geo) ad_bnd.properties.therm.condition = adiabatic ad_bnd.user_data = {'adj_polys': adiabatic_adj} boundary_to_therm_xml(ad_bnd, plane, xml_boundaries, reset_counter=False) # add the cavity boundaries if they exist if cavity_boundary is not None: boundary_to_therm_xml(cavity_boundary, plane, xml_boundaries, reset_counter=False) # reset the handle counter back to 1 and return the root XML element HANDLE_COUNTER = 1 return xml_root
[docs] def shape_to_therm_xml_str(shape): """Generate an THERM XML string from a fairyfly Shape. Args: shape: A fairyfly Shape for which an THERM XML Polygon string will be returned. """ xml_root = shape_to_therm_xml(shape) 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 boundary_to_therm_xml_str(boundary): """Generate an THERM XML string from a fairyfly Boundary. Args: shape_mesh: A fairyfly Boundary for which an THERM XML Boundary string will be returned. """ xml_root = boundary_to_therm_xml(boundary) 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 model_to_therm_xml_str(model, simulation_par=None): """Generate a THERM XML string for a Model. The resulting Element has all geometry (Shapes and Boundaries). Args: model: A fairyfly Model for which an THERM XML text string will be returned. simulation_par: A fairyfly-therm SimulationParameter object to specify how the THERM simulation should be run. If None, default simulation parameters will be generated. (Default: None). """ # create the XML string xml_root = model_to_therm_xml(model, simulation_par) try: # try to indent the XML to make it read-able ET.indent(xml_root, '\t') 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 model_to_thmz(model, output_file, simulation_par=None): """Write a THERM Zip (.thmz) file from a Fairyfly Model. Args: model: A fairyfly Model for which an THMZ file will be written. output_file: The path to the THMZ file that will be written from the model. simulation_par: A fairyfly-therm SimulationParameter object to specify how the THERM simulation should be run. If None, default simulation parameters will be generated. (Default: None). Usage: .. code-block:: python import os from fairyfly.model import Model from fairyfly.config import folders from fairyfly_therm.lib.materials import concrete, air_cavity from fairyfly_therm.lib.conditions import exterior, interior # Crate an input Model model = Model.from_layers([100, 200, 100], height=1000) model.shapes[0].properties.therm.material = concrete model.shapes[1].properties.therm.material = air_cavity model.shapes[2].properties.therm.material = concrete model.boundaries[0].properties.therm.condition = exterior model.boundaries[1].properties.therm.condition = interior model.display_name = 'Roman Bath Wall' # create the THERM Zip file for the model thmz = os.path.join(folders.default_simulation_folder, 'model.thmz') xml_str = model.to_thmz(thmz) """ # make sure the directory exists where the file will be written dir_name = os.path.dirname(os.path.abspath(output_file)) if not os.path.isdir(dir_name): os.makedirs(dir_name) # create a temporary directory where everything will be zipped therm_trans_dir = tempfile.gettempdir() files_to_zip = [] # prepare the files for materials and gases mat_file = os.path.join(therm_trans_dir, 'Materials.xml') gas_file = os.path.join(therm_trans_dir, 'Gases.xml') gases, pure_gases = [], [] xml_materials = ET.Element('Materials') xml_gases = ET.Element('Gases') for xml_rt in (xml_materials, xml_gases): xml_ver = ET.SubElement(xml_rt, 'Version') xml_ver.text = '1' # rename any materials with duplicate display names model_mats = model.properties.therm.materials mat_names, reset_dict = {}, {} for mat in model_mats: if mat.display_name in mat_names: mat_names[mat.display_name] += 1 reset_dict[mat.display_name] = mat mat.unlock() mat.display_name = mat.display_name + '_' + str(mat_names[mat.display_name]) mat.lock() else: mat_names[mat.display_name] = 1 # write the materials and gases to a file for mat in model_mats: mat.to_therm_xml(xml_materials) if isinstance(mat, CavityMaterial): gases.append(mat.gas) for pg in mat.gas.pure_gases: pure_gases.append(pg) _xml_element_to_file(xml_materials, mat_file) files_to_zip.append(mat_file) for pg in pure_gases: pg.to_therm_xml(xml_gases) for g in gases: g.to_therm_xml(xml_gases) _xml_element_to_file(xml_gases, gas_file) files_to_zip.append(gas_file) # write the Model into the temporary directory model_file = os.path.join(therm_trans_dir, 'Model.xml') xml_model = model_to_therm_xml(model, simulation_par) xml_props = xml_model.find('Properties') xml_gen = xml_props.find('General') xml_direct = xml_gen.find('Directory') xml_direct.text = dir_name xml_file_name = xml_gen.find('FileName') xml_file_name.text = os.path.basename(output_file.replace('.thmz', '')) _xml_element_to_file(xml_model, model_file) files_to_zip.append(model_file) # write the boundary conditions to a file bc_file = os.path.join(therm_trans_dir, 'SteadyStateBC.xml') xml_bcs = ET.Element('BoundaryConditions') xml_ver = ET.SubElement(xml_bcs, 'Version') xml_ver.text = '1' for bc in [adiabatic] + model.properties.therm.conditions: bc.to_therm_xml(xml_bcs) _xml_element_to_file(xml_bcs, bc_file) files_to_zip.append(bc_file) # put back any materials that were edited for mat_name, mat in reset_dict.items(): mat.unlock() mat.display_name = mat_name mat.lock() # zip everything together with zipfile.ZipFile(output_file, 'w', zipfile.ZIP_DEFLATED) as zipf: for file in files_to_zip: # Add the file to the zip archive # arcname=os.path.basename(file) ensures only the filename is used # inside the zip, avoiding unwanted directory structures zipf.write(file, arcname=os.path.basename(file)) return output_file
def _xml_element_to_file(xml_root, file_path): """Write an XML element to a file.""" try: # try to indent the XML to make it read-able ET.indent(xml_root, '\t') xml_str = ET.tostring(xml_root, encoding='unicode', short_empty_elements=False) except AttributeError: # we are in Python 2 and no indent is available xml_str = ET.tostring(xml_root) with open(file_path, 'wb') as fp: fp.write(xml_str.encode('utf-8'))