diff --git a/honeybee/face.py b/honeybee/face.py index cb055a91..85d44a8b 100644 --- a/honeybee/face.py +++ b/honeybee/face.py @@ -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 @@ -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 @@ -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 @@ -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. diff --git a/honeybee/model.py b/honeybee/model.py index 1c0255c0..06ab5e2b 100644 --- a/honeybee/model.py +++ b/honeybee/model.py @@ -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 @@ -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. diff --git a/honeybee/room.py b/honeybee/room.py index b50d5225..07e099a8 100644 --- a/honeybee/room.py +++ b/honeybee/room.py @@ -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.