Skip to content

Commit

Permalink
fix(validate): Add a validation check for upside down faces
Browse files Browse the repository at this point in the history
  • Loading branch information
chriswmackey authored and Chris Mackey committed Nov 24, 2023
1 parent d7c5eb2 commit fbeef6d
Show file tree
Hide file tree
Showing 3 changed files with 118 additions and 3 deletions.
41 changes: 38 additions & 3 deletions honeybee/face.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
from ._basewithshade import _BaseWithShade
from .typing import clean_string, invalid_dict_error
from .properties import FaceProperties
from .facetype import face_types, get_type_from_normal, AirBoundary
from .facetype import face_types, get_type_from_normal, AirBoundary, Floor, RoofCeiling
from .boundarycondition import boundary_conditions, get_bc_from_position, \
_BoundaryCondition, Outdoors, Surface, Ground
from .shade import Shade
Expand Down Expand Up @@ -944,7 +944,7 @@ def apertures_by_ratio_rectangle(self, ratio, aperture_height, sill_height,
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 aperture centerlines. If this number is larger than
individual aperture center lines. If this number is larger than
the parent rectangle base, only one aperture will be produced.
vertical_separation: An optional number to create a single vertical
separation between top and bottom apertures. The default is
Expand Down Expand Up @@ -1042,7 +1042,7 @@ def apertures_by_width_height_rectangle(self, aperture_height, aperture_width,
is too large for the sill_height to fit within the rectangle,
the aperture_height will take precedence.
horizontal_separation: A number for the target separation between
individual apertures centerlines. If this number is larger than
individual apertures center lines. If this number is larger than
the parent rectangle base, only one aperture will be produced.
tolerance: The maximum difference between point values for them to be
considered a part of a rectangle. Default: 0.01, suitable for
Expand Down Expand Up @@ -1580,6 +1580,41 @@ def check_sub_faces_overlapping(
return all_overlaps
return [] if detailed else ''

def check_upside_down(self, angle_tolerance=1, raise_exception=True, detailed=False):
"""Check whether the face is pointing in the correct direction for the face type.
This method will only report Floors that are pointing upwards or RoofCeilings
that are pointed downwards. These cases are likely modeling errors and are in
danger of having their vertices flipped by EnergyPlus, causing them to
not see the sun.
Args:
angle_tolerance: The max angle in degrees that the Face normal can
differ from up or down before it is considered a case of a downward
pointing RoofCeiling or upward pointing Floor. Default: 1 degree.
raise_exception: Boolean to note whether an ValueError should be
raised if the Face is an an upward pointing Floor or a downward
pointing RoofCeiling.
detailed: Boolean for whether the returned object is a detailed list of
dicts with error info or a string with a message. (Default: False).
Returns:
A string with the message or a list with a dictionary if detailed is True.
"""
msg = None
if isinstance(self.type, Floor) and self.altitude > 90 - angle_tolerance:
msg = 'Face "{}" is an upward-pointing Floor, which should be ' \
'changed to a RoofCeiling.'.format(self.full_id)
elif isinstance(self.type, RoofCeiling) and self.altitude < angle_tolerance - 90:
msg = 'Face "{}" is an downward-pointing RoofCeiling, which should be ' \
'changed to a Floor.'.format(self.full_id)
if msg:
full_msg = self._validation_message(
msg, raise_exception, detailed, '000109',
error_type='Upside Down Face')
return full_msg
return [] if detailed else ''

def check_planar(self, tolerance=0.01, raise_exception=True, detailed=False):
"""Check whether all of the Face's vertices lie within the same plane.
Expand Down
40 changes: 40 additions & 0 deletions honeybee/model.py
Original file line number Diff line number Diff line change
Expand Up @@ -1931,6 +1931,7 @@ def check_all(self, raise_exception=True, detailed=False):
# perform geometry checks related to parent-child relationships
msgs.append(self.check_sub_faces_valid(tol, ang_tol, False, detailed))
msgs.append(self.check_sub_faces_overlapping(tol, False, detailed))
msgs.append(self.check_upside_down_faces(ang_tol, False, detailed))
msgs.append(self.check_rooms_solid(tol, ang_tol, False, detailed))

# perform checks related to adjacency relationships
Expand Down Expand Up @@ -2230,6 +2231,45 @@ def check_sub_faces_overlapping(
raise ValueError(full_msg)
return full_msg

def check_upside_down_faces(
self, angle_tolerance=None, raise_exception=True, detailed=False):
"""Check that the Model's Faces have the correct direction for the face type.
This method will only report Floors that are pointing upwards or RoofCeilings
that are pointed downwards. These cases are likely modeling errors and are in
danger of having their vertices flipped by EnergyPlus, causing them to
not see the sun.
Args:
angle_tolerance: The max angle in degrees that the Face normal can
differ from up or down before it is considered a case of a downward
pointing RoofCeiling or upward pointing Floor. If None, it
will be the model angle tolerance. (Default: None).
raise_exception: Boolean to note whether an ValueError should be
raised if the Face is an an upward pointing Floor or a downward
pointing RoofCeiling.
detailed: Boolean for whether the returned object is a detailed list of
dicts with error info or a string with a message. (Default: False).
Returns:
A string with the message or a list with a dictionary if detailed is True.
"""
a_tol = self.angle_tolerance if angle_tolerance is None else angle_tolerance
detailed = False if raise_exception else detailed
msgs = []
for rm in self._rooms:
msg = rm.check_upside_down_faces(a_tol, False, detailed)
if detailed:
msgs.extend(msg)
elif msg != '':
msgs.append(msg)
if detailed:
return msgs
full_msg = '\n'.join(msgs)
if raise_exception and len(msgs) != 0:
raise ValueError(full_msg)
return full_msg

def check_rooms_solid(self, tolerance=None, angle_tolerance=None,
raise_exception=True, detailed=False):
"""Check whether the Model's rooms are closed solid to within tolerances.
Expand Down
40 changes: 40 additions & 0 deletions honeybee/room.py
Original file line number Diff line number Diff line change
Expand Up @@ -1329,6 +1329,46 @@ def check_sub_faces_overlapping(
raise ValueError(full_msg)
return full_msg

def check_upside_down_faces(
self, angle_tolerance=1, raise_exception=True, detailed=False):
"""Check whether the Room's Faces have the correct direction for the face type.
This method will only report Floors that are pointing upwards or RoofCeilings
that are pointed downwards. These cases are likely modeling errors and are in
danger of having their vertices flipped by EnergyPlus, causing them to
not see the sun.
Args:
angle_tolerance: The max angle in degrees that the Face normal can
differ from up or down before it is considered a case of a downward
pointing RoofCeiling or upward pointing Floor. Default: 1 degree.
raise_exception: Boolean to note whether an ValueError should be
raised if the Face is an an upward pointing Floor or a downward
pointing RoofCeiling.
detailed: Boolean for whether the returned object is a detailed list of
dicts with error info or a string with a message. (Default: False).
Returns:
A string with the message or a list with a dictionary if detailed is True.
"""
detailed = False if raise_exception else detailed
msgs = []
for f in self._faces:
msg = f.check_upside_down(angle_tolerance, False, detailed)
if detailed:
msgs.extend(msg)
elif msg != '':
msgs.append(msg)
if len(msgs) == 0:
return [] if detailed else ''
elif detailed:
return msgs
full_msg = 'Room "{}" contains upside down Faces.' \
'\n {}'.format(self.full_id, '\n '.join(msgs))
if raise_exception and len(msgs) != 0:
raise ValueError(full_msg)
return full_msg

def check_planar(self, tolerance=0.01, raise_exception=True, detailed=False):
"""Check that all of the Room's geometry components are planar.
Expand Down

0 comments on commit fbeef6d

Please sign in to comment.