Source code for dragonfly.windowparameter

# coding: utf-8
"""Window Parameters with instructions for generating windows."""
from __future__ import division
import math
import sys
if (sys.version_info < (3, 0)):  # python 2
    from itertools import izip as zip  # python 2

from ladybug_geometry.geometry2d import Point2D, Vector2D, Polygon2D
from ladybug_geometry.geometry3d.pointvector import Vector3D, Point3D
from ladybug_geometry.geometry3d import LineSegment3D, Plane, Face3D
from ladybug_geometry.bounding import bounding_rectangle

from honeybee.typing import float_in_range, float_positive
from honeybee.boundarycondition import Surface
from honeybee.aperture import Aperture
from honeybee.door import Door


class _WindowParameterBase(object):
    """Base object for all window parameters.

    This object records all of the methods that must be overwritten on a window
    parameter object for it to be successfully be applied in dragonfly workflows.

    Properties:
        * user_data
    """
    __slots__ = ('_user_data',)

    def __init__(self):
        self._user_data = None

    @property
    def user_data(self):
        """Get or set an optional dictionary for additional meta data for this object.

        This will be None until it has been set. All keys and values of this
        dictionary should be of a standard Python type to ensure correct
        serialization of the object to/from JSON (eg. str, float, int, list dict)
        """
        return self._user_data

    @user_data.setter
    def user_data(self, value):
        if value is not None:
            assert isinstance(value, dict), 'Expected dictionary for honeybee ' \
                'object user_data. Got {}.'.format(type(value))
        self._user_data = value

    def area_from_segment(self, segment, floor_to_ceiling_height):
        """Get the window area generated by these parameters from a LineSegment3D."""
        return 0

    def add_window_to_face(self, face, tolerance=0.01):
        """Add Apertures to a Honeybee Face using these Window Parameters."""
        pass

    def scale(self, factor):
        """Get a scaled version of these WindowParameters.

        This method is called within the scale methods of the Room2D.

        Args:
            factor: A number representing how much the object should be scaled.
        """
        return self

    def trim(self, original_segment, sub_segment, tolerance=0.01):
        """Trim window parameters for a sub segment given the original segment.

        Args:
            original_segment: The original LineSegment3D to which the window
                parameters are assigned.
            sub_segment: A LineSegment3D that is a sub-segment of the original_segment,
                which will be used to trim the window parameters to fit this segment.
                Note that this sub_segment should have the same orientation as
                the original segment.
            tolerance: The minimum distance between a vertex and the edge of the
                wall segment that is considered not touching. (Default: 0.01, suitable
                for objects in meters).
        """
        return self  # windows are assumed to repeat

    @staticmethod
    def merge_to_rectangular(window_parameters, segments, floor_to_ceiling_height):
        """Merge any window parameters together into rectangular windows.

        Args:
            window_parameters: A list of WindowParameters to be merged.
            segments: The segments to which the window parameters are assigned.
                These should be in order as they appear on the parent Room2D.
            floor_to_ceiling_height: The floor-to-ceiling height of the Room2D
                to which the segment belongs.
        """
        base_x = 0
        origins, widths, heights, doors = [], [], [], []
        for wp, s in zip(window_parameters, segments):
            if wp is not None:
                rwp = wp.to_rectangular_windows(s, floor_to_ceiling_height)
                zip_obj = zip(rwp.origins, rwp.widths, rwp.heights, rwp.are_doors)
                for o, w, h, d in zip_obj:
                    origins.append(Point2D(o.x + base_x, o.y))
                    widths.append(w)
                    heights.append(h)
                    doors.append(d)
            base_x += s.length
        return RectangularWindows(origins, widths, heights, doors)

    @classmethod
    def from_dict(cls, data):
        """Create WindowParameterBase from a dictionary.

        .. code-block:: python

            {
            "type": "WindowParameterBase"
            }
        """
        assert data['type'] == 'WindowParameterBase', \
            'Expected WindowParameterBase dictionary. Got {}.'.format(data['type'])
        new_w_par = cls()
        if 'user_data' in data and data['user_data'] is not None:
            new_w_par.user_data = data['user_data']
        return new_w_par

    def to_dict(self):
        """Get WindowParameterBase as a dictionary."""
        return {'type': 'WindowParameterBase'}

    def duplicate(self):
        """Get a copy of this object."""
        return self.__copy__()

    def ToString(self):
        return self.__repr__()

    def _add_user_data(self, new_win_par):
        """Add copies of this object's user_data to new WindowParameters."""
        if self.user_data is not None:
            for w_par in new_win_par:
                w_par.user_data = self.user_data.copy()

    def _apply_user_data_to_honeybee(self, sub_faces, clean_data=None):
        """Apply the WindowParameter user_data to generated honeybee objects.

        Args:
            sub_faces: An array of Honeybee Apertures or Doors to which user_data
                will be applied from this WindowParameter.
            clean_data: An optional dictionary of user_data to be used in place
                of the currently assigned user_data. This is useful when not all
                WindowParameters are able to be applied to the generated Honeybee
                objects. When None, the self.user_data will be used. (Default: None).
        """
        app_data = self.user_data if clean_data is None else clean_data
        if app_data is not None:
            for i, sub_f in enumerate(sub_faces):
                u_dict = {}
                for key, val in app_data.items():
                    if isinstance(val, (list, tuple)) and len(val) != 0:
                        try:
                            u_dict[key] = val[i]
                        except IndexError:  # use longest list logic
                            u_dict[key] = val[-1]
                    else:
                        u_dict[key] = val
                sub_f.user_data = u_dict

    def __copy__(self):
        return _WindowParameterBase()

    def __repr__(self):
        return 'WindowParameterBase'


