# coding=utf-8
"""Methods to write Honeybee core objects to dsbXML."""
import os
import math
import datetime
from copy import deepcopy
import xml.etree.ElementTree as ET
from ladybug_geometry.geometry2d import Point2D, Polygon2D
from ladybug_geometry.geometry3d import Vector3D, Point3D, Face3D, Polyface3D
from honeybee.typing import clean_string
from honeybee.aperture import Aperture
from honeybee.face import Face
from honeybee.room import Room
from honeybee.facetype import Floor, RoofCeiling, AirBoundary, face_types
from honeybee.boundarycondition import Outdoors, Surface, Ground, boundary_conditions
from honeybee_energy.boundarycondition import Adiabatic
DESIGNBUILDER_VERSION = '2025.1.0.085'
HANDLE_COUNTER = 1 # counter used to generate unique handles when necessary
[docs]
def shade_to_dsbxml_element(shade, building_element=None):
"""Generate an dsbXML Plane Element object from a honeybee Shade.
Args:
shade: A honeybee Shade for which an dsbXML Plane Element object will
be returned.
building_element: An optional XML Element for the Building to which the
generated plane object will be added. If None, a new XML Element
will be generated. Note that this Building element should have a
Planes tag already created within it.
"""
# create the Plane element
if building_element is not None:
planes_element = building_element.find('Planes')
xml_shade = ET.SubElement(planes_element, 'Plane', type='2')
else:
xml_shade = ET.Element('Plane', type='2')
# add the vertices for the geometry
xml_geo = ET.SubElement(xml_shade, 'Polygon', auxiliaryType='-1')
_object_ids(xml_geo, shade.identifier, '0')
xml_sub_pts = ET.SubElement(xml_geo, 'Vertices')
for pt in shade.geometry.boundary:
xml_point = ET.SubElement(xml_sub_pts, 'Point3D')
xml_point.text = '{}; {}; {}'.format(pt.x, pt.y, pt.z)
xml_holes = ET.SubElement(xml_geo, 'PolygonHoles')
if shade.geometry.has_holes:
flip_plane = shade.geometry.plane.flip() # flip to make holes clockwise
for hole in shade.geometry.holes:
hole_face = Face3D(hole, plane=flip_plane)
xml_sub_hole = ET.SubElement(xml_holes, 'PolygonHole')
_object_ids(xml_geo, shade.identifier, '0')
xml_sub_hole_pts = ET.SubElement(xml_sub_hole, 'Vertices')
for pt in hole_face:
xml_point = ET.SubElement(xml_sub_hole_pts, 'Point3D')
xml_point.text = '{}; {}; {}'.format(pt.x, pt.y, pt.z)
# add the name of the shade
xml_shd_attr = ET.SubElement(xml_shade, 'Attributes')
xml_shd_name = ET.SubElement(xml_shd_attr, 'Attribute', key='Title')
xml_shd_name.text = str(shade.display_name)
return xml_shade
[docs]
def shade_mesh_to_dsbxml_element(shade_mesh, building_element=None, reset_counter=True):
"""Generate an dsbXML Planes Element object from a honeybee ShadeMesh.
Args:
shade_mesh: A honeybee ShadeMesh for which an dsbXML Planes Element
object will be returned.
building_element: An optional XML Element for the Building to which the
generated objects will be added. If None, a new XML Element
will be generated. Note that this Building element should have a
Planes tag already created within it.
reset_counter: A boolean to note whether the global counter for unique
handles should be reset after the method is run. (Default: True).
"""
global HANDLE_COUNTER # declare that we will edit the global variable
# create the Planes element
xml_planes = building_element.find('Planes') \
if building_element is not None else ET.Element('Planes')
# add a plane element for each mesh face
for i, face in enumerate(shade_mesh.geometry.face_vertices):
xml_shade = ET.SubElement(xml_planes, 'Plane', type='2')
xml_geo = ET.SubElement(xml_shade, 'Polygon', auxiliaryType='-1')
_object_ids(xml_geo, str(HANDLE_COUNTER), '0')
HANDLE_COUNTER += 1
xml_sub_pts = ET.SubElement(xml_geo, 'Vertices')
for pt in face:
xml_point = ET.SubElement(xml_sub_pts, 'Point3D')
xml_point.text = '{}; {}; {}'.format(pt.x, pt.y, pt.z)
ET.SubElement(xml_geo, 'PolygonHoles')
# add the name of the shade
xml_shd_attr = ET.SubElement(xml_shade, 'Attributes')
xml_shd_name = ET.SubElement(xml_shd_attr, 'Attribute', key='Title')
xml_shd_name.text = '{} {}'.format(shade_mesh.display_name, i)
if reset_counter: # reset the counter back to 1 if requested
HANDLE_COUNTER = 1
return xml_planes
[docs]
def sub_face_to_dsbxml_element(sub_face, surface_element=None):
"""Generate an dsbXML Opening Element object from a honeybee Aperture or Door.
Args:
sub_face: A honeybee Aperture or Door for which an dsbXML Opening Element
object will be returned.
surface_element: An optional XML Element for the Surface to which the
generated opening object will be added. If None, a new XML Element
will be generated. Note that this Surface element should have a
Openings tag already created within it.
"""
# create the Opening element
open_type = 'Window' if isinstance(sub_face, Aperture) else 'Door'
if surface_element is not None:
surfaces_element = surface_element.find('Openings')
xml_sub_face = ET.SubElement(surfaces_element, 'Opening', type=open_type)
obj_ids = surface_element.find('ObjectIDs')
block_handle = obj_ids.get('buildingBlockHandle')
zone_handle = obj_ids.get('zoneHandle')
surface_index = obj_ids.get('surfaceIndex')
else:
xml_sub_face = ET.Element('Opening', type=open_type)
block_handle, zone_handle, surface_index = '-1', '-1', '0'
# add the vertices for the geometry
xml_sub_geo = ET.SubElement(xml_sub_face, 'Polygon', auxiliaryType='-1')
_object_ids(xml_sub_geo, '-1', '0', str(block_handle), str(zone_handle), surface_index)
xml_sub_pts = ET.SubElement(xml_sub_geo, 'Vertices')
for pt in sub_face.geometry.boundary:
xml_point = ET.SubElement(xml_sub_pts, 'Point3D')
xml_point.text = '{}; {}; {}'.format(pt.x, pt.y, pt.z)
xml_sub_holes = ET.SubElement(xml_sub_geo, 'PolygonHoles')
if sub_face.geometry.has_holes:
flip_plane = sub_face.geometry.plane.flip() # flip to make holes clockwise
for hole in sub_face.geometry.holes:
hole_face = Face3D(hole, plane=flip_plane)
xml_sub_hole = ET.SubElement(xml_sub_holes, 'PolygonHole')
_object_ids(xml_sub_geo, '-1', '0',
str(block_handle), str(zone_handle), surface_index)
xml_sub_hole_pts = ET.SubElement(xml_sub_hole, 'Vertices')
for pt in hole_face:
xml_point = ET.SubElement(xml_sub_hole_pts, 'Point3D')
xml_point.text = '{}; {}; {}'.format(pt.x, pt.y, pt.z)
# add other required but usually empty tags
xml_sf_attr = ET.SubElement(xml_sub_face, 'Attributes')
xml_sf_name = ET.SubElement(xml_sf_attr, 'Attribute', key='Title')
xml_sf_name.text = str(sub_face.display_name)
ET.SubElement(xml_sub_face, 'SegmentList')
return xml_sub_face
[docs]
def face_to_dsbxml_element(
face, zone_body_element=None, zone_face_indices=None, adjacency_faces=None,
tolerance=0.01, angle_tolerance=1.0, reset_counter=True
):
"""Generate an dsbXML Surface Element object from a honeybee Face.
The resulting Element has all constituent geometry (Apertures, Doors).
Args:
face: A honeybee Face for which an dsbXML Surface Element object will
be returned.
zone_body_element: An optional XML Element for the Zone Body to which the
generated surface object will be added. If None, a new XML Element
will be generated. Note that this Zone Body element should have a
Surfaces tag already created within it.
zone_face_indices: An optional tuple of integers for the vertex indices
of the face in the parent Room Polyface3D. If None, some placeholder
indices will be generated. (Default: None).
adjacency_faces: An optional list of Honeybee Faces for sub-elements
of the input face that specify adjacencies to multiple other
Faces. When specified, these adjacency faces should be coplanar
to the input face and should together completely fill its area.
If None, it will be assumed that the face in dsbXML should
have only one adjacency. (Default: None).
tolerance: The absolute tolerance with which the Room geometry will
be evaluated. (Default: 0.01, suitable for objects in meters).
angle_tolerance: The angle tolerance at which the geometry will
be evaluated in degrees. This is needed to determine whether to
write roof faces as flat or pitched. (Default: 1 degree).
reset_counter: A boolean to note whether the global counter for unique
handles should be reset after the method is run. (Default: True).
"""
global HANDLE_COUNTER # declare that we will edit the global variable
# get the basic attributes of the Face
if isinstance(face.type, RoofCeiling):
face_type = 'Pitched roof' if face.tilt > angle_tolerance else 'Flat roof'
elif isinstance(face.type, AirBoundary):
face_type = 'Wall'
else:
face_type = str(face.type)
face_id_attr = {
'type': face_type,
'area': str(face.area),
'alpha': str(face.geometry.azimuth),
'phi': str(face.geometry.altitude),
'defaultOpenings': 'False',
'adjacentPartitionHandle': '-1',
'thickness': '0.0' # TODO: make better for adjacency
}
if face.user_data is not None and 'partition_handle' in face.user_data:
face_id_attr['adjacentPartitionHandle'] = face.user_data['partition_handle']
# create the Surface element
if zone_body_element is not None:
surfaces_element = zone_body_element.find('Surfaces')
dsb_face_i = len(surfaces_element.findall('Surface'))
xml_face = ET.SubElement(surfaces_element, 'Surface', face_id_attr)
obj_ids = zone_body_element.find('ObjectIDs')
block_handle = obj_ids.get('buildingBlockHandle')
zone_handle = obj_ids.get('handle')
else:
xml_face = ET.Element('Surface', face_id_attr)
dsb_face_i, block_handle, zone_handle = 0, '-1', '-1'
face_obj_ids = _object_ids(xml_face, face.identifier, '0', block_handle,
zone_handle, str(dsb_face_i))
if face.user_data is None:
face.user_data = {'dsb_face_i': str(dsb_face_i)}
else:
face.user_data['dsb_face_i'] = str(dsb_face_i)
if adjacency_faces is not None:
for a_face in adjacency_faces:
if a_face.user_data is None:
a_face.user_data = {'dsb_face_i': str(dsb_face_i)}
else:
a_face.user_data['dsb_face_i'] = str(dsb_face_i)
# add the vertices that define the Face
if zone_face_indices is None:
face_indices = [tuple(range(len(face.geometry.boundary)))]
if face.geometry.has_holes:
counter = len(face_indices[0])
for hole in face.geometry.holes:
face_indices.append(tuple(range(counter, counter + len(hole))))
counter += len(hole)
else:
face_indices = zone_face_indices
xml_pt_i = ET.SubElement(xml_face, 'VertexIndices')
xml_pt_i.text = '; '.join([str(i) for i in face_indices[0]])
# add the holes as duplicated Surfaces
xml_hole_i = ET.SubElement(xml_face, 'HoleIndices')
hole_is = None
if len(face_indices) > 1: # we have holes to add
hole_is = []
for j, (hole_i, hole) in enumerate(zip(face_indices[1:], face.geometry.holes)):
hole_id_attr = face_id_attr.copy()
hole_id_attr['type'] = 'Hole'
hole_geo = Face3D(hole)
hole_id_attr['area'] = str(hole_geo.area)
xml_hole = ET.SubElement(surfaces_element, 'Surface', hole_id_attr)
_object_ids(xml_hole, str(HANDLE_COUNTER), '0', block_handle)
HANDLE_COUNTER += 1
xml_hole_pt_i = ET.SubElement(xml_hole, 'VertexIndices')
xml_hole_pt_i.text = '; '.join([str(i) for i in hole_i])
ET.SubElement(xml_hole, 'HoleIndices')
ET.SubElement(xml_hole, 'Openings')
ET.SubElement(xml_hole, 'Adjacencies')
ET.SubElement(xml_hole, 'Attributes')
hole_is.append(dsb_face_i + 1 + j)
xml_hole_i.text = '; '.join([str(i) for i in hole_is])
# add the various attributes of the Face
xml_face_attr = ET.SubElement(xml_face, 'Attributes')
xml_face_name = ET.SubElement(xml_face_attr, 'Attribute', key='Title')
xml_face_name.text = str(face.display_name)
xml_gbxml_type = ET.SubElement(xml_face_attr, 'Attribute', key='gbXMLSurfaceType')
xml_gbxml_type.text = str(face.gbxml_type)
xml_bc = ET.SubElement(xml_face_attr, 'Attribute', key='AdjacentCondition')
if isinstance(face.boundary_condition, Outdoors):
xml_bc.text = '2-Not adjacent to ground'
elif isinstance(face.boundary_condition, Ground):
xml_bc.text = '3-Adjacent to ground'
elif isinstance(face.boundary_condition, Adiabatic):
xml_bc.text = '4-Adiabatic'
else:
xml_bc.text = '1-Auto'
# add any openings if they exist
ET.SubElement(xml_face, 'Openings')
for ap in face.apertures:
sub_face_to_dsbxml_element(ap, xml_face)
for dr in face.doors:
sub_face_to_dsbxml_element(dr, xml_face)
# remove the surface handles now that the openings no longer need them
face_obj_ids.set('zoneHandle', '-1')
face_obj_ids.set('surfaceIndex', '-1')
# add the adjacency information
adjacency_faces = [face] if adjacency_faces is None else adjacency_faces
xml_face_adjs = ET.SubElement(xml_face, 'Adjacencies')
for adj_f_obj in adjacency_faces:
xml_face_adj = ET.SubElement(xml_face_adjs, 'Adjacency',
type=face_type, adjacencyDistance='0.000')
if isinstance(adj_f_obj.boundary_condition, Surface):
adj_face, adj_room = adj_f_obj.boundary_condition.boundary_condition_objects
_object_ids(xml_face_adj, '-1', '-1', '-1', adj_room, adj_face)
else: # add a ID object with all -1 for outdoors
_object_ids(xml_face_adj, '-1')
xml_adj_geos = ET.SubElement(xml_face_adj, 'AdjacencyPolygonList')
xml_adj_geo = ET.SubElement(xml_adj_geos, 'Polygon', auxiliaryType='-1')
if isinstance(adj_f_obj.boundary_condition, Surface):
_object_ids(xml_adj_geo, '-1') # add a meaningless ID object
else: # add an ID object referencing the self
_object_ids(xml_adj_geo, '-1', '0',
str(block_handle), str(zone_handle), str(dsb_face_i))
xml_adj_pts = ET.SubElement(xml_adj_geo, 'Vertices')
for pt in adj_f_obj.geometry.boundary:
xml_point = ET.SubElement(xml_adj_pts, 'Point3D')
xml_point.text = '{}; {}; {}'.format(pt.x, pt.y, pt.z)
xml_holes = ET.SubElement(xml_adj_geo, 'PolygonHoles')
if adj_f_obj.geometry.has_holes:
hole_inds = hole_is if hole_is is not None else [-1] * len(adj_f_obj.geometry.holes)
for hole, hole_i in zip(adj_f_obj.geometry.holes, hole_inds):
xml_hole = ET.SubElement(xml_holes, 'PolygonHole')
if isinstance(adj_f_obj.boundary_condition, Surface):
_object_ids(xml_hole, '-1') # add a meaningless ID object
else: # add an ID object referencing the self
_object_ids(xml_hole, '-1', '0', str(block_handle), zone_handle, str(hole_i))
xml_hole_pts = ET.SubElement(xml_hole, 'Vertices')
for pt in hole:
xml_point = ET.SubElement(xml_hole_pts, 'Point3D')
xml_point.text = '{}; {}; {}'.format(pt.x, pt.y, pt.z)
if reset_counter: # reset the counter back to 1 if requested
HANDLE_COUNTER = 1
return xml_face
[docs]
def room_to_dsbxml_element(
room, block_element=None, tolerance=0.01, angle_tolerance=1.0, reset_counter=True
):
"""Generate an dsbXML Zone Element object for a honeybee Room.
The resulting Element has all constituent geometry (Faces, Apertures, Doors).
Args:
room: A honeybee Room for which an dsbXML Zone Element object will be returned.
block_element: An optional XML Element for the BuildingBlock to which the
generated zone object will be added. If None, a new XML Element
will be generated. Note that this BuildingBlock element should
have a Zones tag already created within it.
tolerance: The absolute tolerance with which the Room geometry will
be evaluated. (Default: 0.01, suitable for objects in meters).
angle_tolerance: The angle tolerance at which the geometry will
be evaluated in degrees. (Default: 1 degree).
reset_counter: A boolean to note whether the global counter for unique
handles should be reset after the method is run. (Default: True).
"""
global HANDLE_COUNTER # declare that we will edit the global variable
# create the zone element
is_extrusion = room.is_extrusion(tolerance, angle_tolerance)
zone_id_attr = {
'parentZoneHandle': room.identifier,
'inheritedZoneHandle': room.identifier,
'planExtrusion': str(is_extrusion),
'innerSurfaceMode': 'Approximate' # TODO: eventually change to deflation
}
if block_element is not None:
block_zones_element = block_element.find('Zones')
xml_zone = ET.SubElement(block_zones_element, 'Zone', zone_id_attr)
obj_ids = block_element.find('ObjectIDs')
block_handle = obj_ids.get('handle')
else:
xml_zone = ET.Element('Zone', zone_id_attr)
block_handle = '-1'
# rebuild the faces with holes if any are found
if any(f.geometry.has_holes for f in room.faces):
rebuilt_face_3ds = []
for face in room.faces:
if face.geometry.has_holes:
flat_pt_face = Face3D(
face.geometry.vertices, plane=face.geometry.plane
)
rebuilt_face = flat_pt_face.separate_boundary_and_holes(tolerance)
face._geometry = rebuilt_face
rebuilt_face_3ds.append(rebuilt_face)
else:
rebuilt_face_3ds.append(face.geometry)
room._geometry = Polyface3D.from_faces(rebuilt_face_3ds, tolerance)
if not room._geometry.is_solid:
room._geometry = room._geometry.merge_overlapping_edges(tolerance)
# determine whether the room has multiple floor faces to merge
room_faces, room_geometry = room.faces, room.geometry
face_adjs = [None] * len(room_faces)
merge_faces = room.floors
if len(merge_faces) > 1:
if room.properties.designbuilder.floor_geometry is not None:
floor_geos = [room.properties.designbuilder.floor_geometry]
else:
f_geos = [f.geometry for f in merge_faces]
floor_geos = Face3D.join_coplanar_faces(f_geos, tolerance)
if len(floor_geos) != 0 and len(floor_geos) < len(merge_faces): # faces were merged
room_faces, face_adjs = [], []
apertures, doors = [], []
for f in merge_faces:
apertures.extend(f._apertures)
doors.extend(f._doors)
for new_geo in floor_geos:
if len(floor_geos) == 1:
prop_fs = merge_faces
else: # determine which of the faces corresponds to the merged one
prop_fs = []
for f in merge_faces:
f_pt = f._point_on_face(tolerance)
if new_geo.is_point_on_face(f_pt, tolerance):
prop_fs.append(f)
prop_f = prop_fs[0]
fbc = boundary_conditions.outdoors
nf = Face(prop_f.identifier, new_geo, prop_f.type, fbc)
for ap in apertures:
if nf.geometry.is_sub_face(ap.geometry, tolerance, angle_tolerance):
nf.add_aperture(ap)
for dr in doors:
if nf.geometry.is_sub_face(dr.geometry, tolerance, angle_tolerance):
nf.add_door(dr)
room_faces.append(nf)
face_adjs.append(prop_fs)
for f in room.faces:
if not isinstance(f.type, Floor):
room_faces.append(f)
face_adjs.append(None)
room_geometry = Polyface3D.from_faces(
tuple(face.geometry for face in room_faces), tolerance)
else:
floor_geos = [f.geometry for f in merge_faces]
# create the body of the room using the polyhedral vertices
hgt = round(room.max.z - room.min.z, 4)
xml_body = ET.SubElement(
xml_zone, 'Body', volume=str(room.volume), extrusionHeight=str(hgt))
_object_ids(xml_body, room.identifier, '0', block_handle)
xml_vertices = ET.SubElement(xml_body, 'Vertices')
for pt in room_geometry.vertices:
xml_point = ET.SubElement(xml_vertices, 'Point3D')
xml_point.text = '{}; {}; {}'.format(pt.x, pt.y, pt.z)
# add the surfaces
xml_faces = ET.SubElement(xml_body, 'Surfaces')
for face, fi, f_adj in zip(room_faces, room_geometry.face_indices, face_adjs):
face_to_dsbxml_element(
face, xml_body, fi, f_adj, tolerance, angle_tolerance, reset_counter=False
)
# if the room floor plate has holes, write them in the void perimeter list
xml_void = ET.SubElement(xml_body, 'VoidPerimeterList')
for fli, floor_g in enumerate(floor_geos):
if floor_g.has_holes:
flip_plane = floor_g.plane.flip() # flip to make holes clockwise
for hole in floor_g.holes:
xml_v_poly = ET.SubElement(xml_void, 'Polygon', auxiliaryType='-1')
_object_ids(xml_v_poly, '-1', surface=str(fli))
hole_face = Face3D(hole, plane=flip_plane)
for pt in hole_face.boundary:
xml_point = ET.SubElement(xml_v_poly, 'Point3D')
xml_point.text = '{}; {}; {}'.format(pt.x, pt.y, pt.z)
# add the other body attributes
xml_room_attr = ET.SubElement(xml_body, 'Attributes')
xml_room_name = ET.SubElement(xml_room_attr, 'Attribute', key='Title')
xml_room_name.text = str(room.display_name)
if room.user_data is not None and '__identifier__' in room.user_data:
xml_room_id = ET.SubElement(xml_room_attr, 'Attribute', key='ID')
xml_room_id.text = room.user_data['__identifier__']
# add an inner surface body that is a copy of the body
# TODO: consider offsetting the room polyface inwards to create this object
xml_in_body_section = ET.SubElement(xml_zone, 'InnerSurfaceBody')
xml_in_body = ET.SubElement(
xml_in_body_section, 'Body', volume=str(room.volume), extrusionHeight=str(hgt))
_object_ids(xml_in_body, room.identifier, '0', block_handle)
xml_in_vertices = ET.SubElement(xml_in_body, 'Vertices')
for pt in room_geometry.vertices:
xml_point = ET.SubElement(xml_in_vertices, 'Point3D')
xml_point.text = '{}; {}; {}'.format(pt.x, pt.y, pt.z)
xml_in_faces = ET.SubElement(xml_in_body, 'Surfaces')
for xml_face in xml_faces:
in_face = ET.SubElement(xml_in_faces, 'Surface', xml_face.attrib)
obj_ids = xml_face.find('ObjectIDs')
copied_obj_ids = deepcopy(obj_ids)
in_face.append(copied_obj_ids)
pt_i = xml_face.find('VertexIndices')
copied_pt_i = deepcopy(pt_i)
in_face.append(copied_pt_i)
hole_i = xml_face.find('HoleIndices')
copied_hole_i = deepcopy(hole_i)
in_face.append(copied_hole_i)
ET.SubElement(in_face, 'Openings')
ET.SubElement(in_face, 'Adjacencies')
ET.SubElement(in_face, 'Attributes')
in_xml_void = deepcopy(xml_void)
xml_in_body.append(in_xml_void)
ET.SubElement(xml_in_body, 'Attributes')
if reset_counter: # reset the counter back to 1 if requested
HANDLE_COUNTER = 1
return xml_zone
[docs]
def room_group_to_dsbxml_block(
room_group, block_handle, building_element=None, block_name=None,
tolerance=0.01, angle_tolerance=1.0, reset_counter=True
):
"""Generate an dsbXML BuildingBlock Element object for a list of honeybee Rooms.
The resulting Element has all geometry (Rooms, Faces, Apertures, Doors, Shades).
Args:
room_group: A list of honeybee Room objects for which an dsbXML
BuildingBlock Element object will be returned. Note that these rooms
must form a contiguous volume across their adjacencies for the
resulting block to be valid.
block_handle: An integer for the handle of the block. This must be unique
within the larger model.
building_element: An optional XML Element for the Building to which the
generated block object will be added. If None, a new XML Element
will be generated. Note that this Building element should
have a BuildingBlocks tag already created within it.
tolerance: The absolute tolerance with which the Room geometry will
be evaluated. (Default: 0.01, suitable for objects in meters).
angle_tolerance: The angle tolerance at which the geometry will
be evaluated in degrees. (Default: 1 degree).
reset_counter: A boolean to note whether the global counter for unique
handles should be reset after the method is run. (Default: True).
"""
global HANDLE_COUNTER # declare that we will edit the global variable
# get a room representing the fully-joined volume to be used for the block body
block_room = room_group[0].duplicate() if len(room_group) == 1 else \
Room.join_adjacent_rooms(room_group, tolerance)[0]
block_room.identifier = str(HANDLE_COUNTER)
HANDLE_COUNTER += 1
# create the block element
is_extrusion = block_room.is_extrusion(tolerance, angle_tolerance)
block_type = 'Plan extrusion' if is_extrusion else 'General'
hgt = round(block_room.max.z - block_room.min.z, 4)
block_id_attr = {
'type': block_type,
'height': str(hgt),
'roofSlope': '30.0000',
'roofOverlap': '0.0000',
'roofType': 'Gable',
'wallSlope': '80.0000'
}
if building_element is not None:
blocks_element = building_element.find('BuildingBlocks')
xml_block = ET.SubElement(blocks_element, 'BuildingBlock', block_id_attr)
else:
xml_block = ET.Element('Zone', block_id_attr)
# add the extra attributes that are typically empty
_object_ids(xml_block, str(block_handle), '0')
ET.SubElement(xml_block, 'ComponentBlocks')
ET.SubElement(xml_block, 'CFDFans')
ET.SubElement(xml_block, 'AssemblyInstances')
ET.SubElement(xml_block, 'ProfileOutlines')
ET.SubElement(xml_block, 'VoidBodies')
# gather horizontal floor boundaries for the rooms
floor_geos, floor_z_vals, ceil_z_vals, label_pts = [], [], [], []
for room in room_group:
if room.properties.designbuilder.floor_geometry is not None:
flr_geos = [room.properties.designbuilder.floor_geometry]
else:
flr_geos = room.horizontal_floor_boundaries(tolerance=tolerance)
floor_geos.extend(flr_geos)
floor_z_vals.extend([flr_geo.min.z for flr_geo in flr_geos])
ceil_z_vals.append(room.max.z)
# use the floor geometry to determine the room label point
if len(flr_geos) != 0:
label_pt = flr_geos[0].center if flr_geos[0].is_convex else \
flr_geos[0].pole_of_inaccessibility(0.01)
label_pts.append(label_pt)
else:
label_pts.append(room.geometry.center)
min_z, max_z = min(floor_z_vals), max(ceil_z_vals)
# join the flat floors of the rooms together to determine internal partitions
polygons, is_holes = [], []
for f_geo in floor_geos:
is_holes.append(False)
b_poly = Polygon2D(tuple(Point2D(pt.x, pt.y) for pt in f_geo.boundary))
polygons.append(b_poly)
if f_geo.has_holes:
for hole in f_geo.holes:
is_holes.append(True)
h_poly = Polygon2D(tuple(Point2D(pt.x, pt.y) for pt in hole))
polygons.append(h_poly)
if any(r.properties.designbuilder.floor_geometry is None for r in room_group):
polygons = [poly.remove_colinear_vertices(tolerance) for poly in polygons]
polygons = Polygon2D.intersect_polygon_segments(polygons, tolerance)
face_pts, flat_flr_geos = [], []
for poly, is_hole in zip(polygons, is_holes):
pt_3d = [Point3D(pt.x, pt.y, min_z) for pt in poly]
if not is_hole:
face_pts.append((pt_3d, []))
else:
face_pts[-1][1].append(pt_3d)
for f_pts in face_pts:
flat_flr_geos.append(Face3D(f_pts[0], holes=f_pts[1]))
flr_polyface = Polyface3D.from_faces(flat_flr_geos, tolerance)
# add internal partitions to the block
xml_partitions = ET.SubElement(xml_block, 'InternalPartitions')
part_height = max_z - min_z
for part_geo in flr_polyface.internal_edges:
p_min, p_max = part_geo.min, part_geo.max
p_min = Point2D(p_min.x, p_min.y)
p_max = Point2D(p_max.x, p_max.y)
# find the faces associated with the partition
rel_faces = []
for room in room_group:
for face in room:
f_min, f_max = face.min, face.max
f_min = Point2D(f_min.x, f_min.y)
f_max = Point2D(f_max.x, f_max.y)
if p_min.is_equivalent(f_min, tolerance) and \
p_max.is_equivalent(f_max, tolerance):
rel_faces.append(face)
# identify the two faces that coincide with the partition
match_faces = None
for i, i_face in enumerate(rel_faces):
if match_faces is not None:
break
if isinstance(i_face.boundary_condition, Surface):
for o_face in rel_faces[i + 1:]:
if isinstance(o_face.boundary_condition, Surface):
bc_obj = o_face.boundary_condition.boundary_condition_object
if i_face.identifier == bc_obj:
match_faces = (i_face, o_face)
break
# create the internal partition object
if match_faces is not None:
part_id = str(HANDLE_COUNTER)
HANDLE_COUNTER += 1
for face in match_faces:
if face.user_data is None:
face.user_data = {'partition_handle': part_id}
else:
face.user_data['partition_handle'] = part_id
part_type = 'Virtual' if isinstance(face.type, AirBoundary) else 'Solid'
part_id_attr = {
'type': part_type,
'height': str(part_height),
'area': str(part_height * part_geo.length),
'floatingPartition': 'False',
}
xml_part = ET.SubElement(xml_partitions, 'InternalPartition', part_id_attr)
_object_ids(xml_part, part_id, '0', str(block_handle))
st_pt, end_pt = part_geo.p1, part_geo.p2
xml_st_pt = ET.SubElement(xml_part, 'StartPoint')
xml_point = ET.SubElement(xml_st_pt, 'Point3D')
xml_point.text = '{}; {}; {}'.format(st_pt.x, st_pt.y, st_pt.z)
xml_end_pt = ET.SubElement(xml_part, 'EndPoint')
xml_point = ET.SubElement(xml_end_pt, 'Point3D')
xml_point.text = '{}; {}; {}'.format(end_pt.x, end_pt.y, end_pt.z)
# add the rooms to the block
ET.SubElement(xml_block, 'Zones')
for room, label_pt in zip(room_group, label_pts):
xml_room = room_to_dsbxml_element(
room, xml_block, tolerance, angle_tolerance, reset_counter=False
)
xml_label = ET.SubElement(xml_room, 'LabelPosition')
xml_label_pt = ET.SubElement(xml_label, 'Point3D')
xml_label_pt.text = '{}; {}; {}'.format(label_pt.x, label_pt.y, label_pt.z)
# process the faces of the block room to be formatted for a body
for f in block_room.faces:
face_matched = False
for room in room_group:
for f2 in room:
if f.identifier == f2.identifier:
f.user_data = {
'zone_handle': room.identifier,
'surface_index': f2.user_data['dsb_face_i']
}
face_matched = True
break
if face_matched:
break
else:
print('Failed to match the block Face: {}'.format(f.display_name))
f.remove_sub_faces()
f.identifier = str(HANDLE_COUNTER)
HANDLE_COUNTER += 1
# get a version of the block room with coplanar faces merged
blk_room = block_room.duplicate()
blk_room.merge_coplanar_faces(tolerance, angle_tolerance)
face_adjs = []
for nf in blk_room.faces:
nf_adj = []
for of in block_room.faces:
if nf.identifier == of.identifier:
nf_adj.append(of)
else:
f_pt = of.geometry._point_on_face(tolerance)
if nf.geometry.is_point_on_face(f_pt, tolerance):
nf_adj.append(of)
if len(nf_adj) != 0:
face_adjs.append(nf_adj)
else:
face_adjs.append(None)
# create the body of the block using the polyhedral vertices
xml_profile = ET.SubElement(
xml_block, 'ProfileBody', elementSlope='0.0000', roofOverlap='0.0000')
xml_body = ET.SubElement(
xml_profile, 'Body', volume=str(block_room.volume), extrusionHeight=str(hgt))
_object_ids(xml_body, block_room.identifier, '0', str(block_handle))
xml_vertices = ET.SubElement(xml_body, 'Vertices')
for pt in blk_room.geometry.vertices:
xml_point = ET.SubElement(xml_vertices, 'Point3D')
xml_point.text = '{}; {}; {}'.format(pt.x, pt.y, pt.z)
ET.SubElement(xml_body, 'Surfaces')
zip_obj = zip(blk_room.faces, blk_room.geometry.face_indices, face_adjs)
for face, fi, f_adj in zip_obj:
face_xml = face_to_dsbxml_element(
face, xml_body, fi, adjacency_faces=f_adj,
tolerance=tolerance, angle_tolerance=angle_tolerance, reset_counter=False
)
face_xml.set('defaultOpenings', 'True')
face_xml.set('thickness', '0.1')
f_obj_ids_xml = face_xml.find('ObjectIDs')
f_obj_ids_xml.set('zoneHandle', '-1')
f_obj_ids_xml.set('surfaceIndex', '-1')
adjs_xml = face_xml.find('Adjacencies')
if f_adj is None:
f_adj = [face] * len(adjs_xml)
for adj_xml, af in zip(adjs_xml, f_adj):
adj_xml.set('type', 'Floor')
in_adj_ids = adj_xml.find('ObjectIDs')
in_adj_ids.set('handle', '-1')
in_adj_ids.set('buildingHandle', '-1')
in_adj_ids.set('buildingBlockHandle', '-1')
in_adj_ids.set('zoneHandle', af.user_data['zone_handle'])
in_adj_ids.set('surfaceIndex', af.user_data['surface_index'])
polys_xml = adj_xml.find('AdjacencyPolygonList')
for poly_xml in polys_xml:
out_adj_ids = poly_xml.find('ObjectIDs')
out_adj_ids.set('handle', '-1')
out_adj_ids.set('buildingHandle', '-1')
out_adj_ids.set('buildingBlockHandle', '-1')
out_adj_ids.set('zoneHandle', '-1')
out_adj_ids.set('surfaceIndex', '-1')
# add the perimeter to the block
xml_perim = ET.SubElement(xml_block, 'Perimeter')
perim_geo = Room.grouped_horizontal_boundary(room_group, tolerance=tolerance)
if len(perim_geo) != 0:
perim_geo = perim_geo[0]
xml_perim_geo = ET.SubElement(xml_perim, 'Polygon', auxiliaryType='-1')
perim_handle = str(HANDLE_COUNTER)
HANDLE_COUNTER += 1
_object_ids(xml_perim_geo, perim_handle, '0',
str(block_handle), block_room.identifier)
xml_perim_pts = ET.SubElement(xml_perim_geo, 'Vertices')
for pt in perim_geo.boundary:
xml_point = ET.SubElement(xml_perim_pts, 'Point3D')
xml_point.text = '{}; {}; {}'.format(pt.x, pt.y, min_z)
xml_holes = ET.SubElement(xml_perim_geo, 'PolygonHoles')
if perim_geo.has_holes:
flip_plane = perim_geo.plane.flip() # flip to make holes clockwise
for hole in perim_geo.holes:
hole_face = Face3D(hole, plane=flip_plane)
xml_hole = ET.SubElement(xml_holes, 'PolygonHole')
_object_ids(xml_hole, '-1')
xml_hole_pts = ET.SubElement(xml_hole, 'Vertices')
for pt in hole_face:
xml_point = ET.SubElement(xml_hole_pts, 'Point3D')
xml_point.text = '{}; {}; {}'.format(pt.x, pt.y, min_z)
else:
msg = 'Failed to calculate perimeter around block: {}'.format(block_name)
print(msg)
# add the other properties that are usually empty
ET.SubElement(xml_body, 'VoidPerimeterList')
ET.SubElement(xml_body, 'Attributes')
ET.SubElement(xml_block, 'BaseProfileBody')
xml_block_attr = ET.SubElement(xml_block, 'Attributes')
xml_block_name = ET.SubElement(xml_block_attr, 'Attribute', key='Title')
xml_block_name.text = block_name if block_name is not None \
else 'Block {}'.format(block_handle)
if reset_counter: # reset the counter back to 1 if requested
HANDLE_COUNTER = 1
return xml_block
[docs]
def model_to_dsbxml_element(model, xml_template='Default'):
"""Generate an dsbXML Element object for a honeybee Model.
The resulting Element has all geometry (Rooms, Faces, Apertures, Doors, Shades).
Args:
model: A honeybee Model for which an dsbXML ElementTree object will be returned.
xml_template: Text for the type of template file to be used to write the
dsbXML. Different templates contain different amounts of default
assembly library data, which may be needed in order to import the
dsbXML into older versions of DesignBuilder. However, this data can
greatly increase the size of the resulting dsbXML file. Choose from
the following options.
* Default - a minimal file that imports into the latest versions
* Assembly - the Default plus an AssemblyLibrary with typical objects
* Full - a large file with all libraries that can be imported to version 7.3
"""
global HANDLE_COUNTER # declare that we will edit the global variable
# duplicate model to avoid mutating it as we edit it for INP export
original_model = model
model = model.duplicate()
# scale the model if the units are not feet
if model.units != 'Meters':
model.convert_to_units('Meters')
# remove degenerate geometry within DesignBuilder native tolerance
try:
model.remove_degenerate_geometry(0.01)
except ValueError:
error = 'Failed to remove degenerate Rooms.\nYour Model units system is: {}. ' \
'Is this correct?'.format(original_model.units)
raise ValueError(error)
# auto-assign stories if there are none since these are needed for blocks
if len(model.stories) == 0 and len(model.rooms) != 0:
model.assign_stories_by_floor_height(min_difference=2.0)
# erase room user data and use it to store attributes for later
for room in model.rooms:
room.user_data = {'__identifier__': room.identifier}
# reassign types for horizontal faces; remove any AirBoundaries that are not walls
z_axis = Vector3D(0, 0, 1)
for face in model.faces:
angle = math.degrees(z_axis.angle(face.normal))
if angle < 60:
face.type = face_types.roof_ceiling
elif angle >= 130:
face.type = face_types.floor
# set up the ElementTree for the XML
package_dir = os.path.dirname(os.path.abspath(__file__))
template_file = os.path.join(package_dir, '_templates', '{}.xml'.format(xml_template))
xml_tree = ET.parse(template_file)
xml_root = xml_tree.getroot()
model_name = clean_string(model.display_name)
xml_root.set('name', '~{}'.format(model_name))
xml_root.set('date', str(datetime.date.today()))
xml_root.set('version', DESIGNBUILDER_VERSION)
# add the site and the building
xml_site = xml_root.find('Site')
xml_bldgs = xml_site.find('Buildings')
xml_bldg = xml_bldgs.find('Building')
# group the model rooms by story and connected volume so they translate to blocks
block_rooms, block_names = [], []
story_rooms, story_names, _ = Room.group_by_story(model.rooms)
for flr_rooms, flr_name in zip(story_rooms, story_names):
adj_rooms = Room.group_by_adjacency(flr_rooms)
if len(adj_rooms) == 1:
block_rooms.append(flr_rooms)
block_names.append(flr_name)
else:
for i, adj_group in enumerate(adj_rooms):
block_rooms.append(adj_group)
block_names.append('{} {}'.format(flr_name, i + 1))
# give unique integers to each of the building blocks and faces
HANDLE_COUNTER = len(block_rooms) + 2
# convert identifiers to integers as this is the only ID format used by DesignBuilder
HANDLE_COUNTER = model.reset_ids_to_integers(start_integer=HANDLE_COUNTER)
HANDLE_COUNTER += 1
# translate each block to dsbXML; including all geometry
f_index_map = {} # create a map between the face handle the face index
xml_blocks = ET.SubElement(xml_bldg, 'BuildingBlocks')
for i, (room_group, block_name) in enumerate(zip(block_rooms, block_names)):
room_group_to_dsbxml_block(
room_group, i + 2, xml_bldg, block_name, reset_counter=False
)
for room in room_group:
for f in room:
f_index_map[f.identifier] = f.user_data['dsb_face_i']
# replace the face handle in the zone XML with the face index
for xml_block in xml_blocks:
xml_zones = xml_block.find('Zones')
for xml_zone in xml_zones:
xml_zone_body = xml_zone.find('Body')
for xml_srf in xml_zone_body.find('Surfaces'):
xml_adjs = xml_srf.find('Adjacencies')
for xml_adj in xml_adjs:
xml_adj_obj_ids = xml_adj.find('ObjectIDs')
xml_adj_face_id = xml_adj_obj_ids.get('surfaceIndex')
if xml_adj_face_id != '-1':
try:
xml_adj_obj_ids.set(
'surfaceIndex', f_index_map[xml_adj_face_id])
except KeyError: # invalid adjacency; remove the adjacency
xml_adj_obj_ids.set('surfaceIndex', '-1')
xml_adj_obj_ids.set('zoneHandle', '-1')
# translate all of the shade geometries into the Planes section
for shade in model.shades:
shade_to_dsbxml_element(shade, xml_bldg)
for shade_mesh in model.shade_meshes:
shade_mesh.triangulate_and_remove_degenerate_faces(model.tolerance)
shade_mesh_to_dsbxml_element(shade_mesh, xml_bldg, reset_counter=False)
# set the handle of the site to the last index and reset the counter
xml_site.set('handle', '1')
HANDLE_COUNTER = 1
return xml_root
[docs]
def model_to_dsbxml(model, xml_template='Default', program_name=None):
"""Generate an dsbXML string for a Model.
The resulting string will include all geometry (Rooms, Faces, Apertures,
Doors, Shades), all fully-detailed constructions + materials, all fully-detailed
schedules, and the room properties. It will also include the simulation
parameters. Essentially, the string includes everything needed to simulate
the model.
Args:
model: A honeybee Model for which an dsbXML text string will be returned.
xml_template: Text for the type of template file to be used to write the
dsbXML. Different templates contain different amounts of default
assembly library data, which may be needed in order to import the
dsbXML into older versions of DesignBuilder. However, this data can
greatly increase the size of the resulting dsbXML file. Choose from
the following options.
* Default - a minimal file that imports into the latest versions
* Assembly - the Default plus an AssemblyLibrary with typical objects
* Full - a large file with all libraries that can be imported to version 7.3
program_name: Optional text to set the name of the software that will
appear under a comment in the XML to identify where it is being exported
from. This can be set things like "Ladybug Tools" or "Pollination"
or some other software in which this dsbXML export capability is being
run. If None, no comment will appear. (Default: None).
Usage:
.. code-block:: python
import os
from honeybee.model import Model
from honeybee.room import Room
from honeybee.config import folders
# Crate an input Model
room = Room.from_box('Tiny House Zone', 5, 10, 3)
room.properties.energy.program_type = office_program
room.properties.energy.add_default_ideal_air()
model = Model('Tiny House', [room])
# create the dsbXML string for the model
xml_str = model.to.dsbxml(model)
# write the final string into an XML file using DesignBuilder encoding
dsbxml = os.path.join(folders.default_simulation_folder, 'in_dsb.xml')
with open(dsbxml, 'wb') as fp:
fp.write(xml_str.encode('iso-8859-15'))
"""
# create the XML string
xml_root = model_to_dsbxml_element(model, xml_template)
ET.indent(xml_root, '\t')
dsbxml_str = ET.tostring(
xml_root, encoding='unicode', xml_declaration=False
)
# add the declaration and a comment about the authoring program
prog_comment = ''
if program_name is not None:
prog_comment = '<!--File generated by {}-->\n'.format(program_name)
base_template = \
'<?xml version="1.0" encoding="ISO-8859-15" standalone="yes"?>' \
'\n{}'.format(prog_comment)
dsbxml_str = base_template + dsbxml_str
return dsbxml_str
[docs]
def model_to_dsbxml_file(model, output_file, xml_template='Default', program_name=None):
"""Write an dsbXML file from a Honeybee Model.
Note that this method also ensures that the resulting dsbXML file uses the
ISO-8859-15 encoding that is used by DesignBuilder.
Args:
model: A honeybee Model for which an dsbXML file will be written.
output_file: The path to the XML file that will be written from the model.
xml_template: Text for the type of template file to be used to write the
dsbXML. Different templates contain different amounts of default
assembly library data, which may be needed in order to import the
dsbXML into older versions of DesignBuilder. However, this data can
greatly increase the size of the resulting dsbXML file. Choose from
the following options.
* Default - a minimal file that imports into the latest versions
* Assembly - the Default plus an AssemblyLibrary with typical objects
* Full - a large file with all libraries that can be imported to version 7.3
program_name: Optional text to set the name of the software that will
appear under a comment in the XML to identify where it is being exported
from. This can be set things like "Ladybug Tools" or "Pollination"
or some other software in which this dsbXML export capability is being
run. If None, no comment will appear. (Default: None).
"""
# 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)
# get the string of the dsbXML file
xml_str = model_to_dsbxml(model, xml_template, program_name)
# write the string into the file and encode it in ISO-8859-15
with open(output_file, 'wb') as fp:
fp.write(xml_str.encode('iso-8859-15'))
return output_file
[docs]
def room_to_dsbxml(room):
"""Generate an dsbXML Zone string object for a honeybee Room.
The resulting string has all constituent geometry (Faces, Apertures, Doors).
Args:
room: A honeybee Room for which an dsbXML Zone string object will be returned.
"""
xml_root = room_to_dsbxml_element(room)
ET.indent(xml_root)
return ET.tostring(xml_root, encoding='unicode')
[docs]
def face_to_dsbxml(face):
"""Generate an dsbXML Surface string from a honeybee Face.
The resulting string has all constituent geometry (Apertures, Doors).
Args:
face: A honeybee Face for which an dsbXML Surface string object will
be returned.
"""
xml_root = face_to_dsbxml_element(face)
ET.indent(xml_root)
return ET.tostring(xml_root, encoding='unicode')
[docs]
def sub_face_to_dsbxml(sub_face):
"""Generate an dsbXML Opening string from a honeybee Aperture or Door.
Args:
sub_face: A honeybee Aperture or Door for which an dsbXML Opening XML
string will be returned.
"""
xml_root = sub_face_to_dsbxml_element(sub_face)
ET.indent(xml_root)
return ET.tostring(xml_root, encoding='unicode')
[docs]
def shade_to_dsbxml(shade):
"""Generate an dsbXML Plane string from a honeybee Shade.
Args:
shade: A honeybee Shade for which an dsbXML Plane XML string will
be returned.
"""
xml_root = shade_to_dsbxml_element(shade)
ET.indent(xml_root)
return ET.tostring(xml_root, encoding='unicode')
[docs]
def shade_mesh_to_dsbxml(shade_mesh):
"""Generate an dsbXML Planes string from a honeybee ShadeMesh.
Args:
shade_mesh: A honeybee ShadeMesh for which an dsbXML Planes XML string
will be returned.
"""
xml_root = shade_mesh_to_dsbxml_element(shade_mesh)
ET.indent(xml_root)
return ET.tostring(xml_root, encoding='unicode')
def _object_ids(
parent, handle,
building='-1', block='-1', zone='-1', surface='-1', opening='-1'
):
"""Create a sub element for DesignBuilder ObjectIDs."""
bldg_id_attr = {
'handle': handle,
'buildingHandle': building,
'buildingBlockHandle': block,
'zoneHandle': zone,
'surfaceIndex': surface,
'openingIndex': opening
}
return ET.SubElement(parent, 'ObjectIDs', bldg_id_attr)