# coding: utf-8
"""Dragonfly Room2D."""
from __future__ import division
import math
from ladybug_geometry.geometry2d import Point2D, Vector2D, Ray2D, LineSegment2D, \
Polyline2D, Polygon2D
from ladybug_geometry.geometry3d import Point3D, Vector3D, Ray3D, LineSegment3D, \
Plane, Face3D, Polyface3D
from ladybug_geometry.intersection2d import closest_point2d_between_line2d
from ladybug_geometry.intersection3d import closest_point3d_on_line3d, \
closest_point3d_on_line3d_infinite, intersect_line3d_plane_infinite
import ladybug_geometry.boolean as pb
from ladybug_geometry_polyskel.polysplit import perimeter_core_subpolygons, \
perimeter_core_by_offset
from honeybee.typing import float_positive, clean_string, clean_and_id_string
import honeybee.boundarycondition as hbc
from honeybee.boundarycondition import boundary_conditions as bcs
from honeybee.boundarycondition import _BoundaryCondition, Outdoors, Surface, Ground
from honeybee.facetype import Floor, Wall, AirBoundary, RoofCeiling
from honeybee.facetype import face_types as ftyp
from honeybee.shade import Shade
from honeybee.door import Door
from honeybee.face import Face
from honeybee.room import Room
from ._base import _BaseGeometry
from .properties import Room2DProperties
import dragonfly.windowparameter as glzpar
from dragonfly.windowparameter import _WindowParameterBase, _AsymmetricBase, \
SimpleWindowRatio, RectangularWindows, DetailedWindows
import dragonfly.skylightparameter as skypar
from dragonfly.skylightparameter import _SkylightParameterBase, DetailedSkylights, \
GriddedSkylightArea, GriddedSkylightRatio
import dragonfly.shadingparameter as shdpar
from dragonfly.shadingparameter import _ShadingParameterBase
import dragonfly.writer.room2d as writer
[docs]class Room2D(_BaseGeometry):
"""A volume defined by an extruded floor plate, representing a single room or space.
Args:
identifier: Text string for a unique Room2D ID. Must be < 100 characters and
not contain any spaces or special characters.
floor_geometry: A single horizontal Face3D object representing the
floor plate of the Room. Note that this Face3D must be horizontal
to be valid.
floor_to_ceiling_height: A number for the height above the floor where the
ceiling begins. This should be in the same units system as the input
floor_geometry. Typical values range from 3 to 5 meters.
boundary_conditions: A list of boundary conditions that match the number of
segments in the input floor_geometry. These will be used to assign
boundary conditions to each of the walls of the Room in the resulting
model. If None, all boundary conditions will be Outdoors or Ground
depending on whether ceiling of the room is below 0 (the assumed
ground plane). Default: None.
window_parameters: A list of WindowParameter objects that dictate how the
window geometries will be generated for each of the walls. If None,
no windows will exist over the entire Room2D. Default: None.
shading_parameters: A list of ShadingParameter objects that dictate how the
shade geometries will be generated for each of the walls. If None,
no shades will exist over the entire Room2D. Default: None.
is_ground_contact: A boolean noting whether this Room2D has its floor
in contact with the ground. Default: False.
is_top_exposed: A boolean noting whether this Room2D has its ceiling
exposed to the outdoors. Default: False.
tolerance: The maximum difference between z values at which point vertices
are considered to be in the same horizontal plane. This is used to check
that all vertices of the input floor_geometry lie in the same horizontal
floor plane. Default is 0, which will not perform any check.
Properties:
* identifier
* display_name
* full_id
* floor_geometry
* floor_to_ceiling_height
* boundary_conditions
* window_parameters
* shading_parameters
* air_boundaries
* is_ground_contact
* is_top_exposed
* skylight_parameters
* parent
* has_parent
* floor_segments
* floor_segments_2d
* segment_count
* segment_normals
* floor_height
* ceiling_height
* volume
* floor_area
* exterior_wall_area
* exterior_aperture_area
* is_core
* is_perimeter
* min
* max
* center
* user_data
"""
__slots__ = ('_floor_geometry', '_segment_count', '_floor_to_ceiling_height',
'_boundary_conditions', '_window_parameters', '_shading_parameters',
'_air_boundaries', '_is_ground_contact', '_is_top_exposed',
'_skylight_parameters', '_parent', '_abridged_properties')
def __init__(self, identifier, floor_geometry, floor_to_ceiling_height,
boundary_conditions=None, window_parameters=None,
shading_parameters=None, is_ground_contact=False, is_top_exposed=False,
tolerance=0):
"""A volume defined by an extruded floor plate, representing a single room."""
_BaseGeometry.__init__(self, identifier) # process the identifier
# process the floor_geometry
assert isinstance(floor_geometry, Face3D), \
'Expected ladybug_geometry Face3D. Got {}'.format(type(floor_geometry))
if floor_geometry.normal.z >= 0: # ensure upward-facing Face3D
self._floor_geometry = floor_geometry
else:
self._floor_geometry = floor_geometry.flip()
# ensure a global 2D origin, which helps in solve adjacency and the dict schema
o_pl = Plane(Vector3D(0, 0, 1), Point3D(0, 0, self._floor_geometry.plane.o.z))
self._floor_geometry = Face3D(self._floor_geometry.boundary,
o_pl, self._floor_geometry.holes)
# check that the floor_geometry lies in the same horizontal plane.
if tolerance != 0:
z_vals = tuple(pt.z for pt in self._floor_geometry.vertices)
assert max(z_vals) - min(z_vals) <= tolerance, 'Not all of Room2D ' \
'"{}" vertices lie within the same horizontal plane.'.format(identifier)
# process segment count and floor-to-ceiling height
self._segment_count = len(self.floor_segments)
self.floor_to_ceiling_height = floor_to_ceiling_height
# process the boundary conditions
if boundary_conditions is None:
bc = bcs.outdoors if self.ceiling_height > 0 else bcs.ground
self._boundary_conditions = [bc] * len(self)
else:
value = self._check_wall_assigned_object(
boundary_conditions, 'boundary_conditions')
for val in value:
assert isinstance(val, _BoundaryCondition), \
'Expected BoundaryCondition. Got {}'.format(type(value))
self._boundary_conditions = value
# process the window and shading parameters
self.window_parameters = window_parameters
self.shading_parameters = shading_parameters
# ensure all wall-assigned objects align with the geometry if it has been flipped
if floor_geometry.normal.z < 0:
new_bcs, new_win_pars, new_shd_pars = Room2D._flip_wall_assigned_objects(
floor_geometry, self._boundary_conditions, self._window_parameters,
self._shading_parameters)
self._boundary_conditions = new_bcs
self._window_parameters = new_win_pars
self._shading_parameters = new_shd_pars
# process the top and bottom exposure properties
self.is_ground_contact = is_ground_contact
self.is_top_exposed = is_top_exposed
self._skylight_parameters = None
self._air_boundaries = None # will be set if it's ever used
self._parent = None # _parent will be set when Room2D is added to a Story
self._abridged_properties = None # will be set when originating from abridged
self._properties = Room2DProperties(self) # properties for extensions
[docs] @classmethod
def from_dict(cls, data, tolerance=0, persist_abridged=False):
"""Initialize a Room2D from a dictionary.
Args:
data: A dictionary representation of a Room2D object.
tolerance: The maximum difference between z values at which point vertices
are considered to be in the same horizontal plane. This is used to check
that all vertices of the input floor_geometry lie in the same horizontal
floor plane. Default is 0, which will not perform any check.
persist_abridged: Set to True when the properties of the Room2D dictionary
are abridged and you want to ensure that these exact same abridged
properties persist into the output of Room2D.to_dict(abridged=True).
It is useful when trying to edit the Room2D independently of a
Model and there are no plans to edit any extension properties of
the Room2D. THIS IS AN ADVANCED OPTION. (Default: False).
"""
# check the type of dictionary
assert data['type'] == 'Room2D', 'Expected Room2D dictionary. ' \
'Got {}.'.format(data['type'])
# re-assemble the floor_geometry
bound_verts = [Point3D(pt[0], pt[1], data['floor_height'])
for pt in data['floor_boundary']]
if 'floor_holes' in data:
hole_verts = [[Point3D(pt[0], pt[1], data['floor_height'])
for pt in hole] for hole in data['floor_holes']]
else:
hole_verts = None
floor_geometry = Face3D(bound_verts, None, hole_verts)
# re-assemble boundary conditions
if 'boundary_conditions' in data and data['boundary_conditions'] is not None:
b_conditions = []
for bc_dict in data['boundary_conditions']:
try:
bc_class = getattr(hbc, bc_dict['type'])
except AttributeError:
raise ValueError(
'Boundary condition "{}" is not supported in this honeybee '
'installation.'.format(bc_dict['type']))
b_conditions.append(bc_class.from_dict(bc_dict))
else:
b_conditions = None
# re-assemble window parameters
if 'window_parameters' in data and data['window_parameters'] is not None:
glz_pars = []
for i, glz_dict in enumerate(data['window_parameters']):
if glz_dict is not None:
if glz_dict['type'] == 'DetailedWindows':
segment = cls.floor_segment_by_index(floor_geometry, i)
glz_pars.append(DetailedWindows.from_dict(glz_dict, segment))
else:
try:
glz_class = getattr(glzpar, glz_dict['type'])
except AttributeError:
raise ValueError(
'Window parameter "{}" is not recognized.'.format(
glz_dict['type']))
glz_pars.append(glz_class.from_dict(glz_dict))
else:
glz_pars.append(None)
else:
glz_pars = None
# re-assemble shading parameters
if 'shading_parameters' in data and data['shading_parameters'] is not None:
shd_pars = []
for shd_dict in data['shading_parameters']:
if shd_dict is not None:
try:
shd_class = getattr(shdpar, shd_dict['type'])
except AttributeError:
raise ValueError(
'Shading parameter "{}" is not recognized.'.format(
shd_dict['type']))
shd_pars.append(shd_class.from_dict(shd_dict))
else:
shd_pars.append(None)
else:
shd_pars = None
# get the top and bottom exposure properties
grnd = data['is_ground_contact'] if 'is_ground_contact' in data else False
top = data['is_top_exposed'] if 'is_top_exposed' in data else False
# create the Room2D object
room = Room2D(data['identifier'], floor_geometry,
data['floor_to_ceiling_height'],
b_conditions, glz_pars, shd_pars, grnd, top, tolerance)
# assign any skylight parameters if they are specified
if 'skylight_parameters' in data and data['skylight_parameters'] is not None:
try:
sky_class = getattr(skypar, data['skylight_parameters']['type'])
except AttributeError:
raise ValueError(
'Skylight parameter "{}" is not recognized.'.format(
data['skylight_parameters']['type']))
room.skylight_parameters = sky_class.from_dict(data['skylight_parameters'])
# set all of the other optional properties
if 'air_boundaries' in data and data['air_boundaries'] is not None:
room.air_boundaries = data['air_boundaries']
if 'display_name' in data and data['display_name'] is not None:
room._display_name = data['display_name']
if 'user_data' in data and data['user_data'] is not None:
room.user_data = data['user_data']
if data['properties']['type'] == 'Room2DProperties':
room.properties._load_extension_attr_from_dict(data['properties'])
elif persist_abridged and \
data['properties']['type'] == 'Room2DPropertiesAbridged':
room._abridged_properties = data['properties']
return room
[docs] @classmethod
def from_honeybee(cls, room, tolerance):
"""Initialize a Room2D from a Honeybee Room.
Note that Dragonfly Room2Ds are abstractions of Honeybee Rooms and there
will be loss of information if the Honeybee Room is not an extruded floor
plate or if extension properties are assigned to individual Faces
or Apertures instead of at the Room level.
If the Honeybee Room contains no Floor Faces, None will be returned.
Args:
room: A Honeybee Room object.
tolerance: The maximum difference between values at which point vertices
are considered to be the same.
"""
# first get the floor_geometry for the Room2D using the horizontal boundary
flr_geo = room.horizontal_boundary(match_walls=True, tolerance=tolerance)
flr_geo = flr_geo if flr_geo.normal.z >= 0 else flr_geo.flip()
# match the segments of the floor geometry to walls of the Room
segs = flr_geo.boundary_segments if flr_geo.holes is None else \
flr_geo.boundary_segments + \
tuple(seg for hole in flr_geo.hole_segments for seg in hole)
boundary_conditions = [bcs.outdoors] * len(segs)
window_parameters = [None] * len(segs)
air_bounds = [False] * len(segs)
for i, seg in enumerate(segs):
wall_f = cls._segment_wall_face(room, seg, tolerance)
if wall_f is not None:
boundary_conditions[i] = wall_f.boundary_condition
if len(wall_f._apertures) != 0 or len(wall_f._doors) != 0:
sf_objs = wall_f._apertures + wall_f._doors
w_geos = [sf.geometry for sf in sf_objs]
is_drs = [isinstance(sf, Door) for sf in sf_objs]
if abs(wall_f.normal.z) <= 0.01: # vertical wall
window_parameters[i] = DetailedWindows.from_face3ds(
w_geos, seg, is_drs)
else: # angled wall; scale the Y to covert to vertical
w_p = Plane(Vector3D(seg.v.y, -seg.v.x, 0), seg.p, seg.v)
w3d = [Face3D([p.project(w_p.n, w_p.o) for p in geo.boundary])
for geo in w_geos]
window_parameters[i] = DetailedWindows.from_face3ds(
w3d, seg, is_drs)
if isinstance(wall_f.type, AirBoundary):
air_bounds[i] = True
# determine the ceiling height, and top/bottom boundary conditions
floor_to_ceiling_height = room.geometry.max.z - room.geometry.min.z
is_ground_contact = all([isinstance(f.boundary_condition, Ground)
for f in room.faces if isinstance(f.type, Floor)])
is_top_exposed = all([isinstance(f.boundary_condition, Outdoors)
for f in room.faces if isinstance(f.type, RoofCeiling)])
# create the Dragonfly Room2D
room_2d = cls(
room.identifier, flr_geo, floor_to_ceiling_height,
boundary_conditions, window_parameters, None,
is_ground_contact, is_top_exposed, tolerance)
# check if there are any skylights to be added
skylights, are_doors = [], []
for f in room.faces:
if isinstance(f.type, RoofCeiling):
sf_objs = f._apertures + f._doors
for sf in sf_objs:
verts2d = tuple(Point2D(pt.x, pt.y) for pt in sf.geometry.boundary)
skylights.append(Polygon2D(verts2d))
are_doors.append(isinstance(sf, Door))
if len(skylights) != 0:
room_2d.skylight_parameters = DetailedSkylights(skylights, are_doors)
# add the extra optional attributes
final_ab = []
for v, bc in zip(air_bounds, room_2d._boundary_conditions):
v_f = v if isinstance(bc, Surface) else False
final_ab.append(v_f)
room_2d.air_boundaries = final_ab
room_2d._display_name = room._display_name
room_2d._user_data = None if room.user_data is None else room.user_data.copy()
room_2d.properties.from_honeybee(room.properties)
return room_2d
[docs] @classmethod
def from_polygon(cls, identifier, polygon, floor_height, floor_to_ceiling_height,
boundary_conditions=None, window_parameters=None,
shading_parameters=None, is_ground_contact=False,
is_top_exposed=False):
"""Create a Room2D from a ladybug-geometry Polygon2D and a floor_height.
Note that this method is not recommended for a Room with one or more holes
(like a courtyard) since polygons cannot have holes within them.
Args:
identifier: Text string for a unique Room2D ID. Must be < 100 characters
and not contain any spaces or special characters.
polygon: A single Polygon2D object representing the floor plate of the Room.
floor_height: A float value to place the polygon within 3D space.
floor_to_ceiling_height: A number for the height above the floor where the
ceiling begins. Typical values range from 3 to 5 meters.
boundary_conditions: A list of boundary conditions that match the number of
segments in the input floor_geometry. These will be used to assign
boundary conditions to each of the walls of the Room in the resulting
model. If None, all boundary conditions will be Outdoors or Ground
depending on whether ceiling of the room is below 0 (the assumed
ground plane). Default: None.
window_parameters: A list of WindowParameter objects that dictate how the
window geometries will be generated for each of the walls. If None,
no windows will exist over the entire Room2D. Default: None.
shading_parameters: A list of ShadingParameter objects that dictate how the
shade geometries will be generated for each of the walls. If None,
no shades will exist over the entire Room2D. Default: None.
is_ground_contact: A boolean to note whether this Room2D has its floor
in contact with the ground. Default: False.
is_top_exposed: A boolean to note whether this Room2D has its ceiling
exposed to the outdoors. Default: False.
"""
# check the input polygon and ensure it's counter-clockwise
assert isinstance(polygon, Polygon2D), \
'Expected ladybug_geometry Polygon2D. Got {}'.format(type(polygon))
if polygon.is_clockwise:
polygon = polygon.reverse()
if boundary_conditions is not None:
boundary_conditions = list(reversed(boundary_conditions))
if window_parameters is not None:
new_win_pars = []
for seg, win_par in zip(polygon.segments, reversed(window_parameters)):
if isinstance(win_par, _AsymmetricBase):
new_win_pars.append(win_par.flip(seg.length))
else:
new_win_pars.append(win_par)
window_parameters = new_win_pars
if shading_parameters is not None:
shading_parameters = list(reversed(shading_parameters))
# build the Face3D without using right-hand rule to ensure alignment w/ bcs
base_plane = Plane(Vector3D(0, 0, 1), Point3D(0, 0, floor_height))
vert3d = tuple(base_plane.xy_to_xyz(_v) for _v in polygon.vertices)
floor_geometry = Face3D(vert3d, base_plane, enforce_right_hand=False)
return cls(identifier, floor_geometry, floor_to_ceiling_height,
boundary_conditions, window_parameters, shading_parameters,
is_ground_contact, is_top_exposed)
[docs] @classmethod
def from_vertices(cls, identifier, vertices, floor_height, floor_to_ceiling_height,
boundary_conditions=None, window_parameters=None,
shading_parameters=None, is_ground_contact=False,
is_top_exposed=False):
"""Create a Room2D from 2D vertices with each vertex as an iterable of 2 floats.
Note that this method is not recommended for a Room with one or more holes
(like a courtyard) since the distinction between hole vertices and boundary
vertices cannot be derived from a single list of vertices.
Args:
identifier: Text string for a unique Room2D ID. Must be < 100 characters
and not contain any spaces or special characters.
vertices: A flattened list of 2 or more vertices as (x, y) that trace
the outline of the floor plate.
floor_height: A float value to place the polygon within 3D space.
floor_to_ceiling_height: A number for the height above the floor where the
ceiling begins. Typical values range from 3 to 5 meters.
boundary_conditions: A list of boundary conditions that match the number of
segments in the input floor_geometry. These will be used to assign
boundary conditions to each of the walls of the Room in the resulting
model. If None, all boundary conditions will be Outdoors or Ground
depending on whether ceiling of the room is below 0 (the assumed
ground plane). Default: None.
window_parameters: A list of WindowParameter objects that dictate how the
window geometries will be generated for each of the walls. If None,
no windows will exist over the entire Room2D. Default: None.
shading_parameters: A list of ShadingParameter objects that dictate how the
shade geometries will be generated for each of the walls. If None,
no shades will exist over the entire Room2D. Default: None.
is_ground_contact: A boolean to note whether this Room2D has its floor
in contact with the ground. Default: False.
is_top_exposed: A boolean to note whether this Room2D has its ceiling
exposed to the outdoors. Default: False.
"""
polygon = Polygon2D(tuple(Point2D(*v) for v in vertices))
return cls.from_polygon(
identifier, polygon, floor_height, floor_to_ceiling_height,
boundary_conditions, window_parameters, shading_parameters,
is_ground_contact, is_top_exposed)
@property
def floor_geometry(self):
"""A horizontal Face3D object representing the floor plate of the Room."""
return self._floor_geometry
@property
def floor_to_ceiling_height(self):
"""Get or set a number for the distance between the floor and the ceiling."""
return self._floor_to_ceiling_height
@floor_to_ceiling_height.setter
def floor_to_ceiling_height(self, value):
self._floor_to_ceiling_height = float_positive(value, 'floor-to-ceiling height')
assert self._floor_to_ceiling_height != 0, 'Room2D floor-to-ceiling height ' \
'cannot be zero.'
@property
def boundary_conditions(self):
"""Get or set a tuple of boundary conditions for the wall boundary conditions."""
return tuple(self._boundary_conditions)
@boundary_conditions.setter
def boundary_conditions(self, value):
value = self._check_wall_assigned_object(value, 'boundary conditions')
for val, glz in zip(value, self._window_parameters):
assert val in bcs, 'Expected BoundaryCondition. Got {}'.format(type(value))
if glz is not None:
assert isinstance(val, (Outdoors, Surface)), \
'{} cannot be assigned to a wall with windows.'.format(val)
self._boundary_conditions = value
@property
def window_parameters(self):
"""Get or set a tuple of WindowParameters describing how to generate windows.
"""
return tuple(self._window_parameters)
@window_parameters.setter
def window_parameters(self, value):
if value is not None:
value = self._check_wall_assigned_object(value, 'window_parameters')
for val, bc in zip(value, self._boundary_conditions):
if val is not None:
assert isinstance(val, _WindowParameterBase), \
'Expected Window Parameters. Got {}'.format(type(value))
assert isinstance(bc, (Outdoors, Surface)), \
'{} cannot be assigned to a wall with windows.'.format(bc)
self._window_parameters = value
else:
self._window_parameters = [None for i in range(len(self))]
@property
def shading_parameters(self):
"""Get or set a tuple of ShadingParameters describing how to generate shades.
"""
return tuple(self._shading_parameters)
@shading_parameters.setter
def shading_parameters(self, value):
if value is not None:
value = self._check_wall_assigned_object(value, 'shading_parameters')
for val in value:
if val is not None:
assert isinstance(val, _ShadingParameterBase), \
'Expected Shading Parameters. Got {}'.format(type(value))
self._shading_parameters = value
else:
self._shading_parameters = [None for i in range(len(self))]
@property
def air_boundaries(self):
"""Get or set a tuple of booleans for whether each wall has an air boundary type.
False values indicate a standard opaque type while True values indicate
an AirBoundary type. All walls will be False by default. Note that any
walls with a True air boundary must have a Surface boundary condition
without any windows.
"""
if self._air_boundaries is None:
self._air_boundaries = [False] * len(self)
return tuple(self._air_boundaries)
@air_boundaries.setter
def air_boundaries(self, value):
if value is not None:
value = self._check_wall_assigned_object(value, 'air boundaries')
value = [bool(val) for val in value]
all_props = zip(value, self._boundary_conditions, self._window_parameters)
for val, bnd, glz in all_props:
if val:
assert isinstance(bnd, Surface), 'Air boundaries must be assigned ' \
'to walls with Surface boundary conditions. Not {}.'.format(bnd)
assert glz is None, \
'Air boundaries cannot be assigned to a wall with windows.'
self._air_boundaries = value
@property
def is_ground_contact(self):
"""Get or set a boolean noting whether the floor is in contact with the ground.
"""
return self._is_ground_contact
@is_ground_contact.setter
def is_ground_contact(self, value):
self._is_ground_contact = bool(value)
@property
def is_top_exposed(self):
"""Get or set a boolean noting whether the ceiling is exposed to the outdoors.
"""
return self._is_top_exposed
@is_top_exposed.setter
def is_top_exposed(self, value):
self._is_top_exposed = bool(value)
@property
def skylight_parameters(self):
"""Get or set SkylightParameters describing how to generate skylights.
"""
return self._skylight_parameters
@skylight_parameters.setter
def skylight_parameters(self, value):
if value is not None:
assert isinstance(value, _SkylightParameterBase), \
'Expected Skylight Parameters. Got {}'.format(type(value))
self._skylight_parameters = value
@property
def parent(self):
"""Get the parent Story if it is assigned. None if it is not assigned."""
return self._parent
@property
def has_parent(self):
"""Get a boolean noting whether this Room2D has a parent Story."""
return self._parent is not None
@property
def floor_segments(self):
"""Get a list of LineSegment3D objects for each wall of the Room."""
return self._floor_geometry.boundary_segments if self._floor_geometry.holes is \
None else self._floor_geometry.boundary_segments + \
tuple(seg for hole in self._floor_geometry.hole_segments for seg in hole)
@property
def floor_segments_2d(self):
"""Get a list of LineSegment2D objects for each wall of the Room."""
return self._floor_geometry.boundary_polygon2d.segments if \
self._floor_geometry.holes is None else \
self._floor_geometry.boundary_polygon2d.segments + \
tuple(seg for hole in self._floor_geometry.hole_polygon2d
for seg in hole.segments)
@property
def segment_count(self):
"""Get the number of segments making up the floor geometry.
This is equal to the number of walls making up the Room.
"""
return self._segment_count
@property
def segment_normals(self):
"""Get a list of Vector2D objects for the normal of each segment."""
return [Vector2D(seg.v.y, -seg.v.x).normalize() for seg in self.floor_segments]
@property
def floor_height(self):
"""Get a number for the height of the floor above the ground."""
return self._floor_geometry[0].z
@property
def ceiling_height(self):
"""Get a number for the height of the ceiling above the ground."""
return self.floor_height + self.floor_to_ceiling_height
@property
def volume(self):
"""Get a number for the volume of the Room."""
return self.floor_area * self.floor_to_ceiling_height
@property
def floor_area(self):
"""Get a number for the floor area of the Room."""
return self._floor_geometry.area
@property
def exterior_wall_area(self):
"""Get a the total wall area of the Room with an Outdoors boundary condition.
"""
wall_areas = []
for seg, bc in zip(self.floor_segments, self._boundary_conditions):
if isinstance(bc, Outdoors):
wall_areas.append(seg.length * self.floor_to_ceiling_height)
return sum(wall_areas)
@property
def interior_wall_area(self):
"""Get a the total wall area of the Room without an Outdoors or Ground BC.
"""
wall_areas = []
for seg, bc in zip(self.floor_segments, self._boundary_conditions):
if not isinstance(bc, (Outdoors, Ground)):
wall_areas.append(seg.length * self.floor_to_ceiling_height)
return sum(wall_areas)
@property
def exterior_aperture_area(self):
"""Get a the total aperture area of the Room with an Outdoors boundary condition.
"""
glz_areas = []
for seg, bc, glz in zip(self.floor_segments, self._boundary_conditions,
self._window_parameters):
if isinstance(bc, Outdoors) and glz is not None:
area = glz.area_from_segment(seg, self.floor_to_ceiling_height)
glz_areas.append(area)
return sum(glz_areas)
@property
def is_core(self):
"""Get a boolean for whether the Room2D is in the core of a story.
Core Room2Ds have no walls exposed to the outdoors.
"""
return self.exterior_wall_area == 0
@property
def is_perimeter(self):
"""Get a boolean for whether the Room2D is on the perimeter of a story.
Perimeter Room2Ds have walls exposed to the outdoors.
"""
return self.exterior_wall_area != 0
@property
def min(self):
"""Get a Point2D for the min bounding rectangle vertex in the XY plane.
This is useful in calculations to determine if this Room2D is in proximity
to other Room2Ds.
"""
return self._floor_geometry.boundary_polygon2d.min
@property
def max(self):
"""Get a Point2D for the max bounding rectangle vertex in the XY plane.
This is useful in calculations to determine if this Room2D is in proximity
to other Room2Ds.
"""
return self._floor_geometry.boundary_polygon2d.max
@property
def center(self):
"""Get a Point2D for the center bounding rectangle vertex in the XY plane.
This is useful in calculations to determine if this Room2D is inside
other polygons.
"""
return self._floor_geometry.boundary_polygon2d.center
[docs] def label_point(self, tolerance=0.01):
"""Get a Point3D to label this Room2D in 3D space.
This point will always lie within the polygon formed by the floor_geometry
regardless of whether this geometry is concave or has holes.
Args:
tolerance: The tolerance to which the pole_of_inaccessibility will
be computed in the event that the floor_geometry is concave or
has holes. Note that this does not need to be equal to the Model
tolerance and should usually be larger than the Model tolerance
to avoid long calculation times. (Default: 0.01).
"""
return self.floor_geometry.center if self.floor_geometry.is_convex else \
self.floor_geometry.pole_of_inaccessibility(tolerance)
[docs] def segment_orientations(self, north_vector=Vector2D(0, 1)):
"""A list of numbers between 0 and 360 for the orientation of the segments.
0 = North, 90 = East, 180 = South, 270 = West
Args:
north_vector: A ladybug_geometry Vector2D for the north direction.
Default is the Y-axis (0, 1).
"""
normals = (Vector2D(sg.v.y, -sg.v.x) for sg in self.floor_segments)
return [math.degrees(north_vector.angle_clockwise(norm)) for norm in normals]
[docs] def set_outdoor_window_parameters(self, window_parameter):
"""Set all of the outdoor walls to have the same window parameters."""
assert isinstance(window_parameter, _WindowParameterBase), \
'Expected Window Parameters. Got {}'.format(type(window_parameter))
glz_ps = []
for bc in self._boundary_conditions:
glz_p = window_parameter if isinstance(bc, Outdoors) else None
glz_ps.append(glz_p)
self._window_parameters = glz_ps
[docs] def set_outdoor_shading_parameters(self, shading_parameter):
"""Set all of the outdoor walls to have the same shading parameters."""
assert isinstance(shading_parameter, _ShadingParameterBase), \
'Expected Window Parameters. Got {}'.format(type(shading_parameter))
shd_ps = []
for bc in self._boundary_conditions:
shd_p = shading_parameter if isinstance(bc, Outdoors) else None
shd_ps.append(shd_p)
self._shading_parameters = shd_ps
[docs] def to_rectangular_windows(self):
"""Convert all of the windows of the Room2D to the RectangularWindows format."""
glz_ps = []
for seg, glz in zip(self.floor_segments, self._window_parameters):
glz_p = None
if glz is not None:
glz_p = glz.to_rectangular_windows(seg, self.floor_to_ceiling_height)
glz_ps.append(glz_p)
self._window_parameters = glz_ps
[docs] def add_prefix(self, prefix):
"""Change the identifier of this object by inserting a prefix.
This is particularly useful in workflows where you duplicate and edit
a starting object and then want to combine it with the original object
into one Model (like making a model of repeated rooms) since all objects
within a Model must have unique identifiers.
Args:
prefix: Text that will be inserted at the start of this object's
(and child segments') identifier and display_name. It is recommended
that this prefix be short to avoid maxing out the 100 allowable
characters for dragonfly identifiers.
"""
self._identifier = clean_string('{}_{}'.format(prefix, self.identifier))
if self._display_name is not None:
self.display_name = '{}_{}'.format(prefix, self.display_name)
self.properties.add_prefix(prefix)
for i, bc in enumerate(self._boundary_conditions):
if isinstance(bc, Surface):
new_face_id = '{}_{}'.format(prefix, bc.boundary_condition_objects[0])
new_room_id = '{}_{}'.format(prefix, bc.boundary_condition_objects[1])
self._boundary_conditions[i] = \
Surface((new_face_id, new_room_id))
[docs] def generate_grid(self, x_dim, y_dim=None, offset=1.0):
"""Get a gridded Mesh3D object offset from the floor of this room.
Note that the x_dim and y_dim refer to dimensions within the XY coordinate
system of the floor Faces's plane. So rotating the planes of the floor geometry
will result in rotated grid cells.
Args:
x_dim: The x dimension of the grid cells as a number.
y_dim: The y dimension of the grid cells as a number. Default is None,
which will assume the same cell dimension for y as is set for x.
offset: A number for how far to offset the grid from the base face.
Default is 1.0, which will not offset the grid to be 1 unit above
the floor.
"""
return self.floor_geometry.mesh_grid(x_dim, y_dim, offset, False)
[docs] def set_adjacency(
self, other_room_2d, self_seg_index, other_seg_index,
resolve_window_conflicts=True):
"""Set a segment of this Room2D to be adjacent to another and vice versa.
Note that, adjacent segments must possess matching WindowParameters in
order to be valid.
Args:
other_room_2d: Another Room2D object to be set adjacent to this one.
self_seg_index: An integer for the wall segment of this Room2D that
will be set adjacent to the other_room_2d.
other_seg_index:An integer for the wall segment of the other_room_2d
that will be set adjacent to this Room2D.
resolve_window_conflicts: Boolean to note whether conflicts between
window parameters of adjacent segments should be resolved during
adjacency setting or an error should be raised about the mismatch.
Resolving conflicts will default to the window parameters with the
larger are and assign them to the other segment. (Default: True).
"""
assert isinstance(other_room_2d, Room2D), \
'Expected dragonfly Room2D. Got {}.'.format(type(other_room_2d))
# set the boundary conditions of the segments
ids_1 = ('{}..Face{}'.format(self.identifier, self_seg_index + 1),
self.identifier)
ids_2 = ('{}..Face{}'.format(other_room_2d.identifier, other_seg_index + 1),
other_room_2d.identifier)
self._boundary_conditions[self_seg_index] = Surface(ids_2)
other_room_2d._boundary_conditions[other_seg_index] = Surface(ids_1)
# check that the window parameters match between segments
wp1 = self._window_parameters[self_seg_index]
wp2 = other_room_2d._window_parameters[other_seg_index]
if wp1 is not None or wp2 is not None:
if wp1 != wp2 or isinstance(wp1, DetailedWindows):
if resolve_window_conflicts:
ftc1 = self.floor_to_ceiling_height
ftc2 = other_room_2d.floor_to_ceiling_height
min_ftc = min((ftc1, ftc2))
seg1 = self.floor_segments[self_seg_index]
a1 = wp1.area_from_segment(seg1, min_ftc) if wp1 is not None else 0
seg2 = other_room_2d.floor_segments[other_seg_index]
a2 = wp2.area_from_segment(seg2, min_ftc) if wp2 is not None else 0
if a1 > a2:
other_room_2d._window_parameters[other_seg_index] = \
wp1.flip(seg2.length) if isinstance(wp1, _AsymmetricBase) \
else wp1
else:
self._window_parameters[self_seg_index] = wp2.flip(seg1.length) \
if isinstance(wp2, _AsymmetricBase) else wp2
else:
if wp1 != wp2:
msg = 'Window parameters do not match between adjacent ' \
'Rooms "{}" and "{}".'.format(
self.identifier, other_room_2d.identifier)
raise AssertionError(msg)
[docs] def set_boundary_condition(self, seg_index, boundary_condition):
"""Set a single segment of this Room2D to have a certain boundary condition.
Args:
seg_index: An integer for the wall segment of this Room2D for which
the boundary condition will be set.
boundary_condition: A boundary condition object.
"""
assert boundary_condition in bcs, \
'Expected boundary condition. Got {}.'.format(type(boundary_condition))
if self._window_parameters[seg_index] is not None:
assert isinstance(boundary_condition, (Outdoors, Surface)), '{} cannot be ' \
'assigned to a wall with windows.'.format(boundary_condition)
self._boundary_conditions[seg_index] = boundary_condition
[docs] def set_air_boundary(self, seg_index):
"""Set a single segment of this Room2D to have an air boundary type.
Args:
seg_index: An integer for the wall segment of this Room2D for which
the boundary condition will be set.
"""
self.air_boundaries # trigger generation of values if they don't exist
assert self._window_parameters[seg_index] is None, \
'Air boundaries cannot be assigned to a wall with windows.'
assert isinstance(self._boundary_conditions[seg_index], Surface), \
'Air boundaries must be assigned to walls with Surface boundary conditions.'
self._air_boundaries[seg_index] = True
[docs] def set_window_parameter(self, seg_index, window_parameter=None):
"""Set a single segment of this Room2D to have a certain window parameter.
Args:
seg_index: An integer for the wall segment of this Room2D for which
the window parameter will be set.
window_parameter: A window parameter object to be assigned to the segment.
If None, any existing WindowParameters assigned to the segment
will be removed. (Default: None).
"""
if window_parameter is not None:
assert isinstance(window_parameter, _WindowParameterBase), \
'Expected Window Parameters. Got {}'.format(type(window_parameter))
accept_bc = (Outdoors, Surface)
assert isinstance(self._boundary_conditions[seg_index], accept_bc), \
'Windows cannot be assigned to a wall with {} boundary ' \
'condition.'.format(self._boundary_conditions[seg_index])
self._window_parameters[seg_index] = window_parameter
[docs] def offset_skylight_parameters(self, offset_distance=0.05, tolerance=0.01):
"""Offset detailed skylights so all vertices are inside the Room2D.
Args:
offset_distance: Distance from the edge of the room that
the polygons will be offset to. (Default: 0.05, suitable for
objects in meters).
tolerance: The maximum difference between point values for them to be
considered distinct. (Default: 0.01, suitable for objects in meters).
"""
if isinstance(self._skylight_parameters, DetailedSkylights):
self._skylight_parameters.offset_polygons_for_face(
self.floor_geometry, offset_distance, tolerance)
[docs] def reset_adjacency(self):
"""Set all Surface boundary conditions of this Room2D to be Outdoors."""
for i, bc in enumerate(self._boundary_conditions):
if isinstance(bc, Surface):
self._boundary_conditions[i] = bcs.outdoors
[docs] def move(self, moving_vec):
"""Move this Room2D along a vector.
Args:
moving_vec: A ladybug_geometry Vector3D with the direction and distance
to move the room.
"""
self._floor_geometry = self._floor_geometry.move(moving_vec)
if isinstance(self._skylight_parameters, DetailedSkylights):
self._skylight_parameters = self._skylight_parameters.move(moving_vec)
self.properties.move(moving_vec)
[docs] def rotate_xy(self, angle, origin):
"""Rotate this Room2D counterclockwise in the XY plane by a certain angle.
Args:
angle: An angle in degrees.
origin: A ladybug_geometry Point3D for the origin around which the
object will be rotated.
"""
self._floor_geometry = self._floor_geometry.rotate_xy(
math.radians(angle), origin)
if isinstance(self._skylight_parameters, DetailedSkylights):
self._skylight_parameters = self._skylight_parameters.rotate(angle, origin)
self.properties.rotate_xy(angle, origin)
[docs] def reflect(self, plane):
"""Reflect this Room2D across a plane.
Args:
plane: A ladybug_geometry Plane across which the object will be reflected.
"""
assert plane.n.z == 0, \
'Plane normal must be in XY plane to use it on Room2D.reflect.'
self._floor_geometry = self._floor_geometry.reflect(plane.n, plane.o)
if self._floor_geometry.normal.z < 0: # ensure upward-facing Face3D
new_bcs, new_win_pars, new_shd_pars = Room2D._flip_wall_assigned_objects(
self._floor_geometry, self._boundary_conditions,
self._window_parameters, self._shading_parameters)
self._boundary_conditions = new_bcs
self._window_parameters = new_win_pars
self._shading_parameters = new_shd_pars
self._floor_geometry = self._floor_geometry.flip()
if isinstance(self._skylight_parameters, DetailedSkylights):
self._skylight_parameters = self._skylight_parameters.reflect(plane)
self.properties.reflect(plane)
[docs] def scale(self, factor, origin=None):
"""Scale this Room2D by a factor from an origin point.
Note that this will scale both the Room2D geometry and the WindowParameters
and FacadeParameters assigned to this Room2D.
Args:
factor: A number representing how much the object should be scaled.
origin: A ladybug_geometry Point3D representing the origin from which
to scale. If None, it will be scaled from the World origin (0, 0, 0).
"""
# scale the Room2D geometry
self._floor_geometry = self._floor_geometry.scale(factor, origin)
self._floor_to_ceiling_height = self._floor_to_ceiling_height * factor
# scale the window parameters
for i, win_par in enumerate(self._window_parameters):
if win_par is not None:
self._window_parameters[i] = win_par.scale(factor)
# scale the shading parameters
for i, shd_par in enumerate(self._shading_parameters):
if shd_par is not None:
self._shading_parameters[i] = shd_par.scale(factor)
# scale the skylight parameters
if self._skylight_parameters is not None:
self._skylight_parameters = self._skylight_parameters.scale(factor, origin) \
if isinstance(self._skylight_parameters, DetailedSkylights) else \
self._skylight_parameters.scale(factor)
self.properties.scale(factor, origin)
[docs] def snap_to_grid(self, grid_increment):
"""Snap this Room2D's vertices to the nearest grid node defined by an increment.
All properties assigned to the Room2D will be preserved and the number of
vertices will remain constant. This means that this method can often create
duplicate vertices and it might be desirable to run the remove_duplicate_vertices
method after running this one.
Args:
grid_increment: A positive number for dimension of each grid cell. This
typically should be equal to the tolerance or larger but should
not be larger than the smallest detail of the Room2D that you
wish to resolve.
"""
# loop through the vertices and snap them
new_boundary, new_holes = [], None
for pt in self._floor_geometry.boundary:
new_x = grid_increment * round(pt.x / grid_increment)
new_y = grid_increment * round(pt.y / grid_increment)
new_boundary.append(Point3D(new_x, new_y, pt.z))
if self._floor_geometry.holes is not None:
new_holes = []
for hole in self._floor_geometry.holes:
new_hole = []
for pt in hole:
new_x = grid_increment * round(pt.x / grid_increment)
new_y = grid_increment * round(pt.y / grid_increment)
new_hole.append(Point3D(new_x, new_y, pt.z))
new_holes.append(new_hole)
# rebuild the new floor geometry and assign it to the Room2D
self._floor_geometry = Face3D(
new_boundary, self._floor_geometry.plane, new_holes)
[docs] def align(self, line_ray, distance):
"""Move any Room2D vertices within a given distance of a line to be on that line.
This is useful to clean up cases where wall segments have a lot of
zig zags in them.
All properties assigned to the Room2D will be preserved and the number of
vertices will remain constant. This means that this method can often create
duplicate vertices and it might be desirable to run the remove_duplicate_vertices
method after running this one.
Args:
line_ray: A ladybug_geometry Ray2D or LineSegment2D to which the Room2D
vertices will be aligned. Ray2Ds will be interpreted as being infinite
in both directions while LineSegment2Ds will be interpreted as only
existing between two points.
distance: The maximum distance between a vertex and the line_ray where
the vertex will be moved to lie on the line_ray. Vertices beyond
this distance will be left as they are.
"""
# create a 3D version of the line_ray for the closest point calculation
if isinstance(line_ray, Ray2D):
line_ray_3d = Ray3D(
Point3D(line_ray.p.x, line_ray.p.y, self.floor_height),
Vector3D(line_ray.v.x, line_ray.v.y, 0)
)
closest_func = closest_point3d_on_line3d_infinite
elif isinstance(line_ray, LineSegment2D):
line_ray_3d = LineSegment3D(
Point3D(line_ray.p.x, line_ray.p.y, self.floor_height),
Vector3D(line_ray.v.x, line_ray.v.y, 0)
)
closest_func = closest_point3d_on_line3d
else:
msg = 'Expected Ray2D or LineSegment2D. Got {}.'.format(type(line_ray))
raise TypeError(msg)
# loop through the vertices and align them
new_boundary, new_holes = [], None
for pt in self._floor_geometry.boundary:
close_pt = closest_func(pt, line_ray_3d)
if pt.distance_to_point(close_pt) <= distance:
new_boundary.append(close_pt)
else:
new_boundary.append(pt)
if self._floor_geometry.holes is not None:
new_holes = []
for hole in self._floor_geometry.holes:
new_hole = []
for pt in hole:
close_pt = closest_func(pt, line_ray_3d)
if pt.distance_to_point(close_pt) <= distance:
new_hole.append(close_pt)
else:
new_hole.append(pt)
new_holes.append(new_hole)
# rebuild the new floor geometry and assign it to the Room2D
self._floor_geometry = Face3D(
new_boundary, self._floor_geometry.plane, new_holes)
[docs] def snap_to_line_end_points(self, line, distance):
"""Snap this Room2D's vertices to the endpoints of a line segment.
All properties assigned to this Room2D will be preserved and the number of
vertices will remain constant. This means that this method can often create
duplicate vertices and it might be desirable to run the remove_duplicate_vertices
method after running this one.
Args:
line: A ladybug_geometry LineSegment2D to which the Room2D
vertices will be snapped if they are near the end points.
distance: The maximum distance between a Room2D vertex and the polyline where
the vertex will be moved to lie on the polyline. Vertices beyond
this distance will be left as they are.
"""
# create a 3D version of the line segment
if isinstance(line, LineSegment2D):
line_ray_3d = LineSegment3D(
Point3D(line.p.x, line.p.y, self.floor_height),
Vector3D(line.v.x, line.v.y, 0)
)
else:
msg = 'Expected LineSegment2D. Got {}.'.format(type(line))
raise TypeError(msg)
# get lists of vertices for the Room2D.floor_geometry to be edited
edit_boundary = self._floor_geometry.boundary
edit_holes = self._floor_geometry.holes \
if self._floor_geometry.has_holes else None
# perform the snapping operation to snap them
vertices = line_ray_3d.endpoints
new_boundary, new_holes = [], None
for pt in edit_boundary:
dists = [pt.distance_to_point(pt_3d) for pt_3d in vertices]
sort_pt = sorted(zip(dists, vertices), key=lambda pair: pair[0])
if sort_pt[0][0] <= distance:
new_boundary.append(sort_pt[0][1])
else:
new_boundary.append(pt)
if edit_holes is not None:
new_holes = []
for hole in edit_holes:
new_hole = []
for pt in hole:
dists = [pt.distance_to_point(pt_3d) for pt_3d in vertices]
sort_pt = sorted(zip(dists, vertices), key=lambda pair: pair[0])
if sort_pt[0][0] <= distance:
new_hole.append(sort_pt[0][1])
else:
new_hole.append(pt)
new_holes.append(new_hole)
# rebuild the new floor geometry and assign it to the Room2D
self._floor_geometry = Face3D(
new_boundary, self._floor_geometry.plane, new_holes)
[docs] def pull_to_segments(self, line_segments, distance, snap_vertices=True):
"""Pull this Room2D's vertices to several LineSegment2D.
This includes both an alignment to the line segments as well as an optional
snapping to the line end points.
The benefit of calling this method as opposed to iterating over the
segments and calling align (and snap_to_line_end_points) is that this
method will only align (and snap) to the closest segment across all of
the input line_segments. This often helps avoid snapping to undesirable
line segments, particularly when there are two ore more segments that
are within the distance.
Args:
line_segments: A list of ladybug_geometry LineSegment2D to which this
Room2D's vertices will be pulled.
distance: The maximum distance between a Room2D vertex and the line_segments
where the vertex will be moved to lie on the segments. Vertices beyond
this distance will be left as they are.
snap_vertices: A boolean to note whether Room2D vertices that are
close to the segment end points within the distance should be snapped
to the end point instead of simply being aligned to the nearest
segment. (Default: True).
"""
# create a 3D version of the relevant line segments
lines_3d = []
for line in line_segments:
if isinstance(line, LineSegment2D):
if line.length > distance:
line_3d = LineSegment3D(
Point3D(line.p.x, line.p.y, self.floor_height),
Vector3D(line.v.x, line.v.y, 0)
)
lines_3d.append(line_3d)
else:
msg = 'Expected LineSegment2D. Got {}.'.format(type(line))
raise TypeError(msg)
# get lists of vertices for the Room2D.floor_geometry to be edited
edit_boundary = self._floor_geometry.boundary
edit_holes = self._floor_geometry.holes \
if self._floor_geometry.has_holes else None
# loop through the Room2D vertices and align them to the segments
new_boundary = []
for pt in edit_boundary:
dists, c_pts = [], []
for line_ray_3d in lines_3d:
close_pt = closest_point3d_on_line3d(pt, line_ray_3d)
c_pts.append(close_pt)
dists.append(pt.distance_to_point(close_pt))
sort_pt = sorted(zip(dists, c_pts), key=lambda pair: pair[0])
if sort_pt[0][0] <= distance:
new_boundary.append(sort_pt[0][1])
else:
new_boundary.append(pt)
edit_boundary = new_boundary
if edit_holes is not None:
new_holes = []
for hole in edit_holes:
new_hole = []
for pt in hole:
dists, c_pts = [], []
for line_ray_3d in lines_3d:
close_pt = closest_point3d_on_line3d(pt, line_ray_3d)
c_pts.append(close_pt)
dists.append(pt.distance_to_point(close_pt))
sort_pt = sorted(zip(dists, c_pts), key=lambda pair: pair[0])
if sort_pt[0][0] <= distance:
new_hole.append(sort_pt[0][1])
else:
new_hole.append(pt)
new_holes.append(new_hole)
edit_holes = new_holes
# if snap_vertices was requested, perform an additional operation to snap them
if snap_vertices:
vertices = []
for line in lines_3d:
vertices.append(line.p1)
vertices.append(line.p2)
new_boundary = []
for pt in edit_boundary:
dists = [pt.distance_to_point(pt_3d) for pt_3d in vertices]
sort_pt = sorted(zip(dists, vertices), key=lambda pair: pair[0])
if sort_pt[0][0] <= distance:
new_boundary.append(sort_pt[0][1])
else:
new_boundary.append(pt)
edit_boundary = new_boundary
if edit_holes is not None:
new_holes = []
for hole in edit_holes:
new_hole = []
for pt in hole:
dists = [pt.distance_to_point(pt_3d) for pt_3d in vertices]
sort_pt = sorted(zip(dists, vertices), key=lambda pair: pair[0])
if sort_pt[0][0] <= distance:
new_hole.append(sort_pt[0][1])
else:
new_hole.append(pt)
new_holes.append(new_hole)
edit_holes = new_holes
# rebuild the new floor geometry and assign it to the Room2D
self._floor_geometry = Face3D(
edit_boundary, self._floor_geometry.plane, edit_holes)
[docs] def pull_to_polyline(self, polyline, distance, snap_vertices=True):
"""Pull this Room2D's vertices to a Polyline2D.
This includes both an alignment to the polyline's segments as well as an
optional snapping to the polyline's vertices.
All properties assigned to this Room2D will be preserved and the number of
vertices will remain constant. This means that this method can often create
duplicate vertices and it might be desirable to run the remove_duplicate_vertices
method after running this one.
Args:
polyline: A ladybug_geometry Polyline2D to which this Room2D's vertices
will be pulled.
distance: The maximum distance between a Room2D vertex and the polyline where
the vertex will be moved to lie on the polyline. Vertices beyond
this distance will be left as they are.
snap_vertices: A boolean to note whether Room2D vertices that are
close to the polyline vertices within the distance should be snapped
to the polyline vertex instead of simply being aligned to the nearest
polyline segment. (Default: True).
"""
# create LineSegment3Ds from the polyline
line_segs = []
for seg in polyline.segments:
pt_3d = Point3D(seg.p.x, seg.p.y, self.floor_height)
line_ray_3d = LineSegment3D(pt_3d, Vector3D(seg.v.x, seg.v.y, 0))
line_segs.append(line_ray_3d)
line_segs.append(line_segs[0].flip()) # ensure last vertex is counted
# pull this Room2D to the segments
self._pull_to_poly_segments(line_segs, distance, snap_vertices)
[docs] def pull_to_polygon(self, polygon, distance, snap_vertices=True):
"""Pull this Room2D's vertices to a Polygon2D.
This includes both an alignment to the polygon's segments as well as an
optional snapping to the polygon's vertices.
All properties assigned to this Room2D will be preserved and the number of
vertices will remain constant. This means that this method can often create
duplicate vertices and it might be desirable to run the remove_duplicate_vertices
method after running this one.
Args:
polygon: A ladybug_geometry Polygon2D to which this Room2D's vertices
will be pulled.
distance: The maximum distance between a Room2D vertex and the polygon where
the vertex will be moved to lie on the polygon. Vertices beyond
this distance will be left as they are.
snap_vertices: A boolean to note whether Room2D vertices that are
close to the polygon vertices within the distance should be snapped
to the polygon vertex instead of simply being aligned to the nearest
polygon segment. (Default: True).
"""
# create LineSegment3Ds from the polygon
line_segs = []
for seg in polygon.segments:
pt_3d = Point3D(seg.p.x, seg.p.y, self.floor_height)
line_ray_3d = LineSegment3D(pt_3d, Vector3D(seg.v.x, seg.v.y, 0))
line_segs.append(line_ray_3d)
# pull this Room2D to the segments
self._pull_to_poly_segments(line_segs, distance, snap_vertices)
[docs] def pull_to_room_2d(self, room_2d, distance, snap_vertices=True):
"""Pull this Room2D's vertices to another Room2D.
This includes both an alignment to the other Room2D's segments as well
as an optional snapping to the Room2D's vertices.
All properties assigned to this Room2D will be preserved and the number of
vertices will remain constant. This means that this method can often create
duplicate vertices and it might be desirable to run the remove_duplicate_vertices
method after running this one.
Args:
room_2d: A Room2D to which this Room2D's vertices will be pulled.
distance: The maximum distance between a Room2D vertex and the other
Room2D where the vertex will be moved to lie on the other Room2D.
Vertices beyond this distance will be left as they are.
snap_vertices: A boolean to note whether Room2D vertices that are
close to the other Room2D vertices within the distance should be snapped
to the Room2D vertex instead of simply being aligned to the nearest
Room2D segment. (Default: True).
"""
# convert the other Room2D to a list of polygons
f_geo = room_2d.floor_geometry
other_room_polys = [Polygon2D([Point2D(pt.x, pt.y) for pt in f_geo.boundary])]
if f_geo.has_holes:
for hole in f_geo.holes:
h_poly = Polygon2D([Point2D(pt.x, pt.y) for pt in hole])
other_room_polys.append(h_poly)
# pull this Room2D to each of the polygons
for o_poly in other_room_polys:
self.pull_to_polygon(o_poly, distance, snap_vertices)
def _pull_to_poly_segments(self, line_segments, distance, snap_vertices=True):
"""Pull this Room2D's vertices to LineSegment3D originating from a poly-line/gon.
Args:
line_segments: A list of ladybug_geometry LineSegment3D with Z-values at
this Room2D's floor_height to which this Room2D's vertices
will be pulled.
distance: The maximum distance between a Room2D vertex and the line_segments
where the vertex will be moved to lie on the segments. Vertices beyond
this distance will be left as they are.
snap_vertices: A boolean to note whether Room2D vertices that are
close to the segment end points within the distance should be snapped
to the end point instead of simply being aligned to the nearest
segment. (Default: True).
"""
# get lists of vertices for the Room2D.floor_geometry to be edited
edit_boundary = self._floor_geometry.boundary
edit_holes = self._floor_geometry.holes \
if self._floor_geometry.has_holes else None
# loop through the Room2D vertices and align them to the segments
new_boundary = []
for pt in edit_boundary:
dists, c_pts = [], []
for line_ray_3d in line_segments:
close_pt = closest_point3d_on_line3d(pt, line_ray_3d)
c_pts.append(close_pt)
dists.append(pt.distance_to_point(close_pt))
sort_pt = sorted(zip(dists, c_pts), key=lambda pair: pair[0])
if sort_pt[0][0] <= distance:
new_boundary.append(sort_pt[0][1])
else:
new_boundary.append(pt)
edit_boundary = new_boundary
if edit_holes is not None:
new_holes = []
for hole in edit_holes:
new_hole = []
for pt in hole:
dists, c_pts = [], []
for line_ray_3d in line_segments:
close_pt = closest_point3d_on_line3d(pt, line_ray_3d)
c_pts.append(close_pt)
dists.append(pt.distance_to_point(close_pt))
sort_pt = sorted(zip(dists, c_pts), key=lambda pair: pair[0])
if sort_pt[0][0] <= distance:
new_hole.append(sort_pt[0][1])
else:
new_hole.append(pt)
new_holes.append(new_hole)
edit_holes = new_holes
# if snap_vertices was requested, perform an additional operation to snap them
if snap_vertices:
vertices = [line.p for line in line_segments]
new_boundary = []
for pt in edit_boundary:
dists = [pt.distance_to_point(pt_3d) for pt_3d in vertices]
sort_pt = sorted(zip(dists, vertices), key=lambda pair: pair[0])
if sort_pt[0][0] <= distance:
new_boundary.append(sort_pt[0][1])
else:
new_boundary.append(pt)
edit_boundary = new_boundary
if edit_holes is not None:
new_holes = []
for hole in edit_holes:
new_hole = []
for pt in hole:
dists = [pt.distance_to_point(pt_3d) for pt_3d in vertices]
sort_pt = sorted(zip(dists, vertices), key=lambda pair: pair[0])
if sort_pt[0][0] <= distance:
new_hole.append(sort_pt[0][1])
else:
new_hole.append(pt)
new_holes.append(new_hole)
edit_holes = new_holes
# rebuild the new floor geometry and assign it to the Room2D
self._floor_geometry = Face3D(
edit_boundary, self._floor_geometry.plane, edit_holes)
[docs] def coordinate_room_2d_vertices(self, room_2d, distance, tolerance=0.01):
"""Insert vertices to this Room2D to coordinate this Room2D with another Room2D.
This is sometimes a useful operation to run after using the pull_to_room_2d
method in order to address the case that the Room2D to which this one was
pulled has more vertices along the adjacency boundary than this Room2D.
In this case, the adjacency between the two Room2Ds will not be clean and
extra vertices must be inserted into this Room2D so that geometry matches
along the room adjacency.
Any vertices of the neighboring input room_2d that are within the specified
distance but cannot be matched to a vertex on this Room2D within the tolerance
will be inserted into this Room2D, splitting the wall segment in the process.
Args:
room_2d: A Room2D with which the vertices of this Room2D will be coordinated.
distance: The maximum distance between a Room2D vertex and the other
Room2D where the vertex will be moved to lie on the other Room2D.
Vertices beyond this distance will be left as they are.
tolerance: The minimum difference between the coordinate values at
which they are considered co-located. (Default: 0.01,
suitable for objects in meters).
"""
# determine all of the vertices of the other Room2D that should be inserted
self_segs = list(self.floor_segments_2d)
self_pts_2d = [seg.p for seg in self_segs]
other_pts_2d = [seg.p for seg in room_2d.floor_segments_2d]
insert_pts = []
for o_pt in other_pts_2d:
possible_insert = False
for i, seg in enumerate(self_segs):
if seg.distance_to_point(o_pt) < distance:
possible_insert = True
break
if possible_insert:
for s_pt in self_pts_2d:
if s_pt.distance_to_point(o_pt) <= tolerance:
break
else:
insert_pts.append((i, o_pt))
# loop through the segments and split them if insertion points were found
if len(insert_pts) == 0:
return
sort_int_pts = sorted(insert_pts, key=lambda x: x[0], reverse=True)
edit_code = ['K'] * len(self_segs)
for ins_ind, pt in sort_int_pts:
split_seg = self_segs[ins_ind]
new_seg1 = LineSegment2D.from_end_points(split_seg.p1, pt)
new_seg2 = LineSegment2D.from_end_points(pt, split_seg.p2)
self_segs[ins_ind] = new_seg2
self_segs.insert(ins_ind, new_seg1)
edit_code.insert(ins_ind, 'A')
# create a new floor_geometry Face3D and update the geometry with the edit code
z_val = self.floor_geometry.boundary[0].z
if not self.floor_geometry.has_holes:
pts = [Point3D(seg.p.x, seg.p.y, z_val) for seg in self_segs]
new_geo = Face3D(pts, self.floor_geometry.plane)
else:
joined_segs = Polyline2D.join_segments(self_segs, tolerance)
new_loops = []
for p_line in joined_segs:
pts = [Point3D(pt.x, pt.y, z_val) for pt in p_line.vertices[:-1]]
new_loops.append(pts)
new_geo = Face3D(new_loops[0], self.floor_geometry.plane, new_loops[1:])
self.update_floor_geometry(new_geo, edit_code, tolerance)
[docs] def remove_duplicate_vertices(self, tolerance=0.01):
"""Remove duplicate vertices from this Room2D.
All properties assigned to the Room2D will be preserved.
Args:
tolerance: The minimum distance between a vertex and the line it lies
upon at which point the vertex is considered colinear. (Default: 0.01,
suitable for objects in meters).
Returns:
A list of integers for the indices of segments that have been removed.
"""
# loop through the vertices and remove any duplicates
exist_abs = self.air_boundaries
new_bound, new_bcs, new_win, new_shd, new_abs = [], [], [], [], []
b_pts = self.floor_geometry.boundary
b_pts = b_pts[1:] + (b_pts[0],)
removed_indices = []
for i, vert in enumerate(b_pts):
if not vert.is_equivalent(b_pts[i - 1], tolerance):
new_bound.append(b_pts[i - 1])
new_bcs.append(self._boundary_conditions[i])
new_win.append(self._window_parameters[i])
new_shd.append(self._shading_parameters[i])
new_abs.append(exist_abs[i])
else:
removed_indices.append(i)
new_holes = None
if self.floor_geometry.has_holes:
new_holes, seg_count = [], len(b_pts)
for hole in self.floor_geometry.holes:
new_h_pts = []
h_pts = hole[1:] + (hole[0],)
for i, vert in enumerate(h_pts):
if not vert.is_equivalent(h_pts[i - 1], tolerance):
new_h_pts.append(h_pts[i - 1])
new_bcs.append(self._boundary_conditions[seg_count + i])
new_win.append(self._window_parameters[seg_count + i])
new_shd.append(self._shading_parameters[seg_count + i])
new_abs.append(exist_abs[i])
else:
removed_indices.append(i)
new_holes.append(new_h_pts)
seg_count += len(h_pts)
# assign the geometry and properties
try:
self._floor_geometry = Face3D(
new_bound, self.floor_geometry.plane, new_holes)
except AssertionError as e: # usually a sliver face of some kind
raise ValueError(
'Room2D "{}" is degenerate with dimensions less than the '
'tolerance.\n{}'.format(self.display_name, e))
self._segment_count = len(new_bcs)
self._boundary_conditions = new_bcs
self._window_parameters = new_win
self._shading_parameters = new_shd
self._air_boundaries = new_abs
return removed_indices
[docs] def remove_degenerate_holes(self, tolerance=0.01):
"""Remove any holes in this Room2D with an area that evaluates to zero.
All properties assigned to the Room2D will be preserved.
Args:
tolerance: The minimum difference between the coordinate values at
which they are considered co-located. (Default: 0.01,
suitable for objects in meters).
"""
if self.floor_geometry.has_holes: # first identify any zero-area holes
holes_to_remove = []
for i, hole in enumerate(self.floor_geometry.holes):
tf = Face3D(hole, self.floor_geometry.plane)
max_dim = max((tf.max.x - tf.min.x, tf.max.y - tf.min.y))
if tf.area < max_dim * tolerance:
holes_to_remove.append(i)
# if zero-area holes were found, rebuild the Room2D
if len(holes_to_remove) > 0:
self._remove_holes(holes_to_remove)
[docs] def remove_small_holes(self, area_threshold):
"""Remove any holes in this Room2D that are below a certain area threshold.
All properties assigned to the Room2D will be preserved.
Args:
area_threshold: A number for the area below which holes will be removed.
"""
if self.floor_geometry.has_holes: # first identify any holes to remove
holes_to_remove = []
for i, hole in enumerate(self.floor_geometry.holes):
tf = Face3D(hole, self.floor_geometry.plane)
if tf.area < area_threshold:
holes_to_remove.append(i)
# if removable holes were found, rebuild the Room2D
if len(holes_to_remove) > 0:
self._remove_holes(holes_to_remove)
def _remove_holes(self, holes_to_remove):
"""Remove holes in the Room2D given the indices of the holes.
Args:
holes_to_remove: A list of integers for the indices of holes to be removed.
"""
# first collect the properties of the boundary
exist_abs = self.air_boundaries
new_bcs, new_win, new_shd, new_abs = [], [], [], []
seg_count = len(self.floor_geometry.boundary)
for i in range(seg_count):
new_bcs.append(self._boundary_conditions[i])
new_win.append(self._window_parameters[i])
new_shd.append(self._shading_parameters[i])
new_abs.append(exist_abs[i])
# collect the properties of the new holes
new_holes = []
for hi, hole in enumerate(self.floor_geometry.holes):
if hi not in holes_to_remove:
for i, vert in enumerate(hole):
new_bcs.append(self._boundary_conditions[seg_count + i])
new_win.append(self._window_parameters[seg_count + i])
new_shd.append(self._shading_parameters[seg_count + i])
new_abs.append(exist_abs[i])
new_holes.append(hole)
seg_count += len(hole)
# reset the properties of the Room2D
self._floor_geometry = Face3D(
self.floor_geometry.boundary, self.floor_geometry.plane, new_holes)
self._segment_count = len(new_bcs)
self._boundary_conditions = new_bcs
self._window_parameters = new_win
self._shading_parameters = new_shd
self._air_boundaries = new_abs
[docs] def update_floor_geometry(self, new_floor_geometry, edit_code, tolerance=0.01):
"""Change the floor_geometry of the Room2D with segment-altering specifications.
This method is intended to be used when the floor geometry has been edited
by some external means and this Room2D should be updated for coordination.
The method tries to infer whether an removed floor segment means that an
original segment has been merged into another or removed completely using
the colinearity of the original segments. A removed segment that is colinear
with its neighbor will be merged into it while a removed segment that was
not colinear will simply be deleted. Similarly, the method will infer if
an added segment indicates a split in an original segment using colinearity.
When the result in the new_floor_geometry is two colinear segments,
properties of the original segment will be split across the new segments.
Otherwise the new segment will receive default properties.
Args:
new_floor_geometry: A Face3D for the new floor_geometry of this Room2D.
Note that this method expects the plane of this Face3D to match
the original floor_geometry Face3D and for the counter-clockwise
vertex ordering of the segments to be the same as the original
floor geometry (though segments can obviously be added or removed).
edit_code: A text string that indicates the operations that were
performed on the original floor_geometry segments to yield the
new_floor_geometry. The following letters are used in this code
to indicate the following:
* K = a segment that has been kept (possibly moved but not removed)
* X = a segment that has been removed
* A = a segment that has been added
For example, KXKAKKA means that the first segment was kept, the
next removed, the next kept, the next added, followed by two kept
segments and ending in an added segment.
tolerance: The minimum difference between the coordinate values at
which they are considered co-located, used to determine
colinearity. Default: 0.01, suitable for objects in meters.
"""
# get the original and the new floor segments
orig_segs = self.floor_segments
new_segs = new_floor_geometry.boundary_segments if new_floor_geometry.holes is \
None else new_floor_geometry.boundary_segments + \
tuple(seg for hole in new_floor_geometry.hole_segments for seg in hole)
# figure out the new properties based on the edit code
new_bcs, new_win, new_shd = [], [], []
last_o_seg = orig_segs[-1]
orig_i, new_i = 0, 0
for edit_val in edit_code:
if edit_val == 'K':
new_bcs.append(self._boundary_conditions[orig_i])
new_win.append(self._window_parameters[orig_i])
new_shd.append(self._shading_parameters[orig_i])
last_o_seg = orig_segs[orig_i]
orig_i += 1
new_i += 1
elif edit_val == 'X':
# determine if the removed segment is colinear
del_seg = orig_segs[orig_i]
full_line = LineSegment3D.from_end_points(last_o_seg.p1, del_seg.p2)
if full_line.distance_to_point(del_seg.p1) <= tolerance: # colinear!
if len(new_bcs) != 0:
# TODO: figure out a strategy to merge first to end of the list
new_bcs[-1] = bcs.outdoors
new_win[-1] = DetailedWindows.merge(
(new_win[-1], self._window_parameters[orig_i]),
(last_o_seg, del_seg), self.floor_to_ceiling_height)
last_o_seg = full_line
orig_i += 1
elif edit_val == 'A':
# determine if the added segment is colinear and within the original
add_seg = new_segs[new_i]
if last_o_seg.distance_to_point(add_seg.p1) <= tolerance and \
last_o_seg.distance_to_point(add_seg.p2) <= tolerance:
# colinear!
new_bcs.append(self._boundary_conditions[orig_i])
if len(new_win) != 0 and new_win[-1] is not None:
# TODO: figure out a strategy to split the end of the list
p_lin = LineSegment3D.from_end_points(last_o_seg.p1, add_seg.p1)
a_lin = LineSegment3D.from_end_points(add_seg.p1, last_o_seg.p2)
w_to_spl = new_win.pop(-1)
new_win.extend(w_to_spl.split((p_lin, a_lin), tolerance))
last_o_seg = a_lin
else:
new_win.append(None)
new_shd.append(self._shading_parameters[orig_i])
else: # not colinear; use default properties
new_bcs.append(bcs.outdoors)
new_win.append(None)
new_shd.append(None)
new_i += 1
# assign the updated properties to this Room2D
self._floor_geometry = new_floor_geometry
self._segment_count = len(new_segs)
assert self._segment_count == len(new_bcs), 'The operations in the edit_code ' \
'denote a geometry with {} segments but the new_floor_geometry has {} ' \
'segments.'.format(len(new_bcs), self._segment_count)
self._boundary_conditions = new_bcs
self._window_parameters = new_win
self._shading_parameters = new_shd
self._air_boundaries = None # reset to avoid any conflicts
[docs] def remove_colinear_vertices(self, tolerance=0.01, preserve_wall_props=True):
"""Get a version of this Room2D without colinear or duplicate vertices.
Args:
tolerance: The minimum distance between a vertex and the line it lies
upon at which point the vertex is considered colinear. Default: 0.01,
suitable for objects in meters.
preserve_wall_props: Boolean to note whether existing window parameters
and Ground boundary conditions should be preserved as vertices are
removed. If False, all boundary conditions are replaced with Outdoors,
all window parameters are erased, and this method will execute quickly.
If True, an attempt will be made to merge window parameters together
across colinear segments, translating simple window parameters to
rectangular ones if necessary. Also, existing Ground boundary
conditions will be kept. (Default: True).
Returns:
A new Room2D derived from this one with its colinear vertices removed.
"""
if not preserve_wall_props:
try: # remove colinear vertices from the Room2D
new_geo = self.floor_geometry.remove_colinear_vertices(tolerance)
except AssertionError as e: # usually a sliver face of some kind
raise ValueError(
'Room2D "{}" is degenerate with dimensions less than the '
'tolerance.\n{}'.format(self.display_name, e))
rebuilt_room = Room2D(
self.identifier, new_geo, self.floor_to_ceiling_height,
is_ground_contact=self.is_ground_contact,
is_top_exposed=self.is_top_exposed)
else:
ftc_height = self.floor_to_ceiling_height
if not self.floor_geometry.has_holes: # only need to evaluate one list
pts_3d = self.floor_geometry.vertices
pts_2d = self.floor_geometry.polygon2d
segs_2d = pts_2d.segments
bound_cds = self.boundary_conditions
win_pars = self.window_parameters
bound_verts, new_bcs, new_w_par = self._remove_colinear_props(
pts_3d, pts_2d, segs_2d, bound_cds, win_pars, ftc_height, tolerance)
holes = None
else:
pts_3d = self.floor_geometry.boundary
pts_2d = self.floor_geometry.boundary_polygon2d
segs_2d = pts_2d.segments
st_i = len(pts_3d)
bound_cds = self.boundary_conditions[:st_i]
win_pars = self.window_parameters[:st_i]
bound_verts, new_bcs, new_w_par = self._remove_colinear_props(
pts_3d, pts_2d, segs_2d, bound_cds, win_pars, ftc_height, tolerance)
holes = []
for i, pts_3d in enumerate(self.floor_geometry.holes):
pts_2d = self.floor_geometry.hole_polygon2d[i]
segs_2d = pts_2d.segments
bound_cds = self.boundary_conditions[st_i:st_i + len(pts_3d)]
win_pars = self.window_parameters[st_i:st_i + len(pts_3d)]
st_i += len(pts_3d)
h_verts, h_bcs, h_w_par = self._remove_colinear_props(
pts_3d, pts_2d, segs_2d, bound_cds, win_pars,
ftc_height, tolerance)
holes.append(h_verts)
new_bcs.extend(h_bcs)
new_w_par.extend(h_w_par)
# create the new Room2D
new_geo = Face3D(bound_verts, holes=holes)
rebuilt_room = Room2D(
self.identifier, new_geo, self.floor_to_ceiling_height,
boundary_conditions=new_bcs, window_parameters=new_w_par,
is_ground_contact=self.is_ground_contact,
is_top_exposed=self.is_top_exposed)
# assign overall properties to the rebuilt room
rebuilt_room._skylight_parameters = self._skylight_parameters
rebuilt_room._display_name = self._display_name
rebuilt_room._user_data = self._user_data
rebuilt_room._parent = self._parent
rebuilt_room._abridged_properties = self._abridged_properties
rebuilt_room._properties._duplicate_extension_attr(self._properties)
return rebuilt_room
[docs] def remove_short_segments(self, distance, angle_tolerance=1.0):
"""Get a version of this Room2D with consecutive short segments removed.
To patch over the segments, an attempt will first be made to find the
intersection of the two neighboring segments. If these two lines are parallel,
they will simply be connected with a segment.
Properties assigned to the Room2D will be preserved for the segments that
are not removed.
Args:
distance: The maximum length of a segment below which the segment
will be considered for removal.
angle_tolerance: The max angle difference in degrees that vertices
are allowed to differ from one another in order to consider them
colinear. (Default: 1).
"""
# first check if there are contiguous short segments to be removed
segs = [self._floor_geometry.boundary_segments]
if self._floor_geometry.has_holes:
for hole in self._floor_geometry.hole_segments:
segs.append(hole)
sh_seg_i = [[i for i, s in enumerate(sg) if s.length <= distance] for sg in segs]
if len(segs[0]) - len(sh_seg_i[0]) < 3:
return None # large distance means the whole Face becomes removed
if all(len(s) <= 1 for s in sh_seg_i):
return self # no short segments to remove
del_seg_i = []
for sh_seg in sh_seg_i:
del_seg = set()
for i, seg_i in enumerate(sh_seg):
test_val = seg_i - sh_seg[i - 1]
if test_val == 1 or (seg_i == 0 and test_val < 0):
del_seg.add(sh_seg[i - 1])
del_seg.add(seg_i)
if 0 in sh_seg and len(sh_seg) - 1 in sh_seg:
del_seg.add(0)
del_seg.add(len(sh_seg) - 1)
del_seg_i.append(sorted(list(del_seg)))
if all(len(s) == 0 for s in del_seg_i):
return self # there are short segments but they're not contiguous
# contiguous short segments found
# collect the vertices and indices of properties to be removed
a_tol = math.radians(angle_tolerance)
prev_i, final_pts, del_prop_i = 0, [], []
for p_segs, del_i in zip(segs, del_seg_i):
if len(del_i) != 0:
# set up variables to handle getting the last vertex to connect to
new_points, in_del, post_del = [], False, False
if 0 in del_i and len(p_segs) - 1 in del_i:
last_i, in_del = -1, True
try:
while del_i[last_i] - del_i[last_i - 1] == 1:
last_i -= 1
except IndexError: # entire hole to be removed
for i in range(len(p_segs)):
del_prop_i.append(prev_i + i)
p_segs = []
# loop through the segments and delete the short ones
for i, lin in enumerate(p_segs):
if i in del_i:
if not in_del:
last_i = i
in_del = True
del_prop_i.append(prev_i + i)
rel_i = i + 1 if i + 1 != len(p_segs) else 0
if rel_i not in del_i: # we are at the end of the deletion
# see if we can repair the hole by extending segments
l3a, l3b = p_segs[last_i - 1], p_segs[rel_i]
l2a = Ray2D(Point2D(l3a.p.x, l3a.p.y),
Vector2D(l3a.v.x, l3a.v.y))
l2b = Ray2D(Point2D(l3b.p.x, l3b.p.y),
Vector2D(l3b.v.x, l3b.v.y))
v_ang = l2a.v.angle(l2b.v)
if v_ang <= a_tol or v_ang >= math.pi - a_tol: # parallel
new_points.append(p_segs[last_i].p)
del_prop_i.pop(-1) # put back the last property
else: # extend lines to the intersection
int_pt = self._intersect_line2d_infinite(l2a, l2b)
int_pt3 = Point3D(int_pt.x, int_pt.y, self.floor_height)
new_points.append(int_pt3)
post_del = True
in_del = False
else:
if not post_del:
new_points.append(lin.p)
post_del = False
if post_del:
new_points.pop(0) # put back the last property
if len(new_points) != 0:
final_pts.append(new_points)
else: # no short segments to remove on this hole or boundary
final_pts.append([lin.p for lin in p_segs])
prev_i += len(p_segs)
# create the geometry and convert properties for the new segments
holes = None if len(final_pts) == 1 else final_pts[1:]
new_geo = Face3D(final_pts[0], self.floor_geometry.plane, holes)
new_bcs = self._boundary_conditions[:]
new_win = self._window_parameters[:]
new_shd = self._shading_parameters[:]
new_abs = list(self.air_boundaries)
all_props = (new_bcs, new_win, new_shd, new_abs)
for prop_list in all_props:
for di in reversed(del_prop_i):
prop_list.pop(di)
# create the final rebuilt Room2D and return it
rebuilt_room = Room2D(
self.identifier, new_geo, self.floor_to_ceiling_height, new_bcs, new_win,
new_shd, self.is_ground_contact, self.is_top_exposed)
rebuilt_room._air_boundaries = new_abs
rebuilt_room._skylight_parameters = self._skylight_parameters
rebuilt_room._display_name = self._display_name
rebuilt_room._user_data = self._user_data
rebuilt_room._parent = self._parent
rebuilt_room._abridged_properties = self._abridged_properties
rebuilt_room._properties._duplicate_extension_attr(self._properties)
return rebuilt_room
[docs] def check_horizontal(self, tolerance=0.01, raise_exception=True):
"""Check whether the Room2D's floor geometry is horizontal within a tolerance.
Args:
tolerance: The maximum difference between z values at which
face vertices are considered at different heights. Default: 0.01,
suitable for objects in meters.
raise_exception: Boolean to note whether a ValueError should be raised
if the room floor geometry is not horizontal.
"""
z_vals = tuple(pt.z for pt in self._floor_geometry.vertices)
if max(z_vals) - min(z_vals) <= tolerance:
return ''
msg = 'Room "{}" is not horizontal to within {} tolerance.'.format(
self.display_name, tolerance)
if raise_exception:
raise ValueError(msg)
return msg
[docs] def check_degenerate(self, tolerance=0.01, raise_exception=True, detailed=False):
"""Check whether the Room2D's floor geometry is degenerate with zero area.
Args:
tolerance: The minimum difference between the coordinate values of two
vertices at which they can be considered equivalent. (Default: 0.01,
suitable for objects in meters).
raise_exception: If True, a ValueError will be raised if the object
intersects with itself. (Default: True).
detailed: Boolean for whether the returned object is a detailed list of
dicts with error info or a string with a message. (Default: False).
Returns:
A string with the message or a list with a dictionary if detailed is True.
"""
degenerate = False
try:
self.floor_geometry.remove_colinear_vertices(tolerance)
except AssertionError: # degenerate geometry found!
degenerate = True
if degenerate:
msg = 'Room2D "{}" has degenerate floor geometry with zero ' \
'area.'.format(self.display_name)
if raise_exception:
raise ValueError(msg)
full_msg = self._validation_message_child(
msg, self, detailed, '100101',
error_type='Degenerate Room Geometry')
if detailed:
return [full_msg]
if raise_exception:
raise ValueError(full_msg)
return full_msg
return [] if detailed else ''
[docs] def check_self_intersecting(self, tolerance=0.01, raise_exception=True,
detailed=False):
"""Check whether the Room2D's floor geometry intersects itself (like a bowtie).
Note that objects that have duplicate vertices will not be considered
self-intersecting and are valid.
Args:
tolerance: The minimum difference between the coordinate values of two
vertices at which they can be considered equivalent. (Default: 0.01,
suitable for objects in meters).
raise_exception: If True, a ValueError will be raised if the object
intersects with itself. (Default: True).
detailed: Boolean for whether the returned object is a detailed list of
dicts with error info or a string with a message. (Default: False).
Returns:
A string with the message or a list with a dictionary if detailed is True.
"""
if self.floor_geometry.is_self_intersecting:
msg = 'Room2D "{}" has floor geometry with self-intersecting ' \
'edges.'.format(self.display_name)
try: # see if it is self-intersecting because of a duplicate vertex
new_geo = self.floor_geometry.remove_duplicate_vertices(tolerance)
if not new_geo.is_self_intersecting:
return [] if detailed else '' # valid with removed dup vertex
except AssertionError:
pass # zero area face; treat it as self-intersecting
full_msg = self._validation_message_child(
msg, self, detailed, '100102',
error_type='Self-Intersecting Room Geometry')
if detailed:
return [full_msg]
if raise_exception:
raise ValueError(full_msg)
return full_msg
return [] if detailed else ''
[docs] def check_window_parameters_valid(
self, tolerance=0.01, raise_exception=True, detailed=False):
"""Check whether the window and skylight parameters produce valid apertures.
This means that this Room's windows do not overlap with one another and,
in the case of detailed windows, the polygons do not self-intersect. It
also means that skylights do not extend past the boundary of the room.
Args:
tolerance: The minimum difference between the coordinate values of two
vertices at which they can be considered equivalent. (Default: 0.01,
suitable for objects in meters).
raise_exception: Boolean to note whether a ValueError should be raised
if the window parameters are not valid.
detailed: Boolean for whether the returned object is a detailed list of
dicts with error info or a string with a message. (Default: False).
Returns:
A string with the message or a list with a dictionary if detailed is True.
"""
detailed = False if raise_exception else detailed
msgs = []
checkable_par = (RectangularWindows, DetailedWindows)
for i, wp in enumerate(self._window_parameters):
if wp is not None and isinstance(wp, checkable_par):
msg = wp.check_window_overlaps(tolerance)
if msg != '':
msgs.append(' Segment ({}) - {}'.format(i, msg))
if isinstance(wp, DetailedWindows):
msg = wp.check_self_intersecting(tolerance)
if msg != '':
msgs.append(' Segment ({}) - {}'.format(i, msg))
if isinstance(self._skylight_parameters, DetailedSkylights):
msg = self._skylight_parameters.check_valid_for_face(self.floor_geometry)
if msg != '':
msgs.append(' Skylights - {}'.format(msg))
msg = self._skylight_parameters.check_overlaps(tolerance)
if msg != '':
msgs.append(' Skylights - {}'.format(msg))
msg = self._skylight_parameters.check_self_intersecting(tolerance)
if msg != '':
msgs.append(' Skylights - {}'.format(msg))
if len(msgs) == 0:
return [] if detailed else ''
full_msg = 'Room2D "{}" contains invalid window parameters.' \
'\n {}'.format(self.display_name, '\n '.join(msgs))
full_msg = self._validation_message_child(
full_msg, self, detailed, '100103', error_type='Invalid Window Parameters')
if detailed:
return [full_msg]
if raise_exception:
raise ValueError(full_msg)
return full_msg
[docs] def to_core_perimeter(self, perimeter_offset, air_boundary=False, tolerance=0.01):
"""Translate this Room2D into a list of Room2Ds separated by core and perimeter.
All of the resulting Room2Ds will have the same properties as this initial
Room2D with all windows and boundary conditions conserved. All of the
newly-created interior walls between the core and perimeter Room2Ds will
have Surface boundary conditions.
Args:
perimeter_offset: An optional positive number that will be used to offset
the perimeter of the all_story_geometry to create core/perimeter
zones. If this value is 0, no offset will occur and each story
will be represented with a single Room2D per polygon (Default: 0).
air_boundary: A boolean to note whether all of the new wall adjacencies
should be set to an AirBoundary type. (Default: False).
tolerance: The maximum difference between x, y, and z values at which
point vertices are considered to be the same. This is also needed as
a means to determine which floor geometries are equivalent to one
another and should be a part the same Story. Default: 0.01, suitable
for objects in meters.
Returns:
A list of Room2D for core Room2Ds followed by perimeter Room2Ds. If the
current Room2D cannot be converted into core and perimeter Room2Ds,
a list with the current Room2D instance will be returned.
"""
# create the floor Face3Ds from this Room2D's floor_geometry
tol = tolerance
new_face3d_array = []
floor_face = self.floor_geometry
z_val = floor_face[0].z
if floor_face.has_holes:
bound_p = Polygon2D([Point2D(p.x, p.y) for p in floor_face.boundary])
if bound_p.is_clockwise:
bound_p = bound_p.reverse()
hole_p = []
for hole in floor_face.holes:
hp = Polygon2D([Point2D(p.x, p.y) for p in hole])
if hp.is_clockwise:
hp = hp.reverse()
hole_p.append(hp)
subp_perim, subp_core = \
perimeter_core_by_offset(bound_p, perimeter_offset, hole_p)
if subp_core is None: # failed to offset the Face3D with holes
new_face3d_array.append(floor_face) # just use existing floor
else:
core_b = [Point3D(p.x, p.y, z_val) for p in subp_core[0]]
core_h = [[Point3D(p.x, p.y, z_val) for p in hole]
for hole in subp_core[1:]]
core_face = Face3D(core_b, holes=core_h)
new_face3d_array.append(core_face)
for spl in subp_perim:
sub_face = Face3D([Point3D(pt.x, pt.y, z_val) for pt in spl])
new_face3d_array.append(sub_face)
else:
base_p = Polygon2D([Point2D(p.x, p.y) for p in floor_face.boundary])
if base_p.is_clockwise:
base_p = base_p.reverse()
try:
sub_polys_perim, sub_polys_core = perimeter_core_subpolygons(
polygon=base_p, distance=perimeter_offset, tol=tol)
for spl in sub_polys_perim + sub_polys_core:
sub_face = Face3D([Point3D(pt.x, pt.y, z_val) for pt in spl])
new_face3d_array.append(sub_face)
except (RuntimeError, TypeError): # the generation of the polyskel failed
new_face3d_array.append(floor_face) # just use existing floor
# if core/perimeter geometry could not be generated, return the current room
if len(new_face3d_array) <= 1:
return [self]
# create the new Room2D objects from the result
parent_zip = (
self.floor_segments_2d, self.boundary_conditions,
self.window_parameters, self.shading_parameters, self.air_boundaries
)
new_rooms = []
for i, floor_geo in enumerate(new_face3d_array):
# determine the segments of the new Room2D
if floor_geo.normal.z < 0: # ensure upward-facing Face3D
floor_geo = floor_geo.flip()
o_p = Plane(Vector3D(0, 0, 1), Point3D(0, 0, floor_geo.plane.o.z))
floor_geo = Face3D(floor_geo.boundary, o_p, floor_geo.holes)
new_room_seg = floor_geo.boundary_polygon2d.segments \
if not floor_geo.has_holes \
else floor_geo.boundary_polygon2d.segments + \
tuple(seg for hole in floor_geo.hole_polygon2d for seg in hole.segments)
# match the new segments to the existing properties
new_bcs, new_win, new_shd, new_abs = [], [], [], []
for new_seg in new_room_seg:
p1, p2 = new_seg.p1, new_seg.p2
for seg, bc, wp, sp, ab in zip(*parent_zip):
if seg.distance_to_point(p1) <= tol and \
seg.distance_to_point(p2) <= tol:
new_bcs.append(bc)
new_win.append(wp)
new_shd.append(sp)
new_abs.append(ab)
break
else:
new_bcs.append(bcs.outdoors)
new_win.append(None)
new_shd.append(None)
new_abs.append(False)
new_id = '{}_{}'.format(self.identifier, i)
new_room = Room2D(
new_id, floor_geo, self.floor_to_ceiling_height, new_bcs, new_win,
new_shd, self.is_ground_contact, self.is_top_exposed, tol)
new_room.air_boundaries = new_abs
new_room.display_name = '{}_{}'.format(self.display_name, i)
new_room._properties._duplicate_extension_attr(self._properties)
new_rooms.append(new_room)
# solve adjacency between the Room2Ds
adj_info = Room2D.solve_adjacency(new_rooms, tol)
if air_boundary: # set air boundary type if requested
for room_pair in adj_info:
for room_adj in room_pair:
room, wall_i = room_adj
room.set_air_boundary(wall_i)
return new_rooms
[docs] def to_honeybee(
self, multiplier=1, add_plenum=False, tolerance=0.01,
enforce_bc=True, enforce_solid=True):
"""Convert Dragonfly Room2D to a Honeybee Room.
Args:
multiplier: An integer greater than 0 that denotes the number of times
the room is repeated. You may want to set this differently depending
on whether you are exporting each room as its own geometry (in which
case, this should be 1) or you only want to simulate the "unique" room
once and have the results multiplied. Default: 1.
add_plenum: Boolean to indicate whether ceiling/floor plenums should
be auto-generated for the Room in which case this output will
be a list instead of a single Room. The height of the ceiling plenum
will be autocalculated as the difference between the Room2D
ceiling height and Story ceiling height. The height of the floor
plenum will be autocalculated as the difference between the Room2D
floor height and Story floor height. (Default: False).
tolerance: The minimum distance in z values of floor_height and
floor_to_ceiling_height at which adjacent Faces will be split.
This is also used in the generation of Windows, and to check if the
Room ceiling is adjacent to the upper floor of the Story before
generating a plenum. Default: 0.01, suitable for objects in meters.
enforce_bc: Boolean to note whether an exception should be raised if
apertures are assigned to Wall with an illegal boundary conditions
(True) or if the invalid boundary condition should be replaced
with an Outdoor boundary condition (False). (Default: True).
enforce_solid: Boolean to note whether the room should be translated
as a solid extrusion whenever translating the room with custom
roof geometry produces a non-solid result (True) or the non-solid
room geometry should be allowed to remain in the result (False).
The latter is useful for understanding why a particular roof
geometry has produced a non-solid result. (Default: True).
Returns:
A tuple with the two items below.
* hb_room -- If add_plenum is False, this will be honeybee-core Room
representing the dragonfly Room2D. If the add_plenum argument is True,
this item will be a list of honeybee-core Rooms, with the hb_room as
the first item, and up to two additional items:
* ceil_plenum -- A honeybee-core Room representing the ceiling
plenum. If there isn't enough space between the Story
floor_to_floor_height and the Room2D floor_to_ceiling height,
this item will be None.
* floor_plenum -- A honeybee-core Room representing the floor plenum.
If there isn't enough space between the Story floor_height and
the Room2D floor_height, this item will be None.
* adjacencies -- A list of tuples that record any adjacencies that
should be set on the level of the Story to which the Room2D belongs.
Each tuple will have a honeybee Face as the first item and a
tuple of Surface.boundary_condition_objects as the second item.
"""
# create the honeybee Room
has_roof, shade_geo = False, None
if self._parent is not None and self._parent._roof is not None:
# generate the room volume from the slanted roof
if self.is_top_exposed and multiplier == 1:
room_polyface, roof_face_i, shade_geo = \
self._room_volume_with_roof(self._parent._roof, tolerance)
if room_polyface is None: # complete failure to interpret roof
has_roof = False
elif enforce_solid and not room_polyface.is_solid:
has_roof = False
else:
has_roof = True
if not has_roof: # generate the Room volume normally through extrusion
room_polyface = Polyface3D.from_offset_face(
self._floor_geometry, self.floor_to_ceiling_height)
roof_face_i = [-1]
# create the honeybee Room and set the RoofCeiling faces
hb_room = Room.from_polyface3d(
self.identifier, room_polyface, ground_depth=self.floor_height - 1)
roof_faces = []
for i in roof_face_i:
rfc = hb_room[i]
rfc.type = ftyp.roof_ceiling
roof_faces.append(rfc)
# assign BCs and record any Surface conditions to be set on the story level
adjacencies = []
for i, bc in enumerate(self._boundary_conditions):
if not isinstance(bc, Surface):
hb_room[i + 1]._boundary_condition = bc
else:
adjacencies.append((hb_room[i + 1], bc.boundary_condition_objects))
# assign windows, shading, and air boundary properties to walls
for i, glz_par in enumerate(self._window_parameters):
if glz_par is not None:
hb_face = hb_room[i + 1]
try:
glz_par.add_window_to_face(hb_face, tolerance)
except AssertionError as e:
if enforce_bc:
raise e
hb_room[i + 1]._boundary_condition = bcs.outdoors
hb_room[i + 1].remove_sub_faces()
glz_par.add_window_to_face(hb_face, tolerance)
if has_roof and isinstance(glz_par, _AsymmetricBase):
valid_ap = []
for ap in hb_face._apertures:
if hb_face.geometry._is_sub_face(ap.geometry):
valid_ap.append(ap)
if len(hb_face._apertures) != len(valid_ap):
hb_face.remove_apertures()
hb_face.add_apertures(valid_ap)
for i, shd_par in enumerate(self._shading_parameters):
if shd_par is not None:
shd_par.add_shading_to_face(hb_room[i + 1], tolerance)
if self._air_boundaries is not None:
for i, a_bnd in enumerate(self._air_boundaries):
if a_bnd:
hb_room[i + 1].type = ftyp.air_boundary
# ensure matching adjacent Faces across the Story
if self._parent is not None and not has_roof:
new_faces = self._split_walls_along_height(hb_room, tolerance, add_plenum)
if len(new_faces) != len(hb_room):
# rebuild the room with split surfaces
hb_room = Room(self.identifier, new_faces, tolerance, 0.1)
# update adjacencies with the new split face
for i, adj in enumerate(adjacencies):
face_id = adj[0].identifier
for face in hb_room.faces:
if face.identifier == face_id:
adjacencies[i] = (face, adj[1])
break
# set the story, multiplier, display_name, and user_data
if self.has_parent:
hb_room.story = self.parent.display_name
hb_room.multiplier = multiplier
hb_room._display_name = self._display_name
hb_room._user_data = self._user_data
# assign boundary conditions for the roof and floor
try:
hb_room[0].boundary_condition = bcs.adiabatic
for rf in roof_faces:
rf.boundary_condition = bcs.adiabatic
except AttributeError:
pass # honeybee_energy is not loaded and Adiabatic type doesn't exist
if self._is_ground_contact:
hb_room[0].boundary_condition = bcs.ground
if self._is_top_exposed:
for rf in roof_faces:
rf.boundary_condition = bcs.outdoors
# assign any Shade geometries like dormers that might result from the roof
if shade_geo:
for i, s_geo in enumerate(shade_geo):
shd_id = '{}_RoofShade{}'.format(self.identifier, i)
hb_room.add_outdoor_shade(Shade(shd_id, s_geo))
# transfer any extension properties assigned to the Room2D and return result
hb_room._properties = self.properties.to_honeybee(hb_room)
if not add_plenum or has_roof:
if self._skylight_parameters is not None:
if self._is_top_exposed:
for rf in roof_faces:
self._skylight_parameters.add_skylight_to_face(rf, tolerance)
return hb_room, adjacencies
# add plenums if requested and return results
hb_plenums = self._honeybee_plenums(hb_room, tolerance=tolerance)
for hb_plenum in hb_plenums: # transfer the parent's construction set
hb_plenum._properties = self.properties.to_honeybee(hb_plenum)
hb_plenum.exclude_floor_area = True
try: # set the program to unconditioned plenum and assign infiltration
hb_plenum.properties.energy.program_type = None
hb_plenum.properties.energy.hvac = None
hb_plenum.properties.energy._shw = None
hb_plenum.properties.energy.infiltration = \
hb_room.properties.energy.infiltration
except AttributeError:
pass # honeybee-energy is not loaded; ignore all these energy properties
# set the skylights if top is exposed and there's no ceiling plenum
if self._skylight_parameters is not None:
if self._is_top_exposed:
if len(hb_plenums) == 0:
self._skylight_parameters.add_skylight_to_face(
hb_room[-1], tolerance)
elif len(hb_plenums) == 1 and \
hb_plenums[0].identifier.endswith('floor_plenum'):
self._skylight_parameters.add_skylight_to_face(
hb_room[-1], tolerance)
# return the rooms and the adjacency information
return [hb_room] + hb_plenums, adjacencies
[docs] def to_dict(self, abridged=False, included_prop=None):
"""Return Room2D as a dictionary.
Args:
abridged: Boolean to note whether the extension properties of the
object (ie. program_type, construction_set) should be included in detail
(False) or just referenced by identifier (True). Default: False.
included_prop: List of properties to filter keys that must be included in
output dictionary. For example ['energy'] will include 'energy' key if
available in properties to_dict. By default all the keys will be
included. To exclude all the keys from extensions use an empty list.
"""
base = {'type': 'Room2D'}
base['identifier'] = self.identifier
base['display_name'] = self.display_name
if abridged and self._abridged_properties is not None:
base['properties'] = self._abridged_properties
else:
base['properties'] = self.properties.to_dict(abridged, included_prop)
base['floor_boundary'] = [(pt.x, pt.y) for pt in self._floor_geometry.boundary]
if self._floor_geometry.has_holes:
base['floor_holes'] = \
[[(pt.x, pt.y) for pt in hole] for hole in self._floor_geometry.holes]
base['floor_height'] = self._floor_geometry[0].z
base['floor_to_ceiling_height'] = self._floor_to_ceiling_height
base['is_ground_contact'] = self._is_ground_contact
base['is_top_exposed'] = self._is_top_exposed
bc_dicts = []
for bc in self._boundary_conditions:
if isinstance(bc, Outdoors) and 'energy' in base['properties']:
bc_dicts.append(bc.to_dict(full=True))
else:
bc_dicts.append(bc.to_dict())
base['boundary_conditions'] = bc_dicts
if not all((param is None for param in self._window_parameters)):
base['window_parameters'] = []
for glz in self._window_parameters:
val = glz.to_dict() if glz is not None else None
base['window_parameters'].append(val)
if not all((param is None for param in self._shading_parameters)):
base['shading_parameters'] = []
for shd in self._shading_parameters:
val = shd.to_dict() if shd is not None else None
base['shading_parameters'].append(val)
if self._air_boundaries is not None:
if not all((not param for param in self._air_boundaries)):
base['air_boundaries'] = self._air_boundaries
if self._skylight_parameters is not None:
base['skylight_parameters'] = self.skylight_parameters.to_dict()
if self.user_data is not None:
base['user_data'] = self.user_data
return base
@property
def to(self):
"""Room2D writer object.
Use this method to access Writer class to write the room2d in other formats.
"""
return writer
[docs] @staticmethod
def solve_adjacency(room_2ds, tolerance=0.01, resolve_window_conflicts=True):
"""Solve for all adjacencies between a list of input Room2Ds.
Args:
room_2ds: A list of Room2Ds for which adjacencies will be solved.
tolerance: The minimum difference between the coordinate values of two
faces at which they can be considered adjacent. (Default: 0.01,
suitable for objects in meters).
resolve_window_conflicts: Boolean to note whether conflicts between
window parameters of adjacent segments should be resolved during
adjacency setting or an error should be raised about the mismatch.
Resolving conflicts will default to the window parameters with the
larger are and assign them to the other segment. (Default: True).
Returns:
A list of tuples with each tuple containing 2 sub-tuples for wall
segments paired in the process of solving adjacency. Sub-tuples have
the Room2D as the first item and the index of the adjacent wall as the
second item. This data can be used to assign custom properties to the
new adjacent walls (like assigning custom window parameters for
interior windows, assigning air boundaries, or custom boundary
conditions).
"""
rwc = resolve_window_conflicts
adj_info = []
for i, room_1 in enumerate(room_2ds):
try:
for room_2 in room_2ds[i + 1:]:
if not Polygon2D.overlapping_bounding_rect(
room_1._floor_geometry.boundary_polygon2d,
room_2._floor_geometry.boundary_polygon2d, tolerance):
continue # no overlap in bounding rect; adjacency impossible
for j, seg_1 in enumerate(room_1.floor_segments_2d):
for k, seg_2 in enumerate(room_2.floor_segments_2d):
if not isinstance(room_2._boundary_conditions[k], Surface):
if seg_1.distance_to_point(seg_2.p1) <= tolerance and \
seg_1.distance_to_point(seg_2.p2) <= tolerance:
# set the boundary conditions of the segments
room_1.set_adjacency(room_2, j, k, rwc)
adj_info.append(((room_1, j), (room_2, k)))
break
except IndexError:
pass # we have reached the end of the list of rooms
return adj_info
[docs] @staticmethod
def find_adjacency(room_2ds, tolerance=0.01):
"""Get a list with all adjacent pairs of segments between input Room2Ds.
Note that this method does not change any boundary conditions of the input
Room2Ds or mutate them in any way. It's purely a geometric analysis of the
segments between Room2Ds.
Args:
room_2ds: A list of Room2Ds for which adjacencies will be solved.
tolerance: The minimum difference between the coordinate values of two
faces at which they can be considered adjacent. (Default: 0.01,
suitable for objects in meters).
Returns:
A list of tuples for each discovered adjacency. Each tuple contains
2 sub-tuples with two elements. The first element is the Room2D and
the second is the index of the wall segment that is adjacent.
"""
adj_info = [] # lists of adjacencies to track
for i, room_1 in enumerate(room_2ds):
try:
for room_2 in room_2ds[i + 1:]:
if not Polygon2D.overlapping_bounding_rect(
room_1._floor_geometry.boundary_polygon2d,
room_2._floor_geometry.boundary_polygon2d, tolerance):
continue # no overlap in bounding rect; adjacency impossible
for j, seg_1 in enumerate(room_1.floor_segments_2d):
for k, seg_2 in enumerate(room_2.floor_segments_2d):
if seg_1.distance_to_point(seg_2.p1) <= tolerance and \
seg_1.distance_to_point(seg_2.p2) <= tolerance:
adj_info.append(((room_1, j), (room_2, k)))
break
except IndexError:
pass # we have reached the end of the list of rooms
return adj_info
[docs] @staticmethod
def intersect_adjacency(room_2ds, tolerance=0.01, preserve_wall_props=True):
"""Intersect the line segments of an array of Room2Ds to ensure matching walls.
Also note that this method does not actually set the walls that are next to one
another to be adjacent. The solve_adjacency method must be used for this after
running this method.
Args:
room_2ds: A list of Room2Ds for which adjacent segments will be
intersected.
tolerance: The minimum difference between the coordinate values of two
faces at which they can be considered adjacent. Default: 0.01,
suitable for objects in meters.
preserve_wall_props: Boolean to note whether existing window parameters,
shading parameters and boundary conditions should be preserved as
vertices are added during intersection. If False, all boundary
conditions are replaced with Outdoors, all window parameters are
erased, and this method will execute quickly. If True, an attempt
will be made to split window parameters new across colinear segments.
Existing boundary conditions will also be kept. (Default: True).
Returns:
An array of Room2Ds that have been intersected with one another.
"""
# keep track of all data needed to map between 2D and 3D space
master_plane = room_2ds[0].floor_geometry.plane
move_dists = []
is_holes = []
polygon_2ds = []
tol = tolerance
# map all Room geometry into the same 2D space
for room in room_2ds:
# ensure all starting room heights match
dist = master_plane.o.z - room.floor_height
move_dists.append(dist) # record all distances moved
is_holes.append(False) # record that first Polygon doesn't have holes
polygon_2ds.append(room._floor_geometry.boundary_polygon2d)
# of there are holes in the face, add them as their own polygons
if room._floor_geometry.has_holes:
for hole in room._floor_geometry.hole_polygon2d:
move_dists.append(dist) # record all distances moved
is_holes.append(True) # record that first Polygon doesn't have holes
polygon_2ds.append(hole)
# intersect the Room2D polygons within the 2D space
int_poly = Polygon2D.intersect_polygon_segments(polygon_2ds, tol)
# convert the resulting coordinates back to 3D space
face_pts = []
for poly, dist, is_hole in zip(int_poly, move_dists, is_holes):
pt_3d = [master_plane.xy_to_xyz(pt) for pt in poly]
if dist != 0:
pt_3d = [Point3D(pt.x, pt.y, pt.z - dist) for pt in pt_3d]
if not is_hole:
face_pts.append((pt_3d, []))
else:
face_pts[-1][1].append(pt_3d)
# rebuild all of the floor geometries to the input Room2Ds
intersected_rooms = []
for i, face_loops in enumerate(face_pts):
if len(face_loops[1]) == 0: # no holes
new_geo = Face3D(face_loops[0], room_2ds[i].floor_geometry.plane)
else: # ensure holes are included
new_geo = Face3D(face_loops[0], room_2ds[i].floor_geometry.plane,
face_loops[1])
rebuilt_room = Room2D(
room_2ds[i].identifier, new_geo, room_2ds[i].floor_to_ceiling_height,
is_ground_contact=room_2ds[i].is_ground_contact,
is_top_exposed=room_2ds[i].is_top_exposed)
rebuilt_room._skylight_parameters = room_2ds[i].skylight_parameters
rebuilt_room._display_name = room_2ds[i]._display_name
rebuilt_room._user_data = None if room_2ds[i].user_data is None else \
room_2ds[i].user_data.copy()
rebuilt_room._parent = room_2ds[i]._parent
rebuilt_room._abridged_properties = room_2ds[i]._abridged_properties
rebuilt_room._properties._duplicate_extension_attr(room_2ds[i]._properties)
intersected_rooms.append(rebuilt_room)
# transfer the wall properties if requested
if preserve_wall_props:
for orig_r, new_r in zip(room_2ds, intersected_rooms):
# get the relevant original segments to check for matches
rel_segs, rel_bcs, rel_win, rel_shd = [], [], [], []
o_zip_props = zip(orig_r.floor_segments, orig_r._boundary_conditions,
orig_r._window_parameters, orig_r._shading_parameters)
for seg, bc, win, shd in o_zip_props:
rel_segs.append(seg)
rel_win.append(win)
rel_shd.append(shd)
if not isinstance(bc, Surface):
rel_bcs.append(bc)
else:
rel_bcs.append(bcs.outdoors)
# build up new lists of parameters if the segments match
new_bcs, new_win, new_shd = {}, {}, {}
for k, seg1 in enumerate(rel_segs):
m_win_segs, m_i = [], []
for i, seg2 in enumerate(new_r.floor_segments):
if seg1.distance_to_point(seg2.p1) <= tol and \
seg1.distance_to_point(seg2.p2) <= tol: # colinear
new_bcs[i] = rel_bcs[k]
new_shd[i] = rel_shd[k]
m_win_segs.append(seg2)
m_i.append(i)
# split the window parameters across the matched segments
wp_par_to_split = rel_win[k]
if wp_par_to_split is None:
for i in m_i:
new_win[i] = None
elif len(m_i) == 1:
new_win[m_i[0]] = wp_par_to_split
else:
split_par = wp_par_to_split.split(m_win_segs)
for i, w_par in zip(m_i, split_par):
new_win[i] = w_par
new_r.boundary_conditions = [new_bcs[i] for i in range(len(new_r))]
new_r.window_parameters = [new_win[i] for i in range(len(new_r))]
new_r.shading_parameters = [new_shd[i] for i in range(len(new_r))]
return tuple(intersected_rooms)
[docs] @staticmethod
def group_by_adjacency(rooms):
"""Group Room2Ds together that are connected by adjacencies.
This is useful for separating rooms in the case where a Story contains
multiple towers or sections that are separated by outdoor boundary conditions.
Args:
rooms: A list of Room2Ds to be grouped by their adjacency.
Returns:
A list of list with each sub-list containing rooms that share adjacencies.
"""
return Room2D._adjacency_grouping(rooms, Room2D._find_adjacent_rooms)
[docs] @staticmethod
def group_by_air_boundary_adjacency(rooms):
"""Group Room2Ds together that share air boundaries.
This is useful for understanding the radiant enclosures that will exist
when a model is exported to EnergyPlus.
Args:
rooms: A list of Room2Ds to be grouped by their air boundary adjacency.
Returns:
A list of list with each sub-list containing Room2Ds that share adjacent
air boundaries. If a Room has no air boundaries it will the the only
item within its sub-list.
"""
return Room2D._adjacency_grouping(
rooms, Room2D._find_adjacent_air_boundary_rooms)
[docs] @staticmethod
def join_room_2ds(room_2ds, min_separation=0, tolerance=0.01):
"""Join Room2Ds together that are touching one another within a min_separation.
When the min_separation is less than or equal to the tolerance, all
properties of segments for the input Room2Ds will be preserved. When
the min_separation is larger than the tolerance, an attempt is made to
preserve all wall properties but there is a risk of losing some windows
just in the region where two Room2Ds are joined together across a gap
between them. This risk can be overcome by inserting Room2D vertices
around where the gap will be crossed between that Room2D and the
other Room2D.
The largest Room2D that is identified within each connected group will
determine the extension properties of the resulting Room2D. Skylights
will be merged across rooms if they are of the same type or if they are None.
Args:
room_2ds: A list of Room2Ds which will be joined together where they
touch one another.
min_separation: A number for the minimum distance between Room2Ds that
is considered a meaningful separation. Gaps between Room2Ds that
are less than this distance will result in the Room2Ds being
joined across the gap. When the input Room2Ds have floor_geometry
representing the boundaries defined by the interior wall finishes,
this input can be thought of as the maximum interior wall thickness.
When Room2Ds are perfectly touching one another within the tolerance
(with Room2D floor_geometry drawn to the center lines of interior
walls), this value can be set to zero or anything less than or
equal to the tolerance. Doing so will yield a cleaner result for the
boundary, which will be faster and more reliable. Note that care
should be taken not to set this value higher than the length of any
meaningful exterior wall segments. Otherwise, the exterior segments
will be ignored in the result. This can be particularly dangerous
around curved exterior walls that have been planarized through
subdivision into small segments. (Default: 0).
tolerance: The minimum distance between a vertex and the polygon
boundary at which point the vertex is considered to lie on the
polygon. (Default: 0.01, suitable for objects in meters).
"""
# get the horizontal boundaries around the input Room2Ds
h_bnds = Room2D.grouped_horizontal_boundary(room_2ds, min_separation, tolerance)
if len(h_bnds) == len(room_2ds): # no Room2Ds to join; return them as they are
return room_2ds
# join the Room2Ds according to the horizontal boundaries that were found
if min_separation <= tolerance:
room_2ds = Room2D.intersect_adjacency(room_2ds, tolerance)
joined_rooms = []
for h_bnd in h_bnds:
bnd_p_gon = Polygon2D([Point2D(pt.x, pt.y) for pt in h_bnd.boundary])
h_p = None
if h_bnd.has_holes:
h_p = []
for hole in h_bnd.holes:
h_p.append(Polygon2D([Point2D(pt.x, pt.y) for pt in hole]))
new_room = Room2D.join_by_boundary(
room_2ds, bnd_p_gon, h_p, tolerance=tolerance)
joined_rooms.append(new_room)
return joined_rooms
[docs] @staticmethod
def join_by_boundary(
room_2ds, polygon, hole_polygons=None, floor_to_ceiling_height=None,
identifier=None, display_name=None, tolerance=0.01):
"""Join several Room2D together using a boundary Polygon as a guide.
All properties of segments along the boundary polygon will be preserved.
The largest Room2D that is identified within the boundary polygon will
determine the extension properties of the resulting Room unless the supplied
identifier matches an existing Room2D inside the polygon. Skylights
will be merged if they are of the same type or if they are None.
It is recommended that the Room2Ds be aligned to the boundaries
of the polygon and duplicate vertices be removed before passing them
through this method. However, colinear vertices should not be removed
where possible. This helps ensure that relevant Room2D segments
are colinear with the polygon and so they can influence the result.
Args:
room_2ds: A list of Room2Ds which will be joined together using the polygon.
polygon: A ladybug_geometry Polygon2D which will become the boundary
of the output joined Room2D.
hole_polygons: An optional list of hole polygons, which will add
holes into the output joined Room2D polygon. (Default: None).
floor_to_ceiling_height: An optional number to set the floor-to-ceiling
height of the resulting Room2D. If None, it will be the maximum
of the Room2Ds that are found inside the polygon, which ensures
that all window geometries are included in the output. If specified
and it is lower than the maximum Room2D height, any detailed
windows will be automatically trimmed to accommodate the new
floor-to-ceiling height. (Default: None).
identifier: An optional text string for the identifier of the new
joined Room2D. If this matches an existing Room2D inside of the
polygon, the existing Room2D will be used to set the extension
properties of the output Room2D. If None, the identifier
and extension properties of the output Room2D will be those of
the largest Room2D found inside of the polygon. (Default: None).
display_name: An optional text string for the display_name of the new
joined Room2D. If None, the display_name will be taken from the
largest existing Room2D inside the polygon or the existing
Room2D matching the identifier above. (Default: None).
tolerance: The minimum distance between a vertex and the polygon
boundary at which point the vertex is considered to lie on the
polygon. (Default: 0.01, suitable for objects in meters).
"""
tol = tolerance
# ensure that all polygons are counterclockwise
polygon = polygon.reverse() if polygon.is_clockwise else polygon
if hole_polygons is not None:
cc_hole_polygons = []
for p in hole_polygons:
p = p.reverse() if p.is_clockwise else p
cc_hole_polygons.append(p)
hole_polygons = cc_hole_polygons
# identify all Room2Ds inside of the polygon
rel_rooms, rel_ids, rel_a, rel_fh, rel_ch = [], [], [], [], []
test_vec = Vector2D(0.99, 0.01)
for room in room_2ds:
if room.floor_geometry.is_convex:
rm_pt = room.center
else:
rm_pt_3d = room.floor_geometry._point_on_face(tol)
rm_pt = Point2D(rm_pt_3d.x, rm_pt_3d.y)
if polygon.is_point_inside_bound_rect(rm_pt, test_vec):
rel_rooms.append(room)
rel_ids.append(room.identifier)
rel_a.append(room.floor_area)
rel_fh.append(room.floor_height)
rel_ch.append(room.floor_to_ceiling_height)
# if no rooms are inside the polygon, just return a dummy room from the polygon
if len(rel_rooms) == 0:
fh = sum([r.floor_height for r in room_2ds]) / len(room_2ds)
ftc = sum([r.floor_to_ceiling_height for r in room_2ds]) / len(room_2ds) \
if floor_to_ceiling_height is None else floor_to_ceiling_height
bound_verts = [Point3D(p.x, p.y, fh) for p in polygon.vertices]
all_hole_verts = None
if hole_polygons is not None and len(hole_polygons) != 0:
all_hole_verts = []
for hole in hole_polygons:
all_hole_verts.append([Point3D(p.x, p.y, fh) for p in hole.vertices])
new_geo = Face3D(bound_verts, holes=all_hole_verts)
r_id = clean_and_id_string('Room') if identifier is None else identifier
return Room2D(r_id, new_geo, ftc)
# determine the new floor heights using max/average across relevant rooms
new_flr_height = sum(rel_fh) / len(rel_fh)
max_ftc = max(rel_ch)
new_ftc = max_ftc if floor_to_ceiling_height is None else floor_to_ceiling_height
# determine a primary room to set help set properties or the resulting room
if identifier is None or identifier not in rel_ids:
# find the largest room of the relevant rooms
sort_inds = [i for _, i in sorted(zip(rel_a, range(len(rel_a))))]
primary_room = rel_rooms[sort_inds[-1]]
if identifier is None:
identifier = primary_room.identifier
else: # draw properties from the room with the matching identifier
for r_id, rm in zip(rel_ids, rel_rooms):
if r_id == identifier:
primary_room = rm
break
if display_name is None:
display_name = primary_room.display_name
# gather all segments and properties of relevant rooms
rel_segs, rel_bcs, rel_win, rel_shd, rel_abs = [], [], [], [], []
for room in rel_rooms:
rel_segs.extend(room.floor_segments_2d)
rel_bcs.extend(room.boundary_conditions)
rel_shd.extend(room.shading_parameters)
rel_abs.extend(room.air_boundaries)
w_par = room.window_parameters
in_range = new_ftc - tol < room.floor_to_ceiling_height < new_ftc + tol
if not in_range: # adjust window ratios to preserve area
new_w_par = []
for i, wp in enumerate(w_par):
if isinstance(wp, SimpleWindowRatio):
w_area = wp.area_from_segment(
rel_segs[i], room.floor_to_ceiling_height)
new_ratio = w_area / (new_ftc * rel_segs[i].length)
new_wp = wp.duplicate()
new_wp._window_ratio = new_ratio if new_ratio <= 0.99 else 0.99
new_w_par.append(new_wp)
else:
new_w_par.append(wp)
w_par = new_w_par
rel_win.extend(w_par)
# find all of the Room2Ds segments that lie on each polygon segment
new_bcs, new_win, new_shd, new_abs = [], [], [], []
bound_verts = Room2D._segments_along_polygon(
polygon, rel_segs, rel_bcs, rel_win, rel_shd, rel_abs,
new_bcs, new_win, new_shd, new_abs, new_flr_height, tol)
if hole_polygons is not None and len(hole_polygons) != 0:
all_hole_verts = []
for hole in hole_polygons:
hole_verts = Room2D._segments_along_polygon(
hole, rel_segs, rel_bcs, rel_win, rel_shd, rel_abs,
new_bcs, new_win, new_shd, new_abs, new_flr_height, tol)
all_hole_verts.append(hole_verts)
new_geo = Face3D(bound_verts, holes=all_hole_verts)
else:
new_geo = Face3D(bound_verts)
# merge skylights across the input rooms if they are of the same type
new_sky_lights, new_areas = [], []
for room in rel_rooms:
if room.skylight_parameters is not None:
new_sky_lights.append(room.skylight_parameters)
new_areas.append(room.floor_area)
new_sky_light = None
if all(isinstance(sl, DetailedSkylights) for sl in new_sky_lights):
try:
new_polys = new_sky_lights[0].polygons
new_is_dr = new_sky_lights[0].are_doors
for sl in new_sky_lights[1:]:
new_polys += sl.polygons
new_is_dr += sl.are_doors
new_sky_light = DetailedSkylights(new_polys, new_is_dr)
except IndexError:
pass # skylight with no polygons
elif all(isinstance(sl, GriddedSkylightArea) for sl in new_sky_lights):
new_area = sum(sl.skylight_area for sl in new_sky_lights)
new_sky_light = GriddedSkylightArea(new_area)
elif all(isinstance(sl, GriddedSkylightRatio) for sl in new_sky_lights):
zip_obj = zip(new_sky_lights, new_areas)
new_area = sum(sl.skylight_ratio * fa for sl, fa in zip_obj)
new_ratio = new_area / sum(room.floor_area for room in rel_rooms)
new_sky_light = GriddedSkylightRatio(new_ratio)
# merge all segments and properties into a single Room2D
new_room = Room2D(
identifier, new_geo, new_ftc, new_bcs, new_win, new_shd,
primary_room.is_ground_contact, primary_room.is_top_exposed, tol)
new_room.skylight_parameters = new_sky_light
new_room.air_boundaries = new_abs
new_room.display_name = display_name
new_room._properties._duplicate_extension_attr(primary_room._properties)
# if the floor-to-ceiling height is lower than the max, re-trim windows
if new_ftc < max_ftc:
new_w_pars = []
for w_par, seg in zip(new_room._window_parameters, new_room.floor_segments):
if isinstance(w_par, DetailedWindows):
new_w_par = w_par.adjust_for_segment(seg, new_ftc, tolerance)
else:
new_w_par = w_par
new_w_pars.append(new_w_par)
new_room._window_parameters = new_w_pars
return new_room
[docs] @staticmethod
def grouped_horizontal_boundary(room_2ds, min_separation=0, tolerance=0.01):
"""Get a list of Face3D for the horizontal boundary around several Room2Ds.
This method will attempt to produce a boundary that follows along the
walls of the Room2Ds and it is not suitable for groups of Room2Ds that
overlap one another in plan. This method may return an empty list if the
min_separation is so large that a continuous boundary could not be determined
or if overlaps between input Room2Ds result in failure.
Args:
room_2ds: A list of Room2Ds for which the horizontal boundary will
be computed.
min_separation: A number for the minimum distance between Room2Ds that
is considered a meaningful separation. Gaps between Room2Ds that
are less than this distance will be ignored and the boundary
will continue across the gap. When the input Room2Ds have floor_geometry
representing the boundaries defined by the interior wall finishes,
this input can be thought of as the maximum interior wall thickness,
which should be ignored in the calculation of the overall boundary
of the Room2Ds. When Room2Ds are touching one another (with Room2D
floor_geometry drawn to the center lines of interior walls), this
value can be set to zero or anything less than or equal to the
tolerance. Doing so will yield a cleaner result for the
boundary, which will be faster. Note that care should be taken
not to set this value higher than the length of any meaningful
exterior wall segments. Otherwise, the exterior segments
will be ignored in the result. This can be particularly dangerous
around curved exterior walls that have been planarized through
subdivision into small segments. (Default: 0).
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).
"""
# get the floor geometry of the rooms
floor_geos = [room.floor_geometry for room in room_2ds]
# remove colinear vertices and degenerate rooms
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)
# if the min_separation is small, use the more reliable intersection method
if min_separation <= tolerance:
closed_polys = Polygon2D.joined_intersected_boundary(floor_polys, tolerance)
else: # otherwise, use the more intense and less reliable gap crossing method
closed_polys = Polygon2D.gap_crossing_boundary(
floor_polys, min_separation, 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)
[docs] @staticmethod
def generate_alignment_axes(room_2ds, distance, direction=Vector2D(0, 1),
angle_tolerance=1.0):
"""Get suggested LineSegment2Ds for the Room2D.align method.
This method will return the most common axes across the input Room2D
geometry along with the number of Room2D segments that correspond to
each axis. The latter can be used to filter the suggested alignment axes
to get only the most common ones across the input Room2Ds.
Args:
room_2ds: A list of Room2D objects for which common axes will be evaluated.
distance: A number for the distance that will be used in the alignment
operation. This will be used to determine the resolution at which
alignment axes are generated and evaluated. Smaller alignment
distances will result in the generation of more common_axes since
a finer resolution can differentiate common that would typically be
grouped together. For typical building geometry, an alignment distance
of 0.3 meters or 1 foot is typically suitable for eliminating
unwanted details while not changing the geometry too much from
its original location.
direction: A Vector2D object to represent the direction in which the
common axes will be evaluated and generated.
angle_tolerance: The max angle difference in radians that the Room2D
segment direction can differ from the input direction before the
segments are not factored into this calculation of common axes.
Returns:
A tuple with two elements.
- common_axes: A list of LineSegment2D objects for the common
axes across the input Room2Ds.
- axis_values: A list of integers that aligns with the common_axes
and denotes how many segments of the input Room2D each axis
relates to. Higher numbers indicate that that the axis is more
commonly aligned across the Room2Ds.
"""
# process the inputs
min_distance, merge_distance = distance / 3, distance
ang_tol = math.radians(angle_tolerance)
polygons = []
for room in room_2ds:
polygons.append(room.floor_geometry.boundary_polygon2d)
if room.floor_geometry.has_holes:
for hole in room.floor_geometry.hole_polygon2d:
polygons.append(hole)
# return the common axes and values
return Polygon2D.common_axes(
polygons, direction, min_distance, merge_distance, ang_tol)
[docs] @staticmethod
def floor_segment_by_index(geometry, segment_index):
"""Get a particular LineSegment3D from a Face3D object.
The logic applied by this method to select the segment is the same that is
used to assign lists of values to the floor_geometry (eg. boundary conditions).
Args:
geometry: A Face3D representing floor geometry.
segment_index: An integer for the index of the segment to return.
"""
segs = geometry.boundary_segments if geometry.holes is \
None else geometry.boundary_segments + \
tuple(seg for hole in geometry.hole_segments for seg in hole)
return segs[segment_index]
def _room_volume_with_roof(self, roof_spec, tolerance):
"""Get a Polyface3D for the Room volume given a roof_spec above the room.
Args:
roof_spec: A Dragonfly RoofSpecification that describes the Roof
above the room geometry.
tolerance: The minimum distance from roof polygon edges at which a
point is considered to lie on the edge.
Returns:
A tuple with the two items below.
* room_polyface -- A Polyface3D object for the Room volume. This will
be None whenever the Room has no Roof geometries above it or there
are gaps or overlaps in the Roof geometries above the room.
* roof_face_i -- A list of integers for the indices of the faces in
the Polyface3D that correspond to the roof. Will be None whenever
the roof is not successfully applied to the Room.
* shade_geometry -- An list of Face3D for roof geometries that overlapped
with the Room2D but could not be easily incorporated into the Room
volume. Typical examples include roof geometries representing dormers
or roof elements that projected out and overhung into the Room2D
geometry. These should be translated to Room-assigned shades. Will
be None whenever the roof is not successfully applied to the Room.
"""
# get the roof polygons and the bounding Room2D polygon
roof_polys = roof_spec.boundary_geometry_2d
roof_planes = roof_spec.planes
room_poly = Polygon2D(
[Point2D(pt.x, pt.y) for pt in self.floor_geometry.boundary])
# gather all of the relevant roof polygons for the Room2D
rel_rf_polys, rel_rf_planes, is_full_bound = [], [], False
for rf_py, rf_pl in zip(roof_polys, roof_planes):
poly_rel = rf_py.polygon_relationship(room_poly, tolerance)
if poly_rel >= 0:
rel_rf_polys.append(rf_py)
rel_rf_planes.append(rf_pl)
if poly_rel == 1: # simple solution of one roof
is_full_bound = True
rel_rf_polys = [rel_rf_polys[-1]]
rel_rf_planes = [rel_rf_planes[-1]]
break
# make the room volume
p_faces = [self.floor_geometry.flip()] # a list of Room volume faces
proj_dir = Vector3D(0, 0, 1) # direction to project onto Roof planes
# when fully bounded, simply project the segments onto the single Roof face
if is_full_bound:
roof_plane = rel_rf_planes[0]
roof_verts = []
for seg in self.floor_segments:
p1, p2 = seg.p1, seg.p2
p3 = roof_plane.project_point(p2, proj_dir)
p4 = roof_plane.project_point(p1, proj_dir)
p_faces.append(Face3D((p1, p2, p3, p4)))
roof_verts.append(p4)
if not self.floor_geometry.has_holes:
p_faces.append(Face3D(roof_verts))
else:
v_count = len(self.floor_geometry.boundary)
part_roof_verts = [roof_verts[:v_count]]
for hole in self.floor_geometry.holes:
part_roof_verts.append(roof_verts[v_count:v_count + len(hole)])
v_count += len(hole)
p_faces.append(Face3D(part_roof_verts[0], holes=part_roof_verts[1:]))
return Polyface3D.from_faces(p_faces, tolerance), [-1], []
# when multiple roofs, each segment must be intersected with the roof polygons
# gather polygons that account for all of the Room2D holes
all_room_poly = [room_poly]
flr_segs = self.floor_segments
if self.floor_geometry.has_holes:
v_count = len(room_poly)
all_segments = [flr_segs[:v_count]]
for hole in self.floor_geometry.holes:
hole_poly = Polygon2D([Point2D(pt.x, pt.y) for pt in hole])
all_room_poly.append(hole_poly)
all_segments.append(flr_segs[v_count:v_count + len(hole)])
v_count += len(hole)
else:
all_segments = [flr_segs]
# create the walls from the segments by intersecting them with the roof
walls = self._wall_faces_with_roof(
all_room_poly, all_segments, rel_rf_polys, rel_rf_planes, tolerance)
if walls is None: # invalid roof geometry
return None, None, None
p_faces.extend(walls)
# add the roof faces using polygon boolean operations
roof_faces = self._roof_faces(
all_room_poly, rel_rf_polys, rel_rf_planes, tolerance)
if roof_faces is None: # invalid roof geometry
return None, None, None
roof_face_i = list(range(-1, -len(roof_faces) - 1, -1))
p_faces.extend(roof_faces)
# create the Polyface3D and try to repair it if it is not solid
room_polyface = Polyface3D.from_faces(p_faces, tolerance)
shade_geometry = []
ang_tol = math.radians(1)
if not room_polyface.is_solid:
# make sure that overlapping edges are merged so we don't get false readings
room_polyface = room_polyface.merge_overlapping_edges(tolerance, ang_tol)
if not room_polyface.is_solid:
# try to patch any vertical gaps between roofs with new walls
room_polyface, roof_face_i = \
self._patch_vertical_gaps(room_polyface, roof_face_i, tolerance)
if not room_polyface.is_solid:
room_polyface = room_polyface.merge_overlapping_edges(tolerance, ang_tol)
if not room_polyface.is_solid:
# remove disconnected roof geometries from the Polyface (eg. dormers)
room_polyface, roof_face_i, shade_geometry = \
self._separate_disconnected_faces(room_polyface, roof_face_i, tolerance)
if not room_polyface.is_solid:
room_polyface = room_polyface.merge_overlapping_edges(tolerance, ang_tol)
return room_polyface, roof_face_i, shade_geometry
def _wall_faces_with_roof(self, all_room_poly, all_segments,
rel_rf_polys, rel_rf_planes, tolerance):
"""Generate Face3D for the Room Walls when there are multiple Roof Polygons.
Args:
all_room_poly: A list of Polygon2D where each polygon represents either
the boundary of the room or a hole.
all_segments: A list of lists where each sub-list contains LineSegment2D
objects for each polygon in all_room_poly.
rel_rf_polys: A list of Polygon2D for the Roof geometries that are
relevant to the Room2D.
rel_rf_planes: A list of Plane objects for each Roof geometry that
is relevant to the Room2D.
tolerance: The distance value for absolute tolerance.
Returns:
A list of Face3D for the Walls of the Room. Will be None if the Roof
geometries are invalid.
"""
wall_faces = []
proj_dir = Vector3D(0, 0, 1) # direction to project onto Roof planes
# loop through holes and boundary polygons and generate walls from them
for rm_poly, rm_segs in zip(all_room_poly, all_segments):
# find the polygon that the first room vertex is located in
current_poly, current_plane = None, None
other_poly, other_planes = rel_rf_polys[:], rel_rf_planes[:] # copy lists
pt1 = rm_poly[0]
for i, (rf_py, rf_pl) in enumerate(zip(rel_rf_polys, rel_rf_planes)):
if rf_py.point_relationship(pt1, tolerance) >= 0:
current_poly, current_plane = rf_py, rf_pl
other_poly.pop(i)
other_planes.pop(i)
break
if current_poly is None: # first point not inside a roof, invalid roof
return None
# loop through segments and add vertices if they cross outside the roof face
rot_poly = rm_poly.vertices[1:] + (pt1,)
for pt2, seg in zip(rot_poly, rm_segs):
face_pts = [seg.p1, seg.p2]
# see if the segment ends in the same face it starts in
if current_poly.point_relationship(pt2, tolerance) >= 0: # project seg
face_pts.append(current_plane.project_point(seg.p2, proj_dir))
face_pts.append(current_plane.project_point(seg.p1, proj_dir))
else:
int_pts, int_pls = [(seg.p1, 0)], [current_plane]
# find where the segment leaves the polygon
seg_2d = LineSegment2D.from_array(((pt1.x, pt1.y), (pt2.x, pt2.y)))
for rf_seg in current_poly.segments:
int_pt = seg_2d.intersect_line_ray(rf_seg)
if int_pt is None:
dist, cls_pts = closest_point2d_between_line2d(
seg_2d, rf_seg)
if dist <= tolerance:
int_pt = cls_pts[0]
if int_pt is not None:
int_pts.append((int_pt, 0))
int_pls.append(current_plane)
# find where it intersects the other relevant polygons
for o_poly, o_pl in zip(other_poly, other_planes):
for o_seg in o_poly.segments:
int_pt = seg_2d.intersect_line_ray(o_seg)
if int_pt is None:
d, cls_pts = closest_point2d_between_line2d(
seg_2d, o_seg)
if d <= tolerance:
int_pt = cls_pts[0]
if int_pt is not None:
int_pts.append((int_pt, 1))
int_pls.append(o_pl)
# sort the intersections points along the segment
pt_dists = [(ipt[1], seg_2d.p1.distance_to_point(ipt[0]))
for ipt in int_pts]
pts_pls = [(i_pt[0], i_pl) for i_pt, i_pl in zip(int_pts, int_pls)]
sort_obj = sorted(zip(pt_dists, pts_pls), key=lambda pair: pair[0])
sort_pts_pls = [x for _, x in sort_obj]
# if two points are equivalent, reorder with the previous point plane
ord_pts = [x[0] for x in sort_pts_pls]
ord_pls = [x[1] for x in sort_pts_pls]
for i, (pt, pln) in enumerate(sort_pts_pls[1:]):
if pt.distance_to_point(ord_pts[i]) < tolerance:
prev_pl = ord_pls[i - 1]
if pln == prev_pl and ord_pls[i] != prev_pl: # reorder
ord_pts[i], ord_pts[i + 1] = ord_pts[i + 1], ord_pts[i]
ord_pls[i], ord_pls[i + 1] = ord_pls[i + 1], ord_pls[i]
# project the points onto the planes
rf_pts = [ipl.project_point(Point3D.from_point2d(ipt), proj_dir)
for ipt, ipl in zip(ord_pts, ord_pls)]
# add a vertex for where the segment ends in the polygon
for i, (rf_py, rf_pl) in enumerate(zip(other_poly, other_planes)):
if rf_py.point_relationship(pt2, tolerance) >= 0:
other_poly.pop(i)
other_poly.append(current_poly)
other_planes.pop(i)
other_planes.append(current_plane)
current_poly, current_plane = rf_py, rf_pl
rf_pts.append(
rf_pl.project_point(Point3D.from_point2d(pt2), proj_dir))
break
if current_poly is None or len(rf_pts) < 2:
# point not inside a roof, invalid roof
return None
# remove duplicated vertices from the list
rf_pts = [pt for i, pt in enumerate(rf_pts)
if not pt.is_equivalent(rf_pts[i - 1], tolerance)]
# check that the first two vertices are not a sliver
if abs(rf_pts[0].x - rf_pts[1].x) < tolerance and \
abs(rf_pts[0].y - rf_pts[1].y) < tolerance:
rf_pts.pop(0)
# add the points to the Face3D vertices
rf_pts.reverse()
face_pts.extend(rf_pts)
# make the final Face3D
if len(face_pts) == 2: # second point not inside a roof, invalid roof
return None
wall_faces.append(Face3D(face_pts))
pt1 = pt2 # increment for next segment
return wall_faces
def _roof_faces(self, all_room_poly, rel_rf_polys, rel_rf_planes, tolerance):
"""Generate Face3D for the Room Roofs when there are multiple Roof Polygons.
Args:
all_room_poly: A list of Polygon2D where each polygon represents either
the boundary of the room or a hole.
rel_rf_polys: A list of Polygon2D for the Roof geometries that are
relevant to the Room2D.
rel_rf_planes: A list of Plane objects for each Roof geometry that
is relevant to the Room2D.
tolerance: The distance value for absolute tolerance.
Returns:
A list of Face3D for the Roofs of the Room. Will be None if the Roof
geometries are invalid.
"""
roof_faces = []
proj_dir = Vector3D(0, 0, 1) # direction to project onto Roof planes
# create a BooleanPolygon for the Room2D
room_polys = []
for rom_poly in all_room_poly:
rom_poly = rom_poly.remove_colinear_vertices(tolerance)
room_polys.append((pb.BooleanPoint(pt.x, pt.y) for pt in rom_poly.vertices))
b_room_poly = pb.BooleanPolygon(room_polys)
# find the boolean intersection with each roof polygon and project the result
int_tol = tolerance / 100 # intersection tolerance must be finer
for rf_poly, rf_plane in zip(rel_rf_polys, rel_rf_planes):
# snap the polygons to one another to avoid tolerance issues
rf_poly = rf_poly.remove_colinear_vertices(tolerance)
for rom_poly in all_room_poly:
rf_poly = rom_poly.snap_to_polygon(rf_poly, tolerance)
rf_pts = (pb.BooleanPoint(pt.x, pt.y) for pt in rf_poly.vertices)
b_rf_poly = pb.BooleanPolygon([rf_pts])
try:
int_result = pb.intersect(b_room_poly, b_rf_poly, int_tol)
except Exception: # intersection failed for some reason
return None
polys = [Polygon2D(tuple(Point2D(pt.x, pt.y) for pt in new_poly))
for new_poly in int_result.regions]
if self.floor_geometry.has_holes and len(polys) > 1:
# sort the polygons by area and check if any are inside the others
polys.sort(key=lambda x: x.area, reverse=True)
poly_groups = [[polys[0]]]
for sub_poly in polys[1:]:
for i, pg in enumerate(poly_groups):
if pg[0].is_polygon_inside(sub_poly): # it's a hole
poly_groups[i].append(sub_poly)
break
else: # it's a separate Face3D
poly_groups.append([sub_poly])
# convert all vertices to 3D and append the roof Face3D
for pg in poly_groups:
pg_3d = []
for shp in pg:
pt3s = tuple(
rf_plane.project_point(Point3D.from_point2d(pt2), proj_dir)
for pt2 in shp.vertices)
pg_3d.append(pt3s)
roof_faces.append(Face3D(pg_3d[0], rf_plane, holes=pg_3d[1:]))
else: # no holes are possible in the result; project all polygons directly
for sub_poly in polys:
pt3s = tuple(
rf_plane.project_point(Point3D.from_point2d(pt2), proj_dir)
for pt2 in sub_poly.vertices)
roof_faces.append(Face3D(pt3s, rf_plane))
return roof_faces
def _patch_vertical_gaps(self, room_polyface, roof_face_i, tolerance):
"""Patch any vertical gaps in a room_polyface.
Args:
room_polyface: The non-solid Polyface3D to be patched with planar
vertical Faces.
roof_face_i: The indices of the polyface that correspond to the roof.
tolerance: The distance value for absolute tolerance.
Returns:
The patched Room Polyface3D followed by an updated list of face indices
that should become Roofs.
"""
# get the faces and naked edges
p_faces = list(room_polyface.faces)
edges = [ed for ed in room_polyface.naked_edges
if not ed.is_vertical(tolerance)]
vertical_faces = []
# loop through the naked edges and try to match them
matched_segs = set()
edge_indices = list(range(len(edges)))
for i, edge_1 in enumerate(edges):
edge_1_2d = LineSegment2D.from_end_points(
Point2D(edge_1.p1.x, edge_1.p1.y), Point2D(edge_1.p2.x, edge_1.p2.y))
other_edges = edges[:i] + edges[i + 1:]
other_is = edge_indices[:i] + edge_indices[i + 1:]
for oi, edge_2 in zip(other_is, other_edges):
e2p1 = Point2D(edge_2.p1.x, edge_2.p1.y)
e2p2 = Point2D(edge_2.p2.x, edge_2.p2.y)
if edge_1_2d.distance_to_point(e2p1) <= tolerance and \
edge_1_2d.distance_to_point(e2p2) <= tolerance:
# check to be sure that the segments have not been aired already
edge_pair_1, edge_pair_2 = (i, oi), (oi, i)
if edge_pair_1 in matched_segs:
continue
matched_segs.add(edge_pair_1)
matched_segs.add(edge_pair_2)
# build the points of the vertical face
norm = Vector3D(edge_1.v.x, edge_1.v.y, 0)
int_pl_1 = Plane(n=norm, o=edge_2.p1)
int_pl_2 = Plane(n=norm, o=edge_2.p2)
edge_1_1 = intersect_line3d_plane_infinite(edge_1, int_pl_1)
edge_1_2 = intersect_line3d_plane_infinite(edge_1, int_pl_2)
new_face3d = Face3D((edge_1_1, edge_1_2, edge_2.p1, edge_2.p2))
# find the grouping of points that is not self intersecting
if not new_face3d.is_self_intersecting and \
new_face3d.area > tolerance:
vertical_faces.append(new_face3d)
else:
new_face3d = Face3D((edge_1_1, edge_1_2, edge_2.p2, edge_2.p1))
if not new_face3d.is_self_intersecting and \
new_face3d.area > tolerance:
vertical_faces.append(new_face3d)
else:
f_poly = new_face3d.polygon2d
fs1, fs2 = f_poly.segments[0], f_poly.segments[2]
int_pt2d = fs1.intersect_line_ray(fs2)
if int_pt2d is not None:
int_pt = new_face3d.plane.xy_to_xyz(int_pt2d)
new_face3d1 = Face3D((edge_2.p1, int_pt, edge_1_1))
new_face3d2 = Face3D((edge_2.p2, int_pt, edge_1_2))
if new_face3d1.area > tolerance:
vertical_faces.append(new_face3d1)
if new_face3d2.area > tolerance:
vertical_faces.append(new_face3d2)
# remove duplicated vertices in the resulting vertical faces
clean_vert_faces = []
for f in vertical_faces:
try:
clean_vert_faces.append(f.remove_duplicate_vertices(tolerance))
except AssertionError:
pass # invalid sliver face
# rebuild the room polyface
st_v = -len(roof_face_i) - 1
roof_face_i = roof_face_i + list(range(st_v, st_v - len(clean_vert_faces), -1))
p_faces.extend(clean_vert_faces)
room_polyface = Polyface3D.from_faces(p_faces, tolerance)
return room_polyface, roof_face_i
def _separate_disconnected_faces(self, room_polyface, roof_face_i, tolerance):
"""Separate Face3Ds from a room_polyface, with are not connected to the solid.
Args:
room_polyface: The non-solid Polyface3D for which disconnected faces
will be separated out.
roof_face_i: The indices of the polyface that correspond to the roof.
tolerance: The distance value for absolute tolerance.
Returns:
The new Room Polyface3D, followed by an updated list of roof face indices,
followed by a list of shade geometries.
"""
# remove disconnected roof geometries from the Polyface (eg. dormers)
shade_geometry, room_ind, shade_ind = [], [], []
edge_i, edge_t = room_polyface.edge_indices, room_polyface.edge_types
for f_ind, face in enumerate(room_polyface.face_indices):
fe_types = []
for fi in face:
for i, vi in enumerate(fi):
try:
ind = edge_i.index((vi, fi[i - 1]))
et = edge_t[ind]
except ValueError: # make sure reversed edge isn't there
try:
ind = edge_i.index((fi[i - 1], vi))
et = edge_t[ind]
except ValueError: # an edge that was merged in overlapping
et = 1
fe_types.append(et)
if sum(fe_types) <= 1: # disconnected face found!
shade_ind.append(f_ind)
else:
room_ind.append(f_ind)
if len(shade_ind) != 0: # rebuild the Polyface3D
p_faces = [room_polyface.faces[f_ind] for f_ind in room_ind]
shade_geometry = [room_polyface.faces[f_ind] for f_ind in shade_ind]
room_polyface = Polyface3D.from_faces(p_faces, tolerance)
new_roof_len = len(roof_face_i) - len(shade_ind)
roof_face_i = list(range(-1, -new_roof_len - 1, -1))
return room_polyface, roof_face_i, shade_geometry
def _honeybee_plenums(self, hb_room, tolerance=0.01):
"""Get ceiling and/or floor plenums for the Room2D as a Honeybee Room.
This method will check if there is a gap between the Room2D's ceiling and
floor, and the top and bottom of it's corresponding Story, respectively.
If there is a gap along the z axis larger then the specified tolerance,
it will compute the necessary ceiling and/or floor plenum to fill the gap.
Args:
hb_room: A honeybee Room representing the dragonfly Room2D.
tolerance: The minimum distance in z values to check if the Room ceiling
and floor is adjacent to the upper and lower floor of the Story,
respectively. If not adjacent, the corresponding ceiling or floor
plenum is generated. Default: 0.01, suitable for objects in meters.
Returns:
A list of Honeybee Rooms with two items:
* ceil_plenum -- A honeybee-core Room representing the ceiling
plenum. If there isn't enough space between the Story
floor_to_floor_height and the Room2D floor_to_ceiling height,
this item will be None.
* floor_plenum -- A honeybee-core Room representing the floor plenum.
If there isn't enough space between the Story floor_height and
the Room2D floor_height, this item will be None.
"""
# check to be sure that the room2d has a parent story
hb_rooms = []
if not self.has_parent:
raise AttributeError(
'Cannot add plenums to the "{}" Room because the parent Story has '
'not been set. This is required to derive the plenum '
'height.'.format(self.identifier))
parent = self.parent
parent_ceiling = parent.floor_height + parent.floor_to_floor_height
ceil_plenum_height = parent_ceiling - self.ceiling_height
floor_plenum_height = self.floor_height - parent.floor_height
if ceil_plenum_height > tolerance:
ceil_plenum = self._honeybee_plenum(
ceil_plenum_height, plenum_type="ceiling")
# Set the plenum and the rooms to be adjacent to one another
hb_room[-1].set_adjacency(ceil_plenum[0], tolerance)
hb_rooms.append(ceil_plenum)
if floor_plenum_height > tolerance:
floor_plenum = self._honeybee_plenum(
floor_plenum_height, plenum_type="floor")
# Set the plenum and the rooms to be adjacent to one another
hb_room[0].set_adjacency(floor_plenum[-1], tolerance)
try:
hb_room[0].boundary_condition = bcs.adiabatic
except AttributeError:
pass
hb_rooms.append(floor_plenum)
return hb_rooms
def _honeybee_plenum(self, plenum_height, plenum_type='ceiling'):
"""Get a ceiling or floor plenum for the Room2D as a Honeybee Room.
The boundary condition for all plenum faces is adiabatic except for the
ceiling and floor surfaces between the room, and any outdoor walls.
Args:
hb_room: A honeybee Room representing the dragonfly Room2D.
plenum_height: The height of the plenum Room.
plenum_type: Text for the type of plenum to be constructed.
Choose from the following:
* ceiling
* floor
Returns:
A honeybee Room representing a plenum zone.
"""
plenum_id = self.identifier + '_{}_plenum'.format(plenum_type)
# create reference 2d geometry for plenums
ref_face3d = self.floor_geometry.duplicate()
if plenum_type == 'ceiling':
ref_face3d = ref_face3d.move(Vector3D(0, 0, self.floor_to_ceiling_height))
else:
ref_face3d = ref_face3d.move(Vector3D(0, 0, -plenum_height))
# create the honeybee Room
plenum_hb_room = Room.from_polyface3d(
plenum_id, Polyface3D.from_offset_face(ref_face3d, plenum_height))
# get the boundary condition that will be used for interior surfaces
try:
interior_bc = bcs.adiabatic
except AttributeError: # honeybee_energy is not loaded; no Adiabatic BC
interior_bc = bcs.outdoors
# assign wall BCs based on self
for i, bc in enumerate(self._boundary_conditions):
if not isinstance(bc, Surface):
plenum_hb_room[i + 1].boundary_condition = bc
else: # assign boundary conditions for the roof and floor
plenum_hb_room[i + 1].boundary_condition = interior_bc
if plenum_type == 'ceiling': # assign ceiling BCs
if self._is_top_exposed:
plenum_hb_room[-1].boundary_condition = bcs.outdoors
else:
plenum_hb_room[-1].boundary_condition = interior_bc
else: # assign floor BCss
if self._is_ground_contact:
plenum_hb_room[0].boundary_condition = bcs.ground
else:
plenum_hb_room[0].boundary_condition = interior_bc
return plenum_hb_room
def _match_and_transfer_wall_props(self, other_room, tolerance):
"""Transfer wall properties for matching segments between this room and another.
This includes boundary conditions, window/shading parameters, and the
air boundary property.
Args:
other_room: An other Room2D to which wall properties will be transferred.
"""
# build up new lists of parameters if the segments match
exist_abs = self.air_boundaries
new_bcs, new_win, new_shd, new_abs = [], [], [], []
for seg1 in other_room.floor_segments:
for k, seg2 in enumerate(self.floor_segments):
if seg1.p1.is_equivalent(seg2.p1, tolerance):
if seg1.p2.is_equivalent(seg2.p2, tolerance): # a match!
new_bcs.append(self._boundary_conditions[k])
new_win.append(self._window_parameters[k])
new_shd.append(self._shading_parameters[k])
new_abs.append(exist_abs[k])
break
else: # the segment could not be matched
new_bcs.append(bcs.outdoors)
new_win.append(None)
new_shd.append(None)
new_abs.append(False)
other_room.boundary_conditions = new_bcs
other_room.window_parameters = new_win
other_room.shading_parameters = new_shd
other_room.air_boundaries = new_abs
def _check_wall_assigned_object(self, value, obj_name=''):
"""Check an input that gets assigned to all of the walls of the Room."""
try:
value = list(value) if not isinstance(value, list) else value
except (ValueError, TypeError):
raise TypeError('Input {} must be a list or a tuple'.format(obj_name))
assert len(value) == len(self), 'Input {} length must be the ' \
'same as the number of floor_segments. {} != {}'.format(
obj_name, len(value), len(self))
return value
@staticmethod
def _flip_wall_assigned_objects(original_geo, bcs, win_pars, shd_pars):
"""Get arrays of wall-assigned parameters that are flipped/reversed.
This method accounts for the case that a floor geometry has holes in it.
"""
# go through the boundary and ensure detailed parameters are flipped
new_bcs = []
new_win_pars = []
new_shd_pars = []
for i, seg in enumerate(original_geo.boundary_segments):
new_bcs.append(bcs[i])
win_par = win_pars[i]
if isinstance(win_par, _AsymmetricBase):
new_win_pars.append(win_par.flip(seg.length))
else:
new_win_pars.append(win_par)
new_shd_pars.append(shd_pars[i])
# reverse the lists of wall-assigned objects on the floor boundary
new_bcs.reverse()
new_win_pars.reverse()
new_shd_pars.reverse()
# add any objects related to the holes
if original_geo.has_holes:
bound_len = len(original_geo.boundary)
new_bcs = new_bcs + bcs[bound_len:]
new_win_pars = new_win_pars + win_pars[bound_len:]
new_shd_pars = new_shd_pars + shd_pars[bound_len:]
# return the flipped lists
return new_bcs, new_win_pars, new_shd_pars
def _split_walls_along_height(self, hb_room, tolerance, plenums=False):
"""Split adjacent walls to ensure matching surface areas in to_honeybee workflow.
Args:
hb_room: A non-split Honeybee Room representation of this Room2D.
tolerance: The minimum distance in z values of floor_height and
floor_to_ceiling_height at which adjacent Faces will be split.
plenums: A boolean to note whether the resulting model has auto-generated
plenums, which will determine the default boundary condition of
any split wall segments. (Default: False).
"""
new_faces = [hb_room[0]]
for i, bc in enumerate(self._boundary_conditions):
face = hb_room[i + 1]
if not isinstance(bc, Surface):
new_faces.append(face)
else:
try:
adj_rm = self._parent.room_by_identifier(
bc.boundary_condition_objects[-1])
except ValueError: # missing adjacency in Story; just pass invalid BC
new_faces.append(face)
continue
flr_diff = adj_rm.floor_height - self.floor_height
ciel_diff = self.ceiling_height - adj_rm.ceiling_height
if flr_diff <= tolerance and ciel_diff <= tolerance:
# No need to split the surface along its height
new_faces.append(face)
elif flr_diff > tolerance and ciel_diff > tolerance:
# split the face into to 3 smaller faces along its height
lseg = LineSegment3D.from_end_points(face.geometry[0],
face.geometry[1])
mid_dist = self.floor_to_ceiling_height - ciel_diff - flr_diff
vec1 = Vector3D(0, 0, flr_diff)
vec2 = Vector3D(0, 0, self.floor_to_ceiling_height - ciel_diff)
below = Face3D.from_extrusion(lseg, vec1)
mid = Face3D.from_extrusion(
lseg.move(vec1), Vector3D(0, 0, mid_dist))
above = Face3D.from_extrusion(
lseg.move(vec2), Vector3D(0, 0, ciel_diff))
mid_face = face.duplicate()
mid_face._geometry = mid
self._reassign_split_windows(mid_face, i, tolerance)
below_face = Face('{}_Below'.format(face.identifier), below)
above_face = Face('{}_Above'.format(face.identifier), above)
try:
below_face.boundary_condition = bcs.ground \
if self.is_ground_contact and not plenums else bcs.adiabatic
except AttributeError:
pass # honeybee_energy is not loaded; no adiabatic BC
try:
below_face.boundary_condition = bcs.outdoors \
if adj_rm.is_top_exposed and not plenums else bcs.adiabatic
except AttributeError:
pass # honeybee_energy is not loaded; no adiabatic BC
new_faces.extend([below_face, mid_face, above_face])
elif flr_diff > tolerance:
# split the face into to 2 smaller faces along its height
lseg = LineSegment3D.from_end_points(face.geometry[0],
face.geometry[1])
mid_dist = self.floor_to_ceiling_height - flr_diff
vec1 = Vector3D(0, 0, flr_diff)
below = Face3D.from_extrusion(lseg, vec1)
mid = Face3D.from_extrusion(
lseg.move(vec1), Vector3D(0, 0, mid_dist))
mid_face = face.duplicate()
mid_face._geometry = mid
self._reassign_split_windows(mid_face, i, tolerance)
below_face = Face('{}_Below'.format(face.identifier), below)
try:
below_face.boundary_condition = bcs.ground \
if self.is_ground_contact and not plenums else bcs.adiabatic
except AttributeError:
pass # honeybee_energy is not loaded; no adiabatic BC
new_faces.extend([below_face, mid_face])
elif ciel_diff > tolerance:
# split the face into to 2 smaller faces along its height
lseg = LineSegment3D.from_end_points(face.geometry[0],
face.geometry[1])
mid_dist = self.floor_to_ceiling_height - ciel_diff
vec1 = Vector3D(0, 0, mid_dist)
mid = Face3D.from_extrusion(lseg, vec1)
above = Face3D.from_extrusion(
lseg.move(vec1), Vector3D(0, 0, ciel_diff))
mid_face = face.duplicate()
mid_face._geometry = mid
self._reassign_split_windows(mid_face, i, tolerance)
above_face = Face('{}_Above'.format(face.identifier), above)
try:
above_face.boundary_condition = bcs.outdoors \
if adj_rm.is_top_exposed and not plenums else bcs.adiabatic
except AttributeError:
pass # honeybee_energy is not loaded; no adiabatic BC
new_faces.extend([mid_face, above_face])
new_faces.append(hb_room[-1])
return new_faces
def _reassign_split_windows(self, face, i, tolerance):
"""Re-assign WindowParameters to any base surface that has been split.
Args:
face: Honeybee Face to which windows will be re-assigned.
i: The index of the window_parameters that correspond to the face
tolerance: The tolerance, which will be used to re-assign windows.
"""
glz_par = self._window_parameters[i]
if glz_par is not None:
face.remove_sub_faces()
glz_par.add_window_to_face(face, tolerance)
@staticmethod
def _segment_wall_face(room, segment, tolerance):
"""Get a Wall Face that corresponds with a certain wall segment.
Args:
room: A Honeybee Room from which a wall Face will be returned.
segment: A LineSegment3D along one of the walls of the room.
tolerance: The maximum difference between values at which point vertices
are considered to be the same.
"""
for face in room.faces:
if isinstance(face.type, (Wall, AirBoundary)):
fg = face.geometry
try:
verts = fg._remove_colinear(
fg._boundary, fg.boundary_polygon2d, tolerance)
except AssertionError:
return None
for v1 in verts:
if segment.p1.is_equivalent(v1, tolerance):
p2 = segment.p2
for v2 in verts:
if p2.is_equivalent(v2, tolerance):
return face
@staticmethod
def _remove_colinear_props(
pts_3d, pts_2d, segs_2d, bound_cds, win_pars, ftc_height, tolerance):
"""Remove colinear vertices across a boundary while merging window properties."""
new_vertices, new_bcs, new_w_par = [], [], []
skip = 0 # track the number of vertices being skipped/removed
m_segs, m_bcs, m_w_par = [], [], []
# loop through vertices and remove all cases of colinear verts
for i, _v in enumerate(pts_2d):
m_segs.append(segs_2d[i - 2])
m_bcs.append(bound_cds[i - 2])
m_w_par.append(win_pars[i - 2])
_a = pts_2d[i - 2 - skip].determinant(pts_2d[i - 1]) + \
pts_2d[i - 1].determinant(_v) + _v.determinant(pts_2d[i - 2 - skip])
if abs(_a) >= tolerance: # vertex is not colinear; add vertex and merge
new_vertices.append(pts_3d[i - 1])
if all(not isinstance(bc, Ground) for bc in m_bcs):
new_bcs.append(bcs.outdoors)
if all(wp is None for wp in m_w_par):
new_w_par.append(None)
elif len(m_w_par) == 1:
new_w_par.append(m_w_par[0])
else:
new_wp = DetailedWindows.merge(m_w_par, m_segs, ftc_height)
new_w_par.append(new_wp)
else:
new_bcs.append(bcs.ground)
new_w_par.append(None)
skip = 0
m_bcs, m_w_par, m_segs = [], [], []
else: # vertex is colinear; continue
skip += 1
# catch case of last two vertices being equal but distinct from first point
if skip != 0 and pts_3d[-2].is_equivalent(pts_3d[-1], tolerance):
_a = pts_2d[-3].determinant(pts_2d[-1]) + \
pts_2d[-1].determinant(pts_2d[0]) + pts_2d[0].determinant(pts_2d[-3])
if abs(_a) >= tolerance:
new_vertices.append(pts_3d[-1])
if not isinstance(bound_cds[-2], Ground):
new_bcs.append(bcs.outdoors)
new_w_par.append(win_pars[-2])
else:
new_bcs.append(bcs.ground)
new_w_par.append(None)
elif skip != 0:
w_par_for_merge = m_w_par + [new_w_par[0]]
if not all(wp is None for wp in w_par_for_merge):
segs_for_merge = m_segs + [segs_2d[-1]]
new_w_par[0] = DetailedWindows.merge(
w_par_for_merge, segs_for_merge, ftc_height)
# move the first properties to the end to match with the vertices
new_bcs.append(new_bcs.pop(0))
new_w_par.append(new_w_par.pop(0))
return new_vertices, new_bcs, new_w_par
@staticmethod
def _adjacency_grouping(rooms, adj_finding_function):
"""Group Room2Ds together according to an adjacency finding function.
Args:
rooms: A list of Room2Ds to be grouped by their adjacency.
adj_finding_function: A function that denotes which rooms are adjacent
to another.
Returns:
A list of list with each sub-list containing rooms that share adjacencies.
"""
# create a room lookup table and duplicate the list of rooms
room_lookup = {rm.identifier: rm for rm in rooms}
all_rooms = list(rooms)
adj_network = []
# loop through the rooms and find air boundary adjacencies
for room in all_rooms:
adj_ids = adj_finding_function(room)
if len(adj_ids) == 0: # a room that is its own solar enclosure
adj_network.append([room])
else: # there are other adjacent rooms to find
local_network = [room]
local_ids, first_id = set(adj_ids), room.identifier
while len(adj_ids) != 0:
# add the current rooms to the local network
adj_objs = [room_lookup[rm_id] for rm_id in adj_ids]
local_network.extend(adj_objs)
adj_ids = [] # reset the list of new adjacencies
# find any rooms that are adjacent to the adjacent rooms
for obj in adj_objs:
all_new_ids = adj_finding_function(obj)
new_ids = [rid for rid in all_new_ids
if rid not in local_ids and rid != first_id]
for rm_id in new_ids:
local_ids.add(rm_id)
adj_ids.extend(new_ids)
# after the local network is understood, clean up duplicated rooms
adj_network.append(local_network)
i_to_remove = [i for i, room_obj in enumerate(all_rooms)
if room_obj.identifier in local_ids]
for i in reversed(i_to_remove):
all_rooms.pop(i)
return adj_network
@staticmethod
def _find_adjacent_rooms(room):
"""Find the identifiers of all rooms with adjacency to a room."""
adj_rooms = []
for bc in room._boundary_conditions:
if isinstance(bc, Surface):
adj_rooms.append(bc.boundary_condition_objects[-1])
return adj_rooms
@staticmethod
def _find_adjacent_air_boundary_rooms(room):
"""Find the identifiers of all rooms with air boundary adjacency to a room."""
adj_rooms = []
for bc, ab in zip(room._boundary_conditions, room.air_boundaries):
if ab and isinstance(bc, Surface):
adj_rooms.append(bc.boundary_condition_objects[-1])
return adj_rooms
@staticmethod
def _segments_along_polygon(
polygon, rel_segs, rel_bcs, rel_win, rel_shd, rel_abs,
new_bcs, new_win, new_shd, new_abs, new_flr_height, tol):
"""Find the segments along a polygon and add their properties to new lists."""
new_segs = []
for seg in polygon.segments:
seg_segs, seg_bcs, seg_win, seg_shd, seg_abs = [], [], [], [], []
# collect the room segments and properties along the boundary
for i, rs in enumerate(rel_segs):
if seg.distance_to_point(rs.p1) <= tol and \
seg.distance_to_point(rs.p2) <= tol: # colinear
seg_segs.append(rs)
seg_bcs.append(rel_bcs[i])
seg_win.append(rel_win[i])
seg_shd.append(rel_shd[i])
seg_abs.append(rel_abs[i])
if len(seg_segs) == 0:
Room2D._add_dummy_segment(
seg.p1, seg.p2, new_segs, new_bcs, new_win, new_shd, new_abs)
continue
# sort the Room2D segments along the polygon segment
seg_dists = [seg.p1.distance_to_point(s.p1) for s in seg_segs]
sort_ind = [i for _, i in sorted(zip(seg_dists, range(len(seg_dists))))]
seg_segs = [seg_segs[i] for i in sort_ind]
seg_bcs = [seg_bcs[i] for i in sort_ind]
seg_win = [seg_win[i] for i in sort_ind]
seg_shd = [seg_shd[i] for i in sort_ind]
seg_abs = [seg_abs[i] for i in sort_ind]
# identify any gaps and add dummy segments
p1_dists = sorted(seg_dists)
p2_dists = [seg.p1.distance_to_point(s.p2) for s in seg_segs]
last_d, last_seg = 0, None
for i, (p1d, p2d) in enumerate(zip(p1_dists, p2_dists)):
if p1d < last_d - tol: # overlapping segment; ignore it
continue
elif p1d > last_d + tol: # add a dummy segment for the gap
st_pt = last_seg.p2 if last_seg is not None else seg.p1
Room2D._add_dummy_segment(
st_pt, seg_segs[i].p1, new_segs, new_bcs,
new_win, new_shd, new_abs)
# add the segment
new_segs.append(seg_segs[i])
new_bcs.append(seg_bcs[i])
new_win.append(seg_win[i])
new_shd.append(seg_shd[i])
new_abs.append(seg_abs[i])
last_d = p2d
last_seg = seg_segs[i]
return [Point3D(s.p1.x, s.p1.y, new_flr_height) for s in new_segs]
@staticmethod
def _add_dummy_segment(p1, p2, new_segs, new_bcs, new_win, new_shd, new_abs):
"""Add a dummy segment to lists of properties that are being built."""
new_segs.append(LineSegment2D.from_end_points(p1, p2))
new_bcs.append(bcs.outdoors)
new_win.append(None)
new_shd.append(None)
new_abs.append(False)
@staticmethod
def _intersect_line2d_infinite(line_ray_a, line_ray_b):
"""Get the intersection between a Ray2Ds extended infinitely.
Args:
line_ray_a: A Ray2D object.
line_ray_b: Another Ray2D object.
Returns:
Point2D of intersection if it exists. None if lines are parallel.
"""
d = line_ray_b.v.y * line_ray_a.v.x - line_ray_b.v.x * line_ray_a.v.y
if d == 0:
return None
dy = line_ray_a.p.y - line_ray_b.p.y
dx = line_ray_a.p.x - line_ray_b.p.x
ua = (line_ray_b.v.x * dy - line_ray_b.v.y * dx) / d
return Point2D(line_ray_a.p.x + ua * line_ray_a.v.x,
line_ray_a.p.y + ua * line_ray_a.v.y)
def __copy__(self):
new_r = Room2D(self.identifier, self._floor_geometry,
self.floor_to_ceiling_height,
self._boundary_conditions[:]) # copy boundary condition list
new_r._display_name = self._display_name
new_r._user_data = None if self.user_data is None else self.user_data.copy()
new_r._parent = self._parent
new_wp = []
for wp in self._window_parameters:
nwp = wp.duplicate() if wp is not None else None
new_wp.append(nwp)
new_r._window_parameters = new_wp
new_r._shading_parameters = self._shading_parameters[:] # copy shading list
new_r._air_boundaries = self._air_boundaries[:] \
if self._air_boundaries is not None else None
new_r._is_ground_contact = self._is_ground_contact
new_r._is_top_exposed = self._is_top_exposed
new_r._skylight_parameters = self._skylight_parameters.duplicate() \
if self._skylight_parameters is not None else None
new_r._abridged_properties = self._abridged_properties
new_r._properties._duplicate_extension_attr(self._properties)
return new_r
def __len__(self):
return self._segment_count
def __getitem__(self, key):
return self.floor_segments[key]
def __iter__(self):
return iter(self.floor_segments)
def __repr__(self):
return 'Room2D: %s' % self.display_name