Skip to content

Commit

Permalink
feat(model): Add objects for Dragonfly Model and ContextShade
Browse files Browse the repository at this point in the history
  • Loading branch information
chriswmackey authored and Chris Mackey committed Dec 7, 2019
1 parent 56e8c62 commit ad98bff
Show file tree
Hide file tree
Showing 13 changed files with 1,570 additions and 64 deletions.
36 changes: 36 additions & 0 deletions dragonfly/_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
"""Base class for all geometry objects."""
from honeybee.typing import valid_string

from ladybug_geometry.geometry2d.pointvector import Point2D


class _BaseGeometry(object):
"""A base class for all geometry objects.
Expand Down Expand Up @@ -50,6 +52,40 @@ def properties(self):
def duplicate(self):
"""Get a copy of this object."""
return self.__copy__()

@staticmethod
def _calculate_min(geometry_objects):
"""Calculate min Point2D around an array of geometry with min attributes.
This is used in all functions that calculate bounding rectangles around
dragonfly objects and assess when two objects are in close proximity.
"""
min_pt = [geometry_objects[0].min.x, geometry_objects[0].min.y]

for room in geometry_objects[1:]:
if room.min.x < min_pt[0]:
min_pt[0] = room.min.x
if room.min.y < min_pt[1]:
min_pt[1] = room.min.y

return Point2D(min_pt[0], min_pt[1])

@staticmethod
def _calculate_max(geometry_objects):
"""Calculate max Point2D around an array of geometry with max attributes.
This is used in all functions that calculate bounding rectangles around
dragonfly objects and assess when two objects are in close proximity.
"""
max_pt = [geometry_objects[0].max.x, geometry_objects[0].max.y]

for room in geometry_objects[1:]:
if room.max.x > max_pt[0]:
max_pt[0] = room.max.x
if room.max.y > max_pt[1]:
max_pt[1] = room.max.y

return Point2D(max_pt[0], max_pt[1])

def __copy__(self):
new_obj = self.__class__(self.name)
Expand Down
82 changes: 62 additions & 20 deletions dragonfly/building.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ class Building(_BaseGeometry):
* floor_area
* exterior_wall_area
* exterior_aperture_area
* min
* max
"""
__slots__ = ('_unique_stories',)

Expand Down Expand Up @@ -256,6 +258,24 @@ def exterior_aperture_area(self):
"""
return sum([story.exterior_aperture_area * story.multiplier
for story in self._unique_stories])

@property
def min(self):
"""Get a Point2D for the min bounding rectangle vertex in the XY plane.
This is useful in calculations to determine if this Building is in proximity
to other objects.
"""
return self._calculate_min(self._unique_stories)

@property
def max(self):
"""Get a Point2D for the max bounding rectangle vertex in the XY plane.
This is useful in calculations to determine if this Building is in proximity
to other objects.
"""
return self._calculate_max(self._unique_stories)

def all_stories(self):
"""Get a list of all Story objects that form the Building.
Expand Down Expand Up @@ -494,8 +514,9 @@ def buildings_to_honeybee(buildings, use_multiplier, tolerance):
return base_model

@staticmethod
def buildings_to_honeybee_self_shade(buildings, shade_dist, use_multiplier,
tolerance):
def buildings_to_honeybee_self_shade(
buildings, context_shades=None, shade_distance=None, use_multiplier=True,
tolerance=None):
"""Convert an array of Buildings into several honeybee Models with self-shading.
Each input Building will be exported into its own Model. For each Model,
Expand All @@ -506,13 +527,16 @@ def buildings_to_honeybee_self_shade(buildings, shade_dist, use_multiplier,
Args:
buildings: An array of Building objects to be converted into honeybee
Models that account for their own shading of one another.
shade_dist: A number to note the distance beyond which other
buildings' shade should not be exported into a given Model. This is
context_shades: An optional array of ContextShade objects that will be
added to the honeybee Models if their bounding box overlaps with a
given building within the shade_distance.
shade_distance: An optional number to note the distance beyond which other
objects' shade should not be exported into a given Model. This is
helpful for reducing the simulation run time of each Model when other
connected buildings are too far away to have a meaningful impact on
the results. If None, all other buildings will be included as context
shade in each and every Model. Set to 0 to exclude all neighboring
buildings from the resulting models.
buildings from the resulting models. Default: None.
use_multiplier: If True, the multipliers on this Building's Stories will be
passed along to the generated Honeybee Room objects, indicating the
simulation will be run once for each unique room and then results
Expand All @@ -521,41 +545,59 @@ def buildings_to_honeybee_self_shade(buildings, shade_dist, use_multiplier,
multipliers and all resulting multipliers will be 1. Default: True
tolerance: The minimum distance in z values of floor_height and
floor_to_ceiling_height at which adjacent Faces will be split.
If None, no splitting will occur.
If None, no splitting will occur. Default: None.
"""
models = [] # list to be filled with Honeybee Models