[docs] class SingleWindow(_WindowParameterBase): """Instructions for a single window in the face center defined by a width x height. Note that, if these parameters are applied to a base face that is too short or too narrow for the input width and/or height, the generated window will automatically be shortened when it is applied to the face. In this way, setting the width to be `float('inf')` will create parameters that always generate a ribbon window of the input height. Args: width: A number for the window width. height: A number for the window height. sill_height: A number for the window sill height. Default: 1. Properties: * width * height * sill_height * user_data """ __slots__ = ('_width', '_height', '_sill_height') def __init__(self, width, height, sill_height=1): """Initialize SingleWindow.""" _WindowParameterBase.__init__(self) # add the user_data self._width = float_positive(width, 'window width') self._height = float_positive(height, 'window height') self._sill_height = float_positive(sill_height, 'window sill height') @property def width(self): """Get a number for the window width.""" return self._width @property def height(self): """Get a number for the window height.""" return self._height @property def sill_height(self): """Get a number for the sill height.""" return self._sill_height
[docs] def area_from_segment(self, segment, floor_to_ceiling_height): """Get the window area generated by these parameters from a LineSegment3D. Args: segment: A LineSegment3D to which these parameters are applied. floor_to_ceiling_height: The floor-to-ceiling height of the Room2D to which the segment belongs. """ max_width = segment.length max_height = floor_to_ceiling_height - self.sill_height final_width = max_width if self.width > max_width else self.width final_height = max_height if self.height > max_height else self.height if final_height < 0: return 0 else: return final_width * final_height
[docs] def to_rectangular_windows(self, segment, floor_to_ceiling_height): """Get a version of these WindowParameters as RectangularWindows. Args: segment: A LineSegment3D to which these parameters are applied. floor_to_ceiling_height: The floor-to-ceiling height of the Room2D to which the segments belong. """ max_width = segment.length max_height = (floor_to_ceiling_height) - self.sill_height final_width = max_width if self.width > max_width else self.width final_height = max_height if self.height > max_height else self.height if final_height < 0: return None else: origin = Point2D((segment.length - final_width) / 2, self.sill_height) new_w = RectangularWindows([origin], [final_width], [final_height]) new_w._user_data = None if self.user_data is None else self.user_data.copy() return new_w
[docs] def add_window_to_face(self, face, tolerance=0.01): """Add Apertures to a Honeybee Face using these Window Parameters. Args: face: A honeybee-core Face object. tolerance: Optional tolerance value. Default: 0.01, suitable for objects in meters. """ if self._width == 0 or self._height == 0: return None width_seg = LineSegment3D.from_end_points(face.geometry[0], face.geometry[1]) height_seg = LineSegment3D.from_end_points(face.geometry[1], face.geometry[2]) max_width = width_seg.length - tolerance max_height = (height_seg.length - tolerance) - self.sill_height final_width = max_width if self.width > max_width else self.width final_height = max_height if self.height > max_height else self.height if final_height > 0: face.aperture_by_width_height(final_width, final_height, self.sill_height) # if the Aperture is interior, set adjacent boundary condition if isinstance(face._boundary_condition, Surface): ids = face._boundary_condition.boundary_condition_objects adj_ap_id = '{}_Glz1'.format(ids[0]) final_ids = (adj_ap_id,) + ids face.apertures[0].boundary_condition = Surface(final_ids, True) self._apply_user_data_to_honeybee(face.apertures)
[docs] def scale(self, factor): """Get a scaled version of these WindowParameters. This method is called within the scale methods of the Room2D. Args: factor: A number representing how much the object should be scaled. """ new_w = SingleWindow( self.width * factor, self.height * factor, self.sill_height * factor) new_w._user_data = None if self.user_data is None else self.user_data.copy() return new_w
[docs] def split(self, segments, tolerance=0.01): """Split SingleWindow parameters across a list of ordered segments. Args: segments: The segments to which the window parameters are being split across. These should be in order as they appear on the parent Room2D. tolerance: The minimum distance between a vertex and the edge of the wall segment that is considered not touching. (Default: 0.01, suitable for objects in meters). """ lengths = [s.length for s in segments] total_len = sum(lengths) new_w_par = [] for length in lengths: new_w = (length / total_len) * self.width new_w_par.append(SingleWindow(new_w, self.height, self.sill_height)) self._add_user_data(new_w_par) return new_w_par
[docs] @staticmethod def merge(window_parameters, segments, floor_to_ceiling_height): """Merge SingleWindow parameters together using their assigned segments. Args: window_parameters: A list of WindowParameters to be merged. segments: The segments to which the window parameters are assigned. These should be in order as they appear on the parent Room2D. floor_to_ceiling_height: The floor-to-ceiling height of the Room2D to which the segments belong. """ width = 0 weights, heights, sill_heights = [], [], [], [] for wp, s in zip(window_parameters, segments): if wp is not None: if isinstance(wp, SingleWindow): weights.append(s.length) width += wp.width heights.append(wp.height) sill_heights.append(wp.length) else: # not all windows are of the same type; convert all to rectangular return _WindowParameterBase.merge_to_rectangular( window_parameters, segments, floor_to_ceiling_height) tw = sum(weights) weights = [w / tw for w in weights] height = sum([h * w for h, w in zip(heights, weights)]) sill_height = sum([h * w for h, w in zip(sill_heights, weights)]) new_w = SingleWindow(width, height, sill_height) new_w._user_data = None if window_parameters[0].user_data is None else \ window_parameters[0].user_data.copy() return new_w
[docs] @classmethod def from_dict(cls, data): """Create SingleWindow from a dictionary. .. code-block:: python { "type": "SingleWindow", "width": 100, "height": 1.5, "sill_height": 0.8 } """ assert data['type'] == 'SingleWindow', \ 'Expected SingleWindow dictionary. Got {}.'.format(data['type']) sill = data['sill_height'] if 'sill_height' in data else 1 new_w_par = cls(data['width'], data['height'], sill) if 'user_data' in data and data['user_data'] is not None: new_w_par.user_data = data['user_data'] return new_w_par
[docs] def to_dict(self): """Get SingleWindow as a dictionary.""" base = { 'type': 'SingleWindow', 'width': self.width, 'height': self.height, 'sill_height': self.sill_height } if self.user_data is not None: base['user_data'] = self.user_data return base
def __copy__(self): new_w = SingleWindow(self.width, self.height, self.sill_height) new_w._user_data = None if self.user_data is None else self.user_data.copy() return new_w def __key(self): """A tuple based on the object properties, useful for hashing.""" return (self.width, self.height, self.sill_height) def __hash__(self): return hash(self.__key()) def __eq__(self, other): return isinstance(other, SingleWindow) and self.__key() == other.__key() def __ne__(self, other): return not self.__eq__(other) def __repr__(self): return 'SingleWindow: [width: {}] [height: {}] [sill_height: {}]'.format( self.width, self.height, self.sill_height)
[docs] class SimpleWindowArea(_WindowParameterBase): """Instructions for a single window defined by an absolute area. Properties: * window_area * rect_split * user_data Args: window_area: A number for the window area in current model units. If this area is larger than the area of the Wall that it is applied to, the window will fill the parent Wall at a 99% ratio. rect_split: Boolean to note whether rectangular portions of base Face should be extracted before scaling them to create apertures. For pentagonal gabled geometries, the resulting apertures will consist of one rectangle and one triangle, which can often look more realistic and is a better input for engines like EnergyPlus that cannot model windows with more than 4 vertices. However, if a single pentagonal window is desired for such a gabled shape, this input can be set to False to produce such a result. """ __slots__ = ('_window_area', '_rect_split') def __init__(self, window_area, rect_split=True): """Initialize SimpleWindowArea.""" _WindowParameterBase.__init__(self) # add the user_data self._window_area = float_positive(window_area, 'window area') self._rect_split = bool(rect_split) @property def window_area(self): """Get a number for the window area in current model units.""" return self._window_area @property def rect_split(self): """Get a boolean for whether rectangular portions are extracted.""" return self._rect_split
[docs] def area_from_segment(self, segment, floor_to_ceiling_height): """Get the window area generated by these parameters from a LineSegment3D. Args: segment: A LineSegment3D to which these parameters are applied. floor_to_ceiling_height: The floor-to-ceiling height of the Room2D to which the segment belongs. """ wall_area = segment.length * floor_to_ceiling_height * 0.99 return self.window_area if self.window_area < wall_area else wall_area
[docs] def to_rectangular_windows(self, segment, floor_to_ceiling_height): """Get a version of these WindowParameters as RectangularWindows. Args: segment: A LineSegment3D to which these parameters are applied. floor_to_ceiling_height: The floor-to-ceiling height of the Room2D to which the segment belongs. """ if self._window_area == 0: return None wall_area = segment.length * floor_to_ceiling_height * 0.99 window_ratio = self.window_area / wall_area window_ratio = 0.99 if window_ratio > 0.99 else window_ratio scale_factor = window_ratio ** 0.5 final_height = floor_to_ceiling_height * scale_factor final_width = segment.length * scale_factor origin = Point2D((segment.length - final_width) / 2, (floor_to_ceiling_height - final_height) / 2) new_w = RectangularWindows([origin], [final_width], [final_height]) new_w._user_data = None if self.user_data is None else self.user_data.copy() return new_w
[docs] def add_window_to_face(self, face, tolerance=0.01): """Add Apertures to a Honeybee Face using these Window Parameters. Args: face: A honeybee-core Face object. tolerance: Optional tolerance value. Default: 0.01, suitable for objects in meters. """ if self._window_area == 0: return None window_ratio = self.window_area / face.area window_ratio = 0.99 if window_ratio > 0.99 else window_ratio face.apertures_by_ratio(window_ratio, tolerance, self.rect_split) # if the Aperture is interior, set adjacent boundary condition if isinstance(face._boundary_condition, Surface): num_aps = face.apertures for i, ap in enumerate(face.apertures): ids = face._boundary_condition.boundary_condition_objects adj_ap_id = '{}_Glz{}'.format(ids[0], num_aps - i - 1) final_ids = (adj_ap_id,) + ids ap.boundary_condition = Surface(final_ids, True) self._apply_user_data_to_honeybee(face.apertures)
[docs] def scale(self, factor): """Get a scaled version of these WindowParameters. This method is called within the scale methods of the Room2D. Args: factor: A number representing how much the object should be scaled. """ new_w = SimpleWindowArea(self.window_area * factor, self.rect_split) new_w._user_data = None if self.user_data is None else self.user_data.copy() return new_w
[docs] def split(self, segments, tolerance=0.01): """Split SimpleWindowArea parameters across a list of ordered segments. Args: segments: The segments to which the window parameters are being split across. These should be in order as they appear on the parent Room2D. tolerance: The minimum distance between a vertex and the edge of the wall segment that is considered not touching. (Default: 0.01, suitable for objects in meters). """ # if one of the segments is much larger than the others, add all windows to that lengths = [s.length for s in segments] new_wps = self._all_to_primary_segment(lengths) if new_wps is not None: return new_wps # otherwise, just distribute the windows evenly total_len = sum(lengths) n_par = [SimpleWindowArea(self.window_area * (sl / total_len), self.rect_split) for sl in lengths] self._add_user_data(n_par) return n_par
[docs] def trim(self, original_segment, sub_segment, tolerance=0.01): """Trim window parameters for a sub segment given the original segment. Args: original_segment: The original LineSegment3D to which the window parameters are assigned. sub_segment: A LineSegment3D that is a sub-segment of the original_segment, which will be used to trim the window parameters to fit this segment. Note that this sub_segment should have the same orientation as the original segment. tolerance: The minimum distance between a vertex and the edge of the wall segment that is considered not touching. (Default: 0.01, suitable for objects in meters). """ # evenly distribute the windows len_ratio = sub_segment.length / original_segment.length n_par = SimpleWindowArea(self.window_area * len_ratio, self.rect_split) n_par._user_data = self._user_data return n_par
[docs] @staticmethod def merge(window_parameters, segments, floor_to_ceiling_height): """Merge SimpleWindowArea parameters together using their assigned segments. Args: window_parameters: A list of WindowParameters to be merged. segments: The segments to which the window parameters are assigned. These should be in order as they appear on the parent Room2D. floor_to_ceiling_height: The floor-to-ceiling height of the Room2D to which the segments belong. """ win_area, rect_split = 0, True for wp in window_parameters: if wp is not None: if isinstance(wp, SimpleWindowArea): win_area += wp.window_area rect_split = wp.rect_split else: # not all windows are of the same type; convert all to rectangular return _WindowParameterBase.merge_to_rectangular( window_parameters, segments, floor_to_ceiling_height) new_w = SimpleWindowArea(win_area, rect_split) new_w._user_data = None if window_parameters[0].user_data is None else \ window_parameters[0].user_data.copy() return new_w
[docs] @classmethod def from_dict(cls, data): """Create SimpleWindowArea from a dictionary. .. code-block:: python { "type": "SimpleWindowArea", "window_area": 5.5, "rect_split": False } """ assert data['type'] == 'SimpleWindowArea', \ 'Expected SimpleWindowArea dictionary. Got {}.'.format(data['type']) rect_split = True if 'rect_split' not in data else data['rect_split'] new_w_par = cls(data['window_area'], rect_split) if 'user_data' in data and data['user_data'] is not None: new_w_par.user_data = data['user_data'] return new_w_par
[docs] def to_dict(self): """Get SimpleWindowArea as a dictionary.""" base = { 'type': 'SimpleWindowArea', 'window_area': self.window_area } if self.user_data is not None: base['user_data'] = self.user_data if not self.rect_split: base['rect_split'] = False return base
def _all_to_primary_segment(self, lengths, prim_ratio=0.95): """Determine if one segment is primary and should get all the windows.""" total_len = sum(lengths) prim_len = prim_ratio + 0.01 all_to_one_i = None for i, sl in enumerate(lengths): if sl / total_len > prim_len: all_to_one_i = i break if all_to_one_i is not None: new_wps = [None] * len(lengths) new_par = self.duplicate() new_wps[all_to_one_i] = new_par return new_wps def __copy__(self): new_w = SimpleWindowArea(self.window_area, self.rect_split) new_w._user_data = None if self.user_data is None else self.user_data.copy() return new_w def __key(self): """A tuple based on the object properties, useful for hashing.""" return (self._window_area, self._rect_split) def __hash__(self): return hash(self.__key()) def __eq__(self, other): return isinstance(other, SimpleWindowArea) and self.__key() == other.__key() def __ne__(self, other): return not self.__eq__(other) def __repr__(self): return 'SimpleWindowArea: [area: {}]'.format(self.window_area)
[docs] class SimpleWindowRatio(_WindowParameterBase): """Instructions for a single window defined by an area ratio with the base wall. Properties: * window_ratio * rect_split * user_data Args: window_ratio: A number between 0 and 1 for the ratio between the window area and the parent wall surface area. rect_split: Boolean to note whether rectangular portions of base Face should be extracted before scaling them to create apertures. For pentagonal gabled geometries, the resulting apertures will consist of one rectangle and one triangle, which can often look more realistic and is a better input for engines like EnergyPlus that cannot model windows with more than 4 vertices. However, if a single pentagonal window is desired for such a gabled shape, this input can be set to False to produce such a result. """ __slots__ = ('_window_ratio', '_rect_split') def __init__(self, window_ratio, rect_split=True): """Initialize SimpleWindowRatio.""" _WindowParameterBase.__init__(self) # add the user_data self._window_ratio = float_in_range(window_ratio, 0, 1, 'window ratio') self._rect_split = bool(rect_split) @property def window_ratio(self): """Get a number between 0 and 1 for the window ratio.""" return self._window_ratio @property def rect_split(self): """Get a boolean for whether rectangular portions are extracted.""" return self._rect_split
[docs] def area_from_segment(self, segment, floor_to_ceiling_height): """Get the window area generated by these parameters from a LineSegment3D. Args: segment: A LineSegment3D to which these parameters are applied. floor_to_ceiling_height: The floor-to-ceiling height of the Room2D to which the segment belongs. """ return segment.length * floor_to_ceiling_height * self._window_ratio
[docs] def to_rectangular_windows(self, segment, floor_to_ceiling_height): """Get a version of these WindowParameters as RectangularWindows. Args: segment: A LineSegment3D to which these parameters are applied. floor_to_ceiling_height: The floor-to-ceiling height of the Room2D to which the segment belongs. """ if self._window_ratio == 0: return None scale_factor = self.window_ratio ** 0.5 final_height = floor_to_ceiling_height * scale_factor final_width = segment.length * scale_factor origin = Point2D((segment.length - final_width) / 2, (floor_to_ceiling_height - final_height) / 2) new_w = RectangularWindows([origin], [final_width], [final_height]) new_w._user_data = None if self.user_data is None else self.user_data.copy() return new_w
[docs] def add_window_to_face(self, face, tolerance=0.01): """Add Apertures to a Honeybee Face using these Window Parameters. Args: face: A honeybee-core Face object. tolerance: Optional tolerance value. Default: 0.01, suitable for objects in meters. """ if self._window_ratio == 0: return None face.apertures_by_ratio(self.window_ratio, tolerance, self.rect_split) # if the Aperture is interior, set adjacent boundary condition if isinstance(face._boundary_condition, Surface): num_aps = face.apertures for i, ap in enumerate(face.apertures): ids = face._boundary_condition.boundary_condition_objects adj_ap_id = '{}_Glz{}'.format(ids[0], num_aps - i - 1) final_ids = (adj_ap_id,) + ids ap.boundary_condition = Surface(final_ids, True) self._apply_user_data_to_honeybee(face.apertures)
[docs] def split(self, segments, tolerance=0.01): """Split SimpleWindowRatio parameters across a list of ordered segments. Args: segments: The segments to which the window parameters are being split across. These should be in order as they appear on the parent Room2D. tolerance: The minimum distance between a vertex and the edge of the wall segment that is considered not touching. (Default: 0.01, suitable for objects in meters). """ # if one of the segments is much larger than the others, add all windows to that new_ratios = self._all_to_primary_segment(segments) if new_ratios is not None: return new_ratios # otherwise, just distribute the windows evenly new_w_par = [self] * len(segments) self._add_user_data(new_w_par) return new_w_par
[docs] @staticmethod def merge(window_parameters, segments, floor_to_ceiling_height): """Merge SimpleWindowRatio parameters together using their assigned segments. Args: window_parameters: A list of WindowParameters to be merged. segments: The segments to which the window parameters are assigned. These should be in order as they appear on the parent Room2D. floor_to_ceiling_height: The floor-to-ceiling height of the Room2D to which the segments belong. """ win_area, wall_area, rect_split = 0, 0, True for wp, s in zip(window_parameters, segments): if wp is not None: wall_a = s.length * floor_to_ceiling_height wall_area += wall_a if isinstance(wp, SimpleWindowRatio): win_area += (wall_area * wp.window_ratio) rect_split = wp.rect_split else: # not all windows are of the same type; convert all to rectangular return _WindowParameterBase.merge_to_rectangular( window_parameters, segments, floor_to_ceiling_height) new_w = SimpleWindowRatio(win_area / wall_area, rect_split) new_w._user_data = None if window_parameters[0].user_data is None else \ window_parameters[0].user_data.copy() return new_w
[docs] @classmethod def from_dict(cls, data): """Create SimpleWindowRatio from a dictionary. .. code-block:: python { "type": "SimpleWindowRatio", "window_ratio": 0.4, "rect_split": False } """ assert data['type'] == 'SimpleWindowRatio', \ 'Expected SimpleWindowRatio dictionary. Got {}.'.format(data['type']) rect_split = True if 'rect_split' not in data else data['rect_split'] new_w_par = cls(data['window_ratio'], rect_split) if 'user_data' in data and data['user_data'] is not None: new_w_par.user_data = data['user_data'] return new_w_par
[docs] def to_dict(self): """Get SimpleWindowRatio as a dictionary.""" base = { 'type': 'SimpleWindowRatio', 'window_ratio': self.window_ratio } if self.user_data is not None: base['user_data'] = self.user_data if not self.rect_split: base['rect_split'] = False return base
def _all_to_primary_segment(self, segments, prim_ratio=0.95): """Determine if one segment is primary and should get all the windows.""" if self.window_ratio <= prim_ratio: lengths = [s.length for s in segments] total_len = sum(lengths) prim_len = prim_ratio + 0.01 all_to_one_i = None for i, sl in enumerate(lengths): if sl / total_len > prim_len: all_to_one_i = i all_to_one_ratio = self.window_ratio * (total_len / sl) break if all_to_one_i is not None: new_ratios = [None] * len(segments) new_par = self.duplicate() new_par._window_ratio = all_to_one_ratio new_ratios[all_to_one_i] = new_par return new_ratios def __copy__(self): new_w = SimpleWindowRatio(self.window_ratio, self.rect_split) new_w._user_data = None if self.user_data is None else self.user_data.copy() return new_w def __key(self): """A tuple based on the object properties, useful for hashing.""" return (self._window_ratio, self.rect_split) def __hash__(self): return hash(self.__key()) def __eq__(self, other): return isinstance(other, SimpleWindowRatio) and self.__key() == other.__key() def __ne__(self, other): return not self.__eq__(other) def __repr__(self): return 'SimpleWindowRatio: [ratio: {}]'.format(self.window_ratio)
[docs] class RepeatingWindowRatio(SimpleWindowRatio): """Instructions for repeating windows derived from an area ratio with the base face. Args: window_ratio: A number between 0 and 0.95 for the ratio between the window area and the total facade area. window_height: A number for the target height of the windows. Note that, if the window ratio is too large for the height, the ratio will take precedence and the actual window_height will be larger than this value. sill_height: A number for the target height above the bottom edge of the rectangle to start the windows. Note that, if the ratio is too large for the height, the ratio will take precedence and the sill_height will be smaller than this value. horizontal_separation: A number for the target separation between individual window center lines. If this number is larger than the parent rectangle base, only one window will be produced. vertical_separation: An optional number to create a single vertical separation between top and bottom windows. Default: 0. Properties: * window_ratio * window_height * sill_height * horizontal_separation * vertical_separation * user_data """ __slots__ = ('_window_height', '_sill_height', '_horizontal_separation', '_vertical_separation') def __init__(self, window_ratio, window_height, sill_height, horizontal_separation, vertical_separation=0): """Initialize RepeatingWindowRatio.""" _WindowParameterBase.__init__(self) # add the user_data self._window_ratio = float_in_range(window_ratio, 0, 0.95, 'window ratio') self._window_height = float_positive(window_height, 'window height') self._sill_height = float_positive(sill_height, 'sill height') self._horizontal_separation = \ float_positive(horizontal_separation, 'window horizontal separation') self._vertical_separation = \ float_positive(vertical_separation, 'window vertical separation') @property def window_height(self): """Get a number or the target height of the windows.""" return self._window_height @property def sill_height(self): """Get a number for the height above the bottom edge of the floor.""" return self._sill_height @property def horizontal_separation(self): """Get a number for the separation between individual window center lines.""" return self._horizontal_separation @property def vertical_separation(self): """Get a number for a vertical separation between top/bottom windows.""" return self._vertical_separation
[docs] def to_rectangular_windows(self, segment, floor_to_ceiling_height): """Get a version of these WindowParameters as RectangularWindows. Args: segment: A LineSegment3D to which these parameters are applied. floor_to_ceiling_height: The floor-to-ceiling height of the Room2D to which the segment belongs. """ if self._window_ratio == 0: return None base_plane = Plane(segment.v.rotate_xy(math.pi / 2), segment.p, segment.v) sub_rects = Face3D.sub_rects_from_rect_ratio( base_plane, segment.length, floor_to_ceiling_height, self.window_ratio, self.window_height, self.sill_height, self.horizontal_separation, self.vertical_separation ) origins, widths, heights = [], [], [] for rect in sub_rects: poly = Polygon2D(tuple(base_plane.xyz_to_xy(pt) for pt in rect)) min_pt, max_pt = poly.min, poly.max origins.append(min_pt) widths.append(max_pt.x - min_pt.x) heights.append(max_pt.y - min_pt.y) new_w = RectangularWindows(origins, widths, heights) new_w._user_data = None if self.user_data is None else self.user_data.copy() return new_w
[docs] def add_window_to_face(self, face, tolerance=0.01): """Add Apertures to a Honeybee Face using these Window Parameters. Args: face: A honeybee-core Face object. tolerance: The maximum difference between point values for them to be considered a part of a rectangle. Default: 0.01, suitable for objects in meters. """ if self._window_ratio == 0: return None face.apertures_by_ratio_rectangle( self.window_ratio, self.window_height, self.sill_height, self.horizontal_separation, self.vertical_separation, tolerance) # if the Aperture is interior, set adjacent boundary condition if isinstance(face._boundary_condition, Surface): num_aps = face.apertures for i, ap in enumerate(face.apertures): ids = face._boundary_condition.boundary_condition_objects adj_ap_id = '{}_Glz{}'.format(ids[0], num_aps - i - 1) final_ids = (adj_ap_id,) + ids ap.boundary_condition = Surface(final_ids, True) self._apply_user_data_to_honeybee(face.apertures)
[docs] def scale(self, factor): """Get a scaled version of these WindowParameters. This method is called within the scale methods of the Room2D. Args: factor: A number representing how much the object should be scaled. """ new_w = RepeatingWindowRatio( self.window_ratio, self.window_height * factor, self.sill_height * factor, self.horizontal_separation * factor, self.vertical_separation * factor) new_w._user_data = None if self.user_data is None else self.user_data.copy() return new_w
[docs] def split(self, segments, tolerance=0.01): """Split RepeatingWindowRatio parameters across a list of ordered segments. Args: segments: The segments to which the window parameters are being split across. These should be in order as they appear on the parent Room2D. tolerance: The minimum distance between a vertex and the edge of the wall segment that is considered not touching. (Default: 0.01, suitable for objects in meters). """ # if one of the segments is much larger than the others, add all windows to that new_ratios = self._all_to_primary_segment(segments) if new_ratios is not None: return new_ratios # otherwise, just distribute the windows evenly new_w_par = [self] * len(segments) self._add_user_data(new_w_par) return new_w_par
[docs] @staticmethod def merge(window_parameters, segments, floor_to_ceiling_height): """Merge RepeatingWindowRatio parameters together using their assigned segments. Args: window_parameters: A list of WindowParameters to be merged. segments: The segments to which the window parameters are assigned. These should be in order as they appear on the parent Room2D. floor_to_ceiling_height: The floor-to-ceiling height of the Room2D to which the segments belong. """ weights, heights, sill_heights, h_seps, v_seps = [], [], [], [], [] win_area, wall_area = 0, 0 for wp, s in zip(window_parameters, segments): if wp is not None: wall_a = s.length * floor_to_ceiling_height wall_area += wall_a if isinstance(wp, RepeatingWindowRatio): win_area += (wall_area * wp.window_ratio) weights.append(s.length) heights.append(wp.window_height) sill_heights.append(wp.sill_height) h_seps.append(wp.horizontal_separation) v_seps.append(wp.vertical_separation) else: # not all windows are of the same type; convert all to rectangular return _WindowParameterBase.merge_to_rectangular( window_parameters, segments, floor_to_ceiling_height) window_ratio = win_area / wall_area tw = sum(weights) weights = [w / tw for w in weights] height = sum([h * w for h, w in zip(heights, weights)]) sill_height = sum([h * w for h, w in zip(sill_heights, weights)]) h_sep = sum([s * w for s, w in zip(h_seps, weights)]) v_sep = sum([s * w for s, w in zip(v_seps, weights)]) new_w = RepeatingWindowRatio(window_ratio, height, sill_height, h_sep, v_sep) new_w._user_data = None if window_parameters[0].user_data is None else \ window_parameters[0].user_data.copy() return new_w
[docs] @classmethod def from_dict(cls, data): """Create RepeatingWindowRatio from a dictionary. .. code-block:: python { "type": "RepeatingWindowRatio", "window_ratio": 0.4, "window_height": 2, "sill_height": 0.8, "horizontal_separation": 4, "vertical_separation": 0.5 } """ assert data['type'] == 'RepeatingWindowRatio', \ 'Expected RepeatingWindowRatio dictionary. Got {}.'.format(data['type']) vert = data['vertical_separation'] if 'vertical_separation' in data else 0 new_w_par = cls(data['window_ratio'], data['window_height'], data['sill_height'], data['horizontal_separation'], vert) if 'user_data' in data and data['user_data'] is not None: new_w_par.user_data = data['user_data'] return new_w_par
[docs] def to_dict(self): """Get RepeatingWindowRatio as a dictionary.""" base = { 'type': 'RepeatingWindowRatio', 'window_ratio': self.window_ratio, 'window_height': self.window_height, 'sill_height': self.sill_height, 'horizontal_separation': self.horizontal_separation } if self.vertical_separation != 0: base['vertical_separation'] = self.vertical_separation if self.user_data is not None: base['user_data'] = self.user_data return base
def __copy__(self): new_w = RepeatingWindowRatio( self._window_ratio, self._window_height, self._sill_height, self._horizontal_separation, self._vertical_separation) new_w._user_data = None if self.user_data is None else self.user_data.copy() return new_w def __key(self): """A tuple based on the object properties, useful for hashing.""" return (self._window_ratio, self._window_height, self._sill_height, self._horizontal_separation, self._vertical_separation) def __hash__(self): return hash(self.__key()) def __eq__(self, other): return isinstance(other, RepeatingWindowRatio) and self.__key() == other.__key() def __ne__(self, other): return not self.__eq__(other) def __repr__(self): return 'RepeatingWindowRatio: [ratio: {}] [window_height: {}] [sill_height:' \ ' {}] [horizontal: {}] [vertical: {}]'.format( self._window_ratio, self.window_height, self.sill_height, self.horizontal_separation, self.vertical_separation)
[docs] class RepeatingWindowWidthHeight(_WindowParameterBase): """Instructions for repeating rectangular windows of a fixed width and height. This class effectively fills a wall with windows at the specified width, height and separation. Args: window_height: A number for the target height of the windows. Note that, if the window_height is larger than the height of the wall, the generated windows will have a height equal to the wall height in order to avoid having windows extend outside the wall face. window_width: A number for the target width of the windows. Note that, if the window_width is larger than the width of the wall, the generated windows will have a width equal to the wall width in order to avoid having windows extend outside the wall face. sill_height: A number for the target height above the bottom edge of the wall to start the windows. If the window_height is too large for the sill_height to fit within the rectangle, the window_height will take precedence. horizontal_separation: A number for the target separation between individual window center lines. If this number is larger than the parent rectangle base, only one window will be produced. Properties: * window_height * window_width * sill_height * horizontal_separation * user_data """ __slots__ = ('_window_height', '_window_width', '_sill_height', '_horizontal_separation') def __init__(self, window_height, window_width, sill_height, horizontal_separation): """Initialize RepeatingWindowWidthHeight.""" _WindowParameterBase.__init__(self) # add the user_data self._window_height = float_positive(window_height, 'window height') self._window_width = float_positive(window_width, 'window width') self._sill_height = float_positive(sill_height, 'sill height') self._horizontal_separation = \ float_positive(horizontal_separation, 'window horizontal separation') @property def window_height(self): """Get a number or the target height of the windows.""" return self._window_height @property def window_width(self): """Get a number or the target width of the windows.""" return self._window_width @property def sill_height(self): """Get a number for the height above the bottom edge of the floor.""" return self._sill_height @property def horizontal_separation(self): """Get a number for the separation between individual window center lines.""" return self._horizontal_separation
[docs] def area_from_segment(self, segment, floor_to_ceiling_height): """Get the window area generated by these parameters from a LineSegment3D. Args: segment: A LineSegment3D to which these parameters are applied. floor_to_ceiling_height: The floor-to-ceiling height of the Room2D to which the segment belongs. """ hgt = floor_to_ceiling_height - 0.02 * floor_to_ceiling_height if \ self.window_height >= floor_to_ceiling_height else self.window_height max_width_break_up = segment.length / 2 if self.window_width < max_width_break_up: # multiple windows num_div = round(segment.length / self.horizontal_separation) if \ segment.length > self.horizontal_separation / 2 else 1 if num_div * self.window_width + (num_div - 1) * \ (self.horizontal_separation - self.window_width) > segment.length: num_div = math.floor(segment.length / self.horizontal_separation) return num_div * self.window_width * hgt else: # one single window return segment.length * 0.98 * hgt
[docs] def to_rectangular_windows(self, segment, floor_to_ceiling_height): """Get a version of these WindowParameters as RectangularWindows. Args: segment: A LineSegment3D to which these parameters are applied. floor_to_ceiling_height: The floor-to-ceiling height of the Room2D to which the segment belongs. """ base_plane = Plane(segment.v.rotate_xy(math.pi / 2), segment.p, segment.v) sub_rects = Face3D.sub_rects_from_rect_dimensions( base_plane, segment.length, floor_to_ceiling_height, self.window_height, self.window_width, self.sill_height, self.horizontal_separation ) origins, widths, heights = [], [], [] for rect in sub_rects: poly = Polygon2D(tuple(base_plane.xyz_to_xy(pt) for pt in rect)) min_pt, max_pt = poly.min, poly.max origins.append(min_pt) widths.append(max_pt.x - min_pt.x) heights.append(max_pt.y - min_pt.y) new_w = RectangularWindows(origins, widths, heights) new_w._user_data = None if self.user_data is None else self.user_data.copy() return new_w
[docs] def add_window_to_face(self, face, tolerance=0.01): """Add Apertures to a Honeybee Face using these Window Parameters. Args: face: A honeybee-core Face object. tolerance: The maximum difference between point values for them to be considered a part of a rectangle. Default: 0.01, suitable for objects in meters. """ if self._window_width == 0 or self._window_height == 0: return None face.apertures_by_width_height_rectangle( self.window_height, self.window_width, self.sill_height, self.horizontal_separation, tolerance) # if the Aperture is interior, set adjacent boundary condition if isinstance(face._boundary_condition, Surface): num_aps = face.apertures for i, ap in enumerate(face.apertures): ids = face._boundary_condition.boundary_condition_objects adj_ap_id = '{}_Glz{}'.format(ids[0], num_aps - i - 1) final_ids = (adj_ap_id,) + ids ap.boundary_condition = Surface(final_ids, True) self._apply_user_data_to_honeybee(face.apertures)
[docs] def scale(self, factor): """Get a scaled version of these WindowParameters. This method is called within the scale methods of the Room2D. Args: factor: A number representing how much the object should be scaled. """ new_w = RepeatingWindowWidthHeight( self.window_height * factor, self.window_width * factor, self.sill_height * factor, self.horizontal_separation * factor) new_w._user_data = None if self.user_data is None else self.user_data.copy() return new_w
[docs] def split(self, segments, tolerance=0.01): """Split RepeatingWindowWidthHeight parameters across a list of ordered segments. Args: segments: The segments to which the window parameters are being split across. These should be in order as they appear on the parent Room2D. tolerance: The minimum distance between a vertex and the edge of the wall segment that is considered not touching. (Default: 0.01, suitable for objects in meters). """ # just distribute the windows evenly new_w_par = [self] * len(segments) self._add_user_data(new_w_par) return new_w_par
[docs] @staticmethod def merge(window_parameters, segments, floor_to_ceiling_height): """Merge RepeatingWindowWidthHeight parameters using their assigned segments. Args: window_parameters: A list of WindowParameters to be merged. segments: The segments to which the window parameters are assigned. These should be in order as they appear on the parent Room2D. floor_to_ceiling_height: The floor-to-ceiling height of the Room2D to which the segments belong. """ weights, heights, widths, sill_heights, h_seps = [], [], [], [], [] for wp, s in zip(window_parameters, segments): if wp is not None: if isinstance(wp, RepeatingWindowWidthHeight): weights.append(s.length) heights.append(wp.window_height) widths.append(wp.window_width) sill_heights.append(wp.sill_height) h_seps.append(wp.horizontal_separation) else: # not all windows are of the same type; convert all to rectangular return _WindowParameterBase.merge_to_rectangular( window_parameters, segments, floor_to_ceiling_height) tw = sum(weights) weights = [w / tw for w in weights] height = sum([h * w for h, w in zip(heights, weights)]) width = sum([x * w for x, w in zip(widths, weights)]) sill_height = sum([h * w for h, w in zip(sill_heights, weights)]) h_sep = sum([s * w for s, w in zip(h_seps, weights)]) new_w = RepeatingWindowWidthHeight(height, width, sill_height, h_sep) new_w._user_data = None if window_parameters[0].user_data is None else \ window_parameters[0].user_data.copy() return new_w
[docs] @classmethod def from_dict(cls, data): """Create RepeatingWindowWidthHeight from a dictionary. .. code-block:: python { "type": "RepeatingWindowWidthHeight", "window_height": 2, "window_width": 1.5, "sill_height": 0.8, "horizontal_separation": 4 } """ assert data['type'] == 'RepeatingWindowWidthHeight', 'Expected ' \ 'RepeatingWindowWidthHeight dictionary. Got {}.'.format(data['type']) new_w_par = cls(data['window_height'], data['window_width'], data['sill_height'], data['horizontal_separation']) if 'user_data' in data and data['user_data'] is not None: new_w_par.user_data = data['user_data'] return new_w_par
[docs] def to_dict(self): """Get RepeatingWindowWidthHeight as a dictionary.""" base = { 'type': 'RepeatingWindowWidthHeight', 'window_height': self.window_height, 'window_width': self.window_width, 'sill_height': self.sill_height, 'horizontal_separation': self.horizontal_separation } if self.user_data is not None: base['user_data'] = self.user_data return base
def __copy__(self): new_w = RepeatingWindowWidthHeight( self._window_height, self._window_width, self._sill_height, self._horizontal_separation) new_w._user_data = None if self.user_data is None else self.user_data.copy() return new_w def __key(self): """A tuple based on the object properties, useful for hashing.""" return (self._window_height, self._window_width, self._sill_height, self._horizontal_separation) def __hash__(self): return hash(self.__key()) def __eq__(self, other): return isinstance(other, RepeatingWindowWidthHeight) and \ self.__key() == other.__key() def __ne__(self, other): return not self.__eq__(other) def __repr__(self): return 'RepeatingWindowWidthHeight: [window_height: {}] [window_width: ' \ '{}] [sill_height: {}] [horizontal: {}]'.format( self.window_height, self.window_width, self.sill_height, self.horizontal_separation)
class _AsymmetricBase(_WindowParameterBase): """Base class for WindowParameters that can make asymmetric windows on a wall. """ def flip(self, seg_length): """Flip the direction of the windows along a wall segment. This is needed since windows can exist asymmetrically across the wall segment and operations like reflecting the Room2D across a plane will require the window parameters to be flipped. Reversing the Room2D vertices also requires flipping. Args: seg_length: The length of the segment along which the parameters are being flipped. """ return self def _merge_user_data(self, original_w_par): """Build this object's user_data from those of merged window parameters. Args: original_w_par: An array of the original window parameters used to build this one. """ if not all((owp is None or owp.user_data is None) for owp in original_w_par): new_u = {} for ow_par in original_w_par: if ow_par is not None and ow_par.user_data is not None: for key, val in ow_par.user_data.items(): if key not in new_u: new_u[key] = val elif isinstance(new_u[key], (list, tuple)) and \ isinstance(val, (list, tuple)) and \ len(val) >= len(ow_par): new_u[key] = new_u[key] + val self.user_data = new_u def _split_user_data(self, split_win_par, win_par_indices): """Assign user_data to split WindowParameters with the help of split indices. Args: split_win_par: An array of WindowParameters that were derived by splitting this one. win_par_indices: A list of lists where each sub-list represents one of the WindowParameters in the split_win_par ean each item of each sub-list represents one of the rectangles or polygons in the window parameters. """ if self.user_data is not None: for win_par, wp_i in zip(split_win_par, win_par_indices): if win_par is None: continue u_dict = {} for key, val in self.user_data.items(): if isinstance(val, (list, tuple)) and len(val) >= len(self): u_dict[key] = [self.user_data[key][j] for j in wp_i] else: u_dict[key] = val win_par.user_data = u_dict
[docs] class RectangularWindows(_AsymmetricBase): """Instructions for several rectangular windows, defined by origin, width and height. Note that, if these parameters are applied to a base wall that is too short or too narrow such that the windows fall outside the boundary of the wall, the generated windows will automatically be shortened or excluded. This way, a certain pattern of repeating rectangular windows can be encoded in a single RectangularWindows instance and applied to multiple Room2D segments. Args: origins: An array of ladybug_geometry Point2D objects within the plane of the wall for the origin of each window. The wall plane is assumed to have an origin at the first point of the wall segment and an X-axis extending along the length of the segment. The wall plane's Y-axis always points upwards. Therefore, both X and Y values of each origin point should be positive. widths: An array of positive numbers for the window widths. The length of this list must match the length of the origins. heights: An array of positive numbers for the window heights. The length of this list must match the length of the origins. are_doors: An array of booleans that align with the origins and note whether each of the geometries represents a door (True) or a window (False). If None, it will be assumed that all geometries represent windows and they will be translated to Apertures in any resulting Honeybee model. (Default: None). Properties: * origins * widths * heights * are_doors * user_data """ __slots__ = ('_origins', '_widths', '_heights', '_are_doors') def __init__(self, origins, widths, heights, are_doors=None): """Initialize RectangularWindows.""" _WindowParameterBase.__init__(self) # add the user_data if not isinstance(origins, tuple): origins = tuple(origins) for point in origins: assert isinstance(point, Point2D), \ 'Expected Point2D for window origin. Got {}'.format(type(point)) self._origins = origins self._widths = tuple(float_positive(width, 'window width') for width in widths) self._heights = tuple(float_positive(hgt, 'window height') for hgt in heights) assert len(self._origins) == len(self._widths) == len(self._heights), \ 'Number of window origins, widths, and heights must match.' assert len(self._origins) != 0, \ 'There must be at least one window to use RectangularWindows.' if are_doors is None: self._are_doors = (False,) * len(origins) else: if not isinstance(are_doors, tuple): are_doors = tuple(are_doors) for is_dr in are_doors: assert isinstance(is_dr, bool), 'Expected booleans for ' \ 'RectangularWindows.are_doors. Got {}'.format(type(is_dr)) assert len(are_doors) == len(origins), \ 'Length of RectangularWindows.are_doors ({}) does not match length ' \ 'of RectangularWindows.origins ({}).'.format( len(are_doors), len(origins)) self._are_doors = are_doors @property def origins(self): """Get an array of Point2Ds within the wall plane for the origin of each window. """ return self._origins @property def widths(self): """Get an array of numbers for the window widths.""" return self._widths @property def heights(self): """Get an array of numbers for the window heights.""" return self._heights @property def are_doors(self): """Get an array of booleans that note whether each geometry is a door.""" return self._are_doors
[docs] def area_from_segment(self, segment, floor_to_ceiling_height): """Get the window area generated by these parameters from a LineSegment3D. Args: segment: A LineSegment3D to which these parameters are applied. floor_to_ceiling_height: The floor-to-ceiling height of the Room2D to which the segment belongs. """ max_width = segment.length max_height = floor_to_ceiling_height areas = [] for o, width, height in zip(self.origins, self.widths, self.heights): final_width = max_width - o.x if width + o.x > max_width else width final_height = max_height - o.y if height + o.y > max_height else height if final_height > 0 and final_height > 0: # inside wall boundary areas.append(final_width * final_height) return sum(areas)
[docs] def check_window_overlaps(self, tolerance=0.01): """Check whether any windows overlap with one another. Args: tolerance: The minimum distance that two windows must overlap in order for them to be considered overlapping and invalid. (Default: 0.01, suitable for objects in meters). Returns: A string with the message. Will be an empty string if valid. """ # group the rectangular parameters to gether zip_obj = zip(self.origins, self.widths, self.heights) rect_par = [(o, w, h) for o, w, h in zip_obj] # loop through the polygons and check to see if it overlaps with the others grouped_rects = [[rect_par[0]]] for rect in rect_par[1:]: group_found = False for rect_group in grouped_rects: for oth_rect in rect_group: if self._rectangles_overlap(rect, oth_rect, tolerance): rect_group.append(rect) group_found = True break if group_found: break if not group_found: # the polygon does not overlap with any of the others grouped_rects.append([rect]) # make a new group for the polygon # report any polygons that overlap if not all(len(g) == 1 for g in grouped_rects): base_msg = '({} windows overlap one another)' all_msg = [] for r_group in grouped_rects: if len(r_group) != 1: all_msg.append(base_msg.format(len(r_group))) return ' '.join(all_msg) return ''
[docs] def check_valid_for_segment(self, segment, floor_to_ceiling_height): """Check that these window parameters are valid for a given LineSegment3D. Args: segment: A LineSegment3D to which these parameters are applied. floor_to_ceiling_height: The floor-to-ceiling height of the Room2D to which the segment belongs. Returns: A string with the message. Will be an empty string if valid. """ total_area = segment.length * floor_to_ceiling_height win_area = self.area_from_segment(segment, floor_to_ceiling_height) if win_area >= total_area: return 'Total area of windows [{}] is greater than the area of the ' \ 'parent wall [{}].'.format(win_area, total_area) return ''
[docs] def shift_vertically(self, shift_distance): """Shift these WindowParameters up or down in the wall plane. This is useful when the windows are assigned to a Room2D that is vertically split and new windows need to be assigned to new Room2Ds. Args: shift_distance: A number for the distance that the window parameters will be shifted. Positive values will shift the windows upwards in the wall plane and negative values will shift the windows downwards. """ # create the new window origins new_origins = tuple(Point2D(pt.x, pt.y + shift_distance) for pt in self.origins) new_w = RectangularWindows( new_origins, self.widths, self.heights, self.are_doors) new_w._user_data = None if self.user_data is None else self.user_data.copy() return new_w
[docs] def to_rectangular_windows(self, segment, floor_to_ceiling_height): """Returns the class instance. Provided here for consistency with other classes. Args: segment: A LineSegment3D to which these parameters are applied. floor_to_ceiling_height: The floor-to-ceiling height of the Room2D to which the segment belongs. """ return self
[docs] def to_detailed_windows(self): """Get a version of these WindowParameters as DetailedWindows.""" polygons = [] for o, w, h in zip(self.origins, self.widths, self.heights): poly_pts = (o, Point2D(o.x + w, o.y), Point2D(o.x + w, o.y + h), Point2D(o.x, o.y + h)) polygons.append(Polygon2D(poly_pts)) return DetailedWindows(polygons, self.are_doors)
[docs] def add_window_to_face(self, face, tolerance=0.01): """Add Apertures to a Honeybee Face using these Window Parameters. Args: face: A honeybee-core Face object. tolerance: Optional tolerance value. Default: 0.01, suitable for objects in meters. """ # collect the global properties of the face that set limits on apertures wall_plane = face.geometry.plane wall_plane = wall_plane.rotate(wall_plane.n, math.pi, wall_plane.o) width_seg = LineSegment3D.from_end_points(face.geometry[0], face.geometry[1]) h_vec, b_pt = Vector3D(0, 0, face.max.z - face.min.z), face.geometry[1] height_seg = LineSegment3D.from_end_points(b_pt, b_pt.move(h_vec)) max_width = width_seg.length - tolerance max_height = height_seg.length - tolerance # loop through each window and create its geometry sub_faces = [] zip_obj = zip(self.origins, self.widths, self.heights, self.are_doors) for i, (o, wid, hgt, isd) in enumerate(zip_obj): final_width = max_width - o.x if wid + o.x > max_width else wid final_height = max_height - o.y if hgt + o.y > max_height else hgt if final_height > 0 and final_height > 0: # inside wall boundary base_plane = Plane(wall_plane.n, wall_plane.xy_to_xyz(o), wall_plane.x) s_geo = Face3D.from_rectangle(final_width, final_height, base_plane) if isd: sub_f = Door('{}_Door{}'.format(face.identifier, i + 1), s_geo) face.add_door(sub_f) else: sub_f = Aperture('{}_Glz{}'.format(face.identifier, i + 1), s_geo) face.add_aperture(sub_f) sub_faces.append(sub_f) # if the Aperture is interior, set adjacent boundary condition if isinstance(face._boundary_condition, Surface): ids = face._boundary_condition.boundary_condition_objects sf_type = 'Door' if isd else 'Glz' adj_sf_id = '{}_{}{}'.format(ids[0], sf_type, i + 1) final_ids = (adj_sf_id,) + ids sub_f.boundary_condition = Surface(final_ids, True) self._apply_user_data_to_honeybee(sub_faces)
[docs] def scale(self, factor): """Get a scaled version of these WindowParameters. This method is called within the scale methods of the Room2D. Args: factor: A number representing how much the object should be scaled. """ new_w = RectangularWindows( tuple(Point2D(pt.x * factor, pt.y * factor) for pt in self.origins), tuple(width * factor for width in self.widths), tuple(height * factor for height in self.heights), self.are_doors) new_w._user_data = None if self.user_data is None else self.user_data.copy() return new_w
[docs] def remove_small_windows(self, area_threshold): """Remove any small windows that are below a certain area threshold. Args: area_threshold: A number for the area below which a window will be removed. """ new_origins, new_widths, new_heights, new_are_doors = [], [], [], [] for o, w, h, isd in zip(self.origins, self.widths, self.heights, self.are_doors): if w * h > area_threshold: new_origins.append(o) new_widths.append(w) new_heights.append(h) new_are_doors.append(isd) self._origins = tuple(new_origins) self._widths = tuple(new_widths) self._heights = tuple(new_heights) self._are_doors = tuple(new_are_doors)
[docs] def flip(self, seg_length): """Flip the direction of the windows along a segment. This is needed since windows can exist asymmetrically across the wall segment and operations like reflecting the Room2D across a plane will require the window parameters to be flipped to remain in the same place. Args: seg_length: The length of the segment along which the parameters are being flipped. """ new_origins = [] new_widths = [] new_heights = [] new_are_doors = [] zip_obj = zip(self.origins, self.widths, self.heights, self.are_doors) for o, width, height, is_dr in zip_obj: new_x = seg_length - o.x - width if new_x > 0: # fully within the wall boundary new_origins.append(Point2D(new_x, o.y)) new_widths.append(width) new_heights.append(height) new_are_doors.append(is_dr) elif new_x + width > seg_length * 0.001: # partially within the boundary new_widths.append(width + new_x - (seg_length * 0.001)) new_x = seg_length * 0.001 new_origins.append(Point2D(new_x, o.y)) new_heights.append(height) new_are_doors.append(is_dr) if len(new_origins) != 0: new_w = RectangularWindows(new_origins, new_widths, new_heights, new_are_doors) new_w._user_data = None if self.user_data is None else self.user_data.copy() return new_w
[docs] def offset(self, offset_distance, tolerance=0.01): """Offset the edges of all windows by a certain distance. This is useful for translating between interfaces that expect the window frame to be included within or excluded from the geometry. Note that this operation can often create windows that collide with one another or extend past the parent Face. So it may be desirable to convert these parameters to_detailed_windows and run the union_overlaps method after using this one. Args: offset_distance: Distance with which the edges of each window will be offset from the original geometry. Positive values will offset the geometry outwards and negative values will offset the geometries inwards. tolerance: The minimum difference between point values for them to be considered the distinct. (Default: 0.01, suitable for objects in meters). """ new_origins = [] new_widths = [] new_heights = [] zip_obj = zip(self.origins, self.widths, self.heights) for o, width, height in zip_obj: new_origins.append(Point2D(o.x - offset_distance, o.y - offset_distance)) new_widths.append(width + (2 * offset_distance)) new_heights.append(height + (2 * offset_distance)) self._origins = tuple(new_origins) self._widths = tuple(new_widths) self._heights = tuple(new_heights)
[docs] def split(self, segments, tolerance=0.01): """Split RectangularWindows parameters across a list of ordered segments. Args: segments: The segments to which the window parameters are being split across. These should be in order as they appear on the parent Room2D. tolerance: The minimum distance between a vertex and the edge of the wall segment that is considered not touching. (Default: 0.01, suitable for objects in meters). """ new_win_pars, win_par_is = [], [] rel_pt, rel_w, rel_h = self.origins, self.widths, self.heights rel_d, rel_i = self.are_doors, list(range(len(self.origins))) for segment in segments: # loop through the vertices and adjust them to the max width win_par_i, out_i = [], [] seg_len = segment.length max_width = seg_len - tolerance new_pts, new_w, new_h, new_d = [], [], [], [] out_pts, out_w, out_h, out_d = [], [], [], [] for pt, w, h, d, i in zip(rel_pt, rel_w, rel_h, rel_d, rel_i): x_val = pt.x if w < tolerance: continue if x_val >= max_width: # completely outside of the segment out_pts.append(Point2D(x_val - seg_len, pt.y)) out_w.append(w) out_h.append(h) out_d.append(d) out_i.append(i) elif x_val + w >= max_width: # split by segment split_dist = max_width - x_val new_width = split_dist - tolerance if new_width > tolerance: new_pts.append(Point2D(x_val, pt.y)) new_w.append(new_width) new_h.append(h) new_d.append(d) win_par_i.append(i) out_pts.append(Point2D(tolerance, pt.y)) out_w.append(w - split_dist - (2 * tolerance)) out_h.append(h) out_d.append(d) out_i.append(i) else: # completely inside segment new_pts.append(pt) new_w.append(w) new_h.append(h) new_d.append(d) win_par_i.append(i) # build the final window parameters from the adjusted windows if len(new_pts) != 0: new_win_pars.append(RectangularWindows(new_pts, new_w, new_h, new_d)) else: new_win_pars.append(None) win_par_is.append(win_par_i) # shift all windows to be relevant for the next segment rel_pt, rel_w, rel_h, rel_d, rel_i = out_pts, out_w, out_h, out_d, out_i # apply the user_data to the split window parameters self._split_user_data(new_win_pars, win_par_is) return new_win_pars
[docs] def trim(self, original_segment, sub_segment, tolerance=0.01): """Trim RectangularWindows for a sub segment given the original segment. Args: original_segment: The original LineSegment3D to which the window parameters are assigned. sub_segment: A LineSegment3D that is a sub-segment of the original_segment, which will be used to trim the window parameters to fit this segment. Note that this sub_segment should have the same orientation as the original segment. tolerance: The minimum distance between a vertex and the edge of the wall segment that is considered not touching. (Default: 0.01, suitable for objects in meters). """ # shift all origins to be relevant for the sub segment rel_pt = self.origins shift_dist = original_segment.p1.distance_to_point(sub_segment.p1) if shift_dist > tolerance: rel_pt = [] for pt in self.origins: rel_pt.append(Point2D(pt.x - shift_dist, pt.y)) # loop through the rectangles and trim them for the segment max_width = sub_segment.length - tolerance new_pts, new_w, new_h, new_d = [], [], [], [] for pt, w, h, d in zip(rel_pt, self.widths, self.heights, self.are_doors): x_val = pt.x if x_val >= max_width or x_val + w < tolerance: continue # completely outside of the segment if x_val < tolerance: # split by segment on left w = tolerance - x_val x_val = tolerance if x_val + w >= max_width: # split by segment on right split_dist = max_width - x_val new_pts.append(Point2D(x_val, pt.y)) new_w.append(split_dist - tolerance) new_h.append(h) new_d.append(d) else: # completely inside segment new_pts.append(pt) new_w.append(w) new_h.append(h) new_d.append(d) # build the final window parameters from the adjusted windows if len(new_pts) == 0: return None return RectangularWindows(new_pts, new_w, new_h, new_d)
[docs] @staticmethod def merge(window_parameters, segments, floor_to_ceiling_height): """Merge RectangularWindows parameters together using their assigned segments. Args: window_parameters: A list of WindowParameters to be merged. segments: The segments to which the window parameters are assigned. These should be in order as they appear on the parent Room2D. floor_to_ceiling_height: The floor-to-ceiling height of the Room2D to which the segments belong. """ new_w_par = _WindowParameterBase.merge_to_rectangular( window_parameters, segments, floor_to_ceiling_height) new_w_par._merge_user_data(window_parameters) return new_w_par
[docs] @classmethod def from_dict(cls, data): """Create RectangularWindows from a dictionary. .. code-block:: python { "type": "RectangularWindows", "origins": [(1, 1), (3, 0.5)], # array of (x, y) floats in wall plane "widths": [1, 3], # array of floats for window widths "heights": [1, 2.5], # array of floats for window heights "are_doors": [False, True] # array of booleans for whether it's a door } """ assert data['type'] == 'RectangularWindows', \ 'Expected RectangularWindows. Got {}.'.format(data['type']) are_doors = data['are_doors'] if 'are_doors' in data else None new_w_par = cls(tuple(Point2D.from_array(pt) for pt in data['origins']), data['widths'], data['heights'], are_doors) if 'user_data' in data and data['user_data'] is not None: new_w_par.user_data = data['user_data'] return new_w_par
[docs] def to_dict(self): """Get RectangularWindows as a dictionary.""" base = { 'type': 'RectangularWindows', 'origins': [pt.to_array() for pt in self.origins], 'widths': self.widths, 'heights': self.heights, "are_doors": self.are_doors } if self.user_data is not None: base['user_data'] = self.user_data return base
@staticmethod def _rectangles_overlap(rect_par_1, rect_par_2, tolerance): """Test if two rectangles overlap within a tolerance.""" min_pt1, w1, h1 = rect_par_1 max_pt1 = Point2D(min_pt1.x + w1, min_pt1.y + h1) min_pt2, w2, h2 = rect_par_2 max_pt2 = Point2D(min_pt2.x + w2, min_pt2.y + h2) if min_pt2.x > max_pt1.x - tolerance or min_pt1.x > max_pt2.x - tolerance: return False if min_pt2.y > max_pt1.y - tolerance or min_pt1.y > max_pt2.y - tolerance: return False return True def __copy__(self): new_w = RectangularWindows( self.origins, self.widths, self.heights, self.are_doors) new_w._user_data = None if self.user_data is None else self.user_data.copy() return new_w def __key(self): """A tuple based on the object properties, useful for hashing.""" return (hash(self.origins), hash(self.widths), hash(self.heights), hash(self.are_doors)) def __hash__(self): return hash(self.__key()) def __eq__(self, other): return isinstance(other, RectangularWindows) and \ len(self._origins) == len(other._origins) def __ne__(self, other): return not self.__eq__(other) def __len__(self): return len(self.origins) def __repr__(self): return 'RectangularWindows: [{} windows]'.format(len(self.origins))
[docs] class DetailedWindows(_AsymmetricBase): """Instructions for detailed windows, defined by 2D Polygons (lists of 2D vertices). Note that these parameters are intended to represent windows that are specific to a particular segment and, unlike the other WindowParameters, this class performs no automatic checks to ensure that the windows lie within the boundary of the wall they have been assigned to. Args: polygons: An array of ladybug_geometry Polygon2D objects within the plane of the wall with one polygon for each window. The wall plane is assumed to have an origin at the first point of the wall segment and an X-axis extending along the length of the segment. The wall plane's Y-axis always points upwards. Therefore, both X and Y values of each point in the polygon should always be positive. are_doors: An array of booleans that align with the polygons and note whether each of the polygons represents a door (True) or a window (False). If None, it will be assumed that all polygons represent windows and they will be translated to Apertures in any resulting Honeybee model. (Default: None). Properties: * polygons * are_doors * user_data Usage: Note that, if you are starting from 3D vertices of windows, you can use this class to represent them. The DetailedWindows.from_face3ds is the simplest way to do this. Otherwise, below is some sample code to convert from vertices in the same 3D space as a vertical wall to vertices in the 2D plane of the wall (as this class interprets it). In the code, 'seg_p1' is the first point of a given wall segment and is assumed to be the origin of the wall plane. 'seg_p2' is the second point of the wall segment, and 'vertex' is a given vertex of the window that you want to translate from 3D coordinates into 2D. All input points are presented as arrays of 3 floats and the output is an array of 2 (x, y) coordinates. .. code-block:: python def dot_product(v1, v2): return v1[0] * v2[0] + v1[1] * v2[1] + v1[2] * v2[3] def normalize(v): d = (v[0] ** 2 + v[1] ** 2 + v[2] ** 2) ** 0.5 return (v[0] / d, v[1] / d, v[2] / d) def xyz_to_xy(seg_p1, seg_p2, vertex): diff = (vertex[0] - seg_p1[0], vertex[1] - seg_p1[1], vertex[2] - seg_p1[2]) axis = (seg_p2[0] - seg_p1[0], seg_p2[1] - seg_p1[1], seg_p2[2] - seg_p1[2]) plane_x = normalize(axis) plane_y = (0, 0, 1) return (dot_product(plane_x , diff), dot_product(plane_y, diff)) """ __slots__ = ('_polygons', '_are_doors') def __init__(self, polygons, are_doors=None): """Initialize DetailedWindows.""" _WindowParameterBase.__init__(self) # add the user_data if not isinstance(polygons, tuple): polygons = tuple(polygons) for polygon in polygons: assert isinstance(polygon, Polygon2D), \ 'Expected Polygon2D for window polygon. Got {}'.format(type(polygon)) assert len(polygons) != 0, \ 'There must be at least one polygon to use DetailedWindows.' self._polygons = polygons if are_doors is None: self._are_doors = (False,) * len(polygons) else: if not isinstance(are_doors, tuple): are_doors = tuple(are_doors) for is_dr in are_doors: assert isinstance(is_dr, bool), 'Expected booleans for ' \ 'DetailedWindow.are_doors. Got {}'.format(type(is_dr)) assert len(are_doors) == len(polygons), \ 'Length of DetailedWindow.are_doors ({}) does not match the length ' \ 'of DetailedWindow.polygons ({}).'.format(len(are_doors), len(polygons)) self._are_doors = are_doors
[docs] @classmethod def from_face3ds(cls, face3ds, segment, are_doors=None): """Create DetailedWindows from Face3Ds and a segment they are assigned to. Args: face3ds: A list of Face3D objects for the detailed windows. segment: A LineSegment3D that sets the plane in which 3D vertices are located. are_doors: An array of booleans that align with the face3ds and note whether each of the polygons represents a door (True) or a window (False). If None, it will be assumed that all polygons represent windows and they will be translated to Apertures in any resulting Honeybee model. (Default: None). """ plane = Plane(Vector3D(segment.v.y, -segment.v.x, 0), segment.p, segment.v) pt3d = tuple(tuple(pt for pt in face.vertices) for face in face3ds) return cls( tuple(Polygon2D(tuple(plane.xyz_to_xy(pt) for pt in poly)) for poly in pt3d), are_doors )
@property def polygons(self): """Get an array of Polygon2Ds with one polygon for each window.""" return self._polygons @property def are_doors(self): """Get an array of booleans that note whether each polygon is a door.""" return self._are_doors
[docs] def is_flipped_equivalent(self, other, segment, tolerance=0.01): """Check if this WindowParameter is equal to another when flipped. This is useful to know if the window parameters will be valid when translated to Honeybee when they are adjacent to the other. Args: other: Another WindowParameter object for which flipped equivalency will be tested. segment: A LineSegment3D to which these parameters are applied. tolerance: The minimum distance between a vertex and the edge of the wall segment that is considered not touching. (Default: 0.01, suitable for objects in meters). """ # first check some fundamental things if not isinstance(other, DetailedWindows) or \ len(self._polygons) != len(other._polygons): return False # then, check that the polygons actually match other_flip = other.flip(segment.length) found_adjacencies = 0 for poly_1 in self._polygons: for poly_2 in other_flip._polygons: if poly_1.center.distance_to_point(poly_2.center) <= tolerance: found_adjacencies += 1 break if len(self._polygons) == found_adjacencies: return True return False
[docs] def area_from_segment(self, segment, floor_to_ceiling_height): """Get the window area generated by these parameters from a LineSegment3D. Args: segment: A LineSegment3D to which these parameters are applied. floor_to_ceiling_height: The floor-to-ceiling height of the Room2D to which the segment belongs. """ return sum(polygon.area for polygon in self._polygons)
[docs] def check_window_overlaps(self, tolerance=0.01): """Check whether any polygons overlap with one another. Args: tolerance: The minimum distance that two polygons must overlap in order for them to be considered overlapping and invalid. (Default: 0.01, suitable for objects in meters). Returns: A string with the message. Will be an empty string if valid. """ # group the polygons according to their overlaps grouped_polys = Polygon2D.group_by_overlap(self.polygons, tolerance) # report any polygons that overlap if not all(len(g) == 1 for g in grouped_polys): base_msg = '({} windows overlap one another)' all_msg = [] for p_group in grouped_polys: if len(p_group) != 1: all_msg.append(base_msg.format(len(p_group))) return ' '.join(all_msg) return ''
[docs] def check_self_intersecting(self, tolerance=0.01): """Check whether any polygons in these window parameters are self intersecting. Args: tolerance: The minimum distance between a vertex coordinates where they are considered equivalent. (Default: 0.01, suitable for objects in meters). Returns: A string with the message. Will be an empty string if valid. """ self_int_i = [] for i, polygon in enumerate(self.polygons): if polygon.is_self_intersecting: new_geo = polygon.remove_colinear_vertices(tolerance) if new_geo.is_self_intersecting: self_int_i.append(str(i)) if len(self_int_i) != 0: return 'Window polygons with the following indices are ' \ 'self-intersecting: ({})'.format(' '.join(self_int_i)) return ''
[docs] def check_valid_for_segment(self, segment, floor_to_ceiling_height): """Check that these window parameters are valid for a given LineSegment3D. Args: segment: A LineSegment3D to which these parameters are applied. floor_to_ceiling_height: The floor-to-ceiling height of the Room2D to which the segment belongs. Returns: A string with the message. Will be an empty string if valid. """ # first check that the total window area isn't larger than the wall max_width = segment.length total_area = max_width * floor_to_ceiling_height win_area = self.area_from_segment(segment, floor_to_ceiling_height) if win_area >= total_area: return 'Total area of windows [{}] is greater than the area of the ' \ 'parent wall [{}].'.format(win_area, total_area) # next, check to be sure that no window is out of the wall boundary msg_template = 'Vertex 2D {} coordinate [{}] is outside the range allowed ' \ 'by the parent wall segment.' for p_gon in self.polygons: min_pt, max_pt = p_gon.min, p_gon.max if min_pt.x <= 0: return msg_template.format('X', min_pt.x) if min_pt.y <= 0: return msg_template.format('Y', min_pt.y) if max_pt.x >= max_width: return msg_template.format('X', max_pt.x) if max_pt.y >= floor_to_ceiling_height: return msg_template.format('Y', max_pt.y) return ''
[docs] def adjust_for_segment(self, segment, floor_to_ceiling_height, tolerance=0.01, sliver_tolerance=None): """Get these parameters with vertices excluded beyond the domain of a given line. Args: segment: A LineSegment3D to which these parameters are applied. floor_to_ceiling_height: The floor-to-ceiling height of the Room2D to which the segment belongs. tolerance: The minimum distance between a vertex and the edge of the wall segment that is considered not touching. (Default: 0.01, suitable for objects in meters). sliver_tolerance: A number to be used for the tolerance at which sliver polygons should be removed if they are created during the adjustment process. If None, the tolerance will be used. (Default: None). Returns: A new DetailedWindows object that fits entirely in the domain of the input line segment and floor_to_ceiling_height. """ # compute the maximum width and height seg_len = segment.length double_tol = 2 * tolerance if seg_len - double_tol < 0 or floor_to_ceiling_height - double_tol < 0: return None max_width = seg_len - tolerance max_height = floor_to_ceiling_height - tolerance # loop through the vertices and adjust them sliver_tol = sliver_tolerance if sliver_tolerance is not None else tolerance new_polygons, new_are_doors, kept_i = [], [], [] for i, (p_gon, is_dr) in enumerate(zip(self.polygons, self.are_doors)): new_verts = [] for vert in p_gon: x_val, y_val = vert.x, vert.y if x_val <= tolerance: x_val = tolerance if y_val <= tolerance: y_val = tolerance if x_val >= max_width: x_val = max_width if y_val >= max_height: y_val = max_height new_verts.append(Point2D(x_val, y_val)) new_poly = Polygon2D(new_verts) if new_poly.area > sliver_tol: new_polygons.append(new_poly) new_are_doors.append(is_dr) kept_i.append(i) # return the final window parameters new_w_par = None if len(new_polygons) != 0: new_w_par = DetailedWindows(new_polygons, new_are_doors) # update user_data lists if some windows were not added if new_w_par is not None and self.user_data is not None: clean_u = self.user_data if len(new_polygons) != len(self.polygons): clean_u = {} for key, val in self.user_data.items(): if isinstance(val, (list, tuple)) and len(val) >= len(self.polygons): clean_u[key] = [val[j] for j in kept_i] else: clean_u[key] = val new_w_par.user_data = clean_u return new_w_par
[docs] def shift_vertically(self, shift_distance): """Shift these WindowParameters up or down in the wall plane. This is useful when the windows are assigned to a Room2D that is vertically split and new windows need to be assigned to new Room2Ds. Args: shift_distance: A number for the distance that the window parameters will be shifted. Positive values will shift the windows upwards in the wall plane and negative values will shift the windows downwards. """ # create the new window polygons new_polygons = [] for poly in self.polygons: new_poly = Polygon2D(tuple(Point2D(p.x, p.y + shift_distance)) for p in poly) new_polygons.append(new_poly) new_w = DetailedWindows(new_polygons, self.are_doors) new_w._user_data = None if self.user_data is None else self.user_data.copy() return new_w
[docs] def to_rectangular_windows(self, segment, floor_to_ceiling_height): """Get a version of these WindowParameters as RectangularWindows. This will simply translate each window polygon to its own rectangular window. For merging windows that touch or overlap one another into rectangles, the merge_to_bounding_rectangle method should be used before using this method. Args: segment: A LineSegment3D to which these parameters are applied. floor_to_ceiling_height: The floor-to-ceiling height of the Room2D to which the segment belongs. """ origins, widths, heights = [], [], [] for poly in self.polygons: min_pt, max_pt = poly.min, poly.max origins.append(min_pt) widths.append(max_pt.x - min_pt.x) heights.append(max_pt.y - min_pt.y) new_w = RectangularWindows(origins, widths, heights, self.are_doors) new_w._user_data = None if self.user_data is None else self.user_data.copy() return new_w
[docs] def add_window_to_face(self, face, tolerance=0.01): """Add Apertures to a Honeybee Face using these Window Parameters. Args: face: A honeybee-core Face object. tolerance: Optional tolerance value. Default: 0.01, suitable for objects in meters. """ # get the plane of the parent wall wall_plane = face.geometry.plane wall_plane = wall_plane.rotate(wall_plane.n, math.pi, wall_plane.o) width_seg = LineSegment3D.from_end_points(face.geometry[0], face.geometry[1]) h_vec, b_pt = Vector3D(0, 0, face.max.z - face.min.z), face.geometry[1] height_seg = LineSegment3D.from_end_points(b_pt, b_pt.move(h_vec)) max_width = width_seg.length - tolerance max_height = height_seg.length - tolerance # automatically clip any geometries outside of the face so they are bounded by it clean_polygons, clean_are_doors, kept_i = [], [], [] for i, (p_gon, is_dr) in enumerate(zip(self.polygons, self.are_doors)): new_verts, verts_moved = [], [] for vert in p_gon: x_val, y_val, v_moved = vert.x, vert.y, False if x_val <= tolerance: x_val, v_moved = tolerance, True if y_val <= tolerance: y_val, v_moved = tolerance, True if x_val >= max_width: x_val, v_moved = max_width, True if y_val >= max_height: y_val, v_moved = max_height, True new_verts.append(Point2D(x_val, y_val)) verts_moved.append(v_moved) if not all(verts_moved): # original polygon was definitely in the face clean_polygons.append(new_verts) clean_are_doors.append(is_dr) kept_i.append(i) else: # check that the polygon wasn't 100% glazing tol2 = 2 * tolerance if max_height > tol2 and max_width > tol2: min_pt, max_pt = p_gon.min, p_gon.max if not (max_pt.x < tolerance or max_pt.y < tolerance or min_pt.x > max_width or min_pt.y > max_height): clean_polygons.append(new_verts) clean_are_doors.append(is_dr) kept_i.append(i) # update user_data lists if some windows were not added clean_u = self.user_data if self.user_data is not None: if len(clean_polygons) != len(self.polygons): clean_u = {} for key, val in self.user_data.items(): if isinstance(val, (list, tuple)) and len(val) >= len(self.polygons): clean_u[key] = [val[j] for j in kept_i] else: clean_u[key] = val # loop through each window and create its geometry sub_faces = [] for i, (polygon, isd) in enumerate(zip(clean_polygons, clean_are_doors)): pt3d = tuple(wall_plane.xy_to_xyz(pt) for pt in polygon) s_geo = Face3D(pt3d) if isd: sub_f = Door('{}_Door{}'.format(face.identifier, i + 1), s_geo) face.add_door(sub_f) else: sub_f = Aperture('{}_Glz{}'.format(face.identifier, i + 1), s_geo) face.add_aperture(sub_f) sub_faces.append(sub_f) # if the Aperture is interior, set adjacent boundary condition if isinstance(face._boundary_condition, Surface): ids = face._boundary_condition.boundary_condition_objects sf_type = 'Door' if isd else 'Glz' adj_sf_id = '{}_{}{}'.format(ids[0], sf_type, i + 1) final_ids = (adj_sf_id,) + ids sub_f.boundary_condition = Surface(final_ids, True) self._apply_user_data_to_honeybee(sub_faces, clean_u)
[docs] def scale(self, factor): """Get a scaled version of these WindowParameters. This method is called within the scale methods of the Room2D. Args: factor: A number representing how much the object should be scaled. """ new_w = DetailedWindows( tuple(polygon.scale(factor) for polygon in self.polygons), self.are_doors) new_w._user_data = None if self.user_data is None else self.user_data.copy() return new_w
[docs] def flip(self, seg_length): """Flip the direction of the windows along a segment. This is needed since windows can exist asymmetrically across the wall segment and operations like reflecting the Room2D across a plane will require the window parameters to be flipped to remain in the same place. Args: seg_length: The length of the segment along which the parameters are being flipped. """ # set values derived from the property of the segment normal = Vector2D(1, 0) origin = Point2D(seg_length / 2, 0) # loop through the polygons and reflect them across the mid plane of the wall new_polygons = [] for polygon in self.polygons: new_verts = tuple(pt.reflect(normal, origin) for pt in polygon.vertices) new_polygons.append(Polygon2D(tuple(reversed(new_verts)))) new_w = DetailedWindows(new_polygons, self.are_doors) new_w._user_data = None if self.user_data is None else self.user_data.copy() return new_w
[docs] def offset(self, offset_distance, tolerance=0.01): """Offset the edges of all polygons by a certain distance. This is useful for translating between interfaces that expect the window frame to be included within or excluded from the geometry. Note that this operation can often create polygons that collide with one another or extend past the parent Face. So it may be desirable to run the union_overlaps method after using this one. Args: offset_distance: Distance with which the edges of each polygon will be offset from the original geometry. Positive values will offset the geometry outwards and negative values will offset the geometries inwards. tolerance: The minimum difference between point values for them to be considered the distinct. (Default: 0.01, suitable for objects in meters). """ offset_polys, offset_are_doors = [], [] for polygon, isd in zip(self.polygons, self.are_doors): try: poly = polygon.remove_colinear_vertices(tolerance) off_poly = poly.offset(-offset_distance, False) if not off_poly.is_self_intersecting: offset_polys.append(off_poly) else: # polygon became self-intersecting after offset offset_polys.append(poly) offset_are_doors.append(isd) except AssertionError: # degenerate window to ignore pass self._polygons = tuple(offset_polys) self._are_doors = tuple(offset_are_doors)
[docs] def union_overlaps(self, tolerance=0.01): """Union any window polygons that overlap with one another. Args: tolerance: The minimum distance that two polygons must overlap in order for them to be considered overlapping. (Default: 0.01, suitable for objects in meters). """ # group the polygons by their overlap grouped_polys = Polygon2D.group_by_overlap(self.polygons, tolerance) # union any of the polygons that overlap if not all(len(g) == 1 for g in grouped_polys): new_polys = [] for p_group in grouped_polys: if len(p_group) == 1: new_polys.append(p_group[0]) else: union_poly = Polygon2D.boolean_union_all(p_group, tolerance) for new_poly in union_poly: new_polys.append(new_poly.remove_colinear_vertices(tolerance)) self._reassign_are_doors(new_polys) self._polygons = tuple(new_polys)
[docs] def merge_and_simplify(self, max_separation, tolerance=0.01): """Merge window polygons that are close to one another into a single polygon. This can be used to create a simpler set of windows that is easier to edit and is in the same location as the original windows. Args: max_separation: A number for the maximum distance between window polygons at which point they will be merged into a single geometry. Typically, this max_separation should be set to a value that is slightly larger than the window frame. Setting this equal to the tolerance will simply join neighboring windows together. tolerance: The maximum difference between point values for them to be considered distinct. (Default: 0.01, suitable for objects in meters). """ # gather a clean version of the polygons with colinear vertices removed clean_polys = [] for poly in self.polygons: try: clean_polys.append(poly.remove_colinear_vertices(tolerance)) except AssertionError: # degenerate geometry to ignore pass # join the polygons together if max_separation <= tolerance: new_polys = Polygon2D.joined_intersected_boundary( clean_polys, tolerance) else: new_polys = Polygon2D.gap_crossing_boundary( clean_polys, max_separation, tolerance) is_self_intersect = False for poly in new_polys: if poly.is_self_intersecting: is_self_intersect = True if is_self_intersect: # we hit the frame distance exactly in float tolerance max_separation = max_separation - tolerance if max_separation <= tolerance: new_polys = Polygon2D.joined_intersected_boundary( clean_polys, tolerance) else: new_polys = Polygon2D.gap_crossing_boundary( clean_polys, max_separation, tolerance) is_self_intersect = False for poly in new_polys: if poly.is_self_intersecting: is_self_intersect = True if is_self_intersect: new_polys = clean_polys self._reassign_are_doors(new_polys) self._polygons = tuple(new_polys)
[docs] def merge_to_bounding_rectangle(self, tolerance=0.01): """Merge window polygons that touch or overlap with one another to a rectangle. Args: tolerance: The minimum distance from the edge of a neighboring polygon at which a point is considered to touch that polygon. (Default: 0.01, suitable for objects in meters). """ # group the polygons by their overlap grouped_polys = Polygon2D.group_by_touching(self.polygons, tolerance) # union any of the polygons that overlap if not all(len(g) == 1 for g in grouped_polys): new_polys = [] for p_group in grouped_polys: if len(p_group) == 1: new_polys.append(p_group[0]) else: min_pt, max_pt = bounding_rectangle(p_group) rect_verts = ( min_pt, Point2D(max_pt.x, min_pt.y), max_pt, Point2D(min_pt.x, max_pt.y)) rect_poly = Polygon2D(rect_verts) new_polys.append(rect_poly) self._reassign_are_doors(new_polys) self._polygons = tuple(new_polys)
[docs] def remove_small_windows(self, area_threshold): """Remove any small window polygons that are below a certain area threshold. Args: area_threshold: A number for the area below which a window polygon will be removed. """ new_polygons, new_are_doors = [], [] for poly, is_dr in zip(self.polygons, self.are_doors): if poly.area > area_threshold: new_polygons.append(poly) new_are_doors.append(is_dr) self._polygons = tuple(new_polygons) self._are_doors = tuple(new_are_doors)
[docs] def split(self, segments, tolerance=0.01): """Split DetailedWindows parameters across a list of ordered segments. Args: segments: The segments to which the window parameters are being split across. These should be in order as they appear on the parent Room2D. tolerance: The minimum distance between a vertex and the edge of the wall segment that is considered not touching. (Default: 0.01, suitable for objects in meters). """ new_win_pars, win_par_is = [], [] rel_polygons, rel_dr = self.polygons, self.are_doors rel_i = list(range(len(self.polygons))) for segment in segments: # loop through the vertices and adjust them to the max width max_width = segment.length - tolerance win_par_i, out_i = [], [] new_polygons, new_dr, out_polygons, out_dr = [], [], [], [] for p_gon, is_dr, i in zip(rel_polygons, rel_dr, rel_i): new_verts, verts_moved = [], [] for vert in p_gon: x_val, v_moved = vert.x, False if x_val < tolerance: x_val, v_moved = tolerance, True if x_val >= max_width: x_val, v_moved = max_width, True new_verts.append(Point2D(x_val, vert.y)) verts_moved.append(v_moved) if not all(verts_moved): new_polygons.append(Polygon2D(new_verts)) win_par_i.append(i) new_dr.append(is_dr) if True in verts_moved: # outside of the segment out_polygons.append(p_gon) out_dr.append(is_dr) out_i.append(i) else: out_polygons.append(p_gon) out_dr.append(is_dr) out_i.append(i) # build the final window parameters from the adjusted polygons if len(new_polygons) != 0: new_win_pars.append(DetailedWindows(new_polygons, new_dr)) else: new_win_pars.append(None) win_par_is.append(win_par_i) # shift all polygons to be relevant for the next segment shift_dist = segment.length rel_polygons = [] for p_gon in out_polygons: new_v = [Point2D(p.x - shift_dist, p.y) for p in p_gon] rel_polygons.append(Polygon2D(new_v)) rel_dr = out_dr rel_i = out_i # apply the user_data to the split window parameters self._split_user_data(new_win_pars, win_par_is) return new_win_pars
[docs] def trim(self, original_segment, sub_segment, tolerance=0.01): """Trim DetailedWindows for a sub segment given the original segment. Args: original_segment: The original LineSegment3D to which the window parameters are assigned. sub_segment: A LineSegment3D that is a sub-segment of the original_segment, which will be used to trim the window parameters to fit this segment. Note that this sub_segment should have the same orientation as the original segment. tolerance: The minimum distance between a vertex and the edge of the wall segment that is considered not touching. (Default: 0.01, suitable for objects in meters). """ # shift all polygons to be relevant for the sub segment rel_polygons = self.polygons shift_dist = original_segment.p1.distance_to_point(sub_segment.p1) if shift_dist > tolerance: rel_polygons = [] for p_gon in self.polygons: new_v = [Point2D(p.x - shift_dist, p.y) for p in p_gon] rel_polygons.append(Polygon2D(new_v)) # loop through the polygon vertices and trim them for the segment max_width = sub_segment.length - tolerance new_polygons, new_dr, out_polygons, out_dr = [], [], [], [] for p_gon, is_dr in zip(rel_polygons, self.are_doors): new_verts, verts_moved = [], [] for vert in p_gon: x_val, v_moved = vert.x, False if x_val < tolerance: x_val, v_moved = tolerance, True if x_val >= max_width: x_val, v_moved = max_width, True new_verts.append(Point2D(x_val, vert.y)) verts_moved.append(v_moved) if not all(verts_moved): new_polygons.append(Polygon2D(new_verts)) new_dr.append(is_dr) if True in verts_moved: # outside of the segment out_polygons.append(p_gon) out_dr.append(is_dr) else: out_polygons.append(p_gon) out_dr.append(is_dr) # build the final window parameters from the adjusted polygons if len(new_polygons) == 0: return None return DetailedWindows(new_polygons, new_dr)
[docs] @staticmethod def merge(window_parameters, segments, floor_to_ceiling_height): """Merge DetailedWindows parameters together using their assigned segments. Args: window_parameters: A list of WindowParameters to be merged. segments: The segments to which the window parameters are assigned. These should be in order as they appear on the parent Room2D. floor_to_ceiling_height: The floor-to-ceiling height of the Room2D to which the segment belongs. """ base_x = 0 polygons, are_doors = [], [] for wp, s in zip(window_parameters, segments): if wp is not None: if isinstance(wp, DetailedWindows): for p_gon, is_dr in zip(wp.polygons, wp.are_doors): new_pts = [Point2D(pt.x + base_x, pt.y) for pt in p_gon.vertices] polygons.append(Polygon2D(new_pts)) are_doors.append(is_dr) else: # not all windows are of the same type; convert all to rectangular return _WindowParameterBase.merge_to_rectangular( window_parameters, segments, floor_to_ceiling_height) base_x += s.length if len(polygons) == 0: # all of the input window parameters were None return None new_w_par = DetailedWindows(polygons, are_doors) new_w_par._merge_user_data(window_parameters) return new_w_par
[docs] @classmethod def from_dict(cls, data, segment=None): """Create DetailedWindows from a dictionary. Args: data: A dictionary in the format below. The vertices of the "polygons" can either contain 2 values (indicating they are vertices within the plane of a parent wall segment) or they can contain 3 values (indicating they are 3D world coordinates). If 3 values are used, a segment must be specified below to convert them to the 2D format. segment: A LineSegment3D that sets the plane in which 3D vertices are located. .. code-block:: python { "type": "DetailedWindows", "polygons": [((0.5, 0.5), (2, 0.5), (2, 2), (0.5, 2)), ((3, 1), (4, 1), (4, 2))], "are_doors": [False] } """ assert data['type'] == 'DetailedWindows', \ 'Expected DetailedWindows dictionary. Got {}.'.format(data['type']) if len(data['polygons']) == 0: return None # empty object; just treat it as no windows are_doors = data['are_doors'] if 'are_doors' in data else None if len(data['polygons'][0][0]) == 2: new_w_par = cls( tuple(Polygon2D(tuple(Point2D.from_array(pt) for pt in poly)) for poly in data['polygons']), are_doors ) else: assert segment is not None, 'Segment must be specified when using 3D ' \ 'vertices for DetailedWindows.' plane = Plane(Vector3D(segment.v.y, -segment.v.x, 0), segment.p, segment.v) pt3d = tuple(tuple(Point3D.from_array(pt) for pt in poly) for poly in data['polygons']) new_w_par = cls( tuple(Polygon2D(tuple(plane.xyz_to_xy(pt) for pt in poly)) for poly in pt3d), are_doors ) if 'user_data' in data and data['user_data'] is not None: new_w_par.user_data = data['user_data'] return new_w_par
[docs] def to_dict(self): """Get DetailedWindows as a dictionary.""" base = { 'type': 'DetailedWindows', 'polygons': [[pt.to_array() for pt in poly] for poly in self.polygons], 'are_doors': self.are_doors } if self.user_data is not None: base['user_data'] = self.user_data return base
[docs] @staticmethod def is_face3d_in_segment_plane( face3d, segment, height, tolerance=0.01, angle_tolerance=1): """Check if a given Face3D is in the plane and range of a LineSegment3D. Args: face3d: A list of Face3D objects for the detailed windows. segment: A LineSegment3D that sets the plane in which 3D vertices are located. height: A number for the height of the wall formed by extruding the segment. 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. angle_tolerance: The max angle in degrees that the plane normals can differ from one another in order for them to be considered coplanar. Default: 1 degree. Returns: True if the face3d is in the plane and range of the segment. False if it is not. """ plane = Plane(Vector3D(segment.v.y, -segment.v.x, 0), segment.p, segment.v) # test whether the face3d is coplanar if not plane.is_coplanar_tolerance(face3d.plane, tolerance, angle_tolerance): return False seg_face = Face3D.from_rectangle(segment.length, height, plane) return seg_face.is_sub_face(face3d, tolerance, angle_tolerance)
def _reassign_are_doors(self, new_polys): """Reset the are_doors property using a set of new polygons.""" if len(new_polys) != len(self._polygons): if all(not dr for dr in self._are_doors): # case of no doors self._are_doors = (False,) * len(new_polys) else: new_are_doors = [] for n_poly in new_polys: for o_poly, is_door in zip(self.polygons, self.are_doors): if n_poly.is_point_inside_bound_rect(o_poly.center): new_are_doors.append(is_door) break else: new_are_doors.append(False) self._are_doors = tuple(new_are_doors) def __len__(self): return len(self._polygons) def __getitem__(self, key): return self._polygons[key] def __iter__(self): return iter(self._polygons) def __copy__(self): new_w = DetailedWindows(self._polygons, self._are_doors) new_w._user_data = None if self.user_data is None else self.user_data.copy() return new_w def __key(self): """A tuple based on the object properties, useful for hashing.""" return tuple(hash(polygon) for polygon in self._polygons) + self.are_doors def __hash__(self): return hash(self.__key()) def __eq__(self, other): return isinstance(other, DetailedWindows) and \ len(self._polygons) == len(other._polygons) def __ne__(self, other): return not self.__eq__(other) def __repr__(self): return 'DetailedWindows: [{} windows]'.format(len(self._polygons))