"""
This module contains the implementation of `geomeppy.IDF`.
"""
import itertools
from typing import Any, Dict, List, Optional, Union # noqa
from eppy.bunch_subclass import EpBunch # noqa
from eppy.idf_msequence import Idf_MSequence # noqa
from .geom.intersect_match import intersect_idf_surfaces, match_idf_surfaces
from .builder import Block, Zone
from .geom.polygons import bounding_box, Polygon2D # noqa
from .geom.vectors import Vector2D, Vector3D # noqa
from .io.obj import export_to_obj
from .patches import PatchedIDF
from .recipes import (
set_default_constructions,
set_wwr,
rotate,
scale,
translate,
translate_to_origin,
)
from .view_geometry import view_idf
from .geom.core_perim import core_perim_zone_coordinates
def new_idf(fname):
# type: (str) -> IDF
"""Create a new blank IDF.
:param fname: A name for the new IDF.
"""
idf = IDF()
idf.new(fname)
return idf
class IDF(PatchedIDF):
"""Geometry-enabled IDF class, usable in the same way as Eppy's IDF.
This adds geometry functionality to Eppy's IDF class.
"""
[docs] def intersect_match(self):
# type: () -> None
"""Intersect all surfaces in the IDF, then set boundary conditions."""
self.intersect()
self.match()
[docs] def intersect(self):
# type: () -> None
"""Intersect all surfaces in the IDF."""
intersect_idf_surfaces(self)
[docs] def match(self):
# type: () -> None
"""Set boundary conditions for all surfaces in the IDF."""
match_idf_surfaces(self)
[docs] def translate_to_origin(self):
# type: () -> None
"""Move an IDF close to the origin so that it can be viewed in SketchUp."""
translate_to_origin(self)
[docs] def translate(self, vector):
# type: (Vector2D) -> None
"""Move the IDF in the direction given by a vector.
:param vector: A vector to translate by.
"""
surfaces = self.getsurfaces()
translate(surfaces, vector)
subsurfaces = self.getsubsurfaces()
translate(subsurfaces, vector)
shadingsurfaces = self.getshadingsurfaces()
translate(shadingsurfaces, vector)
[docs] def rotate(self, angle, anchor=None):
# type: (float, Optional[Union[Vector2D, Vector3D]]) -> None
"""Rotate the IDF counterclockwise by the angle given.
:param angle: Angle (in degrees) to rotate by.
:param anchor: Point around which to rotate. Default is the centre of the the IDF's bounding box.
"""
anchor = anchor or self.centroid
surfaces = self.getsurfaces()
subsurfaces = self.getsubsurfaces()
shadingsurfaces = self.getshadingsurfaces()
self.translate(-anchor)
rotate(surfaces, angle)
rotate(subsurfaces, angle)
rotate(shadingsurfaces, angle)
self.translate(anchor)
[docs] def scale(self, factor, anchor=None, axes="xy"):
# type: (float, Optional[Union[Vector2D, Vector3D]], str) -> None
"""Scale the IDF by a scaling factor.
:param factor: Factor to scale by.
:param anchor: Point to scale around. Default is the centre of the the IDF's bounding box.
:param axes: Axes to scale on. Default 'xy'.
"""
anchor = anchor or self.centroid
surfaces = self.getsurfaces()
subsurfaces = self.getsubsurfaces()
shadingsurfaces = self.getshadingsurfaces()
self.translate(-anchor)
scale(surfaces, factor, axes)
scale(subsurfaces, factor, axes)
scale(shadingsurfaces, factor, axes)
self.translate(anchor)
def set_default_constructions(self):
# type: () -> None
set_default_constructions(self)
[docs] def bounding_box(self):
# type: () -> Polygon2D
"""Calculate the site bounding box.
:returns: A polygon of the bounding box.
"""
floors = self.getsurfaces("floor")
return bounding_box(floors)
@property
def centroid(self):
# type: () -> Vector2D
"""Calculate the centroid of the site bounding box.
:returns: The centroid of the site bounding box.
"""
bbox = self.bounding_box()
return bbox.centroid
[docs] def getsurfaces(self, surface_type=""):
# type: (str) -> Union[List[EpBunch], Idf_MSequence]
"""Return all surfaces in the IDF.
:param surface_type: Type of surface to get. Defaults to all.
:returns: IDF surfaces.
"""
surfaces = itertools.chain.from_iterable(
[
self.idfobjects[key.upper()]
for key in self.idd_index["ref2names"]["SurfaceNames"]
]
)
if surface_type:
surfaces = filter(
lambda x: x.Surface_Type.lower() == surface_type.lower(), surfaces
)
return list(surfaces)
[docs] def getsubsurfaces(self, surface_type=""):
# type: (str) -> Union[List[EpBunch], Idf_MSequence]
"""Return all subsurfaces in the IDF.
:param surface_type: Type of surface to get. Defaults to all.
:returns: IDF surfaces.
"""
surfaces = itertools.chain.from_iterable(
[
self.idfobjects[key.upper()]
for key in self.idd_index["ref2names"]["SubSurfNames"]
]
)
if surface_type:
surfaces = filter(
lambda x: x.Surface_Type.lower() == surface_type.lower(), surfaces
)
return list(surfaces)
[docs] def getshadingsurfaces(self, surface_type=""):
# type: (str) -> Union[List[EpBunch], Idf_MSequence]
"""Return all subsurfaces in the IDF.
:param surface_type: Type of surface to get. Defaults to all.
:returns: IDF surfaces.
"""
surfaces = itertools.chain.from_iterable(
[
self.idfobjects[key.upper()]
for key in self.idd_index["ref2names"]["AllShadingSurfNames"]
]
)
if surface_type:
surfaces = filter(
lambda x: x.Surface_Type.lower() == surface_type.lower(), surfaces
)
return list(surfaces)
def set_wwr(
self, wwr=0.2, construction=None, force=False, wwr_map={}, orientation=None
):
# type: (Optional[float], Optional[str], Optional[bool], Optional[dict], Optional[str]) -> None
"""Add strip windows to all external walls.
Different WWR can be applied to specific wall orientations using the `wwr_map` keyword arg.
This map is a dict of wwr values, keyed by `wall.azimuth`, which overrides the default passed as `wwr`.
They can also be applied to walls oriented to a compass point, e.g. north, which will apply to walls which
have an azimuth within 45 degrees of due north.
:param wwr: Window to wall ratio in the range 0.0 to 1.0.
:param construction: Name of a window construction.
:param force: True to remove all subsurfaces before setting the WWR.
:param wwr_map: Mapping from wall orientation (azimuth) to WWR, e.g. {180: 0.25, 90: 0.2}.
:param orientation: One of "north", "east", "south", "west". Walls within 45 degrees will be affected.
"""
set_wwr(self, wwr, construction, force, wwr_map, orientation)
[docs] def view_model(self, test=False):
# type: (Optional[bool]) -> None
"""Show a zoomable, rotatable representation of the IDF."""
view_idf(idf=self, test=test)
[docs] def to_obj(self, fname=None, mtllib=None):
# type: (Optional[str], Optional[str]) -> None
"""Export an OBJ file representation of the IDF.
This can be used for viewing in tools which support the .obj format.
:param fname: A filename for the .obj file. If None we try to base it on IDF.idfname and change the filetype.
:param mtllib: The name of a .mtl file to be referenced from the .obj file. If None, we use default.mtl.
"""
if not fname:
try:
fname = self.idfname.replace(".idf", ".obj")
except AttributeError:
fname = "default.obj"
export_to_obj(self, fname, mtllib)
[docs] def add_block(self, *args, **kwargs):
# type: (*Any, **Any) -> None
"""Add a block to the IDF.
:param name: A name for the block.
:param coordinates: A list of (x, y) tuples representing the building outline.
:param height: The height of the block roof above ground level.
:param num_stories: The total number of stories including basement stories. Default : 1.
:param below_ground_stories: The number of stories below ground. Default : 0.
:param below_ground_storey_height: The height of each basement storey. Default : 2.5.
:param zoning: The zoning pattern of the block. Default : by_storey
:param perim_depth: Depth of the perimeter zones if the core/perim zoning pattern is requested. Default : 3.0.
"""
block = Block(*args, **kwargs)
block.zoning = kwargs.get("zoning", "by_storey")
if block.zoning == "by_storey":
zones = [
Zone("Block %s Storey %i" % (block.name, storey["storey_no"]), storey)
for storey in block.stories
]
elif block.zoning == "core/perim":
zones = []
try:
for name, coords in core_perim_zone_coordinates(
block.coordinates, block.perim_depth
)[0].items():
block = Block(
name=name,
coordinates=coords,
height=block.height,
num_stories=block.num_stories,
)
zones += [
Zone(
"Block %s Storey %i" % (block.name, storey["storey_no"]),
storey,
)
for storey in block.stories
]
except NotImplementedError:
raise ValueError("Perimeter depth is too great")
else:
raise ValueError("%s is not a valid zoning rule" % block.zoning)
for zone in zones:
self.add_zone(zone)
[docs] def add_shading_block(self, *args, **kwargs):
# type: (*Any, **Any) -> None
"""Add a shading block to the IDF.
:param name: A name for the block.
:param coordinates: A list of (x, y) tuples representing the building outline.
:param height: The height of the block roof above ground level.
:param num_stories: The total number of stories including basement stories. Default : 1.
:param below_ground_stories: The number of stories below ground. Default : 0.
:param below_ground_storey_height: The height of each basement storey. Default : 2.5.
"""
block = Block(*args, **kwargs)
for i, wall in enumerate(block.walls[0], 1):
if wall.area <= 0:
continue
s = self.newidfobject(
"SHADING:SITE:DETAILED", Name="%s_%s" % (block.name, i)
)
try:
s.setcoords(wall)
except ZeroDivisionError:
self.removeidfobject(s)
[docs] def add_zone(self, zone):
# type: (Zone) -> None
"""Add a zone to the IDF.
:param zone: A Zone object holding details about the zone.
"""
try:
ggr = self.idfobjects["GLOBALGEOMETRYRULES"][
0
] # type: Optional[Dict[str, Idf_MSequence]]
except IndexError:
ggr = None
# add zone object
self.newidfobject("ZONE", Name=zone.name)
for surface_type in zone.__dict__.keys():
if surface_type == "name":
continue
for i, surface_coords in enumerate(zone.__dict__[surface_type], 1):
if not surface_coords:
continue
name = "{name} {s_type} {num:04d}".format(
name=zone.name, s_type=surface_type[:-1].title(), num=i
)
s = self.newidfobject(
"BUILDINGSURFACE:DETAILED",
Name=name,
Surface_Type=surface_type[:-1],
Zone_Name=zone.name,
)
s.setcoords(surface_coords, ggr)