# create a list with all context representations of the buildings
# create lists with all context representations of the buildings + shade
bldg_shades = []
bnd_pts = []
if shade_dist != 0:
bldg_pts = []
con_shades = []
con_pts = []
if shade_distance is None or shade_distance > 0:
for bldg in buildings:
bldg_shades.append(bldg.shade_representation(tolerance))
min, max = bldg.unique_stories[0].min, bldg.unique_stories[0].max
center = Point2D((min.x + max.x) / 2, (min.y + max.y) / 2)
bnd_pts.append((min, center, max))
b_min, b_max = bldg.min, bldg.max
center = Point2D((b_min.x + b_max.x) / 2, (b_min.y + b_max.y) / 2)
bldg_pts.append((b_min, center, b_max))
if context_shades is not None:
for con in context_shades:
con_shades.append(con.to_honeybee())
c_min, c_max = con.min, con.max
center = Point2D((c_min.x + c_max.x) / 2, (c_min.y + c_max.y) / 2)
con_pts.append((c_min, center, c_max))

# loop through each Building and create a model
num_bldg = len(buildings)
for i, bldg in enumerate(buildings):
model = bldg.to_honeybee(use_multiplier, tolerance)

if shade_dist is None: # add all other bldg shades to the model
for j in xrange(i + 1, num_bldg):
if shade_distance is None: # add all other bldg shades to the model
for j in xrange(i + 1, num_bldg): # buildings before this one
for shd in bldg_shades[j]:
model.add_shade(shd)
for k in xrange(i):
for k in xrange(i): # buildings after this one
for shd in bldg_shades[k]:
model.add_shade(shd)
elif shade_dist > 0: # add only context shade within the distance
for j in xrange(i + 1, num_bldg):
if Building._bound_rect_in_dist(bnd_pts[i], bnd_pts[j], shade_dist):
for c_shade in con_shades: # context shades
for shd in c_shade:
model.add_shade(shd)
elif shade_distance > 0: # add only shade within the distance
for j in xrange(i + 1, num_bldg): # buildings before this one
if Building._bound_rect_in_dist(bldg_pts[i], bldg_pts[j],
shade_distance):
for shd in bldg_shades[j]:
model.add_shade(shd)
for k in xrange(i):
if Building._bound_rect_in_dist(bnd_pts[i], bnd_pts[k], shade_dist):
for k in xrange(i): # buildings after this one
if Building._bound_rect_in_dist(bldg_pts[i], bldg_pts[k],
shade_distance):
for shd in bldg_shades[k]:
model.add_shade(shd)
for s in xrange(len(con_shades)): # context shades
if Building._bound_rect_in_dist(bldg_pts[i], con_pts[s],
shade_distance):
for shd in con_shades[s]:
model.add_shade(shd)
models.append(model) # apend to the final list of Models
return models

Expand Down
183 changes: 183 additions & 0 deletions dragonfly/context.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,183 @@
# coding: utf-8
"""Dragonfly Context Shade."""
from ._base import _BaseGeometry
from .properties import ContextShadeProperties

from honeybee.shade import Shade

from ladybug_geometry.geometry3d.face import Face3D

import math


class ContextShade(_BaseGeometry):
"""A Context Shade object defined by an array of Face3Ds (eg. canopy, trees, etc.).
Properties:
* name
* display_name
* geometry
* area
* min
* max
"""
__slots__ = ('_geometry',)

def __init__(self, name, geometry):
"""A Context Shade object defined by an array of Face3Ds.
Args:
name: ContextShade name. Must be < 100 characters.
geometry: An array of ladybug_geometry Face3D objects that together
represent the context shade.
"""
_BaseGeometry.__init__(self, name) # process the name

# process the geometry
if not isinstance(geometry, tuple):
geometry = tuple(geometry)
assert len(geometry) > 0, 'ContextShade must have at least one Face3D.'
for shd_geo in geometry:
assert isinstance(shd_geo, Face3D), \
'Expected ladybug_geometry Face3D. Got {}'.format(type(shd_geo))
self._geometry = geometry

