# 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))