Source code for honeybee_doe2.grouping

# coding=utf-8
"""Methods for grouping rooms to comply with INP rules."""
from __future__ import division
import math

from ladybug_geometry.geometry2d import Point2D, Polygon2D
from ladybug_geometry.geometry3d import Vector3D, Point3D, Face3D
from honeybee.typing import clean_doe2_string
from honeybee.room import Room

from .config import DOE2_TOLERANCE, FLOOR_LEVEL_TOL, RES_CHARS


[docs]def group_rooms_by_doe2_level(rooms, model_tolerance): """Group Honeybee Rooms according to acceptable floor levels in DOE-2. This means that not only will Rooms be on separate DOE-2 levels if their floor heights differ but also Rooms that share the same floor height but are disconnected from one another in plan will also be separate levels. For example, when the model is of two towers on a plinth, each tower will get its own separate level group. Args: rooms: A list of Honeybee Rooms to be grouped. model_tolerance: The tolerance of the model that the Rooms originated from. Returns: A tuple with three elements. - room_groups: A list of lists where each sub-list contains Honeybee Rooms that should be on the same DOE-2 level. - level_geometries: A list of Face3D with the same length as the room_groups, which contains the geometry that represents each floor level. These geometries will always be pointing upwards so that their vertices are counter-clockwise when viewed from above. They will also have colinear vertices removed such that they are ready to be translated to INP POLYGONS. - level_names: A list of text strings that align with the level geometry and contain suggested names for the DOE-2 levels. """ # set up lists of the outputs to be populated room_groups, level_geometries, level_names, existing_levels = [], [], [], {} # first group the rooms by floor height grouped_rooms, _ = Room.group_by_floor_height(rooms, FLOOR_LEVEL_TOL) for fi, room_group in enumerate(grouped_rooms): # determine a base name for the level using the story assigned to the rooms level_name = clean_doe2_string(room_group[0].story, RES_CHARS - 8) \ if room_group[0].story is not None else 'Level_{}'.format(fi) if level_name in existing_levels: existing_levels[level_name] += 1 level_name = level_name + str(existing_levels[level_name]) else: existing_levels[level_name] = 1 # then, group the rooms by contiguous horizontal boundary floor_geos = [] for room in room_group: if room.properties.doe2.space_polygon_geometry is not None: floor_geos.append(room.properties.doe2.space_polygon_geometry) else: try: flr_geo = room.horizontal_floor_boundaries(tolerance=model_tolerance) if len(flr_geo) == 0: # possible when Rooms have no floors flr_geo = room.horizontal_boundary(tolerance=model_tolerance) floor_geos.append(flr_geo) else: floor_geos.extend(flr_geo) except Exception: # level geometry is overlapping or not clean pass # join all of the floors into horizontal boundaries hor_bounds = _grouped_floor_boundary(floor_geos, model_tolerance) # if we got lucky and everything is one contiguous polygon, we're done! if len(hor_bounds) == 0: # we will write the story with NO-SHAPE room_groups.append(room_group) level_geometries.append(None) level_names.append(level_name) elif len(hor_bounds) == 1: # just one clean polygon for the level flr_geo = hor_bounds[0] flr_geo = flr_geo if flr_geo.normal.z >= 0 else flr_geo.flip() if flr_geo.has_holes: # remove holes as we only care about the boundary flr_geo = Face3D(flr_geo.boundary, flr_geo.plane) flr_geo = flr_geo.remove_colinear_vertices(tolerance=DOE2_TOLERANCE) room_groups.append(room_group) level_geometries.append(flr_geo) level_names.append(level_name) else: # we need to figure out which Room belongs to which geometry # first get a set of Point2Ds that are inside each room in plan room_pts, z_axis = [], Vector3D(0, 0, 1) for room in room_group: for face in room.faces: if math.degrees(z_axis.angle(face.normal)) >= 91: down_geo = face.geometry break room_pt3d = down_geo.center if down_geo.is_convex else \ down_geo.pole_of_inaccessibility(DOE2_TOLERANCE) room_pts.append(Point2D(room_pt3d.x, room_pt3d.y)) # loop through floor geometries and determine all rooms associated with them for si, flr_geo in enumerate(hor_bounds): flr_geo = flr_geo if flr_geo.normal.z >= 0 else flr_geo.flip() if flr_geo.has_holes: # remove holes as we only care about the boundary flr_geo = Face3D(flr_geo.boundary, flr_geo.plane) flr_geo = flr_geo.remove_colinear_vertices(tolerance=DOE2_TOLERANCE) flr_poly = Polygon2D([Point2D(pt.x, pt.y) for pt in flr_geo.boundary]) flr_rooms = [] for room, room_pt in zip(room_group, room_pts): if flr_poly.is_point_inside_bound_rect(room_pt): flr_rooms.append(room) room_groups.append(flr_rooms) level_geometries.append(flr_geo) level_names.append('{}_Section{}'.format(level_name, si)) # return all of the outputs return room_groups, level_geometries, level_names
[docs]def group_rooms_by_doe2_hvac(model, hvac_mapping): """Group Honeybee Rooms according to HVAC logic. Args: model: A Honeybee Model for which Rooms will be grouped for HVAC assignment. hvac_mapping: Text to indicate how HVAC systems should be assigned. Model will use only one HVAC system for the whole model and AssignedHVAC will follow how the HVAC systems have been assigned to the Rooms.properties.energy.hvac. Choose from the options below. * Room * Model * AssignedHVAC Returns: A tuple with three elements. - room_groups: A list of lists where each sub-list contains Honeybee Rooms that should ave the same HVAC system. - hvac_names: A list of text strings that align with the room_groups and contain suggested names for the DOE-2 HVAC systems. """ # clean up the hvac_mapping text hvac_mapping = hvac_mapping.upper().replace('-', '').replace(' ', '') # determine the mapping to be used if hvac_mapping == 'MODEL': hvac_name = clean_doe2_string('{}_Sys'.format(model.display_name), RES_CHARS) return [model.rooms], [hvac_name] elif hvac_mapping == 'ROOM': hvac_names = [clean_doe2_string('{}_Sys'.format(room.identifier), RES_CHARS) for room in model.rooms] room_groups = [[room] for room in model.rooms] else: # assume that it is the assigned HVAC hvac_dict = {} for room in model.rooms: if room.properties.energy.hvac is not None: hvac_id = room.properties.energy.hvac.display_name try: hvac_dict[hvac_id].append(room) except KeyError: # the first time that we are encountering the HVAC hvac_dict[hvac_id] = [room] else: try: hvac_dict['Unassigned'].append(room) except KeyError: # the first time that we have an unassigned room hvac_dict['Unassigned'] = [room] room_groups, hvac_names, existing_dict = [], [], {} for hvac_name, rooms in hvac_dict.items(): room_groups.append(rooms) hvac_doe2_name = clean_doe2_string(hvac_name, RES_CHARS - 2) if hvac_doe2_name in existing_dict: existing_dict[hvac_doe2_name] += 1 hvac_names.append(hvac_doe2_name + str(existing_dict[hvac_doe2_name])) else: existing_dict[hvac_doe2_name] = 1 hvac_names.append(hvac_doe2_name) return room_groups, hvac_names
def _grouped_floor_boundary(floor_geos, tolerance=0.01): """Get a list of Face3D for the boundary around several horizontal Face3Ds. Args: floor_geos: A list of Honeybee Rooms for which the horizontal boundary will be computed. tolerance: The maximum difference between coordinate values of two vertices at which they can be considered equivalent. (Default: 0.01, suitable for objects in meters). """ # remove colinear vertices and degenerate faces clean_floor_geos = [] for geo in floor_geos: try: clean_floor_geos.append(geo.remove_colinear_vertices(tolerance)) except AssertionError: # degenerate geometry to ignore pass if len(clean_floor_geos) == 0: return [] # no Room boundary to be found # convert the floor Face3Ds into counterclockwise Polygon2Ds floor_polys, z_vals = [], [] for flr_geo in clean_floor_geos: z_vals.append(flr_geo.min.z) b_poly = Polygon2D([Point2D(pt.x, pt.y) for pt in flr_geo.boundary]) floor_polys.append(b_poly) if flr_geo.has_holes: for hole in flr_geo.holes: h_poly = Polygon2D([Point2D(pt.x, pt.y) for pt in hole]) floor_polys.append(h_poly) z_min = min(z_vals) # find the joined intersected boundary closed_polys = Polygon2D.joined_intersected_boundary(floor_polys, tolerance) # remove colinear vertices from the resulting polygons clean_polys = [] for poly in closed_polys: try: clean_polys.append(poly.remove_colinear_vertices(tolerance)) except AssertionError: pass # degenerate polygon to ignore # figure out if polygons represent holes in the others and make Face3D if len(clean_polys) == 0: return [] elif len(clean_polys) == 1: # can be represented with a single Face3D pts3d = [Point3D(pt.x, pt.y, z_min) for pt in clean_polys[0]] return [Face3D(pts3d)] else: # need to separate holes from distinct Face3Ds bound_faces = [] for poly in clean_polys: pts3d = tuple(Point3D(pt.x, pt.y, z_min) for pt in poly) bound_faces.append(Face3D(pts3d)) return Face3D.merge_faces_to_holes(bound_faces, tolerance)