self._properties = ContextShadeProperties(self) # properties for extensions

@classmethod
def from_dict(cls, data):
"""Initialize an ContextShade from a dictionary.
Args:
data: A dictionary representation of an ContextShade object.
"""
# check the type of dictionary
assert data['type'] == 'ContextShade', 'Expected ContextShade dictionary. ' \
'Got {}.'.format(data['type'])

geometry = tuple(Face3D.from_dict(shd_geo) for shd_geo in data['geometry'])
shade = cls(data['name'], geometry)
if 'display_name' in data and data['display_name'] is not None:
shade._display_name = data['display_name']

if data['properties']['type'] == 'ContextShadeProperties':
shade.properties._load_extension_attr_from_dict(data['properties'])
return shade

@property
def geometry(self):
"""Get a tuple of Face3D objects that together represent the context shade."""
return self._geometry

@property
def area(self):
"""Get a number for the total surface area of the ContextShade."""
return sum([geo.area for geo in self._geometry])

@property
def min(self):
"""Get a Point2D for the min bounding rectangle vertex in the XY plane.
This is useful in calculations to determine if this ContextShade is in
proximity to other objects.
"""
return self._calculate_min(self._geometry)

@property
def max(self):
"""Get a Point2D for the max bounding rectangle vertex in the XY plane.
This is useful in calculations to determine if this ContextShade is in
proximity to other objects.
"""
return self._calculate_max(self._geometry)

def move(self, moving_vec):
"""Move this ContextShade along a vector.
Args:
moving_vec: A ladybug_geometry Vector3D with the direction and distance
to move the object.
"""
self._geometry = tuple(shd_geo.move(moving_vec) for shd_geo in self._geometry)

def rotate_xy(self, angle, origin):
"""Rotate this ContextShade counterclockwise in the XY plane by a certain angle.
Args:
angle: An angle in degrees.
origin: A ladybug_geometry Point3D for the origin around which the
object will be rotated.
"""
self._geometry = tuple(shd_geo.rotate_xy(math.radians(angle), origin)
for shd_geo in self._geometry)

def reflect(self, plane):
"""Reflect this ContextShade across a plane.
Args:
plane: A ladybug_geometry Plane across which the object will be reflected.
"""
self._geometry = tuple(shd_geo.reflect(plane.n, plane.o)
for shd_geo in self._geometry)

def scale(self, factor, origin=None):
"""Scale this ContextShade by a factor from an origin point.
Args:
factor: A number representing how much the object should be scaled.
origin: A ladybug_geometry Point3D representing the origin from which
to scale. If None, it will be scaled from the World origin (0, 0, 0).
"""
self._geometry = tuple(shd_geo.scale(factor, origin)
for shd_geo in self._geometry)

def to_honeybee(self):
"""Convert Dragonfly ContextShade to an array of Honeybee Shades."""
shades = []
for i, shd_geo in enumerate(self._geometry):
# create the shade object
shade = Shade('{}_{}'.format(self.display_name, i), shd_geo)
# transfer any extension properties assigned to the Shade
shade._properties = self.properties.to_honeybee(shade)
shades.append(shade)
return shades

def to_dict(self, abridged=False, included_prop=None):
"""Return ContextShade as a dictionary.
Args:
abridged: Boolean to note whether the extension properties of the
object (ie. materials, transmittance schedule) should be included in
detail (False) or just referenced by name (True). Default: False.
included_prop: List of properties to filter keys that must be included in
output dictionary. For example ['energy'] will include 'energy' key if
available in properties to_dict. By default all the keys will be
included. To exclude all the keys from extensions use an empty list.
"""
base = {'type': 'ContextShade'}
base['name'] = self.name
base['display_name'] = self.display_name
base['properties'] = self.properties.to_dict(abridged, included_prop)
enforce_upper_left = True if 'energy' in base['properties'] else False
base['geometry'] = [shd_geo.to_dict(False, enforce_upper_left)
for shd_geo in self._geometry]
return base

def __copy__(self):
new_shd = ContextShade(self.name, self._geometry)
new_shd._display_name = self.display_name
new_shd._properties._duplicate_extension_attr(self._properties)
return new_shd

def __len__(self):
return len(self._geometry)

def __getitem__(self, key):
return self._geometry[key]

def __iter__(self):
return iter(self._geometry)

def __repr__(self):
return 'ContextShade: %s' % self.display_name
Loading

0 comments on commit ad98bff

Please sign in to comment.