# -*- coding: utf-8 -*-
# -*- Python Version: 2.7 -*-
"""Honeybee-PH-HVAC-Equipment: Ducts."""
from math import radians
try:
from typing import Any, Dict, List, Optional, Union
except ImportError:
pass # IronPython
try:
from ladybug_geometry.geometry3d.pointvector import Point3D
from ladybug_geometry.geometry3d.polyline import LineSegment3D
except ImportError as e:
raise ImportError("\nFailed to import ladybug_geometry:\n\t{}".format(e))
try:
from honeybee_phhvac import _base
except ImportError as e:
raise ImportError("\nFailed to import honeybee_phhvac:\n\t{}".format(e))
[docs]
class PhDuctSegment(_base._PhHVACBase):
"""A single duct segment (linear) with geometry and a attributes.
Note: All geometric information is in the model's unit-type (meters by default).
"""
def __init__(
self,
_geom,
_insul_thickness=0.0254,
_insul_conductivity=0.04,
_insul_refl=True,
_diameter=0.160,
_height=None,
_width=None,
):
# type: (LineSegment3D, float, float, bool, float, Optional[float], Optional[float]) -> None
super(PhDuctSegment, self).__init__()
self.geometry = _geom
self.insulation_thickness = _insul_thickness
self.insulation_conductivity = _insul_conductivity
self.insulation_reflective = _insul_refl
self.diameter = _diameter
self.height = _height
self.width = _width
@property
def length(self):
# type: () -> float
"""Return the length of the duct segment in model-units."""
return self.geometry.length
@property
def shape_type(self):
# type: () -> int
if (self.height is not None) and (self.width is not None):
return 2 # Rectangular Duct
else:
return 1 # Round Duct
@property
def is_round_duct(self):
# type: () -> bool
return self.shape_type == 1
@property
def shape_type_description(self):
# type: () -> str
"""Return a string description of the shape of the duct segment."""
if self.is_round_duct:
return "{:.3f} Θ".format(float(self.diameter))
else:
return "{:.3f} x {:.3f}".format(self.width or 0.0, self.height or 0.0)
[docs]
@classmethod
def default(cls):
# type: () -> PhDuctSegment
"""Return a default Duct segment with a length of 1.0"""
pt1 = Point3D(0, 0, 0)
pt2 = Point3D(1, 0, 0)
geom = LineSegment3D.from_end_points(pt1, pt2)
return cls(geom)
def __copy__(self):
# type: () -> PhDuctSegment
new_obj = PhDuctSegment(self.geometry)
new_obj.geometry = self.geometry
new_obj.insulation_thickness = self.insulation_thickness
new_obj.insulation_conductivity = self.insulation_conductivity
new_obj.insulation_reflective = self.insulation_reflective
new_obj.diameter = self.diameter
new_obj.height = self.height
new_obj.width = self.width
new_obj.identifier = self.identifier
new_obj.display_name = self.display_name
new_obj.user_data = self.user_data
return new_obj
[docs]
def duplicate(self):
# type: () -> PhDuctSegment
return self.__copy__()
[docs]
def to_dict(self):
# type: () -> Dict[str, Union[str, dict]]
d = super(PhDuctSegment, self).to_dict()
d["geometry"] = self.geometry.to_dict()
d["insulation_thickness"] = self.insulation_thickness
d["insulation_conductivity"] = self.insulation_conductivity
d["insulation_reflective"] = self.insulation_reflective
d["diameter"] = self.diameter
d["height"] = self.height
d["width"] = self.width
return d
[docs]
@classmethod
def from_dict(cls, _input_dict):
# type: (Dict) -> PhDuctSegment
new_obj = cls(_geom=LineSegment3D.from_dict(_input_dict["geometry"]))
new_obj.insulation_thickness = _input_dict["insulation_thickness"]
new_obj.insulation_conductivity = _input_dict["insulation_conductivity"]
new_obj.insulation_reflective = _input_dict["insulation_reflective"]
new_obj.diameter = _input_dict["diameter"]
new_obj.height = _input_dict["height"]
new_obj.width = _input_dict["width"]
new_obj.identifier = _input_dict["identifier"]
new_obj.display_name = _input_dict["display_name"]
new_obj.user_data = _input_dict["user_data"]
return new_obj
def __str__(self):
return "{}: diam={}, length={:.3f}".format(self.__class__.__name__, self.diameter, self.length)
def __repr__(self):
return str(self)
[docs]
def ToString(self):
return self.__repr__()
[docs]
def move(self, moving_vec3D):
# type: (Point3D) -> PhDuctSegment
"""Move the duct segment along a vector.
Args:
moving_vec3D: A Vector3D with the direction and distance to move the ray.
"""
new_segment = self.duplicate()
new_segment.geometry = self.geometry.move(moving_vec3D)
return new_segment
[docs]
def rotate(self, axis_vec3D, angle_degrees, origin_pt3D):
# type: (Point3D, float, Point3D) -> PhDuctSegment
"""Rotate the duct segment by a certain angle around an axis and origin.
Right hand rule applies:
If axis has a positive orientation, rotation will be clockwise.
If axis has a negative orientation, rotation will be counterclockwise.
Args:
axis_vec3D: A Vector3D axis representing the axis of rotation.
angle_degrees: An angle for rotation in degrees.
origin_pt3D: A Point3D for the origin around which the object will be rotated.
"""
new_segment = self.duplicate()
new_segment.geometry = self.geometry.rotate(axis_vec3D, radians(angle_degrees), origin_pt3D)
return new_segment
[docs]
def rotate_xy(self, angle_degrees, origin_pt3D):
# type: (float, Point3D) -> PhDuctSegment
"""Rotate the duct segment counterclockwise in the XY plane by a certain angle.
Args:
angle_degrees: An angle in degrees.
origin_pt3D: A Point3D for the origin around which the object will be rotated.
"""
new_segment = self.duplicate()
new_segment.geometry = self.geometry.rotate_xy(radians(angle_degrees), origin_pt3D)
return new_segment
[docs]
def reflect(self, normal_vec3D, origin_pt3D):
# type: (Point3D, Point3D) -> PhDuctSegment
"""Reflected the duct segment across a plane with the input normal vector and origin.
Args:
normal_vec3D: A Vector3D representing the normal vector for the plane across
which the line segment will be reflected. THIS VECTOR MUST BE NORMALIZED.
origin_pt3D: A Point3D representing the origin from which to reflect.
"""
new_segment = self.duplicate()
new_segment.geometry = self.geometry.reflect(normal_vec3D, origin_pt3D)
return new_segment
[docs]
def scale(self, scale_factor, origin_pt3D=None):
# type: (float, Optional[Point3D]) -> PhDuctSegment
"""Scale the duct segment by a factor from an origin point.
Args:
scale_factor: A number representing how much the line segment should be scaled.
origin_pt3D: A Point3D representing the origin from which to scale.
If None, it will be scaled from the World origin (0, 0, 0).
"""
new_segment = self.duplicate()
new_segment.geometry = self.geometry.scale(scale_factor, origin_pt3D)
new_segment.insulation_thickness = self.insulation_thickness * scale_factor
new_segment.diameter = self.diameter * scale_factor
new_segment.height = scale_factor * self.height if self.height else None
new_segment.width = scale_factor * self.width if self.width else None
return new_segment
[docs]
class PhDuctElement(_base._PhHVACBase):
"""A Duct Element made up of one or more individual Duct Segments.
Note: All geometric information is in the model's unit-type (meters by default).
"""
def __init__(self, _display_name=None, _duct_type=1, *args, **kwargs):
# type: (Optional[str], int, *Any, **Any) -> None
super(PhDuctElement, self).__init__()
self.display_name = _display_name or self.identifier_short
self.duct_type = _duct_type
self._segments = {} # type: Dict[str, PhDuctSegment]
@property
def segments(self):
# type: () -> List[PhDuctSegment]
"""Return a list of all the PhDuctSegments that make up the PhDuctElement."""
return list(self._segments.values())
@property
def length(self):
# type: () -> float
"""Return the total duct length of all the PhDuctSegments in model-units."""
return sum(s.length for s in self.segments)
@property
def is_round_duct(self):
# type: () -> bool
return self.shape_type == 1
@property
def shape_type(self):
# type: () -> Optional[int]
segment_types = {s.shape_type for s in self.segments}
if len(segment_types) == 0:
return None
elif len(segment_types) == 1:
return segment_types.pop()
else:
raise ValueError("Mixed shape-types in duct segments.")
@property
def shape_type_description(self):
# type: () -> Optional[str]
descriptions = {s.shape_type_description for s in self.segments}
if len(descriptions) == 0:
return None
elif len(descriptions) == 1:
return descriptions.pop()
else:
raise ValueError("Mixed shapes/sizes in duct segments.")
[docs]
@classmethod
def default_supply_duct(cls, *args, **kwargs):
# type: (*Any, **Any) -> PhDuctElement
"""Returns a default PhDuctElement with a single segment and a length of 1.0"""
duct_element = cls(*args, **kwargs)
duct_element.duct_type = 1
duct_element.add_segment(PhDuctSegment.default())
return duct_element
[docs]
@classmethod
def default_exhaust_duct(cls, *args, **kwargs):
# type: (*Any, **Any) -> PhDuctElement
"""Returns a default PhDuctElement with a single segment and a length of 1.0"""
duct_element = cls(*args, **kwargs)
duct_element.duct_type = 2
duct_element.add_segment(PhDuctSegment.default())
return duct_element
[docs]
def add_segment(self, _segment):
# type: (PhDuctSegment) -> None
"""Add a new PhDuctSegment to the Duct Element."""
# -- Check that the new segment is the right shape type
if len(self.segments) > 0:
if not _segment.shape_type == self.shape_type:
msg = "Error: Cannot join round and rectangular duct segments."
raise Exception(msg)
self._segments[_segment.identifier] = _segment
[docs]
def clear_segments(self):
# type: () -> None
"""Clear all the segments from the duct element."""
self._segments = {}
def __copy__(self):
# type: () -> PhDuctElement
new_obj = PhDuctElement()
for segment in self.segments:
new_obj.add_segment(segment.duplicate())
new_obj.identifier = self.identifier
new_obj.display_name = self.display_name
new_obj.duct_type = self.duct_type
new_obj.user_data = self.user_data
return new_obj
[docs]
def duplicate(self):
# type: () -> PhDuctElement
return self.__copy__()
[docs]
def to_dict(self):
# type: () -> Dict[str, Union[str, dict]]
d = super(PhDuctElement, self).to_dict()
d["segments"] = {}
for segment in self.segments:
d["segments"][segment.identifier] = segment.to_dict()
d["duct_type"] = self.duct_type
return d
[docs]
@classmethod
def from_dict(cls, _input_dict):
# type: (Dict) -> PhDuctElement
new_obj = cls()
for seg_dict in _input_dict["segments"].values():
new_obj._segments[seg_dict["identifier"]] = PhDuctSegment.from_dict(seg_dict)
new_obj.identifier = _input_dict["identifier"]
new_obj.display_name = _input_dict["display_name"]
new_obj.duct_type = _input_dict["duct_type"]
new_obj.user_data = _input_dict["user_data"]
return new_obj
def __str__(self):
return "{}: (display_name={}, identifier={} ) [{} segments, len={:.3f}]".format(
self.__class__.__name__,
self.display_name,
self.identifier,
len(self.segments),
self.length,
)
def __repr__(self):
return str(self)
[docs]
def ToString(self):
return self.__repr__()
[docs]
def move(self, moving_vec3D):
"""Move the duct element's segment along a vector.
Args:
moving_vec3D: A Vector3D with the direction and distance to move the ray.
"""
new_element = self.duplicate()
new_element.clear_segments()
for segment in self.segments:
new_element.add_segment(segment.move(moving_vec3D))
return new_element
[docs]
def rotate(self, axis_vec3D, angle_degrees, origin_pt3D):
"""Rotate the duct element's segment by a certain angle around an axis and origin.
Right hand rule applies:
If axis has a positive orientation, rotation will be clockwise.
If axis has a negative orientation, rotation will be counterclockwise.
Args:
axis_vec3D: A Vector3D axis representing the axis of rotation.
angle_degrees: An angle for rotation in degrees.
origin_pt3D: A Point3D for the origin around which the object will be rotated.
"""
new_element = self.duplicate()
new_element.clear_segments()
for segment in self.segments:
new_element.add_segment(segment.rotate(axis_vec3D, angle_degrees, origin_pt3D))
return new_element
[docs]
def rotate_xy(self, angle_degrees, origin_pt3D):
"""Rotate the duct element's segment counterclockwise in the XY plane by a certain angle.
Args:
angle_degree: An angle in degrees.
origin_pt3D: A Point3D for the origin around which the object will be rotated.
"""
new_element = self.duplicate()
new_element.clear_segments()
for segment in self.segments:
new_element.add_segment(segment.rotate_xy(angle_degrees, origin_pt3D))
return new_element
[docs]
def reflect(self, normal_vec3D, origin_pt3D):
"""Reflected the duct element's segment across a plane with the input normal vector and origin.
Args:
normal_vec3D: A Vector3D representing the normal vector for the plane across
which the line segment will be reflected. THIS VECTOR MUST BE NORMALIZED.
origin_pt3D: A Point3D representing the origin from which to reflect.
"""
new_element = self.duplicate()
new_element.clear_segments()
for segment in self.segments:
new_element.add_segment(segment.reflect(normal_vec3D, origin_pt3D))
return new_element
[docs]
def scale(self, scale_factor, origin_pt3D=None):
# type: (float, Optional[Point3D]) -> PhDuctElement
"""Scale the duct element's segments by a factor from an origin point.
Args:
scale_factor: A number representing how much the line segment should be scaled.
origin_pt3D: A Point3D representing the origin from which to scale.
If None, it will be scaled from the World origin (0, 0, 0).
"""
new_element = self.duplicate()
new_element.clear_segments()
for segment in self.segments:
new_element.add_segment(segment.scale(scale_factor, origin_pt3D))
return new_element